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