Минимальный ELF руками: 132 байта и exit(42)
Обычный «hello world» на C после компоновки весит около 16 КБ. Большая часть — служебные секции, которые ядру для запуска не нужны. Ядру достаточно заголовка ELF и хотя бы одного сегмента PT_LOAD. Соберём такой файл вручную, без компоновщика.
Что нужно ядру
64-байтный заголовок ELF с точкой входа и 56-байтный program header, который говорит: «отобрази этот кусок файла по такому-то адресу с правами на чтение и исполнение». Секции, таблица символов, .dynamic — всё это нужно отладчику и динамическому загрузчику, а не execve.
BITS 64
org 0x400000
ehdr: db 0x7f, "ELF", 2, 1, 1, 0 ; 64-bit, LE, версия 1
times 8 db 0
dw 2 ; e_type: ET_EXEC
dw 0x3e ; e_machine: x86-64
dd 1 ; e_version
dq _start ; e_entry
dq phdr - $$ ; e_phoff
dq 0 ; e_shoff: секций нет
dd 0 ; e_flags
dw 64, 56, 1 ; ehsize, phentsize, phnum
dw 0, 0, 0 ; shentsize, shnum, shstrndx
phdr: dd 1 ; p_type: PT_LOAD
dd 5 ; p_flags: R + X
dq 0 ; p_offset
dq $$, $$ ; p_vaddr, p_paddr
dq filesize, filesize
dq 0x1000 ; p_align
_start: mov edi, 42
mov eax, 60 ; SYS_exit
syscall
filesize equ $ - $$
$ nasm -f bin tiny.asm -o tiny && chmod +x tiny
$ ./tiny; echo $?
42
$ stat -c %s tiny
132
Почему именно 132
64 байта заголовка, 56 байт program header и 12 байт кода: mov edi, imm32 и mov eax, imm32 по 5 байт, syscall — 2. Дальше можно ужимать, перекрывая заголовки и пряча код в неиспользуемые поля, но это уже спорт ради спорта. Для понимания того, что делает execve, 132 байт хватает с головой.
Что проверить самому
readelf -l tiny покажет единственный сегмент, а strace ./tiny — что между execve и exit нет ни одного системного вызова: ни mmap для libc, ни brk.
Исходники всех вариантов из заметки, включая ужатые до 105 байт: tiny-elf-examples.tar.gz.