第3章“程序的机器级表示”:过程

news2025/1/8 20:34:50

文章目录

  • 3.7 过程
    • 3.7.1 栈帧
    • 3.7.2 转移控制
    • 3.7.3 寄存器使用惯例
    • 3.7.4 过程示例
    • 3.7.5 递归过程

3.7 过程

一个过程调用包括将数据(以过程参数和返回值的形式)和控制从代码的一部分传递到另一部分。另外,它还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。大多数机器,包括 IA32,只提供简单的转移控制到过程和成过程中转移出控制的指令。数据传递、局部变量的分配和释放是通过操纵程序栈来实现的。

3.7.1 栈帧

IA32 程序用程序栈来支持过程调用。栈用来传递过程参数、存储返回信息、保存寄存器以供以后恢复之用,以及用于本地存储。

为单个过程分配的那部分栈称为栈帧(stack frame)。

栈帧的通用结构:
在这里插入图片描述
栈帧的最顶端是以两个指针定界的,寄存器 %ebp 作为 帧指针,而寄存器 %esp 作为 栈指针。当程序执行时,栈指针是可以移动的,因此大多数信息的访问都是相对于帧指针的。

假设过程 P(调用者)调用过程 Q(被调用者)。Q的参数放在 P 的栈帧中。另外,当 P 调用 Q 时,P 中的返回地址被压入栈中,形成 P 的栈帧的末尾,返回地址就是当程序从 Q 返回时应该继续执行的地方。Q 的栈帧从保存的帧指针的值(如 %ebp)开始,后面是保存的其他寄存器的值。

过程 Q 也用栈来保存其他不能存放在寄存器中的局部变量。这样做是因为:

  • 寄存器不够存放所有的变量。
  • 有些局部变量是数组或结构,因此必须通过数组或结构引用来访问。
  • 要对一个局部变量使用地址操作符 “&”,因此我们必须能够为它产生一个地址。

最后,Q 会用栈帧来存放它调用其他过程的参数。

如前文所讲,栈向低地址方向增长,而栈指针 %esp 指向栈顶元素。 可以通过 pushlpopl 指令将数据存入栈中和从栈中取出。可以通过将栈指针的值减小适当的值来分配没有指定初始值的数据的空间。类似地,可以通过增加栈指针来释放空间。

3.7.2 转移控制

下表是支持过程调用和返回的指令:

指令描述
call Label过程调用
call *Operand过程调用
leave为返回准备栈
ret从过程调用中返回
  • call 指令有一个目标,指明被调用过程起始的指令地址。同跳转一样,调用可以是直接的,也可以是间接的。在汇编代码中,直接调用的目标是一个标号,而间接调用的目标是 * 后面跟一个操作数指示符,其语法与 movl 指令的操作数的语法相同。
  • call指令的效果是将返回地址入栈,并跳转到被调用过程的起始处。返回地址是紧跟在程序中 call 后面的那条指令的地址,这样当被调用过程返回时,执行会从此继续。
  • ret 指令从栈中弹出地址,并跳转到那个位置。要正确使用这条指令,就要使栈准备好,栈指针要指向前面 call 指令存储返回地址的位置。
  • leave 指令可以用来使栈做好返回的准备。它等价于下面的代码序列:
    在这里插入图片描述

另外,这种准备工作也可以通过直接使用传送和弹出操作来完成。

寄存器 %eax 可以用来返回值,如果函数要返回整数或指针的话。

练习:

下面的代码片段常常出现在库函数的编译版本中:

	call next
next:
	popl %eax

Question:

  1. 寄存器 %eax 设置成了什么值?
  2. 解释为什么这个调用没有匹配的 ret 指令。
  3. 这段代码完成了什么功能?

Answer:

