22

I want to set a watchpoint (break on hardware write) temporarily in my C++ program to find memory corruption.

I've seen all the ways to do it manually through gdb, but I would like to actually set the watchpoint via some method in my code so I don't have to break into gdb, find out the address, set the watchpoint and then continue.

Something like:

#define SET_WATCHPOINT(addr) asm ("set break on hardware write %addr")
Cœur
  • 37,241
  • 25
  • 195
  • 267
Neil
  • 11,059
  • 3
  • 31
  • 56

5 Answers5

17

Set hardware watchpoint from child process.

#include <signal.h>
#include <syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <linux/user.h>

enum {
    DR7_BREAK_ON_EXEC  = 0,
    DR7_BREAK_ON_WRITE = 1,
    DR7_BREAK_ON_RW    = 3,
};

enum {
    DR7_LEN_1 = 0,
    DR7_LEN_2 = 1,
    DR7_LEN_4 = 3,
};

typedef struct {
    char l0:1;
    char g0:1;
    char l1:1;
    char g1:1;
    char l2:1;
    char g2:1;
    char l3:1;
    char g3:1;
    char le:1;
    char ge:1;
    char pad1:3;
    char gd:1;
    char pad2:2;
    char rw0:2;
    char len0:2;
    char rw1:2;
    char len1:2;
    char rw2:2;
    char len2:2;
    char rw3:2;
    char len3:2;
} dr7_t;

typedef void sighandler_t(int, siginfo_t*, void*);

int watchpoint(void* addr, sighandler_t handler)
{
    pid_t child;
    pid_t parent = getpid();
    struct sigaction trap_action;
    int child_stat = 0;

    sigaction(SIGTRAP, NULL, &trap_action);
    trap_action.sa_sigaction = handler;
    trap_action.sa_flags = SA_SIGINFO | SA_RESTART | SA_NODEFER;
    sigaction(SIGTRAP, &trap_action, NULL);

    if ((child = fork()) == 0)
    {
        int retval = EXIT_SUCCESS;

        dr7_t dr7 = {0};
        dr7.l0 = 1;
        dr7.rw0 = DR7_BREAK_ON_WRITE;
        dr7.len0 = DR7_LEN_4;

        if (ptrace(PTRACE_ATTACH, parent, NULL, NULL))
        {
            exit(EXIT_FAILURE);
        }

        sleep(1);

        if (ptrace(PTRACE_POKEUSER, parent, offsetof(struct user, u_debugreg[0]), addr))
        {
            retval = EXIT_FAILURE;
        }

        if (ptrace(PTRACE_POKEUSER, parent, offsetof(struct user, u_debugreg[7]), dr7))
        {
            retval = EXIT_FAILURE;
        }

        if (ptrace(PTRACE_DETACH, parent, NULL, NULL))
        {
            retval = EXIT_FAILURE;
        }

        exit(retval);
    }

    waitpid(child, &child_stat, 0);
    if (WEXITSTATUS(child_stat))
    {
        printf("child exit !0\n");
        return 1;
    }

    return 0;
}

int var;

void trap(int sig, siginfo_t* info, void* context)
{
    printf("new value: %d\n", var);
}

int main(int argc, char * argv[])
{
    int i;

    printf("init value: %d\n", var);

    watchpoint(&var, trap);

    for (i = 0; i < 100; i++) {
        var++;
        sleep(1);
    }

    return 0;
}
tinytaro
  • 380
  • 2
  • 8
  • 2
    Thanks, this will be very useful for debugging bad memory writes. Question, what is the purpose of the sleep(1) statement? Without it, it doesn't work, but since I will have a loop that repeatedly sets and clears watch points, I don't want to wait that long. Also, is it possible to set the watchpoint without a child process? I tried simply moving the ptrace calls to the parent process, but they fail? – Yale Zhang Aug 30 '13 at 22:12
  • @YaleZhang the sleep statement is waiting for parent process to run so that its status can be changed to traced, Will Hawkins' answer is very helpful.Parent process can also do this like GDB, the failure may caused by other reasons. – tinytaro Mar 23 '18 at 04:08
  • 1
    Just want to add that in a multi-threaded program, ptrace needs to use tid and not pid. I found that DR7 needs to be set for all threads that can possibly corrupt the memory. – NeilB Oct 26 '18 at 08:47
  • 1
    how to print the backtrace when the watchpoint is hit? gdb could automatically print it – eric Aug 15 '22 at 13:35
  • @eric Did you ever find the answer? – intrigued_66 Jan 22 '23 at 17:51
  • @intrigued_66 I think GDB's Breakpoint Command Lists could help – eric Jan 28 '23 at 08:55
6

Based on user512106's great answer, I coded up a little "library" that someone might find useful:

It's on github at https://github.com/whh8b/hwbp_lib. I wish I could have commented directly on his answer, but I don't have enough rep yet.

Based on feedback from the community, I am going to copy/paste the relevant code here:

#include <stdio.h>
#include <stddef.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <sys/prctl.h>
#include <stdint.h>
#include <errno.h>
#include <stdbool.h>

