一 汇编指令与C语言

1、 Debug模式下,VC++6.0下断点运行,按CTRL+F11可查看汇编代码;另外可以用cl /c /FAs YourCppFile.cpp命令行在同目录生成YourCppFile.asm汇编文件。


2、 Push将32位操作数压入堆栈,esp指向栈顶,故esp减去4(字节=32位,在64位机器上则是8)。记住:esp为栈顶指针,堆栈越高,这个值越小。由于Intel处理器采用小端存储模式,故当PUSH一个寄存器(如EBP)的值0012FFB4时,先存低字节再存高字节,即堆栈从栈顶向下应该是B4FF1200,当然,采用LONG型HEX解析内存时显示的数值将不变,仍是0012FFB4。


3、 CALL的本质相当于PUSH+JMP(压入CALL语句下一条指令地址,然后跳转至CALL指定的函数入口地址),RET的本质相当于POP+JMP(弹出CALL语句下一条指令地址,然后跳转该地址)。


4、 对堆栈的操作指令除了PUSH、POP、CALL、RET以外,还可以是ADD,SUB。在C语言中局部变量是保持在栈里,可以用esp – 4*4的方式在堆栈中分配4个4字节长的整形空间,而不用PUSH4次。


5、 用XOR EAX,EAX来取代MOV EAX,0,这样来实现清零的好处是:速度更快,占用字节更少。


6、 LEA取址运算符,但如下语句:LEA EDI,[EBP - 0CCH]与MOV EDI,EBP - 0CCH在语义上是等同的([X]表示存储器X中的内容),但是后者会导致语法错误,原因是MOV不支持后一个操作数为寄存器减去一个数字的形式


7、 C语言函数调用默认方式(_cdcall):调用方把参数反序(从右到左)压入堆栈,被调用方把堆栈复原(获取参数)。这些参数须对齐到机器字长,32、64位CPU分别对齐到4、8个字节。


8、 右3可知,CALL和RET并不是函数存在的绝对证据,我们可以自行操作堆栈然后使用JMP绝对跳转实现函数调用。由于高级语言对函数调用的规则各不一样,由此产生了调用约定,在windows上有:Pascal方式、WINAPI方式(_stdcall)、C方式(_cdcall)。


9、 Pascal函数调用方式基本用在16位函数库中,现在基本不用。


10、_stdcall函数调用方式规则(Windows API):参数从右到左入栈;被调用的函数在返回前自行清理堆栈(可变参数函数调用除外)。


11、_cdcall函数调用方式规则(C编译器默认):参数从右到左入栈;函数返回后,调用者负责清理堆栈,由此通常生成较大可执行文件


12、不管何种调用方式,返回值都放入EAX寄存器中。


13、_cdcall函数:

 

int MyFunction(int a,int b)
{
	return a+b;
}

汇编后代码为:

 

int MyFunction(int a,int b)
{
0040D430 push	ebp			;1)
0040D431 move	bp,esp			;2)
0040D433 sub	esp,40h			;3)
0040D436 push	ebx                                       
0040D437 push	esi                                         
0040D438 push	edi			;4)
0040D439 lea	edi,[ebp-40h]                               
0040D43C mov	ecx,10h                                         
0040D441 mov	eax,0CCCCCCCCh                            
0040D446 rep stos dword ptr [edi]	;5)
	return a+b;
0040D448 mov	eax,dword ptr [ebp+8]                        
0040D44B add	eax,dword ptr [ebp+0Ch]	;6)
}
0040D44E pop	edi                                        
0040D44F pop	esi                                         
0040D450 pop	ebx			;7)
0040D451 mov	esp,ebp                                         
0040D453 pop	ebp			;8)
0040D454 ret	

它所做的事情有:

1) 保存EBP(基址寄存器)。保存之前的ESP值,在返回时恢复,使函数对堆栈能够实现正确操作。

2) 保存ESP到EBP,此时两者相等,都是函数调用以后的栈基址(栈顶)。

3) 在栈中腾出40H的字节区域用来保存局部变量。大小是可变的,由编译器自动分配。

4) 保存ebx,esi,edi到堆栈,函数调用完后恢复。

