标签 调试 下的文章

ptrace(2)(“ 进程跟踪 process trace ”)系统调用通常都与调试有关。它是类 Unix 系统上通过原生调试器监测被调试进程的主要机制。它也是实现 strace 系统调用跟踪 system call trace )的常见方法。使用 Ptrace,跟踪器可以暂停被跟踪进程,检查和设置寄存器和内存,监视系统调用,甚至可以 拦截 intercepting 系统调用。

通过拦截功能,意味着跟踪器可以篡改系统调用参数,篡改系统调用的返回值,甚至阻塞某些系统调用。言外之意就是,一个跟踪器本身完全可以提供系统调用服务。这是件非常有趣的事,因为这意味着一个跟踪器可以仿真一个完整的外部操作系统,而这些都是在没有得到内核任何帮助的情况下由 Ptrace 实现的。

问题是,在同一时间一个进程只能被一个跟踪器附着,因此在那个进程的调试期间,不可能再使用诸如 GDB 这样的工具去仿真一个外部操作系统。另外的问题是,仿真系统调用的开销非常高。

在本文中,我们将专注于 x86-64 Linux 的 Ptrace,并将使用一些 Linux 专用的扩展。同时,在本文中,我们将忽略掉一些错误检查,但是完整的源代码仍然会包含这些错误检查。

本文中的可直接运行的示例代码在这里:https://github.com/skeeto/ptrace-examples

strace

在进入到最有趣的部分之前,我们先从回顾 strace 的基本实现来开始。它不是 DTrace,但 strace 仍然非常有用。

Ptrace 一直没有被标准化。它的接口在不同的操作系统上非常类似,尤其是在核心功能方面,但是在不同的系统之间仍然存在细微的差别。ptrace(2) 的原型基本上应该像下面这样,但特定的类型可能有些差别。

long ptrace(int request, pid_t pid, void *addr, void *data);

pid 是被跟踪进程的 ID。虽然同一个时间只有一个跟踪器可以附着到该进程上,但是一个跟踪器可以附着跟踪多个进程。

request 字段选择一个具体的 Ptrace 函数,比如 ioctl(2) 接口。对于 strace,只需要两个:

  • PTRACE_TRACEME:这个进程被它的父进程跟踪。
  • PTRACE_SYSCALL:继续跟踪,但是在下一下系统调用入口或出口时停止。
  • PTRACE_GETREGS:取得被跟踪进程的寄存器内容副本。

另外两个字段,addrdata,作为所选的 Ptrace 函数的一般参数。一般情况下,可以忽略一个或全部忽略,在那种情况下,传递零个参数。

strace 接口实质上是前缀到另一个命令之前。

$ strace [strace options] program [arguments]

最小化的 strace 不需要任何选项,因此需要做的第一件事情是 —— 假设它至少有一个参数 —— 在 argv 尾部的 fork(2)exec(2) 被跟踪进程。但是在加载目标程序之前,新的进程将告知内核,目标程序将被它的父进程继续跟踪。被跟踪进程将被这个 Ptrace 系统调用暂停。

pid_t pid = fork();
switch (pid) {
    case -1: /* error */
        FATAL("%s", strerror(errno));
    case 0:  /* child */
        ptrace(PTRACE_TRACEME, 0, 0, 0);
        execvp(argv[1], argv + 1);
        FATAL("%s", strerror(errno));
}

父进程使用 wait(2) 等待子进程的 PTRACE_TRACEME,当 wait(2) 返回后,子进程将被暂停。

waitpid(pid, 0, 0);

在允许子进程继续运行之前,我们告诉操作系统,被跟踪进程和它的父进程应该一同被终止。一个真实的 strace 实现可能会设置其它的选择,比如: PTRACE_O_TRACEFORK

ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_EXITKILL);

剩余部分就是一个简单的、无休止的循环了,每循环一次捕获一个系统调用。循环体总共有四步:

  1. 等待进程进入下一个系统调用。
  2. 输出系统调用的一个描述。
  3. 允许系统调用去运行并等待返回。
  4. 输出系统调用返回值。

这个 PTRACE_SYSCALL 请求被用于等待下一个系统调用时开始,和等待那个系统调用退出。和前面一样,需要一个 wait(2) 去等待被跟踪进程进入期望的状态。

ptrace(PTRACE_SYSCALL, pid, 0, 0);
waitpid(pid, 0, 0);

wait(2) 返回时,进行了系统调用的线程的寄存器中写入了该系统调用的系统调用号及其参数。尽管如此,操作系统仍然没有为这个系统调用提供服务。这个细节对后续操作很重要。

接下来的一步是采集系统调用信息。这是各个系统架构不同的地方。在 x86-64 上,系统调用号是在 rax 中传递的,而参数(最多 6 个)是在 rdirsirdxr10r8r9 中传递的。这些寄存器是由另外的 Ptrace 调用读取的,不过这里再也不需要 wait(2) 了,因为被跟踪进程的状态再也不会发生变化了。

struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, pid, 0, &regs);
long syscall = regs.orig_rax;

fprintf(stderr, "%ld(%ld, %ld, %ld, %ld, %ld, %ld)",
        syscall,
        (long)regs.rdi, (long)regs.rsi, (long)regs.rdx,
        (long)regs.r10, (long)regs.r8,  (long)regs.r9);

这里有一个警告。由于 内核的内部用途,系统调用号是保存在 orig_rax 中而不是 rax 中。而所有的其它系统调用参数都是非常简单明了的。

接下来是它的另一个 PTRACE_SYSCALLwait(2),然后是另一个 PTRACE_GETREGS 去获取结果。结果保存在 rax 中。

ptrace(PTRACE_GETREGS, pid, 0, &regs);
fprintf(stderr, " = %ld\n", (long)regs.rax);

