《深入理解计算机系统》 练习题3.27-3.28 被调用者保存寄存器 栈指针
3.27
要求你将书中的阶乘函数,利用guarded-do的翻译策略,转换成c的goto版本,答案如上图。
注意第一次测试为if(n <= 1)
,这是因为,第一次测试实际是2 <= n
,它的反面是2 > n
即n < 2
即n <= 1
。
3.28 反转x的二进制
题面上的汇编如上,在用汇编推理c语句时,对于for循环的测试条件,我刚开始没有想通,因为没有看见cmp指令。
原理:jne .L10
是检测ZF标志位的,与它最近的语句subq $1, %rdx
是有可能置ZF为1的,当%rdx为0时。
long fun_b(unsigned long x){
long val = 0;
long i;
for(i = 64; i != 0; i--){
val = (val << 1) | (x & 0x1);
x >>= 1;
}
return val;
}
此函数用来获得x的二进制的反转。
x & 0x1
获得当前x的第0位的数。val << 1
将已经存储的二进制数移到更高一位上去。x >> 1
既然当前x的第0位已经存储在了val,那么便丢弃掉第0位,右移1位,把第1位的数作为新的第0位的数。
3.7.5 寄存器中的局部存储空间
除了栈指针%rsp外,寄存器分为被调用者保存寄存器,和调用者保存寄存器。
在过程P调用Q时,如果值放在了被调用者保存寄存器中,那么它们的值在Q返回到P时,与P刚调用Q时,是一样的。Q如何保证这些值不变(要么不去碰它,要么暂时先放入栈帧中),对于P来说不用知道,因为对于P来说是透明的,P只需要在调用Q之前,将需要存储的值放入这些被调用者保存寄存器即可。
对于如上汇编代码,有三点需要注意:
1.两次push和两次pop。pushq %rbp
和pushq %rbx
先将寄存器中的值放入栈中,注意两个都是被调用者保存寄存器。也许你会觉得奇怪,第5和第8行已经按照要求,在调用Q之前把需要存储的值存入到了%rbp %rbx
,为什么还要把%rbp %rbx
的刚开始的值入栈存起来呢。
这是因为,刚开始的时候%rbp %rbx
这两个寄存器可能就已经存了需要保存的值,但因为调用Q所以%rbp %rbx
要暂时另作它用。换个角度说,当前的调用者P可能是别的过程的被调用者。这也就是原文中的“当然,要先把之前的值保存到栈上”这句话的意思。
2.为了存调用返回地址,需要手动分配栈空间。subq $8, %rsp
这里分配的8字节栈空间,存了两次调用返回地址(分别是第8行和第11行的指令的地址),这里的8字节栈空间是被第二次利用了。
3.注意pushq %rbp
和pushq %rbx
和subq $8, %rsp
的顺序,它们与后面第12、13、14行指令的顺序,这里是符合栈的先进后出的。
栈指针%rsp
阅读了这么多汇编代码后,总结下栈指针%rsp。
1.栈指针%rsp它其实不算是个寄存器,顾名思义,它是一个指针,指向当前栈顶地址。
2.比如%rax
,它一个普通的寄存器,取%rax
时,就会取它装有的8字节数据,可能它是个指针;取(%rax)
时,则成了内存引用,会取出这个指针指向的数据;
而%rsp
,它是栈指针,取%rsp
时,返回的是当前栈指针的地址形如0x7fffffffe820
;取(%rsp)
时,则可能取出从820-827这8个字节里面存的数据;而一般来说,我们不需要取%rsp
,因为要栈顶地址来根本没用啊。
指针的理解
数据在计算机中是一个一个字节存的,而每个字节都有一个序号,我们称这些字节序号为地址,或指针。
这个序号的表示范围为8个字节的表示范围,8*8=64,即可以用64二进制来表示一个地址。按照无符号数来说,这个序号的范围是0 ~ 。由于一个十六进制数可以表示4位二进制,而一个字节是8位二进制,所以一个字节的值可以用两个十六进制数来表示,而地址可以用16个十六进制数来表示。
以movq (%rdi), %rax
为例,假设8字节寄存器%rdi
存的是一个指针,即字节序号,这里假设为A。(%rdi)
为内存引用,配合movq(q为四字,8字节),这条指令就会取A,A+1,A+2…A+6,A+7这8个字节存的数据,再放到%rax
这个8字节寄存器中(取的是8字节的数据,容器的量也是8字节,肯定没有问题)。
接上一条指令,movq %rax, (%rsi)
,首先会取出8字节的数据来,假设%rsi
寄存器存的是一个指针,地址为B,配合movq(q为四字,8字节),这条指令会取出的8个字节的数据,分别放到B,B+1,B+2…B+6,B+7这8个字节中。
注意内存引用时,括号内的寄存器必为8字节寄存器,且这8个字节存的是一个指针。