文章详情

  • 游戏榜单
  • 软件榜单
关闭导航
热搜榜
热门下载
热门标签
php爱好者> php文档>C程序中局部变量的机器级表示

C程序中局部变量的机器级表示

时间:2010-10-14  来源:ymll

某日在 chinaunix 闲逛时,遇到一个贴子,楼主贴出了图1所示这样一段代码。程序运行的结果是打印 10 个相同的地址,楼主的问题是,s 应该属于块里面,所以循环体每一次运行都应该重新分配地址,因此程序应该打印 10 个不同的地址,本文将从机器级代码的层次对本问题进行解释。

#include <unistd.h>

 struct mystruct {
         int i;
 };

 int main()
 {
         int i;
        
         for (i = 0; i < 10; i++) {
                 struct mystruct s;
                
                 s.i = i;
                 printf("%p\n", &s);
         }
        
         return 0;
 }

图1 C源代码

本文的分析基于以下平台:
$ uname -a
Linux laptop 2.6.32-23-generic #37-Ubuntu SMP Fri Jun 11 08:03:28 UTC 2010 x86 64 GNU/Linux
$ gcc --version
gcc (Ubuntu 4.4.3-4ubuntu5) 4.4.3
Copyright (C) 2009 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

我们知道,一个典型的的 C 程序运行于 unix-like 机器上时,其进程中的内存布局如图2所示。由
图可知,栈空间是向低地址扩展的,main 函数的局部变量 i,s 等就存储在栈空间中。为了对 s 的生命周期一探究竟,可以使用 “gcc -S test.c” 来生成汇编代码,得到的输出文件如图3所示。
图2 典型的 C 程序内存布局




    .file    "test.c"
    .section    .rodata
.LC0:
    .string    "%p\n"
    .text
.globl main
    .type    main, @function
main:
.LFB0:
    .cfi_startproc
    pushq    %rbp
    .cfi_def_cfa_offset 16
    movq    %rsp, %rbp
    .cfi_offset 6, -16
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    $0, -4(%rbp)
    jmp    .L2
.L3:
    movl    -4(%rbp), %eax
    movl    %eax, -16(%rbp)
    leaq    -16(%rbp), %rax
    movq    %rax, %rsi
    movl    $.LC0, %edi
    movl    $0, %eax
    call    printf
    addl    $1, -4(%rbp)
.L2:
    cmpl    $9, -4(%rbp)
    jle    .L3
    movl    $0, %eax
    leave
    ret
    .cfi_endproc
.LFE0:
    .size    main, .-main
    .ident    "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

 图 3: 图1对应的汇编代码
   图3代码中,第 11,13 行建立 main 函数的栈帧,程序运行到这里后,%rbp 指向 main 函数帧起始地址,并且此时 main 函数栈帧为空。第 16 行在 main 函数栈帧中分配了 16 个字节,第 17 行的运行结果相当于将 i 存储在栈帧起始地址减 4 的位址并且将其初始化为 0。此时的 main 函数栈帧如图4。


图 4: 汇编代码执行到第 17 行时的栈帧结构图

   接下来,程序在第 18 行跳转到循环体的判断条件(第 29 行),第 20 行至 27 行为循环体。在循环体中,第 20 行,21 行通过%eax 作为中介,将 i 的值(注意 i 存储在 main 栈帧起始地址减 4 的位地址,见图4)赋给某一个变量,而该变量的地址为%rbp(也就是帧起始地址)减 16。对照图1中的 C 代码,可以猜测该变量为 s.i。程序第 22 行证实了我们的猜测,这条指令是取前面这个变量的地址。上面的分析中,我们不加区分的对待&s 与&s.i,这是因为在 C 中,结构体对象的第一个成员地址与结构体对象的地址是一样的。
   程序第 26 行调用系统函数 printf,第 27 行将 i 加 1,第 29 行执行循环的条件判断,第 30 行根据判断的结果产生相应的跳转,第 31 行准备函数返回值,第 32 行释放 main 函数栈帧所占用的内存,第 33 行 main 函数返回。
   通过以上分析可知,第 16 行分配了 16 个字节以后,一直到 main 函数返回前(第 32 行),指向栈顶的指针(寄存器%rsp)都没有变化,因此在栈内也就没有内存的分配与释放,从而使图1 中的代码编译后打印相同的地址。
  至于导致上述结果(块中的局部变量在进入块之前分配),我们可以认为是编译器优化的结果。但是,如果程序每一轮循环都为 s 重新分配和释放内存,对照图4可知,程序将在进入循环体时,下移栈指针(即将%rsp 减某固定值),退出循环体之前,栈指针回到初始值,可以想象,这样运行的结果,程序仍将打印相同的地址值。由此可见,局部变量的生命期更多的是一种编译期行为(在生成汇编代码之前),其作用是让编译器帮助程序员检察代码的合法性。当然,上面的分析仅限于 linux 下的 gcc 平台,其他平台可以同理分析,但不一定有相同的结果。
 
  如果你想知道图3中第 16 行为什么分配了 16 个字节(大于 i 和 s 所需的内存对不对?),而不是 8 个字节,请不要问我,因为我也想知道。如果您知道答案,请一定告诉我,在下的邮件[email protected],谢谢^_^

最后,附上本文的pdf版本:
文件: localVar.pdf
大小: 127KB
下载: 下载



排行榜 更多 +
辰域智控app

辰域智控app

系统工具 下载
网医联盟app

网医联盟app

运动健身 下载
汇丰汇选App

汇丰汇选App

金融理财 下载