Discussion:
Verständnisfrage: C calling convention
(zu alt für eine Antwort)
Markus Wichmann
2006-09-18 18:18:01 UTC
Permalink
Hi all,
ich wollte mal wissen, ob ich das mit der C-Aufrufskonvention (ich weis,
deutsch klingt es blöd) richtig verstanden habe. Also, zunächst pusht
der Rufer die Parameter falschrum auf den Stack. Dann ruft er die
Funktion, und danach löscht er den Stack wieder:

push letzter_param
push erster_param
call my_function
add esp,sizeof(params)

Die Funktion soll dann ebp sichern und eine Stackframe eröffnen (oder
welchen Genus 'frame' im deutschen auch immer hat):

my_function:
push ebp
mov ebp,esp

Meine Frage betrifft die lokalen Variablen. Was ich machen muss, ist
klar: Vom esp die Größe der Variablen in Byte abziehen:

sub esp,4

bzw. initialisierte Variablen pushen (wäre jedenfalls cleverer):

push dword 0

Aber wie greife ich auf diese Variablen jetzt zu? Mit [ebp-Stelle],
wobei ich das schon am Anfang per Präprozessor festlegen wollte. Ach ja,
bei NASM eignet sich welche Direktive besser? %assign oder %define? In
jedem Fall, wie kann ich jetzt z.B. den Inhalt des ersten Parameters
nach ax verschieben? (Unter der Annahme, es handele sich um einen
16-Bit-Wert). Von [ebp-0] bis [ebp-3] liegt ja der alte ebp. Also mit

mov ax,[ebp-4]

oder wie? Und jetzt noch was: Wenn ich Pointer übergeben kriege (als
Parameter), wie groß sind die unter WinNT (also Windows mit NT-Kernel =
NT + 2kx + XP + Vista (glaub)) Ich verwende momentan Windows 2000, aber
mein Programm soll ja auch portierbar sein.
Um es ganz konkret zu machen: Unter C heist die Anweisung

x0 = *in++;

Wie geht das unter Assembler, unter der Maßgabe, dass x0 die erste
lokale Variable und *in der erste Parameter ist (liegt also direkt auf
ebp). Falls das nötig sein sollte: Datentyp ist in beiden Fällen
unsigned int16. Nur das *in eben ein Pointer da drauf ist.

Mein momentaner Code lautet:

mov ax,[ebp+2]
mov [ebp-4],ax
inc ax
... ; hier wird noch viel mehr mit dem Wert gemacht, also lasse ich
; ihn gleich im Register und schreibe ihn erst danach zurück
mov [ebp+2],ax

Ich hab aber das gefühl, dass das nicht so ganz stimmt. Ich hab es noch
nicht ausprobiert (weil ich noch nicht fertig bin), aber stimmt meine
Befürchtung?

tia und cu,
nullplan
--
To err is human. To forgive is divine.
To forget is also human...
Alexander Bartolich
2006-09-21 18:29:59 UTC
Permalink
Post by Markus Wichmann
[...]
push ebp
mov ebp,esp
Nach dem push zeigt [esp+0] auf den am Stack gespeicherten Wert,
also den alten Wert von ebp.

Nach dem mov zeigt auch [ebp+0] auf den am Stack gespeicherten Wert,
also den alten Wert von ebp.

[ebp+4] zeigt auf die Rücksprungadresse (hat call dort hingestellt).

[ebp+8] zeigt auf den ersten Parameter.
Post by Markus Wichmann
In jedem Fall, wie kann ich jetzt z.B. den Inhalt des ersten Parameters
nach ax verschieben? (Unter der Annahme, es handele sich um einen
16-Bit-Wert).
Auf dem i386 werden Stackvariablen immer auf sizeof(int) aufgerundet.
Konkret schiebt "push" im 32-bit-Modus immer die vollen 32-bit auf
den Stack. Die Verwendung von "short" oder "char" als Parameter spart
keinen Speicher und macht nur den Code aufwändiger.
Post by Markus Wichmann
Von [ebp-0] bis [ebp-3] liegt ja der alte ebp.
Nein.
Negative Indizes zeigen auf die lokalen Variablen.
Positive Indizes auf die Parameter.
Post by Markus Wichmann
Also mit
mov ax,[ebp-4]
oder wie?
Nein.

$ nl -ba a.c
1 #include <stdlib.h>
2 void foo(short x0, short* in)
3 {
4 x0 = *in++;
5 exit(x0);
6 }

$ gcc -Wall -O1 -S a.c && nl -ba a.s
1 .file "a.c"
2 .text
3 .globl foo
4 .type foo, @function
5 foo:
6 pushl %ebp
7 movl %esp, %ebp
8 subl $8, %esp

gcc reserviert 8 Byte für lokale Variablen.
Das soll wohl eine Optimierung darstellen.

9 movl 12(%ebp), %eax

[ebp+12] zeigt auf den zweiten Parameter (von links gezählt),
also "short* in".

10 movswl (%eax),%eax

