在编写驱动过程分析中会遇到许多难找的问题,这时候调试的方法就很重要了,下面介绍的是利用打印的方法调试驱动,这种方法同样可以用在应用的调试过程中,而且很有效。
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=”,最终可以在在kernelprintk.c中找到__setup('console=', console_setup);这种形式的定义在Linux内核源码阅读记录一之分析存储在不同段中的函数调用过程中已经介绍过,下面介绍一遍调用过程,__setup被定义在includelinuxinit.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,在initmain.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] == '' || line[n] == '=')
16 had_early_param = 1;
17 } else if (!p->setup_func) {//如果处理函数不存在,则报错
18 printk(KERN_WARNING 'Parameter %s is obsolete,'
19 ' ignoredn', 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: %sn', args);
/* Chew leading spaces */
while (*args == ' ')
args++;
while (*args) {//循环处理剩余的命令行,直到全部处理完成
int ret;
int irq_was_disabled;
args = next_arg(args, ¶m, &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 %pn',
params[i].set);
return params[i].set(val, ¶ms[i]);//如果是内核的参数,那么直接传给内核参数,然后返回。
}
}
if (handle_unknown) {//如果不是内核的参数,并且处理函数存在
DEBUGP('Unknown argument: calling %pn', 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这个函数,看看有哪些设备驱动调用了它。在driversserials3c2410.c文件中找到了它:可以看到,s3c24xx_serial_console 结构体中的名字与命令行传入的名字相符,所以prink最终可以调用s3c24xx_serial_console结构体中的write函数打印到串口输出。