【问题标题】:How to remote-control GDB on Linux如何在 Linux 上远程控制 GDB
【发布时间】:2017-11-22 03:50:11
【问题描述】:

我必须实现这样的东西:

  1. GDB下启动一个程序(例如a.out
  2. 设置一些断点
  3. 定期向GDB发送CTRL-C信号以暂停a.out的执行。
  4. 在停止点或断点处执行一些命令,例如“bt, info threads”
  5. 继续执行a.out
  6. 直到a.out 结束

这些步骤可以在 shell 下以交互方式执行,但我需要在程序中自动执行。我正在考虑使用 fork 为 GDB 创建一个子进程并使用popen 执行初始 GDB 启动命令,但我怎样才能定期发送这些 GDB 子命令(bt,继续)到该子进程并让它执行它们?

我被困在这一点上,任何想法都将不胜感激。提前谢谢。

【问题讨论】:

  • 实际上想解决什么问题?
  • 您通常会用管道将子进程的 stdin/stdout/stderr 文件描述符替换回控制进程。我同意@EmployedRussian 这听起来像是一个 X/Y 问题。
  • 这是一次性项目吗?如果是这样,请尝试expectpexpect,因为它很容易快速搞定。从长远来看,您可能希望使用gdb mi,它比 gdb 的命令行界面更不容易更改。
  • @EmployedRussian 我需要对程序的执行进行采样并在每个采样点获取堆栈跟踪。我知道您可能会建议使用 PAPI 进行采样,但就我而言,我现在必须依赖 gdb。因此,我需要定期暂停 gdb 并在此时获取回溯。
  • @BruceTerp -- “我需要……”但是为什么xyproblem.info

标签: c linux process gdb


【解决方案1】:

这是一个非常简单的实现。它在没有管道的情况下分叉目标进程,我们只需要知道它是pid。然后它使用-p <PID> 选项分叉gdb 以附加到我们的目标。 GDB的fork在执行之前为stdin/stdout/stderr设置了管道,这样我们就可以远程控制GDB了。

一些有趣的笔记:

  1. 当 GDB 运行调试目标时,它不会响应 SIGINT。您必须将SIGINT 发送到调试目标。这就是为什么我分叉两次而不是启动gdb --args <target>。我需要它正在调试的进程的 PID,以便我可以发送SIGINT
  2. 当您将管道附加到进程'stdoutstderr 时,您必须读取它们,否则目标进程最终会阻塞(当它们填满管道的缓冲区时)。我的实现在这里很愚蠢,因为我不想花时间使用线程或进行正确的select 调用。
  3. 您必须注意 API 何时会阻塞。请注意,我使用的是read/write 而不是fread,fwrite,因为他们无法读取我请求的金额时的行为。

“追踪器”程序是:

#include <stdio.h>
#include <string.h>

#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/select.h>

char gdb_pid_buf[20];

char *gdb_argv[] =
{
  "gdb",
  "-p",
  gdb_pid_buf,
  NULL
};

char *child_argv[] =
{
  "./looper",
  NULL
};

const char GDB_PROMPT[] = "(gdb)";

int wait_for_prompt(const char *prefix, int fd)
{
  char readbuf[4096];
  size_t used = 0;
  while(1)
  {
    ssize_t amt;
    char *prompt;
    char *end;

    amt = read(fd, readbuf+used, sizeof(readbuf)-used-1);
    if(amt == -1)
    {
      return 1;
    }
    else if(amt == 0)
    {  }
    else
    {
      used += amt;

      readbuf[used] = '\0';
      for(end = strstr(readbuf, "\n"); end; end= strstr(readbuf, "\n"))
      {
        size_t consumed;
        size_t remaining;

        *end = '\0';
        printf("%s: %s\n", prefix, readbuf);

        consumed = (end-readbuf) + strlen("\n");
        remaining = used - consumed;
        memmove(readbuf, readbuf+consumed, remaining);
        used -= consumed;
      }

      prompt = strstr(readbuf, GDB_PROMPT);
      if(prompt)
      {
        *prompt = '\0';
        printf("%s: %s", prefix, readbuf);
        printf("[PROMPT]\n");
        fflush(stdout);
        break;
      }
    }
  }
  return 0;
}

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

  int stdin_pipe[2];
  int stdout_pipe[2];
  int stderr_pipe[2];

  pipe(stdin_pipe);
  pipe(stdout_pipe);
  pipe(stderr_pipe);

  int gdb_pid;
  int child_pid;

  //Launch child
  child_pid = fork();
  if(child_pid == 0)
  {
    close(stdin_pipe[0]);
    close(stdout_pipe[0]);
    close(stderr_pipe[0]);
    close(stdin_pipe[1]);
    close(stdout_pipe[1]);
    close(stderr_pipe[1]);

    execvp(child_argv[0], child_argv);
    return 0;
  }

  sprintf(gdb_pid_buf, "%d", child_pid);

  //Launch gdb with command-line args to attach to child.
  gdb_pid = fork();
  if(gdb_pid == 0)
  {
    close(stdin_pipe[1]);
    close(stdout_pipe[0]);
    close(stderr_pipe[0]);

    dup2(stdin_pipe[0],0);
    dup2(stdout_pipe[1],1);
    dup2(stderr_pipe[1],2);

    execvp(gdb_argv[0], gdb_argv);
    return 0;
  }

  close(stdin_pipe[0]);
  close(stdout_pipe[1]);
  close(stderr_pipe[1]);

  //Wait for GDB to reach its prompt
  if(wait_for_prompt("GDB", stdout_pipe[0]))
    {fprintf(stderr,"child died\n");return 1;}

  printf("[SENDING \"continue\\n\"]\n");
  fflush(stdout);
  write(stdin_pipe[1], "continue\n", strlen("continue\n"));

  sleep(4);

  printf("[SENDING \"CTRL+C\"]\n");
  fflush(stdout);
  kill(child_pid, SIGINT);

  //Then read through all the output until we reach a prompt.
  if(wait_for_prompt("POST SIGINT", stdout_pipe[0]))
    {fprintf(stderr,"child died\n");return 1;}

  //Ask for the stack trace
  printf("[SENDING \"where\\n\"]\n");
  fflush(stdout);
  write(stdin_pipe[1], "where\n", strlen("where\n"));

  //read through the stack trace output until the next prompt
  if(wait_for_prompt("TRACE", stdout_pipe[0]))
    {fprintf(stderr,"child died\n");return 1;}

  kill(child_pid, SIGKILL);
  kill(gdb_pid, SIGKILL);
}

