汇编中的函数调用与递归

栈帧的结构

 

  倘若我们要想搞清楚过程的实现,就必须先知道栈帧的结构是如何构成的。栈帧其实可以认为是程序栈的一段,而程序栈又是存储器的一段,因此栈帧说到底还是存储器的一段。那么既然是一段,肯定有两个端点,这个不需要LZ再普及了吧。

  这两个端点其实就是两个地址,一个标识着起始地址,一个标识着结束地址,而这两个地址,则分别存储在固定的寄存器当中,即起始地址存在%ebp寄存器当中,结束地址存在%esp寄存器当中。至于为什么要存在这两个寄存器当中,就像程序的下一条指令地址为什么存在PC当中一样,是毫无意义的问题,就是这样规定的,没有为什么。

  起始地址和结束地址还有另外的名字,起始地址通常称为帧指针,结束地址通常称为栈指针(也就是栈顶的地址)。因此,我们就把过程的存储器内存使用区域称为栈帧。这下我们就了解了栈帧的来历以及它们的命名习惯和存储惯例,接下来是LZ画的一幅图,它揭示了栈帧在存储器当中的位置。

  这个图基本上已经包括了程序栈的构成,它由一系列栈帧构成,这些栈帧每一个都对应一个过程,而且每一个帧指针+4的位置都存储着函数的返回地址,每一个帧指针指向的存储器位置当中都备份着调用者的帧指针。各位需要知道的是,每一个栈帧都建立在调用者的下方(也就是地址递减的方向),当被调用者执行完毕时,这一段栈帧会被释放。还有一点很重要的是,%ebp和%esp的值指示着栈帧的两端,而栈指针会在运行时移动,所以大部分时候,在访问存储器的时候会基于帧指针访问,因为在一直移动的栈指针无法根据偏移量准确的定位一个存储器位置。

  还有一点比较重要的内容,就是栈帧当中内存的分配和释放。由于栈帧是向地址递减的方向延伸,因此如果我们将栈指针减去一定的值,就相当于给栈帧分配了一定空间的内存。这个理解起来很简单,因为在栈指针向下移动以后(也就是变小了),帧指针和栈指针中间的区域会变长,这就是给栈帧分配了更多的内存。相反,如果将栈指针加上一定的值,也就是向上移动,那么就相当于压缩了栈帧的长度,也就是说内存被释放了。需要注意的是,上面的一切内容,都基于一个前提,那就是帧指针在过程调用当中是不会移动的。

 

过程的实现

 

  过程虽然很好,但想要实现过程,还是存在一定难度的,尽管现在看来它并不困难。它实现的难度主要就在于数据如何在调用者和被调用者之间传递,以及在被调用者当中局部变量内存的分配以及释放。

  不过天大的难题都难不倒那群计算机界的大神们,他们找出了一种方式,可以简单并有效的处理过程实现当中的难题。这一切似乎看起来十分偶然,但其实也是必然的。世间的很多规律都是客观存在的,只是它在等着我们去发现而已。

  总的来说,过程实现当中,参数传递以及局部变量内存的分配和释放都是通过以上介绍的栈帧来实现的,大部分情况下,我们认为过程调用当中做了以下几个操作。

  1、备份原来的帧指针,调整当前的帧指针到栈指针的位置,这个过程就是我们经常看到的如下两句汇编代码做的事情。

    pushl    %ebpmovl    %esp, %ebp

  2、建立起来的栈帧就是为被调用者准备的,当被调用者使用栈帧时,需要给临时变量分配预留内存,这一步一般是经过下面这样的汇编代码处理的。

    subl    $16,%esp

  3、备份被调用者保存的寄存器当中的值,如果有值的话,备份的方式就是压入栈顶。因此会采用如下的汇编代码处理。

    pushl    %ebx

  4、使用建立好的栈帧,比如读取和写入,一般使用mov,push以及pop指令等等。

  5、恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了。因此在恢复时,大多数会使用pop指令,但也并非一定如此。

  6、释放被调用者的栈帧,释放就意味着将栈指针加大,而具体的做法一般是直接将栈指针指向帧指针,因此会采用类似下面的汇编代码处理(也可能是addl)。

    movl    %ebp,%esp

  7、恢复调用者的栈帧,恢复其实就是调整栈帧两端,使得当前栈帧的区域又回到了原始的位置。因为栈指针已经在第六步调整好了,因此此时只需要将备份的原帧指针弹出到%ebp即可。类似的汇编代码如下。

    popl    %ebp

  8、弹出返回地址,跳出当前过程,继续执行调用者的代码。此时会将栈顶的返回地址弹出到PC,然后程序将按照弹出的返回地址继续执行。这个过程一般使用ret指令完成。

  过程的实现大概就是以上八个步骤组成的,不过这些步骤并不都是必须的(大部分时候,开启编译器的优化会优化掉很多步骤),而且第6和第7步有时会使用leave指令代替。这里猿友们可以先了解一下这些步骤,在接下来的内容当中,还会有这几个步骤的详细示例。

 

