变量的存储布局
#include <stdio.h>
const int A = 10;
int a = 20;
static int b = 30;
int c;
int main(void)
{
static int a = 40;
char b[] = "Hello world";
register int c = 50;
printf("Hello world %d\n", c);
return 0;
}
我们在全局作用域和main函数的局部作用域各定义了一些变量,并且引入一些新的关键字const、 static、 register来修饰变量,那么这些变量的存储空间是怎么分配的呢?我们编译之后用readelf命令看它的符号表,了解各变量的地址分布。注意在下面的清单中我把符号表按地址从低到高的顺序重新排列了,并且只截取我们关心的那几行。
gcc main.c -g
$ readelf -a a.out
...
68: 08048540 4 OBJECT GLOBAL DEFAULT 15 A
69: 0804a018 4 OBJECT GLOBAL DEFAULT 23 a
52: 0804a01c 4 OBJECT LOCAL DEFAULT 23 b
53: 0804a020 4 OBJECT LOCAL DEFAULT 23 a.1589
81: 0804a02c 4 OBJECT GLOBAL DEFAULT 24 c
...
变量A用const修饰,表示A是只读的,不可修改,它被分配的地址是0x8048540,从readelf的输出可以看到这个地址位于.rodata段:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg
Lk Inf Al
...
[13] .text PROGBITS 08048360 000360 0001bc 00
AX 0 0 16
...
[15] .rodata PROGBITS 08048538 000538 00001c 00
A 0 0 4
...
[23] .data PROGBITS 0804a010 001010 000014 00
WA 0 0 4
[24] .bss NOBITS 0804a024 001024 00000c 00 WA
0 0 4
...
它在文件中的地址是0x538~0x554,我们用hexdump命令看看这个段的内容:
hexdump -C a.out
...
00000530 5c fe ff ff 59 5b c9 c3 03 00 00 00 01 00 02 00
|\...Y[..........|
00000540 0a 00 00 00 48 65 6c 6c 6f 20 77 6f 72 6c 64 20
|....Hello world |
00000550 25 64 0a 00 00 00 00 00 00 00 00 00 00 00 00 00
|%d..............|
...
其中0x540地址处的0a 00 00 00就是变量A。我们还看到程序中的字符串字面值"Hello world
%d\n"分配在.rodata段的末尾,字符串字面值是只读的,相当于在全局作用域定义了一个const数组:
const char helloworld[] = {'H', 'e', 'l', 'l', 'o', ' ','w', 'o', 'r', 'l', 'd', ' ', '%', 'd', '\n','\0'};
程序加载运行时, .rodata段和.text段通常合并到一个Segment中,操作系统将这个Segment的页面只读保护起来,防止意外的改写。这一点从readelf的输出也可以看出来:
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .gnu.hash .dynsym .dynstr
.gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini
.rodata .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag
06
07 .ctors .dtors .jcr .dynamic .got
注意,像A这种const变量在定义时必须初始化。因为只有初始化时才有机会给它一个值,一旦定义之后就不能再改写了,也就是不能再赋值了。
从上面readelf的输出可以看到.data段从地址0x804a010开始,长度是0x14,也就是到地
址0x804a024结束。在.data段中有三个变量, a, b和a.1589。
a是一个GLOBAL的符号,而b被static关键字修饰了,导致它成为一个LOCAL的符号,所以static在这里的作用是声明b这个符号为LOCAL的,不被链接器处理,在下一章我们会看到,如果把多个目标文件链接在一起, LOCAL的符号只能在某一个目标文件中定义和使用,而不能定义在一个目标文件中却在另一个目标文件中使用。一个函数定义前面也可以用 修饰,表示这个函数名符号是LOCAL的。
还有一个a.1589是什么呢?它就是main函数中的static int a。函数中的static变量不同于以前我们讲的局部变量,它并不是在调用函数时分配,在函数返回时释放,而是像全局变量一样静态分配,所以用“static”(静态)这个词。另一方面,函数中的static变量的作用域和以前讲的局部变量一样,只在函数中起作用,比如main函数中的a这个变量名只在main函数中起作用,在别的函数中说变量a就不是指它了,所以编译器给它的符号名加了一个后缀,变成a.1589,以便和全局变量a以及其它函数的变量a区分开。
.bss段从地址0x804a024开始(紧挨着.data段),长度为0xc,也就是到地址0x804a030结束。变量c位于这个段。从上面的readelf输出可以看到, .data和.bss在加载时合并到一个Segment中,这个Segment是可读可写的。 .bss段和.data段的不同之处在于,.bss段在文件中不占存储空间,在加载时这个段用0填充。所以全局变量如果不初始化则初值为0,同理可以推断, static变量(不管是函数里的还是函数外的)如果不初始化则初值也是0,也分配在.bss段。
现在还剩下函数中的b和c这两个变量没有分析。上一节我们讲过函数的参数和局部变量是分配在栈上的, b是数组也一样,也是分配在栈上的,我们看main函数的反汇编代码:
$ objdump -dS a.out
...
char b[]="Hello world";
8048430: c7 45 ec 48 65 6c 6c movl $0x6c6c6548,-
0x14(%ebp)
8048437: c7 45 f0 6f 20 77 6f movl $0x6f77206f,-
0x10(%ebp)
804843e: c7 45 f4 72 6c 64 00 movl $0x646c72,-0xc(%ebp)
register int c = 50;
8048445: b8 32 00 00 00 mov $0x32,%eax
printf("Hello world %d\n", c);
804844a: 89 44 24 04 mov %eax,0x4(%esp)
804844e: c7 04 24 44 85 04 08 movl $0x8048544,(%esp)
8048455: e8 e6 fe ff ff call 8048340 <printf@plt>
...
可见,给b初始化用的这个字符串"Hello world"并没有分配在.rodata段,而是直接写在指令里了,通过三条movl指令把12个字节写到栈上,这就是b的存储空间,如下图所示。
注意,虽然栈是从高地址向低地址增长的,但数组总是从低地址向高地址排列的,按从低地址到高地址的顺序依次是b[0]、 b[1]、 b[2]……这样,数组元素b[n]的地址 = 数组的基地址(b做右值就表示这个基地址) + n × 每个元素的字节数当n=0时,元素b[0]的地址就是数组的基地址,因此数组下标要从0开始而不是从1开始。
变量c并没有在栈上分配存储空间,而是直接存在eax寄存器里,后面调用printf也是直接从eax寄存器里取出c的值当参数压栈,这就是register关键字的作用,指示编译器尽可能分配一个寄存器来存储这个变量。我们还看到调用printf时对于"Hello world %d\n"这个参数压栈的是它在.rodata段中的首地址,而不是把整个字符串压栈,所以字符串在使用时可以看作数组名,如果做右值则表示数组首元素的地址(或者说指向数组首元素的指针)。