The smallest Hello World program
blog.lohr.dev
blog.lohr.dev
mov rax, 1
An actual "mov rax, 1" would assemble to 48 B8 01 00 00 00 00 00 00 00, a whopping TEN bytes.nasm will optimize this to the equivalent "mov eax, 1", that's 6 bytes, but still:
xor eax, eax ; 2 bytes
inc eax ; 2 bytes
would be much smaller. Second line: mov rdi, 1
You already have the value 1 in eax, so a "mov edi, eax" (two bytes) would suffice. Etc. etc. push 1
pop rax
is even shorter (credit: https://old.reddit.com/r/programming/comments/q6mnz1/what_is...) mov al,1 ;write
mov edi,eax ;handle=stdout
mov esi,msg ;assumes load address below 4G
mov dl,msg.len
syscall
mov al,60 ;assuming syscall succeeded, EAX was bytes written
xor edi,edi
syscall
The load address stays constant unless there's some magic GNU extension header to enable ASLR. If we could get the code loaded below 64K, we could save another byte by using SI instead of ESI; however this doesn't work by default, you'd have to run 'echo 0 > /proc/sys/vm/mmap_min_addr' as root first.Can you say for certain that no other Linux version ever used GPRs to pass something else?
[1] System V ABI, page 29 (last line) and 30, https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf
(Note for pedants: rsp is technically a "general purpose register", but of course it is initialized to point to the userspace stack instead of zero.)
inc eax
is a byte shorter than mov al, 1 ...
xor rax, rax ; = 0
inc rax ; = 1 - syscall: sys_write
mov rdi, rax ; copy 1 - file descriptor: stdout
lea rsi, [rel msg] ; pointer to message
mov rdx, 14 ; message length
syscall
...
$ nasm -f bin -o elf elf.asm; wc -c elf; ./elf
166 elf
Hello, World!
So I guess NASM already optimizes this quite wellHowever, using the stack-based instructions as xpasky hinted at:
...
push 1 ; syscall: sys_write
pop rax
pop rdi ; copy 1 - file descriptor: stdout
lea rsi, [rel msg] ; pointer to message
push 14 ; message length
pop rdx
syscall
...
I get down to 159 bytes! I updated the article to reflect that push 1
pop rax
pop rdi
You can't push a value once and pop it twice, that's not how a stack works! You're popping something else off the stack. So why does this even work?Linux passes your program arguments on the stack, with argc on top. So when you don't pass any arguments, argc just HAPPENS to be 1. Which you then pop into rdi. Gross!
With that fixed, is there any reason not to use push here?
push 1 ; 6A 01 (2 bytes)
pop rdi ; 5F (1 byte)
is longer than a simple: mov edi, eax ; 89 C7 (2 bytes)But even if it was 32 bit, then we would't have to copy a 1, since the syscall number for sys_write would be 4 instead of 1.
I get the same total size with both variants in 64 bit mode.
push 1
pop rax
mov rdi, rax
Assembling to 48 89 C7 (3 bytes)seems to be same in size as
push 1
pop rax
push 1
pop rdi
Assembling to 6A 01 5F (3 bytes)The default operand size in 64-bit mode is, for most instructions, still 32 bits. So `mov edi, eax` encodes the same in 32- and 64-bit mode.
For `mov rdi, rax` you need an extra REX prefix byte [1], that's the 48 you're seeing above, but you don't need it here.
[1] https://wiki.osdev.org/X86-64_Instruction_Encoding#REX_prefi...
I noticed that I then could also shave of one byte more by using lea esi, [rel msg] instead of lea rsi, [rel msg].
of course
;; 18 bytes
DB 'HELLO_WOIY<$' ; executes as machine code, returning SP to original position without overwriting return address
mov dx, si ; mov dx,0100h MS-DOS (all versions), FreeDOS 1.0, many other DOSes
xchg ax, bp ; mov ah,9 MS-DOS 4.0 and later, and FreeDOS 1.0
int 21h
ret
(credits: https://stackoverflow.com/questions/72635031/assembly-hello-...)I'm a bit disappointed that Linux (or BSD, macOS, etc.) doesn't support them (or similar) out of the box, though Windows will sort of run them via ntvdm.
Joking aside, this page [2] used to be a great tutorial on writing small ELF binaries, but I'm not sure whether it will still work in 64-bit land. It proved very helpful for writing a 4K intro back in 1999.
[1] https://esolangs.org/wiki/HQ9%2B
[2] https://www.muppetlabs.com/~breadbox/software/tiny/teensy.ht...
We've come a long way since then, and is like, at some point, nobody cared about optimizing executable size anymore
debug
-a 100
178A:0100 int 19
178A:0102
-r cx
CX 0000
:2
-n reboot.com
-w
Writing 00002 bytes
-qQuick'n'dirty:
.model small
.code
org 100h
start:
int 19h ; Bootstrap loader
end start
More "correct": .model small
.code
org 100h
start:
db 0EAh ; Jump to Power On Self Test - Cold Boot
dw 0,0FFFFh
end start
Even more "correct": .model small
.code
org 100h
start:
mov ah,0Dh
int 21h ; DOS Services ah=function 0Dh
; flush disk buffers to disk
sti ; Enable interrupts
hlt ; Halt processor
mov al,0FEh
out 64h,al ; port 64h, kybd cntrlr functn
; al = 0FEh, pulse CPU reset
end start 7f454c46488d3537000000ffc7b20eeb03003e00
b001eb1a01000000050000001800000000000000
1800000005000000b03c0f05ebfa380001006865
6c6c0000010068656c6c00006f20776f726c640a
i'm sure this can be improved -- but i could never get any x86_64 linux elf to under 80 bytes. see if you can fit the exclamation point still.> It should be a ‘proper‘ executable binary according to the spec
Because it would be much smaller in a bat file than contains :
echo Hello World!
> Let’s first establish some rules for our ‘Hello World’ program:
> It should be able to execute directly without passing to any other programs first (so no decompression)
> It should be a ‘proper‘ executable binary according to the spec
Hello World
? "hello, world!"
PILOT eliminates the quotes: T:hello, world!
Of course a typical REPL (Python, JavaScript, Lisp, etc.) will print out something similar (but often quoted) if you just type the quoted string.And I'm sure there is already some language (call it HELLO) which simply prints "hello, world!" for an empty program.