x86_64汇编下的函数调用过程

Posted by guosai on June 10, 2020

材料准备

首先手写一个简单的C程序:main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>

void empty();
int  add(int i, int j);
void myPrint(int num);
void testParams(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k);

int main()
{
	// 测试空函数
	empty();

	// 测试传2个参数的函数
	int i = 3;
	int j = 4;
	int k = add(i, j);

	// 测试传多个参数的函数
	testParams(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);

	// 测试print函数(不定参数)
	myPrint(k);
	
	return 0;
}

void empty()
{
	return;
}

int add(int i, int j)
{
	int k = i + j;
	return k;
}

void myPrint(int num)
{
	printf("%s %s %s: %d\n", "hello", "world", "print", num);
}

void testParams(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k)
{
	return;
}

然后执行 clang main.c,默认生成 a.out 文件。查看 a.out,可以看到 a.out 是一个 x86_64 架构下的 Mach-O 格式的文件。

1
2
3
4
5
6
➜  函数调用过程 git:(master) ✗ clang main.c
➜  函数调用过程 git:(master) ✗ ls
a.out  main.c
➜  函数调用过程 git:(master) ✗ file a.out 
a.out: Mach-O 64-bit executable x86_64
➜  函数调用过程 git:(master) ✗ 

查看Mach-O

我们使用工具来分析Mach-O文件:MachOView

展开段:__TEXT_text,我们可以看到 main.c 中我们自定义的函数的以汇编指令的形式顺序的排列在这里。

RBP 和 RSP

分析函数调用过程,其实就是分析程序执行过程中指令的跳转和栈内存区域的变化

在函数调用过程中有2个非常重要的指针寄存器:bp 和 sp

  1. bp始终指向当前最顶部的栈帧的栈底
  2. sp指向当前最顶部的栈帧的栈顶(原则上是需要指向栈顶,但由于代码优化的原因,并不是总是)

空函数的调用过程

接着让我们用debug的方式来看下 x86_64 下函数的调用过程。首先程序会先进入 main 函数,然后调用 empty 函数,empty 函数非常简单,什么都不做就退出了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1 // 调用 lldb 开始调试 a.out
➜  函数调用过程 git:(master) ✗ lldb a.out 
(lldb) target create "a.out"
Current executable set to 'a.out' (x86_64).

2 // 设置断点到 main
(lldb) breakpoint set --name main 
Breakpoint 1: where = a.out`main, address = 0x0000000100000e20

3 // run 程序
(lldb) run
Process 42767 launched: '/Users/guosai/practice_C/函数调用过程/a.out' (x86_64)
Process 42767 stopped

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x0000000100000e20 a.out`main
a.out`main:
->  0x100000e20 <+0>: pushq  %rbp
    0x100000e21 <+1>: movq   %rsp, %rbp
    0x100000e24 <+4>: subq   $0x40, %rsp
    0x100000e28 <+8>: movl   $0x0, -0x4(%rbp)
Target 0: (a.out) stopped.
(lldb) 

可以看到,程序停留在了 main 函数的第一条指令 pushq %rbp。然后我们再来分析看下 main 函数和 empty 函数的汇编代码(用 disassemble –name 命令)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
a.out`main:
->  0x100000e20 <+0>:   pushq  %rbp
    0x100000e21 <+1>:   movq   %rsp, %rbp
    0x100000e24 <+4>:   subq   $0x40, %rsp
    0x100000e28 <+8>:   movl   $0x0, -0x4(%rbp)
    0x100000e2f <+15>:  callq  0x100000f60               ; empty
    0x100000e34 <+20>:  movl   $0x3, -0x8(%rbp)
    0x100000e3b <+27>:  movl   $0x4, -0xc(%rbp)
    0x100000e42 <+34>:  movl   -0x8(%rbp), %edi
    0x100000e45 <+37>:  movl   -0xc(%rbp), %esi
    0x100000e48 <+40>:  callq  0x100000eb0               ; add
    0x100000e4d <+45>:  movl   %eax, -0x10(%rbp)
    0x100000e50 <+48>:  movl   $0x1, %edi
    0x100000e55 <+53>:  movl   $0x2, %esi
    0x100000e5a <+58>:  movl   $0x3, %edx
    0x100000e5f <+63>:  movl   $0x4, %ecx
    0x100000e64 <+68>:  movl   $0x5, %r8d
    0x100000e6a <+74>:  movl   $0x6, %r9d
    0x100000e70 <+80>:  movl   $0x7, (%rsp)
    0x100000e77 <+87>:  movl   $0x8, 0x8(%rsp)
    0x100000e7f <+95>:  movl   $0x9, 0x10(%rsp)
    0x100000e87 <+103>: movl   $0xa, 0x18(%rsp)
    0x100000e8f <+111>: movl   $0xb, 0x20(%rsp)
    0x100000e97 <+119>: callq  0x100000ed0               ; testParams
    0x100000e9c <+124>: movl   -0x10(%rbp), %edi
    0x100000e9f <+127>: callq  0x100000f20               ; myPrint
    0x100000ea4 <+132>: xorl   %eax, %eax
    0x100000ea6 <+134>: addq   $0x40, %rsp
    0x100000eaa <+138>: popq   %rbp
    0x100000eab <+139>: retq   
    0x100000eac <+140>: nopl   (%rax)
