先来看一个很简单且典型的例子:
1 #include <stdio.h>
2
3 void pass_byval(int a, int b)
4 {
5 a = 10;
6 b = 20;
7 }
8
9 void pass_byaddr(int* a, int* b)
10 {
11 *a = 100;
12 *b = 200;
13 }
14
15 int main()
16 {
17 int a = 1;
18 int b = 2;
19 pass_byval(a, b);
20 pass_byaddr(&a, &b);
21
22 return 0;
23 }
很明显,在调用 pass_byval 后 a、b 的值是不会改变的;在调用 pass_bayaddr 后 a、b 的值改变了。为什么呢?
在 C 里,有全局变量,局部变量,静态变量(由 static 修饰),动态变量(由 malloc 分配)。这些变量分别存储在程序内存映像的不同区域:
全局变量
:存储在 read-write 段
局部变量
:存储在 user statck 中
静态变量
:存储在 read-write 段
动态变量
:存储在 runtime heap 中
Linux 下程序运行时存储器映像:
---------------------------------- \
| kernel memory | > memory invisible to user code
0xc0000000 |--------------------------------| /
| user stack |
| (created at runtime) |
|--------------------------------|<-- %rsp
| |
|--------------------------------|
| memory-mapped region for |
| shared libraries |
0x40000000 |--------------------------------|
| |
|--------------------------------|<-- brk
| runtime heap |
| (created by malloc) |
|--------------------------------| \
| read-write segment | |
| (.data, .bss) | |
|--------------------------------| > loaded from the executable file
| read-only segment | |
| (.init, .text, .rodata) | |
0x08048000 |--------------------------------| /
| |
0 ----------------------------------
图 1
如开篇的例子中,在刚进入 main 函数时,整个栈是怎样的呢?当调用 pass_byval 和 pass_byaddr 函数时,整个栈又是怎样的呢? 系统为单个过程调用分配的那部分栈叫栈帧
。即每个过程都有它自己的栈帧。栈帧以 %rbp(帧指针) 开始,以 %rsp(栈指针)结束。因为单程序运行到某个过程时,%rsp 是会移动的(当执行一条 push 指令时),所以信息的访问是相对于没有那么频繁移动的 %rbp 的。假如过程 P(caller)调用过程 Q(callee),P 的返回地址会压入栈中,形成 P 的栈帧末尾;由于 Q 的参数是在 P 的栈帧中的,所以在参数传递时会把 Q 的参数从 P 的栈帧中拷到 Q 的栈帧中;单个过程的局部变量都在自己的栈帧中。
用户栈结构(图 1 中 user stack 那部分):
栈底
--------------------- \
| | |
| . | |
| . | > 较早的帧
| . | |
| | |
|-------------------| /
| | \
|-------------------| |
| | |
|-------------------| |
| | > 调用者的帧
|-------------------| |
| | |
|-------------------| |
+4 | return address | /
|-------------------| \
%rbp -->| saved %rbp | |
|-------------------| |
-4 | | |
|-------------------| > 当前帧
| | |
|-------------------| |
%rsp -->| | |
--------------------- /
栈顶 图 2
一个过程可以分为三个部分: 建立部分:初始化栈帧
主体部分:执行过程的实际计算
结尾部分:恢复栈的状态和过程返回
开篇例子的汇编代码:
1: pass_byval:
2: .LFB0:
3: pushq %rbp
4: movq %rsp, %rbp
5: movl %edi, -20(%rbp)
6: movl %esi, -24(%rbp)
7: movl $10, -8(%rbp)
8: movl $20, -4(%rbp)
9: popq %rbp
10: ret
11:
12: pass_byaddr:
13:.LFB1:
14: pushq %rbp
15: movq %rsp, %rbp
16: movq %rdi, -8(%rbp)
17: movq %rsi, -16(%rbp)
18: movq -8(%rbp), %rax
19: movl $100, (%rax)
20: movq -16(%rbp), %rax
21: movl $200, (%rax)
22: popq %rbp
23: ret
24:
25: main:
26: .LFB2:
27: pushq %rbp
28: movq %rsp, %rbp
29: subq $16, %rsp
30: movl $1, -8(%rbp)
31: movl $2, -4(%rbp)
32: movl -4(%rbp), %edx
33: movl -8(%rbp), %eax
34: movl %edx, %esi
35: movl %eax, %edi
36: call pass_byval
37: leaq -4(%rbp), %rdx
38: leaq -8(%rbp), %rax
39: movq %rdx, %rsi
40: movq %rax, %rdi
41: call pass_byaddr
42: movl $0, %eax
43: leave
44: ret
上面的代码最大的区别出现在 34、35 与 37、38 行。传值的情况是使用 movl 指令把 a 的值拷贝到寄存器 %eax;传地址的情况是使用 leaq 指令把 a 的地址拷贝到寄存器 %rax。先看 pass_byval 的汇编代码:
1: pass_byval:
2: .LFB0:
3: pushq %rbp %rbp 入栈
4: movq %rsp, %rbp 此时,%rbp %rsp 都指向同一内存地址
5: movl %edi, -20(%rbp) 将 b 的值 2 拷贝到距 %rbp 偏移量为 -20 的地方
6: movl %esi, -24(%rbp) 将 a 的值 1 拷贝到距 %rbp 偏移量为 -24 的地方
7: movl $10, -8(%rbp) 将 10 拷贝到距 %rbp 偏移量为 -8 的地方
8: movl $20, -4(%rbp) 将 20 拷贝到距 %rbp 偏移量为 -4 的地方
9: popq %rbp 将栈顶的值弹出到 %rbp
10: ret 该过程返回
可以看到改变的只是 pass_byval 中 a, b 的值,而 main 中 a, b 的值并没有改变。再看 pass_byraddr 的汇编代码:
12: pass_byaddr:
13:.LFB1:
14: pushq %rbp 将 %rbp 入栈
15: movq %rsp, %rbp
16: movq %rdi, -8(%rbp) 将 a 的地址拷贝到距 %rbp 偏移量为 -8 的地方
17: movq %rsi, -16(%rbp) 将 b 的地址拷贝到距 %rbp 偏移量为 -16 的地方
18: movq -8(%rbp), %rax
19: movl $100, (%rax) 将 100 拷贝到 a 的地址
20: movq -16(%rbp), %rax
21: movl $200, (%rax) 将 200 拷贝到 b 的地址
22: popq %rbp
23: ret
可以看到 a, b 的值改变了。
用 gdb 来看一下在调用 pass_byval 前,调用 pass_byval 时,pass_byval 返回时,调用 pass_byaddr 时,pass_byaddr 返回时,这几个点上栈的信息:
调用 pass_byval 前
----------------------------
%rbp -->| | 0x7fffffffe3f0
|--------------------------|
b -->| 2 | 0x7fffffffe3ec
|--------------------------|
a -->| 1 | 0x7fffffffe3e8
|--------------------------|
| | 0x7fffffffe3e4
|--------------------------|
%rsp -->| | 0x7fffffffe3e0
|--------------------------|
图 3
调用 pass_byval 时
------------------------------- \
| | 0x7fffffffe3f0 |
|-----------------------------| |
b -->| 2 | 0x7fffffffe3ec |
|-----------------------------| |
a -->| 1 | 0x7fffffffe3e8 |
|-----------------------------| > main 的栈帧
| | 0x7fffffffe3e4 |
|-----------------------------| |
| | 0x7fffffffe3e0 |
|-----------------------------| |
| return address | 0x7fffffffe3dc /
|-----------------------------|
| saved %rbp(0x7fffffffe3f0) | 0x7fffffffe3d8 \
|-----------------------------| |
| | 0x7fffffffe3d4 |
|-----------------------------| |
%rbp, %rsp --> | | 0x7fffffffe3d0 |
|-----------------------------| |
| 20 | 0x7fffffffe3cc |
|-----------------------------| > pass_byval 的栈帧
| 10 | 0x7fffffffe3c8 |
|-----------------------------| |
| . | |
| . | |
|-----------------------------| |
| 1 | 0x7fffffffe3bc |
|-----------------------------| |
| 2 | 0x7fffffffe3b8 |
|-----------------------------| /
图 4
当 pass_byval 返回时,%rbp、%rsp 如图 3 所示;而 %rsp 后面的栈空间依然如图 4 所示,因为此时还没有用到它们,但你不保证以后它们不会被修改,这就是为什么在过程中返回局部变量的地址是不合法的。
调用 pass_byaddr 时:
------------------------------- \
| | 0x7fffffffe3f0 |
|-----------------------------| |
b -->| 200 | 0x7fffffffe3ec |
|-----------------------------| |
a -->| 100 | 0x7fffffffe3e8 |
|-----------------------------| > main 的栈帧
| | 0x7fffffffe3e4 |
|-----------------------------| |
| | 0x7fffffffe3e0 |
|-----------------------------| |
| return address | 0x7fffffffe3dc /
|-----------------------------|
| saved %rbp(0x7fffffffe3f0) | 0x7fffffffe3d8 \
|-----------------------------| |
| | 0x7fffffffe3d4 |
|-----------------------------| |
%rbp, %rsp --> | | 0x7fffffffe3d0 |
|-----------------------------| > pass_byaddr 的栈帧
| | 0x7fffffffe3cc |
|-----------------------------| |
| | 0x7fffffffe3c8 |
|-----------------------------| /
| . |
| . |
|-----------------------------|
| 1 | 0x7fffffffe3bc
|-----------------------------|
| 2 | 0x7fffffffe3b8
|-----------------------------|
图 5
当 pass_byaddr 返回时,%rbp、%rsp 如图 3 所示;而 %rsp 后面的栈空间有些改变了,有些则没有改变。从 pass_byval 返回后到 pass_byaddr 返回地址 0x7fffffffe3bc 和 0x7fffffffe3b8 的内容一直都没有改变。
学指针的时候,老师告诉我们,函数调用时要改变某个参数的值,要把它的地址传进去,也就是传某个变量的指针进去。那么请看看以下的代码会打印些什么呢:
void foo(char* p)
{
strcpy(p, "i am foo");
}
void test_foo()
{
char str[128];
foo(str);
printf("str = %s\n", str);
}
void bar(char* p)
{
p = malloc(128);
}
void test_bar()
{
char* ptr = NULL;
bar(ptr);
strcpy(ptr, "i am bar");
printf("ptr = %s\n", ptr);
}
第一个 printf 当然能打印出 “i am foo”,因为在 test_foo 的栈中,已经为数组 str 分配好空间了,所以当调用 foo(str) 时,是把数组 str 的地址传到了 foo 中;而在 test_bar 栈中,指针 ptr 如下:
---------------------
| |
|-------------------|
0x7fffbcf0 | ptr (0) |
|-------------------|
| |
---------------------
当调用 bar(ptr) 时,实际上是把 ptr 的值传到了 bar 中,而非 ptr 的地址。所以到了 bar 里面执行 malloc 后,改变的只是 bar 里面的 ptr,而 test_bar 中的 ptr 的值并没有改变,还是空指针。然后继续执行 strcpy。然后就 segmentation fault 了。所以要让指针 ptr 指向 malloc 分配的空间,就必须把 ptr 的地址传给 bar(&ptr),那么函数 bar 就要改成如下形式了:
void bar(char** p);