目标程序looper就是:

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
  while(1)
  {
    printf(".");
    fflush(stdout);
    sleep(1);
  }
}

示例输出为:

$ ./a.out
.GDB: GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-80.el7
GDB: Copyright (C) 2013 Free Software Foundation, Inc.
GDB: License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
GDB: This is free software: you are free to change and redistribute it.
GDB: There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
GDB: and "show warranty" for details.
GDB: This GDB was configured as "x86_64-redhat-linux-gnu".
GDB: For bug reporting instructions, please see:
GDB: <http://www.gnu.org/software/gdb/bugs/>.
GDB: Attaching to process 8057
GDB: Reading symbols from /home/<nope>/temp/remotecontrol/looper...(no debugging symbols found)...done.
GDB: Reading symbols from /lib64/libc.so.6...(no debugging symbols     found)...done.
GDB: Loaded symbols for /lib64/libc.so.6
GDB: Reading symbols from /lib64/ld-linux-x86-64.so.2...(no debugging symbols found)...done.
GDB: Loaded symbols for /lib64/ld-linux-x86-64.so.2
GDB: 0x00007f681b4f9480 in __nanosleep_nocancel () from /lib64/libc.so.6
GDB: Missing separate debuginfos, use: debuginfo-install glibc-2.17-    106.el7_2.4.x86_64
GDB: [PROMPT]
[SENDING "continue\n"]
....[SENDING "CTRL+C"]
POST SIGINT: Continuing.
POST SIGINT:
POST SIGINT: Program received signal SIGINT, Interrupt.
POST SIGINT: 0x00007f681b4f9480 in __nanosleep_nocancel () from /lib64/libc.so.6
POST SIGINT: [PROMPT]
[SENDING "where\n"]
TRACE: #0  0x00007f681b4f9480 in __nanosleep_nocancel () from /lib64/libc.so.6
TRACE: #1  0x00007f681b4f9334 in sleep () from /lib64/libc.so.6
TRACE: #2  0x0000000000400642 in main ()
TRACE: [PROMPT]

您可以从.... 看到目标确实继续运行,即使 GDB 的输出为“Continuing”。直到后来我读到它的标准输出管道时才出现。

【讨论】:

  • 如果这个答案是正确的,我建议将标题更改为“如何在 Linux 上远程控制 GDB”之类的内容,因为惊喜主要与它是 GDB 有关。
  • 非常感谢!这正是我正在寻找的,虽然我花了一段时间才完全理解你的代码,因为我不熟悉 Unix/系统编程。两个问题:[1] 在你的第二个笔记中,你说你的方法很愚蠢,但是当你逐行阅读时,我觉得这很简单,还有什么更有效的方法呢? [2] 目前,在我修改你的代码后,定期查看堆栈直到孩子结束,开销有点高,对于一个复杂的程序,它最多需要孩子原始执行的5倍,有什么改进建议吗?谢谢!
  • 至于第二个注释,它最终在 readbuf 中做了很多额外的数据复制。现在我想起来了,使用strrstr 从 readbuf 的末尾向后扫描会更有效率。您也可以通过不将 GDB 的输出打印到屏幕上来大大加快它的速度(假设您还没有消除 printfs)。我认为这种方法对性能的影响可能是内在的; dwarf 调试信息被压缩,因此必须对其进行解析和解包以解释不同位置的堆栈是昂贵的。
  • 我已经尝试了几种方法,这就是我发现的:[1] 注释掉所有 printf 并没有给我带来明显的加速 [2] 省略 where 只会将运行时间从 5 倍减少到 4 倍[3] 每次都简单地打印出 readbuf 而不在 for 循环中逐行解析只会给我带来微不足道的加速。所以我的问题是:1.这是否意味着主要的性能瓶颈是 int-cont 操作,正如您提到的内在性能影响。 2. 当我确实需要按顺序输出所有信息时,如何按照您的建议更有效地使用 strrstr?谢谢!
  • strrstr 这件事只会让您一步找到缓冲区中的最后一个'\n'。您仍然可以处理readbuf 开头和最后一个换行符之间的文本,但是您只需执行一个memmove 即可使缓冲区为下一次读取做好准备。我还认为您可以在 wait_for_prompt 方法中添加一个标志,以指示您是否希望它写入输出。这样,您可以省去发送SIGINT 和提示之间发生的I/O。我认为您也可以在打印"[PROMPT]" 后摆脱所有fflush()。这可能会节省一点 I/O 时间。
猜你喜欢
  • 2017-08-30
  • 2011-05-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-19
  • 2021-05-25
  • 2012-01-03
  • 2014-06-16
相关资源
最近更新 更多