最近有一个老项目需要从x86下移植到x64环境下, 遇到了如下一个问题需要解决.
其中一个RPC模块通过client向server直接发送callable对象的指针的地址和其内部虚函数成员的index来调用服务(实际上, rpc里直接发送可执行代码地址非常tricky, 需要考虑各种安全问题, 这部分不在本文讨论范围内). 简化后的核心代码如下:
#include
#include
using namespace std;
class service_base
{
public:
service_base() :_m(0) {}
int _m;
virtual void fun1() { cout << _m << "service_base::fun1()" << '\n'; }
virtual void fun2() { cout << _m << "service_base::fun2()" << '\n'; }
};
class service_derived : public service_base
{
public:
service_derived() :_m(1) {}
int _m;
virtual void fun1() { cout << _m << "service_derived::fun1()" << '\n'; }
virtual void fun2() { cout << _m << "service_derived::fun2()" << '\n'; }
};
template <typename T>
void call(T *_ptr, size_t i)
{
typedef void(*FunPtr)();
size_t *vptr = *reinterpret_cast(_ptr);
FunPtr callable = reinterpret_cast(vptr[i]);
__asm {
mov ecx, DWORD PTR _ptr
}
callable();
}
service_base* base = new service_base();
service_derived* derv = new service_derived();
int main()
{
service_base* base = new service_base();
service_derived* derv = new service_derived();
call(base, 0);
call(base, 1);
call(derv, 0);
call(derv, 1);
system("pause");
}
以上代码工作在x86环境下(windows).
对于包含(及继承了)虚函数成员的类对象来说,其开头总是保存着一个4个字节的指针指向虚函数表,虚函数表又是一个简单的线性表其每个元素又都是一个4字节的指针指向对象自己实现的虚函数. C++程序员应该对此很熟悉. 除去其中的inline asm, 其余部分并不难理解.
call函数直接取虚表地址并且获取对应index的虚函数指针, 尝试直接调用.
这里不能忘记this指针, 调用成员函数时this指针是必须被指定的, 在win32 abi里这个this指针必须放在ECX寄存器中. 这个就是inline asm做的工作.
执行上述代码,输出
0service_base::fun1()
0service_base::fun2()
1service_derived::fun1()
1service_derived::fun2()
一切很美好, 但是当移植代码到x64的时候, 麻烦来了.
Visual Studio报告
error C4235: nonstandard extension used: '__asm' keyword not supported on this architecture
google后发现msvc x64下已经不支持inline asm.
使用替代的办法 ,下载masm for x64.
将inline asm抽出来单独写一个asm文件, 把 ECX改为RCX.
用masm编译成obj链接到C++代码中.
测试执行, 代码输出
```
11276778service_base::fun1()
11276778service_base::fun2()
0service_derived::fun1()
0service_derived::fun2()
明显出问题, this指针不正确.
反汇编看到RCX的确被设置为this. 那么应该是win64 abi改变了this指针的寄存器,
再写一个常规的C++对象调用的测试程序, 反汇编发现 this现在被放到了RDI中.
再次尝试把this放到RDI中, 执行代码, 运行正常.
为了多了解一下x64下abi的变化, 搜了一些资料
https://en.wikipedia.org/wiki/X86_calling_conventions
https://msdn.microsoft.com/en-us/library/9b372w95.aspx
https://software.intel.com/sites/default/files/m/d/4/1/d/8/Introduction_to_x64_Assembly.pdf
大概浏览一下后发现一个很有意思的事情, 在system v 64 abi里, RDI也是常规函数第一个int或者pointer参数. 那么上面的代码只需要一个小改动就可以省去必须使用汇编代码的麻烦了.
修改call函数如下:
void call(T *_ptr, size_t i)
{
typedef void(*FunPtr)(T *_ptr);
size_t *vptr = *reinterpret_cast(_ptr);
FunPtr callable = reinterpret_cast(vptr[i]);
/*
__asm {
mov ecx, DWORD PTR _ptr
}
*/
callable(_ptr);
}
把this指针作为call的第一个参数传入, 编译器会自动把this放入RDI.
编译到x64,运行测试, 结果正确.