【操作系统】深入函数调用堆栈

您所在的位置:网站首页 汇编转换c语言函数的方法 【操作系统】深入函数调用堆栈

【操作系统】深入函数调用堆栈

2024-07-12 00:15| 来源: 网络整理| 查看: 265

https://blog.csdn.net/qq_41884002/article/details/81452889(汇编)

https://blog.csdn.net/jxz_dz/article/details/47749603(excel图)

https://blog.csdn.net/cherishinging/article/details/70228949(带图详解※)

https://blog.csdn.net/qq_37340753/article/details/81062424(汇编学习笔记)

https://blog.csdn.net/wangyezi19930928/article/details/16921927(比较好的笔记※)

https://www.cnblogs.com/aliflycoris/p/5746143.html(非常强的动手步骤※看)

背景

代码段:存放代码的空间,这里的代码指的是二进制代码。比如你写了一个hello world的程序,总得有一个地方存放这段程序,而普通c语言代码存放在磁盘中,可是经过编译链接后的代码存放在哪?就是这个代码段中。而代码段中的数据只可读不可写。

BSS(Block Started by Symbol): 用于存放程序中未初始化的全局变量和静态变量的一块内存区域。可读可写。而未初始化的全局变量在编译前会被编译器自动置零。

数据段: 已初始化的全局变量和静态变量存于其中,属于静态内存分配。

区域作用栈(stack)由编译器自动分配和释放,存放函数的参数值,局部变量的值等。操作方式类似与数据结构中的栈堆(heap)一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。与数据结构中的堆是两码事,分配方式类似于链表静态区(static)全局变量和静态变量存放于此文字常量区常量字符串放在此,程序结束后由系统释放程序代码区存放函数体的二进制代码

CPU是完成工作的工人; 数据区,堆区,栈区等则是用来存放原料,半成品,成品等各种东西的场所; 存在代码区的指令则告诉CPU要做什么,怎么做,到哪里去领原材料,用什么工具来做,做完以后把成品放到哪个货舱去; 值得一提的是,栈除了扮演存放原料,半成品的仓库之外,它还是车间调度主任的办公室。     程序中所使用的缓冲区可以是堆区、栈区、甚至存放静态变量的数据区。缓冲区溢出的利用方法和缓冲区到底属于上面哪个内存区域密不可分

寄存器名称作用eax累加(Accumulator)寄存器,常用于函数返回值ebx基址(Base)寄存器,以它为基址访问内存ecx计数器(Counter)寄存器,常用作字符串和循环操作中的计数器edx数据(Data)寄存器,常用于乘除法和I/O指针esi源变址寄存器dsi目的变址寄存器esp栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶ebp基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部eip

 指令寄存器(extended instruction pointer), 其内存放着一个指针,该指针永远指向下一条待执行的指令地址

注: 可以说如果控制了EIP寄存器的内容,就控制了进程——我们让EIP指向哪里,CPU就会去执行哪里的指令

  ---> ebp 在未受改变之前始终指向栈帧的开始,也就是栈底,所以ebp的用途是在堆栈中寻址用的。

  ---> esp是会随着数据的入栈和出栈移动的,也就是说,esp始终指向栈顶。

 

汇编指令:

https://blog.csdn.net/bjbz_cxy/article/details/79467688(汇编指令集)

mov      用来移内存的值                              lea       移地址                                      push        从栈顶入栈

pop       从栈顶出栈                                  rep    stos          循环拷贝                      sub    a,b    a-b之后赋给a

ret       过程返回                                          call              过程调用

POP和PUSH(压栈和出栈)

汇编里把一段内存空间定义为一个栈,栈总是先进后出,栈的最大空间为 64K。由于 "栈" 是由高到低使用的,所以新压入的数据的位置更低,ESP 中的指针将一直指向这个新位置,所以 ESP 中的地址数据是动态的。

PUSH 指令

PUSH 指令首先减少 ESP 的值,再将源操作数复制到堆栈。操作数是 16 位的,则 ESP 减 2,操作数是 32 位的,则 ESP 减 4。PUSH 指令有 3 种格式:

PUSH reg/mem16 PUSH reg/mem32 PUSH inm32

