Why does data/BSS size change in these different cases?

Viewed 527

I'm learning about virtual memory management and memory allocation of process. And do some experiment about it. There are some confusing points as below:

case1

#include <iostream>

int main() {
    return 0;
}

After compile the code and run size with the binary and got the following output:

   text    data     bss     dec     hex filename
   1985     640       8    2633     a49 main

case2: Change the code to:

#include <iostream>

int global;

int main() {
    return 0;
}

and rebuild it and the size output is:

   text    data     bss     dec     hex filename
   1985     640      16    2641     a51 main

Note: data memory part is not changed, but bss changed from 8 to 16. This result make sense to me, since int global define a uninitialized global variable.

case3: Then I change the code to:

#include <iostream>

int global = 5;

int main() {
    return 0;
}

I initialized the global variable. And analyze the binary again:

   text    data     bss     dec     hex filename
   1985     644       4    2633     a49 main

This time, the change doen't make sense to me. Compared to case1, why data part increase by 4 bytes and bss part decrease by 4?

2 Answers

The discrepancy is caused by the linker, and how it decides to layout the binary. The G++ default linker on x86 (GNU ld).

To see the difference, let's compile your test cases in three different ways:

$ for x in *.cpp ; do g++ -g3 $x -o $x.ld.exe ; done                  # GNU ld
$ for x in *.cpp ; do g++ -g3 $x -o $x.gold.exe -fuse-ld=gold ; done  # gold
$ for x in *.cpp ; do g++ -g3 $x -o $x.gold.exe -fuse-ld=lld ; done   # lld

Now, the sizes are as follows:

   text    data     bss     dec     hex filename
   1873     656       8    2537     9e9 test1.cpp.ld.exe
   1873     656      16    2545     9f1 test2.cpp.ld.exe
   1873     660       4    2537     9e9 test3.cpp.ld.exe

   text    data     bss     dec     hex filename
   1877     656       2    2535     9e7 test1.cpp.gold.exe
   1877     656       9    2542     9ee test2.cpp.gold.exe
   1877     660       2    2539     9eb test3.cpp.gold.exe

   text    data     bss     dec     hex filename
   1817     576       2    2395     95b test1.cpp.lld.exe
   1817     576       9    2402     962 test2.cpp.lld.exe
   1817     580       2    2399     95f test3.cpp.lld.exe

The behavior of the bss section can be explained as follows:

There are two or three variables to store in here (completed.0, possibly global and std::__ioinit). The two that are always there, have a size and alignment of 1 byte each. The global that is only there in case 2, has size and alignment of 4 bytes. This is why both gold and lld require 2 bytes for the bss section in cases 1 and 3. For case 2, the layout is such that global is stored in between the two single-byte values, which requires an additional 3 bytes of padding between the first 1-byte value and global.

Generally speaking, GNU ld does something similar, except it uses the bss section to also provide inter-section padding. In case 1 and 3, only 2 bytes of the bss section are actually used, while case 2 uses 9 bytes of the bss section. Since data grows by 4 bytes in case 3 and bss is layouted after data, 4 of these padding bytes are removed to keep the offset of the following sections aligned. This is also why the size of the bss section grows by 8 bytes rather than 4 upon introducing global: GNU ld ensures that the end of the section remains 8-byte aligned.

The much shorter version is: .bss section is mapped to the zero page with copy-on-write — any variable that is not explicitly initialized, will be added to .bss, which does not actually exist on the ELF file, and is only going to be allocated when changing the content of one of its variables (I'm oversimplifying — the allocation happen per-page so there's more details with that).

When you give it a default value, it can't be mapped to the zero page: its value needs to be present in the ELF file itself, and mapped into memory (again, as copy-on-write, so if you then change the value, a new page gets allocated with the modified value of your variable). If you changed that to const int global = 5; it would have been added to .rodata and you would have seen the size of .text increase instead.

I've blogged about the size command in the past: https://flameeyes.blog/2008/12/01/for-elves-size-matters/

Related