我们知道,内核中使用__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, ¶m, &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(¶ms[i]);
err = params[i].ops->set(val, ¶ms[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);