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")
4

5 回答 5

16

从子进程设置硬件观察点。

#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;
}
于 2012-08-03T02:34:20.947 回答
5

基于 user512106's great answer,我编写了一个小“库”,有人可能会觉得有用:

它位于https://github.com/whh8b/hwbp_lib的 github 上。我希望我可以直接评论他的回答,但我还没有足够的代表。

根据社区的反馈,我将在此处复制/粘贴相关代码:

#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;
}

我希望这对某人有帮助!

将要

于 2017-11-05T04:58:13.443 回答
4

在 GDB 中,有两种类型的观察点,硬件和软件。

  • 您无法轻松实现软件观察点:(参见GDB Internals

软件观察点非常慢,因为 gdb 需要单步调试正在调试的程序并在每条指令之后测试观察表达式的值。

编辑:

我仍在尝试了解什么是硬件观察点。

  • 对于硬件断点,本文给出了一些技术:

我们想在地址 100005120h(地址范围 100005120h-100005127h)处观察读取或写入 1 个 qword

 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

完成,现在我们可以等到代码落入陷阱!访问内存范围 100005120h-100005127h 中的任何字节后,将发生 int 1 并且 DR6.B0 位将设置为 1。

您还可以查看 GDB 低端文件(例如amd64-linux-nat.c),但它(当然)涉及 2 个进程:1/您要观看的进程 2/一个轻量级调试器,它使用ptrace附加到第一个进程,并使用:

ptrace (PTRACE_POKEUSER, tid, __regnum__offset__, address);

设置和处理观察点。

于 2012-01-20T12:59:01.910 回答
1

如果你碰巧在使用 Xcode,你可以通过在另一个断点上使用一个动作来设置你的观察点来达到所需的效果(自动设置观察点):

  1. 在您要监视的变量所在的范围内设置断点,在您需要开始监视变量之前将被命中,
  2. 右键单击断点并选择Edit Breakpoint...
  3. 单击添加操作并添加一个带有 LLDB 命令的调试器命令,例如:(或者watchpoint set variable <variablename>,如果您使用的是 GDB 1,则使用类似的命令:) watch <variablename>
  4. 选中评估操作后自动继续复选框。

在此处输入图像描述

1:Xcode 的较新版本不再支持 GDB,但我相信仍然可以手动设置它。

于 2015-06-24T12:35:24.077 回答
0

程序本身可以向 GDB 提供命令。不过,您需要一个特殊的 shell 脚本来运行 GDB。

将此代码复制到名为untee的文件中,然后执行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

现在是 C 代码:

#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;
}

将其复制到名为test.c的文件中,使用命令gcc test.c -O0 -g -o test编译,然后执行./untee /tmp/dbgpipe | gdb -ex "运行" ./test

这适用于我的 64 位 Ubuntu,带有 GDB 7.3(旧的 GDB 版本可能拒绝从非终端读取命令)

于 2012-01-20T16:38:58.387 回答