2017-01-26 4 views
2

Мне было интересно, как V8 JavaScript Engine и любые другие компиляторы JIT выполняют сгенерированный код.Как написать машинный код x64 в виртуальную память и выполнить его для Windows в C++

Вот статьи, которые я прочитал во время своей попытки написать небольшую демонстрационную версию.

Я только знаю очень мало о сборке, поэтому я изначально использовал http://gcc.godbolt.org/ написать функцию и получить разобранном выход, но код не работает на Windows.

Затем я написал небольшой код на C++, скомпилированный с -g -Og, а затем получите вывод данных с помощью gdb.

#include <stdio.h> 

int square(int num) { 
    return num * num; 
} 

int main() { 
    printf("%d\n", square(10)); 
    return 0; 
} 

Выход:

Dump of assembler code for function square(int): 
=> 0x00000000004015b0 <+0>:  imul %ecx,%ecx 
    0x00000000004015b3 <+3>:  mov %ecx,%eax 
    0x00000000004015b5 <+5>:  retq 

копировать-вставить выход ('%' удалены) для online x86 assembler и получить { 0x0F, 0xAF, 0xC9, 0x89, 0xC1, 0xC3 }.

Вот мой последний код. если я скомпилировал его с gcc, я всегда получаю 1. Если я скомпилировал его с VC++, я получаю случайное число. Что происходит?

#include <stdio.h> 
#include <string.h> 
#include <stdlib.h> 
#include <windows.h> 

typedef unsigned char byte; 
typedef int (*int0_int)(int); 

const byte square_code[] = { 
    0x0f, 0xaf, 0xc9, 
    0x89, 0xc1, 
    0xc3 
}; 

int main() { 
    byte* buf = reinterpret_cast<byte*>(VirtualAlloc(0, 1 << 8, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); 
    if (buf == nullptr) return 0; 
    memcpy(buf, square_code, sizeof(square_code)); 
    { 
     DWORD old; 
     VirtualProtect(buf, 1 << 8, PAGE_EXECUTE_READ, &old); 
    } 
    int0_int square = reinterpret_cast<int0_int>(buf); 
    int ans = square(100); 
    printf("%d\n", ans); 
    VirtualFree(buf, 0, MEM_RELEASE); 
    return 0; 
} 

Примечание

Я пытаюсь узнать, как JIT работает, поэтому, пожалуйста, не предложить мне использовать LLVM или любую библиотеку. Я обещаю, что буду использовать надлежащую библиотеку JIT в реальном проекте, а не писать с нуля.

+1

Хороший вопрос, редактировал название, как было предложено. Я хочу, чтобы мой вопрос был полезным для других читателей, так как большинство статей JIT, которые я могу найти, предназначены для POSIX. –

+0

Обратите внимание, что это не «в память кучи», вы выделяете страницу вне любой кучи (что лучше всего, так что ваш вызов «VirtualProtect» не влияет на какие-либо другие объекты) –

+0

Показать сборку, сгенерированную для 'int ans = square (100); 'где вызывается указатель функции. –

ответ

1

Примечание: поскольку Бен Фойгт указывает в комментариях, это действительно действует только для x86, а не x86_64. Для x86_64 у вас просто есть некоторые ошибки в вашей сборке (которые по-прежнему являются ошибками в x86), как отмечает Бен Вейгт в своем ответе.

Это происходит потому, что ваш компилятор мог видеть обе стороны вызова функции при создании сборки. Поскольку компилятор контролировал генерацию кода как для вызывающего, так и для вызываемого абонента, ему не приходилось следовать соглашению о вызове cdecl, и это не так.

Соглашение о вызове по умолчанию для MSVC - это cdecl. В основном, параметры функции помещаются в стек в обратном порядке они перечислены, поэтому вызов foo(10, 100) может привести к сборке:

push 100 
push 10 
call foo(int, int) 

В вашем случае, компилятор будет генерировать нечто вроде следующего на месте вызова:

push 100 
call esi ; assuming the address of your code is in the register esi 

Это не то, что ожидает ваш код. Ваш код ожидает, что его аргумент будет передан в регистр ecx, а не в стек.

Компилятор использовал то, что похоже на соглашение о вызове fastcall. Если я составляю подобную программу (я получаю немного другую сборку) я получить ожидаемый результат:

#include <stdio.h> 
#include <string.h> 
#include <stdlib.h> 
#include <windows.h> 

typedef unsigned char byte; 
typedef int (_fastcall *int0_int)(int); 

const byte square_code[] = { 
    0x8b, 0xc1, 
    0x0f, 0xaf, 0xc0, 
    0xc3 
}; 

int main() { 
    byte* buf = reinterpret_cast<byte*>(VirtualAlloc(0, 1 << 8, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); 
    if (buf == nullptr) return 0; 
    memcpy(buf, square_code, sizeof(square_code)); 
    { 
     DWORD old; 
     VirtualProtect(buf, 1 << 8, PAGE_EXECUTE_READ, &old); 
    } 
    int0_int square = reinterpret_cast<int0_int>(buf); 
    int ans = square(100); 
    printf("%d\n", ans); 
    VirtualFree(buf, 0, MEM_RELEASE); 
    return 0; 
} 

Обратите внимание, что я сказал компилятор использовать _fastcall соглашение о вызовах. Если вы хотите использовать cdecl, узел должен был бы выглядеть следующим образом:

push ebp 
mov ebp, esp 
mov eax, DWORD PTR _n$[ebp] 
imul eax, eax 
pop ebp 
ret 0 

(Юридическая информация: Я не большой при сборке, и который был сгенерирован с помощью Visual Studio)

+2

Соглашение о вызове по умолчанию для x86_64 использует регистры для передачи аргументов. Я думаю, что вы сделали свой анализ с x86 по ошибке. –

+0

Хм, ты прав. Я об этом не думал. –

2

копировать-вставить вывод ('%' удалены)

Ну, это означает, что ваша вторая инструкция была

mov ecx, eax 

, который не имеет никакого смысла (он переписывает результат умножения с неинициализированным возвратным значением).

С другой стороны

mov eax, foo 
ret 

очень общий шаблон для прекращения функции с не- типа void возврата.

Разница между вашими двумя языками сборки (AT & T стиля против стиля Intel) больше, чем просто % маркера, the operand order is reversed и указатели и смещения обозначаются очень по-разному, а также.

Вы хотите выдать команду set disassembly-flavor intel в БГД

Смежные вопросы