From a9b804935c6fbed421139a7c8b615f7c236f2248 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 12 May 2026 13:50:09 +0900 Subject: [PATCH] =?UTF-8?q?w6l+selfhost:=20dynamic-link=20+=20.data=20?= =?UTF-8?q?=E2=80=94=20shared=20R+W=20segment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .data now lives at the end of the dyn-path R+W PT_LOAD, just after .dynamic. The single segment covers .got.plt + .dynamic + .data; its filesz drops trailing zeros (BSS) while memsz spans the full extent. Relocation moves from main.c into each emit function so the static and dynamic paths use their own data_va — text→data refs land on the right VA regardless of path. Removes the early-error in dynout.c that previously refused any .data with -l/-L. Tests: 810_dyn gains two new dyn+.data fixtures (mutable read+write of an i32, plus a zero-init i64 verifying the BSS scan still produces a valid p_fileszdatalen > 0) { - fprintf(stderr, "w6l: top-level mutable globals (.data) are " - "not yet supported with -l/-L; link without shared " - "libraries to use them.\n"); - return 1; - } + /* Writable globals share the dyn-path R+W segment with .got.plt + * and .dynamic. .data is placed after .dynamic; the segment's + * filesz/memsz are extended to cover all three. Relocation + * targeting .data uses data_va computed inside this function + * (l_relocate now runs from here, not main, so the dyn layout's + * data VA is the one that lands in patched offsets). */ const int N = l->dyn_n; @@ -375,10 +370,12 @@ l_emit_dyn_elf(Lnk *l, FILE *f, u64 base, u64 entry) const u64 rx_end = plt_off + plt_sz; /* Page-align the writable segment. We skip a page of file bytes; - * the data lands at file offset gotplt_off, vaddr at base+gotplt_va. */ + * the data lands at file offset gotplt_off, vaddr at base+gotplt_va. + * .data sits after .dynamic so the whole R+W run is one segment. */ const u64 gotplt_off = (rx_end + page - 1) & ~(page - 1); const u64 dynamic_off = gotplt_off + gotplt_sz; - const u64 file_end = dynamic_off + dynamic_sz; + const u64 data_off = dynamic_off + dynamic_sz; + const u64 file_end = data_off + l->datalen; /* Virtual addresses mirror file offsets within their segment. * The R+W segment in particular needs vaddr = base + gotplt_off @@ -394,8 +391,24 @@ l_emit_dyn_elf(Lnk *l, FILE *f, u64 base, u64 entry) const u64 plt_va = base + plt_off; const u64 gotplt_va = base + gotplt_off; const u64 dynamic_va = base + dynamic_off; + const u64 data_va = base + data_off; (void)dynstr_va; (void)hash_va; (void)plt_va; + /* Now that the dyn layout pins text_va/data_va, apply + * relocations. main.c defers this; the static path runs it from + * out.c with its own VAs. */ + if (l_relocate(l, text_va, data_va) != 0) return 1; + + /* BSS optimisation — same trailing-zero scan as out.c. */ + u64 bsslen = 0; + if (l->datalen > 0) { + while (bsslen < l->datalen + && l->data[l->datalen - 1 - bsslen] == 0) + bsslen++; + } + const u64 data_file_len = l->datalen - bsslen; + const u64 file_data_end = data_off + data_file_len; + /* ---- Pass 4: build each section into a buffer ---- */ /* .dynsym */ @@ -593,13 +606,15 @@ l_emit_dyn_elf(Lnk *l, FILE *f, u64 base, u64 entry) ph[0].p_memsz = rx_end; ph[0].p_align = page; - /* PT_LOAD #2 — R+W covering .got.plt and .dynamic. */ + /* PT_LOAD #2 — R+W covering .got.plt, .dynamic, and .data. + * filesz drops the trailing-zero suffix (BSS); memsz keeps the + * full extent so the loader zero-fills the gap. */ ph[1].p_type = PT_LOAD; ph[1].p_flags = PF_R | PF_W; ph[1].p_offset = gotplt_off; ph[1].p_vaddr = gotplt_va; ph[1].p_paddr = gotplt_va; - ph[1].p_filesz = file_end - gotplt_off; + ph[1].p_filesz = file_data_end - gotplt_off; ph[1].p_memsz = file_end - gotplt_off; ph[1].p_align = page; @@ -645,6 +660,9 @@ l_emit_dyn_elf(Lnk *l, FILE *f, u64 base, u64 entry) PAD_TO(plt_off); fwrite(plt, 1, plt_sz, f); PAD_TO(gotplt_off); fwrite(gotplt, 1, gotplt_sz, f); PAD_TO(dynamic_off); fwrite(dynamic, 1, dynamic_sz, f); + if (data_file_len > 0) { + PAD_TO(data_off); fwrite(l->data, 1, data_file_len, f); + } for (int i = 0; i < n_vlibs; i++) free(vlibs[i].versions); free(vlibs); free(versym_for); free(versym); diff --git a/cmd/w6l/main.c b/cmd/w6l/main.c index 0f6054de..c85bd293 100644 --- a/cmd/w6l/main.c +++ b/cmd/w6l/main.c @@ -115,20 +115,9 @@ main(int argc, char **argv) if (l_load(&l, inputs[i]) != 0) return 1; } if (l_resolve(&l) != 0) return 1; - /* Layout for relocation purposes. The static path (out.c) uses - * text_va = base + 0x1000 and places .data at the next page after - * .text — these must agree with the layout in out.c. PC-relative - * displacements between .text symbols cancel the absolute VAs, so - * a dyn-path link whose data_va is "wrong" still relocates text→ - * text correctly; text→data is currently rejected in dynout.c. */ - u64 text_va = base + 0x1000; - u64 data_va = 0; - if (l.datalen > 0) { - u64 page = 0x1000; - u64 data_off = (0x1000 + l.textlen + page - 1) & ~(page - 1); - data_va = base + data_off; - } - if (l_relocate(&l, text_va, data_va) != 0) return 1; + /* Relocation is deferred to the emit functions — each path knows + * its own layout (text_va, data_va), and the static and dynamic + * paths place .data at different VAs. */ Lsym *entry = l_lookup(&l, "_start"); if (entry == NULL || !entry->defined) entry = l_lookup(&l, "main"); diff --git a/cmd/w6l/out.c b/cmd/w6l/out.c index 6d35817a..80dcab6f 100644 --- a/cmd/w6l/out.c +++ b/cmd/w6l/out.c @@ -63,6 +63,16 @@ l_emit_elf(Lnk *l, FILE *f, u64 base, u64 entry) const u64 rx_end = text_off + l->textlen; const int has_data = (l->datalen > 0); + /* data goes at the next page boundary so the loader can grant a + * fresh page of R+W permissions without overlapping the R+X mapping. */ + const u64 data_off = has_data ? ((rx_end + page - 1) & ~(page - 1)) : 0; + const u64 data_va = has_data ? (base + data_off) : 0; + + /* Apply relocations now that the layout's text_va/data_va are + * known. Deferred from main.c so the dynamic-link path can use + * its own data_va. */ + if (l_relocate(l, base + text_off, data_va) != 0) return 1; + /* BSS optimisation: trailing zero bytes in .data can be left out * of the file. The loader zero-fills the gap between p_filesz and * p_memsz, so this shrinks the binary without changing semantics. @@ -75,11 +85,6 @@ l_emit_elf(Lnk *l, FILE *f, u64 base, u64 entry) bsslen++; } const u64 data_file_len = l->datalen - bsslen; - - /* data goes at the next page boundary so the loader can grant a - * fresh page of R+W permissions without overlapping the R+X mapping. */ - const u64 data_off = has_data ? ((rx_end + page - 1) & ~(page - 1)) : 0; - const u64 data_va = has_data ? (base + data_off) : 0; const u64 file_end = has_data ? (data_off + data_file_len) : rx_end; (void)data_va; diff --git a/selfhost/cmd/w6l/dynout.ww b/selfhost/cmd/w6l/dynout.ww index 5711ba23..af7e23ca 100644 --- a/selfhost/cmd/w6l/dynout.ww +++ b/selfhost/cmd/w6l/dynout.ww @@ -163,14 +163,10 @@ fn streqd(a: str, b: str) bool = { // ---- main entry -------------------------------------------------------- export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { - // Writable globals on the dynamic-link path need their own R+W - // segment that the existing gotplt + .dynamic layout doesn't - // account for. Refuse rather than silently miscompile; folding - // .data into the existing R+W block is a follow-up. - if (l.datalen > 0u64) { - os.write(2, "w6l: top-level mutable globals (.data) not yet supported with -l/-L\n".ptr, 67u64); - return 1; - }; + // .data shares the R+W PT_LOAD with .got.plt and .dynamic. + // Placed after .dynamic so the segment is one contiguous run; + // relocate runs from here so the dyn layout's datava lands in + // patched offsets. let a: *arena = l.a; let n: i32 = l.dynn; let nu: u64 = n: u64; @@ -466,7 +462,8 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { let gotpltoff: u64 = alignup(rxend, PAGE); let dynamicoff: u64 = gotpltoff + gotpltsz; - let fileend: u64 = dynamicoff + dynamicsz; + let dataoff: u64 = dynamicoff + dynamicsz; + let fileend: u64 = dataoff + l.datalen; let interpva: u64 = base + interpoff; let dynstrva: u64 = base + dynstroff; @@ -479,6 +476,23 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { let pltva: u64 = base + pltoff; let gotpltva: u64 = base + gotpltoff; let dynamicva: u64 = base + dynamicoff; + let datava: u64 = base + dataoff; + + // Apply relocations now that the dyn layout's textva/datava are + // pinned. main.ww defers this so each path uses its own VAs. + if (relocate(l, textva, datava) != 0) { return 1; }; + + // BSS optimisation — same trailing-zero scan as out.ww. + let bsslen: u64 = 0u64; + if (l.datalen > 0u64) { + for (bsslen < l.datalen) { + let b: u8 = l.data[l.datalen - 1u64 - bsslen]; + if (b != 0u8) { break; }; + bsslen += 1u64; + }; + }; + let datafilelen: u64 = l.datalen - bsslen; + let filedataend: u64 = dataoff + datafilelen; // ---- build .dynsym ---- let dynsymbuf: *u8 = amalloc(a, dynsymsz): *u8; @@ -686,7 +700,9 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { dwr64(filebuf, p1 + 8u64, gotpltoff); dwr64(filebuf, p1 + 16u64, gotpltva); dwr64(filebuf, p1 + 24u64, gotpltva); - dwr64(filebuf, p1 + 32u64, fileend - gotpltoff); + // filesz trims the .data trailing zeros (BSS); memsz covers + // .got.plt + .dynamic + the full .data so the loader zero-fills. + dwr64(filebuf, p1 + 32u64, filedataend - gotpltoff); dwr64(filebuf, p1 + 40u64, fileend - gotpltoff); dwr64(filebuf, p1 + 48u64, PAGE); @@ -727,10 +743,13 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { dbcopy(filebuf, pltoff, pltbuf, pltsz); dbcopy(filebuf, gotpltoff, gotpltbuf, gotpltsz); dbcopy(filebuf, dynamicoff, dynamicbuf, dynamicsz); + if (datafilelen > 0u64) { + dbcopy(filebuf, dataoff, l.data, datafilelen); + }; - let wr: (i64 | os.oserror) = os.writeall(fd, filebuf, fileend); + let wr: (i64 | os.oserror) = os.writeall(fd, filebuf, filedataend); match (wr) { - case let v: i64 => { if (v != fileend: i64) { return 1; }; }; + case let v: i64 => { if (v != filedataend: i64) { return 1; }; }; case let e: os.oserror => return 1; }; return 0; diff --git a/selfhost/cmd/w6l/main.combined.ww b/selfhost/cmd/w6l/main.combined.ww index b1b3a235..dc1671f9 100644 --- a/selfhost/cmd/w6l/main.combined.ww +++ b/selfhost/cmd/w6l/main.combined.ww @@ -1759,14 +1759,10 @@ fn streqd(a: str, b: str) bool = { // ---- main entry -------------------------------------------------------- export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { - // Writable globals on the dynamic-link path need their own R+W - // segment that the existing gotplt + .dynamic layout doesn't - // account for. Refuse rather than silently miscompile; folding - // .data into the existing R+W block is a follow-up. - if (l.datalen > 0u64) { - os.write(2, "w6l: top-level mutable globals (.data) not yet supported with -l/-L\n".ptr, 67u64); - return 1; - }; + // .data shares the R+W PT_LOAD with .got.plt and .dynamic. + // Placed after .dynamic so the segment is one contiguous run; + // relocate runs from here so the dyn layout's datava lands in + // patched offsets. let a: *arena = l.a; let n: i32 = l.dynn; let nu: u64 = n: u64; @@ -2062,7 +2058,8 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { let gotpltoff: u64 = alignup(rxend, PAGE); let dynamicoff: u64 = gotpltoff + gotpltsz; - let fileend: u64 = dynamicoff + dynamicsz; + let dataoff: u64 = dynamicoff + dynamicsz; + let fileend: u64 = dataoff + l.datalen; let interpva: u64 = base + interpoff; let dynstrva: u64 = base + dynstroff; @@ -2075,6 +2072,23 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { let pltva: u64 = base + pltoff; let gotpltva: u64 = base + gotpltoff; let dynamicva: u64 = base + dynamicoff; + let datava: u64 = base + dataoff; + + // Apply relocations now that the dyn layout's textva/datava are + // pinned. main.ww defers this so each path uses its own VAs. + if (relocate(l, textva, datava) != 0) { return 1; }; + + // BSS optimisation — same trailing-zero scan as out.ww. + let bsslen: u64 = 0u64; + if (l.datalen > 0u64) { + for (bsslen < l.datalen) { + let b: u8 = l.data[l.datalen - 1u64 - bsslen]; + if (b != 0u8) { break; }; + bsslen += 1u64; + }; + }; + let datafilelen: u64 = l.datalen - bsslen; + let filedataend: u64 = dataoff + datafilelen; // ---- build .dynsym ---- let dynsymbuf: *u8 = amalloc(a, dynsymsz): *u8; @@ -2282,7 +2296,9 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { dwr64(filebuf, p1 + 8u64, gotpltoff); dwr64(filebuf, p1 + 16u64, gotpltva); dwr64(filebuf, p1 + 24u64, gotpltva); - dwr64(filebuf, p1 + 32u64, fileend - gotpltoff); + // filesz trims the .data trailing zeros (BSS); memsz covers + // .got.plt + .dynamic + the full .data so the loader zero-fills. + dwr64(filebuf, p1 + 32u64, filedataend - gotpltoff); dwr64(filebuf, p1 + 40u64, fileend - gotpltoff); dwr64(filebuf, p1 + 48u64, PAGE); @@ -2323,10 +2339,13 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { dbcopy(filebuf, pltoff, pltbuf, pltsz); dbcopy(filebuf, gotpltoff, gotpltbuf, gotpltsz); dbcopy(filebuf, dynamicoff, dynamicbuf, dynamicsz); + if (datafilelen > 0u64) { + dbcopy(filebuf, dataoff, l.data, datafilelen); + }; - let wr: (i64 | os.oserror) = os.writeall(fd, filebuf, fileend); + let wr: (i64 | os.oserror) = os.writeall(fd, filebuf, filedataend); match (wr) { - case let v: i64 => { if (v != fileend: i64) { return 1; }; }; + case let v: i64 => { if (v != filedataend: i64) { return 1; }; }; case let e: os.oserror => return 1; }; return 0; @@ -2394,10 +2413,17 @@ export fn emitelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { // .data lands at the next page boundary so the loader can give // it fresh R+W permissions without overlapping the R+X mapping. let dataoff: u64 = 0u64; + let datava: u64 = 0u64; if (hasdata) { dataoff = (rxend + PAGE_SZ - 1u64) & ~(PAGE_SZ - 1u64); + datava = base + dataoff; }; + // Apply relocations now that the layout's textva/datava are + // known. Deferred from main.ww so the dyn path uses its own + // datava. + if (relocate(l, base + TEXT_OFF, datava) != 0) { return -1; }; + // BSS optimisation: trailing zero bytes in .data can be left // out of the file. The loader zero-fills the gap between // p_filesz and p_memsz. Scan after l_relocate has applied any @@ -2804,18 +2830,9 @@ export fn main(argc: i32, argv: **u8) i32 = { }; if (resolve(l) != 0) { return 1; }; - // Static layout for relocation purposes. .data lands at the - // next page after .text; the dyn path overrides if its layout - // is different (text→text PC32 cancels the absolute VA so - // only the data delta matters here). - let textva: u64 = BASE + CODE_VA_OFF; - let datava: u64 = 0u64; - if (l.datalen > 0u64) { - let page: u64 = 4096u64; - let dataoff: u64 = (CODE_VA_OFF + l.textlen + page - 1u64) & ~(page - 1u64); - datava = BASE + dataoff; - }; - if (relocate(l, textva, datava) != 0) { return 1; }; + // Relocation is deferred to the emit functions — each path + // knows its own layout (textva, datava); the static and dyn + // paths place .data at different VAs. let entrysym: *lsym = lookup(l, "_start"); if (entrysym == nil) { entrysym = lookup(l, "main"); } diff --git a/selfhost/cmd/w6l/main.ww b/selfhost/cmd/w6l/main.ww index 79b3242a..eacc73ec 100644 --- a/selfhost/cmd/w6l/main.ww +++ b/selfhost/cmd/w6l/main.ww @@ -298,18 +298,9 @@ export fn main(argc: i32, argv: **u8) i32 = { }; if (resolve(l) != 0) { return 1; }; - // Static layout for relocation purposes. .data lands at the - // next page after .text; the dyn path overrides if its layout - // is different (text→text PC32 cancels the absolute VA so - // only the data delta matters here). - let textva: u64 = BASE + CODE_VA_OFF; - let datava: u64 = 0u64; - if (l.datalen > 0u64) { - let page: u64 = 4096u64; - let dataoff: u64 = (CODE_VA_OFF + l.textlen + page - 1u64) & ~(page - 1u64); - datava = BASE + dataoff; - }; - if (relocate(l, textva, datava) != 0) { return 1; }; + // Relocation is deferred to the emit functions — each path + // knows its own layout (textva, datava); the static and dyn + // paths place .data at different VAs. let entrysym: *lsym = lookup(l, "_start"); if (entrysym == nil) { entrysym = lookup(l, "main"); } diff --git a/selfhost/cmd/w6l/out.ww b/selfhost/cmd/w6l/out.ww index df031906..9f3711fd 100644 --- a/selfhost/cmd/w6l/out.ww +++ b/selfhost/cmd/w6l/out.ww @@ -59,10 +59,17 @@ export fn emitelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = { // .data lands at the next page boundary so the loader can give // it fresh R+W permissions without overlapping the R+X mapping. let dataoff: u64 = 0u64; + let datava: u64 = 0u64; if (hasdata) { dataoff = (rxend + PAGE_SZ - 1u64) & ~(PAGE_SZ - 1u64); + datava = base + dataoff; }; + // Apply relocations now that the layout's textva/datava are + // known. Deferred from main.ww so the dyn path uses its own + // datava. + if (relocate(l, base + TEXT_OFF, datava) != 0) { return -1; }; + // BSS optimisation: trailing zero bytes in .data can be left // out of the file. The loader zero-fills the gap between // p_filesz and p_memsz. Scan after l_relocate has applied any diff --git a/test/wcc/810_dyn.c b/test/wcc/810_dyn.c index 47059ff0..92a4c38b 100644 --- a/test/wcc/810_dyn.c +++ b/test/wcc/810_dyn.c @@ -76,6 +76,28 @@ static const struct row rows[] = { " return 0;\n" "};", 5 }, + /* 5. Writable global on the dyn-link path. .data now lives in the + * shared R+W segment with .got.plt and .dynamic; read and write + * exercise the new layout's data_va. */ + { "@symbol(\"_exit\") fn libc_exit(c: i32) void;\n" + "let counter: i32 = 0;\n" + "export fn main() i32 = {\n" + " counter = 11;\n" + " libc_exit(counter);\n" + " return 0;\n" + "};", 11 }, + + /* 6. Zero-init slot under dyn-link: the BSS trailing-zero scan + * should still produce a valid p_filesz