/// Debug a process. /// /// This is the core of the debugger API. It provides APIs to start a new /// process, attach itself onto a process, manage breakpoints, etc. /// /// This is the only module that contains function names without its module /// name. /// /// Authors: dd86k <dd@dax.moe> /// Copyright: © dd86k <dd@dax.moe> /// License: BSD-3-Clause-Clear module adbg.debugger.process; //TODO: Process Pause/Resume // Windows: NtSuspendProcess/NtResumeProcess or SuspendThread/ResumeThread // Linux: Send SIGSTOP/SIGCONT signals via kill(2) //TODO: List threads of process (maybe in a module called threading.d) //TODO: Has remote debugger attached? import adbg.include.c.stdlib; // malloc, calloc, free, exit; import adbg.include.c.stdio; // snprintf; import adbg.include.c.stdarg; import adbg.platform : ADBG_CHILD_STACK_SIZE; import adbg.error; import adbg.utils.strings; import adbg.debugger.exception : adbg_exception_t, adbg_exception_translate; import adbg.machines; import core.stdc.string; version (Windows) { import adbg.include.windows.wow64apiset; import adbg.include.windows.psapi_dyn; import adbg.include.windows.winnt; import core.sys.windows.basetsd; import core.sys.windows.winbase; } else version (Posix) { import adbg.include.posix.mann; import adbg.include.posix.ptrace; import adbg.include.posix.unistd; import adbg.include.posix.sys.wait; import adbg.utils.math; import core.stdc.ctype : isdigit; import core.stdc.errno; import core.sys.posix.fcntl; import core.sys.posix.dirent; import core.sys.posix.libgen : basename; } // I can't remember why, but problably Musl moment version = USE_CLONE; extern (C): /// Debugging events enum AdbgEvent { exception, } /// Process status enum AdbgProcStatus : ubyte { unknown, /// Process status is not known. unloaded = unknown, /// Process is unloaded. standby, /// Process is loaded and waiting to run. running, /// Process is running. paused, /// Process is paused due to an exception or by the debugger. } //TODO: Rename to AdbgDebuggerRelation /// Process creation source. enum AdbgCreation : ubyte { unattached, unloaded = unattached, // Older alias attached, spawned, } //TODO: Deprecate and remove static buffer in process struct enum ADBG_PROCESS_NAME_LENGTH = 256; /// Represents an instance of a process. struct adbg_process_t { version (Windows) { // Original identifiers; Otherwise informal int pid; /// Process identificiation number int tid; /// Thread identification number HANDLE hpid; /// Process handle HANDLE htid; /// Thread handle //TODO: Deprecate and remove wow64 field // Promote AdbgMachine enum version (Win64) int wow64; /// If running under WoW64 char *args; } version (Posix) { pid_t pid; /// Process ID // @suppress(dscanner.suspicious.label_var_same_name) char **argv; } version (linux) { int mhandle; /// Internal memory file handle to /proc/PID/mem int memfailed; /// Set if we fail to open /proc/PID/mem } /// Last known process status. AdbgProcStatus status; /// Process' creation source. AdbgCreation creation; //TODO: Deprecate and remove static buffer in process struct /// Process base module name. char[ADBG_PROCESS_NAME_LENGTH] name; } version (Posix) private struct __adbg_child_t { const(char) **argv, envp; } //TODO: Stream redirection options (FILE* and os handle options) //TODO: "start suspended" option // Windows: CREATE_SUSPENDED // Posix: //TODO: Stack size in KiB // Default should still be 8 MiB (Windows and Linux) /// Options for adbg_spawn. enum AdbgSpawnOpt { /// Pass args line to tracee. /// Type: const(char)* /// Default: null args = 1, /// Pass argv lines to tracee. Vector must terminate with NULL. /// Type: const(char)** /// Default: null argv = 2, /// Set start directory. String must terminate with NULL. /// Type: const(char)* /// Default: Current directory of debugger. directory = 3, /// Pass environment table to tracee. Vector must terminate with NULL. /// Type: const(char)** /// Default: null environment = 4, // Continue after spawning process. //continue_ = 5, // Tell debugger to use the shell instead of the OS interface. //useShell = 6, // Tell debugger to use clone(2) instead of fork(2). //useClone = 7, /// Debug child processes that the target process spawns. /// Type: int /// Default: 0 debugChildren = 10, } /// Load executable image into the debugger. /// /// By default, only debugs the target process. /// Loads an executable into the debugger, with optional null-terminated /// argument list and null-terminated environment. /// /// Windows: CreateProcessA with DEBUG_PROCESS. /// Posix: stat(2), fork(2) or clone(2), ptrace(2) with PT_TRACEME, and execve(2). /// Params: /// path = Command, path to executable. /// ... = Options, with zero ending them. /// Returns: Process instance; Or null on error. adbg_process_t* adbg_debugger_spawn(const(char) *path, ...) { if (path == null) { adbg_oops(AdbgError.invalidArgument); return null; } enum { OPT_DEBUG_ALL = 1, } va_list list = void; va_start(list, path); const(char) *args; const(char) **argv; const(char) *dir; const(char) **envp; int options; LOPT: switch (va_arg!int(list)) { case 0: break; // Temporary until reworked /*case AdbgSpawnOpt.args: args = va_arg!(const(char)*)(list); goto LOPT;*/ case AdbgSpawnOpt.argv: argv = va_arg!(const(char)**)(list); goto LOPT; // Temporary until implemented /*case AdbgSpawnOpt.directory: dir = va_arg!(const(char)*)(list); goto LOPT;*/ // Temporary until reworked /*case AdbgSpawnOpt.environment: envp = va_arg!(const(char)**)(list); goto LOPT;*/ case AdbgSpawnOpt.debugChildren: if (va_arg!(int)(list)) options |= OPT_DEBUG_ALL; goto LOPT; default: adbg_oops(AdbgError.invalidOption); return null; } adbg_process_t *proc = cast(adbg_process_t*)calloc(1, adbg_process_t.sizeof); if (proc == null) { adbg_oops(AdbgError.crt); return null; } version (Windows) { // NOTE: CreateProcessW modifies lpCommandLine // NOTE: lpCommandLine is maximum 32,767 bytes including null Unicode character // NOTE: When given arguments, both lpApplicationName and lpCommandLine // need to be filled. If the former is null, this acts as a shell, and // Windows will search for the external command, which is unwanted. // Add argv is specified, we'll have to cram it into args if (argv) { // Get minimum total buffer size required int argc; size_t commlen = strlen(path); size_t argslen; while (argv[argc]) argslen += strlen(argv[argc++]); size_t minlen = commlen + 2 + argslen + argc + 1; // + quotes and spaces proc.args = cast(char*)malloc(minlen); if (proc.args == null) { free(proc); adbg_oops(AdbgError.crt); return null; } size_t i; proc.args[i++] = '"'; memcpy(proc.args + i, path, commlen); i += commlen; proc.args[i++] = '"'; proc.args[i++] = ' '; // Flatten int cl = cast(int)(minlen - i); // Buffer space left if (cl <= 0) { free(proc); adbg_oops(AdbgError.crt); return null; } size_t o = adbg_strings_flatten(proc.args + i, cl, argc, argv, 1); if (o == 0) { free(proc); adbg_oops(AdbgError.assertion); return null; } version(Trace) trace("args='%s'", proc.args); } //TODO: Parse envp // Setup process info STARTUPINFOA si = void; PROCESS_INFORMATION pi = void; memset(&si, 0, si.sizeof); memset(&pi, 0, pi.sizeof); si.cb = STARTUPINFOA.sizeof; DWORD flags = options & OPT_DEBUG_ALL ? DEBUG_PROCESS : DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS; flags |= CREATE_DEFAULT_ERROR_MODE; if (CreateProcessA( path, // lpApplicationName proc.args, // lpCommandLine null, // lpProcessAttributes null, // lpThreadAttributes FALSE, // bInheritHandles flags, // dwCreationFlags envp, // lpEnvironment null, // lpCurrentDirectory &si, &pi) == FALSE) { free(proc); adbg_oops(AdbgError.os); return null; } proc.hpid = pi.hProcess; proc.htid = pi.hThread; proc.pid = pi.dwProcessId; proc.tid = pi.dwThreadId; // Microsoft recommends getting function pointer with // GetProcAddress("kernel32", "IsWow64Process"), but so far // all 64-bit versions of Windows have WOW64 (does Embedded too?). // Nevertheless, required to support 32-bit processes under // 64-bit builds. //TODO: IsWow64Process2 support // with GetProcAddress("kernel32", "IsWow64Process2") // Introduced in Windows 10, version 1511 // IsWow64Process: 32-bit proc. under aarch64 returns FALSE // NOTE: Could be moved to adbg_process_get_machine version (Win64) { if (IsWow64Process(proc.hpid, &proc.wow64) == FALSE) { free(proc); adbg_oops(AdbgError.os); return null; } } } else version (linux) { // NOTE: Don't remember this check, but I think it was because of // an ambiguous error message // Verify if file exists and we has access to it stat_t st = void; if (stat(path, &st) == -1) { free(proc); adbg_oops(AdbgError.os); return null; } // Allocate arguments, include space for program and null terminator int argc; while (argv[argc]) ++argc; version(Trace) trace("argc=%d", argc); proc.argv = cast(char**)malloc((argc + 2) * size_t.sizeof); if (proc.argv == null) { version(Trace) trace("mmap=%s", strerror(errno)); free(proc); adbg_oops(AdbgError.os); return null; } proc.argv[0] = cast(char*)path; if (argc && argv) memcpy(proc.argv + 1, argv, argc * size_t.sizeof); proc.argv[argc + 1] = null; version (USE_CLONE) { // clone(2) //TODO: get default stack size (glibc constant or function) void *stack = mmap(null, ADBG_CHILD_STACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); if (stack == MAP_FAILED) { free(proc.argv); free(proc); adbg_oops(AdbgError.os); return null; } // Assume stack grows downward void *stacktop = stack + ADBG_CHILD_STACK_SIZE; // Clone //TODO: Get default stack size __adbg_child_t chld = void; chld.argv = cast(const(char)**)proc.argv; chld.envp = envp; proc.pid = clone(&__adbg_exec_child, stacktop, CLONE_PTRACE | CLONE_VFORK, &chld); if (proc.pid < 0) { free(proc); adbg_oops(AdbgError.os); return null; } } else { // fork(2) /*proc.argv = cast(char**)mmap(null, (argc + 2) * size_t.sizeof, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0); if (proc.argv == MAP_FAILED) { version(Trace) trace("mmap=%s", strerror(errno)); free(proc); adbg_oops(AdbgError.os); return null; }*/ // Setup pipes so we can communicate with child process int[2] pipefds = void; if (pipe(pipefds)) { free(proc); adbg_oops(AdbgError.os); return null; } if (fcntl(pipefds[1], F_SETFD, fcntl(pipefds[1], F_GETFD) | FD_CLOEXEC)) { free(proc); adbg_oops(AdbgError.os); return null; } // NOTE: exec family // | | PATH | argv | envp | // POSIX | execl(3) | No | No | No | // | execlp(3) | Yes | No | No | // | execle(3) | No | No | Yes | // | execv(3) | No | Yes | No | // | execvp(3) | Yes | Yes | No | // | execvpe(3) | Yes | Yes | Yes | // Linux | execve(2) | No | Yes | Yes | // FreeBSD | execvP(2) | Yes | Yes | No | // | exect(3) | No | Yes | Yes | // OpenBSD | execve(2) | No | Yes | Yes | // NetBSD | exect(3) | No | Yes | Yes | // NOTE: Execution flow // 1. Child is created with anonymous pipes // 2. While parent waits for a signal, child inits ptrace switch (proc.pid = fork()) { case -1: // Error version(Trace) trace("fork=%s", strerror(errno)); //munmap(proc.argv, free(proc); adbg_oops(AdbgError.os); return null; case 0: // New child process close(pipefds[0]); // Close read version(Trace) for (int i; i < argc + 2; ++i) trace("argv[%d]=%s", i, proc.argv[i]); __adbg_child_t chld = void; chld.argv = cast(const(char)**)proc.argv; chld.envp = envp; int e = __adbg_exec_child(&chld) < 0 ? errno : 0; write(pipefds[1], &e, int.sizeof); // Write result to parent close(pipefds[1]); // Close write _exit(e); return null; // Make compiler happy default: // This parent process close(pipefds[1]); // Close write int chlderr = void; ssize_t count = void; version(Trace) trace("read...", chlderr); while ((count = read(pipefds[0], &chlderr, int.sizeof)) < 0) if (errno != EAGAIN && errno != EINTR) break; //ssize_t count = read(pipefds[0], &chlderr, int.sizeof); version(Trace) trace("close...", chlderr); close(pipefds[0]); // Close read if (count < int.sizeof) { // Error version(Trace) trace("read=%s", strerror(errno)); adbg_oops(AdbgError.os); return null; } if (chlderr) { version(Trace) trace("childerr=%d (%s)", chlderr, strerror(chlderr)); errno = chlderr; adbg_oops(AdbgError.os); return null; } // Waiting for child /*while (waitpid(proc.pid, &chlderr, 0) == -1) if (errno != EINTR) { version(Trace) trace("waitpid=%s", strerror(errno)); adbg_oops(AdbgError.os); return null; } if (WIFEXITED(chlderr)) { version(Trace) trace("child exited by %d", WEXITSTATUS(chlderr)); adbg_oops(AdbgError.os); return null; } else if (WIFSIGNALED(chlderr)) { version(Trace) trace("child kill by %d", WTERMSIG(chlderr)); adbg_oops(AdbgError.os); return null; }*/ } // switch(fork(2)) } // fork(2) } // version (linux) proc.status = AdbgProcStatus.standby; proc.creation = AdbgCreation.spawned; return proc; } version (Posix) private int __adbg_exec_child(void* arg) { __adbg_child_t *chld = cast(__adbg_child_t*)arg; // Baby, Please Trace Me version (Trace) trace("ptrace..."); if (ptrace(PT_TRACEME, 0, 0, 0) < 0) { version (Trace) trace("ptrace=%s", strerror(errno)); return -1; } version (Trace) trace("done"); version (CRuntime_Musl) { version (Trace) trace("raise..."); if (raise(SIGTRAP)) { version (Trace) trace("raise=%s", strerror(errno)); return -1; } version (Trace) trace("done"); } // NOTE: On Glibc, sends a SIGTRAP on success and doesn't return version (Trace) trace("execve..."); if (execve(*chld.argv, chld.argv, chld.envp) < 0) { version (Trace) trace("execve=%s", strerror(errno)); return -1; } version (Trace) trace("done"); Lexit: _exit(errno); } /// Debugger process attachment options enum AdbgAttachOpt { /// When set, stop execution when attached. /// Note: Currently not supported on Windows. Will always stop. /// Type: int /// Default: 0 stop = 1, /// When set, kill tracee when debugger exits. /// Type: int /// Default: 0 exitkill = 2, // Filter exception or stop only on these exceptions //filter = 3, } /// Attach the debugger to a process ID. /// /// Params: /// pid = Process ID. /// ... = Options. Pass 0 for none or to end list. /// Returns: Error code. adbg_process_t* adbg_debugger_attach(int pid, ...) { if (pid <= 0) { adbg_oops(AdbgError.invalidArgument); return null; } enum { OPT_STOP = 1, OPT_EXITKILL = 2, } va_list list = void; va_start(list, pid); int options; L_OPTION: switch (va_arg!int(list)) { case 0: break; case AdbgAttachOpt.stop: if (va_arg!int(list)) options |= OPT_STOP; goto L_OPTION; case AdbgAttachOpt.exitkill: if (va_arg!int(list)) options |= OPT_EXITKILL; goto L_OPTION; default: adbg_oops(AdbgError.invalidOption); return null; } version (Trace) trace("pid=%d options=%#x", pid, options); adbg_process_t *proc = cast(adbg_process_t*)calloc(1, adbg_process_t.sizeof); if (proc == null) { adbg_oops(AdbgError.crt); return null; } proc.creation = AdbgCreation.attached; version (Windows) { //TODO: Integrate ObRegisterCallbacks? // https://blog.xpnsec.com/anti-debug-openprocess/ // NOTE: Emulate ProcessIdToHandle // Uses NtOpenProcess with ClientId.UniqueProcess=PID // Uses PROCESS_ALL_ACCESS, but let's start with the basics proc.pid = cast(DWORD)pid; proc.hpid = OpenProcess( PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_SUSPEND_RESUME | PROCESS_QUERY_INFORMATION, FALSE, cast(DWORD)pid); if (proc.hpid == null) { free(proc); adbg_oops(AdbgError.os); return null; } // Check if process already has an attached debugger BOOL dbgpresent = void; if (CheckRemoteDebuggerPresent(proc.hpid, &dbgpresent) == FALSE) { free(proc); adbg_oops(AdbgError.os); return null; } if (dbgpresent) { free(proc); adbg_oops(AdbgError.debuggerPresent); return null; } // Breaks into remote process and initiates break-in //TODO: try NtContinue for continue option if (DebugActiveProcess(proc.pid) == FALSE) { free(proc); adbg_oops(AdbgError.os); return null; } // DebugActiveProcess, by default, kills the process on exit. // Set exitkill unconditionalled // Default: on if (DebugSetProcessKillOnExit(options & OPT_EXITKILL) == FALSE) { free(proc); adbg_oops(AdbgError.os); return null; } } else version (Posix) { version (Trace) if (options & OPT_STOP) trace("Sending break..."); if (ptrace(options & OPT_STOP ? PT_ATTACH : PT_SEIZE, pid, null, null) < 0) { free(proc); adbg_oops(AdbgError.os); return null; } // Set exitkill on if specified // Default: off if (options & OPT_EXITKILL && ptrace(PT_SETOPTIONS, pid, null, PT_O_EXITKILL) < 0) { free(proc); adbg_oops(AdbgError.os); return null; } proc.pid = cast(pid_t)pid; } // version (Posix) proc.creation = AdbgCreation.attached; proc.status = options & OPT_STOP ? AdbgProcStatus.paused : AdbgProcStatus.running; return proc; } /// Detach debugger from current process. int adbg_debugger_detach(adbg_process_t *tracee) { if (tracee == null) return adbg_oops(AdbgError.invalidArgument); if (tracee.creation != AdbgCreation.attached) return adbg_oops(AdbgError.debuggerInvalidAction); tracee.creation = AdbgCreation.unloaded; tracee.status = AdbgProcStatus.unloaded; scope(exit) free(tracee); version (Windows) { if (DebugActiveProcessStop(tracee.pid) == FALSE) return adbg_oops(AdbgError.os); } else version (Posix) { if (ptrace(PT_DETACH, tracee.pid, null, null) < 0) return adbg_oops(AdbgError.os); } return 0; } //TODO: Check process debugged remotely //bool adbg_process_debugged(int pid) { /// Get the debugger's current state. /// Returns: Debugger status. AdbgProcStatus adbg_process_status(adbg_process_t *tracee) pure { if (tracee == null) return AdbgProcStatus.unknown; return tracee.status; } /// Wait for a debug event. /// /// Continues execution of the process until a new debug event occurs. When an /// exception occurs, the exception_t structure is populated with debugging information. /// /// This call is blocking. /// /// Windows: Uses WaitForDebugEvent, filters anything but EXCEPTION_DEBUG_EVENT. /// Posix: Uses ptrace(2) and waitpid(2), filters SIGCONT out. /// /// Params: /// tracee = Tracee instance. /// userfunc = User function callback on event. /// Returns: Error code. int adbg_debugger_wait(adbg_process_t *tracee, void function(adbg_process_t*, int, void*) userfunc) { if (tracee == null || userfunc == null) return adbg_oops(AdbgError.invalidArgument); if (tracee.creation == AdbgCreation.unloaded) return adbg_oops(AdbgError.debuggerUnattached); //TODO: Urgent: The process instance should ideally not be modified // Changing process information (e.g., PID) can in turn // be bad news for other API functions (e.g., breakpoints). // // Children processes should be allocated and attached to exception // (via adbg_exception_get_process?). adbg_exception_t exception = void; version (Windows) { DEBUG_EVENT de = void; L_DEBUG_LOOP: // Something bad happened if (WaitForDebugEvent(&de, INFINITE) == FALSE) { tracee.status = AdbgProcStatus.unloaded; tracee.creation = AdbgCreation.unloaded; return adbg_oops(AdbgError.os); } // Filter events switch (de.dwDebugEventCode) { case EXCEPTION_DEBUG_EVENT: break; /*case CREATE_THREAD_DEBUG_EVENT: case CREATE_PROCESS_DEBUG_EVENT: case EXIT_THREAD_DEBUG_EVENT: //case EXIT_PROCESS_DEBUG_EVENT: case LOAD_DLL_DEBUG_EVENT: case UNLOAD_DLL_DEBUG_EVENT: case OUTPUT_DEBUG_STRING_EVENT: case RIP_EVENT: goto default;*/ case EXIT_PROCESS_DEBUG_EVENT: goto L_UNLOADED; default: ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE); goto L_DEBUG_LOOP; } // Fixes access to debugger, thread context functions. // Especially when attaching, but should be standard with spawned-in processes too. tracee.tid = de.dwThreadId; tracee.htid = OpenThread( THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME, FALSE, de.dwThreadId); tracee.status = AdbgProcStatus.paused; adbg_exception_translate(&exception, &de, null); } else version (Posix) { int wstatus = void; int stopsig = void; L_DEBUG_LOOP: tracee.pid = waitpid(-1, &wstatus, 0); // Something bad happened if (tracee.pid < 0) { tracee.status = AdbgProcStatus.unloaded; tracee.creation = AdbgCreation.unloaded; return adbg_oops(AdbgError.crt); } version (Trace) trace("wstatus=%08x", wstatus); // If exited or killed by signal, it's gone. if (WIFEXITED(wstatus) || WIFSIGNALED(wstatus)) goto L_UNLOADED; // Skip glibc "continue" signals. version (CRuntime_Glibc) if (WIFCONTINUED(wstatus)) goto L_DEBUG_LOOP; //TODO: Check waitpid status for BSDs // Bits Description (Linux) // 6:0 Signo that caused child to exit // 0x7f if child stopped/continued // or zero if child exited without signal // 7 Core dumped // 15:8 exit value (or returned main value) // or signal that cause child to stop/continue stopsig = WEXITSTATUS(wstatus); // Get fault address switch (stopsig) { case SIGCONT: goto L_DEBUG_LOOP; // NOTE: si_addr is NOT populated under ptrace for SIGTRAP // - linux does not fill si_addr on a SIGTRAP from a ptrace event // - see sigaction(2) // - linux *only* fills user_regs_struct for "user area" // - see arch/x86/include/asm/user_64.h // - "ptrace does not yet supply these. Someday...." // - So yeah, debug registers and "fault_address" not filled // - No access to ucontext_t from ptrace either // - using EIP/RIP is NOT a good idea // - IP ALWAYS point to NEXT instruction // - First SIGTRAP does NOT contain int3 // - Windows does, though, and points to it // - gdbserver and lldb never attempt to do such thing anyway // - RIP-1 (x86) could *maybe* point to int3 or similar. // NOTE: Newer D compilers fixed siginfo_t as a whole // for version (linux). Noticed on DMD 2.103.1. // Old glibc: ._sifields._sigfault.si_addr // Old musl: .__si_fields.__sigfault.si_addr // New: ._sifields._sigfault.si_addr & .si_addr() // NOTE: .si_addr() emits linker errors on Musl platforms. case SIGILL, SIGSEGV, SIGFPE, SIGBUS: siginfo_t sig = void; if (ptrace(PT_GETSIGINFO, tracee.pid, null, &sig) < 0) { exception.fault_address = 0; break; } exception.fault_address = cast(size_t)sig._sifields._sigfault.si_addr; break; // case SIGINT, SIGTERM, SIGABRT: //TODO: Killed? default: exception.fault_address = 0; } tracee.status = AdbgProcStatus.paused; adbg_exception_translate(&exception, &tracee.pid, &stopsig); } userfunc(tracee, AdbgEvent.exception, &exception); return 0; L_UNLOADED: tracee.status = AdbgProcStatus.unloaded; tracee.creation = AdbgCreation.unloaded; return 0; } /// Disconnect and terminate the debuggee process. /// Params: tracee = Process. /// Returns: Error code. int adbg_debugger_terminate(adbg_process_t *tracee) { if (tracee == null) return adbg_oops(AdbgError.invalidArgument); if (tracee.creation == AdbgCreation.unloaded) return adbg_oops(AdbgError.debuggerUnattached); tracee.status = AdbgProcStatus.unloaded; // exited in any case tracee.creation = AdbgCreation.unloaded; scope(exit) { version (Windows) if (tracee.args) free(tracee.args); //version (Posix) if (tracee.argv) close(tracee.argv); free(tracee); } version (Windows) { if (ContinueDebugEvent(tracee.pid, tracee.tid, DBG_TERMINATE_PROCESS) == FALSE) return adbg_oops(AdbgError.os); } else { if (kill(tracee.pid, SIGKILL) < 0) // PT_KILL is deprecated return adbg_oops(AdbgError.os); } return 0; } /// Make the debuggee process continue from its currently stopped state. /// Params: tracee = Process. /// Returns: Error code. int adbg_debugger_continue(adbg_process_t *tracee) { if (tracee == null) return adbg_oops(AdbgError.invalidArgument); if (tracee.creation == AdbgCreation.unloaded) return adbg_oops(AdbgError.debuggerUnattached); if (tracee.status != AdbgProcStatus.paused) return 0; tracee.status = AdbgProcStatus.running; version (Windows) { if (ContinueDebugEvent(tracee.pid, tracee.tid, DBG_CONTINUE) == FALSE) { tracee.status = AdbgProcStatus.unknown; return adbg_oops(AdbgError.os); } } else { if (ptrace(PT_CONT, tracee.pid, null, null) < 0) { tracee.status = AdbgProcStatus.unknown; return adbg_oops(AdbgError.os); } } return 0; } /// Performs an instruction step for the debuggee process. /// Params: tracee = Process being debugged. /// Returns: Error code. int adbg_debugger_stepi(adbg_process_t *tracee) { if (tracee == null) return adbg_oops(AdbgError.invalidArgument); if (tracee.creation == AdbgCreation.unloaded) return adbg_oops(AdbgError.debuggerUnattached); version (Windows) { enum EFLAGS_TF = 0x100; // Enable single-stepping via Trap flag version (Win64) if (tracee.wow64) { WOW64_CONTEXT winctxwow64 = void; winctxwow64.ContextFlags = CONTEXT_CONTROL; Wow64GetThreadContext(tracee.htid, &winctxwow64); winctxwow64.EFlags |= EFLAGS_TF; Wow64SetThreadContext(tracee.htid, &winctxwow64); FlushInstructionCache(tracee.hpid, null, 0); return adbg_debugger_continue(tracee); } // X86, AMD64 CONTEXT winctx = void; winctx.ContextFlags = CONTEXT_ALL; GetThreadContext(tracee.htid, cast(LPCONTEXT)&winctx); winctx.EFlags |= EFLAGS_TF; SetThreadContext(tracee.htid, cast(LPCONTEXT)&winctx); FlushInstructionCache(tracee.hpid, null, 0); return adbg_debugger_continue(tracee); } else { if (ptrace(PT_SINGLESTEP, tracee.pid, null, null) < 0) { tracee.status = AdbgProcStatus.unknown; return adbg_oops(AdbgError.os); } return 0; } } /// Get the process' ID; /// Params: tracee = Debuggee process. /// Returns: PID or 0 on error. int adbg_process_get_pid(adbg_process_t *tracee) { if (tracee == null) return 0; return tracee.pid; } //TODO: Last parameter could be an enum // AdbgProcNameInclude // - program basename (only) // - program full path // - program full path and command-line arguments /// Get the process file path. /// /// The string is null-terminated. /// Bug: On Windows, GetModuleFileNameA causes a crash with MSVC malloc buffers. /// Params: /// pid = Process ID. /// buffer = Buffer. /// bufsize = Size of the buffer. /// absolute = Request for absolute path; Otherwise base filename. /// Returns: String length; Or zero on error. size_t adbg_process_get_name(int pid, char *buffer, size_t bufsize, bool absolute) { version (Trace) trace("pid=%d buffer=%p bufsize=%zd base=%d", pid, buffer, bufsize, absolute); if (pid <= 0 || buffer == null || bufsize == 0) { adbg_oops(AdbgError.invalidArgument); return 0; } version (Windows) { if (__dynlib_psapi_load()) // Sets error return 0; // Get process handle HANDLE procHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid); if (procHandle == null) { adbg_oops(AdbgError.os); return 0; } scope(exit) CloseHandle(procHandle); //TODO: Try with the following? // 1. // GetProcessImageFileNameA + GetModuleHandleA // + GetModuleBaseNameA <- base=true // + GetModuleFileNameA <- base=false // 2. // GetProcessImageFileNameA // + cut string manually <- base=true // + PathGetDriveNumberA <- base=false DWORD needed = void; DWORD pidlist = void; if (EnumProcesses(&pidlist, DWORD.sizeof, &needed) == FALSE) { adbg_oops(AdbgError.os); return 0; } //TODO: Check every PID to match? HMODULE hmod = void; if (absolute == false && EnumProcessModules(procHandle, &hmod, hmod.sizeof, &needed)) { adbg_oops(AdbgError.os); return 0; } // NOTE: GetModuleFileNameA requires module handle // NOTE: GetProcessImageFileNameA returns native path (not Win32 path) // NOTE: QueryFullProcessImageNameA is Vista and later // Get filename or basename uint bf = cast(uint)bufsize; uint r = absolute ? GetModuleFileNameA(hmod, buffer, bf) : GetModuleBaseNameA(procHandle, hmod, buffer, bf); buffer[r] = 0; if (r == 0) adbg_oops(AdbgError.os); return r; } else version (linux) { enum PATHBFSZ = 32; // int.min is "-2147483648", 11 chars char[PATHBFSZ] pathbuf = void; // Path buffer // NOTE: readlink does not append null, this is done later snprintf(pathbuf.ptr, PATHBFSZ, "/proc/%d/exe", pid); ssize_t r = readlink(pathbuf.ptr, buffer, bufsize); // NOTE: cmdline arguments end with one null byte, and an extra null byte at the very end /*snprintf(pathbuf.ptr, PATHBFSZ, "/proc/%d/cmdline", pid); int cmdlinefd = open(pathbuf.ptr, O_RDONLY); if (cmdlinefd > 0) { r = read(cmdlinefd, buffer, bufsize); if (r <= 0) { adbg_oops(AdbgError.os); return 0; } buffer[r] = 0; close(cmdlinefd); }*/ // Error reading /cmdline, retry with /comm // e.g., kthread // NOTE: comm strings can only be up to 16 characters if (r <= 0) { snprintf(pathbuf.ptr, PATHBFSZ, "/proc/%d/comm", pid); int commfd = open(pathbuf.ptr, O_RDONLY); if (commfd == -1) { adbg_oops(AdbgError.os); return 0; } scope(exit) close(commfd); // Read into buffer size_t rdsize = min(bufsize, 16); // Can only read up to 16 chars r = read(commfd, buffer, rdsize); if (r < 0) { adbg_oops(AdbgError.os); return 0; } buffer[r - 1] = 0; // Delete newline // Return now since comm values aren't worth path manipulation return r; } // Base path requested and got absolute instead // e.g. /usr/bin/cat to cat if (absolute == false && buffer[0] == '/') { // Find the last occurance of '/' char *last = strrchr(buffer, '/'); if (last == null) { adbg_oops(AdbgError.assertion); return 0; } ++last; // We're looking past '/' // Write into buffer for (r = 0; last[r]; ++r) buffer[r] = last[r]; } //TODO: Name is not absolute, search in PATH /* else if (absolute && buffer[0] != '/') { }*/ buffer[r < bufsize ? r : r - 1] = 0; return r; } else { adbg_oops(AdbgError.unimplemented); return 0; } } unittest { //TODO: Test one character buffers } /// Get the current runtime machine platform. /// /// This is useful when the debugger is dealing with a process running /// under a subsystem such as WoW or lib32-on-linux64 programs. /// Params: tracee = Debuggee process. /// Returns: Machine platform. AdbgMachine adbg_process_get_machine(adbg_process_t *tracee) { if (tracee == null) return AdbgMachine.unknown; //TODO: There's probably a way to remotely check this // Windows: IsWow64Process/IsWow64Process2 with process handle version (Win64) { if (tracee.wow64) return AdbgMachine.x86; } return adbg_machine_default(); } /// Get a list of process IDs running. /// /// This function allocates memory. The list passed will need to be closed /// using `free(3)`. To get the name of a process, call `adbg_process_get_name`. /// /// Windows: The list is populated by system order using `EnumProcesses`. /// Linux: The list is populated by process ID using procfs. /// /// Params: /// count = Process list structure instance. /// ... = Options, terminated by 0. /// Returns: List of PIDs; Or null on error. int* adbg_process_list(size_t *count, ...) { if (count == null) { adbg_oops(AdbgError.invalidArgument); return null; } enum CAPACITY = 5_000; // * 4 = ~20K int *plist = void; version (Windows) { if (__dynlib_psapi_load()) return null; // Allocate temp PID buffer uint hsize = cast(uint)(CAPACITY * HMODULE.sizeof); DWORD *pidlist = cast(DWORD*)malloc(hsize); if (pidlist == null) { adbg_oops(AdbgError.crt); return null; } scope(exit) free(pidlist); // Enumerate processes // Note that "needed" is reusable after getting the count //TODO: Adjust temporary buffer after calling this DWORD needed = void; if (EnumProcesses(pidlist, hsize, &needed) == FALSE) { adbg_oops(AdbgError.os); return null; } DWORD proccount = needed / DWORD.sizeof; plist = cast(int*)malloc(proccount * int.sizeof); if (plist == null) { adbg_oops(AdbgError.crt); return null; } // Skip PID 0 (idle) and 4 (system) enum SKIP = 2; memcpy(plist, pidlist + SKIP, (proccount * DWORD.sizeof) - (SKIP * DWORD.sizeof)); *count = proccount - SKIP; } else version (linux) { // Count amount of entries to allocate size_t cnt; // minimum amount of entries DIR *procfd = opendir("/proc"); if (procfd == null) { adbg_oops(AdbgError.crt); return null; } scope (exit) closedir(procfd); for (dirent *procent = void; (procent = readdir(procfd)) != null;) { // If not directory starting with a digit, skip entry if (procent.d_type != DT_DIR) continue; if (isdigit(procent.d_name[0]) == 0) continue; ++cnt; } // Allocate list plist = cast(int*)malloc(cnt * int.sizeof); if (plist == null) { adbg_oops(AdbgError.crt); return null; } *count = cnt; // Populate list rewinddir(procfd); size_t i; for (dirent *procent = void; (procent = readdir(procfd)) != null;) { // If not directory starting with a digit, skip entry if (procent.d_type != DT_DIR) continue; if (isdigit(procent.d_name[0]) == 0) continue; // Set PID plist[i++] = atoi(procent.d_name.ptr); } } return plist; } //TODO: Deprecate process enumeration routines /// Options for adbg_process_enumerate. enum AdbgProcessEnumerateOption { /// Set the size of the dynamic buffer for the list of processes. /// Default: 1000 /// Type: uint capcity = 1, /// This option is not yet implemented. sort = 2, } /// Sort option for AdbgProcessEnumerateOption.sort. enum AdbgProcessEnumerateSort { /// Sort processes by system (Windows' default). system, /// Sort processes by ID (Linux's default). id, /// Sort processes by basename. process, } /// Structure used with `adbg_process_enumerate`. /// /// This holds the list of processes and a count. struct adbg_process_list_t { /// Allocated list of processes. adbg_process_t *processes; /// Number of processes. size_t count; } /// Enumerate running processes. /// /// This function allocates memory. The list passed will need to be closed /// using `adbg_process_enumerate_close`. /// /// On Windows, the list is populated by system order using `EnumProcesses`. /// On Linux, the list is populated by process ID using procfs. /// /// Params: /// list = Process list structure instance. /// ... = Options, terminated by 0. /// Returns: Zero for success; Or error code. int adbg_process_enumerate(adbg_process_list_t *list, ...) { // NOTE: KEEP THIS FUNCTION AROUND AND DO NOT TOUCH IT // // This function *must* be kept around until I understand why both // GetModuleBaseNameA and GetModuleFileNameA work here but not // when used separaterely from EnumProcesses/EnumProcessModules. // // Also, on Linux, this somehow gets the comm value, while // the new function does not. if (list == null) return adbg_oops(AdbgError.invalidArgument); /// Default fixed buffer size. enum DEFAULT_CAPACITY = 1000; va_list options = void; va_start(options, list); uint capacity = DEFAULT_CAPACITY; L_OPTION: switch (va_arg!int(options)) { case 0: break; case AdbgProcessEnumerateOption.capcity: capacity = va_arg!uint(options); if (capacity <= 0) return adbg_oops(AdbgError.invalidValue); goto L_OPTION; default: return adbg_oops(AdbgError.invalidOption); } // Allocate main buffer list.processes = cast(adbg_process_t*)malloc(capacity * adbg_process_t.sizeof); if (list.processes == null) return adbg_oops(AdbgError.crt); version (Windows) { if (__dynlib_psapi_load()) { free(list.processes); return adbg_errno(); } // Allocate temp PID buffer uint hsize = cast(uint)(capacity * HMODULE.sizeof); DWORD *pidlist = cast(DWORD*)malloc(hsize); if (pidlist == null) { free(list.processes); return adbg_oops(AdbgError.crt); } scope(exit) free(pidlist); // Enumerate processes // Note that "needed" is reusable after getting the count DWORD needed = void; if (EnumProcesses(pidlist, hsize, &needed) == FALSE) { free(pidlist); return adbg_oops(AdbgError.os); } DWORD proccount = needed / DWORD.sizeof; size_t count; /// Final count for (DWORD i; i < proccount && count < capacity; ++i) { int pid = pidlist[i]; HANDLE procHandle = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); if (procHandle == null) continue; adbg_process_t *proc = &list.processes[count++]; proc.pid = pid; proc.hpid = procHandle; proc.tid = 0; //TODO: Is EnumProcessModules really necessary? HMODULE hmod = void; if (EnumProcessModules(procHandle, &hmod, hmod.sizeof, &needed)) { proc.hpid = hmod; DWORD len = GetModuleBaseNameA( procHandle, hmod, proc.name.ptr, ADBG_PROCESS_NAME_LENGTH); if (len > 0) { proc.name[len] = 0; } else { goto L_NONAME; } } else { L_NONAME: strcpy(proc.name.ptr, "<unknown>"); proc.hpid = null; } CloseHandle(procHandle); } list.count = count; return 0; } else version (linux) { //TODO: Consider pre-running /proc to get initial count size_t count; DIR *procfd = opendir("/proc"); for (dirent *procent = void; (procent = readdir(procfd)) != null;) { // If not directory starting with a digit, skip entry if (procent.d_type != DT_DIR) continue; if (isdigit(procent.d_name[0]) == 0) continue; /// Minimum read size, avoid overwriting enum READSZ = MIN!(adbg_process_t.name.sizeof, dirent.d_name.sizeof); // Set PID adbg_process_t *proc = &list.processes[count++]; proc.pid = atoi(procent.d_name.ptr); // Read /cmdline into process.name buffer enum TBUFSZ = 32; char[TBUFSZ] proc_comm = void; // Path buffer snprintf(proc_comm.ptr, TBUFSZ, "/proc/%s/cmdline", procent.d_name.ptr); int cmdlinefd = open(proc_comm.ptr, O_RDONLY); if (cmdlinefd == -1) continue; scope(exit) close(cmdlinefd); ssize_t r = read(cmdlinefd, proc.name.ptr, READSZ); // Get a baseline from /cmdline or /comm if (procent.d_name[0] && r > 0) { // /cmdline populated // NOTE: Yes, dangerous, but works under Glibc and musl. //TODO: Test under Bionic, uClibc, etc. strcpy(proc.name.ptr, basename(proc.name.ptr)); } else { // /cmdline empty, retrying with /comm snprintf(proc_comm.ptr, TBUFSZ, "/proc/%s/comm", procent.d_name.ptr); int commfd = open(proc_comm.ptr, O_RDONLY); if (commfd == -1) continue; scope(exit) close(commfd); r = read(commfd, proc.name.ptr, READSZ); if (r < 0) continue; proc.name[r - 1] = 0; // Delete newline } } list.count = count; return 0; } else { return adbg_oops(AdbgError.unimplemented); } } unittest { adbg_process_list_t list = void; assert(adbg_process_enumerate(&list, 0) == 0); version (TestVerbose) { import core.stdc.stdio : printf; foreach (adbg_process_t proc; list.processes[0..list.count]) { printf("%5u %s\n", adbg_process_get_pid(&proc), proc.name.ptr); } } assert(list.count); adbg_process_enumerate_close(&list); } void adbg_process_enumerate_close(adbg_process_list_t *list) { if (list == null) return; if (list.processes) free(list.processes); }