函数调用过程汇编分析

235 阅读5分钟

在本文中将通过一段简短的c++代码,演示函数调用的汇编过程,主要是程序执行过程中函数调用过程的演示,基本寄存器、汇编命令说明,以及函数入栈出栈过程中寄存器的使用和使用gdb进行查看的方法。

首先回忆一些基础知识

x86-64汇编基础指令

指令名称描述
mov片内RAM传送指令把操作数从源位置传送到目的位置(数据复制)格式:MOV SRC,DST(从SRC复制数值,覆盖DST)
movl传送32位数据mov 源,目的
callq函数调用示例:callq sum;[rsp] = [rsp] - 8;[rsp] = 下一条指令的地址;[rip] = sum所在地址,跳转到sum
leaveq主要用于函数调用结束时清理栈帧mov %rbp, %rsp;pop %rbp
retqpopq %rip: [%rip] = [%rsp];[%rsp] = [%rsp] + 8
sub算术减
add算术加
push %rbp[%rsp] = [%rsp] - 8;[%rsp] = [%rbp]rsp存储的是rbp的栈地址?caller的栈地址
pop %rbp[%rbp] = [%rsp]; [%rsp] = [%rsp] + 8

函数调用

caller-saved寄存器:rbx,rsp,rbp,r12-r14,r15,x87 cw

callee-saved寄存器:其他

如何传递参数

caller调用callee之前,会把参数放入寄存器(或栈)中,callee按上述规定从对应寄存器中取值即可

函数的前6个整型参数通过寄存器传递,分别存放在rdi,rsi,rdx,rcx,r8,r9寄存器;

(EAX 和 EDX 寄存器是 RAX 和 RDX 寄存器的低 32 位部分,esi和edi分别是 RSI 和 RDI 寄存器的低 32 位部分[extended])

第7个及以后的整型参数放在栈中,按从右到左顺序

如何传递返回值

整型返回值,放在rax寄存器中

栈的对齐问题

(todo)

基础寄存器

寄存器分类

通用寄存器(General-Purpose registers)

状态和控制寄存器(RFLAGS register,64位寄存器,每一个比特位有不同含义)

指令寄存器(RIP)

XMM寄存器(SSE2 Registers)

浮点控制和状态寄存器(MXCSR)

寄存器表示

几个跟本次示例相关的寄存器

寄存器分类寄存器符号寄存器名称功能
instruction pointerRIP指令寄存器下一条指令的逻辑地址。每取出一条指令,rip自增(偏移8字节)指向下一条指令
General-Purpose registersRSP栈顶指针高地址向低地址生长
RBP栈基址指针

示例

c++代码

test.cpp

#include <stdio.h>

int sub(int d, int e) {
  return d - e;
}
int sum(int a, int b) {
  int c = sub(100, 9);
  return a + b + c;
}

int main() {
  int a = 12;
  int b = 98;
  int sum_result = sum(a, b);
  return 0;
}

编译下:g++ test.cpp -g -o test --std=c++11

汇编代码

这里只贴一下三个主体函数main、sum、sub的汇编代码

00000000004004e7 <_Z3subii>:
  4004e7:       55                      push   %rbp
  4004e8:       48 89 e5                mov    %rsp,%rbp
  4004eb:       89 7d fc                mov    %edi,-0x4(%rbp)
  4004ee:       89 75 f8                mov    %esi,-0x8(%rbp)
  4004f1:       8b 45 fc                mov    -0x4(%rbp),%eax
  4004f4:       2b 45 f8                sub    -0x8(%rbp),%eax
  4004f7:       5d                      pop    %rbp
  4004f8:       c3                      retq   

00000000004004f9 <_Z3sumii>:
  4004f9:       55                      push   %rbp
  4004fa:       48 89 e5                mov    %rsp,%rbp
  4004fd:       48 83 ec 18             sub    $0x18,%rsp
  400501:       89 7d ec                mov    %edi,-0x14(%rbp)
  400504:       89 75 e8                mov    %esi,-0x18(%rbp)
  400507:       be 09 00 00 00          mov    $0x9,%esi
  40050c:       bf 64 00 00 00          mov    $0x64,%edi
  400511:       e8 d1 ff ff ff          callq  4004e7 <_Z3subii>
  400516:       89 45 fc                mov    %eax,-0x4(%rbp)
  400519:       8b 55 ec                mov    -0x14(%rbp),%edx
  40051c:       8b 45 e8                mov    -0x18(%rbp),%eax
  40051f:       01 c2                   add    %eax,%edx
  400521:       8b 45 fc                mov    -0x4(%rbp),%eax
  400524:       01 d0                   add    %edx,%eax
  400526:       c9                      leaveq 
  400527:       c3                      retq   

