Microsoft的一个DirectX3D的BUG
2010-09-24 15:59
344 查看
在 C/C++ 语言中,函数是如何被调用的呢?本文就实际的例子,走进汇编代码来看下函数调用的过程。
首先看一个简单的代码例子:
void test(int i)
{
int j = i;
}
void test1()
{
}
int test2()
{
return 1;
}
void test3(int a,int b,int c)
{
}
void test4()
{
int i,j;
}
void test5()
{
int i,j,k,l;
}
int main()
{
int i =0;
test1();
test(10);
test3(1,2,3);
i=test2();
test4();
test5();
return 0;
}
这段代码很简单, mian 函数调用几个被测试的函数,分别是:
1. 没有参数
2. 有一个参数
3. 有 3 个参数
4. 有返回值
5. 有两个临时变量
6. 有多个临时变量
在 VC7 中,我们将断点设置到 main 函数入口的地方;然后 F5 运行程序。再按 ALT+8 反汇编,我们看到下面的代码:
Main 函数变成这样了:
int main()
{
00401120 push ebp
00401121 mov ebp,esp
00401123 sub esp,0CCh
00401129 push ebx
0040112A push esi
0040112B push edi
0040112C lea edi,[ebp-0CCh]
00401132 mov ecx,33h
00401137 mov eax,0CCCCCCCCh
0040113C rep stos dword ptr [edi]
int i =0;
0040113E mov dword ptr [i],0 // 直接将数据 0 放到指定地址中
test1();
00401145 call test1 (401030h)
test(10);
0040114A push 0Ah
0040114C call test (401000h)
00401151 add esp,4
test3(1,2,3);
00401154 push 3
00401156 push 2
00401158 push 1
0040115A call test3 (401090h)
0040115F add esp,0Ch
i=test2();
00401162 call test2 (401060h)
00401167 mov dword ptr [i],eax
test4();
0040116A call test4 (4010C0h)
test5();
0040116F call test5 (4010F0h)
return 0;
00401174 xor eax,eax
}
00401176 pop edi
00401177 pop esi
00401178 pop ebx
00401179 add esp,0CCh
0040117F cmp ebp,esp
00401181 call _RTC_CheckEsp (4011E0h)
00401186 mov esp,ebp
00401188 pop ebp
00401189 ret
函数入口部分:
00401120 push ebp // 保存 ebp 的值
00401121 mov ebp,esp // 将当前栈顶指针送到 ebp
00401123 sub esp,0CCh // 将栈顶指针下移 0XCC 个字节,为临时变量留出空间
00401129 push ebx // 保存 ebx
0040112A push esi // 保存 esi
0040112B push edi // 保存 edi
0040112C lea edi,[ebp-0CCh] // 将 edp-0CC 地址送 EAX
00401132 mov ecx,33h //CC/4 得到的
00401137 mov eax,0CCCCCCCCh // 初始化为 0XCCCCCCCCH
0040113C rep stos dword ptr [edi]// 复制
这写汇编是编译器为我们生成的函数入口部分,基本的含义是为临时变量分配空间,并且初始化临时变量。
这里需要说明几点:
1. 函数调用是通过堆栈来完成的。
2. 函数入口的地方必须为临时变量分配一定空间;实际上如果没有临时变量,也要留出 C0 个字节。
3. 堆栈栈顶指针随数据的进入逐渐减小。因此 sub esp , 0CCh 实际上是留出了 CC 个自己的堆栈空间。
我们看到实现将栈顶指针保存在 ebp 中,然后对该段空间设置初始值。而 0XCCCCCCH 是由堆栈的性质决定,可以看 MSDN 。
如果开始的时候假设 ESP 等于 0X12FEE0 ,那么在保存 EBP 之后, ESP 变成 0X12FEDC ,那么后来 EBP 中的值就是这个值,在保存的空间(从 0X12FE10 到 0X12FEDC )上将所有的内存都初始化为 0XCC 。而 i 被分配在 0X12FED4 处,也就是第一个预留的位置)。
call test1 (401030h)
由于已经知道 i 的地址了,对 i 的赋值就很简单了。这里看调用第一个没有参数没有返回值的 test1 函数;仅仅一条语句,将 test1 的函数地址给 call 指令。
EAX = CCCCCCCC EBX = 7FFDE000 ECX = 00000000 EDX = 00000001
ESI = 00000040 EDI = 0012FEDC EIP = 00401145 ESP = 0012FE04
EBP = 0012FEDC EFL = 00000202
上面是 Call 指令调用前各寄存器的值;下面是调用后的值:
EAX = CCCCCCCC EBX = 7FFD7000 ECX = 00000000 EDX = 00000001
ESI = 00000040 EDI = 0012FEDC EIP = 00401030 ESP = 0012FE00
EBP = 0012FEDC EFL = 00000202
主要变化在于 EIP 和 ESP ;前者是指令指针寄存器,而后者是堆栈指针寄存器。调用前指令的位置在 00401145 位置,而 call 指定将 EIP 改为 test1 的地址;同时将返回地址入栈;可以看到当前栈顶的值是 0040114A ,实际上是 test1 的下条指令。
因此我们说 Call 指定做了两件事情:
1. 将 EIP 从当前值改为被调用函数的值。
2. 将返回地址,也就是当前地址的下条指令放入堆栈。
现在进入 test1 中看个究竟。
void test1()
{
00401030 push ebp
00401031 mov ebp,esp
00401033 sub esp,0C0h
00401039 push ebx
0040103A push esi
0040103B push edi
0040103C lea edi,[ebp-0C0h]
00401042 mov ecx,30h
00401047 mov eax,0CCCCCCCCh
0040104C rep stos dword ptr [edi]
}
0040104E pop edi
0040104F pop esi
00401050 pop ebx
00401051 mov esp,ebp
00401053 pop ebp
00401054 ret
上面的命令基本相同,主要区别在于 test1 内部没有临时变量,因此这里只保留了 C0 个自己的空间。
继续回到主程序:
test(10);
0040114A push 0Ah
0040114C call test (401000h)
00401151 add esp,4
由于 test 函数有一个参数,因此需要首先将参数压入堆栈中,然后执行与前面相似的操作。
这里有一点需要注意:函数返回之后需要将压入的参数弹出;可以使用 pop 命令,也可以使用 add 命令来执行。
对于 test3 的调用:
test3(1,2,3);
00401154 push 3
00401156 push 2
00401158 push 1
0040115A call test3 (401090h)
0040115F add esp,0Ch
由于它需要三个参数,因此都必须压入栈,返回的时候一次性弹出。
下面看如何调用带有返回值的参数:
i=test2();
00401162 call test2 (401060h)
00401167 mov dword ptr [i],eax
其他的相同,但重要的一点是函数的返回值是通过 eax 寄存器来返回的。
其他几个函数的调用不同的是临时变量数目的不同,仅仅在初始化预留空间的时候不同,基本上是每增加一个变量多出 12 个字节的堆栈空间。
而 mian 函数的返回值,有点特别:
return 0;
00401174 xor eax,eax
特别的不在于通过 eax 返回,而是自己和自己异或,大部分返回 0 的函数都这么做。
在 mian 函数退出的时候有这段代码:
00401176 pop edi
00401177 pop esi
00401178 pop ebx
00401179 add esp,0CCh
0040117F cmp ebp,esp
00401181 call _RTC_CheckEsp (4011E0h)
00401186 mov esp,ebp
00401188 pop ebp
00401189 ret
前面几行是将寄存器的值恢复,而 add esp , 0CCh 是将保留的堆栈空间释放,同时比较 ebp 是否与 esp 相等,如果不相等就提示相应的错误,说明有内存泄露等。最后将 ebp 弹出然后返回。
从上面的分析我们可以看到编译器为我们做了很多事情,包括:堆栈空间分配和释放、寄存器状态保存、参数传递等。当然这些事情也可以完全由我们自己来完成,那么需要做的是使用关键字 naked 来声明函数。
首先看一个简单的代码例子:
void test(int i)
{
int j = i;
}
void test1()
{
}
int test2()
{
return 1;
}
void test3(int a,int b,int c)
{
}
void test4()
{
int i,j;
}
void test5()
{
int i,j,k,l;
}
int main()
{
int i =0;
test1();
test(10);
test3(1,2,3);
i=test2();
test4();
test5();
return 0;
}
这段代码很简单, mian 函数调用几个被测试的函数,分别是:
1. 没有参数
2. 有一个参数
3. 有 3 个参数
4. 有返回值
5. 有两个临时变量
6. 有多个临时变量
在 VC7 中,我们将断点设置到 main 函数入口的地方;然后 F5 运行程序。再按 ALT+8 反汇编,我们看到下面的代码:
Main 函数变成这样了:
int main()
{
00401120 push ebp
00401121 mov ebp,esp
00401123 sub esp,0CCh
00401129 push ebx
0040112A push esi
0040112B push edi
0040112C lea edi,[ebp-0CCh]
00401132 mov ecx,33h
00401137 mov eax,0CCCCCCCCh
0040113C rep stos dword ptr [edi]
int i =0;
0040113E mov dword ptr [i],0 // 直接将数据 0 放到指定地址中
test1();
00401145 call test1 (401030h)
test(10);
0040114A push 0Ah
0040114C call test (401000h)
00401151 add esp,4
test3(1,2,3);
00401154 push 3
00401156 push 2
00401158 push 1
0040115A call test3 (401090h)
0040115F add esp,0Ch
i=test2();
00401162 call test2 (401060h)
00401167 mov dword ptr [i],eax
test4();
0040116A call test4 (4010C0h)
test5();
0040116F call test5 (4010F0h)
return 0;
00401174 xor eax,eax
}
00401176 pop edi
00401177 pop esi
00401178 pop ebx
00401179 add esp,0CCh
0040117F cmp ebp,esp
00401181 call _RTC_CheckEsp (4011E0h)
00401186 mov esp,ebp
00401188 pop ebp
00401189 ret
函数入口部分:
00401120 push ebp // 保存 ebp 的值
00401121 mov ebp,esp // 将当前栈顶指针送到 ebp
00401123 sub esp,0CCh // 将栈顶指针下移 0XCC 个字节,为临时变量留出空间
00401129 push ebx // 保存 ebx
0040112A push esi // 保存 esi
0040112B push edi // 保存 edi
0040112C lea edi,[ebp-0CCh] // 将 edp-0CC 地址送 EAX
00401132 mov ecx,33h //CC/4 得到的
00401137 mov eax,0CCCCCCCCh // 初始化为 0XCCCCCCCCH
0040113C rep stos dword ptr [edi]// 复制
这写汇编是编译器为我们生成的函数入口部分,基本的含义是为临时变量分配空间,并且初始化临时变量。
这里需要说明几点:
1. 函数调用是通过堆栈来完成的。
2. 函数入口的地方必须为临时变量分配一定空间;实际上如果没有临时变量,也要留出 C0 个字节。
3. 堆栈栈顶指针随数据的进入逐渐减小。因此 sub esp , 0CCh 实际上是留出了 CC 个自己的堆栈空间。
我们看到实现将栈顶指针保存在 ebp 中,然后对该段空间设置初始值。而 0XCCCCCCH 是由堆栈的性质决定,可以看 MSDN 。
如果开始的时候假设 ESP 等于 0X12FEE0 ,那么在保存 EBP 之后, ESP 变成 0X12FEDC ,那么后来 EBP 中的值就是这个值,在保存的空间(从 0X12FE10 到 0X12FEDC )上将所有的内存都初始化为 0XCC 。而 i 被分配在 0X12FED4 处,也就是第一个预留的位置)。
call test1 (401030h)
由于已经知道 i 的地址了,对 i 的赋值就很简单了。这里看调用第一个没有参数没有返回值的 test1 函数;仅仅一条语句,将 test1 的函数地址给 call 指令。
EAX = CCCCCCCC EBX = 7FFDE000 ECX = 00000000 EDX = 00000001
ESI = 00000040 EDI = 0012FEDC EIP = 00401145 ESP = 0012FE04
EBP = 0012FEDC EFL = 00000202
上面是 Call 指令调用前各寄存器的值;下面是调用后的值:
EAX = CCCCCCCC EBX = 7FFD7000 ECX = 00000000 EDX = 00000001
ESI = 00000040 EDI = 0012FEDC EIP = 00401030 ESP = 0012FE00
EBP = 0012FEDC EFL = 00000202
主要变化在于 EIP 和 ESP ;前者是指令指针寄存器,而后者是堆栈指针寄存器。调用前指令的位置在 00401145 位置,而 call 指定将 EIP 改为 test1 的地址;同时将返回地址入栈;可以看到当前栈顶的值是 0040114A ,实际上是 test1 的下条指令。
因此我们说 Call 指定做了两件事情:
1. 将 EIP 从当前值改为被调用函数的值。
2. 将返回地址,也就是当前地址的下条指令放入堆栈。
现在进入 test1 中看个究竟。
void test1()
{
00401030 push ebp
00401031 mov ebp,esp
00401033 sub esp,0C0h
00401039 push ebx
0040103A push esi
0040103B push edi
0040103C lea edi,[ebp-0C0h]
00401042 mov ecx,30h
00401047 mov eax,0CCCCCCCCh
0040104C rep stos dword ptr [edi]
}
0040104E pop edi
0040104F pop esi
00401050 pop ebx
00401051 mov esp,ebp
00401053 pop ebp
00401054 ret
上面的命令基本相同,主要区别在于 test1 内部没有临时变量,因此这里只保留了 C0 个自己的空间。
继续回到主程序:
test(10);
0040114A push 0Ah
0040114C call test (401000h)
00401151 add esp,4
由于 test 函数有一个参数,因此需要首先将参数压入堆栈中,然后执行与前面相似的操作。
这里有一点需要注意:函数返回之后需要将压入的参数弹出;可以使用 pop 命令,也可以使用 add 命令来执行。
对于 test3 的调用:
test3(1,2,3);
00401154 push 3
00401156 push 2
00401158 push 1
0040115A call test3 (401090h)
0040115F add esp,0Ch
由于它需要三个参数,因此都必须压入栈,返回的时候一次性弹出。
下面看如何调用带有返回值的参数:
i=test2();
00401162 call test2 (401060h)
00401167 mov dword ptr [i],eax
其他的相同,但重要的一点是函数的返回值是通过 eax 寄存器来返回的。
其他几个函数的调用不同的是临时变量数目的不同,仅仅在初始化预留空间的时候不同,基本上是每增加一个变量多出 12 个字节的堆栈空间。
而 mian 函数的返回值,有点特别:
return 0;
00401174 xor eax,eax
特别的不在于通过 eax 返回,而是自己和自己异或,大部分返回 0 的函数都这么做。
在 mian 函数退出的时候有这段代码:
00401176 pop edi
00401177 pop esi
00401178 pop ebx
00401179 add esp,0CCh
0040117F cmp ebp,esp
00401181 call _RTC_CheckEsp (4011E0h)
00401186 mov esp,ebp
00401188 pop ebp
00401189 ret
前面几行是将寄存器的值恢复,而 add esp , 0CCh 是将保留的堆栈空间释放,同时比较 ebp 是否与 esp 相等,如果不相等就提示相应的错误,说明有内存泄露等。最后将 ebp 弹出然后返回。
从上面的分析我们可以看到编译器为我们做了很多事情,包括:堆栈空间分配和释放、寄存器状态保存、参数传递等。当然这些事情也可以完全由我们自己来完成,那么需要做的是使用关键字 naked 来声明函数。
相关文章推荐
- Microsoft的一个关于DirectX3D的BUG
- 小心啦,Microsoft DAAB 2.0中的一个bug!
- Microsoft.Practices.Unity 的一个线程安全Bug浅析
- 分享:Microsoft IE Webcontrols Treeview的一个bug及修正
- 疑是Microsoft Enterprise Library June 2005的一个小bug (续)
- 疑是Microsoft Enterprise Library June 2005的一个小bug
- Microsoft SQL Server 2000中一个讨厌的Bug
- Microsoft.VisualBasic类库的一个小Bug(Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase bug)
- Microsoft SQL Server 2008作业脚本中的一个小Bug
- 自第N次改需求后,如何权衡业务需求与技术交付?业务方策划了一个宏伟的蓝图,只给你和你的团队一个改BUG的时间开发。阿里效能平台技术专家之岳邀你有奖来聊。
- VS里的一个小小bug
- ie里的button标签的一个bug
- 再来一个Bug报告吧
- 关于excel导出功能的一个很奇怪的bug
- 用 Microsoft Visual C++ 创建一个使用 wpcap.dll 的应用程序,
- memcpy引起的一个bug
- 【转】php中iconv函数的一个小bug
- 在Word2007或2010中怎样把“Microsoft 公式 3.0”作为一个按钮放在快速访问工具栏
- .NET2.0正式版的一个BUG!
- 编程是一个严谨的活:修改一个bug,引入了新的bug