首页 > 其他分享 >函数调用栈的一些简单认识

函数调用栈的一些简单认识

时间:2023-05-03 23:03:07浏览次数:30  
标签:函数 认识 简单 mov 函数调用 rbp 指令 寄存器 main

   程序的执行可以理解为连续的函数调用,每一个用户态(用户态指的是CPU指令集权限ring 0,用户只能访问常用CPU指令集,在应用程序中运行)进程都对应一个调用栈结构,当一个函数执行完毕后,会自动回到原先调用函数的位置(call指令)的下一步命令并执行,堆栈结构的作用是保存函数返回地址、传递函数参数、记录本地变量、临时保存函数上下文(上下文,也就是执行函数所需要的相关信息)。

       寄存器:

      寄存器分配:

      寄存器是处理器加工数据和运行程序的重要载体,寄存器在程序执行中中负责存储数据和指令,因此函数调用与寄存器有重要联系。

  32位CPU所含有的寄存器有:

    8个32位通用寄存器,其中包含4个数据寄存器(EAX、EBX、ECX、EDX)、2个变址寄存器(ESI和EDI)和2个指针寄存器(ESP和EBP)
    6个段寄存器(ES、CS、SS、DS、FS、GS)
    1个指令指针寄存器(EIP)
    1个标志寄存器(EFLAGS)
  最初的8086平台使用16位寄存器,每个寄存器都有具体特定的用途,但随着32位寄存器的出现,32位寄存器采用平台寻址方式,因此对特殊寄存器没有过多要求,但由于历史原因,16位寄存器的名字被保存,EAX,EBX,ECX,EDX,ESI,EDI这六个寄存器通常作为通用寄存器使用,但是部分指令会有特定的源寄存器或者目的寄存器(比如%eax通常用于保存函数返回值),因此为避免兼容性问题,ABI规范各个寄存器的作用,EAX通常用于保存函数返回值,EBX用于存储基地址,ECX是计数器,重复前缀指令(REP,X86汇编指令,使指定指令重复n次,但只能指定一条语句)和LOOP指令(循环指令,能够执行代码块)的内定计数器,循环重复执行次数将保留在cx中,EDX一般用来储存整数除法中的余数部分(当函数体中包含除法时,EAX保留整数部分,EDX则负责保存余数部分,乘除关系一般都与EAX、EDX有关),而EDI、ESI则通常用于储存函数参数

  EIP指令寄存器通常指向下一条待执行的指令地址(代码段内的偏移量),每完成一条汇编指令,EIP的值就会增加,ESP指向当前函数的栈帧结构的栈顶位置,EBP则始终指向当前函数的栈帧结构的栈底位置,同时注意EIP寄存器不能通过寻常方式访问到(无法获得opcode)

     在Intel  CPU中,通常将EBP寄存器作为栈帧指针寄存器,存储基地址,对于函数参数,偏移量为正值,对于局部变量,偏移量为负值

  寄存器的使用原则:

     主调函数指的是调用其他函数的函数,被调函数指的是被其他函数调用的函数

    主调函数一般使用eax、ecx、edx寄存器作为主调函数保存寄存器,当函数调用时,若主调函数希望保持这些寄存器的值,则必须在调用前显式地将其保存在栈中;被调函数可以覆盖这些寄存器,而不会破坏主调函数所需的数据,被调函数一般使用ebx、edi、esi作为被调函数保存寄存器,被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器。此外,被调函数必须保持寄存器%ebp和%esp,并在函数返回后将其恢复到调用前的值,亦即必须恢复主调函数的栈帧。

  栈帧结构:

   函数的调用通常是嵌套的,在同一时刻,堆栈中会有多个函数的信息,每一个未执行完成的函数都有一个连续独立的区域即栈帧,栈帧是堆栈的一个逻辑片段,当函数调用时,逻辑栈帧被压入堆栈中,当函数返回时,栈帧从堆栈中弹出,栈帧主要储存函数参数、函数内局部变量以及返回前一栈帧所需要的信息

          栈帧的作用:

         1、保存主调函数的局部变量

   2、向被调函数传递参数

   3、返回被调函数的返回值

   4、返回函数的返回地址(即当被调用函数执行完成时应当执行的下一条指令)

   栈帧的边界由栈帧基寄存器EBP和栈顶寄存器ESP来界定,EBP位于栈底,高地址,在栈帧内位置固定,ESP位于栈顶,低地址,位置随着出栈和入栈而发生变化,因而数据访问通常通过EBP来进行(通过偏移量来访问)

          ESP指向栈顶,EBP一般指向栈帧的开始位置

   现在在假定有一个程序:A()——>B()——>C()

   那么A中元素包括A函数的局部变量,传给函数B的参数、B函数的返回值、执行完B的下一条指令的地址

                  B中元素包括B函数的局部变量、传给函数C的参数、C函数的返回值、执行完C的下一条指令的地址

     C中元素包括C函数的局部变量

