驱动调试之打印

Posted andy_fly

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了驱动调试之打印相关的知识,希望对你有一定的参考价值。

在编写驱动过程分析中会遇到许多难找的问题,这时候调试的方法就很重要了,下面介绍的是利用打印的方法调试驱动,这种方法同样可以用在应用的调试过程中,而且很有效。

1、prink的原理

首先介绍一下打印的函数prink的原理,printk的原理是最终打印在终端上的。所以只要是能成为终端的设备均可被打印,比如串口、网络、LCD等等。

在u-boot的启动参数中,有这么一项console=ttySAC0,其中ttySAC0就是最终printk打印到的设备。

bootargs=console=ttySAC0 root=/dev/nfs nfsroot=192.168.1.101:/work/nfs_andy/first_fs ip=192.168.1.18:192.168.1.101:192.168.1.1:255.255.255.0::eth0:off

 

1.1、__setup调用过程分析

为了分析prink,可以在内核源码中搜索“console=”,最终可以在在kernel\\printk.c中找到__setup("console=", console_setup);这种形式的定义在Linux内核源码阅读记录一之分析存储在不同段中的函数调用过程中已经介绍过,下面介绍一遍调用过程,__setup被定义在include\\linux\\init.h中:

171 #define __setup(str, fn)                    \\
172     __setup_param(str, fn, fn, 0)