这个简单程序的输出也是非常粗糙的。这里的系统调用都没有符号名,并且所有的参数都是以数字形式输出,甚至是一个指向缓冲区的指针也是如此。更完整的 strace 输出将能知道哪个参数是指针,并使用 process_vm_readv(2) 从被跟踪进程中读取哪些缓冲区,以便正确输出它们。

然而,这些仅仅是系统调用拦截的基础工作。

系统调用拦截

假设我们想使用 Ptrace 去实现如 OpenBSD 的 pledge(2) 这样的功能,它是 一个进程 承诺 pledge 只使用一套受限的系统调用。初步想法是,许多程序一般都有一个初始化阶段,这个阶段它们都需要进行许多的系统访问(比如,打开文件、绑定套接字、等等)。初始化完成以后,它们进行一个主循环,在主循环中它们处理输入,并且仅使用所需的、很少的一套系统调用。

在进入主循环之前,一个进程可以限制它自己只能运行所需要的几个操作。如果 程序有缺陷,能够通过恶意的输入去利用该缺陷,这个承诺可以有效地限制漏洞利用的实现。

使用与 strace 相同的模型,但不是输出所有的系统调用,我们既能够阻塞某些系统调用,也可以在它的行为异常时简单地终止被跟踪进程。终止它很容易:只需要在跟踪器中调用 exit(2)。因此,它也可以被设置为去终止被跟踪进程。阻塞系统调用和允许子进程继续运行都只是些雕虫小技而已。

最棘手的部分是当系统调用启动后没有办法去中断它。当跟踪器在入口从 wait(2) 中返回到系统调用时,从一开始停止一个系统调用的仅有方式是,终止被跟踪进程。

然而,我们不仅可以“搞乱”系统调用的参数,也可以改变系统调用号本身,将它修改为一个不存在的系统调用。返回时,在 errno通过正常的内部信号,我们就可以报告一个“友好的”错误信息。

for (;;) {
    /* Enter next system call */
    ptrace(PTRACE_SYSCALL, pid, 0, 0);
    waitpid(pid, 0, 0);

    struct user_regs_struct regs;
    ptrace(PTRACE_GETREGS, pid, 0, &regs);

    /* Is this system call permitted? */
    int blocked = 0;
    if (is_syscall_blocked(regs.orig_rax)) {
        blocked = 1;
        regs.orig_rax = -1; // set to invalid syscall
        ptrace(PTRACE_SETREGS, pid, 0, &regs);
    }

    /* Run system call and stop on exit */
    ptrace(PTRACE_SYSCALL, pid, 0, 0);
    waitpid(pid, 0, 0);

    if (blocked) {
        /* errno = EPERM */
        regs.rax = -EPERM; // Operation not permitted
        ptrace(PTRACE_SETREGS, pid, 0, &regs);
    }
}

这个简单的示例只是检查了系统调用是否违反白名单或黑名单。而它们在这里并没有差别,比如,允许文件以只读而不是读写方式打开(open(2)),允许匿名内存映射但不允许非匿名映射等等。但是这里仍然没有办法去动态撤销被跟踪进程的权限。

跟踪器与被跟踪进程如何沟通?使用人为的系统调用!

创建一个人为的系统调用

对于我的这个类似于 pledge 的系统调用 —— 我可以通过调用 xpledge() 将它与真实的系统调用区分开 —— 我设置 10000 作为它的系统调用号,这是一个非常大的数字,真实的系统调用中从来不会用到它。

#define SYS_xpledge 10000

为演示需要,我同时构建了一个非常小的接口,这在实践中并不是个好主意。它与 OpenBSD 的 pledge(2) 稍有一些相似之处,它使用了一个 字符串接口事实上,设计一个健壮且安全的权限集是非常复杂的,正如在 pledge(2) 的手册页面上所显示的那样。下面是对被跟踪进程的系统调用的完整接口实现:

#define _GNU_SOURCE
#include <unistd.h>

#define XPLEDGE_RDWR (1 << 0)
#define XPLEDGE_OPEN (1 << 1)

#define xpledge(arg) syscall(SYS_xpledge, arg)

如果给它传递个参数 0 ,仅允许一些基本的系统调用,包括那些用于去分配内存的系统调用(比如 brk(2))。 PLEDGE_RDWR 位允许 各种 读和写的系统调用(read(2)readv(2)pread(2)preadv(2) 等等)。PLEDGE_OPEN 位允许 open(2)

为防止发生提升权限的行为,pledge() 会拦截它自己 —— 但这样也防止了权限撤销,以后再细说这方面内容。

在 xpledge 跟踪器中,我需要去检查这个系统调用:

/* Handle entrance */
switch (regs.orig_rax) {
    case SYS_pledge:
        register_pledge(regs.rdi);
        break;
}

操作系统将返回 ENOSYS(函数尚未实现),因为它不是一个真实的系统调用。为此在退出时我用一个 success(0) 去覆写它。

/* Handle exit */
switch (regs.orig_rax) {
    case SYS_pledge:
        ptrace(PTRACE_POKEUSER, pid, RAX * 8, 0);
        break;
}

我写了一小段测试程序去打开 /dev/urandom,做一个读操作,尝试去承诺后,然后试着第二次打开 /dev/urandom,然后确认它能够读取原始的 /dev/urandom 文件描述符。在没有承诺跟踪器的情况下运行,输出如下:

$ ./example
fread("/dev/urandom")[1] = 0xcd2508c7
XPledging...
XPledge failed: Function not implemented
fread("/dev/urandom")[2] = 0x0be4a986
fread("/dev/urandom")[1] = 0x03147604

做一个无效的系统调用并不会让应用程序崩溃。它只是失败,这是一个很方便的返回方式。当它在跟踪器下运行时,它的输出如下:

>$ ./xpledge ./example
fread("/dev/urandom")[1] = 0xb2ac39c4
XPledging...
fopen("/dev/urandom")[2]: Operation not permitted
fread("/dev/urandom")[1] = 0x2e1bd1c4

