Möglichkeit C Code zu parsen

Hallo Kollegen,

kennt ihr eine gute Möglichkeit den Sourcecode eines C-Programms zu parsen? Aktuell benutze ich src2xml, welches ein C-Programm direkt in eine XML-Struktur überführt. Allerdings gibt es evtl. Ansätze die ich direkt in Java nutzen kann. Ziel ist es Variablen, Methoden etc zu erkennen. Ich möchte keine Syntaxprüfung machen, es ist davon auszugehen, dass das Programm kompiliert. Ich möchte einfach die Struktur des Programms erkennen und diese abspeichern und anzeigen zu können.

Z.B. Methode “main” hat den Inhalt “…”

Darin befinden sich weitere Methoden aufrufe nämlich a(), z() und berta();

usw.

Hm :slight_smile: Entweder die Frage ist naiv, oder genau das Gegenteil davon. Ich gehe mal vom Gegenteil aus. (Wenn nicht, noch der Dämpfer: Das ist richtig kompliziert. Ich meine, richtig richtig kompliziert… also, alles was man auf http://www.ioccc.org/ findet, läßt sich compilieren - das sagt nicht viel aus). Ich stand mal vor einer ähnlichen Frage, und habe in Betracht gezogen, mit yacc und AntLR und was-weiß-ich-nicht-alles den kompletten Weg selbst zu gehen. Aber … :verzweifel: nein, da schreibt man sich einen Ast (pun intended :D). Ich bin dann bei Eclipse CDT | The Eclipse Foundation gelandet. Wenn man sich die richtigen JARs zusammenpflückt, bekommt man relativ leicht einen AST, um http://help.eclipse.org/helios/topic/org.eclipse.cdt.doc.isv/reference/api/org/eclipse/cdt/core/dom/ast/IASTNode.html herum…

Hi Marco, in welcher Art könnte die Frage naiv sein? :wink:
Bei der Kreierung eines AST bin ich auch gelandet hab aber nicht tiefer geforscht. Aber damit ich dich richtig verstehe. Du rätst mir mich am Code von cdt zu bedienen? Da ich das noch nie gemacht habe, hast du ein paar konkrete Einstiegspunkte für mich um mich nicht komplett dadrin zu verstricken? Da das nur ein kleiner Teil meines aktuellen Projekts ist, würde ich da ungerne zuviel investieren, muss ich zugeben. Wenn es also eine Lib gibt die ich mir als erstes Anschauen kann, immer her damit.

Esprima macht das ganz gut für JavaScript den Code in JSON abzubilden. Könntest auch mal reinschauen.

Bei C gibt’s halt lästige Syntaxtücken.

http://www.antlr3.org/grammar/list.html
Da gibt die Grammatik für ANSI C.

Ich muss da @Marco13 widersprechen, mit Antlr4 ist das kein problem. Hol dir die C Grammatik und Bau dir einen Visitor-Parser. Das ist ähnlich einfach wie einen Sax- Parser zu schreiben.

„Naiv“ falls du glaubst, es gäbe eine Lib, bei der man

Program program = Programs.parse("file.c");
Function function = p.getFunctions().getFunction(0);
Variable variable = function.getVariables().get(0);
System.out.println("There we go: "+variable.getName());

hinschreiben könnte, um den Namen der ersten Variablen der ersten Funktion eines Programms zu erhalten.

Wie TheDarkRose schon zart angedeutet hat: Bei C gibt’s „Syntaxtücken“ - eine sehr zurückhaltende Formulierung für den Unterschied der Komplexität von JavaScript gegenüber C, mit typedefs, Pointern und Adressen, und den über-parser-Killern: #include und #define! :sick:

Tatsächlich hatte ich für meine Zwecke damals ein „SimpleModel“ definiert - also ein Modell für den geparsten Code, das aus dem von CDT gelieferten AST ein extem vereinfachtes „Modell“ des geparsten Programmes erstellt. Das ganze … sah dann im Endeffekt so ähnlich aus wie der oben angedeutete Pseudocode… ;)… ging aber nicht bis auf den Inhalt der Methoden runter, weil das 1. natürlich beliebig kompliziert werden kann, und ich es 2. (wichtiger) schlicht nicht brauchte. (In betracht gezogen hatte ich diese Funktion aber, und ggf. würde ich das irgendwann erweitern…).

Ich habe dir übrigens NICHT „geraten“, CDT zu verwenden, sondern nur erwähnt, dass ich dann dort gelandet bin :wink: Weil ich eben keine Bibliothek kenne (bzw. damals keine gefunden habe), die einem eine der im Pseudocode angedeuteten Funktionalität bietet. Aber vielleicht gibt es „bessere“, oder „einfachere“ Möglichkeiten, dein Ziel zu erreichen (aber wenn du dich genötigt siehst, den AST selbst zu parsen, kann ich dir versichern, dass das NICHT die „bessere“ oder „einfachere“ Möglichkeit ist :D).

