内核启动参数实现原理

基于上节内核下PCIE的扫描流程内容, 其中有个early_param关键字, 对于启动内核时能够使用诸多参数, 本人早就非常好奇, 趁着这个机会学习一下.

early_param

相关代码:

C 复制代码
#define __initconst __section(.init.rodata)
/*                                                   
 * 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)                    
#ifndef MODULE

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

/*                                                                  
 * 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 }                    
#else /* MODULE */
#define __setup_param(str, unique_id, fn)   /* nothing */
#endif

可见early_param只是给内核用的, modules中不处理.

换句话说, 宏展开之后, 也就是放了一个名字以__setup_str_起始的静态指针常量, 单字节对齐, 置于.init.rodatasection中, 值为参数的名字. 又定义了一个存在于.init.setup中, 以long长度对齐的obs_kernel_param类型结构提, 名字以__setup_开头, 成员为刚刚的静态指针常量, 及函数名等.

那么关键点就是这个.init.rodata区域的值是怎么用作参数的呢?

已经获得的信息:

  1. 两个声明的section分别是.init.rodata.init.setup
  2. 结构体类型为obs_kernel_param.
  3. 变量名以__setup_str_起始.

先简单grep一下相关的变量名发现没找到:

bash 复制代码
[mxd@5 linux-4.19-loongson]$ grep __setup_str_ -rnI arch/loongarch/
[mxd@5 linux-4.19-loongson]$ 

cscope一下结构体obs_kernel_param:

