KGDB原理分析及远程挂载调试ARM64内核

为什么使用KGDB

我们在开发或者是调试驱动过程中,往往会通过串口的打印日志来分析驱动运行逻辑,定位问题。但是这种方法需要我们增加大量的打印日志来实现,有时候日志加的地方不对,可能打印不出来,就要重新加,编译驱动烧录,这个过程是很痛苦的。那有没有一种方法,可以实现驱动的断点调试呢?答案是有的,可以使用KGDB在内核态进行源码级别的调试。

KGDB原理分析

简介

kgdb是内核提供的一种内核调试程序,kgdb将自身代码插入在内核中,编译后,与内核一起作为一个整体的程序运行。所以kgdb本身运行在内核空间,因此kgdb可以访问内核空间或者用户空间,获取相应的被调试程序的数据信息。在2.6.25以后的内核版本,kgdb已经被整合到内核中,是内核源码的一部分,要开启kgdb调试,只需要添加对应的kgdb配置选项即可。

kgdb调试的连接通信过程和gdbserver类似,需要远程服务器的gdb配合。target和host之间通过串口或网络连接。target运行配置了kgdb的内核,host运行gdb,中间使用RSP协议进行通信。gdb向kgdb发送RSP命令包,kgdb解析命令包,从而实现target的内核调试。调试模型见下图:

kgdb+gdb
调试模型
HOST
TARGET
RSP协议
串口或网络
GDB
KGDB
硬件
硬件

调试原理

kgdb是一种stub调试程序,它是Linux内核的一部分,会和内核一起编译运行。那么Linux内核运行的好好的,kgdb如何打断Linux内核获取CPU控制权呢?在获取到CPU控制权,进入到kgdb源码后又会做一些什么事情?

kgdb其实是在异常通知链 (die_notifier)中注册自己的回调函数 kgdb_notify,在step_hook以及break_hook里注册钩子函数,捕获Linux内核产生的异常,实现对Linux的控制。注册代码如下所示:

//arm64的注册oops异常代码
static struct notifier_block kgdb_notifier = {
	.notifier_call	= kgdb_notify,
	/*
	 * Want to be lowest priority
	 */
	.priority	= -INT_MAX,
};
register_die_notifier(&kgdb_notifier);

static struct break_hook kgdb_brkpt_hook = {
	.esr_mask	= 0xffffffff,
	.esr_val	= (u32)ESR_ELx_VAL_BRK64(KGDB_DYN_DBG_BRK_IMM),
	.fn		= kgdb_brk_fn
};

static struct break_hook kgdb_compiled_brkpt_hook = {
	.esr_mask	= 0xffffffff,
	.esr_val	= (u32)ESR_ELx_VAL_BRK64(KGDB_COMPILED_DBG_BRK_IMM),
	.fn		= kgdb_compiled_brk_fn
};

static struct step_hook kgdb_step_hook = {
	.fn		= kgdb_step_brk_fn
};

register_break_hook(&kgdb_brkpt_hook);
register_break_hook(&kgdb_compiled_brkpt_hook);
register_step_hook(&kgdb_step_hook);

可看出arm64,是把 kgdb_notify注册到了最低优先级的位置,内核发生oops异常后,会先处理正常的异常处理函数,完成后才会执行kgdb异常处理函数,kgdb响应完成后,会接着正常往下运行,等待下次异常触发。

由于kgdb捕获了所有的Linux内核异常,所以在启用kgdb的情况下,内核崩溃后,会进入kgdb,等待开发者收集异常信息。其工作流程如下所示:

与HOST GDB通信
RSP协议处理过程
g
G
m
M
z
Z
...
c
s
向Host发送当前异常的线程信息
kgdb_serial_stub

C
P
U


解析RSP

C
P
U
























.
.
.






将信息回复HOST GDB
接收gdb命令包
kgdb_arch_handle_exception
c
s
解析RSP包的PC地址
取消DBG_MDSCR_SS
设置DBG_MDSCR_SS
获取CPU控制权
KGDB获取CPU控制权
KGDB获取CPU控制权
KGDB获取CPU控制权
oops
遍历通知回调函数
CPU oops
异常处理函数
notify_die
进入KGDB异常回调函数kgdb_notify
kgdb_handle_exception
brk
遍历brk_hook
CPU brk
brk_hook
进入KGDB break hook
step
遍历step_hook
CPU step
step_hook
进入KGDB step hook
触发方式
kdgbwait
sysrq -g
内核挂死
断点
单步调试
退出KGDB 等待下次异常触发

可看出,kgdb共有5种触发方式,其中3种是首次触发KGDB的方式:

  1. 内核挂死后自动触发
  2. 内核正常运行时,使用 echo g > /proc/sysrq-trigger手动触发。这里其实就是使用 sysrq执行了一个 bkpt的汇编指令,触发了CPU异常,从而进入kgdb。
  3. 在启动参数中加入 kgdbwait字段,可以在内核启动时触发。

