一些杂七杂八的调试技巧整理

Table of Contents

最后更新:2018-10-04

大多数时候去做调试,无非是几个情况:

1、程序、库遇到 bug

2、遇到靠思考也难以解决的故障

3、学习原理

4、分析漏洞

调试的目的不同,方法就有好多,但是我认为最重要的还是具备对系统调用和网络协议的熟悉,其次就是掌握各种工具。

1. 跟踪函数调用

好多高级语言都能找到可以跟踪函数调用的工具,借助这些工具可以快速摸清函数调用流程。如 PHP 有 Xdebug,只用在 PHP 源码中调用 Xdebug 提供的函数即可:

xdebug_start_trace('输出的日志路径');
....
xdebug_stop_trace();

1.1. strace

对于操作系统层面,strace 命令可以帮我们跟踪程序的系统调用情况。时常会遇到下载的软件没有在文档里说清楚配置文件路径,导致启动时找不到配置文件,借助 strace 命令,我们只用分析程序启动时读取了哪些文件。

1.1.1. 例 1,运行 snort 时遇到加载库的问题

某台 CentOS 上运行 snort 时:

$ sudo snort -v
snort: error while loading shared libraries: libdnet.1: cannot open shared object file: No such file or directory

但是系统已经安装了 libdnet:

$ sudo yum list installed libdnet
Installed Packages
libdnet.x86_64                      1.12-13.1.el7                      @anaconda

通过 strace,看看发生了什么问题:

$ sudo strace snort -vd
execve("/sbin/snort", ["snort", "-vd"], [/* 19 vars */]) = 0
...
open("/usr/lib64/libdnet.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
...
exit_group(127)                         = ?
+++ exited with 127 +++

上面的输出显示,启动时找不到 /usr/lib64/libdnet.1 这个文件,因为系统里已经安装了这个包,所以我只用手动链接下即可解决:

$ sudo ln -s /usr/lib64/libdnet.so.1 /usr/lib64/libdnet.1

要看程序启动时加载了哪些文件,还通过以下表达式就可以捕获所有的 open 调用:

$ sudo strace -e 'trace=open' snort -v

1.1.2. 例 2,Common Lisp 安装 clsql 库遇到无法加载 SQLite3 库

执行 (ql:quickload 'clsql-sqlite3) 后提示如下:

Couldn't load foreign libraries "libsqlite3", "sqlite3"

因为提示的包名和 Fedora 中包命名不同,不知道要加载哪个库,所以用 strace 跟踪下。先另启个 sbcl,然后用 strace 附到进程上去:

strace -p 16845 -o /tmp/install.log

16845 是 sbcl 的进程 PID,将捕获到的系统调用结果输出到 /tmp/install.log中。再执行 (ql:quickload 'clsql-sqlite3) ,提示错误后中断 strace,分析 install.log:

$ fgrep '.so' install.log
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5
openat(AT_FDCWD, "/lib64/tls/libsqlite3.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib64/libsqlite3.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib64/tls/libsqlite3.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib64/libsqlite3.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5
openat(AT_FDCWD, "/lib64/tls/sqlite3.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib64/sqlite3.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib64/tls/sqlite3.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib64/sqlite3.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 5

可以看到试图从 /lib64、/usr/lib64 等目录中加载 sqlite3.so 和 libsqlite3.so。dnf 的 provides 可以根据文件名查询对应的包名,将这些文件拿去搜下发现 sqlite-devel 提供了/usr/lib64/libsqlite3.so:

$ sudo dnf provides /usr/lib64/libsqlite3.so
sqlite-devel-3.26.0-1.fc29.x86_64 : Development tools for the sqlite3 embeddable SQL database
                                  : engine
仓库        :@System
匹配来源:
文件名    :/usr/lib64/libsqlite3.so

安装后即可。

1.1.3. 例 3,跟踪子进程的系统调用

加上 -f 参数即可,-ff 可以把每个子进程的系统调用单独输出到一个文件中。

1.2. Ktrace

Ktrace 是 BSD 下类似 Linux 中 strace 的工具,用于跟踪进程的系统调用。ktrace 输出的是二进制文件,需要 kdump 辅助解析。

例 1,解决找不到文件问题

比如我修改 MySQL 的配置文件 /etc/my.cnf 里数据存储路径后,依旧提示找不到某文件。用 ktrace 命令执行:

# ktrace mysqld_safe

然后会在当前目录下生成 ktrace.out 的二进制文件,用 kdump(参数 -f 指定 dump 文件)即可看到调用过程,类似:

22866 sh       CALL  munmap(0x32be7206000,0xff0)
22866 sh       RET   munmap 0
22866 sh       CALL  write(2,0x32c3dc7b410,0x6a)
22866 sh       GIO   fd 2 wrote 106 bytes
"/usr/local/bin/mysqld_safe[956]: cannot create /var/mysql/host.err: No such file or directory
"
22866 sh       RET   write 106/0x6a
22866 sh       CALL  read(10,0x32c3dc7bc58,0x200)
22866 sh       RET   read 0
22866 sh       CALL  close(10)
22866 sh       RET   close 0

其中 CALL 表示调用某个系统函数,RET 是调用的返回值,NAMI 是访问的文件路径。比如我想找出命令调用操作了哪些外部文件,以及它的返回值:

# kdump | fgrep -A 2 NAMI

类似结果如下:

22866 sh       CALL  sigprocmask(SIG_BLOCK,0x80000<SIGCHLD>)
22866 sh       NAMI  "/var/mysql/host.pid"
22866 sh       RET   stat -1 errno 2 No such file or directory
22866 sh       CALL  pipe(0x7f7ffffda880)
22866 sh       NAMI  "/var/mysql/host.err"
22866 sh       RET   open -1 errno 2 No such file or directory
22866 sh       CALL  issetugid()
22866 sh       NAMI  "/usr/share/nls/C/libc.cat"

或者查看系统调用返回失败的:

kdump | fgrep errno -B 2

例 2,寻找配置文件

运行 Nginx 时,想知道加载的哪个目录配置文件:

# ktrace -t n nginx

参数 -t 指定要跟踪的系统调用类型,n 表示跟踪 namei。

# kdump -f ktrace.out | fgrep .conf