Skip to content

Instantly share code, notes, and snippets.

@ben-cohen
Created February 23, 2025 22:26
Show Gist options
  • Select an option

  • Save ben-cohen/16b6e2417ce1c542fe03abada4ac59aa to your computer and use it in GitHub Desktop.

Select an option

Save ben-cohen/16b6e2417ce1c542fe03abada4ac59aa to your computer and use it in GitHub Desktop.
Set breakpoints and watchpoints on the child process using ptrace on x86-64 Linux
/*
* ptrace_test: Set breakpoints and watchpoints on the child process using
* ptrace on x86-64 Linux
*
* Compile using:
* gcc -o ptrace_test ptrace_test.c -Wall -ggdb
*
* Ben Cohen, February 2025
*/
#include <errno.h>
#include <inttypes.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/user.h>
#include <sys/wait.h>
void *address_of_breakpoint = NULL;
int variable_to_watch = 0;
void ptrace_checked(const char* who,
enum __ptrace_request request,
pid_t pid,
void *addr,
void *data)
{
long result = ptrace(request, pid, addr, data);
if (result != -1)
{
return;
}
char *request_name = NULL;
switch (request)
{
case PTRACE_TRACEME:
request_name = "PTRACE_TRACEME";
break;
case PTRACE_CONT:
request_name = "PTRACE_CONT";
break;
case PTRACE_POKEDATA:
request_name = "PTRACE_POKEDATA";
break;
case PTRACE_PEEKDATA:
request_name = "PTRACE_PEEKDATA";
break;
case PTRACE_POKEUSER:
request_name = "PTRACE_POKEUSER";
break;
default:
request_name = "(unknown)";
break;
}
fprintf(stderr, "%s: %s: %s\n", who, request_name, strerror(errno));
exit(1);
}
enum watchpoint_type
{
WP_EXECUTE = 0x0,
WP_WRITE = 0x1,
WP_IO_RW = 0x2,
WP_DATA_RW = 0x3
};
enum watchpoint_length
{
WP_1 = 0x0,
WP_2 = 0x1,
WP_8 = 0x2,
WP_4 = 0x3
};
enum watchpoint_enable
{
WP_NONE = 0x0,
WP_LOCAL = 0x1,
WP_GLOBAL = 0x2,
WP_BOTH = 0x3
};
void set_watchpoint(pid_t child_pid, void* address_for_breakpoint,
enum watchpoint_enable watchpoint_enable,
enum watchpoint_type watchpoint_type,
enum watchpoint_length watchpoint_length)
{
/* x86 and x86-64 have 8 debug registers DR0 to DR7, which control
* up to 4 memory addresses. They are used as follows:
*
* DR0 Address for Breakpoint 0
* DR1 Address for Breakpoint 1
* DR2 Address for Breakpoint 2
* DR3 Address for Breakpoint 3
* DR4 (reserved)
* DR5 (reserved)
* DR6 Debug Status Register: state reported when an exception is
* generated
* DR7 Debug Control Register: enable or disable and set conditions
* for the breakpoints
*
* References:
* * The Intel® 64 and IA-32 Architectures Software Developer's Manual
* Volume 3: System Programming Guide, chapter 17.2.
* * https://stackoverflow.com/a/40820763/
*
* Here we only use Breakpoint 0.
*/
uint64_t DR0 = (uintptr_t)address_for_breakpoint;
uint64_t DR6 = 0x00000000FFFF0FF0; /* reserved bits */
uint64_t DR7 = 0x0000000000000700; /* reserved bits and LE/GE local and
global exact breakpoint enable */
/* The following bits are for Breakpoint 0. Shift them left by 2, 4 or 6
* bits for Breakpoints 1, 2 or 3. */
/* Bits 00-01: L0, G0: 00 = both disabled
* 01 = local breakpoint enable
* 10 = global breakpoint enable
* 11 = local and global breakpoint enable */
DR7 |= watchpoint_enable;
/* Bits 16-17: R/W0: 00 = instruction execute
* 01 = data write
* 10 = I/O read or write
* 11 = data read or write but not
* instruction fetches */
DR7 |= (watchpoint_type << 16);
/* Bits 18-19: LEN0: 00 = 1 byte
* 01 = 2 byte
* 10 = 8 byte (x86-64)
* 11 = 4 byte */
DR7 |= (watchpoint_length << 18);
/* Set the debug registers. */
ptrace_checked("PARENT", PTRACE_POKEUSER, child_pid,
(void*)(uintptr_t)offsetof(struct user, u_debugreg[0]),
(void*)(uintptr_t)DR0);
ptrace_checked("PARENT", PTRACE_POKEUSER, child_pid,
(void*)(uintptr_t)offsetof(struct user, u_debugreg[6]),
(void*)(uintptr_t)DR6);
ptrace_checked("PARENT", PTRACE_POKEUSER, child_pid,
(void*)(uintptr_t)offsetof(struct user, u_debugreg[7]),
(void*)(uintptr_t)DR7);
}
void clear_watchpoint(pid_t child_pid)
{
set_watchpoint(child_pid, NULL, WP_NONE, 0, 0);
}
void wait_for_signal(pid_t child_pid, int signal)
{
int wstatus;
waitpid(child_pid, &wstatus, 0);
if (!WIFSTOPPED(wstatus))
{
printf("PARENT: Child not stopped!\n");
exit(1);
}
else if (WSTOPSIG(wstatus) != signal)
{
printf("PARENT: Child stopped on unexpected signal %d!\n",
WSTOPSIG(wstatus));
exit(1);
}
}
void wait_for_exit(pid_t child_pid, uint8_t status)
{
int wstatus;
waitpid(child_pid, &wstatus, 0);
if (!WIFEXITED(wstatus))
{
printf("PARENT: Child has not exited!\n");
exit(1);
}
else if (WEXITSTATUS(wstatus) != status)
{
printf("PARENT: Child exited with unexpected status %d!\n",
WEXITSTATUS(wstatus));
exit(1);
}
}
void set_breakpoint(pid_t child_pid, void* address,
uint64_t* original_value)
{
void* aligned_address = (void*)(((uintptr_t)address) & ~7);
uintptr_t remainder = ((uintptr_t)address) & 7;
long result = ptrace(PTRACE_PEEKTEXT, child_pid, aligned_address, NULL);
/* Note: Nasty special case to handle error return value! */
if (result == -1 && errno != 0)
{
printf("PARENT: ptrace: PTRACE_PEEKTEXT: %s\n", strerror(errno));
exit(1);
}
*original_value = result;
const uint8_t ASM_INT3 = 0xCC;
int shift_by = remainder * 8;
uint64_t new_value = result;
new_value &= ~(0xFF << shift_by);
new_value |= ((uint64_t)ASM_INT3) << shift_by;
ptrace_checked("PARENT", PTRACE_POKETEXT, child_pid, aligned_address,
(void*)new_value);
}
void clear_breakpoint(pid_t child_pid, void* address,
uint64_t original_value)
{
/* Here we simply restore the original memory. If we wanted to stop again
* at the same place then we would need to single-step one instruction
* and then re-set the breakpoint. */
void* aligned_address = (void*)(((uintptr_t)address) & ~7);
ptrace_checked("PARENT", PTRACE_POKETEXT, child_pid, aligned_address,
(void*)original_value);
/* The instruction pointer is now one byte past the start of the real
* instruction, so decrement it. */
struct user_regs_struct registers;
ptrace_checked("PARENT", PTRACE_GETREGS, child_pid, NULL, &registers);
registers.rip --;
ptrace_checked("PARENT", PTRACE_SETREGS, child_pid, NULL, &registers);
}
void function_to_break_on()
{
/* The child should stop on the breakpoint here. */
printf(" CHILD: After breakpoint\n");
}
void execute_child()
{
/* We use raise() here because the ptrace() call itself does not
* stop the child and we are not calling execxxx(). */
ptrace_checked(" CHILD", PTRACE_TRACEME, 0, NULL, NULL);
raise(SIGSTOP);
printf(" CHILD: Before breakpoint\n");
function_to_break_on();
printf(" CHILD: Read from memory with 'data write only' watchpoint\n");
/* The child should not stop here. */
printf(" CHILD: variable_to_watch = %d\n", variable_to_watch);
printf(" CHILD: Write to memory with 'data write only' watchpoint\n");
/* The child should stop on the watchpoint here. */
variable_to_watch = 1;
printf(" CHILD: Write to memory with disabled watchpoint\n");
/* The child should not stop here. */
variable_to_watch = 1;
printf(" CHILD: Exiting\n");
exit(0);
}
void execute_parent(pid_t child_pid)
{
printf("PARENT: Child has pid %" PRIdMAX ". Wait for it to stop\n",
(intmax_t) child_pid);
wait_for_signal(child_pid, SIGSTOP);
/* PART 1: Setting breakpoints */
printf("PARENT: Set a breakpoint\n");
uint64_t original_memory = 0;
set_breakpoint(child_pid, &function_to_break_on, &original_memory);
printf("PARENT: Make the child continue and wait for it\n");
ptrace_checked("PARENT", PTRACE_CONT, child_pid, NULL, NULL);
/* Note that we should probably check that we stopped because of the
* break and not for some other reason. */
wait_for_signal(child_pid, SIGTRAP);
printf("PARENT: Clear the breakpoint\n");
clear_breakpoint(child_pid, &function_to_break_on, original_memory);
/* PART 2: Setting watchpoints */
printf("PARENT: Set a watchpoint on variable_to_watch\n");
set_watchpoint(child_pid, (void*)&variable_to_watch, WP_LOCAL,
WP_WRITE, WP_1);
printf("PARENT: Make the child continue and wait for it\n");
ptrace_checked("PARENT", PTRACE_CONT, child_pid, NULL, NULL);
/* Note that we should probably check that we stopped because of the
* watchpoint and not for some other reason. */
wait_for_signal(child_pid, SIGTRAP);
printf("PARENT: Clear the watchpoint\n");
clear_watchpoint(child_pid);
printf("PARENT: Make the child continue\n");
ptrace_checked("PARENT", PTRACE_CONT, child_pid, NULL, NULL);
printf("PARENT: Waiting for child\n");
wait_for_exit(child_pid, 0);
printf("PARENT: Exiting\n");
exit(0);
}
int main()
{
pid_t child_pid = fork();
if (child_pid == 0)
{
/* CHILD PROCESS */
execute_child();
}
else if (child_pid == -1)
{
perror("PARENT: fork");
exit(1);
}
else
{
/* PARENT PROCESS */
execute_parent(child_pid);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment