汇编基础四 --函数调用与堆栈平衡

函数

将高级语言中定义的函数,被编译位汇编代码执行时,会被编译为一堆指令的集合,用来实现特定的功能,并获得执行后的结果。如果不关注函数中的具体实现,就可以将一个函数看作一个整体,函数调用过程等同于执行了一个操作,只不过这个操作比较复杂而已。

汇编中实现一个函数可以使用JMP 和 CALL 指令完成。

函数是一堆完成特定功能的指令集,这些指令集同样需要按照顺序依次执行,所以只要知道函数执行的第一条指令的地址(函数的首地址),函数将会依次执行这些指令,完成函数。

计算两数之和

我们使用C语言实现简单的加法函数

函数分为带参函数和无参函数,对于无参函数,直接使用CALL指令跳转到函数首地址然后开始执行即可,并且CALL指令会将下一行指令地址入栈保存,用于函数结束返回时跳回到原地址。

// 函数体
首地址         指令
00401019     ADD  EAX, 4
0040101D RETN // 将栈中的地址
00401038 取出, 赋值给ESI,下次执行回到00401038地址处执行
...
...

00401034    CALL  00401019       // 将下一行指令地址 00401038存入栈空间,然后执行 00401019地址处的函数
00401038 ...

如果函数带参数,我们使用C语言实现简单的加法函数为例

int  add(int x, int y)
{
  int  z = 0;
  z = x + y;
  return z;
}

void main()
{
  int x = 1;
  int y = 2;
  int z;
  z = add(x, y)
}

该函数执行时可以分为以下的步骤。

  1. add函数需要x, y两个参数至,所以在调用函数前,将参数push 到栈中,为了在函数中使用,多个参数多次执行push即可。 push 1, push  2
  2. 执行CALL 指令,该指令将下一指令地址存入栈中,并将函数首地址写入EIP寄存器,下一次将会执行函数体中的指令。
  3. 函数中的指令开始执行,但是执行我们加法逻辑前,会执行一些操作
    1. 首先进行堆栈提升,并开启一段缓冲区空间用于储存函数中的临时变量,使用 SUB ESP, 40h指令,将ESP向上偏移40个数据宽度。堆栈提升后,使用EBP位置进行寻址。
    2. 将三个寄存器中的值写入栈中,函数执行的过程中会覆盖寄存器的值,保存在栈中后,函数结束时可以从该处恢复。
    3. 开始执行函数的主逻辑
  4. 获取栈中 x,y的值进行加法操作。通过EBP寄存器中保存的位置,函数参数在栈中的位置分别为,EBP+8和EBP+C(16进制)
  5. 执行加法操作
    1. MOV EAX, 0                            -- EAX寄存器中的值设置为0
    2. ADD EAX, ptr ds:[ EBP+8 ]     -- EAX寄存器中的值 + 内存地址 EBP +8位置值,结果保存在EAX中
    3. ADD EAX, ptr ds:[ EBP+C]   -- EAX寄存器中的值 + 内存地址 EBP +C 位置值,结果保存在EAX中
    4. 执行结束后,结果被保存到了EAX寄存器中。
  6. 函数执行结束,开始恢复堆栈,清除函数栈中的信息,保证函数执行前的堆栈信息和函数执行后的堆栈相同,也就是满足堆栈平衡。
    1. 恢复三个寄存器edi, esi, ebx中值,pop edi,   pop esi,    pop ebx
    2. 清除缓冲区,这里的清除并不删除其中的数据,将ESP指针恢复即可,这样缓冲区空间会被作为未使用区域,新的数据写入时候,将原来的数据覆盖。由于ESP提升执行了SUB,所以ADD ESP, 40h即可
    3. EBP恢复到原EBP位置,此时栈中保存了原EBP中的位置,所以 POP EBP ,将堆栈中的原EBP地址保存到EBP中,EBP地址恢复。
    4. 下一行指令地址赋值给EPI寄存器。到此为止,函数执行结束,并回到了CALL指令的下一行指令,但是函数参数空间还没有清除,所以需要在函数外部恢复堆栈平衡。
    5. CALL指令的下一行,清除函数参数。同样的,ESP向下移动,ADD EBP, C 即可。

   

 上面是执行过程,汇编代码为。

// 主程序入口
。。。
push 1
push 2
call  0040107D            -- 调用函数
add esp, 8                   -- 清楚堆栈中的参数值,恢复堆栈

-- 执行结果在eaxz中,需要使用时,获取即可 。。。
// 此处为函数首地址为 0040107D push ebp -- 堆栈提升,保存原ebp值,然后将esp赋值给esp mov ebp, esp sub esp, 40h push ebx push esi push edi mov eax, 0 add eax, ptr ds:[ebp+8] add eax, ptr ds:[ebp+c] pop edi pop esi pop ebx add esp, 40h cmp ebp esp -- 比较esp和ebp是否相同,清除栈信息后应该相,否则说明栈中的内容没有被清除。堆栈不平衡 pop ebp -- 从栈中取出原ebp值,存入ebp即恢复原ebp值 ret

函数执行过程中的堆栈空间是在函数执行时才分配的,这里发生了一次堆栈提升,所以我们总是说函数执行时有独立的栈空间,也是通过这种方式实现。函数执行前后始终需要保证堆栈平衡。

总结

总结函数的执行过程

  • 参数入栈
  • 保存当前执行指令的地址,入栈
  • 进入函数,
    • ESP 和 EBP 分别进行堆栈提升
    • sub esp 开辟缓冲区空间,缓冲区空间地址为 EBP + 4  -> ESP
    • 三个寄存器值保存到栈中
  • 执行函数中内容,EBP为基址,获取通过+8 +C获取函数参数,+4位置为函数返回时跳转的地址
  • 函数执行结束:
    • 恢复三个寄存器
    • add esp恢复 缓冲区,
    • ESP和EBP位置恢复
    • ret
  • 函数外部add ebp  恢复参数空间。
  • 在eax中获取函数返回值即可。

猜你喜欢

转载自www.cnblogs.com/k5210202/p/13368280.html