kgdb模块移植分析文档

本文详细分析了KGDB(Kernel GDB)在X86体系结构下的内核调试原理,包括其初始化、异常捕获和主控制模块。KGDB通过插入到内核中,利用异常处理机制获取内核控制权,与GDB配合进行远程调试。文章还探讨了KGDB在c-sky嵌入式平台上的移植方案,重点在于体系结构相关的kgdb.c文件移植和异常处理函数的修改。

1.概述

本文通过分析X86KGDB内核调试实现机制,理清KGDB内核调试的原理。最后给出KGDB移植至c-sky的可行性方案。

本文后续分为两个章节。第二章主要结合代码分析分析X86体系结构下,KGDB内核调试的实现机制。第三章结合第二章的分析对比,提出KGDB内核调试在c-sky嵌入式平台上的移植的实现方案。

2.原理分析

2.1简介

KgdbLinux内核本身提供的一种内核调试stub程序,kgdb通过将自身代码插入在内核中,编译后,一起作为一个整体的程序运行。所以kgdb本身运行在内核空间,因此kgdb可以访问内核空间或者用户空间,获取相应的被调试程序的数据信息。在linux-2.6.25之前kgdb作为内核的补丁存在,要使用kgdb调试,需要找到相应版本的kgdb,然后把补丁编译进linux内核;linux-2.6.25版本之后,kgdb被整合到linux内核中,要使用kgdb调试的时候只需要在配置内核的时候勾选相应的kgdb选项即可。

Kgdb作为内核调试的调试代理,与调试linux应用程序的调试代理程序gdbserver类似,需要和gdb配合,通过远程调试协议,一起完成调试。使用kgdb调试内核需要两台机器,一台作为host机,另一台作为target机,两台机器通过串口或者网络连接起来。在target机上运行配置了kgdb的待调试linux内核,在host机上运行gdbGdbkgdb采用RSP远程通信协议(RSP协议不作具体介绍,可参考相关资料),gdbkgdb发送相应的命令包,从kgdb端获取内核运行的数据信息包,从而实现对目标机上的linux内核进行控制,实现远程调试。Kgdb的调试模型如图2-1所示。

 

 

        

图2‑1 kgdb调试模型

 

2.2调试原理

2.1 kgdb简介一节中讲到kgdb是一种stub调试程序,将自身的代码插入到待调试的linux内核中,然后一起编译运行。那么linux内核运行得好好的,怎么保证一定能运行到kgdb的代码中去呢?运行到kgdb代码时kgdb会做一些什么事情?运行到kgdb代码时整个系统的运行状况又如何呢?

kgdb通过捕获掌管所有的异常来实现对linux内核的控制。具体机制如下:kgdb通过修改linux内核的异常处理程序,将自己的代码插入到异常处理程序中,这样内核发生异常,进入到异常处理程序中,由于kgdb把自己的代码插入到异常处理程序的前面部分,那么会先进入kgdb的代码(kgdb只接管内核态产生的异常,用户态的异常不会进入kgdb的主代码,只是进行正常的异常处理),待kgdb处理完退出以后,回到异常处理程序继续进行异常处理。打个比方,kgdb就相当于中断服务程序,它恢复被调试内核的过程就相当于是中断返回,在中断返回之前,由kgdb一直占用着CPU,接收调试器的命令,处理命令,返回命令结果的工作。Kgdb的调试流程如图2-2所示:

 

 

2‑2 kgdb调试流程

2.3模块分析

kgdb的代码按功能划分主要可以分为两部分:初始化模块和主控制模块。

主控制模块功能就是初始化模块将kgdb初始化以后,内核发生异常,进入kgdbkgdbhost机上运行的gdb进行通信,主控制模块负责kgdbgdb的通信,负责从gdb接收命令包、解析执行命令包并且将执行结果返回给gdb

2.3.1初始化模块

初始化模块主要包括异常捕捉初始化和底层实现初始化。异常捕捉初始化所做的工作就是修改异常的处理函数,使得内核产生异常能被kgdb捕捉,进而使得kgdb获取内核的控制权;底层实现初始化指的是host机和target机的物理连接可以通过网络或者串口,在内核启动的参数里面可以配置选择kgdbockgdb over console)或者kgdboekdgb over erthnet),底层实现初始化就是注册相关的驱动。

kgdb初始化模块的介绍主要从初始化的时机和初始化的内容进行阐述。在介绍kgdb初始化的时机前,我们先对linux内核的启动初始化过程做一个简要的回顾,如图2-3所示:

2‑3 linux内核启动简析