1
2
3
4
5
a.out`empty:
    0x100000f60 <+0>: pushq  %rbp
    0x100000f61 <+1>: movq   %rsp, %rbp
    0x100000f64 <+4>: popq   %rbp
    0x100000f65 <+5>: retq   

要分析一个空函数的调用过程,主要是分析一下几个指令(或指令组合)

指令(指令组合) 作用
callq 数值 跳转到某函数
pushq %rbp 和 movq %rsp, %rbp 暂存bp指针,并开辟新的栈帧
popq %rbp 和 retq 函数结束,返回上一级函数

callq

callq 就是 call 指令(q代表要处理的字节长度,q是8字节,l是4字节,w是2字节)。call 指令指示 CPU 应跳转到某个地址去执行新的函数。

现在我们执行4次step指令,让 CPU 停留在第五条指令上:callq 0x100000f60(去跳转 empty 函数),在分析这条指令之前,我们先来看下 CPU 中各寄存器的状态(register read 命令)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
a.out`main:
    0x100000e20 <+0>:   pushq  %rbp
    0x100000e21 <+1>:   movq   %rsp, %rbp
    0x100000e24 <+4>:   subq   $0x40, %rsp
    0x100000e28 <+8>:   movl   $0x0, -0x4(%rbp)
->  0x100000e2f <+15>:  callq  0x100000f60               ; empty
    0x100000e34 <+20>:  movl   $0x3, -0x8(%rbp)
    0x100000e3b <+27>:  movl   $0x4, -0xc(%rbp)
    ............

General Purpose Registers:
       rax = 0x0000000100000e20  a.out`main
       rbx = 0x0000000000000000
       rcx = 0x00007ffeefbff5a8
       rdx = 0x00007ffeefbff3a8
       rdi = 0x0000000000000001
       rsi = 0x00007ffeefbff398
       rbp = 0x00007ffeefbff370
       rsp = 0x00007ffeefbff330
        r8 = 0x0000000000000000
        r9 = 0x0000000000000000
       r10 = 0x0000000000000000
       r11 = 0x0000000000000000
       r12 = 0x0000000000000000
       r13 = 0x0000000000000000
       r14 = 0x0000000000000000
       r15 = 0x0000000000000000
       rip = 0x0000000100000e2f  a.out`main + 15
    rflags = 0x0000000000000206
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

可以看到此时:

  • rbp = 0x00007ffeefbff370,main栈帧的栈底的地址就是 0x00007ffeefbff370
  • rsp = 0x00007ffeefbff330,main栈帧的栈顶的地址就是 0x00007ffeefbff330(因为栈是向下生长的,所以sp小于dp)

由此可以算出 main 的栈帧占的内存空间大小是 0x40 ,正好符合第三条指令:subq $0x40, %rsp

  • rip 指向当前等待执行的指令的地址,此时 rip = 0x0000000100000e2f,正好是(0x100000e2f <+15>: callq 0x100000f60)