160 #define __setup_param(str, unique_id, fn, early)            \\
161    static char __setup_str_##unique_id[] __initdata = str;    \\
162     static struct obs_kernel_param __setup_##unique_id    \\
163        __attribute_used__                \\
164        __attribute__((__section__(".init.setup")))    \\
165        __attribute__((aligned((sizeof(long)))))    \\
166        = { __setup_str_##unique_id, fn, early }


148 struct obs_kernel_param {
149    const char *str;
150    int (*setup_func)(char *);
151    int early;
152 };

最终把__setup("console=", console_setup);展开可以得到:从展开的函数可以知道,最终定义了一个位于.init.setup段的结构体__setup_console_setup,并且初始化了它的各个成员,有函数,有名称等等。

static char __setup_str_console_setup[] __initdata = "console=";  

 static struct obs_kernel_param __setup_console_setup    
 __attribute_used__                
 __attribute__((__section__(".init.setup")))   
  __attribute__((aligned((sizeof(long)))))   
= 
{ 
    __setup_str_console_setup, 
    console_setup,
     0
}

接着在arch/arm/kernel/vmlinux.lds中搜索.init.setup,可以得到这个段的初始化地址与结束地址__setup_start、__setup_end

  __setup_start = .;
   *(.init.setup)
  __setup_end = .;

为了得到调用这个段的时机,我们继续接着在内核源码中搜索__setup_start,在init\\main.c中的obsolete_checksetup函数搜索到了它,obsolete_checksetup这个函数的第6行开始会根据__setup_console_setup结构体中的str字符串值与传入的line字符串值是否相等以及early参数来决定是否调用__setup_console_setup结构体中的函数。

 1 static int __init obsolete_checksetup(char *line)
 2 {
 3     struct obs_kernel_param *p;
 4     int had_early_param = 0;
 5 
 6     p = __setup_start;//.init.setup的首地址
 7     do {
 8         int n = strlen(p->str);
 9         if (!strncmp(line, p->str, n)) {//在.init.setup中寻找相符的命令行参数
10             if (p->early) {//如果early大于0,那么这个参数在前面已经处理过了
11                 /* Already done in parse_early_param?
12                  * (Needs exact match on param part).
13                  * Keep iterating, as we can have early
14                  * params and __setups of same names 8( */
15                 if (line[n] == \'\\0\' || line[n] == \'=\')
16                     had_early_param = 1;
17             } else if (!p->setup_func) {//如果处理函数不存在,则报错
18                 printk(KERN_WARNING "Parameter %s is obsolete,"
19                        " ignored\\n", p->str);
20                 return 1;
21             } else if (p->setup_func(line + n))//调用处理函数处理
22                 return 1;
23         }
24         p++;
25     } while (p < __setup_end);
26 
27     return had_early_param;
28 }

到这里我们直接罗列出obsolete_checksetup的调用过程,还是从start_kernel开始:

1 start_kernel
2     parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param,&unknown_bootoption);//后续的命令处理
3         unknown_bootoption
4             obsolete_checksetup

继续往下分析parse_args函数,这个函数会找出命令参数,然后调用parse_one函数处理

int parse_args(const char *name,
           char *args,
           struct kernel_param *params,
           unsigned num,
           int (*unknown)(char *param, char *val))
{
    char *param, *val;

    DEBUGP("Parsing ARGS: %s\\n", args);

    /* Chew leading spaces */
    while (*args == \' \')
        args++;

    while (*args) {//循环处理剩余的命令行,直到全部处理完成
        int ret;
        int irq_was_disabled;

        args = next_arg(args, &param, &val);//找出下一个命令行参数*param为命令名称,*val为参数值
        irq_was_disabled = irqs_disabled();
        ret = parse_one(param, val, params, num, unknown);//处理param为
        if (irq_was_disabled && !irqs_disabled()) {
            printk(KERN_WARNING "parse_args(): option \'%s\' enabled "
                    "irq\'s!\\n", param);
        }
        switch (ret) {
        case -ENOENT:
            printk(KERN_ERR "%s: Unknown parameter `%s\'\\n",
                   name, param);
            return ret;
        case -ENOSPC:
            printk(KERN_ERR
                   "%s: `%s\' too large for parameter `%s\'\\n",
                   name, val ?: "", param);
            return ret;
        case 0:
            break;
        default:
            printk(KERN_ERR
                   "%s: `%s\' invalid for parameter `%s\'\\n",
                   name, val ?: "", param);
            return ret;
        }
    }

    /* All parsed OK. */
    return 0;

继续分析parse_one函数,可以看到,最终调用了obsolete_checksetup处理函数。在obsolete_checksetup会处理相应的命令行参数

static int parse_one(char *param,
             char *val,
             struct kernel_param *params, 
             unsigned num_params,
             int (*handle_unknown)(char *param, char *val))
{
    unsigned int i;

    /* Find parameter */
    for (i = 0; i < num_params; i++) {//从__param段找出与命令行参数相同的名字
        if (parameq(param, params[i].name)) {
            DEBUGP("They are equal!  Calling %p\\n",
                   params[i].set);
            return params[i].set(val, &params[i]);//如果是内核的参数,那么直接传给内核参数,然后返回。
        }
    }

    if (handle_unknown) {//如果不是内核的参数,并且处理函数存在
        DEBUGP("Unknown argument: calling %p\\n", handle_unknown);
        return handle_unknown(param, val);//调用处理函数处理
    }

    DEBUGP("Unknown argument `%s\'\\n", param);
    return -ENOENT;
}

 

1.2、控制台设置函数console_setup分析

介绍完了__setup的调用过程吗,接下来分析console_setup函数:

static int __init console_setup(char *str)
{
    char name[sizeof(console_cmdline[0].name)];
    char *s, *options;
    int idx;

    /*
     * Decode str into name, index, options.
     */
    if (str[0] >= \'0\' && str[0] <= \'9\') {//如果以数字0-9开头
        strcpy(name, "ttyS");
        strncpy(name + 4, str, sizeof(name) - 5);
    } else {
        strncpy(name, str, sizeof(name) - 1);//将str拷贝到name中,去除结束符
    }
    name[sizeof(name) - 1] = 0;
    if ((options = strchr(str, \',\')) != NULL)//如果参数中存在,的话。说明带波特率参数
        *(options++) = 0;
#ifdef __sparc__
    if (!strcmp(str, "ttya"))
        strcpy(name, "ttyS0");
    if (!strcmp(str, "ttyb"))
        strcpy(name, "ttyS1");
#endif
    for (s = name; *s; s++)
        if ((*s >= \'0\' && *s <= \'9\') || *s == \',\')
            break;
    idx = simple_strtoul(s, NULL, 10);//取出波特率参数,转换成整形
    *s = 0;

    add_preferred_console(name, idx, options);//将参数保存在console_cmdline中
    return 1;
}

在console_setup这个函数的最后会调用console_cmdline将参数保存在console_cmdline中。假设我想用名为"ttySAC0"的控制台,先记录下来放到console_cmdline这个全局变量中,接着搜索"console_cmdline"可以找到register_console这个函数。

分析到这里,大胆的假设,如果我想要printk打印到某个设备上,那么这个设备需要调用register_console注册控制台,并且注册的名字需要与命令行参数传入的名字相同。为了验证这个假设,接着搜索register_console这个函数,看看有哪些设备驱动调用了它。在drivers\\serial\\s3c2410.c文件中找到了它:可以看到,s3c24xx_serial_console 结构体中的名字与命令行传入的名字相符,所以prink最终可以调用s3c24xx_serial_console结构体中的write函数打印到串口输出。

1046 register_console(&s3c24xx_serial_console);//注册控制台

1901 static struct console s3c24xx_serial_console =
1902 {
1903    .name        = S3C24XX_SERIAL_NAME,//控制台名称ttySAC
1904     .device        = uart_console_device,//以后使用/dev/console时,用来狗仔设备节点
1905     .flags        = CON_PRINTBUFFER,//控制台可以之前,printk已经在缓1907 冲区打印了,CON_PRINTBUFFER表示可以打印以前的信息了
1906     .index        = -1,
1907    .write        = s3c24xx_serial_console_write,//打印函数
1908    .setup        = s3c24xx_serial_console_setup.//设置函数
1090 };

150 #define S3C24XX_SERIAL_NAME    "ttySAC"

大致看一下prink的函数过程,从调用过程可以看出打印的信息会存在log_buf这个变量中,而我们也可以通过查看cat /proc/kmsg来得到prink的数据,这句命令的作用就是从log_buf这个缓冲区中读出数据。

prink
    vprintk
        /* Emit the output into the temporary buffer */
        // 先把输出信息放到临时的buffer
        vscnprintf
        
        // Copy the output into log_buf
        // 把临时缓冲区的数据稍作处理(处理打印级别),再写入log_buf
        // 比如prink("abc")会得到"<4>abc",再写入log_buf
        // 可以用dmesg把log_buf里的数据打印出来重现内核的输出信息
        
        // 调用硬件的write函数输出
        release_console_sem();
            call_console_drivers(_con_start, _log_end);
                // 从log_buf得到数据,算出级别
                _call_console_drivers(start_print, cur_index, msg_level);
                    // 如果够级别可以打印
                    __call_console_drivers(0, end & LOG_BUF_MASK);
                        con->write(con, &LOG_BUF(start), end - start);

 

2、prink的使用

prink的使用其实很简单,与我们平时使用C语言的printf差不多。一般如果在某个地方出错的话,会使用二分法的方法定位到最终的问题。下面举一个列子,引入了一个错误,我们在使用CPU的寄存器时没有使用虚拟地址,而是直接使用物理地址。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>

static struct class *firstdrv_class;
static struct class_device    *firstdrv_class_dev;

volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;


static int first_drv_open(struct inode *inode, struct file *file)
{
    //printk("first_drv_open\\n");
    /* 配置GPF4,5,6为输出 */
    printk(KERN_DEBUG"%s %s %d\\n", __FILE__, __FUNCTION__, __LINE__);
    *gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
    printk(KERN_DEBUG"%s %s %d\\n", __FILE__, __FUNCTION__, __LINE__);
    *gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
    printk(KERN_DEBUG"%s %s %d\\n", __FILE__, __FUNCTION__, __LINE__);
    return 0;
}

static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
    int val;

    //printk("first_drv_write\\n");

    copy_from_user(&val, buf, count); //    copy_to_user();

    if (val == 1)
    {
        // 点灯
        *gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
    }
    else
    {
        // 灭灯
        *gpfdat |= (1<<4) | (1<<5) | (1<<6);
    }
    
    return 0;
}

static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
    .open   =   first_drv_open,     
    .write    =    first_drv_write,       
};


int major;
static int first_drv_init(void)
{
    printk(KERN_DEBUG"%s %s %d\\n", __FILE__, __FUNCTION__, __LINE__);
    major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核

    printk(KERN_DEBUG"%s %s %d\\n", __FILE__, __FUNCTION__, __LINE__);
    firstdrv_class = class_create(THIS_MODULE, "firstdrv");

    printk(KERN_DEBUG"%s %s %d\\n", __FILE__, __FUNCTION__, __LINE__);
    firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */

    printk(KERN_DEBUG"%s %s %d\\n", __FILE__, __FUNCTION__, __LINE__);
    gpfcon = (volatile unsigned long *)0x56000050; //引入错误
    gpfdat = gpfcon + 1;

    return 0;
}

static void first_drv_exit(void)
{
    unregister_chrdev(major, "first_drv"); // 卸载

    class_device_unregister(firstdrv_class_dev);
    class_destroy(firstdrv_class);
    iounmap(gpfcon);
}

module_init(first_drv_init);
module_exit(first_drv_exit);


MODULE_LICENSE("GPL");

当我们使用程序测试这个驱动时就会出错,出错时我们就利用printk(KERN_DEBUG"%s %s %d\\n", __FILE__, __FUNCTION__, __LINE__);这条打印语句定位到最终程序执行到哪里导致的出错,其中__FILE__可以打印出文件、__FUNCTION__可以打印出函数、__LINE__可以打印出哪一行。而KERN_DEBUG是打印等级,值越小打印等级越高。当我们调试成功后就不需要打印了,那么可以把打印等级的数值变大。而内核的设置的默认打印等级一般为4,我们可以通过两种方法更改打印等级。

  • cat /proc/sys/kernel/printk 更改这个文件可以改变打印级别
  • 修改loglevel这个uboot传入的参数可以更改内核的打印级别

 

以上是关于驱动调试之打印的主要内容,如果未能解决你的问题,请参考以下文章

PHP代码-psysh调试代码片段工具

linux驱动调试--段错误之oops信息分析

代码调试之串口打印

JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段

嵌入式接口之GPIO驱动LED的实验(附完整代码和工程以及详细的调试过程)

3D打印Marlin2.0固件源代码分析之如何使用LOG接口调试代码