5) 初始化局部变量区域,循环10H次,每次赋值4个字节(共10H * 4 = 40H字节,与上面分配的字节区域刚好对应)为0CCCCCCCCH,这个值实际上是INT3的机器码,是一个断点中断指令,因局部变量区域不可能被执行,如果执行了则报错。

6) 第一个参数获取位置为ebp + 双倍CPU字长,这里是ebp + 8,后面的依据类型进行偏移。需要说明一点的是,调用者在调用前将参数倒序压入堆栈,所有参数压入以后,在执行CALL指令时,它会自动将CALL指令下面的一条指令地址压入堆栈;此外,进入调用的函数以后,第一件事就是压入EBP到堆栈。由此可以看出,函数当前栈顶(ESP指向位置)与最后一个压入栈的参数(参数列表的第一个参数)相隔了两个CPU字长。这里是4 * 2 =8个字节,故为ebp + 8。

7) 恢复ebx,esi,edi。这里是与入口对应的现场恢复,没什么好说的

8) EBP是被调用者栈顶指针,其内存单元的值是调用者栈顶指针。所以,这里一方面是使ESP指向被调用者的栈顶(也是调用者的栈底),另一方面是恢复调用者的栈基址。

9) RET执行函数返回,此时,ESP自动加上一个CPU字长,指向最后一个被压入的参数位置。

10)  函数的返回值在EAX中,这里不需要额外操作,如果结果不是在EAX,则在返回前一定有MOV操作(或其等同效果的操作)


14、对上面的程序执行如下调用:

 

int main(void)
{
	MyFunction(1,2);
	return 0;
}

其汇编代码为:

int main(void)
{
0040D3F0 push	ebp				;a)
0040D3F1 mov	ebp,esp
0040D3F3 sub	esp,40h				;b)
0040D3F6 push	ebx				;c)
0040D3F7 push	esi				;d)
0040D3F8 push	edi				;e)
0040D3F9 lea	edi,[ebp-40h]
0040D3FC mov	ecx,10h
0040D401 mov	eax,0CCCCCCCCh
0040D406 rep stos dword ptr [edi]
	MyFunction(1,2);
0040D408 push	2				;f)
0040D40A push	1				;g)
0040D40C call	@ILT+5(MyFunction) (0040100a)	;h)
0040D411 add	esp,8
	return 0;
0040D414 xor	eax,eax
}
0040D416 pop	edi
0040D417 pop	esi
0040D418 pop	ebx
0040D419 add	esp,40h
0040D41C cmp	ebp,esp
0040D41E call	__chkesp (0040d460)
0040D423 mov	esp,ebp
0040D425 pop	ebp
0040D426 ret

对以上代码执行时的内存单元数据情况如下表:

 

操作堆栈地址HEX数据描述EBP寄存器
   
4)0012FED80012FF80MyFunction函数压入edi0012FF24
4)0012FEDC01F8BCE8MyFunction函数压入esi0012FF24
4)0012FEE07FFDF000MyFunction函数压入ebx0012FF24
3)、5)

0012FEE4

0012FF20

CCCCCCCC

CCCCCCCC

MyFunction函数压入预留局部变量存储区,全部初始化为0xCCCCCCCCH0012FF24
1)、2)0012FF240012FF80MyFunction函数压入ebp0012FF24
h)0012FF280040D411mian函数CALL调用,压入返回地址0012FF80
g)0012FF2C00000001参数1入栈0012FF80
d)0012FF3000000002参数2入栈0012FF80
e)0012FF3401DD0000main函数压入edi0012FF80
d)0012FF3801F8BCE8main函数压入esi0012FF80
c)0012FF3C7FFDF000main函数压入ebx0012FF80
b)

0012FF40

0012FF7C

CCCCCCCC

CCCCCCCC

main函数压入预留局部变量存储区,全部初始化为0xCCCCCCCCH0012FF80
a)0012FF800012FFC0main函数压入ebp0012FF80


函数地用过程中的操作由下往上看,注意CALL与RET指令执行时的堆栈变化。其中红色和蓝色为EBP寄存器变化情况,灰绿色为参数压栈。


15、 需要注意的是,以上的汇编代码来自于VC++6.0编译器,在自行写汇编或者嵌入汇编时要稍做改变。比如说CALL指令直接写为call MyFunction


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值