没画完的画

喂马 劈柴 BBQ~
posts - 37, comments - 55, trackbacks - 0, articles - 0
  C++博客 ::  :: 新随笔 :: 联系 :: 聚合  :: 管理
总是试图想把想表达的东西表达清楚,但总是发现表达的不够清楚

 

_BEGIN(废话

一直很奇怪为什么VC中,要把堆栈的内容全初始化为 0xCC

今天才突然想起其中的原因,

原来 0xCC 翻译成汇编代码就是  int3 ;(断点)

[指今执行越界]时,就会产生中断

证实方法: 可以在程序中加上 _asm int3

然后设置断点,查看指令的地址,再查看地址的内存,你会发现是 0xCC

_END(废话)

 

1集,第2集说过的东西,由于记性衰退的缘故,已经忘记了!!!!!!!!

 

为了更简单,把代码进行改动,把第1集中的代码作了删减,

Make it sample!!!!!

Make it sample!!

Make it sample!

一切为了弄懂 Win32 中函数调用做了什么.

 

void func1(int input1, int input2)
{
    int i, j;
    char c;
   
    i = input1;
    j = input2;
    c = 0;
}
int main()
{
    int i, j;
    i=2;
    j=3;
   
    func1(i,j);
    return 0;
}

 

让一切回到调用 func1() 函数这前

 

_BEGIN(废话

本文所列出的 004010XX 地址,会因为机器的配置不同而有所差异,

请不要太在意

_END(废话)

------------------------------------------------------------

13:       i=2;                                                    <--- 很黄很暴力的断点在此
00401078   mov         dword ptr [ebp-4],2
14:       j=3;
0040107F   mov         dword ptr [ebp-8],3
15:
16:       func1(i,j);
00401086   mov         eax,dword ptr [ebp-8]
00401089   push        eax
0040108A   mov         ecx,dword ptr [ebp-4]
0040108D   push        ecx
0040108E   call        @ILT+0(func1) (00401005)
00401093   add         esp,8

------------------------------------------------------------

此时寄存器的状态

 EAX = CCCCCCCC EBX = 7FFDE000 ECX = 00000000
 EDX = 00370D78 ESI = 00000000 EDI = 0012FF80
 EIP = 00401078 ESP = 0012FF2C EBP = 0012FF80
 EFL = 00000212

------------------------------------------------------------

内存的内容

0012FF6B  CC CC CC CC CC CC CC  烫烫烫.
0012FF72  CC CC CC CC CC CC CC 
烫烫烫.
0012FF79  CC CC CC CC CC CC CC 
烫烫烫.
0012FF80  C0 FF 12 00 E9 11 40  ......@
0012FF87  00 01 00 00 00 00 0D  .......
0012FF8E  37 00 78 0D 37 00 00  7.x.7..

 

_BEGIN(废话)

注:像 CC 表示一个字节的内容,一行共 7 个字节

_END(废话)

------------------------------------------------------------

函数内声明的变量,需要一块空间去保存它们,

这块空间,是以 ebp寄存器 指向的地址 的一块内存空间,

请看证据~

 

14:       i=2;
00401078   mov         dword ptr [ebp-4],2   <--- i
存放在 ebp寄存器 指向的地址 减去4 的地方

15:       j=3;
0040107F   mov         dword ptr [ebp-8],3   <--- j 存放在 ebp寄存器 指向的地址 减去8 的地方,

                                                                   因为i 占用了 4个字节, 所以 4 + 4 = 8

 

所以不难想象出下面这个图

               |   ……   |

0012FF80: |--------|     <---  [ebp]寄存器里 存放着 0012FF80

               |           |

0012FF7C: |--------|    

               |           |

0012FF78: |--------|

               |           |

               |--------|

               |           |

               |--------|

 

按了下 F10

14:       i=2;
00401078   mov         dword ptr [ebp-4],2
15:       j=3;                                                  <---
断点来到这儿了
0040107F   mov         dword ptr [ebp-8],3

寄存器变化

-----------------------------------------------------------

EAX = CCCCCCCC EBX = 7FFD9000 ECX = 00000000
EDX = 00370F58 ESI = 00000000 EDI = 0012FF80
EIP = 0040107F ESP = 0012FF2C EBP = 0012FF80
EFL = 00000212
-----------------------------------------------------------

内存的变化

-----------------------------------------------------------

0012FF72  CC CC CC CC CC CC CC  烫烫烫.
0012FF79  CC CC CC 02 00 00 00 
.....
0012FF80  C0 FF 12 00 D9 12 40  ......@

-----------------------------------------------------------

_BEGIN(废话)

注:在VC中,执行一条指令后,寄存器的值或内存的值有变化,变化的部份会显示为红色

_END(废话)

 

再下 F10

寄存器变化

-----------------------------------------------------------

EAX = CCCCCCCC EBX = 7FFD9000 ECX = 00000000
EDX = 00370F58 ESI = 00000000 EDI = 0012FF80
EIP = 00401086 ESP = 0012FF2C EBP = 0012FF80
EFL = 00000212
-----------------------------------------------------------

内存的变化

-----------------------------------------------------------

0012FF72  CC CC CC CC CC CC 03  烫烫烫.
0012FF79  00 00 00 02 00 00 00  .......
0012FF80  C0 FF 12 00 D9 12 40  ......@

-----------------------------------------------------------

 

再想象一下

               |   ……   |

0012FF80: |--------|     <---  [ebp]寄存器里 存放着 0012FF80

               |           |

0012FF7C: |--------|    

               |    2     |      <--- 存放着 i

0012FF78: |--------|

               |    3     |      <--- 存放着 j

               |--------|

               |           |

               |--------|

 

 

调用 func1() 函数前需要做什么? 看下编译器编译后的汇编就知道了~

17:       func1(i,j);
00401086   mov         eax,dword ptr [ebp-8]        <----
断点在此
00401089   push        eax
0040108A   mov         ecx,dword ptr [ebp-4]
0040108D   push        ecx
0040108E   call        @ILT+0(func1) (00401005)
00401093   add         esp,8

地址 0x00401086  地址 0x00401093 的指令,

做的事情就是 把调用 func1() 函数时所需要的参数分别入栈,push

 

00401086   mov         eax,dword ptr [ebp-8]            <--- 看回前面 j = 3; 就知道 这里 j 是先入栈的
00401089   push        eax
0040108A   mov         ecx,dword ptr [ebp-4]
0040108D   push        ecx

 

为什么让 j 先入栈,i j 可爱多了~

因为参数入栈的顺序是有规定的!

 

修饰函数参数的入栈顺序的关键字有

1. __cdecl    

   C/C++MFC默认的约定    

   参数从右至左顺序入栈 并且由[调用者]负责把参数 pop 堆栈  

 

2. __stdcall

    WIN API 采用的约定

    参数从右至左顺序入栈,被调用的函数在返回前清理堆栈的的内容,所以函数的参数个数需要是固定个数                            

 

3. __fastcall  

   用于对性能要求非常高的场合

   参数从左边开始的两个不大于4字节(DWORD)的参数分别放在ECXEDX寄存器,

   其余的参数仍旧自右身左压栈,被调用的函数在返回前负责清理传送参数的堆栈

 

MSDN有云:

Keyword

Stack cleanup

Parameter passing

__cdecl

Caller

Pushes parameters on the stack, in reverse order (right to left)

__stdcall

Callee

Pushes parameters on the stack, in reverse order (right to left)

__fastcall

Callee

Stored in registers, then pushed on stack

thiscall
(not a keyword)

Callee

Pushed on stack; this pointer stored in ECX

 

 

Obsolete Calling Conventions

Microsoft Specific

The __pascal, __fortran, and __syscall calling conventions are no longer supported. You can emulate their functionality by using one of the supported calling conventions and appropriate linker options.

WINDOWS.H now supports the WINAPI macro, which translates to the appropriate calling convention for the target. Use WINAPI where you previously used PASCAL or __far __pascal.

END Microsoft Specific

 

原来如此,这下明白了~

 

00401086   mov         eax,dword ptr [ebp-8]       

00401089   push        eax
0040108A   mov         ecx,dword ptr [ebp-4]
0040108D   push        ecx
0040108E   call        @ILT+0(func1) (00401005)   <----
断点来到这里
00401093   add         esp,8
 

此时的寄存器状态

EAX = 00000003  EBX = 7FFDF000 ECX = 00000002

EDX = 00030EA8  ESI = 00000000  EDI = 0013FF80
EIP = 0040108E ESP = 0013FF24  EBP = 0013FF80

EFL = 00000212

 

F11

它跳转到这里

@ILT+0(?func1@@YAXHH@Z):
00401005   jmp         func1 (00401020)         <----
断点在此

 

此时的寄存器状态

EAX = 00000003 EBX = 7FFDF000 ECX = 00000002
EDX = 00030EA8 ESI = 00000000 EDI = 0013FF80
EIP = 00401005 ESP = 0012FF20 EBP = 0013FF80
EFL = 00000212

此时的 ESP 寄存器的值为什么变了,为什么???

 

_BEGIN(废话)

SP(Stack Pointer)ESP 是堆栈指针寄存器,

SS(Stack Segment,堆栈段寄存器) 相配合,指向指向堆栈的位置

它存放的地址 始终 指向堆顶

当执行 push, pop 操作 SP 的值都会作相应的改变

 

不妨看下以下代码

void main()
{
 _asm
 {
  push 0xAABBCCDD     <----
断点在此
  pop  eax
 }
}

 

此时,寄存器状态

EAX = CCCCCCCC EBX = 7FFD4000 ECX = 00000000
EDX = 00030EA8 ESI = 00000000 EDI = 0013FF80
EIP = 00401038 ESP = 0013FF34 EBP = 0013FF80
EFL = 00000202

 

ESP 寄存器指向的地址是栈顶的地址,0013FF34

 

0013FF10  30 FF 13 00 1F 3F 40 00 00  0....?@..
0013FF19  08 00 00 00 00 00 00 02 00  .........
0013FF22  00 00 30 2F 42 00 83 00 00  ..0/B....
0013FF2B  00 A8 1E 03 00 54 FF 13 00  .....T...
0013FF34  00 00 00 00 00 00 00 00 00  .........              <----
在此
0013FF3D  40 FD 7F CC CC CC CC CC CC  @?烫烫烫

 

执行 push 0xEEEEEEEE 后寄存器的状态

EAX = CCCCCCCC EBX = 7FFDF000 ECX = 00000000
EDX = 00030EA8 ESI = 00000000 EDI = 0013FF80
EIP = 0040103D ESP = 0013FF30 EBP = 0013FF80
EFL = 00000202

 

内存状态

0013FF22  00 00 30 2F 42 00 83 00 00  ..0/B....
0013FF2B  00 A8 1E 03 00 DD CC BB AA  .....
萏华
0013FF34  00 00 00 00 00 00 00 00 00  .........

 

不难看出 push 0xAABBCCDD 指令就是将

AABBCCDD 复制到 0013FF33, 0013FF32, 0013FF31, 0013FF30  这四个内存单元,每个单元一字节

并且把原来 ESP 的值 0013FF34 减去 4 得到 0013FF30

可见 栈顶 是向低地址 前进的

 

再按 F10

5:            push 0xAABBCCDD
00401038   push        0AABBCCDDh
6:            pop  eax
0040103D   pop         ax
7:        }
8:    }
0040103F   pop         edi        <---- 
断点在此

 

