Не прошло и полгода! Как вы можете, поднапрягшись, вспомнить, в прошлый раз мы остановились на унынии и обещании нырнуть в ассемблер.
Ну что же, пацан сказал — пацан сделал. Из этого аляповатого нагромождения букв вы узнаете, как можно инициализировать OpenGL-контекст в GNU/Linux в какие-то 450 байт, высвободив ещё больше места для разворачивания таланта.
Под катом вы узнаете, как в один килобайт нарисовать что-нибудь такое:
Заинтересованные пристёгиваются и вдавливают педаль в пол, а глаз — в экран.
Для начала, давайте поговорим об этом. Почему мы так плохи и никчёмны? Что же именно добавляет нам веса и тянет вниз, в пучины деградации?
Чтобы ответить на эти вопросы, нам потребуются инструменты для настоящих мужчин — readelf и objdump.
Расчехляем первый и натравливаем на оставшийся с прошлого раза файл intro — тот самый, который остаётся после обработки напильником-sstrip'ом:
$ readelf -a intro
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048250
Start of program headers: 52 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 8
Size of section headers: 40 (bytes)
Number of section headers: 0
Section header string table index: 0
There are no sections in this file.
There are no sections in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
INTERP 0x000134 0x08048134 0x08048134 0x00015 0x00015 R 0x1
[Requesting program interpreter: /lib32/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x005ac 0x005ac R E 0x1000
LOAD 0x000f4c 0x08049f4c 0x08049f4c 0x000c4 0x00100 RW 0x1000
DYNAMIC 0x000f4c 0x08049f4c 0x08049f4c 0x000a8 0x000a8 RW 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
GNU_RELRO 0x000f4c 0x08049f4c 0x08049f4c 0x000b4 0x000b4 R 0x1
PAX_FLAGS 0x000000 0x00000000 0x00000000 0x00000 0x00000 0x4
Dynamic section at offset 0xf4c contains 16 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libdl.so.2]
0x00000004 (HASH) 0x804814c
0x6ffffef5 (GNU_HASH) 0x8048164
0x00000005 (STRTAB) 0x80481ac
0x00000006 (SYMTAB) 0x804817c
0x0000000a (STRSZ) 45 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x8049ff4
0x00000002 (PLTRELSZ) 16 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8048210
0x6ffffffe (VERNEED) 0x80481e0
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x80481da
0x00000000 (NULL) 0x0
There are no relocations in this file.
There are no unwind sections in this file.
Histogram for bucket list length (total of 1 buckets):
Length Number % of total Coverage
0 0 ( 0.0%)
1 0 ( 0.0%) 0.0%
2 1 (100.0%) 100.0%
No version information found in this file.
(конкретные цифры, размеры и смещения зависят от всего тулчейна и возраст ваших миль майская Варвара)
Что мы здесь видим? В самом начале, конечно же, стандартный ELF Header, из него слова не выкинешь. Далее мы видим, что sstrip почикал все section headers (упражнение: сравните вывод с readelf -a intro-orig, который до sstrip), что хорошо. Но дальше мы видим какой-то праздник program headers и dynamic section (at offset). Неужели они нам правда такие нужны в платье стоят красивые?
Спойлер: нет!*
(* — поправка: да, но не такие)
Разделываем эльфов
Давайте посмотрим, какого минимального размера можно соорудить корректный запускаемый эльф-файл. За основу возьмём то, что у нас есть сейчас, но просто выкинем все наши полезные кишочечки.
simple.c:
void _start(void)
{
asm(
"xor %eax,%eaxn"
"inc %eaxn"
"int $0x80n"
);
}
Соберём его (скрипт взят из Прошлого™):
cc -Wall -m32 -c simple.c -Os -nostartfiles -o simple-c.o &&
ld -melf_i386 -dynamic-linker /lib32/ld-linux.so.2 simple-c.o -o simple-c-orig &&
cp simple-c-orig simple-c &&
sstrip simple-c &&
cat simple-c | 7z a dummy -tGZip -mx=9 -si -so > simple-c.gz &&
cat unpack_header simple-c.gz > simple-c.sh &&
wc -c simple-c.sh && chmod +x simple-c.sh &&
./simple-c.sh
Смотрим на размеры получившихся и промежуточных файлов и замечаем следующее:
- sstrip решает — размер стрипнутого simple-c (почти) в три раза меньше, чем размер simple-c-orig
- сжатие почти не играет роли — сжатый файл с кодом распаковки занимает почти столько же, сколько и несжатый
- (если дописать к ld параметр -s, то можно выиграть в размере нестрипнутого файла примерно на треть, никак не потеряв и не прибавив в размере промежуточного стрипнутого бинарника, и _проиграть_ 4 байта в размере финального сжатого)
Заглядывая в будущее можно заметить, что эти замечания будут бесполезны чуть менее, чем полностью, но мы-то ещё не в будущем!
Теперь можно натравить readelf на бинарник и посмотреть, что от него осталось, во что он превратился, кем он стал:
$ readelf -a simple-c
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048094
Start of program headers: 52 (bytes into file)
Start of section headers: 0 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 3
Size of section headers: 40 (bytes)
Number of section headers: 0
Section header string table index: 0
There are no sections in this file.
There are no sections in this file.
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x0009e 0x0009e R E 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
PAX_FLAGS 0x000000 0x00000000 0x00000000 0x00000 0x00000 0x4
There is no dynamic section in this file.
There are no relocations in this file.
There are no unwind sections in this file.
No version information found in this file.
Так, sstrip вроде бы и молодчина, раздел нашу эльф-девицу, снял все эти ненужные заголовки секций. Но при этом наша дама ещё не готова — на ней ещё есть лифчик (заголовки GNU_STACK и PAX_FLAGS) и трусы, которые можно обнаружить, если обратить внимание на то, что FileSiz заголовка LOAD почему-то меньше, чем размер самого файла, что значит, что это не трусы вовсе, а просто мусор в конце файла, который необходимо постанывая сорвать.
Кроме того, если быть совсем уж придирчивым и декомпилировать (objdump -D, будет работать только на нестрипнутом файле, т.к. objdump читает section headers, а не program) то, что нам тут нагенерили, в высшей степени необходимо заметить не первой свежести носки из чуть более чем полностью вредных пролога и эпилога функции:
00 55 push ebp
01 89e5 mov ebp, esp
03 31c0 xor eax, eax
05 40 inc eax
06 cd80 int 0x80
08 5d pop ebp
09 c3 ret
Соотношение 5 байт «полезного» кода к 5 — бесполезного так же не может греть наши юные сердца. А что будет греть наши юные сердца? Горящие спрессованные останки динозавров и болот. В эти самые спрессованные болота и предлагаю нам с вами, мой любознательный читатель, погрузиться.
Делаем эльфа пальцем
Как оказывается, собрать эльфа из ничего довольно несложно, надо всего-то-лишь выключить старкрафт, открыть файл /usr/include/elf.h, ничего не понять, загуглить файл elf.pdf, прочитать его по диагонали и сорвать с кончиков пальцев примерно следующее:
bits 32 ; работаем в 32-битном режиме
org 0x00040000 ; эта константа автоматически прибавляется к смещениям всех меток, также, именно это значение будет лежать в спец-константе $$
; Почему здесь лежит именно такое значение? А хер его знает. (см ниже)
; начало elf-заголовка
db 0x7f, 'ELF' ; magic для опознавания файла, иначе система не поймёт, что это ELF
db 1 ; EI_CLASS = ELFCLASS32
db 1 ; EI_DATA ELFDATA2LSB
db 1 ; EI_VERSION = EV_CURRENT
times 9 db 0 ; 9 неиспользуемых байт, которые можно загадить мусором и не переживать об этом
dw 2 ; e_type = ET_EXEC -- запускаемый файл
dw 3 ; e_machine = EM_386
dd 1 ; e_version = EV_CURRENT
dd _start ; e_entry -- адрес точки входа, с этого места начнётся выполнение программы
dd phdrs - $$ ; e_phoff -- смещение относительно начала файла, по которому находятся program headers
dd 0 ; e_shoff -- --//-- section headers, коих у нас, к слову, нет, поэтому 0
dd 0 ; e_flags -- не надо нам никаких флагов
dw ehsize ; e_ehsize -- размер ELF-заголовка (52 байта)
dw phsize ; e_phentsize -- размер одного program header (32 байта)
dw 1 ; e_phnum -- их количество
dw 0 ; e_shentsize -- размер section header
dw 0 ; e_shnum -- их количество (тютюшки)
dw 0 ; e_shstrndx -- что-то там со строками связано, нам не нужно
ehsize equ ($-$$) ; $ означает текущее смещение от начала файла (+значение org), поэтому в ehsize теперь лежит размер elf-заголовка
phdrs: ; начало program header
dd 1 ; p_type = PT_LOAD -- тип заголовка "загрузи кусок файла мне в память, скотина"
dd 0 ; p_offset -- смещение относительно начала файла, откуда читать
dd $$ ; p_vaddr -- адрес в памяти, куда писать
dd $$ ; p_paddr -- физический адрес, какая-то платформенно-специфичная фигота, я сам не разобрался, поэтому давайте не будет выпендриваться и просто повторим за мной
dd file_size ; p_filesz -- размер куска файла, который будем загружать в память
dd file_size ; p_memsz -- размер этого куска в памяти. Если больше, чем e_filesz, будет полиномиально интерполироваться побайтно. Если больше -- преобразовываться ФНЧ Чебышева второго рода 6 порядка. Шутка. ЛОЛ!!11 На самом деле, если больше, то остаток будет забиваться нулями. А если меньше И ТУТ СУКА ВЗРЫЩ КИШКИ ОБ СТЕНУ МОЗГИ ГОВНО РАСЧЛЕНЁНКА.
dd 7 ; p_flags (=PF_RWX) -- загруженные данные можно будет читать, исполнять и изменять.
dd 0x1000 ; p_align -- выравнивание, применяется и к смещению, и к адресу в памяти. 0x1000 является довольно безопасным значением
phsize equ ($-phdrs) ; аналогично рассчитываем размер program header
_start: ; поехали
xor eax, eax ; eax = 0
inc eax ; eax = 1 (exit syscall)
int 0x80 ; вызов syscall
file_size equ ($-$$) ; ну тут уже всё понятно должно быть
(На самом деле, вместо того, чтобы давать вот этот грубый, но слегонца откоментированный asm-файл, я хотел вам, мои лапочки ненаглядные, обнимаю, нарисовать большую схему ELF-формата, но потом понял, что это отложило бы статью еще на пару лет)
Компилировать этот файл нужно nasm'ом с директивой компиляции в «плоский» бинарник, без каких-либо лишних обвесок-заголовков:
$ nasm -f bin simple.asm -o simple-asm
Эта строчка создаст нам файл simple-asm размером 89 байт, который будет полным функциональным аналогом того 206-байтного монстра, что был создан нами ранее. Не говоря уже о том, что те 206 байт — это, на самом деле, 657 байт оригинального несжатого нестрипнутого безумия.
Можно
chmod +x simple-asm
и посмотреть, что он на самом деле валидный и даже запускается. Могу сказать, что 89 байт — далеко не предел, и можно этот файл удавить еще примерно в два раза — наложить elf header на единственный program header, и получить не то, чтобы супер-валидный эльф, но вполне себе запускающийся и имеющий размер меньше, чем сам elf header (последние 6 байт из него будут автоматически добиты нулями, как они и должны быть)[1]. Невольно на ум приходит то, что под линуксом есть замечательные файлы /dev/fb0 и /dev/dsp, а значит можно делать интры в 128-256-512 байт DOS-style. Мы, впрочем, не будем здесь таким заниматься — это тема для отдельного нагромождения знаков.
А будем мы заниматься тем, что разберёмся, как подключить сюда OpenGL.
Демоническая линковка
Как мы, наверное, смутновато помним из предыдущей части, для создания OpenGL-контекста мы использовали библиотеку SDL, подключавшуюся динамически, как libSDL.so. Кроме неё нам, разумеется, нужен ещё и сам OpenGL, который идёт в виде библиотеки libGL.so.
Нетрудно, как говорится, видеть, что нам нужно научиться заставлять уметь иметь возможность наш самопальный бинарник динамически линковаться с чем бы то ни было.
Механизм динамической линковки под линуксом на низком уровне очень смешной. Система при загрузке бинарника обнаруживает у него program header с типом PT_INTERP, который указывает на имя файла интерпретатора. «Да пошли вы в жопу со своей этой динамической линковкой, сами разбирайтесь, раз такие умные», — говорит система и вместо того, чтобы продолжить загружать наш бинарник, загружает интерпретатор и передаёт ему управление.
Дальнейшей загрузкой приложения занимается уже интерпретатор — он, как ни в чём ни бывало, самостоятельно загружает секции PT_LOAD в процесс и, что самое важное, читает PT_DYNAMIC, в котором находится ссылка на описание всего необходимого для динамической линковки — имена необходимых библиотек, функций, куда, и, что немаловажно, как их грузить. Формат данных, на которые ссылается PT_DYNAMIC, сам по себе довольно простой: таблица пар двойных слов d_tag и d_val, где d_tag — код параметра, а d_val — его значение, которое для многих параметров является адресом в уже загруженном в память процессе (или что-то около того).
Какие же параметры и значения нужно указывать? Давайте вернёмся наверх к readelf-дампу нашего первоначального файла и посмотрим на всё ЭТО ЧТО ЗА ПОКЕМОН?! после строчки «Dynamic section at offset ...».
Страшно!
Успокоим себя той мыслью, что треть из них совершенно нам не понадобится. Волнение, впрочем, не отступает — оставшиеся-то поля всё равно страшные. Однако же, я не буду объяснять подробно их смысл — отчасти потому, что сам уже не помню (расковыривал это достаточно давно), a то, что помню, предпочёл бы забыть.
Поэтому вам, дорог(ой/ие) читател(ь/и) я привожу готовый рецепт, аккуратно посыпанный тонким слоем комментариев по-вкусу. Пользуйтесь на здоровье.
Этот кусок добавляется к phdrs сразу после расчёта phsize:
dd 2 ; p_type = PT_DYNAMIC
dd dynamic - $$ ; p_offset
dd dynamic ; p_vaddr
dd dynamic ; p_paddr
dd dynamic_size ; p_filesz
dd dynamic_size ; p_memsz
dd 6 ; p_flags = PF_RW
dd 4 ; p_align
dd 3 ; p_type = PT_INTERP
dd interp - $$ ; p_offset
dd interp ; p_vaddr
dd interp ; p_paddr
dd interp_size ; p_filesz
dd interp_size ; p_memsz
dd 4 ; p_flags = PF_R
dd 1 ; p_align
; данные для PT_DYNAMIC
; только самый необходимый минимум
dynamic:
dd 1, st_libdl_name ; DT_NEEDED -- необходимо прилинковать библиотеку с именем, лежащим в st_libdl_name (индекс в таблице символов)
; опциональные рюшечки, просящие интерпретатор заранее проверить, все ли необходимые библиотеки на месте
;dd 1, st_libSDL_name
;dd 1, st_libGL_name
dd 4, dt_hash ; DT_HASH -- указатель на таблицу хешей
dd 5, dt_strtab ; DT_STRTAB -- указатель на таблицу строк. все ссылки на строки имеют смысл смещения относительно начала таблицы строк
dd 6, dt_symtab ; DT_SYMTAB -- указатель на таблицу символов
dd 10, dt_strtab_size ; DT_STRSZ -- размер таблицы строк
dd 11, dt_symtab_size ; DT_SYMENT -- размер таблицы символов
dd 17, dt_rel ; DT_REL -- указатель на таблицу релокаций
dd 18, dt_rel_size; DT_RELSZ -- размер таблицы релокаций
dd 19, 8 ; DT_RELENT -- размер одной записи в таблице релокаций
dd 0, 0 ; DT_NULL -- конец данных для DT_DYNAMIC
dynamic_size equ $ - dynamic
; данные для DT_HASH
; эти данные нужны для чего-то вроде оптимизации загрузки миллионов импортируемых
; функций из динамических библиотек, однако ж для наших двух функций это оверкилл
; поэтому здесь просто заглушка
dt_hash: dd 1, 3, 0, 0, 0, 0
; данные для DT_SYMTAB
; именно в этом месте мы говорим, какие функции нам нужно подгрузить динамически
dt_symtab:
; 0 -- первая запись пустая (зачем?!)
dd 0, 0, 0
dw 0, 0 ; SHN_UNDEF
; 1 'dlopen'
dd st_dlopen_name, 0, 0
dw 0x12 ; = ELF32_ST_INFO(STB_GLOBAL, STT_FUNC), т.е., короче говоря, тип символа -- глобальная функция
dw 0 ; SHN_UNDEF говорит, что этого символа у нас нет, и его надо искать вовне
; 2 'dlsym'
dd st_dlsym_name, 0, 0
dw 0x12, 0 ; --//--
dt_symtab_size equ $ - dt_symtab
; данные для DT_REL
; таблица релокаций. описывает, куда и как загружать адреса символов из таблицы символов
dt_rel:
dd rel_dlopen ; адрес, куда грузить
dd 0x0101 ; ELF32_R_INFO(1,R_386_32) : dt_symtab[1] ('dlopen'), тип = запиши адрес символа + r_addend(=0 у нас)
dd rel_dlsym ; --//--
dd 0x0201 ; ELF32_R_INFO(2,R_386_32) : dt_symtab[2] ('dlsym'), --//--
dt_rel_size equ $ - dt_rel
; сгруппируем строки вмете -- ожидаем, что так они будут лучше паковаться
; данные для DT_STRTAB
; таблица строк. в ней лежит всё, необходимое для PT_DYNAMIC -- названия библиотек и имена функций
dt_strtab:
st_libdl_name equ $ - dt_strtab ; адрес строки относительно начала таблицы строк
db 'libdl.so.2', 0 ; все строки -- нуль-терминированные
st_dlopen_name equ $ - dt_strtab
db 'dlopen', 0
st_dlsym_name equ $ - dt_strtab
db 'dlsym', 0
dt_strtab_size equ $ - dt_strtab
; стандартный линуксовый интерпретатор для динамической линковки
interp: db '/lib/ld-linux.so.2', 0
interp_size equ $ - interp
Помимо этого надо поправить следующее:
- очевидно, изменить e_phnum на 3
- добавить в самый конец файла
; BSS-секция, в ней лежат неинициализированные данные absolute $ bss: ; резервируем место под адреса функций rel_dlopen: resd 1 rel_dlsym: resd 1 mem_size equ ($-$$)
- в PT_LOAD-заголовке p_memsz установить в mem_size
Всё, теперь наш рукотворный эльф может линковаться динамически. Проверим это.
Раз уж это более не просто тест, а потенциальная интра, переименуем файл в intro.asm. Соберём его:
$ nasm -f bin intro.asm -o intro && chmod +x intro
И запустим через strace, чтобы проверить, что он действительно пытается читать всякие so'шечки:
$ strace ./intro
execve("./intro", ["./intro"], [/* 67 vars */]) = 0
[ Process PID=24135 runs in 32 bit mode. ]
...
open("/lib32/libdl.so.2", O_RDONLY) = 3
...
Теперь можно посмотреть и на размер файла — 368 несжатых байт. Можете самостоятельно проверить, что обычным способом (cc+ld) аналогичный несжатый файл сразу раздуется до 4 килобайт.
Сколько же будет весить сжатый файл?
nasm -f bin intro.asm -o intro && chmod +x intro &&
cat intro | 7z a dummy -tGZip -mx=9 -si -so > intro.gz &&
cat unpack_header intro.gz > intro.sh &&
wc -c intro.sh && chmod +x intro.sh &&
./intro.sh
254 байта.
Но это он еще ничего не делает.
Пусть делает!
Теперь, как только мы заполучили в своё распоряжение dlopen и dlsym, можно наконец-то уже загрузить какие-нибудь функции из libSDL и libGL и попробовать что-нибудь эдакое накалякать.
Особо не выпендриваемся и просто портируем на ассемблер всё то, что мы делали на сях:
; имена библиотек и функций, которые нужно загрузить ручками
libs_to_dl:
st_libSDL_name equ $ - dt_strtab
db 'libSDL-1.2.so.0', 0 ; самое кросс-дистрибутивное название библиотеки, которое мне удалось выяснить эмпирически
db 'SDL_Init', 0
db 'SDL_SetVideoMode', 0
db 'SDL_PollEvent', 0
db 'SDL_GetTicks', 0
db 'SDL_ShowCursor', 0
db 'SDL_GL_SwapBuffers', 0
db 'SDL_Quit', 0
db 0 ; два нуля подряд = конец библиотеки
st_libGL_name equ $ - dt_strtab
db 'libGL.so.1', 0
db 'glViewport', 0
db 'glCreateShader', 0
db 'glShaderSource', 0
db 'glCompileShader', 0
db 'glCreateProgram', 0
db 'glAttachShader', 0
db 'glLinkProgram', 0
db 'glUseProgram', 0
db 'glRectf', 0
db 0, 0 ; три нуля подряд = конец загрузки
_start: ; поехали
mov ebp, bss ; пускай ebp всё время указывает на bss -- будет удобно, поверьте!
; супер-удобные штуки, см далее
%define BSSADDR(a) ebp + ((a) - bss)
%define F(f) [ebp + ((f) - bss)]
; начнём загрузку функций
mov esi, libs_to_dl+1 ; +1, т.к. ld_load ожидает, что мы уже вгрызлись на один символ в строку
lea edi, [BSSADDR(libs_syms)] ; edi = адрес места, куда следует аккуратно сохранять адреса функций
ld_load:
dec esi ; в этом месте мы уже вгрызлись в строку на 1, поэтому отступим назад
; подготовим параметры функции dlopen, они передаются через стек
push 1 ; RTLD_LAZY
push esi ; адрес имени библиотеки
call F(rel_dlopen) ; eax = dlopen([esi], 1)
; обратите внимание, что здесь мы <s>не чистим после себя</s>засираем стек и РАДУЕМСЯ ЭТОМУ
mov ebx, eax ; сохраним то, что dlopen нам вернул, в ebx
; скипаем все до 0
ld_skip_to_zero:
lodsb
test al, al
jnz ld_skip_to_zero
; если следующий тоже то конец текущей библиотеки
lodsb
test al, al
jz ld_second_zero
dec esi ; опять отматываемся на 1 назад
push esi ; начало строки с названием функции
push ebx ; возвращенный из dlopen указатель на загруженную библиотеку
call F(rel_dlsym) ; eax = dlsym([ebx], [esi])
stosd ; запишем eax (возвращенный указатель на функцию) в [edi], edi += 4
jmp ld_skip_to_zero ; перемотаем до следуюшего нуля
ld_second_zero:
; если третий не ноль, то подгрузим что-нибудь еще!
lodsb
test al, al
jnz ld_load
; здесь будет наша умопомрачительная интра!
; вон из Новосибирска!
xor eax, eax ; eax = 0
inc eax ; ex = 1 (exit syscall)
int 0x80 ; вызов syscall
file_size equ ($-$$) ; ну тут уже всё понятно должно быть
; BSS-секция, в ней лежат неинициализированные данные
absolute $
bss:
; резервируем место под адреса функций
libdl_syms:
rel_dlopen: resd 1
rel_dlsym: resd 1
libs_syms:
SDL_Init: resd 1
SDL_SetVideoMode: resd 1
SDL_PollEvent: resd 1
SDL_GetTicks: resd 1
SDL_ShowCursor: resd 1
SDL_GL_SwapBuffers: resd 1
SDL_Quit: resd 1
glViewport: resd 1
glCreateShader: resd 1
glShaderSource: resd 1
glCompileShader: resd 1
glCreateProgram: resd 1
glAttachShader: resd 1
glLinkProgram: resd 1
glUseProgram: resd 1
glRectf: resd 1
mem_size equ ($-$$)
Этот кусок надо вставить вместо всего того, что у нас происходит сразу после _start, включая сам _start.
Компилируем, получаем файл размером 455 байт, запускаем, проверяем, что он не падает. Если падает — пробуем раскомментировать DT_NEEDED-строчки для libSDL и libGL, и смотрим, что происходит, страдаем и перестаём читать дальше.
Если всё хорошо, можно двигаться дальше и наконец-то уже инициализировать OpenGL с шейдерами. Здесь ничего особо хитрого (за исключением того, что написано в комментариях), мы просто повторяем на ассемблере то, что делали ранее на сях.
; nasm -f bin intro.asm -o intro && chmod +x intro &&
; cat intro | 7z a dummy -tGZip -mx=9 -si -so > intro.gz &&
; cat unpack_header intro.gz > intro.sh &&
; wc -c intro.sh && chmod +x intro.sh &&
; ./intro.sh
%define WIDTH 640
%define HEIGHT 360
%define FULLSCREEN 0
;%define FULLSCREEN 0x80000000
bits 32 ; работаем в 32-битном режиме
org 0x00040000 ; эта константа автоматически прибавляется к смещениям всех меток, также, именно это значение будет лежать в спец-константе $$
; Почему здесь лежит именно такое значение? А хер его знает. (см ниже)
; начало elf-заголовка
db 0x7f, 'ELF' ; magic для опознавания файла, иначе система не поймёт, что это ELF
db 1 ; EI_CLASS = ELFCLASS32
db 1 ; EI_DATA ELFDATA2LSB
db 1 ; EI_VERSION = EV_CURRENT
times 9 db 0 ; 9 неиспользуемых байт, которые можно загадить мусором и не переживать об этом
dw 2 ; e_type = ET_EXEC -- запускаемый файл
dw 3 ; e_machine = EM_386
dd 1 ; e_version = EV_CURRENT
dd _start ; e_entry -- адрес точки входа, с этого места начнётся выполнение программы
dd phdrs - $$ ; e_phoff -- смещение относительно начала файла, по которому находятся program headers
dd 0 ; e_shoff -- --//-- section headers, коих у нас, к слову, нет, поэтому 0
dd 0 ; e_flags -- не надо нам никаких флагов
dw ehsize ; e_ehsize -- размер ELF-заголовка (52 байта)
dw phsize ; e_phentsize -- размер одного program header (32 байта)
dw 3 ; e_phnum -- их количество
dw 0 ; e_shentsize -- размер section header
dw 0 ; e_shnum -- их количество (тютюшки)
dw 0 ; e_shstrndx -- что-то там со строками связано, нам не нужно
ehsize equ ($-$$) ; $ означает текущее смещение от начала файла (+значение org), поэтому в ehsize теперь лежит размер elf-заголовка
phdrs: ; начало program header
dd 1 ; p_type = PT_LOAD -- тип заголовка "загрузи кусок файла мне в память, скотина"
dd 0 ; p_offset -- смещение относительно начала файла, откуда читать
dd $$ ; p_vaddr -- адрес в памяти, куда писать
dd $$ ; p_paddr -- физический адрес, какая-то платформенно-специфичная фигота, я сам не разобрался, поэтому давайте не будет выпендриваться и просто повторим за мной
dd file_size ; p_filesz -- размер куска файла, который будем загружать в память
dd mem_size ; p_memsz -- размер этого куска в памяти. Если больше, чем e_filesz, будет полиномиально интерполироваться побайтно. Если больше -- преобразовываться ФНЧ Чебышева второго рода 6 порядка. Шутка. ЛОЛ!!11 На самом деле, если больше, то остаток будет забиваться нулями. А если меньше И ТУТ СУКА ВЗРЫЩ КИШКИ ОБ СТЕНУ МОЗГИ ГОВНО РАСЧЛЕНЁНКА.
dd 7 ; p_flags (=PF_RWX) -- загруженные данные можно будет читать, исполнять и изменять.
dd 0x1000 ; p_align -- выравнивание, применяется и к смещению, и к адресу в памяти. 0x1000 является довольно безопасным значением
phsize equ ($-phdrs) ; аналогично рассчитываем размер program header
dd 2 ; p_type = PT_DYNAMIC
dd dynamic - $$ ; p_offset
dd dynamic ; p_vaddr
dd dynamic ; p_paddr
dd dynamic_size ; p_filesz
dd dynamic_size ; p_memsz
dd 6 ; p_flags = PF_RW
dd 4 ; p_align
dd 3 ; p_type = PT_INTERP
dd interp - $$ ; p_offset
dd interp ; p_vaddr
dd interp ; p_paddr
dd interp_size ; p_filesz
dd interp_size ; p_memsz
dd 4 ; p_flags = PF_R
dd 1 ; p_align
; данные для PT_DYNAMIC
; только самый необходимый минимум
dynamic:
dd 1, st_libdl_name ; DT_NEEDED -- необходимо прилинковать библиотеку с именем, лежащим в st_libdl_name (индекс в таблице символов)
; опциональные рюшечки, просящие интерпретатор заранее проверить, все ли необходимые библиотеки на месте
;dd 1, st_libSDL_name
;dd 1, st_libGL_name
dd 4, dt_hash ; DT_HASH -- указатель на таблицу хешей
dd 5, dt_strtab ; DT_STRTAB -- указатель на таблицу строк. все ссылки на строки имеют смысл смещения относительно начала таблицы строк
dd 6, dt_symtab ; DT_SYMTAB -- указатель на таблицу символов
dd 10, dt_strtab_size ; DT_STRSZ -- размер таблицы строк
dd 11, 16 ; DT_SYMENT -- размер одной записи в таблице символов
dd 17, dt_rel ; DT_REL -- указатель на таблицу релокаций
dd 18, dt_rel_size; DT_RELSZ -- размер таблицы релокаций
dd 19, 8 ; DT_RELENT -- размер одной записи в таблице релокаций
dd 0, 0 ; DT_NULL -- конец данных для DT_DYNAMIC
dynamic_size equ $ - dynamic
; данные для DT_HASH
; эти данные нужны для чего-то вроде оптимизации загрузки миллионов импортируемых
; функций из динамических библиотек, однако ж для наших двух функций это оверкилл
; поэтому здесь просто заглушка
dt_hash: dd 1, 3, 0, 0, 0, 0
; данные для DT_SYMTAB
; именно в этом месте мы говорим, какие функции нам нужно подгрузить динамически
dt_symtab:
; 1 -- первая запись пустая (зачем?!)
dd 0, 0, 0
dw 0, 0 ; SHN_UNDEF
; 2 'dlopen'
dd st_dlopen_name, 0, 0
dw 0x12 ; = ELF32_ST_INFO(STB_GLOBAL, STT_FUNC), т.е., короче говоря, тип символа -- глобальная функция
dw 0 ; SHN_UNDEF говорит, что этого символа у нас нет, и его надо искать вовне
; 3 'dlsym'
dd st_dlsym_name, 0, 0
dw 0x12, 0 ; --//--
; данные для DT_REL
; таблица релокаций. описывает, куда и как загружать адреса символов из таблицы символов
dt_rel:
dd rel_dlopen ; адрес, куда грузить
dd 0x0101 ; ELF32_R_INFO(1,R_386_32) : dt_symtab[1] ('dlopen'), тип = запиши адрес символа + r_addend(=0 у нас)
dd rel_dlsym ; --//--
dd 0x0201 ; ELF32_R_INFO(2,R_386_32) : dt_symtab[2] ('dlsym'), --//--
dt_rel_size equ $ - dt_rel
; сгруппируем строки вмете -- ожидаем, что так они будут лучше паковаться
; стандартный линуксовый интерпретатор для динамической линковки
interp: db '/lib/ld-linux.so.2', 0
interp_size equ $ - interp
; данные для DT_STRTAB
; таблица строк. в ней лежит всё, необходимое для PT_DYNAMIC -- названия библиотек и имена функций
dt_strtab:
st_libdl_name equ $ - dt_strtab ; адрес строки относительно начала таблицы строк
db 'libdl.so.2', 0 ; все строки -- нуль-терминированные
st_dlopen_name equ $ - dt_strtab
db 'dlopen', 0
st_dlsym_name equ $ - dt_strtab
db 'dlsym', 0
dt_strtab_size equ $ - dt_strtab
; имена библиотек и функций, которые нужно загрузить ручками
libs_to_dl:
st_libSDL_name equ $ - dt_strtab
db 'libSDL-1.2.so.0', 0 ; самое кросс-дистрибутивное название библиотеки, которое мне удалось выяснить эмпирически
db 'SDL_Init', 0
db 'SDL_SetVideoMode', 0
db 'SDL_PollEvent', 0
db 'SDL_GetTicks', 0
db 'SDL_ShowCursor', 0
db 'SDL_GL_SwapBuffers', 0
db 'SDL_Quit', 0
db 0 ; два нуля подряд = конец библиотеки
st_libGL_name equ $ - dt_strtab
db 'libGL.so.1', 0
db 'glViewport', 0
db 'glCreateShader', 0
db 'glShaderSource', 0
db 'glCompileShader', 0
db 'glCreateProgram', 0
db 'glAttachShader', 0
db 'glLinkProgram', 0
db 'glUseProgram', 0
db 'glRectf', 0
db 0, 0 ; три нуля подряд = конец загрузки
shader_vtx:
db 'varying vec4 p;'
db 'void main(){gl_Position=p=gl_Vertex;p.z=length(p.xy);}'
db 0
shader_frg:
db 'varying vec4 p;'
db 'void main(){'
db 'float '
db 'z=1./length(p.xy),'
db 'a=atan(p.x,p.y)+sin(p.z+z);'
db 'gl_FragColor='
db '2.*abs(.2*sin(p.z*3.+z*3.)+sin(p.z+a*4.)*p.xyxx*sin(vec4(z,a,a,a)))+(z-1.)*.1;'
db '}', 0
_start: ; поехали
mov ebp, bss ; пускай ebp всё время указывает на bss -- будет удобно, поверьте!
; супер-удобные штуки, см далее
%define BSSADDR(a) ebp + ((a) - bss)
%define F(f) [ebp + ((f) - bss)]
; начнём загрузку функций
mov esi, libs_to_dl+1 ; +1, т.к. ld_load ожидает, что мы уже вгрызлись на один символ в строку
lea edi, [BSSADDR(libs_syms)] ; edi = адрес места, куда следует аккуратно сохранять адреса функций
ld_load:
dec esi ; в этом месте мы уже вгрызлись в строку на 1, поэтому отступим назад
; подготовим параметры функции dlopen, они передаются через стек
push 1 ; RTLD_LAZY
push esi ; адрес имени библиотеки
call F(rel_dlopen) ; eax = dlopen([esi], 1)
; обратите внимание, что здесь мы <s>не чистим после себя</s>засираем стек и РАДУЕМСЯ ЭТОМУ
mov ebx, eax ; сохраним то, что dlopen нам вернул, в ebx
; скипаем все до 0
ld_skip_to_zero:
lodsb
test al, al
jnz ld_skip_to_zero
; если следующий тоже то конец текущей библиотеки
lodsb
test al, al
jz ld_second_zero
dec esi ; опять отматываемся на 1 назад
push esi ; начало строки с названием функции
push ebx ; возвращенный из dlopen указатель на загруженную библиотеку
call F(rel_dlsym) ; eax = dlsym([ebx], [esi])
stosd ; запишем eax (возвращенный указатель на функцию) в [edi], edi += 4
jmp ld_skip_to_zero ; перемотаем до следуюшего нуля
ld_second_zero:
; если третий не ноль, то подгрузим что-нибудь еще!
lodsb
test al, al
jnz ld_load
; здесь наша умопомрачительная интра!
push 0x21 ; SDL_INIT_ TIMER | VIDEO
call F(SDL_Init) ; SDL_Init(SDL_INIT_TIMER | SDL_INIT_VIDEO);
push 2 | FULLSCREEN ; SDL_OPENGL
push 32 ; 32 бита на пиксель
push HEIGHT
push WIDTH
call F(SDL_SetVideoMode) ; SDL_SetVideoMode(WIDTH, HEIGHT, 32, SDL_OPENGL|FULLSCREEN);
; WxH уже есть в стеке! cdecl ftw!
push 0
push 0
call F(glViewport) ; glViewport(0, 0, WIDTH, HEIGHT);
call F(SDL_ShowCursor) ; SDL_ShowCursor(0);
; загрузка шейдеров
call F(glCreateProgram) ; eax = glCreateProgram();
mov edi, eax ; edi = program_id
push 0x8b31
pop esi ; esi = GL_VERTEX_SHADER
; здесь и далее используем 4 байта по адресу ebp как temp переменную -- там лежит ненужный нам более адрес dlopen, можно затирать
mov dword [ebp], shader_vtx
push esi
call F(glCreateShader) ; eax = glCreateShader(GL_VERTEX_SHADER);
mov ebx, eax
push 0
push ebp
push 1
push eax
call F(glShaderSource) ; glShaderSource(shader_id, 1, &shader_vtx, 0);
push ebx ; драйвера nVidia портят стек, поэтому нужно перезаливать аргументы
call F(glCompileShader) ; glCompileShader(shader_id);
push ebx ; опять поганая нвидия всё портит!
push edi
call F(glAttachShader) ; glAttachShader(program_id, shader_id);
dec esi ; esi = GL_FRAGMENT_SHADER
mov dword [ebp], shader_frg
; точная копия того, что вверху = хорошо жмётся!
push esi
call F(glCreateShader)
mov ebx, eax
push 0
push ebp
push 1
push eax
call F(glShaderSource)
push ebx
call F(glCompileShader)
push ebx
push edi
call F(glAttachShader)
push edi
call F(glLinkProgram) ; glLinkProgram(program_id);
call F(glUseProgram) ; glUseProgram(program_id);
mainloop:
call F(SDL_GetTicks) ; eax == SDL_GetTicks(); -- время в миллисекундах
mov [ebp], eax
fninit ; нужно сбросить состояние FPU, потому что все вокруг норовят его испортить
fild dword [ebp] ; st(0) = eax == time в миллисекундах, st(1) = 1000
push 400 ; эта константа регулирует скорость, чем она больше, тем медленнее всё
fild dword [esp] ; st(0) = 1000
fdiv ; st(0) /= 1000 = время в секундах
fld1 ; st(0) = 1, st(1) = время в с
faddp st1 ; st(0) = время в с + 1
fst dword [ebp]
mov eax, [ebp] ; eax = (float-ieee)t в секундах
fchs ; st(0) = -st(0)
fstp dword [ebp]
mov ebx, [ebp] ; ebx = -(float-ieee)t в секундах
push ebx
push ebx
push eax
push eax
call F(glRectf) ; glRectf(-t,-t,t,t)
times 5 pop eax ; здесь, к сожалению, приходится убирать за собой, т.к. мы крутимся в цикле и не можем гадить бесконечно
call F(SDL_GL_SwapBuffers)
lea edx, [BSSADDR(SDL_Event)] ; адрес памяти под структуру SDL_Event
push edx
call F(SDL_PollEvent) ; SDL_PollEvent(&SDL_Event);
pop edx ; восстановим edx
cmp byte [edx], 2 ; SDL_Event.type != SDL_KEYDOWN
jnz mainloop
call F(SDL_Quit) ; восстановим режим экрана и прочее
; вон из Новосибирска!
xor eax, eax ; eax = 0
inc eax ; ex = 1 (exit syscall)
int 0x80 ; вызов syscall
file_size equ ($-$$) ; ну тут уже всё понятно должно быть
; BSS-секция, в ней лежат неинициализированные данные
absolute $
bss:
; резервируем место под адреса функций
libdl_syms:
rel_dlopen: resd 1
rel_dlsym: resd 1
libs_syms:
SDL_Init: resd 1
SDL_SetVideoMode: resd 1
SDL_PollEvent: resd 1
SDL_GetTicks: resd 1
SDL_ShowCursor: resd 1
SDL_GL_SwapBuffers: resd 1
SDL_Quit: resd 1
glViewport: resd 1
glCreateShader: resd 1
glShaderSource: resd 1
glCompileShader: resd 1
glCreateProgram: resd 1
glAttachShader: resd 1
glLinkProgram: resd 1
glUseProgram: resd 1
glRectf: resd 1
SDL_Event: resb 24
mem_size equ ($-$$)
Смотрите, котятки, мы уложили в 750 байт то, что в прошлый раз еле-еле влезло в 1024. Можно ли улучшить этот результат?
Конечно можно:
- многие структуры начинаются на то же самое, на что заканчиваются другие структуры
- похожие данные полезно размещать рядом (рыхлые заголовки вместе, строки — вместе, x86-инструкции — тоже ничем не перемежать)
- менять org
- выкидывать обходимое — например, закомментировать всё с SDL_ShowCursor
При этом стоит помнить, что размер сжатого файла далеко не монотонно зависит от размера несжатого: например, times 5 pop eax заметно выигрывает по размеру перед add esp, 20.
Итого, перенос всех строк в конец файла даёт выигрыш 16 байт, столько же можно выиграть, если закомментировать поганой метлой последние ненужные три поля elf header (e_shentsize и следующие за ним два гуся) поудалять нули и прочие одинаковые данные между phdrs и dynamic, dt_hash и dt_symtab.
Итого: 718 байт за то же самое.
Не знаю, как у вас, а у меня уже чешутся ручки — наконец-то можно заняться тем, зачем мы здесь все сегодня собрались — творчеством! И у нас есть целых 306 байт для него (даже больше, если учесть то, что можно полностью заменить шейдеры с осточертевшим туннелем)!
Что же можно сделать с таким невообразимо огромным холстом?
Например, что-нибудь такое
(осторожно, нужна мощная видеокарта):
shader_vtx:
db 'varying vec4 p,v;'
db 'void main()'
db '{'
db 'gl_Position=gl_Vertex;'
db 'p=vec4(mat3(cos(length(gl_Vertex.xy)),0.,sin(length(gl_Vertex.xy)),0.,1.,0.,-sin(length(gl_Vertex.xy)),0.,cos(length(gl_Vertex.xy)))*vec3(gl_Vertex.xy*.1,-.9),length(gl_Vertex.xy));'
db 'v=vec4(mat3(cos(length(gl_Vertex.xy)),0.,sin(length(gl_Vertex.xy)),0.,1.,0.,-sin(length(gl_Vertex.xy)),0.,cos(length(gl_Vertex.xy)))*vec3(gl_Vertex.xy*.1,.1),length(gl_Vertex.xy));'
db '}'
db 0
shader_frg:
db 'varying vec4 p,v;'
;db 'float mx(vec3 a){return max(a.x,max(a.y,a.z));}'
db 'float mn(vec3 a){return min(a.x,min(a.y,a.z));}'
db 'float F(vec3 a){return min(mn(vec3(1.)-abs(a)),-mn(abs(mod(a+vec3(.1),vec3(.4))-vec3(.2))-.15));}'
;db 'float F(vec3 a){return min(mn(vec3(1.)-abs(a)),length(mod(a,vec3(.4))-vec3(.2))-.06);}'
db 'vec3 n(vec3 a){'
db 'vec3 e=vec3(.0001,.0,.0);'
db 'return normalize(vec3(F(a)-F(a+e.xyy),F(a)-F(a+e.yxy),F(a)-F(a+e.yyx)));'
db '}'
db 'vec4 tr(vec3 E,vec3 D){'
db 'D=normalize(D);'
db 'float L=.01;'
db 'int i=0;'
db 'for(i;i<512;++i){'
db 'float d=F(E+D*L);'
db 'if(d<.0001)break;'
db 'L+=d;'
db '}'
;db 'return vec2(L,float(i)/512.);'
db 'return vec4(E+D*L,float(i)/512.);'
db '}'
db 'float I(vec3 a){'
db 'vec3 l=vec3(sin(p.w*1.3),cos(p.w*4.2),sin(p.w*3.2))*.9,la=l-a;'
db 'return length(tr(a,la).xyz-a)*dot(n(a),-normalize(la))/dot(la,la)+.01;'
;db 'return tr(a,-lv).x*F(a+lv)/dot(lv,lv)+.01;'
db '}'
db 'void main(){'
db 'vec4 t=tr(p.xyz,v.xyz);'
db 'gl_FragColor=I(t.xyz)*(abs(t)+vec4(t.w*5.));'
;db 'vec2 t=tr(p.xyz,v.xyz);'
;db 'vec3 q=p.xyz+normalize(v.xyz)*t.x;'
;db 'gl_FragColor=I(q)+vec4(t.y);'
db '}'
db 0
Или такое:
shader_vtx:
db 'varying vec4 p,v;'
db 'void main()'
db '{'
db 'gl_Position=gl_Vertex;'
db 'p=vec4(mat3(cos(length(gl_Vertex.xy)),0.,sin(length(gl_Vertex.xy)),0.,1.,0.,-sin(length(gl_Vertex.xy)),0.,cos(length(gl_Vertex.xy)))*vec3(gl_Vertex.xy*.1,-.9),length(gl_Vertex.xy));'
db 'v=vec4(mat3(cos(length(gl_Vertex.xy)),0.,sin(length(gl_Vertex.xy)),0.,1.,0.,-sin(length(gl_Vertex.xy)),0.,cos(length(gl_Vertex.xy)))*vec3(gl_Vertex.xy*.1,.1),length(gl_Vertex.xy));'
db '}'
db 0
shader_frg:
db 'varying vec4 p,v;'
db 'float mn(vec3 a){return min(a.x,min(a.y,a.z));}'
db 'float F(vec3 a){return min(mn(vec3(1.)-abs(a)),length(mod(a,vec3(.4))-vec3(.2))-.06);}'
db 'vec3 n(vec3 a){'
db 'vec3 e=vec3(.0001,.0,.0);'
db 'return normalize(vec3(F(a)-F(a+e.xyy),F(a)-F(a+e.yxy),F(a)-F(a+e.yyx)));'
db '}'
db 'vec3 tr(vec3 E,vec3 D){'
db 'D=normalize(D);'
db 'float L=.01;'
db 'int i=0;'
db 'for(i;i<512;++i){'
db 'float d=F(E+D*L);'
db 'if(d<.001)break;'
db 'L+=d;'
db '}'
db 'return E+D*L;'
db '}'
db 'vec3 I(vec3 a,vec3 l,vec3 c){'
db 'return c*(clamp(length(tr(a,l-a)-a),0.,length(l-a))*dot(n(a),normalize(a-l))/dot(l-a,l-a));'
db '}'
db 'void main(){'
db 'vec3 t=tr(p.xyz,v.xyz);'
db 'gl_FragColor=vec4('
db 'I(t,vec3(sin(p.w*1.3),cos(p.w*4.2),sin(p.w*3.2))*.7,vec3(.9,.6,.2))+'
db 'I(t,vec3(sin(p.w*3.2),sin(p.w*4.2),sin(p.w*1.3))*.7,vec3(.0,.3,.5)),1.);'
db '}'
db 0
Почему эти шейдеры дают такую картинку? Об этом я, следуя доброй традиции частоты постинга, расскажу в марте следующего года! Ключевые слова для нетерпеливых и самостоятельных: raymarching distance fields (много инфы есть у этого парня).
Эпилог
Несмотря на то, что в этом тексте полно ошибок, неоптимальностей и просто наглого вранья (посчитайте, сколько раз я здесь злоупотребил доверием), надеюсь, что он оказался полезен, и что хотя бы кто-нибудь теперь не сделает очередной стартап, а вместо этого создаст что-нибудь по-настоящему сложное, интересное и стоящее.
Несмотря на то, что с такими картинками нам уже не должно быть стыдно перед одноклассниками, и вообще мы уже должны слышать нарастающий гул каблуков и прочего визга поклонниц, развиваться всё ещё есть куда, и это ещё не конец, куда собрался, это ещё не конец!
В следующий раз вас ждёт рассказ о том, как на языке программирования «Си» доставать качающий бас из ничего и взрывать танцпол.
До встречи в октябре!
Автор: w23