nxld.nx source
↩ module page · 754 lines · 27671 B
1// nxld.nx -- Nishi sovereign linker.
2//
3// Replaces gcc-ld in verify.sh. Reads RV64 object files (ELF
4// relocatable), resolves symbols across them, and produces a
5// statically-linked RV64 Linux ELF executable.
6//
7// Scope (v0.0.1):
8// - Single-output static linking (no .so / dynamic linking)
9// - RV64 only (Sv39 virtual memory layout)
10// - ELF relocations: R_RISCV_64, R_RISCV_BRANCH, R_RISCV_JAL,
11// R_RISCV_CALL, R_RISCV_HI20, R_RISCV_LO12_I, R_RISCV_LO12_S,
12// R_RISCV_PCREL_HI20, R_RISCV_PCREL_LO12_I
13// - Single .text section + single .data section + single .bss
14// - Entry point = _start (RV64 Linux convention)
15// - Output base vaddr = 0x10000 (matches elf_writer.nx)
16//
17// Out of scope (v0.0.1):
18// - Section ordering directives
19// - Linker scripts
20// - LTO bytecode handling
21// - Position-independent code
22// - Garbage collection of unreferenced sections
23// - Debug info preservation (DWARF stripped)
24//
25// These are deliberate v0.0.1 limits. v0.1.0 closes them.
26//
27// Pipeline:
28// 1. Open + parse each input ELF (header, sections, symbols, relocs)
29// 2. Group sections by type: .text, .data, .bss
30// 3. Resolve undefined symbols across input objects
31// 4. Lay out output sections (text starts at 0x10000 + 0x1000 header)
32// 5. Apply relocations now that final addresses are known
33// 6. Build output ELF (header + program headers + sections)
34// 7. Write to output file
35
36import "syscalls.nx"
37
38// === ELF constants (matches elf_writer.nx encoding) ===
39
40const ELF_MAGIC0: i64 = 0x7F
41const ELF_MAGIC1: i64 = 0x45 // 'E'
42const ELF_MAGIC2: i64 = 0x4C // 'L'
43const ELF_MAGIC3: i64 = 0x46 // 'F'
44const EI_CLASS64: i64 = 2
45const EI_DATA2LSB: i64 = 1
46const ET_EXEC: i64 = 2
47const ET_REL: i64 = 1
48const EM_RISCV: i64 = 0xF3
49
50const SHT_NULL: i64 = 0
51const SHT_PROGBITS: i64 = 1
52const SHT_SYMTAB: i64 = 2
53const SHT_STRTAB: i64 = 3
54const SHT_RELA: i64 = 4
55const SHT_NOBITS: i64 = 8
56
57const PT_LOAD: i64 = 1
58const PF_R: i64 = 4
59const PF_W: i64 = 2
60const PF_X: i64 = 1
61
62// RISC-V relocations (subset for v0.0.1)
63const R_RISCV_64: i64 = 2
64const R_RISCV_BRANCH: i64 = 16
65const R_RISCV_JAL: i64 = 17
66const R_RISCV_CALL: i64 = 18
67const R_RISCV_PCREL_HI20: i64 = 23
68const R_RISCV_PCREL_LO12_I: i64 = 24
69const R_RISCV_HI20: i64 = 26
70const R_RISCV_LO12_I: i64 = 27
71const R_RISCV_LO12_S: i64 = 28
72
73// === in-memory representation ===
74
75struct ElfObject {
76 base: *u8, // mmap'd file start
77 length: i64,
78 e_type: i64, // ET_REL etc.
79 e_machine: i64,
80 e_shoff: i64,
81 e_shnum: i64,
82 e_shentsize: i64,
83 e_shstrndx: i64,
84 text_off: i64, // offset into file of .text section data
85 text_len: i64, // byte length of .text
86 data_off: i64, // offset into file of .data section
87 data_len: i64,
88 symtab_off: i64, // offset into file of symbol table
89 symtab_len: i64,
90 strtab_off: i64, // offset of string table for symtab
91 strtab_len: i64,
92 rela_text_off: i64, // offset of relocations against .text
93 rela_text_len: i64,
94}
95
96struct Symbol {
97 name: *u8, // string table entry
98 name_len: i64,
99 value: i64, // section-relative offset
100 section_idx: i64, // section number; 0 = SHN_UNDEF
101 bind: i64, // STB_LOCAL=0, STB_GLOBAL=1
102 type_: i64, // STT_FUNC=2, STT_OBJECT=1
103 resolved_addr: i64, // final virtual address after layout
104}
105
106// === reader ===
107
108func read_u16_le(buf: *u8, off: i64) -> i64 {
109 return buf[off] | (buf[off + 1] << 8)
110}
111
112func read_u32_le(buf: *u8, off: i64) -> i64 {
113 var v: i64 = 0
114 var i: i64 = 3
115 while i >= 0 {
116 v = (v << 8) | buf[off + i]
117 i = i - 1
118 }
119 return v
120}
121
122func read_u64_le(buf: *u8, off: i64) -> i64 {
123 var v: i64 = 0
124 var i: i64 = 7
125 while i >= 0 {
126 v = (v << 8) | buf[off + i]
127 i = i - 1
128 }
129 return v
130}
131
132// Compare a null-terminated section name `np` against literal `lit`
133// of length `lit_len`. Returns 1 on match, 0 otherwise. Match
134// requires both the literal AND the null terminator at np[lit_len]
135// (so .text doesn't accidentally match .text.cold etc.)
136func sec_name_eq(np: *u8, lit: *u8, lit_len: i64) -> i64 {
137 var i: i64 = 0
138 while i < lit_len {
139 if np[i] != lit[i] { return 0 }
140 i = i + 1
141 }
142 if np[lit_len] != 0 { return 0 }
143 return 1
144}
145
146// Validate magic + class + endianness, parse the header. Returns
147// 0 on success, negative on validation failure.
148func nxld_open(obj: *ElfObject, base: *u8, length: i64) -> i64 {
149 if length < 64 { return -1 }
150 if base[0] != ELF_MAGIC0 { return -2 }
151 if base[1] != ELF_MAGIC1 { return -3 }
152 if base[2] != ELF_MAGIC2 { return -4 }
153 if base[3] != ELF_MAGIC3 { return -5 }
154 if base[4] != EI_CLASS64 { return -6 }
155 if base[5] != EI_DATA2LSB { return -7 }
156
157 obj.base = base
158 obj.length = length
159 obj.e_type = read_u16_le(base, 16)
160 obj.e_machine = read_u16_le(base, 18)
161 obj.e_shoff = read_u64_le(base, 40)
162 obj.e_shentsize = read_u16_le(base, 58)
163 obj.e_shnum = read_u16_le(base, 60)
164 obj.e_shstrndx = read_u16_le(base, 62)
165
166 if obj.e_machine != EM_RISCV { return -8 }
167
168 obj.text_off = 0; obj.text_len = 0
169 obj.data_off = 0; obj.data_len = 0
170 obj.symtab_off = 0; obj.symtab_len = 0
171 obj.strtab_off = 0; obj.strtab_len = 0
172 obj.rela_text_off = 0; obj.rela_text_len = 0
173
174 // Bounds-check the section header table. Header-only ELFs
175 // (e_shnum == 0) skip the section walk entirely.
176 if obj.e_shnum == 0 { return 0 }
177 if obj.e_shoff + obj.e_shnum * obj.e_shentsize > length { return -9 }
178 if obj.e_shstrndx >= obj.e_shnum { return -10 }
179
180 // The section-header string table (.shstrtab) holds the names
181 // of all sections. Resolve its file offset from sh[shstrndx].
182 let shstr_hdr: i64 = obj.e_shoff + obj.e_shstrndx * obj.e_shentsize
183 let shstr_off: i64 = read_u64_le(base, shstr_hdr + 24)
184 let shstr_len: i64 = read_u64_le(base, shstr_hdr + 32)
185 if shstr_off + shstr_len > length { return -11 }
186
187 // Walk every section header, identify sections by name.
188 var i: i64 = 0
189 while i < obj.e_shnum {
190 let hdr: i64 = obj.e_shoff + i * obj.e_shentsize
191 let name_idx: i64 = read_u32_le(base, hdr + 0)
192 let sh_type: i64 = read_u32_le(base, hdr + 4)
193 let sh_off: i64 = read_u64_le(base, hdr + 24)
194 let sh_size: i64 = read_u64_le(base, hdr + 32)
195
196 // Resolve the section name into shstrtab.
197 let name_ptr: *u8 = (base as i64 + shstr_off + name_idx) as *u8
198
199 // Match well-known section names. String compare via
200 // byte-by-byte; sections with longer names won't match
201 // any of these short prefixes.
202 if sec_name_eq(name_ptr, ".text", 5) == 1 {
203 obj.text_off = sh_off
204 obj.text_len = sh_size
205 }
206 if sec_name_eq(name_ptr, ".data", 5) == 1 {
207 obj.data_off = sh_off
208 obj.data_len = sh_size
209 }
210 if sh_type == SHT_SYMTAB {
211 obj.symtab_off = sh_off
212 obj.symtab_len = sh_size
213 }
214 if sh_type == SHT_STRTAB {
215 // .strtab is the symbol-table string table; .shstrtab
216 // is the section-header string table. We want the
217 // first STRTAB that ISN'T shstrtab.
218 if i != obj.e_shstrndx {
219 obj.strtab_off = sh_off
220 obj.strtab_len = sh_size
221 }
222 }
223 if sec_name_eq(name_ptr, ".rela.text", 10) == 1 {
224 obj.rela_text_off = sh_off
225 obj.rela_text_len = sh_size
226 }
227 i = i + 1
228 }
229 return 0
230}
231
232// === symbol table walker =============================================
233//
234// ELF64 symbol table entry layout (24 bytes per Elf64_Sym):
235// u32 st_name (offset into strtab)
236// u8 st_info (high 4 bits = bind, low 4 bits = type)
237// u8 st_other
238// u16 st_shndx (section index, or SHN_UNDEF=0/SHN_ABS=0xFFF1)
239// u64 st_value (section-relative offset / abs value)
240// u64 st_size (object size in bytes; 0 for funcs without size info)
241//
242// We extract every defined symbol into the caller-supplied Symbol
243// pool. Undefined symbols (st_shndx == 0) get section_idx = 0
244// so the caller can collect cross-object references for resolution.
245
246const SYMBOL_BYTES: i64 = 24
247const STN_UNDEF: i64 = 0
248const STB_LOCAL: i64 = 0
249const STB_GLOBAL: i64 = 1
250const STB_WEAK: i64 = 2
251const STT_NOTYPE: i64 = 0
252const STT_OBJECT: i64 = 1
253const STT_FUNC: i64 = 2
254const STT_SECTION: i64 = 3
255
256// Number of symbols in the object file. Returns 0 if no symtab
257// section was identified during nxld_open().
258func nxld_symbol_count(obj: *ElfObject) -> i64 {
259 if obj.symtab_len == 0 { return 0 }
260 return obj.symtab_len / SYMBOL_BYTES
261}
262
263// Read symbol at `idx` into the caller-supplied Symbol struct.
264// Returns 0 on success, -1 on out-of-bounds. Resolves the symbol
265// name pointer into the object's strtab section.
266func nxld_read_symbol(obj: *ElfObject, idx: i64, sym: *Symbol) -> i64 {
267 let n: i64 = nxld_symbol_count(obj)
268 if idx < 0 { return -1 }
269 if idx >= n { return -1 }
270 let sym_off: i64 = obj.symtab_off + idx * SYMBOL_BYTES
271 let name_idx: i64 = read_u32_le(obj.base, sym_off)
272 let st_info: i64 = obj.base[sym_off + 4]
273 let st_shndx: i64 = read_u16_le(obj.base, sym_off + 6)
274 let st_value: i64 = read_u64_le(obj.base, sym_off + 8)
275
276 sym.value = st_value
277 sym.section_idx = st_shndx
278 sym.bind = (st_info >> 4) & 0xF
279 sym.type_ = st_info & 0xF
280 sym.resolved_addr = 0
281
282 // Resolve the name into strtab. name_len is computed by walking
283 // until a NUL terminator.
284 if obj.strtab_len > 0 {
285 sym.name = (obj.base as i64 + obj.strtab_off + name_idx) as *u8
286 var k: i64 = 0
287 while sym.name[k] != 0 { k = k + 1 }
288 sym.name_len = k
289 }
290 if obj.strtab_len == 0 {
291 sym.name = 0 as *u8
292 sym.name_len = 0
293 }
294 return 0
295}
296
297// Symbol-name equality vs a literal. Same shape as sec_name_eq.
298func sym_name_eq(np: *u8, np_len: i64, lit: *u8, lit_len: i64) -> i64 {
299 if np_len != lit_len { return 0 }
300 var i: i64 = 0
301 while i < lit_len {
302 if np[i] != lit[i] { return 0 }
303 i = i + 1
304 }
305 return 1
306}
307
308// Find a symbol by name. Returns symbol index (>= 0) or -1.
309// O(N) scan; for production use a hashtable.
310func nxld_find_symbol(obj: *ElfObject, name: *u8, name_len: i64) -> i64 {
311 let n: i64 = nxld_symbol_count(obj)
312 var sym_raw: *u8 = sys_mmap(64)
313 let sym: *Symbol = sym_raw as *Symbol
314 var i: i64 = 0
315 while i < n {
316 let rc: i64 = nxld_read_symbol(obj, i, sym)
317 if rc == 0 {
318 if sym_name_eq(sym.name, sym.name_len, name, name_len) == 1 {
319 return i
320 }
321 }
322 i = i + 1
323 }
324 return -1
325}
326
327// === relocation reader ===============================================
328//
329// ELF64 relocation entry layout (24 bytes per Elf64_Rela):
330// u64 r_offset (where in target section to apply the reloc)
331// u64 r_info (high 32 bits = symbol idx, low 32 bits = type)
332// i64 r_addend (signed value added to the resolution)
333//
334// We extract relocations from the .rela.text section recorded in
335// nxld_open. The link-time pass walks these, looks up the
336// referenced symbol's resolved address, computes the value to
337// patch in (per type), and writes it into the section data.
338
339const RELOC_BYTES: i64 = 24
340
341struct Reloc {
342 offset: i64, // where in section to apply
343 sym_idx: i64, // index into the symbol table
344 rtype: i64, // R_RISCV_*
345 addend: i64,
346}
347
348// Number of .rela.text relocations. 0 if no relocations were
349// recorded during nxld_open.
350func nxld_reloc_count(obj: *ElfObject) -> i64 {
351 if obj.rela_text_len == 0 { return 0 }
352 return obj.rela_text_len / RELOC_BYTES
353}
354
355// Read relocation `idx` into the caller-supplied Reloc.
356// Returns 0 on success, -1 on out-of-bounds.
357func nxld_read_reloc(obj: *ElfObject, idx: i64, r: *Reloc) -> i64 {
358 let n: i64 = nxld_reloc_count(obj)
359 if idx < 0 { return -1 }
360 if idx >= n { return -1 }
361 let off: i64 = obj.rela_text_off + idx * RELOC_BYTES
362 let r_offset: i64 = read_u64_le(obj.base, off)
363 let r_info: i64 = read_u64_le(obj.base, off + 8)
364 let r_addend: i64 = read_u64_le(obj.base, off + 16)
365
366 r.offset = r_offset
367 r.sym_idx = (r_info >> 32) & 0xFFFFFFFF
368 r.rtype = r_info & 0xFFFFFFFF
369 r.addend = r_addend
370 return 0
371}
372
373// === relocation apply pass ===========================================
374//
375// Walks every relocation in obj.rela_text and patches the 32-bit
376// instruction at obj.text + reloc.offset based on the resolved
377// symbol's address + the relocation type's encoding rules.
378//
379// Pre-condition: each Symbol's resolved_addr field has been set
380// by the layout pass (final virtual address of the symbol's
381// definition).
382//
383// Inputs:
384// obj -- the input ELF object
385// text_buf -- mutable copy of the .text bytes
386// syms_pool -- array of Symbol[N] indexed by symtab idx, with
387// resolved_addr already filled in
388// text_vaddr -- virtual address of text_buf[0] in the linked output
389//
390// Returns 0 on success, negative on unsupported relocation kind
391// or unresolved symbol.
392//
393// RISC-V instruction encoding cheatsheet (LE 32-bit words):
394// I-type addi/jalr: imm[11:0] @ bits 31:20
395// S-type sd/sb: imm[11:5] @ 31:25, imm[4:0] @ 11:7
396// B-type beq/bne: imm[12,10:5] @ 31:25, imm[4:1,11] @ 11:7
397// U-type lui/auipc: imm[31:12] @ 31:12
398// J-type jal: imm[20,10:1,11,19:12] @ 31:12
399
400func write_u32_le(buf: *u8, off: i64, v: i64) -> i64 {
401 buf[off + 0] = v & 0xFF
402 buf[off + 1] = (v >> 8) & 0xFF
403 buf[off + 2] = (v >> 16) & 0xFF
404 buf[off + 3] = (v >> 24) & 0xFF
405 return 0
406}
407
408// Patch the I-type immediate (12 bits, sign-extended) into a word.
409// Used by addi / jalr / lo12_i.
410func patch_i_imm(word: i64, imm: i64) -> i64 {
411 let kept: i64 = word & 0x000FFFFF // clear bits 31:20
412 let imm12: i64 = imm & 0xFFF
413 return kept | (imm12 << 20)
414}
415
416// Patch the S-type split immediate (12 bits) into a word.
417// Used by sd / sb / lo12_s.
418func patch_s_imm(word: i64, imm: i64) -> i64 {
419 let kept: i64 = word & 0x01FFF07F // clear 31:25 + 11:7
420 let hi: i64 = (imm >> 5) & 0x7F
421 let lo: i64 = imm & 0x1F
422 return kept | (hi << 25) | (lo << 7)
423}
424
425// Patch the U-type immediate (high 20 bits). Used by auipc / lui.
426func patch_u_imm(word: i64, imm_hi: i64) -> i64 {
427 let kept: i64 = word & 0x00000FFF // clear 31:12
428 return kept | ((imm_hi & 0xFFFFF) << 12)
429}
430
431// Patch the J-type bit-scrambled 21-bit signed immediate.
432func patch_j_imm(word: i64, imm: i64) -> i64 {
433 let kept: i64 = word & 0x00000FFF
434 let b20: i64 = (imm >> 20) & 1
435 let b10_1: i64 = (imm >> 1) & 0x3FF
436 let b11: i64 = (imm >> 11) & 1
437 let b19_12: i64 = (imm >> 12) & 0xFF
438 let scrambled: i64 = (b20 << 31) | (b10_1 << 21) | (b11 << 20) | (b19_12 << 12)
439 return kept | scrambled
440}
441
442// Patch the B-type bit-scrambled 13-bit signed immediate.
443func patch_b_imm(word: i64, imm: i64) -> i64 {
444 let kept: i64 = word & 0x01FFF07F
445 let b12: i64 = (imm >> 12) & 1
446 let b10_5: i64 = (imm >> 5) & 0x3F
447 let b4_1: i64 = (imm >> 1) & 0xF
448 let b11: i64 = (imm >> 11) & 1
449 let scrambled: i64 = (b12 << 31) | (b10_5 << 25) | (b4_1 << 8) | (b11 << 7)
450 return kept | scrambled
451}
452
453// "Hi 20 + Lo 12" split that handles sign extension correctly:
454// the lo12 immediate is sign-extended at use, so if its top bit
455// is set we must add 0x800 to the hi20 to compensate.
456func split_hi20(value: i64) -> i64 {
457 return (value + 0x800) >> 12
458}
459func split_lo12(value: i64) -> i64 {
460 let hi: i64 = split_hi20(value)
461 return value - (hi << 12)
462}
463
464// Apply one relocation in-place to text_buf.
465// Returns 0 on success, -1 on unknown rtype, -2 on unresolved sym.
466func nxld_apply_one(r: *Reloc, text_buf: *u8, text_vaddr: i64,
467 syms_pool: *Symbol, n_syms: i64) -> i64 {
468 if r.sym_idx >= n_syms { return -2 }
469 let lbase: i64 = syms_pool as i64
470 let sym: *Symbol = (lbase + r.sym_idx * 56) as *Symbol
471 let resolved: i64 = sym.resolved_addr + r.addend
472
473 if r.rtype == R_RISCV_64 {
474 // 8-byte absolute write
475 text_buf[r.offset + 0] = resolved & 0xFF
476 text_buf[r.offset + 1] = (resolved >> 8) & 0xFF
477 text_buf[r.offset + 2] = (resolved >> 16) & 0xFF
478 text_buf[r.offset + 3] = (resolved >> 24) & 0xFF
479 text_buf[r.offset + 4] = (resolved >> 32) & 0xFF
480 text_buf[r.offset + 5] = (resolved >> 40) & 0xFF
481 text_buf[r.offset + 6] = (resolved >> 48) & 0xFF
482 text_buf[r.offset + 7] = (resolved >> 56) & 0xFF
483 return 0
484 }
485
486 // For all instruction-patch relocations, read the current word.
487 let cur_off: i64 = r.offset
488 let cur_word: i64 =
489 text_buf[cur_off]
490 | (text_buf[cur_off + 1] << 8)
491 | (text_buf[cur_off + 2] << 16)
492 | (text_buf[cur_off + 3] << 24)
493 var patched: i64 = 0
494
495 if r.rtype == R_RISCV_BRANCH {
496 let pc: i64 = text_vaddr + cur_off
497 let off: i64 = resolved - pc
498 patched = patch_b_imm(cur_word, off)
499 write_u32_le(text_buf, cur_off, patched)
500 return 0
501 }
502 if r.rtype == R_RISCV_JAL {
503 let pc: i64 = text_vaddr + cur_off
504 let off: i64 = resolved - pc
505 patched = patch_j_imm(cur_word, off)
506 write_u32_le(text_buf, cur_off, patched)
507 return 0
508 }
509 if r.rtype == R_RISCV_CALL {
510 // 2-instruction sequence: auipc rd, hi20 ; jalr rd, rd, lo12
511 let pc: i64 = text_vaddr + cur_off
512 let off: i64 = resolved - pc
513 let hi: i64 = split_hi20(off)
514 let lo: i64 = split_lo12(off)
515 patched = patch_u_imm(cur_word, hi)
516 write_u32_le(text_buf, cur_off, patched)
517 let next_off: i64 = cur_off + 4
518 let next_word: i64 =
519 text_buf[next_off]
520 | (text_buf[next_off + 1] << 8)
521 | (text_buf[next_off + 2] << 16)
522 | (text_buf[next_off + 3] << 24)
523 let next_patched: i64 = patch_i_imm(next_word, lo)
524 write_u32_le(text_buf, next_off, next_patched)
525 return 0
526 }
527 if r.rtype == R_RISCV_HI20 {
528 patched = patch_u_imm(cur_word, split_hi20(resolved))
529 write_u32_le(text_buf, cur_off, patched)
530 return 0
531 }
532 if r.rtype == R_RISCV_LO12_I {
533 patched = patch_i_imm(cur_word, split_lo12(resolved))
534 write_u32_le(text_buf, cur_off, patched)
535 return 0
536 }
537 if r.rtype == R_RISCV_LO12_S {
538 patched = patch_s_imm(cur_word, split_lo12(resolved))
539 write_u32_le(text_buf, cur_off, patched)
540 return 0
541 }
542 if r.rtype == R_RISCV_PCREL_HI20 {
543 let pc: i64 = text_vaddr + cur_off
544 let off: i64 = resolved - pc
545 patched = patch_u_imm(cur_word, split_hi20(off))
546 write_u32_le(text_buf, cur_off, patched)
547 return 0
548 }
549 if r.rtype == R_RISCV_PCREL_LO12_I {
550 let pc: i64 = text_vaddr + cur_off
551 let off: i64 = resolved - pc
552 patched = patch_i_imm(cur_word, split_lo12(off))
553 write_u32_le(text_buf, cur_off, patched)
554 return 0
555 }
556 return -1
557}
558
559// === layout pass =====================================================
560//
561// Assigns final virtual addresses to sections + resolves every
562// defined symbol's resolved_addr. Single-object linking only for
563// v0.0.1 (no inter-object symbol resolution yet).
564//
565// Memory layout matches elf_writer.nx for consistency:
566// 0x10000 ELF header (64 bytes)
567// 0x10040 program header (56 bytes)
568// 0x10078 .text section starts
569// 0x10078+text_len .data section (round to next 16-byte boundary)
570//
571// Returns the start vaddr of .text after layout (typically 0x10078).
572const NXLD_BASE_VADDR: i64 = 0x10000
573const NXLD_HEADER_BYTES: i64 = 120 // 64 ELF hdr + 56 phdr
574
575func nxld_layout(obj: *ElfObject, syms_pool: *Symbol, n_syms: i64,
576 out_text_vaddr: *i64, out_data_vaddr: *i64) -> i64 {
577 let text_vaddr: i64 = NXLD_BASE_VADDR + NXLD_HEADER_BYTES
578 *out_text_vaddr = text_vaddr
579
580 // .data follows .text, 16-byte aligned.
581 let after_text: i64 = text_vaddr + obj.text_len
582 let aligned: i64 = (after_text + 15) & ~15
583 *out_data_vaddr = aligned
584
585 // Resolve every defined symbol. For v0.0.1 we treat any symbol
586 // with section_idx > 0 as living in either .text or .data based
587 // on the section we recorded. A real linker would carry per-
588 // section vaddrs through; here single-section .text covers the
589 // common case (most NishiLang functions live in .text).
590 var i: i64 = 0
591 let lbase: i64 = syms_pool as i64
592 while i < n_syms {
593 let sym: *Symbol = (lbase + i * 56) as *Symbol
594 if sym.section_idx == 0 {
595 // Undefined symbol -- needs cross-object resolution
596 // (out of scope for v0.0.1 single-object linker).
597 sym.resolved_addr = 0
598 }
599 if sym.section_idx > 0 {
600 // For v0.0.1: assume the section is .text. A real
601 // linker would map section_idx -> assigned vaddr base.
602 sym.resolved_addr = text_vaddr + sym.value
603 }
604 i = i + 1
605 }
606 return 0
607}
608
609// === top-level link entry ============================================
610//
611// Take a single input ELF (relocatable .o), apply its relocations,
612// emit a statically-linked ELF executable to fd.
613//
614// Returns the byte count written, or negative on error.
615
616import "elf_writer.nx"
617
618func nxld_link(obj: *ElfObject, fd: i64) -> i64 {
619 // Pre-flight: object must have a .text section.
620 if obj.text_len == 0 { return -100 }
621
622 // Load symbols into a working pool.
623 let n_syms: i64 = nxld_symbol_count(obj)
624 let syms_raw: *u8 = sys_mmap(n_syms * 56 + 64)
625 let syms: *Symbol = syms_raw as *Symbol
626 var i: i64 = 0
627 let lbase: i64 = syms as i64
628 while i < n_syms {
629 let s: *Symbol = (lbase + i * 56) as *Symbol
630 nxld_read_symbol(obj, i, s)
631 i = i + 1
632 }
633
634 // Layout: assign vaddrs + resolve symbols.
635 let tva_raw: *u8 = sys_mmap(16)
636 let dva_raw: *u8 = sys_mmap(16)
637 let tva: *i64 = tva_raw as *i64
638 let dva: *i64 = dva_raw as *i64
639 nxld_layout(obj, syms, n_syms, tva, dva)
640
641 // Allocate a writable copy of the .text bytes (relocations
642 // patch into this).
643 let text_buf: *u8 = sys_mmap(obj.text_len + 64)
644 var k: i64 = 0
645 while k < obj.text_len {
646 text_buf[k] = obj.base[obj.text_off + k]
647 k = k + 1
648 }
649
650 // Apply relocations.
651 let n_relocs: i64 = nxld_reloc_count(obj)
652 let reloc_raw: *u8 = sys_mmap(64)
653 let reloc: *Reloc = reloc_raw as *Reloc
654 var ri: i64 = 0
655 while ri < n_relocs {
656 nxld_read_reloc(obj, ri, reloc)
657 let arc: i64 = nxld_apply_one(reloc, text_buf, *tva, syms, n_syms)
658 if arc < 0 { return -200 + arc }
659 ri = ri + 1
660 }
661
662 // Emit ELF wrapping the patched text. elf_writer's write_elf
663 // handles the standard 0x10000 base; matches our layout.
664 return write_elf(text_buf, obj.text_len, fd)
665}
666
667// === self-test ===
668
669func main() -> i64 {
670 // Build a minimal valid ELF64 RV64 header in a buffer + verify
671 // nxld_open accepts it.
672 let buf: *u8 = sys_mmap(128)
673 buf[0] = ELF_MAGIC0; buf[1] = ELF_MAGIC1
674 buf[2] = ELF_MAGIC2; buf[3] = ELF_MAGIC3
675 buf[4] = EI_CLASS64; buf[5] = EI_DATA2LSB
676 buf[6] = 1 // EI_VERSION
677 // e_type = ET_REL
678 buf[16] = 1; buf[17] = 0
679 // e_machine = EM_RISCV (0xF3)
680 buf[18] = 0xF3; buf[19] = 0
681 // e_version = 1
682 buf[20] = 1; buf[21] = 0; buf[22] = 0; buf[23] = 0
683
684 let obj: *ElfObject = sys_mmap(128) as *ElfObject
685 let rc: i64 = nxld_open(obj, buf, 64)
686 if rc != 0 { return __syscall(93, 100 + (0 - rc), 0, 0, 0, 0, 0) }
687 if obj.e_type != ET_REL { return __syscall(93, 50, 0, 0, 0, 0, 0) }
688 if obj.e_machine != EM_RISCV { return __syscall(93, 51, 0, 0, 0, 0, 0) }
689
690 // Symbol table API: with no symtab section in our minimal
691 // fixture, count is 0 and reads are out of bounds.
692 if nxld_symbol_count(obj) != 0 {
693 return __syscall(93, 52, 0, 0, 0, 0, 0)
694 }
695 let sym_raw: *u8 = sys_mmap(64)
696 let sym: *Symbol = sym_raw as *Symbol
697 if nxld_read_symbol(obj, 0, sym) != -1 {
698 return __syscall(93, 53, 0, 0, 0, 0, 0)
699 }
700
701 // Relocation API: with no .rela.text either, count 0 +
702 // reads -1.
703 if nxld_reloc_count(obj) != 0 {
704 return __syscall(93, 54, 0, 0, 0, 0, 0)
705 }
706 let rel_raw: *u8 = sys_mmap(64)
707 let rel: *Reloc = rel_raw as *Reloc
708 if nxld_read_reloc(obj, 0, rel) != -1 {
709 return __syscall(93, 55, 0, 0, 0, 0, 0)
710 }
711
712 // Layout pass: with no symbols + zero-length text in our
713 // minimal ELF fixture, the layout returns the standard
714 // base+header offsets and resolves zero symbols.
715 let tva_raw: *u8 = sys_mmap(16)
716 let dva_raw: *u8 = sys_mmap(16)
717 let tva: *i64 = tva_raw as *i64
718 let dva: *i64 = dva_raw as *i64
719 let lo_syms_raw: *u8 = sys_mmap(64)
720 let lo_syms: *Symbol = lo_syms_raw as *Symbol
721 nxld_layout(obj, lo_syms, 0, tva, dva)
722 if *tva != 0x10078 {
723 return __syscall(93, 58, 0, 0, 0, 0, 0)
724 }
725
726 // Apply pass: build a synthetic R_RISCV_HI20 relocation against
727 // a fake symbol resolved to 0x12345000. Verify the patched
728 // word's high 20 bits hold the expected value.
729 let text_buf: *u8 = sys_mmap(64)
730 text_buf[0] = 0x37; text_buf[1] = 0x00; text_buf[2] = 0x00; text_buf[3] = 0x00
731 // Synthetic Symbol[1] with resolved_addr = 0x12345000.
732 let syms_raw: *u8 = sys_mmap(128)
733 let syms: *Symbol = syms_raw as *Symbol
734 let s0: *Symbol = syms
735 let lbase: i64 = syms as i64
736 let s1: *Symbol = (lbase + 56) as *Symbol
737 s1.resolved_addr = 0x12345000
738 rel.offset = 0
739 rel.sym_idx = 1
740 rel.rtype = R_RISCV_HI20
741 rel.addend = 0
742 let arc: i64 = nxld_apply_one(rel, text_buf, 0x10000, syms, 2)
743 if arc != 0 { return __syscall(93, 56, 0, 0, 0, 0, 0) }
744 // Patched word's high 20 bits should encode 0x12345 (after sign
745 // adjust: low12 of 0x12345000 = 0; no adjustment needed).
746 let pword: i64 = text_buf[0] | (text_buf[1] << 8)
747 | (text_buf[2] << 16) | (text_buf[3] << 24)
748 let patched_hi: i64 = (pword >> 12) & 0xFFFFF
749 if patched_hi != 0x12345 {
750 return __syscall(93, 57, 0, 0, 0, 0, 0)
751 }
752
753 return __syscall(93, 42, 0, 0, 0, 0, 0)
754}