llvm如何知道一个成员函数指针是否指向了一个虚函数?
How does llvm know whether a member function pointer pointed to a virtual function?
写了一些关于成员函数指针的代码,看了Itanium ABI#Member function pointers,明白了llvm中成员函数指针的布局
但是让我印象深刻的是如何使用成员函数指针获取函数的地址。我没有办法判断成员函数指针 mfptr 指向虚成员函数还是普通成员函数。
我不确定这就是 llvm 所做的,但我发现 Itanium ABI 的普通成员函数和虚函数指针之间的区别通常在于记录本身的结构,就像解释的那样 here:
The form of a virtual function pointer is specified by the
processor-specific C++ ABI for the implementation. In the specific
case of 64-bit Itanium shared library builds, a virtual function
pointer entry contains a pair of components (each 64 bits): the value
of the target GP value and the actual function address. That is,
rather than being a normal function pointer, which points to such a
two-component descriptor, a virtual function pointer entry is the
descriptor.
即普通函数指针是一个地址,而虚函数指针是由全局位置偏移量(GP)和虚函数覆盖的有效地址组成的描述符。现在我想记录的大小和某种 decoration(如果认为它是你指出的 link 中提到的'1')使得有可能将一种类型的指针与另一种类型的指针区分开来。
编辑
我发现了另一个关于 vtable 记录定义的提示 w.r.t。在 llvm 的 clang 前端的 TargetCXXABI
class 实现中的虚函数成员。这个 class 公开了一个 API(第 181 行),它告诉成员函数的主体是否对齐。
正如 PaulR 在他的回答中已经指出的那样,这证实了 LSB 用于区分普通成员函数和虚拟成员函数这一事实。但这不是因为指针大小和对齐方式——最小可寻址单元始终是一个字节,因此指针可以是奇数——而是因为在 Itanium C++ ABI 中,普通成员函数的主体是对齐的,因此它们的地址始终是偶数数字设计。
尽管情况并非总是如此,实际上在该方法的实现中提到了这样一个事实,即某些体系结构(例如 ARM)将鉴别器存储在 this
指针的调整中,而不是在函数地址中。
所以这个功能确实与处理器架构相关,除了 x86_64 的 LSB +1 的一般规则之外,您应该检查每个的 Itanium-like C++ ABI。
在您的文档中 linked 它说:
For a virtual function, it is 1 plus the virtual table offset (in
bytes) of the function
在Virtual Table Layout下,它说:
offsets within the virtual table are determined by that allocation sequence and the natural ABI size and alignment
偏移量必须遵守函数指针的对齐要求。
"POD" 类型的函数指针的对齐要求在相应的 C ABI 中指定。我假设指针与其大小对齐,因此指针的地址(以及偏移量)必须是偶数,并且其最低有效位必须为零。
所以实现可以只检查 offset/pointer 字段的 LSB 并知道当且仅当 LSB 是 1 时它正在处理虚拟方法。
一旦它在虚拟 table 中有了偏移量,它就会从对象中读取虚拟 table 指针,并使用来自的偏移量从虚拟 table 加载函数的实际地址成员指针。
class C {
virtual int someMethod();
};
int invokeAMethod(C *c, int (C::*method)()) {
return (c->*method)();
}
On x86_64 clang 确实为方法指针的 "ptr" 成员的 LSB 创建了一个检查:
invokeAMethod(C*, int (C::*)()): # @invokeAMethod(C*, int (C::*)())
// c is in rdi, method.adj is in rdx, and method.ptr is in rdx
// adjust this pointer
add rdi, rdx
// check whether method is virtual
test sil, 1
// if it is not, skip the following
je .LBB0_2
// load the vtable pointer from the object
mov rax, qword ptr [rdi]
// index into the vtable with the corrected offset to load actual method address
mov rsi, qword ptr [rax + rsi - 1]
.LBB0_2:
// here the actual address of the method is in rsi, we call it
// in this particular case we return the same type
// and do not need to call any destructors
// so we can tail call
jmp rsi # TAILCALL
我无法分享这个特定示例的神栓 link,因为我的浏览器插件之一受到干扰,但您可以自己在 https://gcc.godbolt.org.
上玩类似的示例
写了一些关于成员函数指针的代码,看了Itanium ABI#Member function pointers,明白了llvm中成员函数指针的布局
但是让我印象深刻的是如何使用成员函数指针获取函数的地址。我没有办法判断成员函数指针 mfptr 指向虚成员函数还是普通成员函数。
我不确定这就是 llvm 所做的,但我发现 Itanium ABI 的普通成员函数和虚函数指针之间的区别通常在于记录本身的结构,就像解释的那样 here:
The form of a virtual function pointer is specified by the processor-specific C++ ABI for the implementation. In the specific case of 64-bit Itanium shared library builds, a virtual function pointer entry contains a pair of components (each 64 bits): the value of the target GP value and the actual function address. That is, rather than being a normal function pointer, which points to such a two-component descriptor, a virtual function pointer entry is the descriptor.
即普通函数指针是一个地址,而虚函数指针是由全局位置偏移量(GP)和虚函数覆盖的有效地址组成的描述符。现在我想记录的大小和某种 decoration(如果认为它是你指出的 link 中提到的'1')使得有可能将一种类型的指针与另一种类型的指针区分开来。
编辑
我发现了另一个关于 vtable 记录定义的提示 w.r.t。在 llvm 的 clang 前端的 TargetCXXABI
class 实现中的虚函数成员。这个 class 公开了一个 API(第 181 行),它告诉成员函数的主体是否对齐。
正如 PaulR 在他的回答中已经指出的那样,这证实了 LSB 用于区分普通成员函数和虚拟成员函数这一事实。但这不是因为指针大小和对齐方式——最小可寻址单元始终是一个字节,因此指针可以是奇数——而是因为在 Itanium C++ ABI 中,普通成员函数的主体是对齐的,因此它们的地址始终是偶数数字设计。
尽管情况并非总是如此,实际上在该方法的实现中提到了这样一个事实,即某些体系结构(例如 ARM)将鉴别器存储在 this
指针的调整中,而不是在函数地址中。
所以这个功能确实与处理器架构相关,除了 x86_64 的 LSB +1 的一般规则之外,您应该检查每个的 Itanium-like C++ ABI。
在您的文档中 linked 它说:
For a virtual function, it is 1 plus the virtual table offset (in bytes) of the function
在Virtual Table Layout下,它说:
offsets within the virtual table are determined by that allocation sequence and the natural ABI size and alignment
偏移量必须遵守函数指针的对齐要求。
"POD" 类型的函数指针的对齐要求在相应的 C ABI 中指定。我假设指针与其大小对齐,因此指针的地址(以及偏移量)必须是偶数,并且其最低有效位必须为零。
所以实现可以只检查 offset/pointer 字段的 LSB 并知道当且仅当 LSB 是 1 时它正在处理虚拟方法。
一旦它在虚拟 table 中有了偏移量,它就会从对象中读取虚拟 table 指针,并使用来自的偏移量从虚拟 table 加载函数的实际地址成员指针。
class C {
virtual int someMethod();
};
int invokeAMethod(C *c, int (C::*method)()) {
return (c->*method)();
}
On x86_64 clang 确实为方法指针的 "ptr" 成员的 LSB 创建了一个检查:
invokeAMethod(C*, int (C::*)()): # @invokeAMethod(C*, int (C::*)())
// c is in rdi, method.adj is in rdx, and method.ptr is in rdx
// adjust this pointer
add rdi, rdx
// check whether method is virtual
test sil, 1
// if it is not, skip the following
je .LBB0_2
// load the vtable pointer from the object
mov rax, qword ptr [rdi]
// index into the vtable with the corrected offset to load actual method address
mov rsi, qword ptr [rax + rsi - 1]
.LBB0_2:
// here the actual address of the method is in rsi, we call it
// in this particular case we return the same type
// and do not need to call any destructors
// so we can tail call
jmp rsi # TAILCALL
我无法分享这个特定示例的神栓 link,因为我的浏览器插件之一受到干扰,但您可以自己在 https://gcc.godbolt.org.
上玩类似的示例