# Stack sampler v4: thread start addrs (identify main) + poor-man stack walk. # Usage: python stack_sample2.py --bridge | --probe [secs] import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time sys.stdout.reconfigure(encoding='utf-8', errors='replace') MODE = sys.argv[1] if len(sys.argv) > 1 else "--bridge" SECS = int(sys.argv[2]) if len(sys.argv) > 2 else 15 SHM_NAME = "SonicForge_DAW_IPC_VERIFY" SIZE = 32768 BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe" PROBE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\gui_probe.exe" NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3" LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stack_sample.log" if MODE == "--bridge" else "stack_probe.log") k32 = ctypes.windll.kernel32 k32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_char_p] k32.CreateFileMappingA.restype = ctypes.c_void_p k32.MapViewOfFile.argtypes = [ctypes.c_void_p, w.DWORD, w.DWORD, w.DWORD, ctypes.c_size_t] k32.MapViewOfFile.restype = ctypes.c_void_p INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value hMap = k32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, 0x04, 0, SIZE, SHM_NAME.encode()) ptr = k32.MapViewOfFile(hMap, 0xF001F, 0, 0, 0) def wait_log(pattern, timeout): end = time.time() + timeout while time.time() < end: try: with open(LOG, 'r', encoding='utf-8', errors='replace') as f: if pattern in f.read(): return True except FileNotFoundError: pass time.sleep(0.2) return False if MODE == "--bridge": extra = dict(os.environ) for k in list(extra): if k.startswith("SF_"): del extra[k] extra.update({"SF_SHM_NAME": SHM_NAME, "SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256", "SF_ONCE": "1", "SF_AUTOGUI": "0"}) proc = subprocess.Popen([BRIDGE, "--shm", SHM_NAME], env=extra, stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT) marker = "isPlatformTypeSupported=0" wait_after = 3 else: proc = subprocess.Popen([PROBE, NEXUS, "bridge_exact11", str(SECS)], stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT) marker = "openGUI: attached=0" wait_after = 2 print("pid", proc.pid, "poll", proc.poll(), "mode", MODE) if not wait_log(marker, 120): print("TIMEOUT waiting for", marker) sys.exit(1) time.sleep(wait_after) pid = proc.pid PROCESS_QUERY_INFORMATION = 0x0400 PROCESS_VM_READ = 0x0010 hProc = k32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid) if not hProc: print("OpenProcess failed", ctypes.get_last_error()) sys.exit(1) psapi = ctypes.windll.psapi class MODULEINFO(ctypes.Structure): _fields_ = [("lpBaseOfDll", ctypes.c_void_p), ("SizeOfImage", w.DWORD), ("pad", w.DWORD), ("EntryPoint", ctypes.c_void_p)] psapi.EnumProcessModulesEx.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), w.DWORD, ctypes.POINTER(w.DWORD), w.DWORD] psapi.GetModuleBaseNameA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, w.DWORD] psapi.GetModuleInformation.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(MODULEINFO), w.DWORD] mods = [] buf = (ctypes.c_void_p * 1024)() needed = w.DWORD(0) if psapi.EnumProcessModulesEx(hProc, buf, ctypes.sizeof(buf), ctypes.byref(needed), 3): n = needed.value // ctypes.sizeof(ctypes.c_void_p) for i in range(min(n, 1024)): base = buf[i] name = ctypes.create_string_buffer(260) psapi.GetModuleBaseNameA(hProc, base, name, 260) info = MODULEINFO() psapi.GetModuleInformation(hProc, base, ctypes.byref(info), ctypes.sizeof(info)) mods.append((base, info.SizeOfImage, name.value.decode('utf-8', 'replace'))) mods.sort(key=lambda m: m[0]) def modname(addr): if addr == 0: return "null" for b, s, n in mods: if b <= addr < b + s: return "%s+%x" % (n, addr - b) return "??%x" % addr class THREADENTRY32(ctypes.Structure): _fields_ = [("dwSize", w.DWORD), ("cntUsage", w.DWORD), ("th32ThreadID", w.DWORD), ("th32OwnerProcessID", w.DWORD), ("tpBasePri", ctypes.c_long), ("tpDeltaPri", ctypes.c_long), ("dwFlags", w.DWORD)] class CONTEXT64(ctypes.Structure): _fields_ = [ ("P1Home", ctypes.c_uint64), ("P2Home", ctypes.c_uint64), ("P3Home", ctypes.c_uint64), ("P4Home", ctypes.c_uint64), ("P5Home", ctypes.c_uint64), ("P6Home", ctypes.c_uint64), ("ContextFlags", w.DWORD), ("MxCsr", w.DWORD), ("SegCs", w.WORD), ("SegDs", w.WORD), ("SegEs", w.WORD), ("SegFs", w.WORD), ("SegGs", w.WORD), ("SegSs", w.WORD), ("EFlags", w.DWORD), ("Dr0", ctypes.c_uint64), ("Dr1", ctypes.c_uint64), ("Dr2", ctypes.c_uint64), ("Dr3", ctypes.c_uint64), ("Dr6", ctypes.c_uint64), ("Dr7", ctypes.c_uint64), ("Rax", ctypes.c_uint64), ("Rcx", ctypes.c_uint64), ("Rdx", ctypes.c_uint64), ("Rbx", ctypes.c_uint64), ("Rsp", ctypes.c_uint64), ("Rbp", ctypes.c_uint64), ("Rsi", ctypes.c_uint64), ("Rdi", ctypes.c_uint64), ("R8", ctypes.c_uint64), ("R9", ctypes.c_uint64), ("R10", ctypes.c_uint64), ("R11", ctypes.c_uint64), ("R12", ctypes.c_uint64), ("R13", ctypes.c_uint64), ("R14", ctypes.c_uint64), ("R15", ctypes.c_uint64), ("Rip", ctypes.c_uint64), ] TH32CS_SNAPTHREAD = 0x4 THREAD_SUSPEND_RESUME = 0x0002 THREAD_GET_CONTEXT = 0x0008 THREAD_QUERY_INFORMATION = 0x0040 CONTEXT_CTRL_INT_SEG = 0x100001 | 0x2 | 0x20 ntdll = ctypes.WinDLL("ntdll", use_last_error=True) class THREAD_BASIC_INFORMATION(ctypes.Structure): _fields_ = [("ExitStatus", ctypes.c_long), ("TebBaseAddress", ctypes.c_void_p), ("UniqueProcess", ctypes.c_void_p), ("UniqueThread", ctypes.c_void_p), ("AffinityMask", ctypes.c_void_p), ("Priority", ctypes.c_long), ("BasePriority", ctypes.c_long)] def thread_start_addr(hT): tbi = THREAD_BASIC_INFORMATION() buf = (ctypes.c_uint64 * 4)() try: r = ntdll.NtQueryInformationThread(hT, 9, ctypes.byref(buf), 32, None) # ThreadQuerySetWin32StartAddress if r == 0: return buf[0] except Exception: pass return 0 snap = k32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) te = THREADENTRY32(); te.dwSize = ctypes.sizeof(THREADENTRY32) threads = [] ok = k32.Thread32First(snap, ctypes.byref(te)) while ok: if te.th32OwnerProcessID == pid: threads.append(te.th32ThreadID) ok = k32.Thread32Next(snap, ctypes.byref(te)) exe_name = mods[0][2] if mods else "exe" main_tid = None print("threads(%d): %s" % (len(threads), threads)) for tid in threads: hT = k32.OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, False, tid) if not hT: continue start = thread_start_addr(hT) susp = k32.SuspendThread(hT) ctx = CONTEXT64(); ctx.ContextFlags = CONTEXT_CTRL_INT_SEG r = k32.GetThreadContext(hT, ctypes.byref(ctx)) k32.ResumeThread(hT) k32.CloseHandle(hT) if not r: continue if mods and mods[0][0] <= start < mods[0][0] + mods[0][1] and main_tid is None: main_tid = tid print("tid %d rip=%s rsp=%s start=%s%s" % ( tid, modname(ctx.Rip), modname(ctx.Rsp), modname(start), " <== MAIN?" if (mods and mods[0][0] <= start < mods[0][0] + mods[0][1]) else "")) # poor-man stack walk for MAIN thread if main_tid: print("--- stack walk MAIN tid=%d ---" % main_tid) hT = k32.OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, False, main_tid) k32.SuspendThread(hT) ctx = CONTEXT64(); ctx.ContextFlags = CONTEXT_CTRL_INT_SEG k32.GetThreadContext(hT, ctypes.byref(ctx)) k32.ResumeThread(hT) k32.CloseHandle(hT) rsp = ctx.Rsp PAGE = 0x1000 seen = [] # scan a few pages of stack memory base = rsp & ~0xF data = b"" try: buf2 = ctypes.create_string_buffer(PAGE * 64) read = ctypes.c_size_t(0) if k32.ReadProcessMemory(hProc, ctypes.c_void_p(base), buf2, PAGE * 64, ctypes.byref(read)): data = buf2.raw[:read.value] except Exception: data = b"" for off in range(0, len(data) - 8, 8): val = struct.unpack_from('= 0x7ff000000000: mn = modname(val) if mn not in seen and (mn.startswith("Nexus") or mn.startswith("daw") or mn.startswith("USER32") or mn.startswith("ntdll") or mn.startswith("win32u") or mn.startswith("KERNELBASE") or mn.startswith("ole32") or mn.startswith("combase")): seen.append(mn) print("main stack (module+offset, dedup):") for s in seen: print(" ", s) print("--- log tail ---") try: with open(LOG, 'rb') as f: f.seek(max(0, os.path.getsize(LOG) - 1500)) print(f.read().decode('utf-8', 'replace')) except FileNotFoundError: pass