这是一个汇编代码的习惯用法。刚开始,它看起来很奇怪——call 指令没有与之匹配的 ret。然后就意识到它根本就不是一个真正的过程调用。

  1. %eax 被设置成 popl 指令的地址。
  2. 这不是一个真正的子过程调用,因为控制是按照与指令相同的顺序进行的,而返回值是从栈中弹出的。
  3. 这是 IA32 中将程序计数器的值放到整数寄存器中的唯一方法。

3.7.3 寄存器使用惯例

程序寄存器组是唯一一个被所有过程共享的资源。 虽然在给定时刻只能有一个过程是活动的,但是必须保证当一个过程(调用者)调用另一个(被调用者)时,被调用者不会覆盖某个调用者稍后会使用的寄存器的值。为此,IA32 采用了一组统一的寄存器使用惯例,所有的过程都必须遵守,包括程序库中的过程。

根据惯例,寄存器 %eax、%edx 和 %ecx 被划分为调用者保存(caller save)寄存器。当过程 P 调用 Q 时,Q 可以覆盖这些寄存器,而不会破坏任何 P 所需要的数据。

另外,寄存器 %ebx、%esi 和 %edi 被划分为 被调用者保存(callee save)寄存器。这意味着 Q 必须在覆盖它们之前,将这些寄存器的值保存到栈中,并在返回前恢复它们,因为 P (或某个更高层次的过程)可能会在今后的计算中需要这些值。

此外,根据这里描述的惯例,必须保持寄存器 %ebp 和 %esp。

为什么叫做 “被调用者保存” 和 “调用者保存”?

考虑如下场景:

int P()
{
	int x = f(); /* Some computation */
	Q();
	return x;
}

过程 P 希望它计算出来的 x x x 的值在调用了 Q 之后仍然有效。

如果 x x x 放在一个调用者保存寄存器中,而 P (调用者)必须在调用 Q 之前保存这个值,并在 Q 返回后恢复该值。

如果 x x x 在一个被调用者保存寄存器中,Q(被调用者)想使用这个寄存器,那么 Q 在使用这个寄存器之前,必须保存这个值,并在返回前恢复它。

在这两种情况中,保存就是将寄存器值压入栈中,而恢复是指从栈中弹出到寄存器中。


示例:

int P()
{
	int y = x * x;
	int z = Q(y);
	
	return y + z;
}

过程 P 在调用 Q 之前计算 y y y,但是它必须保证 y y y 的值在 Q 返回后是可用的。有两种方式可以做到:

  • 它可以在调用 Q 之前,将 y y y 的值存放在自己的栈帧中;当 Q 返回时,它可以从栈中取出 y y y 的值。
  • 它可以将 y y y 的值保存在被调用者保存寄存器中。如果 Q,或其他任何 Q 调用的程序,想使用这个寄存器,它必须将这个寄存器的值保存在栈帧中,并在返回前恢复该值。因此,当 Q 返回到 P 时, y y y 的值会在被调用者保存寄存器中,或者是因为寄存器根本就没有改变,或者是因为它被保存并恢复了。

最常见的是,GCC 使用后一种方法,因为它会尽量减少写和读栈的次数。

3.7.4 过程示例

示例,考虑如下定义的C过程。

int swap_add(int *xp, int *yp)
{
	int x = *xp;
	int y = *yp;
	
	*xp = y;
	*yp = x;
	return x + y;
}

int caller()
{
	int arg1 = 534;
	int arg2 = 1057;
	int sum = swap_add(&arg1, &arg2);
	int diff = arg1 - arg2;

	return sum * diff;
}

下图是这两个过程的栈帧:
在这里插入图片描述
swap_add 从 caller 的栈帧中取出它的参数。这些参数的位置的访问都是相对于寄存器 %ebp 中的帧指针的。帧左边的数字表示相对于帧指针的地址偏移。

caller 的栈帧包括局部变量 arg1arg2 的存储,其位置相对于帧指针是-8 和 -4。这些变量必须存在栈中,因为必须为它产生地址。

如下这段来自 caller 编译过的汇编代码显示出它是如何调用 swap_add 的。

