Напоминаю, что мы пишем компилятор для игрушечного языка джей-скрип. Начали с компиляции в п-код, потратили немало сил на его оптимизацию, и приготовились к заключительному этапу компиляции — к выводу машинно-зависимого выполнимого кода.
Никаких замысловатых алгоритмов тут уже нет: по большому счёту, только замена одной системы команд на другую.
Рискну предположить, что вы читаете этот пост на компьютере, внутри которого процессор x86 или x64. Перед пользователями мобильных устройств извиняюсь: генерировать код для модного гаджета было бы занимательно, но мне слишком сложно раздобыть такой экземплярчик для экспериментов.
Постараемся генерировать «кроссплатформенный» код, одинаково работоспособный и на x86, и на x64. Сосредоточимся именно на машинном коде, а не на внутренностях формата ELF и взаимодействии с загрузчиком; поэтому будем создавать код «сплошным куском», аналогично досовскому формату COM. Читать его с диска и запускать будет наш собственный загрузчик.
Пользоваться будем только 32-битными регистрами, так что одна и та же программа, даже если в ней при вычислениях происходят переполнения, будет давать одинаковые результаты и на x64, и на x86.
Для связи со внешним миром (команды
Наш загрузчик создаст для загруженной программы «область связи» — массив указателей на «стандартные функции», и передаст адрес этого массива параметром в программу. Стандартных функций у нас три: ввод числа, вывод числа, вывод строки; поэтому область связи будет состоять из трёх указателей. Для удобства адресации, сразу же на входе в программу сохраним переданный адрес в
Другой интересный момент — доступ к памяти (к вылитым переменным). Вместо типичной для x86 абсолютной адресации в x64 ввели «перемещаемую абсолютную», относительно
Не мудрствуя лукаво, будем адресовать ячейки вылитых переменных относительно того же
Сгенерированный п-код использует четыре «абстрактных физических регистра»
Способ передачи параметров в функцию на x86 и на x64 сильно отличается. На x86 параметры по умолчанию передаются через стек (cdecl), но можно попросить
На обоих процессорах вызываемая функция вправе портить содержимое
Какой код будем генерировать, разобрались; дело за малым — реализовать задуманное.
Берём за основу давнишний интерпретатор п-кода, оставляем всю шелуху (проверка ввода, отображение файла в память), а нутро (цикл выполнения и реализации команд) заменяем вызовом прочитанного кода, как будто бы это обычная сишная функция.
Заметьте, что исчез
Сейчас, когда п-код стал внутренним представлением, невидимым снаружи компилятора, — уже нет смысла сжимать каждую команду в 4 байта; значит, не нужны ни хитрые
Особенность набора инструкций x86/x64 — его разнообразие и неортогональность. Едва ли не любую операцию можно закодировать тремя-четырьмя разными способами. Постараемся для каждой п-команды выбирать самую компактную из возможных реализаций. (Это предельный случай «оптимизации сквозь глазок» — peephole optimization, занимающейся не программой в целом, а короткими блоками смежных команд. У нас вообще каждая команда обрабатывается независимо от окружающих.)
Например, п-команда
Некоторые локальные оптимизации, вроде замены
Весь генерируемый код будем хранить в одном большом векторе
В
Итак, на первом проходе по п-коду генерируем весь исполнимый код в векторе
Я постарался убрать из листинга самые неинтересные куски, чтоб хоть немного его сократить. Полный код компилятора выложен на tyomitch.net.ru/jsk.y.natv.html
Компилируем компилятор, компилируем им тестовую программу, компилируем загрузчик, и запускаем:
Интересно, во что наша программа скомпилировалась?
Невооружённым глазом видно, как минимум, две актуальные оптимизации, которым нужен «глазок» больше одной команды:
Никаких замысловатых алгоритмов тут уже нет: по большому счёту, только замена одной системы команд на другую.
Далее в посте:
- Выбор кода
- Загрузчик
- Изменения в п-коде
- Генерация
- Что получилось?
Выбор кода
Рискну предположить, что вы читаете этот пост на компьютере, внутри которого процессор 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
.