链接与装载---函数调用过程栈帧变化分析

2024-06-16 14:08

本文主要是介绍链接与装载---函数调用过程栈帧变化分析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

概述

函数调用过程中栈帧变化分析

准备知识

汇编代码语法风格

x86寄存器介绍

函数调用约定

函数栈帧分析

总结

参考

附录

cdecl


概述

学过c语言的同学都知道,函数调用过程是通过栈结构来实现的, 在内存空间中, 栈可用于保存函数的参数,局部变量, 返回值,返回地址等。

为什么要用栈来表示呢?

简单来说,栈是一种LIFO形式的数据结构,所有的数据都是后进先出。这种形式的数据结构恰好满足我们调用函数的方式: 父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基本操作,push和pop。push将数据压入栈中,pop将栈中的数据弹出并存储到指定寄存器或者内存中。

我们在写c语言代码时,在函数内部会定义很多局部变量, 在计算时,这些局部变量我们是直接拿来使用的, 而按照栈仅有的两种操作pop和push,  当我们需要取栈中部的数据时,则需要将之前的数据出栈,直到拿到我们需要的数据, 这显然无法满足函数调用过程中对栈的需要,事实上, 函数调用过程中对栈的操作是比较灵活的, 除了有pop和push操作外,还有诸如

movl    -8(%ebp), %edx  // 取出栈底偏移8字节的数据,相当于出栈
movl    $1, -8(%ebp)// 将数据存入到栈底偏移8字节处,相当于入栈

在linux环境下, main函数作为c语言代码的入口, 并不是真正的程序执行入口(关于程序执行入口的说明可以参考:linux c语言main函数调用原理),事实上,main函数是被glibc库函数_start调用的, 这部分代码可以在glibc库中找到。这里我们为了方便阐述函数调用过程,我们不直接分析main函数的栈帧,而是设计了如下的一套代码,通过分析子函数add被调用前, 执行中,调用返回后的栈帧变化,来理解函数栈帧变化过程。 

c语言代码如下:

  1 #include <stdio.h>2 #include <stdlib.h>3 4 5 unsigned int get_x(int _a, int _b)6 {7         unsigned int a = 1;8         unsigned int b = 2;9 10         return a+b + _a + _b;11 }12 13 14 unsigned int add(unsigned int A,unsigned int B)15 {16         unsigned int a = 0x12;17         unsigned int b = 0x13;18         unsigned int x = get_x(a,b);19         unsigned int z = a+b +x;20 21         return z; 22 }2324 int main()25 {26     unsigned int a = 0x55667788;27     unsigned int b = 0x11223344;28     unsigned int ret = add(a,b);29         30     printf("ret:%d\n",ret);31     return 0;32 }

编译平台:

$uname -a
Linux mt-VirtualBox 4.15.0-64-generic #73~16.04.1-Ubuntu SMP Fri Sep 13 09:54:42 UTC 2019 i686 i686 i686 GNU/Linux
$ gcc -v
Target: i686-linux-gnu
gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.10) 

汇编代码如下(gcc -S main.c):

  1         .file   "stack.c"2         .text3         .globl  get_x4         .type   get_x, @function5 get_x:6 .LFB2:7         .cfi_startproc8         pushl   %ebp9         .cfi_def_cfa_offset 810         .cfi_offset 5, -811         movl    %esp, %ebp12         .cfi_def_cfa_register 513         subl    $16, %esp14         movl    $1, -8(%ebp)15         movl    $2, -4(%ebp)16         movl    -8(%ebp), %edx17         movl    -4(%ebp), %eax18         addl    %eax, %edx19         movl    8(%ebp), %eax20         addl    %eax, %edx21         movl    12(%ebp), %eax22         addl    %edx, %eax23         leave24         .cfi_restore 525         .cfi_def_cfa 4, 426         ret27         .cfi_endproc28 .LFE2:29         .size   get_x, .-get_x30         .globl  add31         .type   add, @function32 add:33 .LFB3:34         .cfi_startproc35         pushl   %ebp36         .cfi_def_cfa_offset 837         .cfi_offset 5, -838         movl    %esp, %ebp39         .cfi_def_cfa_register 540         subl    $16, %esp41         movl    $18, -16(%ebp)42         movl    $19, -12(%ebp)43         movl    -12(%ebp), %edx44         movl    -16(%ebp), %eax45         pushl   %edx46         pushl   %eax47         call    get_x48         addl    $8, %esp49         movl    %eax, -8(%ebp)50         movl    -16(%ebp), %edx51         movl    -12(%ebp), %eax52         addl    %eax, %edx53         movl    -8(%ebp), %eax54         addl    %edx, %eax55         movl    %eax, -4(%ebp)56         movl    -4(%ebp), %eax57         leave58         .cfi_restore 559         .cfi_def_cfa 4, 460         ret61         .cfi_endproc62 .LFE3:63         .size   add, .-add64         .section        .rodata65 .LC0:66         .string "ret:%d\n"67         .text68         .globl  main69         .type   main, @function70 main:71 .LFB4:72         .cfi_startproc73         leal    4(%esp), %ecx74         .cfi_def_cfa 1, 075         andl    $-16, %esp76         pushl   -4(%ecx)77         pushl   %ebp78         .cfi_escape 0x10,0x5,0x2,0x75,079         movl    %esp, %ebp80         pushl   %ecx81         .cfi_escape 0xf,0x3,0x75,0x7c,0x682         subl    $20, %esp83         movl    $1432778632, -20(%ebp)84         movl    $287454020, -16(%ebp)85         pushl   -16(%ebp)86         pushl   -20(%ebp)87         call    add88         addl    $8, %esp89         movl    %eax, -12(%ebp)90         subl    $8, %esp91         pushl   -12(%ebp)92         pushl   $.LC093         call    printf94         addl    $16, %esp95         movl    $0, %eax96         movl    -4(%ebp), %ecx97         .cfi_def_cfa 1, 098         leave99         .cfi_restore 5
