压栈, 跳转,执行,返回:从汇编看函数调用

 

From:https://www.jianshu.com/p/594357dff57e

C函数调用过程原理及函数栈帧分析:https://blog.csdn.net/zsy2020314/article/details/9429707

 

 

从本篇开始,我们讨论一些高级语言中的基础设施:堆栈,函数调用,变量生命周期等等话题。因为这里本身会涉及到比较多的汇编层面的基础概念。为了向大家说明汇编层的函数调用实现细节,无奈我只能罗列出很多汇编上的概念,因为本文假定读者不需要具有任何汇编知识。我讨厌长篇大论,但本篇的解释可能仍然不够明晰。在此为自己知识的浅薄表示歉意。

 

 

1. 从代码的顺序执行说起

 

每一个程序员脑子里应该都有这么一种印象:“程序是顺序执行的”。这个观点其实和我们开篇所讲的cpu的流水线执行过程直接相关。
让我们再回忆一下脑海中关于函数调用的概念,也许会是这个样子:

这里的“控制流转移”又是如何发生的呢?在解释这个之前,也许我们需要科普一点有关于汇编的知识。

 

 

 

2. 函数调用中的一些细节说明

 

2.1 函数调用中的关键寄存器

 

2.1.1 程序计数器PC

程序计数器是一个计算机组成原理中讲过的概念,下面给出一个百度百科中的简单解释

程序计数器是用于存放下一条指令所在单元的地址的地方。
当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。

可以看到,程序计数器是一个cpu执行指令代码过程中的关键寄存器:它指向了当前计算机要执行的指令地址,CPU总是从程序计数器取出当前指令来执行。当指令执行后,程序计数器的值自动增加,指向下一条将要执行的指令。

在x86汇编中,执行程序计数器功能的寄存器被叫做EIP,也叫作指令指针寄存器

 

2.1.2 基址指针,栈指针和程序栈

栈是程序设计中的一种经典数据结构,每个程序都拥有自己的程序栈。很重要的一点是,栈是向下生长的。所谓向下生长是指从内存高地址->低地址的路径延伸,那么就很明显了,栈有栈底和栈顶,那么栈顶的地址要比栈底低。对x86体系的CPU而言,其中
---> 寄存器ebp(base pointer )可称为“帧指针”或“基址指针”,其实语意是相同的。
---> 寄存器esp(stack pointer)可称为“ 栈指针”。
在C和C++语言中,临时变量分配在栈中,临时变量拥有函数级的生命周期,即“在当前函数中有效,在函数外无效”。这种现象就是函数调用过程中的参数压栈,堆栈平衡所带来的。对于这种实现的细节,我们会在接下来的环节中详细讨论。

 

2.2. 堆栈平衡

堆栈平衡这个概念指的是函数调完成后,要返还所有使用过的栈空间。这种说法可能有点抽象,我们可以举一个简单的例子来类比:
我们都知道函数的临时变量存放在栈中。那我们来看下面的代码,它是一个很简单的函数,用来交换传入的2个参数的值:

void __stdcall swap(int& a,int& b)
{int c = a;a = b;b = c;
}

我们可以看到,在这个函数中使用了一个临时变量int c;这个变量分配在栈中,我们可以简单的理解为,在声明临时变量c后,我们就向当前的程序栈中压入了一个int值:

int c = a; <==> push(a);   //简单粗暴,临时变量的声明理解为简单地向栈中push一个值。

那现在这个函数swap调用结束了,我们是否需要退栈,把之前临时变量c使用的栈空间返还回去?需要吗?不需要吗?
我们假设不需要,当我们频繁调用swap的时候,会发生什么?每次调用,程序栈都在生长。直到栈满,我们就会收到stack overflow错误,程序挂掉了。
所以为了避免这种乌龙的事情发生,我们需要在函数调用结束后,退栈,把堆栈还原到函数调用前的状态,这些被pop掉的临时变量,自然也就失效了,这也解释了我们一直以来关于临时变量仅在当前函数内有效的认知。其实堆栈平衡这个概念本身比这种粗浅的理解要复杂的多,还应包括压栈参数的平衡,暂时我们可以简单地这样理解,后面再做详细说明。

 

2.3. 函数的参数传递和调用约定

函数的参数传递是一个参数压栈的过程。函数的所有参数,都会依次被push到栈中。那调用约定有是什么呢?
C和C++程序员应该对所谓的调用约定有一定的印象,就像下面这种代码:

void __stdcall add(int a,int b);

函数声明中的__stdcall就是关于调用约定的声明。其中标准C函数的默认调用约定是__stdcall,C++全局函数和静态成员函数的默认调用约定是__cdecl,类的成员函数的调用约定是__thiscall。剩下的还有__fastcall__naked等。

