GDB Remote Serial Protocal

Table of Contents

1. GDB Remote Serial Protocal

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):

  1. 数据以 base 16 形式的字符串表示
  2. 如果数据是包含 `$`, `#`, 需要用 `}` 进行特殊的转义
  3. 数据中通常有大量连续的重复数据, 例如 `(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, 通过 GP 写 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=96
    • 0*+ 表示前面一个 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 读内存, 通过 MX 写内存

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)

Author: [email protected]
Date: 2023-01-29 Sun 11:16
Last updated: 2024-02-28 Wed 10:49

知识共享许可协议