100         leal    -4(%ecx), %esp
101         .cfi_def_cfa 4, 4
102         ret
103         .cfi_endproc
104 .LFE4:
105         .size   main, .-main
106         .ident  "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609"
107         .section        .note.GNU-stack,"",@progbits

函数调用过程中栈帧变化分析

准备知识

了解以下知识点,可以帮助我们更快的分析c语言和汇编代码。

汇编代码语法风格

汇编语言分为intel风格和AT&T风格,前者被Microsoft Windows/Visual C++采用,Linux下,基本采用的是AT&T风格汇编,两者语法有很多不同的地方,这里我们只需要简单了解即可。

类别

AT&T

Intel

寄存器访问格式

pushl %eax

push eax

立即数表示方式pushl $1push 1
操作数顺序addl $2,%eaxadd eax , 2
字长表示movb val,%eaxmov al,byte ptr val
寻址方式section:disp(base, index, scale)section:[base + index*scale + disp]

x86寄存器介绍

  • 1.通用寄存器

顾名思义,通用寄存器是那些你可以根据自己的意愿使用的寄存器,但有些也有特殊作用,IA32处理器包括8个通用寄存器,分为3组

1️⃣数据寄存器

     EAX 累加寄存器,常用于运算;在乘除等指令中指定用来存放操作数,另外,所有的I/O指令都使用这一寄存器与外界设备传送数据。

    EBX 基址寄存器,常用于地址索引

    ECX 计数寄存器,常用于计数;常用于保存计算值,如在移位指令,循环(loop)和串处理指令中用作隐含的计数器.

    EDX 数据寄存器,常用于数据传递。

2️⃣变址寄存器

    ESI 源地址指针

    EDI 目的地址指针

3️⃣指针寄存器

    EBP为基址指针(Base Pointer)寄存器,存储当前栈帧的底部地址。

    ESP为堆栈指针(Stack Pointer)寄存器,一直记录栈顶位置,不可直接访问,push时ESP减小,pop时增大。

  • 2. 指令指针寄存器

EIP 保存了下一条要执行的指令的地址, 每执行完一条指令EIP都会增加当前指令长度的位移,指向下一条指令。用户不可直接修改EIP的值,但jmp、call和ret等指令也会改变EIP的值,jmp将EIP修改为目的指令地址,call修改EIP为被调函数第一条指令地址,ret从栈中取出(pop)返回地址存入EIP。

另外,在64位环境下,寄存器的名字也有变化, 是以r开头命名,比如rbp代表基址指针寄存器,rsp代表堆栈指针寄存器。

函数调用约定

调用函数将被调用函数参数入栈,入栈顺序由调用约定规定,包括cdecl,stdcall,fastcall,naked call等,c语言默认使用cdecl约定,参数从右往左入栈。关于调用约定的详细描述,可以参考:关于函数调用约定的一些知识.

平台调用约定
x86cdecl,stdcall
x64fastcall
arm, arm64ATPCS

函数栈帧分析