过程相关指令:call、leave、ret

 

  由于过程调用当中会经常见到几个新的指令,因此在这里,LZ先给大家介绍一下这三个指令。它们三个都是过程实现当中非常重要的角色,这三个指令很类似,因为它们都是一个指令做了两件事,这里LZ就依次介绍一下它们各自都做了什么事。

  call指令:它一共做两件事,第一件是将返回地址(也就是call指令执行时PC的值)压入栈顶,第二件是将程序跳转到当前调用的方法的起始地址。第一件事是为了为过程的返回做准备,而第二件事则是真正的指令跳转。

  leave指令:它也是一共做两件事,第一件是将栈指针指向帧指针,第二件是弹出备份的原帧指针到%ebp。第一件事是为了释放当前栈帧,第二件事是为了恢复调用者的栈帧。

  ret指令:它同样也是做两件事,第一件是将栈顶的返回地址弹出到PC,第二件事则是按照PC此时指示的指令地址继续执行程序。这两件事其实也可以认为是一件事,因为第二件事是系统自己保证的,系统总是按照PC的指令地址执行程序。

  可以看出,除了call指令之外,leave和ret指令都与上面8个步骤有些不可分割的关系。call指令没有在8个步骤当中体现,是因为它发生在进入过程之前,因此在第1步发生的时候,call指令往往已经被执行了,并且已经为ret指令准备好了返回地址。

 

寄存器使用的规矩

 

  寄存器一共就8个,因此在数目上来说的话,使用起来肯定是捉襟见肘的。在这种情况下,就肯定需要一定的规矩去约束程序如何使用,否则要是一群人翻同一个人的牌子,那到底伺候谁才是呢。其实我们在之前已经或多或少的接触到了寄存器的规矩,比如%eax一般用于存储过程的返回值,%ebp保存帧指针,%esp保存栈指针。这里要介绍的,是另外一个规矩,而这个规矩是与过程实现相关的。

  试想一下,在调用一个过程时,无论是调用者还是被调用者,都可能更新寄存器的值。假设调用者在%edx中存了一个整数值100,而被调用者也使用这个寄存器,并更新成了1000,于是悲剧就发生了。当过程调用完毕返回后,调用者再使用%edx的时候,值已经从100变成了1000,这几乎必将导致程序会错误的执行下去。

  为了避免上面这种情况发生,就需要在调用者和被调用者之间做一个协调。于是便有了这样的规矩,它的描述如下,我们假设这里在过程P中调用了过程Q,P是调用者,Q是被调用者。

  %eax、%edx、%ecx:这三个寄存器被称为调用者保存寄存器。意思就是说,这三个寄存器由调用者P来保存,而对于Q来说,Q可以随便使用,用完了就不用再管了。

  %ebx、%esi、%edi:这三个寄存器被称为被调用者保存寄存器。同样的,这里是指这三个寄存器由被调用者Q来保存,换句话说,Q可以使用这三个寄存器,但是如果里面有P的变量值,Q必须保证使用完以后将这三个寄存器恢复到原来的值,这里的备份,其实就是上面那8个步骤中第3个步骤做的事情。

 

一个过程示例

 

  上面已经做好了充足的准备,接下来我们就要探索真理了,我们随便写一个调用过程的例子,LZ写了以下的代码来做这个十分重要的例子,我们称它为function.c。