格式: PUSH 操作数 //sub esp,4 ;mov [esp],EBP 操作数可以是寄存器,存储器,或者立即数

POP指令

POP 指令首先把 ESP 指向的堆栈元素内容复制到一个 16 位或 32 位目的操作数中,再增加 ESP 的值。如果操作数是 16 位的,ESP 加 2,如果操作数是 32 位的,ESP 加 4:

POP reg/mem16 POP reg/mem32

格式:POP 操作数 //mov EBP,[esp] ;add esp,4 操作数是寄存器,或者存储器,不能是立即数

特点

[EBP-??] // 局部变量 [ EBP+??] //上一个CALL 局部变量, 上一个CALL传入参数 CALL PUSH EIP RETN POP EIP

 

概念:汇编用的最多的是寄存器和内存之间的不断相互传值传地址,井然有序。寄存器能够保存的数据量不多,所以需要存储大量数据的时候就需要保存到内存里面了

什么是栈?

栈是向下生长的,所谓向下生长是指从内存高地址->地地址的路径延伸,那么就很明显了,栈有栈底和栈顶,那么栈顶的地址要比栈底低。对x86体系的CPU而言,其中

  ---> 寄存器ebp(base pointer )可称为“帧指针”或“基址指针”,其实语意是相同的。

  ---> 寄存器esp(stack pointer)可称为“ 栈指针”。

 

那么什么是栈帧呢?

百度百科的解释是:栈帧就是一个函数执行的环境。实际上,栈帧可以简单理解为:栈帧就是存储在用户栈上的(当然内核栈同样适用)每一次函数调用涉及的相关信息的记录单元。 栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(地址地)。  

什么是函数栈帧?

  函数栈帧:ESP和EBP之间的内存空间为当前栈帧,EBP标识了当前栈帧的底部,ESP标识了当前栈帧的顶部。      在函数栈帧中一般包含以下几类重要信息:   局部变量:为函数局部变量开辟内存空间。   栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡计算得到),用于在本帧被弹出后,恢复出上一个栈帧。   函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便函数返回时能够恢复到函数被调用前的代码区中继续执行指令。   注意:函数栈帧的大小并不固定,一般与其对应函数的局部变量多少有关。在以后几讲的调试实验中您会发现,函数运行过程中,其栈帧大小也是在不停变化的。

 

什么是堆栈

调用程序的时候,电脑会自动开辟一块内存空间专门用来放数据的,然后我们的数据就会想货物一样一个一个往上堆,需要用到的时候就拿出来使用,当程序执行完之后再腾出空间给其他程序继续使用,这个就是堆栈。 

栈的最常见操作有两种:压栈(PUSH),弹栈(POP);用于标识栈的属性也有两个:栈顶(TOP),栈底(BASE)

可以把栈想象成一摞扑克牌:   PUSH:为栈增加一个元素的操作叫做PUSH,相当于给这摞扑克牌的最上面再放上一张;   POP:从栈中取出一个元素的操作叫做POP,相当于从这摞扑克牌取出最上面的一张;   TOP:标识栈顶位置,并且是动态变化的。每做一次PUSH操作,它都会自增1;相反每做一次POP操作,它会自减1。栈顶元素相当于扑克牌最上面一张,只有这张牌的花色是当前可以看到的。   BASE:标识栈底位置,它记录着扑克牌最下面一张的位置。BASE用于防止栈空后继续弹栈,(牌发完时就不能再去揭牌了)。很明显,一般情况下BASE是不会变动的。   内存的栈区实际上指的就是系统栈。系统栈由系统自动维护,它用于实现高级语言中函数的调用。对于类似C语言这样的高级语言,系统栈的PUSH,POP等堆栈平衡细节是透明的。一般说来,只有在使用汇编语言开发程序的时候,才需要和它直接打交道。

函数调用流程 步骤总结

