Win32 环境下的堆栈
选择自 slimak 的 Blog
Win32 环境下的堆栈(一)
简介
在Win32环境下利用调试器调试应用程序的时候经常要和堆栈(Stack)打交道,尤其是在需要手工遍历堆栈(Manually Walking Stack)的时候我们需要对堆栈的工作过程有一个比较清晰的了解.接下来的这些文字将通过一个例子程序详细的讲解堆栈的工作过程.
关键字
调试 堆栈 Stack Stack-Frame
目录
1.堆栈是什么?
2.堆栈里面放的都是什么信息?
3.堆栈是在什么时候被建立起来的?它的默认大小是多少?
4.默认才1M??那要是我的程序使用超过了1M的堆栈怎么办?
5.什么叫Stack Frame?
6.在一次函数调用中,堆栈是如何工作的?
7.老大,结合一个例子讲讲吧?
1.堆栈是什么?
从内存管理角度看,堆栈是就是一块连续的内存空间,对它的操作采用先入后出的规则,他的生长方向与内存的生长方向正好相反,也就是说它是从高地址向低地址生长.
从Win32程序内部的角度看,每一个线程有自己的堆栈,它主要用来给线程提供一个暂时存放数据的区域,程序使用POP/PUSH指令来对堆栈进行操作.
2.堆栈里面放的都是什么信息?
堆栈中存放的信息包括:当前正在执行的函数的局部变量,函数返回地址,该函数的上层函数传给该函数的参数,EBP的值,一些通用寄存器(EDI,ESI…)的值,注意这里提到的正在执行的函数,比如有下面的一段C代码:
void B()
{
printf(“B\n”);
}
void A()
{
B();
}
那么当程序执行到B函数的printf函数的时候我们说正在执行的函数包括A和B而不仅仅是B函数,这一点需要注意.
3.堆栈是在什么时候被建立起来了?它的默认大小是多少?
堆栈是在我们的main主函数被系统调用之前被建立起来的,对于非主线程它是在线程被建立之前创建的,它的默认大小是1M,如果需要修改堆栈的大小的话可以在VC6++中通过使用/STACK编译项实现:
#pragma comment(linker,“/STACK:2048,1024″) // 预约(Reserve)2M,提交(Commit)1M
关于预约(Reserve)和提交(Commit)的概念请参看”Programming Applications for Microsoft Windows“( Jeffrey Richter,Chapter 15 Using Virtual Memory in Your Own Applications)
4.默认才1M??那要是我的程序使用超过了1M的堆栈怎么办?
系统通过使用异常捕获(Exception Handling)机制来捕获应用程序企图去访问超过该程序提交(Commit)的堆栈范围这种异常,假如你程序预约了2M并且提交了1M大小的堆栈,那么当你的程序企图访问超过1M的范围的时候会产生一个异常并且被系统捕获,系统会帮你继续从另外1M预约的内存中提交内存来满足你的需求,如果你要求提交的大小甚至超过了2M(你一开始预约的大小)在 NT系统下(98除外)系统也会尝试去分配(allocate)内存来满足你,但是系统并不保证分配会成功
5.什么叫Stack Frame?
Stack Frame这个词你可以在各种各样的汇编书籍中看到,到底它表示什么意思呢?也许你看完文章的后半部分就会明白,在此我们先给它一个定义,你看完整篇文章在回过头来回味一下就会知道它的确切含义了,Stack Frame是堆栈中的一块区域,它保存着一个函数的返回地址,和该函数内部使用的局部数据(Local Data),它是由函数入口处的SUB ESP,48h之类的语句来建立的.
6.在一次函数调用中,堆栈是如何工作的?
假设我们的主角叫A函数…
a.首先上级函数传给A函数的参数被压入堆栈中(至于是谁来做这个压栈操作取决于A函数的调用方式:是__stdcall, __cdecl还是其他);
b.然后是返回地址(A函数执行完后接下来程序继续执行的地址)入栈;
c.接下来是当前的EBP;
d.如果A函数有局部变量,就在堆栈中开辟相应的空间以构造那些变量变量(A函数执行结束,这些局部变量的内容将被忽略/遗弃,但是不被清除,比如A函数中有一个变量int m存在于地址0×0012FFCC处,函数结束时9依然存在于0×0012FFCC处没有被清除,但是此时它已经没有任何意义了,
e.在函数返回的时候,弹出EBP,恢复堆栈到函数调用前的地址,弹出返回地址到EIP以继续执行程序。
7.老大,结合一个例子讲讲吧?
下面就是我们要拿来做模特的代码,程序很简单,wWinMain调用AFunc,AFunc再调用BFunc,下面的讲解过程中我们要观摩这个程序的汇编代码形式,可以通过在VC6++该工程的Debug模式中按F5然后Ctrl+Tab做到,我想这对于Win32程序员应该不是难事.
int BFunc(int i,int j)
{
int m = 1;
int n = 2;
m = i;
n = j;
return m;
}
int AFunc(int i,int j)
{
int m = 3;
int n = 4;
m = i;
n = j;
BFunc(m,n);
return 8;
}
int APIENTRY wWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
AFunc(5,6);
return 0;
}
步骤1.我们从wWinMain调用AFunc函数开始
wWinMain调用AFunc的时候,先把参数压栈(至于为什么压栈顺序是6,5而不是5,6请参看附录.注解1)参数压栈结束后此时ESP = 0×0012FEDC,EBP = 0×0012FF30,
这是进入AFunc函数之前的堆栈形势图:
图 1
步骤2.记住进入AFcun函数之前的ESP,EBP的值,然后我们进入AFunc…
为方便大家观摩,先把AFunc函数的全貌贴出来
29: int AFunc(int i,int j)
30: {
004010D0 push ebp ; 先把EBP入栈保存
004010D1 mov ebp,esp ; 再把此时的ESP赋给EBP,这样EBP就可以拿来访问本函数的局部变量
004010D3 sub esp,48h ; 为AFunc函数在堆栈重开辟一块空间,一般来说开辟的空间大小是40+
; 函数内所有局部变量的大小;
004010D6 push ebx ; 通用寄存器入栈,算保存现场吧
004010D7 push esi
004010D8 push edi
004010D9 lea edi,[ebp-48h]
004010DC mov ecx,12h
004010E1 mov eax,0CCCCCCCCh
004010E6 rep stos dword ptr [edi]
31: int m = 3;
004010E8 mov dword ptr [ebp-4],3 ; 为什么局部变量m位于ebp-3处?
32: int n = 4;
004010EF mov dword ptr [ebp-8],4 ; 为什么局部变量n位于ebp-8处?
33:
34: m = i;
004010F6 mov eax,dword ptr [ebp+8] ; ebp+8处存的是什么?
004010F9 mov dword ptr [ebp-4],eax
35: n = j;
004010FC mov ecx,dword ptr [ebp+0Ch] ; ebp+0ch处存的是什么?
004010FF mov dword ptr [ebp-8],ecx
36:
37: BFunc(m,n);
00401102 mov edx,dword ptr [ebp-8] ; AFunc调用BFunc之前先把传给BFunc的参数入栈
00401105 push edx
00401106 mov eax,dword ptr [ebp-4]
00401109 push eax
0040110A call @ILT+25(BFunc) (0040101e)
0040110F add esp,8 ; 这个出栈操作为什么?
38:
39: return 8;
00401112 mov eax,8
40: }
00401117 pop edi ; 恢复现场
00401118 pop esi
00401119 pop ebx
0040111A add esp,48h ; 收回函数一开始在栈中开辟的空间
; 对应于一开始的sub esp,48h
0040111D cmp ebp,esp
0040111F call __chkesp (00401220)
00401124 mov esp,ebp
00401126 pop ebp ; 恢复调用前的EBP
00401127 ret
Win32 环境下的堆栈(二)
简介
在Win32环境下利用调试器调试应用程序的时候经常要和堆栈(Stack)打交道,尤其是在需要手工遍历堆栈(Manually Walking Stack)的时候我们需要对堆栈的工作过程有一个比较清晰的了解.接下来的这些文字将通过一个例子程序详细的讲解堆栈的工作过程.
关键字
调试 堆栈 Stack Stack-Frame
下面我们要花些篇幅详细的解释AFunc函数执行过程堆栈(主要是ESP,EBP)的变化情况:
29: int AFunc(int i,int j)
30: {
004010D0 push ebp
004010D1 mov ebp,esp
004010D3 sub esp,48h
004010D6 push ebx
004010D7 push esi
004010D8 push edi
;
; 上面几行代码叫做prolog,可以理解成”序曲,开始部分”,与之对应的叫epilog(结束曲,结束部分)对于
; prolog需要逐行解释一下:
;
; 004010D0 PUSH EBP
; 将进入AFunc函数之前的EBP的值入栈保存,这时候的EBP相当于是AFunc上级函数
; 的一个现场信息,所以需要保存起来,以便于AFunc返回后上级函数可以恢复EBP使其指向其调用
; AFunc之前的堆栈位置(当然,这还需要靠恢复ESP来协助达到这一目的),该语句执行完之后堆栈将
; 变成下面这个样子
;
图 2
; 在这里要解释一下什么时候”AFunc结束之后的返回地址”入栈了?导致它入栈的语句就是
; CALL @ILT+20(AFunc) (00401019)
; 也就是说是CALL指令干的
;
; 004010D1 MOV EBP,ESP
; ESP赋给EBP,这样EBP就可以拿来访问本函数的局部变量
图 3
; 004010D3 SUB ESP,48h AFunc函数中有两个int型的变量所以开辟的空间大小是
; 40+2*sizeof(int),我暂时还没有找到正式文档中对于此大小
; 计算的公式.注意:ESP-48h后开辟的新的堆栈中的这块空间就是
; 大名鼎鼎的Stack Frame.
; 004010D6 PUSH EBX 我们知道通用寄存器有时候在程序运算的时候可以用来存放
; 临时结果,如果此结果有必要的话也是需要作为现场信息被保存在
; 堆栈中的.
; 004010D7 PUSH ESI
; 004010D8 PUSH EDI
图 4
; 从上面的图解我们很容易看出在进入AFunc函数执行完prolog之后ESP和EBP指示出了堆栈中
; 存放的当前执行函数的信息(绿色部分,其上级函数的堆栈信息由亮绿色表示,呵呵,我可能有一点色
; 弱所以那到底是不是亮绿色我也不是很确定,夜深人静也没人可问…)
004010D9 lea edi,[ebp-48h]
004010DC mov ecx,12h
004010E1 mov eax,0CCCCCCCCh
004010E6 rep stos dword ptr [edi]
31: int m = 3;
004010E8 mov dword ptr [ebp-4],3 ; 函数的局部变量放置在EBP的负偏移处(Negative
; Offset)也就是向低地址方向(当然,当然,这是针对该函数使用
; 了标准的Stack Frame,如果代码被编译器作了优化了那么你
; 很可能就要遇到FPO这个概念,这可能需要另外写一篇文章
; 来解释,所以这里假设我们的函数使用的是标准的Stack
; Frame)
32: int n = 4;
004010EF mov dword ptr [ebp-8],4 ; 同上
图 5
33:
34: m = i;
004010F6 mov eax,dword ptr [ebp+8] ; 从上图中很容易看出来dword ptr [ebp+8]里面放的是
; 上级函数传给AFunc的第一个参数,这里用ebp+8来访问
; 参数说明上级传给下级函数的参数是放在下级函数
; 的EBP的正向偏移位置处(Positive Offset)
004010F9 mov dword ptr [ebp-4],eax ; 将参数的值赋给局部变量
35: n = j;
004010FC mov ecx,dword ptr [ebp+0Ch] ; 同上
004010FF mov dword ptr [ebp-8],ecx ; 同上
图 6
Win32 环境下的堆栈(三)
简介
在Win32环境下利用调试器调试应用程序的时候经常要和堆栈(Stack)打交道,尤其是在需要手工遍历堆栈(Manually Walking Stack)的时候我们需要对堆栈的工作过程有一个比较清晰的了解.接下来的这些文字将通过一个例子程序详细的讲解堆栈的工作过程.
关键字
调试 堆栈 Stack Stack-Frame
步骤3.现在AFcun函数要调用BFunc了…
这是调用前的准备工作:
a.参数被压栈;
b.CALL指令导致返回地址0040110F入栈;
37: BFunc(m,n);
00401102 mov edx,dword ptr [ebp-8]
00401105 push edx
00401106 mov eax,dword ptr [ebp-4]
00401109 push eax
0040110A call @ILT+25(BFunc) (0040101e)
0040110F add esp,8
图 7
; 这和一开始wWinMain调用AFunc是差不多的过程
38:
39: return 8;
00401112 mov eax,8
40: }
00401117 pop edi
00401118 pop esi
00401119 pop ebx
0040111A add esp,48h
0040111D cmp ebp,esp
0040111F call __chkesp (00401220)
00401124 mov esp,ebp
00401126 pop ebp
00401127 ret
步骤4.进入BFcun函数之后堆栈的变化…
老规矩,我们先通篇看看BFunc在VC6++中的汇编代码:
18: int BFunc(int i,int j)
19: {
00401090 push ebp
00401091 mov ebp,esp
00401093 sub esp,48h
00401096 push ebx
00401097 push esi
00401098 push edi
00401099 lea edi,[ebp-48h]
0040109C mov ecx,12h
004010A1 mov eax,0CCCCCCCCh
004010A6 rep stos dword ptr [edi]
20: int m = 1;
004010A8 mov dword ptr [ebp-4],1
21: int n = 2;
004010AF mov dword ptr [ebp-8],2
22:
23: m = i;
004010B6 mov eax,dword ptr [ebp+8]
004010B9 mov dword ptr [ebp-4],eax
24: n = j;
004010BC mov ecx,dword ptr [ebp+0Ch]
004010BF mov dword ptr [ebp-8],ecx
25:
26: return m;
004010C2 mov eax,dword ptr [ebp-4]
27: }
004010C5 pop edi
004010C6 pop esi
004010C7 pop ebx
004010C8 mov esp,ebp
004010CA pop ebp
004010CB ret
; 先看看BFunc的prolog:
18: int BFunc(int i,int j)
19: {
00401090 push ebp
00401091 mov ebp,esp
00401093 sub esp,48h
00401096 push ebx
00401097 push esi
00401098 push edi
图 8
; 这个时候BFunc的堆栈信息也搭建好了(灰色部分)
20: int m = 1;
004010A8 mov dword ptr [ebp-4],1 ; 没什么新意的操作,和AFunc中发生的一模一样
21: int n = 2;
004010AF mov dword ptr [ebp-8],2 ; 没新意
图 9
22:
23: m = i;
004010B6 mov eax,dword ptr [ebp+8] ; 没新意
004010B9 mov dword ptr [ebp-4],eax
24: n = j;
004010BC mov ecx,dword ptr [ebp+0Ch] ; 没新意
004010BF mov dword ptr [ebp-8],ecx
25:
26: return m;
004010C2 mov eax,dword ptr [ebp-4] ; 函数的返回值是放在EAX里面返回的,如果说一个个函
; 数之间是行星的话EAX就是神5那载着杨天人的返回
; 舱了.
27: }
; 我们把重点放在BFunc函数返回时执行的这些指令上(epilog)
004010C5 pop edi
004010C6 pop esi
004010C7 pop ebx
004010C8 mov esp,ebp
004010CA pop ebp
004010CB ret
图 10
图 11
; 此时你会发现图11与图 7时的堆栈情况完全(ESP,EBP的值相同)一样,也就是说调用完BFunc函数后
; 堆栈恢复到了调用前的状态.
0040110F add esp,8 ; 注意BFunc执行完返回AFunc后AFunc将通过改变ESP将先前传给BFunc
; 的参数出栈,但不清空.
图12
就此AFunc调用BFunc函数结束了,接下来堆栈继续重演着:父函数调用子函数,子函数执行结束后返回.然后父函数又作为别人的子函数,执行结束,返回…..
附录
注解1
因为默认C/C++函数的调用约定是__cdecl,这种调用约定参数是从右到左压栈的,Windows提供的函数大部分是__stdcall的调用约定,符合该约定的函数在传参数的时候也是从右到左压栈.
参考书目
[1] Jeffrey Richter,”Programming Applications for Microsoft Windows 4rd”.( Microsoft Press,1999)
[2] Intel Architecture Software Developer Manual
[3] Randy Kath. “The Win32 Debugging API.” MSDN
选择自 slimak 的 Blog
Win32 环境下的堆栈(一)
简介
在Win32环境下利用调试器调试应用程序的时候经常要和堆栈(Stack)打交道,尤其是在需要手工遍历堆栈(Manually Walking Stack)的时候我们需要对堆栈的工作过程有一个比较清晰的了解.接下来的这些文字将通过一个例子程序详细的讲解堆栈的工作过程.
关键字
调试 堆栈 Stack Stack-Frame
目录
1.堆栈是什么?
2.堆栈里面放的都是什么信息?
3.堆栈是在什么时候被建立起来的?它的默认大小是多少?
4.默认才1M??那要是我的程序使用超过了1M的堆栈怎么办?
5.什么叫Stack Frame?
6.在一次函数调用中,堆栈是如何工作的?
7.老大,结合一个例子讲讲吧?
1.堆栈是什么?
从内存管理角度看,堆栈是就是一块连续的内存空间,对它的操作采用先入后出的规则,他的生长方向与内存的生长方向正好相反,也就是说它是从高地址向低地址生长.
从Win32程序内部的角度看,每一个线程有自己的堆栈,它主要用来给线程提供一个暂时存放数据的区域,程序使用POP/PUSH指令来对堆栈进行操作.
2.堆栈里面放的都是什么信息?
堆栈中存放的信息包括:当前正在执行的函数的局部变量,函数返回地址,该函数的上层函数传给该函数的参数,EBP的值,一些通用寄存器(EDI,ESI…)的值,注意这里提到的正在执行的函数,比如有下面的一段C代码:
void B()
{
printf(“B\n”);
}
void A()
{
B();
}
那么当程序执行到B函数的printf函数的时候我们说正在执行的函数包括A和B而不仅仅是B函数,这一点需要注意.
3.堆栈是在什么时候被建立起来了?它的默认大小是多少?
堆栈是在我们的main主函数被系统调用之前被建立起来的,对于非主线程它是在线程被建立之前创建的,它的默认大小是1M,如果需要修改堆栈的大小的话可以在VC6++中通过使用/STACK编译项实现:
#pragma comment(linker,“/STACK:2048,1024″) // 预约(Reserve)2M,提交(Commit)1M
关于预约(Reserve)和提交(Commit)的概念请参看”Programming Applications for Microsoft Windows“( Jeffrey Richter,Chapter 15 Using Virtual Memory in Your Own Applications)
4.默认才1M??那要是我的程序使用超过了1M的堆栈怎么办?
系统通过使用异常捕获(Exception Handling)机制来捕获应用程序企图去访问超过该程序提交(Commit)的堆栈范围这种异常,假如你程序预约了2M并且提交了1M大小的堆栈,那么当你的程序企图访问超过1M的范围的时候会产生一个异常并且被系统捕获,系统会帮你继续从另外1M预约的内存中提交内存来满足你的需求,如果你要求提交的大小甚至超过了2M(你一开始预约的大小)在 NT系统下(98除外)系统也会尝试去分配(allocate)内存来满足你,但是系统并不保证分配会成功
5.什么叫Stack Frame?
Stack Frame这个词你可以在各种各样的汇编书籍中看到,到底它表示什么意思呢?也许你看完文章的后半部分就会明白,在此我们先给它一个定义,你看完整篇文章在回过头来回味一下就会知道它的确切含义了,Stack Frame是堆栈中的一块区域,它保存着一个函数的返回地址,和该函数内部使用的局部数据(Local Data),它是由函数入口处的SUB ESP,48h之类的语句来建立的.
6.在一次函数调用中,堆栈是如何工作的?
假设我们的主角叫A函数…
a.首先上级函数传给A函数的参数被压入堆栈中(至于是谁来做这个压栈操作取决于A函数的调用方式:是__stdcall, __cdecl还是其他);
b.然后是返回地址(A函数执行完后接下来程序继续执行的地址)入栈;
c.接下来是当前的EBP;
d.如果A函数有局部变量,就在堆栈中开辟相应的空间以构造那些变量变量(A函数执行结束,这些局部变量的内容将被忽略/遗弃,但是不被清除,比如A函数中有一个变量int m存在于地址0×0012FFCC处,函数结束时9依然存在于0×0012FFCC处没有被清除,但是此时它已经没有任何意义了,
e.在函数返回的时候,弹出EBP,恢复堆栈到函数调用前的地址,弹出返回地址到EIP以继续执行程序。
7.老大,结合一个例子讲讲吧?
下面就是我们要拿来做模特的代码,程序很简单,wWinMain调用AFunc,AFunc再调用BFunc,下面的讲解过程中我们要观摩这个程序的汇编代码形式,可以通过在VC6++该工程的Debug模式中按F5然后Ctrl+Tab做到,我想这对于Win32程序员应该不是难事.
int BFunc(int i,int j)
{
int m = 1;
int n = 2;
m = i;
n = j;
return m;
}
int AFunc(int i,int j)
{
int m = 3;
int n = 4;
m = i;
n = j;
BFunc(m,n);
return 8;
}
int APIENTRY wWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
AFunc(5,6);
return 0;
}
步骤1.我们从wWinMain调用AFunc函数开始
wWinMain调用AFunc的时候,先把参数压栈(至于为什么压栈顺序是6,5而不是5,6请参看附录.注解1)参数压栈结束后此时ESP = 0×0012FEDC,EBP = 0×0012FF30,
这是进入AFunc函数之前的堆栈形势图:
图 1
步骤2.记住进入AFcun函数之前的ESP,EBP的值,然后我们进入AFunc…
为方便大家观摩,先把AFunc函数的全貌贴出来
29: int AFunc(int i,int j)
30: {
004010D0 push ebp ; 先把EBP入栈保存
004010D1 mov ebp,esp ; 再把此时的ESP赋给EBP,这样EBP就可以拿来访问本函数的局部变量
004010D3 sub esp,48h ; 为AFunc函数在堆栈重开辟一块空间,一般来说开辟的空间大小是40+
; 函数内所有局部变量的大小;
004010D6 push ebx ; 通用寄存器入栈,算保存现场吧
004010D7 push esi
004010D8 push edi
004010D9 lea edi,[ebp-48h]
004010DC mov ecx,12h
004010E1 mov eax,0CCCCCCCCh
004010E6 rep stos dword ptr [edi]
31: int m = 3;
004010E8 mov dword ptr [ebp-4],3 ; 为什么局部变量m位于ebp-3处?
32: int n = 4;
004010EF mov dword ptr [ebp-8],4 ; 为什么局部变量n位于ebp-8处?
33:
34: m = i;
004010F6 mov eax,dword ptr [ebp+8] ; ebp+8处存的是什么?
004010F9 mov dword ptr [ebp-4],eax
35: n = j;
004010FC mov ecx,dword ptr [ebp+0Ch] ; ebp+0ch处存的是什么?
004010FF mov dword ptr [ebp-8],ecx
36:
37: BFunc(m,n);
00401102 mov edx,dword ptr [ebp-8] ; AFunc调用BFunc之前先把传给BFunc的参数入栈
00401105 push edx
00401106 mov eax,dword ptr [ebp-4]
00401109 push eax
0040110A call @ILT+25(BFunc) (0040101e)
0040110F add esp,8 ; 这个出栈操作为什么?
38:
39: return 8;
00401112 mov eax,8
40: }
00401117 pop edi ; 恢复现场
00401118 pop esi
00401119 pop ebx
0040111A add esp,48h ; 收回函数一开始在栈中开辟的空间
; 对应于一开始的sub esp,48h
0040111D cmp ebp,esp
0040111F call __chkesp (00401220)
00401124 mov esp,ebp
00401126 pop ebp ; 恢复调用前的EBP
00401127 ret
Win32 环境下的堆栈(二)
简介
在Win32环境下利用调试器调试应用程序的时候经常要和堆栈(Stack)打交道,尤其是在需要手工遍历堆栈(Manually Walking Stack)的时候我们需要对堆栈的工作过程有一个比较清晰的了解.接下来的这些文字将通过一个例子程序详细的讲解堆栈的工作过程.
关键字
调试 堆栈 Stack Stack-Frame
下面我们要花些篇幅详细的解释AFunc函数执行过程堆栈(主要是ESP,EBP)的变化情况:
29: int AFunc(int i,int j)
30: {
004010D0 push ebp
004010D1 mov ebp,esp
004010D3 sub esp,48h
004010D6 push ebx
004010D7 push esi
004010D8 push edi
;
; 上面几行代码叫做prolog,可以理解成”序曲,开始部分”,与之对应的叫epilog(结束曲,结束部分)对于
; prolog需要逐行解释一下:
;
; 004010D0 PUSH EBP
; 将进入AFunc函数之前的EBP的值入栈保存,这时候的EBP相当于是AFunc上级函数
; 的一个现场信息,所以需要保存起来,以便于AFunc返回后上级函数可以恢复EBP使其指向其调用
; AFunc之前的堆栈位置(当然,这还需要靠恢复ESP来协助达到这一目的),该语句执行完之后堆栈将
; 变成下面这个样子
;
图 2
; 在这里要解释一下什么时候”AFunc结束之后的返回地址”入栈了?导致它入栈的语句就是
; CALL @ILT+20(AFunc) (00401019)
; 也就是说是CALL指令干的
;
; 004010D1 MOV EBP,ESP
; ESP赋给EBP,这样EBP就可以拿来访问本函数的局部变量
图 3
; 004010D3 SUB ESP,48h AFunc函数中有两个int型的变量所以开辟的空间大小是
; 40+2*sizeof(int),我暂时还没有找到正式文档中对于此大小
; 计算的公式.注意:ESP-48h后开辟的新的堆栈中的这块空间就是
; 大名鼎鼎的Stack Frame.
; 004010D6 PUSH EBX 我们知道通用寄存器有时候在程序运算的时候可以用来存放
; 临时结果,如果此结果有必要的话也是需要作为现场信息被保存在
; 堆栈中的.
; 004010D7 PUSH ESI
; 004010D8 PUSH EDI
图 4
; 从上面的图解我们很容易看出在进入AFunc函数执行完prolog之后ESP和EBP指示出了堆栈中
; 存放的当前执行函数的信息(绿色部分,其上级函数的堆栈信息由亮绿色表示,呵呵,我可能有一点色
; 弱所以那到底是不是亮绿色我也不是很确定,夜深人静也没人可问…)
004010D9 lea edi,[ebp-48h]
004010DC mov ecx,12h
004010E1 mov eax,0CCCCCCCCh
004010E6 rep stos dword ptr [edi]
31: int m = 3;
004010E8 mov dword ptr [ebp-4],3 ; 函数的局部变量放置在EBP的负偏移处(Negative
; Offset)也就是向低地址方向(当然,当然,这是针对该函数使用
; 了标准的Stack Frame,如果代码被编译器作了优化了那么你
; 很可能就要遇到FPO这个概念,这可能需要另外写一篇文章
; 来解释,所以这里假设我们的函数使用的是标准的Stack
; Frame)
32: int n = 4;
004010EF mov dword ptr [ebp-8],4 ; 同上
图 5
33:
34: m = i;
004010F6 mov eax,dword ptr [ebp+8] ; 从上图中很容易看出来dword ptr [ebp+8]里面放的是
; 上级函数传给AFunc的第一个参数,这里用ebp+8来访问
; 参数说明上级传给下级函数的参数是放在下级函数
; 的EBP的正向偏移位置处(Positive Offset)
004010F9 mov dword ptr [ebp-4],eax ; 将参数的值赋给局部变量
35: n = j;
004010FC mov ecx,dword ptr [ebp+0Ch] ; 同上
004010FF mov dword ptr [ebp-8],ecx ; 同上
图 6
Win32 环境下的堆栈(三)
简介
在Win32环境下利用调试器调试应用程序的时候经常要和堆栈(Stack)打交道,尤其是在需要手工遍历堆栈(Manually Walking Stack)的时候我们需要对堆栈的工作过程有一个比较清晰的了解.接下来的这些文字将通过一个例子程序详细的讲解堆栈的工作过程.
关键字
调试 堆栈 Stack Stack-Frame
步骤3.现在AFcun函数要调用BFunc了…
这是调用前的准备工作:
a.参数被压栈;
b.CALL指令导致返回地址0040110F入栈;
37: BFunc(m,n);
00401102 mov edx,dword ptr [ebp-8]
00401105 push edx
00401106 mov eax,dword ptr [ebp-4]
00401109 push eax
0040110A call @ILT+25(BFunc) (0040101e)
0040110F add esp,8
图 7
; 这和一开始wWinMain调用AFunc是差不多的过程
38:
39: return 8;
00401112 mov eax,8
40: }
00401117 pop edi
00401118 pop esi
00401119 pop ebx
0040111A add esp,48h
0040111D cmp ebp,esp
0040111F call __chkesp (00401220)
00401124 mov esp,ebp
00401126 pop ebp
00401127 ret
步骤4.进入BFcun函数之后堆栈的变化…
老规矩,我们先通篇看看BFunc在VC6++中的汇编代码:
18: int BFunc(int i,int j)
19: {
00401090 push ebp
00401091 mov ebp,esp
00401093 sub esp,48h
00401096 push ebx
00401097 push esi
00401098 push edi
00401099 lea edi,[ebp-48h]
0040109C mov ecx,12h
004010A1 mov eax,0CCCCCCCCh
004010A6 rep stos dword ptr [edi]
20: int m = 1;
004010A8 mov dword ptr [ebp-4],1
21: int n = 2;
004010AF mov dword ptr [ebp-8],2
22:
23: m = i;
004010B6 mov eax,dword ptr [ebp+8]
004010B9 mov dword ptr [ebp-4],eax
24: n = j;
004010BC mov ecx,dword ptr [ebp+0Ch]
004010BF mov dword ptr [ebp-8],ecx
25:
26: return m;
004010C2 mov eax,dword ptr [ebp-4]
27: }
004010C5 pop edi
004010C6 pop esi
004010C7 pop ebx
004010C8 mov esp,ebp
004010CA pop ebp
004010CB ret
; 先看看BFunc的prolog:
18: int BFunc(int i,int j)
19: {
00401090 push ebp
00401091 mov ebp,esp
00401093 sub esp,48h
00401096 push ebx
00401097 push esi
00401098 push edi
图 8
; 这个时候BFunc的堆栈信息也搭建好了(灰色部分)
20: int m = 1;
004010A8 mov dword ptr [ebp-4],1 ; 没什么新意的操作,和AFunc中发生的一模一样
21: int n = 2;
004010AF mov dword ptr [ebp-8],2 ; 没新意
图 9
22:
23: m = i;
004010B6 mov eax,dword ptr [ebp+8] ; 没新意
004010B9 mov dword ptr [ebp-4],eax
24: n = j;
004010BC mov ecx,dword ptr [ebp+0Ch] ; 没新意
004010BF mov dword ptr [ebp-8],ecx
25:
26: return m;
004010C2 mov eax,dword ptr [ebp-4] ; 函数的返回值是放在EAX里面返回的,如果说一个个函
; 数之间是行星的话EAX就是神5那载着杨天人的返回
; 舱了.
27: }
; 我们把重点放在BFunc函数返回时执行的这些指令上(epilog)
004010C5 pop edi
004010C6 pop esi
004010C7 pop ebx
004010C8 mov esp,ebp
004010CA pop ebp
004010CB ret
图 10
图 11
; 此时你会发现图11与图 7时的堆栈情况完全(ESP,EBP的值相同)一样,也就是说调用完BFunc函数后
; 堆栈恢复到了调用前的状态.
0040110F add esp,8 ; 注意BFunc执行完返回AFunc后AFunc将通过改变ESP将先前传给BFunc
; 的参数出栈,但不清空.
图12
就此AFunc调用BFunc函数结束了,接下来堆栈继续重演着:父函数调用子函数,子函数执行结束后返回.然后父函数又作为别人的子函数,执行结束,返回…..
附录
注解1
因为默认C/C++函数的调用约定是__cdecl,这种调用约定参数是从右到左压栈的,Windows提供的函数大部分是__stdcall的调用约定,符合该约定的函数在传参数的时候也是从右到左压栈.
参考书目
[1] Jeffrey Richter,”Programming Applications for Microsoft Windows 4rd”.( Microsoft Press,1999)
[2] Intel Architecture Software Developer Manual
[3] Randy Kath. “The Win32 Debugging API.” MSDN