Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Verständnisfrage: C calling convention

17 views
Skip to first unread message

Markus Wichmann

unread,
Sep 18, 2006, 2:18:01 PM9/18/06
to
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

unread,
Sep 21, 2006, 2:29:59 PM9/21/06
to
Markus Wichmann schrieb:
> [...]

> my_function:
> 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.

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

> Von [ebp-0] bis [ebp-3] liegt ja der alte ebp.

Nein.
Negative Indizes zeigen auf die lokalen Variablen.
Positive Indizes auf die Parameter.

> 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

> 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ß.

> [...]


> 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

unread,
Sep 22, 2006, 4:09:05 AM9/22/06
to
Markus Wichmann <null...@gmx.net> writes:

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

> Dann ruft er die Funktion, und danach löscht er den Stack wieder:

Gelöscht wird auf dem Stack genau genommen nichts.

> 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

Genau.

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

Nein, ist einfach nur weniger effizient. Bei vielen lokalen Variablen
ist es geschickter, mit nur einer sub-Intruktion Platz zu schaffen.

> 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
>
> mov ax,[ebp-4]
>
> oder wie?

Ja.

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

> 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

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

unread,
Sep 22, 2006, 12:45:44 PM9/22/06
to
Hi erstmal.

Urs Thuermann schrieb:


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

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

Stefan Reuther

unread,
Sep 22, 2006, 3:32:14 PM9/22/06
to
Urs Thuermann <u...@isnogud.escape.de> wrote:
> 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).

>> 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

0 new messages