Assembler / [Из песочницы] Минималистичная программа в формате ELF

в 14:04, , рубрики: asm, elf, gas, gcc, linux, ассемблер, метки: , , , , ,

Вдохновившись статьёй Привет из свободного от libc мира, я так же решил проделать нечто подобное. Чтобы не заниматься этим бесцельно, я решил поставить перед собой следующую задачу. Сделать программу, выводящую какую-нибудь простую строку, вроде «ELF, hello!». Разобраться с тем, как именно она будет представлена в исполняемом файле. Ну и попутно, постараться уложиться в 100 байт.
Для начала, стандартный helloworld на C++
#include
using namespace std;
int main()
{
cout << "ELF, hello!n";
return 0;
}

Компилируем, смотри размер:$ g++ test.cpp -static && ls -s -h a.out
1,3M a.out
Сколько, сколько? 1.3 Мб? Для вывода одного единственное сообщения размером в 12 байт? Хм… Ладно, попробуем Си.
#include "stdio.h"
int main()
{
printf("ELF, hello!n");
return 0;
}

Так же компилируем и его. При компиляции я указал опцию -static — мне интересен целиком весь код, который будет выполнятся. При динамической компиляции размеры конечно меньше, но всё равно не настолько как хотелось бы.$ gcc test.c -static && ls -s -h a.out
568K a.out
На пол мегабайта меньше. Вот она, плата за STL. Но, всё равно очень много. Видимо, без тяжелой артиллерии в виде ассемблера не обойтись. Пишем helloworld на асме, и без stdlib. Я предпочитаю AT&T-шный синтаксис.
.data
str:
.ascii "ELF, hello!"
.byte 10
.text
.global _start
_start:
movl $4, %eax
movl $1, %ebx
movl $str, %ecx
movl $12, %edx
int $0x80

movl $1, %eax
movl $0, %ebx
int $0x80

Две секции, в секции данных — наше сообщение (и 10-ка для перевода на новую строку), в секции кода (.text) — два раза вызываем 80-е прерывание (с нужными параметрами в регистрах), первый раз для вывода сообщения, второй раз для корректного завершения.
Компилируем (а вернее транслируем и линкуем) созданную программу:$ gcc easy.s -nostdlib && du -sb a.out
752 a.out
752 байта — вот это уже намного ближе к тому, что требуется. Уберём отладочные символы утилитой strip:$ strip a.out && du -sb a.out
476 a.out
Лучше, но всё ещё не достаточно. Что-же в нашем файле на целых 476 байт? Дизассемблируем a.out используя objdump:
$ objdump -D a.out

a.out: file format elf32-i386

Disassembly of section .note.gnu.build-id:

08048094 :
8048094: 04 00 add $0x0,%al
...
Какой-то код, который мы не писали
...
80480b6: b6 08 mov $0x8,%dh

Disassembly of section .text:

080480b8 :
80480b8: b8 04 00 00 00 mov $0x4,%eax
80480bd: bb 01 00 00 00 mov $0x1,%ebx
80480c2: b9 dc 90 04 08 mov $0x80490dc,%ecx
80480c7: ba 0c 00 00 00 mov $0xc,%edx
80480cc: cd 80 int $0x80
80480ce: b8 01 00 00 00 mov $0x1,%eax
80480d3: bb 00 00 00 00 mov $0x0,%ebx
80480d8: cd 80 int $0x80

Disassembly of section .data:

080490dc :
80490dc: 45 inc %ebp
80490dd: 4c dec %esp
80490de: 46 inc %esi
80490df: 2c 20 sub $0x20,%al
80490e1: 68 65 6c 6c 6f push $0x6f6c6c65
80490e6: 21 0a and %ecx,(%edx)

И так, мы видим три секции, хотя писали только две. В секции .text находится наш код. В секции data — наш elf hello в виде 12-и байт (objdump их тоже дизассемблировал). А что ещё за секция .note.gnu.build-id? Мы её не заказывали, поэтому смело удаляем:$ strip -R .note.gnu.build-id a.out && du -sb a.out
416 a.out
Выиграли ещё 60 байт. Неплохо. Давайте попробуем немного оптимизировать наш код. Во первых, программа в принципе может завершаться с любым кодом, а не обязательно с нулевым. Во вторых — при запуске программы регистры обнуляются (однако не стоит на это полагаться при создании реальных программ — проверяйте ABI той системы, для которой пишите).
В итоге вместо movl $4, %eax, которая транслируется в 5 байт, мы можем использовать movb $4, %al, которые транслируются в 2 байта. В третьих, избавимся от секции .data, разместив нашу строку в коде после последнего прерывания (всё равно программа дальше не выполняется):
.text
.global _start
_start:
movb $4, %al
movb $1, %bl
movl $str, %ecx
movb $12, %dl
int $0x80

movb $1, %al
int $0x80
str:
.ascii "ELF, hello!"
.byte 10

