断点是软件调试的常用功能。凡是做过几年编程的,一般都会使用调试器的断点命令,比如gdb中的b命令就是用来设置断点。
举例来说,要拦截使用pthread函数创建子线程的动作,那么就可以b pthread_create来设置断点。然后使用info观察:
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000555555096c in main at iotest.c:8
breakpoint already hit 2 times
2 breakpoint keep y 0x0000007ff7e73928 <pthread_create+4>
设好断点后,执行c命令恢复被调试程序执行,如果CPU执行到了pthread_create函数,那么就会中断到调试器。
很多人都用过断点,而且不少人都知道断点的工作原理是调试器会插一条指令到要中断的位置。但是很少有人亲眼观察到这样的断点指令.
断点指令就像炸弹一样,一碰即爆, 我常常把它比喻为炸弹或者地雷。
为什么难得一见呢?因为GDB不是在执行b命令时就把炸弹指令埋下去,而是在执行c命令或者r命令时才做这个动作。
而一旦执行了c或者r命令后,gdb便进入禁言状态,停止接受命令。
(gdb) c
Continuing.
而当断电命中或者用Ctrl + C再进入命令行时,GDB会把动态埋伏的所有炸弹指令都恢复成本来的样子。
因为此,当我们执行b pthread_create命令后,观察pthread_create的位置,看到的是如下指令:
(gdb) x /2wx 0x0000007ff7e73928
0x7ff7e73928 <pthread_create+4>: 0x900008e4 0xf9473484
反汇编观察,是adrp指令,不是断点指令。
(gdb) x /2i 0x0000007ff7e73928
0x7ff7e73928 <pthread_create+4>: adrp x4, 0x7ff7f8f000
0x7ff7e7392c <pthread_create+8>: ldr x4, [x4, #3688]
那么如何才能观察到GDB动态埋伏的断点指令呢?
简单的回答是不能使用同一个GDB,要使用其它工具。基本的步骤是先记下断点的地址(info b)以及被调试程序的进程ID。
(gdb) info inferiors
Num Description Connection Executable
* 1 process 2270 1 (native) /gewu/nanocode/nd3/dbg/iotest
然后一种方法是通过Linux系统的虚文件机制来观察被调试程序的内存空间。内存虚文件的路径为:/proc/<pid>/mem。
比如,使用下面这条dd命令可以观察上面设的pthread_create断点:
dd bs=1 if=/proc/2270/mem skip=$((0x0000007ff7e73928)) count=8 | hexdump -x
其中的2270是进程ID,0x0000007ff7e73928断点地址。
geduer@ulan:~$ dd bs=1 if=/proc/2270/mem skip=$((0x0000007ff7e73928)) count=8 | hexdump -x
dd: /proc/2270/mem: cannot skip to specified offset
8+0 records in
8+0 records out
8 bytes copied, 0.000435459 s, 18.4 kB/s
0000000 0000 d420 3484 f947
0000008
dd的结果中,0000 d420便是ARM64的断点指令。如果你是用x86做试验,那么就应该看到著名的0xCC。
ARM64的A64指令都是4字节,所以上面的0000 d420一般写为d4200000。其中的d420是关键的操作符,低位可以跟一个立即数,用于传递调试服务编号。
除了使用dd命令外,也可以使用NDB调试器来观察GDB埋伏的断点。这时需要使用NDB的-pv选项开启“非入侵”(non-invasize)模式.
./ndb -pv 2270
附加好后, 只要执行dd命令就可以了.
dd 0x0000007ff7e73928
0000007f`f7e73928 d4200000 f9473484 a9107bfd 910403fd
0000007f`f7e73938 a91153f3 90000933 aa0103f4 a9125bf5
0000007f`f7e73948 39626265 f9400086 f9007fe6 d2800006
0000007f`f7e73958 a9008fe2 f90013e0 35002dc5 d1000680
0000007f`f7e73968 b1000c1f 540003e9 910183f3 aa1303e0
0000007f`f7e73978 94000382 2a0003f6 350001a0 52800020
0000007f`f7e73988 b9002fe0 a91363f7 a9146bf9 a91573fb
0000007f`f7e73998 14000019 528002d6 b9402fe0 35002360
dd命令结果的第一行,第二列便是断点指令: d4200000.
凑巧的是上面两种方法都是使用dd命令, 但是它们的源头差别很大.第一个dd一般是用来操作磁盘的. 第二个dd是用来观察内存的.
写了一天的代码, 临下班前写了这篇短文. 为什么要写这么基础的东西呢?
与其回答这么枯燥的问题, 不如分享两句我很喜欢的话给大家:
君子戒慎乎其所不睹,恐惧乎其所不闻.
君子务本, 本立而道生.
(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
*************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物
也欢迎关注格友公众号