通常来说,我们将%ebp与%esp之间的区域称为函数栈帧(之所以这么划分,是因为只有这部分空间是属于本函数的),每个函数都有自己独立的函数栈,每调用一个新函数,就会生成一个新的栈帧。在一个函数栈中, %ebp代表函数栈底,在函数执行过程中是不会变的(除非发生新的函数调用), %esp作为栈顶指针会随着入栈出栈操作来变化, 我们将重点分析这两个寄存器在传递参数, 局部变量分配空间,函数返回过程中的变化。

在函数调用过程中,我们将调用函数的函数称为“调用者(caller)”,将被调用的函数称为“被调用者(callee)”。在这个过程中,我们需要弄明白以下几个问题,这也是分析函数栈帧的核心:

1. “被调用者”需要知道传入的参数在哪里;

2. “调用者”需要知道在哪里获取“被调用者”返回的值;

3. 局部变量在栈上是怎么分配空间的;

4. 返回的地址在哪里;

5. 需要保证在“被调用者”返回后,%ebp%esp 等寄存器的值应该和调用前一致,仿佛从来没有发生过函数调用一样;

下面,我们就以前文的c语言和汇编代码来分析函数栈帧的变化,我们重点分析add函数在调用前,调用中,调用返回后的栈帧变化, 相信,经过以下分析之后,我们就能够解决上面提到5个问题,对函数调用过程中栈帧变化有了新的认知。

汇编从line70~line77之间的代码是固定的, 在linux下编译出的程序汇编代码都是一样的,line77行可以认为是main函数栈帧的起始,将%ebp入栈,作为main函数的栈底。

函数栈帧变化总览图

 执行到这里, 就从get_x函数返回了,紧接着将会执行line48,  get_x的栈空间也被释放, 但是此时, 传递给get_x的两个参数依然位于栈上,所以line48就要将传递给get_x的参数从栈上去除。

总结

1. %ebp作为函数的栈底指针,在函数执行过程中是通过ebp寄存器来保存栈底指针, 是不变的(而且也不需要将ebp存入内存), 除非发生函数调用切换,被调用者将调用者的ebp入栈(这个不要理解错了,  调用者自己是不会保存自己的ebp的), %esp作为栈顶指针,会随着入栈出栈的操作来变化。

2. 函数参数传递方式依赖于选择的函数调用约定, x86平台c语言默认的函数调用约定为cdecl,即从右向左入栈,这个和基于ARM平台进行嵌入式开发时的ATPCS是不一样的。

3. 函数调用通过call实现, call命令做了两件事情,一是将EIP寄存器内的值压入栈中,称为返回地址,函数完成后还要返回到这个地址继续执行程序。然后将被调用函数第一条指令地址存入EIP中,由此进入被调函数,相当于执行push %eip和jump指令

4. 被调用函数栈帧空间是通过以下3条指令组合实现的:

pushl   %ebp        // 将调用者的栈底指针%ebp入栈
movl    %esp, %ebp  // 将栈顶指针赋值给栈底指针寄存器, 从此以后, 被调用者有了自己新的栈底指针
subl    $16, %esp   // 被调用者分配16字节的栈帧空间, 事实上是空出16字节

所有的函数调用,汇编代码的前几行 都是这样的。

5. 函数返回时要恢复现场(其实就是将本函数的栈帧空间释放), return语句相当于leave,ret两条指令

leave指令相当于

movl %ebp, %esp   //将栈底指针赋值给栈顶指针, 即释放被调函数栈空间
popl %ebp         //此操作将取出调用者的栈底指针, 赋值给ebp寄存器, 即恢复ebp为调用函数基址

ret指令相当于

pop %EIP

从函数栈帧变化总览图可以看出,经过leave指令之后, 栈顶存放的是eip,即返回地址,所以,经过ret指令后, 就成功的从被调用函数返回了。

经过leave,ret指令,被调用函数的栈帧空间被释放,但是依然没有恢复到调用之前的状态, 想一想,还差点什么?   由于在函数调用发生之前,调用者需要将被调用函数的参数入栈, 所以, 函数调用返回后,我们还需要从调用者的栈空间中还保留着传递给被调用函数的参数,所以,还需要执行

addl $8, %esp

将栈顶指针恢复到调用操作之前的状态(因为有两个整形参数,所以,栈顶需要移8字节), 到此, 调用者的栈空间就恢复如初了,仿佛没有发生过函数调用一样。

6.  被调用函数的栈空间

函数add作为main函数的被调用者, get_x函数的调用者, 它的函数栈帧空间包含了

  • 调用者main的栈底指针
  • 函数自身的局部变量
  • 传递给子函数get_x的参数
  • 子函数get_x的返回地址

7. 函数返回值

我们分析了函数栈帧变化过程,唯独没有在栈上发现函数返回值,事实上,它比较神秘。

