Linux内核调试技术
嵌入式Linux系统移植之Linux内核调试技术
《嵌入式Linux应用完全开发手册》第3篇第18章总结归纳
本章目标
- 掌握几种调试内核的方法:printk、kgdb、分析Oops、栈回溯
- 使用调试工具:gdb、ddd
内核打印函数printk
printk的使用
printk函数的记录级别
调试内核、驱动最简单的方法,是使用printk函数打印信息。printk函数与用户空间的printf函数格式完全相同,它所打印的字符串头部可以加入“
在内核代码include/linux/kernel.h
中,下面几个宏控制了printk函数所能输出的信息的记录级别。
1 |
举例说明这几个宏的含义。
①对于printk(“
②假设default_message_loglevel的值小于4,如果printk的参数开头没有“
③minimun_console_loglevel是一个预设值,平时不起作用。通过其他工具来设置console_loglevel的值时,这个值不能小于minimun_console_loglevel。
④default_console_loglevel也是一个预设值,平时不起作用。它表示设置console_loglevel时的默认值,通过其他工具来设置console_loglevel的值时,会用到这个值。
minimun_console_loglevel和default_console_loglevel这两个值的作用,可以参考内核源文件kernel/printk.c
的do_syslog
函数。
上面代码中,console_printk是一个数组,它在kernel/printk.c
中定义:
1 | /* printk's without a loglevel use this.. */ |
在用户空间修改printk函数的记录级别
挂接proc文件系统后,读取/proc/sys/kernel/printk
文件可以得知console_loglevel、default_message_loglevel、minimun_console_loglevel和default_console_loglevel这4个值。
比如执行以下命令,它的结果是“7 4 1 7”表示这4个值。
1 | cat /proc/sys/kernel/printk |
也可以直接修改/proc/sys/kernel/printk
文件来改变这4个值,比如:
1 | echo "1 4 1 7" > /proc/sys/kernel/printk |
这使得console_loglevel被改为1,于是所有的printk信息都不会被打印。
printk函数记录级别的名称及使用
在内核代码include/linux/kernel.h
中有如下代码,它们表示0-7这8个记录级别的名称。
1 |
在使用printk函数时,可以这样使用记录级别;
1 | printk(KERN_WARNING"there is a warning here!\n"); |
串口控制台
串口与printk函数的关系
在嵌入式Linux开发中,printk信息常常从串口输出,这时串口被称为串口控制台。从内核kernel/printk.c
的printk函数开始,往下查看它的调用关系,可以知道printk函数是如何与具体设备的输出函数挂钩的。
printk函数调用的子函数的主要脉络如下:
1 | printk -> |
对于可以作为控制台的设备,在初始化时会通过register_console函数向console_drivers链表注册一个console结构,里面有write函数指针。
以drivers/serial/s3c2410.c
文件中的串口初始化函数s3c24xx_serail_initconsole为例,它的部分代码如下:
1 | static int s3c24xx_serial_initconsole(void) |
第4行的s3c24xx_serial_console就是console结构,它在相同的文件中定义,部分内容如下:
1 | static struct console s3c24xx_serial_console = |
第5行的CON_PRINTBUFFER表示注册这个结构之后,要把log_buf缓冲区中的所有信息打印出来。这表明,在实际的硬件被初始化之前,就可以使用printk函数,只不过这时的打印信息时保存在log_buf缓冲区中,还没有真正输出。
第7行的s3c24xx_serial_console_write是串口输出函数,它会调用s3c24xx_serial_console_putchar函数将要打印的字符一个一个的从串口输出。
s3c24xx_serial_console_putchar是最底层的函数,代码如下:
1 | static void |
从上面的代码可以知道,从串口输出printk打印信息时,是一个一个字符地发送、等待发送完成、发送、接着等待,…,效率很低。调试完毕后,通常需要将printk信息去掉。
设置内核命令行参数使用串口控制台
在使用U-Boot时,设置了命令行参数“console=ttySAC0”,它使得printk的信息从串口0输出。
内核是怎样根据这些命令行参数确定printk的输出设备呢,在kernel/printk.c
中有如下代码:
1 | __setup("console=",console_setup); |
内核开始执行时,发现形如“console=…”的命令行参数时,就会调用console_setup函数进行解析。对于命令行参数“console=ttySAC0”,它会解析出:设备名(name)为ttySAC,索引(index)为0,这些信息被保存在类型为console_cmdline、名称为console_cmdline的全局数组中。
在后面使用“register_console(&s3c24xx_serial_cosole)”注册控制台时,会将s3c24xx_serial_cosole结构与console_cmdline数组中的设备进行比较,发现名字、索引相同。
①s3c24xx_serial_console结构中名字(name)为S3C24XX_SERIAL_NAME,即“ttySAC”,而根据“console=ttySAC0”解析出来的名字也是“ttySAC”。
②s3c24xx_serial_console结构中索引为-1,表示使用命令行中解析出来的索引0,表示串口0。
综上所述,命令行参数“console=ttySAC0”决定printk信息将通过s3c24xx_serial_console结构中的相关函数,从串口0输出。
最后,既然printk输出的信息是先保存在缓冲区log_buf中,那么也可以读取log_buf以获得这些信息:系统启动后,想查看printk信息时,直接运行dmesg命令即可。通过其他非串口的手段(ssh、telnet)登录系统时,也可以使用dmesg查看printk信息。
内核源码级别的调试方法
内核调试工具KGDB的作用与原理
KGDB介绍
KGDB是一个源码级别的Linux内核调试器。使用KGDB调试内核时,需要结合GDB一起使用。它们使得调试内核就像调试调试应用程序一样,可以在内核代码中设置断点、一步一步地执行指令、观察变量的值。
使用KGDB时,需要两台机器,即主机和目标机,两者通过串口线相连。要调试的内核需要增加KGDB功能,它在目标机上运行,GDB在主机上运行。串口线被GDB用来与内核通信。
KGDB是一个内核补丁,目前支持i386、x86_64、ppc、s390、ARM等架构。将内核打上KGDB补丁后才能够使用GDB来调试。
KGDB的原理
安装KGDB调试环境需要为Linux内核加上kgdb补丁,补丁实现GDB远程调试所需要的功能,包括命令处理、陷阱处理及串口通信3个主要的部分。KGDB补丁的主要作用是在Linux内核中添加一个调试stub。调试stub是Linux内核中的一小段代码,是运行GDB的开发机和所调试内核之间的一个媒介。GDB和调试stub之间通过GDB串行协议进行通信。GDB串行协议是一种基于消息的ASCII码协议,包含了各种调试命令。当设置断点时,KGDB将断点的指令替换为一条trap指令,当执行到断点时控制权就转移到调试stub中去。此时,调试stub的任务就是使用远程串行通信协议将当前环境传送给GDB,然后从GDB处接收命令。GDB命令告诉stub下一步做什么,当stub收到继续执行的命令时,将恢复程序的运行环境,把对CPU的控制权重新交给内核。
KGDB补丁给内核添加以下3个部件。
- GDB stub
GDB stub被称为调试桩机(简称stub),是KGDB调试器的核心。它是Linux内核中的一小段代码,用来处理主机上GDB发来的各种请求;并且在内核处于被调试状态时,控制目标板上的机器。 - 修改异常处理函数
当这个异常发生时,内核将控制权交给KGDB调试器,程序进入KGDB提供的异常处理函数中。在里面,可以分析程序的各种情况。 - 串口通信
GDB和stub之间通过GDB串行协议进行通信。它是基于消息的ASCII码协议,包含了各种调试命令。
除串口外,也可以使用网卡进行通信。
以设置内核断点为例说明KGDB与GDB之间的工作过程。设置断点时,KGDB修改内核代码,将断点位置的指令替换成一条异常指令(在ARM中这是一条未定义的指令)。当执行到断点发生异常时,控制权将转移到stub的异常处理函数中。此时,stub的任务就是使用GDB串行通信协议将当前环境传送给GDB,然后从GDB接收命令,GDB命令告诉stub下一步该做什么。当stub收到继续执行的命令时,将恢复原来替换的指令、恢复程序的运行环境,把对CPU的控制权重新交还给内核。
给内核添加KGDB功能支持S3C2410/S3C2440
给内核添加KGDB补丁
略
修改补丁本身带入的错误
略
编写S3C2410/S3C2440的KGDB串口函数
目前的KGDB补丁不支持S3C2410/S3C2440的串口,需要自己编写相关函数。可以参考arch/arm/mach-pxa/kgdb-serial.c
,在arch/arm/mach-s3c2410/
目录下也建立一个kgdb-serial.c文件。
KGDB只需要3个函数:初始化函数、发送单字符函数、接收单字符函数。然后将它们填入同一文件中,一个名为kgdb_io_ops的struct kgdb_io结构中。
- 串口初始化函数要使用串口,需要选择相关的GPIO引脚用作串口,并且设置串口的数据格式、时钟源、波特率等。
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
53static int kgdb_serial_init(void)
{
struct clk *clock_p;
u32 pclk;
u32 ubrdiv;
u32 val;
u32 index = CONFIG_KGDB_PORT_NUM;
clock_p = clk_get(NULL,"pclk");
pclk = clk_get_rate(clock_p);
ubrdiv = (pclk / (UART_BAUDRATE * 16)) - 1;
/*
设置GPIO用作串口,并且禁止内部上拉
GPH2 GPH3 用作TXD0、RXD0
GPH4 GPH5 用作TXD1、RXD1
GPH6 GPH3 用作TXD2、RXD2
*/
if(index < MAX_PORT)
{
index = 2 + index * 2;
val = inl(S3C2410_GPHUP) | (0x3 << index);
outl(val,S3C2410_GPHUP);
index *= 2;
val = (inl(S3C2410_GPHCON) & ~(~(0xF << index))) | \
(0xA << index);
outl(val,S3C2410_GPHCON);
}
else
{
return -1;
}
//8N1(8个数据位,无校验位,1个停止位)
wr_reg1(CONFIG_KGDB_PORT_NUM,S3C2410_ULCON,0x03);
//中断/查询方式,UART时钟源为PCLK
wr_reg1(CONFIG_KGDB_PORT_NUM,S3C2410_UCON,0x3c5);
//使用FIFO
wr_reg1(CONFIG_KGDB_PORT_NUM,S3C2410_UFCON,0x51);
//不使用流控
wr_reg1(CONFIG_KGDB_PORT_NUM,S3C2410_UMCON,0x00);
//设置波特率
wr_reg1(CONFIG_KGDB_PORT_NUM,S3C2410_UBRDIV,ubrdiv);
return 0;
} - 发送单字符函数
1
2
3
4
5
6
7
8static void kgdb_serial_putchar(u8 c)
{
/*等待,知道发送缓冲区中的数据已经全部发送出去*/
while(!(rd_regb(CONFIG_KGDB_PORT_NUM,S3C2410_UTRSTAT) & S3C2410_UTRSTAT_TXE));
/*向UTXH寄存器中写入数据,UART即自动将它发送出去*/
wr_regb(CONFIG_KGDB_PORT_NUM,S3C2410_UTXH,c);
} - 接收单字符函数
1
2
3
4
5
6
7
8static int kgdb_serial_getchar(void)
{
/*等待,直到接收缓冲区中有数据*/
while(!(rd_regb(CONFIG_KGDB_PORT_NUM,S3C2410_UTRSTAT) & S3C2410_UTRSTAT_RXDR));
/*直接读取URXH寄存器,即可获得接收到的数据*/
return rd_regb(CONFIG_KGDB_PORT_NUM,S3C2410_TRXH);
} - 使用这些函数构建kgdb_io_ops结构。kgdb_io_opsj结构将在
1
2
3
4
5struct kgdb_io kgdb_io_ops = {
.init = kgdb_serial_init,
.read_char = kgdb_serial_getchar,
.write_char = kgdb_serial_putchar,
};kernel/kgdb.c
中被用到,这个结构封装了开发板相关的串口操作函数。其他的KGDB代码都是具体开发板无关的。
修改内核配置文件、Makefile
修改
arch/arm/mach-s3c2410/Makefile
,将新增的kgdb-serial.c文件编译进内核。1
+ obj-$(CONFIG_KGDB_S3C24XX_SERIAL) += kgdb-serial.o
上面的CONFIG_KGDB_S3C24XX_SERIAL是新加的配置项,是修改配置文件lib/Kconfig.kgdb来支持它。
修改了4个地方,下面的修改内容仿照补丁文件的格式,首字母为“-”的行表示是老文件中的代码,首字母为“+”的行表示是新文件中的代码。
①在“Method for KGDB communication”下增加一个选择项1
2
3
4choice
prompt "Method for KGDB communication"
depends on KGDB
+ default KGDB_S3C24XX_SERIAL if ARCH_S3C2410②用来配置KGDB_S3C24XX_SERIAL选项
1
2
3
4
5+ config KGDB_S3C24XX_SERIAL
+ bool "KGDB: On the S3C24XX serial port"
+ depends on ARCH_S3C2410
+ help
+ Enables the KGDB serial driver for s3c24xx③配置KGDB_S3C24XX_SERIAL后,也可以设置KGDB所用的串口的波特率
1
2
3
4
5
6config KGDB_BAUDRATE
int "Debug serial port baud rate"
depends on (KGDB_8250 && KGDB_SIMPLE_SERIAL) || \
KGDB_MPSC || KGDB_CPM_UART || \
- KGDB_TXX9 || KGDB_PXA_SERIAL || KGDB_AMBA_PL011
- KGDB_TXX9 || KGDB_PXA_SERIAL || KGDB_AMBA_PL011 || KGDB_S3C24XX_SERIAL④配置KGDB_S3C24XX_SERIAL后,也可以设置KGDB使用哪个串口,默认使用第1个
1
2
3
4
5
6
7
8config KGDB_PORT_NUM
int "Serial port number for KGDB"
range 0 1 if KGDB_MPSC
range 0 3
- depends on (KGDB_8250 && KGDB_SIMPLE_SERIAL) || KGDB_MPSC || KGDB_TXX9
- default "1"
+ depends on (KGDB_8250 && KGDB_SIMPLE_SERIAL) || KGDB_MPSC || KGDB_TXX9 || KGDB_S3C24XX_SERIAL
+ default "0"配置内核,使能KGDB功能
执行“make menuconfig”来配置内核,如下配置以使能KGDB功能1
2
3
4
5
6
7Kernel hacking --->
[*]KGDB: kernel debugging with remote gdb //表示使能KGDB
[*]KGDB: Console messages through gdb //表示控制台信息(printk)会发送到GDB
Method for KGDB communication (KGDB: On the S3C24XX serial port) ---> //S3C24XX串口
<>KGDB: On ethernet (NEW)
(115200) Debug serial port baud rate (NEW) //波特率115200
(0) Serial port number for KGDB (NEW) //使用第一个S3C24XX串口然后执行“make uImage”即可生成内核vmlinux、arch/arm/boot/uImage。
结合可视化图形前端DDD和GDB来调试内核
DDD介绍与安装
略
GDB介绍及安装
通过GDB这类调试器,程序员可以知道一个程序执行时内部动作过程,可以知道一个程序崩溃时发生了什么事。
GDB可以完成以下4个主要功能,这可以帮助程序员捕捉到程序的错误。
- 启动程序,并指定各类能够影响程序运行的参数。
- 使程序在指定条件下停止运行。
- 当程序停止时,观察各种状态,检查发生了什么事情
- 修改程序的执行参数,比如修改某个变量,这使得查错时可以试验各种参数。
GDB支持多种编程语言,可以调试用c/c++、Modula-2和Fortan等语言编写的程序。GDB是基于命令行的,GDB启动后,在它的控制界面使用各种命令进行操作。
Ubuntu 7.10自带的GDB工具是基于X86系列的,需要自己下载源码为ARM平台编译的一个GDB工具,为便于区分,将它命名为arm-linux-gdb。1
2
3
4
5tar xjf gdb-6.7.tar.bz2
cd gdb-6.7/
./configure --target=arm-linux
make
sudo make install
使用arm-linux-gdb 调试内核(命令行方式)
先启动支持KGDB的内核,然后在主机上启动arm-linux-gdb。
- 启动内核
要使用KGDB功能,需要增加两个命令参数,:console=kgdb和kgdbwait。前者表示内核打印信息会被发送给GDB,即通过什么增加的kgdb-serial.c中的相关函数进行发送;后者表示内核启动时先停住,等待GDB的连接。
假设将上面编译好的内核uImage放在/work/nfs_root
目录下,则可以在U-Boot上使用以下命令设置命令行参数、启动内核。这时可以看到以下启动信息:1
2
3set bootargs noinitrd root=/dev/mtdblock 2 console=kgdb kgdbwait
nfs 0x31000000 192.168.1.57:/work/nfs_root/uImage
bootm 0x31000000内核在等待主机arm-linux-gdb的连接。1
2
3Starting kernel ...
Uncompressing
Linux .......................................done,booting the kernel. - 启动arm-linux-gdb
启动arm-linux-gdb之前,先退出刚才操作U-Boot所用的工具,因为arm-linux-gdb也要使用这个串口。
然后在主机上进入内核目录,启动arm-linux-gdb,可以执行以下命令:这时会看到arm-linux-gdb的启动信息,进入控制界面:1
2cd /work/system/linux-2.6.22.6
sudo arm-linux-gdb ./vmlinux
最后,执行两个命令设置口、连接目标板。这时就会看到如下信息,表明已经连接上目标板,目标板在1
2(gdb) set remotebaud 115200
(gdb) target remote /dev/ttyS0kernel/kgdb.c
的1775行暂停运行。现在就可以使用GDB的命令控制内核的执行、进行调试了。比如输入n命令执行下一条指令,输入c命令全速运行,输出q命令退出。1
2
3
4Remote debugging using /dev/ttyS0
0xc0067a28 in breakpoint () at kernel/kdb.c:1775
1775 atomic_set(&kgdb_setting_breakpoint,1);
(gdb)
为了避免每次启动arm-linux-gdb时手工设置串口、连接目标板,可以在内核目录建立一个名为“.gdbinit”文件,内容如下:1
2set remotebaud 115200
target remote /dev/ttyS0
通过DDD调用arm-linux-gdb来调试内核(图形界面)
略