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 才是调试的主角,它做了大部分的事情.