注:以下内容均来自开源学习组织DataWhale
程序的机器级表示(三)
1 过程
在大型软件的构建过程中,需要对复杂功能进行切分,过程提供了一种封装代码的 方式,它可以隐藏某个行为的具体实现,同时提供清晰简洁的接口定义。C语言中的函数和Java中的方法都是一种过程。
例如函数P调用函数Q,函数Q执行完毕返回函数P,这其中包含:
- 传递控制
- 传递数据
- 分配和释放内存
2 运行时栈
栈为函数调用提供了后进先出的内存管理机制。
以函数P调用函数Q为例,当函数Q正在执行时,函数P及相关调用链上的函数都会被暂时挂起。
2.1 栈帧
当函数执行所需要的存储空间超过寄存器能存放的大小时,就会借助栈上的存储空间,这部分存储空间称为函数的栈帧。
对于函数P调用函数Q,栈帧包括:较早的帧、调用函数P的帧、正在执行函数Q的帧,如图所示:
其中,函数P调用函数Q时,会把返回地址(途中Return address)压入栈中,以便当函数Q执行结束后返回时继续从函数P的调用函数Q的位置继续执行函数P。
这个返回地址的压栈操作不是通过push
指令完成的,而是由函数掉调用指令call
实现的。
call
指令不仅要将被调用函数得第一条指令得地址写到寄存器中,以实现函数调用,同时还要将返回地址(即被调用函数执行完毕后下一条指令的地址)压入栈中。
2.2 参数传递
如果一个函数的参数个数超过6个,超出部分就需要通过栈来传递。
参数1至参数6可以使用对应的寄存器如下:
例如有如下C代码:
void proc(long a1, long *a1p,
int a2, long *a2p,
short a3, long *a3p,
char a4, long *a4p){
a1p += a1;
a2p += a2;
a3p += a3;
a4p += a4;
}
对应前6个参数通过寄存器来传递,而参数7和参数8都是通过栈来传递,如下示意:
a1 → \rightarrow →%rdi a1p → \rightarrow →%rsi
a2 → \rightarrow →%edx a2p → \rightarrow →%rcx
a3 → \rightarrow →%r8w a3p → \rightarrow →%r9
a4 → \rightarrow →%rsp + 8
a4p → \rightarrow →%rsp + 16
注:通过栈传递参数时,所有数据大小都是8字节的倍数,a4是char类型只占一个字节,但在栈上仍然分配了8个字节的存储空间。如图所示:
注:同时寄存器的时使用有特殊的顺序规定,如下表,寄存器名字的使用取决于传递参数的大小,比如第一个参数大小是4字节,则需要用edi保存。
2.3 递归调用实例
有C代码如下:
long rfact(long n){
long result;
if(n <= 1){
resutl = 1;
}else{
result = n * rfact(n - 1);
}
return rsult;
}
对应汇编代码如下:
rfact:
pushq %rbx
movq %rdi, %rbx // n在rdi中,这句话将n的值赋给rbx
movl $1, %eax // 将1赋给eax
cmpq $1, %rdi // 比较1和n
jle .L35 // n = 1就跳到L35处执行
leaq -1(%rdi), %rdi // n - 1
call rfact // 递归调用
imulq %rbx, %rax // n*(n - 1)
.L35
popq %rbx // 将上次保存的值弹出栈
ret //返回上次调用处
假设n=3,第一次执行rfact时,由于需要使用rbx存储n的值,所以根据rbx的使用惯例,首先pushq
保存rbx的值。当执行到
n=3,第一次执行到cmpq $1, %rdi
时,不会跳到L35处,会继续执行leaq -1(%rdi), %rdi
,然后call rfact
再调用自身,再
pushq %rbx
,这时压进栈的值是3,…,一直执行,直到n=1,会跳到L35处执行popq %rbx
,这时2被弹出栈,rbx值恢复为2,ret返回到上次call处,继续执行imulq %rbx, %rax
,此时rbx是2,rax是1,这句话执行完毕,rax为1*2=2,再顺序执行popq %rbx
,rbx又被恢复为3,再ret,又是2*3,再次pop就恢复了rfact函数调用前的rbx的值,ret就结束了rfact。
可以看出,递归调用一个函数本身与调用其他函数是一样的,每次函数调用都有它自己私有的状态信息,栈分配与释放的规则与函数调用返回的顺序也是匹配的
3 数组
对于数组char A[8]
,每个元素占1个byte,假设其在内存中的首地址为
X
a
X_a
Xa,则A[i]的地址为
X
a
+
i
X_{a+i}
Xa+i,如下图所示:
而对于数组int B[4]
,每个元素占4个bytes,假设其在内存中的首地址为
X
b
X_b
Xb,则B[i]的地址为
X
b
+
4
i
X_{b+4i}
Xb+4i,如下图所示:
指针运算
对于一个char类型指针char *p
,和一个int类型指针int *q
,对于指针p,q都进行加一操作后,p指向的内存地址向后移动1个位置,q向后移动4个位置,如下图所示:
嵌套数组
嵌套数组也称二维数组,例如数组int A[5][3]
,可看成5行3列的二维数组。在计算机中,二维数组在内存中按照“行优先”的顺序进行存储,即按行放入内存,如下图示意:
对于嵌套数组D的任意一个元素都可以通过下图中的计算公式来计算地址:
例:int A[5][3]
,
&
A
[
i
]
[
j
]
=
x
A
+
4
(
3
i
+
j
)
\&A[i][j]=x_A+4(3i+j)
&A[i][j]=xA+4(3i+j)