复制代码
int add(int a,int b){register int c = a + b; return c; } int main(){ int a = 100; int b = 101; int c = add(a,b); return c; }
复制代码

  这里LZ为了完整的展现那8个步骤,因此给变量c加了register关键字修饰,这将会将c送入寄存器,从而更改被调用者保存寄存器,就会导致步骤3的发生。接下来我们就使用参数-S来编译这段代码,然后使用cat来看看这段代码的汇编形式。以下是main函数以及add函数各自的栈帧情况,LZ已经详细标记了它们属于哪个步骤。

  由于我们没有使用编译优化,因此汇编代码会多出很多,这也为了完整的诠释我们的步骤。可以看到,图中包含了完整的8个步骤,但是无论是main函数还是add函数,它们单独来讲,都没有完整的8个步骤,这其实是大多数的情况。大部分时候,一个函数不会完全包含上述的8个步骤。LZ这里不再一一拆分各个步骤,各位猿友可以严格按照各个指令的作用,自己画图理解一下这个过程,答案自会浮现。

  LZ这里只说几点各位需要注意的地方,首先第一点是,add函数会将返回结果存入%eax(前提是返回值可以使用整数来表示),在main函数中,call指令之后,默认将%eax作为返回结果来使用。第二点是,所有函数(包括main函数)都必须有第1步和第6、7、8步,这是必须的4步。最后一点是,我们的栈指针和帧指针有固定的大小关系,即栈指针永远小于等于帧指针,当二者相等时,当前栈帧被认为没有分配内存空间。

  还有一点十分有趣的事情,注意main函数当中100和101的传递过程,是先进入存储器,然后再进去寄存器,然后再进去存储器,准备作为add函数的参数。这一来一回产生了四次寄存器与存储器之间的数据传输,倘若我们加上-O1参数去编译这个程序,编译器将产生如下的汇编代码。

  可以看到,整个main函数的指令数骤降,100和101将直接进入存储器,准备作为add函数的参数。可见编译器的优化当中至少会有一项,就是减少数据的来回传输,增加效率。不过这一点其实与过程的实现没有什么关系,只是让以前可能不知道的猿友看一下,编译器其实会将我们的程序做很大的改动。

  

递归过程调用

 

  书中对递归调用还进行了说明,这是为了让我们相信,栈帧的建立和销毁惯例,可以保证递归过程的正常运行。其实如果各位猿友愿意一点一点的,将上面main函数和add函数的汇编代码搞清楚,那么递归调用其实也可以很轻松的搞定。因为指令就这么多了,只要严格按照-S编译出的汇编指令,一步一步的推算寄存器和存储器的状态,那么递归调用的实现也会自动浮现。

  LZ这里准备给各位猿友诠释一下递归的过程,各位猿友可以对照着上面的示例看一下,以下是一段简单的求n的阶乘的代码。

复制代码
int rfact(int n){int result; if(n<=1){ result = 1; }else{ result = n * rfact(n-1); } return result; }
复制代码

  接下来我们编译一下这段代码,使用-O1优化,我们可以得到如下的汇编代码。

  LZ在图中详细标注了各个步骤所做的事情,其实严格按照各个指令的作用分析,很轻松的就可以分析出图中的解释部分(即注释)。难点就在于,栈帧的变化是如何的,LZ这里就给各位演示一下栈帧的变化过程,如果各位已经把前面的那个main函数和add函数搞定了,那么可以在这里验证一下自己的理解是否正确。

  需要特殊说明的是,以上每一个栈帧(大括号括起来的),最上面(也就是地址递增方向)的都是帧指针位置,最下面的都是栈指针位置。然而寄存器中只有%ebp和%esp保存栈帧指针,因此同一时间只能保存一对。当进展到第三层的时候,已经有了三个栈帧(原则上来讲一定是多于3个),寄存器当然是存不下的,因此就需要在存储器当中备份一下,之后再恢复。于是就出现了每个栈帧的帧指针指向的存储器位置,都会备份着外层方法(也就是调用者)的帧指针。

  当方法递归到n=1结束时,栈帧会自下向上依次收回,栈帧指针(也就是%ebp和%esp当中的值)都会依次向上移动,直到程序结束。也就是说,上面的三幅图,如果倒过来,就是递归方法依次结束时栈帧的状态。

  由此就可以看出,过程当中栈帧建立以及完成的惯例,可以保证递归调用的正常运行,包括循环调用。不得不说,这群计算机界的大神们实在是太牛了,尽管当栈帧出现以后,看起来也并不复杂,但难点就在于无中生有的发现或者说某种意义上的创造。

 