0000000000400528 <main>:
  400528:       55                      push   %rbp
  400529:       48 89 e5                mov    %rsp,%rbp
  40052c:       48 83 ec 10             sub    $0x10,%rsp
  400530:       c7 45 fc 0c 00 00 00    movl   $0xc,-0x4(%rbp)
  400537:       c7 45 f8 62 00 00 00    movl   $0x62,-0x8(%rbp)
  40053e:       8b 55 f8                mov    -0x8(%rbp),%edx
  400541:       8b 45 fc                mov    -0x4(%rbp),%eax
  400544:       89 d6                   mov    %edx,%esi
  400546:       89 c7                   mov    %eax,%edi
  400548:       e8 ac ff ff ff          callq  4004f9 <_Z3sumii>
  40054d:       89 45 f4                mov    %eax,-0xc(%rbp)
  400550:       b8 00 00 00 00          mov    $0x0,%eax
  400555:       c9                      leaveq 
  400556:       c3                      retq   
  400557:       66 0f 1f 84 00 00 00    nopw   0x0(%rax,%rax,1)
  40055e:       00 00 

单步看一下寄存器

gdb -tui test

首先在main函数中打一个端点,r使程序断到指定位置

layout asm 显示汇编状态

layout regs 显示寄存器状态

几个单步相关的命令

ni 单步执行

si 进入函数调用

main

image.png

断点位置前面的三行,是处理函数调用栈

// 将调用main函数的栈帧底存入栈中
0x400528 <main()>       push   %rbp  
// 将当前函数栈帧的底地址赋给rbp 
0x400529 <main()+1>     mov    %rsp,%rbp   
// 给当前函数创建栈帧,rsp指向栈帧顶部 
0x40052c <main()+4>     sub    $0x10,%rsp 

断点位置,后面,是已经执行完压栈过程,rbp为0x7fffffffd6a0,rsp为0x7fffffffd690 ,差为16,即16个字节

然后将2个操作数入栈、如edx/eax寄存器,两个操作数作为sum的参数,存入esi及edi寄存器,然后进去sum函数的callq函数调用

sum

si进去,先看下寄存器内容

这里可以看到,在压栈之前rsp寄存器已经发生了变化,变化为rsp=rsp-8,那么rsp里面保存的内容呢

image1.png

x/xg $rsp

打印出rsp中8个字节的内容,结果为0x000000000040054d,可以看到是main函数中callq后面的一行,即执行完sum之后的返回代码地址

前面三行,依然是压栈和创建栈帧过程

0x4004f9 <sum(int, int)>        push   %rbp                                                                                                                                                                                0x4004fa <sum(int, int)+1>      mov    %rsp,%rbp                                                                                                                                                                           0x4004fd <sum(int, int)+4>      sub    $0x18,%rsp   

这里创建的栈帧大小是0x18字节

四五行,两个mov,是参数入栈,仍然是函数调用常规过程

0x400501 <sum(int, int)+8>      mov    %edi,-0x14(%rbp)                                                                                                                                                                    0x400504 <sum(int, int)+11>     mov    %esi,-0x18(%rbp)  

六七行,仍然是函数使用esi和edi寄存器传递参数

0x400507 <sum(int, int)+14>     mov    $0x9,%esi                                                                                                                                                                           0x40050c <sum(int, int)+19>     mov    $0x64,%edi           

第8行是一个函数调用

0x400511 <sum(int, int)+24>     callq  0x4004e7 <sub(int, int)> 

准备操作数,从寄存器eax中取出返回值,存到栈中-0x4(%rbp)位置

0x400516 <sum(int, int)+29>     mov    %eax,-0x4(%rbp)                                                                                                                                                                     0x400519 <sum(int, int)+32>     mov    -0x14(%rbp),%edx                                                                                                                                                                    0x40051c <sum(int, int)+35>     mov    -0x18(%rbp),%eax  

加法的计算

0x40051f <sum(int, int)+38>     add    %eax,%edx                                                                                                                                                                           0x400521 <sum(int, int)+40>     mov    -0x4(%rbp),%eax                                                                                                                                                                     0x400524 <sum(int, int)+43>     add    %edx,%eax 

最后两行,是返回值相关的

leaveq:

mov %rbp, %rsp //撤销栈空间,回滚%rsp。

pop %rbp //恢复上一个栈帧的%rbp。

400526:       c9                      leaveq 
400527:       c3                      retq   

可以看到,是进行的退栈操作,通过之前保存到栈中的返回代码地址0x000000000040054d,返回到main函数中

sub

同上

整体栈分布情况

栈.png

参考

代码示例来自www.cnblogs.com/INnoVationv…

sourceware.org/gdb/current… gdb手册

www.intel.com/content/www… intel指令查询