Newer
Older
alicedbg / src / adbg / dbg / debugger.d
@dd86k dd86k on 20 Mar 2021 16 KB context: small tweak
/**
 * 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.
 *
 * License: BSD-3-Clause
 */
module adbg.dbg.debugger;

import core.stdc.string : memset;
public import adbg.dbg.exception;
import adbg.platform, adbg.error;

version (Windows) {
	import core.sys.windows.windows;
	import adbg.sys.windows.wow64;
} else
version (Posix) {
	import core.sys.posix.sys.stat;
	import core.sys.posix.sys.wait : waitpid, SIGCONT, WUNTRACED;
	import core.sys.posix.signal : kill, SIGKILL, siginfo_t, raise;
	import core.sys.posix.sys.uio;
	import core.sys.posix.fcntl : open;
	import core.stdc.stdlib : exit, malloc, free;
	import core.stdc.stdio : snprintf;
	import adbg.sys.posix.mann;
	import adbg.sys.posix.ptrace;
	import adbg.sys.posix.unistd;
	import adbg.sys.linux.user;
	private enum __WALL = 0x40000000;
}

version (linux)
	version = USE_CLONE;

version (X86)
	private enum opcode_t BREAKPOINT = 0xCC; // INT3
else version (X86_64)
	private enum opcode_t BREAKPOINT = 0xCC; // INT3
else version (ARM_Thumb)
	version (LittleEndian)
		private enum opcode_t BREAKPOINT = 0xDDBE; // BKPT #221 (0xdd)
	else
		private enum opcode_t BREAKPOINT = 0xBEDD; // BKPT #221 (0xdd)
else version (ARM) {
	version (LittleEndian)
		private enum opcode_t BREAKPOINT = 0x7D0D20E1; // BKPT #221 (0xdd)
	else
		private enum opcode_t BREAKPOINT = 0xE1200D7D; // BKPT #221 (0xdd)
} else version (AArch64) {
	// NOTE: Checked under ODA, endianness seems to be moot
	version (LittleEndian)
		private enum opcode_t BREAKPOINT = 0xA01B20D4; // BKPT #221 (0xdd)
	else
		private enum opcode_t BREAKPOINT = 0xA01B20D4; // BKPT #221 (0xdd)
} else
	static assert(0, "Missing BREAKPOINT value for target platform");

extern (C):
__gshared:

/// 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
}

/// States the currently debugger state
public
enum AdbgState {
	idle,	/// Waiting for input
	loaded,	/// 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;
	}
}+/

private
struct debuggee_t {
	AdbgState state;
	breakpoint_t[ADBG_MAX_BREAKPOINTS] breakpoints;
	size_t bpindex;	/// breakpoint index
	bool attached;	/// if debuggee was attached to
	version (Windows) {
		HANDLE hpid;	/// Process handle
		HANDLE htid;	/// Thread handle
		int pid;	/// Process identificiation number
		int tid;	/// Thread identification number
		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
	}
}

private
struct breakpoint_t {
	size_t address;
	align(4) opcode_t opcode;
}

version(USE_CLONE)
private
struct __adbg_child_t {
	const(char) *dev;
	const(char) **argv, envp;
}

package debuggee_t g_debuggee;	/// Debuggee information
private int g_options;	/// Debugger options

//TODO: Load/Attach flags
//      - processOnly (only this process)
//      - useClone (linux 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) (PTRACE_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_error(AdbgError.invalidArgument);
	
	version (Windows) {
		import core.stdc.stdlib : malloc, free;
		import core.stdc.stdio : snprintf;
		import adbg.utils.str : adbg_util_argv_flatten;
		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_error_system;
		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
		// only 64-bit versions of Windows really have WOW64.
		// 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_error_system;
	} else
	version (Posix) {
		// Verify if file exists and we has access to it
		stat_t st = void;
		if (stat(path, &st) == -1)
			return adbg_error_system;
		// 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_error_system;

			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_error_system;
		} else { // fork(2)
			g_debuggee.pid = fork();
			if (g_debuggee.pid < 0)
				return adbg_error_system;
			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(PTRACE_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.state = AdbgState.loaded;
	return 0;
}

version (Posix)
version (USE_CLONE)
private int __adbg_chld(void* arg) {
	__adbg_child_t *c = cast(__adbg_child_t*)arg;
	if (ptrace(PTRACE_TRACEME, 0, 0, 0))
		return adbg_error_system;
	execve(c.argv[0], c.argv, c.envp);
	return adbg_error_system;
}

/**
 * Attach the debugger to a process ID.
 * Windows: Uses DebugActiveProcess
 * Posix: Uses ptrace(PTRACE_SEIZE)
 * Params:
 * 	pid = Process ID
 * 	flags = Reserved
 * Returns: Non-zero on error: (Posix) errno or (Windows) GetLastError
 */
int adbg_attach(int pid, int flags = 0) {
	version (Windows) {
		if (DebugActiveProcess(pid) == FALSE)
			return adbg_error_system;
	} else
	version (Posix) {
		if (ptrace(PTRACE_SEIZE, pid, null, null) == -1)
			return adbg_error_system;
	}
	g_debuggee.attached = true;
	//TODO: Check if the process is really paused
	g_debuggee.state = AdbgState.paused;
	return 0;
}

/**
 * Get the debugger's current state.
 * Returns: AdbgState enum
 */
AdbgState adbg_state() {
	return g_debuggee.state;
}

/**
 * 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: User function callback
 * Returns: Zero on success; Otherwise an error occured
 */
int adbg_run(int function(exception_t*) userfunc) {
	if (userfunc == null)
		return adbg_error(AdbgError.nullAddress);
	
	exception_t e = void;
	
	version (Windows) {
		DEBUG_EVENT de = void;
L_DEBUG_LOOP:
		g_debuggee.state = AdbgState.running;
		if (WaitForDebugEvent(&de, INFINITE) == FALSE)
			return adbg_error_system;
		g_debuggee.state = AdbgState.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.state = AdbgState.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.state = AdbgState.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.state = AdbgState.idle;
				return adbg_error_system;
			}
			goto L_DEBUG_LOOP;
		}
	} else
	version (Posix) {
		int wstatus = void;
L_DEBUG_LOOP:
		g_debuggee.state = AdbgState.running;
		g_debuggee.pid = waitpid(-1, &wstatus, 0);
		
		if (g_debuggee.pid == -1) {
			g_debuggee.state = AdbgState.idle;
			return adbg_error_system;
		}
		
		g_debuggee.state = AdbgState.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.state = AdbgState.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(PTRACE_GETSIGINFO, g_debuggee.pid, null, &sig) >= 0) {
				version (CRuntime_Glibc)
					e.faultaddr = sig._sifields._sigfault.si_addr;
				else version (CRuntime_Musl)
					e.faultaddr = sig.__si_fields.__sigfault.si_addr;
				else static assert(0, "hack me");
			} else {
				e.faultaddr = null;
			}
			break;