这个承诺很成功,第二次的 fopen(3) 并没有进行,因为跟踪器用一个 EPERM 阻塞了它。

可以将这种思路进一步发扬光大,比如,改变文件路径或返回一个假的结果。一个跟踪器可以很高效地 chroot 它的被跟踪进程,通过一个系统调用将任意路径传递给 root 从而实现 chroot 路径。它甚至可以对用户进行欺骗,告诉用户它以 root 运行。事实上,这些就是 Fakeroot NG 程序所做的事情。

仿真外部系统

假设你不满足于仅拦截一些系统调用,而是想拦截全部系统调用。你就会有了 一个打算在其它操作系统上运行的二进制程序,无需系统调用,这个二进制程序可以一直运行。

使用我在前面所描述的这些内容你就可以管理这一切。跟踪器可以使用一个假冒的东西去代替系统调用号,允许它失败,以及为系统调用本身提供服务。但那样做的效率很低。其实质上是对每个系统调用做了三个上下文切换:一个是在入口上停止,一个是让系统调用总是以失败告终,还有一个是在系统调用退出时停止。

从 2005 年以后,对于这个技术,PTrace 的 Linux 版本有更高效的操作:PTRACE_SYSEMU。PTrace 仅在每个系统调用发出时停止一次,在允许被跟踪进程继续运行之前,由跟踪器为系统调用提供服务。

for (;;) {
    ptrace(PTRACE_SYSEMU, pid, 0, 0);
    waitpid(pid, 0, 0);

    struct user_regs_struct regs;
    ptrace(PTRACE_GETREGS, pid, 0, &regs);

    switch (regs.orig_rax) {
        case OS_read:
            /* ... */

        case OS_write:
            /* ... */

        case OS_open:
            /* ... */

        case OS_exit:
            /* ... */

        /* ... and so on ... */
    }
}

从任何具有(足够)稳定的系统调用 ABI(LCTT 译注:应用程序二进制接口),在相同架构的机器上运行一个二进制程序时,你只需要 PTRACE_SYSEMU 跟踪器、一个加载器(用于代替 exec(2)),和这个二进制程序所需要(或仅运行静态的二进制程序)的任何系统库即可。

事实上,这听起来有点像一个有趣的周末项目。

参见


via: http://nullprogram.com/blog/2018/06/23/

作者:Chris Wellons 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

你也许用过调速器检查过你的代码,但你知道它们是如何做到的吗?

调试器是大多数(即使不是每个)开发人员在软件工程职业生涯中至少使用过一次的那些软件之一,但是你们中有多少人知道它们到底是如何工作的?我在悉尼 linux.conf.au 2018 的演讲中,将讨论从头开始编写调试器……使用 Rust

在本文中,术语 调试器 debugger 跟踪器 tracer 可以互换。 “ 被跟踪者 Tracee ”是指正在被跟踪器跟踪的进程。

ptrace 系统调用

大多数调试器严重依赖称为 ptrace(2) 的系统调用,其原型如下:

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

这是一个可以操纵进程几乎所有方面的系统调用;但是,在调试器可以连接到一个进程之前,“被跟踪者”必须以请求 PTRACE_TRACEME 调用 ptrace。这告诉 Linux,父进程通过 ptrace 连接到这个进程是合法的。但是……我们如何强制一个进程调用 ptrace?很简单!fork/execve 提供了在 fork 之后但在被跟踪者真正开始使用 execve 之前调用 ptrace 的简单方法。很方便地,fork 还会返回被跟踪者的 pid,这是后面使用 ptrace 所必需的。

现在被跟踪者可以被调试器追踪,重要的变化发生了:

  • 每当一个信号被传送到被跟踪者时,它就会停止,并且一个可以被 wait 系列的系统调用捕获的等待事件被传送给跟踪器。
  • 每个 execve 系统调用都会导致 SIGTRAP 被传递给被跟踪者。(与之前的项目相结合,这意味着被跟踪者在一个 execve 完全发生之前停止。)

这意味着,一旦我们发出 PTRACE_TRACEME 请求并调用 execve 系统调用来实际在被跟踪者(进程上下文)中启动程序时,被跟踪者将立即停止,因为 execve 会传递一个 SIGTRAP,并且会被跟踪器中的等待事件捕获。我们如何继续?正如人们所期望的那样,ptrace 有大量的请求可以用来告诉被跟踪者可以继续:

  • PTRACE_CONT:这是最简单的。 被跟踪者运行,直到它接收到一个信号,此时等待事件被传递给跟踪器。这是最常见的实现真实世界调试器的“继续直至断点”和“永远继续”选项的方式。断点将在下面介绍。
  • PTRACE_SYSCALL:与 PTRACE_CONT 非常相似,但在进入系统调用之前以及在系统调用返回到用户空间之前停止。它可以与其他请求(我们将在本文后面介绍)结合使用来监视和修改系统调用的参数或返回值。系统调用追踪程序 strace 很大程度上使用这个请求来获知进程发起了哪些系统调用。
  • PTRACE_SINGLESTEP:这个很好理解。如果您之前使用过调试器(你会知道),此请求会执行下一条指令,然后立即停止。

我们可以通过各种各样的请求停止进程,但我们如何获得被调试者的状态?进程的状态大多是通过其寄存器捕获的,所以当然 ptrace 有一个请求来获得(或修改)寄存器:

  • PTRACE_GETREGS:这个请求将给出被跟踪者刚刚被停止时的寄存器的状态。
  • PTRACE_SETREGS:如果跟踪器之前通过调用 PTRACE_GETREGS 得到了寄存器的值,它可以在参数结构中修改相应寄存器的值,并使用 PTRACE_SETREGS 将寄存器设为新值。
  • PTRACE_PEEKUSERPTRACE_POKEUSER:这些允许从被跟踪者的 USER 区读取信息,这里保存了寄存器和其他有用的信息。 这可以用来修改单一寄存器,而避免使用更重的 PTRACE_{GET,SET}REGS 请求。

