code wiki / _hdl_build / nx_eng_asm_scan.nx
nx_eng_asm_scan.nx source
↩ module page · 228 lines · 9948 B
1// nx_eng_asm_scan.nx -- the ENGINEER reads the bug straight from the MACHINE CODE. Not a
2// synthetic probe (those don't reproduce it) -- a single deterministic pass over the
3// emitted x86-64 assembly that finds the register-pressure miscompile WHERE IT LIVES:
4//
5// a CALLER-SAVED register is written, a `call` happens, then that register is READ
6// again with no spill+reload in between -> the call clobbered it -> wrong value.
7//
8// x86-64 SysV: caller-saved = rax,rcx,rdx,rsi,rdi,r8,r9,r10,r11 (a call destroys them);
9// callee-saved = rbx,rbp,r12-r15 (survive). Per-register state: CLEAN(0) -> FRESH(1) on a
10// write -> CLOBBERED(2) when a call crosses it while FRESH. A read of a CLOBBERED caller-
11// saved reg is the bug; a write or a reload-from-memory makes it FRESH (safe) again. State
12// resets at each function label. One pass, milliseconds -- identify from machine code up.
13// RACI: detection only. The Engineer reports (reg + the clobbering call's line); the Doctor
14// consumes that artifact to fix. license_tier: ORIGINAL
15
16import "nx_syscalls.nx"
17
18const AS_CLEAN: i64 = 0
19const AS_FRESH: i64 = 1
20const AS_CLOBBERED: i64 = 2
21
22func as_isalnum(c: i64) -> i64 {
23 if c >= 48 { if c <= 57 { return 1 } }
24 if c >= 65 { if c <= 90 { return 1 } }
25 if c >= 97 { if c <= 122 { return 1 } }
26 if c == 95 { return 1 }
27 return 0
28}
29
30// does buf[p..] equal the NUL-terminated literal lit, ending on a word boundary?
31func as_match(buf: *u8, p: i64, lim: i64, lit: *u8) -> i64 {
32 var j: i64 = 0
33 while lit[j] != (0 as u8) {
34 if p + j >= lim { return 0 }
35 if buf[p + j] != lit[j] { return 0 }
36 j = j + 1
37 }
38 if p + j < lim { if as_isalnum(buf[p + j] as i64) == 1 { return 0 } }
39 return 1
40}
41
42// buf[p] is just past a '%'. Identify the 64-bit register; return id 0..15 or -1.
43func as_reg_at(buf: *u8, p: i64, lim: i64) -> i64 {
44 if as_match(buf, p, lim, "rax" as *u8) == 1 { return 0 }
45 if as_match(buf, p, lim, "rbx" as *u8) == 1 { return 1 }
46 if as_match(buf, p, lim, "rcx" as *u8) == 1 { return 2 }
47 if as_match(buf, p, lim, "rdx" as *u8) == 1 { return 3 }
48 if as_match(buf, p, lim, "rsi" as *u8) == 1 { return 4 }
49 if as_match(buf, p, lim, "rdi" as *u8) == 1 { return 5 }
50 if as_match(buf, p, lim, "rbp" as *u8) == 1 { return 6 }
51 if as_match(buf, p, lim, "rsp" as *u8) == 1 { return 7 }
52 if as_match(buf, p, lim, "r10" as *u8) == 1 { return 10 }
53 if as_match(buf, p, lim, "r11" as *u8) == 1 { return 11 }
54 if as_match(buf, p, lim, "r12" as *u8) == 1 { return 12 }
55 if as_match(buf, p, lim, "r13" as *u8) == 1 { return 13 }
56 if as_match(buf, p, lim, "r14" as *u8) == 1 { return 14 }
57 if as_match(buf, p, lim, "r15" as *u8) == 1 { return 15 }
58 if as_match(buf, p, lim, "r8" as *u8) == 1 { return 8 }
59 if as_match(buf, p, lim, "r9" as *u8) == 1 { return 9 }
60 return 0 - 1
61}
62
63func as_is_caller(id: i64) -> i64 {
64 if id == 0 { return 1 }
65 if id == 2 { return 1 }
66 if id == 3 { return 1 }
67 if id == 4 { return 1 }
68 if id == 5 { return 1 }
69 if id == 8 { return 1 }
70 if id == 9 { return 1 }
71 if id == 10 { return 1 }
72 if id == 11 { return 1 }
73 return 0
74}
75
76// find the first '%'-register inside [s,e); set ismem[0]=1 if a '(' appears in the operand.
77func as_operand_reg(buf: *u8, s: i64, e: i64, ismem: *i64) -> i64 {
78 ismem[0] = 0
79 var i: i64 = s
80 var reg: i64 = 0 - 1
81 while i < e {
82 if buf[i] == (40 as u8) { ismem[0] = 1 } // '('
83 if buf[i] == (37 as u8) { if reg < 0 { reg = as_reg_at(buf, i + 1, e) } } // '%'
84 i = i + 1
85 }
86 return reg
87}
88
89// classify the mnemonic token [ms,me): 0 ignore, 1 move(write dst), 2 arith(rmw dst),
90// 3 push(read), 4 pop(write), 5 call, 6 cmp/test(read only).
91func as_mnem_class(buf: *u8, ms: i64, lim: i64) -> i64 {
92 if as_match(buf, ms, lim, "call" as *u8) == 1 { return 5 }
93 if as_match(buf, ms, lim, "callq" as *u8) == 1 { return 5 }
94 if as_match(buf, ms, lim, "pushq" as *u8) == 1 { return 3 }
95 if as_match(buf, ms, lim, "popq" as *u8) == 1 { return 4 }
96 if as_match(buf, ms, lim, "movq" as *u8) == 1 { return 1 }
97 if as_match(buf, ms, lim, "movabsq" as *u8) == 1 { return 1 }
98 if as_match(buf, ms, lim, "leaq" as *u8) == 1 { return 1 }
99 if as_match(buf, ms, lim, "addq" as *u8) == 1 { return 2 }
100 if as_match(buf, ms, lim, "subq" as *u8) == 1 { return 2 }
101 if as_match(buf, ms, lim, "xorq" as *u8) == 1 { return 2 }
102 if as_match(buf, ms, lim, "andq" as *u8) == 1 { return 2 }
103 if as_match(buf, ms, lim, "orq" as *u8) == 1 { return 2 }
104 if as_match(buf, ms, lim, "imulq" as *u8) == 1 { return 2 }
105 if as_match(buf, ms, lim, "cmpq" as *u8) == 1 { return 6 }
106 if as_match(buf, ms, lim, "testq" as *u8) == 1 { return 6 }
107 return 0
108}
109
110// READ check: a read of a CLOBBERED caller-saved reg is the bug -> record a flag.
111func as_read(reg: i64, state: *i64, clobline: *i64, fl_reg: *i64, fl_line: *i64, nf: *i64) -> i64 {
112 if reg < 0 { return 0 }
113 if as_is_caller(reg) == 1 { if state[reg] == AS_CLOBBERED {
114 let k: i64 = nf[0]
115 fl_reg[k] = reg; fl_line[k] = clobline[reg]; nf[0] = k + 1
116 state[reg] = AS_FRESH // report once, then treat as redefined
117 } }
118 return 0
119}
120func as_write(reg: i64, state: *i64) -> i64 { if reg >= 0 { state[reg] = AS_FRESH } return 0 }
121
122// is c whitespace (space or tab)?
123func as_is_ws(c: i64) -> i64 { if c == 32 { return 1 } if c == 9 { return 1 } return 0 }
124
125// process one line [ls,le) at the given line index: update register state and record flags.
126func as_proc_line(buf: *u8, ls: i64, le: i64, line: i64, state: *i64, clobline: *i64,
127 fl_reg: *i64, fl_line: *i64, nf: *i64) -> i64 {
128 // skip leading whitespace -> ms (first non-ws, or le)
129 var ms: i64 = ls
130 var d1: i64 = 0
131 while d1 == 0 { if ms >= le { d1 = 1 } else { if as_is_ws(buf[ms] as i64) == 1 { ms = ms + 1 } else { d1 = 1 } } }
132 if ms >= le { return 0 } // blank line
133 if buf[ms] == (46 as u8) { return 0 } // '.' directive
134
135 // read mnemonic token [ms, me) -- ends at ws, ':' or le
136 var me: i64 = ms
137 var d2: i64 = 0
138 while d2 == 0 {
139 if me >= le { d2 = 1 }
140 else { if as_is_ws(buf[me] as i64) == 1 { d2 = 1 } else { if buf[me] == (58 as u8) { d2 = 1 } else { me = me + 1 } } }
141 }
142 if me < le { if buf[me] == (58 as u8) { // 'name:' label -> reset function state
143 var r: i64 = 0
144 while r < 16 { state[r] = AS_CLEAN; r = r + 1 }
145 return 0
146 } }
147
148 let cls: i64 = as_mnem_class(buf, ms, le)
149 if cls == 0 { return 0 }
150
151 // operands start after the mnemonic; split on the first comma
152 var os: i64 = me
153 var d3: i64 = 0
154 while d3 == 0 { if os >= le { d3 = 1 } else { if as_is_ws(buf[os] as i64) == 1 { os = os + 1 } else { d3 = 1 } } }
155 var comma: i64 = 0 - 1
156 var p: i64 = os
157 while p < le { if buf[p] == (44 as u8) { if comma < 0 { comma = p } } p = p + 1 }
158
159 let mem1: *i64 = sys_mmap(8) as *i64
160 let mem2: *i64 = sys_mmap(8) as *i64
161 var o1r: i64 = 0 - 1
162 var o2r: i64 = 0 - 1
163 mem1[0] = 0; mem2[0] = 0
164 if comma < 0 {
165 o1r = as_operand_reg(buf, os, le, mem1)
166 } else {
167 o1r = as_operand_reg(buf, os, comma, mem1)
168 o2r = as_operand_reg(buf, comma + 1, le, mem2)
169 }
170
171 if cls == 5 { // call: clobber caller-saved FRESH regs
172 var r: i64 = 0
173 while r < 16 { if as_is_caller(r) == 1 { if state[r] == AS_FRESH { state[r] = AS_CLOBBERED; clobline[r] = line } } r = r + 1 }
174 return 0
175 }
176 if cls == 3 { as_read(o1r, state, clobline, fl_reg, fl_line, nf); return 0 } // push: read
177 if cls == 4 { as_write(o1r, state); return 0 } // pop: write
178 if cls == 6 { // cmp/test: read both
179 as_read(o1r, state, clobline, fl_reg, fl_line, nf)
180 as_read(o2r, state, clobline, fl_reg, fl_line, nf)
181 return 0
182 }
183 // cls 1 (move) or 2 (arith): reads first, then write dst
184 if o1r >= 0 { as_read(o1r, state, clobline, fl_reg, fl_line, nf) }
185 if mem2[0] == 1 { if o2r >= 0 { as_read(o2r, state, clobline, fl_reg, fl_line, nf) } } // store: base read
186 if cls == 2 { if o2r >= 0 { as_read(o2r, state, clobline, fl_reg, fl_line, nf) } } // arith reads dst too
187 if mem2[0] == 0 { if o2r >= 0 { as_write(o2r, state) } } // dst write (bare reg)
188 return 0
189}
190
191// SCAN a whole assembly file. Fills fl_reg[]/fl_line[] (the clobbering call's line index);
192// returns the flag count.
193func eng_asm_scan(path: *u8, fl_reg: *i64, fl_line: *i64) -> i64 {
194 let lenp: *i64 = sys_mmap(8) as *i64
195 let buf: *u8 = sys_read_file(path, lenp)
196 let blen: i64 = lenp[0]
197 let state: *i64 = sys_mmap(8 * 16) as *i64
198 let clobline: *i64 = sys_mmap(8 * 16) as *i64
199 let nf: *i64 = sys_mmap(8) as *i64
200 nf[0] = 0
201 var r: i64 = 0
202 while r < 16 { state[r] = AS_CLEAN; clobline[r] = 0; r = r + 1 }
203
204 var line: i64 = 0
205 var i: i64 = 0
206 while i < blen {
207 var le: i64 = i
208 var done: i64 = 0
209 while done == 0 { if le >= blen { done = 1 } else { if buf[le] == (10 as u8) { done = 1 } else { le = le + 1 } } }
210 as_proc_line(buf, i, le, line, state, clobline, fl_reg, fl_line, nf)
211 i = le + 1
212 line = line + 1
213 }
214 return nf[0]
215}
216
217func as_regname(id: i64) -> *u8 {
218 if id == 0 { return "rax" as *u8 }
219 if id == 2 { return "rcx" as *u8 }
220 if id == 3 { return "rdx" as *u8 }
221 if id == 4 { return "rsi" as *u8 }
222 if id == 5 { return "rdi" as *u8 }
223 if id == 8 { return "r8" as *u8 }
224 if id == 9 { return "r9" as *u8 }
225 if id == 10 { return "r10" as *u8 }
226 if id == 11 { return "r11" as *u8 }
227 return "r?" as *u8
228}