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}