在调试器仅仅修改寄存器是不够的。调试器有时需要读取一部分内存,甚至对其进行修改。GDB 可以使用 print 得到一个内存位置或变量的值。ptrace 通过下面的方法实现这个功能:

  • PTRACE_PEEKTEXTPTRACE_POKETEXT:这些允许读取和写入被跟踪者地址空间中的一个字。当然,使用这个功能时被跟踪者要被暂停。

真实世界的调试器也有类似断点和观察点的功能。 在接下来的部分中,我将深入体系结构对调试器支持的细节。为了清晰和简洁,本文将只考虑 x86。

体系结构的支持

ptrace 很酷,但它是如何工作? 在前面的部分中,我们已经看到 ptrace 跟信号有很大关系:SIGTRAP 可以在单步跟踪、execve 之前以及系统调用前后被传送。信号可以通过一些方式产生,但我们将研究两个具体的例子,以展示信号可以被调试器用来在给定的位置停止程序(有效地创建一个断点!):

  • 未定义的指令:当一个进程尝试执行一个未定义的指令,CPU 将产生一个异常。此异常通过 CPU 中断处理,内核中相应的中断处理程序被调用。这将导致一个 SIGILL 信号被发送给进程。 这依次导致进程被停止,跟踪器通过一个等待事件被通知,然后它可以决定后面做什么。在 x86 上,指令 ud2 被确保始终是未定义的。
  • 调试中断:前面的方法的问题是,ud2 指令需要占用两个字节的机器码。存在一条特殊的单字节指令能够触发一个中断,它是 int $3,机器码是 0xCC。 当该中断发出时,内核向进程发送一个 SIGTRAP,如前所述,跟踪器被通知。

这很好,但如何我们才能胁迫被跟踪者执行这些指令? 这很简单:利用 ptracePTRACE_POKETEXT 请求,它可以覆盖内存中的一个字。 调试器将使用 PTRACE_PEEKTEXT 读取该位置原来的值并替换为 0xCC ,然后在其内部状态中记录该处原来的值,以及它是一个断点的事实。 下次被跟踪者执行到该位置时,它将被通过 SIGTRAP 信号自动停止。 然后调试器的最终用户可以决定如何继续(例如,检查寄存器)。

好吧,我们已经讲过了断点,那观察点呢? 当一个特定的内存位置被读或写,调试器如何停止程序? 当然你不可能为了能够读或写内存而去把每一个指令都覆盖为 int $3。有一组调试寄存器为了更有效的满足这个目的而被设计出来:

  • DR0DR3:这些寄存器中的每个都包含一个地址(内存位置),调试器因为某种原因希望被跟踪者在那些地址那里停止。 其原因以掩码方式被设定在 DR7 寄存器中。
  • DR4DR5:这些分别是 DR6DR7 过时的别名。
  • DR6:调试状态。包含有关 DR0DR3 中的哪个寄存器导致调试异常被引发的信息。这被 Linux 用来计算与 SIGTRAP 信号一起传递给被跟踪者的信息。
  • DR7:调试控制。通过使用这些寄存器中的位,调试器可以控制如何解释 DR0DR3 中指定的地址。位掩码控制监视点的尺寸(监视1、2、4 或 8 个字节)以及是否在执行、读取、写入时引发异常,或在读取或写入时引发异常。

由于调试寄存器是进程的 USER 区域的一部分,调试器可以使用 PTRACE_POKEUSER 将值写入调试寄存器。调试寄存器只与特定进程相关,因此在进程抢占并重新获得 CPU 控制权之前,调试寄存器会被恢复。

冰山一角

我们已经浏览了一个调试器的“冰山”:我们已经介绍了 ptrace,了解了它的一些功能,然后我们看到了 ptrace 是如何实现的。 ptrace 的某些部分可以用软件实现,但其它部分必须用硬件来实现,否则实现代价会非常高甚至无法实现。

当然有很多我们没有涉及。例如“调试器如何知道变量在内存中的位置?”等问题由于空间和时间限制而尚未解答,但我希望你从本文中学到了一些东西;如果它激起你的兴趣,网上有足够的资源可以了解更多。

想要了解更多,请查看 linux.conf.au 中 Levente Kurusa 的演讲 Let's Write a Debugger!,于一月 22-26 日在悉尼举办。


via: https://opensource.com/article/18/1/how-debuggers-really-work

作者:Levente Kurusa 译者:stephenxs 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

本周工作中,我花了整整一周的时间来尝试调试一个段错误。我以前从来没有这样做过,我花了很长时间才弄清楚其中涉及的一些基本事情(获得核心转储、找到导致段错误的行号)。于是便有了这篇博客来解释如何做那些事情!

在看完这篇博客后,你应该知道如何从“哦,我的程序出现段错误,但我不知道正在发生什么”到“我知道它出现段错误时的堆栈、行号了! ”。

什么是段错误?

段错误 segmentation fault ”是指你的程序尝试访问不允许访问的内存地址的情况。这可能是由于:

  • 试图解引用空指针(你不被允许访问内存地址 0);
  • 试图解引用其他一些不在你内存(LCTT 译注:指不在合法的内存地址区间内)中的指针;
  • 一个已被破坏并且指向错误的地方的 C++ 虚表指针 C++ vtable pointer ,这导致程序尝试执行没有执行权限的内存中的指令;
  • 其他一些我不明白的事情,比如我认为访问未对齐的内存地址也可能会导致段错误(LCTT 译注:在要求自然边界对齐的体系结构,如 MIPS、ARM 中更容易因非对齐访问产生段错误)。

