Donnerstag, 2. Januar 2014

Ein Aufrufgraph für sweph.c

Das Kernstück der Swiss Ephemeris ist das Includefile sweph.c. Um die Kohäsion und die Kopplung der einzelnen Funktionen einzuschätzen und das Programm insgesamt besser zu verstehen, hatte ich das Bedürfnis nach einem Aufrufgraphen. Es soll für ein bestimmtes C-Sourcefile (natürlich sweph.c, das fetteste) ein Diagramm erstellt werden, das für jede Funktion zeigt, welche anderen Funktionen sie jeweils aufruft.

Der in Eclipse CDT vorgesehene View Call Graph klingt vom Namen her verheissungsvoll, bricht aber bei Aufruf mit folgendem Stacktrace ab:

org.eclipse.core.runtime.AssertionFailedException: null argument:Action must not be null
 at org.eclipse.core.runtime.Assert.isNotNull(Assert.java:85)
 at org.eclipse.jface.action.ContributionManager.add(ContributionManager.java:75)
 at org.eclipse.linuxtools.internal.callgraph.CallgraphView.createPartControl(CallgraphView.java:532)
 at org.eclipse.ui.internal.e4.compatibility.CompatibilityPart.createPartControl(CompatibilityPart.java:138)
 at org.eclipse.ui.internal.e4.compatibility.CompatibilityView.createPartControl(CompatibilityView.java:155)
 at org.eclipse.ui.internal.e4.compatibility.CompatibilityPart.create(CompatibilityPart.java:313)
        ...
Der Hauptgrund für den Abbruch sind nicht etwa die in meinem System fehlenden Komponenten wie System Tap (ein interessantes Programm, wie mir scheint), sondern dass der View Call Graph nicht statisch, sondern dynamisch gebildet wird. Dieser Call Graph ist ein Werkzeug fürs Profiling, zum Studium des Laufzeitverhaltens – und nicht das statische Analysetool, nach dem ich suche. Also suchte ich nach einer Alternative.

Das Projekt besteht aus einer Reihe von C-Dateien, die wie üblich jede einzeln in eine gleichnamige Objektdatei compiliert wird. Um einen Aufrufgraphen zu erstellen, müssen wir wissen,

  • welche Funktion welche anderen Funktionen aufruft, und
  • welche Funktionen in welchem Include enthalten sind, um die Funktionen als Gruppe markieren zu können.

Listen der in einem Include enthaltenen Funktionen kann man aus dem jeweils generierten Objectfile (*.o) herauslesen, das ja eine Symboltabelle der compilierten Funktionen enthält.

Funktionen, die mittels Compilerschaltern ausgeblendet sind, werden bei diesem Vorgehen allerdings ignoriert, aber das stört mich hier nicht besonders. Falls das in anderen Situationen stören sollte, kann man auch CTags verwenden, um den Quellcode selbst auf Funktionsdefinitionen zu durchforsten.

Mit dem GCC-Tool objdump kann diese Symboltabelle mit der Option -t ausgelesen werden. Für sweph.o beginnt der Symboltabellen-Dump z.B. wie folgt:

ruediger@herschel:~/workspace/swepar/src$ objdump -t sweph.o

sweph.o:     file format elf32-i386

SYMBOL TABLE:
00000000 l    df *ABS* 00000000 sweph.c
00000000 l    d  .text 00000000 .text
00000000 l    d  .data 00000000 .data
00000000 l    d  .bss 00000000 .bss
00000000 l    d  .rodata 00000000 .rodata
00000000 l     O .rodata 000000a8 pla_diam
000000c0 l     O .rodata 000001e0 ayanamsa
00000000 l    d  .data.rel.local 00000000 .data.rel.local
00000000 l     O .data.rel.local 0000006c ayanamsa_name
00000420 l     O .rodata 0000002c pnoint2jpl
00000460 l     O .rodata 00000054 pnoext2int
00000204 l     O .bss 00000004 epheflag_sv.3938
000005b1 l     F .text 00001a56 swecalc
0000f554 l     F .text 00000165 denormalize_positions
0000f6b9 l     F .text 000000d6 calc_speed
0000faa0 l     F .text 000000c4 plaus_iflag
0000395c l     F .text 000004e1 jplplan
00003169 l     F .text 000007f3 sweplan
In der Symboltabelle wird so allerhand Mystisches aufgeführt; aber eben auch alle enthaltenen Funktionen; diese sind dadurch zu erkennen, dass sie in der Liste das Kürzel F haben und ihr Name mit .text eingeleitet wird.

