Pull to refresh

Компиляция. 9: исполняемый код

Programming *
Напоминаю, что мы пишем компилятор для игрушечного языка джей-скрип. Начали с компиляции в п-код, потратили немало сил на его оптимизацию, и приготовились к заключительному этапу компиляции — к выводу машинно-зависимого выполнимого кода.
Никаких замысловатых алгоритмов тут уже нет: по большому счёту, только замена одной системы команд на другую.

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

  1. Выбор кода
  2. Загрузчик
  3. Изменения в п-коде
  4. Генерация
  5. Что получилось?

Выбор кода


Рискну предположить, что вы читаете этот пост на компьютере, внутри которого процессор x86 или x64. Перед пользователями мобильных устройств извиняюсь: генерировать код для модного гаджета было бы занимательно, но мне слишком сложно раздобыть такой экземплярчик для экспериментов.

Постараемся генерировать «кроссплатформенный» код, одинаково работоспособный и на x86, и на x64. Сосредоточимся именно на машинном коде, а не на внутренностях формата ELF и взаимодействии с загрузчиком; поэтому будем создавать код «сплошным куском», аналогично досовскому формату COM. Читать его с диска и запускать будет наш собственный загрузчик.

Пользоваться будем только 32-битными регистрами, так что одна и та же программа, даже если в ней при вычислениях происходят переполнения, будет давать одинаковые результаты и на x64, и на x86.

Для связи со внешним миром (команды ECHO и INPUT) сгенерированный код должен будет как-то вызывать функции операционной системы. Откуда он узнает их адреса?
Наш загрузчик создаст для загруженной программы «область связи» — массив указателей на «стандартные функции», и передаст адрес этого массива параметром в программу. Стандартных функций у нас три: ввод числа, вывод числа, вывод строки; поэтому область связи будет состоять из трёх указателей. Для удобства адресации, сразу же на входе в программу сохраним переданный адрес в EBP/RBP.

Другой интересный момент — доступ к памяти (к вылитым переменным). Вместо типичной для x86 абсолютной адресации в x64 ввели «перемещаемую абсолютную», относительно RIP. Чтобы код действительно получился кроссплатформенным, надо пользоваться явной относительной адресацией.
Не мудрствуя лукаво, будем адресовать ячейки вылитых переменных относительно того же EBP/RBP — т.е. сразу за областью связи, по смещению 0x18, начнётся область переменных.

Сгенерированный п-код использует четыре «абстрактных физических регистра» R01..R04, которым естественно сопоставить настоящие регистры EAX,ECX,EDX,EBX: т.е. номер настоящего регистра получается из номера «абстрактного» вычитанием единицы. Если бы мы захотели задействовать остальные регистры процессора, соответствие между номерами было бы более сложным.

Способ передачи параметров в функцию на x86 и на x64 сильно отличается. На x86 параметры по умолчанию передаются через стек (cdecl), но можно попросить gcc передавать до трёх параметров в регистрах ECX,EDX,EAX (fastcall). На x64 шесть параметров передаются в регистрах RDI,RSI,RDX,RCX,R8,R9, и gcc не позволяет менять способ вызова. Из-за этой несовместимости придётся реализовать в генераторе кода оба механизма передачи, и выбирать нужный. К счастью, передача параметров будет в нашем коде единственным платформо-зависимым местом.
На обоих процессорах вызываемая функция вправе портить содержимое EAX,ECX,EDX, так что их понадобится сохранять перед вызовом (если они живы), и восстанавливать после. Таким образом, информация о живости физических регистров, которой мы пользовались в ходе их назначения, — оказывается нужна и при генерации кода.

Какой код будем генерировать, разобрались; дело за малым — реализовать задуманное.

Загрузчик


Берём за основу давнишний интерпретатор п-кода, оставляем всю шелуху (проверка ввода, отображение файла в память), а нутро (цикл выполнения и реализации команд) заменяем вызовом прочитанного кода, как будто бы это обычная сишная функция.
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>