extern int errno;

enum {
    BREAK_EXEC = 0x0,
    BREAK_WRITE = 0x1,
    BREAK_READWRITE = 0x3,
};

enum {
    BREAK_ONE = 0x0,
    BREAK_TWO = 0x1,
    BREAK_FOUR = 0x3,
    BREAK_EIGHT = 0x2,
};

#define ENABLE_BREAKPOINT(x) (0x1<<(x*2))
#define ENABLE_BREAK_EXEC(x) (BREAK_EXEC<<(16+(x*4)))
#define ENABLE_BREAK_WRITE(x) (BREAK_WRITE<<(16+(x*4)))
#define ENABLE_BREAK_READWRITE(x) (BREAK_READWRITE<<(16+(x*4)))

/*
 * This function fork()s a child that will use
 * ptrace to set a hardware breakpoint for 
 * memory r/w at _addr_. When the breakpoint is
 * hit, then _handler_ is invoked in a signal-
 * handling context.
 */
bool install_breakpoint(void *addr, int bpno, void (*handler)(int)) {
    pid_t child = 0;
    uint32_t enable_breakpoint = ENABLE_BREAKPOINT(bpno);
    uint32_t enable_breakwrite = ENABLE_BREAK_WRITE(bpno);
    pid_t parent = getpid();
    int child_status = 0;

    if (!(child = fork()))
    {
        int parent_status = 0;
        if (ptrace(PTRACE_ATTACH, parent, NULL, NULL))
            _exit(1);

        while (!WIFSTOPPED(parent_status))
            waitpid(parent, &parent_status, 0);

        /*
         * set the breakpoint address.
         */
        if (ptrace(PTRACE_POKEUSER,
                   parent,
                   offsetof(struct user, u_debugreg[bpno]),
                   addr))
            _exit(1);

        /*
         * set parameters for when the breakpoint should be triggered.
         */
        if (ptrace(PTRACE_POKEUSER,
                   parent,
                   offsetof(struct user, u_debugreg[7]),
                   enable_breakwrite | enable_breakpoint))
            _exit(1);

        if (ptrace(PTRACE_DETACH, parent, NULL, NULL))
            _exit(1);

        _exit(0);
    }

    waitpid(child, &child_status, 0);

    signal(SIGTRAP, handler);

    if (WIFEXITED(child_status) && !WEXITSTATUS(child_status))
        return true;
    return false;
}

/*
 * This function will disable a breakpoint by
 * invoking install_breakpoint is a 0x0 _addr_
 * and no handler function. See comments above
 * for implementation details.
 */
bool disable_breakpoint(int bpno) 
{
    return install_breakpoint(0x0, bpno, NULL);
}

/*
 * Example of how to use this /library/.
 */
int handled = 0;

void handle(int s) {
    handled = 1;
    return;
}

int main(int argc, char **argv) {
    int a = 0;

    if (!install_breakpoint(&a, 0, handle))
        printf("failed to set the breakpoint!\n");

    a = 1;
    printf("handled: %d\n", handled);

    if (!disable_breakpoint(0))
        printf("failed to disable the breakpoint!\n");

    return 1;
}

I hope that this helps someone!

Will

Will Hawkins
  • 111
  • 1
  • 4
  • I wanted to use this, but where do `BREAK_ONE`, `BREAK_TWO`, `BREAK_FOUR`, and `BREAK_EIGHT` get used? I assume they control the size of memory being watched? – ulatekh Oct 05 '21 at 19:12
  • That's a very good question! I honestly don't remember -- looks like they are probably just extraneous. I have an updated version of this code on github (at the URL posted above) that will probably be what you want to use! – Will Hawkins Oct 09 '21 at 18:35
4

In GDB, there are two types of watchpoints, hardware and software.

  • you can't implement easily software watchpoints: (cf. GDB Internals)

Software watchpoints are very slow, since gdb needs to single-step the program being debugged and test the value of the watched expression(s) after each instruction.

EDIT:

I'm still trying to understand what are hardware watchpoint.

  • for hardware breakpoints, this article gives some technics:

We want to watch reading from or writing into 1 qword at address 100005120h (address range 100005120h-100005127h)

 lea rax, [100005120h]
 mov dr0, rax
 mov rax, dr7
 and eax, not ((1111b shl 16) + 11b)    ; mask off all
 or eax, (1011b shl 16) + 1     ; prepare to set what we want
 mov 
 dr7, rax               ; set it finally

Done, now we can wait until code falls into the trap! After accessing any byte at memory range 100005120h-100005127h, int 1 will occur and DR6.B0 bit will be set to 1.

You can also take a look at GDB low-end files (eg, amd64-linux-nat.c) but it (certainly) involves 2 processes: 1/ the one you want to watch 2/a lightweight debugger who attaches to the first one with ptrace, and uses:

ptrace (PTRACE_POKEUSER, tid, __regnum__offset__, address);

to set and handle the watchpoint.

