C++:如何用简单的汇编指令,实现C++复杂抽象的面向对象概念?——「2、简单继承」

146 阅读4分钟

简单的继承C++编译器是怎么把逻辑上是继承关系的C++代码,父类和子类,怎么用汇编表现这种关系呢?

老规矩,先上C++代码:

#include<iostream>
/**
 * 简单继承
*/
class Father
{
public:
    int a;
    Father(){
        a = 11;
    }
    ~Father(){
        a = 22;
    }
protected:
    int b;
private:
    int c;
};
class Son : public Father{
public:
    int s;
    Son(){
        s = 66;
    }
};
int main(){
    Son son;
    son.a = 888;
    return 0;
}

汇编是如何表现的呢?

		.file	"3-object-class-inherit-0.cpp"
	.text
	.section	.rodata
	.type	_ZStL19piecewise_construct, @object
	.size	_ZStL19piecewise_construct, 1
_ZStL19piecewise_construct:
	.zero	1
	.local	_ZStL8__ioinit
	.comm	_ZStL8__ioinit,1,1
	.section	.text._ZN6FatherC2Ev,"axG",@progbits,_ZN6FatherC5Ev,comdat
	.align 2
	.weak	_ZN6FatherC2Ev
	.type	_ZN6FatherC2Ev, @function
_ZN6FatherC2Ev:
.LFB1523:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movq	%rdi, -8(%rbp) # 把main函数的rbp-48这个值,放到 rbp-8
	movq	-8(%rbp), %rax # 把main函数的rbp-48这个值,放到rax寄存器
	movl	$11, (%rax) # 把11放到main函数的rbp-48这个值所指向的内存处(弄了半天,绕这么大弯子,就是想让父类构造函数的赋值语句,赋值到main函数栈帧所指向的内存,因为编译器无法直接算出父类成员变量a的内存地址,所以只能用传值的方式一路传过来)
	nop
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1523:
	.size	_ZN6FatherC2Ev, .-_ZN6FatherC2Ev
	.weak	_ZN6FatherC1Ev
	.set	_ZN6FatherC1Ev,_ZN6FatherC2Ev
	.section	.text._ZN6FatherD2Ev,"axG",@progbits,_ZN6FatherD5Ev,comdat
	.align 2
	.weak	_ZN6FatherD2Ev
	.type	_ZN6FatherD2Ev, @function
_ZN6FatherD2Ev:
.LFB1526:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movq	%rdi, -8(%rbp)
	movq	-8(%rbp), %rax
	movl	$22, (%rax) # 把22放到main函数栈帧rbp-48这个值,这是变量a的内存地址,放到a所在的内存地址处。注意(%rax)是指rax寄存器里面的数值所指向的内存,而%rax是指寄存器
	nop
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1526:
	.size	_ZN6FatherD2Ev, .-_ZN6FatherD2Ev
	.weak	_ZN6FatherD1Ev
	.set	_ZN6FatherD1Ev,_ZN6FatherD2Ev
	.section	.text._ZN3SonC2Ev,"axG",@progbits,_ZN3SonC5Ev,comdat
	.align 2
	.weak	_ZN3SonC2Ev
	.type	_ZN3SonC2Ev, @function
_ZN3SonC2Ev:
.LFB1529:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp # 给Son子类的构造函数分配内存
	movq	%rdi, -8(%rbp) # 把main函数的rbp-48这个值放到Son构造函数的rbp-8内存里
	movq	-8(%rbp), %rax # 把main函数的rbp-48这个值放到rax
	movq	%rax, %rdi # 把main函数的rbp-48这个值放到rdi寄存器用作传参数,在子构造函数里调用父构造函数,可以看出,是先调用的父构造函数的逻辑,再执行子构造函数的代码逻辑
	call	_ZN6FatherC2Ev # 调用父构造函数
	movq	-8(%rbp), %rax
	movl	$66, 12(%rax) # 执行子构造函数的代码逻辑 rax+12 == rbp-48+12 == rbp-36
	nop
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1529:
	.size	_ZN3SonC2Ev, .-_ZN3SonC2Ev
	.weak	_ZN3SonC1Ev
	.set	_ZN3SonC1Ev,_ZN3SonC2Ev
	.section	.text._ZN3SonD2Ev,"axG",@progbits,_ZN3SonD5Ev,comdat
	.align 2
	.weak	_ZN3SonD2Ev
	.type	_ZN3SonD2Ev, @function
