前面详细的说了,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函数调用与栈--代码真相的更多相关文章

  1. 【.NET进阶】函数调用--函数栈

    原文:http://www.cnblogs.com/rain-lei/p/3622057.html 函数调用大家都不陌生,调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回 ...

  2. C函数调用与栈

    这篇blog试图说明这么一个问题,当一个c函数被调用时,一个栈帧(stack frame)是如何被建立,又如何被消除的.这些细节跟操作系统平台及编译器的实现有关,下面的描述是针对运行在Linux的gc ...

  3. C语言函数调用及栈帧结构

    source:http://blog.csdn.net/qq_29403077/article/details/53205010 一.地址空间与物理内存 (1)地址空间与物理内存是两个完全不同的概念, ...

  4. VC 函数调用的 汇编代码 浅析

    摘要:主要谈谈vc里面函数调用汇编成汇编代码的情形,首先针对之前的一个小程序,说说vc编译器的优化. 例子程序: #include <iostream>using namespace st ...

  5. 关于C语言函数调用压栈和返回值问题的疑惑

    按照C编译器的约定调用函数时压栈的顺序是从右向左,并且返回值是保存在eax寄存器当中.这个命题本该是成立的,下面用一个小程序来反汇编观察执行过程: #include<stdio.h> in ...

  6. 科幻大片中那些牛X代码真相

    在<黑客帝国>中,救世主Neo的队友通过屏幕上"1"和"0"构成的数据流,就能看到鲜活的画面,这应该算是科幻大片中对代码最极致的表现了.其他科幻电影 ...

  7. C语言实现栈代码

    /* 栈的特性:先进后出. 栈在计算语言处理和将递归算法改为非递归算法等方面起着非常重要的作用. */ #define INITSIZE 100 //储存空间的初始分配量 typedef int El ...

  8. 顺序栈代码实现&&stack库

    #include<iostream> using namespace std; ; typedef int Elemtype; struct SqStack { Elemtype *bas ...

  9. go语言调度器源代码情景分析之四:函数调用栈

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第3小节. 什么是栈 栈是一种“后进先出”的数据结构,它相当于一个容器,当需要往容器里面添加元素时只能放在最上面的一个元素之上,需 ...

随机推荐

  1. C#对象赋值出现的诡异问题,或许你也遇到过,有待你的解决

    前言:今天在代码中,又出现了这个问题,就是对象赋值给一个新的对象时,然后更改新对象中的属性,就会把老对象的值也更改,以前也遇到这个问题,只是没有深究,今天刚好又遇到了此问题,我决定写下来,和大家一起分 ...

  2. discuz_style_default.xml 修改

    <?xml version="1.0" encoding="ISO-8859-1"?> <root> <item id=" ...

  3. HBase ElasticSearch

    http://www.open-open.com/doc/view/c2af706064f84e128d0f74826523a1ea http://www.open-open.com/doc/view ...

  4. HelloX项目github协同开发指南

    概述 为了提高协同开发效率,HelloX项目已托管到github网站上.根据目前的开发进展,创建了下列几个子项目: HelloX操作系统内核项目:https://github.com/hellox-p ...

  5. 给即将面临Noip的二班同学

    给即将面临Noip的二班同学: 我们共同走过了一年,在这里,真正认识彼此…… 失落过,但更多是欢笑…… 或许我们班的信息学竞赛承受着巨大的压力,但正因为这样,我们才学会了坚持:或许我们得不到他人的认可 ...

  6. UI基础:UILabel.UIFont

    UILabel:标签 继承自UIView ,在UIView基础上扩充了显示文本的功能.(文本框) UILabel的使用步骤 1.创建控件 UILabel *aLabel=[[UILabel alloc ...

  7. Asp.net 提供程序模型

    需要说明一下几点 1.什么是提供程序? 2.ASP.NET 4.5 中的提供程序 3.配置提供程序 有一下几种存储状态的方式 1.应用程序状态 2.会话状态 3.高速缓存状态 4.cookie 5.查 ...

  8. jQuery 插件 flexslider 初步使用

    发现了个不错的 jQuery 幻灯片插件 flexslider,有 接近 3000 Star,应该说是很靠谱的,下面是简单使用教程. 引入代码 所有代码都可以在 flexlslider 的 Githu ...

  9. ajax post传值

    一.字符串             $.ajax({                type: "POST",                data: {"ID&quo ...

  10. SQL SERVER 2008 R2 自动备份并删除过期备份数据

        我们的系统维护的过程中肯定需要对数据库进行定期的备份,但是如果定时手工备份的话,不但浪费时间,也不能保证每次都可以按时备份,所以自动备份成为了我们的不二选择,但是定时备份需要定期清理备份文件, ...