Der „Einstieg“ in CDT kann holprig sein. Das ist ja eigentlich keine Lib, die für Standalone-Verwendung gedacht ist. Stattdessen ist sie teil der Eclipse-C-Entwicklungsumgebung. Aber ich kann ggf. heute Abend mal ein „Starterpaket“ schnüren, das du dir anschauen kannst, um dann zu bewerten, ob das ein für dich geeigneter Weg sein könnte - oder ob du noch weitersuchst, ob du etwas einfacheres/besseres findest (und wenn du etwas findest, sag’ bescheid, würde mich auch interessieren :slight_smile: ).

EDIT: Oh, @schlingel Wenn du einfach so eine Bibliothek erstellen könntest, mit der man etwa die angedeutete Funktionalität hat, wäre das super! AntLR schien mir damals nichts zu sein, wo man sich „mal kurz“ einen Parser generiert, aber wenn das einfach so geht, wäre das praktisch. (Fragt sich nur, warum die Eclipse CDT-Leute sich dann so viel Arbeit gemacht haben…)

Ich hab gerade nur mein tablet zur Hand aber zwischen Version 3 und 4 hat sich viel bei Antlr getan. Früher war es ja wie Yacc zu verwenden, das funktioniert mittlerweile ganz anders. Und Version 4 ist deutlich jünger als das Eclipse Projekt.

Ich schau ob ich am Abend dazu komme und einen kleinen Parser bastle.

Ihr seid super. @marco : Ich muss gestehen, aufgrund des Java-Lib Angebots bin ich doch sehr verwöhnt. Ich werde mir mal antlr4 angucken. Danke

Ja, habe gerade auch mal einem Blick auf Antlr V4 geworfen … und muss sagen, dass das mit Antlr V3 irgendwie nicht mehr sooo viel gemeinsam zu haben scheint (aber Versionsnummern in Packagenamen (!) unterzubringen muss auch seine Rechtfertigung haben :sick: :wink: ).

Da gibt es ja jetzt eine Methode
parser.setBuildParseTree(true);
die man einfach aufruft, und dann kommt die magische Parse-Baum-Erzeuge-Fee und zaubert einem den Baum zurecht - bei V3 war das nicht ganz so einfach … … … :wink:

Tatsächlich sieht das jetzt insgesamt SO einfach aus, dass ich wohl ggf. mal schauen werde, ob ich nicht meine Verwendung von CDT dadurch ersetzen kann (auch wenn #defines und #includes immernoch so eine Sache sind - da müßte man wohl das ganze noch mit der C Preprocessor Grammatik verwursten, aber das habe ich mir jetzt nicht näher angeschaut)

EDIT: @timbeau Ich gehe mal davon aus, dass das mit dem CDT jetzt erstmal nicht mehr relevant ist.

So, ich hab mich kurz hin gesetzt und erst mal nur den Code gemacht, der einem erlaubt herauszufinden welche Funktionen innerhalb einer Funktion aufgerufen werden. Die C Grammatik habe ich aus diesem Git-Repository genommen, da gibt’s auch noch ein paar andere.

Kurz zur Vorbereitung: Um sich Lexer, Parser, etc. generieren zu lassen genügt es Antlr einzurichten und dann in der Konsole folgendes aufzurufen:


antlr4 C.g4

Das erzeugt dann ein paar Java-Files die man in seinem Projekt einbinden können. Mit folgendem Code bekommt man immer den aktuellen Funktionsnamen und dann darauf folgend die darin aufgerufenen Funktionen:

public class CStructureListener extends CBaseListener {
	@Override
	public void enterFunctionDefinition(FunctionDefinitionContext ctx) {
		super.enterFunctionDefinition(ctx);
		
		System.out.println("Funktion: " + ctx.declarator().getText());
	}
	
	@Override
	public void enterPostfixExpression(PostfixExpressionContext ctx) {
		boolean isFunctionCall = ctx.getText().endsWith(")");
		
		if(isFunctionCall) {
			System.out.println("   " + ctx.getText());
		}
	}
}

Das dazu gehörige Hauptprogramm sieht so aus:

public class CAnalyzer {
	public static void main(String[] args) throws IOException {
		ANTLRFileStream stream = new ANTLRFileStream(args[0]);
		CLexer lexer = new CLexer(stream);
		CommonTokenStream tokens = new CommonTokenStream(lexer);
		CParser parser = new CParser(tokens);
		ParseTree tree = parser.compilationUnit();
		
		ParseTreeWalker walker = new ParseTreeWalker();
		CStructureListener listener = new CStructureListener();
		walker.walk(listener, tree);
	}
}

Das ist natürlich trivial und noch nicht das was du möchtest, aber ich denke, das ist ein guter Startpunkt um weiter zu machen. Das Projekt habe ich dir hier auch angehängt.

Das parser.setBuildParseTree(true); war nicht nötig? Bei mir hatte er ohne das alles in einen Baum der Tiefe 1 geklatscht (also Root und alle 1000 Tokens als direkte Children von Root) … aber vielleicht hat da noch was anderes nicht gestimmt, war nur ein schneller Test…

Ist in dem Fall nicht nötig. Der Listener wird trotzdem immer beim Erreichen bzw. Verlassen der Token aufgerufen. Ich würde das oben genannte Tool so bauen, dass ich eine Modelklasse anlege die aus dem Namen der Funktion und zwei Listen für die Funktionsaufrufe und die Variablendeklarationen besteht. Beim Erreichen des Funktionsdeklarationstokens würd ich so ein Objekt anlegen und dann bei postfixexpressions die jeweiligen Elemente in den Listen anlegen.

Damit hast du 0 Herumgehangle im Baum und der Code ist mit zwei Klassen erledigt. Du brauchst den kompletten Baum gar nicht mehr oder nur noch selten. Ich hab das ganze für einen Teil meiner Bac.-Arbeit verwendet und habe auschließlich die Listener verwendet.

@schlingel : 1000 Dank schonmal. Habe das Projekt selber nachvollzogen und es hat alles so funktioniert wie von dir und der antlr4 HP beschrieben. Sehr schön! Allerdings habe ich aktuell ein Problem mit der Erkennung von #include und #define Zeilen. Da hat die Grammatik wohl noch Schwächen. Evtl kriege ich das hin ansonsten wird das ignoriert. Ich setze mich jetzt erst mal an die Erweiterung des Listeners und arbeite damit weiter.

Ja, dass das schwierig wird, war zu erwarten. Auf http://www.antlr3.org/grammar/list.html gibt’s auch eine explizite “C Preprocessor Grammar”. Das kann ja arg kompliziert sein, spätestens wenn da irgendwelche Files includet werden, die im PATH gesucht werden oder so :sick: aber das habe ich mir noch nicht angesehen. Kannst ja mal schauen, ob Antlr4 auch mit der Grammatik vom 3er klarkommt (vielleicht sind die Unterschiede da ja nicht sooo groß…?!). Für die #includes könnte man notfalls auch irgendwas hacken, aber die define’s müssen wohl “richtig” behandelt werden - sonst geht ja auch der Parsebaum kaputt…

Da das eher ein Nebenschauplatz ist und ich recht wenig Erfahrung -> keine Erfahrung mit C habe , werde ich entweder die einzelnen Funktionen auslesen oder, wenn das garnichts bringt einfach den Plain-Code anzeigen. Aber ein guter Tiup mit der 3er Grammatik.

Oder du lässt das File durch den Preprozessor laufen, dann hät sich das auch erledigt :slight_smile:

Bedeutet was genau? #include und #define wird ersetzt? Oder nur #define? Aber auch eine gute Idee, evtl mach ich das. Aber ich bin mir nicht sicher, ob ich das übernehmen kann.

Alle Preprozessor-Anweisungen. Die Fangen alle mit # an. Der Preprozessor ist ja ein relativ primitiver Compiler der nur String-Ersetzungen durchführt bevor der resultierende Code dann dem C-Compiler weiter gereicht wird.

Verarbeitet alle Preprozessordirektiven. Der Inhalt von Dateien die per #include referenziert werden, wird an dieser Stelle eingefügt. Der Wert einer #define wird dort eingefügt, wo dies Konstante verwendet wird. Verzweigungen mit #ifdef und co. werden ausgewertet und nur der true Part übernommen:

Z.B. aus foo.c


#define KROCHN "baaaam oida"

int main() {
#ifdef BAAM
	char* voll = KROCHN;
	return 1;
#else
	foo();
	return 0;
#endif
}```
und bar.h
```#define BAAM 1

int foo();

wird im preprocessor beim verarbeiten von foo.c zu (gcc -E foo.c)

# 1 "<built-in>"
# 1 "<command-line>"
# 1 "foo.c"
# 1 "bar.h" 1


int foo();
# 2 "foo.c" 2



int main() {

 char* voll = "baaaam oida";
 return 1;




}

Erst dann wird richtig kompelliert. Mit so #ifef Konstrukten kann man Programmcode komplett aus der Binary herauslassen, je nach einstellungen durch gewisse #define Settings.