C语言之反汇编查看函数栈帧的创建与销毁(二)

简介: C语言之反汇编查看函数栈帧的创建与销毁(二)

C语言之反汇编查看函数栈帧的创建与销毁(一):https://developer.aliyun.com/article/1427046

第十一、十二、十三条指令

  • 我们开始初始化三个变量,每条指令对应上一条代码
  int a = 10;
00EE18F5  mov         dword ptr [ebp-8],0Ah  
  int b = 20;
00EE18FC  mov         dword ptr [ebp-14h],14h  
  int c = 0;
00EE1903  mov         dword ptr [ebp-20h],0
  • 其中【mov】是数据转移指令,也就是是将10这个值【ebp - 8】这块地址上
  • 为什么说0Ah就是10呢?因为0Ah是10的十六进制表示形式,在十六进制中A值得就是10
  • 对于14h的话就是16 * 1 + 4 = 20,那就是将20这个值放到【ebp - 14】这块地址上去
  • 最后一句就是将0这个值放到【ebp - 20】这块地址上去
  • 对于为什么-8,-14,-20呢,这是取决于编译器本身的,我是用的是VS2022,可能你到其他编译器上就不一样了
  • 这就可以得出一个结论:我们所定义的变量在栈内存中并不是呈现一个连续存放的,可能是分散的
  • 接下去继续来看这三次的存放值的变化~~

  • 我们再来看图,也将这些画出来。

第十四、十五、十六、十七条指令

  • 此时main函数中的变量创建好了,那就要调用Add函数了
00EE190A  mov         eax,dword ptr [ebp-14h]  
00EE190D  push        eax  
00EE190E  mov         ecx,dword ptr [ebp-8]  
00EE1911  push        ecx
  • 来看第一条,将【ebp-14h】这块地址的内容放到寄存器【eax】中去,那这个时候你就会想到这个【ebp-14】是刚才放数值20,然后压栈。
  • 第三条就是将【ebp-8】中的内容放到寄存器【ecx】中去,它【ebp-8】的地方存放的就是我们刚才放10的地方,然后压栈。

  • 这样就可以看出,这两个变量相当于实参的一份临时拷贝,这里就回到我们前面学的函数的形参就是实参的一份临时拷贝

再来到VS中看看

第十八条指令

00EE1912  call        00EE10B9
  • 对于这条【call】指令而言,比较特殊,它有两个作用

①压入返回地址

②转入目标函数

  • 这里就是要压的是 call指令的下一条地址
00EE1917  //这条就是要压入的地址
  • 然后我们来在vs中看一下,当运行到图中的那条语句的时候就要按F11,不能按F10,和调试一个道理

  • 把这块地址压入栈中

第十九、二十、二一条指令

  • 到19条指令开始,就进入Add函数了,这里的函数前面和在main函数中的前面也是非常的相似
  • 所以这个就是在开辟栈帧
00EE1790  push        ebp  
00EE1791  mov         ebp,esp  
00EE1793  sub         esp,0CCh  
00EE1799  push        ebx  
00EE179A  push        esi  
00EE179B  push        edi
  • 首先来看第一条指令。也就是将之前的【ebp】栈底寄存器的值压入到栈顶中
00EE1790  push        ebp
  • 对于此处的【ebp】,自从它在维护main函数的栈底后就没有再动过来,所以这里push上来的就是main函数的【ebp】

00EE1791  mov         ebp,esp
  • 接着再来看第二条,也就是将main函数的【esp】重新赋给【ebp】,这里要注意了,不要搞混,此时的【ebp】应该算是在维护Add函数的栈底了

  • 于是,栈就变成了这样:

00EE1793  sub         esp,0CCh
  • 接着第三条,【sub】命令使得【esp】存放的地址块减去一个CC的大小,继续结合上面那条指令,此时Add函数的栈顶和栈底都被找到了

  • 此时就相当于是在做一个迭代的操作

第二二、二三、二四条指令

00EE1799  push        ebx  
00EE179A  push        esi  
00EE179B  push        edi
  • 接下去还是一样的三条压栈操作
  • 来到VS中观看【esp】的变化

  • 接着将这三个寄存器压入栈

第二五、二六、二七、二八条指令

  • 对于这四条指令和上面main函数的创建过程类似,便不做不过分析
00EE179C  lea         edi,[ebp-0Ch]  
00EE179F  mov         ecx,3  
00EE17A4  mov         eax,0CCCCCCCCh  
00EE17A9  rep stos    dword ptr es:[edi]
  • 继续到VS中观看的变化

第二十九条指令

  • 接下去我们进入第二十九条指令,也就是对Add函数中存放计算总和的变量z进行初始化操作。
  • 【mov】做数据转移,将0放到【ebp-8】这块地址上去
