总是试图想把想表达的东西表达清楚,但总是发现表达的不够清楚
_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)的参数分别放在ECX和EDX寄存器,
其余的参数仍旧自右身左压栈,被调用的函数在返回前负责清理传送参数的堆栈
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]←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;
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 指向的一块内存空间
然后,j,i 分别入栈
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,~~~ 然后,肚子好饿,需要先去吃饭~~