Как стать автором
Обновить

Минималистичная программа в формате ELF

Время на прочтение 6 мин
Количество просмотров 22K
Вдохновившись статьёй Привет из свободного от libc мира, я так же решил проделать нечто подобное. Чтобы не заниматься этим бесцельно, я решил поставить перед собой следующую задачу. Сделать программу, выводящую какую-нибудь простую строку, вроде «ELF, hello!». Разобраться с тем, как именно она будет представлена в исполняемом файле. Ну и попутно, постараться уложиться в 100 байт.

Для начала, стандартный helloworld на C++

#include <iostream>
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 <.note.gnu.build-id>:
 8048094:       04 00                   add    $0x0,%al
 ...
               Какой-то код, который мы не писали
 ...
 80480b6:       b6 08                   mov    $0x8,%dh

Disassembly of section .text:

080480b8 <.text>:
 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 <.data>:
 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 байт! В ~10 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 не позволило подобрать такие параметры, чтобы нужные поля структур совпадали.

P. S. В комментариях был предложен ещё более оптимизированный вариант, размером в 61 байт, в котором таки удалось наложить phd на elf. Компилируется с помощью nasm/yasm с параметром -f bin.

BITS 32;
ORG 05430000h;

DB 0x7F, "ELF";
DD 01h, 00h, $$;
DW 02h, 03h;
DD @main;
DW @main - $$;

@main:
  INC EBX;
  DB 05h; <-- ADD EAX,
  DD 04h; <-- LONG(04h)
  MOV ECX, @text;
  MOV DL, 12;
  INT 80h;
  AND EAX, 00010020h;
  XCHG EAX, EBX;
  INT 80h;

@text:
  DB "ELF, hello!", 0Ah;


Источники информации


wikibooks.org — Ассемблер в Linux для программистов C
stackoverflow.com — “Hello World” in less than 20 bytes
muppetlabs.com — Teensy ELF Executables for Linux
Теги:
Хабы:
+108
Комментарии 49
Комментарии Комментарии 49

Публикации

Истории

Работа

Программист C++
121 вакансия
QT разработчик
13 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн