NASM und C/C++Assembler-Funktionen in C/C++ verwendenLetzte Änderung: 2. Januar 2000 |
Das Ziel dieses Dokumentes ist es, an Beispielen zu erläutern, wie man
eine C/C++ - Funktion in Assembler erstellt und diese in einem C/C++ Compiler
verwenden kann. Es wird dabei nicht eingegangen auf die Assemblerprogrammierung
selbst. Es wird vorausgesetzt, dass die Prozessorbefehle bekannt sind.
Alle
Beispiele gehen davon aus, dass ein x86 Prozessor und NASM verwendet wird. Es
ist aber möglich das vermittelte Wissen auch auf andere Prozessoren und
Assembler zu übertragen.
1. Assembler |
1.1 Wieso Assembler?
"Assembler ist etwas, dass keiner mehr bracht. Es gibt schliesslich Hochsprachen wie C, C++, Pascal, Delphi, SmallTalk, ..."
Hochsprachen erleichtern das Leben schon sehr. Eine Applikation nur in Assembler zu schreiben, wie den HTML Editor Dreamweaver auf dem gerade dieses Dokument geschrieben wird, halte ich für fast unmöglich. Wenn es aber darum geht eine "kleine" Funktion zu programmieren die sehr zeitkritisch ist. Oder wenn eine Funktion viel einfacher in Assembler zu schreiben ist, weil die Funktion vom Prozessor unterstützt wird, nicht aber von der Hochsprachen, dann macht das ganze doch einen Sinn.1.2 Was ist NASM?
NASM ist die Abkürzung für Netwide Assembler. Es ist ein Freeware-Assembler für x86 kompatible Prozessoren und unter der Adresse http://www.cryogen.com/Nasm/ downloadbar. NASM verwendet einen Syntax, der von anderen x86 Assemblern wie Microsofts MASM oder Borlands TASM abweicht, aber nicht prinzipiell anders ist.
NASM gibt es für die Betriebsysteme Linux und Dos/Win32.1.3 Was kann NASM?
NASM ist in der Lage unter anderem Object Files in verschiedenen Formaten zu erzeugen, welche dann mit einem Linker zu einem Executable zusammengelinkt werden können. NASM unterstützt die Formate COFF, OMF, ELF und noch ein paar weiter Formate. Beim COFF muss noch unterschieden werden zwischen dem Standard COFF und dem COFF welches Microsoft bei Win32 verwendet.
Mit NASM ist es nicht möglich inline Code zu schreiben. Mit dem integrierten Assemblern der C/C++ Compiler ist dies möglich und manchmal auch sinnvoller als eine Funktion mit NASM zu schreiben.
2. C/C++ Compiler und Object Formate |
2.1 DJGPP (GNU)
DJGPP ist ein Freeware C/C++ Compiler für Win32. Dieser Compiler kann aber nur 16-Bit Code für Dos erstellen. DJGPP kann unter http://www.delorie.com/ downgeloadet werden. DJGPP verwendet das Object Format COFF. Beim erstellen eines Object Files mit NASM muss entsprechend COFF als Object File Format angegeben werden.
2.2 MS (Visual) C/C++
Microsofts 16-Bit C/C++ Compiler verwende das Object Format OMF. Die 32-Bit C/C++ Compiler verwenden nach Microsofts Aussage das COFF Format. Da Microsoft gerne eigene Standards macht, ist natürlich Microsoft COFF nicht gleich dem der restlichen Welt. NASM sagt diesem Object Format WIN32.
2.3 Borland C++ (Builder)
Borlands C++ Compiler verwenden ganz einheitlich das Object Format OMF. (???)
3. C/C++ Funktion |
3.1 Aufrufkonvention
Der Aufruf einer C-Funktion erfolgt in folgenden Schritten:
1. Der Aufrufer legt die Übergabewerte in umgekehrter Reihenfolge auf den Stack.
2. Der Aufrufer legt den Instruction Pointer auf den Stack.
3. Der Aufrufer ruft die Funktion mit CALL auf.
4. Die aufgerufene Funktion wird ausgeführt.
5. Mit RET wird die Funktion beendet. Als Rücksprungadresse wird die Adresse verwendet die auf dem Stack liegt.
6. Der Aufrufer ändert den Stack Pointer auf den Anfangszustand zurück.In C/C++ besteht die Möglichkeit eine andere Aufrufkonvention zu verwenden indem die Aufrufkonvention spezifiziert wird. So es ist möglich die PASCAL, _fastcall oder _stdcall Aufrufkonvention zu verwenden.
3.2 Register
Eine Funktion darf nicht x-beliebige Register verändern. Was für Register verändert werden dürfen, hängt vom eingesetzten Compiler ab. Als Faustregel kann gelten, dass eine Funktion nur die Register EAX, EBX, ECX und EDX verändern darf. Alle anderen sollten - es gibt immer Ausnahmen - die gleichen Werte wie beim Eintritt in die Funktion haben. Werden andere Register benutzt, so müssen sie auf den Stack gesichert und vor dem Rücksprung zurückgeschrieben werden.
Ausnahmen von dieser Regel:
- Microsoft C/C++ Compiler mit eingeschalteter Optimierung O1 (min Size) und O2 (max Speed). Das Register EBX ist ebenfalls zu sichern!3.3 Nameskonvention
Eine Funktion hat bekanntlich immer einen Namen. Der Namen wird als Label für den Einsprungpunkt in die Funktion verwendet. Der Name des Labels in einem Object File ist aber nicht gleich dem Funktionsnamen.
C Funktion und Variablen
Das Label für eine C-Funktion und ein C-Variable ist noch einfach und ist bei jedem Compiler gleich und lautet _Funktionsname beziehungsweise _Variablenname.C++ Funktion und Variablen
C++ Funktionen können bekanntlich gleiche Namen und unterschiedliche Übergabeparameter haben. Die Übergabeparameter sind deshalb im Label enthalten. Die Codierung für die Parameter ist leider bei allen Compilern verschieden.
Entweder kennt man die Codierung, die der Compiler verwendet oder man compiliert die C++ Datei, welche die Assemblerfunktion aufruft und schaut im Object File dieser C++ Datei nach, wie das Label der Funktion heisst.Hier nun die Auflistungen für verschiedene Compiler mit ein paar Beispielen zu den Codierungen.
DJGPP C++ Funktionen
_Funktionsname__F + Parameter 1 + ... + Parameter n P * c char f float R & s short d double PC const * i int r long double RC const & l long U unsigned x long long v void G + Namenlänge + Namen struct, union, class
DJGPP C++ Variable_Variablenname
_Konstantenname
DJGPP Beispiele Pointer
Referenz
Konstantevoid rename(char* const a, const char* b); _rename__FPcPCc void xyz(const unsigned char** const &b); _xyz__RCPPCUc double* fft(const double* &a, const int size); _fft__RPCdi int add(float* const &a, const unsigned int b); _add__FRCPfUi float blabla(void a, void* b, void c); _blabla__FvPvT0 float blabla(const double a, double b, double c); _blabla__FddT1 Eigene Typen void test(myType a, const herType* b) _test__FG6myTypePC7herType char haha(CoolClass z, Union& x) _haha__FG9CoolClassR5Union Wichtig:
1. Kam ein Parameter bereits einmal vor, so wird der Parameter abgekürzt Tx. Wobei x die Position des ersten Vorkommens des Parameters kennzeichnet. Der 1. Parameter hat dabei die Position 0.
2. Wird ein eigener Typ per Referenz oder Pointer übergeben, entfällt das G
MSVC C++ Funktionen
?Funktionsname@@YA + Return + Parameter 1 + ... + Parameter n + @Z AA & D char E unsigned char M float PA * F short G unsigned short N double QA *const H int I unsigned int O long double AB const & J long K unsigned long X void PB const * T + UnionName + @@ struct QB const *const U + StructName + @@ class V + ClassName + @@ union
MSVC C++ Variable?Variablenname@@3 + Datentyp + A
?Konstantenname@@3 + Datentyp + B
MSVC Beispiele Pointer
Referenz
Konstantevoid rename(char* const a, const char* b); ?rename@@YAXQADPBD@Z void xyz(const unsigned char** const &b); ?xyz@@YAXABQAPBE@Z double* fft(const double* &a, const int size); ?fft@@YAPANAAPBNH@Z int add(float* const &a, const unsigned int b); ?add@@YAHABQAMI@Z float blabla(void a, void* b, void c); ?blabla@@YAMXXX@Z float blabla(const double a, double b, double c); ?blabla@@YAMNNN@Z Eigene Typen void test(myType a, const herType* b) ?test@@YAXUmyType@@PBUherType@@Z char haha(CoolClass z, Union& x) ?haha@@YADVCoolClass@@AATUnion@@Z Wichtig:
1. Ist der Returntyp ein struct, class oder union, so wird ?A vorangestellt.
2. Ist der Returnwert konstant, dann wird ?B vorangestellt.
3. Mehr als zwei @@ gibt es nicht (@@@ => @@)
4. Kommt ein struct, class oder union Type mehrmals vor, so wird der Type abgekürzt mit Tx@@, Ux@@ oder Vx@@. Wobei x die Position des Parameter angibt. Der Returnwert ist dabei Position 1 und der 1.Parameter Position 2.
Borland C++ Funktionen
@Funktionsname$q + Parameter 1 + ... + Parameter n p * c char f float r & s short d double x const i int g long double l long u unsigned v void Namenlänge + Namen struct, class, union
Borland C++ Variable_Variablenname
_Konstantenname
Borland Beispiele Pointer
Referenz
Konstantevoid rename(char* const a, const char* b); @rename$qxpcpxc void xyz(const unsigned char** const &b); @xyz$qrxppxuc double* fft(const double* &a, const int size); @fft$qrpxdxi int add(float* const &a, const unsigned int b); @add$qrxpfxui float blabla(void a, void* b, void c); @blabla$qvpvv float blabla(const double a, double b, double c); @blabla$qxddd Eigene Typen void test(myType a, const herType* b) @test$q6myTypepx7herType char haha(CoolClass z, Union& x) @haha$q9CoolClassr5Union
3.4 Code Beispiel
Code Beispiel 3.4 ; AsmFile: Sample1.asm ; Header: Sample1.h ; Testprog: Test1.cpp ; For: C++ - Compiler ; Author: Stefan Schwendeler ; Datum: 26. September 1999 [BITS 32] [GLOBAL _astrupr__FPc] [GLOBAL ?astrupr@@YAPADPAD@Z] [GLOBAL @astrupr$qpc] [SECTION .text] ; ------------------------------------------------------------------ ; Prototype: char* astrupr(char* string); ; Wandelt einen String in Grossbuchstaben um ; ------------------------------------------------------------------ string equ 8 ;1. Parameter _astrupr__FPc: ;für DJGPP ?astrupr@@YAPADPAD@Z: ;für MSVC @astrupr$qpc: ;für Borland push ebp ;ebp sichern mov ebp, esp ;esp -> ebp mov edx, [ebp + string] dec edx .WHILE inc edx cmp byte[edx], 00h ;Ende des Strings? je .END CMP byte[edx], 'a' ;kleiner 'a'? jl .WHILE cmp byte[edx], 'z' ;grösser 'z'? jg .WHILE sub byte[edx], 20h ;klein -> gross jmp .WHILE .END mov eax, [ebp + string] ;Return-Wert in eax mov esp, ebp ;ebp -> esp pop ebp ;ebp zurück schreiben ret [SECTION .data] [SECTION .bss]
4. Parameterübergabe |
4.1 Typenlänge
:
StackParameter n
:
Parameter 1Alter EIP für RET In Kapitel 3.1 Aufrufkonvention wurde bereits beschrieben wie in C/C++ die Parameterübergabe von sich geht. Die Parameter werden rückwärts auf den Stack gelegt. Wobei der Stack im Adressraum nach unten wächst, also Richtung Null. Beim Zugriff auf die Parameter ist es wichtig zu wissen, wie lang die übergebenen Parameter sind und wie das Stack Alignment des Compilers ist. Wenn ein 1 Byte langer Wert übergeben wird, wird meist 4 Byte auf dem Stack belegt. Bei 16-Bit Compilern können es auch nur 2 Byte sein.
char 1 Bytefloat 4 Byteshort int 2 Bytedouble 8 Byteint 2 oder 4 Bytelong double 8, 10 oder 12 Bytelong int 4 Bytevoid 1 Bytelong long int 8 BytePointer, Referenz 4 ByteZu beachten ist, dass bei int und long double die Länge abhängig vom Compiler ist. Zum Beispiel ist bei Visual C++ bis zur Version 4.2 ein int 2 Byte lang, ab Version 5 aber 4 Byte lang. Um zu prüfen wie lang ein Typ ist, kann ein Programm eingesetzt werden, welches mit sizeof() die Länge des Typs bestimmt. Es steht dazu auch das Programm TestSize zur Verfügung.
Unions, Strukturen und Klassen werden auch auf den Stack gelegt. Die Länge einer Union entspricht dem längsten Datentyp. Bei Strukturen und Klassen entspricht die Länge der Addition aller Einzellängen. Wobei es hier auch auf das Memory Alignment ankommt. Zum Beispiel ein char zählt auch hier meist 4 Byte und nicht 1 Byte.
4.2 Zugriff auf Parameter
:
StackParameter n
:
Parameter 1Alter EIP für RET Alter EBP Local Var 1
:
Local Var nDer Zugriff auf die Parameter erfolgt über den Stackpointer. Beim Eintritt in die Funktion steht der Stackpointer nicht auf dem ersten Parameter sondern auf der Rücksprungadresse, die für den Befehl RET gebraucht wird. Bei Funktionen ist es üblich am Anfang das Register EBP auf den Stack zu sichern und dann den Stackpointer ESP in EBP zu kopieren. Der Vorteil dieser Aktion ist es, dass lokale Variablen auf den Stack gelegt werden können und über EBP einfach zugegriffen werden kann, ohne viel rechnen zu müssen.
Der erste Parameter befindet sich nun an der Position EBP+8. Ein allfälliger zweiter Parameter der zum Beispiel 4 Byte lang ist, befindet sich dann an der Position EBP+12. Lokale, auf den Stack gelegte Variablen die zum Beispiel 4 Byte lang sind, befinden sich an der Position EBP-4, EBP-8 so weiter.4.3 Code Beispiel
Code Beispiel 4.3 ; AsmFile: Sample2.asm ; Header: Sample2.h ; Testprog: Test2.cpp ; For: C++ - Compiler ; Author: Stefan Schwendeler ; Datum: 28. September 1999 [BITS 32] [GLOBAL _add__FcsiRi] [GLOBAL ?add@@YAXDFHAAH@Z] [GLOBAL @add$qcsiri] [SECTION .text] ; ------------------------------------------------------------------ ; Prototype: void add(char _a, short _b, int _c, int& _d); ; _d = _a + _b + _c ; ------------------------------------------------------------------ _a equ 8 ;1. Parameter _b equ 12 ;2. Parameter _c equ 16 ;3. Parameter _d equ 20 ;4. Parameter tmp equ -4 ;int tmp _add__FcsiRi: ;für DJGPP ?add@@YAXDFHAAH@Z: ;für MSVC @add$qcsiri: ;für BORLAND push ebp ;ebp sichern mov ebp, esp ;esp -> ebp xor eax, eax push eax ;int tmp = 0 mov al, [ebp + _a] ;tmp += _a cbw ;char -> short cwde ;short -> int add [ebp + tmp], eax ; mov ax, [ebp + _b] ;tmp += _b cwde ;short -> int add [ebp + tmp], eax ; mov eax, [ebp + _c] ;tmp += _c add [ebp + tmp], eax ; mov eax, [ebp + _d] ;*_d = tmp mov edx, [ebp + tmp] ; mov [eax], edx ; mov esp, ebp ;ebp -> esp pop ebp ;ebp zurück schreiben ret [SECTION .data] [SECTION .bss]
5. Rückgabewert |
5.1 Rückgabewert
Wohin mit dem Rückgabewert?
- Integrale Datentypen
Der Rückgabewert wird ins EAX, AX oder AL Register gelegt. Ist der Datentype 64 Bit lang (long long int DJGPP, 64 Bit struct MSVC), so wird das higher Doubleword ins EDX Register gelegt.- Gleitkomma Datentypen
Der Rückgabewert wird ins Register ST0 der FPU gelegt, sofern eine vorhanden ist.- Pointer
Der Rückgabewert wird ins EAX Register gelegt.- Strukturen, Klassen, Unions
Der Aufrufer wird vor dem Funktionsaufruf einen Pointer auf den Stack legen. Der Rückgabewert muss an die Stelle gelegt werden, wohin dieser Pointer zeigt. Bei MSVC muss zusätzlich ein Pointer auf diese Struktur ins EAX Register gelegt werden.
:
StackParameter n
:
Parameter 1Struct Pointer Alter EIP für RET
Achtung !!! MSVC macht eine Ausnahme, wenn die Struktur <= 64 Bit lang ist. Die Struktur wird dann wie ein Integraler Datentyp zurückgegeben und es wird kein zusätzlicher Pointer auf den Stack gelegt. Dies ist ein bisschen schneller als die "normale" Art.
5.2 Code Beispiel
Code Beispiel 5.2 ; AsmFile: Sample3.asm ; Header: Sample3.h ; Testprog: Test3.cpp ; For: C++ - Compiler ; Author: Stefan Schwendeler ; Datum: 29. September 1999 [BITS 32] [GLOBAL _fadd__Fdf] [GLOBAL ?fadd@@YAMNM@Z] [GLOBAL @fadd$qxdxf] [GLOBAL _structInc__FG5zzTop] [GLOBAL ?structInc@@YA?AUzzTop@@U1@@Z] [GLOBAL @structInc$q5zzTop] [SECTION .text] ; ------------------------------------------------------------------ ; Prototype: float fadd(const double _a, const float _b=1.0f); ; _a + _b ; ------------------------------------------------------------------ _a equ 8 ;1. Parameter _b equ 16 ;2. Parameter _fadd__Fdf: ;für DJGPP ?fadd@@YAMNM@Z: ;für MSVC @fadd$qxdxf: ;für BORLAND push ebp ;ebp sichern mov ebp, esp ;esp -> ebp FLD qword [ebp + _a] ;_a -> ST0 FADD dword [ebp + _b] ;ST0 + _b mov esp, ebp ;ebp -> esp pop ebp ;ebp zurück schreiben ret ; ------------------------------------------------------------------ ; Prototype: zzTop structInc(zzTop _s); ; _c.a++; _c.b++ ; ------------------------------------------------------------------ return equ 8 return.a equ 0 return.b equ 4 _s.a equ 12 ;1. Parameter, short _s.b equ 16 ; float _structInc__FG5zzTop: ;für DJGPP @structInc$q5zzTop: ;für BORLAND push ebp ;ebp sichern mov ebp, esp ;esp -> ebp mov eax, [ebp + return] ; mov dx, [ebp + _s.a] ;_s.a++ inc dx ; mov [eax + return.a], dx ;return.a = _s.a FLD dword[ebp + _s.b] ;_s.b++ FLD1 ; FADD st1 ; FST dword[eax + return.b];return.b = _s.b pop ebp ;ebp zurück schreiben ret ;------------------------------------------------------------------- ;struct <= 8-Byte MSVC macht eine Speed-Optimierung ;------------------------------------------------------------------- _s.a2 equ 8 ;1. Parameter short _s.b2 equ 12 ;2. Parameter float ?structInc@@YA?AUzzTop@@U1@@Z: ;für MSVC push ebp ;ebp sichern mov ebp, esp ;esp -> ebp mov ax, [ebp + _s.a2] ;a++ inc ax ;a->ax FLD dword[ebp + _s.b2] ;b++ FLD1 ; FADD st1 ; FSTP dword [ebp + _s.b2] ; mov edx, [ebp + _s.b2] ;b->edx pop ebp ;ebp zurück schreiben ret [SECTION .data] [SECTION .bss]
6. Externe Funktionen und Variablen |
6.1 Externe Funktionen und Variablen
In der in Assembler geschriebenen Funktion können auch Funktionen und Variablen verwendet werden, die im C/C++ Code definiert sind. Die Funktion oder die Variable sind dann als EXTERN du deklarieren.
6.2 Code Beispiel
Code Beispiel 6.2 ; AsmFile: Sample4.asm ; Header: Sample4.h ; Testprog: Test4.cpp ; For: C - Compiler ; Author: Stefan Schwendeler ; Datum: 29. September 1999 [BITS 32] [GLOBAL _writeText] ;Funktion [GLOBAL _COUNTER] ;Variable [EXTERN _printf] ;Funktion [EXTERN _n] ;Variable [SECTION .text] ; ------------------------------------------------------------------ ; Prototype: void writeText(const char* string); ; Gibt string _n-mal aus und inkrementiert globale var COUNTER ; ------------------------------------------------------------------ string equ 8 ;1. Parameter _writeText: inc dword[_COUNTER] ;COUNTER++ mov ecx, [_n] .FOR push ecx ;ECX sichern push dword [esp + string];Parameter für printf call _printf add esp, byte 4 ;esp += 4 pop ecx ;ECX zurück loop .FOR ;ECX!=0 mov eax, [_n] ;n zurück geben ret [SECTION .data] _COUNTER dw 0 [SECTION .bss]
7. Header File |
7.1 Deklaration
Wenn man ein paar C/C++-Funktionen schreibt, ist es sinnvoll eine Headerdatei für das Assemblerfile zu schreiben. Alle Funktionen und Variablen die in Assembler geschrieben wurden, müssen im Headerfile als extern deklariert werden.
Wenn eine Funktion im C-Style geschrieben ist und diese Funktion mit einem C++ Compiler verwendet werden soll, so muss dem Compiler gesagt werden, dass es sich um eine C-Funktion und nicht um eine C++-Funktion handelt. Andernfalls würde es einen Linkerfehler geben.7.2 Beispiel
Header File des Beispiels 6.2. Mit extern "C" wird dem C++-Compiler mitgeteilt, dass es sich um eine C-Funktion handelt.
Code Beispiel 7.2a // AsmFile: Sample4.asm // Header: Sample4.h // Testprog: Test4.cpp // For: C++ - Compiler // Author: Stefan Schwendeler // Datum: 29. September 1999 #ifndef Sample4_h #define Sample4_h #include#ifdef __cplusplus extern "C" { #endif extern int writeText(const char* string); extern int COUNTER; extern int n; #ifdef __cplusplus } #endif #endif Sample4_h
Headerfile des Beispiels 4.3. Hier ist extern "C" nicht notwendig, da die Funktionen als C++-Funktionen geschrieben wurden.
Code Beispiel 7.2b // AsmFile: Sample3.asm // Header: Sample3.h // Testprog: Test3.cpp // For: C++ - Compiler // Author: Stefan Schwendeler // Datum: 28. September 1999 #ifndef Sample3_h #define Sample3_h struct zzTop {short a; float b;}; extern float fadd(const double _a, const float _b=1.0f); extern zzTop structInc(zzTop _s); #endif Sample3_h
8. Schlusswort |
8.1 Schlusswort
Es würde mich freuen, wenn ich ein paar Rückmeldungen bekommen würde. Ich bin nicht gerade der Guru in Assembler. Ich gebe mir aber grosse Mühe. Es gibt sicher ein paar Fehler im Text und Code. Möglicherweise sind auch ein paar Informationen falsch, da dieser Text alleinig auf meinen Erfahrungen basieren und nicht einfach abgeschrieben wurde. Für Fehlerhinweise, Verbesserungsvorschläge, konstruktive Kritik und schleimige Loblieder bin ich jederzeit zu haben. Ich bin erreichbar unter folgenden Adressen.
Email: Stefan.Schwendeler@gmx.ch
Homepage: http://studenten.freepage.de/schwendi