为什么要用所谓的调用约定?调用约定其实是一种约定方式,它指明了函数调用中的参数传递方式和堆栈平衡方式。

 

2.3.1 参数传递方式

还是之前那个例子,swap函数有2个参数,int a,int b。这两个参数,入栈的顺序谁先谁后?
其实是从左到右入栈还是从右到左入栈都可以,只要函数调用者和函数内部使用相同的顺序存取参数即可。在上述的所有调用约定中,参数总是从右到左压栈,也就是最后一个参数先入栈。我们可以使用一份伪代码描述这个过程

push b;      //先压入参数b
push a;      //再压入参数a
call swap;  //调用swap函数

其实从这里我们就可以理解为什么在函数内部,不能改变函数外部参数的值:因为函数内部访问到的参数其实是压入栈的变量值,对它的修改只是修改了栈中的"副本"。指针和引用参数才能真正地改变外部变量的值。

 

2.3.2 堆栈平衡方式

因为函数调用过程中,参数需要压栈,所以在函数调用结束后,用于函数调用的压栈参数也需要退栈。那这个工作是交给调用者完成,还是在函数内部自己完成?其实两种都可以。调用者负责平衡堆栈的主要好处是可以实现可变参数(关于可变参数的话题,在此不做过多讨论。如果可能的话,我们可以以一篇单独的文章来讲这个问题),因为在参数可变的情况下,只有调用者才知道具体的压栈参数有几个。
下面列出了常见调用约定的堆栈平衡方式:

调用约定堆栈平衡方式
__stdcall函数自己平衡
__cdecl调用者负责平衡
__thiscall调用者负责平衡
__fastcall调用者负责平衡
__naked编译器不负责平衡,由编写者自己负责

2.4. 栈帧的概念:从esp和ebp说起

为什么我们需要ebp和esp2个寄存器来访问栈?这种观念其实来自于函数的层级调用:函数A调用函数B,函数B调用函数C,函数C调用函数D...
这种调用可能会涉及非常多的层次。编译器需要保证在这种复杂的嵌套调用中,能够正确地处理每个函数调用的堆栈平衡。所以我们引入了2个寄存器:

  • 1. ebp指向了本次函数调用开始时的栈顶指针,它也是本次函数调用时的“栈底”(这里的意思是,在一次函数调用中,ebp向下是函数的临时变量使用的空间)。在函数调用开始时,我们会使用 mov ebp,esp 把当前的esp保存在ebp中。
  • 2. esp,它指向当前的栈顶,它是动态变化的,随着我们申请更多的临时变量,esp值不断减小(正如前文所说,栈是向下生长的)。
  • 3. 函数调用结束,我们使用 mov esp,ebp 来还原之前保存的esp。

在函数调用过程中,ebp和esp之间的空间被称为本次函数调用的“栈帧”。函数调用结束后,处于栈帧之前的所有内容都是本次函数调用过程中分配的临时变量,都需要被“返还”。这样在概念上,给了函数调用一个更明显的分界。下图是一个程序运行的某一时刻的栈帧图:

 

 

 

3. 汇编中关于“函数调用”的实现

 

上面铺陈了很多的汇编层面的概念后,我们终于可以切回到我们本次的主题:函数调用
函数调用其实可以看做4个过程,也就是本篇标题:

  1. 压栈: 函数参数压栈,返回地址压栈
  2. 跳转: 跳转到函数所在代码处执行
  3. 执行: 执行函数代码
  4. 返回: 平衡堆栈,找出之前的返回地址,跳转回之前的调用点之后,完成函数调用

 

1. call指令 压栈和跳转

下面我们看一下函数调用指令

0x210000 call swap;
0x210005 mov ecx,eax; 

我们可以把它理解为2个指令:

push 0x210005;
jmp swap;

 

也就是,首先把call指令的下一条指令地址作为本次函数调用的返回地址压栈,然后使用jmp指令修改指令指针寄存器EIP,使cpu执行swap函数的指令代码。

 

 

2. ret指令 返回

汇编中有ret相关的指令,它表示取出当前栈顶值,作为返回地址,并将指令指针寄存器EIP修改为该值,实现函数返回。
下面给出一组示意图来演示函数的返回过程:

 

1. 当前EIP的值为0x210004,指向指令ret 4,程序需要返回

2. 执行ret指令,将当前esp指向的堆栈值当做返回地址,设置eip跳转到此处并弹出该值

经过这两步,函数就返回到了调用处。

 

 

 

4. 从实际汇编代码看函数调用

 

4.1 程序源码和运行结果

源码:

main.cpp#include <stdio.h>void __stdcall swap(int& a, int& b);int main(int argc, char* argv)
{int a = 1, b = 2;printf("before swap: a = %d, b = %d\r\n", a, b);swap(a, b);printf("after swap: a = %d, b = %d\r\n", a, b);
}void __stdcall swap(int& a, int& b)
{int c = a;a = b;b = c;
}

