失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > 浅析Intel处理器体系结构:函数调用实现

浅析Intel处理器体系结构:函数调用实现

时间:2019-01-24 04:52:58

相关推荐

浅析Intel处理器体系结构:函数调用实现

文章目录

概述函数调用过程使用CALL和RET指令的调用过程近调用执行过程远调用执行过程 参数传递返回值 运行时堆栈栈帧局部变量在栈上的存储 寄存器使用规则x86体系寄存器使用规则x86_64体系寄存器器使用规则调用者保存与被调用者保存规则 函数调用实例相关参考

概述

函数调用是现代绝大多数编程语言实现所依赖的基础抽象机制。函数调用通过使用一组指定的参数和可选的返回值实现了某种功能,然后,可以在程序的不同地方进行调用这个过程以实现特定的需求。为了实现函数调用,各个硬件体系都约束了在函数调用过程需要遵循的一系列规则,包括控制转移、硬件寄存器使用、参数传递以及返回值处理等,本文着重关注于Intel体系结构下的函数调用机制实现。

函数调用过程

Intel处理器支持两种类型的函数调用指令:

CALL和RET指令;ENTER和LEAVE指令。

使用CALL和RET指令的调用过程

CALL指令根据执行过程是否会切换代码段,可分为近调用(near call)和远调用(far call),在汇编代码中,远调用会增加far标记,即CALLF,对应的RET返回指令为RETF。

近调用执行过程

Intel处理器执行近调用的堆栈变化示意如下:

当执行近调用指令时,处理器执行以下操作:

将当前EIP寄存器值压入到栈上;加载被调用过程的指令地址到EIP寄存器。执行被调用过程;

被调用过程处理完后,执行近返回指令,处理器执行以下操作:

弹出栈顶的值到EIP寄存器;根据RET指令的参数,调整堆栈位置(释放存放在堆栈上的函数传入参数);恢复调用过程执行 。

远调用执行过程

Intel处理器执行近调用的堆栈变化示意如下:

远程调用由于涉及到代码段的变化,因此在处理过程中需要保存和恢复CS寄存器的值。当执行远调用指令时,处理器执行动作如下:

将当前CS寄存器的值压入到堆栈中;将当前EIP寄存器的值压入到堆栈中;加载被调用过程的段选择子到CS寄存器;加载被调用过程的偏移地址到EIP寄存器;执行被调用过程

被调用过程处理完后,执行远返回指令,处理器执行以下操作:

弹出当前栈顶(存储返回指令地址)的值到EIP寄存器继续弹出当前栈底(存储调用过程的段选择子)到CS寄存器;根据RET指令的参数,调整堆栈位置;恢复调用过程执行。

参数传递

在过程调用过程中,Intel体系结构支持使用两种方式进行函数参数传递:

使用通用寄存器:将参数存放在寄存器中,函数执行时,访问特定的寄存器以获取参数数据;使用堆栈:寄存器数量是有限的,如果需要将大量参数传递给被调用过程,可以将参数放在堆栈中进行传递。当然,相对于使用寄存器,堆栈的传递方式在效率上要低一些。

现代的硬件体系结构下,通常是混合使用了寄存器传参和堆栈两种方式,在通用寄存器数量足够的情况下,优先使用寄存器进行传参,当参数数量过多,则将多余的参数通过堆栈的方式传递。考虑上图中的函数调用栈,我们是从第7个函数参数进行统计的,这就是因为其它的参数使用特定的寄存器进行传递了,x86_64体系结构可支持使用最多6个寄存器来传递参数给被调用函数。

返回值

通常情况下,函数参数的返回值都是通过一个单独的寄存器进行存放。在x86体系下,使用eax寄存器存放返回值,到了x86_64体系中,使用扩展成64位长度的rax寄存器。

运行时堆栈

过程调用的过程中,有各种数据需要进行处理,典型的数据包括函数参数、局部变量、函数返回值处理等等。此外,函数需要支持嵌套调用,并在子函数调用返回后恢复执行函数的上下文状态。综合这些特性,现代体系结构普遍使用堆栈来实现函数调用。如下分别是x86和x86_64体系结构下,函数运行时堆栈的模型图:

