kgdb源代码分析(2.6.27)第四章-4.2 continue 和 step

/ 0评 / 0

kgdb源代码分析(2.6.27)

本文由penny编写, 你可以通过发邮件到[email protected]联系penny,
也可以直接评论本文章与penny交流.

0. 概述
1. 异步通知
2. 准备工作
3.gdb 远程串行协议(GDB remote serial protocol)
4. 命令实现:
4.1 断点.
4.2 continue 和 step
5 初始化时机.

4.2 continue 和 step

'c'(continue)和's'(step)命令就稍微复杂一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(kernel/kgdb.c) gdb_serial_stub()
1303                  case 'c': /* Continue packet */
1304                  case 's': /* Single step packet */
1305                                   if (kgdb_contthread && kgdb_contthread != current) {
1306                                      /* Can't switch threads in kgdb */
1307                                             error_packet(remcom_out_buffer, -EINVAL);
1308                                      break;
1309                            }
1310                            kgdb_activate_sw_breakpoints();
1311                            /* Fall through to default processing */
1312                  default:
1313 default_handle:
1314                                       error = kgdb_arch_handle_exception(ks->ex_vector,
1315                                                       ks->signo,
1316                                                       ks->err_code,
1317                                                       remcom_in_buffer,
1318                                                       remcom_out_buffer,
1319                                                       ks->linux_regs);

1305 行对变量 kgdb_contthread 进行判断,kgdb_contthread 在正常情况下就是等
于current,它是为 gdb 的'Hc'命令服务的,为了更好地了解这个变量,我们再看看这个命令在
gdb 在远程协议中的意义:

