函数调用中栈的使用情况


FEB 28TH, 2012 | COMMENTS

先来看一个很简单且典型的例子:

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);
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值