异常捕获的时机就在start_kernel()->trap_init()初始化函数执行的时候。该函数实现体在体系结构目录下,如/arch/x86/kernel/traps.c。该文件中实现了各种异常的处理函数。Kgdb对异常的捕获就是对异常处理函数的修改。Kgdb可以捕获任意感兴趣的异常,比如在x86体系结构下,kgdb对所有的异常都进行捕获。具体捕获异常处理的方法就是在异常处理函数中插入kgdb的函数,这样异常一发生就能进入kgdbkgdb获取控制权。

Kgdb在异常处理函数中插入自己的入口函数采用了一种回调函数链的机制。在内核中有一种die_chain的回调函数链,该链是个数据结构链表的数据结构包括回调函数指针和优先级。Kgdb也把自己的回调函数注册在里面。而函数notify_die()就是按照优先级遍历执行die-_chain中的回调函数。只要在异常处理函数中插入notify_die()函数,那么kgdb就可以实现对异常的捕获。下面通过x86异常捕获的代码直观看看这个异常捕获机制。图2-4x86kgdb的回调函数注册数据结构。图2-5是异常处理函数中插入notify_die()函数。

 

2‑4异常事件回调函数链数据结构

 

 

2‑5 kgdb修改异常处理函数

 

如图4,可知x86的异常事件的回调函数是kgdb_notify()函数,该函数对异常进行判断,如果是内核态异常,那么进入kgdbkgdb_handle_exception()函数,如果不是则进行正常的异常处理。通过psr寄存器的特权位进行判断。

kgdb回调函数的注册的时机则是与底层实现初始化同时进行的,在进行底层io模块初始化的同时注册kgdb的回调函数。调用关系如图2-6所示。

 

2‑6 kgdb异常事件回调函数注册时机

下面介绍底层实现的初始化,底层连接通过串口或者网口,二者的驱动在linux内核中都有实现,所以我们的初始化就是根据kgdb的启动参数进行注册。如kgdb启动参数是配置串口,那么只要调用init_kgdboc()函数即可。此初始化函数的时机在start_kernel()->rest_init()->do_basic_setup()->do_initcalls()函数完成。

Do_initcalls()函数从名字看得出来其作用就是实现一系列的初始化。Do_initcalls()的代码如图7所示。该函数遍历执行从__early_initcall_end开始到__initcall_end结束的函数指针数组。而init_kgdboc()就是该数组中的元素。

2‑7 do_initcalls()函数

下面问题就是,该数组的是怎么形成的呢?Init_kgdb()函数指针是怎样放到这个数组中去的?先看如图8的一段代码。从该段代码中将相应的函数指针fn(如init_kgdboc)赋值给一个特殊属性特殊格式的变量*.initcall.init

 

2‑8初始化函数指针数组生成-1

赋值好这一系列的特定属性,特定格式的变量后,我们就可以通过链接描述文件,将所有符合这个格式的函数指针放到一个连续的数据段中。链接描述文件如图9所示,这样就形成了__early_initcall_end开始到__initcall_end结束的指针数组。

 

2‑9初始化函数指针数组生成-2

 

2.3.2主控制模块

控制模块主要完成kgdbgdb的通信,接收命令、解析执行命令、将命令的执行结果返回。这里kgdbgdb通过rsp协议进行通信,具体的rsp协议不作具体介绍了。

KGDB获取内核控制权进入kgdb_handle_exception()函数后,首先进行一系列的关于多CPU的操作,然后进入while1)循环,执行gdb_serial_stub(ks)函数,该函数完成与gdb的通信。该函数的主要流程如图2-10所示。

 

2‑10 kgdb主控制模块逻辑 

2.4的断点机制和单步机制

2.4.1 kgdb的断点机制

Kgdb的断点和普通应用程序调试一样,是通过bkpt指令实现的。将欲设断点处的指令替换成bkpt指令,当内核执行到bkpt指令时,发生bkpt异常,跳转到相应的异常处理函数。X86体系结构的bkpt指令为do $int 3,执行后产生异常,内核跳转到异常处理函数中,如图11所示。异常处理函数中采用2.3.1节介绍的异常捕获机制。

 

 

2‑11断点异常处理

2.4.2 kgdb的单步机制

单步操作分为硬单步和软单步。硬单步,顾名思义,由cpu硬件支持的指令单步调试,即cpu,每执行一条指令就会产生一个单步异常,然后进入异常处理函数,利用2.3.1节的异常捕获机制,异常被kgdb捕获,kgdb获得控制权。