const char* fdata = NULL; // весь прочитанный код

int input() { int d; scanf(" %d", &d); return d; }
void echoi(int i) { printf("%d", i); }
void echos(int offset) { printf("%s", fdata+offset); }

void* linkarea[1000] = {(void*)input, (void*)echoi, (void*) echos};

int main(int argc, char** argv) {

    if(argc!=2) {
        printf("Missing code file name.\n");
        exit(1);
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd<0) {
        printf("Cannot open code file.\n");
        exit(1);
    }
    struct stat finfo;
    fstat(fd, &finfo);
    fdata = (const char*)mmap(0, finfo.st_size, PROT_READ|PROT_EXEC, MAP_PRIVATE, fd, 0);
    if (!fdata) {
        printf("Cannot read code file.\n");
        exit(1);
    }

    ((void (*)(void**)) fdata)(linkarea); // запуск

    munmap((void*)fdata, finfo.st_size);
    close(fd);
    return 0;
}

Заметьте, что исчез #include "jsk.h", задававший определения команд: новый загрузчик ничего не знает ни про джей-скрип, ни про наш п-код.

Изменения в п-коде


Сейчас, когда п-код стал внутренним представлением, невидимым снаружи компилятора, — уже нет смысла сжимать каждую команду в 4 байта; значит, не нужны ни хитрые union, ни ограничения размера поля. Заменяем jsk.h на объявление обычной структуры с полями int:
struct command {
    enum opcodes {
        hlt, store, jz, echo, mov, load, input, add, sub, mul, div,
        eq, ne, ge, le, gt, lt
    };
    opcodes opcode;
    regnum dest, src1, src2;
    int imm;
    command(opcodes opcode, regnum dest, regnum src1, regnum src2) :
            opcode(opcode), dest(dest), src1(src1), src2(src2) {}
    command(opcodes opcode, regnum dest, int imm) :
            opcode(opcode), dest(dest), imm(imm) {}
};


Особенность набора инструкций x86/x64 — его разнообразие и неортогональность. Едва ли не любую операцию можно закодировать тремя-четырьмя разными способами. Постараемся для каждой п-команды выбирать самую компактную из возможных реализаций. (Это предельный случай «оптимизации сквозь глазок» — peephole optimization, занимающейся не программой в целом, а короткими блоками смежных команд. У нас вообще каждая команда обрабатывается независимо от окружающих.)

Например, п-команда add у нас может скомпилироваться в MOV, в INC, в DEC, в одну из трёх форм ADD или в одну из трёх форм LEA — в зависимости от сочетания аргументов.

Некоторые локальные оптимизации, вроде замены mul r, r, 2 на add r, r, r, удобно выполнить ещё на уровне п-кода, до трансляции в выполнимый код. На этом же этапе — после выбора физических регистров, но до окончательной трансляции — избавимся от регистров, значение которых известно во всех местах, где они используются: при трансляции будем пользоваться известным значением.
// в подстановке констант:
//      ...
        if(i->cmd.opcode==command::mul) { // несколько замен
            if(i->known.count(i->cmd.src1)) std::swap(i->cmd.src1, i->cmd.src2);
            if(i->known.count(i->cmd.src2)) switch(i->known[i->cmd.src2]) {
                case -1: i->cmd = command(command::sub, i->cmd.dest, 0, i->cmd.src1); break;
                case 0:  i->cmd = command(command::mov, i->cmd.dest, 0); break;
                case 1:  nopOut(i); break;
                case 2:  i->cmd = command(command::add, i->cmd.dest, i->cmd.src1, i->cmd.src1); break;
            }
        }

