FlushInstructionCache 在 MSDN 中解释说是 flush 指定进程的指令缓存。其声明如下所示:
BOOL WINAPI FlushInstructionCache( HANDLE hProcess, LPCVOID lpBaseAddress, SIZE_T dwSize );
MSDN 中说,如果应用程序生成或更改了内存中的代码那么就应该调用此函数。这是因为 CPU 并不能捕获到对内存所做的更改,从而可能会导致 CPU 继续执行其缓存的旧的代码而不是去执行新的代码。
MSDN 中也没有找到对此函数的用法示例,而且其解释让人看了好像明白又好像不太明白。在网上参考了别人的代码又经过自己的思考、测试后好像对此函数有了一点儿感悟。
以下观点纯属个人见解不保证一定正确:
1. 普通的程序编译好后其代码是固定不变的,CPU 只要读取、执行就行了,这时用不到 FlushInstructionCache 函数;
2. 有些特殊程序有可能会动态的生成或更改一些代码然后再让 CPU 去执行,比如软件的防破解技术以及 thunk 技术等。此时就要用 FlushInstructionCache 函数来通知 CPU 某段代码已被更改了;
3. CPU 一般对指令都有缓存,缓存中保存的是内存中某一小段数据的副本。既然有了缓存那就说明内存中的同一小段数据有可能被保存在了两个地方,这就有可能使这两个地方的数据不同步。比如使用了动态代码生成更改了内存某处位置的代码(此位置处的旧代码已经缓存在了 CPU 中),但 CPU 并不知道内存中相应位置的代码已经被替换掉了从而有可能会继续执行其缓存的旧的代码,这就导致了错误的发生;
4. 调用 FlushInstructionCache 后会导致 CPU 清除掉对指定内存位置的代码的缓冲,当需要执行该内存位置处的代码时 CPU 会重新从真实的内存中重新加载代码而不是去执行缓存中的代码,这就保证了 CPU 执行的是更改以后的新的代码。
下面是一个使用 thunk 技术动态生成代码的示例:
#include <windows.h> #include <iostream> // 按字节对齐,因为此结构中保存的是一段机器码 #pragma pack(push,1) struct Thunk{ // 以下三个字段用于存储机器码 BYTE m_moveeax; DWORD m_data; BYTE m_retn; // 初始化 Thunk。 // 如果参数 data = 10,则初始化结果为: // mov eax,10 // retn void Init(int data) { m_moveeax = 0xB8; // mov eax,data m_data = data; m_retn = 0xC3; // retn // flush 缓存 // 其实不调用这个API,程序也可正常执行,但既然微软说让调那咱就调吧 ::FlushInstructionCache(::GetCurrentProcess(),this,sizeof(Thunk)); } }; #pragma pack(pop) typedef int (*TEST)(); int main(){ // 在堆栈中生成以下两行代码: // mov eax,10 // retn Thunk thunk; thunk.Init(10); // thunk 中具有汇编指令 retn 所以可把 thunk 的地址当成一个函数的地址来执行之 TEST f = (TEST)&thunk; // 在汇编中,寄存器 EAX 保存的是函数的返回值 int r = f(); // 将显示:r = 10 std::cout << "r = " << r; }
如果你在执行以上代码时出错,说明你的机器有数据执行保护(无法执行堆栈中的代码),可更改数据执行保护设置,或者使用下面这个例子:
#include <windows.h> #include <iostream> typedef int (*TEST)(); // 按字节对齐,因为此结构中保存的是一段机器码 #pragma pack(push,1) struct Thunk{ // 以下三个字段用于存储机器码 BYTE m_moveeax; DWORD m_data; BYTE m_retn; // 初始化 Thunk。 // 如果参数 data = 10,则初始化结果为: // mov eax,10 // retn void Init(int data) { m_moveeax = 0xB8; // mov eax,data m_data = data; m_retn = 0xC3; // retn // flush 缓存 // 其实不调用这个API,程序也可正常执行,但既然微软说让调那咱就调吧 ::FlushInstructionCache(::GetCurrentProcess(),this,sizeof(Thunk)); } }; #pragma pack(pop) // 封装 Thunk 结构的创建,以解决操作系统的执行保护问题 class CThunk { public: Thunk* m_pThunk; // 如果具有执行保护功能,则从进程的虚地址空间分配一块可读可写可执行的内存 // 否则直接从默认堆中分配 CThunk() { if (IsProcessorFeaturePresent(PF_NX_ENABLED)) { m_pThunk = (Thunk*)::VirtualAlloc(NULL,sizeof(Thunk),MEM_RESERVE | MEM_COMMIT,PAGE_EXECUTE_READWRITE); } else { m_pThunk = new Thunk; } } BOOL Init(int data) { if (m_pThunk) { m_pThunk->Init(data); return TRUE; } return FALSE; } TEST GetAddress() { return (TEST)m_pThunk; } ~CThunk() { if (m_pThunk) { if (IsProcessorFeaturePresent(PF_NX_ENABLED)) { ::VirtualFree(m_pThunk,0,MEM_RELEASE); } else { delete m_pThunk; } m_pThunk = NULL; } } }; int main(){ // 在内存中生成以下两行代码: // mov eax,10 // retn CThunk thunk; thunk.Init(10); // 得到所生成的代码的地址 TEST f = thunk.GetAddress(); // 在汇编中,寄存器 EAX 保存的是函数的返回值 int r = f(); // 将显示:r = 10 std::cout << "r = " << r; }
第二个例子很好地解决了操作系统的数据执行保护问题。