1)用VS2010新建Win32 Console Application,工程名为ACECore,工程建立完成后得到打开文件ACECore.cpp,代码如下:
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
return 0;
}
2)用VS2010查看汇编代码的方法:
1. VC必须处于debug状态才能看到汇编指令窗口。因此在上面代码return 0一句上设置断点。
2.按下F5键调试程序,当程序停在断点处时,打开菜单“Debug”下的“Windows”子菜单,选择“Disassembly”。这样就出现反汇编窗口,显示汇编代码:
--- g:/acecore/acecore/acecore.cpp ---------------------------------------------
// ACECore.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
00411350 push ebp
00411351 mov ebp,esp
00411353 sub esp,0C0h
00411359 push ebx
0041135A push esi
0041135B push edi
0041135C lea edi,[ebp-0C0h]
00411362 mov ecx,30h
00411367 mov eax,0CCCCCCCCh
0041136C rep stos dword ptr es:[edi]
return 0;
0041136E xor eax,eax
}
3)相关汇编指令:
push:把一个32位的操作数压入堆栈中,这个操作导致esp被减4。esp被称为栈顶,压入堆栈的数据越多,这个堆栈也就越堆越高,esp地址就越来越小。在32位平台上,esp每次减少4(字节)。
pop:esp被加4,一个数据出栈。pop的参数一般是一个寄存器,栈顶数据被弹出到这个寄存器中。
某些指令会“自动”地操作堆栈:call指令会把它的下一条指令的地址压入堆栈中,然后跳转到它调用的函数的开头处;而单纯的jmp是不会这样做的。同时,ret会自动弹出返回地址。
call的本质相当于push+jmp,ret的本质相当于pop+jmp。
不但push、pop、call和ret会操作堆栈,sub和add也可以用于操作堆栈。如果要一次在堆栈中分配4个4字节长整型的空间,那么没有必要4次调用push,很简单地把esp减去4*4=16即可。当然,也可以同样地用add指令来恢复它。
lea:取得地址(第二个参数)后放入到前面的寄存器(第一个参数)中。实际上,有时候lea用来做和mov同样的事情,比如赋值:
lea edi,[ebp – 0cch]
其中,方括弧表示存储器,也就是ebp-0cch这个地址所指的存储器内容。但是lea语法要求取[ebp-0cch]的地址,这个地址就是ebp-0cch,把这个地址放到edi中,也就是说,这等同于:
mov edi,ebp-0cch
但以上的mov指令时错误的,因为mov不支持后一个操作数写成寄存器减去数字。而lea支持,因此可以用lea来代替它。
Stos指令:
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(也就是int3指令的机器码),这样发生意外时执行堆栈里面的内容会引发调试中断。
==============lost的分割线===============
C函数的参数传递过程:
1)C语言程序通过堆栈把参数从函数外部传入到函数内部,同时,在堆栈中划分区域来容纳函数的内部变量。对于C语言默认的调用方式,函数调用方把参数反序(从右到左)地压入堆栈中,被调用方把堆栈复原。这些参数对齐到机器字长,16位、32位、64位CPU下分别对齐到2、4、8个字节。
2)函数调用规则指的是调用者和被调用者函数间传递参数及返回参数的方法,在Windows上,常用的有Pascal方式、WINAPI方式(_stdcall)、C方式(_cdecl)。
_cdecl调用规则:
(1)参数从右到左进入堆栈;
(2)在函数返回后,调用者要负责清除堆栈,所以这个调用常会生成较大的可执行文件。
_stdcall又称为WINAPI,其调用规则:
(1)参数从右到左入栈;
(2)被调用的函数在返回前自行清理堆栈,所以生成的代码比cdecl下。
Pascal调用规则:
(1)参数从左到右入栈;
(2)被调用参数在返回前自行清理堆栈;
(3)不支持可变参数的函数调用。
此外,在Windows内核中还常见有快速调用方式(_fastcall);在C++编译的代码中有this call方式(_thiscall)。
3)以如下函数作为例子分析:
void ACEFunction(int a, int b)
{
int c = a + b;
}
int _tmain(int argc, _TCHAR* argv[])
{
int a = 1;
int b = 2;
ACEFunction(a, b);
return 0;
}
标准的C函数调用方式(_cdecl):
(1)调用者把参数反序地压入堆栈中;
(2)调用函数;
(3)调用者负责把堆栈清理复原。
注意:在Windows中,不管哪种调用方式都是返回值放在eax中,然后返回。外部从eax中得到返回值。
_cdecl方式下被调用函数需要做以下的事情:
(1)保存ebp。ebp总是被我们用来保存这个函数执行之前的esp的值,执行完毕后,我们用ebp恢复esp;同时,调用此函数的上层函数也用ebp做同样的事情,所以先把ebp压入堆栈,函数返回之前弹出,避免ebp被我们改动。
(2)保存esp到ebp中。
上面两步的代码如下:
;保存ebp,并把esp放入ebp中,此时ebp和esp同
;都是这次函数调用时的栈顶
00411360 push ebp
00411361 mov ebp,esp
(3)在堆栈中腾出一个区域用来保存局部变量,这就是常说的所谓局部变量时保存在栈空间中的。方法是:把esp减少一个数值,这样就等于压入了一堆变量。恢复时,只有把esp恢复成ebp中保存的数据就行。
(4)保存ebx、esi、edi到堆栈中,函数调用完后恢复。
上面两步对应代码如下:
;把esp往下移动一个范围,等于在堆栈中放出一片新
;的空间来保存局部变量
00411363 sub esp,0CCh
00411369 push ebx
0041136A push esi
0041136B push edi
(5)把局部变量区域初始化成全0CCCCCCCCh。0CCh实际上是int3指令的机器码,这是一个断点中断指令。因为局部变量不可能被执行,如果执行了,必然程序出错,这时发生中断来提示开发者。这时VC编译Debug版本的特有操作:
0041136C lea edi,[ebp-0CCh]
00411372 mov ecx,33h
00411377 mov eax,0CCCCCCCCh
0041137C rep stos dword ptr es:[edi]
(6)然后做函数里应该做的事情。参数的获取是ebp+12字节为第二个参数,ebp+8字节为第一个参数(反序入栈),依次增加。最后ebp+4字节处是要返回的地址。
(7)恢复ebx、esi、edi、esp、ebp,最后返回:
00411387 pop edi
00411388 pop esi
00411389 pop ebx
0041138A mov esp,ebp
0041138C pop ebp
0041138D ret
用VS2010编译Debug版本,完整的反汇编代码如下:
--- g:/acecore/acecore/acecore.cpp ---------------------------------------------
// ACECore.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
void ACEFunction(int a, int b)
{
00411360 push ebp ;保存ebp,并把esp放入ebp中。此时ebp与esp相同
00411361 mov ebp,esp ;都是这次函数调用时的栈顶
00411363 sub esp,0CCh ;把esp往上移动一个范围,等于在堆栈中放出一片新
;的空间用来存储局部变量
00411369 push ebx ;下面保存三个寄存器:ebx、esi、edi
0041136A push esi
0041136B push edi
0041136C lea edi,[ebp-0CCh] ;原本是想使用“mov edi, ebp-0CCh”,但是mov不支持
;“-”操作,所以先对ebp-0CCh取内容(即[ebp-0CCh]),
;再利用lea把[ebp-0CCh]的地址也就是ebp-0CCh放到edi中
;目的是把保存局部变量的区域(即ebp-0CCh开始的区域)
;初始化成全部为0CCCCCCCCh。
00411372 mov ecx,33h
00411377 mov eax,0CCCCCCCCh
0041137C rep stos dword ptr es:[edi] ;写入0CCh指令(中断)
int c = a + b;
0041137E mov eax,dword ptr [a] ;加法操作,从堆栈中取得从外部传入的参数。
00411381 add eax,dword ptr [b] ;通过ida反汇编可以看到,其实这两天指令是:
;mov eax, [ebp+8]
;add eax, [ebp+0Ch]
;参数是通过ebp从堆栈中取得的。这里看到的是
;VC调试器的显示结果,是为了方便阅读,
;直接加上了参数名
00411384 mov dword ptr [c],eax
}
00411387 pop edi ;恢复edi、dsi、ebx
00411388 pop esi
00411389 pop ebx
0041138A mov esp,ebp ;恢复原来的ebp和esp,让上一级调用的
0041138C pop ebp ; 函数可以正常使用
0041138D ret
主程序中对这个函数的调用方式是:
004123DC mov eax,dword ptr [b] ;把b, a两个参数压入堆栈
004123DF push eax
004123E0 mov ecx,dword ptr [a]
004123E3 push ecx
004123E4 call ACEFunction (411014h) ;调用函数ACEFunction
004123E9 add esp,8 ;恢复堆栈