C函数调用与栈--代码真相
前面详细的说了,C函数调用的过程中,栈的变化情况的原理部分,这里在看一下汇编代码的真正的实现。
有关前面的那一片博客,主要记住的就是函数调用时栈的变化,4+3+2的步骤:
(1)设置栈帧边界
(2)开辟本函数的局部区域
(3)保存寄存器的内容
(4)初始化局部区域(int3)
(5)如果有函数调用
(a)push实参入栈
(b)call执行,设置返回地址,然后执行被调函数代码
(c)调整sp栈顶指针,删除实参
(6)恢复之前保存的寄存器的值
(7)取消栈帧的边界(main函数还要做一次校验)
这段代码反汇编后,代码部分看一下:
#include <stdio.h> long test(int a,int b)
{
a = a + ;
b = b + ; return a + b;
} int main(int argc, char* argv[])
{ printf("%d",test(,)); return ;
}
先来看一下main函数的汇编代码:
: int main(int argc, char* argv[])
: {
//设置栈帧边界
push ebp
mov ebp,esp
//设置局部变量区域
sub esp,40h
//保存寄存器内容
push ebx
push esi
push edi
//初始化局部变量区域
lea edi,[ebp-40h]
0040107C mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi] : printf("%d",test(,));
//push实参入栈
push 5Ah
0040108A push 0Ah
//call指令,把EIP保存到栈中
0040108C call @ILT+(test) ()
//删除实参的空间
add esp,
//test()函数的返回值存在了eax中,它是printf函数的实参,直接实参入栈
push eax
push offset string "%d" (0042201c)
//call指令,把EIP保存到栈中
0040109A call printf (004010d0)
//删除实参的空间
0040109F add esp,
: return ; 004010A2 xor eax,eax
: }
下面来解释一下:
开始进入Main函数 esp=0x12FF84 ebp=0x12FFC0
完成椭圆形框起来的部分
00401070 push ebp ebp的值入栈,保存现场(调用现场,从test函数看,如红线所示,即保存的0x12FF80用于从test函数堆栈返回到main函数)
00401071 mov ebp,esp 此时ebp=0x12FF80 此时ebp就是“当前函数堆栈”的基址 以便访问堆栈中的信息;还有就是从当前函数栈顶返回到栈底
00401073 sub esp,40h
函数使用的堆栈,默认64个字节,堆栈上就是16个横条(密集线部分)此时esp=0x12FF40
在上图中,上面密集线是test函数堆栈空间,下面是Main的堆栈空间 (补充,其实这个就叫做 Stack Frame)
00401076 push ebx
00401077 push esi
00401078 push edi 入栈
00401079 lea edi,[ebp-40h]
0040107C mov ecx,10h
00401081 mov eax,0CCCCCCCCh
00401086 rep stos dword ptr [edi]
初始化用于该函数的栈空间为0XCCCCCCCC 即从0x12FF40~0x12FF80所有的值均为0xCCCCCCCC
REP CX不等于0 ,则重复执行字符串指令
格式: STOS OPRD
功能: 把AL(字节)或AX(字)中的数据存储到DI为目的串地址指针所寻址的存储器单元中去.指针DI将根据DF的值进行自动
调整. 其中OPRD为目的串符号地址.
以上的语句就是在栈中开辟一块空间放局部变量
然后把这块空间都初始化为0CCCCCCCCh,就是int3断点,一个中断指令。
因为局部变量不可能被执行,执行了就会出错,这时候发生中断提示开发者。
18: printf("%d",test(10,90));
00401088 push 5Ah 参数入栈 从右至左 先90 后10
0040108A push 0Ah
0040108C call @ILT+0(test) (00401005)
函数调用,转向eip 00401005
注意,此时仍入栈,入栈的是call test 指令下一条指令的地址00401091 下一条指令是add esp,8
@ILT+0(?test@@YAJHH@Z):
00401005 jmp test (00401020)
00401005就是这个test函数在ILT静态表的入口,这里有个jmp指令,直接跳转到test函数的代码存储的区域。
注意:
汇编语言每条指令的最前面就是就是这条指令在内存代码区的位置,每次运行的时候,都根据事先把指令的地址放到程序计数器(EIP寄存器)中,指令是挨着盘存放的,所有两条指令的地址相减,就能看出这条汇编指令占用的字节数了。拿这两条指令为例:
//call指令,把EIP保存到栈中
0040108C call @ILT+(test) ()
//删除实参的空间
add esp,
两个地址相减,得到的字节数就是call指令占用的存储空间。
然后就转向了被调函数test:
: long test(int a,int b)
: {
push ebp
mov ebp,esp
sub esp,40h
push ebx
push esi
push edi
lea edi,[ebp-40h]
0040102C mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi] //这些和上面一样
: a = a + ;
mov eax,dword ptr [ebp+] //ebp=0x12FF24 加8 [0x12FF30]即取到了参数10
0040103B add eax,
0040103E mov dword ptr [ebp+],eax
: b = b + ;
mov ecx,dword ptr [ebp+0Ch]
add ecx,
mov dword ptr [ebp+0Ch],ecx
: return a + b;
0040104A mov eax,dword ptr [ebp+]
0040104D add eax,dword ptr [ebp+0Ch] //最后的结果保存在eax, 结果得以返回
: }
pop edi
pop esi
pop ebx
mov esp,ebp //esp指向0x12FF24, test函数的堆栈空间被放弃,从当前函数栈顶返回到栈底
pop ebp //此时ebp=0x12FF80, 恢复现场 esp=0x12FF28
ret ret负责栈顶0x12FF28之值00401091弹出到指令寄存器中,esp=0x12FF30
因为win32汇编一般用eax返回结果 所以如果最终结果不是在eax里面的话 还要把它放到eax
注意,从被调函数返回时,是弹出EBP,恢复堆栈到函数调用前的地址,弹出返回地址到EIP以继续执行程序。
从test函数返回,执行
00401091 add esp,8
清栈,清除两个压栈的参数10 90 调用者main负责
(所谓__cdecl调用由调用者负责恢复栈,调用者负责清理的只是入栈的参数,test函数自己的堆栈空间自己返回时自己已经清除,靠!一直理解错)
00401094 push eax 入栈,计算结果108入栈,即printf函数的参数之一入栈
00401095 push offset string "%d" (0042201c) 入栈,参数 "%d" 当然其实是%d的地址
0040109A call printf (004010d0) 函数调用 printf("%d",108) 因为printf函数时
0040109F add esp,8 清栈,清除参数 ("%d", 108)
19: return 0;
004010A2 xor eax,eax eax清零
20: }
main函数执行完毕 此时esp=0x12FF34 ebp=0x12FF80
004010A4 pop edi
004010A5 pop esi
004010A6 pop ebx
004010A7 add esp,40h //为啥不用mov esp, ebp? 是为了下面的比较
004010AA cmp ebp,esp //比较,若不同则调用chkesp抛出异常
004010AC call __chkesp (00401150)
004010B1 mov esp,ebp
004010B3 pop ebp //ESP=0X12FF84 EBP=0x12FFC0 尘归尘 土归土 一切都恢复最初的平静了 :)
004010B4 ret
注意:
1. 如果函数调用方式是__stdcall 不同之处在于 main函数call 后面没有了 add esp, 8 test函数最后一句是ret 8 (由test函数清栈, ret 8意思是执行ret后,esp+8)
2. 运行过程中0x12FF28 保存了指令地址 00401091是怎么保存的?
栈每个空间保存4个字节(粒度4字节) 例如下一个栈空间0x12FF2C保存参数10
因此
0x12FF28 0x12FF29 0x12FF2A 0x12FF2B
91 10 40 00
little-endian 认为其读的第一个字节为最小的那位上的数
3. char a[] = "abcde"
对局部字符数组变量(栈变量)赋值,是利用寄存器从全局数据内存区把字符串“abcde”拷贝到栈内存中的
下面这两行代码就能看出来,同时还能看出来内存中各变量的对齐方式:
: char a[] = "abcde";
mov eax,[string "abcde" (0042b01c)]
0040102D mov dword ptr [ebp-],eax
mov cx,word ptr [string "abcde"+ (0042b020)]
mov word ptr [ebp-],cx
: int c = ;
0040103B mov dword ptr [ebp-0Ch],0Ah
4. int szNum[5] = { 1, 2, 3, 4, 5 }; 栈中是如何分布的?
mov dword ptr [ebp-14h],
0040179F mov dword ptr [ebp-10h],
004017A6 mov dword ptr [ebp-0Ch],
004017AD mov dword ptr [ebp-],
004017B4 mov dword ptr [ebp-],
可以看出来 是从右边开始入栈,所以是 5 4 3 2 1 入栈
int *ptrA = (int*)(&szNum+);
int *ptrB = (int*)((int)szNum + );
std::cout<< ptrA[-] << *ptrB << std::endl;
结果如何?
: int *ptrA = (int*)(&szNum+);
004017BB lea eax,[ebp]
004017BE mov dword ptr [ebp-18h],eax
&szNum是指向数组指针;加1是加一个数组宽度;&szNum+1指向移动5个int单位之后的那个地方, 就是把EBP的地址赋给指针
ptrA[-]是回退一个int*宽度,即ebp-
: int *ptrB = (int*)((int)szNum + );
004017C1 lea ecx,[ebp-13h]
004017C4 mov dword ptr [ebp-1Ch],ecx
如果上面是指针算术,那这里就是地址算术,只是首地址+1个字节的offset,即ebp-13h给指针
实际保存是这样的
01 00 00 00 02 00 00 00
ebp-14h ebp-13h ebp-10h
注意是int*类型的,最后获得的是 00 00 00 02
由于Little-endian, 实际上逻辑数是02000000 转换为十进制数就为33554432
最后输出:
5
33554432
C函数调用与栈--代码真相的更多相关文章
- 【.NET进阶】函数调用--函数栈
原文:http://www.cnblogs.com/rain-lei/p/3622057.html 函数调用大家都不陌生,调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回 ...
- C函数调用与栈
这篇blog试图说明这么一个问题,当一个c函数被调用时,一个栈帧(stack frame)是如何被建立,又如何被消除的.这些细节跟操作系统平台及编译器的实现有关,下面的描述是针对运行在Linux的gc ...
- C语言函数调用及栈帧结构
source:http://blog.csdn.net/qq_29403077/article/details/53205010 一.地址空间与物理内存 (1)地址空间与物理内存是两个完全不同的概念, ...
- VC 函数调用的 汇编代码 浅析
摘要:主要谈谈vc里面函数调用汇编成汇编代码的情形,首先针对之前的一个小程序,说说vc编译器的优化. 例子程序: #include <iostream>using namespace st ...
- 关于C语言函数调用压栈和返回值问题的疑惑
按照C编译器的约定调用函数时压栈的顺序是从右向左,并且返回值是保存在eax寄存器当中.这个命题本该是成立的,下面用一个小程序来反汇编观察执行过程: #include<stdio.h> in ...
- 科幻大片中那些牛X代码真相
在<黑客帝国>中,救世主Neo的队友通过屏幕上"1"和"0"构成的数据流,就能看到鲜活的画面,这应该算是科幻大片中对代码最极致的表现了.其他科幻电影 ...
- C语言实现栈代码
/* 栈的特性:先进后出. 栈在计算语言处理和将递归算法改为非递归算法等方面起着非常重要的作用. */ #define INITSIZE 100 //储存空间的初始分配量 typedef int El ...
- 顺序栈代码实现&&stack库
#include<iostream> using namespace std; ; typedef int Elemtype; struct SqStack { Elemtype *bas ...
- go语言调度器源代码情景分析之四:函数调用栈
本文是<go调度器源代码情景分析>系列 第一章 预备知识的第3小节. 什么是栈 栈是一种“后进先出”的数据结构,它相当于一个容器,当需要往容器里面添加元素时只能放在最上面的一个元素之上,需 ...
随机推荐
- Scale-up(纵向扩展) vs Scale-out(横向扩展)
转载:http://wuaner.iteye.com/blog/1843799 http://www.javaworld.com/article/2077780/java-web-developmen ...
- QCombobox设置下拉框的宽度
这几天写一个项目,里面用到qcombobox组件,其中下拉框含有129个子项,所以在点击的时候,一个下拉框就将整个电脑屏幕给占满了,很不好看并且在使用中会造成很大的苦恼.其实我就是想设置一个下拉框最大 ...
- CodeForces 235C Cyclical Quest(后缀自动机)
[题目链接] http://codeforces.com/contest/235/problem/C [题目大意] 给出一个字符串,给出一些子串,问每个子串分别在母串中圆环匹配的次数,圆环匹配的意思是 ...
- Ubuntu下远程访问MySQL数据库
MySQL远程访问的命令 格式: mysql -h主机地址 -u用户名 -p用户密码 jack@jack:~$ mysql -h192.168.5.154 -usaledata -pEnter pas ...
- [Docker]初次接触
Docker 初次接触 近期看了不少docker介绍性文章,也听了不少公开课,于是今天去官网逛了逛,发现了一个交互式的小教程于是决定跟着学习下. 仅仅是把认为重点的知识记录下来,不是非常系统的学习和笔 ...
- Objective-C分类 (category)和扩展(Extension)
1.分类(category) 使用Object-C中的分类,是一种编译时的手段,允许我们通过给一个类添加方法来扩充它(但是通过category不能添加新的实例变量),并且我们不需要访问类中的代码就可以 ...
- javascript绑定事件
本质:不同的库或者工具中总是封装了不同的事件绑定形式,但是究其根源,还是IE事件模型和W3C事件模型不同的处理方式 1)W3C事件模型:支持事件捕捉和冒泡 addEventListener('type ...
- 雪碧图(sprite)
雪碧图 是一种将网页上常用且不经常变动的小图标集中在一张大图中,根据网页需求来显示图片的技术. 可以提高网页加载速度,增加用户体验. 其原理是通过html块状元素建立一个满足需求的视图窗口,然后在窗口 ...
- 用JQUERY实现给当前页面导航一个CSS
今天遇到一个问题 当我在导航中点击一个标签后 希望用户知道自己所在导航的位置 只需要根据点击的页面是否加载完成 给这个标签用JS 添加一个CCcurr的 也就是我们常说的current CLASS 代 ...
- Linux新手笔记 sudo
centos 6.4 32bit 你是也像我一样,厌烦了在root用户和个人用户之间来回切换.或者干脆直接用root用户.可以这样设置,然后在命令前加sudo 即可使用自己到密码,临时用root身份执 ...