因此:

         (1)被调函数的参数和返回值保存在主调函数的栈帧中

    (2)以栈帧为单位,那么C函数栈帧位于栈顶,ESP寄存器指向C函数栈帧的栈顶(即整个栈的栈顶),而EBP寄存器则指向C函数栈帧的起始位置

    (3)同时,因为主调函数尚未执行完成,所以被调函数的栈帧并不能覆盖主调函数的栈帧,只能通过push和pop指令实现调用

         (4)栈的生长方向为由高地址到低地址,数据填充则是由低地址到高地址

接下来再介绍几个汇编指令:

  (1)call指令:执行call指令时,会将EIP的值通过push压入栈中(因为EIP保存的是CPU即将执行的下一条指令的地址,所以这一步就对应前面说的保存函数返回地址,解释了为什么函数的返回地址保存在主调函数中),然后将EIP的值修改为被调函数的值,则当call执行完成后,将自动调用目标函数(被调函数)

  (2)ret指令:将call指令中压入栈中的EIP的值(返回地址)pop回到EIP中,则ret指令执行完成后,将执行主调函数的下一条指令

  (3)push指令:将ESP寄存器减去八,将ESP寄存器向高地址移动,从而开辟新的空间,然后把操作数复制到ESP所指的位置上

          在AT&T格式下:

         sub   $8    %esp

         mov   源操作数   esp

  (4)pop指令:将ESP寄存器的所存的值传到指定位置,然后将ESP寄存器加上八

          在AT&T格式下:

          mov   %esp    目标操作数

          add    $8   %esp

  (5)leave指令:跟在ret指令后面,作用是交换esp和ebp的值

 

以下面一段程序为例:

 

#include <stdio.h>

int sum(int a, int b)
{
    int s = a + b;

    return s;
}

int main(int argc, char *argv[])
{
    int n = sum(1, 2);
    return 0;
}

 

通过gdb调试后:

  0x0000000000400540 <+0>:push  %rbp
  0x0000000000400541 <+1>:mov   %rsp,%rbp
  0x0000000000400544 <+4>:sub   $0x20,%rsp
  0x0000000000400548 <+8>:mov   %edi,-0x14(%rbp)
  0x000000000040054b <+11>:mov   %rsi,-0x20(%rbp)
  0x000000000040054f <+15>:mov   $0x2,%esi
  0x0000000000400554 <+20>:mov   $0x1,%edi
  0x0000000000400559 <+25>:callq 0x400526 <sum>
  0x000000000040055e <+30>:mov   %eax,-0x4(%rbp)
  0x0000000000400561 <+33>:mov   -0x4(%rbp),%eax
  0x0000000000400564 <+36>:mov   %eax,%esi
  0x0000000000400566 <+38>:mov   $0x400604,%edi
  0x0000000000400575 <+53>:mov   $0x0,%eax
  0x000000000040057a<+58>:leaveq  0x000000000040057b <+59>:retq  

现在开始执行第一条指令:

0x0000000000400540 <+0>:push %rbp 
push %rbp:push指令将rsp寄存器减8开辟新的空间后,将rbp的值压入栈中,此时rbp的值为调用main函数的函数的帧基地址,push rbp的原因是main函数需要rbp寄存器存储自己的帧基地址,

但是又不能覆盖调用main函数的函数的帧基地址,因此通过push指令开辟八个字节的空间来存储调用main函数的函数的帧基地址,所以目前为止,main函数的栈帧中只有调用main函数的函数的帧基地址
同时在这条指令之前,代码还没有到main函数,从这条指令开始进入main函数