那么 callq 0x100000f60 指令做了什么呢?我们首先抛出结论:

  1. 首先 call 指令把当前指令的下一条指令 push 入栈(先把这条指令的地址暂存下来,等后面被调用函数返回的时候才能找到它,并继续执行)
  2. 找到下一条需要执行的指令的地址(即 empty 的地址),并跳转执行。

首先我们来验证第2点,当前的跳转指令是:callq 0x100000f60,empty 的第一条指令的地址也是 0x100000f60(0x100000f60 <+0>: pushq %rbp)。这里 0x100000f60 是 lldb帮我们计算出来的值,算法是:目的值 = 偏移值+下一条指令的地址,这个逻辑就是地址无关代码(position independent code)

然后我们验证第一点,先看一下当前内存中栈顶附近(rsp = 0x00007ffeefbff330)存储的数值(memory read 指令)

1
2
3
4
5
6
7
memory read --size 4 --format x --count 16 0x00007ffeefbff310

0x7ffeefbff310: 0x00000000 0x00000000 0xefbff398 0x00007ffe
0x7ffeefbff320: 0xefbff380 0x00007ffe (0x00000000 0x00000001)
0x7ffeefbff330: 0x00000001 0x0000000e 0x00000000 0x00000000
0x7ffeefbff340: 0x00000000 0x00000000 0x00000000 0x00000000
(lldb) 

上面的(0x00000000 0x00000001)表示0x7ffeefbff328地址存的现在存的值是 0000000100000000(小端模式),后面在会被覆盖成函数返回地址 0x100000e34。

然后执行 step。再次观察寄存器和内存数值的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
memory read --size 4 --format x --count 16 0x00007ffeefbff310
0x7ffeefbff310: 0x00000000 0x00000000 0xefbff398 0x00007ffe
0x7ffeefbff320: 0xefbff380 0x00007ffe (0x00000e34 0x00000001)
0x7ffeefbff330: 0x00000001 0x0000000e 0x00000000 0x00000000
0x7ffeefbff340: 0x00000000 0x00000000 0x00000000 0x00000000

(lldb) register read
General Purpose Registers:
       rax = 0x0000000100000e20  a.out`main
       rbx = 0x0000000000000000
       rcx = 0x00007ffeefbff5a8
       rdx = 0x00007ffeefbff3a8
       rdi = 0x0000000000000001
       rsi = 0x00007ffeefbff398
       rbp = 0x00007ffeefbff370
       rsp = 0x00007ffeefbff328
        r8 = 0x0000000000000000
        r9 = 0x0000000000000000
       r10 = 0x0000000000000000
       r11 = 0x0000000000000000
       r12 = 0x0000000000000000
       r13 = 0x0000000000000000
       r14 = 0x0000000000000000
       r15 = 0x0000000000000000
       rip = 0x0000000100000f60  a.out`empty
    rflags = 0x0000000000000206
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

