是否可以以编程方式设置 gdb 观察点?

Posted

技术标签:

【中文标题】是否可以以编程方式设置 gdb 观察点?【英文标题】:Is it possible to set a gdb watchpoint programmatically? 【发布时间】:2012-01-20 12:48:06 【问题描述】:

我想在我的 C++ 程序中临时设置一个观察点(硬件写入中断)以查找内存损坏。

我已经看到了通过 gdb 手动执行此操作的所有方法,但我想通过代码中的某些方法实际设置观察点,这样我就不必闯入 gdb,找出地址,设置观察点,然后继续。

类似:

#define SET_WATCHPOINT(addr) asm ("set break on hardware write %addr")

【问题讨论】:

【参考方案1】:

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

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

【讨论】:

谢谢,这对于调试错误的内存写入非常有用。问题, sleep(1) 语句的目的是什么?没有它,它就不起作用,但由于我将有一个循环反复设置和清除观察点,我不想等那么久。另外,是否可以在没有子进程的情况下设置观察点?我尝试简单地将 ptrace 调用移动到父进程,但它们失败了? @YaleZhang 睡眠语句正在等待父进程运行,以便将其状态更改为已跟踪,Will Hawkins 的回答非常有帮助。父进程也可以像 GDB 一样这样做,失败可能其他原因造成的。 只是想在多线程程序中添加,ptrace需要使用tid而不是pid。我发现需要为所有可能损坏内存的线程设置 DR7。【参考方案2】:

根据 user512106 的出色回答,我编写了一个小“库”,可能有人会觉得有用:

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

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

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

我希望这对某人有所帮助!

【讨论】:

我想用这个,但是BREAK_ONEBREAK_TWOBREAK_FOURBREAK_EIGHT在哪里使用?我假设他们控制被监视的内存大小? 这是一个非常好的问题!老实说,我不记得了——看起来它们可能只是无关紧要的。我在 github 上有这个代码的更新版本(在上面发布的 URL),这可能是你想要使用的!【参考方案3】:

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

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

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

编辑:

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

对于硬件断点,this article 提供了一些技术:

我们想观察在地址 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);

设置和处理观察点。

【讨论】:

来自您的链接:硬件断点有时可用作某些芯片的内置调试功能。通常,这些工作通过具有可以存储断点地址的专用寄存器来工作。 @Neil 是的,我的意思是它是如何实现的;我已经更新了答案 GDB 可能不支持它,但 x86 肯定支持。事实上,您链接到的 gdb 文件显示了如何做到这一点!请参阅函数 i386_insert_aligned_watchpoint。它似乎有效地设置了 DR7 寄存器,但我认为这是一条特权指令,所以我不能在非内核模式下使用它。 @neil 是啊,我没看对地方,这就是为什么我(暂时)回滚了我的答案,我会在我更好地理解它后尽快完成它!跨度> 【参考方案4】:

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

    在您要监视的变量所在的范围内设置断点,在您需要开始监视变量之前将其命中, 右键单击断点并选择Edit Breakpoint..., 点击 Add Action 并添加一个 Debugger Command 与 LLDB 命令,例如:watchpoint set variable &lt;variablename&gt;(或者如果您使用的是 GDB1,类似的命令:watch &lt;variablename&gt;), 选中评估操作后自动继续复选框。

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

【讨论】:

如果我在 Windows 上编程,从 90 年代中期我就能做到这一点。仍在等待 Linux 赶上来!【参考方案5】:

程序本身可以向 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 版本可能拒绝从非终端读取命令)

【讨论】:

以上是关于是否可以以编程方式设置 gdb 观察点?的主要内容,如果未能解决你的问题,请参考以下文章

是否可以以编程方式设置应用程序的 DPI?

是否可以以编程方式设置 Windows 服务的用户帐户?

以编程方式启动 Mac 的系统偏好设置屏幕 -> 声音

在 iOS 中以编程方式将用户带到后台刷新设置

Joomla 3以编程方式为组件设置默认模板

在 iOS 中以编程方式禁用设置警报