Mit der folgenden Perl-Routine kann ich alle Object Files eines Verzeichnisses auf einmal durchgehen und eine Hashtabelle erstellen, die für jede Funktion die Nummer des Object Files liefert, in der sie vorhanden ist:

sub get_all_symbols {

# Generates a hash containing all symbols 
# of all object files in this directory

  my %symbols;
  my @obj = split /\s+/, `ls *.o`;
  my $count = -1;
  my $exceptions = join "|", @ignore_object_files;
  for my $fname (@obj) {
    next if $fname =~ /$exceptions/; # Skip the exceptions
    $count++;
    foreach (`objdump -t $fname`) {
      chomp;
# This is very GCC specific, expecting the OBJDUMP output format
      next unless /^[0-9a-f]{8}.*?F\s+\.text\s+[0-9a-f]{8}/;
      my $name = $_;
      $name =~ s/^.*?(\w+)$/$1/;
      $symbols{$name} = $count;
      }
    }
  return \%symbols;
  }
Hierbei kann man mit dem Array @ignore_object_files noch eine Reihe von Object Files aufführen, die nicht analysiert werden sollen.

Für die eigentliche Aufrufanalyse gibt es das Programm cflow. Mit der Option -l aufgerufen liefert es die gewünschte Vorwärtsreferenztabelle: Für jede Funktion werden alle von dieser Funktion aufgerufenen Funktionen gelistet. Für sweph.c beginnt die Liste beispielsweise so:

ruediger@herschel:~/workspace/swepar/src$ cflow -l sweph.c | head
{   0} swe_calc() :
{   1}     memset()
{   1}     fopen()
{   1}     fgets()
{   1}     strchr()
{   1}     atol()
{   1}     fclose()
{   1}     swi_open_trace()
{   1}     trace_swe_calc() <void trace_swe_calc (int swtch, double tjd, int ipl, int32 iflag, double *xx, char *serr) at sweph.c:5976>:
{   2}         fputs()
Wieder ist es so, dass bei einem automatischen Auswerten dieser Information bestimmte Aufrufe ignoriert werden sollten, hier z.B. die Aufrufe von Standardfunktionen wie fclose() usw.

Im Grunde sind damit alle Informationen beisammen, um den Graphen zu erstellen, was am besten auf dem Weg über die graphische Sprache dot von graphviz geschieht. Um die GraphViz-Befehle zu erforschen, gibt es übrigens eine schöne Webanwendung, den GraphViz Workspace. Er erzeugt bereits während der Eingabe das resultierende Diagramm.

Ich orientierte mich, was die dot-Syntax und Gestalt der zu erzeugenden Knoten angeht, an dem Script cflow2dot.pl.

Meine Version unterscheidet sich von der Vorlage im wesentlichen nur dadurch, dass es Filtermöglichkeiten für die Objektfiles und die Listen der aufgerufenen Funktionen gibt, und dass die verschiedenen Farbcodes dafür genutzt werden, die Funktionen nach ihrer Zugehörigkeit zu Source-Code-Includes zu gruppieren. Die Formen dagegen entsprechen der jeweiligen Aufruftiefe (so ist es auch im Originalskript).

Hier ist das Ergebnis für sweph.c (noch mit inkscape von SVG ins PDF-Format convertiert):

Die Trace-Funktionen sind gelb markiert, da sie in keinem Objektfile gefunden werden konnten (denn ich kompiliere das Projekt nicht mit dem Traceschalter). Auch konnten die Macro-Funktionen dot_prod und square_sum nicht gefunden werden. Aus Gründen der Compilersicherheit wäre es sowieso besser, diese Funktionen als Inline-Funktionen zu deklarieren (was ich in meinem Prototyp test_calc_reduce.c auch schon umgestellt habe - glaube ich).

Keine Kommentare:

Kommentar veröffentlichen