9

《Debug Hacks》和调试技巧

 3 years ago
source link: http://maskray.me/blog/2013-07-25-debug-hacks
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.

《Debug Hacks》和调试技巧

下篇见调试技巧2

Debug Hacks

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

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

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

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

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

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

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

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

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

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

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

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

修改任意内存地址的值

set {int}0x83040 = 4

显示intel风格的汇编指令

set disassembly-flavor intel

断点在function prologue前

先说一下function prologue吧,每个函数最前面一般有三四行指令用来保存旧的帧指针(rbp),并腾出一部分栈空间(通常用于储存局部变量、为当前函数调用其他函数腾出空间存放参数,有时候还会存储字面字符串,当有nested function时也会用于保存当前的栈指针)。

在x86-64环境下典型的funcition prologue长成这样:

push rbp
mov rbp, rsp
sub rsp, 0x10

可能还会有and指令用于对齐rsp。如果编译时加上-fomit-frame-pointer(Visual Studio中文版似乎译作“省略框架指针”),那么生成的指令就会避免使用rbp,function prologue就会简化成下面一行:

sub rsp, 0x10

设置断点时如果使用了b *func的格式,也就是说在函数名前加上*gdb就会在执行function prologue前停下,而b func则是在执行function prologue后停下。参考下面的会话:

% 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
(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'查看。

% 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
(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.
[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好用。

Mudflap

使用了compile-time instrumentation(CTI)的工具。编译时加上-fmudflap -lmudflap选项即可,会在很多不安全代码生成的指令前加上判断合法性的指令。

% 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=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的运行期行为,具体参见Mudflap Pointer Debugging

AddressSanitizer

和Mudflap类似的工具,clanggcc可以加上选项-fsanitize=address使用,比如:

clang -fsanitize=address a.c

如果想在出错的地方断点停下来,可以用gdb打开,输入b __asan_report_store1回车,再输入r回车运行程序。

-ftrapv

这个选项是调试有符号整型溢出问题的利器。在i386环境下,gcc会把int32_t运算编译成call __addvsi3__addvsi3函数会在运行时检查32位有符号加法运算是否产生溢出,如果是则调用abort函数中止程序。减法、乘法和取反运算也有类似的运行时函数检查溢出,另外也有64位版本的__addvdi3等函数。但不存在对无符号整型的溢出检测函数。比如下面这些代码均会触发trap:

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

#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位溢出。考虑下面这段代码:

#include <limits.h>
#include <stdio.h>
int main()
int a = INT_MAX;
puts("barrier");
long b = LONG_MAX;

在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的一类安全函数,会在运行期检查是否产生了缓冲区溢出。比如,下面的代码会在运行时报错:

#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的了:

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 <foo+0x33>
80484e3: e8 68 fe ff ff call 8048350 <__stack_chk_fail@plt>
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 <foo+0x3b>
400623: e8 88 fe ff ff call 4004b0 <__stack_chk_fail@plt>
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

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能诊断出大量但不是全部的访问错误,比如下面这样有问题的代码就没法检查出来:

int main()
int a[1];
a[1992] = 12;

因为a[1992]的地址在栈上,允许访问。

Valgrind启动时会读取~/.valgrindrc,对于memcheck我配置了下面这几行:

--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类似,使用非常简单:

strace ./a.out

有一些选项可以attach到现有进程上去(-p)、记录时刻(-t)、统计系统调用使用次数(-c)、过滤特定的系统调用(-e)等。

带上-c选项可以统计系统调用的使用次数:

% 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选项只跟踪指定系统调用:

% 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,观察它的输出:

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

ltrace

记录程序调用的动态库中的函数。名字和strace很像,使用方式和很多命令行选项也如出一辙。

查看echo test

% 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
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内函数的情况:

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 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分区。

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的东西,也没有更新了。不够这些思路很奇特,想到了并试图去解决调试时常受困扰的问题,很棒。

Share Comments


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK