[SOLVED] Interface zwischen Assembler und C-Code
-
Gleich vorweg: ich mache erst seit Mitte der Woche wieder Assembler und bin ein Grünschnabel. Ich bitte daher um Nachsicht.
Zu Selbstlernzwecken lese ich mich derzeit in x86-Assembler (Real Mode) ein und schreibe einen Pseudo-Kernel. Virtuelle Maschine mit QEMU auf Linux am Laufen, kompiliere mit NASM. MBR angegeben, und er schmiert nicht ab, das verbuche ich als Erfolg.
Jetzt will ich allerdings nicht für jeden Scheiß wieder zum Assembler laufen und Frickelcode kompilieren, sondern hatte an eine Art API gedacht, die z.B. BIOS-Funktionsaufrufe kapselt - also so was wie:
print_char: ;Auszugebendes Zeichen liegt im Lowbyte, das Highbyte ist uns Latte/wird eh für den Interrupt ;ueberschrieben. mov ax,[bp + 4] mov ah,0x0E int 0x10 ret
Und wenn der Code dann später in ELF (oder was auch immer) vorliegt, dann brauch ich nur noch einen C-Header zu schreiben, und kann dann auf C-Ebene versuchen, das Ding weiterzuentwickeln.
Die Art und Weise, WIE jetzt eine Funktion aufgerufen wird, kann vielfältig sein. Ich habe mich (im jugendlichen Leichtsinn?) für
cdecl
entschieden und wollte mir jetzt extern Informationen darüber holen. Aber entweder verstehe ich das Beispiel falsch, oder bei der Wikipedia hat jemand was geschrieben, was nicht stimmt, oder der Rest der Welt weiß nicht so recht, wiecdecl
jetzt funktioniert. Ich vermute, ich hab' nur was falsch verstanden.Wikipedia sagt folgendes:
push ebp ;Alten Basispointer auf Stack schieben mov ebp, esp;Neuen Basispointer setzen push 3 ;Parameter in umgekehrter Reihenfolge auf den Stack legen push 2 push 1 call callee ;Call machen
Effektiv liegt dann zuerst (von oben nach unten):
1. Der Basispointer
2. 3
3. 2
4. 1
5. Die Return-Adresse (bei ESP)auf dem Stack.
Jetzt sagen aber die hier, dass es eigentlich so sein sollte:
push 3 ;Parameter in umgekehrter Reihenfolge auf den Stack legen push 2 push 1 call callee ;Call machen
Sprich, die Reihenfolge ist:
1. 3
2. 2
3. 1
4. Die Return-Adresse
5. Der Basispointer (was dann ESP ist)Und der Callee kümmert sich dann halt um das Pushen von EBP. Muss er ja, weil EIP ja erst durch
call
auf den Stack gelegt wird. Den Hauptartikel zum Bild ist hier (nach "Stack during Subroutine Call" suchen) - und nennen tun die das ebenfalls "C Calling Convention" (was zumindest ich mitcdecl
übersetze).Und hier wird in das gleiche Horn wie bei der Virgina-Universität geblasen.
Was ist jetzt also korrekt? Wo kommt mein Basispointer hin?
EDIT: GCC habe ich bereits angeschmissen:
int do_something(int a,int b,int c) __attribute__((cdecl)); int main(int argc,char*argv[]) __attribute__((cdecl)); int do_something(int a,int b,int c) { return a + b + c; } int main(int argc,char*argv[]) { return do_something(5,4,3); }
Kompiliert zu:
080483fd <main>: 80483fd: 55 push ebp 80483fe: 89 e5 mov ebp,esp 8048400: 6a 03 push 0x3 8048402: 6a 04 push 0x4 8048404: 6a 05 push 0x5 8048406: e8 e0 ff ff ff call 80483eb <do_something> 804840b: 83 c4 0c add esp,0xc 804840e: c9 leave 804840f: c3 ret
Was so aussieht, als ob Wikipedia eher recht hat. Oder ich verwechsele die Konventionen jetzt.
EDIT 2: Oder doch nicht:
080483eb <do_something>: 80483eb: 55 push ebp 80483ec: 89 e5 mov ebp,esp 80483ee: 8b 55 08 mov edx,DWORD PTR [ebp+0x8] 80483f1: 8b 45 0c mov eax,DWORD PTR [ebp+0xc] 80483f4: 01 c2 add edx,eax 80483f6: 8b 45 10 mov eax,DWORD PTR [ebp+0x10] 80483f9: 01 d0 add eax,edx 80483fb: 5d pop ebp 80483fc: c3 ret
Wieso wird denn HIER EBP gesichert? Laut der cdecl-Convention nach Wikipedia sollte der Caller einen Stack-Frame für den Callee erstellen ... warum passiert das denn hier?
-
dachschaden schrieb:
080483fd <main>: 80483fd: 55 push ebp 80483fe: 89 e5 mov ebp,esp 8048400: 6a 03 push 0x3 8048402: 6a 04 push 0x4 8048404: 6a 05 push 0x5 8048406: e8 e0 ff ff ff call 80483eb <do_something> 804840b: 83 c4 0c add esp,0xc 804840e: c9 leave 804840f: c3 ret
Das '
push ebp; mov ebp,esp
' und 'leave
' ist der Stackframe der main() und hat nichts mit dem Funktionsaufruf zu tun.
-
Grundsätzlich kannst Du davon ausgehen, dass GCC und Wikipedia (in dieser Reihenfolge) Recht haben. Wenn da ein Fehler drin wäre, gäbe es einen massenhaften Aufschrei.
Der Begriff stack frame wird nicht einheitlich gebraucht. Die meisten verstehen darunter den Speicherbereich, auf den eine Funktion mittels EBP zugreifen kann, also Argumente und lokale Variablen und mittendrin die Rückkehradresse. Wenn GCC anfängt zu optimieren, kann es sein, dass sich Stack Frames überlappen (eine lokale Variable ist gleichzeitig das Argument für eine Funktion) oder EBP wegfällt oder gar in einen nicht reservierten Bereich geschrieben wird (Stichwort: Red Zone).
push ebp mov ebp,esp sub esp, x
wird "Prolog" genannt und betrifft nur den Stack Frame der aktuellen Funktion. Die dortigen Veränderungen werden am Ende der Funktion mit
add esp, x pop ebp
wieder rückgängig gemacht.
Der Caller stellt nicht den Stack Frame für den Callee her, höchstens nur einen Teil, wenn er die Argumente auf den Stack legt. Dein Missverständnis rührt daher, dass Du den Prolog der main-Funktion für die Herstellung des Stack Frames für die do_something-Funktion gehalten hast - das ist er aber nicht. Die do_something-Funktion hat einen eigenen Prolog.
viele grüße
ralph
-
Ahh, vielen Dank, das erklärt einiges!