此时寄存器状态

EAX = AABBCCDD EBX = 7FFD6000 ECX = 00000000
EDX = 00030EA8 ESI = 00000000 EDI = 0013FF80
EIP =
0040103E ESP = 0013FF34 EBP = 0013FF80
EFL = 00000202 MM0 = 0000000000000000

内存

0013FF22  00 00 30 2F 42 00 83 00 00  ..0/B....
0013FF2B  00 A8 1E 03 00 DD CC BB AA  .....
萏华
0013FF34  00 00 00 00 00 00 00 00 00  .........
0013FF3D  60 FD 7F CC CC CC CC CC CC  `?
烫烫烫

 

可见, pop eax 指令做的事情,就是

0013FF33, 0013FF32, 0013FF31, 0013FF30 四个字节的内存单元 里面的内容 复制到 eax 寄存器

同时把 ESP 寄存器  4,得到 0013FF34

换句话说,ESP 寄存器 的值改变了,意味了执行过 push pop 操作

地址向低地址偏移,说明进行了 push 操作

地址向高地址偏移,说明时行了 pop  操作

_END(废话)

 

说完废话后,看回 call 指令

 

@ILT+0(?func1@@YAXHH@Z):
00401005   jmp         func1 (00401020)         <----
断点在此

 

此时的寄存器状态

EAX = 00000003 EBX = 7FFDF000 ECX = 00000002
EDX = 00030EA8 ESI = 00000000 EDI = 0013FF80
EIP = 00401005 ESP = 0012FF20 EBP = 0013FF80
EFL = 00000212

 

在执行 call 指令前的 寄存器状态

此时的寄存器状态

EAX = 00000003  EBX = 7FFDF000 ECX = 00000002

EDX = 00030EA8  ESI = 00000000  EDI = 0013FF80
EIP = 0040108E ESP = 0012FF24  EBP = 0013FF80

EFL = 00000212

 

很明显,ESP 向低地址偏移了四个字节,说它 call 指令执行了 push  操作,入栈

它把什么东西入栈了????

 

看下 ESP = 0012FF20 指向的内存地址的内容先

0013FF0E  03 00 30 FF 13 00 1F 3F 40  ..0....?@
0013FF17  00 00 08 00 00 00 00 00 00  .........
0013FF20  83 10 40 00 02 00 00 00 03  ..@......
0013FF29  00 00 00 00 00 00 00 00 00  .........

 

从内存中看出, 00401083  就是刚刚 push 进去的内容了

 

再看回前面的 call 指令

18:       func1(i,j);
00401076   mov         eax,dword ptr [ebp-8]
00401079   push        eax
0040107A   mov         ecx,dword ptr [ebp-4]
0040107D   push        ecx
0040107E   call        @ILT+10(func1) (00401020)
00401093   add         esp,8                                <----
请注意,这条指令的地址

 

原来 00401093  是一个地址,

那么可以肯定地说

call        @ILT+10(func1) (00401020)  指令做的事情就是

先将 下一条指令 的地址 push 进去堆栈,然后再无条件跳转到 (00401020) 这个地址

 

奇怪,为什么在

call 指令将 下一条指令的地址 push 进去 堆栈后,为什么不直接跳转到 func1 (00401020)

而是 先跳转到 00401005 呢?????

why? why?? why???

 

还是一个 @ILT+0(?func1@@YAXHH@Z) 的东东是什么?

 

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

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

 

经过千亲万苦,终于进入 func1() 函数了

 

1:    void func1(int input1, int input2)
2:    {
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]

00401023   sub         esp,4Ch 

然后,堆栈寄存器 ESP 向低地址偏移76(0x4C)字节。这里相当于为 func1()函数层 分配了栈内存 76个字节。

76个字节,是编译器根据你定义的变量的个数和大小,算出来的!

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

 

ebx, esi, edi 分别保存了什么内容? 为什么要把它们分别压入堆栈????

 

再接着,

0xCC初始化上述为func1()函数层所分配的栈内存的每个字节。这里每一步用F11单步跟踪,栈内存的变化你会看得更清楚。
00401029   lea         edi,[ebp-4Ch]     ;
将有效的地址 [ebp-0x4Ch] 赋值到 edi,

                                                    ; [ebp-0x4Ch] 的值正是为 func1() 函数分配的 76个字节内存块的 初始地址
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]AXDIDI±4   (DI 加或减是由 DF 标志位确定的)
如果是 那么 stos word ptr [edi] 的话那么就是
ES:[DI]ALDIDI±2   (DI 加或减是由 DF 标志位确定的)
不然推出 stos BYTE ptr [edi]

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

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

REP 每执行一次后面的字符串指令后, cx1, 直至 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;

6:        i = input1;
00401038   mov         eax,dword ptr [ebp+8]               <----
断点现在在这里
0040103B   mov         dword ptr [ebp-4],eax

 

这两句语句,读取了 输入的第一个参数,并将它赋给了 i

在上面的

 

12:       int i, j;
13:       i=2;
00401078   mov         dword ptr [ebp-4],2   

14:       j=3;
0040107F   mov         dword ptr [ebp-8],3
15:

16:       func1(i,j);                                             <-- 断点在此,EBP = 0012FF80
00401086   mov         eax,dword ptr [ebp-8]
00401089   push        eax
0040108A   mov         ecx,dword ptr [ebp-4]
0040108D   push        ecx

 

--------------------------------------------------

 

12:       int i, j;
13:       i=2;
00401078   mov         dword ptr [ebp-4],2             <--
main 的函数层也像 func1() 函数层一样

                                                                      <-- 在函数内变量 存放在 EBP 指向的一块内存空间

 

然后,ji 分别入栈

 

1:    void func1(int input1, int input2)
2:    {
00401020   push        ebp
00401021   mov         ebp,esp 

 

EBP 原来是 0012FF80,现在 变成了 0012FF1C

在进入 func1 前,

 

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]

00401023   sub         esp,4Ch 

 

ESP 向低地址偏移了 4CH~~~  然后,肚子好饿,需要先去吃饭~~

 

 

Feedback

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

2008-10-02 22:44 by feosun
很赞的说

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

2008-10-03 15:26 by kouprey
很强呀

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

2008-10-05 00:45 by NewSketcher
很好,我很喜欢这种东西,学习了...

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

2008-10-09 09:51 by Garfiled
膜拜ing....

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