[COFF] [TUHS] Re: What does `ld` stand for?

Dan Cross via COFF coff at tuhs.org
Tue Oct 27 01:52:43 AEST 2026


On Sun, Oct 25, 2026 at 10:55 PM Warner Losh <imp at bsdimp.com> wrote:
> On Sun, Oct 25, 2026, 8:44 PM Dan Cross via COFF <coff at tuhs.org> wrote:
>> > [snip]
>> >         define N 1<<15
>> >         static struct { bool h:1;  bool v:1; } hv[N][N] = {{1,1}};
>> >
>> > Apparently there is no way to tell ELF to place a given data section
>> > next to an uninitialized section, so gcc had to instantiate the whole
>> > 1GB array in the object file, when I had imagined it would take at
>> > most 8 bytes plus a few words of metadata.
>>
>> Curiously, ELF has the expressive power to produce a loadable
>> executable object file that could operate in the way you expected at
>> load time, but I think you're right that it probably can't split a
>> single variable across multiple sections at build time.
>>
>> For those who are unfamiliar, ELF divides the world into sections and
>> segments; to a first order approximation, sections are used at build
>> time, consumed by linkers, debuggers, disassemblers, and the like.
>> Segments are used at load time by the OS or program loader. Segments
>> are far simpler than sections, and are defined by a simple data
>> structure called a, "program header."  Among the data a program header
>> includes are load addresses, file offset, type, flags, permissions,
>> and the segment's size.  The last is interesting, because there are
>> _two_ sizes: the "file size" (p_filesz) is the size of the segment's
>> contents in the executable file, while the "memory size" (p_memsz) is
>> the amount of memory the segment uses.  If the program header has
>> `p_memsz > p_filesz` for a loadable ELF segment, then the segment is
>> understood to consist of both initialized data and uninitiated data;
>> this is how ELF defines a .BSS segment; the delta is filed with zeros
>> at load (or CoW's a zero page or whatever).
>>
>> Thus, one could imagine Doug's large array appearing in a dedicated
>> loadable segment, with filesz == 8, and memsz in the giga-range
>> (though I suspect that `p_filesz` might be rounded up to a page size,
>> regardless). But an ELF linker is allowed to rearrange and combine
>> segments; for example, grouping all of the executable text into one
>> segment, while moving all of the BSS spans into a dedicated `.bss`
>> segment with filesz==0; GCC can't know that this won't happen at
>> compile time, so if it sees a single data filled into a large,
>> otherwise uninitialized, array, then it hedges its bets and puts it
>> all into the same segment in its object file.  And of course, there
>> could be multiple such values, straddling the boundary between init
>> and uninit data; but any given segment can only have, since it only
>> has one boundary for it to straddle; without explicit annotations, the
>> compiler can't know what to do with any one of them.
>>
>> However, one _could_ rewrite the ELF binary after linking to juggle
>> segments around to accommodate this, but I doubt that's common, and it
>> wouldn't help compilation.
>
> Two comments. Setting the array = {0} makes it initialized so no split between sections.

No, initialization to `{0}` specifically is a special case; that
remains in the BSS:

```
term% cat big.c
int big[1<<21] = {0};
term% cc -c big.c
term% objdump -x big.o

big.o:     file format elf64-riscv
big.o
architecture: riscv:rv64, flags 0x00000010:
HAS_SYMS
start address 0x0000000000000000

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000000  0000000000000000  0000000000000000  00000040  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .bss          00800000  0000000000000000  0000000000000000  00000040  2**2
                  ALLOC
  2 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, READONLY
  3 .riscv.attributes 00000068  0000000000000000  0000000000000000
00000040  2**0
                  CONTENTS, READONLY
SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 big.c
0000000000000000 l       .bss   0000000000000000 $d
0000000000000000 l       .riscv.attributes      0000000000000000 $d
0000000000000000 g     O .bss   0000000000800000 big
```

The problem here is that Doug initialized the first value to non-zero
which forces it into .data (at least as far as GCC is concerned).

```
term% cat big.c
struct {_Bool h:1; _Bool v:1;} big[1<<21] = {{1,1}};
term% cc -c big.c
term% objdump -x big.o

big.o:     file format elf64-riscv
big.o
architecture: riscv:rv64, flags 0x00000010:
HAS_SYMS
start address 0x0000000000000000

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000000  0000000000000000  0000000000000000  00000040  2**1
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00200000  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .note.GNU-stack 00000000  0000000000000000  0000000000000000  00200040  2**0
                  CONTENTS, READONLY
  3 .riscv.attributes 00000068  0000000000000000  0000000000000000
00200040  2**0
                  CONTENTS, READONLY
SYMBOL TABLE:
0000000000000000 l    df *ABS*  0000000000000000 big.c
0000000000000000 l       .data  0000000000000000 $d
0000000000000000 l       .riscv.attributes      0000000000000000 $d
0000000000000000 g     O .data  0000000000200000 big
```

We can futz with it, though:

```
term% cc -fdata-sections -c big.c
term% cat big.ld
SECTIONS {
        .data.big ALIGN(4096) : { KEEP(big.o(.data.big)) }
}
INSERT AFTER .bss;
term% cat bigmain.c
#include <stdio.h>
extern unsigned char big[1<<21];  // On this ABI, Doug's struct is a uchar.
int
main(void)
{
        printf("big[0] = %d\n", big[0]);
        printf("big[1] = %d\n", big[1]);
        printf("big[last] = %d\n", big[(1<<21) - 1]);
        return 0;
}
term% cc -v -o big bigmain.o big.o 2>&1 | grep ld
 "/usr/bin/ld" -e __start --eh-frame-hdr -dynamic-linker
/usr/libexec/ld.so -X -o big /usr/lib/crt0.o /usr/lib/crtbegin.o
-L/usr/lib bigmain.o big.o -lcompiler_rt -lc -lcompiler_rt
/usr/lib/crtend.o
term% ld -Tbig.ld -e __start --eh-frame-hdr -dynamic-linker
/usr/libexec/ld.so -X -o big /usr/lib/crt0.o /usr/lib/crtbegin.o -L
/usr/lib bigmain.o big.o -lcompiler_rt -lc -lcompiler_rt
/usr/lib/crtend.o
term% ./big
big[0] = 3
big[1] = 0
big[last] = 0
term%
```
Running `objdump -x` on that executable, I see that the `big` array
has indeed been isolated to its own segment:
```
    PHDR off    0x0000000000000040 vaddr 0x0000000000000040 paddr
0x0000000000000040 align 2**3
         filesz 0x0000000000000310 memsz 0x0000000000000310 flags r--
    ...
    LOAD off    0x0000000000001000 vaddr 0x0000000000004000 paddr
0x0000000000004000 align 2**12
         filesz 0x0000000000200000 memsz 0x0000000000200000 flags rw-
```
Note that `ojbdump` also tells me where the program headers are and
how much space they take; they take 784 bytes, starting at 64 bytes
into the file.  This is a 64-bit binary, so each entry is 56-bytes
long and the file size offset is 32 bytes from the start of the
struct. With some targeted surgery, I can change that to, say 4KiB:

```
    LOAD off    0x0000000000001000 vaddr 0x0000000000004000 paddr
0x0000000000004000 align 2**12
         filesz 0x0000000000001000 memsz 0x0000000000200000 flags rw-
```
It is all zeros from there to the end of the file (I'm ignoring the
RISC-V attributes), so I can truncate it, too:
```
term% ls -l big
-rwxr-xr-x  1 cross  staff  2104960 Oct 26 15:39 big
term% dd if=big of=big.smaller bs=4096 count=2
2+0 records in
2+0 records out
8192 bytes transferred in 0.000 secs (12804761 bytes/sec)
term% ls -l big.smaller
-rwxr-xr-x  1 cross  cross  8192 Oct 26 15:47 big.smaller
term% chmod +x big.smaller
term% ./big.smaller
big[0] = 3
big[1] = 0
big[last] = 0
term%
```
Anyway, I thought that was kind of a fun exercise.

> And there are also flags that control the alignment of the sections (or you can use a custom linker script).

Sure.  Though I would expect most loadable sections to be page-aligned
in the file so that they can be mapped directly into a process's
virtual address space from the page cache. For very small segments,
they may just be copied; I didn't look.

        - Dan C.


More information about the COFF mailing list