在class A中定义一个静态变量,然后对其进行操作,我们看看这个静态变量的是怎么被操作的。
#include "stdlib.h"
class A{
public:
static int test0;
static int test1;
};
int A::test0 = 1;
int A::test1 = 1;
int main(){
A::test0 = 4;
A::test1 = 5;
}
对应的汇编
其中对静态变量的操作,是通过RIP+相对地址实现的,因为静态变量不是在stack或者heap阶段,而是在bss或者data段,因此可以直接通过RIP+相对地址索引到。
实际上也就是这样
在main函数中,将A的静态成员设置为4,对应的汇编是
mov DWORD PTR A::record[rip], 4
dword 双字 就是四个字节
ptr pointer缩写 即指针
而之所以使用RIP,是因为RIP是保存了当前的程序的指针,加上偏移,就可以索引到静态变量的空间。
本质上就是当前RIP肯定指向的是text,在编译器编译时,静态成员变量的地址在bss或者data段,是可以计算出RIP和静态成员变量的相对地址的,因此这样就可以索引到了。
则被编译成了以下内容,需要注意的是,只调用了一次的new[],但是调用了10次的构造函数
如果只生成一个对象
那么会被编译为(此时A只有成员变量int a):
- mov edi, 4 ----------- edi 是默认的传参寄存器,而4是当前的class的大小。 有意思的是,如果成员变量是一个int,那么传入的是4;如果一个int,一个char* ,那么 int占4个字节, 64bit环境中,char* 占据8个字节,因此int会补齐到8个字节+char*的8个字节,一共16个字节
- 调用构造函数
- 构造函数的结果默认是存在rax中的,将rax的结果存到rbx中
- 将rbx的存取rdi,实际上就是new出来的地址,传递给构造函数做参数
- 调用A::A() 构造函数
定义三个类A的对象,因为此时A不是new出来的,所以不会call new函数分配在堆上,而是在栈上分配
- lea rax,[rbp-0x1c] ----------- 将rbp-0x1c的计算结果存入rax中,rbp是栈指针寄存器,指明了当前帧的基地址,而减去0x1c就是在减去了开头的传入的参数空间之后,为a0分配的栈内的起始地址;
- 将rax的结果传递给rdi,以作为实参传递给构造函数
- 调用构造函数
可以看出,地址分别是rbp-0x1c, rbp-0x20,rbp-0x24, 这就是为是三个对象分配的栈内的空间,间隔4字节,因为A的成员只有一个整型。
然后设置成员变量的值
被编译为:
最后一行,就会调用A的setA成员函数,而这个函数,会被编译为:
这里传入的rdi,应该就是this指针,即当前a的地址,而esi就是int类型形参的值了。我们可以看到rdi的值最后传到了rbp-8这个地址的空间上,后面我们如果对这个地址对应的空间操作,实际上就是操作this所指向的对象的空间了。
参数传递规则:
- 一个参数用rdi(edi)传
- 两个参数用rdi、rsi(edi、rsi)传
- 三个参数用rdi、rsi、rdx(edi、esi、edx)传
- 四个参数用rdi、rsi、rdx、rcx(edi、esi、edx、ecx)传
- 五个参数用rdi、rsi、rdx、rcx、r8(edi、esi、edx、ecx、r8)传
- 六个参数用rdi、rsi、rdx、rcx、r8、r9(edi、esi、edx、ecx、r8、r9)传
整体的代码:
#include "stdlib.h"
class A{
public:
static int record;
public:
int a;
public:
A(){
a = 1;
}
~A(){}
void setA(int aTmp){
a+=aTmp;
}
};
int A::record = 1;
int main(){
A::record = 4;
A *a = new A();
delete a;
A::record = 5;
A a0,a1,a2;
a0.setA(0);
a1.setA(1);
a2.setA(2);
return 0;
}
比较复杂的情况:类A中存在指针,并且调用时,声明了A对象数组。
#include "stdlib.h"
class A{
public:
static int record;
public:
int a;
char *ptr=nullptr;
public:
A(){
a = 1;
ptr = (char *)malloc(100);
}
~A(){
free(ptr);
}
void setA(int aTmp){
a = aTmp;
}
};
int A::record = 1;
int main(){
A::record = 4;
A *a = new A[10];
delete []a;
A::record = 5;
//A b,c;
//b.setA(1);
//c.setA(2);
return 0;
}
.Ltext0:
A::A() [base object constructor]:
.LFB15:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
.LBB2:
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax+8], 0
mov rax, QWORD PTR [rbp-8]
mov DWORD PTR [rax], 1
mov edi, 100
call malloc
mov rdx, rax
mov rax, QWORD PTR [rbp-8]
mov QWORD PTR [rax+8], rdx
.LBE2:
nop
leave
ret
.LFE15:
A::~A() [base object destructor]:
.LFB18:
push rbp
mov rbp, rsp
sub rsp, 16
mov QWORD PTR [rbp-8], rdi
.LBB3:
mov rax, QWORD PTR [rbp-8]
mov rax, QWORD PTR [rax+8]
mov rdi, rax
call free
.LBE3:
nop
leave
ret
.LFE18:
A::record:
.long 1
main:
.LFB21:
push rbp
mov rbp, rsp
push r13
push r12
push rbx
sub rsp, 24
mov DWORD PTR A::record[rip], 4
mov edi, 168
call operator new[](unsigned long)
mov rbx, rax
mov QWORD PTR [rbx], 10
lea rax, [rbx+8]
mov r12d, 9
mov r13, rax
.L5:
test r12, r12
js .L4
mov rdi, r13
call A::A() [complete object constructor]
add r13, 16
sub r12, 1
jmp .L5
.L4:
lea rax, [rbx+8]
mov QWORD PTR [rbp-40], rax
mov rax, QWORD PTR [rbp-40]
mov eax, DWORD PTR [rax]
lea edx, [rax+1]
mov rax, QWORD PTR [rbp-40]
mov DWORD PTR [rax], edx
cmp QWORD PTR [rbp-40], 0
je .L6
mov rax, QWORD PTR [rbp-40]
sub rax, 8
mov rax, QWORD PTR [rax]
sal rax, 4
mov rdx, rax
mov rax, QWORD PTR [rbp-40]
lea rbx, [rdx+rax]
.L8:
cmp rbx, QWORD PTR [rbp-40]
je .L7
sub rbx, 16
mov rdi, rbx
call A::~A() [complete object destructor]
jmp .L8
.L7:
mov rax, QWORD PTR [rbp-40]
sub rax, 8
mov rax, QWORD PTR [rax]
sal rax, 4
lea rdx, [rax+8]
mov rax, QWORD PTR [rbp-40]
sub rax, 8
mov rsi, rdx
mov rdi, rax
call operator delete[](void*, unsigned long)
.L6:
mov DWORD PTR A::record[rip], 5
mov eax, 0
add rsp, 24
pop rbx
pop r12
pop r13
pop rbp
ret
总结:
- 寄存器:rbp 栈指针, rsp 栈顶指针,rax函数返回值,rdi,rsi,函数默认传参寄存器;
- 全局变量,静态变量通过RIP相对索引进行访问更改;
- new() 的本质上是先调用operator new函数,然后将new出的指针传递给构造函数,进行构造;new[] 则是new一次,但是调用构造函数N次
- 老生常谈,new出的类对象是在heap上;临时类对象是在stack上,使用ebp索引;
- char* 类型,不是一个字节,而是8个字节(64bit环境)
- push ebp;保存调用函数的帧基址;mov ebp esp;生成被调用函数的新帧基址;
Reference:
欢迎关注我的公众号《处理器与AI芯片》