一. 前言
适配内核到新的平台,基本环境搭建好之后,首要的就是要调通调试串口,方便后面的信息打印。正常流程init/main.c中start_kernel入口,要到console_init之后才能真正打印,前面的打印都是缓存在printk的ringbuffer中的。如果在console_init前就异常了,此时就看不到打印信息,为了调试console_init前的状态,需要能更早的打印。内核提供了一种early打印的方式,尤其是riscv平台我们可以直接ecall调用opensbi的打印,这样opensbi适配好之后这里就可以直接使用。这一篇就来分析下kernel的early打印数据流。
二. 配置
使能early打印需要做一些配置
menuconfig
Device Drivers --->
Character devices --->
Serial drivers --->
[*] Early console using RISC-V SBI
对应配置项drivers/tty/serial/Kconfig中
SERIAL_EARLYCON_RISCV_SBI
config SERIAL_EARLYCON_RISCV_SBI
bool "Early console using RISC-V SBI"
depends on RISCV_SBI_V01
select SERIAL_CORE
select SERIAL_CORE_CONSOLE
select SERIAL_EARLYCON
help
Support for early debug console using RISC-V SBI. This enables
the console before standard serial driver is probed. This is enabled
with "earlycon=sbi" on the kernel command line. The console is
enabled when early_param is processed.
依赖RISCV_SBI_V01,该选项配置之后默认也会配置
SERIAL_CORE
SERIAL_CORE_CONSOLE
SERIAL_EARLYCON
其中RISCV_SBI_V01是默认使能的,依赖于RISCV_SBI
config RISCV_SBI_V01
bool "SBI v0.1 support"
default y
depends on RISCV_SBI
help
This config allows kernel to use SBI v0.1 APIs. This will be
deprecated in future once legacy M-mode software are no longer in use.
RISCV_SBI默认也是y,依赖!RISCV_M_MODE
config RISCV_SBI
bool
depends on !RISCV_M_MODE
default y
RISC_M_MODE又依赖!MMU,即如果不适用MMU则内核跑M模式
set if we run in machine mode, cleared if we run in supervisor mode
config RISCV_M_MODE
bool
default !MMU
MMU默认是y
config MMU
bool "MMU-based Paged Memory Management Support"
default y
help
Select if you want MMU-based virtualised addressing space
support by paged memory management. If unsure, say 'Y'.
配置后output/.config中
CONFIG_SERIAL_EARLYCON_RISCV_SBI=y
output/include/config/auto.conf中
CONFIG_SERIAL_EARLYCON_RISCV_SBI=y
output/include/generated/autoconf.h中
#define CONFIG_SERIAL_EARLYCON_RISCV_SBI 1
drivers/tty/serial/Makefile中编译对应的文件 earlycon-riscv-sbi.c
obj-$(CONFIG_SERIAL_EARLYCON_RISCV_SBI) += earlycon-riscv-sbi.o
三. 相关代码
3.1 Kernel中ecall调用opensbi的打印
drivers/tty/serial/earlycon-riscv-sbi.c
其中early_sbi_setup设置对应的写接口
static int __init early_sbi_setup(struct earlycon_device *device,
const char *opt)
{
device->con->write = sbi_console_write;
return 0;
}
实现如下
static void sbi_console_write(struct console *con,
const char *s, unsigned n)
{
struct earlycon_device *dev = con->data;
uart_console_write(&dev->port, s, n, sbi_putc);
}
最终实际是调用sbi_putc接口写
static void sbi_putc(struct uart_port *port, int c)
{
sbi_console_putchar(c);
}
sbi_console_putchar在arch/riscv/kernel/sbi.c中实现
通过ecall调用opensbi中的实现,
前面我们看到CONFIG_RISCV_SBI_V01是使能的
/**
* sbi_console_putchar() - Writes given character to the console device.
* @ch: The data to be written to the console.
*
* Return: None
*/
void sbi_console_putchar(int ch)
{
sbi_ecall(SBI_EXT_0_1_CONSOLE_PUTCHAR, 0, ch, 0, 0, 0, 0, 0);
}
EXPORT_SYMBOL(sbi_console_putchar);
sbi_ecall在
arch/riscv/kernel/sbi.c中实现
struct sbiret sbi_ecall(int ext, int fid, unsigned long arg0,
unsigned long arg1, unsigned long arg2,
unsigned long arg3, unsigned long arg4,
unsigned long arg5)
{
struct sbiret ret;
register uintptr_t a0 asm ("a0") = (uintptr_t)(arg0);
register uintptr_t a1 asm ("a1") = (uintptr_t)(arg1);
register uintptr_t a2 asm ("a2") = (uintptr_t)(arg2);
register uintptr_t a3 asm ("a3") = (uintptr_t)(arg3);
register uintptr_t a4 asm ("a4") = (uintptr_t)(arg4);
register uintptr_t a5 asm ("a5") = (uintptr_t)(arg5);
register uintptr_t a6 asm ("a6") = (uintptr_t)(fid);
register uintptr_t a7 asm ("a7") = (uintptr_t)(ext);
asm volatile ("ecall"
: "+r" (a0), "+r" (a1)
: "r" (a2), "r" (a3), "r" (a4), "r" (a5), "r" (a6), "r" (a7)
: "memory");
ret.error = a0;
ret.value = a1;
return ret;
}
其中第一个参数ext即SBI_EXT_0_1_CONSOLE_PUTCHAR=1放在寄存器a7中。
3.2 Kernel中初始化路径
3.2.1命令行参数param_setup_earlycon(line)
配置参数
要使能earlycon功能,需要给内核传入参数earlycon=xxx,
xxx表示对应的驱动和参数,我们这里是sbi使用sbi的串口输出,如果没有指定xxx则从设备树的chosen节点解析串口。
参考《https://www.kernel.org/doc/html/v4.14/admin-guide/kernel-parameters.html》下搜索earlycon。
对应流程如下,如下位置对early后面是否有参数进行不同的处理
可以手动添加参数:
arch/riscv/Kconfig中如下配置CMDLINE
config CMDLINE
string "Built-in kernel command line"
help
For most platforms, the arguments for the kernel's command line
are provided at run-time, during boot. However, there are cases
where either no arguments are being provided or the provided
arguments are insufficient or even invalid.
When that occurs, it is possible to define a built-in command
line here and choose how the kernel should use it later on.
menuconfig配置
Boot options --->
(earlycon=sbi) Built-in kernel command line
配置完对应output/.config中
CONFIG_CMDLINE="earlycon=sbi"
include/generated/autoconf.h中
#define CONFIG_CMDLINE "earlycon=sbi"
参数解析过程
我们就来分析下earlycon=sbi时的路径。
drivers/tty/serial/earlycon.c中
early_param("earlycon", param_setup_earlycon);
其中include/linux/init.h中
__setup_param(str, fn, fn, 1)
static const char __setup_str_
__aligned(1) = str; \
static struct obs_kernel_param __setup_
__used __section(".init.setup") \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_
展开为
__setup_param("earlycon", param_setup_earlycon, param_setup_earlycon,1)
继续展开
static const char __setup_str_ param_setup_earlycon[] __initconst
__aligned(1) = “earlycon”;
static struct obs_kernel_param __setup_param_setup_earlycon
__used __section(".init.setup")
__attribute__((aligned((sizeof(long)))))
={__setup_str_param_setup_earlycon, param_setup_earlycon, 1}
即定义了一个static struct obs_kernel_param类型结构体变量__setup_param_setup_earlycon,
放置在段.init.setup中按照sizeof(long)对齐,结构体内容是
{.str=__setup_str_param_setup_earlycon,
.setup_func=param_setup_earlycon,
.early= 1}
__setup_str_param_setup_earlycon即前面的字符数组内容是”earlycon”。
param_setup_earlycon是回调函数
其中结构体 struct obs_kernel_param如include/linux/init.h
struct obs_kernel_param {
const char *str;
int (*setup_func)(char *);
int early;
};
include/asm-generic/vmlinux.lds.h中
#define INIT_SETUP(initsetup_align) \
. = ALIGN(initsetup_align); \
__setup_start = .; \
KEEP(*(.init.setup)) \
__setup_end = .;
#define INIT_DATA_SECTION(initsetup_align) \
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \
INIT_DATA \
INIT_SETUP(initsetup_align) \
INIT_CALLS \
CON_INITCALL \
INIT_RAM_FS \
}
arch/riscv/kernel/vmlinux.lds.S中
INIT_DATA_SECTION(16)
所以上述变量放在了.init.setup段
开始位置是__setup_start
init/main.c中申明变量以便访问
extern const struct obs_kernel_param __setup_start[], __setup_end[];
如下函数遍历上述.init.setup段,遍历每个结构体,回调对应的setup_func,这里即param_setup_earlycon。
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;
}
在obsolete_checksetup这里打断点,我们来跟踪分析函数的处理过程
此时我们看到
unknown_bootoption传过来的参数值为param=earlycon,val=sbi,
正是我们配置的参数。
至于参数是怎么解析的参考parse_args。
调用路径是start_kernel->pares_args->unknown_bootoption
执行完repair_env_string
参数param变为了earlycon=sbi
继续单步进入obsolete_checksetup
然后从__setup_start开始遍历段,需要满足参数即line和p->str参数匹配这里line就是earlycon=sbi,通过函数parameqn匹配,然后要满足p->early=1
再来看我们定义的结构体
early_param("earlycon", param_setup_earlycon);
会满足这两个条件
static const char __setup_str_ param_setup_earlycon[] = “earlycon”;
static struct obs_kernel_param __setup_param_setup_earlycon
{.str=__setup_str_param_setup_earlycon,
.setup_func=param_setup_earlycon,
.early= 1}
接下来
line[n] == '='满足
这里直接返回true,所以如下路径是不通的,这里路只能使early不带参数时,走early_init_dt_scan_chosen_stdout查找/chosen的节点。
此时就不会调用param_setup_earlycon(line)
而只有此时不带参数即只有early时才会调用setup_func即param_setup_earlycon,传入的参数line=空
实际early=sbi这里走到是另外一条路,如下图所示
对应do_early_param时会回调setup_func,但是此时传递的参数param_setup_earlycon(line)中的line是val即sbi了。和上面一条路传递的参数early不一样了。
do_early_param时param=early,val=sbi
param_setup_earlycon(line)时line=sbi
而如果走obsolete_checksetup这边的路径,line传入的参数为空
此时回调
param_setup_earlycon(line)
时走early_init_dt_scan_chosen_stdout
注册处理
EARLYCON_DECLARE(sbi, early_sbi_setup);在指定段中放置结构体指针,
初始化时遍历该结构体指针,找到结构体,调用其setup函数进行初始化。
在include/linux/serial_core.h中
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
_OF_EARLYCON_DECLARE(_name, compat, fn, \
__UNIQUE_ID(__earlycon_
就是定义了一个结构体
struct earlycon_id
在include/linux/serial_core.h中
struct earlycon_id {
char name[15];
char name_term; /* In case compiler didn't '\0' term name */
char compatible[128];
int (*setup)(struct earlycon_device *, const char *options);
};
static const struct earlycon_id __earlycon_sbi且
其成员
.setup = early_sbi_setup
然后定义了一个指针变量__p__earlycon_sbi指向了这个结构体
static const struct earlycon_id * const __p__earlycon_sbi = & __earlycon_sbi
该指针放在了段__earlycon_table中
source/include/asm-generic/vmlinux.lds.h中
__earlycon_table = .; \
KEEP(*(__earlycon_table)) \
__earlycon_table_end = .;
符号__earlycon_table表示该段的开始
drivers/tty/serial/earlycon.c中
setup_earlycon
如果是走右边这条路
drivers/of/fdt.c中
early_init_dt_scan_chosen_stdout
最终都是注册控制台register_console
而走左边的sbi路径会回调early_sbi_setup将写接口改为
device->con->write = sbi_console_write;调用sbi打印。
3.3. Opensbi代码中实现
lib/sbi/sbi_ecall.c中
ecall处理入口如下
int sbi_ecall_handler(struct sbi_trap_regs *regs)
{
int ret = 0;
struct sbi_ecall_extension *ext;
unsigned long extension_id = regs->a7;
unsigned long func_id = regs->a6;
struct sbi_trap_info trap = {0};
unsigned long out_val = 0;
bool is_0_1_spec = 0;
ext = sbi_ecall_find_extension(extension_id);
if (ext && ext->handle) {
ret = ext->handle(extension_id, func_id,
regs, &out_val, &trap);
if (extension_id >= SBI_EXT_0_1_SET_TIMER &&
extension_id <= SBI_EXT_0_1_SHUTDOWN)
is_0_1_spec = 1;
} else {
ret = SBI_ENOTSUPP;
}
ext即传过来的a7,待打印字符通过a0传递。以下即获取该ext。
ext = sbi_ecall_find_extension(extension_id);
include/sbi/sbi_ecall_interface.h中定义对应的ext宏。
#define SBI_EXT_0_1_CONSOLE_PUTCHAR 0x1
#define SBI_EXT_0_1_CONSOLE_GETCHAR 0x2
然后回调对应的处理接口
ret = ext->handle(extension_id, func_id,
regs, &out_val, &trap);
Handle回调在如下地方注册
sbi_ecall_init->
ret = sbi_ecall_register_extension(&ecall_legacy);
sbi_ecall_register_extension在lib/sbi/sbi_ecall.c中实现
int sbi_ecall_register_extension(struct sbi_ecall_extension *ext)
{
struct sbi_ecall_extension *t;
if (!ext || (ext->extid_end < ext->extid_start) || !ext->handle)
return SBI_EINVAL;
sbi_list_for_each_entry(t, &ecall_exts_list, head) {
unsigned long start = t->extid_start;
unsigned long end = t->extid_end;
if (end < ext->extid_start || ext->extid_end < start)
/* no overlap */;
else
return SBI_EINVAL;
}
SBI_INIT_LIST_HEAD(&ext->head);
sbi_list_add_tail(&ext->head, &ecall_exts_list);
return 0;
}
ecall_legacy在
lib/sbi/sbi_ecall_legacy.c中定义
struct sbi_ecall_extension ecall_legacy = {
.extid_start = SBI_EXT_0_1_SET_TIMER,
.extid_end = SBI_EXT_0_1_SHUTDOWN,
.handle = sbi_ecall_legacy_handler,
};
所以最终回调其handle即
sbi_ecall_legacy_handler在lib/sbi/sbi_ecall_legacy.c中实现
该函数就根据ext=SBI_EXT_0_1_CONSOLE_PUTCHAR,最终调用sbi_putc发送一个字符
case SBI_EXT_0_1_CONSOLE_PUTCHAR:
sbi_putc(regs->a0);
break;
case SBI_EXT_0_1_CONSOLE_GETCHAR:
ret = sbi_getc();
break;
四. 仿真调试
查看early_sbi_setup的调用路径
参数为sarlycon=sbi时
走setup_earlycon
在如下位置打断点运行到到断点处,查看调用栈
hb setup_earlycon
c
hb early_sbi_setup
c
bt
对应如下路径
参数为earlycon时
此时走设备树/chosen的串口
走early_init_dt_scan_chosen_stdout
hb early_init_dt_scan_chosen_stdout
c
查看sbi_console_write调用路径
hb sbi_console_write
c
bt
看到在console_init();之前此时就可以打印了,之前是必须要在console_init();之后才真正打印,之前是打印在ringbuffer中的缓存的。
最终是调用ecall完成打印
五. 总结
如果要使能early通过opensbi打印,要使配置参数early=sbi,且使能early打印,此时在console_init之前就可以进行直接打印。