汇编-函数调用的理解

2023-05-16

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

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

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

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向下是函数的临时变量使用的空间)。在函数调用开始时,我们会使用把当前的esp保存在ebp中。
mov ebp,esp 
  1. esp,它指向当前的栈顶,它是动态变化的,随着我们申请更多的临时变量,esp值不断减小(正如前文所说,栈是向下生长的)。函数调用结束,我们使用来还原之前保存的esp。
mov esp,ebp

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

Paste_Image.png

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

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

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

3.1 call指令 压栈和跳转

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

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

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

push 0x210005;
jmp swap;

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

3.2 ret指令 返回

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

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

Paste_Image.png

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

Paste_Image.png

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

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;
}

程序运行结果:

Paste_Image.png

4.2 反汇编

Paste_Image.png

Paste_Image.png

可以看到,在函数调用前,函数参数已被压栈,此时:
EBP = 00AFFCAC
ESP = 00AFFBBC
EIP = 00BF1853

我们按F11,进入函数内部,此时:

Paste_Image.png

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

Paste_Image.png

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

Paste_Image.png

当程序运行到ret 8时

Paste_Image.png

执行返回后:

Paste_Image.png

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

4.3 调用堆栈

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

Paste_Image.png

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

Paste_Image.png

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

转载于:https://www.jianshu.com/p/594357dff57e

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

汇编-函数调用的理解 的相关文章

  • 利用FindWindow和SendMessage进程通信

    利用FindWindow和SendMessage xff0c 特此记下 首先说FindWindow FindWindow返回与指定字符创相匹配的窗口类名或窗口名的最顶层窗口的窗口句柄 函数原型为 xff1a C 43 43 xff1a HW
  • vnc viewer,什么是vnc viewer

    在日常工作中 xff0c 经常会用到vnc xff0c 自然也会用到vncviewer xff0c 那你知道什么是vncviewer吗 xff1f 怎么使用vncviewer呢 xff1f 接下来让我们一起去看看吧 首先先让我们来看看一款v
  • 句柄的概念详解

    1 这里将句柄所能标识的所有东西 xff08 如窗口 文件 画笔等 xff09 统称为 对象 2 图中一个小横框表示一定大小的内存区域 xff0c 并不代表一个字节 xff0c 如标有0X00000AC6的横框表示4个字节 程序运行到某时刻
  • Server.Transfer 和 Response.Redirect 的用法

    在ASP NET中 xff0c 在后台传值方式目前大多都是用 Response Redirect 34 页面地址 34 来重定向页面的 xff0c 但是现在还有一种方式也可以达到重定向页面的作用 xff0c 而且在某些时刻会起到一种很棒的效
  • WebForm连接数据库实例

    登录页面 xff1a 用户名文本框 密码文本框 登录按钮 当用户名密码输入正确 xff0c 点击确定可以跳转到下一个页面 我们需要先引入命名空间 xff1a using System Data using System Data SqlCl
  • ASP.NET WebForm和Mvc开发的比较

    在初步了解MVC后 xff0c 发现很多人对于MVC和三层架构开发概念上会有很大的混淆 xff0c 所以把这两天的学习笔记整理一下 xff0c 分享给自己的同学们 同时也做一个小Demo xff0c 让没有接触过MVC开发的同学 xff0c
  • ASP.NET - MVC框架及搭建教程

    一 MVC简介 MVC xff1a Model View Controller xff08 模型 视图 控制器 xff09 xff0c MVC是一种软件开发架构模式 1 模型 xff08 Model xff09 模型对象是实现应用程序数据域
  • Web 绘图—服务器端绘图

    Web服务器端绘图的基本原理是 xff1a 首先在内存中创建一个Bitmap位图 xff0c 然后在此位图上绘制任意想要的图形 xff0c 绘制完成后保存输出到页面的输出流 这样 xff0c 一个页面就转换成了绘制的图片 1 xff0e 简
  • 在VS2015中安装Qt环境

    1 安装Qt 1 xff09 下载 下载网址 xff1a https download qt io archive qt 选择要下载的版本 xff0c 我选择5 12 5 xff0c 点击下载即可 点击 Detail 按钮可以看到安装文件资
  • Qt Quick 中 QML 与 C++ 混合编程详解

    Qt Quick 技术的引入 xff0c 使得你能够快速构建 UI xff0c 具有动画 各种绚丽效果的 UI 都不在话下 但它不是万能的 xff0c 也有很多局限性 xff0c 原来 Qt 的一些技术 xff0c 比如低阶的网络编程如 Q
  • windows消息机制深入详解-1

    Windows 是一个事件驱动的操作系统 事件驱动围绕着消息的产生与处 理展开 xff0c 事件驱动是靠消息循环机制来实现的 也可以理解为消息是一种报告有关事件发生 的通知 xff0c 消息是Windows 操作系统的灵魂 在屏幕显示一个窗
  • windows消息机制详解-3

    1 引言 Windows 在操作系统平台占有绝对统治地位 xff0c 基于Windows 的编程和开发越来越广泛 Dos 是过程驱动的 xff0c 而Windows 是事件驱动的 6 xff0c 这种差别的存在使得很多Dos 程序员不能 习
  • vnc server安装教程,在win系统下如何进行vnc server安装

    在日常工作中 xff0c 经常会使用到vnc server xff0c 那有小伙伴知道vnc server是什么吗 xff1f vnc server 是一般 Linux 发行版都会附带的 vnc服务器软件 vncserver 是一个为了满足
  • Windows消息机制详解-2

    消息是指什么 xff1f 消息系统对于一个win32程序来说十分重要 xff0c 它是一个程序运行的动力源泉 一个消息 xff0c 是系统定义的一个32位的值 xff0c 他唯一的定义了一个事件 xff0c 向 Windows发出一个通知
  • windows消息机制-4(MFC)

    消息分类与消息队列 Windows中 xff0c 消息使用统一的结构体 xff08 MSG xff09 来存放信息 xff0c 其中message表明消息的具体的类型 xff0c 而wParam xff0c lParam是其最灵活的两个变量
  • Windows消息机制详解-5

    一 什么是消息 在解释什么是消息之前 xff0c 我们先讨论一下程序的执行机制问题 大体上说 xff0c 程序按照执行机制可以分为两类 xff1a 第一类是过程驱动 比如我们最早接触编程时写的C程序 xff0c 又或者单片机程序 这类程序往
  • Windows句柄-2

    这里需要说明 xff1a 1 这里将句柄所能标识的所有东西 xff08 如窗口 文件 画笔等 xff09 统称为 对象 2 图中一个小横框表示一定大小的内存区域 xff0c 并不代表一个字节 xff0c 如标有0X00000AC6的横框表示
  • Windows消息机制详解-6

    消息系统对于一个win32程序来说十分重要 xff0c 它是一个程序运行的动力源泉 一个消息 xff0c 是系统定义的一个32位的值 xff0c 他唯一的定义了一个事件 xff0c 向 Windows发出一个通知 xff0c 告诉应用程序某
  • TranslateMessage ,GetMessage, DispatchMessage分析

    TranslateMessage amp msg TranslateMessage是用来把快捷键消息转换为字符消息 并将转换后的新消息投递到调用线程的消息队列中 由于Windows对所有键盘编码都是采用虚拟键的定义 xff0c 这样当按键按
  • Windows消息机制-PreTranslateMessage

    PreTranslateMessage作用和使用方法 Windows消息机制的流程 xff1a A 操作系统接收应用程序的窗口消息 xff0c 将消息投递到该应用程序的消息队列中 B 应用程序在消息循环中调用GetMessage函数从消息队