int z = 0;
00EE17B5  mov         dword ptr [ebp-8],0

  • 然后我们在Add的栈帧中初始化这个变量z

第三十、三十一、三十二条指令

  • 接下去的三条指令就是对两个形参的值进行一个相加
  z = x + y;
00EE17BC  mov         eax,dword ptr [ebp+8]  
00EE17BF  add         eax,dword ptr [ebp+0Ch]  
00EE17C2  mov         dword ptr [ebp-8],eax
  • 那么上面不是只初始化了一个变量z吗,变量x和变量y在哪里呢?
  • 我们之前有做过了一步操作,也就是将这两个实参的拷贝进行了一个压栈操作,那时就说了对于这个就是形参
00EE190A  mov         eax,dword ptr [ebp-14h]  
00EE190D  push        eax  
00EE190E  mov         ecx,dword ptr [ebp-8]  
00EE1911  push        ecx
  • 此时我们就要通过这三句指令去找回这两个形参的值,关键的就是【ebp+8】和【ebp+0Ch】。因为我们在入栈的时候【ebp】寄存器存放的地址都是逐渐变小的,因为 栈是从高地址往低地址生长的,所以我们要去找回之前压入的内容,就要把地址加回去
  • 如下图所示

  • 找到这两个值之后,首先将【10】放到【eax】寄存器中去,然后再将【20】在加到寄存器【eax】原有的值上去,此时【eax】中存放的便是【30】

  • 注意看寄存器【eax】的变化

  • 这里还可以直接到指令这里来看。直接将鼠标放到【z】上面就可以看到了

  • 然后再将计算出来存放在【eax】中的值再放回【ebp-8】这块地址上去
00EE17C2  mov         dword ptr [ebp-8],eax
  • 首先到VS中来看看变化

  • 然后修改一下之前Add函数栈帧中存放z的内容

第三十三条指令

  • z计算出来了,此时就要执行【return z】这句代码,将z返回给main函数,但是函数栈帧中可不是这么做的
return z;
00EE17C5  mov         eax,dword ptr [ebp-8]
  • 看上面的指令可以看到,是将【ebp-8】中的内容转存到寄存器【eax】中去
  • 从【eax】~【ebx】这些寄存器都可以用来存放临时数据,并不是说上一次用过了就不能再用了,这其实和我们在定义一个变量后进行反复使用是一个道理。
  • 然后在Add函数调用结束后,它所对应的函数栈帧就会被销毁,此时被创建出来的临时变量【z】就不复存在了,因为【z】也是存放在Add的函数栈帧中的,所以这一步的操作其实就是将我们在Add函数中计算出来的值给保存起来,因为寄存器而言程序没有结束的话它是不会被销毁的,我们后面还可以到这个寄存器中去取数据

3.3.4 函数栈帧的销毁

接下去要进行的就是函数栈帧的销毁操作

第三十四、三十五、三十六条指令

  • 接下来就是三条pop的指令,也就是在栈顶弹出对应的值,然后放到对应的寄存器中去
00EE17C8  pop         edi      //在栈顶弹出一个值,存放到edi中,esp+4
00EE17C9  pop         esi     //在栈顶弹出一个值,存放到esi中,esp+4
00EE17CA  pop         ebx     //在栈顶弹出一个值,存放到ebx中,esp+4
  • 我们先到VS中来看看

  • 通过图示来看一下

第三十七条指令

  • 当给Add函数开辟函数栈帧的时候,最后一步是把【esp】中存放的内容给到【ebp】,也就是相当于就是让【ebp】指向和【esp】的同一块空间
  • 下面这句指令就是将【ebp】中存放的内容给到【esp】,那其实就是让【esp】指向和【ebp】的同一块空间
00EE17D8  mov         esp,ebp
  • 通过图示来看一下

  • 到VS中来看一下

第三十八条指令

00EE17DA  pop         ebp
  • 这句指令很重要,因为此时Add函数的函数栈帧已经被销毁了,此时我们要回到main函数的函数栈帧,那么两个维护栈顶和栈底的寄存器就要发生变化,此时我们要pop的【ebp】是之前压栈进来的main函数的ebp
  • pop的作用:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
  • pop了之后【esp】也要发生一个变化

  • 到VS中再来看一下变化。此时不要混淆了,栈是从高地址往低地址增长的,所以栈底的地址来的大一些

第三十九条指令

  • 这里只有一个【ret】,这个指令会从栈顶弹出一个值,那这个时候从上图其实可以看到此时的【esp】栈顶寄存器指向的这块地址,这块地址是call指令的下一条指令地址,就是我们在进入Add函数前提前压入的地址
