汇编指令

X86和ARM函数调用中参数传递和返回值

arm属于RISC指令集,而x86则是CISC指令集的代表,编译器生成的结果比较有代表性。其中,参数传递和返回值是汇编/C混合编程比较关注的部分,尤其是在bootloader中编程中非常重要。总的来说,RISC倾向于寄存器穿参数,而x86则是通过堆栈传参数。而返回值则都通过效率最高的寄存器完成,arm中是r0,x86是eax

以下的示例是通过写一个简单例子,并且反编译objdump来看生成的汇编来了解这些机制,呵呵,比较实验化,可是也只能这样了,编译技术也不是那么简单就能理解得了的。注意,这样,编译选项影响就可能比较大,我的实验仅局限在加/减-O选项,而其他的比较细节的就无能为力了。不过,这个方法对我理解参数和返回值这点来说,还是比较有效的。



返回值:
1) X86采用eax作为返回值。
return i;          2d: 89 c0                        mov    %eax,%eax

2) ARM使用r0作为返回值。
RETURN I;      4C: E1A00003            MOV R0, R3

参数传递
1)X86:主要是采用堆栈,除非指定以寄存器传递(通过"regparm (NUMBER)"注:NUMBER<=3指定)。
如果指定寄存器传递参数,则eax为第一个参数,edx为第二个参数, ecx为第三个参数。
int hello(int );
t=hello(13);              
9: 6a 0d               push  $0xd 
b: e8 fc ff ff ff      call  <hello>
2)ARM:寄存器到堆栈,首先将参数赋给r0, r1等,同时,未经优化的代码,在函数的堆栈中,也会为每个参数预留一个参数堆栈。
ARM的参数结构看起来比较奇怪,对其的解释是:出于效率考虑,如果在函数中的寄存器足够分配的话,则经过优化后,它不会进栈,而直接使用寄存器即可。这样的方式可以保证优化只局限于函数内部,实际上一般使用-O优化过的代码最终普遍在函数中不再进栈的。
int hello(int );
t=hello(13); 

未优化: 
10: e3a0000d  mov r0, #13 ; 0xd  
14: ebfffffe      bl <hello> 
... ...
<hello>
......
3c: e50b0010  str r0, [fp, -#16]

优化后:-O选项
4: e3a0000d  mov r0, #13 ; 
...
bl <hello>
...
1c: e1a0f00e  mov pc, lr



汇编指令

SUB

sub ax,9                 给ax减9,之后的结果赋值给ax
sub ax,bx                语意是ax = bx - ax  
sub ax,[0]                将偏移地址为0的内存单元 - ax 再赋值给ax


LEA

LEA指令的功能是取偏移地址,例如LEA   AX,[1000H],作用是将源操作数[1000H]的偏移地址1000H送至AX;

MOV

MOV指令的功能是传送数据,例如MOV AX,[1000H],作用是将1000H作为偏移地址,寻址找到内存单元,将该内存单元中的数据送至AX。

CMP

cmp是比较指令,cmp的功能相当于减法指令。它不保存结果,只是影响相应的标志位。其他的指令通过识别这些被影响的标志位来得知比较结果。 cmp指令格式:   cmp   操作对象1, 操作对象2 计算 操作对象1 - 操作对象2 但不保存结果,只是根据结果修改相应的标志位。 举例假如此时eax = 0h 那么cmp eax, eax     (eax - eax = 0) 此时我们的指令执行后, ZF标志位 = 1, PF = 0, SF =0 , CF =0 , OF = 0 。

数据常量定义

var_CC= byte ptr -0CCh var_8= dword ptr -8 argc= dword ptr  8 argv= dword ptr  0Ch envp= dword ptr  10h

函数调用

EBP是"基址指针"(BASE POINTER), 它最经常被用作高级语言函数调用的"框架指针"(frame pointer). 在破解的时候,经常可以看见一个标准的函数起始代码:

push ebp ;保存当前ebp
mov ebp,esp ;EBP设为当前堆栈指针
sub esp, xxx ;预留xxx字节给函数临时变量.
  ...  
这样一来,EBP 构成了该函数的一个框架, 在EBP上方分别是原来的EBP, 返回地址和参数. EBP下方则是临时变量. 函数返回时作 mov esp,ebp/pop ebp/ret 即可.

ESP 专门用作堆栈指针,被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,ESP也就越来越小。在32位平台上,ESP每次减少4字节。

int __cdecl Sum(int a, int b)  
{ 
push        ebp                 // 保存上一层函数栈底指针  
mov         ebp,esp             // 设置本层函数栈底指针  
sub         esp,0C0h            // 设置本层函数栈顶指针
push        ebx                 // 保存寄存器的值:ebx、esi、edi  
push        esi    
push        edi 

......
}

pop         edi                // 恢复寄存器的值:edi、esi、ebx(相反的顺序弹出)  
pop         esi    
pop         ebx    
mov         esp,ebp            // 恢复上层函数栈顶指针  
pop         ebp                // 恢复上层函数栈底指针  
ret                            // 没有栈平衡操作


所以在外面调用的地方平衡栈
call        Sum (13611A9h)       // 调用函数  
add         esp,8                // 调用方平衡堆栈(弹出参数)  
mov         dword ptr [sum],eax  // 返回值保存在eax中

如果在函数里面把栈平衡了,则调用的地方就不需要做了
int __stdcall Sum(int a, int b)  
{  
 push        ebp    
 mov         ebp,esp    
 sub         esp,0C0h    
 push        ebx    
 push        esi    
 push        edi    
 ........ 
}  
 pop         edi    
 pop         esi    
 pop         ebx    
 mov         esp,ebp    
 pop         ebp    
 ret         8                  // 平衡栈操作,栈弹出8个字节,等价于esp += 8  
在保存寄存器的后面,会有一段调试使用的指令(debug模式才有,release没有)
push ebx;压入ebx
push esi;压入esi
push edi;压入edi

比如:
lea edi, [ebp+var_CC]    ;将申请的栈空间大小存入变址寄存器 EDI 
mov ecx, 33h    ;循环次数33h,ecx = 33h(51),33h*4 = 0CCh
mov eax, 0CCCCCCCCh    ;重复在es:[edi]存入33h个;0CCCCCCCCh, Debug模式下把Stack上的变量初始化为0xcc,检查未初始化的问题(debug才有)
rep stosd

【伪代码】
int stack_new_space[36h];
_asm { mov ecx, 36h; }
for (int i = 0; i < ecx; i++)
{
stack_new_space[i] = 0xCCCCCCCC;
}

检查堆栈平衡

在debug下, 函数调用结束,对ESP恢复后,有段编译器添加的堆栈平衡的功能: j_RTC_CheckEsp



参考:

1. 函数调用(ebp,esp,堆栈快照)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值