NASM und C/C++

Assembler-Funktionen in C/C++ verwenden

Letzte Änderung: 2. Januar 2000
Version 0.02


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.

 

Inhaltsverzeichnis

  1. Assembler
    1.1 Wieso Assembler?
    1.2 Was ist NASM?
    1.3 Was kann NASM?
  2. C/C++ Compiler und Object Formate
    2.1 DJGPP (GNU)
    2.2 MS (Visual) C++
    2.3 Borland C++ (Builder)
  3. C/C++ Funktion
    3.1 Aufrufkonvention
    3.2 Register
    3.3 Namenskonvention
    3.4 Code Beispiel
  4. Parameterübergabe
    4.1 Typenlänge
    4.2 Zugriff auf Parameter
    4.3 Code Beispiel
  5. Rückgabewert
    5.1 Rückgabewert
    5.2 Code Beispiel
  6. Externe Funktionen und Variablen
    6.1 Externe Funktionen und Variablen
    6.2 Code Beispiel
  7. Header File erstellen
    7.1 Deklaration
    7.2 Code Beispiel
  8. Schlusswort
    8.1 Schlusswort

 

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

:
Stack
Parameter n
:
Parameter 1
Alter 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 Byte
float
4 Byte
short int
2 Byte
double
8 Byte
int
2 oder 4 Byte
long double
8, 10 oder 12 Byte
long int
4 Byte
void
1 Byte
long long int
8 Byte
Pointer, Referenz
4 Byte

Zu 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

:
Stack
Parameter n
:
Parameter 1
Alter EIP für RET
Alter EBP
Local Var 1
:
Local Var n

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

 

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