C语言中数据段和bss段的区别

43

当通过readelf检查目标文件的反汇编时,我发现databss段包含相同的偏移地址。 data部分将包含初始化的全局和静态变量。BSS将包含未初始化的全局和静态变量。

  1 #include<stdio.h>
  2 
  3 static void display(int i, int* ptr);
  4 
  5 int main(){
  6  int x = 5;
  7  int* xptr = &x;
  8  printf("\n In main() program! \n");
  9  printf("\n x address : 0x%x x value : %d  \n",(unsigned int)&x,x);
 10  printf("\n xptr points to : 0x%x xptr value : %d \n",(unsigned int)xptr,*xptr);
 11  display(x,xptr);
 12  return 0;
 13 }
 14 
 15 void display(int y,int* yptr){
 16  char var[7] = "ABCDEF";
 17  printf("\n In display() function  \n");
 18  printf("\n y value : %d y address : 0x%x  \n",y,(unsigned int)&y);
 19  printf("\n yptr points to : 0x%x yptr value : %d  \n",(unsigned int)yptr,*yptr);
 20 }

输出:

   SSS:~$ size a.out 
   text    data     bss     dec     hex filename
   1311     260       8    1579     62b a.out

在上面的程序中,我没有任何未初始化的数据,但BSS占用了8个字节。为什么会占用8个字节? 当我反汇编对象文件时,

  [ 3] .data             PROGBITS        00000000 000110 000000 00  WA  0   0  4
  [ 4] .bss              NOBITS          00000000 000110 000000 00  WA  0   0  4
  [ 5] .rodata           PROGBITS        00000000 000110 0000cf 00   A  0   0  4
datarodatabss具有相同的偏移地址。这是否意味着rodatadatabss引用相同的地址? 数据段、只读数据段和BSS段是否包含相同地址中的数据值,如果是,如何区分数据段、BSS段和只读数据段?

3
在C语言中,使用%p来打印指针,并将参数转换为(void*)类型。 - Jens
1
我没有任何未初始化的数据,但BSS已经占用了8个字节——链接的库也有数据。事实上,由于您的程序只有局部变量和文字,我预计所有的databss部分都来自库。 - Michael Burr
2个回答

84

.bss段在程序加载到内存时保证全部为零。所以,任何未初始化的全局数据,或者初始化为零的数据都会放置在.bss段。例如:

static int g_myGlobal = 0;     // <--- in .bss section

这里的好处是,.bss节的数据不必包含在磁盘上的ELF文件中(也就是说,文件中没有一个全是零的区域来存放.bss节)。相反,装载器从节头中知道为.bss节分配了多少空间,并在将控制权交给程序之前将其清零。

请注意readelf输出:

[ 3] .data PROGBITS 00000000 000110 000000 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 000110 000000 00 WA 0 0 4

.data 被标记为 PROGBITS。这意味着在 ELF 文件中有程序数据的“位”需要加载器读取到内存中。另一方面,.bss 被标记为 NOBITS,这意味着文件中没有任何需要在加载时读入内存的内容。

// bss.c
static int g_myGlobal = 0;

int main(int argc, char** argv)
{
   return 0;
}

使用 $ gcc -m32 -Xlinker -Map=bss.map -o bss bss.c 进行编译。

使用 $ readelf -S bss 查看部分头。

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
   :
  [13] .text             PROGBITS        080482d0 0002d0 000174 00  AX  0   0 16
   :
  [24] .data             PROGBITS        0804964c 00064c 000004 00  WA  0   0  4
  [25] .bss              NOBITS          08049650 000650 000008 00  WA  0   0  4
   :

现在我们要在符号表中查找变量:$ readelf -s bss | grep g_myGlobal
37: 08049654     4 OBJECT  LOCAL  DEFAULT   25 g_myGlobal

请注意,g_myGlobal被显示为25节的一部分。如果我们回顾一下节标题,我们可以看到25是.bss
回答您真正的问题:

在上面的程序中,我没有任何未初始化的数据,但BSS占用了8个字节。为什么它会占用8个字节?

继续使用我的例子,我们寻找第25节中的任何符号:
$ readelf -s bss | grep 25
     9: 0804825c     0 SECTION LOCAL  DEFAULT    9 
    25: 08049650     0 SECTION LOCAL  DEFAULT   25 
    32: 08049650     1 OBJECT  LOCAL  DEFAULT   25 completed.5745
    37: 08049654     4 OBJECT  LOCAL  DEFAULT   25 g_myGlobal