0x0000000000400541 <+1>:mov %rsp,%rbp 
将rsp寄存器的值赋值给rbp,使rbp和rsp指向同一个位置,即main函数栈帧的起始位置

0x0000000000400544 <+4>:sub $0x20,%rsp
将rsp寄存器减去32字节,使其指向更低位置,这是为了给main函数中局部变量和临时变量预留空间,这里注意的是,当程序开始运行时,操作系统会自动为程序分配32字节空间,但具体使用多少由rsp寄存器决定
另外,当该指令执行完后,main函数的空间就全部分配完成,分别是存储主调函数的8字节和预留的32字节

0x0000000000400548 <+8> :mov %rdi,-0x14(%rbp) #保存main函数的第1个参数
0x000000000040054b <+11>:mov %rsi,-0x20(%rbp) #保存main函数的第2个参数
0x000000000040054f <+15>:mov $0x2,%rsi #sum函数的第2个参数放入esi寄存器
0x0000000000400554 <+20>:mov $0x1,%rdi #sum函数的第1个参数放入edi寄存器
前两条指令的目的是保存rdi和rsi的值,因为在调用main函数时,rdi和rsi分别保存了argc和argv两个参数,而接下来要调用sum函数,则为了防止rdi和rsi中的数值被覆盖,就提前将他们存入栈帧中
通过rbp加偏移量的方式
后两条指令的目的是传递sum函数的实参,将rsi和rdi分别赋值为2和1,同时这里有一条规定,就是函数参数保存默认寄存器顺序为rdi、rsi、rdx。。。

0x0000000000400559 <+25>:callq 0x400526 <sum>  
使用call指令,如上文提到一般,call指令先将rip的值压入栈中保存起来,也就是0x40055e 这个地址,这里会将rsp的值减8来开辟新的空间,然后将rip的值修改为目标函数的值,
也就是call指令的操作数0x400526,执行完成后
跳转到sum函数

0x0000000000400526 <+0>:push  %rbp  # 保存main函数的rbp的值入栈             
0x0000000000400527 <+1>:mov   %rsp,%rbp # 修改当前rbp的值为当前的栈顶
0x000000000040052a <+4>:mov   %edi,-0x14(%rbp) # 把第1个参数放入临时变量
0x000000000040052d <+7>:mov   %esi,-0x18(%rbp) # 把第2个参数放入临时变量
0x0000000000400530 <+10>:mov  -0x14(%rbp),%edx # 将第1个临时变量放入到 edx 当中
0x0000000000400533 <+13>:mov  -0x18(%rbp),%eax # 将第2个临时变量放入到 eax 当中
0x0000000000400536 <+16>:add  %edx, %eax # 进行加法计算, 结果保存在 eax 当中
0x0000000000400538 <+18>:mov  %eax,-0x4(%rbp) # 将 eax 的值保存到临时变量中
0x000000000040053b <+21>:mov  -0x4(%rbp),%eax # 将临时变量的值放入到 eax 寄存器当中
0x000000000040053e <+24>:pop  %rbp # 出栈, 恢复main函数的 rbp 的值
0x000000000040053f <+25>:retq  # 函数返回

这里要注意一点就是之所以sum函数没有修改rsp的值来预留空间是因为sum是最后一个被调用的函数,他没有使用call指令,也就是说没有将rip的值压入栈中,
不需要修改rsp的值,也就是它预留的空间为栈中的所有剩余空间

然后继续执行 retq 指令, 该指令把 rsp 指向的栈单元当中的 0x40055e 取出给 rip 寄存器, 同时 rsp 加8, 这样,
rip 寄存器 中的值就变成了 main 函数中调用 sum 的 call 指令的下一条指令, 于是返回到 main 函数中继续执行.

继续执行 main 函数中的:

mov %eax,-0x4(%rbp)  # 把sum函数的返回值赋给变量n

该指令是把 rax 寄存器当中的值(sum函数返回值), 放入到 rbp-4 所指的内存, 也就是变量 n 所在的位置,继续执行程序结束



 

 

   

    

         

           

          