可以看到有三个地方变化了

  1. 0x7ffeefbff328 地址内存的值有(0x00000000 0x00000001)变成了(0x00000e34 0x00000001,即小端模式下的 0000000100000e34),正好是 callq 0x100000f60 的下一条指令的地址 0x100000e34。
  2. 因为有新的值入栈了,所以rsp的值也需要更新到新的栈顶,即 0x00007ffeefbff330 - 8 = 0x00007ffeefbff328(push入栈的指针占8个字节)
  3. rip指向了新的指令地址:empty函数的第一条指令:rip = 0x0000000100000f60 a.out`empty

执行完call指令后,执行逻辑就从main跳出来,去执行empty了。

pushq %rbp & movq %rsp, %rbp

所有函数的开头都需要执行这两行函数,它的目的是暂存上一个函数的rbp指针,并把rbp指针更新指向到新的栈帧

  1. pushq %rbp:暂存一个函数的rbp指针
  2. movq %rsp, %rbp:rbp指针更新指向到新的栈帧

第一步:验证一下 pushq %rbp,首先看下当前寄存器和内存的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(lldb) memory read --size 4 --format x --count 16 0x00007ffeefbff310
0x7ffeefbff310: 0x00000000 0x00000000 0xefbff398 0x00007ffe
0x7ffeefbff320: (0xefbff380 0x00007ffe) 0x00000e34 0x00000001
0x7ffeefbff330: 0x00000001 0x0000000e 0x00000000 0x00000000
0x7ffeefbff340: 0x00000000 0x00000000 0x00000000 0x00000000
(lldb) register read
General Purpose Registers:
       rax = 0x0000000100000e20  a.out`main
       rbx = 0x0000000000000000
       rcx = 0x00007ffeefbff5a8
       rdx = 0x00007ffeefbff3a8
       rdi = 0x0000000000000001
       rsi = 0x00007ffeefbff398
       rbp = 0x00007ffeefbff370
       rsp = 0x00007ffeefbff328
        r8 = 0x0000000000000000
        r9 = 0x0000000000000000
       r10 = 0x0000000000000000
       r11 = 0x0000000000000000
       r12 = 0x0000000000000000
       r13 = 0x0000000000000000
       r14 = 0x0000000000000000
       r15 = 0x0000000000000000
       rip = 0x0000000100000f60  a.out`empty
    rflags = 0x0000000000000206
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

其中 0x7ffeefbff320 地址中的值(0x00000001 0x0000000e)一会儿是要被当前 rbp 的值(rbp = 0x00007ffeefbff370)覆盖的,然后 rsp 会减“1”,变成 0x00007ffeefbff320。ok,我们执行以下 step,看下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(lldb) memory read --size 4 --format x --count 16 0x00007ffeefbff310
0x7ffeefbff310: 0x00000000 0x00000000 0xefbff398 0x00007ffe
0x7ffeefbff320: (0xefbff370 0x00007ffe) 0x00000e34 0x00000001
0x7ffeefbff330: 0x00000001 0x0000000e 0x00000000 0x00000000
0x7ffeefbff340: 0x00000000 0x00000000 0x00000000 0x00000000
(lldb) register read
General Purpose Registers:
       rax = 0x0000000100000e20  a.out`main
       rbx = 0x0000000000000000
       rcx = 0x00007ffeefbff5a8
       rdx = 0x00007ffeefbff3a8
       rdi = 0x0000000000000001
       rsi = 0x00007ffeefbff398
       rbp = 0x00007ffeefbff370
       rsp = 0x00007ffeefbff320
        r8 = 0x0000000000000000
        r9 = 0x0000000000000000
       r10 = 0x0000000000000000
       r11 = 0x0000000000000000
       r12 = 0x0000000000000000
       r13 = 0x0000000000000000
       r14 = 0x0000000000000000
       r15 = 0x0000000000000000
       rip = 0x0000000100000f61  a.out`empty + 1
    rflags = 0x0000000000000206
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

结果和预料的一样,现在:rsp = 0x00007ffeefbff320,地址 0x7ffeefbff320 存储的值是 0x00007ffeefbff370(小端模式)。

第二步:movq %rsp, %rbp,是把 rsp 的值赋值给 rbp,这样 rbp 就指向了新的栈帧的栈底,代表开辟了了一个新的栈帧,即 empty 函数的栈帧。执行 step ,看下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
General Purpose Registers:
       rax = 0x0000000100000e20  a.out`main
       rbx = 0x0000000000000000
       rcx = 0x00007ffeefbff5a8
       rdx = 0x00007ffeefbff3a8
       rdi = 0x0000000000000001
       rsi = 0x00007ffeefbff398
       rbp = 0x00007ffeefbff320 // rbp的值更新了,目前和 rsp 相同
       rsp = 0x00007ffeefbff320
        r8 = 0x0000000000000000
        r9 = 0x0000000000000000
       r10 = 0x0000000000000000
       r11 = 0x0000000000000000
       r12 = 0x0000000000000000
       r13 = 0x0000000000000000
       r14 = 0x0000000000000000
       r15 = 0x0000000000000000
       rip = 0x0000000100000f64  a.out`empty + 4
    rflags = 0x0000000000000206
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

以上就是一个空函数调用时,CPU跳转新函数和新栈帧开辟的逻辑。因为 empty 函数中没有任何实现,所以接着会马上返回。

popq %rbp & retq

所有函数的汇编指令也都是有这两条指令作为结尾,这两条指令的含义是:取回暂存的 rbp 指针并恢复 rbp 寄存器的值,然后取出暂存的返回指令的地址,并跳转执行。

  1. popq %rbp: 取回暂存的 rbp 指针并恢复 rbp 寄存器的值。
  2. 取出暂存的返回指令的地址,并跳转执行。

第一步验证:popq %rbp。看下执行这一步之前,寄存器和内存的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(lldb) register read
General Purpose Registers:
       rax = 0x0000000100000e20  a.out`main
       rbx = 0x0000000000000000
       rcx = 0x00007ffeefbff5a8
       rdx = 0x00007ffeefbff3a8
       rdi = 0x0000000000000001
       rsi = 0x00007ffeefbff398
       rbp = 0x00007ffeefbff320
       rsp = 0x00007ffeefbff320
        r8 = 0x0000000000000000
        r9 = 0x0000000000000000
       r10 = 0x0000000000000000
       r11 = 0x0000000000000000
       r12 = 0x0000000000000000
       r13 = 0x0000000000000000
       r14 = 0x0000000000000000
       r15 = 0x0000000000000000
       rip = 0x0000000100000f64  a.out`empty + 4
    rflags = 0x0000000000000206
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

(lldb) memory read --size 4 --format x --count 16 0x00007ffeefbff310
0x7ffeefbff310: 0x00000000 0x00000000 0xefbff398 0x00007ffe
0x7ffeefbff320: 0xefbff370 0x00007ffe 0x00000e34 0x00000001
0x7ffeefbff330: 0x00000001 0x0000000e 0x00000000 0x00000000
0x7ffeefbff340: 0x00000000 0x00000000 0x00000000 0x00000000

当前栈顶地址是:rsp = 0x00007ffeefbff320,栈顶地址存储的值是:0xefbff370 0x00007ffe,也就是我们之前暂存的上一个函数的栈帧的基地址。然后执行 step,看下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(lldb) register read 
General Purpose Registers:
       rax = 0x0000000100000e20  a.out`main
       rbx = 0x0000000000000000
       rcx = 0x00007ffeefbff5a8
       rdx = 0x00007ffeefbff3a8
       rdi = 0x0000000000000001
       rsi = 0x00007ffeefbff398
       rbp = 0x00007ffeefbff370
       rsp = 0x00007ffeefbff328
        r8 = 0x0000000000000000
        r9 = 0x0000000000000000
       r10 = 0x0000000000000000
       r11 = 0x0000000000000000
       r12 = 0x0000000000000000
       r13 = 0x0000000000000000
       r14 = 0x0000000000000000
       r15 = 0x0000000000000000
       rip = 0x0000000100000f65  a.out`empty + 5
    rflags = 0x0000000000000206
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

现在 rbp = 0x00007ffeefbff370,rbp 寄存器恢复了,同时 rsp 增加了 “1”(因为执行了 pop 指令,栈的长度减少了),此时:

第二步执行:retq,这一步会取出当前栈顶的值,即暂存的需要返回到的指令的地址(此时是 0x100000e34)。先看一下当前状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
(lldb) memory read --size 4 --format x --count 16 0x00007ffeefbff310
0x7ffeefbff310: 0x00000000 0x00000000 0xefbff398 0x00007ffe
0x7ffeefbff320: 0xefbff370 0x00007ffe (0x00000e34 0x00000001)
0x7ffeefbff330: 0x00000001 0x0000000e 0x00000000 0x00000000
0x7ffeefbff340: 0x00000000 0x00000000 0x00000000 0x00000000
(lldb) register read
General Purpose Registers:
       rax = 0x0000000100000e20  a.out`main
       rbx = 0x0000000000000000
       rcx = 0x00007ffeefbff5a8
       rdx = 0x00007ffeefbff3a8
       rdi = 0x0000000000000001
       rsi = 0x00007ffeefbff398
       rbp = 0x00007ffeefbff370
       rsp = 0x00007ffeefbff328
        r8 = 0x0000000000000000
        r9 = 0x0000000000000000
       r10 = 0x0000000000000000
       r11 = 0x0000000000000000
       r12 = 0x0000000000000000
       r13 = 0x0000000000000000
       r14 = 0x0000000000000000
       r15 = 0x0000000000000000
       rip = 0x0000000100000f65  a.out`empty + 5
    rflags = 0x0000000000000206
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

rsp = 0x00007ffeefbff328,rsp 执行当前栈顶,当前栈顶存储的值是 (0x00000e34 0x00000001)。然后执行 step, 看下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
(lldb) disassemble 
a.out`empty:
    0x100000f60 <+0>: pushq  %rbp
    0x100000f61 <+1>: movq   %rsp, %rbp
    0x100000f64 <+4>: popq   %rbp
