理解.bss section原理

Last Updated: 2023-06-26 09:26:12 Monday

-- TOC --

可执行ELF文件的.bss section仅存放未初始化的全局变量和静态变量的总大小,还有这个section的虚拟地址!指令中,已经计算出了存放在.bss中的变量的地址。

将section翻译为段,那么segment翻译成什么呢?因此很多时候,我直接用英文了。

所谓未初始化,就是全局变量或静态变量,在源代码中没有赋初值,这些变量在程序被加载的时候,OS保证将他们初始化为0。如果变量(全局或静态)在源代码中初始化为0,也有可能被编译器认定为没有初始化,计入.bss段以节省硬盘空间。

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 --