Pull to refresh

Компиляция. 10: компиляция в ELF

Reading time 13 min
Views 6.8K
В прошлый раз мы ограничились компиляцией джей-скрипа в файл в нашем собственном формате, которому требовался специальный загрузчик. Кроме того, мы задумали было пару оптимизаций исполнимого кода, требующих анализа соседних команд.

Далее в посте:

  1. Оптимизация «в глазок»
  2. Стандартные функции
  3. Вывод в ELF
  4. Как это работает?
  5. Что получилось?

Оптимизация «в глазок»


Чтобы реализовать задуманные оптимизации, во-первых, разделим общий вектор 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. Зато здесь каждый байт — собственными руками.
Tags:
Hubs:
+53
Comments 9
Comments Comments 9

Articles