随机推荐

  • 线程中发送消息阻塞问题解决

    发送消息时阻塞的两种方案1 此处应post发送消息放到消息队列中 xff0c 直接send调用响应过程的话如果消息响应未结束则会一直阻塞工作线程2 用send的话在此处开辟工作线程执行逻辑
  • qt+visa实现程控实例

    软件环境 系统 windows 10开发环境 Qt 5 80visa库版本 visa 6 0 软件下载 QtNI MAXIVI 步骤 1 添加依赖库 在Demo pro中添加依赖 win32 INCLUDEPATH 43 61 34 C P
  • Qt中标准对话框实例,QObject::tr()的作用

    函数 tr 全名是QObject tr 被它处理的 字符串可以 使用工具提 取出来翻译 成其他语言 也就是做国际化使用 只要记住 Qt 的最佳实践 如果你想让你的程序国际化的话 那么 所有用户可见的字符串都要使用 QObject tr 但是
  • 公司电脑安装了vncserver,公司电脑安装了vncserver怎么卸载vncserver

    在使用vncserver时感觉很方便 xff0c 但在卸载vncserver时往往比较让人头疼 xff0c 之前公司电脑安装了vncserver使用时我还比较庆幸 xff0c 但当我卸载时真的费了很多力气 xff0c 那今天小编就来教教大家
  • 进程,线程,消息循环的关系

    一个进程有n个线程 xff0c 一个线程有一个消息队列或事件队列和一个活动的消息循环 xff0c 每个模态窗体有一个消息循环函数和一个消息响应过程函数 xff0c 一个线程里哪个模态窗体激活即运行哪个窗体的消息循环 xff0c 一个模态窗体
  • Qt匿名函数的写法

    匿名函数也可以被叫做Lambda表达式 xff0c 自C 43 43 11中引入该特性 本文主要介绍Qt里使用到的匿名函数 c11新特性中加入了lambda表达式 xff0c 所以Qt 也支持 需在 pro文件中加入 CONFIG 43 6
  • QT c++ 中使用PostMessage/SendMessage实例

    PostMessage是Windows API 应用程序接口 中的一个常用函数 xff0c 用于将一条消息放入到消息队列中 并且不会等待响应的线程处理消息 xff0c 而是直接返回 xff08 简单的理解就是异步 xff09 而SendMe
  • Qt-线程启动与关闭实例

    养成资源回收的好习惯 xff0c 任何时候都要想起开辟过的内存回收 就是利用关闭窗口时调用槽函数回收掉 具体步骤不难 xff0c 如下 xff1a 1 xff09 退出线程 xff1b 2 xff09 回收子线程 xff1b 3 xff09
  • qt中QListView的用法和QModelIndex的使用

    使用QTreeView xff0c 对于很多函数中针对item的唯一标识QModelIndex的使用 xff0c 记录下两种对于QModelIdex的使用 1 xff0c 树形结构的item设置为选中 QModelIndex rootInd
  • Qt-QMessageBox用法详解

    QMessageBox 是 Qt 框架中常用的一个类 xff0c 可以生成各式各样 各种用途的消息对话框 xff0c 如图 1 所示 图 1 QMessageBox消息对话框 很多 GUI 程序都会用到消息对话框 xff0c 且很多场景中使
  • Qt-手动布局

    简述 手动布局 xff0c 可以实现和水平布局 垂直布局 网格布局等相同的效果 xff0c 也可实现属于自己的自定义布局 xff0c 当窗体缩放时 xff0c 控件可以随之变化 其对于坐标系的建立有严格要求 xff0c 纯代码思维 xff0
  • Qt-5种布局控件详解

    实际开发中 xff0c 一个界面上可能包含十几个控件 xff0c 手动调整它们的位置既费时又费力 作为一款成熟的 GUI 框架 xff0c Qt 提供了很多摆放控件的辅助工具 xff08 又称布局管理器或者布局控件 xff09 xff0c
  • vnc怎么设置自适应屏幕,2种方法教你vnc怎么设置自适应屏幕

    VNC 是在基于 UNIX和 LINUX 操作系统的免费的开源软件 xff0c 远程控制能力强大 xff0c 高效实用 xff0c 其性能可以和 WIndows和 MAC 中的任何远程控制软件媲美 在使用vnc软件的过程中 xff0c 往往
  • C++中的野指针问题

    文章目录 1 C和C 43 43 中的野指针问题 1 1 野指针的概念 1 2 野指针的由来 1 3 杜绝野指针的基本原则 2 C和C 43 43 中的常见内存错误 2 1 常见内存错误 2 2 内存操作的基本规则 1 C和C 43 43
  • Qt-调用dll动态链接库

    事先写一个简单的dll文件 myDLL dll C版接口的 并且用我前两篇有关DLL文章里面的方法 xff0c 从dll中导出了导入库 lib 文件 xff0c dll中有两个函数 xff0c 原型如下 xff1a void HelloWo
  • Qt调用动态链接库ControlCAN.dll实例

    注意 xff1a controlCan引用静态库时需要将kerneldlls文件夹放置程序的输出路径下设备才能链接成功 首先添加外部库文件 xff08 lib文件 xff09 一 添加第三方的头文件 这个问题再简单不过了 xff0c 不过我
  • asposeword.dll通过word模板生成word、PDF

    效果图 1 word模板 xff08 部分 xff09 书签 2 生成结果图 开始上代码 Dictionary lt string string gt dictSource 61 new Dictionary lt string strin
  • Qt-捕获Windows消息

    Qt4版本的实现 方法1 xff1a 通过继承QWidget的类中重新实现winEvent接口 xff0c 以接收在消息参数中传递的本机Windows事件 bool QWidget winEvent MSG message long res
  • c语言-指针

    目录 1 指针是什么 xff1f 2 指针和指针类型 2 1 指针 43 整数 2 2 指针的解引用 3 野指针 3 1 野指针成因 3 2 如何规避野指针 4 指针运算 4 1 指针 43 整数 4 2 指针 指针 4 3 指针的关系运算
  • 汇编-函数调用的理解

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