在这里插入图片描述
注意,这段代码计算的是局部变量 arg2arg1 的地址(用 leal 指令),并将它们压入栈中。然后再调用 swap_add

swap_add 编译过的代码有三个部分:

  • “建立”部分,初始化栈帧;
  • “主体”部分,执行过程的实际计算;
  • “结尾”部分,恢复栈的状态和过程返回。

swap_add 建立部分

如下是 swap_add 的建立代码。回想一下,call 指令已经将返回地址压入栈中。
在这里插入图片描述
过程 swap_add 需要用寄存器 %ebx 作为临时存储。因为这是一个被调用保存的寄存器,它会将旧值作为栈帧建立的一部分压入栈中。

swap_add 的主体部分

如下是 swap_add 的主体代码:
在这里插入图片描述
这段代码从 caller 的栈帧中取出它的参数。因为帧指针已经移动了,这些参数的位置已经从相对于 %ebp 的旧值的位置-12 和 -6 移到了相对于 %ebp 的新值的位置 +12 和 +8。注意,变量 x x x y y y 的和是存放在寄存器 %eax 中作为返回值传递的。

swap_add的结尾部分

如下是 swap_add 的结尾代码:
在这里插入图片描述
这段代码就是恢复三个寄存器 %ebx、%esp 和 %ebp 的值,然后执行 ret 指令。注意,可以用一个条 leave 指令代替指令 13 和 14。不同版本的 GCC 对此可能有不同的习惯。

caller 中跟在 swap_add 指令后的指令

下面的 caller 中的代码紧跟在调用 swap_add 的指令后面:
在这里插入图片描述
从 swap_add 返回时,过程 caller 会从这条指令开始继续执行。注意,这条指令将返回值从 %eax 拷贝到另一个寄存器。

3.7.5 递归过程

上一节中描述的栈和寄存器惯例使得过程能够递归地调用它们自身。因为每个调用在栈中都有它自己的私有空间,多个未完成调用的局部变量不会相互影响。此外,栈的原则很自然地就提供了适当的策略,当过程被调用时分配局部存储(storage),当返回时释放存储。

下面是递归的 Fibonacci 函数的 C 代码。(效率很低)

int fib_rec(int n)
{
	int prev_val, val;
	if (n <= 2)
		return 1;
	prev_val = fib_rec(n - 2);
	val = fib_rec(n - 1);
	return prev_val + val;
}

完整的汇编代码如下:
在这里插入图片描述

  • 建立代码(第 2 ~ 6 行)创建一个栈帧,其中包含 %ebp 的旧值、未使用的 16 个字节、保存的被调用者保存寄存器 %esi 和 %ebx 的值,如下图左边所示。然后它用寄存器 %ebx 来保存过程参数 n n n (第 7 行)。一旦满足中止条件,代码会跳转到第 22 行,在此将返回值设为1。
  • 对于不满足中止条件的情况,指令 10 ~ 12 会进行第一次递归调用。这包括在栈中分配不会被使用的 12 个字节,然后将计算出来的值 n − 2 n-2 n2 压入栈中。此时,栈帧如下图右边所示。然后,它会进行递归调用,引起一连串的调用、分配栈帧、对局部存储进行操作,等等。每次调用返回时,它都会释放栈空间,恢复所有被修改过的被调用者保存寄存器。因此,当我们返回到当前调用时(第14行),我们可以假设寄存器 %eax 包含着递归调用返回的值,而寄存器 %ebx 包含函数参数 n n n 的值。返回值(C代码中的局部变量 prev_val)存放在寄存器 %esi 中(第14行)。通过使用被调用者保存寄存器,能保证在第二次调用调用后这个值仍然是可用的。
  • 指令 15 ~ 17 进行第二次递归调用。它会再次分配不会被使用的 12 个字节,并将值 n − 1 n-1 n1 压入栈中。在这个调用之后(第18行),计算出来的结果会放在寄存器 %eax 中,而假设前一次调用的结果放在寄存器 %esi 中。两者相加得到返回值(第19行)。
  • 完成代码恢复寄存器和释放栈帧。它首先将栈帧设置为保存的 %ebx 值的位置。注意,通过计算相对于 %ebp 值的栈的位置,无论是否满足中止条件,计算都会是正确的。
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/602601.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

