Как asmjit получает перемещение кода при использовании AsmParser(&a).parse
Я использую asmjit в своем коде на С++ и определил функцию, как показано ниже:
// parse asm_str to byte code, return the length of byte code
int assemble(bool isx64, unsigned long long addr, const char* asm_str, char buffer[MAX_INSTRUCTION_LENGTH])
{
// for test, I modified param's value
isx64 = true;
addr = 0x6a9ec0;
asm_str = "call 0x00007FFF1CF8CEE0";
auto arch = isx64 ? Arch::kX64 : Arch::kX86;
// Initialize Environment with the requested architecture.
Environment environment;
environment.setArch(arch);
// Initialize CodeHolder.
CodeHolder code;
Error err = code.init(environment, addr);
if (err) {
dbg_print_err("code.init failed, reason:%s", DebugUtils::errorAsString(err));
return 0;
}
x86::Assembler a(&code);
err = AsmParser(&a).parse(asm_str, strlen(asm_str));
if (err) {
dbg_print_err("AsmParser(&a).parse failed, asm_str=\"%s\" addr=0x%llx reason:%s", asm_str, addr, DebugUtils::errorAsString(err));
return 0;
}
else {
CodeBuffer& buf = code.sectionById(0)->buffer();
memcpy(buffer, buf.data(), buf.size());
print_byte_hex(buffer, buf.size());
return (int)buf.size();
}
}
Когда я запускаю эту функцию и получаю результат буфера,40 E8 00 00 00 00
и не найти ни одной ошибки. На самом деле я знал, что эта инструкция не может скомпилироваться в байт-код в addr(0x6a9ec0
). Итак, я хочу знать, как определить, успешно ли компилируются такие инструкции в коде.
Как определить, составлены ли такие инструкции с ошибками в байт-коде.
У меня есть еще такие вопросы:
После введения @Petr я сгенерировал следующий байт-код инструкции:
FF 15 02 00 00 00 CC CC E0 CE F8 1C FF 7F 00 00
Результат дизассемблирования этого байт-кода по этому адресу с помощью X64dbg выглядит следующим образом:
00006a9ec0| FF15 02000000 | call qword ptr ds:[0x6a9ec8]
00006a9ec6| cc | int 3
00006a9ec7| cc | int 3
00006a9ec8| E0CEF81CFF7F0000 | dq 7FFF1CF8CEE0
Однако это все равно не решает проблему, поскольку после того, как инструкция завершит этот вызов, байт-код по адресу 0x6a9ec6 будет считаться инструкцией для выполнения, что явно не соответствует логике программы.
После поиска соответствующей информации я обнаружил, что использование байт-кода непосредственно для кодирования позволяет получить правильную логику. Конкретный метод заключается в преобразовании этой инструкции вызова в следующие инструкции:
7FFB694C0960 | FF15 04000000 | call qword ptr ds:[7FFB694C096a]
7FFB694C0966 | EB 0a | jmp 7FFB694C0972
7FFB694C0968 | 48 a1 0102030405060708 | mov rax, storage address for calls
7FFB694C0972 | 90 | nop
У новичка еще есть много подобных проблем, которые необходимо решить. Все эти проблемы возникают после перемещения 64-битной инструкции на другой адрес, например:
lea rax, ds:[rip+0x9DCAA]
mov rax,ds:[rip+0x100]
Near jump within a function
Far jump
Loop instruction
...
Это принесло много трудностей в моё обучение ассемблеру. Мое текущее решение — заменить эти инструкции соответствующим образом, не меняя исходную логику.
Например:
0x00007fff1f618d5f cmp byte ptr ds:[rip+0x1637C6], 0x0
будет заменено на:
push rax
mov rax,0x7fff1f77c52c
cmp byte ptr ds:[rax], 0x0
pop rax
Примечание. Здесь введите rip+0x1637C6=0x7fff1f77c52c.
Я не знаю, есть ли какие-либо побочные эффекты при этом, и есть ли лучшее решение при использовании мощного ASmJIT?
1 ответ
Данные, хранящиеся в CodeHolder AsmJit, не являются окончательным машинным кодом, пока вы не вызоветеcode.flatten()
иcode.relocateTo(...)
- это машинный код без примененных к нему релокаций - поэтому вы видите начальный40 E8 .. .. .. ..
последовательность — префикс REX добавляется AsmJit, чтобы можно было переписать вызов во что-то вродеcall [rip + offset]
позже.
Чтобы иметь возможность сделать это, AsmJit должен вставить новый раздел под названием - этот раздел будет содержать абсолютные адреса целей перехода и вызова, которые находятся за пределами 32-битного диапазона смещения. По сути, это удобная функция, которая позволяет пользователям комфортно использовать эти две инструкции без необходимости проверять, будут ли они доступны, что может быть сложно, если вы используете JIT-распределители, которым необходимо знать размер выделяемого вами кода.
Вы можете получить доступ к записям перемещения,code.relocEntries()
получатель, или просто позвониcode.hasRelocEntries()
чтобы быстро проверить, есть ли они.
Добавлю немного подробнее о том, как это работает в данном случае. Когдаcall 0x00007FFF1CF8CEE0
встречается, AsmJit вставляет машинный код в буфер (нулевые байты теперь являются заполнителем, не более того) и добавляетRelocEntry
кCodeHolder
. Кроме того, он также вызываетCodeHolder::addAddressToAddressTable()
, который вставит туда абсолютный адрес вызова. Раздел создается лениво, его бы не существовало, если бы он не требовался.
Это означает, что CodeHolder теперь будет иметь два раздела:.text
и - тогда в конце генерации машинного кода эти разделы должны быть сглажены - либо с помощью asmjitCodeHolder::flatten()
или пользователем, если для выравнивания разделов требуется более сложная логика.
Затем, после выравнивания кода, каждый раздел будет иметь свое собственное смещение, поэтому теперь разделы можно рассматривать как промежутки в буфере, который может содержать их все. Это позволяет, наконец, позвонитьCodeHolder::relocateTo()
, который будет перебирать все записи RelocEntry и применять их.
В нашем случае, поскольку буфер .text содержит40 E8 00 00 00 00
байт, таким образом, он имеет размер 6 байт, смещение раздела будет равно 8 (он выровнен по 8 байтам), а инструкция вызова будет исправлена до чего-то вроде - что указывает на первую запись .
Таким образом, окончательный буфер, содержащий плоский код, может выглядеть так:
-
FF 15 02 00 00 00
(call [rip + 2]
) -
.. ..
(2 фиктивных байта) -
E0 CE F8 1C FF 7F 00 00
(абсолютный адрес, на который указывает вызов).
Обновление ответа:
Если вы знаете, что адрес вызова находится вне диапазона, и вы не хотите использовать дополнительный регистр для хранения адреса, вы можете просто использовать AsmJit для кодирования последовательности, которая будет работать, не полагаясь на.addrtab
- что-то вроде этого должно работать из коробки:
using namespace asmjit;
void encodeCall(x86::Assembler& assembler, uint64_t address) {
Label a = assembler.newLabel();
Label b = assembler.newLabel();
assembler.call(x86::ptr(a)); // Would be encoded as [rip+offset].
assembler.jmp(b);
assembler.bind(a);
assembler.dq(address);
assembler.bind(b);
}
В противном случае я рекомендую всегда иметь адрес в реестре — это самый простой способ, который работает лучше всего:
using namespace asmjit;
void encodeCall(x86::Assembler& assembler, x86::Gp tmpReg, uint64_t address) {
assembler.mov(tmpReg, address);
assembler.call(tmpReg);
}