SDL Anwendungen mit Assembler programmieren
Da ich auf der Suche über Informationen zur Programmierung von SDL Anwendungen mit Assembler im Internet kaum nutzbare Informationen fand, sondern mehr Beiträge mit Fragen dazu, entschloss ich mich ein Tutorial über dieses Thema zu schreiben.
In dem Tutorial versuche ich Schritt für Schritt in die Programmierung von SDL mit Assembler einzuführen, sodass außer grundlegenden Kenntnissen in Assmbler keine weiteren Kenntnisse vonnöten sind. Kenntnisse über die Linux System Calls und deren Verwendung in Assembler sind an manchen Stellen hilfreich, aber nicht unbedingt notwendig. Gleiches gilt für die SDL Funktionen und Datentypen.
Die Beispiele wurden mit dem nasm Assembler für Linux entwickelt. Als Ausgabeformat für die kompilierten Dateien verwende ich das elf-Format. Zum Übersetzen des Quellcodes und Linken der generierten Objektdatei mit den SDL Bibliotheken habe ich folgendes Script geschrieben:
#!/bin/sh
nasm -f elf $1.asm
ld -dynamic-linker /lib/ld-linux.so.2 -rpath /usr/lib -L/usr/lib -lSDL -lpthread $1.o -o $1
rm *.o
Speichere es unter sdl_asm_ld.sh, oder einem Dateinamen deiner Wahl. Aufgerufen wird das Script dann mit ./sdl_asm_ld.sh dateiname. Der Dateiname ist der Dateiname des Assemblerquellcodes ohne Dateiendung.
Das Flag -dynamic-linker /lib/ld-linux.so.2 ist notwendig, damit später nicht die Meldung
beim Starten des Programms erscheint.
Nun zur Entwicklung und dem Aufbau des Quellcodes.
Generell werden die SDL Funktionen in Assembler wie jede andere Funktion aufgerufen. Doch dazu muss dem Assembler erst mitgeteilt werden, dass die Funktion nicht in der Datei mit dem Assemblerquellcode definiert wurde. Um dies dem Assembler mitzuteilen existiert die extern-Direktive. Das folgende Beispiel zeigt, wie die Funktion SDL_Init eingebunden wird.
extern SDL_Init
Damit ist dem Assembler die Existenz der Funktion SDL_Init bekannt. In höheren Programmiersprachen kann man den Funktionen Parameter übergeben, doch wie wird das in Assembler geregelt? Auch SDL benötigt zum Ausführen seiner Funktionen Paramter. Damit man aber nicht alle verschieden Register mit den Werten füllen muss, werden die Parameter in umgekehrter Reihenfolge auf dem Stack gespeichert.
push DWORD SDL_INIT_VIDEO ;SDL_INIT_VIDEO ist eine Konstante mit dem Wert 0x20
call SDL_Init
add esp, 4
An diesem Beispiel kann man noch nicht erkennen, in welcher Reihenfolge die Parameter auf dem Stack gespeichert werden, dies verdeutlichen spätere Beispiele noch sehr deutlich.
Funktionen haben für ihre Abarbeitung zwei Möglichkeiten. Erstens, sie holen die Argumente vom Stack oder sie belassen sie auf dem Stack und greifen direkt auf den Speicher zu. Die zweite Variante, die auch SDL verwendet, wird häufiger eingesetzt. Allerdings muss dann nach dem Ausführen der Funktion der Stack-Pointer angepasst werden. Dazu addiert man die Größe der Parameter in Byte zu dem Stack-Pointer hinzu.
Im obigen Beispiel wäre dies also 4, denn wir haben ein Doppelwort (4 Byte) auf den Stack gepushed. Vergisst man den Stack-Pointer anzupassen kann dies zu einem Segmentation Fault führen.
Ebenfalls aus höheren Programmiersprachen dürfte die Möglichkeit von Rückgabewerten
bei Funktionen bekannt sein. In Assembler wird dafür in der Regel das EAX, AX oder eines der beiden 8 Bit Register AH / AL verwendet, je nach Größe des Rückgabewerts.
Die Funktion SDL_Init liefert den Rückgabewert beispielsweise in EAX. Wenn dieser den Wert 0 hat, wurde die Funktion erfolgreich ausgeführt.
or eax, eax
mov ecx, init_error
mov edx, init_error_len
jnz error
Wenn die Funkion nicht fehlerfrei ausgeführt wurde, der Wert in eax also nicht 0 ist, wird zu dem Label error verzweigt, andernfalls wird im Programm normal fortgefahren. Näheres zu der Funktion error später im kompletten Programmbeispiel.
Im Folgenden werde ich auf einzelne Funktionen, wie SDL_SetVideoMode, das Anzeigen eines Bildes und dem Abfragen von (Tastatur-, etc.) Ereignissen näher eingehen.
Zum Einstellen des Video-Modus und Anzeigen eines Fenster steht die Funktion SDL_SetVideoMode zur Verfügung.
Also erst einmal dem Assembler von der Existenz der Funktion mitteilen.
extern SDL_SetVideoMode
Diese Zeile fügt ihr am besten hinter der ersten extern-Direktive ein. Im Folgenden werde ich nicht mehr die einzelnen extern-Direktiven aufführen.
Die Funktion benötigt 4 Doppelwort-Parameter, die Breite, die Höhe, die Farbtiefe und ein Flag, das verschiedene Informationen für das Fenster enthält.
push DWORD SDL_SWSURFACE ;SDL_SWSURFACE ist eine Konstante mit dem Wert 0
push DWORD 24
push DWORD 480 ;Höhe
push DWORD 480 ;Breite
call SDL_SetVideoMode
add esp, 16 ;4 * 4 Byte
An diesem Beispiel sieht man sehr deutlich, dass die Parameter in umgekehrter Reihenfolge auf dem Stack gespeichert werden.
Die Funktion liefert einen Pointer zurück, der auf ein SDL_Surface zeigt. Hat der Rückgabewert aus eax den Wert 0, so trat während der Ausführung der Funktion ein Fehler auf, andernfalls entspricht der Wert der Adresse, auf die der Pointer zeigt. Diesen speichern wir in einer Variablen.
mov [screen], eax
Die Variable screen ist 4 Byte groß. Auf das Abfangen des Fehlers und das Definieren der Variable bin ich hier bewusst nicht eingegangen, da dies nicht Teil des Tutorials sein soll. Das Beispielprogramm enthält
selbstverständlich den Code dazu.
Nun sollte, wenn alles geklappt hat, ein Fenster erscheinen und gleich darauf wieder verschwinden.
Damit SDL allerdings ordnungsgemäß beendet wird, muss die Funktion SDL_Quit aufgerufen werden. Diese benötigt keine Parameter und kann direkt mit
call SDL_Quit
aufgerufen werden.
Das Anzeigen eines Bildes ist schon etwas umständlicher. In C/C++ würde zum Laden des Bildes die Funktion SDL_LoadBMP zur Verfügung stehen. Der Versuch, sie in Assembler zu verwenden, führt allerdings zu der Fehlermeldung
Ein Blick hinter die Kulissen in den SDL Header-Dateien für C/C++ verrät warum. Ausschnit aus SDL_video.h:
[...]
/* Convenience macro -- load a surface from a file */
#define SDL_LoadBMP(file) SDL_LoadBMP_RW(SDL_RWFromFile(file, "rb"), 1)
[...]
Bei SDL_LoadBMP handelt es sich also lediglich um ein Makro, das sich aus zwei Funktionen zusammensetzt. SDL_RWFromFile liest die Datei ein und SDL_LoadBMP_RW liest die Bilddaten aus dem Dateinhalt aus.
Ein Aufruf, um eine Datei und seine Bilddaten zu laden, sieht folgendermaßen aus:
push DWORD rw_mode ;Modus zum öffnen, ein zero-terminated String, 'rb'
push DWORD filename ;der Dateiname, ebenfalls ein zero-terminated String
call SDL_RWFromFile
add esp, 8
push DWORD 1 ;Datei nach Funktionsende wieder freigeben
push eax ;Zeiger auf die Datei, Rückgabewert von SDL_RWFromFile
call SDL_LoadBMP_RW
add esp, 8
mov [image], eax
Der Rückgabewert von SDL_LoadBMP_RW liefert wie die Funktion SDL_SetVideoMode einen Zeiger auf ein SDL_Surface.
Damit das Bild nun auch angezeigt wird, muss noch die Funktion SDL_UpperBlit (In C/C++ kann auch SDL_BlitSurface verwendet werden) aufgerufen werden.
push DWORD 0
push DWORD [screen] ;Ziel
push DWORD 0
push DWORD [image] ;Quelle
call SDL_UpperBlit
add esp, 16
Ist der Rückgabewert in eax ungleich 0, dann ist beim Blitten ein Fehler aufgetreten. Andernfalls sollte das Bild jetzt erscheinen.
Zum Schluss des Tutorials möchte ich noch das Reagieren auf Ereignisse behandeln. Dafür stellt SDL die Funktion SDL_PollEvent zur Verfügung, die das aufgetretene Ereignis in einer relativ großen Struktur speichert. Das Abdrucken der Struktur erspare ich mir an dieser Stelle und verweise auf das Beispielprogramm. Die Namensgebung wurde entsprechend den Vorgaben in den SDL Header-Dateien für C/C++ gestaltet.
Die Funktion SDL_PollEvent erwartet als Parameter lediglich einen Zeiger auf die Struktur.
push DWORD event
call SDL_PollEvent
add esp, 4
cmp [event.type], BYTE SDL_KEYDOWN ;wurde eine beliebige Taste gedrückt?
je end
publicminx
yes, its really difficult to find any informations about assembler + sdl (and more) ...
do you also have an 64 bit example?
N43
Hallo,
man muss zunächst das Linker-Script anpassen, damit die 64-Bit Libraries verwendet werden.
nasm -f elf64 $1.asm
ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 -rpath /usr/lib64 -L/usr/lib64 -lSDL -lpthread $1.o -o $1
rm *.o
Dann hat sich die Calling-Convention geändert. Anstatt alle Parameter auf den Stack zu legen, werden (in der Reihenfolge) die Register edi, esi, edc, ecx, r8d, r9d verwendet. Alle weiteren Parameter werden dann wieder auf den Stack gelegt.
Die entfernten Code-Zeilen habe ich nur auskommentiert, damit man den Unterschied sieht:
init_error DB "Init Fehler!", 0xA
init_error_len EQU $-init_error
video_error DB "Video Fehler!", 0xA
video_error_len EQU $-video_error
bmp_error DB "BMP Fehler!", 0xA
bmp_error_len EQU $-bmp_error
blit_error DB "Blit Fehler!", 0xA
blit_error_len EQU $-blit_error
filename DB 'test.bmp', 0
rw_mode DB 'rb', 0
screen DD 0
image DD 0
event:
.type DB 0
.active_type DB 0
.active_gain DB 0
.active_state DB 0
.key_type DB 0
.key_which DB 0
.key_state DB 0
.key_keysym_scancode DB 0
.key_keysym_sym DD 0
.key_keysym_mod DD 0
.key_keysym_unicode DB 0
.motion_type DB 0
.motion_which DB 0
.motion_state DB 0
.motion_x DW 0
.motion_y DW 0
.motion_xrel DW 0
.motion_yrel DW 0
.button_type DB 0
.button_which DB 0
.button_button DB 0
.button_state DB 0
.button_x DW 0
.button_y DW 0
.jaxis_type DB 0
.jaxis_which DB 0
.jaxis_axis DB 0
.jaxis_value DW 0
.jball_type DB 0
.jball_which DB 0
.jball_ball DB 0
.jball_xrel DW 0
.jball_yrel DW 0
.jhat_type DB 0
.jhat_which DB 0
.jhat_hat DB 0
.jhat_value DB 0
.jbutton_type DB 0
.jbutton_which DB 0
.jbutton_button DB 0
.jbutton_state DB 0
.resize_type DB 0
.resize_w DD 0
.resize_h DD 0
.expose_type DB 0
.quit_type DB 0
.user_type DB 0
.user_code DD 0
.user_data1 DD 0 ;pointer
.user_data2 DD 0 ;pointer
.syswm_type DB 0
.syswm_msg DD 0 ;pointer to syswm
section .text
extern SDL_Init
extern SDL_Quit
extern SDL_SetVideoMode
extern SDL_RWFromFile
extern SDL_LoadBMP_RW
extern SDL_UpperBlit
extern SDL_UpdateRect
extern SDL_PollEvent
global _start
%define SDL_INIT_VIDEO 0x00000020
%define SDL_SWSURFACE 0x00000000
%define SDL_KEYDOWN 2
%define SDLK_ESCAPE 27
_start:
;PUSH DWORD SDL_INIT_VIDEO
mov edi, SDL_INIT_VIDEO
call SDL_Init
;add esp, 4
or eax, eax
mov ecx, init_error
mov edx, init_error_len
jnz NEAR error
;PUSH DWORD SDL_SWSURFACE
;PUSH DWORD 24
;PUSH DWORD 480
;PUSH DWORD 480
mov ecx, SDL_SWSURFACE
mov edx, 24
mov esi, 480
mov edi, 480
call SDL_SetVideoMode
;add esp, 16
or eax, eax
mov ecx, video_error
mov edx, video_error_len
jz NEAR error
mov [screen], eax
;PUSH DWORD rw_mode
;PUSH DWORD filename
mov esi, rw_mode
mov edi, filename
call SDL_RWFromFile
;add esp, 8
;PUSH DWORD 1
;push rax
mov esi, 1
mov edi, eax
call SDL_LoadBMP_RW
;add esp, 8
or eax, eax
mov ecx, bmp_error
mov edx, bmp_error_len
jz error
mov [image], eax
;PUSH DWORD 0
;PUSH DWORD [screen]
;PUSH DWORD 0
;PUSH DWORD [image]
mov ecx, 0
mov edx, [screen]
mov esi, 0
mov edi, [image]
call SDL_UpperBlit
or eax, eax
mov ecx, blit_error
mov edx, blit_error_len
jnz error
;PUSH DWORD [screen+12]
;PUSH DWORD [screen+8]
;PUSH DWORD 0
;PUSH DWORD 0
;PUSH DWORD [screen]
mov r8d, [screen+12]
mov ecx, [screen+8]
mov edx, 0
mov esi, 0
mov edi, [screen]
call SDL_UpdateRect
repeat:
;PUSH DWORD event
mov edi, event
call SDL_PollEvent
;add esp, 4
cmp [event.type], BYTE SDL_KEYDOWN
je end
jmp repeat
error:
mov eax, 4
mov ebx, 1
int 0x80
end:
call SDL_Quit
mov eax, 1
mov ebx, 0
int 0x80
hang:
jmp hang
publicminx
ah, cool. thank you!
i am going to try it out ...
N43
0x13 hat in dem Code ein paar Fehler entdeckt.
Bei SDL_UpperBlit wurde der Stack nach dem Aufruf nicht wieder geleert, wird jetzt durch add esp, 16 (4-Parameter) erledigt.
Die Funktion SDL_UpdateRect (nur im kompletten Quellcode) hatte die falschen Parameter übergeben bekommen. Die Adresse, auf die der Pointer screen zeigt, muss erst in eax geladen werden. Darüber kann dann auf die Elemente der Struktur SDL_Surface (in diesem Fall Breite und Höhe) zugegriffen werden. Außerdem wurde auch hier der Stack nicht wieder geleert.
N43
Kommentare
Download