在main函数调用func_A的时候,首先在自己的栈帧中压入函数返回地址,然后为func_A创建新栈帧并压入系统栈   在func_A调用func_B的时候,同样先在自己的栈帧中压入函数返回地址,然后为func_B创建新栈帧并压入系统栈   在func_B返回时,func_B的栈帧被弹出系统栈,func_A栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址重新跳到func_A代码区中执行   在func_A返回时,func_A的栈帧被弹出系统栈,main函数栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址跳到main函数代码区中执行  注意:在实际运行中,main函数并不是第一个被调用的函数,程序被装入内存前还有一些其他操作,上图只是栈在函数调用过程中所起作用的示意图  每一个函数独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。

函数调用

  参数入栈:将参数从右向左依次压入系统栈中  返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行  代码区跳转:处理器从当前代码区跳转到被调用函数的入口处  栈帧调整:具体包括  保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP入栈)  将当前栈帧切换到新栈帧。(将ESP值装入EBP,更新栈帧底部)  给新栈帧分配空间。(把ESP减去所需空间的大小,抬高栈顶)

函数返回

  保存返回值:通常将函数的返回值保存在寄存器EAX中   弹出当前栈帧,恢复上一个栈帧:   具体包括

  在堆栈平衡的基础上,给ESP加上栈帧的大小,降低栈顶,回收当前栈帧的空间  将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器,恢复出上一个栈帧  将函数返回地址弹给EIP寄存器  跳转:按照函数返回地址跳回母函数中继续执行 具体例:

通常我们规定使用寄存器里面的EBP寄存器保存一个地址,作为堆栈的栈底,ESP寄存器保存一个地址,作为堆栈的栈顶,用EAX寄存器来保存需要输出的数据。   

堆栈中:

 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓现在要实现以下功能↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

                                          

以上的指令含义分别为:

                                        1.将立即数“2”压进栈道里,并且栈顶提升4个字节                                         2.将立即数“1”压进栈道里,并且栈顶提升4个字节                                         3.进入名为“kong”的函数,把地址“0040100F”保存到EIP寄存器     里,并且把00401070(0040106c+0x4)压进栈道,并且栈顶提升4个 字节                                         4.从函数中出来以后,栈顶esp+8(恢复栈顶)

                 ↓↓↓↓↓↓↓↓↓↓进入名为“kong”的函数后,需要执行以下的指令↓↓↓↓↓↓↓↓↓↓

     ↓↓↓↓↓↓↓↓↓↓所以,“准备工作”堆栈图如下↓↓↓↓↓↓↓↓↓↓    

↓↓↓↓↓↓↓↓↓↓然后,执行完程序之后,堆栈的收尾工作如下↓↓↓↓↓↓↓↓↓↓

更细节图示:

1.初始

2.压入参数

两个Push指令,ESP-8.

3.调用函数

    注: call指令两个要点  A : 将下一条指令地址 (返回地址)入栈 , ESP - 4

                                          B : 将call指令后的地址存至 EIP 寄存器

4.保存ebp

5.更新当前栈顶作为栈底

6.开辟当前函数栈空间

               4,5,6 这三条指令合称为打开栈帧. 

               注 : sub  esp , 0xC0 其中的0xC0为编译器为该函数分配的缓冲区大小,其大小可由编译器设置

7.保存现场

8.进入函数调用(Debug版 编译器向缓冲区填充 int 3(即 0xCC)指令)

  注:后四条指令才真正为函数的核心功能

     EBP本身的地址编号存放的是 上一个函数的EBP的值

                    EBP - XX 为局部变量,  EBP + 4为返回地址 , EBP + 8及以后为参数

9.恢复现场

这里首先edi, esi, ebx逐步出栈,每出栈一个,esp向下移动一步

10.恢复栈帧

将ebp的值赋给esp,也就是将esp移至ebp的位置,最后ebp出栈,将出栈的内容保存到ebp(此时ebp中保存的是main函数的ebp地址),回到main函数的栈帧。

11.结束函数调用,返回ret

                                            RET

注意:ret指令会使得出栈一次,并将出栈的内容当作地址,将程序执行跳转到该地址。

                   注: 该处采用的是C调用方式, 调用者平衡堆栈 

                         RET指令 将返回地址弹出栈  ESP+4

12.平衡堆栈

可以对比第一幅图的栈顶栈帧,是相同的。另外,ESP , EBP的值随机 。