00EE17DB  ret
  • 此时就会直接跳转到call指令下一条指令的地址处,继续往下执行

  • 再来看看【esp】的变化

第四十条指令

  • 有的同学看到的就是一个【esp】的变化,【add】是加法命令,也就是将【esp】的位置加上一个8,一块内存空间是4,加8的话那此时【esp】是不是就来到了【edi】的位置
  • 这其实就是在【销毁Add函数的函数形参x,y】,这下你应该明白函数形参是在什么时候销毁的了吧,没错,就是从Add函数回到main函数之后
0046185D 83 C4 08      add      esp,8

  • 一样,VS也来看看【esp】的变化

第四十一条指令

00EE191A  mov         dword ptr [ebp-20h],eax
  • 将eax中值,存档到ebp-0x20的地址处,其实就是存储到main函数中ret变量中,而此时eax中就是Add函数中计算的x和y的和,可以看出来,本次函数的返回值是由eax寄存器带回来的。程序是在函数调用返回之后,在eax中去读取返回值的。
  • 先前在Add函数中计算出来的30,首先放到【eax】寄存器中保存起来,现在过来好几条指令后,它还保存在里面,我们只需要使用【mov】将数据做一个转移即可
  • 到VS里来看看变化

  • 最后main函数栈帧的销毁也同理,这里就不再介绍了
  • 以下是这个栈的全局浏览图
  • 拓展了解:

其实返回对象时内置类型时,一般都是通过寄存器来带回返回值的,返回对象如果时较大的对象时,一般会在主调函数的栈帧中开辟一块空间,然后把这块空间的地址,隐式传递给被调函数,在被调函数中通过地址找到主调函数中预留的空间,将返回值直接保存到主调函数的。具体可以参考《程序员的自我修养》一书的第10章。

到这里我们给大家完整的演示了main函数栈帧的创建,Add函数站真的额创建和销毁的过程,相信大家已经能够基本理解函数的调用过程,函数传参的方式,也能够回答最开始的问题了

四、总结与开局疑难解答

① 局部变量是如何创建的?


首先为函数分配好栈帧空间,将这块栈帧空间初始化好后,然后给局部在栈帧里分配空间

② 为什么局部变量不初始化内容是随机的?

  • 因为函数栈帧中的空间是预先初始化好的【0xCCCCCCCCh】,若是不为变量初始化内容,那使用的就是初始化好后的内容,以字符的形式打印出来便是烫烫烫烫烫烫

③ 函数调用时参数时如何传递的?传参的顺序是怎样的?

  • 当还没有进入函数的时候,就已经将函数实参做了一份临时拷贝,并从右向左压入栈中【FILO】,当真正进入到函数栈帧中时,通过指针的偏移量,就可以顺着找回来,找到这份临时拷贝的形参

④ 函数的形参和实参分别是怎样实例化的?

  • 形参确实是我在压栈的时候开辟的一块空间,它和实参只是值相同,但是空间是独立的,所以形参是实参的一份临时拷贝,改变形参的值不会影响到实参

⑤ 函数调用是怎么做的?返回值是如何带会的?

  • 当执行到【call】指令的时候,把call指令的下一条指令地址压入栈中,相当于记住了这个地址。接着进入到函数中,当函数执行结束的时候,回到主函数中,再执行【ret】指令就可以回到call指令的下一条指令地址
  • 返回值是通过寄存器带回来的、将函数中计算出来的返回值存放到寄存器中,因为寄存器不会随着函数的调用结束而被销毁,最后再将寄存器中存放的数据转存回对应的内存块中即可


相关文章
|
6月前
|
存储 安全 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
|
6月前
|
存储 编译器 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
|
1月前
|
Linux C语言 iOS开发
MacOS环境-手写操作系统-06-在mac下通过交叉编译:C语言结合汇编
MacOS环境-手写操作系统-06-在mac下通过交叉编译:C语言结合汇编
19 0
|
3月前
|
存储 C语言
【C语言】——函数栈帧的创建与销毁
【C语言】——函数栈帧的创建与销毁
|
3月前
|
Linux C# C语言
C 语言与嵌入汇编
C 语言与嵌入汇编
27 0
|
6月前
|
算法 C语言
约瑟夫环的C语言和86/88汇编非递归算法
约瑟夫环的C语言和86/88汇编非递归算法
67 0
|
6月前
|
存储 编译器 C语言
C语言:底层剖析——函数栈帧的创建和销毁
C语言:底层剖析——函数栈帧的创建和销毁
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
程序员 C语言 C++
要想精通C语言,必须先学习汇编吗?
要想精通C语言,必须先学习汇编吗?
1192 0
|
6天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
22 6