->  0x100000f65 <+5>: retq   
(lldb) step
Process 45685 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x0000000100000e34 a.out`main + 20
a.out`main:
->  0x100000e34 <+20>: movl   $0x3, -0x8(%rbp)
    0x100000e3b <+27>: movl   $0x4, -0xc(%rbp)
    0x100000e42 <+34>: movl   -0x8(%rbp), %edi
    0x100000e45 <+37>: movl   -0xc(%rbp), %esi
Target 0: (a.out) stopped.
(lldb) register read 
General Purpose Registers:
       rax = 0x0000000100000e20  a.out`main
       rbx = 0x0000000000000000
       rcx = 0x00007ffeefbff5a8
       rdx = 0x00007ffeefbff3a8
       rdi = 0x0000000000000001
       rsi = 0x00007ffeefbff398
       rbp = 0x00007ffeefbff370
       rsp = 0x00007ffeefbff330
        r8 = 0x0000000000000000
        r9 = 0x0000000000000000
       r10 = 0x0000000000000000
       r11 = 0x0000000000000000
       r12 = 0x0000000000000000
       r13 = 0x0000000000000000
       r14 = 0x0000000000000000
       r15 = 0x0000000000000000
       rip = 0x0000000100000e34  a.out`main + 20
    rflags = 0x0000000000000206
        cs = 0x000000000000002b
        fs = 0x0000000000000000
        gs = 0x0000000000000000

