没画完的画

喂马 劈柴 BBQ~
posts - 37, comments - 55, trackbacks - 0, articles - 0
  C++博客 ::  :: 新随笔 :: 联系 :: 聚合  :: 管理
注: 本文摘自互联网,本人看了按理解加了些东西

为了弄清楚函数的堆栈,Google了一下,找到下面代码作为实验
 1#include <stdio.h>
 2#include <string.h>
 3
 4void func1(int input1, int input2)
 5{
 6    int j;
 7    char c;
 8    short k;
 9
10    j = 0;
11    c = 'a';
12    k = 1;
13
14    printf("sum=%d\n", input1+input2);
15
16    return;
17}

18
19int main()
20{
21    char output[8= "abcdef";
22    int i, j;
23
24    i=2;
25    j=3;
26    func1(i,j);
27
28    printf("%s\r\n", output);
29
30    return 0;
31}

32


本人在 vc6 + winxp 进行调试
vc6 中在 fun1 设置断点,按F5,  到达断点后,选择菜单 view -> Disassembly 就可以看到代码对应的汇编语句

调用 func1() 之前

26:       func1(i,j);
004010D7   mov         ecx,dword ptr [ebp-10h]
004010DA   push        ecx
004010DB   mov         edx,dword ptr [ebp-0Ch]
004010DE   push        edx
004010DF   call        @ILT+0(func1) (00401005)
004010E4   add         esp,8
------------------------------------------------
EAX = 00000000 EBX = 7FFDE000 ECX = 00006665 EDX = 00370E00
ESI = 00000000 EDI = 0012FF80
EIP = 004010D7 ESP = 0012FF24 EBP = 0012FF80 EFL = 00000246
------------------------------------------------
i,j 分别存放在栈中,地址分别是
------------------------------------------------
ebp-10h = 0x0012FF80h - 0x10h = 0x0012FF70h
ebp-0Ch = 0x0012FF80h - 0x0Ch = 0x0012FF74h

0012FF70  03 00 00 00 02 00 00  .......
0012FF77  00 61 62 63 64 65 66  .abcdef

0012FF6D  CC CC CC 03 00 00 00  烫.....
0012FF74  02 00 00 00 61 62 63  ....abc

从内存存放的内容可知, i, j 分别存放于 0x0012FF70H, 0x0012FF74H
分别占了四个字节

在调用 func1() 之前, 先将 i, j 压入堆栈, 压入堆栈的顺序是 i, j
(故出栈的顺序是j, i, 请记住, 这里的 传递函数参数 默认是 __cdecl) 

接着调用了 call 指令
执行 call 指令之前的寄存器状态
------------------------------------------------
EAX = 00000000 EBX = 7FFDE000 ECX = 00000003 EDX = 00000002
ESI = 00000000 EDI = 0012FF80
EIP = 004010DF ESP = 0012FF1C EBP = 0012FF80 EFL = 00000246
------------------------------------------------
执行 call 指令之后的寄存器状态
EAX = 00000000 EBX = 7FFDE000 ECX = 00000003 EDX = 00000002
ESI = 00000000 EDI = 0012FF80
EIP = 00401005 ESP = 0012FF18 EBP = 0012FF80 EFL = 00000246
------------------------------------------------
变化的是 EIP 与 ESP
栈顶指针寄存器ESP:保存栈顶地址(指针)
此时 ESP 向低地址偏移了四个字节
可见 call 指令执行了一个 push 操作

查看 ESP 对象的地址 0x0012FF18 的内容
0012FF18  E4 10 40 00 02 00 00  ..@....
0012FF1F  00 03 00 00 00 00 00  .......

push 进去的数是 0x004010E4, 再看回调用 call 之后的代码

26:       func1(i,j);
004010D7   mov         ecx,dword ptr [ebp-10h]
004010DA   push        ecx
004010DB   mov         edx,dword ptr [ebp-0Ch]
004010DE   push        edx
004010DF   call        @ILT+0(func1) (00401005)
004010E4   add         esp,8

push 进去的数刚好是 call 指令后面的指令地址 0x004010E4  

为什么没有看到push指令呢?
原来在CALL操作中,隐式地把call指令后续第一条指令的地址0x004010E4 入栈,
然后再无条件地跳转到func1()函数继续执行。

@ILT+0(?func1@@YAXHH@Z):
00401005   jmp         func1 (00401020)

004010DF   call        @ILT+0(func1) (00401005)

奇怪的事情是 call 后面的操作数应该是 func1 的地址 0x00401020 才对, 为何是 0x00401005?
还是一个 @ILT+0 的东东是什么?

在DEBUG版本中,VC汇编程序会产生一个函数跳转指令表,
该表的每个表项存放一个函数的跳转指令。
程序中的函数调用就是利用这个表来实现跳转到相应函数的入口地址。

ILT就是函数跳转指令表的名称,是Import Lookup Table的缩写;
@ILT就是函数跳转指令表的首地址。
在DEBUG版本中增加函数跳转指令表,其目的是加快编译速度,当某函数的地址发生变化时,只需要修改ILT相应表项即可,而不需要修改该函数的每一处引用。
注意:在RELEASE版本中,不会生成ILT,也就是说call指令的操作数直接是函数的入口地址,例如在本例中是这样的:call 00401020


接下来, 应该看一下 jmp func1 后, 做了哪些东西

4:    void func1(int input1, int input2)
5:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,4Ch
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-4Ch]
0040102C   mov         ecx,13h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]
------------------------------------------------
此时各寄存器的值为
EAX = 00000000 EBX = 7FFDE000 ECX = 00000003 EDX = 00000002
ESI = 00000000 EDI = 0012FF80
EIP = 00401020 ESP = 0012FF18 EBP = 0012FF80 EFL = 00000246

