SDL Anwendungen mit Assembler programmieren

10.08.2009
Author: N43

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:

Code:

#!/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
Zitat:
./dateiname: No such file or directory

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.

ASM - Code:
 
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.

ASM - Code:
 
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.

ASM - Code:
 
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.

ASM - Code:
 
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.

ASM - Code:
 
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.

ASM - Code:
 
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

ASM - Code:
 
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
Zitat:
undefined reference to `SDL_LoadBMP'


Ein Blick hinter die Kulissen in den SDL Header-Dateien für C/C++ verrät warum. Ausschnit aus SDL_video.h:
CPP - Code:
 
       [...]
       /* 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:

ASM - Code:
 
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.

ASM - Code:
 
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.

ASM - Code:
 
push DWORD event
call SDL_PollEvent
add esp, 4
cmp [event.type], BYTE SDL_KEYDOWN     ;wurde eine beliebige Taste gedrückt?
je end
 

publicminx

22.02.2009
Author: publicminx

yes, its really difficult to find any informations about assembler + sdl (and more) ...
do you also have an 64 bit example?

N43

22.02.2009
Author: N43

Hallo,

man muss zunächst das Linker-Script anpassen, damit die 64-Bit Libraries verwendet werden.

Code:
#!/bin/sh
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:

ASM - Code:
section .data
 
    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

06.03.2009
Author: publicminx

ah, cool. thank you!
i am going to try it out ...

N43

10.08.2009
Author: 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

Einen neuen Kommentar erstellen...