从零开始之驱动发开、linux驱动(六十三、内核调试篇--基础知识1[earlyprintk建立过程])

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_16777851/article/details/89433308

我们知道,内核中使用__section设置了很多的段属性,使用段属性可以很方便的对函数调用时间分层。

比如我们之前常见的subsys_initcall,就要比module_init更早的执行,因为subsys_initcall在驱动中通常是bus和class,驱动程序调用执行需要class和bus已经创建才能执行驱动函数。

关于这点我之前的博文已经有过分析。

https://blog.csdn.net/qq_16777851/article/details/82121456

和subsys_initcall、arch_initcall、core_initcall、device_initcall等类似,内核在别的地方也是用了这样的模式。

这里我们要说的就是__setup

可以搜索一下__setup,可以发现这个宏的调用高达300次,可见这个宏的种重要性。

这个宏里面又是调用了另一个宏

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

在看这个#define __setup_param(str, unique_id, fn, early) 宏之前,我们先看一个结构体,这个结构体也算是这个宏的基础。

struct obs_kernel_param {
	const char *str;
	int (*setup_func)(char *);
	int early;
};

很简单,一个字符指针,主要是为了匹配。一个函数指针,主要是为了绑定函数。一个eraly主要是为了确定执行次序。

接下来就看这个宏了

/*
 * Only for really core code.  See moduleparam.h for the normal way.
 *
 * Force the alignment so the compiler doesn't space elements of the
 * obs_kernel_param "array" too far apart in .init.setup.
 */
