These challenges are funny - they remind me of the old days. Back in the DOS/Windows days, we used to have the .com format, which was perfect for tiny programs. One could even write a program of less than 10 bytes that could actually do something!
We've come a long way since then, and is like, at some point, nobody cared about optimizing executable size anymore
I learned to write COM programs at some point but quickly unlearned it. There were some spots where you can use them and not .bat files, but outside of that it’s a lot.
Great example, a two bytes reboot utility. From the times when we could turn off the computer with a push of a button without fearing a global catastrophe...
.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
I feel like I shouldn't love x86 encoding, but there is something charming about this. Probably echoing its 8-bit predecessors. It seems like it's designed for tiny memory environments (embedded, bootstrapping, etc.) where you don't mind taking a hit for memory access.
Linux initializes all general purpose registers to zero. It's not documented AFAIK, but should be reliable - it has to init them to some value anyway to avoid leaking kernel state. So you can get away with:
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.
Yes, but only in 32 bit mode. Not that it matters, except for the hypothetical future processor or Linux kernel that is no longer compatible with that :)
Thanks, that makes total sense. I was so focused on the ELF part that I didn't even consider optimizing the initial assembly further. Will fix it and edit the article.
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!
My favorite language for implementing short Hello World programs in is HQ9+ [1].
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.
Here's a tiny DOS COM file that does it in 18 bytes:
;; 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
COM files for CP/M and DOS really are a no-nonsense executable format.
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.
Yeah I thought sth like this is possible, but (correct me if I'm wrong) this (ab)uses the ELF header and punts data in there, which goes against my requirement
> It should be a ‘proper‘ executable binary according to the spec
52 comments
[ 3.9 ms ] story [ 132 ms ] threadWe've come a long way since then, and is like, at some point, nobody cared about optimizing executable size anymore
Quick'n'dirty:
More "correct": Even more "correct":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.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.)
However, using the stack-based instructions as xpasky hinted at:
I get down to 159 bytes! I updated the article to reflect thatLinux 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?
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.
Assembling to 48 89 C7 (3 bytes)seems to be same in size as
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
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...
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
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.
> It should be a ‘proper‘ executable binary according to the spec
And I'm sure there is already some language (call it HELLO) which simply prints "hello, world!" for an empty program.