本文用到的反汇编工具是objconv,使用方法可以看我另一篇文章https://blog.csdn.net/weixin_45001971/article/details/128660642。
其它文章:
从汇编的角度了解C++原理——类的储存结构和函数调用
从汇编的角度了解C++原理——new和malloc的区别
从汇编的角度了解C++原理——虚函数
以这段代码为例。
编译后对obj文件反汇编,得到以下汇编代码,配合常量的值来分析汇编的含义。
main:
sub rsp, 72
mov dword [rsp+20H], 10 //定义变量num,地址为rsp+20H
lea rcx, [rsp+28H] //定义对象a,地址为rsp+28H,把地址存入rcx寄存器
call ??0A@@QEAA@XZ //调用A类构造函数
mov eax, 4294967295 //4294967295就是-1,eax寄存器用来存放返回值
add rsp, 72
ret
??0A@@QEAA@XZ: //A类构造函数
/* 从rcx寄存器中取执行构造的对象,放到rsp+8H */
mov qword [rsp+8H], rcx
/* 初始化变量d1,地址是对象的首地址 */
mov rax, qword [rsp+8H]
mov dword [rax], 10
/* 初始化变量d2,地址是对象的首地址+4H */
mov rax, qword [rsp+8H]
mov byte [rax+4H], 20
/* 初始化变量d3,地址是对象的首地址+8H */
mov rax, qword [rsp+8H]
mov dword [rax+8H], 30
/* 初始化变量d4,地址是对象的首地址+0CH */
mov rax, qword [rsp+8H]
mov byte [rax+0CH], 40
mov rax, qword [rsp+8H]
ret
; ??0A@@QEAA@XZ End of function
从这个例子中分析可以了解到函数的执行过程和类的基本储存结构。
执行对象的函数过程分为以下两步:
1、把对象的地址放入rcx寄存器。
2、执行函数。执行函数时,如果需要访问对象的成员,会先从rcx寄存器拿到对象的首地址,然后通过地址偏移访问到变量。
类的对象的成员是按照定义的顺序从对象首地址依次排列下去的,在本例中,类成员最大的是int类型,所以按4个字节的规则来对齐,下图是本例a对象储存结构的示意图。
在C++的继承中,子类也可以定义变量,这时候又是怎么储存变量的呢?下面分别介绍单继承和多继承的储存结构。
main:
sub rsp, 72
lea rcx, [rsp+20H]
call ??0B@@QEAA@XZ //调用B类构造
lea rcx, [rsp+20H]
call ?func1@A@@QEAAXXZ //调用A类的func1
lea rcx, [rsp+20H]
call ?func2@A@@QEAAXXZ //调用A类的func2
mov eax, 4294967295
add rsp, 72
ret
??0A@@QEAA@XZ: //A类的构造函数,与上一节例子一样
mov qword [rsp+8H], rcx
mov rax, qword [rsp+8H]
mov dword [rax], 10
mov rax, qword [rsp+8H]
mov byte [rax+4H], 20
mov rax, qword [rsp+8H]
mov dword [rax+8H], 30
mov rax, qword [rsp+8H]
mov byte [rax+0CH], 40
mov rax, qword [rsp+8H]
ret
??0B@@QEAA@XZ: //B类的构造函数
mov qword [rsp+8H], rcx //对象首地址放到rsp+8H
sub rsp, 40 //压栈
mov rcx, qword [rsp+30H] //结合上一条指令等价于rsp-40+48,也就是取对象首地址
call ??0A@@QEAA@XZ //调用A类构造
mov rax, qword [rsp+30H]
mov dword [rax+10H], 50 //对象首地址往后偏移16字节的位置定义为d5
mov rax, qword [rsp+30H]
add rsp, 40 //出栈
ret
?func1@A@@QEAAXXZ:; Function begin
mov qword [rsp+8H], rcx
mov rax, qword [rsp+8H]
mov dword [rax], 11
ret
?func2@A@@QEAAXXZ:; Function begin
mov qword [rsp+8H], rcx
mov rax, qword [rsp+8H]
mov dword [rax], 12
ret
从示例中可以看到,子类中定义的成员,是拼接在父类成员的后面的,A类的大小是16字节,B类的成员d5被定义在this指针往后偏移16个字节的位置,如下图所示。
反汇编,我们直接看到C类的构造函数。
??0C@@QEAA@XZ:
mov qword [rsp+8H], rcx
sub rsp, 40 //rsp指针减40
mov rcx, qword [rsp+30H] //rsp+30H取的是首地址,因为上一条指令减40(rsp-40+30H 等价第一句的 rsp+8H)
call ??0A@@QEAA@XZ //在首地址初始化A类
mov rax, qword [rsp+30H]
add rax, 8 //指针基于首地址往后偏移8字节
mov rcx, rax
call ??0B@@QEAA@XZ //在首地址+8 初始化B类
mov rax, qword [rsp+30H]
mov dword [rax+0CH], 3 //在首地址+0CH 处初始化C类
mov rax, qword [rsp+30H]
add rsp, 40
ret