ebp 为 main 函数层的 栈内存基地址
esp 为 当前的栈顶地址

00401020   push        ebp
00401021   mov         ebp,esp

这两个语句 先把 ebp 压入堆栈, 再把 esp 赋值给 ebp
这两句的作用将在后面说明

00401023   sub         esp,4Ch
然后,栈顶指针esp向低地址偏移76(0x4C)字节。这里相当于为func1()函数层分配了栈内存。
(为什么偏偏是 76 是字节?)
(题外话: 平时程序调试的 stack over 又是如何造成的?)

接着又把 ebx, esi, edi 分别入栈, 目的是为了保存 main 函数层的相关内容
00401026   push        ebx
00401027   push        esi
00401028   push        edi

最后用0xCC初始化上述为func1()函数层所分配的栈内存的每个字节。这里每一步用F11单步跟踪,栈内存的变化你会看得更清楚。
00401029   lea         edi,[ebp-4Ch]     ; 将有效的地址 [ebp-0x4Ch] 赋值到 edi
0040102C   mov         ecx,13h           ;
00401031   mov         eax,0CCCCCCCCh    ;
00401036   rep stos    dword ptr [edi]   ;

stos指令:
字符串存储指令 STOS
格式: STOS OPRD
其中OPRD为目的串符号地址.
功能: 把AL(字节)或AX(字)中的数据存储到DI为目的串地址指针所寻址的存储器单元中去.指针DI将根据DF的值进行自动调整.
由于上面的指令是 dword ptr 类型
dword 表示双字 ptr 表示取首地址
那么 stos    dword ptr [edi] 执行的操作就是
将 ES:[DI]←AX,DI←DI±4   (DI 加或减是由 DF 标志位确定的)
如果是 那么 stos word ptr [edi] 的话那么就是
将 ES:[DI]←AL,DI←DI±2   (DI 加或减是由 DF 标志位确定的)
不然推出 stos BYTE ptr [edi]

注:
DF:方向标志DF位用来决定在串操作指令执行时有关指针寄存器发生调整的方向。

重复前缀
格式: REP           ;CX<>0 重复执行字符串指令

REP 每执行一次后面的字符串指令后, cx减1, 直至 cx 为0
在本例中, 每次拷贝 sizeof(DWORD) 四个字节, 而堆栈大小是 76(0x4C) 个字节, 故 只需要重复执行 76 / 4 = 19(0x13) 次就可以了

0040102C   mov         ecx,13h           ;

现在终于清楚

00401029   lea         edi,[ebp-4Ch]     ; 将有效的地址 [ebp-0x4Ch] 赋值到 edi
0040102C   mov         ecx,13h           ;
00401031   mov         eax,0CCCCCCCCh    ;
00401036   rep stos    dword ptr [edi]   ;

的作用就是把堆栈的数据置为 0xCC;


困了,先去睡下再写第2集~~

Feedback

# re: 函数堆栈是这么回事 第1集[未登录]  回复  更多评论   

2008-09-26 10:43 by flyswift
很好很强大。LZ继续。

# re: 函数堆栈是这么回事 第1集  回复  更多评论   

2008-09-26 13:55 by ljbxc
支持,继续

# re: 函数堆栈是这么回事 第1集  回复  更多评论   

2008-09-26 15:23 by luke
入栈的顺序应该是j,i吧?

# re: 函数堆栈是这么回事 第1集  回复  更多评论   

2008-09-27 16:03 by 908971
mark

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理