X86的硬件单步实现靠EFLAGS寄存器,该寄存器的TF位来设置CPU的单步模式。当kgdb执行单步命令时,会把pt_regs中的EFLAGS寄存器的TF位置位。Kgdb运行结束返回,内核回到异常处理函数,执行完异常处理后,内核返回异常发生处,并且把进程栈中的pt_regs pop出来到cpu的寄存器中,此时CPUEFALGS寄存器的TF位物理上被置位,CPU进入单步模式,CPU执行一条指令后产生单步异常。实现硬单步。

软单步即某些体系结构不支持硬件单步,如arm的某些cpu。这时就采用软单步的方式,就是在pc的下一条地址处设置一个断点,即把下一条指令换成bkpt指令,那么程序执行一条指令后,就会碰到bkpt,产生一个bkpt异常,被kgdb捕获,kgdb获得控制权,重新进入调试模式。

3.kgdb的移植分析

Kgdb的代码结构主要可以分为三部分:内核调试主代码,由内核调试目录下debug_core.c文件实现;体系结构相关函数,由体系结构目录下的kgdb.c实现;远程RSP协议,由内核调试目录下的gdbstub.c文件实现。所以移植的部分主要是与体系结构相关的kgdb.c文件。

Kgdb移植的文件列表:

表格3‑1 kgdb移植文件列表

文件名

说明

/arch/*/kernel/kgdb.c

kgdb体系结构相关数据结构与函数

/arch/*/include/asm/kgdb.h

Kgdb体系结构头文件

/arch/*/include/asm/kdebug.h

主要用来定义kgdb关心的异常

/arch/*/kernel/traps.c

修改异常处理函数

 

而上述文件中,traps.c文件只需要修改相应的异常处理函数即可。Kdebug.h是体系结构相关的一个头文件,其主要功能就是定义体系结构下kgdb关心的异常,通过一个枚举型数组定义。

kgdb.ckgdb.h需要在体系结构目录下创建,以实现相应的函数接口。Kgdb.c需要实现的接口在/include/linux/kgdb.h头文件中有定义。列表如下:

 

表格3‑2 kgdb移植函数接口列表

函数名称

参数

说明

Kgdb_skipexception

Int (异常向量号)

