Thinking in ASM
1.关于C中的函数调用(call by value)
值传递,一个太老的话题了,我只不过是把多数编程书上讲的用汇编语言展开了,希望您能耐着性子看下去。
先看一个例子:
代码:
#include <stdio.h>
void swap(int a,int b)
{
int temp;
temp=a;
a=b;
b=temp;
}
int main()
{
int a,b;
a=1; b=2;
swap(a,b); /* 交换了吗? */
printf("a=%d b=%d\n",a,b);
return 0;
}
我们设计了一个函数swap(a,b),希望它能对给定的两数交换次序,但实际输出为"a=1 b=2",显然没有成功。刚学编程的初学者可能被这个问题困扰,为什么在函数中对参数的改变不会影响到原参数的实际值?
下面我们用汇编来看一下。(注:这里我用的是VC6,在调试时用Disassembly[Alt+8]得到的。如果用默认的Debug设置,产生的多余代码太多,但Release设置又不含调试信息,只好自己定义了一种方式,把优化方案改为Default,就可以带上调试信息了,代码也与Release版的差别不大。如果哪位有更好的方法请告诉我。)
代码:
1: #include <stdio.h>
2:
3: void swap(int a,int b)
4: {
00401000 push ebp ;把当前ebp入栈,保留起来
00401001 mov ebp,esp ;esp指向栈顶,这句执行完后下面对变量的访问均由ebp指示
00401003 sub esp,44h ;为函数内局部变量留出空间
00401006 push ebx ;保护现场
00401007 push esi ;保护现场
00401008 push edi ;保护现场
5: int temp;
6: temp=a;
00401009 mov eax,dword ptr [ebp+8] ;[ebp+8]值为第一个参数a
0040100C mov dword ptr [ebp-4],eax ;通过eax把[ebp+8]的值给了局部变量[ebp-4]
7: a=b;
0040100F mov ecx,dword ptr [ebp+0Ch] ;[ebp+c]值为第二个参数b
00401012 mov dword ptr [ebp+8],ecx ;通过ecx把[ebp+c]的值给了[ebp+8]
8: b=temp;
00401015 mov edx,dword ptr [ebp-4] ;[ebp-4]是局部变量,现在值为a
00401018 mov dword ptr [ebp+0Ch],edx ;通过edx把[ebp-4]的值给了[ebp+c]
9: }
0040101B pop edi ;恢复现场
0040101C pop esi ;恢复现场
0040101D pop ebx ;恢复现场
0040101E mov esp,ebp ;恢复esp,这样[ebp-xx]的局部变量不再有效
00401020 pop ebp ;恢复第一行保留的ebp
00401021 ret
10:
11: int main()
12: {
00401030 push ebp
00401031 mov ebp,esp
00401033 sub esp,48h
00401036 push ebx
00401037 push esi
00401038 push edi
13: int a,b;
14: a=1; b=2;
00401039 mov dword ptr [ebp-4],1 ;这是变量a
00401040 mov dword ptr [ebp-8],2 ;这是变量b
15: swap(a,b); /* 交换了吗? */
00401047 mov eax,dword ptr [ebp-8] ;取出b的值
0040104A push eax ;变量b入栈
0040104B mov ecx,dword ptr [ebp-4] ;取出a的值
0040104E push ecx ;变量a入栈
0040104F call swap (00401000) ;调用401000处的swap
00401054 add esp,8 ;函数外平衡堆栈,结果swap函数内[ebp+8][ebp+c]全无效
16: printf("a=%d b=%d\n",a,b);
00401057 mov edx,dword ptr [ebp-8]
0040105A push edx
0040105B mov eax,dword ptr [ebp-4]
0040105E push eax
0040105F push offset string "a=%d b=%d\n" (004060cc)
00401064 call _printf (00401075)
00401069 add esp,0Ch
17: return 0;
0040106C xor eax,eax
18: }
0040106E pop edi
0040106F pop esi
00401070 pop ebx
00401071 mov esp,ebp
00401073 pop ebp
00401074 ret
首先明确一下,esp始终指向栈顶,在C中每个函数内都用ebp指针来指示传给它的参数和它自己的局部变量,不同的函数ebp值不同。在上面例子中swap函数的[ebp+8][ebp+C]是参数a,b,[ebp-4]是局部变量temp,main函数的[ebp-8][ebp-4]是局部变量a,b。
可以看出,我们想交换的变量在main函数的局部变量[ebp-8]和[ebp-4]中,而我们调用swap之前先访问这两个地址取出两个值,然后把这两个值压入堆栈,接着一进函数马上把main函数的ebp保存起来,而用当前esp代替ebp在swap函数内指示变量,我们根本没有机会得到main函数中的ebp是多少,swap只交换了它自己堆栈里两个参数的值,而当返回后平衡堆栈时esp+8,这两个参数地址都无效了。main函数的变量[ebp-8][ebp-4]在整个过程中并没有被改动过,只是值被复制了一份传给函数swap而已,因此当然不会被交换,交换的只是两个临时的复制品。这就是K&R的《The C Programming Language》里所说的call by value,即只传递参数的值,不传递参数的地址。
最后写个BT的解决方案:
代码:
void swap(int a,int b)
{
int temp;
temp=a;
a=b;
b=temp;
}
int main()
{
int a,b;
a=1; b=2;
swap(a,b); /* 交换了吗? */
__asm {
MOV EAX,[ESP-8]
MOV a,EAX
MOV EAX,[ESP-4]
MOV b,EAX
}
printf("a=%d b=%d\n",a,b);
return 0;
}
呵呵,开个玩笑啦,这么写程序会让人发疯的。关于如何让函数返回多个值的解释,请关注下文。
第一篇写得有些无聊,因为大家肯定都很熟悉了,以后我好好学习,争取写一些复杂点的,比如C++里的一些特性甚至分析MFC啦……