HyperDbg/hyperdbg/linux/PORTING_STATUS.md
Max Raulea 622ea9df97
Some checks are pending
vs2026-ci / win-amd64-build (debug, x64) (push) Waiting to run
vs2026-ci / win-amd64-build (release, x64) (push) Waiting to run
vs2026-ci / Deploy release (push) Blocked by required conditions
Porting Documentation update
2026-08-01 17:09:55 +02:00

54 KiB
Raw Permalink Blame History

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:

  1. In-body #ifdef _WIN32 — for a few Windows-only functions inside an otherwise shared file. The Windows body stays; the Linux #else returns a safe default with a TODO(Linux) comment. Examples: DebuggerGetNtoskrnlBase, the $peb pseudo-register, the two test harnesses in script-engine-wrapper.cpp.

  2. Separate *-linux.cpp file + CMake swap — for a whole translation unit that is entirely Windows-specific with only a few public entry points. The original Windows .cpp is left 100% untouched; a Linux stub file implements the same public functions; libhyperdbg/CMakeLists.txt's if(UNIX) block does list(REMOVE_ITEM ...) + list(APPEND ...) to swap them. Examples: symbol.cppsymbol-linux.cpp, pe-parser.cpppe-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, …) as static inline for 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 (as UINT16), 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 _WIN32 guards around Windows-only headers (dbghelp.h, SCM, etc.); unconditional includes of the new platform headers + nt-list.h; include-order fixes. install.h is now included unconditionally (it was Windows-only) since its Linux-unsafe SC_HANDLE decls are self-guarded — see below.
  • header/debugger/driver-loader/install.h — the 4 SC_HANDLE driver helpers (InstallDriver/RemoveDriver/StartDriver/StopDriver) guarded #ifdef _WIN32 (they use the Windows-only SC_HANDLE type and have no callers outside install.cpp); ManageDriver, SetupPathForFileName and the DRIVER_FUNC_* macros stay visible on both so the Linux callers (libhyperdbg.cpp, hwdbg, export.cpp) compile.
  • CMakeLists.txt (top-level) — if(LINUX) branch builds only script-engine, libhyperdbg, hyperdbg-cli; links Threads::Threads. (CMake is Linux-only; Windows builds from the .vcxproj / MSBuild.)
  • libhyperdbg/CMakeLists.txtif(UNIX) swaps symbol.cppsymbol-linux.cpp, pe-parser.cpppe-parser-linux.cpp, and install.cppinstall-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, its CMakeLists.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_SOURCE on Linux ahead of every libc header. The float-literal parser's #else branch calls strtof_l/strtod_l, which glibc declares in <stdlib.h> only under __USE_GNU. newlocale/ freelocale in 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 are CHAR[32] formatting a single %d, so the dropped _TRUNCATE truncation semantics are unreachable.
  • code/hardware.c (+ header/hardware.h) — added to script-engine CMake SourceFiles (was in the vcxproj, missing from the Linux build). Provides the HardwareScriptInterpreter* family the hwdbg TUs call. One bucket-1 swap to compile: RtlZeroMemoryPlatformZeroMemory ×2 (lines 499, 564). Resolves the HardwareScriptInterpreter* link errors. NOTE: two more files are still in the vcxproj but missing from script-engine's CMake — code/script_include.c (undefined ResolveIncludePath/ParseIncludeFile/FileExists/InsertStrNew) and include/platform/user/code/platform-lib-calls.c (undefined Platform* 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.so is now fully self-contained (zero undefined refs):

    • Added ../include/platform/user/code/platform-lib-calls.c to the base SourceFiles — it's compiled into libhyperdbg too, but each .so needs its own copy of the Platform* symbols (script-engine calls PlatformSnprintf/ 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_MONOTONIC in PlatformQueryPerformanceCounter); it previously only compiled because libhyperdbg's pch pulled <time.h> in transitively, but script-engine's pch doesn't.
    • Added code/script_include.c to base SourceFiles, then swapped it for a new empty stub code/script_include-linux.c under if(UNIX) (user chose stubs over porting the Win32 path logic for now). The stub implements ResolveIncludePath/FileExists/ParseIncludeFile/InsertStrNew as no-op/failure; script #include resolution is unsupported on Linux until a real resolver (readlink("/proc/self/exe") + stat) lands. script_include.c left pristine (Windows path uses GetModuleFileNameA/GetFileAttributesA).

    Result: every remaining CLI-link undefined ref (23) now belongs to liblibhyperdbg.so alone — keystone (ks_*, 5), the excluded pt.cpp (CommandPt*/HyperDbgPt*, 4), and missing libhyperdbg TUs behind the PCI-ID/Vendor, Stepping, text-callback, ShowMessages, IrpBasedBufferThread symbols (14).