1
2
3
4
$HCT...#xx                (set thread)
Set thread for subsequent operations (`m', `M', `g', `G', et.al.). C = `c' for
thread used in step and continue; T... can be -1 for all threads. C = `g' for
thread used in other operations. If zero, pick a thread, any thread.

我们这里只关心'Hc'命令,因为 kgdb_contthread 和'Hc'相关.上面的文档也说了,'H'命
令是位下面的操作选择线程,而'Hc'这是针对 step 和 continue 这两个动作的.再看看邮件列
表上面的同志是怎样说的:

1
2
3
4
> Before s command gdb sets thread to be stepped using Hc. An Hc0 packet
> indicates that all threads are to be resumed and current thread is to be
> single stepped. An Hc<thread id> indicates that only current thread is to be
> single stepped while holding other threads where they are.

在 gdb发起单步调试命令之前都会先使用'Hc'命令对线程进行一些指定,'Hc0'代表让所
有的线程都恢复工作,只有当前的这个线程继续被单步调试,如果是'Hc'则代表
只有当前线程可以跑,其它的都得等等.对于单步调试'Hc'是最常见的.那问一
个问题,gdb 是怎样知道这个 pid 的?大家应该不会忘了上面在讲远程协议时关于's','c'的返
回值吧,当后面一个断点被触发时它们的返回值才发回给 gdb,而返回的内容就包含了这个
pid('T'回复),告诉 gdb 现在断点是从那个线程里面触发的.kgdb 在处理'Hc'命令时只是对这
个pid进行合法检查, 然后就把这个pid对应的task_struct 找出来赋给了
kgdb_contthread. 第 1305 行的这个检查是在看 gdb 要求这个 pid 是不是当前进程(也是
kgdb 报告上去的),如果不是就代表进程发生过切换,而在 kgdb 中这是不应该发生的,向
gdb 报告这个错误,然后等待下一个指令.

1310 行激活所有断点,这是因为,'c','s'指令其实就意味着程序要往下走了.断点要归位,
准备好下次的触发.

接着进入体系结构相关的'c','s'处理:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
(arch/x86/kernel/kgdb.c)
354 /**
355 *            kgdb_arch_handle_exception - Handle architecture specific GDB packets.
356 *        @vector: The error vector of the exception that happened.
357 *        @signo: The signal number of the exception that happened.
358 *        @err_code: The error code of the exception that happened.
359 *        @remcom_in_buffer: The buffer of the packet we have read.
360 *            @remcom_out_buffer: The buffer of %BUFMAX bytes to write a packet into.
361 *        @regs: The &struct pt_regs of the current process.
362 *
363 *        This function MUST handle the 'c' and 's' command packets,
364 *        as well packets to set / remove a hardware breakpoint, if used.
365 *        If there are additional packets which the hardware needs to handle,
366 *        they are handled here. The code should return -1 if it wants to
367 *        process more packets, and a %0 or %1 if it wants to exit from the
368 *        kgdb callback.
369 */
370 int kgdb_arch_handle_exception(int e_vector, int signo, int err_code,
371                                                   char *remcomInBuffer, char *remcomOutBuffer,
372                                    struct pt_regs *linux_regs)
373 {
374        unsigned long addr;
375        unsigned long dr6;
376        char *ptr;
377        int newPC;
378
379        switch (remcomInBuffer[0]) {
380        case 'c':
381        case 's':
382                   /* try to read optional parameter, pc unchanged if no parm */
383                  ptr = &remcomInBuffer[1];
384                  if (kgdb_hex2long(&ptr, &addr))
385                            linux_regs->ip = addr;
386        case 'D':
387        case 'k':
388                  newPC = linux_regs->ip;
389
390                  /* clear the trace bit */
391                  linux_regs->flags &= ~X86_EFLAGS_TF;
392                  atomic_set(&kgdb_cpu_doing_single_step, -1);
393
394                  /* set the trace bit if we're stepping */
395                  if (remcomInBuffer[0] == 's') {
396                            linux_regs->flags |= X86_EFLAGS_TF;
397                            kgdb_single_step = 1;
398                            atomic_set(&kgdb_cpu_doing_single_step,
399                                         raw_smp_processor_id());
400                  }
401
402                  get_debugreg(dr6, 6);
403                  if (!(dr6 & 0x4000)) {
404                            int breakno;
405
406                            for (breakno = 0; breakno < 4; breakno++) {
407                                     if (dr6 & (1 << breakno) &&
408                                          breakinfo[breakno].type == 0) {
409                                              /* Set restore flag: */
410                                                             linux_regs->flags |= X86_EFLAGS_RF;
411                                              break;
412                                     }
413                    }
414            }
415            set_debugreg(0UL, 6);
416            kgdb_correct_hw_break();
417
418            return 0;
419   }
420
421   /* this means that we do not want to exit from the handler: */
422   return -1;
423 }

对应'c','s'命令的处理其实十分相近,都是先看看命令里面带没带地址参数,如果有就改
变 pc 寄 存 器 的 值 .x86 需 要 清 除 标 志 寄 存 器 里 面 的 X86_EFLAGS_TF 位 和
kgdb_cpu_doing_single_step,这两个东东前面都提到过了,X86_EFLAGS_TF 是用来触发
debug异常的,而 kgdb_cpu_doing_single_step则是防止竞态(kgdb_handle_exception
中).对应'c'命令这些清理是必要的.如果是's'命令 396~399 行则重新对这些变量和寄存器
赋值,我们也终于看到老朋友 kgdb_single_step 和 kgdb_cpu_doing_single_step 被赋值
了,而这个地方也是它们唯一一处被设置的地方(不包括清 0 操作).

下面又是一个调试寄存器,dr6.前面我们已经看到过 dr7.dr6 是什么意思呢?它是一个
调试状态寄存器,当调试事件被触发时,它会告诉我们触发的原因. dr6 里面只有 7 位被用到
了(第 0,1,2,3 和 13,14,15),其余的都保留.403 行判断条件关心的是第 14 位,第 14 位用来
表示调试事件是否因为单步调试即控制寄存器里面的 TF 位(X86_EFLAGS_TF)被值位而造
成的.如果是就正常,把寄存器归0,以便下一次检测.如果不是单步调试造成的,再检查低4位,
低 4 位分别代表 4 个硬件调试断点(x86 的 4 个硬件断点),谁触发谁的那一位就被置成 1,而
breakinfo[]也是关于硬件断点的数据结构,这里我们只关心软件断点,所以就不多说了.对硬
件断点有兴趣的同学可以在网上找找相关资料.

当一切正常,0 被返回,然后 kgdb 对这个异常处理也结束了,所有东西又回到想以前一
样.

到这里大家可能已经发现了一个问题,对于这几个关于's','c'命令的处理代码里面,并
没有看到对 pc 寄存器(指令寄存器)的修改。如果这是真的话,被断点指令替换了的那部分
指令不就没有执行而直接到下一条指令了?这绝对是影响运行结果的,大家都不会相信这是
事实,那真相是怎样的呢?光在 kgdb 这边已经没有什么线索了,让我们换个角度看看 gdb
在处理用户的 s 和 c(注意,前面也看到平均每条用户的指令就对应十多条 gdb 发给 kgdb
的指令).用上面提到的 gdb 提供的 log 来看这个问题不太好,因为 gdb 似乎对发给 kgdb
和从 kgdb 收到的命令和回复在 log 里面进行了修改,把它们的顺序重新排了一下,隐藏了
真相。 也没太好的办法, 只好在 gdb_serial_stub()里面加了三条 printk语句,分别是 kgdb
收到的消息和 kgdb 发出去的消息,还有当 kgdb 处理完一次和 gdb 通信后打出一条消息表
示表示。

下面一段是这样一种情况: 在系统起来后用 gdb 在 sys_mount(),sys_mknod()上面设
置两个断点,然后让目标己继续。在目标机上 mount 一个设备,随便比如 sda1,不一定
要有效的命令只要能触发 sys_mount()就够了,当然目标机这时也就停下来了。在开发机
上的 gdb 执行's'(用户的)我们可以看到下面的输出:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
Nov 21 16:45:41 localhost kernel: kgdb exit
Nov 21 16:46:09 localhost kernel: kgdb in: g
Nov       21       16:46:09        localhost      kernel:    kgdb  out:
1500000058297c2300000000e887050898cff7c1b4cff7c10000edc0000000008
0ae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
Nov 21 16:46:09 localhost kernel: kgdb in: P8=7fae18c0
Nov 21 16:46:09 localhost kernel: kgdb out:
Nov       21       16:46:09         localhost      kernel:    kgdb  in:
G1500000058297c2300000000e887050898cff7c1b4cff7c10000edc000000000
7fae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov       21       16:46:09         localhost      kernel:    kgdb  in:
G1500000058297c2300000000e887050898cff7c1b4cff7c10000edc000000000
7fae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: z0,c018ae7f,1
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: z0,c017dbaf,1
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfbc,4
Nov 21 16:46:09 localhost kernel: kgdb out: e8870508
Nov 21 16:46:09 localhost kernel: kgdb in: m80587e8,8
Nov 21 16:46:09 localhost kernel: kgdb out: 2f6465762f736461
Nov 21 16:46:09 localhost kernel: kgdb in: m80587f0,8
Nov 21 16:46:09 localhost kernel: kgdb out: 3100000011000000
Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfc0,4
Nov 21 16:46:09 localhost kernel: kgdb out: f8870508
Nov 21 16:46:09 localhost kernel: kgdb in: m80587f8,8
Nov 21 16:46:09 localhost kernel: kgdb out: 2f6d6e742f736461
Nov 21 16:46:09 localhost kernel: kgdb in: m8058800,8
Nov 21 16:46:09 localhost kernel: kgdb out: 312f000041000000
Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfc4,4
Nov 21 16:46:09 localhost kernel: kgdb out: 008a0508
Nov 21 16:46:09 localhost kernel: kgdb in: m8058a00,8
Nov 21 16:46:09 localhost kernel: kgdb out: 686673706c757300
Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfc8,4
Nov 21 16:46:09 localhost kernel: kgdb out: 0000edc0
Nov 21 16:46:09 localhost kernel: kgdb in: mc1f7cfcc,4
Nov 21 16:46:09 localhost kernel: kgdb out: 00000000
Nov 21 16:46:09 localhost kernel: kgdb in: Hc982
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: s
Nov 21 16:46:09 localhost kernel: kgdb exit
Nov 21 16:46:09 localhost kernel: kgdb in: g
Nov       21       16:46:09        localhost      kernel: kgdb  out:
1500000058297c23a4cff7c1e887050898cff7c1b4cff7c10000edc00000000082a
e18c08203200060000000680000007b0000007b000000ffff0000ffff0000
Nov 21 16:46:09 localhost kernel: kgdb in: Z0,c018ae7f,1
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: Z0,c017dbaf,1
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: s
Nov 21 16:46:09 localhost kernel: kgdb exit

这是's'(用户)命令的一部分,不过这部分已经足够说明问题了。"kgdb in"就是 gdb 发过
来的命令,"kgdb out"自然就是回复,"kgdb exit"代表离开一次 kgdb 的代码,也就是完成
一次和 gdb 的通信。(一次通信可以完成很多条命令)

我们把关注的指令按顺序摘出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kgdb in: g
kgdb                                                            out:
1500000058297c2300000000e887050898cff7c1b4cff7c10000edc0000000008
0ae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
kgdb in: G
15000000 58297c23 00000000 e8870508 98cff7c1 b4cff7c1 0000edc0
00000000
7fae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
kgdb out: OK
kgdb in: z0,c018ae7f,1
kgdb out: OK
kgdb in: z0,c017dbaf,1
kgdb out: OK
kgdb in: s
kgdb exit
kgdb in: Z0,c018ae7f,1
kgdb out: OK
kgdb in: Z0,c017dbaf,1
kgdb out: OK
kgdb in: s
kgdb exit

先轻轻回顾一下协议命令,'g',gdb向kgdb要寄存器的值;'G',gdb设置目标机的寄
存器;'z'取消一个断点;'Z'设置一个断点;'s'单步执行.这里先指明地址c018ae7f
是sys_mount的地址,c017dbaf是sys_mknod的地址。

故事一开始是 sys_mount 断点被触发,gdb 先读取目标机的寄存器'g',然后 gdb 对这
些值进行修改,和上面'g'的值对比,只修改了第 65,66 个 16 进制数,我们在讲协议的时
候讲过 gdb 把每 8 个 16 进制数变成一个寄存器值,那我们按 8 拆分,发现只有第 9 个寄存
器被修改了它的值从 80ae18c0 改为 7fae18c0,7fae18c0 这个数看上去很眼熟,是的它正
是我们的 sys_mount,只不过数字有点错位了.那我们一定会想这个寄存器是不是 pc 寄存器
呢?没错就是它.在 include/asm-x86/kgdb.h 定义了每个寄存器的含义.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
24 enum regnames {
25        GDB_AX,   /* 0 */
26        GDB_CX,   /* 1 */
27        GDB_DX,    /* 2 */
28        GDB_BX,   /* 3 */
29        GDB_SP,   /* 4 */
30        GDB_BP,   /* 5 */
31        GDB_SI,  /* 6 */
32        GDB_DI,  /* 7 */
33        GDB_PC,   /* 8 also known as eip */
34        GDB_PS,   /* 9 also known as eflags */
35        GDB_CS,   /* 10 */
36        GDB_SS,   /* 11 */
37        GDB_DS,   /* 12 */
38        GDB_ES,   /* 13 */
39        GDB_FS,   /* 14 */
40        GDB_GS,   /* 15 */
41 };

pc 寄存器正是第 9 个.寄存器赋值函数在 gdb_cmd_setregs()中实现,结构十分简单,这
里就不罗嗦了.这里 gdb 把 pc 指针减了一,让 cpu 再次运行断点所在位置的指令.这里又会
有另外一个问题,刚刚在 gdb_serial_stub()中我们也看到了,如果是's'或'c'命令,kgdb 会在
恢复运行之前激活所有断点.这样断点不就又被触发吗?gdb 肯定没那么傻 B,在发's'命令之
前,先发两个'z'命令把所有断点都清了,再单步调试,这样,kgdb 上面都没有断点了,随便让它
激活多少遍断点都没所谓了.而且's'单步调试马上又会把控制权交给 kgdb.在执行完断点原
来位置的那条指令后,马上又用两条'Z'把原来两个断点加上.然后继续单步调试.

pc 寄存器正是第 9 个.寄存器赋值函数在 gdb_cmd_setregs()中实现,结构十分简单,这
里就不罗嗦了.这里 gdb 把 pc 指针减了一,让 cpu 再次运行断点所在位置的指令.这里又会
有另外一个问题,刚刚在 gdb_serial_stub()中我们也看到了,如果是's'或'c'命令,kgdb 会在
恢复运行之前激活所有断点.这样断点不就又被触发吗?gdb 肯定没那么傻 B,在发's'命令之
前,先发两个'z'命令把所有断点都清了,再单步调试,这样,kgdb 上面都没有断点了,随便让它
激活多少遍断点都没所谓了.而且's'单步调试马上又会把控制权交给 kgdb.在执行完断点原
来位置的那条指令后,马上又用两条'Z'把原来两个断点加上.然后继续单步调试.

总结一下这个过程:
1.gdb 获得当前 pc 寄存器值,并把它减一.
2.取消所有断点.
3.单步调试原来断点位置的指令.
4.单步调试引起的 debug 中断让 kgdb 在次拿到控制权,kgdb 再把所有断点加回去.
5.后面继续单步调试.

'c'命令的过程和's'的过程差不多,只是在第 5 步不会继续单步而是'c'继续执行.

一句话,gdb 控制了整个过程. 查看变量的 gdb 操作其实也没太多好说的了,无非就变成一个或多个 gdb 的'm'命令,还
是那句,gdb 才是调试的主角,它做了大部分的事情.

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据