作者:zuoxiaolong(左潇龙)

出处:博客园左潇龙的技术博客--http://www.cnblogs.com/zuoxiaolong

转载于:https://www.cnblogs.com/zzdbullet/p/9629909.html

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

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

相关文章

php 相亲 段子,精彩的男女幽默段子

精彩的男女幽默段子。撒嬌老婆洗完澡對老公撒嬌說&#xff1a;老公&#xff0c;抱我到床上去吧。老公看了看老婆&#xff0c;冷冷的回答道&#xff1a;我還是把床搬過來吧&#xff01;所以&#xff0c;撒嬌還是要看體型&#xff01;單身老公說&#xff1a;老婆&#xff0c;你不…

Redmine数据库备份及搬家

Bitnami Redmine的备份分2种方式&#xff1a; 1.导出数据库 2.整个目录搬家 不管是哪种都想停掉服务&#xff0c;redmine相关的服务有以下5个&#xff1a; redmineApache   redmineMySQL   redmineSubversion   redmineThin1   redmineThin2 可以打开windows服务控制面…

且看BCH开启的“信用本位”时代

​​​ 且看BCH开启的“信用本位”时代 比特币向来被称为“金本位”的互联网实验&#xff0c;由于中本聪先生的天才发明&#xff0c;POW机制给予了比特币与黄金同样的生产模式。所以&#xff0c;时至今日&#xff0c;BCE依然自称为“数字黄金”。 只可惜&#xff0c;“一叶障目…

oracle设置临时表空间,Oracle临时表空间查看、添加临时表空间数据文件、修改默认临时表空间 方法!...

--查表空间使用率情况(含临时表空间)SELECT d.tablespace_name "Name", d.status "Status",TO_CHAR (NVL (a.BYTES / 1024 / 1024, 0), 99,999,990.90) "Size (M)",TO_CHAR (NVL (a.BYTES - NVL (f.BYTES, 0), 0) / 1024 / 1024,99999999.99) US…

Redmine项目管理工具安装

Redmine免费开源的项目管理工具 下载 一键安装工具 https://bitnami.com/stack/redmine/installer 安装 Redmine一键安装工具集成了php服务&#xff0c;mysql服务。尽管安装就好。 安装完成后&#xff0c;在开始菜单&#xff0c;找到-----Bitnami Redmine Stack--------Bi…

Oracle创建假脱机文件,oracle – 在sqlplus中假脱机csv文件时的标头格式

我需要使用sqlplus从Oracle中的表中调整csv.以下是所需的格式&#xff1a;"HOST_SITE_TX_ID","SITE_ID","SITETX_TX_ID","SITETX_HELP_ID""664436565","16","2195301","0""664700792&qu…

方便微信公众号等手机网页调试插件eruda和vConsole

原文地址&#xff1a;https://blog.csdn.net/qq_39234840/article/details/80951710 ---------------------------------------------------------- 调试插件一&#xff1a;eruda&#xff08;推荐&#xff0c;因为比vConsole功能多&#xff09; <script src"//cdn.js…

HDU 3530Subsequence(单调队列)

题意 题目链接 给出$n$个数&#xff0c;找出最长的区间&#xff0c;使得区间中最大数$-$最小数 $> m$ 且$< k$ Sol 考虑维护两个单调队列。 一个维护$1 - i$的最大值&#xff0c;一个维护$1 - i$的最小值。 至于两个限制条件。 $<k$可以通过调整队首来满足 $>a$可以…

oracle权限培训,Java培训-ORACLE数据库学习【2】用户权限

查询用户拥有的权限&#xff1a;1.查看所有用户&#xff1a;select *from dba_users;select *from all_users;select *from user_users; 2.查看用户或角色系统权限(直接赋值给用户或角色的系统权限)&#xff1a;select *from dba_sys_privs;select *from user_sys_privs; 3.查看…

linux 中文件夹的文件按照时间倒序或者升序排列