Kernel-level debugger (remote protocol)

  • kd.cpp — largest sweep (~46 Platform*): serial open/configure/close via PlatformSerial*; events/threads/handles via Platform*; RtlZeroMemory, DeviceIoControl, GetLastError, GetCurrentProcessId sweeps; the raw Win32 serial data-path functions kept under #ifdef _WIN32 with a Linux interface #else.
  • kernel-listening.cppRtlZeroMemory, strnlen_sPlatformStrnlen, serial wait/read via PlatformSerial*, DebugBreakPlatformDebugBreak.
  • readmem.cppZeroMemory / DeviceIoControl / GetLastError sweep.

common

  • common.cpp — multi-category port:
    • _stricmpPlatformStrCaseCmp (new wrapper, see platform-lib-calls).
    • CpuIdExCpuCpuIdEx. This also fixes an upstream typo, not just a Linux shim. Commit 85843494 ("add CPU intrinsics for user mode") swept the MSVC intrinsics onto the new Cpu* wrappers but wrote CpuIdEx where the wrapper is actually named CpuCpuIdEx (cf. __cpuidCpuCpuId done correctly alongside). CpuIdEx is defined nowhere in the tree, so this line does not compile on Windows either — it was just never rebuilt there. Same bug also hit cpu.cpp:148 / cpu.cpp:200now fixed there too (user-confirmed the swap); still present on master.
    • IsFileExistA — kept as-is (POSIX struct stat/stat()); added #include <sys/stat.h> under __linux__.
    • Whole Windows-only bodies guarded #ifdef _WIN32 with 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_STARTED kept verbatim; they resolve on GCC via <immintrin.h> (included under __linux__) once -mrtm is set. Added target_compile_options(libhyperdbg PRIVATE -mrtm) in libhyperdbg/CMakeLists.txt (UNIX block). Real 1:1 mapping, not a stub — the path is gated by g_RtmSupport, which is live on Linux now that cpuid works.
  • platform-lib-calls.{h,c} — added PlatformStrCaseCmp(Str1, Str2): Windows _stricmp; Linux strcasecmp (added <strings.h> to the Linux includes).

Core debugger

  • debugger.cppDeviceIoControl / GetLastError / RtlZeroMemory sweep; DebuggerGetNtoskrnlBase body guarded #ifdef _WIN32 (Linux returns NULL).
  • interpreter.cppSetConsoleCtrlHandlerPlatformInstallCtrlHandler; script-engine message-callback cast.
  • break-control.cpp — console-control handler routed through platform-signal.

App / export layer

  • export.cppstrcpy_s×2 → PlatformStrCpy (new bounded-copy wrapper; also unblocked once SetupPathForFileName became visible via the install-linux swap).
  • platform-lib-calls.{h,c} — added PlatformStrCpy(Dest, DestSize, Src): Windows strcpy_s; Linux does the same bounds check (empty-string + non-zero on overflow) since glibc has no strcpy_s. ⚠️ Linux branch not yet tested against the exact strcpy_s semantics — verify before relying on it.
  • platform-lib-calls.{h,c} — added PlatformCopyMemory(Destination, Source, Size): Windows RtlCopyMemory; Linux memcpy (same arg order/signature).

hwdbg

  • hwdbg-interpreter.cppRtlCopyMemoryPlatformCopyMemory, RtlZeroMemoryPlatformZeroMemory.
  • hwdbg-scripts.cpp + hwdbg-commands/hw.cpp — added to libhyperdbg CMake SourceFiles (were missing from the Linux build; present in the vcxproj all along), plus the header/hwdbg/hwdbg-scripts.h list entry. hw.cpp built clean; hwdbg-scripts.cpp needed one bucket-1 swap: RtlZeroMemoryPlatformZeroMemory (line 415). Both compile on Linux now. NOTE: their HardwareScriptInterpreter* callees live in script-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, RtlZeroMemoryPlatformZeroMemory, 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.cppDeviceIoControlPlatformDeviceIoControl, GetLastErrorPlatformGetLastError.