// удалить регистры, значение которых известно всюду, где используется
void postalloc() {
        std::vector<bool> needed(lastreg+1);
        foreach(i, pcode) {
            if(i->has2src()) {
                if(!i->known.count(i->cmd.src1)) needed[i->cmd.src1]=true;
                if(!i->known.count(i->cmd.src2)) needed[i->cmd.src2]=true;
                else // src2 известен: есть один особый случай
                    if(i->cmd.opcode==command::div && i->known[i->cmd.src2]!=2)
                        needed[i->cmd.src2]=true; // под делитель нужен регистр
            }
            if(i->readsdest() && !i->known.count(i->cmd.dest))
                needed[i->cmd.dest]=true;
        }
        foreach(i, pcode)
            if(i->writesdest() && !needed[i->cmd.dest])
                nopOut(i);
}

// после успешного выбора всех физических регистров:
            // ...
            postalloc();
            // вычистить NOP-ы
            simpleopt();
            // подставляем физические регистры: и в командах, и в known
            foreach(i, pcode) {
                std::map<regnum,int> known;
                if(i->known.count(i->cmd.dest))
                    known[physmap[i->cmd.dest]] = i->known[i->cmd.dest];
                i->cmd.dest = physmap[i->cmd.dest];
                if (i->has2src()) {
                    if(i->known.count(i->cmd.src1))
                        known[physmap[i->cmd.src1]] = i->known[i->cmd.src1];
                    if(i->known.count(i->cmd.src2))
                        known[physmap[i->cmd.src2]] = i->known[i->cmd.src2];
                    i->cmd.src1 = physmap[i->cmd.src1];
                    i->cmd.src2 = physmap[i->cmd.src2];
                }
                i->known = known;
            }
            break;


Генерация


Весь генерируемый код будем хранить в одном большом векторе std::vector<char> code;
В struct commandn нам понадобится несколько новых полей. int offset, length; будут определять позицию команды в векторе code. Кроме того, для команд JZ и ECHO, значение смещения в которых неизвестно до конца генерации кода, в поле int needfixat; будем хранить индекс незаполненного смещения.

Итак, на первом проходе по п-коду генерируем весь исполнимый код в векторе code; затем пройдём по коду второй раз, и заполним недостающие смещения. После этого выводим в результат весь код и все строки.

Я постарался убрать из листинга самые неинтересные куски, чтоб хоть немного его сократить. Полный код компилятора выложен на tyomitch.net.ru/jsk.y.natv.html

