[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, wie cdecl 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 mit cdecl ü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!


Log in to reply