App

  • dllmain.cpp — whole DllMain body 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, WaitForSingleObjectPlatformWaitForSingleObject, CreateEvent(NULL,FALSE,FALSE,NULL)PlatformCreateEvent(FALSE,FALSE), CreateThread(...)PlatformCreateThread(fn,NULL), and the 2-arg strcpy_s(g_DriverName, ...) template form→PlatformStrCpy(g_DriverName, sizeof(g_DriverName), ...). The local-driver device open (CreateFileA("\\.\HyperDbgDebuggerDevice", ...)) → new PlatformOpenDevice wrapper (see platform-ioctl); the surrounding error-handling block stays at the call site (ERROR_ACCESS_DENIED/ERROR_GEN_FAILURE added to Environment.h so it compiles on Linux). WindowsSetDebugPrivilege now resolves via windows-privilege.c (see below).
  • windows-only/windows-privilege.{c,h}WindowsSetDebugPrivilege was already ported (Windows: token/SeDebugPrivilege; Linux branch: return TRUE), just not wired into the Linux build. Added windows-privilege.c to libhyperdbg/CMakeLists.txt (both the source list and the LANGUAGE CXX block); un-guarded its header include in pch.h (was #ifdef _WIN32, header is Linux-safe); fixed the header's #ifdef __linux__ SDK include path (../../../../../../../../../, it sits one dir deeper in windows-only/).

User-level debugger

  • ud.cpp — wrapper sweep (bucket 1): DeviceIoControlPlatformDeviceIoControl, the "ioctl failed" GetLastErrorPlatformGetLastError, RtlZeroMemoryPlatformZeroMemory, the event-handle CloseHandlePlatformCloseHandle, CreateEvent(NULL,x,y,NULL)PlatformCreateEvent(x,y). Win32 process/thread-management (bucket 2): 5 new Platform* process wrappers (Group A)
    • whole-body #ifdef _WIN32 guards on the Toolhelp walkers / UdPrintError (Group B). See the Process-control section of the TODO ledger for details.
  • platform-lib-calls.{h,c} — added PlatformCreateProcess/PlatformOpenProcess/ PlatformTerminateProcess/PlatformResumeThread/PlatformGetExitCodeProcess (Windows real, Linux stub).

Script engine

  • script-engine-wrapper.cpp — 6× RtlZeroMemoryPlatformZeroMemory; the two wide-char test harnesses (AllocateStructForCasting, ScriptEngineWrapperTestParser) guarded out on Linux (see wide-char TODO).
  • script-eval/Functions.csprintf_s, __rdtsc(p), Interlocked*, RtlZeroMemory, QueryPerformance*Platform*/Cpu* wrappers.
  • script-eval/PseudoRegisters.c$peb guarded #ifdef _WIN32 (returns 0); $tid/$pid/$core/$pname → new Platform* 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_sPlatformVsnprintf), spinlock.cpp (_interlockedbittestandsetCpuInterlockedBitTestAndSet). (libhyperdbg.cpp itself 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
DeviceIoControlPlatformDeviceIoControl 19
GetLastErrorPlatformGetLastError 20
RtlZeroMemory / ZeroMemoryPlatformZeroMemory 15
GetCurrentProcessIdPlatformGetCurrentProcessId 7
CloseHandlePlatformCloseHandle 2
TerminateThreadPlatformTerminateThread 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 (defines CommandUserCpuid / CommandUserCpuidHelp / CommandCpuidRequestCpuid / CommandShowUserCpuidMessage; lives at libhyperdbg/ucpuid.cpp, not under code/, which is why the earlier diff missed it).

Stragglers the 07-20 sweep didn't cover, fixed to compile (all mechanical):

  • apic.cpp — 2× RtlCopyMemoryPlatformCopyMemory (sweep only did the ZeroMemory family).
  • Enum-first aggregate init = {0}= {} (GCC rejects int→enum in {0}; {} value-inits identically): lbr.cpp:332, lbrdump.cpp:242, pcicam.cpp:51, pcitree.cpp:49, smi.cpp:125. (apic's LAPIC_PAGE {0} and lbrdump's CHAR[] {0} are scalar-first and compile fine, left as-is.)
  • ucpuid.cppDeviceIoControlPlatformDeviceIoControl, GetLastErrorPlatformGetLastError (1 each; same drop-in as the sweep).
  • Environment.h — added the two missing generic Win32 aliases ucpuid.cpp needs: #define CONST const and typedef 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_t typedef guarded #ifdef _WIN32 (rdmsr.cpp lines 36111). Windows bodies untouched.
  • Call site (CommandRdmsr, ~line 199): Windows path keeps the NUMA-then-fallback logic; Linux #else calls the new PlatformGetActiveProcessorCount().
  • Pure addition: PlatformGetActiveProcessorCount(VOID) in platform-lib-calls.{h,c} — Windows GetSystemInfodwNumberOfProcessors; Linux sysconf(_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 in CommandDebugCheckComPort) → PlatformStrCaseCmp (the existing common.cpp wrapper; Windows _stricmp, Linux strcasecmp).
  • CBR_* baud-rate constants (CommandDebugCheckBaudrate validation) — pure addition to Environment.h Linux block: the 15 winbase.h CBR_110CBR_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: WriteFilePlatformWriteFile (exact match; the original assigns the result then unconditionally return TRUE, so the error-check below was already dead code — BytesWritten out-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): existing PlatformOpenFileForWriting did NOT fit (it is wide + CREATE_ALWAYS/truncate), so pure addition PlatformOpenFileForWritingNarrow(const CHAR *) — Windows CreateFileA(...OPEN_ALWAYS...); Linux fopen("r+b") then fopen("w+b") (open-existing-no-truncate, else create) returning the FILE* as the HANDLE. Named ...Narrow (user preference) to flag the char-width difference vs the wide variant. Because the path is already a narrow std::string, this sink actually works on Linux — no wide-char blocker.
  • Module/plugin sink (LoadLibraryA/GetProcAddress/FreeLibrary): pure additions PlatformLoadLibrary/PlatformGetProcAddress/PlatformFreeLibrary in platform-lib-calls — Windows real; Linux dlopen(RTLD_NOW|RTLD_LOCAL)/ dlsym/dlclose (dlclose return inverted to keep "non-zero == success"). PlatformGetProcAddress returns PVOID (no FARPROC on Linux); caller casts.
  • Build: added #include <dlfcn.h> to the platform-lib-calls Linux includes; added ${CMAKE_DL_LIBS} to the libhyperdbg link (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/WriteFile via g_OverlappedIoStructureFor*Debugger). Followed pattern-2 (like symbol/pe-parser/install): new namedpipe-linux.cpp #ifdef __linux__ stubs of the 10 public NamedPipe{Server,Client}* fns; namedpipe.cpp left 100% untouched; CMake if(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 to Platform*: WSAStartup(MAKEWORD(2,2),&wsaData)PlatformSocketInitialize() (WSADATA local + MAKEWORD dropped; keeps the IResult != 0 shape — wrapper returns 0 on success), WSACleanup()PlatformSocketCleanup(), closesocketPlatformCloseSocket, shutdown(...,SD_SEND)PlatformShutdownSocketSend, WSAGetLastError()PlatformGetSocketError(), plus the bucket-1 ZeroMemoryPlatformZeroMemory. tcpserver's accept() length out-param INT AddrLenPLATFORM_SOCKLEN AddrLen — the one genuine type incompatibility (Winsock int* vs POSIX socklen_t*).
  • communication/remote-connection.cpp — bucket-1 sweep (.listen/.connect command layer over the sockets): RtlZeroMemory×3→PlatformZeroMemory, SetEventPlatformSetEvent, CreateEvent(NULL,FALSE,FALSE,NULL)PlatformCreateEvent(FALSE,FALSE), CreateThread(...)PlatformCreateThread(fn,NULL) (unused DWORD ThreadId local dropped), WaitForSingleObjectPlatformWaitForSingleObject.
  • platform-socket.{h,c} (pure addition): PlatformSocketInitialize/ PlatformSocketCleanup/PlatformCloseSocket/PlatformShutdownSocketSend/ PlatformGetSocketError + the PLATFORM_SOCKLEN typedef. The .c maps 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 .h also 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: RtlZeroMemoryPlatformZeroMemory (line 57).
  • packets.cpp — bucket-1 sweep: ZeroMemoryPlatformZeroMemory, DeviceIoControlPlatformDeviceIoControl, SetEventPlatformSetEvent, CloseHandlePlatformCloseHandle, GetLastError×2→PlatformGetLastError. Plus the packet-reader's dedicated device handle: the same CreateFileA("\\.\HyperDbgDebuggerDevice", GENERIC_READ|GENERIC_WRITE, …) block already ported in libhyperdbg.cppPlatformOpenDevice(...), with the surrounding ERROR_ACCESS_DENIED/ERROR_GEN_FAILURE handling left at the call site (identical shape to the libhyperdbg.cpp call site).
  • pci-id.cpp — 4× strncpy_s→ new PlatformStrNCpy (below), and GetVendorById body 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-level CMakeLists.txt and 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.

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.cpp for 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_t 2-vs-4-byte / WCHAR / UNICODE_STRING. On Linux WCHAR is 2 bytes but native wchar_t is 4. Current state: bogus 2-byte reinterpret casts at call sites (each marked TEMPORARY LINUX SHIM / TODO(Linux)), and the script-engine-wrapper.cpp test harnesses are guarded out on Linux. Needs a real std::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 15 Sym* exports (SymConvertNameToAddress, SymLoadFileSymbol, SymbolInitLoad, SymGetFieldOffset, SymShowDataBasedOnSymbolTypes, SymSetTextMessageCallback, …) live in the Windows-only symbol-parser/ subproject (DbgHelp + DIA-SDK pdbex, ~3800 LOC, not built on Linux). They're called only by script-engine/code/script-engine.c, so libscript-engine.so was the one with the unresolved refs. Added script-engine/code/symbol-stub-linux.c (new file, #ifdef __linux__) implementing all 15 as no-op/failure stubs (return 0/FALSE, out-params cleared), signatures mirroring HyperDbgSymImports.h. Wired into script-engine/CMakeLists.txt under if(UNIX). User chose the stub path over a real backend port. Resolves all 15 Sym* link errors.
  • Replace the symbol-linux.cpp (Symbol*) and symbol-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). Needs readlink("/proc/self/exe") plus a portable path separator and a portable PCI_ID_DATABASE_PATH (pci-id.h:44 is "constants\\pci.ids"). Until then !pcitree / !pcicam print no vendor/device names on Linux.