这个“C++ 虚表指针”是我的程序发生段错误的情况。我可能会在未来的博客中解释这个,因为我最初并不知道任何关于 C++ 的知识,并且这种虚表查找导致程序段错误的情况也是我所不了解的。

但是!这篇博客后不是关于 C++ 问题的。让我们谈论的基本的东西,比如,我们如何得到一个核心转储?

步骤1:运行 valgrind

我发现找出为什么我的程序出现段错误的最简单的方式是使用 valgrind:我运行

valgrind -v your-program

这给了我一个故障时的堆栈调用序列。 简洁!

但我想也希望做一个更深入调查,并找出些 valgrind 没告诉我的信息! 所以我想获得一个核心转储并探索它。

如何获得一个核心转储

核心转储 core dump 是您的程序内存的一个副本,并且当您试图调试您的有问题的程序哪里出错的时候它非常有用。

当您的程序出现段错误,Linux 的内核有时会把一个核心转储写到磁盘。 当我最初试图获得一个核心转储时,我很长一段时间非常沮丧,因为 - Linux 没有生成核心转储!我的核心转储在哪里?

这就是我最终做的事情:

  1. 在启动我的程序之前运行 ulimit -c unlimited
  2. 运行 sudo sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t

ulimit:设置核心转储的最大尺寸

ulimit -c 设置核心转储的最大尺寸。 它往往设置为 0,这意味着内核根本不会写核心转储。 它以千字节为单位。 ulimit 是按每个进程分别设置的 —— 你可以通过运行 cat /proc/PID/limit 看到一个进程的各种资源限制。

例如这些是我的系统上一个随便一个 Firefox 进程的资源限制:

$ cat /proc/6309/limits 
Limit                     Soft Limit           Hard Limit           Units     
Max cpu time              unlimited            unlimited            seconds   
Max file size             unlimited            unlimited            bytes     
Max data size             unlimited            unlimited            bytes     
Max stack size            8388608              unlimited            bytes     
Max core file size        0                    unlimited            bytes     
Max resident set          unlimited            unlimited            bytes     
Max processes             30571                30571                processes 
Max open files            1024                 1048576              files     
Max locked memory         65536                65536                bytes     
Max address space         unlimited            unlimited            bytes     
Max file locks            unlimited            unlimited            locks     
Max pending signals       30571                30571                signals   
Max msgqueue size         819200               819200               bytes     
Max nice priority         0                    0                    
Max realtime priority     0                    0                    
Max realtime timeout      unlimited            unlimited            us   

内核在决定写入多大的核心转储文件时使用 软限制 soft limit (在这种情况下,max core file size = 0)。 您可以使用 shell 内置命令 ulimitulimit -c unlimited) 将软限制增加到 硬限制 hard limit

kernel.core\_pattern:核心转储保存在哪里

kernel.core_pattern 是一个内核参数,或者叫 “sysctl 设置”,它控制 Linux 内核将核心转储文件写到磁盘的哪里。

内核参数是一种设定您的系统全局设置的方法。您可以通过运行 sysctl -a 得到一个包含每个内核参数的列表,或使用 sysctl kernel.core_pattern 来专门查看 kernel.core_pattern 设置。

所以 sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t 将核心转储保存到目录 /tmp 下,并以 core 加上一系列能够标识(出故障的)进程的参数构成的后缀为文件名。

如果你想知道这些形如 %e%p 的参数都表示什么,请参考 man core

有一点很重要,kernel.core_pattern 是一个全局设置 —— 修改它的时候最好小心一点,因为有可能其它系统功能依赖于把它被设置为一个特定的方式(才能正常工作)。

kernel.core\_pattern 和 Ubuntu

默认情况下在 ubuntu 系统中,kernel.core_pattern 被设置为下面的值:

$ sysctl kernel.core_pattern
kernel.core_pattern = |/usr/share/apport/apport %p %s %c %d %P

这引起了我的迷惑(这 apport 是干什么的,它对我的核心转储做了什么?)。以下关于这个我了解到的:

  • Ubuntu 使用一种叫做 apport 的系统来报告 apt 包有关的崩溃信息。
  • 设定 kernel.core_pattern=|/usr/share/apport/apport %p %s %c %d %P 意味着核心转储将被通过管道送给 apport 程序。
  • apport 的日志保存在文件 /var/log/apport.log 中。
  • apport 默认会忽略来自不属于 Ubuntu 软件包一部分的二进制文件的崩溃信息

我最终只是跳过了 apport,并把 kernel.core_pattern 重新设置为 sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t,因为我在一台开发机上,我不在乎 apport 是否工作,我也不想尝试让 apport 把我的核心转储留在磁盘上。

现在你有了核心转储,接下来干什么?

好的,现在我们了解了 ulimitkernel.core_pattern ,并且实际上在磁盘的 /tmp 目录中有了一个核心转储文件。太好了!接下来干什么?我们仍然不知道该程序为什么会出现段错误!

下一步将使用 gdb 打开核心转储文件并获取堆栈调用序列。

从 gdb 中得到堆栈调用序列

你可以像这样用 gdb 打开一个核心转储文件:

$ gdb -c my_core_file

接下来,我们想知道程序崩溃时的堆栈是什么样的。在 gdb 提示符下运行 bt 会给你一个 调用序列 backtrace 。在我的例子里,gdb 没有为二进制文件加载符号信息,所以这些函数名就像 “??????”。幸运的是,(我们通过)加载符号修复了它。

下面是如何加载调试符号。

symbol-file /path/to/my/binary
sharedlibrary

这从二进制文件及其引用的任何共享库中加载符号。一旦我这样做了,当我执行 bt 时,gdb 给了我一个带有行号的漂亮的堆栈跟踪!

如果你想它能工作,二进制文件应该以带有调试符号信息的方式被编译。在试图找出程序崩溃的原因时,堆栈跟踪中的行号非常有帮助。:)

查看每个线程的堆栈

