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