本文主要从汇编的角度,看C++类的内存模型,即C++类的各种数据是如何分布的。
1.假设有如下Cpp文件:
classMemoryLayout.cpp
class Base {
public:
Base(){}
virtual int func1() = 0;
virtual int func2() {
return 2;
}
int func3(){
return 3;
}
static int func4();
static int m_4;
int m_1;
int m_2;
private:
int m_3;
};
class ExtBase :public Base {
public:
ExtBase(){}
int func1();
};
int Base::m_4;
int Base::func4(){
return 4;
}
int ExtBase::func1(){
return 1;
}
int main()
{
ExtBase b;
b.m_1 = 1;
b.m_2 = 2;
b.m_4 = 4;
b.func1();
b.func2();
b.func3();
b.func4();
Base* bb = (Base*)&b;
bb->m_1 = 2;
bb->func1();
bb->func2();
bb->func3();
bb->func4();
return 0;
}
2.通过GCC命令生成汇编文件
g++ -o classMemoryLayout.s -fverbose-asm -S -O0 -g classMemoryLayout.cpp
classMemoryLayout.s
.section .text._ZN4BaseC2Ev,"axG",@progbits,_ZN4BaseC5Ev,comdat
.align 2
.weak _ZN4BaseC2Ev
.type _ZN4BaseC2Ev, @function
_ZN4BaseC2Ev:
endbr64
pushq %rbp #
movq %rsp, %rbp #,
movq %rdi, -8(%rbp) # this, this
# classMemoryLayout.cpp:3: Base(){}
leaq 16+_ZTV4Base(%rip), %rdx #, _1
movq -8(%rbp), %rax # this, tmp83
movq %rdx, (%rax) # _1, this_3(D)->_vptr.Base
# classMemoryLayout.cpp:3: Base(){}
nop
popq %rbp #
ret
.LFE1:
.size _ZN4BaseC2Ev, .-_ZN4BaseC2Ev
.weak _ZN4BaseC1Ev
.set _ZN4BaseC1Ev,_ZN4BaseC2Ev
.section .text._ZN4Base5func2Ev,"axG",@progbits,_ZN4Base5func2Ev,comdat
.align 2
.weak _ZN4Base5func2Ev
.type _ZN4Base5func2Ev, @function
_ZN4Base5func2Ev:
endbr64
pushq %rbp #
movq %rsp, %rbp #,
movq %rdi, -8(%rbp) # this, this
# classMemoryLayout.cpp:6: return 2;
movl $2, %eax #, _1
# classMemoryLayout.cpp:7: }
popq %rbp #
ret
.LFE3:
.size _ZN4Base5func2Ev, .-_ZN4Base5func2Ev
.section .text._ZN4Base5func3Ev,"axG",@progbits,_ZN4Base5func3Ev,comdat
.align 2
.weak _ZN4Base5func3Ev
.type _ZN4Base5func3Ev, @function
_ZN4Base5func3Ev:
endbr64
pushq %rbp #
movq %rsp, %rbp #,
movq %rdi, -8(%rbp) # this, this
# classMemoryLayout.cpp:9: return 3;
movl $3, %eax #, _1
# classMemoryLayout.cpp:10: }
popq %rbp #
ret
.LFE4:
.size _ZN4Base5func3Ev, .-_ZN4Base5func3Ev
.section .text._ZN7ExtBaseC2Ev,"axG",@progbits,_ZN7ExtBaseC5Ev,comdat
.align 2
.weak _ZN7ExtBaseC2Ev
.type _ZN7ExtBaseC2Ev, @function
_ZN7ExtBaseC2Ev:
endbr64
pushq %rbp #
movq %rsp, %rbp #,
subq $16, %rsp #,
movq %rdi, -8(%rbp) # this, this
# classMemoryLayout.cpp:21: ExtBase(){}
movq -8(%rbp), %rax # this, _1
movq %rax, %rdi # _1,
call _ZN4BaseC2Ev #
leaq 16+_ZTV7ExtBase(%rip), %rdx #, _2
movq -8(%rbp), %rax # this, tmp84
movq %rdx, (%rax) # _2, this_4(D)->D.2349._vptr.Base
# classMemoryLayout.cpp:21: ExtBase(){}
nop
leave
ret
.LFE6:
.size _ZN7ExtBaseC2Ev, .-_ZN7ExtBaseC2Ev
.weak _ZN7ExtBaseC1Ev
.set _ZN7ExtBaseC1Ev,_ZN7ExtBaseC2Ev
_ZN4Base3m_4E:
.zero 4
.globl _ZN4Base5func4Ev
.type _ZN4Base5func4Ev, @function
_ZN4Base5func4Ev:
endbr64
pushq %rbp #
movq %rsp, %rbp #,
# classMemoryLayout.cpp:26: return 4;
movl $4, %eax #, _1
# classMemoryLayout.cpp:27: }
popq %rbp #
ret
.LFE8:
.size _ZN4Base5func4Ev, .-_ZN4Base5func4Ev
_ZN7ExtBase5func1Ev:
endbr64
pushq %rbp #
movq %rsp, %rbp #,
movq %rdi, -8(%rbp) # this, this
# classMemoryLayout.cpp:29: return 1;
movl $1, %eax #, _1
# classMemoryLayout.cpp:30: }
popq %rbp #
ret
main:
endbr64
pushq %rbp #
movq %rsp, %rbp #,
subq $48, %rsp #,
# classMemoryLayout.cpp:32: {
movq %fs:40, %rax # MEM[(<address-space-1> long unsigned int *)40B], tmp101
movq %rax, -8(%rbp) # tmp101, D.2450
xorl %eax, %eax # tmp101
# classMemoryLayout.cpp:33: ExtBase b;
leaq -32(%rbp), %rax #, tmp89
movq %rax, %rdi # tmp89,
call _ZN7ExtBaseC1Ev #
# classMemoryLayout.cpp:34: b.m_1 = 1;
movl $1, -24(%rbp) #, b.D.2349.m_1
# classMemoryLayout.cpp:35: b.m_2 = 2;
movl $2, -20(%rbp) #, b.D.2349.m_2
# classMemoryLayout.cpp:36: b.m_4 = 4;
movl $4, _ZN4Base3m_4E(%rip) #, m_4
# classMemoryLayout.cpp:37: b.func1();
leaq -32(%rbp), %rax #, tmp90
movq %rax, %rdi # tmp90,
call _ZN7ExtBase5func1Ev #
# classMemoryLayout.cpp:38: b.func2();
leaq -32(%rbp), %rax #, tmp91
movq %rax, %rdi # tmp91,
call _ZN4Base5func2Ev #
# classMemoryLayout.cpp:39: b.func3();
leaq -32(%rbp), %rax #, tmp92
movq %rax, %rdi # tmp92,
call _ZN4Base5func3Ev #
# classMemoryLayout.cpp:40: b.func4();
call _ZN4Base5func4Ev #
# classMemoryLayout.cpp:41: Base* bb = (Base*)&b;
leaq -32(%rbp), %rax #, tmp93
movq %rax, -40(%rbp) # tmp93, bb
# classMemoryLayout.cpp:42: bb->m_1 = 2;
movq -40(%rbp), %rax # bb, tmp94
movl $2, 8(%rax) #, bb_16->m_1
# classMemoryLayout.cpp:43: bb->func1();
movq -40(%rbp), %rax # bb, tmp95
movq (%rax), %rax # bb_16->_vptr.Base, _1
movq (%rax), %rdx # *_1, _2
# classMemoryLayout.cpp:43: bb->func1();
movq -40(%rbp), %rax # bb, tmp96
movq %rax, %rdi # tmp96,
call *%rdx # _2
# classMemoryLayout.cpp:44: bb->func2();
movq -40(%rbp), %rax # bb, tmp97
movq (%rax), %rax # bb_16->_vptr.Base, _3
addq $8, %rax #, _4
movq (%rax), %rdx # *_4, _5
# classMemoryLayout.cpp:44: bb->func2();
movq -40(%rbp), %rax # bb, tmp98
movq %rax, %rdi # tmp98,
call *%rdx # _5
# classMemoryLayout.cpp:45: bb->func3();
movq -40(%rbp), %rax # bb, tmp99
movq %rax, %rdi # tmp99,
call _ZN4Base5func3Ev #
# classMemoryLayout.cpp:46: bb->func4();
call _ZN4Base5func4Ev #
# classMemoryLayout.cpp:47: return 0;
movl $0, %eax #, _22
# classMemoryLayout.cpp:48: }
movq -8(%rbp), %rcx # D.2450, tmp102
xorq %fs:40, %rcx # MEM[(<address-space-1> long unsigned int *)40B], tmp102
je .L13 #,
call __stack_chk_fail@PLT #
leave
ret
.weak _ZTV7ExtBase
.section .data.rel.ro.local._ZTV7ExtBase,"awG",@progbits,_ZTV7ExtBase,comdat
.align 8
.type _ZTV7ExtBase, @object
.size _ZTV7ExtBase, 32
_ZTV7ExtBase:
.quad 0
.quad _ZTI7ExtBase
.quad _ZN7ExtBase5func1Ev
.quad _ZN4Base5func2Ev
.weak _ZTV4Base
.section .data.rel.ro._ZTV4Base,"awG",@progbits,_ZTV4Base,comdat
.align 8
.type _ZTV4Base, @object
.size _ZTV4Base, 32
_ZTV4Base:
.quad 0
.quad _ZTI4Base
.quad __cxa_pure_virtual
.quad _ZN4Base5func2Ev
.weak _ZTI7ExtBase
.section .data.rel.ro._ZTI7ExtBase,"awG",@progbits,_ZTI7ExtBase,comdat
.align 8
.type _ZTI7ExtBase, @object
.size _ZTI7ExtBase, 24
_ZTI7ExtBase:
# <anonymous>:
# <anonymous>:
.quad _ZTVN10__cxxabiv120__si_class_type_infoE+16
# <anonymous>:
.quad _ZTS7ExtBase
# <anonymous>:
.quad _ZTI4Base
.weak _ZTS7ExtBase
.section .rodata._ZTS7ExtBase,"aG",@progbits,_ZTS7ExtBase,comdat
.align 8
.type _ZTS7ExtBase, @object
.size _ZTS7ExtBase, 9
_ZTS7ExtBase:
.string "7ExtBase"
.weak _ZTI4Base
.section .data.rel.ro._ZTI4Base,"awG",@progbits,_ZTI4Base,comdat
.align 8
.type _ZTI4Base, @object
.size _ZTI4Base, 16
_ZTI4Base:
# <anonymous>:
# <anonymous>:
.quad _ZTVN10__cxxabiv117__class_type_infoE+16
# <anonymous>:
.quad _ZTS4Base
.weak _ZTS4Base
.section .rodata._ZTS4Base,"aG",@progbits,_ZTS4Base,comdat
.type _ZTS4Base, @object
.size _ZTS4Base, 6
_ZTS4Base:
.string "4Base"
3.从汇编角度一步步分析代码
_ZTV开头的Lable代表virtual table
_ZTS开头的Lable代表type name
_ZTI开头的Lable代表type info
_ZTV7ExtBase:这个label存储了ExtBase虚函数表开始的位置,里面存放了2个虚函数func1和func2的地址
.quad 0
.quad _ZTI7ExtBase
.quad _ZN7ExtBase5func1Ev
.quad _ZN4Base5func2Ev
_ZTV4Base:这个label存储了Base虚函数表开始的位置,里面存放了2个虚函数func1和func2的地址,注意第一个虚函数并没有被定义,所以是__cxa_pure_virtual
.quad 0
.quad _ZTI4Base
.quad __cxa_pure_virtual
.quad _ZN4Base5func2Ev
.weak _ZTI7ExtBase
_ZTI7ExtBase: 一些ExtBase类的信息
.quad _ZTVN10__cxxabiv120__si_class_type_infoE+16
.quad _ZTS7ExtBase
.quad _ZTI4Base
.weak _ZTS7ExtBase
_ZTS7ExtBase:ExtBase的类名字
.string "7ExtBase"
.weak _ZTI4Base
_ZN4BaseC2Ev:
这个Label实现了Base::Base(){}这个构造器函数,里面有3句非常重要的codes, 这3句把虚函数表的地址放到了类的起始位置,也就是this所指向的地址。
leaq 16+_ZTV4Base(%rip), %rdx #, _1
movq -8(%rbp), %rax # this, tmp83
movq %rdx, (%rax) # _1, this_3(D)->_vptr.Base
_ZN4Base5func2Ev:
这个Label实现了虚函数Base::func2(),和普通的函数没什么区别。第一个参数%(rdi)代表传入的指针。
_ZN4Base5func3Ev:
这个Label实现了普通函数Base::func3(),和普通的函数没什么区别。第一个参数%(rdi)代表传入的指针。
_ZN7ExtBaseC2Ev:
这个Label实现了继承类ExtBase的构造函数。值得注意的是,子类ExtBase的构造函数通过语句1调用了基类Base类的构造函数,并且通过语句2,3,4把虚表的位置给到了类的内存的起始位置。
1.call _ZN4BaseC2Ev #
2.leaq 16+_ZTV7ExtBase(%rip), %rdx #, _2
3.movq -8(%rbp), %rax # this, tmp84
4.movq %rdx, (%rax) # _2, this_4(D)->D.2349._vptr.Base
_ZN4Base3m_4E:
这个Label开辟了静态变量M_4的位置,值得注意的是,这个m_4的内存位置并不在stack中,而在代码段中,或者data段中。
.zero 4
_ZN4Base5func4Ev:
这个Label实现了静态函数Base::func4(),这就是一个普通C语言的函数,第一个传入的参数并不是this
_ZN7ExtBase5func1Ev:
这个Label实现了纯虚函数ExtBase::func4()。
main:主函数的入口
ExtBase b:起始地址是-32(%rbp),并且调用了构造器_ZN7ExtBaseC1Ev。
classMemoryLayout.cpp:33: ExtBase b;
leaq -32(%rbp), %rax #, tmp89
movq %rax, %rdi # tmp89,
call _ZN7ExtBaseC1Ev #
需要注意,静态变量不是在stack中,而是通过rip indirect addresing找到的。
classMemoryLayout.cpp:36: b.m_4 = 4;
movl $4, _ZN4Base3m_4E(%rip) #, m_4
调用静态变量的时候 并没有设置 this 指针
classMemoryLayout.cpp:40: b.func4();
call _ZN4Base5func4Ev
强转其实没什么鸟用
classMemoryLayout.cpp:41: Base* bb = (Base*)&b;
leaq -32(%rbp), %rax #, tmp93
movq %rax, -40(%rbp) # tmp93, bb
classMemoryLayout.cpp:42: bb->m_1 = 2;
movq -40(%rbp), %rax # bb, tmp94
movl $2, 8(%rax) #, bb_16->m_1
调用虚函数func1的时候是从虚表调用的。
classMemoryLayout.cpp:43: bb->func1();
movq -40(%rbp), %rax # bb, tmp95
movq (%rax), %rax # bb_16->_vptr.Base, _1
movq (%rax), %rdx # *_1, _2 这里拿到了虚函数的地址
classMemoryLayout.cpp:43: bb->func1();
movq -40(%rbp), %rax # bb, tmp96
movq %rax, %rdi # tmp96,
call *%rdx # _2 这里调用了虚函数
func2同理
classMemoryLayout.cpp:44: bb->func2();
movq -40(%rbp), %rax # bb, tmp97
movq (%rax), %rax # bb_16->_vptr.Base, _3
addq $8, %rax #, _4 ,因为func2在虚拟表的第二个位置,所以需要加8字节,64位机器
movq (%rax), %rdx # *_4, _5
classMemoryLayout.cpp:44: bb->func2();
movq -40(%rbp), %rax # bb, tmp98
movq %rax, %rdi # tmp98,
call *%rdx # _5
普通函数和静态函数正常调用。
classMemoryLayout.cpp:45: bb->func3();
movq -40(%rbp), %rax # bb, tmp99
movq %rax, %rdi # tmp99,
call _ZN4Base5func3Ev #
classMemoryLayout.cpp:46: bb->func4();
call _ZN4Base5func4Ev #
Summary
类的构造函数中会自动把虚函数的地址放入类的0字节开始的地方。子类会自动调用父类的默认构造器,静态变量所在位置不在stack,而是在代码段。静态方法和普通方法的唯一区别就是,静态类没有传入this指针(%rdi),静态表会维护类中静态变量的位置。
Reference
[1]https://docs.oracle.com/cd/E26502_01/html/E28388/eoiyg.html
[2]https://stackoverflow.com/questions/69464871/assembly-and-rip-usage
[3]https://stackoverflow.com/questions/3250277/how-to-use-rip-relative-addressing-in-a-64-bit-assembly-program?rq=1
[4]https://sourceware.org/binutils/docs/as/i386_002dMemory.html
[5]https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangling-special-vtables