| FAQ der Newsgroup de.comp.lang.assembler (d.c.l.a.) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Was sind "calling conventions"? Aufrufkonvention (engl. calling convention) nennt man die Art und Weise, wie Werte (Parameter, Argumente) an eine Funktion übergeben und Ergebnisse der Funktion zurückgegeben werden. In Assembler ist man völlig frei darin, das nach Zweck und Funktionalität zu organisieren. Will man jedoch mit einer Funktion zusammenarbeiten, die von anderen Sprachen erstellt worden sind, dann muss man dessen Aufrufkonvention beachten. Nicht zur Aufrufkonvention gehört die Organisation von lokalen Variablen. Bei den meisten Aufrufkonventionen werden die Argumente oder Parameter auf dem Stack übergeben. Ein einfaches Beispiel:
Das Programm ruft zunächst das Unterprogramm Prozedur auf und übergibt ihm die Zahl 3 als Argument, was dort als Arg1 ankommt. Prozedur übergibt dieses Argument und zusätzlich eine 4 an die Funktion Funktion, was dort als Arg1 und Arg2 ankommt. Funktion addiert die beiden übergebenen Werte, speichert das Ergebnis in der lokalen Variable Ergebnis und gibt Ergebnis an Prozedur zurück, womit die lokale Variable Rechnung gefüllt wird. Das Programm beendet sich ohne Ausgabe. Das Programm ist ziemlich sinnlos, dafür einfach und durchschaubar. Bei Programmbeginn zeigt ESP auf eine Stelle im Stack, die die Rückkehradresse zum Betriebssystem enthält:
Nun wird 3 gepusht und Prozedur aufgerufen. Auf dem Stack befinden sich somit eine 3 (DWord) und die Rückkehradresse nach _main. ESP wurde um 8 Byte (2 DWord) dekrementiert und zeigt nun im Stack auf die Adresse, die auf die Rückkehradresse zeigt. Bei Beginn von Prozedur sieht der Stack also so aus:
In Prozedur wird zunächst EBP gesichert und dann als zweiter Zeiger auf den Stack initialisiert (push ebp & mov ebp,esp). Dann wird die lokale Variable Ergebnis angelegt, indem Platz dafür auf dem Stack reserviert wird (sub esp, 4). Danach sieht der Stack so aus:
Um nun Arg1 und 4 an Funktion übergeben zu können, werden
diese gepusht, und zwar zuerst 4 und dann Arg1. Im C-Beispiel sieht
dieser Funktionsaufruf so aus: Rechnung = Funktion (Arg1, 4);. Rechts steht
die 4 und links steht Arg1. Die Push-Reihenfolge kann man somit als
"von rechts nach links" bezeichnen. Innerhalb von Funktion wird EBP neu gesetzt. Damit kann mittels EBP auf die zueletzt übergebenen Werte zugegriffen werden. Bezogen auf den Funktionskopf int Funktion (int Arg1, int Arg2) steht bei [ebp+8] das Argument Arg1 und bei [ebp+12] das Argument Arg2. Zurückgegeben wird das Funktionsergebnis im EAX-Register.
ESP zeigt nicht auf eine Rückkehradresse, sondern auf Arg1. Sollte jetzt ein RET-Befehl kommen, dann stürzt das Programm ab, weil ein Rücksprung zur Adresse 0000003 versucht wird. Die beiden PUSH-Befehle vor dem CALL-Befehl müssen also rückgängig gemacht werden - man spricht von "Aufräumen des Stacks". Wenn der Caller den Stack aufräumen muss, hat man es meist mit der Aufrufkonvention cdecl zu tun. Das kann man mit zwei POP-Befehlen bewerkstelligen, oder - wie hier - durch ein Inkrementieren des ESP-Registers um 2 DWord oder 8 Byte (add esp, 8). Nun muss noch die lokale Variable Ergebnis mit dem Funktionsergebnis EAX gefüllt werden. Ergebnis befindet sich 4 Bytes unterhalb des EBP-Zeigers: [ebp-4]. Nach dem Befehl mov [ebp-4], eax sieht der Stack so aus:
ESP zeigt immer noch nicht auf eine Rückkehradresse. Auch die Befehlsfolge push ebp (...) sub esp, 4 muss rückgängig gemacht werden. AMD empfiehlt dafür leave, INTEL rät von leave ab. Hier wurde die Befehlsfolge mov esp, ebp & pop ebp gewählt. Eine Besonderheit stellt der Befehl ret 4 dar: Nach dem Rücksprung wird der Stack nochmals um 4 Byte inkrementiert. Der Stack wurde somit innerhalb der Prozedur aufgeräumt, der Aufrufer (Caller) muss das also nicht mehr machen. Dieses Verfahren entspricht der Aufrufkonvention stdcall. Zum Schluss wird in _main noch das Rückgaberegister EAX mit 0 gefüllt (alles ok) und zum Betriebssystem zurückgesprungen. Microsoft benutzt hauptsächlich vier Modelle: __cdecl, __stdcall, __fastcall, thiscall. Die zwei Unterstriche vor den Bezeichnungen weisen den MS-C-Compiler darauf hin, dass es sich um Schlüsselwörter handelt. Weitere Links: http://www.3rd-evolution.de/docs/windows/callconv/ (deutsch)http://www.cs.cornell.edu/courses/cs412/2001sp/resources/microsoft-calling-conventions.html (englisch) http://msdn.microsoft.com/de-de/library/ek8tkfbw(en-us,VS.80).aspx (thiscall - englisch) http://www.codeproject.com/KB/cpp/calling_conventions_demystified.aspx (englisch) In Linux wird häufig der Begriff Application Binary Interface (ABI) verwendet. Zu beachten ist ferner, dass in der Linux-Welt mehr auf den verwendeten Compiler (meist GCC) abgestellt wird. Das ABI enthält die Aufrufkonventionen, aber auch noch weitere Konventionen. Weitere Links:
http://asm.sourceforge.net/howto/conventions.html (englisch)
Sehr gut und ausführlich:
Agner Fog: Calling conventions for different C++ compilers and operating systems (Optimization manual No. 5) Ralph 'rkhb' Bauer Dez 2008 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||