栈帧

这里牵涉到一个重要的概念:栈帧。对于每一个过程调用,系统都会为其分配一个栈帧,用于保存函数执行过程中所要处理的数据。栈帧是函数调用栈中的一段,它包含起始地址和结束地址,在x86_64体系下,栈帧的起始地址保存在rbp寄存器(帧指针)中,结束地址存放在rsp寄存器(栈指针)中。

在实际运行时,帧指针和栈指针的分工是比较明确的:

帧指针的值通常保持不变,程序使用帧指针加偏移的方式,用来寻址函数参数、局部变量等数据栈指针则用来控制栈帧中内存的申请和释放,减小栈指针的值则相当于分配内存,增大栈指针的值则等同于释放内存

局部变量在栈上的存储

一个函数在执行过程中使用的局部变量的空间在编译期间就已经明确了,因此通过修改栈指针的值,就可以提前在堆栈中预留空间。

寄存器使用规则

对于Intel体系结构,函数调用过程中的寄存器使用都需要遵循统一的规则,对于32位体系和64位体系,规则略有差异。

x86体系寄存器使用规则

%eax作为函数返回值使用;%esp栈指针寄存器,指向栈顶;x86体系下,参数传递默认是通过堆栈实现的,除非使用GCC扩展显示指定使用寄存器传参,不然不会使用。

C语言函数参数压栈的顺序与形参定义顺序是相反的,即从函数的最后一个参数开始压栈

x86_64体系寄存器器使用规则

%rax:作为函数返回值使用;%rsp:栈指针寄存器,指向栈顶;%rdi,%rsi,%rdx,%rcx,%r8,%r9:用作函数参数,依次对应第1参数,第2参数。。。%rbx,%rbp,%r12,%r13,%14,%15:用作数据存储,遵循被调用者保存规则;%r10,%r11:用作数据存储,遵循调用者保存规则。

调用者保存与被调用者保存规则

调用者保存寄存器: 这类寄存器被视为易失的,因此在过程调用时视为已销毁。若要在过程调用之后恢复该值,则调用者有责任将这些寄存器进行保存;被调用者保存寄存器:这类寄存器被视为非易失的,必须由使用它们的函数进行保存和还原。简单来说,当调用者进行调用时,期望这些寄存器在被调用者返回后保持原值,则被调用者有义务保存并在返回调用者之前将其还原。

函数调用实例

考虑一个简单的函数调用示例:

#include <stdio.h>#include <stdlib.h>#include <string.h>int do_foo_func(int arg1, int arg2){int result = 0;int val1 = arg1, val2 = arg2;result = val1 + val2;printf("The result is %ld.\n", result);return result;}int main(int argc, char *argv[]){int result = 0;result = do_foo_func(4, 9);return result;}

在此,我们关注do_foo_func函数的栈帧形成,查看do_foo_func的反汇编实现(补充说明一点,这里的程序是使用-O0选项(即不优化)进行编译的。在gcc更高的优化等级下,rbp寄存器已经没有再作为帧指针使用,而是另作他用):

顺着函数do_foo_func的汇编指令代码,我们可以总结出x86_64体系下过程调用实现的一些基本步骤:

备份帧指针(rbp寄存器)的值(一般来说,rbp寄存器保存了上层过程调用的帧指针的值),设置帧指针的值为当前栈指针

push %rbpmov %rsp, %rbp

根据过程调用内存的使用情况,调整栈指针的值,预留空间

sub $0x20, %rsp

使用栈帧中保存的数据,继续后续指令的执行

使用帧指针中的值,恢复栈指针,完成当前过程调用栈帧的释放。然后从堆栈中弹出帧指针的原先值,用以恢复帧指针

leaveq/* leave指令是x86_64体系新加入的指令,用于执行过程返回前的准备工作 */

过程调用返回

retq

相关参考

《深入理解计算机系统》《Intel处理器手册》《Linux汇编语言》

如果觉得《浅析Intel处理器体系结构:函数调用实现》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。