从汇编看C++类的内存Layout(3)

本文主要从汇编的角度,看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

posted @ 2022-08-30 22:39  哇哩顾得  阅读(144)  评论(0编辑  收藏  举报