另外的断点和单步调试触发方式是进入到kgdb后,打断掉或者单步调试触发的。

其工作机制决定了使用kgdb调试的过程是被动调试:即Host的GDB不能主动暂停内核的运行,只有target自己主动触发异常,Host的GDB才可以通过kgdb接管内核。

部分RSP命令的解析

m/M命令

m/M命令读/写内存的命令,kgdb收到该命令后,会解析协议包中的要读/写的地址:addr和数量:count,最终会调用到 probe_kernel_read()/probe_kernel_write()函数。这两个函数其实是在内核态访问内存地址的函数。所以kgdb当前只能返回内核态的内存数据给HOST GDB,对于硬件寄存器的数据,就没法返回了,不过可以根据地址分布去拓展这个指令。

Z/z命令

Z/z命令是设置/移除断点的命令,kgdb收到该命令后,会解析协议包中的要设置/移除断点的PC地址:addr。随后会调用 kgdb_arch_set_breakpoint()/kgdb_arch_remove_breakpoint()函数设置/移除断点。函数原型如下所示:

int kgdb_arch_set_breakpoint(struct kgdb_bkpt *bpt)
{
	int err;

	BUILD_BUG_ON(AARCH64_INSN_SIZE != BREAK_INSTR_SIZE);

	err = aarch64_insn_read((void *)bpt->bpt_addr, (u32 *)bpt->saved_instr);
	if (err)
		return err;

	return aarch64_insn_write((void *)bpt->bpt_addr,
			(u32)AARCH64_BREAK_KGDB_DYN_DBG);
}

int kgdb_arch_remove_breakpoint(struct kgdb_bkpt *bpt)
{
	return aarch64_insn_write((void *)bpt->bpt_addr,
			*(u32 *)bpt->saved_instr);
}

可看出,设置断点其实就是在 addr处添加 AARCH64_BREAK_KGDB_DYN_DBG,即 bkpt异常。当程序走到该地址时,就会触发break异常,进入到break的钩子函数,,从而kgdb获得控制权,继续和HOST GDB通信。在设置断点时,kgdb会把 addr处的值取出存起来,移除断点就是将先前 addr的值恢复。

s命令

s命令用于响应HOST GDB的s/n指令,即GDB单步调试的步入和步出指令。最终会调用到平台相关的 kgdb_arch_handle_exception()函数中。当HOST GDB执行步入指令时,HOST GDB发送的RSP协议包中的 addr部分会是0。当HOST GDB执行步出指令时,HOST GDB发送的RSP协议包中的 addr部分会是步出后的程序地址。kgdb会根据addr的值来判断是步入还是步出,具体函数如下:

static void kgdb_arch_update_addr(struct pt_regs *regs,
				char *remcom_in_buffer)
{
	unsigned long addr;
	char *ptr;

	ptr = &remcom_in_buffer[1];
	if (kgdb_hex2long(&ptr, &addr))
		kgdb_arch_set_pc(regs, addr);
	else if (compiled_break == 1)
		kgdb_arch_set_pc(regs, regs->pc + 4);

	compiled_break = 0;
}
void kgdb_arch_set_pc(struct pt_regs *regs, unsigned long pc)
{
	regs->pc = pc;
}

可看出,当解出的addr为0时,kgdb会将 pc+4的值存到 regs->pc中。当addr不为0时kgdb会将 addr的值存到 regs->pc中。以arm64平台为例,之后会执行 kernel_enable_single_step()函数对处的地址加上 DBG_MDSCR_SS(step异常)。当走到对应的程序地址处便会触发step异常,kgdb获得控制权,继续与HOST KGDB通信。

void kernel_enable_single_step(struct pt_regs *regs)
{
	WARN_ON(!irqs_disabled());
	set_regs_spsr_ss(regs);
	mdscr_write(mdscr_read() | DBG_MDSCR_SS);
	enable_debug_monitors(DBG_ACTIVE_EL1);
}

这里需要注意一下,触发step异常时,某些ARM64平台的CPU会一直挂死在 el1_irq里无法退出,无法继续调试,原因未知。具体代码如下:

el1_irq:
	kernel_entry 1
	enable_da_f
#ifdef CONFIG_TRACE_IRQFLAGS
	bl	trace_hardirqs_off
#endif

	irq_handler