程序运行结果:

 

 

4.2 反汇编

可以看到,在函数调用前,函数参数已被压栈,此时:
EBP = 00AFFCAC
ESP = 00AFFBBC
EIP = 00BF1853
我们按F11,进入函数内部,此时:

其实就是call swap指令的下一条指令地址,它就是本次函数调用的返回地址

下面是一个swap函数的详细注释:

当程序运行到 ret 8时

执行返回后:

在返回前,ESP = 00AFFBB8,返回后 ESP = 00AFFBC4
0x00AFFBC4 - 0x00AFFBB8 = 0xC
这里的数值是字节数,而我们知道,int是4字节长度。所以0xC/4 = 3
正好是2个压栈参数+一个返回地址。

 

 

4.3 调用堆栈

调试程序的时候,我们经常关注的一个点就是VisualStudio显示给我们的“调用堆栈”功能,这次让我们来仔细看一下它:
我们重新执行一次程序,这次我们关注一下vs显示的调用堆栈,如下图

第一行是当前指令地址
第二行是外层调用者,我们双击它,跳转到如下地址:

也许这也是为什么这个功能被叫做“调用堆栈”的原因:它正是通过对程序栈的分析实现的。

 

 

 

 

 

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

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

相关文章

IBM AIX 5.3 系统管理 -- 系统启动过程详解

一. 启动过程 启动过程包含下面的一些步骤&#xff1a; 1.1启动一个系统的初始步骤是上电自检&#xff08;Power On Self Test&#xff0c;POST&#xff09;。其目的是验证基本硬件是否处于正常的工作状态。同时初始化内存、键盘、通信&#xff0c;以及音频设备。您可以看到在屏…

作弊阴影罩棋盘,人工智能咋避嫌?

来源&#xff1a;奇怪的科学家为什么要写这样一句话&#xff0c;就是为了避免剧情和现实生活中发生的事情万一差不多&#xff0c;会侵犯到别人的隐私&#xff0c;发生侵权&#xff0c;给双方带来不必要的麻烦。这位名为刘超的棋手把手机插在上衣兜里&#xff0c;摄像头正对棋盘…

ubuntu server版本安装指南(1)

ubuntu是基于GNU/Linux 的操作系统&#xff0c;本身是在同样GNU/Linux 架构下的Debian的基础上的一个版本。由于它在桌面环境上的易用性和精细度是许多人认识了他。ubuntu的强大在一定程度上体现在apt包管理系 统。安装软件不必像以前那样幸苦找到下载地址。编译安装。还要非常…

Arm中国合资公司具体布局浮出水面

来源&#xff1a;经济观察报摘要&#xff1a;作为全球最具影响力的芯片技术供应商之一&#xff0c;Arm在中国正迎来新的时代。对于Arm与中国合资公司事宜&#xff0c;5月4日下午&#xff0c;Arm授权的代表邮件回复《经济观察报》称&#xff1a;“合资公司目前刚开始运营”&…

汇编逆向基础

汇编逆向基础&#xff1a;https://www.xmind.net/m/kvJK/

深入理解Nginx~运行中的Nginx进程间的关系

在正式提供服务的产品环境下&#xff0c;部署Nginx时都是使用一个master进程来管理多个worker 进程&#xff0c;一般情况下&#xff0c;worker进程的数量与服务器上的CPU核心数相等。每一个worker进程都 是繁忙的&#xff0c;它们在真正地提供互联网服务&#xff0c;master进程…

哥伦比亚大学AI实验室主任Hod Lipson:阻碍无人驾驶技术发展的7个误区

来源&#xff1a;智车科技摘要&#xff1a;我们发现有些针对无人驾驶的误解还在广泛肆意传播&#xff0c;并且这些信息会被反对者拿来和对抗无人驾驶的推广政策。每年&#xff0c;全世界都有将近120万人死于车祸&#xff0c;这个死亡率相当于每年释放10个广岛级别的原子弹爆炸。…

PE文件结构详解 --(完整版)

From&#xff1a;https://blog.csdn.net/adam001521/article/details/84658708 PE结构详解&#xff1a;https://www.cnblogs.com/zheh/p/4008268.html PE格式解析-区段表及导入表结构详解&#xff1a;https://blog.csdn.net/qq_30145355/article/details/78859214 PE文件基本…

人工智能下一个热点探讨,为什么要提出互联网大脑模型 ?

作者&#xff1a;刘锋 计算机博士 《互联网进化论作者》前言从2008年发表论文第一次提出互联网大脑模型&#xff0c;时间已经过去十年。撰写这篇文章&#xff0c;主要是详细介绍我们在十年前提出互联网大脑模型的原因&#xff1b;十年来在计算机和智能领域产生了哪些进展&…