通过以下方式在 gdb 中获取每个线程的调用栈!

thread apply all bt full

gdb + 核心转储 = 惊喜

如果你有一个带调试符号的核心转储以及 gdb,那太棒了!您可以上下查看调用堆栈(LCTT 译注:指跳进调用序列不同的函数中以便于查看局部变量),打印变量,并查看内存来得知发生了什么。这是最好的。

如果您仍然正在基于 gdb 向导来工作上,只打印出栈跟踪与bt也可以。 :)

ASAN

另一种搞清楚您的段错误的方法是使用 AddressSanitizer 选项编译程序(“ASAN”,即 $CC -fsanitize=address)然后运行它。 本文中我不准备讨论那个,因为本文已经相当长了,并且在我的例子中打开 ASAN 后段错误消失了,可能是因为 ASAN 使用了一个不同的内存分配器(系统内存分配器,而不是 tcmalloc)。

在未来如果我能让 ASAN 工作,我可能会多写点有关它的东西。(LCTT 译注:这里指使用 ASAN 也能复现段错误)

从一个核心转储得到一个堆栈跟踪真的很亲切!

这个博客听起来很多,当我做这些的时候很困惑,但说真的,从一个段错误的程序中获得一个堆栈调用序列不需要那么多步骤:

  1. 试试用 valgrind

如果那没用,或者你想要拿到一个核心转储来调查:

  1. 确保二进制文件编译时带有调试符号信息;
  2. 正确的设置 ulimitkernel.core_pattern
  3. 运行程序;
  4. 一旦你用 gdb 调试核心转储了,加载符号并运行 bt
  5. 尝试找出发生了什么!

我可以使用 gdb 弄清楚有个 C++ 的虚表条目指向一些被破坏的内存,这有点帮助,并且使我感觉好像更懂了 C++ 一点。也许有一天我们会更多地讨论如何使用 gdb 来查找问题!


via: https://jvns.ca/blog/2018/04/28/debugging-a-segfault-on-linux/

作者:Julia Evans 译者:stephenxs 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

当进行调试时,你有很多选择,但是很难给出一直有效的通用建议(除了“你试过关闭再打开么?”以外)。

这里有一些我最喜欢的 Python 调试技巧。

建立一个分支

请相信我。即使你从来没有打算将修改提交回上游,你也会很乐意将你的实验被包含在它们自己的分支中。

不说别的,它会使清理更容易!

安装 pdb++

认真地说,如果你使用命令行,它会让你的生活更轻松。

pdb++ 所做的一切就是用更好的模块替换标准的 pdb 模块。以下是你在 pip install pdbpp 会看到的:

  • 彩色提示!
  • 制表符补全!(非常适合探索!)
  • 支持切分!

好的,也许最后一个是有点多余……但是非常认真地说,安装 pdb++ 非常值得。

探索

有时候最好的办法就是胡乱试试,然后看看会发生什么。在“明显”的位置放置一个断点并确保它被命中。在代码中加入 print() 和/或 logging.debug() 语句,并查看代码执行的位置。

检查传递给你的函数的参数,检查库的版本(如果你已经非常绝望了)。

一次只能改变一件事

在你在探索了一下后,你将会对你可以做的事情有所了解。但在你开始摆弄代码之前,先退一步,考虑一下你可以改变什么,然后只改变一件事。

做出改变后,然后测试一下,看看你是否接近解决问题。如果没有,请将它改回来,然后尝试其他方法。

只更改一件事就可以让你知道什可以工作,哪些不工作。另外,一旦可以工作后,你的新提交将会小得多(因为将有更少的变化)。

这几乎是 科学过程 Scientific Process 中所做的事情:一次只更改一个变量。通过让自己看到并衡量一次更改的结果,你可以节省你的理智,并更快地找到解决方案。

不要假设,提出问题

偶尔一个开发人员(当然不是你咯!)会匆忙提交一些有问题的代码。当你去调试这段代码时,你需要停下来,并确保你明白它想要完成什么。

不要做任何假设。仅仅因为代码在 model.py 文件中并不意味着它不会尝试渲染一些 HTML。

同样,在做任何破坏性的事情之前,仔细检查你的所有外部关联。要删除一些配置数据?请确保你没有连接到你的生产系统。

聪明,但不要聪明过头

有时候我们编写的代码神奇般地奏效,不知道它是如何做的。

当我们发布代码时,我们可能会觉得自己很聪明,但当代码崩溃时,我们往往会感到愚蠢,我们必须记住它是如何工作的,以便弄清楚它为什么不起作用。

留意任何看起来过于复杂、冗长或极短的代码段。这些可能是隐藏复杂并导致错误的地方。


via: https://pythondebugging.com/articles/python-debugging-tips

作者:PythonDebugging.com 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

这个简单优雅的模块可以让你包含调试或仅用于开发环境的代码,而在产品环境中隐藏它们。

仅用于调试或开发调整时的 Perl 代码块有时会很有用。这很好,但是这样的代码块可能会对性能产生很大的影响, 尤其是在运行时才决定是否执行它。

Curtis "Ovid" Poe 最近编写了一个可以帮助解决这个问题的模块:Keyword::DEVELOPMENT。该模块利用 Keyword::Simple 和 Perl 5.012 中引入的可插入关键字架构来创建了新的关键字:DEVELOPMENT。它使用 PERL_KEYWORD_DEVELOPMENT 环境变量的值来确定是否要执行一段代码。

使用它不能更容易了:

use Keyword::DEVELOPMENT;

sub doing_my_big_loop {
    my $self = shift;
    DEVELOPMENT {
        # insert expensive debugging code here!
    }
}

在编译时,DEVELOPMENT 块内的代码已经被优化掉了,根本就不存在。

你看到好处了么?在沙盒中将 PERL_KEYWORD_DEVELOPMENT 环境变量设置为 true,在生产环境设为 false,并且可以将有价值的调试工具提交到你的代码库中,在你需要的时候随时可用。

