复习常用的汇编指令
注:该部分参考《天书夜读:从汇编语言到windows内核编程》谭文,邵坚磊著的第一章部分。1.1用VC 6.0查看汇编代码
在VC 6.0下创建工程,并建立1.cpp源文件。如下:
#include <stdio.h>
int main()
{
return 0;
}
设置断点,打开菜单“Debug” 下的“Windows”子菜单,选择“Disassembly"。出现如下的反汇编代码:
view plaincopy to clipboardprint?
01.--- d:\exp\1\1.cpp -----------------------------------
02.//1.cpp: Defines the entry point for the console application
03.1: #include <stdio.h>
04.2:
05.3: int main()
06.4: {
07.00401010 push ebp
08.00401011 mov ebp,esp
09.00401013 sub esp,40h ;在堆栈区随机分配空间存放局部变量
10.00401016 push ebx ;esp -= 4
11.00401017 push esi ;esp -= 4
12.00401018 push edi ;esp -= 4
13.00401019 lea edi,[ebp-40h]
14.0040101C mov ecx,10h
15.00401021 mov eax,0CCCCCCCCh
16.00401026 rep stos dword ptr [edi]
17.5: return 0;
18.00401028 xor eax,eax
19.6: }
20.0040102A pop edi
21.0040102B pop esi
22.0040102C pop ebx
23.0040102D mov esp,ebp
24.0040102F pop ebp
25.00401030 ret
26.--- No source file -----------------------------------
--- d:\exp\1\1.cpp -----------------------------------
//1.cpp: Defines the entry point for the console application
1: #include <stdio.h>
2:
3: int main()
4: {
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,40h ;在堆栈区随机分配空间存放局部变量
00401016 push ebx ;esp -= 4
00401017 push esi ;esp -= 4
00401018 push edi ;esp -= 4
00401019 lea edi,[ebp-40h]
0040101C mov ecx,10h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
5: return 0;
00401028 xor eax,eax
6: }
0040102A pop edi
0040102B pop esi
0040102C pop ebx
0040102D mov esp,ebp
0040102F pop ebp
00401030 ret
--- No source file -----------------------------------
下面讲解常用的汇编指令。
1.2常用汇编指令
1.2.1堆栈相关指令
push:把一个32位的操作数压入堆栈中。这个操作导致esp被减4。esp被形象地称为栈顶。我们认为顶部是
地址小的区域。那么,压入的堆栈的数据越多,这个堆栈也就越堆越高,esp也就越来越小。在32位平台上,esp每次减少4(字节)。
pop:相反,esp被加4,一个数据出栈。pop的参数一般是一个寄存器,栈顶的数据被弹出到这个寄存器中。
sub:减法。第一个参数是被减数所在的寄存器;第二个参数是减数。(对应的还有add指令。)
add:加法。
ret:返回。相当于跳转回调用函数的地方。(对应的call指令来调用函数,返回到call之后的下一条指令。)
call:调用函数。
call和jmp的不同之处:call指令会把它的下一条指令的地址,然后跳转到它调用的函数的开头处;而单纯的jmp是不会这样做的。同时,ret会自动地弹出返回地址。
注:call的本质相当于push+jmp。ret的本质相当于pop+jmp。
C语言函数的变量保存在栈里。
1.2.2数据传送指令
mov:数据移动。第一个参数是目的,第二个参数是来源。在C语言中相当于赋值号。
xor:异或。这虽然是逻辑运算的指令,但是有趣的是,xor eax,eax这样的操作常用来代替mov eax,0。好处是速度更快,占用字节数更少。
lea:取得地址(第二个参数)后放入到前面的寄存器(第一个参数)中。
注:见到xor eax,eax,应该马上明白这是清零操作。
但实际上,有时候lea用来做和mov同样的事情,比如赋值。看下面一条指令:
lea edi,[ebp-0cch]
方括弧表示存储器,也就是ebp-0cch这个地址所指的存储器的内容。但是lea要求取[ebp-0cch]的地址,那么地址也就是ebp-0cch,这个地址将被放入edi中。换句话说,这等同于:
mov edi,ebp-0cch
但是以上mov指令是错误的,因为mov不支持后一个操作数写成寄存器减去数字,但是lea支持,所以可以用lea来代替它。
为了讲解stos,下面解说前面提到的代码:
view plaincopy to clipboardprint?
01.mov ecx,30h
02.mov eax,0CCCCCCCCh
03.rep stos dword ptr es:[edi]
mov ecx,30h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
stos是串存储指令,它的功能是将eax中的数据放入edi所指的地址中,同时edi会增加4(字节数)。rep使指令重复执行ecx中填写的次数。方括弧表示存储器,这个地址实际上就是edi的内容所指向的地址。这里stos其实对应的是stosd,其它还有stosb、stosw,分别对应于处理4、1、2个字节,这里对堆栈中30h*4(0c0h)个字节初始化为0cch(也就是int 3指令的机器码),这样发生意外时执行堆栈里面的内容会引发调试中断。
1.2.3跳转与比较指令
jmp:无条件跳转。
jg:大于的时候跳转。
jl:小于的时候跳转。
jge:大于等于的时候跳转。
cmp:比较。是jg、jl、jge之类的条件指令的执行条件。
1.3C函数的参数传递过程
函数和堆栈的关系密切,这是因为:我们通过堆栈把参数从外部传入到内部。此外,我们在堆栈中划分区域来容纳函数的内部变量。
调用 push 和 pop 指令的时候,寄存器 esp 用于指向栈顶的位置——栈顶总是栈中地址最小的位置。push 执行的结果,esp 总是减少。pop 则增加。对于C程序默认的调用方式,堆栈总是调用方把参数反序(从右到左)地压入堆栈中,被调用方把堆栈复原。这些参数对齐到机器字长,16位、32位、64位CPU分别对齐到2、4、8个字节。这种调用是C编译器默认的C方式。
view plaincopy to clipboardprint?
01.函数调用规则指的是调用者和被调用函数间传递参数及返回参数的方法,在Windows上,常用的有Pascal方式、WINAPI方式(_stdcall)、C方式(_cdecl)。
02._cdecl C调用规则:
03.(1)参数从右到左进入堆栈;
04.(2)在函数返回后,调用者要负责清除堆栈,所以这种调用常会生成较大的可执行程序。
05.
06._stdcall又称为WINAPI,其调用规则:
07.(1)参数从右到左进入堆栈;
08.(2)被调用的函数在返回前自行清理堆栈,所以生成的代码比cdecl小。
09.
10.Pascal调用规则:
11.Pascal调用规则主要用在Win16函数库中,现在基本不用。
12.(1)参数从左到右进入堆栈;
13.(2)被调用的函数在返回前自行清理堆栈。
14.(3)不支持可变参数的函数调用。
15.此外,在Windows内核中还常见有快速调用方式(_fastcall);在C++编译的代码中有this call(_thiscall)。
函数调用规则指的是调用者和被调用函数间传递参数及返回参数的方法,在Windows上,常用的有Pascal方式、WINAPI方式(_stdcall)、C方式(_cdecl)。
_cdecl C调用规则:
(1)参数从右到左进入堆栈;
(2)在函数返回后,调用者要负责清除堆栈,所以这种调用常会生成较大的可执行程序。
_stdcall又称为WINAPI,其调用规则:
(1)参数从右到左进入堆栈;
(2)被调用的函数在返回前自行清理堆栈,所以生成的代码比cdecl小。
Pascal调用规则:
Pascal调用规则主要用在Win16函数库中,现在基本不用。
(1)参数从左到右进入堆栈;
(2)被调用的函数在返回前自行清理堆栈。
(3)不支持可变参数的函数调用。
此外,在Windows内核中还常见有快速调用方式(_fastcall);在C++编译的代码中有this call(_thiscall)。
C 语言所写的程序中,堆栈用于传递函数参数。这时称为调用栈。 写一个简单的函数如下:
view plaincopy to clipboardprint?
01.void myfunction(int a,int b)
02.{
03. int c = a+b;
04.}
void myfunction(int a,int b)
{
int c = a+b;
}
这是标准的 C 函数调用方式.其过程是:
1) 调用方把参数反序的压入堆栈中。
2) 调用函数。
3) 调用者把堆栈清理复原。
这就是C编译器默认的_cdecl方式,而Windows API一般采用的_stdcall则是被调用者恢复堆栈(可变参数函数调用除外)。
至于返回值都是写入eax中,然后返回的。
注:在Windows中,不管哪种调用方式都是返回值放在eax中,然后返回。外部从eax中得到返回值。
_cdecl方式下被调用函数需要做以下一些事情:
1) 保存 ebp。 ebp 总是被我们用来保存这个函数执行之前的 esp 的值。执行完毕之后,我们用 ebp 恢复 esp。同时,调用我们的上层函数也用 ebp 做同样的事情。所以先把 ebp 压入堆栈。返回之前弹出,避免 ebp 被我们改动。
2) 保存 esp 到 ebp 中。 上面两步的代码如下:
view plaincopy to clipboardprint?
01.push ebp ;保存 ebp,并把 esp 放入 ebp 中;此时 ebp 与 esp 同。
02.mov ebp,esp ;都是这次函数调用时的栈顶。
push ebp ;保存 ebp,并把 esp 放入 ebp 中;此时 ebp 与 esp 同。
mov ebp,esp ;都是这次函数调用时的栈顶。
3) 在堆栈中腾出一个区域用来保存局部变量。这就是常说的所谓局部变量在栈空间中。方法为把 esp 减少一个数值。这样等于压入了一堆变量。日后要恢复时,只要把 esp 恢复成 ebp 中保存的数据就可以了。
4) 保存 ebx,esi,edi 到堆栈中。函数调用完后恢复。这是一个编程规范。 对应的代码如下:
view plaincopy to clipboardprint?
01.sub esp,0cch ;把 esp 往上移动一个范围,等于在堆栈中放出一片新;的空间用来存局部变量.
02.push ebx ;下面保存三个寄存器:ebx,esi,edi,这也是 C 规范.
03.push esi
04.push edi
sub esp,0cch ;把 esp 往上移动一个范围,等于在堆栈中放出一片新;的空间用来存局部变量.
push ebx ;下面保存三个寄存器:ebx,esi,edi,这也是 C 规范.
push esi
push edi
5) 把局部变量区域初始化成 0cccccccch。0cccccccch 实际是 INT 3指令的机器码。这是一个中断指令
。因为局部变不可能被执行,如果执行了必然程序有错,这时发生中断来提示开发者。这是 VC 编译
DEBUG 版本的特有操作。
相关代码如下:
view plaincopy to clipboardprint?
01.lea edi,[ebp-0cch] ;本来是要 mov edi,ebp-0cch,但是 mov 不支持-操作。所
02.;以对 ebp-0cch 取内容,而 lea 把内容的地址也就是 ebp
03.;-0cch 加载到 edi 中.目的是把保存局部变量的区域(从
04.;ebp-0cch 开始的区域)初始化成全部 0cccccccch.
05.
06.mov ecx,33h
07.mov eax,0cccccccch
08.rep stos dword ptr [edi] ;拷贝字符串
lea edi,[ebp-0cch] ;本来是要 mov edi,ebp-0cch,但是 mov 不支持-操作。所
;以对 ebp-0cch 取内容,而 lea 把内容的地址也就是 ebp
;-0cch 加载到 edi 中.目的是把保存局部变量的区域(从
;ebp-0cch 开始的区域)初始化成全部 0cccccccch.
mov ecx,33h
mov eax,0cccccccch
rep stos dword ptr [edi] ;拷贝字符串
6) 然后做函数里应该做的事情。参数的获取是 ebp+8 字节为第一个参数,ebp+12 为第二个参数(注意倒序压入),依次加。ebp+4 字节处是 要返回的地址。
7) 恢复 ebx,esi,edi ,esp,ebp,最后返回。 代码如下:
view plaincopy to clipboardprint?
01.pop edi ;恢复 edi,esi,ebx
02.pop esi
03.pop ebx
04.mov esp,ebp ;恢复原来的 ebp 和 esp,让上一个调用的函数正常使用.
05.pop ebp
06.ret
pop edi ;恢复 edi,esi,ebx
pop esi
pop ebx
mov esp,ebp ;恢复原来的 ebp 和 esp,让上一个调用的函数正常使用.
pop ebp
ret
为了简单起见,我的函数没有返回值。如果要返回值,函数应该在返回之前,把返回值放入 eax 中。外
部通过 eax 得到返回值。 所以用 VC2003 编译 Debug 版本,反汇编代码如下:
view plaincopy to clipboardprint?
01.void myfunction(int a,int b)
02.
03.{
04.push ebp ;保存 ebp,并把 esp 放入 ebp 中。此时 ebp 与 esp 同。
05.mov ebp,esp ;都是这次函数调用时的栈顶。
06.sub esp,0cch ;把 esp 往上移动一个范围,等于在堆栈中放出一片新
07.;的空间用来存局部变量.
08.push ebx ;下面保存三个寄存器:ebx,esi,edi,这也是 C 规范.
09.push esi
10.push edi
11.lea edi,[ebp-0cch] ;本来是要 mov edi,ebp-0cch,但是 mov 不支持-操作。所
12.;以对 ebp-0cch 取内容,而 lea 把内容的地址也就是 ebp
13.;-0cch 加载到 edi 中.目的是把保存局部变量的区域(从
14.;ebp-0cch 开始的区域)初始化成全部 0cccccccch.
15.
16.mov ecx,33h ;0cch/4
17.mov eax,0cccccccch
18.rep stos dword ptr [edi] ;拷贝字符串
19.int c = a+b;
20.
21.mov eax,dword ptr [a] ;简单的相加操作.这里从堆栈中取得从外部传入的参数。那么
22.add eax,dword ptr[b] ;a 和 b 到底是怎么取得的呢,通过 ida 反汇编可以看到,其实
23.;这两条指令是 mov eax, [ebp+8] ,add eax, [ebp+0Ch],参数是
24.;通过 ebp 从堆栈中取得的。这里看到的是 VC 调试器的显示结
25.;果,为了阅读方便直接加上了参数名。
26.
27.mov dword ptr[c],eax
28.}
29.pop edi ;恢复 edi,esi,ebx
30.pop esi
31.pop ebx
32.mov esp,ebp ;恢复原来的 ebp 和 esp,让上一个调用的函数正常使用.
33.pop ebp
34.ret ;对应pop EIP
void myfunction(int a,int b)
{
push ebp ;保存 ebp,并把 esp 放入 ebp 中。此时 ebp 与 esp 同。
mov ebp,esp ;都是这次函数调用时的栈顶。
sub esp,0cch ;把 esp 往上移动一个范围,等于在堆栈中放出一片新
;的空间用来存局部变量.
push ebx ;下面保存三个寄存器:ebx,esi,edi,这也是 C 规范.
push esi
push edi
lea edi,[ebp-0cch] ;本来是要 mov edi,ebp-0cch,但是 mov 不支持-操作。所
;以对 ebp-0cch 取内容,而 lea 把内容的地址也就是 ebp
;-0cch 加载到 edi 中.目的是把保存局部变量的区域(从
;ebp-0cch 开始的区域)初始化成全部 0cccccccch.
mov ecx,33h ;0cch/4
mov eax,0cccccccch
rep stos dword ptr [edi] ;拷贝字符串
int c = a+b;
mov eax,dword ptr [a] ;简单的相加操作.这里从堆栈中取得从外部传入的参数。那么
add eax,dword ptr[b] ;a 和 b 到底是怎么取得的呢,通过 ida 反汇编可以看到,其实
;这两条指令是 mov eax, [ebp+8] ,add eax, [ebp+0Ch],参数是
;通过 ebp 从堆栈中取得的。这里看到的是 VC 调试器的显示结
;果,为了阅读方便直接加上了参数名。
mov dword ptr[c],eax
}
pop edi ;恢复 edi,esi,ebx
pop esi
pop ebx
mov esp,ebp ;恢复原来的 ebp 和 esp,让上一个调用的函数正常使用.
pop ebp
ret ;对应pop EIP
而这个函数的调用方式是:
view plaincopy to clipboardprint?
01.mov eax,dword ptr[b] ; 把 b,a 两个参数压入堆栈
02.push eax
03.mov ecx,dword ptr[a]
04.push ecx
05.call myfunction ; 调用函数 myfunction.
06.add esp,8 ; 恢复堆栈.清除eax, ecx中保存的函数参数的值
mov eax,dword ptr[b] ; 把 b,a 两个参数压入堆栈
push eax
mov ecx,dword ptr[a]
push ecx
call myfunction ; 调用函数 myfunction.
add esp,8 ; 恢复堆栈.清除eax, ecx中保存的函数参数的值
这样一来,函数调用的过程就很清楚了。
总结:
EBP在地址高端,ESP在地址低端。函数参数在EBP的正偏移处,函数内部局部变量在EBP的负偏移处。调用函数时,参数是倒序压栈(从右到左)。
函数被调用过程(以myfunction为例):
1)保存调用者的EBP,将ESP赋给EBP;
2)ESP减小一个范围,用来存放函数内部的局部变量;
3)保存EBX, ESI, EDI;
4)堆栈区内的局部变量区初始化为0cch(对应int 3中断),访问不存在的局部变量会引发该中断;
5)EBP+X访问函数参数,EBP-X分配函数局部变量;
6)返回值放在EAX中;
7)恢复EDI, ESI, EBX, EBP;
8)调用者:恢复堆栈,清楚堆栈内部被调用函数的函数参数.
#ebp #edi #esp #堆栈 #eax
注:该部分参考《天书夜读:从汇编语言到windows内核编程》谭文,邵坚磊著的第一章部分。1.1用VC 6.0查看汇编代码
在VC 6.0下创建工程,并建立1.cpp源文件。如下:
#include <stdio.h>
int main()
{
return 0;
}
设置断点,打开菜单“Debug” 下的“Windows”子菜单,选择“Disassembly"。出现如下的反汇编代码:
view plaincopy to clipboardprint?
01.--- d:\exp\1\1.cpp -----------------------------------
02.//1.cpp: Defines the entry point for the console application
03.1: #include <stdio.h>
04.2:
05.3: int main()
06.4: {
07.00401010 push ebp
08.00401011 mov ebp,esp
09.00401013 sub esp,40h ;在堆栈区随机分配空间存放局部变量
10.00401016 push ebx ;esp -= 4
11.00401017 push esi ;esp -= 4
12.00401018 push edi ;esp -= 4
13.00401019 lea edi,[ebp-40h]
14.0040101C mov ecx,10h
15.00401021 mov eax,0CCCCCCCCh
16.00401026 rep stos dword ptr [edi]
17.5: return 0;
18.00401028 xor eax,eax
19.6: }
20.0040102A pop edi
21.0040102B pop esi
22.0040102C pop ebx
23.0040102D mov esp,ebp
24.0040102F pop ebp
25.00401030 ret
26.--- No source file -----------------------------------
--- d:\exp\1\1.cpp -----------------------------------
//1.cpp: Defines the entry point for the console application
1: #include <stdio.h>
2:
3: int main()
4: {
00401010 push ebp
00401011 mov ebp,esp
00401013 sub esp,40h ;在堆栈区随机分配空间存放局部变量
00401016 push ebx ;esp -= 4
00401017 push esi ;esp -= 4
00401018 push edi ;esp -= 4
00401019 lea edi,[ebp-40h]
0040101C mov ecx,10h
00401021 mov eax,0CCCCCCCCh
00401026 rep stos dword ptr [edi]
5: return 0;
00401028 xor eax,eax
6: }
0040102A pop edi
0040102B pop esi
0040102C pop ebx
0040102D mov esp,ebp
0040102F pop ebp
00401030 ret
--- No source file -----------------------------------
下面讲解常用的汇编指令。
1.2常用汇编指令
1.2.1堆栈相关指令
push:把一个32位的操作数压入堆栈中。这个操作导致esp被减4。esp被形象地称为栈顶。我们认为顶部是
地址小的区域。那么,压入的堆栈的数据越多,这个堆栈也就越堆越高,esp也就越来越小。在32位平台上,esp每次减少4(字节)。
pop:相反,esp被加4,一个数据出栈。pop的参数一般是一个寄存器,栈顶的数据被弹出到这个寄存器中。
sub:减法。第一个参数是被减数所在的寄存器;第二个参数是减数。(对应的还有add指令。)
add:加法。
ret:返回。相当于跳转回调用函数的地方。(对应的call指令来调用函数,返回到call之后的下一条指令。)
call:调用函数。
call和jmp的不同之处:call指令会把它的下一条指令的地址,然后跳转到它调用的函数的开头处;而单纯的jmp是不会这样做的。同时,ret会自动地弹出返回地址。
注:call的本质相当于push+jmp。ret的本质相当于pop+jmp。
C语言函数的变量保存在栈里。
1.2.2数据传送指令
mov:数据移动。第一个参数是目的,第二个参数是来源。在C语言中相当于赋值号。
xor:异或。这虽然是逻辑运算的指令,但是有趣的是,xor eax,eax这样的操作常用来代替mov eax,0。好处是速度更快,占用字节数更少。
lea:取得地址(第二个参数)后放入到前面的寄存器(第一个参数)中。
注:见到xor eax,eax,应该马上明白这是清零操作。
但实际上,有时候lea用来做和mov同样的事情,比如赋值。看下面一条指令:
lea edi,[ebp-0cch]
方括弧表示存储器,也就是ebp-0cch这个地址所指的存储器的内容。但是lea要求取[ebp-0cch]的地址,那么地址也就是ebp-0cch,这个地址将被放入edi中。换句话说,这等同于:
mov edi,ebp-0cch
但是以上mov指令是错误的,因为mov不支持后一个操作数写成寄存器减去数字,但是lea支持,所以可以用lea来代替它。
为了讲解stos,下面解说前面提到的代码:
view plaincopy to clipboardprint?
01.mov ecx,30h
02.mov eax,0CCCCCCCCh
03.rep stos dword ptr es:[edi]
mov ecx,30h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
stos是串存储指令,它的功能是将eax中的数据放入edi所指的地址中,同时edi会增加4(字节数)。rep使指令重复执行ecx中填写的次数。方括弧表示存储器,这个地址实际上就是edi的内容所指向的地址。这里stos其实对应的是stosd,其它还有stosb、stosw,分别对应于处理4、1、2个字节,这里对堆栈中30h*4(0c0h)个字节初始化为0cch(也就是int 3指令的机器码),这样发生意外时执行堆栈里面的内容会引发调试中断。
1.2.3跳转与比较指令
jmp:无条件跳转。
jg:大于的时候跳转。
jl:小于的时候跳转。
jge:大于等于的时候跳转。
cmp:比较。是jg、jl、jge之类的条件指令的执行条件。
1.3C函数的参数传递过程
函数和堆栈的关系密切,这是因为:我们通过堆栈把参数从外部传入到内部。此外,我们在堆栈中划分区域来容纳函数的内部变量。
调用 push 和 pop 指令的时候,寄存器 esp 用于指向栈顶的位置——栈顶总是栈中地址最小的位置。push 执行的结果,esp 总是减少。pop 则增加。对于C程序默认的调用方式,堆栈总是调用方把参数反序(从右到左)地压入堆栈中,被调用方把堆栈复原。这些参数对齐到机器字长,16位、32位、64位CPU分别对齐到2、4、8个字节。这种调用是C编译器默认的C方式。
view plaincopy to clipboardprint?
01.函数调用规则指的是调用者和被调用函数间传递参数及返回参数的方法,在Windows上,常用的有Pascal方式、WINAPI方式(_stdcall)、C方式(_cdecl)。
02._cdecl C调用规则:
03.(1)参数从右到左进入堆栈;
04.(2)在函数返回后,调用者要负责清除堆栈,所以这种调用常会生成较大的可执行程序。
05.
06._stdcall又称为WINAPI,其调用规则:
07.(1)参数从右到左进入堆栈;
08.(2)被调用的函数在返回前自行清理堆栈,所以生成的代码比cdecl小。
09.
10.Pascal调用规则:
11.Pascal调用规则主要用在Win16函数库中,现在基本不用。
12.(1)参数从左到右进入堆栈;
13.(2)被调用的函数在返回前自行清理堆栈。
14.(3)不支持可变参数的函数调用。
15.此外,在Windows内核中还常见有快速调用方式(_fastcall);在C++编译的代码中有this call(_thiscall)。
函数调用规则指的是调用者和被调用函数间传递参数及返回参数的方法,在Windows上,常用的有Pascal方式、WINAPI方式(_stdcall)、C方式(_cdecl)。
_cdecl C调用规则:
(1)参数从右到左进入堆栈;
(2)在函数返回后,调用者要负责清除堆栈,所以这种调用常会生成较大的可执行程序。
_stdcall又称为WINAPI,其调用规则:
(1)参数从右到左进入堆栈;
(2)被调用的函数在返回前自行清理堆栈,所以生成的代码比cdecl小。
Pascal调用规则:
Pascal调用规则主要用在Win16函数库中,现在基本不用。
(1)参数从左到右进入堆栈;
(2)被调用的函数在返回前自行清理堆栈。
(3)不支持可变参数的函数调用。
此外,在Windows内核中还常见有快速调用方式(_fastcall);在C++编译的代码中有this call(_thiscall)。
C 语言所写的程序中,堆栈用于传递函数参数。这时称为调用栈。 写一个简单的函数如下:
view plaincopy to clipboardprint?
01.void myfunction(int a,int b)
02.{
03. int c = a+b;
04.}
void myfunction(int a,int b)
{
int c = a+b;
}
这是标准的 C 函数调用方式.其过程是:
1) 调用方把参数反序的压入堆栈中。
2) 调用函数。
3) 调用者把堆栈清理复原。
这就是C编译器默认的_cdecl方式,而Windows API一般采用的_stdcall则是被调用者恢复堆栈(可变参数函数调用除外)。
至于返回值都是写入eax中,然后返回的。
注:在Windows中,不管哪种调用方式都是返回值放在eax中,然后返回。外部从eax中得到返回值。
_cdecl方式下被调用函数需要做以下一些事情:
1) 保存 ebp。 ebp 总是被我们用来保存这个函数执行之前的 esp 的值。执行完毕之后,我们用 ebp 恢复 esp。同时,调用我们的上层函数也用 ebp 做同样的事情。所以先把 ebp 压入堆栈。返回之前弹出,避免 ebp 被我们改动。
2) 保存 esp 到 ebp 中。 上面两步的代码如下:
view plaincopy to clipboardprint?
01.push ebp ;保存 ebp,并把 esp 放入 ebp 中;此时 ebp 与 esp 同。
02.mov ebp,esp ;都是这次函数调用时的栈顶。
push ebp ;保存 ebp,并把 esp 放入 ebp 中;此时 ebp 与 esp 同。
mov ebp,esp ;都是这次函数调用时的栈顶。
3) 在堆栈中腾出一个区域用来保存局部变量。这就是常说的所谓局部变量在栈空间中。方法为把 esp 减少一个数值。这样等于压入了一堆变量。日后要恢复时,只要把 esp 恢复成 ebp 中保存的数据就可以了。
4) 保存 ebx,esi,edi 到堆栈中。函数调用完后恢复。这是一个编程规范。 对应的代码如下:
view plaincopy to clipboardprint?
01.sub esp,0cch ;把 esp 往上移动一个范围,等于在堆栈中放出一片新;的空间用来存局部变量.
02.push ebx ;下面保存三个寄存器:ebx,esi,edi,这也是 C 规范.
03.push esi
04.push edi
sub esp,0cch ;把 esp 往上移动一个范围,等于在堆栈中放出一片新;的空间用来存局部变量.
push ebx ;下面保存三个寄存器:ebx,esi,edi,这也是 C 规范.
push esi
push edi
5) 把局部变量区域初始化成 0cccccccch。0cccccccch 实际是 INT 3指令的机器码。这是一个中断指令
。因为局部变不可能被执行,如果执行了必然程序有错,这时发生中断来提示开发者。这是 VC 编译
DEBUG 版本的特有操作。
相关代码如下:
view plaincopy to clipboardprint?
01.lea edi,[ebp-0cch] ;本来是要 mov edi,ebp-0cch,但是 mov 不支持-操作。所
02.;以对 ebp-0cch 取内容,而 lea 把内容的地址也就是 ebp
03.;-0cch 加载到 edi 中.目的是把保存局部变量的区域(从
04.;ebp-0cch 开始的区域)初始化成全部 0cccccccch.
05.
06.mov ecx,33h
07.mov eax,0cccccccch
08.rep stos dword ptr [edi] ;拷贝字符串
lea edi,[ebp-0cch] ;本来是要 mov edi,ebp-0cch,但是 mov 不支持-操作。所
;以对 ebp-0cch 取内容,而 lea 把内容的地址也就是 ebp
;-0cch 加载到 edi 中.目的是把保存局部变量的区域(从
;ebp-0cch 开始的区域)初始化成全部 0cccccccch.
mov ecx,33h
mov eax,0cccccccch
rep stos dword ptr [edi] ;拷贝字符串
6) 然后做函数里应该做的事情。参数的获取是 ebp+8 字节为第一个参数,ebp+12 为第二个参数(注意倒序压入),依次加。ebp+4 字节处是 要返回的地址。
7) 恢复 ebx,esi,edi ,esp,ebp,最后返回。 代码如下:
view plaincopy to clipboardprint?
01.pop edi ;恢复 edi,esi,ebx
02.pop esi
03.pop ebx
04.mov esp,ebp ;恢复原来的 ebp 和 esp,让上一个调用的函数正常使用.
05.pop ebp
06.ret
pop edi ;恢复 edi,esi,ebx
pop esi
pop ebx
mov esp,ebp ;恢复原来的 ebp 和 esp,让上一个调用的函数正常使用.
pop ebp
ret
为了简单起见,我的函数没有返回值。如果要返回值,函数应该在返回之前,把返回值放入 eax 中。外
部通过 eax 得到返回值。 所以用 VC2003 编译 Debug 版本,反汇编代码如下:
view plaincopy to clipboardprint?
01.void myfunction(int a,int b)
02.
03.{
04.push ebp ;保存 ebp,并把 esp 放入 ebp 中。此时 ebp 与 esp 同。
05.mov ebp,esp ;都是这次函数调用时的栈顶。
06.sub esp,0cch ;把 esp 往上移动一个范围,等于在堆栈中放出一片新
07.;的空间用来存局部变量.
08.push ebx ;下面保存三个寄存器:ebx,esi,edi,这也是 C 规范.
09.push esi
10.push edi
11.lea edi,[ebp-0cch] ;本来是要 mov edi,ebp-0cch,但是 mov 不支持-操作。所
12.;以对 ebp-0cch 取内容,而 lea 把内容的地址也就是 ebp
13.;-0cch 加载到 edi 中.目的是把保存局部变量的区域(从
14.;ebp-0cch 开始的区域)初始化成全部 0cccccccch.
15.
16.mov ecx,33h ;0cch/4
17.mov eax,0cccccccch
18.rep stos dword ptr [edi] ;拷贝字符串
19.int c = a+b;
20.
21.mov eax,dword ptr [a] ;简单的相加操作.这里从堆栈中取得从外部传入的参数。那么
22.add eax,dword ptr[b] ;a 和 b 到底是怎么取得的呢,通过 ida 反汇编可以看到,其实
23.;这两条指令是 mov eax, [ebp+8] ,add eax, [ebp+0Ch],参数是
24.;通过 ebp 从堆栈中取得的。这里看到的是 VC 调试器的显示结
25.;果,为了阅读方便直接加上了参数名。
26.
27.mov dword ptr[c],eax
28.}
29.pop edi ;恢复 edi,esi,ebx
30.pop esi
31.pop ebx
32.mov esp,ebp ;恢复原来的 ebp 和 esp,让上一个调用的函数正常使用.
33.pop ebp
34.ret ;对应pop EIP
void myfunction(int a,int b)
{
push ebp ;保存 ebp,并把 esp 放入 ebp 中。此时 ebp 与 esp 同。
mov ebp,esp ;都是这次函数调用时的栈顶。
sub esp,0cch ;把 esp 往上移动一个范围,等于在堆栈中放出一片新
;的空间用来存局部变量.
push ebx ;下面保存三个寄存器:ebx,esi,edi,这也是 C 规范.
push esi
push edi
lea edi,[ebp-0cch] ;本来是要 mov edi,ebp-0cch,但是 mov 不支持-操作。所
;以对 ebp-0cch 取内容,而 lea 把内容的地址也就是 ebp
;-0cch 加载到 edi 中.目的是把保存局部变量的区域(从
;ebp-0cch 开始的区域)初始化成全部 0cccccccch.
mov ecx,33h ;0cch/4
mov eax,0cccccccch
rep stos dword ptr [edi] ;拷贝字符串
int c = a+b;
mov eax,dword ptr [a] ;简单的相加操作.这里从堆栈中取得从外部传入的参数。那么
add eax,dword ptr[b] ;a 和 b 到底是怎么取得的呢,通过 ida 反汇编可以看到,其实
;这两条指令是 mov eax, [ebp+8] ,add eax, [ebp+0Ch],参数是
;通过 ebp 从堆栈中取得的。这里看到的是 VC 调试器的显示结
;果,为了阅读方便直接加上了参数名。
mov dword ptr[c],eax
}
pop edi ;恢复 edi,esi,ebx
pop esi
pop ebx
mov esp,ebp ;恢复原来的 ebp 和 esp,让上一个调用的函数正常使用.
pop ebp
ret ;对应pop EIP
而这个函数的调用方式是:
view plaincopy to clipboardprint?
01.mov eax,dword ptr[b] ; 把 b,a 两个参数压入堆栈
02.push eax
03.mov ecx,dword ptr[a]
04.push ecx
05.call myfunction ; 调用函数 myfunction.
06.add esp,8 ; 恢复堆栈.清除eax, ecx中保存的函数参数的值
mov eax,dword ptr[b] ; 把 b,a 两个参数压入堆栈
push eax
mov ecx,dword ptr[a]
push ecx
call myfunction ; 调用函数 myfunction.
add esp,8 ; 恢复堆栈.清除eax, ecx中保存的函数参数的值
这样一来,函数调用的过程就很清楚了。
总结:
EBP在地址高端,ESP在地址低端。函数参数在EBP的正偏移处,函数内部局部变量在EBP的负偏移处。调用函数时,参数是倒序压栈(从右到左)。
函数被调用过程(以myfunction为例):
1)保存调用者的EBP,将ESP赋给EBP;
2)ESP减小一个范围,用来存放函数内部的局部变量;
3)保存EBX, ESI, EDI;
4)堆栈区内的局部变量区初始化为0cch(对应int 3中断),访问不存在的局部变量会引发该中断;
5)EBP+X访问函数参数,EBP-X分配函数局部变量;
6)返回值放在EAX中;
7)恢复EDI, ESI, EBX, EBP;
8)调用者:恢复堆栈,清楚堆栈内部被调用函数的函数参数.
#ebp #edi #esp #堆栈 #eax