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 aufgerufensoweit 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 Dateitest.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