Struct pt_regs*(当前pt_regs

在多cpu结构下,如果程序hit一个断点,而这个断点被其它cpu删除掉了,就采取skip操作

Kgdb_disable_hw_debug

Struct pt_regs*(当前pt_regs

禁止硬件调试

Kgdb_breakpoint()

Void

产生bkpt异常,内核主动让kgdb接管,进入调试模式

Kgdb_arch_init

Void

体系结构相关初始化(如注册kgdb回调函数)

Kgdb_arch_exit

Void

体系结构相关注销操作(如注销kgdb回调函数)

Pt_regs_to_gdb_regs

Unsigned long*gdb寄存器buffer

Struct pt_regs*(当前pt_regs

将内核的pt_regs转化成gdb的寄存器描述

Sleeping_thread_to_gdb_regs

Unsigned long*gdb寄存器buffer

Struct pt_regs*(当前pt_regs

将睡眠中的进程的pt_regs转化成gdb的寄存器描述

Gdb_regs_to_pt_regs

Unsigned long*gdb寄存器buffer

Struct pt_regs*(当前pt_regs

gdb顺序的寄存器转换成pt_regs

Kgdb_arch_handle_exception

Int vector(异常向量号)

Int signo(异常发生信号量)

Int error_code(错误码)

Char *remcom_in_buffer(从gdb读取的packet

Char *remcom_out_buffer(发送给gdbpacket

Struct pt_regs*(当前进程pt_regs

处理体系结构相关的gdb的命令包:至少得处理cs命令。

Kgdb_roundup_cpus

Unsigned long flags(当前IRQ状态)

SMP系统中,此函数让其它cpu进入holding pattern

Kgdb_arch_set_pc

Struct pt_regs*(当前进程pt_regs

Unsigned long pc(欲设置的pc值)

设置PC

Kgdb_arch_late

Void

处理体系结构相关的初始化。(此函数处理体系结构相关回调函数的初始化,此函数是可选的,通常用来处理一些像硬断点之类的稍微晚一些的初始化工作)

Kgdb_arch_pc

Int exception(异常号)

Struct pt_regs*(当前进程pt_regs

获取异常发生时的PC

__kgdb_notify

Struct die_args *(异常发生现场描述参数,包括pt_regs)

Unsigned long cmd(异常名称)

Kgdb异常事件回调函数的实现体

 

上述函数接口中并没有断点相关的,kgdb使用一个断点处理后端结构(hardware breakpoint layer)来管理断点相关的函数。此后端数据结构为struct kgdb_arch arch_kgdb_ops,数据项如下表,相应的函数接口都需要在kgdb.c中移植实现。

表格3‑3断点管理后端列表

数据项

说明

Uchar Gdb_bpt_instr[BREAK_INSTR_SIZE]

Bkpt指令

Unsigned long flags

硬断点标识

Int (*set_breakpoint)

设置断点

Int (*remove_breakpoint)

删除断点

Int*set_hw_breakpoint

设置硬断点

Int*remove_hw_breakpoint

删除硬断点

void (*remove_all_hw_break)

cpu结构中,删除所有cpu的硬断点

Void*correct_hw_break

恢复硬断点

 

4.kgdb问题解答

1、我们使用gdb调试内核时,可以随时使用Ctrl+C打断内核的运行,获取内核控制权,那么KGDB是否有类似的可以随时获取内核控制权的手段?

回答:在使用kgdb+gdb调试内核时,在gdbctrl+c只会中断kgdbgdbremote连接,然后内核退出调试模式,内核要进入调试模式,只能在root权限下,echo g >/proc/sysrq-triggers

2、一旦CPU有异常发生,KGDB都能通过一定的手段获取控制权。这样的机制有何意义?按理说KGDB只需要在发生bkpt异常时获得控制权就足够了。

回答:如果kgdb只关心bkpt异常的话,那么当内核执行出现异常时,就会进入内核的异常处理程序,那么这样的话出现的异常对于gdb来说就是不可见的,gdb为了更全面获取内核的运行信息,并且做出相应的操作。所以捕获所有的异常。

至于为什么不捕获中断,由于中断是外设产生的,和cpu执行内核程序没有很直接关系,kgdb只关心cpu对内核的执行,所以中断产生时只需进入系统的中断服务程序即可,不用通知gdb。举例,比如外设每次中断kgdb都捕获,那么调试就会经常被异步地打断,个人觉得这样子很不好。

3、每次异常发生KGDB获得控制权,会在异常返回之前执行一个已经注册好的回调函数链,该回调函数链何时注册?何时注销?

回答:

kgdb回调函数的注册在do_initcalls()->init_kgdboc()函数中完成,通过调用register_die_notifier()函数完成。

Init_kgdboc作为驱动模块注册到module_init()函数被初始化,那么cleanup_kgdboc注册在module_exit()中。在cleanup_kgdboc()函数中调用kgdb_unregister_io_module()->kgdb_unregister_callbacks()->kgdb_arch_exit()->unregister_die_nottify(&kgdb_notifier);kgdb的回调函数注销。

4、是否所有的异常共用同一个回调函数链die_chain,还是不同的异常各自有自己的回调函数链?

回答:die_chain是一个回调函数链,notify_die()函数的任务就是按回调函数的优先级遍历执行die_chain中的回调函数。每个异常都有自己的异常处理函数,如果该异常要被捕获那么在该异常处理函数中插入notify_die()函数即可。

5、KGDB硬单步利用CPUtrace模式实现,那么何时让CPU进入trace模式?是刚回到内核的异常处理函数时,还是从内核的异常处理函数退出时?

回答:kgdb对硬单步的实现,kgdb解析si命令,把pt_regsTF位置位,然后等kgdb结束后返回到异常处理程序,异常处理程序执行完把进程栈里的pt_regs pop到寄存器中,cpu进入单步模式。Cpu从前一次发生异常处执行一条指令后,产生debug异常,被kgdb捕获,kgdb获取控制权。

6、无法访问HAD寄存器的情况下,KGDB硬断点如何实现?

回答c-sky cpu对于硬断点的控制都是通过had寄存器实现的,硬断点本身要硬件支持,没有had模块,应该实现不了硬断点。

7、通过Echo g刚进入调试模式时,直接在KGDB命令行中输入si单步命令,此时被单步的是哪段程序?

回答:echo g后会调用到kgdbkgdb_breakpoint()函数,该函数中会执行一条bkpt的汇编指令,发生异常,进入kgdb。此时如果单步,那么单步的是发生bkpt的下一条指令。哪里发生异常,进入kgdb的,那么执行si命令的话就是单步发生异常处的下一条指令。

 

参考:

【1】李红卫 李翠萍 韩红宇 kgdb调试Linux内核的剖析与改进 软件天地 2004

【2】黄红燕 史烈 gdbstub的剖析与改进 应用系统 2006

【3】Linux系统内核的调试 IBM Developer Works

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值