54 KiB
HyperDbg Linux Port — Status & TODO Ledger
This file tracks what has been changed for the Linux port and what is still stubbed / deferred, so that when the port compiles end-to-end we have a single list of the shortcuts that must be revisited before Linux is actually functional.
It complements README.md (the contributor how-to). This file is
the state of the work; the README is the method.
Status in one line: the userspace library (
libhyperdbg) compiles file-by-file on Linux. Many Windows-only paths are stubbed to compile+link, not yet implemented. See the TODO ledger below. Work has now also started on the kernel module (HyperDbg.ko) — see its section near the end.
Conventions used in this port
Two patterns, applied consistently:
-
In-body
#ifdef _WIN32— for a few Windows-only functions inside an otherwise shared file. The Windows body stays; the Linux#elsereturns a safe default with aTODO(Linux)comment. Examples:DebuggerGetNtoskrnlBase, the$pebpseudo-register, the two test harnesses inscript-engine-wrapper.cpp. -
Separate
*-linux.cppfile + CMake swap — for a whole translation unit that is entirely Windows-specific with only a few public entry points. The original Windows.cppis left 100% untouched; a Linux stub file implements the same public functions;libhyperdbg/CMakeLists.txt'sif(UNIX)block doeslist(REMOVE_ITEM ...)+list(APPEND ...)to swap them. Examples:symbol.cpp→symbol-linux.cpp,pe-parser.cpp→pe-parser-linux.cpp.
Guard style:
- Windows-only code →
#ifdef _WIN32 - Linux-only additions →
#ifdef __linux__ - Cross-platform wrappers →
#if defined(_WIN32) / #elif defined(__linux__)internally
Golden rule (see README): don't scatter #ifdef through program logic — route
Windows API calls through the platform interface so both OSes share the call site.
Platform interface layer (new)
User-mode abstractions in include/platform/user/ (header/ = interface,
code/ = implementation):
| File | Abstracts | Linux status |
|---|---|---|
platform-lib-calls.{h,c} |
OS lib calls: events, handles, threads, sprintf/vsnprintf, perf counters, get-last-error, process/thread ids & names, OS version, strnlen, DebugBreak, zero-memory |
Mostly implemented; a few stubbed (see TODO) |
platform-intrinsics.{h,c} |
CPU ops: rdtsc/rdtscp, interlocked 64-bit ops, bit-test-and-set |
Implemented (GCC builtins) |
platform-serial.{h,c} |
Serial byte transport for remote kernel debugging | Stub — Linux branch returns false; termios impl TODO |
platform-ioctl.{h,c} |
Local kernel-driver IOCTL interface (PlatformDeviceIoControl) + device open (PlatformOpenDevice) |
Stub — no Linux kernel module yet; PlatformOpenDevice returns INVALID_HANDLE_VALUE |
platform-signal.{h,c} |
Console control handler (Ctrl-C / Ctrl-Break) | Implemented (blocks signals + sigwait thread) |
platform-socket.{h,c} |
TCP remote-debugging transport: the few Winsock ops that diverge from POSIX (WSAStartup/WSACleanup lifecycle, closesocket, SD_SEND shutdown, WSAGetLastError) + the accept() length-type (PLATFORM_SOCKLEN). Also owns the Linux POSIX socket-header includes |
Implemented (BSD sockets) — the portable socket calls stay at the tcpclient/tcpserver call sites |
Kernel-mode equivalents live in include/platform/kernel/. Two were extended for
the port because the shared script-eval/ code compiles in both user and kernel
builds: PlatformIntrinsics.c (interlocked 64-bit ops) and PlatformMem.c
(PlatformSprintf).
Shared, OS-neutral headers:
include/platform/general/header/nt-list.h(new) — NT doubly-linked-list helpers (InitializeListHead,InsertHeadList,CONTAINING_RECORD, …) asstatic inlinefor Linux; inert on Windows.include/platform/general/header/Environment.h— SAL annotations, string typedefs,CTRL_*_EVENT,Sleep,INFINITE/WAIT_OBJECT_0,NTAPI/WINAPI,SOCKET, etc.include/SDK/headers/BasicTypes.h— Linux compat typedefs:WCHAR(asUINT16),LARGE_INTEGER,PSIZE_T,LONGLONG, and pointer aliases (PLONG,PULONG,PDWORD,PUCHAR, …).
Linux-only replacement files (stubs)
| File | Replaces | What it stubs | TODO to make real |
|---|---|---|---|
.../script-engine/symbol-linux.cpp |
symbol.cpp (DbgHelp + PDB) |
All Symbol* functions. Only SymbolConvertNameOrExprToAddress does real work: parses a plain hex/decimal literal so numeric addresses work. |
Real ELF/DWARF symbol parser (libdw / libelf / libbfd). |
.../user-level/pe-parser-linux.cpp |
pe-parser.cpp (Windows PE format) |
The 3 public fns: PeShowSectionInformationAndDump, PeIsPE32BitOr64Bit (→ FALSE), PeGetSyscallNumber (→ 0). |
Recreate the Windows IMAGE_* headers for Linux, then port pe-parser.cpp. Only needed for Windows-target debugging on Linux. |
.../driver-loader/install-linux.cpp |
install.cpp (SCM driver loader) |
The 2 Linux-visible public fns: ManageDriver (→ FALSE) and SetupPathForFileName (→ FALSE). The 4 SC_HANDLE helpers (InstallDriver/RemoveDriver/StartDriver/StopDriver) are guarded out of install.h on Linux (never referenced there). |
ManageDriver: load/unload a future HyperDbg Linux kernel module via finit_module/delete_module (needs CAP_SYS_MODULE). SetupPathForFileName: readlink("/proc/self/exe") + strip + append + access() (generic "find a file beside my binary"; also used by hwdbg). |
.../communication/namedpipe-linux.cpp |
namedpipe.cpp (Win32 named-pipe IPC) |
All 10 public NamedPipeServer*/NamedPipeClient* fns. Create* → INVALID_HANDLE_VALUE (print); send/read → 0/FALSE; close → no-op (quiet, unreachable once Create fails). The two internal *Example() demos are not in the Linux TU. |
Back with a filesystem FIFO (mkfifo) or, better for framed bidirectional messages, an AF_UNIX socket derived from the \\.\pipe\NAME string; overlapped/event I/O collapses to blocking read/write. |
All four self-guard with #ifdef __linux__ and print
"... is not supported on Linux yet" at runtime (named-pipe: only in the
Create* entry points, to avoid per-loop spam).
Changes so far (files that build on Linux)
Swept from the port markers (Platform* calls, _WIN32 / __linux__ guards).
"Wrapper sweep" = mechanical rename of a raw Win32 call to its Platform*
equivalent, behavior-preserving.
Build / precompiled header
libhyperdbg/pch.h— the big one:#ifdef _WIN32guards around Windows-only headers (dbghelp.h, SCM, etc.); unconditional includes of the new platform headers +nt-list.h; include-order fixes.install.his now included unconditionally (it was Windows-only) since its Linux-unsafeSC_HANDLEdecls are self-guarded — see below.header/debugger/driver-loader/install.h— the 4SC_HANDLEdriver helpers (InstallDriver/RemoveDriver/StartDriver/StopDriver) guarded#ifdef _WIN32(they use the Windows-onlySC_HANDLEtype and have no callers outside install.cpp);ManageDriver,SetupPathForFileNameand theDRIVER_FUNC_*macros stay visible on both so the Linux callers (libhyperdbg.cpp, hwdbg, export.cpp) compile.CMakeLists.txt(top-level) —if(LINUX)branch builds onlyscript-engine,libhyperdbg,hyperdbg-cli; linksThreads::Threads. (CMake is Linux-only; Windows builds from the.vcxproj/ MSBuild.)libhyperdbg/CMakeLists.txt—if(UNIX)swapssymbol.cpp→symbol-linux.cpp,pe-parser.cpp→pe-parser-linux.cpp, andinstall.cpp→install-linux.cpp; header entries point at the real nested paths.
script-engine subproject
-
GCC-compatibility fixes across
script-engine/(pch.h,type.h,scanner.h,globals.{h,c},script-engine.c, itsCMakeLists.txt) so the shared script-engine builds as a Linux.so. -
SDK import/interface headers (
include/SDK/HyperDbgSdk.h,include/SDK/imports/user/HyperDbg*Imports.h) adjusted for the Linux build. -
Upstream
d44c726d("add float type in script engine") re-broke the Linux build; fixed with two mechanical swaps:pch.h— define_GNU_SOURCEon Linux ahead of every libc header. The float-literal parser's#elsebranch callsstrtof_l/strtod_l, which glibc declares in<stdlib.h>only under__USE_GNU.newlocale/freelocalein the same branch are plain POSIX-2008 and already resolved.script-engine.c:1381,1400— 2×_snprintf_s(Buf, sizeof(Buf), _TRUNCATE, "%d", …)→PlatformSprintf(Buf, sizeof(Buf), "%d", …). Both buffers areCHAR[32]formatting a single%d, so the dropped_TRUNCATEtruncation semantics are unreachable.
-
code/hardware.c(+header/hardware.h) — added to script-engine CMakeSourceFiles(was in the vcxproj, missing from the Linux build). Provides theHardwareScriptInterpreter*family the hwdbg TUs call. One bucket-1 swap to compile:RtlZeroMemory→PlatformZeroMemory×2 (lines 499, 564). Resolves theHardwareScriptInterpreter*link errors. NOTE: two more files are still in the vcxproj but missing from script-engine's CMake —code/script_include.c(undefinedResolveIncludePath/ParseIncludeFile/FileExists/InsertStrNew) andinclude/platform/user/code/platform-lib-calls.c(undefinedPlatform*in libscript-engine.so). Adding both is the next script-engine build step. -
code/script_include.c+platform-lib-calls.c— DONE (2026-07-24).libscript-engine.sois now fully self-contained (zero undefined refs):- Added
../include/platform/user/code/platform-lib-calls.cto the baseSourceFiles— it's compiled into libhyperdbg too, but each.soneeds its own copy of thePlatform*symbols (script-engine callsPlatformSnprintf/PlatformStrDup/PlatformVsnprintf/PlatformZeroMemory). No swap: it builds on both OSes. Needed one root-cause fix — added#include <time.h>to its Linux include block (clock_gettime/CLOCK_MONOTONICinPlatformQueryPerformanceCounter); it previously only compiled because libhyperdbg's pch pulled<time.h>in transitively, but script-engine's pch doesn't. - Added
code/script_include.cto baseSourceFiles, then swapped it for a new empty stubcode/script_include-linux.cunderif(UNIX)(user chose stubs over porting the Win32 path logic for now). The stub implementsResolveIncludePath/FileExists/ParseIncludeFile/InsertStrNewas no-op/failure; script#includeresolution is unsupported on Linux until a real resolver (readlink("/proc/self/exe")+stat) lands.script_include.cleft pristine (Windows path usesGetModuleFileNameA/GetFileAttributesA).
Result: every remaining CLI-link undefined ref (23) now belongs to
liblibhyperdbg.soalone — keystone (ks_*, 5), the excludedpt.cpp(CommandPt*/HyperDbgPt*, 4), and missing libhyperdbg TUs behind the PCI-ID/Vendor, Stepping, text-callback,ShowMessages,IrpBasedBufferThreadsymbols (14). - Added
Kernel-level debugger (remote protocol)
kd.cpp— largest sweep (~46Platform*): serial open/configure/close viaPlatformSerial*; events/threads/handles viaPlatform*;RtlZeroMemory,DeviceIoControl,GetLastError,GetCurrentProcessIdsweeps; the raw Win32 serial data-path functions kept under#ifdef _WIN32with a Linux interface#else.kernel-listening.cpp—RtlZeroMemory,strnlen_s→PlatformStrnlen, serial wait/read viaPlatformSerial*,DebugBreak→PlatformDebugBreak.readmem.cpp—ZeroMemory/DeviceIoControl/GetLastErrorsweep.
common
common.cpp— multi-category port:_stricmp→PlatformStrCaseCmp(new wrapper, see platform-lib-calls).CpuIdEx→CpuCpuIdEx. This also fixes an upstream typo, not just a Linux shim. Commit85843494("add CPU intrinsics for user mode") swept the MSVC intrinsics onto the newCpu*wrappers but wroteCpuIdExwhere the wrapper is actually namedCpuCpuIdEx(cf.__cpuid→CpuCpuIddone correctly alongside).CpuIdExis defined nowhere in the tree, so this line does not compile on Windows either — it was just never rebuilt there. Same bug also hitcpu.cpp:148/cpu.cpp:200— now fixed there too (user-confirmed the swap); still present onmaster.IsFileExistA— kept as-is (POSIXstruct stat/stat()); added#include <sys/stat.h>under__linux__.- Whole Windows-only bodies guarded
#ifdef _WIN32with a Linux stub + TODO:SetPrivilege(token/LUID; no Linux callers → returns FALSE),IsFileExistW(_wstat; wide-char deferred → FALSE),GetConfigFilePath(GetModuleFileNameW/shlwapi; wide-char deferred → empties path),ListDirectory(FindFirstFileA; → empty vector, only caller is eval.cpp test harness). CheckAddressValidityUsingTsx— TSX_xbegin/_xend/_XBEGIN_STARTEDkept verbatim; they resolve on GCC via<immintrin.h>(included under__linux__) once-mrtmis set. Addedtarget_compile_options(libhyperdbg PRIVATE -mrtm)inlibhyperdbg/CMakeLists.txt(UNIX block). Real 1:1 mapping, not a stub — the path is gated byg_RtmSupport, which is live on Linux now that cpuid works.
platform-lib-calls.{h,c}— addedPlatformStrCaseCmp(Str1, Str2): Windows_stricmp; Linuxstrcasecmp(added<strings.h>to the Linux includes).
Core debugger
debugger.cpp—DeviceIoControl/GetLastError/RtlZeroMemorysweep;DebuggerGetNtoskrnlBasebody guarded#ifdef _WIN32(Linux returns NULL).interpreter.cpp—SetConsoleCtrlHandler→PlatformInstallCtrlHandler; script-engine message-callback cast.break-control.cpp— console-control handler routed throughplatform-signal.
App / export layer
export.cpp—strcpy_s×2 →PlatformStrCpy(new bounded-copy wrapper; also unblocked onceSetupPathForFileNamebecame visible via the install-linux swap).platform-lib-calls.{h,c}— addedPlatformStrCpy(Dest, DestSize, Src): Windowsstrcpy_s; Linux does the same bounds check (empty-string + non-zero on overflow) since glibc has nostrcpy_s. ⚠️ Linux branch not yet tested against the exactstrcpy_ssemantics — verify before relying on it.platform-lib-calls.{h,c}— addedPlatformCopyMemory(Destination, Source, Size): WindowsRtlCopyMemory; Linuxmemcpy(same arg order/signature).
hwdbg
hwdbg-interpreter.cpp—RtlCopyMemory→PlatformCopyMemory,RtlZeroMemory→PlatformZeroMemory.hwdbg-scripts.cpp+hwdbg-commands/hw.cpp— added to libhyperdbg CMakeSourceFiles(were missing from the Linux build; present in the vcxproj all along), plus theheader/hwdbg/hwdbg-scripts.hlist entry.hw.cppbuilt clean;hwdbg-scripts.cppneeded one bucket-1 swap:RtlZeroMemory→PlatformZeroMemory(line 415). Both compile on Linux now. NOTE: theirHardwareScriptInterpreter*callees live inscript-engine/code/hardware.c, now added to script-engine's CMake (see the script-engine subproject section).
objects
objects.cpp— wrapper sweep:RtlCopyMemory×2→PlatformCopyMemory,RtlZeroMemory→PlatformZeroMemory,DeviceIoControl×4→PlatformDeviceIoControl,GetLastError×4 (the"ioctl failed"sites)→PlatformGetLastError; plus the two enum-first-member= {0}→= {}value-init fixes (lines 30/31; line 145's struct isn't enum-first, left as= {0}).
rev
rev-ctrl.cpp—DeviceIoControl→PlatformDeviceIoControl,GetLastError→PlatformGetLastError.
App
dllmain.cpp— wholeDllMainbody guarded#ifdef _WIN32(Windows DLL loader entry point; no Linux equivalent, no callers in our code, body was a no-op). Linux TU is intentionally empty.libhyperdbg.cpp— the main app (load/unload driver, open device, event loop). Wrapper sweeps:DeviceIoControl×5→PlatformDeviceIoControl,GetLastError×7→PlatformGetLastError,CloseHandle×4→PlatformCloseHandle,WaitForSingleObject→PlatformWaitForSingleObject,CreateEvent(NULL,FALSE,FALSE,NULL)→PlatformCreateEvent(FALSE,FALSE),CreateThread(...)→PlatformCreateThread(fn,NULL), and the 2-argstrcpy_s(g_DriverName, ...)template form→PlatformStrCpy(g_DriverName, sizeof(g_DriverName), ...). The local-driver device open (CreateFileA("\\.\HyperDbgDebuggerDevice", ...)) → newPlatformOpenDevicewrapper (see platform-ioctl); the surrounding error-handling block stays at the call site (ERROR_ACCESS_DENIED/ERROR_GEN_FAILUREadded toEnvironment.hso it compiles on Linux).WindowsSetDebugPrivilegenow resolves viawindows-privilege.c(see below).windows-only/windows-privilege.{c,h}—WindowsSetDebugPrivilegewas already ported (Windows: token/SeDebugPrivilege; Linux branch:return TRUE), just not wired into the Linux build. Addedwindows-privilege.ctolibhyperdbg/CMakeLists.txt(both the source list and theLANGUAGE CXXblock); un-guarded its header include inpch.h(was#ifdef _WIN32, header is Linux-safe); fixed the header's#ifdef __linux__SDK include path (../../../../→../../../../../, it sits one dir deeper inwindows-only/).
User-level debugger
ud.cpp— wrapper sweep (bucket 1):DeviceIoControl→PlatformDeviceIoControl, the"ioctl failed"GetLastError→PlatformGetLastError,RtlZeroMemory→PlatformZeroMemory, the event-handleCloseHandle→PlatformCloseHandle,CreateEvent(NULL,x,y,NULL)→PlatformCreateEvent(x,y). Win32 process/thread-management (bucket 2): 5 newPlatform*process wrappers (Group A)- whole-body
#ifdef _WIN32guards on the Toolhelp walkers /UdPrintError(Group B). See the Process-control section of the TODO ledger for details.
- whole-body
platform-lib-calls.{h,c}— addedPlatformCreateProcess/PlatformOpenProcess/PlatformTerminateProcess/PlatformResumeThread/PlatformGetExitCodeProcess(Windows real, Linux stub).
Script engine
script-engine-wrapper.cpp— 6×RtlZeroMemory→PlatformZeroMemory; the two wide-char test harnesses (AllocateStructForCasting,ScriptEngineWrapperTestParser) guarded out on Linux (see wide-char TODO).script-eval/Functions.c—sprintf_s,__rdtsc(p),Interlocked*,RtlZeroMemory,QueryPerformance*→Platform*/Cpu*wrappers.script-eval/PseudoRegisters.c—$pebguarded#ifdef _WIN32(returns 0);$tid/$pid/$core/$pname→ newPlatform*wrappers.
Commands & app (wrapper sweeps + wide-char casts)
- Meta:
dump.cpp,pagein.cpp,pe.cpp,start.cpp,restart.cpp(the last two carry(WCHAR *)wide-char shim casts). - Debugging/extension:
a.cpp,dt-struct.cpp,k.cpp,preactivate.cpp,prealloc.cpp,sleep.cpp,track.cpp,pci-id.cpp,pcicam.cpp,pcitree.cpp. - App:
messaging.cpp,packets.cpp(vsprintf_s→PlatformVsnprintf),spinlock.cpp(_interlockedbittestandset→CpuInterlockedBitTestAndSet). (libhyperdbg.cppitself has its own entry under App above.)
Commands batch sweep — DONE (2026-07-20)
Mechanical bucket-1 sweep across 23 files in libhyperdbg/code/debugger/commands/.
Behaviour-preserving 1:1 substitutions only, no logic touched:
| Swap | Count |
|---|---|
DeviceIoControl → PlatformDeviceIoControl |
19 |
GetLastError → PlatformGetLastError |
20 |
RtlZeroMemory / ZeroMemory → PlatformZeroMemory |
15 |
GetCurrentProcessId → PlatformGetCurrentProcessId |
7 |
CloseHandle → PlatformCloseHandle |
2 |
TerminateThread → PlatformTerminateThread |
1 |
- Debugging:
flush.cpp,lm.cpp,load.cpp,output.cpp,rdmsr.cpp,s.cpp,test.cpp,wrmsr.cpp - Extension:
apic.cpp,hide.cpp,idt.cpp,ioapic.cpp,lbr.cpp,lbrdump.cpp,pa2va.cpp,pcicam.cpp,pcitree.cpp,pte.cpp,smi.cpp,unhide.cpp,va2pa.cpp - Meta:
sym.cpp,disconnect.cpp
Pure addition: PlatformTerminateThread in platform-lib-calls.{h,c} — real
TerminateThread on Windows, Linux stub returning TRUE (see TODO below).
After this sweep pt.cpp is the only file left in commands/ holding raw Win32
calls. Note lm.cpp still does not compile, but for unrelated pre-existing
reasons (RTL_PROCESS_MODULES / RTL_PROCESS_MODULE_INFORMATION undeclared, and
WCHAR * vs wchar_t * — the wide-char item below); none of those are on lines
this sweep touched.
Command files wired into Linux CMake — DONE (2026-07-24)
The 2026-07-20 sweep ported these files' bucket-1 calls but never added them to
libhyperdbg/CMakeLists.txt, so they were compiled on Windows only and their
Command* symbols were unresolved at the Linux CLI link. Added the 12 missing
command TUs to SourceFiles:
- Debugging:
continue.cpp,gg.cpp - Extension:
apic.cpp,idt.cpp,ioapic.cpp,lbr.cpp,lbrdump.cpp,pcicam.cpp,pcitree.cpp,smi.cpp,xsetbv.cpp - Plus top-level
ucpuid.cpp(definesCommandUserCpuid/CommandUserCpuidHelp/CommandCpuidRequestCpuid/CommandShowUserCpuidMessage; lives atlibhyperdbg/ucpuid.cpp, not undercode/, which is why the earlier diff missed it).
Stragglers the 07-20 sweep didn't cover, fixed to compile (all mechanical):
apic.cpp— 2×RtlCopyMemory→PlatformCopyMemory(sweep only did the ZeroMemory family).- Enum-first aggregate init
= {0}→= {}(GCC rejectsint→enum in{0};{}value-inits identically):lbr.cpp:332,lbrdump.cpp:242,pcicam.cpp:51,pcitree.cpp:49,smi.cpp:125. (apic'sLAPIC_PAGE {0}and lbrdump'sCHAR[] {0}are scalar-first and compile fine, left as-is.) ucpuid.cpp—DeviceIoControl→PlatformDeviceIoControl,GetLastError→PlatformGetLastError(1 each; same drop-in as the sweep).Environment.h— added the two missing generic Win32 aliasesucpuid.cppneeds:#define CONST constandtypedef float FLOAT;(winnt.h spellings; benefits any future file too).
pt.cpp deliberately excluded from the Linux build via an if(UNIX)
REMOVE_ITEM (like namedpipe/symbol/pe-parser). It's the un-started
process-control port (OpenProcess(PROCESS_ALL_ACCESS),
CreateToolhelp32Snapshot, CreateThread, WaitForMultipleObjects, Win32
process/thread handles) — see the pt.cpp TODO below. CommandPt/CommandPtHelp
stay unresolved, same as before it was added to the list.
Result: every Command* link error is resolved except the two CommandPt*.
Remaining CLI-link buckets are unrelated: Sym* (symbol-linux stub, 15), ks_*
(keystone Linux lib, 5), Platform* + include-family (script_include.c /
platform-lib-calls.c missing from script-engine's CMake, 8).
rdmsr.cpp core-count — DONE (2026-07-22)
Follow-up to the bucket-1 sweep of rdmsr.cpp above (this is a separate bucket-2
change, not part of the mechanical batch). The command needs the online logical-CPU
count to size its per-core transfer buffer; on Windows that came from two static
helpers (GetWindowsCompatibleNumberOfCores via GetSystemInfo, and
GetWindowsNumaNumberOfCores via GetLogicalProcessorInformationEx — the latter
GetProcAddress-loaded from kernel32.dll, so entirely Win32).
- Both static helpers + their
glpie_ttypedef guarded#ifdef _WIN32(rdmsr.cpp lines 36–111). Windows bodies untouched. - Call site (
CommandRdmsr, ~line 199): Windows path keeps the NUMA-then-fallback logic; Linux#elsecalls the newPlatformGetActiveProcessorCount(). - Pure addition:
PlatformGetActiveProcessorCount(VOID)inplatform-lib-calls.{h,c}— WindowsGetSystemInfo→dwNumberOfProcessors; Linuxsysconf(_SC_NPROCESSORS_ONLN)(returns 0 if unknown). ⚠️ Linux branch marked "Not yet tested!!" in the source — verify before relying on it.
settings.cpp config-file I/O — DONE (2026-07-22)
debugging-commands/settings.cpp reads/writes the settings INI via a wide-char
(WCHAR[MAX_PATH]) path and std::ifstream/std::ofstream. Two Linux-only
blockers, both the deferred wide-char item: (1) GetConfigFilePath(PWCHAR) — on
Linux WCHAR is unsigned short but PWCHAR is short * (signedness mismatch,
-fpermissive); (2) libstdc++ has no basic_ifstream/basic_ofstream
constructor taking a 2-byte WCHAR*, and no cast can bridge wide→char*.
Both CommandSettingsGetValueFromConfigFile and CommandSettingsSetValueFromConfigFile
are already dead on Linux (GetConfigFilePath empties the path,
IsFileExistW returns FALSE), so their whole bodies were guarded #ifdef _WIN32
(Windows verbatim) with a Linux #else stub (return FALSE / no-op +
UNREFERENCED_PARAMETER + TODO(Linux)). Same pattern-1 convention already used
for GetConfigFilePath/IsFileExistW/ListDirectory in common.cpp. This also
moves the GetConfigFilePath call sites into the Windows branch, resolving the
PWCHAR signedness error without touching the shared typedef.
CommandSettingsLoadDefaultValuesFromConfigFile only calls the guarded getter —
no wide-char of its own, left as-is.
debug.cpp serial connect — DONE (2026-07-22)
meta-commands/debug.cpp (the .debug command — connect to a remote debuggee
over serial/namedpipe). Two mechanical fixes:
_stricmp×4 (COM-port name compare inCommandDebugCheckComPort) →PlatformStrCaseCmp(the existing common.cpp wrapper; Windows_stricmp, Linuxstrcasecmp).CBR_*baud-rate constants (CommandDebugCheckBaudratevalidation) — pure addition toEnvironment.hLinux block: the 15 winbase.hCBR_110…CBR_256000#defines kept at their canonical Windows values (each equals its baud rate). Matches the CTRL_/PROCESS_/ERROR_* constant blocks already there. Actual Linux serial I/O is still the platform-serial termios TODO.
formats.cpp DECIMAL_DIG — DONE (2026-07-22)
meta-commands/formats.cpp:94 uses DECIMAL_DIG (the ISO C99 <float.h> macro,
widest-float round-trip digit count) in a .formats output format string. MSVC
exposes it transitively via its CRT/pch; glibc needs the explicit include.
Pure addition: #include <float.h> in the Environment.h Linux block
(next to <wchar.h>/<unistd.h>). Standard header, cross-platform-safe.
forwarding.cpp output-event forwarding — DONE (2026-07-22)
communication/forwarding.cpp is the debug-output forwarding subsystem (sinks:
file / TCP / named-pipe / loadable module). Bucket-2, multi-category. User chose
new Platform* wrappers for both non-trivial subsystems (not guards).
- Clean swap:
WriteFile→PlatformWriteFile(exact match; the original assigns the result then unconditionallyreturn TRUE, so the error-check below was already dead code —BytesWrittenout-param dropped, still referenced by that dead code so no unused-var).CloseHandle(FILE source)→PlatformCloseFile(fclose on Linux — matches the FILE* the new open returns). - File sink (
CreateFileA, narrow path +OPEN_ALWAYS): existingPlatformOpenFileForWritingdid NOT fit (it is wide +CREATE_ALWAYS/truncate), so pure additionPlatformOpenFileForWritingNarrow(const CHAR *)— WindowsCreateFileA(...OPEN_ALWAYS...); Linuxfopen("r+b")thenfopen("w+b")(open-existing-no-truncate, else create) returning theFILE*as the HANDLE. Named...Narrow(user preference) to flag the char-width difference vs the wide variant. Because the path is already a narrowstd::string, this sink actually works on Linux — no wide-char blocker. - Module/plugin sink (
LoadLibraryA/GetProcAddress/FreeLibrary): pure additionsPlatformLoadLibrary/PlatformGetProcAddress/PlatformFreeLibraryin platform-lib-calls — Windows real; Linuxdlopen(RTLD_NOW|RTLD_LOCAL)/dlsym/dlclose(dlclose return inverted to keep "non-zero == success").PlatformGetProcAddressreturnsPVOID(noFARPROCon Linux); caller casts. - Build: added
#include <dlfcn.h>to the platform-lib-calls Linux includes; added${CMAKE_DL_LIBS}to thelibhyperdbglink (top-level CMakeLists) — the portable dl link (empty where dl is in libc). Only libhyperdbg compiles platform-lib-calls.c on Linux, so no other target needed it. - ⚠️ All four new Linux branches marked
NOT YET TESTED!!in source.
namedpipe.cpp — DONE via namedpipe-linux.cpp + CMake swap (2026-07-22)
communication/namedpipe.cpp is a whole Windows-only TU (Win32 named-pipe IPC:
server CreateNamedPipe/ConnectNamedPipe, client CreateFileA on \\.\pipe\
- overlapped
ReadFile/WriteFileviag_OverlappedIoStructureFor*Debugger). Followed pattern-2 (like symbol/pe-parser/install): newnamedpipe-linux.cpp#ifdef __linux__stubs of the 10 publicNamedPipe{Server,Client}*fns;namedpipe.cppleft 100% untouched; CMakeif(UNIX)REMOVE_ITEM + APPEND swap. 6 callers link the stubs transparently (forwarding/kd/debug/export/tests/test). See the Linux-replacement-files table above for the FIFO/AF_UNIX TODO.
TCP transport (tcpclient/tcpserver/remote-connection) — DONE (2026-07-22)
The TCP remote-debugging path. Unlike namedpipe, the socket code is genuinely
cross-platform — Winsock and POSIX share the BSD-socket API almost verbatim — so
it stays as shared .cpp (no -linux.cpp fork). A new platform-socket.{h,c}
module (sibling to platform-serial/-signal; wired into pch.h, top-level +
libhyperdbg CMake, and the Windows .vcxproj/.filters) isolates the handful of
things that actually diverge. User chose the platform-API route over Winsock-name
shims in Environment.h.
communication/tcpclient.cpp/tcpserver.cpp— the portable calls (socket/connect/bind/listen/accept/send/recv/shutdown/getaddrinfo) stay at the call sites unchanged. Swapped only the divergent ones toPlatform*:WSAStartup(MAKEWORD(2,2),&wsaData)→PlatformSocketInitialize()(WSADATA local + MAKEWORD dropped; keeps theIResult != 0shape — wrapper returns 0 on success),WSACleanup()→PlatformSocketCleanup(),closesocket→PlatformCloseSocket,shutdown(...,SD_SEND)→PlatformShutdownSocketSend,WSAGetLastError()→PlatformGetSocketError(), plus the bucket-1ZeroMemory→PlatformZeroMemory. tcpserver'saccept()length out-paramINT AddrLen→PLATFORM_SOCKLEN AddrLen— the one genuine type incompatibility (Winsockint*vs POSIXsocklen_t*).communication/remote-connection.cpp— bucket-1 sweep (.listen/.connectcommand layer over the sockets):RtlZeroMemory×3→PlatformZeroMemory,SetEvent→PlatformSetEvent,CreateEvent(NULL,FALSE,FALSE,NULL)→PlatformCreateEvent(FALSE,FALSE),CreateThread(...)→PlatformCreateThread(fn,NULL)(unusedDWORD ThreadIdlocal dropped),WaitForSingleObject→PlatformWaitForSingleObject.platform-socket.{h,c}(pure addition):PlatformSocketInitialize/PlatformSocketCleanup/PlatformCloseSocket/PlatformShutdownSocketSend/PlatformGetSocketError+ thePLATFORM_SOCKLENtypedef. The.cmaps the divergent primitives (close / shutdown-flag / last-error) via small per-OS macros so each wrapper body is written once; only the WSAStartup vs no-op lifecycle keeps a small in-body guard. The.halso owns the Linux POSIX socket-header includes (<sys/socket.h>/<netdb.h>/<netinet/in.h>/<arpa/inet.h>/<unistd.h>), so any TU using sockets gets them via pch. ⚠️ Linux branches not yet exercised at runtime (compile-verified only).
Note the pre-existing latent teardown-ordering issue is unchanged; see the
PlatformTerminateThread TODO below (remote-connection's listening thread).
asm-vmx-checks — DONE via GAS port + CMake swap (2026-07-24)
code/assembly/asm-vmx-checks-masm-windows.asm (MASM, AsmVmxSupportDetection:
CPUID.1 → bt ecx,5 → return 1/0 for VMX support) only assembles with ml64.
Ported to a new GAS/AT&T-syntax code/assembly/asm-vmx-checks-gas-unix.s —
instruction-for-instruction equivalent, .globl AsmVmxSupportDetection, plus a
.note.GNU-stack non-exec-stack marker. No logic change. The Windows .asm is
left untouched. CMake: base SourceFiles entry renamed to -masm-windows.asm,
and the if(UNIX) block REMOVE_ITEMs it, APPENDs the .s, and calls
enable_language(ASM) so CMake assembles it with the system assembler. Windows
libhyperdbg.vcxproj + .filters <MASM Include=...> updated to the renamed
-masm-windows.asm. Assemble-verified with as (exports AsmVmxSupportDetection).
Remaining libhyperdbg TUs wired into Linux CMake — DONE (2026-07-24)
Same gap class as the 2026-07-24 command-file batch: four TUs present in
libhyperdbg.vcxproj all along but never added to libhyperdbg/CMakeLists.txt,
so they were compiled on Windows only and their symbols were unresolved at the
Linux CLI link. Added to SourceFiles (plus their four header entries):
| TU | Symbols it was missing |
|---|---|
code/app/messaging.cpp |
ShowMessages, SetTextMessageCallback, SetTextMessageCallbackUsingSharedBuffer, UnsetTextMessageCallback |
code/app/packets.cpp |
IrpBasedBufferThread |
code/debugger/core/steppings.cpp |
SteppingStepOver, SteppingStepOverForGu, SteppingRegularStepIn, SteppingInstrumentationStepIn, SteppingInstrumentationStepInForTracking |
code/debugger/misc/pci-id.cpp |
GetVendorById, GetDeviceFromVendor, FreeVendor, FreePciIdDatabase |
steppings.cpp compiled with no changes at all. The others needed:
messaging.cpp— 1 bucket-1 swap:RtlZeroMemory→PlatformZeroMemory(line 57).packets.cpp— bucket-1 sweep:ZeroMemory→PlatformZeroMemory,DeviceIoControl→PlatformDeviceIoControl,SetEvent→PlatformSetEvent,CloseHandle→PlatformCloseHandle,GetLastError×2→PlatformGetLastError. Plus the packet-reader's dedicated device handle: the sameCreateFileA("\\.\HyperDbgDebuggerDevice", GENERIC_READ|GENERIC_WRITE, …)block already ported inlibhyperdbg.cpp→PlatformOpenDevice(...), with the surroundingERROR_ACCESS_DENIED/ERROR_GEN_FAILUREhandling left at the call site (identical shape to the libhyperdbg.cpp call site).pci-id.cpp— 4×strncpy_s→ newPlatformStrNCpy(below), andGetVendorByIdbody guarded#ifdef _WIN32(below).
Pure addition: PlatformStrNCpy(Dest, DestSize, Src, Count) in
platform-lib-calls.{h,c} — Windows strncpy_s verbatim; Linux reproduces the
documented rules: copies D = min(Count, strlen(Src)) chars and null-terminates,
or empties Dest + returns non-zero if D doesn't fit; Count == _TRUNCATE instead
copies as much as fits and returns STRUNCATE. Sibling of the existing
PlatformStrCpy; a plain PlatformStrCpy could not be reused because
ReadLine (pci-id.cpp:76) copies a substring out of a longer stream buffer.
Also pure addition to the Environment.h Linux block: _TRUNCATE
(((SIZE_T)-1)) and STRUNCATE (80) at their canonical MSVC values, matching
the existing CBR_*/ERROR_*/PROCESS_* constant blocks.
⚠️ Linux branch marked NOT YET TESTED!! in source, like PlatformStrCpy.
GetVendorById body guarded #ifdef _WIN32 (pattern 1; user chose the stub
over porting). It resolves the PCI ID database relative to the executable:
GetModuleHandle/GetModuleFileName, then strrchr(Path, '\\') to strip the
exe name and append PCI_ID_DATABASE_PATH. The two Win32 calls would wrap
cleanly (readlink("/proc/self/exe")), but the surrounding logic is
Windows-path-shaped in two places — the '\\' separator and the constant itself
(pci-id.h:44, "constants\\pci.ids") — so wrapping only the calls would leave
strrchr returning NULL, silently overwriting the whole path and resolving
against the cwd. That is a behaviour change, not a port, so the whole body is
Windows-only and Linux returns NULL. GetVendorByIdStr, GetDeviceFromVendor,
FreeVendor and FreePciIdDatabase are plain C and compile unchanged.
Consequence: !pcitree / !pcicam show no vendor or device names on Linux.
Result: the CLI link is down from 23 undefined refs to 9, and every
"missing TU" bucket is now closed. What is left is both known and deliberate:
ks_* (5 — keystone, no Linux lib linked) and CommandPt*/HyperDbgPt*
(4 — pt.cpp excluded from the Linux build, port not started).
keystone assembler stubbed on Linux — DONE (2026-07-24)
The 5 ks_* link errors (ks_open/ks_option/ks_asm/ks_errno/ks_close).
Only a Windows keystone.lib is vendored (libraries/keystone/release-lib/,
PE/COFF) and dependencies/keystone/ ships headers only — no Linux library,
no source, not a git submodule. The link_directories(...keystone...) and the
keystone entry in target_link_libraries were previously commented out in the
top-level CMakeLists.txt to get past cannot find -lkeystone, which is what
left the 5 symbols unresolved.
Because dependencies/keystone/include/keystone/keystone.h is present (and
included unconditionally from pch.h:138), every ks_* type and constant
(ks_engine, ks_err, ks_arch, KS_ARCH_X86, KS_MODE_64,
KS_OPT_SYNTAX_INTEL, …) resolves fine on Linux — only the 5 functions are
missing. That means no header surgery and no -linux.cpp fork were needed:
assembler.h's class declaration (which has ks_err KsErr as a member and
ks_arch/KS_* as default arguments) compiles untouched.
All 5 calls are confined to one method, so this is pattern 1 — the body of
AssembleData::Assemble (assembler.cpp:119) is guarded #ifdef _WIN32
(Windows verbatim) with a Linux #else that emits
"err, the assembler is not supported on Linux yet" and returns -1, plus
UNREFERENCED_PARAMETER ×4 and a TODO(Linux). No call-site changes were needed:
both callers (HyperDbgAssembleGetLength, HyperDbgAssemble) already treat a
non-zero Assemble() return as failure and return FALSE, so the stub flows
through the existing error paths. The rest of the TU stays live on Linux —
notably ParseAssemblyData, which does the <symbol> resolution.
Affects the a (assemble) command and anything calling HyperDbgAssemble.
- Real fix: build upstream Keystone for Linux →
libkeystone.a/.so, then restore the two commented-out lines in the top-levelCMakeLists.txtand drop the guard.
Result: the CLI link is down to 4 undefined refs, all pt.cpp
(CommandPt, CommandPtHelp, HyperDbgPtMmapSendRequest,
HyperDbgPerformPtOperation) — the one remaining deliberate exclusion.
pt.cpp stubbed on Linux — DONE (2026-07-24) — THE LINK NOW SUCCEEDS
The last 4 undefined refs (CommandPt, CommandPtHelp,
HyperDbgPerformPtOperation, HyperDbgPtMmapSendRequest). pt.cpp was already
REMOVE_ITEM'd from the Linux build; it now gets a replacement stub instead of
leaving the symbols dangling, following the same pattern as symbol.cpp,
pe-parser.cpp, install.cpp and namedpipe.cpp.
New code/debugger/commands/extension-commands/pt-linux.cpp (#ifdef __linux__)
implements only the 4 externally visible functions — the two command entry
points reached from the dispatch table (CommandPt, CommandPtHelp) and the two
kernel-request helpers declared in debugger.h. Each prints a "not supported on
Linux yet" note; the BOOLEAN pair returns FALSE. Everything else in pt.cpp is
helper code reached only through those entry points, so it does not exist in the
Linux TU. pt.cpp is still left 100% untouched. CMake if(UNIX) now does the
usual REMOVE_ITEM + APPEND pair.
Result: hyperdbg-cli links and runs on Linux for the first time. With the
symbol-visibility fix below also in place, the binary starts, reaches the
HyperDbg> prompt and executes host-side commands correctly — verified with
.help, .help !monitor, .formats 0x1337 (full hex/decimal/octal/binary/char/
time/float/double output) and ? 5 * 8 (script engine) — then exits cleanly on
.exit. The port is out of the compile/link phase and into the runtime phase.
Run it with:
LD_LIBRARY_PATH=$PWD/libhyperdbg:$PWD/script-engine ./hyperdbg-cli/hyperdbg-cli
- Port
pt.cppfor real — see the process-control entry in the TODO ledger.
Symbol visibility: honour the existing IMPORT_EXPORT_* model — DONE (2026-07-24)
Build-system change in the top-level CMakeLists.txt, two parts:
target_compile_definitions(script-engine PRIVATE HYPERDBG_SCRIPT_ENGINE)
target_compile_definitions(libhyperdbg PRIVATE HYPERDBG_LIBHYPERDBG)
set_target_properties(script-engine libhyperdbg PROPERTIES
C_VISIBILITY_PRESET hidden
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON)
Why. include/SDK/imports/user/HyperDbg*Imports.h already carries a Linux
branch for each library's export macro — IMPORT_EXPORT_LIBHYPERDBG and friends
expand to __attribute__((visibility("default"))) when the library's own
HYPERDBG_* macro is defined, and to nothing otherwise, mirroring the
__declspec(dllexport)/dllimport pair used on Windows. 77 symbols are annotated
for libhyperdbg and 29 for script-engine. Neither half was active on Linux: the
HYPERDBG_* defines were never set by CMake, and visibility("default") is a
no-op unless the compiler's baseline visibility is hidden (it can only raise a
symbol above the baseline, and the baseline was already default). So the whole
export model existed in the headers but did nothing, and every symbol in both
libraries was exported.
That matters because ELF merges same-named exported symbols across shared objects, whereas a Windows DLL's non-exported globals are private to it. Three globals were defined independently in both libraries and were being silently collapsed into one object at load time:
| Symbol | libhyperdbg | script-engine |
|---|---|---|
g_MessageHandler |
header/globals/globals.h:460 |
code/globals.c:24 |
g_HwdbgInstanceInfo |
” | ” |
g_HwdbgInstanceInfoIsValid |
” | ” |
Turning both halves on restores the Windows semantics (private unless explicitly exported). Exported data symbols drop to 0 in both libraries; total exports go from everything to 264 (libhyperdbg) and 25 (script-engine). The link stays clean — 0 undefined references — so nothing was relying on an unannotated symbol crossing a library boundary. No source file was touched.
⚠️ VISIBILITY_INLINES_HIDDEN is included to match the usual CMake pairing; if a
future change takes the address of an inline member across a library boundary and
compares it, that flag is the first thing to re-check.
Grouped by subsystem. These are the shortcuts taken to reach compilation.
Wide characters (the big deferred item)
wchar_t2-vs-4-byte /WCHAR/UNICODE_STRING. On LinuxWCHARis 2 bytes but nativewchar_tis 4. Current state: bogus 2-byte reinterpret casts at call sites (each markedTEMPORARY LINUX SHIM / TODO(Linux)), and thescript-engine-wrapper.cpptest harnesses are guarded out on Linux. Needs a realstd::wstring→ 2-byte-WCHAR/UTF-16 conversion helper before Linux file I/O and the user-debugger path can actually open files.
Symbols
- symbol-parser (
Sym*) Linux stubs — DONE (2026-07-24). The 15Sym*exports (SymConvertNameToAddress,SymLoadFileSymbol,SymbolInitLoad,SymGetFieldOffset,SymShowDataBasedOnSymbolTypes,SymSetTextMessageCallback, …) live in the Windows-onlysymbol-parser/subproject (DbgHelp + DIA-SDK pdbex, ~3800 LOC, not built on Linux). They're called only byscript-engine/code/script-engine.c, solibscript-engine.sowas the one with the unresolved refs. Addedscript-engine/code/symbol-stub-linux.c(new file,#ifdef __linux__) implementing all 15 as no-op/failure stubs (return0/FALSE, out-params cleared), signatures mirroringHyperDbgSymImports.h. Wired intoscript-engine/CMakeLists.txtunderif(UNIX). User chose the stub path over a real backend port. Resolves all 15Sym*link errors. - Replace the
symbol-linux.cpp(Symbol*) andsymbol-stub-linux.c(Sym*) stubs with a real ELF/DWARF (or LLVM DebugInfo/PDB) symbol parser.
Assembler (keystone)
assembler.cpp::AssembleData::Assemble— Linux body stubbed (return -1). Needs a Linux Keystone build plus the two restored CMake lines; see the keystone section above.
PCI ID database
pci-id.cpp::GetVendorById— Linux returns NULL (whole body Windows-only). Needsreadlink("/proc/self/exe")plus a portable path separator and a portablePCI_ID_DATABASE_PATH(pci-id.h:44is"constants\\pci.ids"). Until then!pcitree/!pcicamprint no vendor/device names on Linux.
PE parsing
- Recreate Windows
IMAGE_*headers for Linux and portpe-parser.cpp(replacepe-parser-linux.cpp). Affects!pe;!hidecurrently gets0for all syscall numbers (a Windows-guest feature, meaningless on a Linux host).
Transport
platform-serial.cLinux branch — implement termios serial I/O (currently stub).platform-ioctl.cLinux branch — needs a Linux kernel module + real ioctl (currently stub). This is the local driver interface used across many files.
Process control — ud.cpp DONE (2026-07-18)
Mechanical wrapper sweep (bucket 1): DeviceIoControl×10→PlatformDeviceIoControl,
GetLastError×10 (the "ioctl failed" sites)→PlatformGetLastError,
RtlZeroMemory×3→PlatformZeroMemory, CloseHandle×1 (event-handle close)→PlatformCloseHandle,
CreateEvent(NULL,FALSE,FALSE,NULL)→PlatformCreateEvent(FALSE,FALSE).
Bucket 2 (Win32 process/thread mgmt) resolved two ways (user decision):
- Group A — new guarded
Platform*wrappers (real on Windows, Linux stub) for the self-contained calls:PlatformCreateProcess(keepsSTARTUPINFOinternal),PlatformOpenProcess,PlatformTerminateProcess,PlatformResumeThread,PlatformGetExitCodeProcess— all inplatform-lib-calls.{h,c}. ud.cpp call sites swapped 1:1;UdCreateSuspendedProcessnow callsPlatformCreateProcesswith theCREATE_SUSPENDED|CREATE_NEW_CONSOLEflags kept at the call site. - Group B — whole-body
#ifdef _WIN32guards (Windows verbatim, Linux stub) for the calls interleaved with UI/walk logic:UdListProcessThreads,UdCheckThreadByProcessId(Toolhelp snapshot walk),UdPrintError(FormatMessage/MAKELANGID). No Linux Toolhelp/THREADENTRY32types needed. - Pure additions: Linux
PROCESS_INFORMATIONstruct inSDK/headers/BasicTypes.h;PROCESS_TERMINATE/PROCESS_QUERY_LIMITED_INFORMATION/CREATE_SUSPENDED/CREATE_NEW_CONSOLE/STILL_ACTIVE#defines inEnvironment.h(Linux block).
TODO(Linux) still open in the wrapper bodies: real fork+execve/ptrace process
backend, and the Toolhelp thread-enumeration equivalent (/proc) — all stubbed for now.
Process control — pt.cpp NOT STARTED
extension-commands/pt.cpp (Intel PT) is the last file in commands/ with raw
Win32 calls — 27 sites, and no #ifdef guards anywhere in the file. This is
not a mechanical sweep: it is bucket-2 work structurally like ud.cpp was, so
it needs the same Group A / Group B decision per call site before anything is
swapped. What is in there:
-
Toolhelp process/thread snapshot walk (
CreateToolhelp32Snapshot+ enumeration) -
OpenProcess/OpenThread+ handle lifetime (CloseHandle×8) -
Thread affinity pinning (
SetThreadAffinityMask) -
Trace-thread lifecycle (
CreateEvent/CreateThread/ stop-event signalling) -
Two
DeviceIoControl+GetLastErrorpairs (these two are bucket-1 and could be swapped in isolation, but leaving the file wholly untouched is cleaner than a half-ported file) -
Decide Group A vs Group B per call site, then port
pt.cpp.
PlatformTerminateThread — Linux stub
- Real teardown for the remote-debuggee listening thread.
There is no POSIX equivalent to TerminateThread by design: nothing forcibly
kills a thread without unwinding, because doing so never releases the target's
locks. pthread_cancel is the nearest primitive but differs semantically —
deferred by default, and glibc implements it as a forced unwind that runs
destructors and cleanup handlers (any catch (...) that does not rethrow breaks
it). PTHREAD_CANCEL_ASYNCHRONOUS recovers the abruptness at the cost of
undefined behaviour for anything not async-cancel-safe.
The only caller does not need a cancellation primitive at all.
RemoteConnectionThreadListeningToDebuggee (remote-connection.cpp:211) is a
while (g_IsConnectedToRemoteDebuggee) loop that blocks in recv() and breaks
on any receive error. Correct Linux teardown is therefore: clear the flag →
shutdown(fd, SHUT_RDWR) to kick the thread out of recv → pthread_join. It
exits through its own existing error path.
Deferred because that requires a call-site reorder in disconnect.cpp
(teardown before thread-kill instead of after) plus SD_SEND → SHUT_RDWR in
CommunicationClientShutdownConnection to wake a blocked reader — a behaviour
change to shared logic, which the no-logic-changes rule rules out mid-port.
Also blocked on PlatformCreateThread, which returns NULL on Linux, so the
listening thread never starts there in the first place.
Observation, not this batch's job: on Windows the current ordering kills the
listener while it may be mid-recv holding the socket, and only then shuts the
socket down. That looks like a latent upstream bug — worth raising separately,
but out of scope for the port.
Misc runtime stubs
PlatformGetOsVersion— Linux returns FALSE; implement viauname.$pebpseudo-register — returns 0 on Linux (PEB is NT-only).DebuggerGetNtoskrnlBase— returns NULL on Linux (NT system-module enum).- File I/O / user-debugger paths — stubbed (also blocked on wide-char above).
common.cpp::ListDirectory— Linux returns empty vector; reimplement withopendir/readdir+fnmatch(only caller today is eval.cpp's test harness).common.cpp::GetConfigFilePath— Linux empties the path; resolve viareadlink("/proc/self/exe")+ appendCONFIG_FILE_NAME(also blocked on wide-char).common.cpp::SetPrivilege/IsFileExistW— Linux stubs (no callers / wide-char deferred respectively).cpu.cpp:148&cpu.cpp:200— sameCpuIdEx→CpuCpuIdExtypo fix as common.cpp. Done.
Build system
- Add
.gitignorerules for the in-source CMake build output (CMakeCache.txt,CMakeFiles/, generatedMakefile,cmake_install.cmake,*.o,*.so) — or switch to an out-of-sourcebuild/directory. Now also applies to the kbuild output (*.ko,*.mod,*.mod.c,.*.cmd,modules.order,Module.symvers), which lands in-tree at the repo root.
Kernel module (HyperDbg.ko) — IN PROGRESS
Phase two. Windows' seven kernel binaries (hyperkd.sys + hyperhv/hyperlog/
hyperevade/hypertrace/hyperperf/kdserial, KMDF export drivers) collapse into one
Linux module. Design notes: the root Kbuild header.
cd linux/kernel # NOT the CMake root Makefile — that builds the user-mode CLI
make # -> HyperDbg.ko at the repo root
make clean / load / unload
make KDIR=/path/to/linux-headers
kbuild prefers the root Kbuild under M=, so both build systems coexist.
Kbuild lists every future object with the un-ported ones commented out. Current
front: the ten include/platform/kernel/ TUs, all uncommented — so the build
stops at the first #error "Not yet implemented".
| TU | Status |
|---|---|
PlatformMem.c |
✅ builds (pre-existing) |
PlatformIntrinsics.c |
✅ builds (pre-existing) |
PlatformIntrinsicsVmx.c |
✅ builds (pre-existing) |
PlatformBroadcast.c |
✅ ported 2026-08-01 |
PlatformCpu.c |
✅ ported 2026-08-01 — see below |
PlatformDbg.c |
✅ ported 2026-08-01 — see below |
PlatformDpc.c |
❌ 2 stubs — next to fail; also 2 type errors (see below) |
PlatformIrql.c |
❌ 2 stubs |
PlatformEvent.c |
❌ 3 stubs |
PlatformIo.c |
❌ 3 stubs |
PlatformSpinlock.c |
❌ 3 stubs |
PlatformTime.c |
❌ 3 stubs |
PlatformProcess.c |
❌ 5 stubs |
PlatformCpu.c — DONE (2026-08-01)
| Windows | Linux |
|---|---|
KeQueryActiveProcessorCount(0) |
num_online_cpus() |
KeGetCurrentProcessorNumberEx(NULL) |
raw_smp_processor_id() |
Both Windows calls return system-wide (all-group) values, matching Linux's flat CPU numbering — no group translation.
raw_ chosen deliberately: plain smp_processor_id() asserts preemption is
disabled (CONFIG_DEBUG_PREEMPT splat), a precondition the Windows API doesn't
have and no caller establishes. Both current callers can run preemptible
(Logging.c:1001 per-core VMX buffer index, but only on the vmx-root path;
PseudoRegisters.c:49 $core). The 72 raw KeGetCurrentProcessorNumberEx sites
in 21 files still pending (__CPU_INDEX__, g_GuestState[]/g_DbgState[]) are
all vmx-root/raised-IRQL — identical either way.
No new include needed (resolves via linux/kernel/pch.h; add <linux/smp.h> if
that ever breaks).
PlatformDbg.c — DONE (2026-08-01)
| Windows | Linux |
|---|---|
vDbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_INFO_LEVEL, Format, ArgList) |
vprintk(Format, ArgList) |
va_start/va_end scaffolding unchanged; no new include needed.
Nuances, deliberately not "fixed" (would be logic changes; callers unaffected):
-
printk reads its level from a
KERN_*prefix on the format string and no caller has one → messages land at the default loglevel, not "info". -
printk isn't MSVC's CRT (no
%I64d/%ws). All six callers use only%s/%x. -
Logging.c:833chunks atDbgPrintLimitation(512,Constants.h:223) = DbgPrint's limit, not printk'sLOG_LINE_MAX. Works, just mis-sized. -
Revisit loglevel +
DbgPrintLimitationsizing once kernel logging is exercised.
Next up: PlatformDpc.c
Not a body swap — the signatures fail too (PRKDPC, PKDEFERRED_ROUTINE unknown
on Linux). Needs Linux types for the KDPC object + deferred-routine fn-ptr first.
Backing it with smp_call_function_single_async / irq_work / tasklet is a
design decision, not a mechanical swap.
Building
cmake . # re-run only when CMake files change
make # build; find the next file that fails, port it, repeat