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 push 1
pop rax
is even shorter (credit: https://old.reddit.com/r/programming/comments/q6mnz1/what_is...) ...
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].
nasm will optimize this to the equivalent "mov eax, 1", that's 6 bytes, but still:
would be much smaller. Second line: You already have the value 1 in eax, so a "mov edi, eax" (two bytes) would suffice. Etc. etc.