1、每个CALL会分配一个独立的栈段空间,供局部变量使用. 栈段空间大小一般要大于局部变量所需空间大小之和 ebp-esp=栈段空间大小。 2、CALL栈平衡。进CALL前与出CALL后 EBP和ESP的值不变。

 

例2:

 见下图,假设函数A调用函数B,我们称A函数为"调用者",B函数为“被调用者”则函数调用过程可以这么描述:

  (1)先将调用者(A)的堆栈的基址(ebp)入栈,以保存之前任务的信息。

  (2)然后将调用者(A)的栈顶指针(esp)的值赋给ebp,作为新的基址(即被调用者B的栈底)。

  (3)然后在这个基址(被调用者B的栈底)上开辟(一般用sub指令)相应的空间用作被调用者B的栈空间。

  (4)函数B返回后,从当前栈帧的ebp即恢复为调用者A的栈顶(esp),使栈顶恢复函数B被调用前的位置;然后调用者A再从恢复后的栈顶可弹出之前的ebp值(可以这么做是因为这个值在函数调用前一步被压入堆栈)。这样,ebp和esp就都恢复了调用函数B前的位置,也就是栈恢复函数B调用前的状态。

\

函数调用栈※

https://blog.csdn.net/wangyezi19930928/article/details/16921927

当发生函数调用的时候,栈空间中存放的数据是这样的: 1、调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中,即:从右向左依次把被调函数所需要的参数压入栈; 2、调用者函数使用call指令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call指令中); 3、在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp); 4、在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即:这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈; 所以,发生函数调用时,入栈的顺序为: 参数N 参数N-1 参数N-2 ..... 参数3 参数2 参数1 函数返回地址 上一层调用函数的EBP/BP 局部变量1 局部变量2 .... 局部变量N  

解释:  //EBP 基址指针,是保存调用者函数的地址,总是指向函数栈栈底,ESP被调函数的指针,总是指向函数栈栈顶。 首 先,将调用者函数的EBP入栈(pushebp),然后将调用者函数的栈顶指针ESP赋值给被调函数的EBP(作为被调函数的栈底,movebp,esp),此时,EBP寄存器处于一个非常重要的位置,该寄存器中存放着一个地址(原EBP入栈后的栈顶),以该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数的局部变量值,而该地址处又存放着上一层函数调用时的EBP值;

一般规律,SS:[ebp+4]处为被调函数的返回地址,SS:[EBP+8]处为传递给被调函数的第一个参数(最后一个入栈的参数,此处假设其占用4字节内存)的值,SS:[EBP-4]处为被调函数中的第一个局部变量,SS:[EBP]处为上一层EBP值;由于EBP中的地址处总是"上一层函数调用时的EBP值",而在每一层函数调用中,都能通过当时的EBP值"向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取被调函数的局部变量值";

如此递归,就形成了函数调用栈;

 这个图片中反映的是一个典型的函数调用栈的内存布局; 访问函数的局部变量和访问函数参数的区别: 局部变量总是通过将ebp减去偏移量来访问,函数参数总是通过将ebp加上偏移量来访问。对于32位变量而言,第一个局部变量位于ebp-4,第二个位于ebp-8,以此类推,32位局部变量在栈中形成一个逆序数组;第一个函数参数位于ebp+8,第二个位于ebp+12,以此类推,32位函数参数在栈中形成一个正序数组。  

 

总结

在了解了函数栈帧的调用过程后,我们来思考一下为什么要研究栈帧呢? 一个码农要是没遇见过coredump,那就幸运了。core file(coredump的转储文件)中保存的最重要内容之一,就是函数的call trace。还原这部分内容(栈回溯),并与原代码对应上,尽快找出程序崩溃的位置和原因,是码农们一生的责任。当然,你如果有良好的开发环境和开发习惯,保留了现场环境(core file and lib file等)和unstrip的原程序,那么恭喜,也许你不用太费神,直接用GDB的backtrace功能,就可以找到症结所在。当然如果栈被冲掉了一部分,backtrace出来的就是一堆问号,要找出call trace就不容易了。这在缓冲区溢出时经常碰到。 总而言之,研究栈帧可以对内存管理有更深刻的认识。  



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3