Linux内核调试技术

嵌入式Linux系统移植之Linux内核调试技术

《嵌入式Linux应用完全开发手册》第3篇第18章总结归纳

本章目标

  1. 掌握几种调试内核的方法:printk、kgdb、分析Oops、栈回溯
  2. 使用调试工具:gdb、ddd

内核打印函数printk

printk的使用

printk函数的记录级别

调试内核、驱动最简单的方法,是使用printk函数打印信息。printk函数与用户空间的printf函数格式完全相同,它所打印的字符串头部可以加入“”样式的字符,其中n为0-7,表示这条信息的记录级别。
在内核代码include/linux/kernel.h中,下面几个宏控制了printk函数所能输出的信息的记录级别。

1
2
3
4
#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimun_message_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])

举例说明这几个宏的含义。
①对于printk(“…”),只有n小于console_loglevel时,这个信息才会被打印。
②假设default_message_loglevel的值小于4,如果printk的参数开头没有“”样式的字符,则在printk函数中进一步处理前会自动加上“<4>”;
③minimun_console_loglevel是一个预设值,平时不起作用。通过其他工具来设置console_loglevel的值时,这个值不能小于minimun_console_loglevel。
④default_console_loglevel也是一个预设值,平时不起作用。它表示设置console_loglevel时的默认值,通过其他工具来设置console_loglevel的值时,会用到这个值。

minimun_console_loglevel和default_console_loglevel这两个值的作用,可以参考内核源文件kernel/printk.cdo_syslog函数。
上面代码中,console_printk是一个数组,它在kernel/printk.c中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* printk's without a loglevel use this.. */
#define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING */

/* We show everything that is MORE important than this.. */
#define MINIMUM_CONSOLE_LOGLEVEL 1 /* Minimum loglevel we let people use */
#define DEFAULT_CONSOLE_LOGLEVEL 7 /* anything MORE serious than KERN_DEBUG */
...
int console_printk[4] = {
DEFAULT_CONSOLE_LOGLEVEL, /* console_loglevel */
DEFAULT_MESSAGE_LOGLEVEL, /* default_message_loglevel */
MINIMUM_CONSOLE_LOGLEVEL, /* minimum_console_loglevel */
DEFAULT_CONSOLE_LOGLEVEL, /* default_console_loglevel */
};

在用户空间修改printk函数的记录级别

挂接proc文件系统后,读取/proc/sys/kernel/printk文件可以得知console_loglevel、default_message_loglevel、minimun_console_loglevel和default_console_loglevel这4个值。
比如执行以下命令,它的结果是“7 4 1 7”表示这4个值。

1
2
cat /proc/sys/kernel/printk
7 4 1 7

也可以直接修改/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
2
3
4
5
6
7
8
#define	KERN_EMERG	"<0>"	/* system is unusable			*/
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages */

在使用printk函数时,可以这样使用记录级别;

1
printk(KERN_WARNING"there is a warning here!\n");

串口控制台

串口与printk函数的关系

在嵌入式Linux开发中,printk信息常常从串口输出,这时串口被称为串口控制台。从内核kernel/printk.c的printk函数开始,往下查看它的调用关系,可以知道printk函数是如何与具体设备的输出函数挂钩的。
printk函数调用的子函数的主要脉络如下:

1
2
3
4
5
6
7
8
printk  ->
vprintk ->
emit_log_char //把要打印的数据写入一个全局缓冲区(log_buf)中
release_console_sem ->
call_console_drivers ->
_call_console_drivers ->
__call_console_drivers ->
con->write //con是console_drivers链表的表项,调用具体的输出函数

对于可以作为控制台的设备,在初始化时会通过register_console函数向console_drivers链表注册一个console结构,里面有write函数指针。
drivers/serial/s3c2410.c文件中的串口初始化函数s3c24xx_serail_initconsole为例,它的部分代码如下:

1
2
3
4
5
6
static int s3c24xx_serial_initconsole(void)
{
...
register_console(&s3c24xx_serial_console);
return 0;
}

第4行的s3c24xx_serial_console就是console结构,它在相同的文件中定义,部分内容如下:

1
2
3
4
5
6
7
8
9
static struct console s3c24xx_serial_console =
{
.name = S3C24XX_SERIAL_NAME, //这个宏被定义为“SAC”
.device = uart_console_device, //init进行、用户程序打开/dev/console时用到
.flags = CON_PRINTBUFFER, //打印先前在log_buf中保存的信息
.index = -1, //表示使用哪个串口由命令行决定
.write = s3c24xx_serial_console_write, //串口控制台的输出函数
.setup = s3c24xx_serial_console_setup //串口控制台的设置函数
};

第5行的CON_PRINTBUFFER表示注册这个结构之后,要把log_buf缓冲区中的所有信息打印出来。这表明,在实际的硬件被初始化之前,就可以使用printk函数,只不过这时的打印信息时保存在log_buf缓冲区中,还没有真正输出。
第7行的s3c24xx_serial_console_write是串口输出函数,它会调用s3c24xx_serial_console_putchar函数将要打印的字符一个一个的从串口输出。
s3c24xx_serial_console_putchar是最底层的函数,代码如下:

1
2
3
4
5
6
7
8
static void
s3c24xx_serial_console_putchar(struct uart_port *port, int ch)
{
unsigned int ufcon = rd_regl(cons_uart, S3C2410_UFCON);
while (!s3c24xx_serial_console_txrdy(port, ufcon))
barrier();
wr_regb(cons_uart, S3C2410_UTXH, ch);
}