1&#xff0c;按照时间升序 命令:ls -lrt 详细解释: -l use a long listing format 以长列表方式显示&#xff08;详细信息方式&#xff09; -t sort by modification time 按修改时间排序&#xff08;最新的在最前面&#xff09; -r reverse order while sorti…

PHP中关于时间(戳)、时区、本地时间、UTC时间等的梳理

PHP中关于时间&#xff08;戳&#xff09;、时区、本地时间、UTC时间等的梳理 在PHP开发中&#xff0c;我们经常会在时间问题上被搞糊涂&#xff0c;比如我们希望显示一个北京时间&#xff0c;但是当我们使用date函数进行输出时&#xff0c;却发现少了8个小时。几乎所有的php猿…

WebServiceStudio.exe测试webservice接口工具

WebServiceStudio.exe测试webservice接口工具 下载链接 https://pan.baidu.com/s/1gf8ajS3 打开工具WebServiceStudio&#xff0c;如下填写地址&#xff0c;点击【Get】按钮 会显示出需要传参的地方&#xff0c;在value中填写xml参数 输入完value值后&#xff0c;点击【Invok…

oracle最大实例数,【ORA-16196】一个实例在其生命周期里最多只能装载和打开一个数据库...

如果使用“alter database open;”命令打开一个曾经被“alter database close;”命令关闭的数据库时&#xff0c;您将会收到如下的报错信息&#xff1a;"ORA-16196: database has been previously opened and closed"这个报错的原因是什么呢&#xff1f;原因是&#…

Navicat工具导出Mysql数据表结构到Excel文件中

原文链接&#xff1a;https://blog.csdn.net/zt15732625878/article/details/77978266 ------------------------------------------------------------------------ 前言 项目中数据库设计已经完成&#xff0c;现在到了代码实现的阶段&#xff0c;数据库中没有数据&#xff…

利用MAVEN的profile 实现打包环境的切换

乐哉码农产生问题的背景 由于在项目开发的时候&#xff0c;我们一般都是使用的本地库&#xff0c;数据库连接写的是本地的&#xff0c;如果我们将项目打成war的时候&#xff0c;里面的配置连接写的是我们本地的&#xff0c;当我们直接把war拷贝到服务器上面进行部署的时候&…

服务器oracle优化,oracle服务器配置及优化

1.在ORACLE中实现分布式快速存取和充实内存是很重要的。要不惜任何代价避免页面调度和交换﹐每次都必须把系统全局区(SGA)放到内存。将SGA放到内存中﹐在INIT.ORA中设置参数 PRE_PAGE_SGAPRE_PAGE_SGAYES2.回卷段的竟争会降低系统的性能。SELECT GETS,WAITS from V$ROLLSTAT;…

Android 常用的数据加密方式

前言 Android 很多场合需要使用到数据加密&#xff0c;比如&#xff1a;本地登录密码加密&#xff0c;网络传输数据加密&#xff0c;等。在android 中一般的加密方式有如下&#xff1a; 亦或加密AES加密RSA非对称加密当然还有其他的方式&#xff0c;这里暂且介绍以上三种加密算…

oracle可以注入吗,ORACLE 注入

1判断是什么数据库and exist(select * from dual)and exists(select * from user_tables)原理&#xff1a;dual表和user_tables表是oracle中的系统表返回正常&#xff0c;那么就可以肯定这是oracle。2查字段数order by 10-- //错误,列数小于10order by 3-- //正常,列数等于…

centos升级glibc(升级到 2.17版)

1、原先的系统glibc库的版本是2.12&#xff0c;需要升级到2.17版本。 下载地址&#xff1a; http://ftp.gnu.org/gnu/glibc/ http://ftp.gnu.org/gnu/glibc/glibc-2.17.tar.gz 这里可以选择你所需要的版本。 2、安装部署 [rootkafzook1 common]# tar -xf glibc-2.17.tar.g…

Day31 python基础--网络编程基础-socketserver

一&#xff0c;验证客户端合法性 #server端 import os import hmac import socket secret_key balex_sbdef auth(conn):msg os.urandom(32) #生成一个随机的字符串conn.send(msg) #发送到client端result hmac.new(secret_key,msg) #处理这个随机字符串&#xff0c;得到一…