// последний этап: генерация кода для x86/x64
const char regsize = sizeof(void*); // 4 либо 8
// пролог
if(regsize==4) { // PUSH EBP / MOV EBP, [ESP+8] / PUSH EDI / PUSH ESI / PUSH EBX
    code.push_back(0x55); code.push_back(0x8b); code.push_back(0x6c); code.push_back(0x24);
    code.push_back(0x08); code.push_back(0x57); code.push_back(0x56); code.push_back(0x53);
} else {         // PUSH EBP / MOV RBP, RDI / PUSH EBX
    code.push_back(0x55); code.push_back(0x48); code.push_back(0x8b); code.push_back(0xef);
    code.push_back(0x53);
}
foreach(i, pcode) {
    int imm, doffset, joffset;
    bool jshort, saveAX, saveDX;
    i->offset = code.size();
    switch(i->cmd.opcode) {
    case command::hlt:
        if(regsize==4) {
            i->emit(0x5b, 0x5e, 0x5f, 0x5d); // POP EBX / POP ESI / POP EDI / POP EBP
            i->emit(0xc2, 2, 0);             // RET 2
        } else
            i->emit(0x5b, 0x5d, 0xc3);       // POP EBX / POP EBP / RET
        break;
    case command::store: // MOV [EBP+18+(imm-1)*4], dst
        doffset = 0x18+(i->cmd.imm-1)*4;
        if(i->known.count(i->cmd.dest)) {
            i->emit(0xc7);
            if(doffset<128)
                i->emit(0x45, (char)doffset);
            else {
                i->emit14(0x85, doffset);
            }
            i->emit4(i->known[i->cmd.dest]);
        } else {
            i->emit(0x89);
            if(doffset<128)
                i->emit(0x45|(i->cmd.dest-1)<<3, (char)doffset);
            else
                i->emit14(0x85|(i->cmd.dest-1)<<3, doffset);
        }
        break;
    case command::jz:
        joffset = i->tgt->offset - (i->offset+2); // предполагаем JMP SHORT
        jshort = i->tgt->offset && (joffset>=-128); // прыжок назад и близко
        if(!i->cmd.dest) { // JMP off
            if(jshort)
                i->emit(0xeb, (char)joffset);
            else {
                i->emit(0xe9);
                if(i->tgt->offset) // прыжок назад
                    i->emit4(joffset-3);
                else {
                    i->needfixat = code.size();
                    i->emit4(0);
                }
            }
            break;
        }
        if(jshort && (i->cmd.dest==2)) { // JECXZ
            i->emit(0xe3, (char)joffset);
            break;
        }
        // OR dst, dst / JZ off
        i->emit(0x0b, 0xc0|(i->cmd.dest-1)<<3|(i->cmd.dest-1));
        if(jshort && (joffset>=-126))
            i->emit(0x74, (char)(joffset-2));
        else {
            i->emit(0x0f, 0x84);
            if(i->tgt->offset) // прыжок назад
                i->emit4(joffset-6);
            else {
                i->needfixat = code.size();
                i->emit4(0);
            }
        }
        break;
    case command::echo:  // PUSH live / PUSH dst / CALL [EBP+?] / ADD ESP, 4 / POP live
        foreach(rp, i->onexitp) if(*rp!=4) i->emit(0x50|(*rp-1));
        if(!i->cmd.dest) { // imm, [EBP+8]
            if(regsize==4) i->emit(0x68); else i->emit(0xbf);
            i->needfixat = code.size();
            i->emit4(0);
            i->emit(0xff, 0x55, 2*regsize);
        } else {
            if(i->known.count(i->cmd.dest)) { // imm, [EBP+4]
                imm = i->known[i->cmd.dest];
                if(regsize==8)
                    i->emit14(0xbf, imm);
                else if((imm>=-128)&&(imm<128))
                    i->emit(0x6a, (char)imm);
                else
                    i->emit14(0x68, imm);
            } else if(regsize==4) // dst, [EBP+4]
                i->emit(0x50|(i->cmd.dest-1));
            else
                i->emit(0x8b, 0xf8|(i->cmd.dest-1));
            i->emit(0xff, 0x55, regsize);
        }
        if(regsize==4) i->emit(0x83, 0xc4, 4);
        foreachr(rp, i->onexitp) if(*rp!=4) i->emit(0x58|(*rp-1));
        break;
    case command::mov:   // MOV dst, imm
        if(i->cmd.imm)
            i->emit14(0xb8|(i->cmd.dest-1), i->cmd.imm);
        else             // XOR dst, dst
            i->emit(0x33, 0xc0|(i->cmd.dest-1)<<3|(i->cmd.dest-1));
        break;
    case command::load:  // MOV dst, [EBP+(3+i)*4]
        doffset = 0x18+(i->cmd.imm-1)*4;
        i->emit(0x8b);
        if(doffset<128)
            i->emit(0x45|(i->cmd.dest-1)<<3, (char)doffset);
        else
            i->emit14(0x85|(i->cmd.dest-1)<<3, doffset);
        break;
    case command::input: // PUSH live / CALL [EBP+0] / XCHG EAX, dst / POP live
        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));
        break;
    case command::add:
        // максимум 1 операнд известен; поставим его в src2
        if(i->known.count(i->cmd.src1) || (i->cmd.src2==i->cmd.dest))
            std::swap(i->cmd.src1, i->cmd.src2);
        if(!i->cmd.src2) // MOV dst, src1
            i->emit(0x8b, 0xc0|(i->cmd.dest-1)<<3|(i->cmd.src1-1));
        else if(i->known.count(i->cmd.src2)) {
            imm = i->known[i->cmd.src2];
    addimm:
            if(i->cmd.dest==i->cmd.src1) // ADD dst, imm
                if(imm==1) // INC dst
                    i->emit(0xff, 0xc0|(i->cmd.dest-1));
                else if(imm==-1) // DEC dst
                    i->emit(0xff, 0xc8|(i->cmd.dest-1));
                else if((imm>=-128)&&(imm<128))
                    i->emit(0x83, 0xc0|(i->cmd.dest-1), (char)imm);
                else // for imm=128 we might use SUB Edst, -128
                    i->emit114(0x81, 0xc0|(i->cmd.dest-1), imm);
            else // LEA dst, [src1+imm]
                if((imm>=-128)&&(imm<128))
                    i->emit(0x8d, 0x40|(i->cmd.dest-1)<<3|(i->cmd.src1-1), (char)imm);
                else
                    i->emit114(0x8d, 0x80|(i->cmd.dest-1)<<3|(i->cmd.src1-1), imm);
        } else if(i->cmd.dest==i->cmd.src1) // ADD dst, src2
            i->emit(0x03, 0xc0|(i->cmd.dest-1)<<3|(i->cmd.src2-1));
        else // LEA dst, [src1+src2]
            i->emit(0x8d, (i->cmd.dest-1)<<3|4, (i->cmd.src1-1)<<3|(i->cmd.src2-1));
        break;
    case command::sub: // ...
    case command::mul: // ...
    case command::div:
        if(i->known.count(i->cmd.src2) && i->known[i->cmd.src2]==2) {
            if(i->cmd.dest!=i->cmd.src1) // MOV dst, src1 / SAR dst, 1
                i->emit(0x8b, 0xc0|(i->cmd.dest-1)<<3|(i->cmd.src1-1));
            i->emit(0xd1, 0xf8|(i->cmd.dest-1));
            break;
        }
        // для деления нужны именно EAX,EDX;
        // сохраним их текущее содержимое в EDI,ESI, а после деления восстановим
        // MOV EDI, EAX / MOV EAX, src1 / MOV ESI, EDX / XOR EDX, EDX
        // IDIV src2 / XCHG dst, EAX / MOV EDX, ESI / MOV EAX, EDI
        saveAX = i->onexitp.count((physreg)1) && (i->cmd.dest!=1);
        saveDX = i->onexitp.count((physreg)3) && (i->cmd.dest!=3);
        if(saveAX || (i->cmd.src2==1)) i->emit(0x8b, 0xf8);
        if(i->cmd.src1!=1)
            if(i->known.count(i->cmd.src1))
                i->emit14(0xb8, i->known[i->cmd.src1]);
            else
                i->emit(0x8b, 0xc0|(i->cmd.src1-1));
        if(saveDX || (i->cmd.src2==3)) i->emit(0x8b, 0xf2);
        i->emit(0x33, 0xd2);
        if(i->cmd.src2==1) // переехал в EDI
            i->emit(0xf7, 0xff);
        else if(i->cmd.src2==3) // переехал ESI
            i->emit(0xf7, 0xfe);
        else
            i->emit(0xf7, 0xf8|(i->cmd.src2-1));
        if(i->cmd.dest!=1)
            i->emit(0x90|(i->cmd.dest-1));
        if(saveDX) i->emit(0x8b, 0xd6);
        if(saveAX) i->emit(0x8b, 0xc7);
        break;
    case command::eq: case command::ne: case command::ge:
    case command::le: case command::gt: case command::lt:
        // максимум 1 операнд известен; поставим его в src2
        if(i->known.count(i->cmd.src1)) {
            std::swap(i->cmd.src1, i->cmd.src2);
            switch(i->cmd.opcode) {
            case command::ge: i->cmd.opcode = command::le; break;
            case command::le: i->cmd.opcode = command::ge; break;
            case command::gt: i->cmd.opcode = command::lt; break;
            case command::lt: i->cmd.opcode = command::gt; break;
            }
        }
        // CMP src1, src2 / SETcc dstL / MOVZX dst, dstL
        if(i->known.count(i->cmd.src2)) {
            imm = i->known[i->cmd.src2];
            if((imm>=-128)&&(imm<128))
                i->emit(0x83, 0xf8|(i->cmd.src1-1), (char)imm);
            else if(i->cmd.src1==1)
                i->emit14(0x3d, imm);
            else
                i->emit114(0x81, 0xf8|(i->cmd.src1-1), imm);
        } else
            i->emit(0x3b, 0xc0|(i->cmd.src1-1)<<3|(i->cmd.src2-1));
        i->emit(0x0f);
        switch(i->cmd.opcode) {
        case command::eq: i->emit(0x94); break;
        case command::ne: i->emit(0x95); break;
        // ...
        }
        i->emit(0xc0|(i->cmd.dest-1));
        i->emit(0x0f, 0xb6, 0xc0|(i->cmd.dest-1)<<3|(i->cmd.dest-1));
        break;
    default:
        yyerror("unknown opcode");
    }
}
// после всего кода -- строки
int offset = code.size();
std::vector<int> offsets(strings.size()+1);
foreach(i, strings) {
    offsets[i->second] = offset;
    offset += i->first.length();
    offset++;
}
// второй проход: подставляем смещения
foreach(i, pcode) if(i->needfixat)
    if(i->cmd.opcode==command::jz)
        i->fix4(i->tgt->offset-(i->offset+i->length));
    else if (i->cmd.opcode==command::echo)
        i->fix4(offsets[i->cmd.imm]);

