Skip to content

Commit cecfcba

Browse files
dancinlifedancinlifeclaude
authored
feat(F-NSWINDOW-E5 r9): IAT name-based autobind + CMPXCHG 0F B0/B1 decoder+executor + Run E test (#31)
(1) i386_iat_autobind(): walks IMAGE_IMPORT_DESCRIPTOR → INT → IMAGE_IMPORT_BY_NAME, matches each import name against the shim registry by strcmp — no pre-knowledge of slot VAs needed. Unknown names produce fn=NULL entries (honest; run loop halts UNBOUND_IMPORT with name in last_import). Header: i386_shim_entry_t typedef + decl. (2) pe_parse: adds import_dir_rva/import_dir_size to pe_image_t; reads DataDirectory[1] for both PE32 (opt-header offset 104) and PE32+ (offset 120). (3) CMPXCHG decoder: 0F B0 (cmpxchg r/m8,r8) and 0F B1 (cmpxchg r/m32,r32) added to i386_decode.c (enum ordinals 65/66) and byte-equal i386_decode.hexa mirror (RUNEQ). Executor: compare EAX/AL with r/m; equal → ZF=1, r/m←reg; not-equal → ZF=0, EAX/AL←r/m (Intel SDM Vol.2). Adds rm_set8/mem_write8 helpers. (4) Run E: synthetic in-memory PE import directory (base 0x540000), autobind call, 18-point assertion suite — total=3, bound=3, unbound=0; mini-run through GetCurrentThreadId slot verifies end-to-end dispatch (EAX=0x1A2B, halt=ret); CMPXCHG equal + not-equal path tests. Run A (25 checks) stays green. B/D wall sentinel updated 0F B1 → 0F C1 C0 (XADD, r10 gap). own1: standard PE import resolution = LOADING (not bypass); Intel SDM CMPXCHG; validated_manjeom STAYS 0. r10 target: DX→Metal bridge (lib/loader/dx_d3d11.hexa). Co-authored-by: dancinlife <mk55911@proton.me> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 164e4ce commit cecfcba

9 files changed

Lines changed: 539 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,38 @@ All notable changes to `gamebox` are documented in this file.
66

77
### Added
88

9+
- feat(F-NSWINDOW-E5 r9): **IAT 이름 기반 자동 바인딩 + CMPXCHG 디코더·실행**
10+
— r8 이 `INFO real_pe_path=structurally_ready needs:IAT_autobind_by_import_name`
11+
으로 명시한 다음 과제를 완료한다. (1) **`i386_iat_autobind()`**
12+
(`native/i386_cpu.c`): `IMAGE_IMPORT_DESCRIPTOR` → INT(OriginalFirstThunk) →
13+
`IMAGE_IMPORT_BY_NAME`(+2 hint skip) 를 직접 파싱해 이름 문자열로 셰임 레지스트리와
14+
`strcmp` 매칭 — **슬롯 VA 사전지식 없이** 어떤 i386 PE 의 IAT 도 바인딩 가능. 미등록
15+
이름은 `fn=NULL`(정직; 런루프는 `HALT_UNBOUND_IMPORT` + `last_import` 에 이름 기록).
16+
선언은 `native/i386_cpu.h`(`i386_shim_entry_t` typedef + 함수 선언, forward-decl
17+
`const struct i386_image *`). (2) **Import Directory RVA 파싱** (`native/pe_parse.c`):
18+
`pe_image_t``import_dir_rva`/`import_dir_size` 추가, DataDirectory[1] 을 PE32
19+
(optional-header 오프셋 104) 와 PE32+(오프셋 120) 양쪽에서 읽음. (3) **CMPXCHG
20+
디코더**: `0F B0`(`cmpxchg r/m8,r8`) + `0F B1`(`cmpxchg r/m32,r32`) 를 C 디코더
21+
(`native/i386_decode.c`, enum ordinal 65·66) 와 byte-equal hexa 미러
22+
(`native/i386_decode.hexa`) 양쪽에 추가(RUNEQ 규율). 인터프리터 실행부: EAX/AL 과
23+
r/m 비교 → 같으면 ZF=1·r/m←reg, 다르면 ZF=0·EAX/AL←r/m(Intel SDM Vol.2). `rm_set8`
24+
/ `mem_write8` 보조 함수 추가. fn=NULL(autobind 미등록 슬롯)에 대한 IAT 디스패치
25+
halt 처리도 업데이트(`last_import` 이름 보존). (4) **Run E 테스트**
26+
(`native/i386_cpu_test.c`): 합성 in-memory PE import 디렉터리(base `0x540000`,
27+
`0x1000B`): `IMAGE_IMPORT_DESCRIPTOR` 1개 + INT 3개 + `IMAGE_IMPORT_BY_NAME` 3개
28+
(GetCurrentThreadId·GetTickCount·GetCommandLineW) 를 레이아웃하고 `i386_iat_autobind`
29+
호출 → total=3·bound=3·unbound=0·슬롯 VA·fn·name 18-항목 전수 검증. GetCurrentThreadId
30+
슬롯을 통한 미니런(`FF 15 [0x540100]``C3`) → `halt=ret, EAX=0x1A2B, imports_bound=1`.
31+
CMPXCHG equal 패스(EAX=ECX=`0x12345678` → ZF=1·ECX 불변)와 not-equal 패스
32+
(EAX=`0xAAAAAAAA`·ECX=`0xBBBBBBBB` → ZF=0·EAX=`0xBBBBBBBB`) 각 4개 CHECK. B/D
33+
sentinel 은 `0F B1`(이제 CMPXCHG 로 실행됨) → `0F C1 C0`(XADD, r10 진짜 갭) 으로
34+
교체. 측정 라인 `__SHIM__ PARTIAL phase=e4_iat_name_autobind names_resolved=3
35+
unbound=0 dispatch_ok=1`, `__SHIM_TEST__ PASS`(CI 게이트 · Run A 25/B 4/C 5/D 3/E 18
36+
checks 전부 green). **own1**: 표준 PE import 해석 = 자신의 import 를 네이티브 구현에
37+
묶는 **로딩**(bypass 아님); 합성 PE 구조·Intel SDM CMPXCHG — Wine/보호 없음.
38+
`validated_manjeom` 여전히 **0**. r10 목표: DX→Metal 브리지 라이브러리
39+
(`lib/loader/dx_d3d11.hexa` 스캐폴드, 0 실 Metal 호출).
40+
941
- feat(F-NSWINDOW-E5 r8): **CRT→user-entry 핸드오프 도달(합성) + BT/BTS/BTR/BTC
1042
디코더 갭 종결 + 마지막 CRT import 2개 바인딩** — r7 의 벽(미등록 IAT 호출
1143
@`0x539318`, insns=44)을 **넘어 CRT-init 의 종착점인 `call WinMain`(CRT→user-entry

native/i386_cpu.c

Lines changed: 188 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ static int mem_read16(const i386_image_t *img, uint32_t va, uint16_t *out) {
207207
*out = (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
208208
return 1;
209209
}
210+
// Byte write (E5 r9 — for CMPXCHG r/m8,r8 write-back).
211+
static int mem_write8(const i386_image_t *img, uint32_t va, uint8_t val) {
212+
if (va < img->base || (uint64_t)va + 1 > (uint64_t)img->base + img->size) return 0;
213+
img->host[va - img->base] = val;
214+
return 1;
215+
}
210216

211217
// Compute the effective address for a memory-form ModR/M (mod != 3).
212218
// Supports the [reg+disp] / [disp32] forms the entry block uses; SIB is
@@ -248,6 +254,25 @@ static int rm_get8(const i386_cpu_t *cpu, const i386_image_t *img,
248254
return 1;
249255
}
250256

257+
// Write an 8-bit value to the r/m operand. Register-direct: same byte-
258+
// register map as rm_get8 (rm<4 = low byte of gpr[rm]; rm>=4 = high byte
259+
// of gpr[rm-4] / ah/ch/dh/bh). (E5 r9 — for CMPXCHG r/m8,r8 write-back.)
260+
static int rm_set8(i386_cpu_t *cpu, const i386_image_t *img,
261+
const i386_insn_t *insn, uint8_t val, i386_halt_t *why) {
262+
int mod = (insn->modrm >> 6) & 3;
263+
if (mod == 3) {
264+
int rm = insn->modrm & 7;
265+
if (rm < 4) cpu->gpr[rm] = (cpu->gpr[rm] & ~(uint32_t)0xFFu) | (uint32_t)val;
266+
else cpu->gpr[rm - 4] = (cpu->gpr[rm - 4] & ~(uint32_t)0xFF00u)
267+
| ((uint32_t)val << 8);
268+
return 1;
269+
}
270+
uint32_t a;
271+
if (!i386_ea(cpu, insn, &a)) { *why = I386_HALT_UNSUPPORTED; return 0; }
272+
if (!mem_write8(img, a, val)) { *why = I386_HALT_OOB; return 0; }
273+
return 1;
274+
}
275+
251276
// Read a 16-bit r/m operand. Register-direct = low word of gpr[rm].
252277
static int rm_get16(const i386_cpu_t *cpu, const i386_image_t *img,
253278
const i386_insn_t *insn, uint16_t *val, i386_halt_t *why) {
@@ -776,6 +801,65 @@ void i386_cpu_run(i386_cpu_t *cpu, const i386_image_t *img,
776801
}
777802
break;
778803
}
804+
// ── CMPXCHG r/m32,r32 (E5 r9) — 0F B1 /r ─────────────────────────────
805+
// Compare EAX with r/m32; if equal: ZF=1, r/m32←reg32; else ZF=0,
806+
// EAX←r/m32. Flags set per alu_sub (CF/OF/AF/SF/ZF/PF). Intel SDM
807+
// Vol.2 CMPXCHG. Plain lock-free CRT init primitive — no protection.
808+
case I386_OP_CMPXCHG_RM_R: {
809+
int reg = (insn.modrm >> 3) & 7; // the "new value" source register
810+
i386_halt_t why = I386_HALT_UNSUPPORTED;
811+
uint32_t rm_v;
812+
if (!rm_get32(cpu, img, &insn, &rm_v, &why)) {
813+
res->halt = why; res->halt_va = eip; res->halt_op = insn.op; return;
814+
}
815+
uint32_t eax_v = cpu->gpr[I386_REG_EAX];
816+
(void)alu_sub(&cpu->eflags, eax_v, rm_v, 0); // flags per SDM CMP
817+
if (eax_v == rm_v) {
818+
// ZF already set to 1 by alu_sub (result==0); write reg into r/m
819+
if (!rm_set32(cpu, img, &insn, cpu->gpr[reg], &why)) {
820+
res->halt = why; res->halt_va = eip; res->halt_op = insn.op; return;
821+
}
822+
} else {
823+
// ZF already 0; EAX ← r/m32
824+
cpu->gpr[I386_REG_EAX] = rm_v;
825+
}
826+
break;
827+
}
828+
// ── CMPXCHG r/m8,r8 (E5 r9) — 0F B0 /r ──────────────────────────────
829+
// Compare AL with r/m8; if equal: ZF=1, r/m8←reg8; else ZF=0, AL←r/m8.
830+
// Flags set per 8-bit alu_sub semantics. Intel SDM Vol.2 CMPXCHG.
831+
case I386_OP_CMPXCHG_RM8_R8: {
832+
int reg = (insn.modrm >> 3) & 7; // 8-bit "new value" source reg
833+
i386_halt_t why = I386_HALT_UNSUPPORTED;
834+
uint8_t rm_v8;
835+
if (!rm_get8(cpu, img, &insn, &rm_v8, &why)) {
836+
res->halt = why; res->halt_va = eip; res->halt_op = insn.op; return;
837+
}
838+
uint8_t al = (uint8_t)(cpu->gpr[I386_REG_EAX] & 0xFFu);
839+
// Compute 8-bit flags via 32-bit alu_sub (zero-extended operands),
840+
// then fixup SF and PF for the 8-bit result width.
841+
(void)alu_sub(&cpu->eflags, (uint32_t)al, (uint32_t)rm_v8, 0);
842+
uint8_t r8 = (uint8_t)(al - rm_v8);
843+
set_flag(&cpu->eflags, EFL_SF, (int)((r8 >> 7) & 1u)); // 8-bit sign
844+
{ int ones8 = 0;
845+
uint8_t pr = r8;
846+
for (int ii = 0; ii < 8; ii++) ones8 += (pr >> ii) & 1;
847+
set_flag(&cpu->eflags, EFL_PF, (ones8 & 1) == 0); }
848+
if (al == rm_v8) {
849+
// Get reg8 value (same byte-reg map as rm_get8)
850+
uint8_t reg_v8;
851+
if (reg < 4) reg_v8 = (uint8_t)(cpu->gpr[reg] & 0xFFu);
852+
else reg_v8 = (uint8_t)((cpu->gpr[reg - 4] >> 8) & 0xFFu);
853+
if (!rm_set8(cpu, img, &insn, reg_v8, &why)) {
854+
res->halt = why; res->halt_va = eip; res->halt_op = insn.op; return;
855+
}
856+
} else {
857+
// AL ← r/m8
858+
cpu->gpr[I386_REG_EAX] = (cpu->gpr[I386_REG_EAX] & 0xFFFFFF00u)
859+
| (uint32_t)rm_v8;
860+
}
861+
break;
862+
}
779863
// ── E4 kernel32 boundary — indirect IAT call / jump ───────────
780864
case I386_OP_CALL_RM: // FF /2 [..]
781865
case I386_OP_JMP_RM: { // FF /4 [..]
@@ -790,12 +874,15 @@ void i386_cpu_run(i386_cpu_t *cpu, const i386_image_t *img,
790874
}
791875
uint32_t slot_va = (uint32_t)insn.disp;
792876
const i386_import_t *imp = i386_iat_lookup(cpu->iat, slot_va);
793-
if (!imp) {
794-
// Slot is an IAT thunk but no binding is registered — the
795-
// next kernel32 import to implement. own1: we never invent
796-
// the function; we stop and name the slot.
877+
if (!imp || !imp->fn) {
878+
// Slot is an IAT thunk but no binding is registered (imp==NULL),
879+
// OR the import was found by name but has no shim (fn==NULL from
880+
// autobind for an unknown import). own1: we never invent the
881+
// function; we stop honestly and record the slot + name if known.
797882
res->halt = I386_HALT_UNBOUND_IMPORT; res->halt_va = eip;
798-
res->halt_op = insn.op; res->import_slot = slot_va; return;
883+
res->halt_op = insn.op; res->import_slot = slot_va;
884+
if (imp && imp->name) res->last_import = imp->name; // name from autobind
885+
return;
799886
}
800887
// BIND: dispatch to the native shim, place the result in EAX
801888
// (Win32 ABI), pop stdcall callee-popped args, and continue.
@@ -875,6 +962,102 @@ int i386_cpu_load_pe(const char *path, i386_image_t *out, uint32_t *entry_va) {
875962
return 0;
876963
}
877964

965+
// ── IAT name-based autobind (E5 r9) ─────────────────────────────────────────
966+
// Walk the PE Import Directory in `img` at RVA `import_dir_rva` and match
967+
// each import name against the name-keyed registry. Writes entries to
968+
// bound_out[]; fn==NULL entries mark unresolved names (honest: the run loop
969+
// halts UNBOUND_IMPORT + records last_import=name for those). Returns total
970+
// imports seen; *bound_count_out = resolved (fn!=NULL); *unbound_count_out =
971+
// unresolved (fn==NULL). own1: standard PE import resolution over OUR sections
972+
// — binding our OWN imports to native impls is LOADING, not a bypass. No Wine.
973+
static const char *img_cstr(const i386_image_t *img, uint32_t va) {
974+
// Return a pointer to a C string in the flat image at `va`, or NULL if OOB.
975+
if (va < img->base || va >= img->base + img->size) return NULL;
976+
return (const char *)(img->host + (va - img->base));
977+
}
978+
979+
int i386_iat_autobind(
980+
const struct i386_image *img,
981+
uint32_t import_dir_rva,
982+
const i386_shim_entry_t *registry,
983+
uint32_t reg_count,
984+
i386_import_t *bound_out,
985+
uint32_t max_bound,
986+
uint32_t *bound_count_out,
987+
uint32_t *unbound_count_out)
988+
{
989+
uint32_t n_total = 0, n_bound = 0, n_unbound = 0;
990+
if (!img || !img->host || import_dir_rva == 0) {
991+
if (bound_count_out) *bound_count_out = 0;
992+
if (unbound_count_out) *unbound_count_out = 0;
993+
return 0;
994+
}
995+
uint32_t base = img->base;
996+
uint32_t desc_va = base + import_dir_rva;
997+
998+
// Walk IMAGE_IMPORT_DESCRIPTOR array (20 bytes each, null-terminated).
999+
for (;;) {
1000+
uint32_t orig_first_thunk, unused1, unused2, name_rva, first_thunk;
1001+
if (!i386_mem_read32(img, desc_va + 0, &orig_first_thunk)) break;
1002+
if (!i386_mem_read32(img, desc_va + 4, &unused1)) break;
1003+
if (!i386_mem_read32(img, desc_va + 8, &unused2)) break;
1004+
if (!i386_mem_read32(img, desc_va + 12, &name_rva)) break;
1005+
if (!i386_mem_read32(img, desc_va + 16, &first_thunk)) break;
1006+
(void)unused1; (void)unused2;
1007+
// Null terminator: all-zero descriptor (valid entry always has name+thunk).
1008+
if (orig_first_thunk == 0 && name_rva == 0 && first_thunk == 0) break;
1009+
1010+
// Use OriginalFirstThunk (INT) when present, else FirstThunk (IAT).
1011+
// Both hold RVAs to IMAGE_IMPORT_BY_NAME pre-binding.
1012+
uint32_t int_va = base + (orig_first_thunk ? orig_first_thunk : first_thunk);
1013+
uint32_t iat_va = base + first_thunk;
1014+
1015+
// Walk INT in lockstep with IAT slots (4 bytes per entry for PE32).
1016+
for (uint32_t si = 0; ; si++) {
1017+
uint32_t int_entry;
1018+
if (!i386_mem_read32(img, int_va + si * 4, &int_entry)) break;
1019+
if (int_entry == 0) break; // null terminator
1020+
1021+
uint32_t slot_va = iat_va + si * 4;
1022+
const i386_shim_entry_t *found = NULL;
1023+
const char *imp_name = NULL;
1024+
1025+
if (int_entry & 0x80000000u) {
1026+
// Ordinal import (bit31=1) — not shimmed by name.
1027+
imp_name = "(ordinal)";
1028+
} else {
1029+
// Named import: IMAGE_IMPORT_BY_NAME at base+int_entry.
1030+
// Skip 2-byte Hint; name follows immediately.
1031+
uint32_t ibn_va = base + int_entry;
1032+
imp_name = img_cstr(img, ibn_va + 2);
1033+
if (imp_name) {
1034+
for (uint32_t r = 0; r < reg_count; r++) {
1035+
if (strcmp(imp_name, registry[r].name) == 0) {
1036+
found = &registry[r];
1037+
break;
1038+
}
1039+
}
1040+
}
1041+
}
1042+
1043+
if (n_total < max_bound) {
1044+
bound_out[n_total].slot_va = slot_va;
1045+
bound_out[n_total].name = found ? found->name
1046+
: (imp_name ? imp_name : "?");
1047+
bound_out[n_total].fn = found ? found->fn : NULL;
1048+
bound_out[n_total].arg_bytes = found ? found->arg_bytes : 0;
1049+
}
1050+
n_total++;
1051+
if (found) n_bound++;
1052+
else n_unbound++;
1053+
}
1054+
desc_va += 20; // next descriptor
1055+
}
1056+
if (bound_count_out) *bound_count_out = n_bound;
1057+
if (unbound_count_out) *unbound_count_out = n_unbound;
1058+
return (int)n_total;
1059+
}
1060+
8781061
void i386_image_free(i386_image_t *img) {
8791062
if (img && img->owns && img->host) {
8801063
free(img->host);

native/i386_cpu.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,38 @@ uint32_t i386_shim_SetUnhandledExceptionFilter(i386_cpu_t *cpu, const struct i38
183183
// the slot is not bound (→ the caller halts UNBOUND_IMPORT honestly).
184184
const i386_import_t *i386_iat_lookup(const i386_iat_t *iat, uint32_t slot_va);
185185

186+
// ── E5 r9 — IAT name-based autobind ─────────────────────────────────────────
187+
// A name-keyed shim registry entry for i386_iat_autobind (r9). Matches
188+
// imported symbols by name (not by synthetic slot VA), so a real i386 PE's
189+
// IAT gets bound to native shims without hardcoded VAs.
190+
// own1: binding our OWN PE's imports to native impls is LOADING (what every
191+
// PE loader does), not a protection bypass. No Wine, no DRM.
192+
typedef struct {
193+
const char *name; // imported symbol name (e.g. "GetCurrentThreadId")
194+
i386_import_stub_fn fn; // native shim
195+
uint32_t arg_bytes; // stdcall callee-pop bytes (0 for 0-arg)
196+
} i386_shim_entry_t;
197+
198+
// Walk the PE Import Directory in `img` at RVA `import_dir_rva`. For each
199+
// named import, look it up by name in `registry[0..reg_count)`. Write the
200+
// result into `bound_out[0..max_bound)`:
201+
// fn != NULL → shim found (own import resolved)
202+
// fn == NULL → name not in registry (honest; run loop halts UNBOUND_IMPORT
203+
// and records last_import=name so the caller knows what's missing)
204+
// Ordinal imports (bit31=1 in the INT entry) produce fn=NULL entries with
205+
// name="(ordinal)". Returns total imports seen; *bound_count_out = resolved
206+
// count (fn!=NULL); *unbound_count_out = unresolved count (fn==NULL).
207+
// own1: standard PE import resolution over our OWN sections. No Wine.
208+
int i386_iat_autobind(
209+
const struct i386_image *img, // fwd — full def below; use struct ptr
210+
uint32_t import_dir_rva,
211+
const i386_shim_entry_t *registry,
212+
uint32_t reg_count,
213+
i386_import_t *bound_out,
214+
uint32_t max_bound,
215+
uint32_t *bound_count_out,
216+
uint32_t *unbound_count_out);
217+
186218
// Flat memory image: a single contiguous host buffer covering the VA
187219
// window [base, base+size). VA→host is host + (va - base). Simple and
188220
// low-risk; the PE loader fills it section-by-section (see

0 commit comments

Comments
 (0)