PE parsing

  • Recreate Windows IMAGE_* headers for Linux and port pe-parser.cpp (replace pe-parser-linux.cpp). Affects !pe; !hide currently gets 0 for all syscall numbers (a Windows-guest feature, meaningless on a Linux host).

Transport

  • platform-serial.c Linux branch — implement termios serial I/O (currently stub).
  • platform-ioctl.c Linux 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 (keeps STARTUPINFO internal), PlatformOpenProcess, PlatformTerminateProcess, PlatformResumeThread, PlatformGetExitCodeProcess — all in platform-lib-calls.{h,c}. ud.cpp call sites swapped 1:1; UdCreateSuspendedProcess now calls PlatformCreateProcess with the CREATE_SUSPENDED|CREATE_NEW_CONSOLE flags kept at the call site.
  • Group B — whole-body #ifdef _WIN32 guards (Windows verbatim, Linux stub) for the calls interleaved with UI/walk logic: UdListProcessThreads, UdCheckThreadByProcessId (Toolhelp snapshot walk), UdPrintError (FormatMessage/MAKELANGID). No Linux Toolhelp/THREADENTRY32 types needed.
  • Pure additions: Linux PROCESS_INFORMATION struct in SDK/headers/BasicTypes.h; PROCESS_TERMINATE/PROCESS_QUERY_LIMITED_INFORMATION/CREATE_SUSPENDED/ CREATE_NEW_CONSOLE/STILL_ACTIVE #defines in Environment.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 + GetLastError pairs (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 recvpthread_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_SENDSHUT_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 via uname.
  • $peb pseudo-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 with opendir/readdir + fnmatch (only caller today is eval.cpp's test harness).
  • common.cpp::GetConfigFilePath — Linux empties the path; resolve via readlink("/proc/self/exe") + append CONFIG_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 — same CpuIdExCpuCpuIdEx typo fix as common.cpp. Done.

Build system

  • Add .gitignore rules for the in-source CMake build output (CMakeCache.txt, CMakeFiles/, generated Makefile, cmake_install.cmake, *.o, *.so) — or switch to an out-of-source build/ 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:833 chunks at DbgPrintLimitation (512, Constants.h:223) = DbgPrint's limit, not printk's LOG_LINE_MAX. Works, just mis-sized.

  • Revisit loglevel + DbgPrintLimitation sizing 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