可以看到,执行完 retq 后,CPU指向了需要返回的指令地址:0x100000e34(rip = 0x0000000100000e34);rsp 继续加“1”。此时 函数的栈帧恢复了之前在main函数的状态:

到此为止,一个空函数的调用和返回过程就结束了,我们再来总结一下整个过程:

函数调用过程

传参 & 返回值

函数调用时,有两种传参方式:

  1. 寄存器够用时,是寄存器传参
  2. 寄存器不够用时,把参数 push 到栈上,用栈内存传参。

返回值:

  1. 利用寄存器 eax 存储返回值,返回给上一级函数。

寄存器传参

我们以 add 函数为例分析:

1
2
3
4
5
6
7
8
9
10
// 测试传2个参数的函数
int i = 3;
int j = 4;
int k = add(i, j);

	int add(int i, int j)
{
	int k = i + j;
	return k;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
a.out`main:
->  0x100000e20 <+0>:   pushq  %rbp
    0x100000e21 <+1>:   movq   %rsp, %rbp
    0x100000e24 <+4>:   subq   $0x40, %rsp
    0x100000e28 <+8>:   movl   $0x0, -0x4(%rbp)
    0x100000e2f <+15>:  callq  0x100000f60
    0x100000e34 <+20>:  movl   $0x3, -0x8(%rbp)      
    0x100000e3b <+27>:  movl   $0x4, -0xc(%rbp)
    0x100000e42 <+34>:  movl   -0x8(%rbp), %edi      // 参数:数字 3 放入 edi 中
    0x100000e45 <+37>:  movl   -0xc(%rbp), %esi      // 参数:数字 4 放入 esi 中
    0x100000e48 <+40>:  callq  0x100000eb0           ; add
    ..........

a.out`add:
    0x100000eb0 <+0>:  pushq  %rbp
    0x100000eb1 <+1>:  movq   %rsp, %rbp
    0x100000eb4 <+4>:  movl   %edi, -0x4(%rbp)      // 从 edi 中取出参数 3
    0x100000eb7 <+7>:  movl   %esi, -0x8(%rbp)      // 从 esi 中取出参数 4
    0x100000eba <+10>: movl   -0x4(%rbp), %esi      
    0x100000ebd <+13>: addl   -0x8(%rbp), %esi      // 计算 3 + 4
    0x100000ec0 <+16>: movl   %esi, -0xc(%rbp)
    0x100000ec3 <+19>: movl   -0xc(%rbp), %eax      // 3 + 4 的结果放入 eax 中,作为返回参数返回
    0x100000ec6 <+22>: popq   %rbp
    0x100000ec7 <+23>: retq   
    0x100000ec8 <+24>: nopl   (%rax,%rax)

栈内存传参

我们以 testParams 函数为例分析:

1
2
3
4
5
6
7
// 测试传多个参数的函数
testParams(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);

void testParams(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j, int k)
{
	return;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
a.out`main:
    ........
    0x100000e50 <+48>:  movl   $0x1, %edi        // 数字1,2,3,4,5,6 分别放入了 6 个寄存器
    0x100000e55 <+53>:  movl   $0x2, %esi
    0x100000e5a <+58>:  movl   $0x3, %edx
    0x100000e5f <+63>:  movl   $0x4, %ecx
    0x100000e64 <+68>:  movl   $0x5, %r8d
    0x100000e6a <+74>:  movl   $0x6, %r9d
    0x100000e70 <+80>:  movl   $0x7, (%rsp)      // 数字7,8,9,10,11 分别放入栈内存(main的栈帧空间)
    0x100000e77 <+87>:  movl   $0x8, 0x8(%rsp)
    0x100000e7f <+95>:  movl   $0x9, 0x10(%rsp)
    0x100000e87 <+103>: movl   $0xa, 0x18(%rsp)
    0x100000e8f <+111>: movl   $0xb, 0x20(%rsp)
    0x100000e97 <+119>: callq  0x100000ed0               ; testParams
    ........

a.out`testParams:
    0x100000ed0 <+0>:  pushq  %rbp
    0x100000ed1 <+1>:  movq   %rsp, %rbp
    0x100000ed4 <+4>:  pushq  %r14
    0x100000ed6 <+6>:  pushq  %rbx
    0x100000ed7 <+7>:  movl   0x30(%rbp), %eax   // 从栈内存中把参数读取出来,并放入testParams的栈帧空间
    0x100000eda <+10>: movl   0x28(%rbp), %r10d
    0x100000ede <+14>: movl   0x20(%rbp), %r11d
    0x100000ee2 <+18>: movl   0x18(%rbp), %ebx
    0x100000ee5 <+21>: movl   0x10(%rbp), %r14d
    0x100000ee9 <+25>: movl   %edi, -0x14(%rbp)
    0x100000eec <+28>: movl   %esi, -0x18(%rbp)
    0x100000eef <+31>: movl   %edx, -0x1c(%rbp)
    0x100000ef2 <+34>: movl   %ecx, -0x20(%rbp)
    0x100000ef5 <+37>: movl   %r8d, -0x24(%rbp)
    0x100000ef9 <+41>: movl   %r9d, -0x28(%rbp)
    0x100000efd <+45>: movl   %eax, -0x2c(%rbp)
    0x100000f00 <+48>: movl   %r10d, -0x30(%rbp)
    0x100000f04 <+52>: movl   %r11d, -0x34(%rbp)
    0x100000f08 <+56>: movl   %ebx, -0x38(%rbp)
    0x100000f0b <+59>: movl   %r14d, -0x3c(%rbp)
    0x100000f0f <+63>: popq   %rbx
    0x100000f10 <+64>: popq   %r14
    0x100000f12 <+66>: popq   %rbp
    0x100000f13 <+67>: retq   
    0x100000f14 <+68>: nopw   %cs:(%rax,%rax)
    0x100000f1e <+78>: nop    

不定参数传参