Search code examples
gccldstatic-linkingelfthread-local-storage

gcc / ld: overlapping sections (.tbss, .init_array) in statically-linked ELF binary


I'm compiling a very simple hello-world one-liner statically on Debian 7 system on x86_64 machine with gcc version 4.8.2 (Debian 4.8.2-21):

gcc test.c -static -o test

and I get an executable ELF file that includes the following sections:

[17] .tdata            PROGBITS         00000000006b4000  000b4000
     0000000000000020  0000000000000000 WAT       0     0     8
[18] .tbss             NOBITS           00000000006b4020  000b4020
     0000000000000030  0000000000000000 WAT       0     0     8
[19] .init_array       INIT_ARRAY       00000000006b4020  000b4020
     0000000000000010  0000000000000000  WA       0     0     8
[20] .fini_array       FINI_ARRAY       00000000006b4030  000b4030
     0000000000000010  0000000000000000  WA       0     0     8
[21] .jcr              PROGBITS         00000000006b4040  000b4040
     0000000000000008  0000000000000000  WA       0     0     8
[22] .data.rel.ro      PROGBITS         00000000006b4060  000b4060
     00000000000000e4  0000000000000000  WA       0     0     32

Note that .tbss section is allocated at addresses 0x6b4020..0x6b4050 (0x30 bytes) and it intersects with allocation of .init_array section at 0x6b4020..0x6b4030 (0x10 bytes), .fini_array section at 0x6b4030..0x6b4040 (0x10 bytes) and with .jcr section at 0x6b4040..0x6b4048 (8 bytes).

Note it does not intersect with the following sections, for example, .data.rel.ro, but that's probably because .data.rel.ro alignment is 32 and thus it can't be placed any earlier than 0x6b4060.

The resulting file runs ok, but I still don't exactly get how it works. From what I read in glibc documentation, .tbss is a just .bss section for thread local storage (i.e. allocated memory scratch space, not really mapped in physical file). Is it that .tbss section is so special that it can overlap other sections? Are .init_array, .fini_array and .jcr are so useless (for example, they are not needed anymore then TLS-related code runs), so they can be overwritten by bss? Or is it some sort of a bug?

Basically, what do I get to read and write if I'll try to read address 0x6b4020 in my application? .tbss contents or .init_array pointers? Why?


Solution

  • The virtual address of .tbss is meaningless as that section only serves as a template for the TLS storage as allocated by the threading implementation in GLIBC.

    The way this virtual address comes into place is that .tbss follows .tbdata in the default linker script:

    ...
    .gcc_except_table   : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) }
    /* Thread Local Storage sections  */
    .tdata          : { *(.tdata .tdata.* .gnu.linkonce.td.*) }
    .tbss           : { *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) }
    .preinit_array     :
    {
      PROVIDE_HIDDEN (__preinit_array_start = .);
      KEEP (*(.preinit_array))
      PROVIDE_HIDDEN (__preinit_array_end = .);
    }
    .init_array     :
    {
       PROVIDE_HIDDEN (__init_array_start = .);
       KEEP (*(SORT(.init_array.*)))
       KEEP (*(.init_array))
       PROVIDE_HIDDEN (__init_array_end = .);
    }
    ...
    

    therefore its virtual address is simply the virtual address of the preceding section (.tbdata) plus the size of the preceding section (eventually with some padding in order to reach the desired alignment). .init_array (or .preinit_array if present) comes next and its location should be determined the same way, but .tbss is known to be so very special, that it is given a deeply hard-coded treatment inside GNU LD:

    /* .tbss sections effectively have zero size.  */
    if ((os->bfd_section->flags & SEC_HAS_CONTENTS) != 0
        || (os->bfd_section->flags & SEC_THREAD_LOCAL) == 0
        || link_info.relocatable)
      dotdelta = TO_ADDR (os->bfd_section->size);
    else
      dotdelta = 0;    // <----------------
    dot += dotdelta;
    

    .tbss is not relocatable, it has the SEC_THREAD_LOCAL flag set, and it does not have contents (NOBITS), therefore the else branch is taken. In other words, no matter how large the .tbss is, the linker does not advance the location of the section that follows it (also know as "the dot").

    Note also that .tbss sits in a non-loadable ELF segment:

    Program Headers:
      Type           Offset             VirtAddr           PhysAddr
                     FileSiz            MemSiz              Flags  Align
      LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                     0x00000000000b1f24 0x00000000000b1f24  R E    200000
      LOAD           0x00000000000b2000 0x00000000006b2000 0x00000000006b2000
                     0x0000000000002288 0x00000000000174d8  RW     200000
      NOTE           0x0000000000000158 0x0000000000400158 0x0000000000400158
                     0x0000000000000044 0x0000000000000044  R      4
      TLS            0x00000000000b2000 0x00000000006b2000 0x00000000006b2000 <---+
                     0x0000000000000020 0x0000000000000060  R      8              |
      GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000     |
                     0x0000000000000000 0x0000000000000000  RW     8              |
                                                                                  |
     Section to Segment mapping:                                                  |
      Segment Sections...                                                         |
       00     .note.ABI-tag ...                                                   |
       01     .tdata .ctors ...                                                   |
       02     .note.ABI-tag ...                                                   |
       03     .tdata .tbss    <---------------------------------------------------+
       04