Assemblercode eines C-Programms



  • Hallo erstmal!

    Bin mir nicht sicher gewesen, ob das ins C-Forum oder hierhin gehört, ich hoffe das ist in Ordnung so.

    Ich versuche den Assemblercode zu folgendem kleinen Beispiel nachzuvollziehen:

    #include <stdio . h>
    
     void function (int a) {
                   long some_long;
    	        char * some_strings[2];
    	        some_long = 42;
    	        some_strings[0] = "foo";
    	        some_strings[1] = "bar";
    	        some_long -= 19;
    	        printf("%ld\n%d\n%s\n", some_long, a, some_strings[0]);
    	}
    	void main () {
    	        int x = 0; 
    	        function(x);
    	}
    
    Dump of assembler code for function main:
       0x0000000000400563 <+0>:	push   %rbp
       0x0000000000400564 <+1>:	mov    %rsp,%rbp
       0x0000000000400567 <+4>:	sub    $0x10,%rsp
       0x000000000040056b <+8>:	movl   $0x0,-0x4(%rbp)
       0x0000000000400572 <+15>:	mov    -0x4(%rbp),%eax
       0x0000000000400575 <+18>:	mov    %eax,%edi
       0x0000000000400577 <+20>:	callq  0x40051c <function>
       0x000000000040057c <+25>:	leaveq 
       0x000000000040057d <+26>:	retq   
    End of assembler dump.
    (gdb) disassemble function
    Dump of assembler code for function function:
       0x000000000040051c <+0>:	push   %rbp
       0x000000000040051d <+1>:	mov    %rsp,%rbp
       0x0000000000400520 <+4>:	sub    $0x30,%rsp
       0x0000000000400524 <+8>:	mov    %edi,-0x24(%rbp)
       0x0000000000400527 <+11>:	movq   $0x2a,-0x8(%rbp)
       0x000000000040052f <+19>:	movq   $0x400624,-0x20(%rbp)
       0x0000000000400537 <+27>:	movq   $0x400628,-0x18(%rbp)
       0x000000000040053f <+35>:	subq   $0x13,-0x8(%rbp)
       0x0000000000400544 <+40>:	mov    -0x20(%rbp),%rcx
       0x0000000000400548 <+44>:	mov    -0x24(%rbp),%edx
       0x000000000040054b <+47>:	mov    -0x8(%rbp),%rax
       0x000000000040054f <+51>:	mov    %rax,%rsi
       0x0000000000400552 <+54>:	mov    $0x40062c,%edi
       0x0000000000400557 <+59>:	mov    $0x0,%eax
       0x000000000040055c <+64>:	callq  0x4003f0 <printf@plt>
       0x0000000000400561 <+69>:	leaveq 
       0x0000000000400562 <+70>:	retq   
    End of assembler dump.
    

    Ich habe da noch kaum Ahnung von, aber ich hoffe das hilft mir auch etwas zu verstehen, wie der Stack organisiert wird.
    Leider komme ich nciht besonders weit.
    Ok...am Anfang wird der framepointer auf den Stack gepusht.
    Beim zweiten Befehl werde ich schon stutzig und zwar wegen des dritten. Ich hab online gelesen, dass es lauten müsste sub (Ziel,Quelle) demnach dürfte das erste Argument also keine Konstante sein. Ich schließe daraus, dass das bei mir andersrum ist (warum auch immer). Infolgedessen bin ich mir beim zweiten Befehl nciht mehr sicher, was jetzt worauf kopiert wird.

    Bin über jede Hilfe und Erklärungen froh. Werde natürlich nebenbei ncoh weiterrecherchieren.

    vG Wolf



  • Wolfone schrieb:

    Ich hab online gelesen, dass es lauten müsste sub (Ziel,Quelle) demnach dürfte das erste Argument also keine Konstante sein. Ich schließe daraus, dass das bei mir andersrum ist (warum auch immer).

    In der Intel-Syntax hast du immer Ziel, Quelle; bei der AT&T-Syntax (das ist die hier verwendete) hast du Quelle, Ziel. Dazu findet man sicher auch bei Wikipedia was.

    Noch ein Tipp, wahrscheinlich ist es sinnvoller, sich den Assembler-Code vom Compiler geben zu lassen als das Programm zu disassemblieren. Falls ich mich recht erinnere, gcc -S blabla.c erzeugt ein blabla.s.



  • Vielen Dank zunächst für die schnelle Antwort!

    Der Befehl funktioniert schonmal. Ich weiß allerdings nicht, wie ich die erzeugt Datei öffne 😑

    Zum weiteren Verständnis dessen, was ich da sehe:

    -es wird also hier in Z3 der Stackpointerwert auf den Framepointer kopiert. Demnach zeigen beide auf den obersten Punkt des Stacks
    -Z4 der Stackpointer wird um 16 vermindert (ich schätze mal das hat damit zu tun, dass der Stack von "oben nach unten" wächst, aber warum gerade minus 16?)
    Sehe ich das richtig, dass das noch alles quasi die "Vorbereitung" bzw. das Erstellen des frames für main ist?
    -danach wird der Wert 0 an die Adresse framepointer-4 kopiert (das dürfte das Anlegen meines int x = 0; sein)
    -Z6: de Wert an Adresse framepointer-4 (also unsere 0) wird in Register eax kopiert
    -Z7 der Wert in Register eax wird in Register edi kopiert (warum??)
    -Z8 (was ist der Unterschied zwischen callq und call) es wird eine Rücksprungadresse gesetzt und die Funktion function aufgerufen

    soweit in Ordnung?



  • Der Stack nimmt lokale Variablen auf, in main also x = 4 Bytes. Damit der Prozessor schneller arbeiten kann, wird der Stack "aligned", sein Stackpointer also auf eine durch 16 teilbare Adresse gesetzt -> noch einmal 12 abziehen, sind 16.

    Der Funktion wird ein Wert übergeben. Da Du mit einem 64-Bit-System arbeitest, geschieht das nach den 64-Bit-Calling-Conventions: http://en.wikipedia.org/wiki/X86_calling_conventions#System_V_AMD64_ABI. Der erste Wert wird also in EDI übergeben und somit muss EDI vor dem CALL damit belegt werden. Der Compiler hat sich aber für einen etwas umständlichen Weg dafür entschieden. Vielleicht ist es aber auch schneller, erst EAX mit Speicher zu laden und dann EDI mit EAX.

    Zwischen CALLQ und CALL gibt es keinen Unterschied. Das eine ist AT&T-Syntax, das andere Intel-Syntax.

    viele grüße
    ralph



  • Wolfone schrieb:

    Der Befehl funktioniert schonmal. Ich weiß allerdings nicht, wie ich die erzeugt Datei öffne 😑

    Das ist eine normale Textdatei, wo ist das Problem?



  • Danke euch allen !

    @Basher tatsächlich -.- war mir nciht klar, muss ich gestehen.

    zu dem Assemblercode weiterhin:

    Ich verstehe die Zeilen 19 und 20 zum Beispiel nciht so wirklich.
    Da gehts ja wohl um die Zeilen:

    some_strings[0] = "foo";
    some_strings[1] = "bar";

    im c code.
    Was passiert da?
    Die Zeilen 22,23,24 würde ich mal so interpretieren, dass da die Ausgabe vorbereitet wird, weil ja die Werte an den Adressen, an denen die drei auszugebenden Sachen stehen in verschiedene Register kopiert werden. Stimmt das?

    Die nächsten mov Befehle verstehe ich nicht wwirklich.
    Zeile 28 generiert also offensichtlich dei Ausgabe wiederum durch Aufruf einer Funktion (deren Arbeitsweise ich auch nciht verstehe; woher weiß sie, von wo die auszugebenden Argumente zu nehmen sind?)

    Das leave verstehe ich ebenfalls nciht so wenig; nehmen wir mal als Basis folgende Beschreibung:
    frees the space saved on the stack by copying ebp into esp,
    then popping the saved value of ebp back to ebp
    Insbesondere werde ich aus dem zweiten Teil des Satzes nciht so recht schlau...ich kopiere den Wert des Stackpointers in den Stackpointer? Was will mir das denn sagen?

    Die Erklärung zu dem ret (returns control to the calling procedure by popping the saved
    instruction pointer from the stack) macht mich auch nicht so richtig schlauer. Wo kommt denn der Instruction pointer auf einmal her?

    vG Wolf



  • Du kannst Dir mit objdump etwas Lesbareres basteln:

    objdump -S -d -w -Mintel --no-show-raw-insn test >test.lst
    

    -S: Zeige Source-Code an (in diesem Fall: test.c)
    -d: disassemble
    -Mintel: Intel-Syntax (gebräuchlicher und verständlicher als AT&T-Syntax)
    --no-show-raw-insn: nur Adressen und Text, kein Hex-Speicherinhalt
    test: Name der ausführbaren Datei

    test.lst: Textdatei, wo alles hineinkommt

    Danach kannst Du Dir in Ruhe test.lst anschauen:

    void function (int a) {
      40053c:	push   rbp
      40053d:	mov    rbp,rsp
      400540:	sub    rsp,0x30
      400544:	mov    DWORD PTR [rbp-0x24],edi
                   long some_long;
                char * some_strings[2];
                some_long = 42;
      400547:	mov    QWORD PTR [rbp-0x8],0x2a
                some_strings[0] = "foo";
      40054f:	mov    QWORD PTR [rbp-0x20],0x40064c
                some_strings[1] = "bar";
      400557:	mov    QWORD PTR [rbp-0x18],0x400650
                some_long -= 19;
      40055f:	sub    QWORD PTR [rbp-0x8],0x13
                printf("%ld\n%d\n%s\n", some_long, a, some_strings[0]);
      400564:	mov    rcx,QWORD PTR [rbp-0x20]
      400568:	mov    edx,DWORD PTR [rbp-0x24]
      40056b:	mov    rax,QWORD PTR [rbp-0x8]
      40056f:	mov    rsi,rax
      400572:	mov    edi,0x400654
      400577:	mov    eax,0x0
      40057c:	call   400410 <printf@plt>
        }
      400581:	leave
      400582:	ret
    
    0000000000400583 <main>:
        void main () {
      400583:	push   rbp
      400584:	mov    rbp,rsp
      400587:	sub    rsp,0x10
                int x = 0;
      40058b:	mov    DWORD PTR [rbp-0x4],0x0
                function(x);
      400592:	mov    eax,DWORD PTR [rbp-0x4]
      400595:	mov    edi,eax
      400597:	call   40053c <function>
        }
      40059c:	leave
      40059d:	ret
      40059e:	nop
      40059f:	nop
    

    Der Assemblercode steht direkt unter der zugehörigen C-Zeile.

    Das Array 'strings' besteht aus Zeigern auf Strings. Die zugewiesenen Strings sind Literale, d.h. sie befinden sich als Konstanten bereits im Speicher und deren Adressen (hier: 0x40064c und 0x400650) wird nun den jeweiligen Zeigern von 'strings' zugewiesen.

    Das Bündel mov-Befehle gehört zu printf. Übergabefolge: RDI, RSI, RDX, RCX, R8, and R9, also:

    RDI (bzw. EDI, weil 32-Bit-Adresse): Zeiger auf Formatstring ("%ld\n%d\n%s\n")
    RSI: some_long
    RDX (bzw. EDX, weil 32-Bit-int): a
    RCX: some_strings[0] (Zeiger)

    leave: Die Befehlsfolge 'push rbp; mov rbp,rsp' wird wieder rückgängig gemacht, so dass das Programm mit dem alten RBP zurückkehren kann.

    Woher hast Du denn diese verkorkste Erklärung zu RET? Auf dem Stack befindet sich oben (Hoffentlich! Sonst Absturz!) die Rückkehradresse, wo sie von CALL hingespeichert wurde. RET holt sich diese Adresse, inkrementiert den Stackpointer und springt dahin.

    viele grüße
    ralph



  • Super 🙂 Danke dir schonmal!

    Dann werde ich mich jetzt nochmal dahinter klemmen!



  • Ach ich vergaß: Du musst die Quelldatei mit -g kompilieren, z.B.:

    gcc -g -o test test.c
    

Anmelden zum Antworten