金融人不能错过的中国人民大学与加拿大女王大学金融硕士,你不能不知道

金融行业是一个发展飞速的行业&#xff0c;越来越多的优秀人士的涌入&#xff0c;让本就卷起来的金融行业变得异常拥挤&#xff0c;怎么办&#xff0c;想留有一席之地只能不断的提升与攀登&#xff0c;金融人不能错过的中国人民大学与加拿大女王大学金融硕士&#xff0c;你不能…

架构EA演进

架构演进 目录概述需求&#xff1a; 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,wait for change,challenge Survive. happy for hardess to solve den…

spring cloud搭建(service)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

【C++】海量数据处理面试题(位图和布隆过滤器)

都是大厂面试题哦~ 文章目录 一.位图面试题 1.给定100亿个整数&#xff0c;设计算法找到只出现一次的整数 2.给两个文件&#xff0c;分别有100亿个整数&#xff0c;我们只有1G内存&#xff0c;如何找到两个文件交集&#xff1f; 3.1个文件有100亿个int&#xff0c;1G内…

等价类,边界值,场景法的使用方法和运用场景

等价类&#xff1a; 在很多情况下&#xff0c;很多人想到的测试方法是穷举测试&#xff0c;穷举测试是最全面的测试&#xff0c;但是数据量很大的情况下不太现实&#xff0c;测试效率太低&#xff0c;后来为了减少测试人员的工作量和提高测试的效率和以达到最好的测试质量&…

启明星辰集团CEO严望佳:与AI共生,共建以人为本的数字善治生态体系

近日&#xff0c;2023中国国际大数据产业博览会在贵阳成功召开。启明星辰集团董事长兼首席执行官严望佳应邀出席大会“数据安全产业高质量发展”高端对话&#xff0c;发表“主动应对ChatGPT技术冲击&#xff0c;加强数据安全风险防控”主题演讲&#xff0c;同与会人士共探数据安…

32.有序序列插入一个整数(刷题)

描述 有一个有序数字序列&#xff0c;从小到大排序&#xff0c;将一个新输入的数插入到序列中&#xff0c;保证插入新数后&#xff0c;序列仍然是升序。 输入描述&#xff1a; 第一行输入一个整数N(0≤N≤50)。 第二行输入N个升序排列的整数&#xff0c;输入用空格分隔的N个…

2023年前端面试题汇总-浏览器原理

1. 浏览器安全 1.1. 什么是 XSS 攻击&#xff1f; 1.1. 1. 概念 XSS 攻击指的是跨站脚本攻击&#xff0c;是一种代码注入攻击。攻击者通过在网站注入恶意脚本&#xff0c;使之在用户的浏览器上运行&#xff0c;从而盗取用户的信息如 cookie 等。 XSS 的本质是因为网站没有对…

企业要从哪些方面着手进行数据安全治理?

什么是数据安全治理&#xff1f; 数据安全治理是指组织基于业务发展与合规要求&#xff0c;制定全面且系统的数据安全策略、流程与技术措施&#xff0c;对数据生命周期中的安全风险进行管控与优化的一系列管理活动。它需要从组织层面建立数据安全管理框架&#xff0c;保证敏感数…

2023-6-2-DIS研究

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f4a5;&#x1f4a5;&#x1f4a5;欢迎来到&#x1f91e;汤姆&#x1f91e;的csdn博文&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f49f;&#x1f49f;喜欢的朋友可以关注一下&#xf…

Java 列表导出

