From coff at tuhs.org Mon Oct 26 10:26:33 2026 From: coff at tuhs.org (Douglas McIlroy via COFF) Date: Sun, 25 Oct 2026 20:26:33 -0400 Subject: [COFF] [TUHS] Re: What does `ld` stand for? In-Reply-To: <20261025214939.e5znado45xcetpmk@illithid> References: <20261025214939.e5znado45xcetpmk@illithid> Message-ID: Branden wrote on TUHS: > I ... recommend David Salomon's "Assemblers and Loaders". > When I got through the latter's giant chapter on assemblers, > many things fell into place for me. This conjured the memory of a learning experience with ELF, which usually flies under the radar. Gcc took remarkably long to compile a program that included a fragment like this: 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. Doug On Sun, Oct 25, 2026 at 5:50 PM G. Branden Robinson via TUHS wrote: > > At 2026-10-25T10:42:45-0700, christopher fujino via TUHS wrote: > > My friend, taking an OS course was wondering what `ld` stood for. > > After doing an online search, concluded "no one knows." Surely someone > > around here must remember why the linker is called `ld`? Thanks! > > At 2026-10-25T11:08:51-0700, christopher fujino via TUHS wrote: > > It looks like the earliest source for `ld` I can find on TUHS is from > > v2: > > https://www.tuhs.org/cgi-bin/utree.pl?file=V2/cmd/ld1.s > > > > / ld1 -- link editor > > > > Could it stand for "link editor"? > > At 2026-10-25T13:40:04-0600, Warner Losh via TUHS wrote: > > The first edition manual is at > > https://www.tuhs.org/cgi-bin/utree.pl?file=V1/man/man1/ld.1 > > > > And says the same thing. As do later editions. > > In my own education I found myself scandalzied by the meager emphasis > given to linking and loading in the curricula offered to those receiving > university educations in software engineering and/or computing science. > > I suppose this is because there is thought to be no "deep theory" behind > linking and loading techniques, at least not enough to sustain a steady > research program. Coming up with object file formats and linkers and > loaders to produce and consume them is, after all, a "simple matter of > programming". > > But I wonder. We don't seem to have a deep theory of operating systems, > but that discipline does, just barely, sustain a research program. > > Not that industry seems to realize it, however.[1] Once one has > achieved "world domination",[2] little of interest remains except > increasing returns to the dominators' capital. > > I second recommendations of John Levine's book, and also recommend David > Salomon's "Assemblers and Loaders". When I got through the latter's > giant chapter on assemblers, many things fell into place for me. > > Regards, > Branden > > [1] https://microkerneldude.org/2022/02/17/a-story-of-betrayal-csiros-war-on-ts/ > [2] https://www.computing.co.uk/news/1831329/linux-promises-world-domination From coff at tuhs.org Mon Oct 26 12:44:07 2026 From: coff at tuhs.org (Dan Cross via COFF) Date: Sun, 25 Oct 2026 22:44:07 -0400 Subject: [COFF] [TUHS] Re: What does `ld` stand for? In-Reply-To: References: <20261025214939.e5znado45xcetpmk@illithid> Message-ID: On Sun, Oct 25, 2026 at 8:38 PM Douglas McIlroy via COFF wrote: > Branden wrote on TUHS: > > > I ... recommend David Salomon's "Assemblers and Loaders". > > When I got through the latter's giant chapter on assemblers, > > many things fell into place for me. > > This conjured the memory of a learning experience with ELF, > which usually flies under the radar. > > Gcc took remarkably long to compile a program that included > a fragment like this: > > 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. - Dan C. > On Sun, Oct 25, 2026 at 5:50 PM G. Branden Robinson via TUHS > wrote: > > > > At 2026-10-25T10:42:45-0700, christopher fujino via TUHS wrote: > > > My friend, taking an OS course was wondering what `ld` stood for. > > > After doing an online search, concluded "no one knows." Surely someone > > > around here must remember why the linker is called `ld`? Thanks! > > > > At 2026-10-25T11:08:51-0700, christopher fujino via TUHS wrote: > > > It looks like the earliest source for `ld` I can find on TUHS is from > > > v2: > > > https://www.tuhs.org/cgi-bin/utree.pl?file=V2/cmd/ld1.s > > > > > > / ld1 -- link editor > > > > > > Could it stand for "link editor"? > > > > At 2026-10-25T13:40:04-0600, Warner Losh via TUHS wrote: > > > The first edition manual is at > > > https://www.tuhs.org/cgi-bin/utree.pl?file=V1/man/man1/ld.1 > > > > > > And says the same thing. As do later editions. > > > > In my own education I found myself scandalzied by the meager emphasis > > given to linking and loading in the curricula offered to those receiving > > university educations in software engineering and/or computing science. > > > > I suppose this is because there is thought to be no "deep theory" behind > > linking and loading techniques, at least not enough to sustain a steady > > research program. Coming up with object file formats and linkers and > > loaders to produce and consume them is, after all, a "simple matter of > > programming". > > > > But I wonder. We don't seem to have a deep theory of operating systems, > > but that discipline does, just barely, sustain a research program. > > > > Not that industry seems to realize it, however.[1] Once one has > > achieved "world domination",[2] little of interest remains except > > increasing returns to the dominators' capital. > > > > I second recommendations of John Levine's book, and also recommend David > > Salomon's "Assemblers and Loaders". When I got through the latter's > > giant chapter on assemblers, many things fell into place for me. > > > > Regards, > > Branden > > > > [1] https://microkerneldude.org/2022/02/17/a-story-of-betrayal-csiros-war-on-ts/ > > [2] https://www.computing.co.uk/news/1831329/linux-promises-world-domination From coff at tuhs.org Mon Oct 26 12:55:13 2026 From: coff at tuhs.org (Warner Losh via COFF) Date: Sun, 25 Oct 2026 20:55:13 -0600 Subject: [COFF] [TUHS] Re: What does `ld` stand for? In-Reply-To: References: <20261025214939.e5znado45xcetpmk@illithid> Message-ID: On Sun, Oct 25, 2026, 8:44 PM Dan Cross via COFF wrote: > On Sun, Oct 25, 2026 at 8:38 PM Douglas McIlroy via COFF > wrote: > > Branden wrote on TUHS: > > > > > I ... recommend David Salomon's "Assemblers and Loaders". > > > When I got through the latter's giant chapter on assemblers, > > > many things fell into place for me. > > > > This conjured the memory of a learning experience with ELF, > > which usually flies under the radar. > > > > Gcc took remarkably long to compile a program that included > > a fragment like this: > > > > 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. And there are also flags that control the alignment of the sections (or you can use a custom linker script). Warner - Dan C. > > > > > On Sun, Oct 25, 2026 at 5:50 PM G. Branden Robinson via TUHS > > wrote: > > > > > > At 2026-10-25T10:42:45-0700, christopher fujino via TUHS wrote: > > > > My friend, taking an OS course was wondering what `ld` stood for. > > > > After doing an online search, concluded "no one knows." Surely > someone > > > > around here must remember why the linker is called `ld`? Thanks! > > > > > > At 2026-10-25T11:08:51-0700, christopher fujino via TUHS wrote: > > > > It looks like the earliest source for `ld` I can find on TUHS is from > > > > v2: > > > > https://www.tuhs.org/cgi-bin/utree.pl?file=V2/cmd/ld1.s > > > > > > > > / ld1 -- link editor > > > > > > > > Could it stand for "link editor"? > > > > > > At 2026-10-25T13:40:04-0600, Warner Losh via TUHS wrote: > > > > The first edition manual is at > > > > https://www.tuhs.org/cgi-bin/utree.pl?file=V1/man/man1/ld.1 > > > > > > > > And says the same thing. As do later editions. > > > > > > In my own education I found myself scandalzied by the meager emphasis > > > given to linking and loading in the curricula offered to those > receiving > > > university educations in software engineering and/or computing science. > > > > > > I suppose this is because there is thought to be no "deep theory" > behind > > > linking and loading techniques, at least not enough to sustain a steady > > > research program. Coming up with object file formats and linkers and > > > loaders to produce and consume them is, after all, a "simple matter of > > > programming". > > > > > > But I wonder. We don't seem to have a deep theory of operating > systems, > > > but that discipline does, just barely, sustain a research program. > > > > > > Not that industry seems to realize it, however.[1] Once one has > > > achieved "world domination",[2] little of interest remains except > > > increasing returns to the dominators' capital. > > > > > > I second recommendations of John Levine's book, and also recommend > David > > > Salomon's "Assemblers and Loaders". When I got through the latter's > > > giant chapter on assemblers, many things fell into place for me. > > > > > > Regards, > > > Branden > > > > > > [1] > https://microkerneldude.org/2022/02/17/a-story-of-betrayal-csiros-war-on-ts/ > > > [2] > https://www.computing.co.uk/news/1831329/linux-promises-world-domination > From coff at tuhs.org Tue Oct 27 01:04:38 2026 From: coff at tuhs.org (Douglas McIlroy via COFF) Date: Mon, 26 Oct 2026 11:04:38 -0400 Subject: [COFF] [TUHS] Re: What does `ld` stand for? In-Reply-To: <7wpkwwlm2u.fsf@junk.nocrew.org> References: <28614.1792951109@cesium.clock.org> <7wtsm9klr1.fsf@junk.nocrew.org> <7wpkwwlm2u.fsf@junk.nocrew.org> Message-ID: >> Technically exec(2) was the loader. ld was generally invoked >> subliminally and rarely recursively. Thus, in conversation it was >> known as "loader", a converter from relocatable binary to core image. >> The man page, however, carefully characterized its full recursive >> capability. > How was ld invoked subliminally? What do you mean by invoking it > recursively? > but is that called "recursion"? If yes, ok I'll accept that. > If not, then what is "loader recursion"? Subliminality: Ld is involved in creating almost every a.out, but rarely appears in the shell scripts that cause the creation. Ld does not call itself recursively, but it can be used recursively. For example, one can write a recursive shell script to combine a list of .o files one at a time instead of all at once. Doug From coff at tuhs.org Tue Oct 27 01:52:43 2026 From: coff at tuhs.org (Dan Cross via COFF) Date: Mon, 26 Oct 2026 11:52:43 -0400 Subject: [COFF] [TUHS] Re: What does `ld` stand for? In-Reply-To: References: <20261025214939.e5znado45xcetpmk@illithid> Message-ID: On Sun, Oct 25, 2026 at 10:55 PM Warner Losh wrote: > On Sun, Oct 25, 2026, 8:44 PM Dan Cross via COFF 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 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. From coff at tuhs.org Tue Oct 27 08:49:00 2026 From: coff at tuhs.org (Douglas McIlroy via COFF) Date: Mon, 26 Oct 2026 18:49:00 -0400 Subject: [COFF] [TUHS] Re: What does `ld` stand for? In-Reply-To: References: <20261025214939.e5znado45xcetpmk@illithid> Message-ID: Neat hack! Am I right in intuiting that this couldn't be done if there were two such arrays rather than one? It depends, though, on the data section coming last. That presumably explains why gcc doesn't employ the trick. Doug On Mon, Oct 26, 2026 at 11:53 AM Dan Cross wrote: > > On Sun, Oct 25, 2026 at 10:55 PM Warner Losh wrote: > > On Sun, Oct 25, 2026, 8:44 PM Dan Cross via COFF 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 > 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. From coff at tuhs.org Wed Oct 28 03:04:55 2026 From: coff at tuhs.org (Dan Cross via COFF) Date: Tue, 27 Oct 2026 13:04:55 -0400 Subject: [COFF] [TUHS] Re: What does `ld` stand for? In-Reply-To: References: <20261025214939.e5znado45xcetpmk@illithid> Message-ID: On Mon, Oct 26, 2026 at 6:49 PM Douglas McIlroy wrote: > Neat hack! Thanks! > Am I right in intuiting that this couldn't be done if there > were two such arrays rather than one? Not quite. I believe you can do it once per loadable segment, and you can have a good number of those: the ELF header has a 16 bit field for the total number program headers, but I suspect most systems will impose some practical limit well below the full allowable range; still, it's more than one. Of course, if you're using this trick for more than one, you'll have to compact the binary more carefully. > It depends, though, on the data section coming last. That presumably > explains why gcc doesn't employ the trick. Along with a couple of other things, yes. GCC (and other compilers) are likely written in terms of sections, not segments, which don't have this properly of different size values for memory vs. file. When GCC writes out the header for a section consisting of only uninitialized (or explicitly initialized with all zeros) data, it will set the section type field to `SHT_NOBITS`, and won't write out data for it in the object file; the linker will almost certainly aggregate all such sections into one or more BSS segments, but playing the aforementioned game with the memory size vs the file size on a loadable segment. For other data-bearing sections, it sets the type to `SHT_PROGBITS`. When a symbol is partially initialized, it will be put into a progbits section. I suppose GCC _could_ try and arrange for an object that is partially initialized to split across two sections, one containing the initialization prefix, followed immediately followed by a nobits section (the section headers have address and offset fields) covering the uninitialized part, but I don't know that a linker would honor that; I imagine the linker expects to fill those fields of the section header in with the values it computes for where the sections are laid out, and can presumably reorder them any number of ways when the executable object is generated. In general, I suspect that the once GCC sees any amount of initialization it punts on being clever about the layout and just does the naive thing you observed. - Dan C. > On Mon, Oct 26, 2026 at 11:53 AM Dan Cross wrote: > > > > On Sun, Oct 25, 2026 at 10:55 PM Warner Losh wrote: > > > On Sun, Oct 25, 2026, 8:44 PM Dan Cross via COFF 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 > > 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.