В прошлый раз мы ограничились компиляцией джей-скрипа в файл в нашем собственном формате, которому требовался специальный загрузчик. Кроме того, мы задумали было пару оптимизаций исполнимого кода, требующих анализа соседних команд.
Чтобы реализовать задуманные оптимизации, во-первых, разделим общий вектор
Во-вторых, двух проходов нам уже не хватит: на первом проходе генерируем для каждой команды машинный код, на втором — выполняем оптимизации, на третьем — «заполировываем», заполняя смещения прыжков и строк. Одновременно с расчётом смещений прыжков будем по возможности заменять близкие (near) прыжки на короткие (short), так что на третьем проходе код дополнительно сократится, и потребуется ещё один проход — четвёртый; на нём вновь будем исправлять смещения прыжков, изменившиеся из-за сокращения кода.
В каждой команде придётся хранить количество сгенерированных
В прошлый раз реализация стандартных функций
Мы можем, в принципе, вставлять их код в каждый генерируемый бинарник; но это некрасиво. Вместо этого скомпилируем их в отдельный файл
Нам удобно, чтобы наши стандартные функции принимали параметр в
А вот — для x86:
Исполняемый код у нас практически готов; осталось лишь «упаковать» его так, чтобы ОС смогла его слинковать и запустить.
В памяти код будет состоять из тех же частей, что и прежде: код, данные, область связи для вызова стандартных функций. Отличие формата файлов
Можно было бы разделить данные на две секции: отдельно инициализированные и неизменяемые (область связи и строки), отдельно неинициализированные и изменяемые (ячейки для вылитых регистров); но тогда на x86 нам не хватило бы регистров постоянно хранить базовые адреса обеих секций данных, — значит, пришлось бы генерировать по релокации на каждое обращение. Упростим себе жизнь, и обойдёмся одной секцией данных.
По той же самой причине для вызова стандартных функций мы пользуемся областью связи: для вызова напрямую потребовалось бы создавать по релокации на каждый вызов. Сделаем область связи 24-байтной и на x86, и на x64 — опять же, для простоты и кроссплатформенности. На x64 это будет просто массив из трёх указателей (на
Получается, что релокаций у нас будет всегда четыре: три адреса стандартных функций в области связи, и загрузка базового адреса данных в
Релокации в ELF бывают в двух форматах:
Сгенерируем восемь стандартных секций: пустую,
В неизменном заголовке, который мы дописываем к сгенерированному коду, куча непонятных циферок. Что все они значат?
Пройдёмся по порядку.
Релокации типа
Три других релокации — в данных, получение адресов трёх импортируемых функций по смещениям 0,8,16. Эти три релокации ссылаются на импортируемые символы (№№3,4,5), и используют код привязки 1, всегда равный машинному слову (
Полный код компилятора: tyomitch.net.ru/jsk.y.elf.html
Если вас заинтересовал сбор бинарников вручную, советую глянуть способы уменьшения размеров ELF-файлов, от практически полезных трюков до чокнутых хаков; и заодно примеры донельзя ужатых программ.
Теперь окончательный бинарник получается из двух независимых компонент, которые можем скомпилировать по отдельности.
Далее в посте:
- Оптимизация «в глазок»
- Стандартные функции
- Вывод в ELF
- Как это работает?
- Что получилось?
Оптимизация «в глазок»
Чтобы реализовать задуманные оптимизации, во-первых, разделим общий вектор
code
на маленькие вектора сгенерированного машинного кода для каждой команды, и в конце будем склеивать их вместе.Во-вторых, двух проходов нам уже не хватит: на первом проходе генерируем для каждой команды машинный код, на втором — выполняем оптимизации, на третьем — «заполировываем», заполняя смещения прыжков и строк. Одновременно с расчётом смещений прыжков будем по возможности заменять близкие (near) прыжки на короткие (short), так что на третьем проходе код дополнительно сократится, и потребуется ещё один проход — четвёртый; на нём вновь будем исправлять смещения прыжков, изменившиеся из-за сокращения кода.
В каждой команде придётся хранить количество сгенерированных
POP
, потому что по одному лишь машинному коду тяжело понять, является ли последний байт инструкцией POP
, или просто совпал по значению. Количество сгенерированных PUSH
нет надобности хранить: первый байт машинного кода расшифровывается однозначно. struct commandn {
// ...
int offset; // от начала кода
int needfixat; // для JZ, ECHO
int popcnt; // INPUT, ECHO
std::vector<unsigned char> code;// окончательный выполнимый
// ...
void pop_back(int c = 1) {
code.resize(code.size()-c);
}
void pop_front(int c = 1) {
code.erase(code.begin(), code.begin()+c);
}
};
// ...
// "предварительная" генерация кода для JZ:
// предполагаем, что прыжок будет близкий
case command::jz:
if(!i->cmd.dest) // JMP off
i->emit(0xe9);
else { // OR dst, dst / JZ off
i->emit(0x0b, 0xc0|(i->cmd.dest-1)<<3|(i->cmd.dest-1));
i->emit(0x0f, 0x84);
}
i->needfixat = i->code.size();
i->emit4(0);
break;
// ...
// в ECHO и INPUT добавляем счётчик popcnt;
// остальная генерация -- без изменений
case command::input:
foreach(rp, i->onenterp) if(*rp!=4) i->emit(0x50|(*rp-1));
i->emit(0xff, 0x55, 0);
if(i->cmd.dest!=1) i->emit(0x90|(i->cmd.dest-1));
foreachr(rp, i->onenterp) if(*rp!=4) {
i->emit(0x58|(*rp-1));
i->popcnt++;
}
break;
// ...
// второй проход: оптимизация в глазок, расчёт смещений команд
int offset = 0;
foreach2(i,pcode,next) {
i->offset = offset;
// пары "POP-PUSH"
while(i->popcnt && ((next->code[0]&0xfc) == 0x50) &&
((i->code.back()&3) == (next->code[0]&3)) &&
// особый случай: нужно загрузить и пересохранить
!((next->cmd.opcode==command::echo) && (next->cmd.dest==(next->code[0]&3)+1))) {
i->pop_back();
next->pop_front();
i->popcnt--;
if(next->needfixat) next->needfixat--;
}
// пары "сравнение-JZ"
if((i->cmd.opcode>=command::eq) && (next->cmd.opcode==command::jz) &&
(i->cmd.dest==next->cmd.dest) && !next->onexitp.count((physreg)next->cmd.dest)) {
char cc = i->code[i->code.size()-5]; // cond code
i->pop_back(6); // SETcc / MOVZX
next->code.clear(); // заменяем всю команду
next->emit(0x0f, cc^0x11);
next->needfixat = next->code.size();
next->emit4(0);
}
offset += i->code.size();
}
// третий проход: окончательная генерация прыжков
offset = 0;
foreach(i, pcode) {
i->offset = offset;
if(i->cmd.opcode==command::jz) {
int joffset = i->tgt->offset-(i->offset+i->code.size());
if((joffset>=-128) && (joffset<128)) { // заменяем команду
if(!i->cmd.dest) { // JMP SHORT
i->code.clear();
i->emit(0xeb, (char)joffset);
} else if(i->code[0]==0x0b && i->code[1]==0xc9) { // OR ECX, ECX
i->code.clear();
i->emit(0xe3, (char)joffset); // JECXZ
} else {
char cc = i->code[i->code.size()-5]; // cond code
i->pop_back(6);
i->emit(cc^0xf0, (char)joffset);// Jcc SHORT
}
i->needfixat = i->code.size()-1; // в последнем байте смещение
}
}
offset += i->code.size();
}
// четвёртый проход: подставляем смещения в прыжках
foreach(i, pcode) if(i->needfixat)
if(i->cmd.opcode==command::jz) {
int joffset = i->tgt->offset-(i->offset+i->code.size());
switch(i->code[i->needfixat]-1) {
case 0xeb: case 0xe3: case 0x74: case 0x75: case 0x7c: case 0x7d: case 0x7e: case 0x7f:
i->code[i->needfixat] = (char)joffset; break; // short
default:
i->fix4(joffset);
}
}
else if (i->cmd.opcode==command::echo)
i->fix4(offsets[i->cmd.imm]);
Стандартные функции
В прошлый раз реализация стандартных функций
input,echoi,echos
была в нашем загрузчике. В этот раз загрузчика не будет; где же будут функции?Мы можем, в принципе, вставлять их код в каждый генерируемый бинарник; но это некрасиво. Вместо этого скомпилируем их в отдельный файл
.o
, который будет потом линковаться с нашим файлом. Преимущество этого подхода — в изоляции платформо-зависимости: все различия между x86 и x64, которые, с точки зрения нашего компилятора, заключаются в способе вызова сишных функций, — спрячем в платформо-зависимую библиотечку, а сами будем генерировать платформо-независимый код.Нам удобно, чтобы наши стандартные функции принимали параметр в
ESI
, а результат возвращали в EAX
. Так и сделаем. Вот реализация для x64:.global input,echoi,echos
.text
fd: .asciz "%d"
fs: .asciz "%s"
input: push %rax
lea fd, %edi
xor %eax, %eax
mov %rsp, %rsi
call scanf
pop %rax
ret
echoi: lea fd, %edi
echo: xor %eax, %eax
jmp printf
echos: movslq %esi, %rsi
add %rbp, %rsi
lea fs, %edi
jmp echo
А вот — для x86:
.global input,echoi,echos
.text
fd: .asciz "%d"
fs: .asciz "%s"
input: push %eax
push %esp
push $fd
call scanf
pop %eax
pop2: pop %eax
pop %eax
ret
echoi: push %esi
push $fd
echo: call printf
jmp pop2
echos: add %ebp, %esi
push %esi
push $fs
jmp echo
Вывод в ELF
Исполняемый код у нас практически готов; осталось лишь «упаковать» его так, чтобы ОС смогла его слинковать и запустить.
В памяти код будет состоять из тех же частей, что и прежде: код, данные, область связи для вызова стандартных функций. Отличие формата файлов
.o
от непосредственно исполнимых файлов — что в .o
неизвестно сорасположение частей программы (секций) в памяти; поэтому компилятор не может сгенерировать ссылку из одной секции на адрес в другой. Вместо этого компилятор генерирует указание линкеру — релокацию, указывающую, какой адрес нужно вычислить при линковке, и как его нужно вычислять. Поэтому файлы .o
в спецификации формата ELF называются релоцируемыми.Можно было бы разделить данные на две секции: отдельно инициализированные и неизменяемые (область связи и строки), отдельно неинициализированные и изменяемые (ячейки для вылитых регистров); но тогда на x86 нам не хватило бы регистров постоянно хранить базовые адреса обеих секций данных, — значит, пришлось бы генерировать по релокации на каждое обращение. Упростим себе жизнь, и обойдёмся одной секцией данных.
По той же самой причине для вызова стандартных функций мы пользуемся областью связи: для вызова напрямую потребовалось бы создавать по релокации на каждый вызов. Сделаем область связи 24-байтной и на x86, и на x64 — опять же, для простоты и кроссплатформенности. На x64 это будет просто массив из трёх указателей (на
input,echoi,echos
); на x86 за каждым указателем будет следовать 4-байтная «заглушка».Получается, что релокаций у нас будет всегда четыре: три адреса стандартных функций в области связи, и загрузка базового адреса данных в
RBP/EBP
первой командой программы. В итоге, генерация «релоцируемого» кода почти не будет отличаться от генерации «сплошного куска» в прошлый раз. В некоторых отношениях она даже упростится: поскольку сейчас строки будут храниться отдельно от кода, мы можем запоминать их смещения прямо на этапе синтаксического разбора; так мы полностью избавимся от «временных идентификаторов строк» и от этапа их привязки перед выводом скомпилированного кода. Кроме того, способ вызова стандартных функций теперь не будет зависеть от платформы. typedef std::map<std::string,int> stringmap;
stringmap strings;
int laststr = 24; // строки идут сразу за областью связи
std::vector<stringmap::iterator> strdata; // порядок важен
// ...
// в коде свёртки VAL: ID '(' ARGS ')'
if (!$1.compare("echo")) {
if(!$3.size())
yyerror("Input: too many arguments");
$$ = 0;
foreach(i, $3)
if(!i->dest) // string
if(strings.count(i->str))
emit(command::echo, 0, strings[i->str]);
else {
strdata.push_back(strings.insert(stringmap::value_type(i->str,laststr)).first);
emit(command::echo, 0, laststr);
laststr += i->str.length()+1;
}
else
emit(command::echo, i->dest, 0);
}
// ...
// (на первом проходе по п-коду)
case command::hlt:
i->emit(0x5b, 0x5e, 0x5f, 0x5d); // POP EBX / POP ESI / POP EDI / POP EBP
i->emit(0xc3); // RET
break;
case command::echo: // PUSH live / MOV EDI, dst / CALL [EBP+?] / POP live
foreach(rp, i->onexitp) if(*rp!=4) i->emit(0x50|(*rp-1));
if(!i->cmd.dest) { // imm / [EBP+16]
i->emit14(0xbe, i->cmd.imm);
i->emit(0xff, 0x55, 16);
} else {
if(i->known.count(i->cmd.dest)) // imm / [EBP+4]
i->emit14(0xbe, i->known[i->cmd.dest]);
else // dst / [EBP+8]
i->emit(0x8b, 0xf0|(i->cmd.dest-1));
i->emit(0xff, 0x55, 8);
}
foreachr(rp, i->onexitp) if(*rp!=4) {
i->emit(0x58|(*rp-1));
i->popcnt++;
}
break;
Релокации в ELF бывают в двух форматах:
rel
или rela
. Формат rel
компактнее, но линкеру проще работать с rela
; поэтому при переходе на платформу x64 релокации типа rel
объявили «осуждаемыми» (deprecated). Тем не менее, моя версия ld
поддерживает rel
в 64-битном коде, поэтому генерировать будем именно rel
.Сгенерируем восемь стандартных секций: пустую,
.shstrtab, .strtab, .symtab, .rel.text, .rel.data, .text, .data
. Заголовок ELF и первые шесть секций имеют предопределённое содержимое; только .text
и .data
наполняются в зависимости от сгенерированного кода.// объявления ELF
#include <linux/elf.h>
#if ELF_CLASS == ELFCLASS32
#define Elf_Shdr Elf32_Shdr
#define Elf_Sym Elf32_Sym
#define Elf_Rel Elf32_Rel
#define ELF_R_INFO(s,t) (((s)<<8)+(unsigned char)(t))
#define R_32 R_386_32
#else
#define Elf_Shdr Elf64_Shdr
#define Elf_Sym Elf64_Sym
#define Elf_Rel Elf64_Rel
#define ELF_R_INFO(s,t) (((unsigned long)(s)<<32)+(t))
#define R_32 R_X86_64_32
#endif
#define ST_GLOBAL_NOTYPE STB_GLOBAL<<4
// ...
// после четырёх проходов генерации:
// привязка смещений строк больше не нужна
// осталось только вывести код
// пролог: PUSH EBP / PUSH EDI / PUSH ESI / PUSH EBX / MOV RBP, ...
const char prolog[] = {0x55,0x57,0x56,0x53,0x48,0xc7,0xc5,0,0,0,0};
offset += sizeof(prolog);
int alignment = ((offset+3)&~3) - offset; // выравнивание на dword
offset += alignment;
const struct {
elfhdr hdr;
Elf_Shdr Shdr[8];
char shstrtab[64];
char strtab[24];
Elf_Sym symtab[6];
Elf_Rel reltext[1];
Elf_Rel reldata[3];
} elf =
{{{ELFMAG0, ELFMAG1, ELFMAG2, ELFMAG3, ELF_CLASS, ELF_DATA, EV_CURRENT}, // identification
ET_REL, ELF_ARCH, EV_CURRENT, 0, 0, sizeof(elfhdr), 0, sizeof(elfhdr), 0, 0, sizeof(Elf_Shdr),8, 1},
{{0, SHT_NULL},
{1, SHT_STRTAB, 0, 0, (char*)&elf.shstrtab-(char*)&elf, sizeof(elf.shstrtab), 0, 0, 1, 0},
{11, SHT_STRTAB, 0, 0, (char*)&elf.strtab-(char*)&elf, sizeof(elf.strtab), 0, 0, 1, 0},
{19, SHT_SYMTAB, 0, 0, (char*)&elf.symtab-(char*)&elf, sizeof(elf.symtab), 2, 2, 8,sizeof(Elf_Sym)},
{27, SHT_REL, 0, 0, (char*)&elf.reltext-(char*)&elf, sizeof(elf.reltext), 3, 6, 8, sizeof(Elf_Rel)},
{37, SHT_REL, 0, 0, (char*)&elf.reldata-(char*)&elf, sizeof(elf.reldata), 3, 7, 8, sizeof(Elf_Rel)},
{47, SHT_PROGBITS, SHF_ALLOC|SHF_EXECINSTR, 0, sizeof(elf), offset, 0, 0, 4, 0},
{53, SHT_PROGBITS, SHF_ALLOC|SHF_WRITE, 0, sizeof(elf)+offset, laststr+lastspill*4, 0, 0, 4, 0}},
"\0.shstrtab\0.strtab\0.symtab\0.rel.text\0.rel.data\0.text\0.data", // shstrtab
"\0main\0input\0echoi\0echos", // strtab
{{},
#if ELF_CLASS == ELFCLASS32
{0, 0, 0, STT_SECTION, 0, 7},
{1, 0, 0, ST_GLOBAL_NOTYPE, 0, 6}, // main
{6, 0, 0, ST_GLOBAL_NOTYPE, 0, 0}, // input
{12, 0, 0, ST_GLOBAL_NOTYPE, 0, 0}, // echoi
{18, 0, 0, ST_GLOBAL_NOTYPE, 0, 0}}, // echos
#else
{0, STT_SECTION, 0, 7, 0, 0},
{1, ST_GLOBAL_NOTYPE, 0, 6, 0, 0}, // main
{6, ST_GLOBAL_NOTYPE, 0, 0, 0, 0}, // input
{12, ST_GLOBAL_NOTYPE, 0, 0, 0, 0}, // echoi
{18, ST_GLOBAL_NOTYPE, 0, 0, 0, 0}}, // echos
#endif
{{7, ELF_R_INFO(1,R_32)}}, // reltext
{{0, ELF_R_INFO(3,1)}, // input
{8, ELF_R_INFO(4,1)}, // echoi
{16, ELF_R_INFO(5,1)}} // echos
};
write(1, &elf, sizeof(elf));
// вывод кода
write(1, prolog, sizeof(prolog));
foreach(i, pcode)
write(1, &*i->code.begin(), i->code.size());
// дополнение до dword
const char zero[24] = {};
write(1, zero, alignment);
// область связи
write(1, zero, 24);
// вывод строк
foreach(i, strdata)
write(1, (*i)->first.c_str(), (*i)->first.length()+1);
// резервируем место для выливания
ftruncate(1, sizeof(elf)+offset+laststr+lastspill*4);
Как это работает?
В неизменном заголовке, который мы дописываем к сгенерированному коду, куча непонятных циферок. Что все они значат?
Пройдёмся по порядку.
ELFMAG0,ELFMAG1,ELFMAG2,ELFMAG3
: четыре «волшебных байта», с которых должен начинаться ELF-файл;ELF_CLASS,ELF_DATA,EV_CURRENT
: идентификаторы «битности» (ширины слова), порядка байтов в слове, и версии формата ELF. В зависимости от этих идентификаторов расшифровывается весь остальной заголовок (в его 32- и 64-битной версиях разный размер полей);ET_REL
: тип ELF-файла («релоцируемый», «исполняемый», «динамическая библиотека»);ELF_ARCH,EV_CURRENT
: идентификатор процессора и ещё раз версия формата;0,0
: адрес точки входа и смещение таблицы сегментов. В релоцируемых файлах нет ни того, ни другого;sizeof(elfhdr)
: смещение таблицы секций. У нас она пойдёт сразу же за заголовком;0
: флаги, на x86/x64 не определены;sizeof(elfhdr)
: размер заголовка;0,0
: размер и количество записей в таблице сегментов. У нас этой таблицы как не было, так и нет;sizeof(Elf_Shdr),8
: размер и количество записей в таблице секций;1
: номер секции, содержащей названия всех секций;"\0.shstrtab\0.strtab\0.symtab\0.rel.text\0.rel.data\0.text\0.data"
: названия всех секций, одно за другим. Первым символом строки должен быть\0
, чтобы нулевое смещение указывало на пустое название;{0, SHT_NULL}
: секция №0 по стандарту должна быть пустой;1
: название секции (смещение внутриshstrtab
);SHT_STRTAB
: тип секции (в данном случае, «таблица строк»);0
: флаги (чтение, запись, выполнение), задающие для секции защиту памяти. Для служебных секций, таких как таблицы имён, не нужно ничего из этого;0
: адрес секции в памяти. В релоцируемых файлах не определён;(char*)&elf.shstrtab-(char*)&elf,sizeof(elf.shstrtab)
: смещение секции в файле, и её размер;0,0
: два дополнительных поля, трактовка которых зависит от типа секции. Для таблицы строк не определены;1
: размер блока выравнивания; для строк — 1 байт, т.е. без выравнивания;0
: размер записи внутри секции. В таблице строк записи неопределённой длины;{11,SHT_STRTAB,0,0,(char*)&elf.strtab-(char*)&elf,sizeof(elf.strtab),0,0,1,0}
: ещё одна таблица строк. Отличается только именем (11—.strtab
);{19,SHT_SYMTAB,0,0,(char*)&elf.symtab-(char*)&elf,sizeof(elf.symtab),2,2,8,sizeof(Elf_Sym)}
: секция типа «таблица символов». Содержимое дополнительных полей: первая двойка — номер секции с именами символов (предыдущая, т.е..strtab
), вторая двойка — номер первого глобального символа;{27,SHT_REL,0,0,(char*)&elf.reltext-(char*)&elf,sizeof(elf.reltext),3,6,8,sizeof(Elf_Rel)}
: секция типа «таблица релокаций». Содержимое дополнительных полей: ссылка на таблицу символов (предыдущая секция, она же №3) и на релоцируемую секцию (.text
, она же №6);{37,SHT_REL,0,0,(char*)&elf.reldata-(char*)&elf,sizeof(elf.reldata),3,7,8,sizeof(Elf_Rel)}
: ещё одна таблица релокаций. Ссылается на ту же самую таблицу символов (№3) и на секцию.data
(№7);{47,SHT_PROGBITS,SHF_ALLOC|SHF_EXECINSTR,0,sizeof(elf),offset,0,0,4,0}
: секция данных (SHT_PROGBITS
) с возможностью выполнения (SHF_EXECINSTR
) размеромoffset
;{53,SHT_PROGBITS,SHF_ALLOC|SHF_WRITE,0,sizeof(elf)+offset,laststr+lastspill*4,0,0,4,0}
: секция данных с возможностью изменения (SHF_WRITE
), в которой будут строки и 4-байтные ячейки для выливания;"\0main\0input\0echoi\0echos"
: имена символов (импортируемых и экспортируемых), в таком же формате, как имена секций;{}
: символ №0 должен остаться пустым;{0, 0, 0, STT_SECTION, 0, 7}
: безымянный (0) локальный символ, указывающий на секцию (STT_SECTION
) №7, т.е..data
;{1, 0, 0, ST_GLOBAL_NOTYPE, 0, 6}
: глобальный (ST_GLOBAL_NOTYPE) символ по имениmain
(смещение имени 1), указывает на смещение 0 в секции №6, т.е..text
;{6, ST_GLOBAL_NOTYPE, 0, 0, 0, 0}
: глобальный символ по имениinput
(смещение имени 6), не относящийся ни к одной секции, т.е. импортируемый;- два других импортируемых символа,
echoi
иechos
, определены аналогично.
Elf32_Sym
и Elf64_Sym
определены с одинаковыми полями, но в разном порядке. У нас не остаётся выхода, кроме как написать два варианта кода, и при помощи #ifdef
выбирать один из них.Релокации типа
rel
состоят из троек «смещение релоцируемого поля, номер символа, код типа привязки». Первая релокация ({7, ELF_R_INFO(1,R_32)}
) — в прологе, по смещению 7 от начала кода; она задаёт базовый адрес данных, загружаемый в EBP/RBP
. Эта релокация ссылается на символ №1, т.е. на секцию .data
. На любой архитектуре она имеет размер 32 бита. Код типа привязки при этом отличается: R_386_32=1
на x86, и R_X86_64_32=10
на x64.Три других релокации — в данных, получение адресов трёх импортируемых функций по смещениям 0,8,16. Эти три релокации ссылаются на импортируемые символы (№№3,4,5), и используют код привязки 1, всегда равный машинному слову (
R_386_32,R_X86_64_64
).Полный код компилятора: tyomitch.net.ru/jsk.y.elf.html
Если вас заинтересовал сбор бинарников вручную, советую глянуть способы уменьшения размеров ELF-файлов, от практически полезных трюков до чокнутых хаков; и заодно примеры донельзя ужатых программ.
Получился файл размером 45 байт: в пять раз меньше, чем на ассемблере, и в пятьдесят раз меньше, чем на Си. Мы выкинули из файла всё, что смогли; а то, что не смогли выкинуть, используем одновременно в двух-трёх целях.
Примерно половина значений в этом файле так или иначе нарушают стандарт ELF; нормальный программист постеснялся бы признаться, что такая программа — плод его рук. Поразительно, что Linux соглашается присвоить PID этому кошмару.
С другой стороны, про каждый байт в этом файле я могу объяснить, зачем он нужен. Часто вы можете сказать то же самое про скомпилированные вами файлы?
Что получилось?
Теперь окончательный бинарник получается из двух независимых компонент, которые можем скомпилировать по отдельности.
[tyomitch@home ~]$ as jskstd.s -o jskstd.o
[tyomitch@home ~]$
[tyomitch@home ~]$ bison jsk.y
[tyomitch@home ~]$ c++ jsk.tab.c lex.yy.c -o jskc
[tyomitch@home ~]$
[tyomitch@home ~]$ ./jskc < test.jsk > code.o
[tyomitch@home ~]$ cc jskstd.o code.o
[tyomitch@home ~]$ ./a.out
Задумай число от 0 до 1000, а я буду угадывать
Это 500? (1=меньше, 2=больше, 3=попал) 1
Это 249? (1=меньше, 2=больше, 3=попал) 3
Ура! Я молодец!
[tyomitch@home ~]$ objdump -d code.o
code.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 57 push %rdi
2: 56 push %rsi
3: 53 push %rbx
4: 48 c7 c5 00 00 00 00 mov $0x0,%rbp
b: 33 c0 xor %eax,%eax
d: b9 e8 03 00 00 mov $0x3e8,%ecx
12: 50 push %rax
13: 51 push %rcx
14: be 18 00 00 00 mov $0x18,%esi
19: ff 55 10 callq *0x10(%rbp)
1c: 59 pop %rcx
1d: 58 pop %rax
1e: 50 push %rax
1f: 51 push %rcx
20: be 00 00 00 00 mov $0x0,%esi
25: ff 55 08 callq *0x8(%rbp)
28: be 38 00 00 00 mov $0x38,%esi
2d: ff 55 10 callq *0x10(%rbp)
30: 59 pop %rcx
31: 51 push %rcx
32: be e8 03 00 00 mov $0x3e8,%esi
37: ff 55 08 callq *0x8(%rbp)
3a: be 3f 00 00 00 mov $0x3f,%esi
3f: ff 55 10 callq *0x10(%rbp)
42: 59 pop %rcx
43: 58 pop %rax
44: 3b c1 cmp %ecx,%eax
46: 0f 8f 6c 00 00 00 jg b8 <main+0xb8>
4c: 8d 14 01 lea (%rcx,%rax,1),%edx
4f: d1 fa sar %edx
51: 50 push %rax
52: 51 push %rcx
53: 52 push %rdx
54: be 64 00 00 00 mov $0x64,%esi
59: ff 55 10 callq *0x10(%rbp)
5c: 5a pop %rdx
5d: 52 push %rdx
5e: 8b f2 mov %edx,%esi
60: ff 55 08 callq *0x8(%rbp)
63: be 6c 00 00 00 mov $0x6c,%esi
68: ff 55 10 callq *0x10(%rbp)
6b: ff 55 00 callq *0x0(%rbp)
6e: 93 xchg %eax,%ebx
6f: 5a pop %rdx
70: 59 pop %rcx
71: 58 pop %rax
72: 89 85 04 01 00 00 mov %eax,0x104(%rbp)
78: 83 fb 01 cmp $0x1,%ebx
7b: 75 0b jne 88 <main+0x88>
7d: 8b 85 04 01 00 00 mov 0x104(%rbp),%eax
83: 8d 4a ff lea 0xffffffffffffffff(%rdx),%ecx
86: eb bc jmp 44 <main+0x44>
88: 83 fb 02 cmp $0x2,%ebx
8b: 75 05 jne 92 <main+0x92>
8d: 8d 42 01 lea 0x1(%rdx),%eax
90: eb b2 jmp 44 <main+0x44>
92: 8b 85 04 01 00 00 mov 0x104(%rbp),%eax
98: 83 fb 03 cmp $0x3,%ebx
9b: 75 0d jne aa <main+0xaa>
9d: be 9f 00 00 00 mov $0x9f,%esi
a2: ff 55 10 callq *0x10(%rbp)
a5: 5b pop %rbx
a6: 5e pop %rsi
a7: 5f pop %rdi
a8: 5d pop %rbp
a9: c3 retq
aa: 50 push %rax
ab: 51 push %rcx
ac: be bb 00 00 00 mov $0xbb,%esi
b1: ff 55 10 callq *0x10(%rbp)
b4: 59 pop %rcx
b5: 58 pop %rax
b6: eb 8c jmp 44 <main+0x44>
b8: be dd 00 00 00 mov $0xdd,%esi
bd: ff 55 10 callq *0x10(%rbp)
c0: 5b pop %rbx
c1: 5e pop %rsi
c2: 5f pop %rdi
c3: 5d pop %rbp
c4: c3 retq
Код выглядит сносно, хотя, наверное, и не дотягивает по качеству до сгенерированного llvm. Зато здесь каждый байт — собственными руками.