_ZN3SonD2Ev:
.LFB1533:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp # 分配栈桢
	movq	%rdi, -8(%rbp) # # 把main函数栈帧rbp-48这个值,这是变量a的内存地址,放到rbp-8内存处
	movq	-8(%rbp), %rax
	movq	%rax, %rdi # 计算main函数栈帧rbp-48这个值,这是变量a的内存地址,父类析构函数用到它
	call	_ZN6FatherD2Ev
	nop
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1533:
	.size	_ZN3SonD2Ev, .-_ZN3SonD2Ev
	.weak	_ZN3SonD1Ev
	.set	_ZN3SonD1Ev,_ZN3SonD2Ev
	.text
	.globl	main
	.type	main, @function
main:
.LFB1531:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	pushq	%rbx
	subq	$40, %rsp # 为main分配40字节栈帧
	.cfi_offset 3, -24
	movq	%fs:40, %rax
	movq	%rax, -24(%rbp)
	xorl	%eax, %eax

	leaq	-48(%rbp), %rax # 这不是超出栈了吗?
	movq	%rax, %rdi # 把rbp-48这个值传到构造函数参数?(这里构造函数是无参构造啊)注意:无参构造只是C++语法特性上的说法,汇编里,用到什么数据就传什么数据,具体用到什么,编译器自己计算
	call	_ZN3SonC1Ev
	movl	$888, -48(%rbp) # 父类的成员变量是在main函数的栈帧里分配的,该对象是main函数的局部变量,所以在栈上分配,如果是全局变量,就不在栈上分配了
	movl	$0, %ebx
	leaq	-48(%rbp), %rax # 计算main函数栈帧rbp-48这个值,这是变量a的内存地址,是为了一路传给父类析构函数用的
	movq	%rax, %rdi # 
	call	_ZN3SonD1Ev # 析构函数
	movl	%ebx, %eax
	movq	-24(%rbp), %rdx
	xorq	%fs:40, %rdx
	je	.L7
	call	__stack_chk_fail@PLT
.L7:
	addq	$40, %rsp
	popq	%rbx
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE1531:
	.size	main, .-main
	.type	_Z41__static_initialization_and_destruction_0ii, @function
_Z41__static_initialization_and_destruction_0ii:
.LFB2015:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movl	%edi, -4(%rbp)
	movl	%esi, -8(%rbp)
	cmpl	$1, -4(%rbp)
	jne	.L10
	cmpl	$65535, -8(%rbp)
	jne	.L10
	leaq	_ZStL8__ioinit(%rip), %rdi
	call	_ZNSt8ios_base4InitC1Ev@PLT
	leaq	__dso_handle(%rip), %rdx
	leaq	_ZStL8__ioinit(%rip), %rsi
	movq	_ZNSt8ios_base4InitD1Ev@GOTPCREL(%rip), %rax
	movq	%rax, %rdi
	call	__cxa_atexit@PLT
.L10:
	nop
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE2015:
	.size	_Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii
	.type	_GLOBAL__sub_I_main, @function





上述汇编,我删掉了一点点末尾的非核心汇编,防止篇幅太长。

上面汇编代码从main函数开始分析,你会发现,由于Son类的实例化对象是在main函数内部创建的,属于main函数内部的局部变量,所以该对象的成员变量的内存,都是在main函数栈帧上分配的,若rbp存的是main函数的栈帧地址,那么a是在rbp-48处分配的内存,rbp-48+12即rbp-36是s变量的地址,这些都是编译器自己计算出来的相对地址,想对于rbp栈帧的地址。[[[我的疑问是,rsp明明指向的是rbp-40处,怎么能在rbp-48处给a分配内存,这不是超出栈首地址的范围了吗?需要探索]]]

然后,我从汇编上看出,原来就是先调用的父类构造函数的代码逻辑,然后再调用子类构造函数的代码逻辑,这个先后顺序在汇编上也是这么干的。

总之通过上述分析发现,虽然c++继承很复杂,涉及到父类变量与子类变量,涉及到先调用父类的构造函数,再调用子类构造函数等等,但实际上还是变量分配在哪的事。父类成员变量和子类成员变量最终都要落到内存某个位置上,而这里的变量,由于对象是在main函数里的局部变量,所以它的成员变量都是栈帧上分配的。而且,所谓的对象,只是个抽象概念,汇编里并没有对象这个概念,有的只是对象内部的那些成员变量以及它们对应的内存地址,操作对象,最终操作的也只是对象内的成员变量,它和函数内部的局部整型int变量没有任何区别。

因此,编译器所干的事,就是读懂你的C++代码以及相关对象、类与类之间的继承关系,然后决定,我去哪里分配相关的成员变量的内存,计算好这些变量的地址,最后体现到汇编里,也只是操作内存地址中的数据而已,只要内存地址算对了,剩下的只是加减乘除去对内存里存储的数据进行运算。即我该去哪块内存分配这些成员变量的存储空间,以及我该用什么代码逻辑去处理这些存储空间中的数据。