【CSAPP-深入理解计算机系统】3-7. 过程(函数调用)_哔哩哔哩_bilibili
【CSAPP-深入理解计算机系统】3-6.跳转指令与循环_哔哩哔哩_bilibili
跳转指令、
接下来看下跳转指令相关代码及汇编指令:
long absdiff_se(long x, long y)
{
long result;
if (x < y)
{
result = y - x;
}
else
{
result = x - y;
}
return result;
}
汇编后得到下面指令:
_Z10absdiff_sell:
cmpq %rsi, %rdi
jge .L2
movq %rsi, %rax
subq %rdi, %rax
ret
.L2:
movq %rdi, %rax
subq %rsi, %rax
ret
通过cmp
指令首先设置x<y
对应的标志寄存器的符号标志(SF)和溢出标志(OF),然后跳转指令进行相应的位运算来判断其布尔值的真假,以此来判断是否发生跳转至.L2
处,位运算计算方法如下图:

只不过,相比于代码中的x < y,汇编指令通过jge判断x是否大于等于y,ge是greater >和equal =的缩写。对于代码中的if-else语句,当满足条件时,程序洽着一条执行路径执行,当不满足条件时,就走另外一条路径。这种机制比较简单和通用,但是在现代处理器上,它的执行效率可能会比较低。针对这种情况,有一种替代的策略,就是使用数据的条件转移来代替控制的条件转移。还是针对两个数差的绝对值问题,给出了另外一种实现方式,我们既要计算y-x的值,也要计算x-y的值,分别用两个变量来记录结果,然后再判断x与y的大小,根据测试情况来判断是否更新返回值。这两种写法看上去差别不大,但第二种效率更高。具体如下所示:

上图c前面这几条指令都是普通的数据传送和减法操作。cmovge
是根据条件码的某种组合来进行有条件的传送数据,当满足规定的条件时,将寄存器rdx
内的数据复制到寄存器rax
内。在这个例子中,只有当x
大于等于y
时,才会执行这一条指令。

更多指令如下所示:

为什么基于条件传送的代码会比基于跳转指令的代码效率高呢?这里涉及到现代处理器通过流水线来获得高性能。当遇到条件跳转时,处理器会根据分支预测器来猜测每条跳转指令是否执行,当发生错误预测时,会浪费大量的时间,导致程序性能严重下降。
循环、
汇编指令中并没有专门用于循环的指令,循环的实现是通过跳转和条件判断实现的。



对比一下for
与while
的汇编代码:

可以发现除了这一句跳转指令不同,其他部分都是一致的。这两个汇编代码是采用-Og
选项产生的。综上所述,三种形式的循环语句都是通过条件测试和跳转指令来实现。以上则是三种循环的示例说明。
swich语句、

对于上面的代码,汇编代码如下:

cmpq
指令设置状态寄存器,ja
指令判断n是否超过6
,超过的话跳转至default
对应的L8程序段,case0、case6可通过跳转表访问不同分支。代码跳转表声明为一个长度为7的数组,每个元素都是一个指向代码位置的指针,具体关系如下图所示:



在这个例子中,程序使用跳转表来处理多重分支,甚至当switch有上百种情况时,虽然跳转表的长度会增加,但是程序的执行只需要一次跳转也能处理复杂分支的情况,与使用一组很长的if-else相比,使用跳转表的优点是执行switch语句的时间与case的数量是无关的。因此在处理多重分支的时,与一组很长的if-else相比,switch的执行效率要高。
程序调用过程相关知识、
为了方便讨论,以C语言代码函数调用为例,假设函数P调用函数Q,函数Q执行完毕后返回函数P,这一系列操作包括图中一个或多个机制:

C语言过程调用机制的关键特性在于使用栈数据结构提供FIFO内存管理原则,在过程P调用过程Q的例子中,可以看到当Q在执行时,P以及所有在向上追溯到P的调用链中的过程都被暂时挂起。当Q运行时,它只需要为局部变量分配新的存储空间,或设置到另一个过程的调用。另一方面,当Q返回时,任何它所分配的局部存储空间都可被释放。因此,程序可以用栈来管理它的过程所需要的存储空间,栈和程序寄存器存放着传递控制和数据、分配内存所需要的信息。当P调用Q时,控制和数据信息添加到栈尾。当P返回时,这些信息会被释放掉。

函数P
调用函数Q
时,会把返回地址压入栈中,该地址指明了当函数Q
执行结束 返回时要从函数P
的哪个位置继续执行。这个返回地址的压栈操作并不是由指令push
来执行的,而是由函数调用call
来实现的。具体以multstore
代码为例我们可以查看返回地址细节:


编译并使用命令objdump进行反汇编,查看其具体调用情况:

上图可知中4005a9: e8 26 00 00 00 callq 4005d4 <multstore>
这一行,指令call
不仅要将函数multstore
的第一条指令的地址写入到程序指令寄存器rip
中,以此实现函数调用,同时还要将返回地址压入栈中。

当函数multstore
调用完毕后,指令ret
从栈中返回地址弹出,写入程序指令寄存器rip
中:

函数返回,继续执行上面反汇编代码中main
函数第7行相关的操作。以上整个过程就是函数调用与返回所涉及的操作。那么函数调用的参数传递是如何实现的呢?在一开始我们知道,函数传递参数分别通过6个寄存器可以实现,但是如果传递的参数大于6个呢?超出的参数就会通过压栈来实现存储。


代码中函数有8个参数,包括字节数不同的整数以及不同类型的指针,参数1到参数6是通过寄存器来传递,参数7和参数8是通过栈来传递。

这里补充说明:
通过栈来传递参数时,所有数据的大小都是向8的倍数对齐,虽然变量a4
只占一个字节,但是仍然为其分配了8
个字节的存储空间。由于返回地址占用了栈顶的位置,所以这两个参数距离栈顶指针的距离分别为8
和16
。


栈局部存储:
当代码中对一个局部变量使用地址运算符时,我们需要在栈上为这个局部变量开辟 相应的存储空间,接下来我们看一个与地址运算符相关的例子。


函数caller
定义了两个局部变量arg1
和arg2
,函数swap
的功能是交换这两个变量的值,最后返回二者之和。我们通过分析函数caller
的汇编代码来看一下地址运算符的处理方式:

subq $16, %rsp
第一条减法指令将栈顶指针减去16
,它表示的含义是在栈上分配16
个字节的空间,以此来保存两个局部变量的值。
我们再来看一个较为复杂的栈上存放局部变量的例子,代码如下:

根据上面的C
代码,我们来画一下这个函数的栈帧。根据变量的类型可知x1
占8
个字节,x2
占4
个字节,x3
占两个字节,x4
占一个字节,因此,这四个变量在栈帧中的空间分配如图所示。

由于上面call_proc
代码中第6行调用的函数proc
需要8
个参数,前六个参数保存在寄存器上,因此参数7
和参数8
需要通过栈帧来传递。注意,传递的参数需要8
个字节对齐,而局部变量是不需要对齐的。


Comments NOTHING