C程序中局部变量的机器级表示
时间:2010-10-14 来源:ymll
#include <unistd.h> |
本文的分析基于以下平台:
$ 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所示。
.file "test.c" |
图 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版本:
|