html 复制代码
Cscope tag: obs_kernel_param
   #   line  filename / context / line
   1    241  include/linux/init.h <<GLOBAL>>
             struct obs_kernel_param {
   2    175  init/main.c <<GLOBAL>>
             extern const struct obs_kernel_param __setup_start[], __setup_end[];
   3    256  include/linux/init.h <<__setup_param>>
             static struct obs_kernel_param __setup_##unique_id \
   4    179  init/main.c <<obsolete_checksetup>>
             const struct obs_kernel_param *p;
   5    448  init/main.c <<do_early_param>>
             const struct obs_kernel_param *p;

可见, 第一个是结构提定义, 第二个和第三个是定义了一个全局数组, 第四个和第五个是在函数中调用. 总之在init/main.c中, 进去看看:

C 复制代码
extern const struct obs_kernel_param __setup_start[], __setup_end[];

/* 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;                                                   
}                                                               

其中obsolete_checksetup函数是在参数未知情况下的注册, 本文暂不讨论.

do_early_param中, 就是从先前声明的__setup_start__setup_end中取值, 那么这俩值又是从哪来的呢?

通过cscope并没有找到相应的定义, 这时只有两种可能, 一是该变量由链接脚本提供, 二是在非C源文件中定义, 比如汇编代码. 但无论如何, 这两者均在arch目录下. grep一下看看:

bash 复制代码
[mxd@5 linux-4.19-loongson]$ grep __setup_start -rn arch/loongarch/
arch/loongarch/kernel/vmlinux.lds:60: .init.data : AT(ADDR(.init.data) - 0) { KEEP(*(SORT(___kentry+*))) *(.init.data init.data.*) . = ALIGN(16); __setup_start = .; KEEP(*(.init.setup)) __setup_end = .; 

处理一下核心信息:

C 复制代码
. = ALIGN(16); 
__setup_start = .; 
KEEP(*(.init.setup)) 
__setup_end = .;

也就是在.init.setupsection中的内容, 也就是前面得到的线索1:

  1. 两个声明的section分别是.init.rodata.init.setup

继续分析代码, 从__setup_start开始遍历, 这里也有两种情况:

  1. 如果成员是early类型, 且成员中的变量名与传入的参数名一致
  2. 如果参数名是console, 且成员中的变量名是earlycon时.

这时则会调用该成员的setup_func函数.

举例

earlycon为例:

C 复制代码
/* 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);

根据前面对宏和代码的分析, 上面代码注册了一个obs_kernel_param类型的变量存在.init.setup中: __setup_param_setup_earlycon, 及一个存在于.init.rodata中的字符数组:__setup_str_param_setup_earlycon. 宏展开为:

C 复制代码
static const char __setup_str_param_setup_earlycon[] __section(.init.rodata)
    __aligned(1) = "earlycon";
    
static struct obs_kernel_param __setup_param_setup_earlycon __attribute__((__used__))
__section(.init.setup) __attribute__((aligned((sizeof(long)))))
    = {
        .str        = __setup_str_param_setup_earlycon, 
        .setup_func = param_setup_earlycon, 
        .early      = 1,
       };

所以当do_early_param函数中传入的参数是"earlycon"时, 其对应的early类型已经是1, 满足情景1:

  1. 如果成员是early类型, 且成员中的变量名与传入的参数名一致
  2. 如果参数名是console, 且成员中的变量名是earlycon时.

所以会执行setup_func函数, 也就是param_setup_earlycon函数, 执行的参数是do_early_param函数中传入的val, 这里假设上述代码中第一个分支便成立, 则执行earlycon_acpi_spcr_enable = true;return 0; 这其中的earlycon_acpi_spcr_enable是一个全局变量, 将在其他驱动中被调用, 此文不继续展开.

至此, 内核启动时, 若传入earlycon参数, 将会由此继续注册设备并生效.

参数格式

到现在, 我们已经明白了参数是如何注册和解析的, 那么参数的形式是怎么样的, 还并不清楚. 我们从新回到do_early_param函数. 通过cscope查看其调用过程:

C 复制代码
void __init parse_early_options(char *cmdline)               
{                                                            
    parse_args("early options", cmdline, NULL, 0, 0, 0, NULL,
           do_early_param);                                  
}                                                            
/* 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;                                                             
}                                                                         

除此之外还有一个early_platform_driver_register_all函数会调用parse_early_options, 经过查看这是特殊行为, 不继续展开了.

所以核心代码是从boot_command_line复制一份到tmp_cmdline并解析. 而boot_command_line通过early_memremap_ro(fw_arg1, COMMAND_LINE_SIZE)而来, 细追了一下发现是从一个物理地址传来, 也就是bootloader传递进来的, 暂不讨论.

解析过程其中涉及许多字符串, 锁, 及中断的解析, 不详细在本文展开, 在此仅作简单说明, 大致解析的格式按照如下顺序进行:

stateDiagram-v2 state 传入cmdline给parse_early_options { parse_early_options --> parse_args parse_args --> skip_spaces去除空格 skip_spaces去除空格 --> next_arg处理cmdline 去除双引号和空格,\n在'='位置处增加'\0'作为分隔符区分参数名和参数值,\n在空格处标记为参数值的结束位置,\n举例earlycon=uart,mmio,0x1fe001e0将会\n被拆分为earlycon\0和uart,mmio,0x1fe001e0\n前者作为参数名,后者作为参数值\n而类似single的参数将只有参数名,\n没有参数值 --> irqs_disabled关闭中断 irqs_disabled关闭中断 --> parse_one parse_one --> do_early_param传入对应的参数和值,\n进而执行相应的setup_func函数,\n如第一章内容 -- next_arg处理cmdline --> 去除双引号和空格,\n在'='位置处增加'\0'作为分隔符区分参数名和参数值,\n在空格处标记为参数值的结束位置,\n举例earlycon=uart,mmio,0x1fe001e0将会\n被拆分为earlycon\0和uart,mmio,0x1fe001e0\n前者作为参数名,后者作为参数值\n而类似single的参数将只有参数名,\n没有参数值 }

所以参数的格式将为:arg1=xxxarg, 千万不可使用空格随意拆分.

日常使用

除了根据代码学习增加参数的方式, 还要会看已经传递的参数:

bash 复制代码
mxd@mxd:~$ cat /proc/cmdline 
BOOT_IMAGE=/vmlinuz-linux root=UUID=a67aaec3-baec-4239-af30-f0a8ed5346ed rw loglevel=3 quiet

详细的功能即可在代码实现层进一步了解

参考文献

  1. 内核源码
相关推荐
落笔太慌张~2 分钟前
Linux系统(Ubuntu和树莓派)的远程操作练习
linux·运维·ubuntu
CZIDC15 分钟前
Linux系统安全-开发中注意哪些操作系统安全
linux·安全·系统安全
czhc114007566318 分钟前
LINUX 5 vim cat zip unzip
linux·编辑器·vim
明灯L1 小时前
《深度剖析 Linux 权限管理:从基础到进阶,解锁系统安全密钥》
linux·运维·全网最全权限管理·小白0基础
是覆盖对于变化2 小时前
ubuntu22.04 进入不了系统设置
linux·ubuntu
应以大橘为重2 小时前
interrupt子系统中的数据结构
linux·数据结构·驱动开发
kfepiza2 小时前
硬盘分区格式方案之 MBR(Master Boot Record)主引导记录详解 笔记250407
linux·windows·笔记
mzak2 小时前
已经安装了pip,出现pip command not found【解决方法】
linux·pip·python3
南风与鱼2 小时前
Linux 线程池
linux·线程池
Mazy.v3 小时前
Linux图形化界面
linux·运维·数据库