Die 16-Bit, auf die eax zeigt, werden nach eax geladen und auf
32-bit (vorzeichenbehaftet) erweitert.

11 movl %eax, (%esp)

Das ist entspricht einem "push %eax" ohne esp zu verändern.

12 call exit
Post by Markus Wichmann
Und jetzt noch was: Wenn ich Pointer übergeben kriege (als
Parameter), wie groß sind die unter WinNT (also Windows mit NT-Kernel =
NT + 2kx + XP + Vista (glaub))
Im 32-bit-Modus sind die 32-bit groß.
Post by Markus Wichmann
[...]
Ich hab aber das gefühl, dass das nicht so ganz stimmt. Ich hab es noch
nicht ausprobiert (weil ich noch nicht fertig bin), aber stimmt meine
Befürchtung?
Du solltest dir ein paar C-Programme schreiben.
Zum Beispiel um sich die Ausgabe von

printf("%u\n", sizeof(void*));

ansehen zu können.

Auch der C-Compiler von Microsoft lässt sich auf der Kommandozeile
verwenden (nennt sich cl.exe) und kann Assembler-Listings erzeugen.

--
Urs Thuermann
2006-09-22 08:09:05 UTC
Permalink
Post by Markus Wichmann
ich wollte mal wissen, ob ich das mit der C-Aufrufskonvention (ich
weis, deutsch klingt es blöd) richtig verstanden habe. Also, zunächst
pusht der Rufer die Parameter falschrum auf den Stack.
Nein, die Argumente müssen schon in der richtigen Reihenfolge auf den
Stack gepush't werden, sonst funktioniert's nicht. Das heißt also das
letzte Argument zuerst, dann das vorletzte, usw. bis zum ersten.
Diese Reihenfolge ist notwendig, weil es in C Funktionen mit variabler
Anzahl von Argumenten gibt, deren Typ und Anzahl aus einem Argument
davor hervorgehen muß, also eins, das im festen Teil der
Parameterliste steht, also z.B. das fmt in printf(const char*fmt,...);
Durch die Reihenfolge der Argumente auf dem Stack wird erreicht, daß
das fmt an einer festen Position relativ zum stack pointer steht.
Gelöscht wird auf dem Stack genau genommen nichts.
Post by Markus Wichmann
push letzter_param
push erster_param
call my_function
add esp,sizeof(params)
Die Funktion soll dann ebp sichern und eine Stackframe eröffnen (oder
push ebp
mov ebp,esp
Meine Frage betrifft die lokalen Variablen. Was ich machen muss, ist
sub esp,4
Genau.
Nein, ist einfach nur weniger effizient. Bei vielen lokalen Variablen
ist es geschickter, mit nur einer sub-Intruktion Platz zu schaffen.
Post by Markus Wichmann
Aber wie greife ich auf diese Variablen jetzt zu? Mit [ebp-Stelle],
wobei ich das schon am Anfang per Präprozessor festlegen wollte. Ach
ja, bei NASM eignet sich welche Direktive besser? %assign oder
%define? In jedem Fall, wie kann ich jetzt z.B. den Inhalt des ersten
Parameters nach ax verschieben? (Unter der Annahme, es handele sich um
einen 16-Bit-Wert). Von [ebp-0] bis [ebp-3] liegt ja der alte
ebp.
Nein, nicht so ganz. Ich nehme an, Du redest von 32-bit-Code, dann
haben alle Werte auf dem Stack auch eine Größe, die durch 4 teilbar
ist, d.h. auch ein push %ax (also nur 16 Bits), dekrementiert den
stack pointer um 4.

Der stack frame sieht nach

push $2 # call foo(1,2);
push $1
call foo

foo: push %ebp # foo(int a, int b)
mov %esp, %ebp # {
sub $4, %esp # int x; /*4 bytes */

so aus

ebp + 12: 02 00 00 00
ebp + 8: 01 00 00 00
ebp + 4: < eip >
ebp + 0: < old ebp >
ebp - 4: < local x >

D.h. Du greifst mit [ebp+8] auf das erste Argument, mit [ebp+12] auf
das zweite Argument und mit [ebp-4] auf die lokale Variable x zu. x
liegt also bei [ebp-4] bis [ebp-1]. Das hängt aber auch noch vom
compiler ab. Es gibt da keine Vorschriften, wo er lokale Variablen
auf dem stack positioniert. Wie eben beschrieben, ist aber zumindest
einigermaßen üblich (je nach Optimierung).

Also mit
Post by Markus Wichmann
mov ax,[ebp-4]
oder wie?
Ja.
Post by Markus Wichmann
Und jetzt noch was: Wenn ich Pointer übergeben kriege (als
Parameter), wie groß sind die unter WinNT (also Windows mit NT-Kernel
= NT + 2kx + XP + Vista (glaub)) Ich verwende momentan Windows 2000,
aber mein Programm soll ja auch portierbar sein.
Habe noch nicht auf Windows programmiert. Aber NT, 2000, XP, haben
sicher 4 Byte große pointer. Davor gab es schräge Dinge mit near und
far pointern, verschiedenen Speichermodellen (small, large, huge, oder
so), die aus dem segmentierten Speicher des x86 entstanden sind.
Post by Markus Wichmann
Um es ganz konkret zu machen: Unter C heist die Anweisung
x0 = *in++;
Wie geht das unter Assembler, unter der Maßgabe, dass x0 die erste
lokale Variable und *in der erste Parameter ist (liegt also direkt auf
ebp). Falls das nötig sein sollte: Datentyp ist in beiden Fällen
unsigned int16. Nur das *in eben ein Pointer da drauf ist.
mov ax,[ebp+2]
mov [ebp-4],ax
inc ax
Nein. Der pointer ist 4 Bytes lang und liegt bei [ebp+8]. Also
sollte das so aussehen (AT&T-Syntax):

mov 8(%ebp), %eax in Intel-Syntax: mov eax,[ebp+8]
mov (%eax), %bx (bin ich aber mov bx,[eax]
mov %bx, -4(%ebp) nicht so firm) mov [ebp-4],bx
inc %eax inc eax

Außer dem falschen offset 2 statt 8 für das erste Argument hast Du vor
allem die indirection (*-Operator) vergessen. Mit mov [ebp-4],ax
kopierst Du ja den pointer nach x0, nicht das, worauf er zeigt.

urs
Markus Wichmann
2006-09-22 16:45:44 UTC
Permalink
Hi erstmal.
Post by Urs Thuermann
Post by Markus Wichmann
ich wollte mal wissen, ob ich das mit der C-Aufrufskonvention (ich
weis, deutsch klingt es blöd) richtig verstanden habe. Also, zunächst
pusht der Rufer die Parameter falschrum auf den Stack.
Nein, die Argumente müssen schon in der richtigen Reihenfolge auf den
Stack gepush't werden, sonst funktioniert's nicht. Das heißt also das
letzte Argument zuerst, dann das vorletzte, usw. bis zum ersten.
Meine ich ja: Falschrum, also andersrum als man sie im C-Quelltext
vereinbart.
Post by Urs Thuermann
Außer dem falschen offset 2 statt 8 für das erste Argument hast Du vor
allem die indirection (*-Operator) vergessen. Mit mov [ebp-4],ax
kopierst Du ja den pointer nach x0, nicht das, worauf er zeigt.
Bedeutet also, das ich das, worauf der Zeiger zeigt mit

mov word ptr [ebp-4],ax

nach ax schiebe?
Danke für den ganzen Rest des Posts, jetzt weis ich, wo die Parameter
anfangen etc. (deshalb der falsche Offset. Ich dachte, nach

push ebp

liegt der Stackpointer am Anfang vom alten ebp. Puh, das hätte ich jetzt
also kapiert: Nein, _dahinter_.

Ich werd dann mal weiterbasteln.

cu,
nullplan
--
To err is human. To forgive is divine.
To forget is also human...
Stefan Reuther
2006-09-22 19:32:14 UTC
Permalink
Post by Urs Thuermann
Nein, nicht so ganz. Ich nehme an, Du redest von 32-bit-Code, dann
haben alle Werte auf dem Stack auch eine Größe, die durch 4 teilbar
ist, d.h. auch ein push %ax (also nur 16 Bits), dekrementiert den
stack pointer um 4.
Das ist so nicht richtig. "push %ax" (66 50) dekrementiert
selbstverständlich nur um zwei. Man sollte sowas natürlich nicht
ausarten lassen, weil man sonst bei folgenden 32-bit-Stack-
operationen (z.B. call) Strafzyklen für misalignment kassiert.

Evtl. verwechselst du das mit "push %cs" (0E), der im 32-bit-Modus
den %esp um 4 dekrementiert, obwohl %cs auch hier nur 16 Bit hat
(mit Präfix, "pushw %cs" (66 0E), ergibt auch dieser Befehl nur
2 Bytes auf dem Stack).
Post by Urs Thuermann
Post by Markus Wichmann
Und jetzt noch was: Wenn ich Pointer übergeben kriege (als
Parameter), wie groß sind die unter WinNT (also Windows mit NT-Kernel
= NT + 2kx + XP + Vista (glaub)) Ich verwende momentan Windows 2000,
aber mein Programm soll ja auch portierbar sein.
Habe noch nicht auf Windows programmiert. Aber NT, 2000, XP, haben
sicher 4 Byte große pointer. Davor gab es schräge Dinge mit near und
far pointern, verschiedenen Speichermodellen (small, large, huge, oder
so), die aus dem segmentierten Speicher des x86 entstanden sind.
FAR-Pointer gibt's unter 32-bit-Windowsen normalerweise nicht mehr.
Zeiger sind einfach 32-bittige Register, wie unter anderen 32-bit-
Betrübssystemen auch. Als 32-bit-Windows zählen von der API her die
NTs und alles ab Win95.


Stefan

Loading...