#ifdef CONFIG_PREEMPT
	ldr	x24, [tsk, #TSK_TI_PREEMPT]	// get preempt count
	cbnz	x24, 1f				// preempt count != 0
	bl	el1_preempt
1:
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
	bl	trace_hardirqs_on
#endif
	kernel_exit 1

为了解决这个问题,我在内核社区找到了一个补丁,不过该补丁未合进内核,补丁的作用是在kgdb单步调试时禁用中断,等单步调试完成后恢复中断。具体修改如下:

int kgdb_arch_handle_exception(int exception_vector, int signo,
			       int err_code, char *remcom_in_buffer,
			       char *remcom_out_buffer,
			       struct pt_regs *linux_regs)
{
	int err;

	switch (remcom_in_buffer[0]) {
	//前面的代码省略,只看修改内容...........
	case 's':
		/* 单步调试时禁用中断 */
		__this_cpu_write(kgdb_pstate, linux_regs->pstate);
		linux_regs->pstate |= PSR_I_BIT;

		/*
		 * Update step address value with address passed
		 * with step packet.
		 * On debug exception return PC is copied to ELR
		 * So just update PC.
		 * If no step address is passed, resume from the address
		 * pointed by PC. Do not update PC
		 */
		kgdb_arch_update_addr(linux_regs, remcom_in_buffer);
		atomic_set(&kgdb_cpu_doing_single_step, raw_smp_processor_id());
		kgdb_single_step =  1;

		/*
		 * Enable single step handling
		 */
		if (!kernel_active_single_step())
			kernel_enable_single_step(linux_regs);
		err = 0;
		break;
	default:
		err = -1;
	}
	return err;
}

static int kgdb_step_brk_fn(struct pt_regs *regs, unsigned int esr)
{
	unsigned int pstate;

	if (user_mode(regs) || !kgdb_single_step)
		return DBG_HOOK_ERROR;

	if (kernel_active_single_step())
		kernel_disable_single_step();

	/* 单步调试后恢复中断 */
	pstate = __this_cpu_read(kgdb_pstate);
	if (pstate & PSR_I_BIT)
		regs->pstate |= PSR_I_BIT;
	else
		regs->pstate &= ~PSR_I_BIT;

	kgdb_handle_exception(0, SIGTRAP, 0, regs);
	return DBG_HOOK_HANDLED;
}

如何使用KGDB

内核启用KGDB

在2.6.25以后的内核版本,kgdb已经被整合到内核中,是内核源码的一部分,开启KGDB只需要启用如下内核配置选项:

CONFIG_KGDB=y
CONFIG_KGDB_KDB=y
CONFIG_DEBUG_INFO=y
CONFIG_GDB_SCRIPTS=y
CONFIG_DEBUG_INFO_DWARF4=y
configdesc
CONFIG_KGDB=y使能KGDB
CONFIG_KGDB_KDB=y使能KGDB_KDB
CONFIG_DEBUG_INFO=y生成debug信息
CONFIG_DEBUG_INFO_DWARF4=y生成等级4的debug信息
CONFIG_GDB_SCRIPTS=y生成linux GDB调试脚本

重新编译内核,烧入设备。

修改内核启动参数

在内核启动参数后面加入 nokaslr kgdboc=ttyAMA0,115200

KGDB与Server的远程连接

在上文中,我们了解到,KGDB是使用串口和HOST连接的。但是当我们是PC连接Server开发的话,我们的设备只能通过串口跟我们的PC机连接,无法连接至开发的Server。但是我们PC机又无法运行GDB连接target。为了解决这个矛盾,可以在PC机上将与板子相连的串口虚拟成TCP端口,编译服务器端运行GDB,远程连接PC机虚拟出的TCP端口进行调试。具体流程如下:

TARGET
SERVER
RSP协议
ETH
串口
KGDB
硬件
GDB
硬件
PC
串口
转发
TCP

示例

设备启动后,进入根文件系统。设备端执行步骤:

  1. 关闭看门狗
  2. 使用sysrq触发异常 echo g > /proc/sysrq-trigger
  3. 输入kgdb等待远程主机调试 kgdb

PC端执行步骤:
如果是使用服务器开发的话,使用虚拟串口软件将PC连接设备的com口转发为tcp端口,

  1. 设置监听端口
  2. 设置串口
  3. 设置波特率
  4. 开启TCP转发服务
    服务器运行gdb直接target remote pc_ip:port,其中pc_ip是自己电脑的本地ip,port是com口转tcp的端口号

就跟正常使用gdb是一样的,功能很强大,当调试驱动时,可以在server端gdb连接上设备kgdb后使用内核调试脚本自带的工具lx-symbols将要调试的ko符号表链接到vmlinux后,进行源码级调试。

Linux内核GDB Script命令集简介

命令功能
lx-cmdline显示当前内核的命令行
lx-cpus列出CPUS当前状态
lx-dmesg打印内核日志,类似直接在shell敲dmesg
lx-fdtdumpdump设备树到文件
lx-iomem打印当前iomem资源
lx-ioports打印当前ioport资源
lx-list-check验证列表一致性
lx-lsmod列出当前插入的ko
lx-mounts显示当前VFS的挂载点
lx-psdump当前linux的tasks
lx-symbols加载ko符号表到当前调试内核
lx-version显示当前内核的版本信息
lx_current显示当前正在运行的task
lx_module按名称查找模块
lx_per_cpu查看per cpu变量
lx_task_by_pid通过pid寻找task结构体指针
lx_thread_info通过task指针查看thread信息
lx_thread_info_by_pid通过pid来查看thread信息
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值