在get_x函数中, 汇编line22行显示,最终的计算结果存放在%eax寄存器中, 而没有存放在栈中。

22         addl    %edx, %eax

在get_x函数返回后, 程序继续执行line48,line49,

48         addl    $8, %esp
49         movl    %eax, -8(%ebp)

line49行将%eax入栈, 即将get_x函数的返回值存入栈中, 为了方便后续的计算.

为什么get_x将结果存入%eax而不是直接入栈呢, 这其实是调用约定cdecl的规定,具体参考附录.

8. 值传递,引用传递本质区别

从函数栈帧变化过程中,我们也能看到通过值传递的形式传递参数,是将参数重新拷贝一份保存在栈中(试想,如果通过值传递形式传递一个大结构体,效率将是多么低下),这也就证实了我们对值传递的参数进行修改是无法真正修改变量本身的, 我们修改的只是变量的一份拷贝。如果使用指针传递参数,则在汇编代码中会借助leal指令将变量地址入栈, 使得子函数可以真正的修改变量的值。

参考

c函数调用过程栈帧分析

常见函数调用约定

附录

cdecl

在x86架构上,其内容包括:

  • 函数实参在线程栈上按照从右至左的顺序依次压栈。
  • 函数结果保存在寄存器EAX/AX/AL中
  • 浮点型结果存放在寄存器ST0中
  • 编译后的函数名前缀以一个下划线字符
  • 调用者负责从线程栈中弹出实参(即清栈)
  • 8比特或者16比特长的整形实参提升为32比特长。
  • 受到函数调用影响的寄存器(volatile registers):EAX, ECX, EDX, ST0 - ST7, ES, GS
  • 不受函数调用影响的寄存器: EBX, EBP, ESP, EDI, ESI, CS, DS
  • RET指令从函数被调用者返回到调用者(实质上是读取寄存器EBP所指的线程栈之处保存的函数返回地址并加载到IP寄存器)
     

这篇关于链接与装载---函数调用过程栈帧变化分析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1066647

相关文章

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

作业提交过程之HDFSMapReduce

作业提交全过程详解 (1)作业提交 第1步:Client调用job.waitForCompletion方法,向整个集群提交MapReduce作业。 第2步:Client向RM申请一个作业id。 第3步:RM给Client返回该job资源的提交路径和作业id。 第4步:Client提交jar包、切片信息和配置文件到指定的资源提交路径。 第5步:Client提交完资源后,向RM申请运行MrAp

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

安卓链接正常显示,ios#符被转义%23导致链接访问404

原因分析: url中含有特殊字符 中文未编码 都有可能导致URL转换失败,所以需要对url编码处理  如下: guard let allowUrl = webUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {return} 后面发现当url中有#号时,会被误伤转义为%23,导致链接无法访问

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者

MOLE 2.5 分析分子通道和孔隙

软件介绍 生物大分子通道和孔隙在生物学中发挥着重要作用,例如在分子识别和酶底物特异性方面。 我们介绍了一种名为 MOLE 2.5 的高级软件工具,该工具旨在分析分子通道和孔隙。 与其他可用软件工具的基准测试表明,MOLE 2.5 相比更快、更强大、功能更丰富。作为一项新功能,MOLE 2.5 可以估算已识别通道的物理化学性质。 软件下载 https://pan.quark.cn/s/57

衡石分析平台使用手册-单机安装及启动

单机安装及启动​ 本文讲述如何在单机环境下进行 HENGSHI SENSE 安装的操作过程。 在安装前请确认网络环境,如果是隔离环境,无法连接互联网时,请先按照 离线环境安装依赖的指导进行依赖包的安装,然后按照本文的指导继续操作。如果网络环境可以连接互联网,请直接按照本文的指导进行安装。 准备工作​ 请参考安装环境文档准备安装环境。 配置用户与安装目录。 在操作前请检查您是否有 sud

线性因子模型 - 独立分量分析(ICA)篇

序言 线性因子模型是数据分析与机器学习中的一类重要模型,它们通过引入潜变量( latent variables \text{latent variables} latent variables)来更好地表征数据。其中,独立分量分析( ICA \text{ICA} ICA)作为线性因子模型的一种,以其独特的视角和广泛的应用领域而备受关注。 ICA \text{ICA} ICA旨在将观察到的复杂信号

【软考】希尔排序算法分析

目录 1. c代码2. 运行截图3. 运行解析 1. c代码 #include <stdio.h>#include <stdlib.h> void shellSort(int data[], int n){// 划分的数组,例如8个数则为[4, 2, 1]int *delta;int k;// i控制delta的轮次int i;// 临时变量,换值int temp;in