Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
ASM Space Invaders
In this Wiki I will highlight the project structure of the bootloader and the game. There will also be a list of all the resources I used to get started.
Some General Notes
While assembly looks incredibly intimidating at first sight, getting started is surprisingly easy. Its syntax is very straight forward and there is basically one single rule that applies to all of the instructions. I certainly benefited a lot from this project and can only recommend diving into assembly yourself if you are interested in understanding what is going on "under the hood".
Table Of Contents
- Coding Style
- Space Invaders
The project is written in x86 Assembly and can be build using the NASM assembler. I will not go into detail about how exactly assembly works. Check out these sources if you want to get an introduction to assembly:
- PCASM - An awesome book about x86 Assembly that will give you a real good overview
- Assembly Programming Tutorial from tutorialspoint.com
The set of instructions I used is very small. Here is a short list of concepts and instructions necessary for this project:
- First of all, you should be familiar with registers
- The difference between physical memory addresses and their
- General instructions:
jmpas well as conditional jumps
- Arithmetic instructions:
That is basically it. The
int instruction is by far the most complex in this list. It let's you call existing BIOS functionality in order to
access the display, the drives and IO devices. Here you can read more about interrupts
Besides beeing able to read assembly there is also some knowledge required about how the booting process works. I will try to briefly explain the parts necessary for this project. If you are interested in this topic and want to read more, check out OSDev.org.
One of the most significant differences to higher level languages is the way of how to pass arguments to functions and how to return their results. Since the registers are the fastest option and are used by the interrupts as well, they naturally were my first choice.
Very early on I ran into serious problems, though. The set of registers is very limited and I ended up overwriting important data within nested function calls. As it turns out there already exist calling conventions to avoid exactly this problem. Sadly, I did only learn about those afterwards and ended up with my own hacky solution. But at least for this small scale project it worked pretty well.
Arguments And Return Value
For this I stayed consistent with the interrupts by passing arguments and the return value in registers. I tried to follow some general conventions:
DIas source and destination pointers respectively
AXfor return values
Preserving Registers Among Function Calls
To not accidentally invalidate registers I use the stack to preserve all registers I will modify within a function. You will find patterns like this throughout the project:
some_function: push ax push dx [modify ax, dx] .done: pop dx pop ax ret
The bootloader is the very first program started by the BIOS after a bootable device is available and has been selected.
Here is a more detailed description of this process.
This first program is always loaded to the memory address
0x7c00 and is excatly 512 bytes long.
To be considered bootable it has to have a fixed 2 byte signature
at the very end:
000: 0x?? . . . 509: 0x?? 510: 0x55 511: 0xaa
This leaves exactly 510 bytes for the actual bootloader code and all of its variables.
Its only purpose is to load the game into the RAM and then pass execution to it.
The loader is implemented in
bootloader.asm. There are three essential parts for this to work.
Loading The Game
; the segment of the game mov ax, 0x07e0 ; setup the file-source mov ch, 0x00 ; cylinder 0 mov cl, 0x02 ; sector 2 (skip first sector, which is the bootloader) mov dh, 0x00 ; head 0 mov dl, 0x00 ; drive 0 (floppy disk) ; setup the destination mov es, ax ; segment starts directly after the bootloader (7c00 - 7dff) mov bx, 0x0000 ; copy data into RAM read: mov al, 0x04 ; read four sectors mov ah, 0x02 ; int 13h subfunction 2 -> read sectors (512 bytes) from disk int 0x13 ; copy sectors to ES:BX jc read ; carry-flag is set -> there was a read-error, retry
This loads 2 kb of data from the floppy disk and saves it to
0x7e00. The game is therefore placed into RAM immediately
after the code of the bootloader. Here is a more detailed description
of the used interrupt
0x13. Apparently floppy reads have a chance of random failures. The loader will therefore retry
after a failed loading attempt. This solution is far from perfect, but for now I am just happy it works at all.
Starting The Game
; rebase segments for game execution mov ax, 0x07e0 mov ds, ax ; data segment mov es, ax ; additional segments mov fs, ax mov gs, ax mov ss, ax ; stack segment ; enter the game code -> set CS:IP jmp 0x07e0:0x0000
After the game is successfully loaded the segment pointers are redefined to point to the game's base address. The bootloader then calls the game's first instruction at the very beginning of the file.
Creating A Bootable Program
; spacing and signature times 510 - ($ - $$) db 0 dw 0xaa55
In order to be interpreted as bootable the bootloader includes the necessary signature in the last two bytes.
The main file of the game is
space-invaders.asm. It includes all additional files and contains the
program's main loop. Also all the variables are still located here.
I used the NASM pre-processor to
%include the files located in
Notice, that the pre-processor replaces every
%include with the included files content. The resulting, single file
is then interpreted by the assembler. In contrast to e.g. multiple C files, this does not require any additional linking.
After doing some initializations, like calculating the position of the centered game screen, the program jumps to its main function.
main: mov ah, [program_state] cmp ah, 1 je .game cmp ah, 2 je .end .intro: call intro jmp main .game: call game jmp main .end: call end jmp main
This function will never return and just calls the subroutine that corresponds to the current program state.
The game is always in one of the three states
end. Each state function only returns if the state
has been changed.
This is the initial state that is only entered after the game started. Once left, the program alternates between the
end. It just prints some strings and waits for a key to be pressed.
intro: call clear_screen mov ax, intro_string_t mov bx, intro_string_o call print_window .wait: call get_key mov al, [key_pressed] cmp al, ' ' je .game jmp .wait .game: mov byte [program_state], 1 ret
After the message is presented the function waits for
SPACE to be pressed. The state is then changed to
the function returns.
This state does exactly the same as the intro state besides that it changes the presented string corresponding to the winner of the previous game.
This state needs to reset the game at the start and implement a nonblocking loop in order to move and render the game. The general structure looks like this:
game: call init_game .loop: ; check whether a key has been pressed ; check whether player or invaders have won .execute: ; move the game ; render the game ; sleep ; repeat loop .done: mov byte [program_state], 2 ret
All the necessary logic for these steps is implemented in the files in
This class of files contains
The purpose of these is to wrap most of the explicit interrupt calls and provide some general purpose features.
game.asm contains the logic to reset the game state and determine whether the game is still running.
move functions are defined in here.
move_invaders an internal counter is increased on every call.
When this counter is equal to
INVADERS_MOVE_CYCLES, the invaders are moved and the counter is reset.
Within the function an additional counter is compared to
The general structure is as follows:
move_invaders: ; check move counter .move: ; loop over all invaders .loop: ; skip destroyed invaders ; move invader ; check for bullet collisions ; check shoot counter .shoot: ; invader shoots a bullet .done: ret
render_invaders essentially just loops over all invaders and prints them to the screen.
player.asm basically has the same logic and structere as
bullets.asm contains the logic to create, remove, move and render bullets as well as to check for collisions.
It contains a special help function
_iterate_bullets which expects a pointer to a loop function at
This function is then called in every iteration with
SI pointing to the current invader.
_iterate_bullets: push si mov si, bullet_list .loop: cmp si, [bullet_list_end] je .done call di add si, 3 jmp .loop .done: pop si ret
The list of bullets starts at the very end of the binary:
; space-invaders.asm bullet_list resw 1 bullet_list_start resb 1
bullet_list_end points to the first byte after the list. The list itself starts at the last byte
of the binary and then grows into uninitialized RAM. This implementation is still from the time when I tried to
put everything into the bootloader.