#define __setup_param(str, unique_id, fn, early)			\
	static const char __setup_str_##unique_id[] __initconst		\
		__aligned(1) = str; 					\
	static struct obs_kernel_param __setup_##unique_id		\
		__used __section(.init.setup)				\
		__attribute__((aligned((sizeof(long)))))		\
		= { __setup_str_##unique_id, fn, early }

看到这个宏我想大家就明白了,定义了一个字符串数组放置字符串。定义了一个obs_kernel_param结构体用来存放参数,这个结构体用__section(.init.setup)做了修饰,表示调用这个宏定义的obs_kernel_param结构体都会被编译进一个.init.setup段中。

结构体就占用3个long,一个是存放前面定义的这个字符串的地址,另一个就是函数指针,随后一个就是一个整数。

这里看一下内核的链接脚本

看一下这个宏有在那里调用。


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

/*
 * NOTE: fn is as per module_param, not __setup!
 * Emits warning if fn returns non-zero.
 */
#define early_param(str, fn)						\
	__setup_param(str, fn, fn, 1)

#define early_param_on_off(str_on, str_off, var, config)		\
									\
	int var = IS_ENABLED(config);					\
									\
	static int __init parse_##var##_on(char *arg)			\
	{								\
		var = 1;						\
		return 0;						\
	}								\
	__setup_param(str_on, parse_##var##_on, parse_##var##_on, 1);	\
									\
	static int __init parse_##var##_off(char *arg)			\
	{								\
		var = 0;						\
		return 0;						\
	}								\
	__setup_param(str_off, parse_##var##_off, parse_##var##_off, 1)

一个是__setup宏,一个是early_param宏,最后就是early_param_on_off宏

但是经过我搜索内核,实际上这个宏early_param_on_off是从未使用过的,可能是为了新功能预留的,

__setup和early_param宏只是最后一个整数参数的不同。我们这里就以__setup为例分析下去

这里就以我们内核调试主体要分析的这个为例

__setup("console=", console_setup);

这个宏将来就被定义为在一个段属性里面了。

static const char __setup_str_console_setup[] __initconst	__aligned(1) = "console="; 				
static struct obs_kernel_param __setup_console_setup
		__used __section(.init.setup)			
		__attribute__((aligned((sizeof(long)))))
= { 
        __setup_str_console_setup 
        console_setup, 
        0
}

定义这个段属性,肯定就是为了在某个阶段里面有函数执行,那么什么时候执行呢?

这里我们再次回到链接脚本

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

可以看到有两个标号在这个段属性前后。

搜索一下这两个标号

在init目录下的main.c函数导出了。

既然已经知道了这段在内存的位置,那么就可以直接使用里面的结构体,进而函数执行了。

而执行的函数就在extern导出的下面


static bool __init obsolete_checksetup(char *line)
{
	const struct obs_kernel_param *p;
	bool had_early_param = false;

	p = __setup_start;
	do {
		int n = strlen(p->str);
		if (parameqn(line, p->str, n)) {
			if (p->early) {
				/* Already done in parse_early_param?
				 * (Needs exact match on param part).
				 * Keep iterating, as we can have early
				 * params and __setups of same names 8( */
				if (line[n] == '\0' || line[n] == '=')
					had_early_param = true;
			} else if (!p->setup_func) {
				pr_warn("Parameter %s is obsolete, ignored\n",
					p->str);
				return true;
			} else if (p->setup_func(line + n))
				return true;
		}
		p++;
	} while (p < __setup_end);

	return had_early_param;
}

可以看到,这是一个do_while的循环,就是调用parameqn比较,在obsolete_checksetup这个函数传入字符串参数和段属性中所有结构体比较,相等的情况下执行,找到的结构体obs_kernel_param对应的函数。

字符串比较函数很简单,唯一就是把'-'字符替换,按照‘_’进行比较


static char dash2underscore(char c)
{
	if (c == '-')
		return '_';
	return c;
}

bool parameqn(const char *a, const char *b, size_t n)
{
	size_t i;

	for (i = 0; i < n; i++) {
		if (dash2underscore(a[i]) != dash2underscore(b[i]))
			return false;
	}
	return true;
}

看到这里我想已经明白了一大半,调用这个函数的上层函数给这个函数传入一个字符串,段属性中找到对应的字符串,然后执行这个字符串对应的结构体里面的函数。

搜索后,这个函数只被下面这个函数调用了一次。


/*
 * Unknown boot options get handed to init, unless they look like
 * unused parameters (modprobe will find them in /proc/cmdline).
 */
static int __init unknown_bootoption(char *param, char *val,
				     const char *unused, void *arg)
{
	repair_env_string(param, val, unused, NULL);

	/* Handle obsolete-style parameters */
	if (obsolete_checksetup(param))
		return 0;

	/* Unused module parameter. */
	if (strchr(param, '.') && (!val || strchr(param, '.') < val))
		return 0;

	if (panic_later)
		return 0;

	if (val) {
		/* Environment option */
		unsigned int i;
		for (i = 0; envp_init[i]; i++) {
			if (i == MAX_INIT_ENVS) {
				panic_later = "env";
				panic_param = param;
			}
			if (!strncmp(param, envp_init[i], val - param))
				break;
		}
		envp_init[i] = param;
	} else {
		/* Command line option */
		unsigned int i;
		for (i = 0; argv_init[i]; i++) {
			if (i == MAX_INIT_ARGS) {
				panic_later = "init";
				panic_param = param;
			}
		}
		argv_init[i] = param;
	}
	return 0;
}

我们看一下他的调用之处,可以看到这个函数是以函数指针的形式调用的。

	after_dashes = parse_args("Booting kernel",
				  static_command_line, __start___param,
				  __stop___param - __start___param,
				  -1, -1, NULL, &unknown_bootoption);

看到这里我们看到了很重要的一条就是需要comdline

我们看一下condline是设置的

asmlinkage __visible void __init start_kernel(void)
{
    
    ......
    
    setup_arch(&command_line);
	/*
	 * Set up the the initial canary and entropy after arch
	 * and after adding latent and command line entropy.
	 */
	add_latent_entropy();
	add_device_randomness(command_line, strlen(command_line));
	boot_init_stack_canary();
	mm_init_cpumask(&init_mm);
	setup_command_line(command_line);
	setup_nr_cpu_ids();
	setup_per_cpu_areas();
	smp_prepare_boot_cpu();	/* arch-specific boot-cpu hooks */
	boot_cpu_hotplug_init();

	build_all_zonelists(NULL);
	page_alloc_init();

	pr_notice("Kernel command line: %s\n", boot_command_line);
	parse_early_param();
	after_dashes = parse_args("Booting kernel",
				  static_command_line, __start___param,
				  __stop___param - __start___param,
				  -1, -1, NULL, &unknown_bootoption);

    ......
}

当然最初的comdline是通过uboot传过来的,可以是设备树,也可以是地址传,在下面这个函数解析的。

	setup_arch(&command_line);

内核中定义了好多的comdline,刚启动时,是完全一样使用uboot的传参,后面可以根据需要更改。

/*
 * We need to store the untouched command line for future reference.
 * We also need to store the touched command line since the parameter
 * parsing is performed in place, and we should allow a component to
 * store reference of name/value for future reference.
 */
static void __init setup_command_line(char *command_line)
{
	saved_command_line =
		memblock_virt_alloc(strlen(boot_command_line) + 1, 0);
	initcall_command_line =
		memblock_virt_alloc(strlen(boot_command_line) + 1, 0);
	static_command_line = memblock_virt_alloc(strlen(command_line) + 1, 0);
	strcpy(saved_command_line, boot_command_line);
	strcpy(static_command_line, command_line);
}

这里我们看两个函数

	parse_early_param();
	after_dashes = parse_args("Booting kernel",
				  static_command_line, __start___param,
				  __stop___param - __start___param,
				  -1, -1, NULL, &unknown_bootoption);

我看先看一下早期的解析函数,

/* Arch code calls this early on, or if not, just before other parsing. */
void __init parse_early_param(void)
{
	static int done __initdata;
	static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;

	if (done)
		return;

	/* All fall through to do_early_param. */
	strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
	parse_early_options(tmp_cmdline);
	done = 1;
}


void __init parse_early_options(char *cmdline)
{
	parse_args("early options", cmdline, NULL, 0, 0, 0, NULL,
		   do_early_param);
}

/* Args looks like "foo=bar,bar2 baz=fuz wiz". */
char *parse_args(const char *doing,
		 char *args,
		 const struct kernel_param *params,
		 unsigned num,
		 s16 min_level,
		 s16 max_level,
		 void *arg,
		 int (*unknown)(char *param, char *val,
				const char *doing, void *arg))
{
	char *param, *val, *err = NULL;

	/* Chew leading spaces */
    /* 忽略掉参数最前面的的空格 */
	args = skip_spaces(args);

	if (*args)
		pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args);

	while (*args) {
		int ret;
		int irq_was_disabled;

		args = next_arg(args, &param, &val);
		/* Stop at -- */
		if (!val && strcmp(param, "--") == 0)
			return err ?: args;
		irq_was_disabled = irqs_disabled();
		ret = parse_one(param, val, doing, params, num,
				min_level, max_level, arg, unknown);
		if (irq_was_disabled && !irqs_disabled())
			pr_warn("%s: option '%s' enabled irq's!\n",
				doing, param);

		switch (ret) {
		case 0:
			continue;
		case -ENOENT:
			pr_err("%s: Unknown parameter `%s'\n", doing, param);
			break;
		case -ENOSPC:
			pr_err("%s: `%s' too large for parameter `%s'\n",
			       doing, val ?: "", param);
			break;
		default:
			pr_err("%s: `%s' invalid for parameter `%s'\n",
			       doing, val ?: "", param);
			break;
		}

		err = ERR_PTR(ret);
	}

	return err;
}

上函数很简单,因为cmdline里面可能有多个命令,每个命令之间应用空格间隔,依次拿出每一个命令执行parse_one函数

root=/dev/nfs nfsroot=192.168.0.101:/home/run/work/rootfs/rootfs_3.16.57
ip=192.168.0.20:192.168.0.101:192.168.0.1:255.255.255.0::eth0:off init=/linuxrc 
console=ttySAC2,115200 earlyprintk

这个parse_one函数也比较简单,因为上面传入的params为NULL,num_params为0,min_level和max_level都是0,所以for里面的也肯定不会执行的,也就是只执行handle_unknown


static int parse_one(char *param,
		     char *val,
		     const char *doing,
		     const struct kernel_param *params,
		     unsigned num_params,
		     s16 min_level,
		     s16 max_level,
		     void *arg,
		     int (*handle_unknown)(char *param, char *val,
				     const char *doing, void *arg))
{
	unsigned int i;
	int err;

	/* Find parameter */
	for (i = 0; i < num_params; i++) {
		if (parameq(param, params[i].name)) {
			if (params[i].level < min_level
			    || params[i].level > max_level)
				return 0;
			/* No one handled NULL, so do it here. */
			if (!val &&
			    !(params[i].ops->flags & KERNEL_PARAM_OPS_FL_NOARG))
				return -EINVAL;
			pr_debug("handling %s with %p\n", param,
				params[i].ops->set);
			kernel_param_lock(params[i].mod);
			param_check_unsafe(&params[i]);
			err = params[i].ops->set(val, &params[i]);
			kernel_param_unlock(params[i].mod);
			return err;
		}
	}

	if (handle_unknown) {
		pr_debug("doing %s: %s='%s'\n", doing, param, val);
		return handle_unknown(param, val, doing, arg);
	}

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

这个handle_unknown就是我们上面传进去的早期的参数函数了。


/* Check for early params. */
static int __init do_early_param(char *param, char *val,
				 const char *unused, void *arg)
{
	const struct obs_kernel_param *p;

	for (p = __setup_start; p < __setup_end; p++) {
		if ((p->early && parameq(param, p->str)) ||
		    (strcmp(param, "console") == 0 &&
		     strcmp(p->str, "earlycon") == 0)
		) {
			if (p->setup_func(val) != 0)
				pr_warn("Malformed early option '%s'\n", param);
		}
	}
	/* We accept everything at this stage. */
	return 0;
}

可以看到这里也用到了段属性定义的__setup_start,__setup_end。在这个段属性中结构体中扫描,看是否那个结构体中rerlay这个整数不为0,且命令行参数中有传给内核,或者比较命令行参数有为consloe,且这个.init.setup段属性有earlycon这个名字的结构体。

看一下我们的命令行参数,有定义earlayprintk和console=ttySAC2

root=/dev/nfs nfsroot=192.168.0.101:/home/run/work/rootfs/rootfs_3.16.57
ip=192.168.0.20:192.168.0.101:192.168.0.1:255.255.255.0::eth0:off init=/linuxrc 
console=ttySAC2,115200 earlyprintk

内核搜索"earlyprintk",所以早期的rarlay_printk就是在这里初始化的。

static int __init setup_early_printk(char *buf)
{
	early_console = &early_console_dev;
	register_console(&early_console_dev);
	return 0;
}

early_param("earlyprintk", setup_early_printk);

这里把早期的打印原理也说一下,打印字符串。


static void early_write(const char *s, unsigned n)
{
	char buf[128];
	while (n) {
		unsigned l = min(n, sizeof(buf)-1);
		memcpy(buf, s, l);
		buf[l] = 0;
		s += l;
		n -= l;
		printascii(buf);
	}
}

static void early_console_write(struct console *con, const char *s, unsigned n)
{
	early_write(s, n);
}

static struct console early_console_dev = {
	.name =		"earlycon",
	.write =	early_console_write,
	.flags =	CON_PRINTBUFFER | CON_BOOT,
	.index =	-1,
};

最终就调用一个串口的发送函数printascii,这个函数使用汇编定义的。

发送函数很简单

ENTRY(printascii)
		addruart_current r3, r1, r2
1:		teq	r0, #0
		ldrneb	r1, [r0], #1
		teqne	r1, #0
		reteq	lr
2:		teq     r1, #'\n'
		bne	3f
		mov	r1, #'\r'
		waituart r2, r3
		senduart r1, r3
		busyuart r2, r3
		mov	r1, #'\n'
3:		waituart r2, r3
		senduart r1, r3
		busyuart r2, r3
		b	1b
ENDPROC(printascii)

可以看到,早期的earlay_printk只能在串口打印,而且实现方式很简单,

earlay_printk完了,我们看一下下一个,我们还定义了console,那么就需要在内核搜索一下"earlycon"字符串


early_param("earlycon", param_setup_earlycon);

这个搜索后也是定义了的,所以这函数也要执行。

关于这个也涉及到另一个段,__earlycon_table

extern const struct earlycon_id *__earlycon_table[];
extern const struct earlycon_id *__earlycon_table_end[];

搜索后也都是串口。而且基本所有厂家都做了适配。看一下三星。

我们芯片是s5pv210,所以s5pv210的串口驱动肯定是包含的。

我这里不打算分析串口驱动,只是对内核调试的原理分析一下。

static int __init s5pv210_early_console_setup(struct earlycon_device *device,
					      const char *opt)
{
	device->port.private_data = &s5pv210_early_console_data;
	return samsung_early_console_setup(device, opt);
}
OF_EARLYCON_DECLARE(s5pv210, "samsung,s5pv210-uart",
			s5pv210_early_console_setup);

搜索所有的earlaycon后,和值比较,找到匹配的。

可以看一下上面的earlay函数,这里传的参数是val,earlayprintk没val,但console有val,为tyySAC2

root=/dev/nfs nfsroot=192.168.0.101:/home/run/work/rootfs/rootfs_3.16.57
ip=192.168.0.20:192.168.0.101:192.168.0.1:255.255.255.0::eth0:off init=/linuxrc 
console=ttySAC2,115200 earlyprintk

可以看一下面的解析函数

/* early_param wrapper for setup_earlycon() */
static int __init param_setup_earlycon(char *buf)
{
	int err;

	/* Just 'earlycon' is a valid param for devicetree and ACPI SPCR. */
	if (!buf || !buf[0]) {
		if (IS_ENABLED(CONFIG_ACPI_SPCR_TABLE)) {
			earlycon_acpi_spcr_enable = true;
			return 0;
		} else if (!buf) {
			return early_init_dt_scan_chosen_stdout();
		}
	}

	err = setup_earlycon(buf);
	if (err == -ENOENT || err == -EALREADY)
		return 0;
	return err;
}
early_param("earlycon", param_setup_earlycon);


/**
 *	setup_earlycon - match and register earlycon console
 *	@buf:	earlycon param string
 *
 *	Registers the earlycon console matching the earlycon specified
 *	in the param string @buf. Acceptable param strings are of the form
 *	   <name>,io|mmio|mmio32|mmio32be,<addr>,<options>
 *	   <name>,0x<addr>,<options>
 *	   <name>,<options>
 *	   <name>
 *
 *	Only for the third form does the earlycon setup() method receive the
 *	<options> string in the 'options' parameter; all other forms set
 *	the parameter to NULL.
 *
 *	Returns 0 if an attempt to register the earlycon was made,
 *	otherwise negative error code
 */
int __init setup_earlycon(char *buf)
{
	const struct earlycon_id **p_match;

	if (!buf || !buf[0])
		return -EINVAL;

	if (early_con.flags & CON_ENABLED)
		return -EALREADY;

	for (p_match = __earlycon_table; p_match < __earlycon_table_end;
	     p_match++) {
		const struct earlycon_id *match = *p_match;
		size_t len = strlen(match->name);

		if (strncmp(buf, match->name, len))
			continue;

		if (buf[len]) {
			if (buf[len] != ',')
				continue;
			buf += len + 1;
		} else
			buf = NULL;

		return register_earlycon(buf, match);
	}

	return -ENOENT;
}

比较的是ttySAC2和name,很明显,没有一个设备的name是ttySAC2。

OF_EARLYCON_DECLARE(s5pv210, "samsung,s5pv210-uart",
			s5pv210_early_console_setup);

#define _OF_EARLYCON_DECLARE(_name, compat, fn, unique_id)		\
	static const struct earlycon_id unique_id			\
	     EARLYCON_USED_OR_UNUSED __initconst			\
		= { .name = __stringify(_name),				\
		    .compatible = compat,				\
		    .setup = fn  };					\
	static const struct earlycon_id EARLYCON_USED_OR_UNUSED		\
		__section(__earlycon_table)				\
		* const __PASTE(__p, unique_id) = &unique_id

#define OF_EARLYCON_DECLARE(_name, compat, fn)				\
	_OF_EARLYCON_DECLARE(_name, compat, fn,				\
			     __UNIQUE_ID(__earlycon_##_name))

我们这里的name是s5pv210,不是ttySAC2

那ttySAC2什么时候执行呢,这个就不属于parse_early_param函数的功能了,而是下面的这个parse_args了。我们放在下一节分析。

	parse_early_param();
	after_dashes = parse_args("Booting kernel",
				  static_command_line, __start___param,
				  __stop___param - __start___param,
				  -1, -1, NULL, &unknown_bootoption);

猜你喜欢

转载自blog.csdn.net/qq_16777851/article/details/89433308
今日推荐