Last Updated: 2023-06-26 09:26:12 Monday
-- TOC --
可执行ELF文件的.bss
section仅存放未初始化的全局变量和静态变量的总大小,还有这个section的虚拟地址!指令中,已经计算出了存放在.bss中的变量的地址。
将section翻译为段,那么segment翻译成什么呢?因此很多时候,我直接用英文了。
所谓未初始化,就是全局变量或静态变量,在源代码中没有赋初值,这些变量在程序被加载的时候,OS保证将他们初始化为0。如果变量(全局或静态)在源代码中初始化为0,也有可能被编译器认定为没有初始化,计入.bss段以节省硬盘空间。
.bss段只存放一个总的大小,这个大小的空间在程序被OS加载的时候保证初始化为0,这样做的目的是为了减少目标文件的大小,减少磁盘占用,也加快程序加载速度。试想一个几百兆的数组,如果代码初始化不为0,光这个数组占用的文件大小就有几百兆,全部读入内存会非常耗时。因此,如果逻辑上允许,就不要对这样大的数据结构进行初始化,在程序加载的时候,统一初始化为0,然后代码在使用此数据结构的时候,再做初始化处理。
.bss不占据实际的磁盘空间,只有一个section,在section中记录了大小和起始虚拟地址。
size命令看到的bss段大小,其实是bss section的值(section header中记录的size)。size命令统计的其实是程序加载到内存时,.text,.data和.bss占用的大小(比实际需要的size小一些,还有.rodata等也需要load进内存)。
下面通过一个case,我们来分析一下.bss section涉及的所有细节。使用的代码如下:
#include <stdio.h>
int a;
static int b;
int c = 3;
void f(){
static int d;
static int e = 5;
printf("%d %d\n", d, e);
}
int main(){
printf("%d %d %d\n", a,b,c);
f();
return 0;
}
编译后,看看.bss section的信息:
$ gcc -c test.c -o test.o
$ readelf -S test.o
...
[ 4] .bss NOBITS 0000000000000000 000000a4
000000000000000c 0000000000000000 WA 0 0 4
...
$ size test.o
text data bss dec hex filename
245 8 12 265 109 test.o
变量a,b,d都会进入.bss,它们有12bytes。
再看一下符号表,链接不能没有符号表:
Symbol table '.symtab' contains 14 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS test.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3 .data
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4 .bss
5: 0000000000000004 4 OBJECT LOCAL DEFAULT 4 b
6: 0000000000000000 0 SECTION LOCAL DEFAULT 5 .rodata
7: 0000000000000004 4 OBJECT LOCAL DEFAULT 3 e.1
8: 0000000000000008 4 OBJECT LOCAL DEFAULT 4 d.0
9: 0000000000000000 4 OBJECT GLOBAL DEFAULT 4 a
10: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 c
11: 0000000000000000 36 FUNC GLOBAL DEFAULT 1 f
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
13: 0000000000000024 56 FUNC GLOBAL DEFAULT 1 main
a,b,d三个变量在Value部分,表示偏移(相对于.bss的偏移,Ndx=4,这就相当于是在分配地址了,当.bss地址确定,这几个变量的位置也就确定),他们的size都是4字节。有这两个信息,就可以定位链接之后这3个变量的地址。链接后:
$ gcc test.c -o test
$ readelf -S test
...
[25] .bss NOBITS 000000000040402c 0000302c
0000000000000014 0000000000000000 WA 0 0 4
...
.bss section的地址为0x40402c,长度变成20了,链接进来其它未初始化的全局变量。
反汇编看看:
$ objdump -M intel -d --disassemble=main test
000000000040114a <main>:
40114a: 55 push rbp
40114b: 48 89 e5 mov rbp,rsp
40114e: 8b 0d d0 2e 00 00 mov ecx,DWORD PTR [rip+0x2ed0] # 404024 <c>
401154: 8b 15 da 2e 00 00 mov edx,DWORD PTR [rip+0x2eda] # 404034 <b>
40115a: 8b 05 d0 2e 00 00 mov eax,DWORD PTR [rip+0x2ed0] # 404030 <a>
401160: 89 c6 mov esi,eax
401162: bf 17 20 40 00 mov edi,0x402017
401167: b8 00 00 00 00 mov eax,0x0
40116c: e8 bf fe ff ff call 401030 <printf@plt>
401171: b8 00 00 00 00 mov eax,0x0
401176: e8 ab ff ff ff call 401126 <f>
40117b: b8 00 00 00 00 mov eax,0x0
401180: 5d pop rbp
401181: c3 ret
$ objdump -M intel -d --disassemble=f test
0000000000401126 <f>:
401126: 55 push rbp
401127: 48 89 e5 mov rbp,rsp
40112a: 8b 15 f8 2e 00 00 mov edx,DWORD PTR [rip+0x2ef8] # 404028 <e.1>
401130: 8b 05 02 2f 00 00 mov eax,DWORD PTR [rip+0x2f02] # 404038 <d.0>
401136: 89 c6 mov esi,eax
401138: bf 10 20 40 00 mov edi,0x402010
40113d: b8 00 00 00 00 mov eax,0x0
401142: e8 e9 fe ff ff call 401030 <printf@plt>
401147: 90 nop
401148: 5d pop rbp
401149: c3 ret
这5个变量的地址如下:
# 404024 <c>
# 404034 <b>
# 404030 <a>
# 404028 <e.1>
# 404038 <d.0>
函数内部的static变量被轻微修饰过,这是因为有可能其它函数内部也定义了同名static变量,这样可以将他们区分开来。
有初始值的变量c和e,它两的地址都小于0x40402c,这说明它两在.data内。而未初始化的变量a,b和d,它们的地址都大于0x40402c,这说明它们在.bss内。(.bss紧跟在.data后面)
.bss section中数据的空间分配和初始化到底是如何进行的?这是最困惑的地方...
程序加载是按segment走,segment由多个地址连续,权限相同section组合而成,这样设计是为了节省内存,如果section的size不大,多个section可以共享page。我们来看看segment信息:
$ readelf -l test
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
...
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000510 0x0000000000000510 R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000191 0x0000000000000191 R E 0x1000
LOAD 0x0000000000002000 0x0000000000402000 0x0000000000402000
0x0000000000000104 0x0000000000000104 R 0x1000
LOAD 0x0000000000002e10 0x0000000000403e10 0x0000000000403e10
0x000000000000021c 0x0000000000000230 RW 0x1000
...
Section to Segment mapping:
Segment Sections...
...
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
...
有4个load,最后那个RW的segment,包含了从.init_array到.bss。因此这个segment的起始地址为0x403e10,它刚好是.init_array的地址。因此,这个segment的size是0x230,它刚好是所有包含的section的size总和。OK,这里就是魔鬼细节了。.bss的size被加到了这个segment的size中去了,这就是.bss的空间分配了!当这个segment被load进内存的某个page中去的过程中,OS要对这个page清零(凭直觉猜的,或者按照segment的size清零,或者只对.bss那一部分清零...),然后按序将这几个section的内容copy进入page,因为.bss在最后面(它只能在最后,因为这个section只有header,没有内容),这相当于.bss所需要的那块地方,被清零了!这部分的处理,还是很精妙的...
本文链接:https://cs.pynote.net/sf/c/cdm/202302252/
-- EOF --
-- MORE --