Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2034563
  • 博文数量: 610
  • 博客积分: 11499
  • 博客等级: 上将
  • 技术积分: 5511
  • 用 户 组: 普通用户
  • 注册时间: 2008-03-12 19:27
文章分类

全部博文(610)

文章存档

2016年(5)

2015年(18)

2014年(12)

2013年(16)

2012年(297)

2011年(45)

2010年(37)

2009年(79)

2008年(101)

分类:

2012-09-29 19:09:52

原文地址:kgdb 作者:wmiss414

0. 概述
前段时间用kgdb调了一下内核,感觉这个东西还不错,不过更令人感兴趣的是它的工作原理. 内核运行得好好的,那么多线程在好几个cpu上面跑,就是因为踩到一个断点,它就把所有东西都放下来让gdb来调,kgdb是怎样办到的?
本文是本人看kgdb的代码的一些记录,如有错误的地方敬请指出.

一般软件调试的原理是,调试器根据目标文件的调试信息找到源码和机器码之间的映射关系,并把它关心的指令替换成一条断点指令,x86对应的就是 asm("int $3"),它的二进制代码是0xcc.当程序执行到这个地方时断点指令被执行,程序被中断,调试器接管它的控制权,这时可以查看内存信息甚至修改内存.当调试器完成任务后把断点指令替换回原来的指令,并把pc寄存器减一,让cpu从被中断的那条指令开始执行.这样程序可以在没有影响执行的情况下被调试.
调试内核也离不开一般原理,只不过内核不同一般程序,不能单靠同一台机的gdb来完成.要依靠kgdb,它是内核里面的一小段程序,它可以和另外一台机器上的gdb进行通信,可以执行gdb过来的一些命令并返回信息,完成调试过程.
关于kgdb的配置,网上有很多,而且非常详细,这里就不重复了.

本文的内容:
1.从内核是怎样把控制权交给kgdb的开始,到
2.kgdb的入口函数,看kgdb怎样处理竞态,再了解
3.kgdb和gdb之间的通信协议,接着是
4.kgdb对具体命令的执行,最后再看看
5.kgdb是在什么时候被启动的.
内核版本是2.6.27.

>>>>>>>>>1. 异步通知>>>>>>>>>
当内核踩到一个断点时,当前进程是用什么方式通知kgdb,控制权又是怎样到kgdb手上的呢?
先回顾一下kgdb的patch在2.4上是怎样让内核通知kgdb的处理代码的,下面是2.4.23的内核打上kgdb补丁的部分代码:

(arch/i386/kernel/traps.c)
#ifdef CONFIG_KGDB
#define CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,after) /
{ /
if (linux_debug_hook != (gdb_debug_hook *) NULL && !user_mode(regs)) /
{ /
(*linux_debug_hook)(trapnr, signr, error_code, regs) ; /
after; /
} /
}
#else
#define CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,after)
#endif

#define DO_VM86_ERROR(trapnr, signr, str, name) /
asmlinkage void do_##name(struct pt_regs * regs, long error_code) /
{ /
CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,goto skip_trap)/
do_trap(trapnr, signr, str, 1, regs, error_code, NULL); /
skip_trap: /
return; /
}
DO_VM86_ERROR这个宏是用来生产异常处理函数的。比如你的name参数是int3那就会出来一个do_int3()的函数,用它来处理3号异常,也就是断点异常.而linux_debug_hook是2.4里面指向kgdb主处理函数的函数指针。
从上面代码可以看到,在2.4里面kgdb把它的主处理函数直接插入到异常处理函数的开头,当异常发生时kgdb就直接得到控制权.
说实话,这种做法比较难看.而在2.6里面,做法其实差不多,但我们有了notifier chian这种异步通知机制.什么是notifier chain?它就是一条回调函数的链表,每一条链表代表一种事件,每个关心这个事件的模块或子系统就在这条特定链上面注册自己的回调函数.代码中 notifier_block结构体代表链表里面的一个节点,包含一个函数指针和一个整形变量(描述优先级).当事件发生时对应的函数链上的函数就会按优先级被执行.
在内核中根据不同的事件定义了一些不同的链,比如:die_chain,reboot_notifier_list,netlink_chain...等等,更加具体的描述可以在内核Documentation目录中或网上找到.而它们的实现都是对notifier chain进行封装,notifier chain的具体实现请见kernel/notifier.c.
die_chain这条链关心的是中断和异常事件,kgdb在它上面注册了自己的函数.

(arch/x86/kernel/kgdb.c)
515 static struct notifier_block kgdb_notifier = {
516 .notifier_call = kgdb_notify,
517
518 /*
519 * Lowest-prio notifier priority, we want to be notified last:
520 */
521 .priority = -INT_MAX,
522 };

kgdb很谦虚,把自己的优先级设为最低,先让别人执行,最后才考虑自己.
异常发生时die_chain上的函数都会被触发.对于kgdb,最关心的当然是3号异常,它就是程序踩到一个断点时引发的异常.函数 notify_die()就是遍历die_chain链,执行所有上面的函数:

(arch/x86/kernel/traps_32.c)
851 void __kprobes do_int3(struct pt_regs *regs, long error_code)
852 {
853 trace_hardirqs_fixup();
854
855 if (notify_die(DIE_INT3, "int3", regs, error_code, 3, SIGTRAP)
856 == NOTIFY_STOP)
857 return;
858 /*
859 * This is an interrupt gate, because kprobes wants interrupts
860 * disabled. Normal trap handlers don't.
861 */
862 restore_interrupts(regs);
863
864 do_trap(3, SIGTRAP, "int3", 1, regs, error_code, NULL);
865 }

kgdb的回调函数kgdb_notify()(定义在arch/x86/kernel/kgdb.c)最后被调用,而它又直接调用 __kgdb_notify(),这个函数对某些特殊情况做了处理,我们后面在遇到这些情况时会回到这个地方进行说明,在大多数情况下程序会直接进入 kgdb的入口函数kgdb_handle_exception(),它定义在kernel/kgdb.c.

int3中断
|
v
do_int3()
'->notify_die()
|-> .. //其他关心异常的函数们
|-> ..
|-> ..
'-> kgdb_notify()
'->__kgdb_notify() //关中断
'->kgdb_handle_exception() //kgdb入口

所以kgdb是通过异常触发的,而且整个过程都处在异常处理中.当前进程被异常中断,而异常处理通过notifier chain把控制权交给了kgdb。