第三列是大小。我们看到了我们期望的4字节的g_myGlobal和这个1字节的completed.5745。这可能是来自C运行时初始化中某处的函数内静态变量 - 记住,在调用main()之前会发生很多“事情”。
4+1=5字节。但是,如果我们回顾一下.bss部分的头部,我们会看到最后一列Al是4。这是部分对齐,意味着加载时,这个部分将始终是4字节的倍数。从5开始的下一个倍数是8,这就是为什么.bss部分是8字节的原因。
另外,我们可以查看链接器生成的映射文件,以查看哪些目标文件被放置在最终输出的位置。
.bss            0x0000000008049650        0x8
 *(.dynbss)
 .dynbss        0x0000000000000000        0x0 /usr/lib/gcc/x86_64-redhat-linux/4.7.2/../../../../lib/crt1.o
 *(.bss .bss.* .gnu.linkonce.b.*)
 .bss           0x0000000008049650        0x0 /usr/lib/gcc/x86_64-redhat-linux/4.7.2/../../../../lib/crt1.o
 .bss           0x0000000008049650        0x0 /usr/lib/gcc/x86_64-redhat-linux/4.7.2/../../../../lib/crti.o
 .bss           0x0000000008049650        0x1 /usr/lib/gcc/x86_64-redhat-linux/4.7.2/32/crtbegin.o
 .bss           0x0000000008049654        0x4 /tmp/ccKF6q1g.o
 .bss           0x0000000008049658        0x0 /usr/lib/libc_nonshared.a(elf-init.oS)
 .bss           0x0000000008049658        0x0 /usr/lib/gcc/x86_64-redhat-linux/4.7.2/32/crtend.o
 .bss           0x0000000008049658        0x0 /usr/lib/gcc/x86_64-redhat-linux/4.7.2/../../../../lib/crtn.o

再次强调,第三列是大小。

我们可以看到,4个字节的.bss来自于/tmp/ccKF6q1g.o。在这个简单的例子中,我们知道这是从编译我们的bss.c文件生成的临时对象文件。另外的1个字节来自于crtbegin.o,它是C运行时的一部分。


最后,因为我们知道这个1个字节的神秘bss变量来自于crtbegin.o,它的名字为completed.xxxx,它的真实名称是completed,很可能是某个函数内的静态变量。查看crtstuff.c,我们找到了罪魁祸首:一个位于__do_global_dtors_aux()函数内部的static _Bool completed变量。


1
我不会初始化示例 static int g_myGlobal; - 确保它进入 .bss - 并且应该在代码中使用,否则编译器可能会通过完全删除它来进行优化。 - Basile Starynkevitch
更有可能的是,.bss 来自于包含的标准库中的某个内部变量,很可能是来自 printf。这就是为什么 printf 等函数不是线程安全的原因。 - Lundin
1
@Lundin printf将成为libc的一部分,它是动态链接的 - 因此其bss不会出现在此可执行文件中。此外,我想不到任何原因特别是printf会保留任何状态。 - Jonathon Reinhart
1
@Lundin 此外,我明确指出另外的1个字节来自于 crtbegin.o。一些最小化的 CRT 启动代码总是要静态链接的。 - Jonathon Reinhart
@ Jonathon Reinhart:stdoutprintf 使用,显然具有一些状态。 - Basile Starynkevitch
1
也许我有点迂腐,但那是标准输出,不是 printf。无论如何,这都是 libc,所以没关系。 - Jonathon Reinhart

5

根据定义,bss 段在程序启动时占用一定的内存空间,但不需要任何磁盘空间。您需要定义一些变量来填充它,因此请尝试:

int bigvar_in_bss[16300];
int var_in_data[5] = {1,2,3,4,5};

你的简单程序可能没有任何数据在.bss中,而共享库(如libc.so)可能会有“它们自己的.bss”。
文件偏移和内存地址不容易相关联。
阅读更多关于ELF规范的内容,还可以使用/proc/(例如cat /proc/self/maps将显示运行该命令的cat进程的地址空间)。 还需阅读proc(5)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接