Using this, I managed to get the file down to 114 bytes, while still printing "Hello, world!\n" and returning 0:
[bits 64]
file_load_va: equ 4096 * 40
db 0x7f, 'E', 'L', 'F'
db 2
db 1
db 1
entry_point:
mov al, 1
mov esi, file_load_va + message
jmp code_chunk_2
dw 2
dw 0x3e
dd 1
dq entry_point + file_load_va
dq program_headers_start
code_chunk_2:
mov edi, eax
mov dl, message_length
syscall
mov al, 60
xor edi, edi
syscall
db 0 ; usable
db 0 ; usable
dw 0x38
dw 1
; We simply deleted the three two-byte fields that used to be here. The only
; one that mattered, the number of section headers, will still be zero due to
; the upper two bytes of the field at the start of the program header being
; zero.
program_headers_start:
; These next two fields also serve as the final six bytes of the ELF header.
dd 1 ; Program header type: must be 1 (loadable segment)
dd 5 ; Program header flags: must be 5 (readable and executable)
dq 0 ; Offset of loadable segment in the file
dq file_load_va ; Address in memory to load the segment into ; could change
message_length: equ 14
message:
db `Hello, w`
; size in file then size in memory; can be anything non-zero and equal
last_bytes: equ `orld!\n`
dq last_bytes
dq last_bytes
dq 0 ; alignment; usable
This compiles and runs, and it's 114 bytes: $ nasm -f bin hello.asm -o hello && chmod a+x hello && ./hello
Hello, world!
$ ls -l hello
-rwxr-xr-x 1 josh josh 114 Oct 13 10:28 hello
Getting the file any smaller would require finding a way to overlap the program header further inside the ELF header. As the article observes, that seems challenging given the validation the kernel does.