>>>>>>>>>2. 准备工作>>>>>>>>>
下面来看kgdb的主函数kgdb_handle_exception(),这个函数前面一段只是在做准备工作,为后面和远程的gdb通信打下基础。
在这个地方又有一个问题:在当今多线程和多cpu的情况下,到处都充满了竞态,那个线程都怕被别人搞破坏,那kgdb又是怎样让自己立于不败之地,控制好 cpu们的?

(kernel/kgdb.c)
1392 int
1393 kgdb_handle_exception(int evector, int signo, int ecode, struct pt_regs *regs)
1394 {
1395 struct kgdb_state kgdb_var;
1396 struct kgdb_state *ks = &kgdb_var;
1397 unsigned long flags;
1398 int error = 0;
1399 int i, cpu;
1400
1401 ks->cpu = raw_smp_processor_id();
1402 ks->ex_vector = evector;
1403 ks->signo = signo;
1404 ks->ex_vector = evector;
1405 ks->err_code = ecode;
1406 ks->kgdb_usethreadid = 0;
1407 ks->linux_regs = regs;
1408
1409 if (kgdb_reenter_check(ks))
1410 return 0; /* Ouch, double exception ! */

参数evector是中断向量,当这个异常发生时signo是发送给当前进程的信号编号,ecode是错误码,regs是当前进程的寄存器值。
ks的类型为kgdb_state,它记录了这次断点发生的信息。不知道是别有用心还是不小心作者给同一个变量ks->ex_vector赋了两次值.
kgdb_reenter_check()函数在检查是否出现了一种递归触发的现象,也就是当kgdb正在一个cpu上处理一个断点的过程中,嵌套地触发了kgdb,再次进入kgdb_handle_exception()中,kgdb都已经拿到控制权了,谁还那么无聊自己抢自己的东西。如果遇到这种情况,kgdb尝试在kgdb_reenter_check()里面纠正这个错误,并直接结束这次调用,这只是一个特殊的错误处理,我们暂时不关心它。

(kernel/kgdb.c)
1412 acquirelock:
1413 /*
1414 * Interrupts will be restored by the 'trap return' code, except when
1415 * single stepping.
1416 */
1417 local_irq_save(flags);
1418
1419 cpu = raw_smp_processor_id();
1420
1421 /*
1422 * Acquire the kgdb_active lock:
1423 */
1424 while (atomic_cmpxchg(&kgdb_active, -1, cpu) != -1)
1425 cpu_relax();

local_irq_save()保存当前中断状态,然后禁中断。raw_smp_processor_id()获得当前CPU号。
atomic_cmpxchg()函数实现了一个比较+交换的原子操作(原子就是说cpu要不就不做,要做就一定要做完某些操作才能干别的事情,对应这里就是比较和交换要一次过做完).atomic_cmpxchg()比较kgdb_active->count的值是否等用-1,如果是则把cpu的值赋给kgdb_active->count,否则不修改它的值,atomic_cmpxchg返回kgdb_active->count赋值前的值.
kgdb_active是一个全局原子变量,定义在kernel/kgdb.c中,用来记录当前正在执行kgdb代码的cpu号,它起到一个锁的作用,因为同一时间只能有一个cpu执行kgdb的代码,这是可以想象得到的,如果两个cpu在两个不同断点被触发,那究竟是谁和远端gdb通信呢?前一条命令被 cpu1拿了,后一条却去了cpu2那里,那还得了。
kgdb_active的初始值为-1,-1表示当前kgdb的处理函数并没有被触发,相反如果kgdb已经在运行,那么kgdb_active就有它自己的值,这些处理都是针对多cpu的,如果只有一个cpu,这个世界就简单多了。这里是防止多个kgdb的实例在不同cpu被触发引起互相干扰。考虑这种情况,在cpu1上有一个断点让kgdb起来,这时,kgdb_active还是-1,cpu1很顺利就给kgdb_active赋值然后进入后面的操作.这时cpu2中kgdb也被触发.它也想进入后面的操作,但是这时候kgdb_active已经不再是-1,cpu2只能不断地比较 kgdb_active的值和执行cpu_relax(),宏cpu_relax()可以简化为一条pause汇编,通过引入一个很短的延迟,加快了紧跟在锁后面的代码的执行并减少能源的消耗,实际上就是让cpu2等。当cpu1在退出kgdb_handle_exception()前会把 kgdb_active赋回-1,这样cpu2就可以进行后面的操作了。kgdb使用大量的原子操作来完成锁的功能,后面还会看到. atomic操作加上cpu_relax()跟一个自旋锁很相似。

(kernel/kgdb.c)
1427 /*
1428 * Do not start the debugger connection on this CPU if the last
1429 * instance of the exception handler wanted to come into the
1430 * debugger on a different CPU via a single step
1431 */
1432 if (atomic_read(&kgdb_cpu_doing_single_step) != -1 &&
1433 atomic_read(&kgdb_cpu_doing_single_step) != cpu) {
1434
1435 atomic_set(&kgdb_active, -1);
1436 touch_softlockup_watchdog();
1437 clocksource_touch_watchdog();
1438 local_irq_restore(flags);
1439
1440 goto acquirelock;
1441 }

注释告诉了我们大部分事情.它在考虑这样一种情况:cpu1先进入kgdb,并和远端gdb通信中确定要进行单步调试(step),设置好cpu1上面标志寄存器的X86_EFLAGS_TF位(也就是trap位,或单步调试位)好让cpu在运行完后面一条指令后引起一个debug异常(1号异常)再次进入kgdb,接下来cpu1让程序继续下一条指令.这时候cpu2闯进来了.当cpu1退出kgdb时会把kgdb_active设置为-1,这样上面提到的那个锁就解开了。cpu2就可以进来了,但是kgdb在一个cpu处在单步调试时不想让别的cpu进来扰乱,就让cpu2放弃kgdb_active 锁,然后让它再拿一次锁,期望cpu1能够先处理,cpu2的事以后再说。kgdb_cpu_doing_single_step用来记录当前那个cpu 处于单步调试的状态,-1代表没有cpu在单步调试状态。后面我们将看到当kgdb收到远程gdb单步执行命令时会设置这个变量.

(kernel/kgdb.c)
1443 if (!kgdb_io_ready(1)) {
1444 error = 1;
1445 goto kgdb_restore; /* No I/O connection, so resume the system */
1446 }

检查kgdb要用的I/O驱动模块是否已经加载好了,如果没有准备好就没有必要往下走直接返回,让系统起来。关于I/O的初始化这里暂时就不讨论了。

(kernel/kgdb.c)
1448 /*
1449 * Don't enter if we have hit a removed breakpoint.
1450 */
1451 if (kgdb_skipexception(ks->ex_vector, ks->linux_regs))
1452 goto kgdb_restore;

kgdb_skipexception(),一个挺有意思的函数,注释写着"如果我们命中了一个已经被删除的断点,下面的代码就不要走下去了".这是什么意思?断点被删除了还能够被命中吗?迫不及待看看它的代码,这是一个与体系结构相关的函数,暂时只有x86对它有实质性的实现:

(arch/x86/kernel/kgdb.c)
530 /**
531 *
532 * kgdb_skipexception - Bail out of KGDB when we've been triggered.
533 * @exception: Exception vector number
534 * @regs: Current &struct pt_regs.
535 *
536 * On some architectures we need to skip a breakpoint exception when
537 * it occurs after a breakpoint has been removed.
538 *
539 * Skip an int3 exception when it occurs after a breakpoint has been
540 * removed. Backtrack eip by 1 since the int3 would have caused it to
541 * increment by 1.
542 */
543 int kgdb_skipexception(int exception, struct pt_regs *regs)
544 {
545 if (exception == 3 && kgdb_isremovedbreak(regs->ip - 1)) {
546 regs->ip -= 1;
547 return 1;
548 }
549 return 0;
550 }

这段注释基本上没有说明什么问题,实现更是看不懂,kgdb_isremovedbreak()是干那行的?为什么被删除了的断点还会起作用呢?google搜了一些,基本上只能找到一堆patch的信息,还有什么线索呢?不要忘记邮件列表这个好东西,牛B门们都在上面.在kgdb的邮件列表 ( bugreport)搜了一下,找到了一个位大侠06年发的这样一封信:
We keep track of removed entries to prevent breakpoints from occuring after
they were removed. Removed breakpoints might have occured on other processors
before there removal from code, which will pop up on continuing and cause
segfaults on i386. kgdb_skipexception function identifies them.
看到"other processors"让人恍然大悟,又是多cpu惹的祸.考虑这样一种情况:有两个cpu,cpu1,cpu2,gdb在之前设置了两个两个断点 B1,B2,cpu1命中了B1,并且拿到了前面的kgdb_active锁,在还没有停止其它cpu的活动前(后面我们马上会讲到),cpu2命中了 B2,但是由于kgdb_active锁已经被cpu1拿了, cpu2将忙等kgdb_active. cpu1进入了kgdb的执行代码,在和远程gdb通信过程中,gdb要求删除B2,cpu1没法知道正在门外等着的cpu2正是因为B2而来的,cpu1直接把B2删除,然后gdb让程序继续.在cpu1放弃kgdb_active锁之后,cpu2进来了,但是cpu2进来已经没有意义了,如果我们在这里不加以判断,后面就会出大bug了,所以我们需要判断一下触发我们的这个断点是不是已经被删除了,kgdb_isremovedbreak()正是用来作这个判断,如果真的是这种情况,pc寄存器(regs->ip)就退一.

(kernel/kgdb.c)
1458 kgdb_info[ks->cpu].debuggerinfo = ks->linux_regs;
1459 kgdb_info[ks->cpu].task = current;
1460
1461 kgdb_disable_hw_debug(ks->linux_regs);

pre_exception()让驱动在进行连接前有机会去做一些自己特有的初始化,。我们可以先看看关于串口的这个函数.

(drivers/serial/kgdboc.c)
143 static void kgdboc_pre_exp_handler(void)
144 {
145 /* Increment the module count when the debugger is active */
146 if (!kgdb_connected)
147 try_module_get(THIS_MODULE);
148 }

看到了,对于串口来说根本就没有做什么事情,只是增加模块的使用计数。
回到我们的kgdb_handle_exception(),kgdb用kgdb_info数组记录当前进程和它的寄存器,方便日后和远程gdb通信时使用。kgdb_disable_hw_debug()对应到系统结构上是调用arch/x86/kernel/kgdb.c中的 kgdb_disable_hw_debug:

(arch/x86/kernel/kgdb.c)
300 /**
301 * kgdb_disable_hw_debug - Disable hardware debugging while we in kgdb.
302 * @regs: Current &struct pt_regs.
303 *
304 * This function will be called if the particular architecture must
305 * disable hardware debugging while it is processing gdb packets or
306 * handling exception.
307 */
308 void kgdb_disable_hw_debug(struct pt_regs *regs)
309 {
310 /* Disable hardware debugging while we are in kgdb: */
311 set_debugreg(0UL, 7);
312 }

就正如注释所说的那样,对于某些体系结构kgdb在运行时需要设置禁止硬件调试,DR7寄存器就是断点控制寄存器,他的低半个字用来允许断点和允许所选择的调试条件,清0就是禁止硬件调试。
再次回到主题kgdb_handle_exception()

(kernel/kgdb.c)
1463 /*
1464 * Get the passive CPU lock which will hold all the non-primary
1465 * CPU in a spin state while the debugger is active
1466 */
1467 if (!kgdb_single_step) {
1468 for (i = 0; i < NR_CPUS; i++)
1469 atomic_set(&passive_cpu_wait[i], 1);
1470 }

在这里出来了一个十分重要的变量kgdb_single_step,名字告诉我们这个变量和单步调试有关,或者可以直接说当kgdb要进行单步调试时就会把这个变量设成1,这个条件有些让人费解,大家可以先想想为什么要有这样一个条件,我们在这里先忽略掉它,就当这里根本没有这个条件,这是可以的.在这节最后我们会对这个条件作更多的说明.
这里又一个用原子变量作为锁的例子,我们先把下面一段代码也贴出来:

(kernel/kgdb.c)
1472 /*
1473 * spin_lock code is good enough as a barrier so we don't
1474 * need one here:
1475 */
1476 atomic_set(&cpu_in_kgdb[ks->cpu], 1);
1477
1478 #ifdef CONFIG_SMP
1479 /* Signal the other CPUs to enter kgdb_wait() */
1480 if ((!kgdb_single_step) && kgdb_do_roundup)
1481 kgdb_roundup_cpus(flags);
1482 #endif
1483
1484 /*
1485 * Wait for the other CPUs to be notified and be waiting for us:
1486 */
1487 for_each_online_cpu(i) {
1488 while (!atomic_read(&cpu_in_kgdb[i]))
1489 cpu_relax();
1490 }

kgdb把正在执行kgdb代码的cpu叫做主cpu(primary cpu),其他的cpu叫从cpu(passive cpu).passive_cpu_wait[]和cpu_in_kgdb[]两个数组存的都是原子变量,都是每个cpu占一个元素。它们组合在一起又形成了一道锁,而这个锁锁的是从cpu,让他们进入忙等待的状态。具体看看它的实现过程。1469行把所有cpu的passive_cpu_wait[]都设成了1,从名字上就可以看出它的意思(passive:被动的),就是kgdb想让那个cpu进入等待状态,这里把自己也选上没关系吗?是的,没有关系,后面自然会明白。我们再跳到1481,kgdb_roundup_cpus()被调用,现在来看看这个函数:

(arch/x86/kernel/kgdb.c)
348 void kgdb_roundup_cpus(unsigned long flags)
349 {
350 send_IPI_allbutself(APIC_DM_NMI);
351 }

再找send_IPI_allbutself,发现一大堆的实现,什么bigsmp,es7000,numaq,summit。不过不用担心,其实它们所做的事情都是一样,只不过是硬件不同有不同的软件实现,有兴趣的同学可以在arch/x86/Kconfig中可以看到他们的说明。从名字我们可以猜出这个函数的作用:send发送,IPI(interprocessor interrupt)处理器间的中断,allbutself除了自己,那串起来就是向除了自己之外的cpu发中断,了解中断的同学应该不会对 APIC(Advanced Programmable Interrupt Controller)陌生,APIC能够让一个cpu向另外一个发送一个中断。kgdb向所有从cpu(passive cpu)发送了一个NMI(不可屏蔽)中断,用来通知它们,kgdb要正式干活了,其它同志们暂时歇会,不要进来捣乱.那么从cpu在被NMI中断后有什么反应呢?让我们暂时离开主函数,在次回到kgdb在被触发时的情景.和int3一样,对于NMI,x86对NMI也设立了相关的处理函数 do_nmi()(定义在arch/x86/kernel/traps_32.c).do_nmi()调用default_do_nmi(),它也会引起 die_chain的注意。我们又回到前面的kgdb在die_chain中注册的处理函数__kgdb_notify()

(arch/x86/kernel/kgdb.c)
442 static int __kgdb_notify(struct die_args *args, unsigned long cmd)
443 {
444 struct pt_regs *regs = args->regs;
445
446 switch (cmd) {
447 case DIE_NMI:
448 if (atomic_read(&kgdb_active) != -1) {
449 /* KGDB CPU roundup */
450 kgdb_nmicallback(raw_smp_processor_id(), regs);
451 was_in_debug_nmi[raw_smp_processor_id()] = 1;
452 touch_nmi_watchdog();
453 return NOTIFY_STOP;
454 }
455 return NOTIFY_DONE;
456
457 case DIE_NMI_IPI:
458 /* Just ignore, we will handle the roundup on DIE_NMI. */
459 return NOTIFY_DONE;

注释也说了,主cpu发过来的中断由DIE_NMI来处理。在2.6.26上面DIE_NMI_IPI和DIE_NMI的处理是一样的.kgdb_nmicallback()会被调用.究竟kgdb用什么方法让从cpu停下来的呢?

(kernel/kgdb.c)
1536 int kgdb_nmicallback(int cpu, void *regs)
1537 {
1538 #ifdef CONFIG_SMP
1539 if (!atomic_read(&cpu_in_kgdb[cpu]) &&
1540 atomic_read(&kgdb_active) != cpu &&
1541 atomic_read(&cpu_in_kgdb[atomic_read(&kgdb_active)])) {
1542 kgdb_wait((struct pt_regs *)regs);
1543 return 0;
1544 }
1545 #endif
1546 return 1;
1547 }

看到名字我迫不及待地想看看kgdb_wait

(kernel/kgdb.c)
564 static void kgdb_wait(struct pt_regs *regs)
565 {
566 unsigned long flags;
567 int cpu;
568
569 local_irq_save(flags);
570 cpu = raw_smp_processor_id();
571 kgdb_info[cpu].debuggerinfo = regs;
572 kgdb_info[cpu].task = current;
573 /*
574 * Make sure the above info reaches the primary CPU before
575 * our cpu_in_kgdb[] flag setting does:
576 */
577 smp_wmb();
578 atomic_set(&cpu_in_kgdb[cpu], 1);
579
580 /* Wait till primary CPU is done with debugging */
581 while (atomic_read(&passive_cpu_wait[cpu]))
582 cpu_relax();
583
584 kgdb_info[cpu].debuggerinfo = NULL;
585 kgdb_info[cpu].task = NULL;
586
587 /* fix up hardware debug registers on local cpu */
588 if (arch_kgdb_ops.correct_hw_break)
589 arch_kgdb_ops.correct_hw_break();
590
591 /* Signal the primary CPU that we are done: */
592 atomic_set(&cpu_in_kgdb[cpu], 0);
593 touch_softlockup_watchdog();
594 clocksource_touch_watchdog();
595 local_irq_restore(flags);
596 }

代码都列出来了,估计大家也都看到这个锁是怎样让从cpu停止活动的,cpu_relax()前面已经提到过了,它可以转换成一条pause汇编.在这里小弟再总结一下:主cpu在kgdb的主函数中(kgdb_handle_exception)设置数组passive_cpu_wait[]中所有的元素(包括描述自己的那个元素),然后向除了自己以外的所有cpu发NMI IPI中断,因此所有的从cpu都会进入kgdb_wait(),进来后从cpu先设置cpu_in_kgdb[]中自己的那个元素,以通知主cpu,自己已经准备好了,然后反复对自己的那个passive_cpu_wait元素进行判断然后relax,而另外一方面,主cpu也正反复检查 cpu_in_kgdb[]的元素,等待所有从cpu都进入等待状态.这样passive_cpu_wait让从cpu等待,cpu_in_kgdb让主 cpu保证所有从cpu都进入了等待状态.
当然,有上锁就有解锁,在主函数中,当处理完远端gdb请求后退出时就会解锁:

(kernel/kgdb.c)
1513 if (!kgdb_single_step) {
1514 for (i = NR_CPUS-1; i >= 0; i--)
1515 atomic_set(&passive_cpu_wait[i], 0);
1516 /*
1517 * Wait till all the CPUs have quit
1518 * from the debugger.
1519 */
1520 for_each_online_cpu(i) {
1521 while (atomic_read(&cpu_in_kgdb[i]))
1522 cpu_relax();
1523 }
1524 }
一样的做法,只不过这次是解锁和等待所有从cpu离开kgdb_wait()回到日常生活.

好了,大家应该不会忘记前面我们提的一个问题,就是为什么要有kgdb_single_step这么一个判断,它在我们前面的代码中连续出现了3 次.kgdb_single_step,就正如大家想的那样,当gdb向kgdb发送一个's'单步执行命令时这个变量的值就会变成1.那为什么在 kgdb处于单步调试的情况下反而不去停止其它cpu的活动呢?
在邮件列表里面,一位仁兄04年写的一封信里给出了答案,道理很简单.如果想看原文,可以在邮件列表里面搜debugger_step这个关键字(在进内核树之前,kgdb_single_step就叫这个名字),在最老的几封信里面. 在2.6.27内核x86平台上,当gdb要求kgdb进行单步调试时,kgdb是会让所有从cpu都处于忙等待,只让被单步调试的那个cpu继续运行, 一直到gdb过来一个'c'(continue)为止,才让从cpu们继续.这样我们再回头看看前面的代码,然后考虑这样一个情况:主cpu由于一个断点进入kgdb的代码,一开始,它并不是处于单步调试的状态,所以kgdb_single_step为0,这样kgdb把其它所有cpu都锁住,让它们等待,然后和远程的gdb通信,假如这时候gdb要求单步调试,kgdb就把kgdb_single_step设成1,然后让程序继续执行,在1513行, 也是判断kgdb_single_step(用于解锁),这时候,kgdb_single_step已经不是0了,所以主cpu并没有帮其它从cpu解锁,所以主cpu在下一条指令完成后再次进入kgdb主函数,这时候,所有从cpu还在忙等待,所有没有必要在设置passive_cpu_wait[] 和向它们发IPI NMI中断,不然从cpu为了响应这个NMI会在kgdb_wait里面再调用一次kgdb_wait,这正是之前kgdb解决过的一个bug.
主函数还剩下一点点了,我们把他看完:

(kernel/kgdb.c)
1492 /*
1493 * At this point the primary processor is completely
1494 * in the debugger and all secondary CPUs are quiescent
1495 */
1496 kgdb_post_primary_code(ks->linux_regs, ks->ex_vector, ks->err_code);
1497 kgdb_deactivate_sw_breakpoints();
1498 kgdb_single_step = 0;
1499 kgdb_contthread = current;
1500 exception_level = 0;

这些是在进入和远程gdb通信之前kgdb所做的最后的初始化,kgdb_post_primary_code的意思就是在主cpu在控制整个机器后,它还有什么事情需要做的,这是体系结构相关的,x86对kgdb_post_primary_code实现非常简单,就是保存了一下中断向量号和错误代码。在开始前我们先禁止所有断点,把内存还原成原来的样子。主函数在最后就是还原环境,中间夹着的是最重要的一行代码了,和gdb通信,执行他发来的命令。

(kernel/kgdb.c)
1502 /* Talk to debugger with gdbserial protocol */
1503 error = gdb_serial_stub(ks);

主函数这样就差不多了,有些同志可能注意到了kgdb_cpu_doing_single_step和前面 passive_cpu_wait[]/cpu_in_kgdb[]形成的锁在功能上有些重叠了,前者是当有一个cpu在单步调试时,其它cpu不能进 kgdb的代码.而后者则是当kgdb在处在单步调试时让其它cpu处在忙等待中.忙等待有怎么会进kgdb的代码呢?对于这个问题,我没有很确切的回答,如果大家知道就请指点指点小弟.这两段代码是一位大侠在一次提交中放到库里的。但是它们的目标都是一样,就是保护单步调试的cpu.

在离开之前,主函数在前面究竟做了什么事情?其实没干多少活,大部分时间都在控制竞态,包括单步调试,停止从cpu的活动等。

>>>>>>>>>3.gdb远程串行协议(GDB remote serial protocol)>>>>>>>>>
在研究kgdb命令具体执行过程之前,我们先来看看gdb和远程的kgdb是通过一种什么的方式通信的。在这里我们只做简略的介绍,详细说明可以在这里找到: /remote-protocol.html
这个现实世界中充满了各种各样的协议,很多事情都是遵循协议来做事,网络有TCP/IP,SPX/IPX, 电信有ISUP,PRI,SIP,就业,租房,离婚都有协议.gdb也不例外,gdb和远程目标之间用gdb远程串行协议(下面简称为RSP)来通信。
RSP是一种基于ASCII码的协议,以gdb发送命令,目标端(在这里是kgdb)返回执行结果或信息的方式来进行的,而命令和回复在传输过程中又是封装在一个包(packet)里面的.每个包以一个'$'开始,接着是实际数据,数据以一个'#'结束,后面跟着两位十六进制数字用作校验和 ($packet-data#checksum).校验和需要对256取模,因为只有两位。
当收到一个数据包,(无论是命令还是回复,无论是gdb端还是目标端)收到数据包的一方应该根据收到的数据和校验和检查这个包的合法性,如果是一个正常的包,应该返回一个'+',如果是一个损坏的包,则应该返回一个'-'。在包里面的数据,可以用',',';',':'三个符号来分隔成不同的段,他们的用法根据不同的命令不同而不同.
gdb提供了一个很方便的方式让我们把通信过程记录下来,在gdb里面使用"set remotelogfile [filepath]"指定你要记录的文件,但是这个文件一定要在开始记录前手动创建,下面一段是在sys_mount设置一个断点的log:

c info source
c b sys_mount
w $mc018ae77,1#fa
r +$55#6a
w +$mc018ae77,1#fa
r +$55#6a
w +$mc018ae77,1#fa
r +$55#6a
w +$mc018ae77,a#2a
r +$5589e5565383ec148d55#e6
w +$mc018ae77,1#fa
r +$55#6a
w +$mc018ae78,1#fb
r +$89#71
w +$mc018ae79,5#00
r +$e5565383ec#a0
w +$mc018ae79,5#00
r +$e5565383ec#a0
w +$mc018ae78,1#fb
r +$89#71
w +$mc018ae79,1#fc
r +$e5#9a
w +$mc018ae7a,1#24
r +$56#6b
w +$mc018ae7a,1#24
r +$56#6b
w +$mc018ae7b,1#25
r +$53#68
w +$mc018ae7c,1#26
r +$83#6b
w +$mc018ae7c,1#26
r +$83#6b
w +$mc018ae77,1#fa
r +$55#6a
w +
c info breakpoints
c c
w $Z0,c018ae7f,1#72

gdb规定目标端起码应该实现:'g' 'G' 'm' 'M' 'c' 's',kgdb当然也有实现了,而且还不止这些,有一些是和体系结构没有太紧密的关系,比如断点的设置和删除,kgdb只是记录起这个地址,和设置这个断点的状态;内存的读取和修改,也许这个和体系结构有关,但是具体实现内核已经搞定,kgdb不用关心。另外一方面一些比如's'单步执行这种命令就和体系结构关系十分密切,x86上是通过设置标志寄存器的Trap标记,让cpu运行完下一条指令后触发一个debug中断,其他的体系结构的做法都有所不同。
下面具体看看一些具体的命令:

'g'
格式:$g#67
描述:gdb向目标端发送这个命令来获取目标机当前寄存器的值
回复:+ $123456789abcdef0...#xx
'+'用来应答'g'这个命令,表明目标端正确地收到这个命令,然后就是目标端的回复包,gdb规定用8十六进制个数字来表示一个寄存器的值,所以第一个寄存器的值为12345678,第二个为9abcdef0,依此类推,而具体每个寄存器的含义和寄存器个数又体系结构决定,定义在gdb的代码中. 当然这里8个数字是对32位系统来说的,为什么是8位?限于我们这个协议是基于ASCII的,一个十六进制数只能标记4位,那32位自然是8个十六进制数了。

'G'
格式:$GXXXXXXXXXXX...#xx
描述:和g相反,这个命令用来设置目标机当前寄存器的值
回复: + $OK#9a
OK表示设置成功,后面我们会讲到不成功的情况.

'm'
格式:$m6a1bbb,2#b9
描述:读取一段内存的值,这里是读取以6a1bbb位起始地址的两个字节
回复: + $f488#0a 目标端把值返回.

'M'
格式:$Mccc5cc,2:a340#01
描述:设置一段内存的值,这里是把以ccc5cc位开始地址的两个字节设成a340
回复: + $OK#9a

's'
格式:$sADDR#xx
描述:用户进行单步调试时用到,ADDR指明了程序将从那个地址恢复运行,如果忽略ADDR,程序就从断点处继续运行.
回复:+ 目标端会马上返回数据正确或错误接收,但不会马上返回信息,具体信息要到下一次断点被触发时才会返回.下面会提到.

'c'
格式:$cADDR#xx
描述:让程序恢复正常运行
回复:和's'一样.

'Z'
格式: $ZTADDR,LENGTH#xx
'Z'命令用来设置断点或watch点,用过gdb的同志应该不会陌生了
'T'字段定义了这个命令的对象,0:软件断点,1:硬件断点,2:写watch点,3:读watch点,4:访问watch点.
'ADDR'就是我们所关心的内存地址,'LENGTH',对于软件中断它指明被断点指令覆盖的内存长度,kgdb对于软件断点忽略掉它,因为触发 kgdb的指令与体系结构相关,已经定义在kgdb这边,就如x86的int3在内存里面的二进制指令为"0xcc";对于硬件断点和watch 点,'LENGTH'指明gdb关注的内存长度.

'z'
格式: $zTADDR,LENGTH#xx
各项与'Z'相同.用来取消断点。

回复:
错误回复:
格式:+ $E01#a6
描述:如果目标端在执行gdb的命令时出错时返回错误回复,比如访问内存时出错.E后面根两位的错误码,错误码在gdb里面没有定义,没有定义其实更加方便,可以让开发端和目标端对错误码的使用带来灵活.

空回复:
格式:+ $#00
描述:当目标端不认识gdb发来的命令时,返回空回复表示自己不支持这个命令.

对'c','s'的回复:
有好几种对'c','s'的回复,其中比较常见的是'S'和'T'
'S'
格式: $SAA#b8
作用: AA表明触发这次通信的那个异常相关的信号,这个信号就是posix标准中的信号.

'T'
格式: $TAAN..:R..;N..:R..#xx
作用: AA同样是信号号, N..:R.. 这表明一个寄存器和它的值,N标记寄存器号,R是它对应的值,其中,如果N不是一个16进制数而是"thread",那么后面R的值就指明当前的进程号. 如果是其它的字符串gdb会省略.

Kgdb在回复's','c'时选用了'T'的方式,不过在'T'消息里面只有thread一个字段,没有给gdb传更多的寄存器信息.
协议就在这里打住,有兴趣的同学可以继续深入研究各种高级用法.
顺便把gdb_serial_stub()开头的代码列出来:

(kernel/kgdb.c)
1215 /*
1216 * This function performs all gdbserial command procesing
1217 */
1218 static int gdb_serial_stub(struct kgdb_state *ks)
1219 {
1220 int error = 0;
1221 int tmp;
1222
1223 /* Clear the out buffer. */
1224 memset(remcom_out_buffer, 0, sizeof(remcom_out_buffer));
1225
1226 if (kgdb_connected) {
1227 unsigned char thref[8];
1228 char *ptr;
1229
1230 /* Reply to host that an exception has occurred */
1231 ptr = remcom_out_buffer;
1232 *ptr++ = 'T';
1233 ptr = pack_hex_byte(ptr, ks->signo);
1234 ptr += strlen(strcpy(ptr, "thread:"));
1235 int_to_threadref(thref, shadow_pid(current->pid));
1236 ptr = pack_threadid(ptr, thref);
1237 *ptr++ = ';';
1238 put_packet(remcom_out_buffer);
1239 }
1240
1241 kgdb_usethread = kgdb_info[ks->cpu].task;
1242 ks->kgdb_usethreadid = shadow_pid(kgdb_info[ks->cpu].task->pid);
1243 ks->pass_exception = 0;

实现传输的两个函数为put_packet()和get_packet()它们的实现十分简单直接,而传输时使用的是底层驱动的读写操作,这部分的实现和 kgdb没有直接的关系,kgdb只是使用了现成的东西而已,这里就不罗嗦了.前面这段就是'T'回复.

>>>>>>>>>>4. 命令实现:>>>>>>>>>>
kgdb实现了众多的命令,这里不一一进行说明,有些实现十分简单,有些就需要仔细看看,不过其实很多工作gdb端已经完成了,到kgdb这边的都是很直接的,比如用户试图在gdb中打印一个局部变量的值,gdb通过和kgdb得到当前进程寄存器的信息,最终计算出要读的内存地址和长度,kgdb只要应 gdb的要求提供相应的信息和设置相应的地址就够了,所以gdb是调试里面的主角.不过我们这里关心的还是kgdb,gdb的工作就忽略了.
我们不妨用一个gdb用户的角度来看看,大多数情况下我们只要:设置断点,让程序继续,单步调试,打印内存这些功能.对应gdb发给kgdb的命令并不就是'Z','c','s','m'命令,一般一个gdb的用户的命令都对上十几二十条kgdb命令的发送和接收.

4.1 断点
先从数据结构入手,kgdb用一个数组kgdb_break[]来描述软件断点(我们这里暂且只讨论软断点):

(kernel/kgdb.c)
107 /*
108 * Holds information about breakpoints in a kernel. These breakpoints are
109 * added and removed by gdb.
110 */
111 static struct kgdb_bkpt kgdb_break[KGDB_MAX_BREAKPOINTS] = {
112 [0 ... KGDB_MAX_BREAKPOINTS-1] = { .state = BP_UNDEFINED }
113 };

每个元素为kgdb_bkpt定义在

(include/linux/kgdb.h)
98 struct kgdb_bkpt {
99 unsigned long bpt_addr;
100 unsigned char saved_instr[BREAK_INSTR_SIZE];
101 enum kgdb_bptype type;
102 enum kgdb_bpstate state;
103 };

结构体里面的每个成员都很好理解,bpt_addr就是断点设置的地址,saved_instr[]放的是被断点指令所替换的真实指令.大家也知道一般调试器的实质就是在断点对于的内存地址中把原来的指令或部分指令替换成一条断点指令,对于x86就是"int 3",这条指令要能让系统进入被gdb控制的状态.type对应上面所说的'Z'命令的type字段,state标志每个断点的状态4中状态:

(include/linux/kgdb.h)
83 enum kgdb_bptype {
84 BP_BREAKPOINT = 0,
85 BP_HARDWARE_BREAKPOINT,
86 BP_WRITE_WATCHPOINT,
87 BP_READ_WATCHPOINT,
88 BP_ACCESS_WATCHPOINT
89 };
90
91 enum kgdb_bpstate {
92 BP_UNDEFINED = 0,
93 BP_REMOVED,
94 BP_SET,
95 BP_ACTIVE
96 };

在kgdb_break[]被初始化时state成员都被设置成BP_UNDEFINED,表明这个数组元素可用,当gdb设置一个断点时,一个数组元素会被设成BP_SET,相反取消断点时则把相应的数组项设成BP_REMOVED.当断点被激活时元素被设置为BP_ACTIVE.


几个断点状态的重要切换点:
1. BP_UNDEFINED/BP_REMOTED -> DB_SET 和 BP_SET -> BP_REMOVED分别发生在收到gdb的'Z'和'z'指令时.由函数kgdb_set_sw_break()和 kgdb_remove_sw_break()实现
2. BP_SET -> DB_ACTIVE 一个断点处于BP_SET状态并没有生效,只是说明这个断点是有效的.当gdb发来's'或'c'命令恢复原来程序执行时才会让断点变成 BP_ACTIVE.函数kgdb_activate_sw_breakpoints()实现了这个状态转换.
3. BP_ACTIVE -> BP_SET 发生在进入和gdb_serial_stub()之前,先把断点都失效在与远端的gdb通信,在讲主函数的时候几经提过.
4. 其它状态 -> DB_UNDEFINED 函数remove_all_break()完成这个功能,它主要是在远程gdb与kgdb断开链接时,即进行'D','k','C15'等命令时kgdb 对所有断点进行重置.

下面具体看看kgdb是怎样设置断点的:

(kernel/kgdb.c)
616 /*
617 * SW breakpoint management:
618 */
619 static int kgdb_activate_sw_breakpoints(void)
620 {
621 unsigned long addr;
622 int error = 0;
623 int i;
624
625 for (i = 0; i < KGDB_MAX_BREAKPOINTS; i++) {
626 if (kgdb_break[i].state != BP_SET)
627 continue;
628
629 addr = kgdb_break[i].bpt_addr;
630 error = kgdb_arch_set_breakpoint(addr,
631 kgdb_break[i].saved_instr);
632 if (error)
633 return error;
634
635 kgdb_flush_swbreak_addr(addr);
636 kgdb_break[i].state = BP_ACTIVE;
637 }
638 return 0;
639 }

kgdb_arch_set_breakpoint()是一个和体系结构相关的函数,但是暂时在内核代码里面只有一个地方实现了,这里所做的就是读取断点地址对应的指令并保存起来,然后用一条可以触发断点的指令替换这段内存:

(kernel/kgdb.c)
167 /*
168 * Weak aliases for breakpoint management,
169 * can be overriden by architectures when needed:
170 */
171 int __weak kgdb_arch_set_breakpoint(unsigned long addr, char *saved_instr)
172 {
173 int err;
174
175 err = probe_kernel_read(saved_instr, (char *)addr, BREAK_INSTR_SIZE);
176 if (err)
177 return err;
178
179 return probe_kernel_write((char *)addr, arch_kgdb_ops.gdb_bpt_instr,
180 BREAK_INSTR_SIZE);
181 }

这里出来一个有趣的单词__weak,它是一个宏,和__packed是同一种东西都是gcc的扩展属性:

#define __packed __attribute__((packed))
#define __weak __attribute__((weak))

如果这个关键字用在函数定义上面,一般情况下和一般函数没有两样。但是当有一个同名函数但是不带__weak被定义时,所有对这个函数的调用都是指向后者 (不带__weak那个),如果有两个一样的函数都用了__weak,那么真正调用那个,就要看连接器了。
设置断点的过程其实就是像大家想像的那样,把相应地址的指令读上来并保存起来,把一条断点指令写到这个地址上去.
关于断点的函数像kgdb_set_sw_break(),kgdb_remove_sw_break()这些函数的实现都是直接了当的,这里就不一一说明了.

4.2 continue 和 step
'c'(continue)和's'(step)命令就稍微复杂一点。
(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在远程协议中的意义:
$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这两个动作的.再看看邮件列表上面的同志是怎样说的:

> 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 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'处理:

(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'(用户的)我们可以看到下面的输出:
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: 1500000058297c2300000000e887050898cff7c1b4cff7c10000edc00000000080ae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
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: G1500000058297c2300000000e887050898cff7c1b4cff7c10000edc0000000007fae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
Nov 21 16:46:09 localhost kernel: kgdb out: OK
Nov 21 16:46:09 localhost kernel: kgdb in: G1500000058297c2300000000e887050898cff7c1b4cff7c10000edc0000000007fae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
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: 1500000058297c23a4cff7c1e887050898cff7c1b4cff7c10000edc00000000082ae18c08203200060000000680000007b0000007b000000ffff0000ffff0000
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的通信。(一次通信可以完成很多条命令)
我们把关注的指令按顺序摘出来:
kgdb in: g
kgdb out: 1500000058297c2300000000e887050898cff7c1b4cff7c10000edc00000000080ae18c08202200060000000680000007b0000007b000000ffff0000ffff0000
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定义了每个寄存器的含义.

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'把原来两个断点加上.然后继续单步调试.
总结一下这个过程:
1.gdb获得当前pc寄存器值,并把它减一.
2.取消所有断点.
3.单步调试原来断点位置的指令.
4.单步调试引起的debug中断让kgdb在次拿到控制权,kgdb再把所有断点加回去.
5.后面继续单步调试.
'c'命令的过程和's'的过程差不多,只是在第5步不会继续单步而是'c'继续执行.
一句话,gdb控制了整个过程.

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

>>>>>>>>>>5 初始化时机.>>>>>>>>>>
看完上面这些内容,难免有人会问,那kgdb在什么时候起来的呀?假如我们用"kgdboc=ttyS0,115200 kgdbwait"作为启动参数.就在 drivers/serial/kgdboc.c 中的module_init(init_kgdboc)里面.就这样完了吗?那内核究竟在启动的那个阶段对kgdb有反应?这里就扯远一点,暂时离开一下 kgdb.我们从module_init开始,它的定义在include/linux/init.h

259 #define module_init(x) __initcall(x);

204 #define __initcall(fn) device_initcall(fn)

199 #define device_initcall(fn) __define_initcall("6",fn,6)

159 /* initcalls are now grouped by functionality into separate
160 * subsections. Ordering inside the subsections is determined
161 * by link order.
162 * For backwards compatibility, initcall() puts the call in
163 * the device init subsection.
164 *
165 * The `id' arg to __define_initcall() is needed so that multiple initcalls
166 * can point at the same handler without causing duplicate-symbol build errors.
167 */
168
169 #define __define_initcall(level,fn,id) /
170 static initcall_t __initcall_##fn##id __used /
171 __attribute__((__section__(".initcall" level ".init"))) = fn

(include/linux/init.h)
135 typedef int (*initcall_t)(void);

一个程序编译好之后有代码段,只读数据段,数据段等一些段,而__attribute__((__section__(...)))这个gcc的编译扩展属性,它能够把函数或数据放入指定名字的段中。
那__define_initcall这一团是什么意思呢?首先这是一个定义和初始化的语句,initcall_t是一个函数指针类型,所以这里定义一个静态的函数指针,它的名字是__initcall_##fn##id,"##"在宏里面是连接两个字符串,所以这个变量名是根据进来的参数名字不一样而不同.并且把传进来fn(我们这里就是init_kgdboc())赋给这个变量。最重要的是把这个变量放在".initcall" level ".init"段中,我们的level是6,所以定义的这个函数指针放在.initcall6.init这个段中。

连接器允许我们通过连接脚本修改默认段的起始位置,段的内容等很多东西,先来看看x86的脚本arch/x86/kernel /vmlinux_32.lds.S。从名字我们大概可以猜出我们上面的那些段对应于这个文件中的:

133 .initcall.init : AT(ADDR(.initcall.init) - LOAD_OFFSET) {
134 __initcall_start = .;
135 INITCALLS
136 __initcall_end = .;
137 }

连接脚本里面,'.'代表当前位置,所以__initcall_start和__initcall_end分别代表.initcall.init段的开始位置和结束位置,这种东西可以理解为一个常量,后者我们下面马上会用到。
而INITCALLS则在include/asm-generic/vmlinux.lds.h里面定义:

363 #define INITCALLS /
364 *(.initcallearly.init) /
365 VMLINUX_SYMBOL(__early_initcall_end) = .; /
366 *(.initcall0.init) /
367 *(.initcall0s.init) /
368 *(.initcall1.init) /
369 *(.initcall1s.init) /
370 *(.initcall2.init) /
371 *(.initcall2s.init) /
372 *(.initcall3.init) /
373 *(.initcall3s.init) /
374 *(.initcall4.init) /
375 *(.initcall4s.init) /
376 *(.initcall5.init) /
377 *(.initcall5s.init) /
378 *(.initcallrootfs.init) /
379 *(.initcall6.init) /
380 *(.initcall6s.init) /
381 *(.initcall7.init) /
382 *(.initcall7s.init)

像__initcall_start和__initcall_end一样,__early_initcall_end的值也是段里面的一个位置。
上面两段话合起来就是说在连接内核时,把所有放在中间文件(输入文件)中的.initcallxx.init段里面的东西都连接到输出文件的.initcall.init段中。
所以我们刚才的那个函数指针最后会在目标文件的.initcall.init段中.那现在问题是谁会去管这个段? 它就是do_initcalls

(init/main.c)
749 static void __init do_initcalls(void)
750 {
751 initcall_t *call;
752
753 for (call = __early_initcall_end; call < __initcall_end; call++)
754 do_one_initcall(*call);
755
756 /* Make sure there is no pending stuff from the initcall sequence
*/
757 flush_scheduled_work();
758 }

do_initcalls()遍历一次放在__early_initcall_end和__initcall_end之间的函数指针,并依次调用它们指向的函数,当然包括我们的init_kgdboc()了。那do_initcalls()又是在那里被调用了呢?

start_kernel()
'->rest_init()
'->kernel_init()
'->do_basic_setup()
'->do_initcalls()

这样清楚了吧,do_initcalls是在init进程里面才被调用的。init_kgdboc()所做的事情由于本人知识有限,暂时说不出来,不过它所干的最重要的两件事情是:根据内核启动参数寻找对应的驱动和在初始化完成后直接执行一条asm("int $3")汇编手动触发一个中断,把控制权交给kgdb。
阅读(973) | 评论(0) | 转发(0) |
0

上一篇:gdb详解

下一篇:SQLite学习整理

给主人留下些什么吧!~~