学会了这些技术,你离BAT大厂不远了

每一个程序员都有一个梦想&#xff0c;梦想着能够进入阿里、腾讯、字节跳动、百度等一线互联网公司&#xff0c;由于身边的环境等原因&#xff0c;不知道 BAT 等一线互联网公司使用哪些技术&#xff1f;或者该如何去学习这些技术&#xff1f;或者我该去哪些获取这些技术资料&am…

张钹院士:可解释、可理解是人工智能研究的主攻方向 | CCF-GAIR 2018

作者&#xff1a;刘鹏摘要&#xff1a;张钹院士历经了中国人工智能的从无到有&#xff0c;从弱到强&#xff0c;因而他也最能清楚地针对中国人工智能近年来的不同发展状态&#xff0c;发表适合的看法和提出正确的建议。2017 年末清华大学举办的「从阿尔法 Go 到通用人工智能&am…

小甲鱼 OllyDbg 教程系列 (二) :从一个简单的实例来了解PE文件

小甲鱼视频讲解&#xff1a;https://www.bilibili.com/video/av6889190?p6https://www.bilibili.com/video/av6889190?p7 从一个简单的实例来了解PE文件&#xff1a;https://www.freebuf.com/articles/system/86596.htmlhttps://blog.csdn.net/billvsme/article/details/383…

一键解决 go get golang.org/x 包失败

From&#xff1a;https://segmentfault.com/a/1190000018264719 问题描述 在 ubuntu 上用 sudo apt install golang-go 安装 go 的 sdk&#xff0c;之后使用 go get、go install、go mod 等命令时 (会自动下载相应的包或依赖包) 时&#xff0c;但由于众所周知的原因(墙)&#x…

「对抗深度强化学习」是如何解决自动驾驶汽车系统中的「安全性」问题的?...

原文来源&#xff1a;arXiv作者&#xff1a;Aidin Ferdowsi、 Ursula Challita、Walid Saad、Narayan B. Mandayam「雷克世界」编译&#xff1a;嗯~是阿童木呀、KABUDA对于自动驾驶汽车&#xff08;AV&#xff09;而言&#xff0c;要想在未来的智能交通系统中以真正自主的方式运…

小甲鱼 OllyDbg 教程系列 (五) : 破解 PC Surgeon 之 查找字符串

https://www.bilibili.com/video/av6889190/?p11 https://www.bilibili.com/video/av6889190/?p12 程序下载地址&#xff1a;https://pan.baidu.com/s/1eVTLQ_AatLrmrz3FLwM5ww 提取码&#xff1a;wny9 修复 OllyDBG 右键 -> 复制到可执行文件 -> 所有修改 中 所…

深度概览卷积神经网络全景图,没有比这更全的了

来源&#xff1a; 人工智能头条 翻译 | 林椿眄摘要&#xff1a;深度卷积神经网络是这一波 AI 浪潮背后的大功臣。虽然很多人可能都已经听说过这个名词&#xff0c;但是对于这个领域的相关从业者或者科研学者来说&#xff0c;浅显的了解并不足够。通过这篇文章&#xff0c;我们…

小甲鱼 OllyDbg 教程系列 (四) : 逆向 VisualSite Designer 之 硬件断点

去掉程序开始之前的界面&#xff1a;https://www.bilibili.com/video/av6889190?p9 去掉关闭程序后的广告&#xff1a;https://www.bilibili.com/video/av6889190?p10 VisualSite Designer.exe 下载地址&#xff1a;https://pan.baidu.com/s/1i-fi1wW-m0Cp72yyB_SBFw 提取码…

复杂人机智能系统功能分配方法综述

本文来源&#xff1a;人机与认知实验室摘要:功能分配是复杂人机智能系统设计进程中的重要内容, 它需要应用系统的分析方法, 合理地进行人、机两者的任务分配和科学地设计两者的功能结合。本文分析了国内外功能分配的研究现状和存在的问题。针对复杂人机智能系统的设计需求, 指出…

生物学将是下一代计算平台:DNA是代码,CRISPR是编程语言

来源&#xff1a;36氪每一个行业都在向Crispr投入大量的资金——制药、农业、能源、材料制造。甚至连那些大麻贩子都想砸钱进去。机器里面&#xff0c;运行的并不是由0和1组成的互联网编码&#xff0c;而是能重写生命密码的分子。日前&#xff0c;《连线》杂志发表了一篇文章&a…

Python 中使用 jsonpath

JSONPath 解析 JSON 内容详解&#xff08;翻译自 github&#xff09;&#xff1a;https://blog.csdn.net/freeking101/article/details/103048514 JSONPath Online Evaluator&#xff1a;http://jsonpath.com Python 处理 JSON 我选择 ujson 和 orjson&#xff1a;https://bl…