程序的机器级表示:过程(续)
寄存器存储惯例
调用者保存暂存的值在调用者的帧内
被调用者保存暂存的值在被调用者的帧内
对于调用者而言:
%rax用于保存返回值
六个寄存器%rdi,%rsi,%rdx,%rcx,%r8,%r9用于保存参数
超过6个参数后,多出的参数将会通过栈进行传递
调用者需要保存%r10和%r11两个寄存器,之后会用到
对于被调用者而言:
被调用者保存4个寄存器:%rbx,%r12,%r13,%r14
被调用者也保存%rbp(帧指针)(返回时返还)、%rsp(栈指针,由被调用者保存)(用完后恢复原来的值)
关于被调用者和调用者使用寄存器的情况举例:
依旧是下述C语言指令:
long call_incr2(long x) {
long v1 = 15213;
long v2 = incr(&v1, 3000);
return x+v2;
}
其对应的汇编代码为:
call_incr2:
pushq %rbx
subq $16, %rsp
movq %rdi, %rbx
movq $15213, 8(%rsp)
movl $3000, %esi
leaq 8(%rsp), %rdi
call incr
addq %rbx, %rax
addq $16, %rsp
popq %rbx
ret
关于这段代码的分行叙述(个人理解,不一定对):
1、在调用call_incr2时,%rbx可能是调用者正在使用的寄存器,而现在,被调用者call_incr2却打算用到这个寄存器。因此,先对%rbx进行进栈操作
2、栈指针下移两个字节,给接下来的存储(帧)留下空间
3、将调用者的%rdi的值(传入到call_incr2函数中的实参),赋给被调用者(call_incr2)%rbx,此时%rbx存储了x值
4、将局部变量v1=15213放入此过程的帧中,其地址紧接刚才被压入栈中的%rbx
5、call_incr2作为incr的调用者,将准备传递的第二个实参3000赋值给寄存器%rsi
6、将%rsp上移8个字节的地址赋给作为调用者使用的寄存器%rdi,意图在于将准备传递的第一个实参&v1(v1的地址)赋值给寄存器%rdi。注意到这两个要传递的参数都被存入了调用者寄存器,以及%rsp+8这个地址上存储了v1
7、调用incr过程
8、调用完成后,%rax存储的是incr返回的值,即v2。现在,用v2加上%rbx存储的x,即返回值变为v2+x,此时,主要运算便完成了
9、还原栈指针,将刚才暂时放在call_incr2帧中的常数15213抹去,因为不再需要它了
10、弹出之前的%rbx的值,恢复%rbx在刚开始调用call_incr2时的状态,这样一来,在返回后,call_incr2的调用者传入的实参就不会被改变
有的过程不需要帧,因为不需要保存
递归
递归是以栈实现的,没有栈,就无法支持递归
和过程一样,递归在返回时,也是以寄存器%rax返回的
递归的汇编语言举例:
有下述计算x的二进制码中1的总数的C语言程序:
/* Recursive popcount */
long pcount_r(unsigned long x) {
if (x == 0)
return 0;
else
return (x & 1)
+ pcount_r(x >> 1);
}
对应的汇编语言为:
pcount_r:
movl $0, %eax
testq %rdi, %rdi
je .L6
pushq %rbx
movq %rdi, %rbx
andl $1, %ebx
shrq %rdi
call pcount_r
addq %rbx, %rax
popq %rbx
.L6:
rep; ret
现以输入x=1011 2 _2 2为例,描述此情形下的递归全过程:
1、初始时刻,栈指针指向栈中某个位置,寄存器%rdi存放着输入的x值:1011 2 _2 2
2、数值0被放入到寄存器%rax中(初始化),随后判断%rdi是否为空(为0)。此时%rdi=1011,所以判断不为空,ZF=0,je处不跳转
3、将寄存器%rbx压入栈中,对当前的值进行保存。注意,在此次分析中,现在并不知道%rbx存的值是多少,个人感觉它可能是初始值(或者是调用者的某个值)
4、%rdi的值赋给%rbx
5、%rbx和常数1进行按位与运算,得到1
6、%rdi向右逻辑移位1次,得101
7、调用自身。先将过程结束后,addq那一行的地址压入栈中,再跳转回到pcount_r开始处
8、与第一次同理。先将%rax置0,再检验%rdi是否为0,结果为非0,je不执行。随后%rbx=0001被压入栈中以进行保存,再将%rdi的值赋给%rbx。%rbx与1进行按位与运算,得到%rbx=0001。最后再将%rdi右移,得到%rdi=10,随后进入下一次对自身的调用
9、经过同样的流程,完成第二次对自身的调用:
10、经过同样的流程,完成第三次对自身的调用。注意到进入第四次调用时,%rdi的值变为0
11、第四次调用时,判断%rdi的结果为0,因此直接跳转至L6处,进行了返回。此时,从栈中弹出第三次调用的addq地址,并移动到那个地方
12、执行addq语句,将%rbx的值加到%rax上,得%rax=1
13、从栈中弹出%rbx,恢复%rbx之前存入栈中的值
14、返回后,又回到上一级的addq处,运算同理
15、再返回一级:
16、最后一级返回后,%rbx恢复到最开始时的值,而%rax则是最终的返回值,11 2 _2 2,即十进制的3
无论是哪种高级语言,实现的过程调用都是一样的
程序的机器级表示:特殊数据结构
目前已经知道了基本数据类型的底层表示方法:
字符:ASCII
整数:unsigned、signed(所有有符号数使用二进制补码表示)
浮点数:float(单精度,32位)、double(双精度,64位)、80位(仅Intel)
但特殊的数据结构在底层如何表示?
数组
将同类型的数据放在一起
占用连续的空间
一维数组
从首地址开始,每个元素都占用相同的空间,因此下标为k的元素地址的计算方式为:首地址+每个元素所占空间大小*元素的下标k
多维数组
二维数组的存储方式:
C语言——按行优先(从第0行开始,一行一行地存)
Java——按列优先
多级数组:数组中存储地址,每个地址指向一个数组
利用指针可以实现动态数组