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

摘要:
下面是一篇综述,其中还包含我自己组织的一些调试技术。以下用户列表是我在GitHub上找到的按顺序配置点文件的人:1alejandrogomezbhjcraigbarnesdotvimhanaco joedicadorourentbok100pyxroylezsjltrapd00rvodik0ng对于上述点文件,大多数其他人都不想看到它们。在x86-64环境中,典型的函数序言增长如下:123pushrbpmovrbp,rspsubrsp,0x10也可能有一个和指令来对齐rsp。断点1,main()数据.c:44{disasDumpofassembler或deferfunctionmain:=˃0x00000000004005cc<+0>:pushrbp000000004005cd<+1>:movrbp,rsp0x000000004005d0<+4>:子sp,0x100x000000004005 d4<+8>:movDWORDPTR[rbp-0x4],0x0000000004005db<+15>:moveax,DWORDPTN[rbp-0x4]0x00000000004005de<+18>:movesi,eax00000000400e0<+20>:movedi,0x4006ec0x00000000004005e5:移动,0x00x000000004005ea:调用0x4004540x0000000000004005ef:leave0.000000000004005f0:retEndofassemblerdump。Checkpointgdb可以创建调试程序的快照,即保存程序运行时的状态,并等待稍后恢复。Ch是创建快照,dcID是删除具有指定数量的快照,ich是查看所有快照,restartID是切换到具有指定编号的快照。有关详细信息,您可以在要查看的shell中键入info'Checkpoint/Restart'。

转自: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 ", 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 ", 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__ main() { 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 __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 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

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, "177ELF211

免责声明:内容来源于网络,仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇DevExpress 编译成功的 dllAndroid中LocalSocket使用下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Visual Studio 14 初试,vNext

 下了几天的VS 2014 .终于安装上了,花了好几天时间,  VS 2014  下载地址, http://www.visualstudio.com/en-us/downloads/visual-studio-14-ctp-vs http://download.microsoft.com/download/A/E/A/AEA8A39F-E281-448E-...

CentOS 6、7升级gcc至4.8、4.9、5.2、6.3、7.3等高版本

CentOS 7虽然已经出了很多年了,但依然会有很多人选择安装CentOS 6,CentOS 6有些依赖包和软件都比较老旧,如今天的主角gcc编译器,CentOS 6的gcc版本为4.4,CentOS 7为4.8。gcc 4.8最主要的一个特性就是全面支持C++11,如果不清楚什么用的也没关系,简单说一些C++11标准的程序都需要gcc 4.8以上版本的g...

更改Ubuntu gcc、g++默认编译器版本

转一篇文章: 升级Ubuntu到11.10,但在编译Android的时候出错了。这个Android在升级系统之前编译是没有错误的,对比发现升级到Ubuntu 11.10后gcc、g++的版本都是4.6.1。而升级之前的版本是4.4.6。我想多半原因就在这里了。要想解决问题需要更改Ubuntu gcc、g++默认编译器版本。google一把发现有两种方法可以...

jdb--gdb---java 远程调试(java application与web application)

命令比较gdb jdbbt wheredel clearstop breakfinish step up更多http://www.fas.harvard.edu/~cscie119/resources/jdb_reference.pdf5)如果想看但 source code用命令jdb -classpath robocod...

Linux下C++编程环境搭建

  有更简单的方法:在装机器的时候选择  开发工作站系统  development workstation 工作站。免去安装java jdk ,eclipse ,g++,ssh等等各种工具的麻烦。  需要注意的是通过虚拟机安装的时候,要先创建空虚拟机,再从虚拟光驱安装,不要直接选择操作系统类型,不然会默认给安装最简化版的。光中文的设置,和输入法安装 就能让...

vscode利用dev配置c语言,VSCode搭建C++/C调试编译环境(使用DevC++)

关于VSCode使用Dev C++的MinGW64来调试C++/C网上的教程试了很多,大部分都已经过时了或者说是不适配了,最后就选择使用Dev原有的东西来实现,不建议自己下载MinGW64,里面安装的时候有些选项不知道选什么的话很容易出现问题。 配置Dev下MinGW64的路径 假设Dev已经安装好了,然后现在要做的就是将Dev目录下的MinGW添加到环境...