Компилируем, удаляем лишнее, смотрим размер:$ gcc -nostdlib easy.s
$ strip a.out
$ strip -R .note.gnu.build-id a.out
$ du -sb a.out
320 a.out
Кажется, мы достигли предела. 320 байт — ничего лишнего. Или нет? Откуда вообще эти 320 байт? Наш код — явно меньше. Однако кроме кода в нашем бинарном файле есть ещё ELF заголовок. И если мы хотим сделать по настоящему минимальную программу, то придется открывать описание ELF (например, тут), и формировать заголовок в ручную.
В ручную — это не значит в hex редакторе. Просто можно дать понять линкеру что к нашему файлу ничего приписывать не надо, и он выдаст на выходе именно то, что мы напишем. Правда в этом случае вся ответственность за то, чтобы файл запустился ложиться на нас.
Реализация программы с вручную составленным заголовком у меня получилась такой:
.set ofs, 0x10000 /* ofs - тут храним смещение */
/* ELF Заголовок: */
.byte 0x7F
.ascii "ELF"
.long 0, 0, 0 /* ident */
.word 2 /* type */
.word 3 /* machine */
.long 0 /* version */
.long _start + ofs /* entry - адрес начала кода (абсолютный) */
.long phdr /* phoff - адрес программного заголовка
(phdr) (относительный ) */
.long 0 /* shoff */
.long 0 /* flags */
.word 0 /* ehsize - размер elf заголовка */
.word phdrsize /* phentsize - размер прогр. заголовка */
.word 1 /* phnum - количество пр. заголовк. */
.word 0 /* shentsize */
.word 0 /* shnum */
.word 0 /* e_shstrndx */
/* Программный заголовок */
phdr:
.long 1 /* type */
.long 0 /* offset */
.long ofs /* vaddr - абсолютный адрес начала кода
программы (с учетом смещения) */
.long 0 /* paddr */
.long filesize /* filesz - размер программы на носителе */
.long filesize /* memsz - размер программы в памяти */
.long 5 /* pflags */
.long 0 /* palign */
.set phdrsize, . - phdr
_start:
/* Код программы */
movb $4, %al
movb $1, %bl
movl $(str+ofs), %ecx
movb $12, %dl
int $0x80

movb $1, %al
int $0x80
str:
.ascii "ELF, hello!"
.byte 10
.set filesize, .

Теперь нам так же приходится вручную оперировать со смещением программы. Под смещением, упрощенно, можно понимать разницу в адресации между кодом который лежит в нашей программе, и тем, где он будет размещён в оперативной памяти (на самом деле в оперативной памяти он будет лежать совсем не там, но это уже другая история). Обычно определением нужных смещений занимается линкер, но теперь мы сами по себе. Смещение я поместил в параметр ofs. Размер смещения взял минимально возможный на моей машине (10 000). По умолчанию он равен 8048000, но это не обязательное условие.
Сам ELF заголовок — это на самом деле не один ELF заголовок. Их должно быть как минимум два — elf заголовок, и программный заголовок. Вообще есть ещё заголовки секций, но мы их не будем использовать для экономии места. Опытным путём были установлены поля заголовков, которые используются. Остальные были заполнены нулями.
Транслируем программу, на этот раз вручную вызывая as и ld:$ as w3test.s -o w3test.o
$ ld -Ttext 0 --oformat binary -o w3test w3test.o
$ du -sb w3test
115 w3test
115 байт! В ~100 000 раз меньше чем первоначальный вариант. Казалось бы всё. Есть только необходимый для запуска минимум, и ничего лишнего. И начальную задачу преодолеть 100 байт выполнить не удастся. Однако это не предел! В заголовке есть неиспользуемые байты а это значит, что мы можем использовать их для своих целей. Сам код к сожалению ни в одно поле не влезет, слишком большой. Зато влезет строка.
Если внимательно присмотреться, то сразу после идентификатора ELF у нас идёт три неиспользуемых поля типа long (по четыре байта каждое). Это значит что мы можем положить туда строку. Да к тому же не всю строку, а только последнюю её часть, ведь «ELF» в виде ascii символов у нас и так уже есть.
Кроме этого мы можем сократить код, разместив заголовок phd не после elf, а сразу после последнего используемого в ELF байта. Т. е. заголовок phd будет немного наслаиваться на elf, но это не вызовет никаких последствий, т. к. те поля, которые наслаиваются, в elf не используются.
Точно так же мы можем разместить нашу программу, «наслаивая» её на phd заголовок (по тем же самым причинам).
В итоге получиться следующий код:
.set ofs, 0x10000 /* ofs - тут храним смещение */
/* ELF Заголовок: 8*/
.byte 0x7F
str: .ascii "ELF"
.ascii ", hello!"
.byte 10, 0, 0, 0
.word 2 /* type */
.word 3 /* machine */
.long 0 /* version */
.long _start+ofs /* entry - адрес начала кода (абсолютный) */
.long phdr /* phoff - адрес программного заголовка
(phdr) (относительный ) */
.long 0 /* shoff */
.long 0 /* flags */
.word 0 /* ehsize - размер elf заголовка */
.word phdrsize /* phentsize - размер прогр. заголовка */
/* Программный заголовок */
phdr:
.long 1 /* type */
.long 0 /* offset */
.long ofs /* vaddr - абсолютный адрес начала кода
программы (с учетом смещения) */
.long 0 /* paddr */
.long filesize /* filesz - размер программы на носителе */
.long filesize /* memsz - размер программы в памяти */
.long 5 /* pflags */
.set phdrsize, . - phdr + 4
_start:
/* Код программы */
movb $4, %al
movb $1, %bl
movl $(str+ofs), %ecx
movb $12, %dl
int $0x80

movb $1, %al
int $0x80
.set filesize, .

После трансляции получаем программу размером в 89 байт. Можно считать задачу выполненной.
Ещё была идея по оптимизации — запихнуть phd заголовок внутрь elf заголовка. Но эта идея провалилась, т. к. минимальное смещение в 10 000 не позволило подобрать такие параметры, чтобы нужные поля структур совпадали.
Источники информации
wikibooks.org — Ассемблер в Linux для программистов Cstackoverflow.com — “Hello World” in less than 20 bytesmuppetlabs.com — Teensy ELF Executables for Linux

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js