我爱普洱茶 发表于 2023-5-4 04:34:59

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

   程序的执行可以理解为连续的函数调用,每一个用户态(用户态指的是CPU指令集权限ring 0,用户只能访问常用CPU指令集,在应用程序中运行)进程都对应一个调用栈结构,当一个函数执行完毕后,会自动回到原先调用函数的位置(call指令)的下一步命令并执行,堆栈结构的作用是保存函数返回地址、传递函数参数、记录本地变量、临时保存函数上下文(上下文,也就是执行函数所需要的相关信息)。
       寄存器:<br><br>      寄存器分配:<br><br>      寄存器是处理器加工数据和运行程序的重要载体,寄存器在程序执行中中负责存储数据和指令,因此函数调用与寄存器有重要联系。
  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寄存器作为栈帧指针寄存器,存储基地址,对于函数参数,偏移量为正值,对于局部变量,偏移量为负值
  寄存器的使用原则:<br><br>     主调函数指的是调用其他函数的函数,被调函数指的是被其他函数调用的函数
    主调函数一般使用eax、ecx、edx寄存器作为主调函数保存寄存器,当函数调用时,若主调函数希望保持这些寄存器的值,则必须在调用前显式地将其保存在栈中;被调函数可以覆盖这些寄存器,而不会破坏主调函数所需的数据,被调函数一般使用ebx、edi、esi作为被调函数保存寄存器,被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器。此外,被调函数必须保持寄存器%ebp和%esp,并在函数返回后将其恢复到调用前的值,亦即必须恢复主调函数的栈帧。
  栈帧结构:<br><br>   函数的调用通常是嵌套的,在同一时刻,堆栈中会有多个函数的信息,每一个未执行完成的函数都有一个连续独立的区域即栈帧,栈帧是堆栈的一个逻辑片段,当函数调用时,逻辑栈帧被压入堆栈中,当函数返回时,栈帧从堆栈中弹出,栈帧主要储存函数参数、函数内局部变量以及返回前一栈帧所需要的信息
          栈帧的作用:
         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函数栈帧的起始位置<br><br>    (3)同时,因为主调函数尚未执行完成,所以被调函数的栈帧并不能覆盖主调函数的栈帧,只能通过push和pop指令实现调用<br><br>         (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寄存器加上八<br><br>          在AT&T格式下:
          mov   %esp    目标操作数
          add    $8   %esp
  (5)leave指令:跟在ret指令后面,作用是交换esp和ebp的值
 
以下面一段程序为例:
 
<strong>#include <stdio.h><br><br>int sum(int a, int b)
{
    int s = a + b;<br><br>    return s;
}<br><br>int main(int argc, char *argv[])
{
    int n = sum(1, 2);
    return 0;
}</strong> 
通过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>:leaveq0x000000000040057b <+59>:retq
<br>现在开始执行第一条指令:
0x0000000000400540 <+0>:push %rbp <br>push %rbp:push指令将rsp寄存器减8开辟新的空间后,将rbp的值压入栈中,此时rbp的值为调用main函数的函数的帧基地址,pushrbp的原因是main函数需要rbp寄存器存储自己的帧基地址,<br><br><em id="__mceDel">但是又不能覆盖调用main函数的函数的帧基地址,因此通过push指令开辟八个字节的空间来存储调用main函数的函数的帧基地址,所以目前为止,main函数的栈帧中只有调用main函数的函数的帧基地址</em><em id="__mceDel"><em id="__mceDel">同时在这条指令之前,代码还没有到main函数,从这条指令开始进入main函数<br><br></em></em>0x0000000000400541 <+1>:mov %rsp,%rbp <br>将rsp寄存器的值赋值给rbp,使rbp和rsp指向同一个位置,即main函数栈帧的起始位置<br><br>0x0000000000400544 <+4>:sub $0x20,%rsp<br>将rsp寄存器减去32字节,使其指向更低位置,这是为了给main函数中局部变量和临时变量预留空间,这里注意的是,当程序开始运行时,操作系统会自动为程序分配32字节空间,但具体使用多少由rsp寄存器决定<br>另外,当该指令执行完后,main函数的空间就全部分配完成,分别是存储主调函数的8字节和预留的32字节<br><br>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中的数值被覆盖,就提前将他们存入栈帧中<br>通过rbp加偏移量的方式<br>后两条指令的目的是传递sum函数的实参,将rsi和rdi分别赋值为2和1,同时这里有一条规定,就是函数参数保存默认寄存器顺序为rdi、rsi、rdx。。。<br><br>0x0000000000400559 <+25>:callq 0x400526 <sum><br>使用call指令,如上文提到一般,call指令先将rip的值压入栈中保存起来,也就是0x40055e 这个地址,这里会将rsp的值减8来开辟新的空间,然后将rip的值修改为目标函数的值,<br>也就是call指令的操作数0x400526,执行完成后<br>跳转到sum函数<br><br>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# 函数返回<br><br>这里要注意一点就是之所以sum函数没有修改rsp的值来预留空间是因为sum是最后一个被调用的函数,他没有使用call指令,也就是说没有将rip的值压入栈中,<br>不需要修改rsp的值,也就是它预留的空间为栈中的所有剩余空间<br><br>然后继续执行 retq 指令, 该指令把 rsp 指向的栈单元当中的 0x40055e 取出给 rip 寄存器, 同时 rsp 加8, 这样, <br>rip 寄存器 中的值就变成了 main 函数中调用 sum 的 call 指令的下一条指令, 于是返回到 main 函数中继续执行.<br><br>继续执行 main 函数中的:
mov %eax,-0x4(%rbp)# 把sum函数的返回值赋给变量n该指令是把 rax 寄存器当中的值(sum函数返回值), 放入到 rbp-4 所指的内存, 也就是变量 n 所在的位置,继续执行程序结束
<br><br>  
   <br><br>    
         
           
          <br><br>免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: 函数调用栈的一些简单认识