GDB Remote Serial Protocal
Table of Contents
1. GDB Remote Serial Protocal
http://aosabook.org/en/gdb.html
https://sourceware.org/gdb/onlinedocs/gdb/Remote-Protocol.html#Remote-Protocol
https://www.chciken.com/tlmboy/2022/04/03/gdb-z80.html
https://medium.com/swlh/implement-gdb-remote-debug-protocol-stub-from-scratch-1-a6ab2015bfc5
https://www.embecosm.com/appnotes/ean4/embecosm-howto-rsp-server-ean4-issue-2.html
1.1. Overview
GDB Remote Serial Protocal (RSP) 是一个简单的纯文本协议, 一般运行在串口或 tcp 上.
RSP 的 client 端运行在 host 设备上. gdb 本身包含了 client 的功能 (gdb/remote.c), 通过`target remote xxx` 命令就可以连接到 xxx 指定的 server
RSP 的 server 端运行在被调试的设备上, server 可以在遵守 RSP 协议的基础上自由开发, 例如 qemu 和 openocd 自带的 server. 如果被调试的设备本身就有 gdb 的支持, 则可以使用 gdb 自带的 gdbserver, 例如 android 上的 gdbserver
RSP server 只需要实现和 target 相关的功能, 例如读写内存, 读写寄存器, 设置断点等, 不需要和符号处理相关的功能, 因此更容易实现, 也更容易运行在嵌入式设备上.
1.2. Packet
RSP packet 格式都是 `$xxx#yy` 的形式, xxx 是真正的数据, yy 是 checksum.
由于 RSP 是纯文本的协议, 所以在 xxx 中传输二进制数据会有额外的要求 (https://sourceware.org/gdb/onlinedocs/gdb/Overview.html#Overview):
- 数据以 base 16 形式的字符串表示
- 如果数据是包含 `$`, `#`, 需要用 `}` 进行特殊的转义
- 数据中通常有大量连续的重复数据, 例如 `(int64)0x1` 正常编码时为 `0000000000000001`, 针对这种情况 RSP 设计了一种节省空间的编码方式, 这个数据最终会被编码成 `0*+1`
1.3. Example
# server: gdbserver --remote-debug localhost:12345 test.elf # client: echo "target remote localhost:12345">/tmp/test.cmd gdb ./test.elf -x /tmp/test.cmd
1.3.1. 读写 register
client 通过 g
读 register, 通过 G
或 P
写 register
1.3.1.1. 读 register
client: (gdb) info reg rax 0x0 0 rbx 0x0 0 rcx 0x0 0 rdx 0x0 0 rsi 0x0 0 rdi 0x0 0 rbp 0x0 0x0 rsp 0x7fffffffc270 0x7fffffffc270 r8 0x0 0 r9 0x0 0 r10 0x0 0 r11 0x0 0 r12 0x0 0 r13 0x0 0 r14 0x0 0 r15 0x0 0 rip 0x7ffff7fd0100 0x7ffff7fd0100 <_start> eflags 0x200 [ IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 gdb server: getpkt ("g"); [no ack sent] putpkt ("$0*}0*+70c2f*"7f0*}0*B1fdf7ff7f0*"020* 330*"2b0*}0*}0* 7f030*(f* 0*}0*}0*}0*}0*}0*c801f0* 3b0*}0*}0*}0*}0*}0*}0*}0*C#bf"); [noack mode]
- client 使用
g
packet 读取所有寄存器的值 server 返回的的数据这样解释:
以开头的
0*}0*+
为例:0*}
表示前面一个 0, 然后后面有 M 个重复的 0, M 等于 ord(}
)-29=125-29=960*+
表示前面一个 0, 然后后面有 N 个重复的 0, N 等于 ord(+
)-29=43-29=14
所以开头一共有 1+96+1+14 = 112 个 0, 每个字符表示 4 bit, 即 448 bit, 表示 448/64=7 个寄存器的值都是 0
之所以用
}
和+
两次来表示连续的 0, 是因为连续的 0 太多, 无法用一个 ascii 表示
1.3.1.2. 写 register
client: p $rax=0x1 server: getpkt ("P0=0100000000000000"); [no ack sent] putpkt ("$#00"); [noack mode] getpkt ("G010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000070c2ffffff7f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001fdf7ff7f000000020000330000002b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f03000000000000ffff0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000801f00003b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); [no ack sent] putpkt ("$OK#9a"); [noack mode] getpkt ("g"); [no ack sent] putpkt ("$010*}0*)70c2f*"7f0*}0*B1fdf7ff7f0*"020* 330*"2b0*}0*}0* 7f030*(f* 0*}0*}0*}0*}0*}0*c801f0* 3b0*}0*}0*}0*}0*}0*}0*}0*C#1e"); [noack mode]
使用 P
可以修改单个寄存器, 使用 G
可以一次修改所有寄存器
1.3.2. 读写内存
client 通过 m
读内存, 通过 M
或 X
写内存
client: x /xb 0x7fffffffc270 x /xw 0x7fffffffc270 x /2xb 0x7fffffffc270 set *(char*)0x7fffffffc270=0xab set *(short*)0x7fffffffc270=0xab gdb server: getpkt ("m7fffffffc270,1"); [no ack sent] putpkt ("$01#61"); [noack mode] getpkt ("m7fffffffc270,4"); [no ack sent] putpkt ("$010*"#dd"); [noack mode] getpkt ("m7fffffffc270,1"); [no ack sent] putpkt ("$01#61"); [noack mode] getpkt ("m7fffffffc271,1"); [no ack sent] putpkt ("$00#60"); [noack mode] getpkt ("X7fffffffc270,1:�"); [no ack sent] putpkt ("$OK#9a"); [noack mode] getpkt ("X7fffffffc270,2:�"); [no ack sent] putpkt ("$OK#9a"); [noack mode]
x /N
时 client 使用了多个 m packet, 而不是一次读取大段内存
1.3.3. break
client 通过 Z
设置断点, 通过 z
清除断点
client: b main d br 1 gdb server: getpkt ("m555555555180,40"); [no ack sent] putpkt ("$8345f001817df00f270* 7eda8b45f4c9c3f30f1efa554889e54883ec10897dfc488975f08b45fc89c7e89af*"c9c3662e0f1f840*'f1f440* #1d"); [noack mode] getpkt ("m555555555192,1"); [no ack sent] putpkt ("$f3#99"); [noack mode]
设置 break 时 gdb server 用 m 读取了断点处的内存…但并没有立即用 Z packet 设置断点. 而清除断点时没有任何操作.
因为让 server 设置和清除断点的动作是在真正执行时才去做的.
ps. 这个行为和 gdb 的 `always-inserted` 有关
1.3.4. c
client: c server: getpkt ("Z0,555555555149,1"); [no ack sent] putpkt ("$OK#9a"); [noack mode] getpkt ("vCont;c:p3f89c.-1"); [no ack sent] putpkt ("$T05swbreak:;06:80c1f*"7f0* ;07:68c1f*"7f0* ;10:49515*"550* ;thread:p3f89c.3f89c;core:0;#25"); [noack mode] getpkt ("qXfer:threads:read::0,1000"); [no ack sent] putpkt ("$l<threads> <thread id="p3f89c.3f89c" core="0" name="test.elf"/> </threads> #6b"); [noack mode] getpkt ("z0,555555555149,1"); [no ack sent] putpkt ("$OK#9a"); [noack mode]
- continue 之前设置的断点在 0x555555555149, continue 时 client 先用
Z0,555555555149,1
在 0x555555555149 设置断点 - client 用
vCont;c:p3f89c.-1
开始 continue - server step 后返回
$T05swbreak:;06:80c1f*"7f0* ;07:68c1f*"7f0* ;10:49515*"550* ;thread:p3f89c.3f89c;core:0;#25
, 表示收到 SIGTRAP (05), 并且返回了一些有用的信息, 例如 06 寄存器的值是 0x7fffffffc180 - client 用
z0,555555555149,1
删除了这个 break, 因为后续执行时会用Z0,555555555149,1
重新设置断点
1.3.5. si
si 直接使用 vCont;s
即可
client: (gdb) si 0x000055555555512d 3 int foo(int init) { (gdb) disass /m Dump of assembler code for function foo: 3 int foo(int init) { 0x0000555555555129 <+0>: endbr64 => 0x000055555555512d <+4>: push %rbp 0x000055555555512e <+5>: mov %rsp,%rbp 0x0000555555555131 <+8>: mov %edi,-0x14(%rbp) server: getpkt ("vCont;s:p46eeb.46eeb;c:p46eeb.-1"); [no ack sent] putpkt ("$T0506:80c1f*"7f0* ;07:68c1f*"7f0* ;10:2d515*"550* ;thread:p46eeb.46eeb;core:3;#3f"); [noack mode] getpkt ("qXfer:threads:read::0,1000"); [no ack sent]
1.3.6. s
step 是以高级语言的 stmt 为单位的 (而不是以指令为单位), 但 server 端由于没有符号信息, 无法知道 stmt 对应的指令的范围, 因为需要 client 端通过 vCont;r
packet 指定对应的指令的范围.
client: (gdb) b foo (gdb) c (gdb) s (gdb) disass /m Dump of assembler code for function foo: 3 int foo(int init) { 0x0000555555555149 <+0>: endbr64 0x000055555555514d <+4>: push %rbp 0x000055555555514e <+5>: mov %rsp,%rbp 0x0000555555555151 <+8>: sub $0x20,%rsp 0x0000555555555155 <+12>: mov %edi,-0x14(%rbp) 4 int x = init; => 0x0000555555555158 <+15>: mov -0x14(%rbp),%eax 0x000055555555515b <+18>: mov %eax,-0xc(%rbp) server: getpkt ("vCont;r555555555149,555555555158:p40be4.40be4;c:p40be4.-1"); [no ack sent] putpkt ("$T0506:60c1f*"7f0* ;07:40c1f*"7f0* ;10:58515*"550* ;thread:p40be4.40be4;core:0;#99"); [noack mode] getpkt ("qXfer:threads:read::0,1000"); [no ack sent] putpkt ("$l<threads> <thread id="p40be4.40be4" core="0" name="test.elf"/> </threads> #4f"); [noack mode]
client 会发送 vCont;r555555555149,555555555158
, 表示 server 可以连续执行指令,直接指令不在 [0x555555555149,0x555555555158) 范围内, 这个范围实际上就是 foo 函数的
prolog 对应的指令
如果要 step 的 stmt 是 `for` 这种形式, step 会比较复杂, 因为它需要根据运行的结果决定停在哪个地方, 例如:
int foo(int init) { int x = init; for (int i = 0; i < 10000; i++) { x += i; } return x; }
在 `for ()` 处 step 前 disass 的数据:
(gdb) disass /m 3 int foo(int init) { 0x0000555555555129 <+0>: endbr64 0x000055555555512d <+4>: push %rbp 0x000055555555512e <+5>: mov %rsp,%rbp 0x0000555555555131 <+8>: mov %edi,-0x14(%rbp) 4 int x = init; 0x0000555555555134 <+11>: mov -0x14(%rbp),%eax 0x0000555555555137 <+14>: mov %eax,-0x8(%rbp) 5 for (int i = 0; i < 10000; i++) { => 0x000055555555513a <+17>: movl $0x0,-0x4(%rbp) 0x0000555555555141 <+24>: jmp 0x55555555514d <foo+36> 0x0000555555555149 <+32>: addl $0x1,-0x4(%rbp) 0x000055555555514d <+36>: cmpl $0x270f,-0x4(%rbp) 0x0000555555555154 <+43>: jle 0x555555555143 <foo+26> 6 x += i; 0x0000555555555143 <+26>: mov -0x4(%rbp),%eax 0x0000555555555146 <+29>: add %eax,-0x8(%rbp) 7 } 8 return x; 0x0000555555555156 <+45>: mov -0x8(%rbp),%eax 9 } 0x0000555555555159 <+48>: pop %rbp 0x000055555555515a <+49>: retq
step 后 client 的输出:
getpkt ("vCont;r55555555513a,555555555141:p4546b.4546b;c:p4546b.-1"); [no ack sent] putpkt ("$T0506:60c1f*"7f0* ;07:60c1f*"7f0* ;10:41515*"550* ;thread:p4546b.4546b;core:1;#40"); [noack mode] getpkt ("vCont;r555555555141,555555555143:p4546b.4546b;c:p4546b.-1"); [no ack sent] putpkt ("$T0506:60c1f*"7f0* ;07:60c1f*"7f0* ;10:4d515*"550* ;thread:p4546b.4546b;core:1;#73"); [noack mode] getpkt ("vCont;r555555555149,555555555156:p4546b.4546b;c:p4546b.-1"); [no ack sent] putpkt ("$T0506:60c1f*"7f0* ;07:60c1f*"7f0* ;10:43515*"550* ;thread:p4546b.4546b;core:1;#42"); [noack mode]
这里使用了三个 vCont
packet, 因为 `for` 的下一个 stmt 有两种可能: 一个是
`x+=i`, 一个是 `return x`, 多个 packet 实际是在跟踪执行的结果
1.3.7. 其它
1.3.7.1. vFile
1.3.7.1.1. file io
client: remote put x y server: getpkt ("vFile:setfs:0"); [no ack sent] putpkt ("$F0#76"); [noack mode] getpkt ("vFile:open:79,601,1c0"); [no ack sent] putpkt ("$F8#7e"); [noack mode] getpkt ("vFile:pwrite:8,0,x "); [no ack sent] putpkt ("$F2#78"); [noack mode] getpkt ("vFile:close:8"); [no ack sent] putpkt ("$F0#76"); [noack mode]
把 client 端的 x 文件复制为 server 端的 y 文件, 使用了 vFile
packet
1.3.7.1.2. mapping
client: info proc mappings server: getpkt ("vFile:open:2f70726f632f3239323637322f6d617073,0,1c0"); [no ack sent] putpkt ("$F8#7e"); [noack mode] getpkt ("vFile:pread:8,47ff,0"); [no ack sent] putpkt ("$F819;5*"554000-5*%000 r--p 0*"00 103:02 2317316 *//home/sunway/download/xxx/test.elf 5*%000-5*"556000 r-xp 0* 1000 103:02 2317316 *//home/sunway/download/xxx/test.elf 5*"556000-5*"557000 r--p 0* 2000 103:02 2317316 *//home/sunway/download/xxx/test.elf 5*"557000-5*"558000 r--p 0* 2000 103:02 2317316 *//home/sunway/download/xxx/test.elf 5*"558000-5*"559000 rw-p 0* 3000 103:02 2317316 *//home/sunway/download/xxx/test.elf 7f* 7da2000-7f* 7dc4000 r--p 0*"00 103:02 19535855 *./usr/lib/x86_64-linux-gnu/libc-2.31.so 7f* 7dc4000-7f* 7f3c000 r-xp 00022000 103:02 19535855 *./usr/lib/x86_64-linux-gnu/libc-2.31.so 7f* 7f3c000-7f* 7f8a000 r--p 0019a000 103:02 19535855 *./usr/lib/x86_64-linux-gnu/libc-2.31.so 7f* 7f8a000-7f* 7f8e000 r--p 001e7000 103:02 19535855 *./usr/lib/x86_64-linux-gnu/libc-2.31.so 7f* 7f8e000-7f* 7f90* rw-p 001eb000 103:02 19535855 *./usr/lib/x86_64-linux-gnu/libc-2.31.so 7f* 7f90* -7f* 7f96000 rw-p 0*"00 00:00 0 7f* 7fc9000-7f* 7fcd000 r--p 0*"00 00:00 0 *6[vvar] 7f* 7fcd000-7f* 7fcf000 r-xp 0*"00 00:00 0 *6[vdso] 7f* 7fcf000-7f* 7fd0* r--p 0*"00 103:02 19535838 *./usr/lib/x86_64-linux-gnu/ld-2.31.so 7f* 7fd0* -7f* 7ff3000 r-xp 0* 1000 103:02 19535838 *./usr/lib/x86_64-linux-gnu/ld-2.31.so 7f* 7ff3000-7f* 7ffb000 r--p 00024000 103:02 19535838 *./usr/lib/x86_64-linux-gnu/ld-2.31.so 7f* 7ffc000-7f* 7ffd000 r--p 0002c000 103:02 19535838 *./usr/lib/x86_64-linux-gnu/ld-2.31.so 7f* 7ffd000-7f* 7ffe000 rw-p 0002d000 103:02 19535838 *./usr/lib/x86_64-linux-gnu/ld-2.31.so 7f* 7ffe000-7f* 7fff000 rw-p 0*"00 00:00 0 7f*"dc000-7f*"ff000 rw-p 0*"00 00:00 0 *6[stack] f*&60*!-f*&601000 --xp 0*"00 00:00 0 *.[vsyscall] #f6"); [noack mode] getpkt ("vFile:pread:8,47ff,819"); [no ack sent] putpkt ("$F0;#b1"); [noack mode] getpkt ("vFile:close:8"); [no ack sent] putpkt ("$F0#76"); [noack mode]
`info proc mapping` 是通过 vFile
读取 `/proc/<pid>/maps` 实现的
1.3.7.2. qSearch
client: (gdb) x /20xb $rsp 0x7fffffffc168: 0x78 0x51 0x55 0x55 0x55 0x55 0x00 0x00 0x7fffffffc170: 0x78 0xc2 0xff 0xff 0xff 0x7f 0x00 0x00 0x7fffffffc178: 0x00 0x00 0x00 0x00 (gdb) find /b $rsp,+20,0x55 0x7fffffffc16a 0x7fffffffc16b 0x7fffffffc16c 0x7fffffffc16d 4 patterns found. server: getpkt ("qSearch:memory:7fffffffc168;14;U"); [no ack sent] putpkt ("$1,7f*"fc16a#d7"); [noack mode] getpkt ("qSearch:memory:7fffffffc16b;11;U"); [no ack sent] putpkt ("$1,7f*"fc16b#d8"); [noack mode] getpkt ("qSearch:memory:7fffffffc16c;10;U"); [no ack sent] putpkt ("$1,7f*"fc16c#d9"); [noack mode] getpkt ("qSearch:memory:7fffffffc16d;f;U"); [no ack sent] putpkt ("$1,7f*"fc16d#da"); [noack mode] getpkt ("qSearch:memory:7fffffffc16e;e;U"); [no ack sent] putpkt ("$0#30"); [noack mode]
1.3.7.3. qSupported
gdb 启动时 client 通过 qSupported
packet 确定 server 支持的功能
Backlinks
GDB (GDB > GDB Remote Serial Protocal): GDB Remote Serial Protocal
GDB Target Arch (GDB Target Arch > Overview): 2. target side, 其上层任务 (execution control, stack frame analysis 等) 都依赖于 底层 target 的功能来实现. 例如, amd64_linux_nat_target 这个 target 是通过 ptrace 实现的, 而 remote_target 是通过 RSP 实现的.
GDB Target Arch (GDB Target Arch > Overview): 5. gdbserver
GDB Target Arch (GDB Target Arch > backtrace > insert breakpoint): breakpoint 每次在执行时 (run, cont, step, …) 会重新插入一次, 因为每次执行完都 会被删除 (参考 GDB Remote Serial Protocal::Example::break)