//		case SIGINT, SIGTERM, SIGABRT: //TODO: Kill?
		default:
			e.faultaddr = null;
		}
		
		adbg_ex_dbg(&e, g_debuggee.pid, chld_signo);
		
		with (AdbgAction)
		switch (userfunc(&e)) {
		case exit:
			g_debuggee.state = AdbgState.idle; // in either case
			// Because PTRACE_KILL is deprecated
			if (kill(g_debuggee.pid, SIGKILL) == -1)
				return adbg_error_system;
			return 0;
		case step:
			if (ptrace(PTRACE_SINGLESTEP, g_debuggee.pid, null, null) == -1) {
				g_debuggee.state = AdbgState.idle;
				return adbg_error_system;
			}
			goto L_DEBUG_LOOP;
		case proceed:
			if (ptrace(PTRACE_CONT, g_debuggee.pid, null, null) == -1) {
				g_debuggee.state = AdbgState.idle;
				return adbg_error_system;
			}
			goto L_DEBUG_LOOP;
		default: assert(0);
		}
	}
}

//
// Breakpoint handling
//

int adbg_bp_add(size_t addr) {
	if (g_debuggee.bpindex >= ADBG_MAX_BREAKPOINTS - 1)
		return 2;
	breakpoint_t *bp = &g_debuggee.breakpoints[g_debuggee.bpindex];
	assert(0);
}
breakpoint_t* adbg_bp_index(int index) {
	assert(0, "adbg_bp_index not implemented");
}
breakpoint_t* adbg_bp_addr(size_t addr) {
	assert(0, "adbg_bp_addr not implemented");
}
int adbg_bp_list(breakpoint_t [ADBG_MAX_BREAKPOINTS]*l, uint *n) {
	assert(0, "adbg_bp_list not implemented");
}
int adbg_bp_rm_index(int index) {
	assert(0, "adbg_bp_rm_index not implemented");
}
int adbg_bp_rm_addr(size_t addr) {
	assert(0, "adbg_bp_rm_addr not implemented");
}

//
// Memory handling
//

enum {	// adbg_mm flags
	// Flags
	MM_READ	= 0,	/// Read from memory to data pointer
	MM_WRITE	= 0x8000,	/// Write to memory from data pointer
}

/**
 * Read or write to a memory region from or to the opened debugee process.
 * This does not include subchildren processes.
 * Params:
 * 	op   = MM operation
 * 	addr = Memory address location
 * 	data = Data pointer
 *      size = Size to read or write
 * Returns: Zero on success, oscode on error
 */
deprecated
int adbg_mm(int op, size_t addr, void *data, uint size) {
	version (Windows) {
		if (op >= MM_WRITE) {
			if (WriteProcessMemory(g_debuggee.hpid, cast(void*)addr, data, size, null) == 0)
				return GetLastError();
		} else {
			if (ReadProcessMemory(g_debuggee.hpid, cast(void*)addr, data, size, null) == 0)
				return GetLastError();
		}
	} else
	version (linux) {
		//TODO: adbg_mm (linux)
		//      use open(2) and pread64/pwrite64(2)
		if (g_debuggee.mhandle <= 0) {
			char* cb = cast(char*)malloc(4096);
			if (cb == null)
				return adbg_error_system;
			int n = snprintf(cb, 4096, "/proc/%d/mem", g_debuggee.pid);
			if (n < 0)
				return adbg_error_system;
			g_debuggee.mhandle = open(cb, 0);
			free(cb);
		}
		if (op >= MM_WRITE) {
			if (pwrite(g_debuggee.mhandle, data, size, addr) == -1)
				return adbg_error_system;
		} else {
			if (pread(g_debuggee.mhandle, data, size, addr) == -1)
				return adbg_error_system;
		}
	}

	return 0;
}