从上面的代码可以知道,从串口输出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个部件。

  1. GDB stub
    GDB stub被称为调试桩机(简称stub),是KGDB调试器的核心。它是Linux内核中的一小段代码,用来处理主机上GDB发来的各种请求;并且在内核处于被调试状态时,控制目标板上的机器。
  2. 修改异常处理函数
    当这个异常发生时,内核将控制权交给KGDB调试器,程序进入KGDB提供的异常处理函数中。在里面,可以分析程序的各种情况。
  3. 串口通信
    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结构中。

  1. 串口初始化函数
    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
    static 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;
    }
    要使用串口,需要选择相关的GPIO引脚用作串口,并且设置串口的数据格式、时钟源、波特率等。
  2. 发送单字符函数
    1
    2
    3
    4
    5
    6
    7
    8
    static 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);
    }
  3. 接收单字符函数
    1
    2
    3
    4
    5
    6
    7
    8
    static 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);
    }
  4. 使用这些函数构建kgdb_io_ops结构。
    1
    2
    3
    4
    5
    struct kgdb_io kgdb_io_ops = {
    .init = kgdb_serial_init,
    .read_char = kgdb_serial_getchar,
    .write_char = kgdb_serial_putchar,
    };
    kgdb_io_opsj结构将在kernel/kgdb.c中被用到,这个结构封装了开发板相关的串口操作函数。其他的KGDB代码都是具体开发板无关的。

修改内核配置文件、Makefile

  1. 修改arch/arm/mach-s3c2410/Makefile,将新增的kgdb-serial.c文件编译进内核。

    1
    + obj-$(CONFIG_KGDB_S3C24XX_SERIAL) += kgdb-serial.o
  2. 上面的CONFIG_KGDB_S3C24XX_SERIAL是新加的配置项,是修改配置文件lib/Kconfig.kgdb来支持它。
    修改了4个地方,下面的修改内容仿照补丁文件的格式,首字母为“-”的行表示是老文件中的代码,首字母为“+”的行表示是新文件中的代码。
    ①在“Method for KGDB communication”下增加一个选择项

    1
    2
    3
    4
    choice 
    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
    6
    config 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
    8
    config 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"
  3. 配置内核,使能KGDB功能
    执行“make menuconfig”来配置内核,如下配置以使能KGDB功能

    1
    2
    3
    4
    5
    6
    7
    Kernel 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个主要功能,这可以帮助程序员捕捉到程序的错误。

  1. 启动程序,并指定各类能够影响程序运行的参数。
  2. 使程序在指定条件下停止运行。
  3. 当程序停止时,观察各种状态,检查发生了什么事情
  4. 修改程序的执行参数,比如修改某个变量,这使得查错时可以试验各种参数。
    GDB支持多种编程语言,可以调试用c/c++、Modula-2和Fortan等语言编写的程序。GDB是基于命令行的,GDB启动后,在它的控制界面使用各种命令进行操作。
    Ubuntu 7.10自带的GDB工具是基于X86系列的,需要自己下载源码为ARM平台编译的一个GDB工具,为便于区分,将它命名为arm-linux-gdb。
    1
    2
    3
    4
    5
    tar 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。

  1. 启动内核
    要使用KGDB功能,需要增加两个命令参数,:console=kgdb和kgdbwait。前者表示内核打印信息会被发送给GDB,即通过什么增加的kgdb-serial.c中的相关函数进行发送;后者表示内核启动时先停住,等待GDB的连接。
    假设将上面编译好的内核uImage放在/work/nfs_root目录下,则可以在U-Boot上使用以下命令设置命令行参数、启动内核。
    1
    2
    3
    set bootargs noinitrd root=/dev/mtdblock 2 console=kgdb kgdbwait
    nfs 0x31000000 192.168.1.57:/work/nfs_root/uImage
    bootm 0x31000000
    这时可以看到以下启动信息:
    1
    2
    3
    Starting kernel ...
    Uncompressing
    Linux .......................................done,booting the kernel.
    内核在等待主机arm-linux-gdb的连接。
  2. 启动arm-linux-gdb
    启动arm-linux-gdb之前,先退出刚才操作U-Boot所用的工具,因为arm-linux-gdb也要使用这个串口。
    然后在主机上进入内核目录,启动arm-linux-gdb,可以执行以下命令:
    1
    2
    cd /work/system/linux-2.6.22.6
    sudo arm-linux-gdb ./vmlinux
    这时会看到arm-linux-gdb的启动信息,进入控制界面:
    img not found
    最后,执行两个命令设置口、连接目标板。
    1
    2
    (gdb) set remotebaud 115200
    (gdb) target remote /dev/ttyS0
    这时就会看到如下信息,表明已经连接上目标板,目标板在kernel/kgdb.c的1775行暂停运行。
    1
    2
    3
    4
    Remote debugging using /dev/ttyS0
    0xc0067a28 in breakpoint () at kernel/kdb.c:1775
    1775 atomic_set(&kgdb_setting_breakpoint,1);
    (gdb)
    现在就可以使用GDB的命令控制内核的执行、进行调试了。比如输入n命令执行下一条指令,输入c命令全速运行,输出q命令退出。
    为了避免每次启动arm-linux-gdb时手工设置串口、连接目标板,可以在内核目录建立一个名为“.gdbinit”文件,内容如下:
    1
    2
    set remotebaud 115200
    target remote /dev/ttyS0

通过DDD调用arm-linux-gdb来调试内核(图形界面)