write(1, &*code.begin(), code.size()); // вывод кода
foreach(i, strings)                     // вывод строк
    write(1, i->first.c_str(), i->first.length()+1);


Что получилось?


Компилируем компилятор, компилируем им тестовую программу, компилируем загрузчик, и запускаем:

[tyomitch@home ~]$ bison jsk.y
[tyomitch@home ~]$ c++ jsk.tab.c lex.yy.c -o jskc
[tyomitch@home ~]$ ./jskc < test.jsk > code
[tyomitch@home ~]$ cc jskld.c -o jskld
[tyomitch@home ~]$ ./jskld code
Задумай число от 0 до 1000, а я буду угадывать
Это 500? (1=меньше, 2=больше, 3=попал) 1
Это 249? (1=меньше, 2=больше, 3=попал) 3
Ура! Я молодец!

Интересно, во что наша программа скомпилировалась?
00: 55                 push rbp
01: 48 8b ef           mov rbp,rdi
04: 53                 push rbx
05: 33 c0              xor eax,eax
07: b9 e8 03 00 00     mov ecx,0x3e8
0c: 50                 push rax
0d: 51                 push rcx
0e: bf 80 01 00 00     mov edi,0x180
13: ff 55 10           call qword ptr [rbp+0x10]
16: 59                 pop rcx
17: 58                 pop rax
18: 50                 push rax
19: 51                 push rcx
1a: bf 00 00 00 00     mov edi,0x0
1f: ff 55 08           call qword ptr [rbp+0x8]
22: 59                 pop rcx
23: 58                 pop rax
24: 50                 push rax
25: 51                 push rcx
26: bf fa 00 00 00     mov edi,0xfa
2b: ff 55 10           call qword ptr [rbp+0x10]
2e: 59                 pop rcx
2f: 58                 pop rax
30: 50                 push rax
31: 51                 push rcx
32: bf e8 03 00 00     mov edi,0x3e8
37: ff 55 08           call qword ptr [rbp+0x8]
3a: 59                 pop rcx
3b: 58                 pop rax
3c: 50                 push rax
3d: 51                 push rcx
3e: bf 01 01 00 00     mov edi,0x101
43: ff 55 10           call qword ptr [rbp+0x10]
46: 59                 pop rcx
47: 58                 pop rax
48: 3b c1              cmp eax,ecx
4a: 0f 9e c2           setle dl
4d: 0f b6 d2           movzx edx,dl
50: 0b d2              or edx,edx
52: 0f 84 97 00 00 00  je 0xef
58: 8d 14 01           lea edx,[rcx+rax*1]
5b: d1 fa              sar edx,1
5d: 50                 push rax
5e: 51                 push rcx
5f: 52                 push rdx
60: bf bc 01 00 00     mov edi,0x1bc
65: ff 55 10           call qword ptr [rbp+0x10]
68: 5a                 pop rdx
69: 59                 pop rcx
6a: 58                 pop rax
6b: 50                 push rax
6c: 51                 push rcx
6d: 52                 push rdx
6e: 8b fa              mov edi,edx
70: ff 55 08           call qword ptr [rbp+0x8]
73: 5a                 pop rdx
74: 59                 pop rcx
75: 58                 pop rax
76: 50                 push rax
77: 51                 push rcx
78: 52                 push rdx
79: bf 26 01 00 00     mov edi,0x126
7e: ff 55 10           call qword ptr [rbp+0x10]
81: 5a                 pop rdx
82: 59                 pop rcx
83: 58                 pop rax
84: 50                 push rax
85: 51                 push rcx
86: 52                 push rdx
87: ff 55 00           call qword ptr [rbp+0x0]
8a: 93                 xchg ebx,eax
8b: 5a                 pop rdx
8c: 59                 pop rcx
8d: 58                 pop rax
8e: 89 45 18           mov dword ptr [rbp+0x18],eax
91: 83 fb 01           cmp ebx,0x1
94: 0f 94 c0           sete al
97: 0f b6 c0           movzx eax,al
9a: 0b c0              or eax,eax
9c: 0f 84 08 00 00 00  je 0xaa
a2: 8b 45 18           mov eax,dword ptr [rbp+0x18]
a5: 8d 4a ff           lea ecx,[rdx-0x1]
a8: eb 9e              jmp 0x48
aa: 83 fb 02           cmp ebx,0x2
ad: 0f 94 c0           sete al
b0: 0f b6 c0           movzx eax,al
b3: 0b c0              or eax,eax
b5: 0f 84 04 00 00 00  je 0xbf
bb: 03 c2              add eax,edx
bd: eb 89              jmp 0x48
bf: 8b 45 18           mov eax,dword ptr [rbp+0x18]
c2: 83 fb 03           cmp ebx,0x3
c5: 0f 94 c2           sete dl
c8: 0f b6 d2           movzx edx,dl
cb: 0b d2              or edx,edx
cd: 0f 84 0b 00 00 00  je 0xde
d3: bf a0 01 00 00     mov edi,0x1a0
d8: ff 55 10           call qword ptr [rbp+0x10]
db: 5b                 pop rbx
dc: 5d                 pop rbp
dd: c3                 ret
de: 50                 push rax
df: 51                 push rcx
e0: bf c4 01 00 00     mov edi,0x1c4
e5: ff 55 10           call qword ptr [rbp+0x10]
e8: 59                 pop rcx
e9: 58                 pop rax
ea: e9 59 ff ff ff     jmp 0x48
ef: bf 59 01 00 00     mov edi,0x159
f4: ff 55 10           call qword ptr [rbp+0x10]
f7: 5b                 pop rbx
f8: 5d                 pop rbp
f9: c3                 ret

Невооружённым глазом видно, как минимум, две актуальные оптимизации, которым нужен «глазок» больше одной команды:
  • когда команда заканчивается на POP reg, а следующая начинается на PUSH reg, обе эти инструкции можно удалить;
  • комбинацию SETcc regl / MOVZX reg, regl / OR reg, reg / JZ offset можно заменить на одну инструкцию JNcc offset, если reg не жив на выходе JZ.
Этой «оптимизацией в большой глазок» и займёмся в следующий раз. Более важная оставшаяся задача — генерировать настоящий ELF, чтоб не нужен был самодельный загрузчик.
Tags:
Hubs:
Total votes 53: ↑52 and ↓1 +51
Views 5.1K
Comments Comments 10