如何将x64机器代码写入虚拟内存并在C++中为Windows执行它
我一直在想如何V8 JavaScript引擎和任何其他JIT编译器执行生成的代码。如何将x64机器代码写入虚拟内存并在C++中为Windows执行它
以下是我在尝试编写小型演示时阅读的文章。
- http://eli.thegreenplace.net/2013/11/05/how-to-jit-an-introduction
- http://nullprogram.com/blog/2015/03/19/
我只知道很少装配,所以我最初使用http://gcc.godbolt.org/编写一个函数,并得到分解输出,但是代码不工作在Windows上。
然后我写了一个小的C++代码,用-g -Og
进行编译,然后用gdb获得disassmbled输出。
#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库,而不是从头开始编写。
注:本·福格特在评论中指出,这是真的只适用于x86的,不是x86_64的。对于x86_64,你的程序集中只有一些错误(在x86中仍然存在错误),Ben Voigt在他的回答中也指出了这一点。
发生这种情况是因为编译器在生成程序集时可能会看到函数调用的两端。由于编译器控制着为调用者和被调用者生成代码,因此它不必遵循cdecl调用约定,也不需要遵循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
(DISCLAMER:我不是在组装大,这是由Visual Studio生成)
x86_64的默认调用约定确实使用寄存器来传递参数。我认为你错误地用x86完成了你的分析。 –
嗯,你说得对。我没有想到这一点。 –
我复制粘贴的输出(删除“%”)
嗯,这意味着你的第二个指令被
mov ecx, eax
这使得没有任何意义(它覆盖的结果与未初始化的返回值相乘)。
在另一方面
mov eax, foo
ret
是用于结束与非void
返回类型的功能的非常常见的图案。
你的两个汇编语言之间的差异(AT & T型VS英特尔的风格)是不仅仅是%
标记,the operand order is reversed和指针和偏移表示非常不同,以及更多。
你会想在gdb发出set disassembly-flavor intel
命令
好一点,编辑建议的标题。我希望我的问题对其他读者有所帮助,因为大多数在线JIT文章都是针对POSIX的。 –
请注意,这不是“堆内存”,你在任何堆外分配一个页面(这是最好的,这样你的'VirtualProtect'调用不会影响任何其他对象) –
显示为int生成的程序集ans = square(100);'调用函数指针的地方。 –