Kevin
  • 4,618
  • 3
  • 38
  • 61
  • From your link: **Hardware breakpoints are sometimes available as a builtin debugging features with some chips. Typically these work by having dedicated register into which the breakpoint address may be stored.** – Neil Jan 20 '12 at 13:01
  • @Neil yes, I meant how it was implemented; I've updated the answer – Kevin Jan 20 '12 at 13:07
  • GDB may not support it, but x86 certainly does. Infact the gdb file you link to shows how to do it ! See the function **i386_insert_aligned_watchpoint**. It seems to effectively set the DR7 register, but I presume that is a privileged instruction so I can't use it from non-kernel mode. – Neil Jan 20 '12 at 13:26
  • @neil yeah, I didn't look at the right place, that's why I've (temporarily) rolled back my answer, I'll complete it as soon as I understand it better! – Kevin Jan 20 '12 at 13:32
2

The program itself can supply commands to the GDB. You'll need a special shell script to run GDB though.

Copy this code into the file named untee, and execute chmod 755 untee

#!/bin/bash

if [ -z "$1" ]; then
    echo "Usage: $0 PIPE | COMMAND"
    echo "This script will read the input from both stdin and PIPE, and supply it to the COMMAND."
    echo "If PIPE does not exist it will be created with mkfifo command."
    exit 0
fi

PIPE="$1"

if [ \! -e "$PIPE" ]; then
    mkfifo "$PIPE"
fi

if [ \! -p "$PIPE" ]; then
    echo "File $PIPE does not exist or is not a named pipe" > /dev/stderr
    exit 1
fi

# Open the pipe as a FD 3
echo "Waiting for $PIPE to be opened by another process" > /dev/stderr
exec 3<"$PIPE"
echo "$PIPE opened" > /dev/stderr
OPENED=true

while true; do
    read -t 1 INPUT
    RET=$?
    if [ "$RET" = 0 ]; then
        echo "$INPUT"
    elif [ "$RET" -lt 128 ]; then
        echo "stdin closed, exiting" > /dev/stderr
        break
    fi

    if $OPENED; then
        while read -t 1 -u 3 INPUT; do
            RET=$?
            if [ "$RET" = 0 ]; then
                echo "$INPUT"
            else
                if [ "$RET" -lt 128 ]; then
                    echo "$PIPE closed, ignoring" > /dev/stderr
                    OPENED=false
                fi
                break
            fi
        done
    fi
done

And now the C code:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <unistd.h>

void gdbCommand(const char *c)
{
    static FILE * dbgpipe = NULL;
    static const char * dbgpath = "/tmp/dbgpipe";
    struct stat st;

    if( !dbgpipe && stat(dbgpath, &st) == 0 && S_ISFIFO(st.st_mode) )
            dbgpipe = fopen(dbgpath, "w");
    if( !dbgpipe )
        return;
    fprintf(dbgpipe, "%s\n", c);
    fflush(dbgpipe);
}

void gdbSetWatchpoint(const char *var)
{
    char buf[256];
    snprintf(buf, sizeof(buf), "watch %s", var);

    gdbCommand("up"); /* Go up the stack from the kill() system call - this may vary by the OS, you may need to walk the stack more times */
    gdbCommand("up"); /* Go up the stack from the gdbSetWatchpoint() function */
    gdbCommand(buf);
    gdbCommand("continue");
    kill(getpid(), SIGINT); /* Make GDB pause our process and execute commands */
}

int subfunc(int *v)
{
    *v += 5; /* GDB should pause after this line, and let you explore stack etc */
    return v;
}

int func()
{
    int i = 10;
    printf("Adding GDB watch for var 'i'\n");
    gdbSetWatchpoint("i");

    subfunc(&i);
    return i;
}

int func2()
{
    int j = 20;
    return j + func();
}


int main(int argc, char ** argv)
{
    func();
    func2();
    return 0;
}

Copy that to the file named test.c, compile with command gcc test.c -O0 -g -o test then execute ./untee /tmp/dbgpipe | gdb -ex "run" ./test

This works on my 64-bit Ubuntu, with GDB 7.3 (older GDB versions might refuse to read commands from non-terminal)

pelya
  • 4,326
  • 2
  • 24
  • 23
1

If you happen to be using Xcode, you can achieve the required effect (automatic setting of watchpoints) by using an action on another breakpoint to set your watchpoint:

  1. Set up a breakpoint somewhere where the variable you want to watch will be in scope that will be hit before you need to start watching the variable,
  2. Right-click on the breakpoint and select Edit Breakpoint...,
  3. Click on Add Action and add a Debugger Command with an LLDB command like: watchpoint set variable <variablename> (or if you're using GDB1, a command like: watch <variablename>),
  4. Check the Automatically continue after evaluating actions checkbox.

enter image description here

1: GDB is no longer supported in more recent versions of Xcode, but I believe it is still possible to set it up manually.

Rich
  • 7,348
  • 4
  • 34
  • 54
  • 1
    If I was programming on Windows, I would have been able to do this since the mid 90s. Still waiting for Linux to catch up ! – Neil Jun 25 '15 at 14:48