x86 Assembly: Hello World!
Let's write “Hello World!” for 16-bit DOS, 32-bit and 64-bit Linux...
16-bit (DOS/API)
Using the DOS API is dead simple, however it doesn't provide a simple method to print NUL (0
) terminated strings. DOS expects strings to be terminated by a dollar-sign ($
) and the user cannot override this behavior.
org 100h ; Set code segment offset
; DOS PE header is 0FFh bytes long
jmp start
;
; ---- data ----
;
msg: db "Hello world!", "$"
;
; ---- main program ----
;
start:
mov ah, 9 ; DOS: Print string
mov dx, msg ; Load buffer address
int 21h ; Execute DOS API
ret
Compile with
nasm -f bin -o hello.com hello.asm
16-bit (DOS/BIOS)
The BIOS video services 10h, 0eh
function prints a character to a selected video page and updates the cursor position. This “TTY” function is clunkier than the builtin string printing function (10h, 13h
), but it doesn't require its user to modify the base pointer register (bp
), which can potentially lead to undefined behavior if mishandled. According to just about every resource available the “TTY” function is considered too slow to use. Fortunately computers and memory are much faster than they were in the 1980s so you probably won't notice.
org 100h ; Set code segment offset
; DOS PE header is 0FFh bytes long
jmp start
;
; ---- data ----
;
msg:
db "Hello world!", 0
;
; ---- main program ----
;
start:
push 07h ; Set text attribute
push 00h ; Set video page
push msg ; Set buffer address
call write ; Write buffer to console
add sp, 2 * 3 ; Clean up stack arguments
ret
;
; ---- print string buffer ----
;
write:
push bp ; Save base pointer
mov bp, sp ; Load base pointer with stack pointer
cld ; Clear direction flag (increment)
push ax ; Save registers used
push bx
push si
mov ah, 0eh ; Load high byte of accumulator
; BIOS video service function: TTY
mov si, [bp + 2 * 2] ; Load source index with buffer address
mov bh, [bp + 2 * 3] ; Load video page (0..n)
mov bl, [bp + 2 * 4] ; Load text attribute (0-FF)
.read:
lodsb ; Load low byte of accumulator with value at [es:si]
cmp al, 0 ; Stop processing on NUL terminator
je .return
int 10h ; Execute BIOS video service request
jmp .read ; Continue reading
.return:
mov al, 0dh ; Load carriage return character
int 10h ; Execute BIOS video service request
mov al, 0ah ; Load new line character
int 10h ; Execute BIOS video service request
pop si ; Restore registers used
pop bx
pop ax
mov sp, bp ; Restore stack pointer
pop bp ; Restore base pointer
ret ; Return from routine
Compile with
nasm -f bin -o hello.com hello.asm
16-bit (DOS/VRAM)
VGA (color) RAM is mapped to 0b800:0000
and may be written to directly by the user. However, while this approach provides a massive speed advantage the cursor must be updated manually.
org 100h ; Set code segment offset
; DOS PE header is 0FFh bytes long
jmp start
;
; ---- data ----
;
msg: db "Hello world!", 0
;
; ---- main program ----
;
start:
push 07h ; Set text attribute
push msg ; Set buffer address
call write ; Write buffer to console
add sp, 2 * 3 ; Clean up stack arguments
ret
;
; ---- print string buffer ----
;
write:
push bp ; Save base pointer
mov bp, sp ; Load base pointer with stack pointer
push si ; Save registers used
push ax
cld ; Clear direction flag (increment)
mov si, [bp + 2 * 2] ; Load source index with buffer address
mov ah, [bp + 2 * 3] ; Load text attribute (0-FF)
push es ; Save extra segment
push 0b800h ; Save VRAM address
pop es ; Load extra segment with VRAM address
mov di, 00h ; Store VRAM offset
.read:
lodsb ; Load low byte of accumulator with value at [es:si]
cmp al, 0 ; Stop processing on NUL terminator
je .return
stosw ; Store text attribute and character
jmp .read ; Continue reading
.return:
mov al, 0dh ; Load carriage return character
stosw ; Store text attribute and character
mov al, 0ah ; Load new line character
stosw ; Store text attribute and character
pop cx ; Restore registers used
pop bx
pop ax
pop es
mov sp, bp ; Restore stack pointer
pop bp ; Restore base pointer
ret ; Return from routine
Compile with
nasm -f bin -o hello.com hello.asm
32-bit (Linux)
There is no way to access video RAM like in 16-bit real-mode, so we resort to calling the Linux kernel's services.
section .text
global _start
;
; ---- main program ----
;
_start:
push msglen ; Length of buffer
push msg ; Address of buffer
push FD_STDOUT ; File descriptor
call write ; Write buffer to console
add esp, 4 * 3 ; Clean up stack arguments
mov ebx, 0 ; Set exit code
mov eax, SYS_EXIT ; Kernel: exit()
int 80h ; Execute syscall
;
; ---- print string buffer ----
;
write:
push ebp ; Save base pointer
mov ebp, esp ; Load base pointer with stack pointer
push ebx ; Save registers used
push ecx
push edx
mov eax, SYS_WRITE ; Kernel: write()
mov ebx, [ebp + 4 * 2] ; Set fileno
mov ecx, [ebp + 4 * 3] ; Set buffer address
mov edx, [ebp + 4 * 4] ; Set count
int 80h ; Execute syscall
pop edx ; Restore registers used
pop ecx
pop ebx
mov esp, ebp ; Restore stack pointer
pop ebp ; Restore base pointer
ret ; Return from routine
section .data
;
; ---- data ----
;
msg db "Hello world!", 0ah, 0
msglen equ $ - msg
SYS_EXIT equ 1
SYS_WRITE equ 4
FD_STDOUT equ 1
Compile with
nasm -f elf32 -o hello.o hello.asm
ld -m elf_i386 -o hello hello.o
64-bit (Linux)
The modifications required to port from 32-bit to 64-bit are subtle. The calling convention is different (rdi, rsi, rdx, ...
versus ebx, ecx, edx, ...
), stack offset calculations are eight bytes instead of four, interrupt vector calls are replaced by syscall
, and most importantly the kernel service used to access the write()
function have changed. If nothing else the body of the program remains largely the same.
section .text
global _start
;
; ---- main program ----
;
_start:
push msglen ; Length of buffer
push msg ; Address of buffer
push FD_STDOUT ; File descriptor
call write ; Write buffer to console
add rsp, 8 * 3 ; Clean up stack arguments
mov rbx, 0
mov rax, SYS_EXIT ; Kernel: exit()
syscall ; Execute syscall
;
; ---- print string buffer ----
;
write:
push rbp ; Save base pointer
mov rbp, rsp ; Load base pointer with stack pointer
push rdx ; Save registers used
push rsi
push rdi
mov rax, SYS_WRITE ; Kernel: write()
mov rdi, [rbp + 8 * 2] ; Set fileno
mov rsi, [rbp + 8 * 3] ; Set buffer address
mov rdx, [rbp + 8 * 4] ; Set count
syscall ; Execute syscall
pop rdi ; Restore registers used
pop rsi
pop rdx
mov rsp, rbp ; Restore stack pointer
pop rbp ; Restore base pointer
ret ; Return from routine
section .data
;
; ---- data ----
;
msg db "Hello world!", 0ah, 0
msglen equ $ - msg
SYS_EXIT equ 60
SYS_WRITE equ 1
FD_STDOUT equ 1
Compile with
nasm -f elf64 -o hello.o hello.asm
ld -m elf_x86_64 -o hello hello.o