nxasm_x86_main.nx source
↩ module page · 244 lines · 12606 B
1// RECOVERED + ADAPTED into buildroot/runtime/ 2026-08-07 (debts 1786109488 / 1786109910).
2// This driver was ABSENT from the NAS build tree: buildroot/nxasm/ does not exist and no
3// *x86_main*.nx existed under buildroot, so the x86 assembler -- half the sovereign toolchain --
4// had a LIVE binary and no source to rebuild it from. Sole copy was nxc2/nxasm/ on the laptop.
5//
6// TWO ADAPTATIONS, both forced by consolidations this tree has had since the copy was taken:
7// (1) import "../runtime/nx_syscalls_x86_64.nx" -> "nx_syscalls_x86_64.nx". The relative path
8// only resolves from a sibling dir; buildroot/nxasm/ could not be created (nx_fs_write does
9// not make parents -- silent exit 4 -- and nx_mkdirp is allowlisted to knowledge/|sites/|/tmp/).
10// (2) sys_read_file_x86_64 -> sys_read_file. THIS ONE NEARLY WENT THE WRONG WAY. The symbol is
11// defined NOWHERE in buildroot, and three laptop copies (.build/runtime, .tmp-rv64/inputs,
12// bench/ir_diff_corpus) hold an 11,763 B nx_syscalls_x86_64.nx that DOES define it -- so the
13// obvious read was "buildroot's 1,388 B copy is truncated, restore the big one".
14// THAT READ IS WRONG. buildroot's copy is a DELIBERATE ALIAS STUB (2026-07-31, debt
15// 1785528831) forwarding to nx_syscalls.nx, which now owns the wrapper set once. It exists
16// to FIX a measured defect: 37 translation units reached BOTH layers, held every wrapper
17// twice, and resolved silently by definition order. Restoring the 11,763 B file would have
18// reverted that fix and re-armed the double-definition bug.
19// READING THE FILE'S OWN HEADER IS WHAT STOPPED IT.
20// ★A SMALLER FILE IS NOT EVIDENCE OF TRUNCATION -- IT CAN BE THE RESULT OF A CONSOLIDATION THAT
21// MOVED THE CONTENT SOMEWHERE BETTER. ASK THE FILE BEFORE YOU RESTORE OVER IT.
22//
23// ⚠NOT YET PROVEN AUTHENTIC: a successful build does NOT establish that this is the source the
24// live nxasm_x86_main.elf was built from. buildroot/_build/nxasm_x86_main.s survives from a prior
25// build -- rebuild-and-compare against it (or against the live ELF) is the real authenticity test.// nxasm_x86_main.nx -- CLI front-end for the sovereign x86_64 assembler.
26//
27// Usage: nxasm_x86 <input.s> <output.elf>
28//
29// Reads an nxc2 --target x86_64 AT&T .s file, assembles it to machine
30// code via nxasm_x86_assemble, wraps it in a static ELF (entry at the
31// _start label), and writes a runnable binary. This single tool
32// replaces BOTH `gcc-as` and `ld` on the x86_64 output path -- the
33// whole-program model nxld uses (no intermediate .o). gcc/as/ld are
34// retained ONLY as one-time Wheeler bootstrap + byte-exact oracle.
35//
36// license_tier: ORIGINAL
37
38import "nxasm_x86.nx"
39// DWARF v5 .debug_line builder. Its header emitter is validated against GNU readelf byte-for-byte
40// (knowledge/dwarf_debugline_fixture_2026_08_07.md) -- NOT against our own decoder, which has no
41// header support and would only have proved two organs by one author agree (debt 1786111455).
42import "nx_dwarf_line.nx"
43// DDR-009 step 2: .debug_info / .debug_abbrev emitters. Imported EXPLICITLY -- a transitive symbol
44// is a coincidence, not a dependency.
45import "nx_dwarf_info.nx"
46import "nx_dwarf_abbrev.nx"
47import "nx_syscalls_x86_64.nx"
48// Crash diagnostics (v11 rung 1a, 2026-08-13): an assembler SEGV used to print NOTHING.
49// One call at main entry turns it into instruction+fault addresses on stderr.
50import "nx_crash.nx"
51
52// 16 MiB code + ELF buffers -- the rebuilt self-host compiler emits a 3 MB .s; 1 MiB was a
53// module-scale assumption that the compiler-scale target broke (2026-06-09).
54const NXASM_BUF: i64 = 16777216
55const NXASM_PATH_MAX: i64 = 4096
56
57// bounded strlen: byte length up to NUL, or -1 if no NUL within NXASM_PATH_MAX.
58// Caller must guarantee s is READABLE for up to NXASM_PATH_MAX bytes (never call on
59// a possibly-wild pointer the kernel has not just validated).
60func nxm_clen(s: *u8) -> i64 {
61 var n: i64 = 0
62 while n < NXASM_PATH_MAX {
63 if s[n] == (0 as u8) { return n }
64 n = n + 1
65 }
66 return 0 - 1
67}
68
69// write a NUL-terminated string to fd (bounded). Only call on a validated pointer.
70func nxm_puts(fd: i64, s: *u8) -> i64 {
71 let L: i64 = nxm_clen(s)
72 if L < 0 { return sys_write(fd, "<unterminated>" as *u8, 14) }
73 return sys_write(fd, s, L)
74}
75
76// write a signed decimal i64 to fd.
77func nxm_putn(fd: i64, v: i64) -> i64 {
78 let bb: *u8 = sys_mmap(28)
79 var m: i64 = v
80 if m < 0 { sys_write(fd, "-" as *u8, 1); m = 0 - m }
81 let t: *u8 = sys_mmap(28)
82 var k: i64 = 0
83 if m == 0 { t[0] = 48 as u8; k = 1 }
84 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
85 var i: i64 = 0
86 while i < k { bb[i] = t[k - 1 - i]; i = i + 1 }
87 return sys_write(fd, bb, k)
88}
89
90func main(argc: i64, argv: *i64) -> i64 {
91 nx_crash_guard()
92 if argc < 3 { return 2 } // usage error
93 let inpath: *u8 = argv[1] as *u8
94 let outpath: *u8 = argv[2] as *u8
95
96 let lenbox: *i64 = sys_mmap(16) as *i64
97 let src: *u8 = sys_read_file(inpath, lenbox)
98 if (src as i64) == 0 { return 3 }
99 let n: i64 = lenbox[0]
100 if n <= 0 { return 4 }
101
102 let code: *u8 = sys_mmap(NXASM_BUF)
103 let entrybox: *i64 = sys_mmap(16) as *i64
104 let code_len: i64 = nxasm_x86_assemble(src, n, code, NXASM_BUF, entrybox)
105 if code_len < 0 { return 5 }
106
107 // ---- DEBUG LINE (DDR-002) ------------------------------------------------
108 // The .s carries .file/.loc ONLY when the compiler was run with -g, so the presence of rows
109 // IS the opt-in: no rows -> dbg_len 0 -> x86_build_elf_dbg_at returns the original bytes and
110 // this build is byte-identical to a toolchain with no DWARF at all.
111 // Addresses are made ABSOLUTE here (X86_BASE + X86_HDRLEN + text offset) because a debugger
112 // hands back a runtime PC, not a section offset -- the assembler is the first place that knows
113 // both, which is exactly why the line table has to be built here and not in the compiler.
114 var dbg_ptr: *u8 = 0 as *u8
115 var dbg_len: i64 = 0
116 if axc_n_loc > 0 {
117 if axc_n_dfile > 0 {
118 let dw: *NxDwLine = nx_dwline_new(65536)
119 nx_dwline_header(dw, src, axc_dfile_off, axc_dfile_len, axc_n_dfile)
120 nx_dwline_set_address(dw, X86_BASE + X86_HDRLEN + axc_loc_off[0])
121 var r: i64 = 0
122 while r < axc_n_loc {
123 nx_dwline_record(dw, X86_BASE + X86_HDRLEN + axc_loc_off[r],
124 axc_loc_file[r], axc_loc_line[r])
125 r = r + 1
126 }
127 nx_dwline_end_sequence(dw)
128 nx_dwline_finish(dw)
129 dbg_ptr = nx_dwline_bytes(dw)
130 dbg_len = nx_dwline_size(dw)
131 }
132 }
133
134 // ---- DEBUG INFO (DDR-009 step 2) --------------------------------------------------------
135 // .debug_line answers "which source line is this address"; .debug_info answers "which FUNCTION
136 // am I in", which is what makes a backtrace name its frames. Both are needed and neither
137 // substitutes for the other.
138 // Gated on the SAME signal as the line table -- the presence of .type rows, which only appear
139 // when the compiler ran under -g -- so a non-debug build still emits nothing and stays
140 // byte-identical (proven by nx_nxasm_neutral_gate).
141 var info_ptr: *u8 = 0 as *u8
142 var info_len: i64 = 0
143 var abbr_ptr: *u8 = 0 as *u8
144 var abbr_len: i64 = 0
145 if axc_n_fn > 0 {
146 if axc_n_loc > 0 {
147 // Abbrev table: 1 = compile_unit (has children), 2 = subprogram (leaf).
148 let ab: *NxDwAbbrev = nx_dwabbrev_new(4096)
149 nx_dwabbrev_begin(ab, NX_DW_TAG_compile_unit, 1)
150 nx_dwabbrev_attr(ab, NX_DW_AT_name, NX_DW_FORM_string)
151 nx_dwabbrev_attr(ab, NX_DW_AT_low_pc, NX_DW_FORM_addr)
152 nx_dwabbrev_attr(ab, NX_DW_AT_high_pc, NX_DW_FORM_addr)
153 nx_dwabbrev_end(ab)
154 nx_dwabbrev_begin(ab, NX_DW_TAG_subprogram, 0)
155 nx_dwabbrev_attr(ab, NX_DW_AT_name, NX_DW_FORM_string)
156 nx_dwabbrev_attr(ab, NX_DW_AT_low_pc, NX_DW_FORM_addr)
157 nx_dwabbrev_attr(ab, NX_DW_AT_high_pc, NX_DW_FORM_addr)
158 nx_dwabbrev_end(ab)
159 nx_dwabbrev_finish(ab)
160
161 let df: *NxDwInfo = nx_dwinfo_new(65536)
162 nx_dwinfo_cu_begin(df, 0)
163 nx_dwinfo_die_open(df, 1)
164 let cuname: *u8 = sys_mmap(512)
165 var cn: i64 = 0
166 if axc_n_dfile > 0 {
167 while cn < axc_dfile_len[0] { cuname[cn] = src[axc_dfile_off[0] + cn]; cn = cn + 1 }
168 }
169 cuname[cn] = 0 as u8
170 nx_dwinfo_attr_string(df, cuname)
171 nx_dwinfo_attr_addr(df, X86_BASE + X86_HDRLEN)
172 nx_dwinfo_attr_addr(df, X86_BASE + X86_HDRLEN + code_len)
173
174 let fname: *u8 = sys_mmap(512)
175 var fi: i64 = 0
176 while fi < axc_n_fn {
177 var q: i64 = 0
178 var flen: i64 = axc_fn_len[fi]
179 if flen > 500 { flen = 500 }
180 while q < flen { fname[q] = src[axc_fn_off[fi] + q]; q = q + 1 }
181 fname[flen] = 0 as u8
182 // high_pc is DERIVED, never stored: the next function starts where this one ends,
183 // and the last runs to the end of .text. Storing it would be a second source of
184 // truth that can drift from the table it was derived from.
185 var hi: i64 = code_len
186 if fi + 1 < axc_n_fn { hi = axc_fn_low[fi + 1] }
187 nx_dwinfo_die_open(df, 2)
188 nx_dwinfo_attr_string(df, fname)
189 nx_dwinfo_attr_addr(df, X86_BASE + X86_HDRLEN + axc_fn_low[fi])
190 nx_dwinfo_attr_addr(df, X86_BASE + X86_HDRLEN + hi)
191 fi = fi + 1
192 }
193 nx_dwinfo_die_null(df)
194 nx_dwinfo_cu_finalize(df)
195
196 info_ptr = nx_dwinfo_bytes(df)
197 info_len = nx_dwinfo_size(df)
198 abbr_ptr = nx_dwabbrev_bytes(ab)
199 abbr_len = nx_dwabbrev_size(ab)
200 nxm_puts(2, "nxasm_x86: debug-info subprograms=" as *u8)
201 nxm_putn(2, axc_n_fn)
202 nxm_puts(2, "\n" as *u8)
203 }
204 }
205
206 let elf: *u8 = sys_mmap(NXASM_BUF + 262144)
207 let total: i64 = x86_build_elf_dbg3_at(code, code_len, entrybox[0], elf, dbg_ptr, dbg_len,
208 info_ptr, info_len, abbr_ptr, abbr_len)
209
210 // BOUNDARY VALIDATION (rule 12): outpath is external (argv[2]). R1-T1-004 -- a
211 // parent-residue uninit-read once corrupted this slot, surfacing as a SILENT rc=6.
212 // NULL-check only here (a deref of a possibly-wild pointer would segfault; we let the
213 // kernel validate the string via openat, which faults safely with EFAULT).
214 if (outpath as i64) == 0 {
215 nxm_puts(2, "nxasm: FATAL outpath argv[2] is NULL -> rc=8\n" as *u8)
216 nxm_puts(2, " why the build stopped: the assembler was started without an output path, so there is nowhere to write the ELF.\n" as *u8)
217 nxm_puts(2, " fix: call nxasm_x86 <input.s> <output.elf>; a build lane that reached here dropped its second argument -- check the lane, not the assembler.\n" as *u8)
218 return 8
219 }
220 let fd: i64 = sys_openat_wr(outpath, 0x1ff) // 0777
221 if fd < 0 {
222 // LOUD rc=6 (was a silent generic return): name the openat errno + the pointer
223 // VALUE so a future R1-T1-004 recurrence is debuggable. Deref-print the path
224 // string ONLY when not EFAULT (-14) -- a wild residue pointer must never be
225 // dereferenced in userspace; the kernel already proved it unreadable.
226 nxm_puts(2, "nxasm: FATAL openat-WR failed openat_rc=" as *u8)
227 nxm_putn(2, fd)
228 nxm_puts(2, " outpath_ptr=" as *u8)
229 nxm_putn(2, outpath as i64)
230 if fd != (0 - 14) {
231 nxm_puts(2, " path='" as *u8)
232 nxm_puts(2, outpath)
233 nxm_puts(2, "'" as *u8)
234 }
235 nxm_puts(2, "\n" as *u8)
236 nxm_puts(2, " why the build stopped: the output file could not be opened for writing (the openat errno above says why: a missing directory, no permission, or a wild pointer for the path), so the ELF has nowhere to land.\n" as *u8)
237 nxm_puts(2, " fix: create the output directory or fix its permissions; an openat_rc of -14 (EFAULT) means the path pointer itself was invalid -- a build-lane defect, report it with this line.\n" as *u8)
238 return 6
239 }
240 let w: i64 = sys_write(fd, elf, total)
241 sys_close(fd)
242 if w != total { return 7 }
243 return 0
244}