17

有了汇编指令和 C 程序的一些背景知识,我可以想象编译函数的样子,但有趣的是,我从来没有仔细考虑过编译后的 C++ 类的样子。

bash$ cat class.cpp
#include<iostream>
class Base
{
  int i;
  float f;
};

bash$ g++ -c class.cpp

我跑了:

bash$objdump -d class.o
bash$readelf -a class.o

但我得到的东西让我很难理解。

有人可以向我解释或建议一些好的起点。

4

6 回答 6

25

这些类(或多或少)构造为常规结构。这些方法(或多或少......)转换为第一个参数是“this”的函数。对类变量的引用是作为“this”的偏移量完成的。

至于继承,让我们引用 C++ FAQ LITE,它反映在这里http://www.parashift.com/c++-faq-lite/virtual-functions.html#faq-20.4。本章展示了如何在真实硬件中调用虚函数(编译在机器代码中做什么。


让我们举个例子。假设 Base 类有 5 个虚函数:virt0()通过virt4().

 // Your original C++ source code
 class Base {
 public:
   virtual arbitrary_return_type virt0(...arbitrary params...);
   virtual arbitrary_return_type virt1(...arbitrary params...);
   virtual arbitrary_return_type virt2(...arbitrary params...);
   virtual arbitrary_return_type virt3(...arbitrary params...);
   virtual arbitrary_return_type virt4(...arbitrary params...);
   ...
 };

步骤#1:编译器构建一个包含 5 个函数指针的静态表,将该表埋入静态内存中的某处。许多(不是全部)编译器在编译定义 Base 的第一个非内联虚函数的 .cpp 时定义此表。我们称该表为 v-table;让我们假设它的技术名称是Base::__vtable. 如果一个函数指针适合目标硬件平台上的一个机器字,Base::__vtable最终将消耗 5 个隐藏字的内存。不是每个实例 5 个,不是每个函数 5 个;只是 5. 它可能看起来像下面的伪代码:

 // Pseudo-code (not C++, not C) for a static table defined within file Base.cpp

 // Pretend FunctionPtr is a generic pointer to a generic member function
 // (Remember: this is pseudo-code, not C++ code)
 FunctionPtr Base::__vtable[5] = {
   &Base::virt0, &Base::virt1, &Base::virt2, &Base::virt3, &Base::virt4
 };

第 2 步:编译器向 Base 类的每个对象添加一个隐藏指针(通常也是一个机器字)。这称为 v 指针。将此隐藏指针视为隐藏数据成员,就好像编译器将您的类重写为如下所示:

 // Your original C++ source code
 class Base {
 public:
   ...
   FunctionPtr* __vptr;  ← supplied by the compiler, hidden from the programmer
   ...
 };

步骤#3:编译器this->__vptr在每个构造函数中初始化。这个想法是让每个对象的 v-pointer 指向其类的 v-table,就好像它在每个构造函数的 init-list 中添加了以下指令:

 Base::Base(...arbitrary params...)
   : __vptr(&Base::__vtable[0])  ← supplied by the compiler, hidden from the programmer
   ...
 {
   ...
 }

现在让我们设计一个派生类。假设您的 C++ 代码定义了继承自类 Base 的类 Der。编译器重复步骤#1 和#3(但不是#2)。在第 1 步中,编译器创建一个隐藏的 v-table,保留与 in 相同的函数指针,Base::__vtable但替换与覆盖对应的那些插槽。例如,如果 Der 按原样覆盖virt0()virt2()继承其他的,则 Der 的 v-table 可能看起来像这样(假设 Der 没有添加任何新的虚拟):

 // Pseudo-code (not C++, not C) for a static table defined within file Der.cpp

 // Pretend FunctionPtr is a generic pointer to a generic member function
 // (Remember: this is pseudo-code, not C++ code)
 FunctionPtr Der::__vtable[5] = {
   &Der::virt0, &Der::virt1, &Der::virt2, &Base::virt3, &Base::virt4
 };                                        ^^^^----------^^^^---inherited as-is

在第 3 步中,编译器在 Der 的每个构造函数的开头添加了一个类似的指针赋值。这个想法是改变每个 Der 对象的 v 指针,使其指向其类的 v 表。(这不是第二个 v 指针;它与基类 Base 中定义的 v 指针相同;请记住,编译器不会在 Der 类中重复步骤 #2。)

最后,让我们看看编译器如何实现对虚函数的调用。您的代码可能如下所示:

 // Your original C++ code
 void mycode(Base* p)
 {
   p->virt3();
 }

编译器不知道这是否会调用Base::virt3(),或者Der::virt3()可能是virt3()另一个甚至还不存在的派生类的方法。它只确定您正在调用virt3()的函数恰好是 v-table 的 slot #3 中的函数。它将调用重写为如下内容:

 // Pseudo-code that the compiler generates from your C++

 void mycode(Base* p)
 {
   p->__vptr[3](p);
 } 

我强烈建议每位 C++ 开发人员阅读常见问题解答。这可能需要几个星期(因为它很难阅读而且很长),但它会教你很多关于 C++ 的知识以及可以用它做什么。

于 2010-07-09T09:33:01.270 回答
2

ok. there is nothing special with compiled classes. compiled classes even does not exists. what exist is objects wich are flat chunk of memory with possible paddings between fields? and standalone member functions somewhere in code which take pointer to an object as first parameter.

so object of class Base should be something

(*base_address) : i (*base_address + sizeof(int)) : f

it is possible to have paddings between fields? but that is hardware specific. based on processors memory model.

also... in debug version it is possible to catch class description in debug symbols. but that is compiler specific. you should search for a program which dumps debug symbols for your compiler.

于 2010-07-09T09:20:31.100 回答
2

“编译的类”是指“编译的方法”。

方法是带有额外参数的普通函数,通常放在寄存器中(我相信大多数情况下是 %ecx,对于大多数必须使用 __thiscall 约定生成 COM 对象的 Windows 编译器来说,这至少是正确的)。

因此,C++ 类与一堆普通函数并没有太大的不同,除了名称修改和构造函数/析构函数中用于设置 vtable 的一些魔法。

于 2010-07-09T09:24:48.597 回答
1

与读取 C 对象文件的主要区别在于 C++ 方法名称是mangled。您可以尝试将选项-C|--demangleobjdump.

于 2010-07-09T09:49:05.637 回答
0

就像一个 C 结构和一组带有附加参数的函数,该参数是指向该结构的指针。

遵循编译器所做的最简单的方法可能是在没有优化的情况下构建,然后将代码加载到调试器中,并以混合源/汇编器模式逐步执行。

但是,编译器的重点是您不需要知道这些东西(除非您正在编写编译器)。

于 2010-07-09T09:55:29.303 回答
0

Try the

g++ -S class.cpp

That will give you an assembly file 'class.s' (text file) which you can read with a text editor. However, your code doesn't do anything (declaring a class doesn't generate code on its own) so you won't have much in the assembly file.

于 2010-07-09T09:19:19.607 回答