CLI#

For a freestanding (no-libc) Linux binary the entry point is _start. The kernel sets up the stack so that [rsp] holds argc, the next 8 bytes hold argv[0], then argv[1], argv[argc] is NULL, then envp[], then auxv. The operator either parses this layout by hand or goes through libc’s main (the C runtime handles the layout and calls main for the operator).

/io`. For the ELF entry point in detail, see Runtime.

The freestanding entry point#

; nasm -felf64 hello.asm -o hello.o
; ld -o hello hello.o   ; no libc

global  _start

section .rodata
msg:    db "hello, operator", 0x0a
len     equ $ - msg

section .text
_start:
    ; argc at [rsp], argv at [rsp+8], envp after argv[argc] = NULL
    mov     rax, 1              ; write
    mov     rdi, 1              ; stdout
    lea     rsi, [rel msg]
    mov     rdx, len
    syscall

    mov     rax, 60             ; exit
    xor     rdi, rdi
    syscall

Reading argc and argv#

The Linux start stack layout (x86-64).

high address
...
envp strings
argv strings
NULL
envp[envc-1]
...
envp[0]
NULL
argv[argc-1]
...
argv[0]
argc                   <- rsp
low address

The operator reads them directly.

_start:
    mov     rdi, qword [rsp]            ; argc
    lea     rsi, [rsp + 8]              ; argv

    ; print argv[0]
    mov     r12, qword [rsi]            ; rsi[0] = first arg's string ptr

    ; compute strlen(r12)
    mov     rdi, r12
    call    strlen                       ; rax = length

    ; write
    mov     rax, 1
    mov     rdi, 1
    mov     rsi, r12
    mov     rdx, rax
    syscall

    mov     rax, 60
    xor     rdi, rdi
    syscall

strlen:
    xor     rax, rax
.loop:
    cmp     byte [rdi + rax], 0
    je      .done
    inc     rax
    jmp     .loop
.done:
    ret

Iterating argv#

; rsi -> argv[0]; argv terminates with NULL
mov     rsi, qword [rsp + 8]

.argloop:
    mov     rdi, qword [rsi]            ; argv[i]
    test    rdi, rdi
    jz      .done                       ; end of argv
    call    print_z                     ; print null-terminated string
    add     rsi, 8                      ; argv[i+1]
    jmp     .argloop
.done:

Reading environment#

After argv’s terminating NULL, the next pointer is envp[0].

; rsi already past the argv NULL
; rsi -> envp[0]

mov     rdi, qword [rsp]                ; argc
lea     rsi, [rsp + 8]                  ; argv
lea     rsi, [rsi + rdi*8 + 8]          ; skip argc * 8 + the NULL

.envloop:
    mov     rdi, qword [rsi]
    test    rdi, rdi
    jz      .done
    call    print_z
    add     rsi, 8
    jmp     .envloop
.done:

With libc (hosted)#

A simpler path: link against libc and write main(argc, argv) as a normal function. The C runtime (crt0) handles the stack-layout parsing and calls main.

extern  printf

section .rodata
fmt:    db "argc=%d argv[0]=%s", 10, 0

section .text
global  main
main:
    ; rdi = argc, rsi = argv
    push    rbx
    mov     rbx, rsi
    lea     rdi, [rel fmt]
    mov     rsi, rdi                    ; pass argc as 2nd printf arg
    mov     rdi, qword [rbx]            ; first argv string
    xor     rax, rax
    call    printf
    xor     eax, eax
    pop     rbx
    ret

Build.

$ nasm -felf64 main.asm -o main.o
$ gcc -o app main.o                     # links libc

Exit codes#

x86-64 Linux freestanding.

; exit_group(n)
mov     rax, 231
mov     rdi, 1                          ; status
syscall

Hosted (with libc):

mov     edi, 1                          ; return value of main
; ret from main, or:
extern  exit
call    exit

The convention matches C / Unix: 0 success, 1+ failure, 2 usage error, 130 = SIGINT-terminated.

Stdin / stdout streaming#

A canonical “uppercase stdin to stdout” filter.

section .bss
buf:    resb 4096

section .text
global  _start
_start:
.loop:
    ; n = read(0, buf, 4096)
    xor     rax, rax
    xor     rdi, rdi
    lea     rsi, [rel buf]
    mov     rdx, 4096
    syscall
    test    rax, rax
    jle     .done                       ; 0 = EOF, < 0 = error

    mov     rcx, rax                    ; bytes to transform
    lea     rsi, [rel buf]
.up:
    mov     al, [rsi]
    cmp     al, 'a'
    jb      .skip
    cmp     al, 'z'
    ja      .skip
    sub     al, 32
    mov     [rsi], al
.skip:
    inc     rsi
    dec     rcx
    jnz     .up

    ; write(1, buf, n)
    mov     rdx, rax                    ; rax still holds bytes read
    mov     rax, 1
    mov     rdi, 1
    lea     rsi, [rel buf]
    syscall
    jmp     .loop

.done:
    mov     rax, 60
    xor     rdi, rdi
    syscall

References#