共计 4981 个字符,预计需要花费 13 分钟才能阅读完成。
简略的继承 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 ++ 代码以及相干对象、类与类之间的继承关系,而后决定,我去哪里调配相干的成员变量的内存,计算好这些变量的地址,最初体现到汇编里,也只是操作内存地址中的数据而已,只有内存地址算对了,剩下的只是加减乘除去对内存里存储的数据进行运算。即我该去哪块内存调配这些成员变量的存储空间,以及我该用什么代码逻辑去解决这些存储空间中的数据。