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.

上玩类似的示例