/** * Debugger core * * 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 */ module adbg.v1.debugger.debugger; import adbg.include.c.stdlib : malloc, free; import adbg.include.c.stdio; import core.stdc.config : c_long; import core.stdc.string : memset; import adbg.platform, adbg.error; import adbg.utils.strings : adbg_util_argv_flatten; public import adbg.v1.debugger.exception; version (Windows) { import core.sys.windows.windows; import adbg.include.windows.wow64; import adbg.include.windows.psapi_dyn; } else version (Posix) { import core.sys.posix.sys.stat; import core.sys.posix.sys.wait : waitpid, SIGCONT, WUNTRACED; import core.sys.posix.sys.uio; import core.sys.posix.fcntl : open; import core.stdc.stdlib : exit, malloc, free; import adbg.include.posix.mann; import adbg.include.posix.ptrace; import adbg.include.posix.unistd; import adbg.include.posix.signal; import adbg.include.linux.user; private enum __WALL = 0x40000000; } version (linux) version = USE_CLONE; extern (C): /// Actions that a user function handler may return public enum AdbgAction { exit, /// Close the process and stop debugging // close, /// Close process or detach // stop, /// Stop debugging // pause, /// Pause debugging proceed, /// Continue debugging step, /// Proceed with a single step } /// Debugger status public enum AdbgStatus { idle, /// Waiting for input ready, /// Program loaded, waiting to run running, /// Executing debuggee paused, /// Exception occured } /// Debugger event /+public enum AdbgEvent { exception, processCreated, processExit, threadCreated, threadExit, } /// Debugger event structure public struct adbg_debugger_event_t { AdbgEvent event; public union { exception_t exception; } }+/ package struct debuggee_t { AdbgStatus status; size_t bpindex; /// breakpoint index /// Set when debuggee was attached to rather than created. /// This is used in the debugger loop. bool attached; version (Windows) { HANDLE hpid; /// Process handle HANDLE htid; /// Thread handle int pid; /// Process identificiation number int tid; /// Thread identification number char[MAX_PATH] execpath; /// version (Win64) int wow64; /// If running under WoW64 } version (Posix) { pid_t pid; /// Process ID // @suppress(dscanner.suspicious.label_var_same_name) int mhandle; /// Memory file handle } } version(USE_CLONE) private struct __adbg_child_t { const(char) *dev; const(char) **argv, envp; } package __gshared debuggee_t g_debuggee; /// Debuggee information private __gshared int g_options; /// Debugger options //TODO: adbg_create seems more of an appropriate name... //TODO: Consider adbg_create(const(char) *path, ...) // ADBG_CREATE_OPT_ARGS - const(char) *args // ADBG_CREATE_OPT_ARGV - const(char) **argv // ADBG_CREATE_OPT_ENVP - const(char) **argv // ADBG_CREATE_OPT_DIR - const(char) *args // ADBG_CREATE_OPT_DONTSTOP // Make debuggee run as soon as possible // ADBG_CREATE_OPT_CLONE // (Linux) Use clone(2) instead of forking // Shouldn't this stay a compile flag? // ADBG_CREATE_OPT_PROCESS_ONLY // Debug this process only /** * Load executable image into the debugger. * * Loads an executable into the debugger, with optional null-terminated * argument list and null-terminated environment. * This does not start the process, nor the debugger. * On Posix systems, stat(2) is used to check if the file exists. * Windows: CreateProcessA (DEBUG_PROCESS). * Posix: stat(2), fork(2) or clone(2), ptrace(2) (PT_TRACEME), and execve(2). * Params: * path = Command, path to executable * argv = Argument vector, null-terminated, can be null * dir = New directory for the debuggee, null for current directory * envp = Environment vector, null-terminated, can be null * flags = Reserved * Returns: Zero on success; Otherwise os error code is returned */ int adbg_load(const(char) *path, const(char) **argv = null, const(char) *dir = null, const(char) **envp = null, int flags = 0) { if (path == null) return adbg_oops(AdbgError.invalidArgument); version (Windows) { int bs = 0x4000; // buffer size, 16 KiB ptrdiff_t bi; char *b = cast(char*)malloc(bs); /// flat buffer // Copy execultable path into buffer bi = snprintf(b, bs, "%s ", path); if (bi < 0) return 1; // Flatten argv if (argv) bi += adbg_util_argv_flatten(b + bi, bs, argv); //TODO: Parse envp // Create process STARTUPINFOA si = void; PROCESS_INFORMATION pi = void; memset(&si, 0, si.sizeof); // memset faster than _init functions memset(&pi, 0, pi.sizeof); // memset faster than _init functions si.cb = STARTUPINFOA.sizeof; // Not using DEBUG_ONLY_THIS_PROCESS because our posix // counterpart is using -1 (all children) for waitpid. if (CreateProcessA( null, // lpApplicationName b, // lpCommandLine null, // lpProcessAttributes null, // lpThreadAttributes FALSE, // bInheritHandles DEBUG_PROCESS, // dwCreationFlags envp, // lpEnvironment null, // lpCurrentDirectory &si, &pi) == FALSE) return adbg_oops(AdbgError.os); free(b); g_debuggee.hpid = pi.hProcess; g_debuggee.htid = pi.hThread; g_debuggee.pid = pi.dwProcessId; g_debuggee.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 version (Win64) if (IsWow64Process(g_debuggee.hpid, &g_debuggee.wow64) == FALSE) return adbg_oops(AdbgError.os); } else version (Posix) { // Verify if file exists and we has access to it stat_t st = void; if (stat(path, &st) == -1) return adbg_oops(AdbgError.os); // Proceed normally, execve performs executable checks version (USE_CLONE) { // clone(2) void *chld_stack = mmap(null, ADBG_CHILD_STACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0); if (chld_stack == MAP_FAILED) return adbg_oops(AdbgError.os); const(char)*[16] __argv = void; const(char)*[1] __envp = void; // Adjust argv if (argv) { size_t i, __i = 1; while (argv[i] && __i < 15) __argv[__i++] = argv[i++]; __argv[__i] = null; } else { __argv[1] = null; } __argv[0] = path; // Adjust envp if (envp == null) { envp = cast(const(char)**)&__envp; envp[0] = null; } // Clone __adbg_child_t chld = void; chld.envp = cast(const(char)**)&__envp; chld.argv = cast(const(char)**)&__argv; g_debuggee.pid = clone(&__adbg_chld, chld_stack + ADBG_CHILD_STACK_SIZE, CLONE_PTRACE, &chld); // tid if (g_debuggee.pid < 0) return adbg_oops(AdbgError.os); } else { // fork(2) g_debuggee.pid = fork(); if (g_debuggee.pid < 0) return adbg_oops(AdbgError.os); if (g_debuggee.pid == 0) { // Child process const(char)*[16] __argv = void; const(char)*[1] __envp = void; // Adjust argv if (argv) { size_t i, __i = 1; while (argv[i] && __i < 15) __argv[__i++] = argv[i++]; __argv[__i] = null; } else { __argv[1] = null; } __argv[0] = path; // Adjust envp if (envp == null) { envp = cast(const(char)**)&__envp; envp[0] = null; } // Trace me if (ptrace(PT_TRACEME, 0, 0, 0)) return adbg_error_system; version (CRuntime_Musl) { if (raise(SIGTRAP)) return adbg_error_system; } // Execute if (execve(path, cast(const(char)**)__argv, cast(const(char)**)__envp) == -1) return adbg_error_system; } } // USE_CLONE } g_debuggee.attached = false; g_debuggee.status = AdbgStatus.ready; return 0; } version (Posix) version (USE_CLONE) private int __adbg_chld(void* arg) { __adbg_child_t *c = cast(__adbg_child_t*)arg; if (ptrace(PT_TRACEME, 0, 0, 0)) return adbg_oops(AdbgError.os); execve(c.argv[0], c.argv, c.envp); return adbg_oops(AdbgError.os); } enum { /// Stop debuggee when attached. ADBG_ATTACH_OPT_STOP = 1, /// Don't kill debuggee when debugger exits. ADBG_ATTACH_OPT_EXITKILL = 1 << 1, } /** * Attach the debugger to a process ID. * Windows: Uses DebugActiveProcess * Posix: Uses ptrace(PT_SEIZE) * Params: * pid = Process ID * flags = Reserved * Returns: OS error code on error */ int adbg_attach_(int pid, int flags = 0) { bool stop = (flags & ADBG_ATTACH_OPT_STOP) != 0; bool exitkill = (flags & ADBG_ATTACH_OPT_EXITKILL) != 0; version (Windows) { // Creates events: // - CREATE_PROCESS_DEBUG_EVENT // - CREATE_THREAD_DEBUG_EVENT if (DebugActiveProcess(pid) == FALSE) return adbg_oops(AdbgError.os); g_debuggee.pid = cast(DWORD)pid; g_debuggee.hpid = OpenProcess( PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, cast(DWORD)pid); // Default is TRUE if (exitkill == false) DebugSetProcessKillOnExit(FALSE); // DebugActiveProcess stops debuggee when attached if (stop == false) { DEBUG_EVENT e = void; wait: while (WaitForDebugEvent(&e, 100)) { switch (e.dwDebugEventCode) { case CREATE_PROCESS_DEBUG_EVENT: case CREATE_THREAD_DEBUG_EVENT: continue; case EXCEPTION_DEBUG_EVENT: ContinueDebugEvent(e.dwProcessId, e.dwThreadId, DBG_CONTINUE); continue; default: break wait; } } // This was my second attempt, but, could be useful later... /*import core.sys.windows.tlhelp32 : CreateToolhelp32Snapshot, Thread32First, Thread32Next, THREADENTRY32, TH32CS_SNAPTHREAD; // CreateToolhelp32Snapshot ignores th32ProcessID for TH32CS_SNAPTHREAD HANDLE h_thread_snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (h_thread_snapshot == INVALID_HANDLE_VALUE) return adbg_oops(AdbgError.os); THREADENTRY32 te32 = void; te32.dwSize = THREADENTRY32.sizeof; // If the first fails, all successive ones will fail if (Thread32First(h_thread_snapshot, &te32) == FALSE) { CloseHandle(h_thread_snapshot); return adbg_oops(AdbgError.os); } do { if (te32.th32OwnerProcessID == pid) { ContinueDebugEvent(pid, te32.th32ThreadID, DBG_CONTINUE); } } while (Thread32Next(h_thread_snapshot, &te32));*/ } } else version (Posix) { if (ptrace(stop ? PT_ATTACH : PT_SEIZE, pid, null, null) == -1) return adbg_oops(AdbgError.os); g_debuggee.pid = cast(pid_t)pid; if (exitkill) if (ptrace(PT_SETOPTIONS, pid, null, PT_O_EXITKILL) == -1) return adbg_oops(AdbgError.os); } g_debuggee.attached = true; g_debuggee.status = stop ? AdbgStatus.paused : AdbgStatus.running; return 0; } /// Detach debugger from current process. int adbg_detach_() { version (Windows) { if (DebugActiveProcessStop(g_debuggee.pid) == FALSE) return adbg_oops(AdbgError.os); } else version (Posix) { if (ptrace(PT_DETACH, g_debuggee.pid, null, null) == -1) return adbg_oops(AdbgError.os); } return 0; } /// Insert a debuggee break. //TODO: bool checkDebugger = false // POSIX: https://stackoverflow.com/a/24969863 void adbg_break_() { version (Windows) { DebugBreak(); } else version (Posix) { ptrace(PT_TRACEME, 0, null, null); } } /** * Get the debugger's current status. * Returns: AdbgStatus enum */ AdbgStatus adbg_state() { return g_debuggee.status; } /** * Enter the debugging loop. Continues execution of the process until a new * debug event occurs. When an exception occurs, the exception_t structure is * populated with debugging information. * (Windows) Uses WaitForDebugEvent, filters any but EXCEPTION_DEBUG_EVENT * (Posix) Uses ptrace(2) and waitpid(2), filters SIGCONT out * Params: userfunc = User function callback * Returns: Zero on success; Otherwise an error occured */ int adbg_run(int function(exception_t*) userfunc) { if (userfunc == null) return adbg_oops(AdbgError.nullAddress); exception_t e = void; version (Windows) { DEBUG_EVENT de = void; L_DEBUG_LOOP: g_debuggee.status = AdbgStatus.running; if (WaitForDebugEvent(&de, INFINITE) == FALSE) return adbg_oops(AdbgError.os); g_debuggee.status = AdbgStatus.paused; // 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: return 0; default: ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE); goto L_DEBUG_LOOP; } adbg_ex_dbg(&e, &de); g_debuggee.status = AdbgStatus.paused; with (AdbgAction) final switch (userfunc(&e)) { case exit: if (g_debuggee.attached) DebugActiveProcessStop(g_debuggee.pid); else ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_TERMINATE_PROCESS); g_debuggee.status = AdbgStatus.idle; return 0; case step: // Enable single-stepping via Trap flag version (Win64) { CONTEXT winctx = void; WOW64_CONTEXT winctxwow64 = void; if (g_debuggee.wow64) { winctxwow64.ContextFlags = CONTEXT_CONTROL; Wow64GetThreadContext(g_debuggee.htid, &winctxwow64); FlushInstructionCache(g_debuggee.hpid, null, 0); winctxwow64.EFlags |= 0x100; Wow64SetThreadContext(g_debuggee.htid, &winctxwow64); } else { winctx.ContextFlags = CONTEXT_CONTROL; GetThreadContext(g_debuggee.htid, &winctx); FlushInstructionCache(g_debuggee.hpid, null, 0); winctx.EFlags |= 0x100; SetThreadContext(g_debuggee.htid, &winctx); } } else { CONTEXT winctx = void; winctx.ContextFlags = CONTEXT_ALL; GetThreadContext(g_debuggee.htid, &winctx); FlushInstructionCache(g_debuggee.hpid, null, 0); winctx.EFlags |= 0x100; SetThreadContext(g_debuggee.htid, &winctx); } goto case; case proceed: if (ContinueDebugEvent( de.dwProcessId, de.dwThreadId, DBG_CONTINUE) == FALSE) { g_debuggee.status = AdbgStatus.idle; return adbg_oops(AdbgError.os); } goto L_DEBUG_LOOP; } } else version (Posix) { int wstatus = void; L_DEBUG_LOOP: g_debuggee.status = AdbgStatus.running; g_debuggee.pid = waitpid(-1, &wstatus, 0); if (g_debuggee.pid == -1) { g_debuggee.status = AdbgStatus.idle; return adbg_oops(AdbgError.os); } g_debuggee.status = AdbgStatus.paused; // 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 int chld_signo = wstatus >> 8; // Only interested if child is continuing or stopped; Otherwise // it exited and there's nothing more we can do about it. // So return its status code if ((wstatus & 0x7F) != 0x7F) { g_debuggee.status = AdbgStatus.idle; return chld_signo; } // Signal filtering switch (chld_signo) { 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 case SIGILL, SIGSEGV, SIGFPE, SIGBUS: siginfo_t sig = void; if (ptrace(PT_GETSIGINFO, g_debuggee.pid, null, &sig) < 0) { e.fault.raw = null; break; } e.fault.raw = sig._sifields._sigfault.si_addr; break; // case SIGINT, SIGTERM, SIGABRT: //TODO: Kill? default: e.fault.raw = null; } adbg_ex_dbg(&e, g_debuggee.pid, chld_signo); with (AdbgAction) switch (userfunc(&e)) { case exit: g_debuggee.status = AdbgStatus.idle; // in either case // Because PT_KILL is deprecated if (kill(g_debuggee.pid, SIGKILL) == -1) return adbg_oops(AdbgError.os); return 0; case step: if (ptrace(PT_SINGLESTEP, g_debuggee.pid, null, null) == -1) { g_debuggee.status = AdbgStatus.idle; return adbg_oops(AdbgError.os); } goto L_DEBUG_LOOP; case proceed: if (ptrace(PT_CONT, g_debuggee.pid, null, null) == -1) { g_debuggee.status = AdbgStatus.idle; return adbg_oops(AdbgError.os); } goto L_DEBUG_LOOP; default: assert(0); } } } /// Read memory from debuggee child. /// Params: /// addr = Memory address (within the children address space) /// data = Pointer to data /// size = Size of data /// Returns: Non-zero on error int adbg_mm_read(size_t addr, void *data, uint size) { version (Windows) { if (ReadProcessMemory(g_debuggee.hpid, cast(void*)addr, data, size, null) == 0) return adbg_oops(AdbgError.os); } else { // Based on https://www.linuxjournal.com/article/6100 c_long *d = cast(c_long*)data; /// destination int r = size / c_long.sizeof; /// number of "long"s to read for (; r > 0; --r, ++d, addr += c_long.sizeof) *d = ptrace(PT_PEEKDATA, g_debuggee.pid, addr, null); r = size % c_long.sizeof; if (r) { c_long c = ptrace(PT_PEEKDATA, g_debuggee.pid, addr, null); ubyte* dest8 = cast(ubyte*)d, src8 = cast(ubyte*)&c; for (; r; --r) *dest8++ = *src8++; // inlined memcpy } } return 0; } /// Write memory to debuggee child. /// Params: /// addr = Memory address (within the children address space) /// data = Pointer to data /// size = Size of data /// Returns: Non-zero on error int adbg_mm_write(size_t addr, void *data, uint size) { version (Windows) { if (WriteProcessMemory(g_debuggee.hpid, cast(void*)addr, data, size, null) == 0) return adbg_oops(AdbgError.os); } else { // Mostly taken from https://www.linuxjournal.com/article/6100 c_long *user = cast(c_long*)data; /// user data pointer int i; /// offset index int j = size / c_long.sizeof; /// number of "blocks" to process for (; i < j; ++i, ++user) ptrace(PT_POKEDATA, g_debuggee.pid, addr + (i * c_long.sizeof), user); j = size % c_long.sizeof; if (j) ptrace(PT_POKEDATA, g_debuggee.pid, addr + (i * c_long.sizeof), user); } return 0; } // adbg_mm_maps options enum { /// Only get the memory regions for this process ADBG_MM_OPT_PROCESS_ONLY = 1, // With given Process ID instead // Permission issues may be raised //ADBG_MM_OPT_PID = 2, } private enum MM_MAP_NAME_LEN = 512; enum { ADBG_ACCESS_R = 1, ADBG_ACCESS_W = 1 << 1, ADBG_ACCESS_X = 1 << 2, ADBG_ACCESS_P = 1 << 8, ADBG_ACCESS_S = 1 << 9, } /// Represents a mapped memory region struct adbg_mm_map { /// Base memory region address. void *base; /// Size of region. size_t size; /// Access permissions. /// int access; /// char[MM_MAP_NAME_LEN] name; } /// Obtain the memory maps for the current process int adbg_mm_maps(adbg_mm_map **mmaps, size_t *mcount, ...) { version (Windows) { if (__dynlib_psapi_load()) return adbg_oops(AdbgError.libLoader); if (g_debuggee.pid == 0) { return adbg_oops(AdbgError.notAttached); } if (mmaps == null || mcount == null) { return adbg_oops(AdbgError.nullArgument); } enum SIZE = 512 * HMODULE.sizeof; HMODULE *mods = cast(HMODULE*)malloc(SIZE); DWORD needed = void; if (EnumProcessModules(g_debuggee.hpid, mods, SIZE, &needed) == FALSE) { free(mods); return adbg_oops(AdbgError.os); } DWORD modcount = needed / HMODULE.sizeof; adbg_mm_map *map = *mmaps = cast(adbg_mm_map*)malloc(modcount * adbg_mm_map.sizeof); size_t i; /// (user) map index for (DWORD mod_i; mod_i < modcount; ++mod_i) { HMODULE mod = mods[mod_i]; MODULEINFO minfo = void; if (GetModuleInformation(g_debuggee.hpid, mod, &minfo, MODULEINFO.sizeof) == FALSE) { continue; } // \Device\HarddiskVolume5\xyz.dll if (GetMappedFileNameA(g_debuggee.hpid, minfo.lpBaseOfDll, map.name.ptr, MM_MAP_NAME_LEN) == FALSE) { // xyz.dll if (GetModuleBaseNameA(g_debuggee.hpid, mod, map.name.ptr, MM_MAP_NAME_LEN) == FALSE) { map.name[0] = 0; } } MEMORY_BASIC_INFORMATION mem = void; VirtualQuery(minfo.lpBaseOfDll, &mem, MEMORY_BASIC_INFORMATION.sizeof); // Needs a bit for Copy-on-Write? if (mem.AllocationProtect & PAGE_EXECUTE_WRITECOPY) map.access = ADBG_ACCESS_R | ADBG_ACCESS_X; else if (mem.AllocationProtect & PAGE_EXECUTE_READWRITE) map.access = ADBG_ACCESS_R | ADBG_ACCESS_W | ADBG_ACCESS_X; else if (mem.AllocationProtect & PAGE_EXECUTE_READ) map.access = ADBG_ACCESS_R | ADBG_ACCESS_X; else if (mem.AllocationProtect & PAGE_EXECUTE) map.access = ADBG_ACCESS_X; else if (mem.AllocationProtect & PAGE_READONLY) map.access = ADBG_ACCESS_R; else if (mem.AllocationProtect & PAGE_READWRITE) map.access = ADBG_ACCESS_R | ADBG_ACCESS_W; else if (mem.AllocationProtect & PAGE_WRITECOPY) map.access = ADBG_ACCESS_R; else map.access = 0; map.access |= mem.Type == MEM_PRIVATE ? ADBG_ACCESS_P : ADBG_ACCESS_S; map.base = minfo.lpBaseOfDll; map.size = minfo.SizeOfImage; ++i; ++map; } free(mods); *mcount = i; return 0; } else version (linux) { // Inspired by libscanmem // https://github.com/scanmem/scanmem/blob/main/maps.c import core.stdc.config : c_long; import core.stdc.stdlib : malloc, free; import core.sys.linux.unistd : readlink; import adbg.utils.strings : adbg_util_getline, adbg_util_getlinef; import core.sys.linux.unistd : read, close; import core.sys.linux.fcntl : open, O_RDONLY; if (g_debuggee.pid == 0) { return adbg_oops(AdbgError.notAttached); } if (mmaps == null || mcount == null) { return adbg_oops(AdbgError.nullArgument); } *mcount = 0; // Formulate proc map path enum PROC_MAPS_LEN = 32; char[PROC_MAPS_LEN] proc_maps = void; snprintf(proc_maps.ptr, PROC_MAPS_LEN, "/proc/%u/maps", g_debuggee.pid); version (Trace) trace("maps: %s", proc_maps.ptr); // Open process maps int fd_maps = open(proc_maps.ptr, O_RDONLY); if (fd_maps == -1) return adbg_oops(AdbgError.os); // Formulate proc exe path enum PROC_EXE_LEN = 32; char[PROC_EXE_LEN] proc_exe = void; snprintf(proc_exe.ptr, PROC_EXE_LEN, "/proc/%u/exe", g_debuggee.pid); // Read link from proc exe for process path (e.g., /usr/bin/cat) enum EXE_PATH_LEN = 256; char[EXE_PATH_LEN] exe_path = void; version (Trace) trace("exe: %s", proc_exe.ptr); ssize_t linksz = readlink(proc_exe.ptr, exe_path.ptr, EXE_PATH_LEN); if (linksz > 0) { exe_path[linksz] = 0; } else { // Fail or empty exe_path[0] = 0; } // Allocate 4 MiB for input maps buffer // WebKit has about 164K worth of maps, for example // And then read as much as possible (not possible with fread) enum READSZ = 4 * 1024 * 1024; //TODO: Consider mmap(2) char *procbuf = cast(char*)malloc(READSZ); if (procbuf == null) { version (Trace) trace("malloc failed"); close(fd_maps); return adbg_oops(AdbgError.crt); } ssize_t readsz = read(fd_maps, procbuf, READSZ); if (readsz == -1) { version (Trace) trace("read failed"); free(procbuf); close(fd_maps); return adbg_oops(AdbgError.os); } version (Trace) trace("flen=%zu", readsz); // Count number of newlines for number of items to allocate // Cut lines don't have newlines, so no worries here size_t itemcnt; for (size_t i; i < readsz; ++i) if (procbuf[i] == '\n') ++itemcnt; // Allocate map items version (Trace) trace("allocating %zu items", itemcnt); adbg_mm_map *map = *mmaps = cast(adbg_mm_map*)malloc(itemcnt * adbg_mm_map.sizeof); if (map == null) { free(procbuf); close(fd_maps); return adbg_oops(AdbgError.crt); } // Go through each entry, which may look like this (without header): // Address Perm Offset Dev inode Path // 55adaf007000-55adaf009000 r--p 00000000 08:02 1311130 /usr/bin/cat // Perms: r=read, w=write, x=execute, s=shared or p=private (CoW) // Path: Path or [stack], [stack:%id] (3.4 to 4.4), [heap] // [vdso]: https://lwn.net/Articles/615809/ // [vvar]: Stores a "mirror" of kernel variables required by virt syscalls // [vsyscall]: Legacy user-kernel (jump?) tables for some syscalls enum LINE_LEN = 256; char[LINE_LEN] line = void; size_t linesz = void; /// line size size_t srcidx; /// maps source buffer index size_t i; /// maps index while (adbg_util_getline(line.ptr, LINE_LEN, &linesz, procbuf, &srcidx)) { size_t range_start = void; size_t range_end = void; char[4] perms = void; // rwxp uint offset = void; uint dev_major = void; uint dev_minor = void; uint inode = void; //char[512] path = void; if (sscanf(line.ptr, "%zx-%zx %4s %x %x:%x %u %512s", &range_start, &range_end, perms.ptr, &offset, &dev_major, &dev_minor, &inode, map.name.ptr) < 8) { continue; } // ELF load address regions // // When the ELF loader loads an executable or library image into // memory, there is one memory region per section created: // .text (r-x), .rodata (r--), .data (rw-), and .bss (rw-). // // The 'x' permission of .text is used to detect the load address // (start of memory region) and the end of the ELF file in memory. // // .bss section: // - Except for the .bss section, all memory sections typically // have the same filename of the executable image. // - Empty filenames typically indicates .bss memory regions, and // may be consecutive with .data memory regions. // - With some ELF images, .bss and .rodata may not be present. // // Resources: // http://en.wikipedia.org/wiki/Executable_and_Linkable_Format // http://wiki.osdev.org/ELF // http://lwn.net/Articles/531148/ //TODO: Adjust memory region permissions like libscanmem does version (Trace) trace("entry: %zx %s", range_start, map.name.ptr); map.base = cast(void*)range_start; map.size = range_end - range_start; map.access = perms[3] == 'p' ? ADBG_ACCESS_P : ADBG_ACCESS_S; if (perms[0] == 'r') map.access |= ADBG_ACCESS_R; if (perms[1] == 'w') map.access |= ADBG_ACCESS_W; if (perms[2] == 'x') map.access |= ADBG_ACCESS_X; ++i; ++map; } *mcount = i; free(procbuf); return 0; } else // FreeBSD: procstat(1) // - https://man.freebsd.org/cgi/man.cgi?query=vm_map // - https://github.com/freebsd/freebsd-src/blob/main/lib/libutil/kinfo_getvmmap.c // - args[0] = CTL_KERN // - args[1] = KERN_PROC // - args[2] = KERN_PROC_VMMAP // - args[3] = pid // NetBSD: pmap(1) // OpenBSD: procmap(1) return adbg_oops(AdbgError.notImplemented); }