标签:函数,认识,简单,mov,函数调用,rbp,指令,寄存器,main
From: https://www.cnblogs.com/alexlance/p/17354085.html

相关文章

  • 我在困难时激励自己的一个简单策略
    很容易忘记你所取得的成就。无论是在工作还是家庭中,你都会记得自己的失败,而不是成功。也许这是进化的结果,或者这就是我们大脑的本质。但我一再注意到这一点。糟糕的时刻总是在我们的记忆中格外鲜明。好的成就则在我们的生活中融为一体。这种人性的怪癖对你的自尊和动力有很大......
  • python学习笔记8(json数据格式、pycharts简单折线图)
    1.jsonjson是一种轻量级的数据交互格式,可以以json指定的格式去组织和封装数据;json本质上是一个带有特定格式的字符串;json负责不同编程语言中的数据传递和交互;1.1python数据与json数据相互转化引入json模块importjson1.1.1python数据转json数据importjson#python列表......
  • CsvHelper简单使用
    发现一个比较好用的处理csv的C#库,CsvHelper:CsvHelper是一个用于读取和写入CSV文件的C#库,支持自动类型转换、自定义类型转换器和灵活的映射选项等功能,使得读写CSV文件变得非常方便。安装:Install-PackageCsvHelper读取csv使用CsvHelper读取CSV文件非常简单。首先,您需要创建......
  • 简单工厂模式(Static Factory Method)
    创建性设计模式——简单工厂模式(StaticFactorymethod)模式动机只需要知道参数的名字则可得到相应的对象软件开发时,有时需要创建一些来自于相同父类的类的实例。可以专门定义一个类(工厂)负责创建这些类的实例。可以通过传入不同的参数从而获得不同的对象。Java中可以将创建其......
  • python-docx的简单使用
    '''设置表格所有单元格的四个边为0.5磅,黑色,实线可以使用返回值,也可以不使用'''def设置表格网格线为黑色实线(table_object:object):kwargs={"top":{"sz":4,"val":"single","color":"#000000"},......
  • 用spring做一个简单的员工管理系统
    一、首先我们需要一个数据库,这里我用MySQL,也可以用其他的数据库二、开始写后台代码,这里我用的IDEA,也可以用EClipse,看个人习惯1、先新建项目,并且完善项目结构 2、项目完善好就可以在pom.xml文件中导入需要用到的jar包,我个人建议先导常用的,后面还有需要用到的再回来导就行......
  • 10.起火迷宫(简单搜索 多源BFS)
    起火迷宫↑题目链接题目一个迷宫可以看作一个\(R\)行\(C\)列的方格矩阵。其中一些方格是空地,用.表示,其他方格是障碍,用#表示。开始时,乔位于一块空地之中。迷宫中一些空地已经起火了,幸运的是火还没有蔓延至乔所在的位置。为了避免被火烧伤,乔需要尽快逃离迷宫。已知......
  • 12.石油储备(简单搜索 DFS/BFS 统计连通块个数)
    石油储备题目一片土地可以看作是一个\(n\)行\(m\)列的方格矩阵。其中一些方格藏有石油,用@表示,其余方格没有石油,用*表示。每个方格都与其上、下、左、右、左上、右上、左下、右下八个方格视为相邻。如果两个藏有石油的方格相邻,则它们被认为是处于同一片油田,否则它们被......
  • 14.找路(简单搜索 BFS 最短步数)
    找路↑题目链接题目给定一个\(n\)行\(m\)列的方格矩阵。其中有些方格是空地(可以进入),有些方格是餐厅(可以进入),有些方格是障碍(不可进入)。开始时,小\(Y\)和小\(M\)各自位于一个空地方格中。每个人都可以沿上下左右四个方向进行移动,移动一格距离需要花费\(11\)分钟时间。......
  • 简单聊聊,使用Vue.js编写命令行界面,前端开发CLI的利器
    Temir介绍Temir,一个用Vue组件来编写命令行界面应用的工具.开发者只需要使用Vue就可以编写命令行应用,不需要任何额外的学习成本.<scriptlang="ts"setup>import{ref}from'@vue/runtime-core'import{TBox,TText}from'@temir/core'constcounter=ref(0)setIn......