x86-64 汇编 —— C++ 特性在汇编层的体现


本文件连接 C++ 语言特性与其编译后的汇编实现,是理解 C++ 教程底层原理部分的汇编基础。

内存分段

编译后的程序在内存中分为若干段:

高地址
┌──────────┐
│   栈      │ ← 局部变量、函数调用帧(向低地址增长)
├──────────┤
│   ...    │
├──────────┤
│   堆      │ ← new/malloc 分配(向高地址增长)
├──────────┤
│  .bss    │ ← 未初始化全局/静态变量(零填充)
├──────────┤
│  .data   │ ← 已初始化全局/静态变量
├──────────┤
│  .rodata │ ← 只读数据(字符串常量、虚函数表)
├──────────┤
│  .text   │ ← 代码(机器指令)
└──────────┘
低地址

C++ 中各类内容的归属:

C++ 内容所属段说明
成员函数体.text所有对象共享同一份代码
虚函数体.text与普通函数相同
虚函数表(vtable).rodata只读,每类一张
静态成员变量.data / .bss类外存储,全局唯一
字符串字面量.rodata只读
局部非静态对象函数返回后自动销毁
new 分配的对象需手动 delete

this 指针

非静态成员函数编译为普通函数 + 隐式第一参数 this

struct Point {
    int x, y;
    void setX(int v) { x = v; }   // 实际:void setX(Point* this, int v)
};

编译后(Linux):

; Point::setX(Point* this, int v)
; this = rdi, v = esi
setX:
    mov [rdi], esi    ; this->x = v  (x 在偏移 0)
    ret

this->x 就是 *(this + 0),即 [rdi]。这是所有成员访问的汇编真相。

虚函数与虚表

虚函数通过虚表(vtable)间接调用:

struct Base {
    int data;
    virtual void f() {}
};
 
Base b;
b.f();          // 直接调用 Base::f(静态绑定)
 
Base* p = &b;
p->f();         // 间接调用:p->vptr[0]()

对象内存布局:

b:
┌─────────┬─────────┐
│  vptr   │  data   │
│ (8B)    │ (4B)    │
└─────────┴─────────┘
    │
    └──→ vtable (在 .rodata):
         ┌──────────────┐
    [0]  │ &Base::f()   │  ← 函数指针
         └──────────────┘

汇编(Linux,p 指向的 Base 对象):

; p->f()  等价于:
mov  rax, [rdi]      ; rax = p->vptr(取虚表地址)
call [rax]           ; 调用 vtable[0] 指向的函数

普通函数:call 固定地址。虚函数:先取 vptr → 再取函数指针 → 间接 call。多一次内存访问,这就是虚函数有额外开销的汇编根源。详见 07_面向对象(三)多态与虚函数

构造函数与析构函数

构造函数在对象分配后自动调用,析构函数在对象销毁前自动调用:

struct Foo {
    int x;
    Foo() : x(0) {}      // 构造函数
    ~Foo() {}             // 析构函数
};
{
    Foo obj;              // 栈对象 → 构造在此调用
}                         // 出作用域 → 析构在此调用

汇编:

; 分配栈空间
sub  rsp, 16
; 调用构造函数(this = 栈上的对象地址)
lea  rdi, [rsp+4]
call Foo::Foo
; ... 使用 obj ...
; 调用析构函数
lea  rdi, [rsp+4]
call Foo::~Foo
; 释放栈空间
add  rsp, 16

new Foo 等价于 operator new(sizeof(Foo)) + Foo::Foo(this) 的调用。delete p 等价于 p->~Foo() + operator delete(p) 的调用。

初始化列表 vs 函数体赋值

// 初始化列表:直接构造
Foo::Foo(int v) : x(v) {}
 
// 函数体赋值:先默认初始化,再赋值
Foo::Foo(int v) { x = v; }

初始化列表生成更高效的汇编(少一条指令),对于 std::string 等非平凡类型差异更大。这也是 C++ 教程 05_面向对象(一)类与对象基础 中强调初始化列表的原因。

名称修饰(Name Mangling)

C++ 支持函数重载,编译器通过名称修饰区分不同重载版本:

void func(int x);
void func(double x);

编译后的符号名类似:

_Z4funci    ← func(int)
_Z4funcd    ← func(double)

名称修饰解释了为什么不同编译器(GCC vs MSVC)编译的 .o 文件不能混链——修饰规则不同。extern "C" 禁止修饰,使 C 和 C++ 可以互调。