在缺乏高级配置管理的系统中,你也可以使用此模块来处理生产和开发或测试环境之间的设置差异:

sub connect_to_my_database {

    my $dsn = "dbi:mysql:productiondb";
    my $user = "db_user";
    my $pass = "db_pass";

    DEVELOPMENT {
        # Override some of that config information
        $dsn = "dbi:mysql:developmentdb";
    }

    my $db_handle = DBI->connect($dsn, $user, $pass);
}

稍后对此代码片段的增强使你能在其他地方,比如 YAML 或 INI 中读取配置信息,但我希望您能在此看到该工具。

我查看了关键字 Keyword::DEVELOPMENT 的源码,花了大约半小时研究,“天哪,我为什么没有想到这个?”安装 Keyword::Simple 后,Curtis 给我们的模块就非常简单了。这是我长期以来在自己的编码实践中所需要的一个优雅解决方案。


via: https://opensource.com/article/18/4/perl-module-debugging-code

作者:Ruth Holloway 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

(之前的 gdb 系列文章:gdb 如何工作(2016)三步上手 gdb(2014)

在这周,我发现我可以从 gdb 上调用 C 函数。这看起来很酷,因为在过去我认为 gdb 最多只是一个只读调试工具。

我对 gdb 能够调用函数感到很吃惊。正如往常所做的那样,我在 Twitter 上询问这是如何工作的。我得到了大量的有用答案。我最喜欢的答案是 Evan Klitzke 的示例 C 代码,它展示了 gdb 如何调用函数。代码能够运行,这很令人激动!

我(通过一些跟踪和实验)认为那个示例 C 代码和 gdb 实际上如何调用函数不同。因此,在这篇文章中,我将会阐述 gdb 是如何调用函数的,以及我是如何知道的。

关于 gdb 如何调用函数,还有许多我不知道的事情,并且,在这儿我写的内容有可能是错误的。

从 gdb 中调用 C 函数意味着什么?

在开始讲解这是如何工作之前,我先快速的谈论一下我是如何发现这件令人惊讶的事情的。

假如,你已经在运行一个 C 程序(目标程序)。你可以运行程序中的一个函数,只需要像下面这样做:

  • 暂停程序(因为它已经在运行中)
  • 找到你想调用的函数的地址(使用符号表)
  • 使程序(目标程序)跳转到那个地址
  • 当函数返回时,恢复之前的指令指针和寄存器

通过符号表来找到想要调用的函数的地址非常容易。下面是一段非常简单但能够工作的代码,我在 Linux 上使用这段代码作为例子来讲解如何找到地址。这段代码使用 elf crate。如果我想找到 PID 为 2345 的进程中的 foo 函数的地址,那么我可以运行 elf_symbol_value("/proc/2345/exe", "foo")

fn elf_symbol_value(file_name: &str, symbol_name: &str) -> Result<u64, Box<std::error::Error>> {
    // 打开 ELF 文件 
    let file = elf::File::open_path(file_name).ok().ok_or("parse error")?;
    // 在所有的段 & 符号中循环,直到找到正确的那个
    let sections = &file.sections;
    for s in sections {
        for sym in file.get_symbols(&s).ok().ok_or("parse error")? {
            if sym.name == symbol_name {
                return Ok(sym.value);
            }
        }
    }
    None.ok_or("No symbol found")?
}

这并不能够真的发挥作用,你还需要找到文件的内存映射,并将符号偏移量加到文件映射的起始位置。找到内存映射并不困难,它位于 /proc/PID/maps 中。

总之,找到想要调用的函数地址对我来说很直接,但是其余部分(改变指令指针,恢复寄存器等)看起来就不这么明显了。

你不能仅仅进行跳转

我已经说过,你不能够仅仅找到你想要运行的那个函数地址,然后跳转到那儿。我在 gdb 中尝试过那样做(jump foo),然后程序出现了段错误。毫无意义。

如何从 gdb 中调用 C 函数

首先,这是可能的。我写了一个非常简洁的 C 程序,它所做的事只有 sleep 1000 秒,把这个文件命名为 test.c

#include <unistd.h>

int foo() {
    return 3;
}
int main() {
    sleep(1000);
}

接下来,编译并运行它:

$ gcc -o test  test.c
$ ./test

最后,我们使用 gdb 来跟踪 test 这一程序:

$ sudo gdb -p $(pgrep -f test)
(gdb) p foo()
$1 = 3
(gdb) quit

我运行 p foo() 然后它运行了这个函数!这非常有趣。

这有什么用?

下面是一些可能的用途:

  • 它使得你可以把 gdb 当成一个 C 应答式程序(REPL),这很有趣,我想对开发也会有用
  • 在 gdb 中进行调试的时候展示/浏览复杂数据结构的功能函数(感谢 @invalidop
  • 在进程运行时设置一个任意的名字空间(我的同事 nelhage 对此非常惊讶)
  • 可能还有许多我所不知道的用途

它是如何工作的

当我在 Twitter 上询问从 gdb 中调用函数是如何工作的时,我得到了大量有用的回答。许多答案是“你从符号表中得到了函数的地址”,但这并不是完整的答案。

有个人告诉了我两篇关于 gdb 如何工作的系列文章:原生调试:第一部分原生调试:第二部分。第一部分讲述了 gdb 是如何调用函数的(指出了 gdb 实际上完成这件事并不简单,但是我将会尽力)。

步骤列举如下:

  1. 停止进程
  2. 创建一个新的栈框(远离真实栈)
  3. 保存所有寄存器
  4. 设置你想要调用的函数的寄存器参数
  5. 设置栈指针指向新的 栈框 stack frame
  6. 在内存中某个位置放置一条陷阱指令
  7. 为陷阱指令设置返回地址
  8. 设置指令寄存器的值为你想要调用的函数地址
  9. 再次运行进程!

(LCTT 译注:如果将这个调用的函数看成一个单独的线程,gdb 实际上所做的事情就是一个简单的线程上下文切换)

我不知道 gdb 是如何完成这些所有事情的,但是今天晚上,我学到了这些所有事情中的其中几件。

创建一个栈框

如果你想要运行一个 C 函数,那么你需要一个栈来存储变量。你肯定不想继续使用当前的栈。准确来说,在 gdb 调用函数之前(通过设置函数指针并跳转),它需要设置栈指针到某个地方。

这儿是 Twitter 上一些关于它如何工作的猜测:

我认为它在当前栈的栈顶上构造了一个新的栈框来进行调用!

以及

你确定是这样吗?它应该是分配一个伪栈,然后临时将 sp (栈指针寄存器)的值改为那个栈的地址。你可以试一试,你可以在那儿设置一个断点,然后看一看栈指针寄存器的值,它是否和当前程序寄存器的值相近?

我通过 gdb 做了一个试验:

(gdb) p $rsp
$7 = (void *) 0x7ffea3d0bca8
(gdb) break foo
Breakpoint 1 at 0x40052a
(gdb) p foo()
Breakpoint 1, 0x000000000040052a in foo ()
(gdb) p $rsp
$8 = (void *) 0x7ffea3d0bc00

这看起来符合“gdb 在当前栈的栈顶构造了一个新的栈框”这一理论。因为栈指针($rsp)从 0x7ffea3d0bca8 变成了 0x7ffea3d0bc00 —— 栈指针从高地址往低地址长。所以 0x7ffea3d0bca80x7ffea3d0bc00 的后面。真是有趣!

所以,看起来 gdb 只是在当前栈所在位置创建了一个新的栈框。这令我很惊讶!

改变指令指针

让我们来看一看 gdb 是如何改变指令指针的!

(gdb) p $rip
$1 = (void (*)()) 0x7fae7d29a2f0 <__nanosleep_nocancel+7>
(gdb) b foo
Breakpoint 1 at 0x40052a
(gdb) p foo()
Breakpoint 1, 0x000000000040052a in foo ()
(gdb) p $rip
$3 = (void (*)()) 0x40052a <foo+4>

的确是!指令指针从 0x7fae7d29a2f0 变为了 0x40052afoo 函数的地址)。

我盯着输出看了很久,但仍然不理解它是如何改变指令指针的,但这并不影响什么。

如何设置断点

上面我写到 break foo 。我跟踪 gdb 运行程序的过程,但是没有任何发现。

下面是 gdb 用来设置断点的一些系统调用。它们非常简单。它把一条指令用 cc 代替了(这告诉我们 int3 意味着 send SIGTRAP https://defuse.ca/online-x86-assembler.html),并且一旦程序被打断了,它就把指令恢复为原先的样子。

我在函数 foo 那儿设置了一个断点,地址为 0x400528

PTRACE_POKEDATA 展示了 gdb 如何改变正在运行的程序。

// 改变 0x400528 处的指令
25622 ptrace(PTRACE_PEEKTEXT, 25618, 0x400528, [0x5d00000003b8e589]) = 0
25622 ptrace(PTRACE_POKEDATA, 25618, 0x400528, 0x5d00000003cce589) = 0
// 开始运行程序
25622 ptrace(PTRACE_CONT, 25618, 0x1, SIG_0) = 0
// 当到达断点时获取一个信号
25622 ptrace(PTRACE_GETSIGINFO, 25618, NULL, {si_signo=SIGTRAP, si_code=SI_KERNEL, si_value={int=-1447215360, ptr=0x7ffda9bd3f00}}) = 0
// 将 0x400528 处的指令更改为之前的样子
25622 ptrace(PTRACE_PEEKTEXT, 25618, 0x400528, [0x5d00000003cce589]) = 0
25622 ptrace(PTRACE_POKEDATA, 25618, 0x400528, 0x5d00000003b8e589) = 0

在某处放置一条陷阱指令

当 gdb 运行一个函数的时候,它也会在某个地方放置一条陷阱指令。这是其中一条。它基本上是用 cc 来替换一条指令(int3)。

5908  ptrace(PTRACE_PEEKTEXT, 5810, 0x7f6fa7c0b260, [0x48f389fd89485355]) = 0
5908  ptrace(PTRACE_PEEKTEXT, 5810, 0x7f6fa7c0b260, [0x48f389fd89485355]) = 0
5908 ptrace(PTRACE_POKEDATA, 5810, 0x7f6fa7c0b260, 0x48f389fd894853cc) = 0

0x7f6fa7c0b260 是什么?我查看了进程的内存映射,发现它位于 /lib/x86_64-linux-gnu/libc-2.23.so 中的某个位置。这很奇怪,为什么 gdb 将陷阱指令放在 libc 中?

让我们看一看里面的函数是什么,它是 __libc_siglongjmp 。其他 gdb 放置陷阱指令的地方的函数是 __longjmp___longjmp_chkdl_main_dl_close_worker

为什么?我不知道!也许出于某种原因,当函数 foo() 返回时,它调用 longjmp ,从而 gdb 能够进行返回控制。我不确定。

gdb 如何调用函数是很复杂的!

我将要在这儿停止了(现在已经凌晨 1 点),但是我知道的多一些了!

看起来“gdb 如何调用函数”这一问题的答案并不简单。我发现这很有趣并且努力找出其中一些答案,希望你也能够找到。

我依旧有很多未回答的问题,关于 gdb 是如何完成这些所有事的,但是可以了。我不需要真的知道关于 gdb 是如何工作的所有细节,但是我很开心,我有了一些进一步的理解。


via: https://jvns.ca/blog/2018/01/04/how-does-gdb-call-functions/

作者:Julia Evans 译者:ucasFL 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出