diff --git a/ARCHITECTURE.json b/ARCHITECTURE.json index 1bc7de8..8824cd4 100644 --- a/ARCHITECTURE.json +++ b/ARCHITECTURE.json @@ -27,16 +27,20 @@ "summary": "validated_manjeom=0(실 게임 프레임 미증명) 돌파의 명명된 사다리 = native E-series (docs/CLOSURE_ROADMAP.md §5). fleet-lab F-NSWINDOW-E5 가 추적. 각 라운드 measured 결과를 여기 update-in-place 박제 → 차세션 research 가 흩어진 메모리 대신 한 곳에서 표적을 본다.", "children": [ { - "name": "F-NSWINDOW-E5 — E1✅ E2✅ E3✅ E4🛠️ E5⬜", - "summary": "validated_manjeom=0 돌파 사다리(docs/CLOSURE_ROADMAP.md §5). E1 PE-parse+mmap done(CM-25a)·E2 i386 디코더 done(CM-25b)·**E3 i386 인터프리터 done**(실 game-binary EXECUTE)·**E4 kernel32 IAT 바인딩 IN-PROGRESS**·E5 first NSWindow=validated_manjeom>0 첫후보(CM-25e). 실행은 native/ C(i386_cpu.c)·lib/loader/pe_i386_*.hexa는 TSV 트래커(실행 아님)." + "name": "F-NSWINDOW-E5 — E1✅ E2✅ E3✅ E4🛠️(user-entry 도달) E5⬜", + "summary": "validated_manjeom=0 돌파 사다리(docs/CLOSURE_ROADMAP.md §5). E1 PE-parse+mmap done(CM-25a)·E2 i386 디코더 done(CM-25b)·E3 i386 인터프리터 done(실 game-binary EXECUTE)·E4 kernel32 IAT 바인딩 IN-PROGRESS(합성 prologue가 CRT→user-entry handoff 도달, insns=60, K32 import 13)·E5 first NSWindow=validated_manjeom>0 첫후보(CM-25e). 실행은 native/ C(i386_cpu.c)·lib/loader/pe_i386_*.hexa는 TSV 트래커(실행 아님)." }, { "name": "E4 r5 measured (2026-06-26·#19/24/25/26 merged) — 18명령 실행·kernel32 5 import 바인딩", "summary": "native/i386_cpu.c 인터프리터가 실 __scrt_common_main prologue 18명령 실행. import-stub registry 5엔트리 바인딩(GetCurrentThreadId·GetCurrentProcessId·GetTickCount·GetSystemTimeAsFileTime[버퍼쓰기 shim·LPFILETIME 8B mem_write]·QueryPerformanceCounter). 디코더 group-1 imm+mov r/m imm+byte mov+test+and/or+shift(0xC1/D1/D3) 커버(C+i386_decode.hexa RUNEQ byte-eq). EFLAGS(CF/PF/AF/ZF/SF/OF) 모델. 측정: insns 1(r2)→9(r3)→11(r4)→**18(r5)**·halt@0x5392A1=unbound IAT slot 0x538014. Blacksmith CI 게이트(i386_cpu_test PASS·ci.yml native step). own1: 로더가 자기 import 바인딩=로딩(바이패스 아님)·kernel32=OS API·Wine/QEMU 0. ⚠️ validated_manjeom 여전히 0(로더 진척·프레임 아님·E5가 프레임)." }, { - "name": "r6 다음 표적 (measured wall, reopenable)", - "summary": "벽=unbound IAT slot 0x538014@0x5392A1. r6: 다음 kernel32 import 바인딩 + 0F B6 MOVZX 디코더 gap + __security_init_cookie(첫 TEB/PEB 접근·bounded-synthetic). prologue 아직 main/WinMain 미도달 — CRT-init import set 소진→mainCRTStartup→main→user32 CreateWindowEx(E5 경로). 매 rung CI 검증·머지·단일직렬(swap 19.7G 피크 경험)." + "name": "E4 r7/r8 measured (2026-06-26·#29 + r8 PR) — 60명령·CRT→user-entry handoff(합성)·K32 import 13", + "summary": "native/i386_cpu.c 인터프리터가 합성 __scrt_common_main prologue를 r7 벽(unbound IAT @0x539318, insns=44) 너머로 전진 → CRT init 종점 = `call WinMain` CRT→user-entry handoff(i386_cpu_t.user_entry_va + I386_HALT_USER_ENTRY). 디코더: 0F AF/69/6B IMUL·0F A2 CPUID·0F 31 RDTSC(r7) + 0F A3/AB/B3/BB BT/BTS/BTR/BTC(r8) — C + i386_decode.hexa RUNEQ byte-eq. import: security cookie 산술 + GetCommandLineW(0x53802C)·SetUnhandledExceptionFilter(0x538030) 바인딩, K32 11→13. 측정: insns 44(r7)→60(r8)·halt@0x539400=user_entry(call). Blacksmith CI 게이트(i386_cpu_test 25 checks PASS·-Wall -Wextra -std=c11). real-PE 경로 i386_cpu_load_pe 구조적 배선완료(섹션+entry 매핑)·미실행(IAT name-autobind 필요). ⚠️ validated_manjeom 여전히 0(합성 user-entry·실 프레임 아님)." + }, + { + "name": "🧱 E5 게이트 재분류 (2026-06-26 실측 — substrate/투자 프론티어, 하드웨어 천장 아님)", + "summary": "validated_manjeom>0(실 프레임) 도달 게이트를 실측 재분류. ① real i386 D3D PE — 최소 D3D11 테스트 PE 자작 가능(own1 클린·꼭 AAA 아님). ② D3DMetal SDK — 'CI 부재'는 틀림: `brew tap apple/apple && brew install game-porting-toolkit`로 Blacksmith macos-15(실 Apple Silicon·GPU)에 설치 가능(하드웨어 무관). ③ 디스플레이 — 불필요: Metal 헤드리스(오프스크린 MTLTexture→getBytes readback)로 화면 없이 프레임 증명 가능(M4/Metal4 확인). → 진짜 벽 = 엔지니어링 거리: lib/loader/dx_d3d11.hexa 등 DX→Metal 브리지는 MTL* 이름만 적힌 텔레메트리 스캐폴드(실 Metal 호출 0건). 다음 사다리(자율가능, 실-GPU CI 위): real-PE IAT name-autobind → 넓은 opcode 커버 → D3D11 device shim → 실 MTLDevice/오프스크린 → readback. 이전 'real-asset user-only 게이트' 분류는 verify 안 한 게으른 벽(break-walls 위반)이라 정정." } ] }, @@ -495,4 +499,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f17a3a..a285bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,48 @@ All notable changes to `gamebox` are documented in this file. ### Added +- feat(F-NSWINDOW-E5 r8): **CRT→user-entry 핸드오프 도달(합성) + BT/BTS/BTR/BTC + 디코더 갭 종결 + 마지막 CRT import 2개 바인딩** — r7 의 벽(미등록 IAT 호출 + @`0x539318`, insns=44)을 **넘어 CRT-init 의 종착점인 `call WinMain`(CRT→user-entry + 핸드오프)에 도달**한다. **이것이 정직한 E4 완료 마일스톤이며, 렌더된 게임 프레임이 + 아니다 — `validated_manjeom` 은 여전히 0.** (1) `native/i386_cpu.{c,h}` 에 두 개의 + kernel32 셰임: `GetCommandLineW()`(0-arg → **합성 in-image 커맨드라인 포인터** + `base+0x1FE0`), `SetUnhandledExceptionFilter(fn)`(1-arg → 이전 필터 NULL=0). IAT + 슬롯 `0x53802C`/`0x538030` 으로 13개(count=13). (2) `native/i386_decode.{c,h}` + + byte-equal `.hexa` 미러: enum `I386_OP_BT_RM_R`(61,`0F A3`)/`_BTS`(62,`0F AB`)/ + `_BTR`(63,`0F B3`)/`_BTC`(64,`0F BB`) + 0F 2-byte 분기(0F AF IMUL 과 동일한 `/r` + ModR/M 형태), op_name="bt"/"bts"/"btr"/"btc". 인터프리터: **CF ← bit(r/m, idx)** + (idx=ModR/M.reg 레지스터, mod 32 마스크), BTS/BTR/BTC 는 수정 비트 write-back — + 평범한 Intel SDM Vol.2 비트 연산(보호 아님). RUNEQ corpus D(0F 전수)에서 C↔hexa + byte-equal 확인. (3) **핸드오프 검출**: `i386_cpu_t.user_entry_va` 필드 + halt + `I386_HALT_USER_ENTRY` 추가 — `call` 의 타깃이 `user_entry_va` 와 같으면 리턴 주소를 + push 한 뒤(faithful "about to enter") 정직하게 정지. (4) hermetic 테스트 + (`native/i386_cpu_test.c`) Run A 는 r7 체인(44) 뒤에 `GetCommandLineW` → `pop ecx` + (잔여 push 정리) → `mov esi,eax` → `SetUnhandledExceptionFilter` → `bt/bts/btr/btc` + → WinMain 4-인자 stdcall 프레임 push(nShowCmd/lpCmdLine/hPrevInstance/hInstance) → + `call 0x539400`(합성 user entry) 를 이어 붙여 **insns 44→60, halt + `0x539318`(unbound)→`0x539400`(user_entry)**, bound=13, last=SetUnhandledException- + Filter 를 증명. WinMain 인자 프레임([esp+4..0x10] = hInstance·hPrevInstance· + lpCmdLine·nShowCmd) + entry CALL 리턴주소(`0x5388AB`) 무결성까지 검증. B/D + sentinel 은 `0F A3`(이제 BT 로 실행됨) → `0F B1`(CMPXCHG, 다음 진짜 갭, r9) 으로 + 교체. 측정 라인 `__SHIM__ PARTIAL phase=e4_reached_user_entry insns=60 bound=13 + last=SetUnhandledExceptionFilter entry_va=0x539400 halt=user_entry halt_op=call + (SYNTHETIC user-entry; validated_manjeom=0)` + `__SHIM__ INFO + real_pe_path=structurally_ready needs:real_i386_PE+IAT_autobind_by_import_name+ + wider_opcode_coverage+D3DMetal+display`, `__SHIM_TEST__ PASS`(CI 게이트, Run A 25 + checks 전부 green, 로컬 clang `-Wall -Wextra -Wpedantic -std=c11` 청정). (5) + **실-바이너리 경로 점검**: `i386_cpu_load_pe`(argv[1]) 는 섹션 매핑·엔트리 설정· + 이미지 할당까지 **구조적으로 배선됨** — 실제 i386 PE 를 주면 엔트리부터 실행 가능. + 단, K32_IAT 슬롯이 hermetic 합성 VA(`0x538000..`)라 실제 PE 의 IAT 와는 매칭되지 + 않으므로, 실행이 첫 import 를 넘으려면 **PE import 테이블 파싱 + 이름 기반 자동 + 바인딩**이 필요(real-PE emit 가 INFO 로 명시). **own1**: 자기 import 를 네이티브 + 구현에 묶는 **로딩**(우회 아님), 합성 TEB/커맨드라인/user-entry VA 는 **정직 라벨**, + BT 류는 평범한 CPU 비트 연산 — Wine/보호 없음. **`validated_manjeom>0`(실 프레임) + 까지 남은 거리**: real-asset 게이트(실 i386 게임 PE + D3DMetal SDK + 실 디스플레이 — + 사용자만 제공 가능) + 남은 엔지니어링(실 PE IAT 이름 자동바인딩, 더 넓은 opcode + 커버리지, 게임 자신의 WinMain → 메시지 루프 → CreateWindowEx → D3D→Metal 프레임). + 현재는 **합성 user-entry 도달**까지이며 실 프레임은 자율 범위 밖(real-asset-gated). + - feat(F-NSWINDOW-E5 r7): **CRT import 3개 추가 바인딩 + IMUL/CPUID/RDTSC 디코더 갭 종결 + `__security_init_cookie` 산술 실행** — r6 의 벽(미등록 IAT 호출 @`0x5392D4`, insns=30)을 **넘는다**. (1) `native/i386_cpu.{c,h}` 에 세 개의 diff --git a/native/i386_cpu.c b/native/i386_cpu.c index 71112f7..2ddb352 100644 --- a/native/i386_cpu.c +++ b/native/i386_cpu.c @@ -24,6 +24,7 @@ const char *i386_halt_name(i386_halt_t h) { case I386_HALT_TRUNC: return "trunc"; case I386_HALT_GUARD: return "guard"; case I386_HALT_UNBOUND_IMPORT: return "unbound_import"; + case I386_HALT_USER_ENTRY: return "user_entry"; default: return "?"; } } @@ -148,6 +149,26 @@ uint32_t i386_shim_GetProcAddress(i386_cpu_t *cpu, const struct i386_image *img) return img->base + 0x1FF0u; // synthetic in-image stub addr } +// GetCommandLineW() — WINAPI/stdcall, 0 args, returns LPWSTR. own1: the LOADER +// hands the program a SYNTHETIC in-image command-line pointer (its own argv +// blob). The CRT-startup `__scrt_common_main_seh` reads this before invoking the +// user entry. Not a bypass — this is the program's OWN command line. +uint32_t i386_shim_GetCommandLineW(i386_cpu_t *cpu, const struct i386_image *img) { + (void)cpu; + return img->base + 0x1FE0u; // synthetic in-image cmdline ptr +} + +// SetUnhandledExceptionFilter(LPTOP_LEVEL_EXCEPTION_FILTER) — WINAPI/stdcall, 1 +// ptr arg, returns the PREVIOUS top-level filter. own1: a CRT-startup OS-API +// call installing the program's OWN handler; we return NULL (none installed). +// An exception-handler registration, NOT a protection / anti-debug mechanism. +uint32_t i386_shim_SetUnhandledExceptionFilter(i386_cpu_t *cpu, const struct i386_image *img) { + uint32_t pfn = 0; + i386_mem_read32(img, cpu->gpr[I386_REG_ESP], &pfn); // arg (unused — no real SEH) + (void)pfn; + return 0; // previous filter == NULL +} + const i386_import_t *i386_iat_lookup(const i386_iat_t *iat, uint32_t slot_va) { if (!iat || !iat->imports) return NULL; for (uint32_t i = 0; i < iat->count; i++) { @@ -427,8 +448,18 @@ void i386_cpu_run(i386_cpu_t *cpu, const i386_image_t *img, if (!i386_mem_write32(img, cpu->gpr[I386_REG_ESP], ret)) { res->halt = I386_HALT_OOB; res->halt_va = eip; return; } - cpu->eip = eip + insn.len + (uint32_t)insn.imm; // same formula as the disassembler + uint32_t target = eip + insn.len + (uint32_t)insn.imm; // disassembler formula + cpu->eip = target; res->insns++; + // CRT→user-entry handoff: this CALL transfers control to the + // user entry (main/WinMain). Faithful "about to enter" — the + // return address is already pushed. Honest E4 milestone, NOT a + // rendered frame. own1: we stop AT the handoff; we do not run + // the (absent) real game body. + if (cpu->user_entry_va && target == cpu->user_entry_va) { + res->halt = I386_HALT_USER_ENTRY; res->halt_va = target; + res->halt_op = insn.op; return; + } continue; // branch — skip linear advance } case I386_OP_JMP_REL: { // E9 cd / EB cb @@ -713,6 +744,38 @@ void i386_cpu_run(i386_cpu_t *cpu, const i386_image_t *img, cpu->gpr[I386_REG_EDX] = (uint32_t)(cpu->tsc >> 32); break; } + // ── BT / BTS / BTR / BTC r/m32, r32 (E5 r8) — 0F A3/AB/B3/BB ───── + // CF ← bit(r/m, index). The bit index is the ModR/M.reg register. + // For a register-direct operand the index is masked mod 32 (SDM); + // for a memory operand we model the same 32-bit-window form (bounded + // — bit base + index/8 byte-stride not modeled). BTS/BTR/BTC write + // the modified value back. own1: plain Intel SDM Vol.2 bit ops. + case I386_OP_BT_RM_R: case I386_OP_BTS_RM_R: + case I386_OP_BTR_RM_R: case I386_OP_BTC_RM_R: { + i386_halt_t why = I386_HALT_UNSUPPORTED; + uint32_t v; + if (!rm_get32(cpu, img, &insn, &v, &why)) { + res->halt = why; res->halt_va = eip; res->halt_op = insn.op; return; + } + uint32_t idx = cpu->gpr[(insn.modrm >> 3) & 7] & 0x1Fu; // bit index + uint32_t bit = (v >> idx) & 1u; + set_flag(&cpu->eflags, EFL_CF, (int)bit); + uint32_t out_v = v; + int wb = 1; + switch (insn.op) { + case I386_OP_BT_RM_R: wb = 0; break; + case I386_OP_BTS_RM_R: out_v = v | (1u << idx); break; + case I386_OP_BTR_RM_R: out_v = v & ~(1u << idx); break; + case I386_OP_BTC_RM_R: out_v = v ^ (1u << idx); break; + default: break; + } + if (wb) { + if (!rm_set32(cpu, img, &insn, out_v, &why)) { + res->halt = why; res->halt_va = eip; res->halt_op = insn.op; return; + } + } + break; + } // ── E4 kernel32 boundary — indirect IAT call / jump ─────────── case I386_OP_CALL_RM: // FF /2 [..] case I386_OP_JMP_RM: { // FF /4 [..] diff --git a/native/i386_cpu.h b/native/i386_cpu.h index 3a774b4..6f30df2 100644 --- a/native/i386_cpu.h +++ b/native/i386_cpu.h @@ -118,6 +118,13 @@ struct i386_cpu { // I386_TSC_STEP and writes the running 64-bit count to edx:eax. own1: a // plausible synthetic counter, NOT the host TSC and NOT a protection clock. uint64_t tsc; + // ── E5 r8 CRT→user-entry handoff marker (NOT a frame) ──────────────────── + // When non-zero, a CALL (rel32 or bound import-thunk) whose computed target + // equals user_entry_va is the CRT→user-entry handoff: the interpreter pushes + // the return address (faithful "about to enter"), records it, and halts + // I386_HALT_USER_ENTRY. This is the honest E4 milestone (CRT init reached the + // `call main`/`call WinMain` site) — it is NOT a rendered game frame. + uint32_t user_entry_va; }; // Per-rdtsc increment for the synthetic timestamp counter (plausible, fixed). @@ -163,6 +170,15 @@ uint32_t i386_shim_GetStartupInfoW(i386_cpu_t *cpu, const struct i386_image *img uint32_t i386_shim_GetSystemInfo(i386_cpu_t *cpu, const struct i386_image *img); uint32_t i386_shim_GetProcAddress(i386_cpu_t *cpu, const struct i386_image *img); +// CRT→user-entry kernel32 shims (E5 r8). own1: native re-impls of the OS API +// surface bound to the program's OWN imports — loading, not a bypass. +// GetCommandLineW() — 0 args, returns a synthetic in-image +// command-line pointer (the loader hands the program its own argv blob). +// SetUnhandledExceptionFilter(fn) — 1 ptr arg, returns the previous filter +// (NULL — none installed). A CRT-startup OS-API call, not a protection. +uint32_t i386_shim_GetCommandLineW(i386_cpu_t *cpu, const struct i386_image *img); +uint32_t i386_shim_SetUnhandledExceptionFilter(i386_cpu_t *cpu, const struct i386_image *img); + // Look up an IAT slot VA in the registry. Returns NULL if `iat` is NULL or // the slot is not bound (→ the caller halts UNBOUND_IMPORT honestly). const i386_import_t *i386_iat_lookup(const i386_iat_t *iat, uint32_t slot_va); @@ -191,6 +207,9 @@ typedef enum { I386_HALT_UNBOUND_IMPORT, // indirect IAT call FF 15 [slot] to an UNregistered // kernel32 import — the next import to bind (own1: // honest stop, we never invent the function) + I386_HALT_USER_ENTRY, // CRT init reached the `call main`/`call WinMain` site + // (cpu->user_entry_va). The honest E4 milestone — the + // CRT→user-entry handoff. NOT a rendered game frame. } i386_halt_t; typedef struct { diff --git a/native/i386_cpu_test.c b/native/i386_cpu_test.c index 9c737e6..8808e05 100644 --- a/native/i386_cpu_test.c +++ b/native/i386_cpu_test.c @@ -66,6 +66,20 @@ // plain CPU instructions; the cookie is OUR arithmetic over OUR buffers. kernel32 // is the OS API, not DRM/Warden. Our own bytes, Intel SDM semantics, native // shims — no Wine, no protection. +// +// E5 r8 — REACH THE CRT→USER-ENTRY HANDOFF. Bind the last two CRT imports +// (GetCommandLineW [slot 2C] / SetUnhandledExceptionFilter [slot 30]), close the +// 0F A3/AB/B3/BB BT/BTS/BTR/BTC decoder gap, then set up the WinMain stdcall +// frame and `call` the synthetic user entry. The interpreter detects the call +// target == cpu.user_entry_va and HALTS I386_HALT_USER_ENTRY (insns 44 → 60). +// This is the honest E4 completion milestone: CRT init reached `call WinMain`. +// It is NOT a rendered game frame — validated_manjeom STAYS 0. The WinMain target +// is OUR OWN synthetic VA; a real frame is real-asset-gated (real i386 game PE + +// the binary's own IAT auto-bound by import name + its real WinMain → message +// loop → CreateWindowEx → D3D→Metal, + D3DMetal SDK + a real display) — none of +// which exists on this host/CI. The B/D walls now use a `0F B1` (cmpxchg) +// sentinel — the next genuinely-uncovered decoder gap (r9). own1: synthetic +// shape, our bytes, Intel SDM semantics — no Wine, no protection. #include "i386_cpu.h" @@ -157,8 +171,25 @@ static void put_at(uint8_t *img, uint32_t va, const uint8_t *src, size_t n) { // 0x53930A B8 00 00 00 00 mov eax, 0 ; cpuid leaf 0 // 0x53930F 0F A2 cpuid ; → ebx/edx/ecx "GenuineIntel" (r7 decoder) // 0x539311 0F 31 rdtsc ; → edx:eax synthetic monotonic tsc (r7) -// 0x539313 68 D0 9F 53 00 push 0x539FD0 ; & next buffer -// 0x539318 FF 15 2C 80 53 00 call [0x53802C] ; slot 2C → UNBOUND (the new r8 wall) +// 0x539313 68 D0 9F 53 00 push 0x539FD0 ; & scratch buffer (stray push, r8 discards) +// 0x539318 FF 15 2C 80 53 00 call [0x53802C] ; slot 2C → GetCommandLineW (r8, → cmdline ptr) +// E5 r8 continuation — bind the last two CRT imports, exercise BT/BTS/BTR/BTC, +// then set up the WinMain stdcall frame and reach the CRT→user-entry handoff: +// 0x53931E 59 pop ecx ; discard the stray scratch push (balance esp) +// 0x53931F 89 C6 mov esi, eax ; esi = lpCmdLine (synthetic GetCommandLineW ret) +// 0x539321 6A 00 push 0 ; SetUnhandledExceptionFilter(NULL) +// 0x539323 FF 15 30 80 53 00 call [0x538030] ; slot 30 → SetUnhandledExceptionFilter (r8, →0) +// 0x539329 BA 05 00 00 00 mov edx, 5 ; bit index 5 +// 0x53932E B8 20 00 00 00 mov eax, 0x20 ; feature word (bit 5 set) +// 0x539333 0F A3 D0 bt eax, edx ; CF ← bit5 = 1 (0F A3 — r8 decoder) +// 0x539336 0F AB D0 bts eax, edx ; set bit5 (CF←1), eax=0x20 (0F AB — r8) +// 0x539339 0F B3 D0 btr eax, edx ; clear bit5 (CF←1), eax=0 (0F B3 — r8) +// 0x53933C 0F BB D0 btc eax, edx ; flip bit5 (CF←0), eax=0x20 (0F BB — r8) +// 0x53933F 6A 0A push 0x0A ; nShowCmd +// 0x539341 56 push esi ; lpCmdLine +// 0x539342 6A 00 push 0 ; hPrevInstance NULL +// 0x539344 68 00 80 53 00 push 0x538000 ; hInstance (image base, synthetic) +// 0x539349 E8 B2 00 00 00 call 0x539400 ; CRT→user-entry handoff → WinMain (SYNTHETIC) static const uint8_t SCRT_PROLOGUE[] = { 0x83, 0xEC, 0x28, 0x33, 0xC0, @@ -204,16 +235,33 @@ static const uint8_t SCRT_PROLOGUE[] = { 0xB8, 0x00, 0x00, 0x00, 0x00, // 0x53930A mov eax, 0 (cpuid leaf 0) 0x0F, 0xA2, // 0x53930F cpuid (0F A2 — r7 decoder gap closed) 0x0F, 0x31, // 0x539311 rdtsc (0F 31 — r7) - 0x68, 0xD0, 0x9F, 0x53, 0x00, // 0x539313 push 0x539FD0 (& next buffer) - 0xFF, 0x15, 0x2C, 0x80, 0x53, 0x00, // 0x539318 call [0x53802C] → UNBOUND (the new r8 wall) + 0x68, 0xD0, 0x9F, 0x53, 0x00, // 0x539313 push 0x539FD0 (& scratch buffer; r8 discards) + 0xFF, 0x15, 0x2C, 0x80, 0x53, 0x00, // 0x539318 call [0x53802C] → GetCommandLineW (r8, → cmdline) + // ── E5 r8 continuation ── + 0x59, // 0x53931E pop ecx (discard the stray scratch push) + 0x89, 0xC6, // 0x53931F mov esi, eax (esi = lpCmdLine) + 0x6A, 0x00, // 0x539321 push 0 (SetUnhandledExceptionFilter NULL) + 0xFF, 0x15, 0x30, 0x80, 0x53, 0x00, // 0x539323 call [0x538030] → SetUnhandledExceptionFilter + 0xBA, 0x05, 0x00, 0x00, 0x00, // 0x539329 mov edx, 5 (bit index) + 0xB8, 0x20, 0x00, 0x00, 0x00, // 0x53932E mov eax, 0x20 (feature word, bit5 set) + 0x0F, 0xA3, 0xD0, // 0x539333 bt eax, edx (0F A3 — r8 decoder gap closed) + 0x0F, 0xAB, 0xD0, // 0x539336 bts eax, edx (0F AB — r8) + 0x0F, 0xB3, 0xD0, // 0x539339 btr eax, edx (0F B3 — r8) + 0x0F, 0xBB, 0xD0, // 0x53933C btc eax, edx (0F BB — r8) + 0x6A, 0x0A, // 0x53933F push 0x0A (nShowCmd) + 0x56, // 0x539341 push esi (lpCmdLine) + 0x6A, 0x00, // 0x539342 push 0 (hPrevInstance NULL) + 0x68, 0x00, 0x80, 0x53, 0x00, // 0x539344 push 0x538000 (hInstance, synthetic) + 0xE8, 0xB2, 0x00, 0x00, 0x00, // 0x539349 call 0x539400 → WinMain (CRT→user-entry handoff) }; -// The native kernel32 imports bound across IAT slots 0x538000..0x538028 (r4-r7). +// The native kernel32 imports bound across IAT slots 0x538000..0x538030 (r4-r8). // own1: loading the OS API surface to native impls, not a protection bypass. // The buffer-writing shims (slots 8 / C / 18 / 20 / 24) take one stdcall pointer // arg (4 bytes, callee-popped) and write into image memory; IsProcessorFeature- -// Present (14) / GetModuleHandleW (1C) take 1 arg; GetProcAddress (28) takes 2 -// (8 bytes); the rest are 0-arg. +// Present (14) / GetModuleHandleW (1C) / SetUnhandledExceptionFilter (30) take 1 +// arg; GetProcAddress (28) takes 2 (8 bytes); the rest (incl. GetCommandLineW +// at slot 2C) are 0-arg. static const i386_import_t K32_IMPORTS[] = { { 0x538000u, "GetCurrentThreadId", i386_shim_GetCurrentThreadId, 0 }, { 0x538004u, "GetCurrentProcessId", i386_shim_GetCurrentProcessId, 0 }, @@ -226,8 +274,13 @@ static const i386_import_t K32_IMPORTS[] = { { 0x538020u, "GetStartupInfoW", i386_shim_GetStartupInfoW, 4 }, { 0x538024u, "GetSystemInfo", i386_shim_GetSystemInfo, 4 }, { 0x538028u, "GetProcAddress", i386_shim_GetProcAddress, 8 }, + // ── E5 r8: the last two CRT→user-entry imports ── + { 0x53802Cu, "GetCommandLineW", i386_shim_GetCommandLineW, 0 }, + { 0x538030u, "SetUnhandledExceptionFilter", i386_shim_SetUnhandledExceptionFilter, 4 }, }; -static const i386_iat_t K32_IAT = { K32_IMPORTS, 11 }; +static const i386_iat_t K32_IAT = { K32_IMPORTS, 13 }; +#define WINMAIN_VA 0x539400u // synthetic user entry (CRT→user-entry handoff target) +#define CMDLINE_PTR 0x539FE0u // GetCommandLineW synthetic return (img base + 0x1FE0) #define SHIM_TID 0x00001A2Bu // GetCurrentThreadId stub return #define SHIM_PID 0x00000D04u // GetCurrentProcessId stub return #define SHIM_TICK 0x0001D4C0u // GetTickCount stub return @@ -256,12 +309,12 @@ static void build_hermetic(uint8_t *img) { i386_mem_write32(&m, TEB_BASE + 0x18, TEB_BASE); // TEB self-pointer (NtCurrentTeb shape) i386_mem_write32(&m, TEB_BASE + 0x30, PEB_BASE); // TEB→PEB i386_mem_write32(&m, PEB_BASE + 0x08, IMG_BASE); // PEB→ImageBaseAddress - // 0F A3 = BT r/m32, r32 — still uncovered after r7 (0F A2 cpuid / 0F 31 rdtsc - // / 0F AF imul now decode+execute). The 0x0F two-byte map recognizes Jcc - // (80..8F) + movzx/movsx (B6/B7/BE/BF) + imul/cpuid/rdtsc (AF/A2/31), so - // 0F A3 → I386_OP_UNKNOWN. Keeps the B/D walls honest and names the next real - // decoder gap (BT bit-test) for r8. - uint8_t wall[] = { 0x0F, 0xA3 }; // representative `bt` (next gap) + // 0F B1 = CMPXCHG r/m32, r32 — still uncovered after r8 (0F A3/AB/B3/BB bt/bts/ + // btr/btc now decode+execute). The 0x0F two-byte map recognizes Jcc (80..8F) + + // movzx/movsx (B6/B7/BE/BF) + imul/cpuid/rdtsc (AF/A2/31) + bit-test (A3/AB/B3/ + // BB), so 0F B1 → I386_OP_UNKNOWN. Keeps the B/D walls honest and names the next + // real decoder gap (CMPXCHG, the lock-free CRT init primitive) for r9. + uint8_t wall[] = { 0x0F, 0xB1 }; // representative `cmpxchg` (next gap) put_at(img, 0x538BABu, wall, sizeof(wall)); // CALL target (insn 6) put_at(img, 0x53872Au, wall, sizeof(wall)); // JMP target (insn 2) } @@ -282,11 +335,18 @@ int main(int argc, char **argv) { cpu.gpr[I386_REG_ESP] = pe.base + pe.size - 0x100; // writable top of image cpu.iat = &K32_IAT; // native kernel32 binding cpu.teb_base = TEB_BASE; // synthetic TEB (own1: our block) + // NB: user_entry_va stays 0 for a real PE — the user entry VA is found + // by the binary's own CRT, not known a priori; the run halts honestly at + // its first unbound import / uncovered opcode. The K32_IAT slots here are + // the HERMETIC synthetic VAs (0x538000..), so a real PE's IAT will not + // match until a future round parses the PE's import table and auto-binds + // by name (see the INFO line below). own1: honest, no invented functions. i386_run_result_t r; i386_cpu_run(&cpu, &pe, 100000, &r); - printf("__SHIM__ PARTIAL phase=e4_crt_security_cookie insns=%u bound=%u last=%s halt_va=0x%X halt=%s halt_op=%s unbound_slot=0x%X\n", + printf("__SHIM__ PARTIAL phase=e4_real_pe_probe insns=%u bound=%u last=%s halt_va=0x%X halt=%s halt_op=%s unbound_slot=0x%X\n", r.insns, r.imports_bound, r.last_import ? r.last_import : "-", r.halt_va, i386_halt_name(r.halt), i386_op_name(r.halt_op), r.import_slot); + printf("__SHIM__ INFO real_pe_path=loaded sections+entry mapped; needs:IAT_autobind_by_import_name+wider_opcode_coverage+D3DMetal+display for a frame (validated_manjeom=0)\n"); i386_image_free(&pe); emitted = 1; } else { @@ -300,20 +360,21 @@ int main(int argc, char **argv) { uint8_t *img_buf = (uint8_t *)malloc(IMG_SIZE); i386_image_t img = { img_buf, IMG_BASE, IMG_SIZE, 0 }; - // === Run A — MILESTONE: 11 CRT imports + security cookie + CPUID/RDTSC === - // E5 r7 — execution runs the entry CALL (insn 1) → __scrt_common_main - // prologue → the r6 chain (8 kernel32 binds + buffer writes + MOVZX/MOVSX + - // the synthetic TEB→PEB→ImageBase walk, insns 1..30) → then the r7 - // continuation: GetStartupInfoW (31, BUFFER-WRITE STARTUPINFOW cb=0x44) → - // push (32) → GetSystemInfo (33, BUFFER-WRITE SYSTEM_INFO) → push/push (34,35) - // → GetProcAddress (36, → synthetic in-image stub) → the __security_init_cookie - // shape: mov eax,[FT] (37) → xor eax,[FT+4] (38) → imul eax,eax,imm (39, the - // r7 IMUL) → mov [COOKIE],eax (40, store the global cookie) → mov eax,0 (41) → - // CPUID (42, → "GenuineIntel") → RDTSC (43, → synthetic edx:eax) → push (44) - // → HALTS at the TWELFTH, UNregistered IAT call @0x539318 (UNBOUND_IMPORT — - // slot 0x53802C, the next import, r8). own1: CPUID/RDTSC are plain CPU ops, - // the cookie is our arithmetic over our buffers — no protection, no Wine. - printf("\n[cpu] Run A — 11 CRT imports + security-cookie (imul) + CPUID/RDTSC\n"); + // === Run A — MILESTONE: 13 CRT imports + BT family → CRT→user-entry handoff === + // E5 r8 — execution runs the entry CALL (insn 1) → __scrt_common_main prologue + // → the r6 chain (insns 1..30) → the r7 continuation (StartupInfo/SysInfo/ + // ProcAddr + security cookie + CPUID/RDTSC, insns 31..44) → THEN the r8 + // continuation: GetCommandLineW (45, slot 2C, → synthetic cmdline ptr) → + // pop ecx (46, discard the stray scratch push) → mov esi,eax (47) → + // push/SetUnhandledExceptionFilter (48,49, slot 30) → mov edx,5 / mov eax,0x20 + // (50,51) → bt/bts/btr/btc eax,edx (52..55, the freshly-decoded 0F A3/AB/B3/BB) + // → push the four WinMain args (56..59: nShowCmd, lpCmdLine, hPrevInstance, + // hInstance) → CALL 0x539400 (60) = the CRT→user-entry handoff. The interpreter + // detects the call target == cpu.user_entry_va, pushes the return address, and + // HALTS I386_HALT_USER_ENTRY. own1: this is the honest E4 milestone — CRT init + // reached `call WinMain` — NOT a rendered game frame (the WinMain target is OUR + // synthetic VA; the real game body needs the real PE + D3DMetal + a display). + printf("\n[cpu] Run A — 13 CRT imports + BT family → CRT→user-entry handoff (SYNTHETIC)\n"); build_hermetic(img_buf); { i386_cpu_t cpu; memset(&cpu, 0, sizeof(cpu)); @@ -321,18 +382,17 @@ int main(int argc, char **argv) { cpu.gpr[I386_REG_ESP] = 0x539E00u; // stack inside the image cpu.iat = &K32_IAT; // register the native kernel32 binding cpu.teb_base = TEB_BASE; // synthetic TEB (own1: our block, not OS TEB) + cpu.user_entry_va = WINMAIN_VA; // CRT→user-entry handoff marker (own1: synthetic) uint32_t esp0 = cpu.gpr[I386_REG_ESP]; i386_mem_write32(&img, SLIST_BUF + 0, 0xFFFFFFFFu); // pre-dirty → prove the zeroing i386_mem_write32(&img, SLIST_BUF + 4, 0xFFFFFFFFu); i386_run_result_t r; i386_cpu_run(&cpu, &img, 1000, &r); - printf(" executed=%u halt=%s halt_va=0x%X eip=0x%X esp=0x%X eax=0x%X ebx=0x%X ecx=0x%X edx=0x%X bound=%u(%s) slot=0x%X\n", + printf(" executed=%u halt=%s halt_va=0x%X eip=0x%X esp=0x%X eax=0x%X ebx=0x%X edx=0x%X bound=%u(%s)\n", r.insns, i386_halt_name(r.halt), r.halt_va, cpu.eip, cpu.gpr[I386_REG_ESP], - cpu.gpr[I386_REG_EAX], cpu.gpr[I386_REG_EBX], cpu.gpr[I386_REG_ECX], cpu.gpr[I386_REG_EDX], - r.imports_bound, r.last_import ? r.last_import : "-", r.import_slot); - uint32_t entry_ret = 0; - i386_mem_read32(&img, cpu.gpr[I386_REG_ESP] + 4, &entry_ret); // [esp+4] (under final push) + cpu.gpr[I386_REG_EAX], cpu.gpr[I386_REG_EBX], cpu.gpr[I386_REG_EDX], + r.imports_bound, r.last_import ? r.last_import : "-"); uint32_t ft_lo = 0, ft_hi = 0, qpc_lo = 0, qpc_hi = 0, sl_lo = 0, sl_hi = 0; uint32_t si_cb = 0, sys_page = 0, sys_ncpu = 0, cookie = 0; i386_mem_read32(&img, FT_BUF + 0, &ft_lo); @@ -348,12 +408,19 @@ int main(int argc, char **argv) { // Re-derive the cookie the prologue computes: (FT_lo ^ FT_hi) * 0x01000193 // (the low 32 bits of the signed imul == the low 32 of the 32×32 product). uint32_t cookie_expect = (uint32_t)((0xC3D4E5F6u ^ 0x01D7A1B2u) * 0x01000193u); - uint64_t tsc1 = I386_TSC_STEP; // after exactly one rdtsc + // WinMain stdcall frame at the handoff. esp points at the pushed return + // address (0x53934E); the four args sit just above it. + uint32_t wm_ret = 0, wm_hinst = 0, wm_hprev = 0, wm_cmd = 0, wm_show = 0, entry_ret = 0; + i386_mem_read32(&img, cpu.gpr[I386_REG_ESP] + 0x00, &wm_ret); // return address + i386_mem_read32(&img, cpu.gpr[I386_REG_ESP] + 0x04, &wm_hinst); // hInstance + i386_mem_read32(&img, cpu.gpr[I386_REG_ESP] + 0x08, &wm_hprev); // hPrevInstance + i386_mem_read32(&img, cpu.gpr[I386_REG_ESP] + 0x0C, &wm_cmd); // lpCmdLine + i386_mem_read32(&img, cpu.gpr[I386_REG_ESP] + 0x10, &wm_show); // nShowCmd + i386_mem_read32(&img, cpu.gpr[I386_REG_ESP] + 0x14, &entry_ret); // entry CALL retaddr (deep) - CHECK(r.insns == 44, "44 insns (30 r6 + StartupInfo/SysInfo/ProcAddr + cookie-imul + cpuid + rdtsc)"); - CHECK(r.insns > 30, "advanced PAST the r6 30-insn / 0x5392D4 wall"); - CHECK(entry_ret == 0x5388ABu, "entry CALL return addr 0x5388AB intact under the stdcall pops"); - CHECK(cpu.gpr[I386_REG_ESP] == esp0 - 8, "esp net == esp0-8 (entry retaddr + final unpopped push)"); + CHECK(r.insns == 60, "60 insns (44 r7 + GetCommandLineW/SUEF + BT family + WinMain frame + handoff call)"); + CHECK(r.insns > 44, "advanced PAST the r7 44-insn / 0x539318 wall"); + // r6/r7 buffer-write evidence is still intact (those shims ran en route). CHECK(ft_lo == 0xC3D4E5F6u && ft_hi == 0x01D7A1B2u, "GetSystemTimeAsFileTime wrote 8B FILETIME via the pushed ptr"); CHECK(qpc_lo == 0x12345678u && qpc_hi == 0x00000000u, @@ -362,27 +429,35 @@ int main(int argc, char **argv) { CHECK(si_cb == 0x44u, "GetStartupInfoW stamped STARTUPINFOW.cb = 0x44 (buffer-write)"); CHECK(sys_page == 0x1000u && sys_ncpu == 4u, "GetSystemInfo wrote dwPageSize=0x1000 + nCPU=4 (bounded-synthetic)"); - // E5 r7 cookie + CPU-identity ops (reaching here proves the fs:/TEB walk - // resolved — a broken FS override would have halted OOB at 0x5392C2). CHECK(cookie == cookie_expect && cookie != 0u, "__security cookie = imul(FT_lo^FT_hi, 0x01000193) stored to global"); - CHECK(cpu.gpr[I386_REG_EBX] == 0x756E6547u, "cpuid leaf0 → ebx vendor 'Genu'"); - CHECK(cpu.gpr[I386_REG_ECX] == 0x6C65746Eu, "cpuid leaf0 → ecx vendor 'ntel' (GenuineIntel)"); - CHECK(cpu.gpr[I386_REG_EAX] == (uint32_t)tsc1 && cpu.gpr[I386_REG_EDX] == (uint32_t)(tsc1 >> 32), - "rdtsc → synthetic monotonic edx:eax"); - CHECK(r.imports_bound == 11, "eleven IAT calls dispatched to native shims"); - CHECK(r.last_import && strcmp(r.last_import, "GetProcAddress") == 0, - "most-recent bound import == GetProcAddress"); - CHECK(r.halt == I386_HALT_UNBOUND_IMPORT, "halt: twelfth IAT call is UNbound (r8 boundary)"); - CHECK(r.halt_op == I386_OP_CALL_RM, "wall op == CALL_RM (FF 15 [disp32])"); - CHECK(r.halt_va == 0x539318u, "wall VA == 0x539318 (the twelfth IAT call)"); - CHECK(r.import_slot == 0x53802Cu, "unbound slot reported == 0x53802C (next import, r8)"); + CHECK(cpu.gpr[I386_REG_EBX] == 0x756E6547u, "cpuid leaf0 → ebx vendor 'Genu' (un-clobbered)"); + CHECK(cpu.tsc == I386_TSC_STEP, "rdtsc advanced the synthetic tsc by exactly one step"); + // E5 r8 — the BT family (eax 0x20 → bt/bts/btr/btc by edx=5 → eax 0x20 again). + CHECK(cpu.gpr[I386_REG_EAX] == 0x20u, "bt/bts/btr/btc sequence left eax = 0x20 (write-back correct)"); + // E5 r8 — the CRT→user-entry handoff itself. + CHECK(r.imports_bound == 13, "thirteen IAT calls dispatched to native shims (r7 11 + GetCommandLineW + SUEF)"); + CHECK(r.last_import && strcmp(r.last_import, "SetUnhandledExceptionFilter") == 0, + "most-recent bound import == SetUnhandledExceptionFilter"); + CHECK(r.halt == I386_HALT_USER_ENTRY, "halt: reached the CRT→user-entry handoff (call WinMain)"); + CHECK(r.halt_op == I386_OP_CALL_REL, "handoff op == CALL_REL (E8 call WinMain)"); + CHECK(r.halt_va == WINMAIN_VA, "handoff VA == WINMAIN_VA 0x539400 (synthetic user entry)"); + CHECK(cpu.eip == WINMAIN_VA, "eip parked at the synthetic user entry"); + // The WinMain stdcall frame the CRT set up (own1: synthetic args). + CHECK(wm_ret == 0x53934Eu, "WinMain return address == 0x53934E (pushed by the handoff call)"); + CHECK(wm_hinst == 0x538000u, "WinMain arg hInstance == image base 0x538000 (synthetic)"); + CHECK(wm_hprev == 0u, "WinMain arg hPrevInstance == NULL"); + CHECK(wm_cmd == CMDLINE_PTR, "WinMain arg lpCmdLine == GetCommandLineW synthetic ptr 0x539FE0"); + CHECK(wm_show == 0x0Au, "WinMain arg nShowCmd == 0x0A"); + CHECK(entry_ret == 0x5388ABu, "entry CALL return addr 0x5388AB still intact deep in the stack"); + CHECK(cpu.gpr[I386_REG_ESP] == esp0 - 0x18, "esp net == esp0-0x18 (entry ret + 4 WinMain args + handoff ret)"); (void)SHIM_PID; (void)SHIM_TID; (void)SHIM_TICK; (void)TEB_BASE; (void)PEB_BASE; if (!emitted) { - printf("__SHIM__ PARTIAL phase=e4_crt_security_cookie insns=%u bound=%u last=%s halt_va=0x%X halt=%s halt_op=%s unbound_slot=0x%X\n", + printf("__SHIM__ PARTIAL phase=e4_reached_user_entry insns=%u bound=%u last=%s entry_va=0x%X halt=%s halt_op=%s (SYNTHETIC user-entry; validated_manjeom=0)\n", r.insns, r.imports_bound, r.last_import ? r.last_import : "-", r.halt_va, - i386_halt_name(r.halt), i386_op_name(r.halt_op), r.import_slot); + i386_halt_name(r.halt), i386_op_name(r.halt_op)); + printf("__SHIM__ INFO real_pe_path=structurally_ready needs:real_i386_PE+IAT_autobind_by_import_name+wider_opcode_coverage+D3DMetal+display\n"); } } diff --git a/native/i386_decode.c b/native/i386_decode.c index 23f55a6..c7b3681 100644 --- a/native/i386_decode.c +++ b/native/i386_decode.c @@ -92,6 +92,10 @@ const char *i386_op_name(i386_op_t op) { case I386_OP_IMUL_R_RM_IMM: return "imul"; case I386_OP_CPUID: return "cpuid"; case I386_OP_RDTSC: return "rdtsc"; + case I386_OP_BT_RM_R: return "bt"; + case I386_OP_BTS_RM_R: return "bts"; + case I386_OP_BTR_RM_R: return "btr"; + case I386_OP_BTC_RM_R: return "btc"; case I386_OP_PREFIX_ONLY: return "(prefix-only)"; case I386_OP_UNKNOWN: default: return "(unknown)"; @@ -347,6 +351,22 @@ int i386_decode_one(const uint8_t *code, size_t buf_len, size_t off, } else if (op2 == 0x31) { out->op = I386_OP_RDTSC; // 0F 31 (E5 r7) — no operands taken += 1; + } else if (op2 == 0xA3 || op2 == 0xAB || op2 == 0xB3 || op2 == 0xBB) { + // BT/BTS/BTR/BTC r/m32, r32 (E5 r8) — bit index in ModR/M.reg + // register; CF ← selected bit. Same /r ModR/M shape as 0F AF. + int m_n = decode_modrm_disp(p + 2, avail - 2, + &out->modrm, &out->sib, + &out->disp, &out->has_disp); + if (m_n == 0) return 0; + out->reg_a = (int8_t)((out->modrm >> 3) & 7); // bit-index register + if (((out->modrm >> 6) & 3) == 3) out->reg_b = (int8_t)(out->modrm & 7); + switch (op2) { + case 0xA3: out->op = I386_OP_BT_RM_R; break; + case 0xAB: out->op = I386_OP_BTS_RM_R; break; + case 0xB3: out->op = I386_OP_BTR_RM_R; break; + case 0xBB: out->op = I386_OP_BTC_RM_R; break; + } + taken += 1 + m_n; // +1 for op2, +m_n for ModR/M } else { out->op = I386_OP_UNKNOWN; taken = n + 1; // skip 0x0F only diff --git a/native/i386_decode.h b/native/i386_decode.h index 3faa7b3..5e1b6c3 100644 --- a/native/i386_decode.h +++ b/native/i386_decode.h @@ -93,6 +93,14 @@ typedef enum { I386_OP_IMUL_R_RM_IMM, // 69 /r id / 6B /r ib — imul r32, r/m32, imm (3-operand) I386_OP_CPUID, // 0F A2 — CPUID (synthetic feature set into eax/ebx/ecx/edx) I386_OP_RDTSC, // 0F 31 — RDTSC (synthetic monotonic counter into edx:eax) + // ── E5 r8 additions (ordinals 61+; appended) — bit-test family (BT/BTS/BTR/ + // BTC, register-second-operand /r forms). The CRT-startup / __scrt_common_main + // path bit-tests feature/flag words. CF ← selected bit; BTS/BTR/BTC also write + // the modified bit back. Plain Intel SDM Vol.2 ops — no protection mechanism. + I386_OP_BT_RM_R, // 0F A3 /r — bt r/m32, r32 (CF ← bit; no write-back) + I386_OP_BTS_RM_R, // 0F AB /r — bts r/m32, r32 (CF ← bit; set bit) + I386_OP_BTR_RM_R, // 0F B3 /r — btr r/m32, r32 (CF ← bit; clear bit) + I386_OP_BTC_RM_R, // 0F BB /r — btc r/m32, r32 (CF ← bit; flip bit) } i386_op_t; #define I386_PREFIX_OPSZ 0x01u diff --git a/native/i386_decode.hexa b/native/i386_decode.hexa index eaaabf7..e00913c 100644 --- a/native/i386_decode.hexa +++ b/native/i386_decode.hexa @@ -234,6 +234,11 @@ fn i386_op_name(op: int) -> str { if op == 58 { return "imul" } // IMUL_R_RM_IMM if op == 59 { return "cpuid" } // CPUID if op == 60 { return "rdtsc" } // RDTSC + // ── E5 r8 additions (ordinals 61+) — bit-test family BT/BTS/BTR/BTC ── + if op == 61 { return "bt" } // BT_RM_R + if op == 62 { return "bts" } // BTS_RM_R + if op == 63 { return "btr" } // BTR_RM_R + if op == 64 { return "btc" } // BTC_RM_R return "(unknown)" // UNKNOWN (0) + default } @@ -640,6 +645,24 @@ fn i386_decode_one(code: [int], buf_len: int, off: int) -> (int, int, int, int, } else if op2 == 0x31 { op = 60 // RDTSC — no operands taken = taken + 1 + } else if (op2 == 0xA3) || (op2 == 0xAB) || (op2 == 0xB3) || (op2 == 0xBB) { + // BT/BTS/BTR/BTC r/m32, r32 (E5 r8) — same /r ModR/M shape as 0F AF. + let tail = modrm_tail(code, opb + 2, avail - 2) + let (m_n, m_modrm, m_sib, m_disp, m_has) = decode_modrm_disp(tail, avail - 2) + modrm = m_modrm + sib = m_sib + disp = m_disp + has_disp = m_has + if m_n == 0 { return (0, op, len, prefixes, reg_a, reg_b, imm, has_imm, has_disp, disp, modrm, sib) } + reg_a = (modrm >> 3) & 7 // bit-index register + if ((modrm >> 6) & 3) == 3 { + reg_b = modrm & 7 + } + if op2 == 0xA3 { op = 61 } // BT_RM_R + else if op2 == 0xAB { op = 62 } // BTS_RM_R + else if op2 == 0xB3 { op = 63 } // BTR_RM_R + else if op2 == 0xBB { op = 64 } // BTC_RM_R + taken = taken + 1 + m_n // +1 for op2, +m_n for ModR/M } else { op = 0 // UNKNOWN taken = n + 1 // skip 0x0F only