一、具体实现 import java.net.URLEncoder; import com.alibaba.excel.EasyExcel;List<实体> targets xxx; response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("UTF-8"); String fileName URLEncoder.encode(&…

深入理解设计原则之里氏替换原则(LSP)

系列文章目录 C高性能优化编程系列 深入理解设计原则系列 深入理解设计模式系列 高级C并发线程编程 LSP&#xff1a;里氏替换原则 系列文章目录1、里氏替换原则的定义和解读2、里氏替换原则可以用于哪些设计模式中&#xff1f;3、如何使用里氏替换原则来降低代码耦合度&#…

Julia系列14:调用自定义C/C++库

1. 基础调用&#xff1a;ccall 调用的基本格式为&#xff1a; ccall((:函数名, 库地址), 输出格式, (输入格式列表), 输入数据) 下面是例子&#xff1a; 1.1 基础数据结构 1.2 数组 首先是输入数组&#xff0c;注意需要convert 接着是输出数组&#xff0c;需要使用unsafe…

《操作系统》—— 处理机调度算法

前言&#xff1a; 在之前的文章中&#xff0c;我们已经了解了进程和线程相关的基本概念&#xff0c;今天我们将要了解的是关于处理机调度相关的知识。 目录 &#xff08;一&#xff09;调度的概念 1、调度的基本概念 2、调度的层次 3、三级调度的关系 &#xff08;二&…

遗传算法(Genetic Algorithm)

本文为阅读《遗传算法原理及应用》的笔记和心得 ISBN&#xff1a;7-118-02062-1 遗传算法简介 遗传算法是模拟生物在自然环境中的遗传和进化过程中而形成的一种自适应全局优化概率搜索算法 总的来说&#xff0c;求最优解解或近似最优解的方法主要有三种&#xff1a;枚举法、启…

【PCB专题】Allegro设置禁止铺铜区域但仍可以走线和打过孔

在PCB设计中我们有时候需要做一些净空区,但是净空区内有一些走线和过孔。如果使用Route Keepout画一个框的话,那是不允许走线和打过孔的,会报DRC。 那么如何才能既禁止区域铺铜,又可以走线和打过孔不报DRC呢? Setup->Areas->Shape Keepout Options选择要禁止…

第二十一篇、基于Arduino uno,控制有源蜂鸣器和无源蜂鸣器发出声音——结果导向

0、结果 说明&#xff1a;有源蜂鸣器按照一定的频率报警&#xff0c;无源蜂鸣器则是一直报警&#xff0c;都采用非阻塞方式编写&#xff0c;如果是你想要的&#xff0c;可以接着往下看。 1、外观 说明&#xff1a;有源蜂鸣器和无源蜂鸣器看上去一样&#xff0c;但是背面不一…

详解Handler

详解Handler 文章目录 详解Handler1.Handler的工作流程1.1主线程具有如上性质的原因1.2流程图 2.Handler流程中的重要的几个方法2.1Message中的属性2.2.1what2.2.2replyTo2.2.3obtain 2.2Handler.post()与Handler.sendMessage()2.2.1post的源码2.2.1.1sendMessageDelayed()源码…

centos6离线安装docker

参考 RedHat 6.8 离线安装Docker &#xff08;rpm包安装&#xff09; - 神奇二进制 - 博客园 (cnblogs.com) 可参考&#xff0c;但本次安装未参考 CentOS6 完全离线安装Docker - 简书 (jianshu.com) 走了一遍&#xff0c;大雾 (1条消息) 离线安装Docker_洒家肉山大魔王的博客…

萌啦科技参加ICBE跨境电商博览会完美落幕,期待再相会!

“ 萌啦科技联合DNY123、喜运达物流共同亮相2023 ICBE跨境电商博览会&#xff0c;更全面地服务东南亚电商卖家&#xff0c;把握新兴市场电商发展商机&#xff01;” 跨境电商“万人”博览会 5月15日-5月17日&#xff0c;ICBE国际跨境电商交易博览会在广州琶洲保利世贸博览馆隆重…