Linux Early printk技术
整理了代码、资料、博客,发现一个小小的调试阶段的串口打印,自己移植还是需要修改很多部分:
xxx_defconfig 、 menuconfig 、 Kconfig 、 Kconfig.debug 、 uboot的bootargs...
一、early_print 、printk、early_printk关系
early_print会调用printk;
printk在register_console之前,可以调用early_printk(如果启用了earlyprintk功能)进行打印;
printk在register_console之前,会调用正式的串口控制台consele->write 进行打印;
early_print
\
printk
/ \
early_printk console->write
early_printk -> early_vprintk -> early_console_dev.write -> early_console_write -> early_write -> printch/printascii
printk() --> vprintk_func() --> vprintk_default() --> vprintk_emit --> console_unlock() --> call_console_drivers() --> con->write()
con->write()就是early_console_dev->write().
earlyprintk的目的: 打印、调试 在register_console之前的内核代码!
关于register_console
register_sonsole()是console的重要流程,可以此为切入点学习console的实现。
这个函数实现了一些约束,也是console的约束:
console分为两类,一类是boot console,另一类是real console
可以注册多个boot console
一旦注册了一个real console,则所有的boot console都会被注销
一旦注册了real console则无法再注册boot console
通过阅读源码,可知注册多个real console是可行的。
所有注册的console是通过console_drivers链表管理的。
earlycon
并非所有的arch都有early_printk。
arm64就没有实现early_printk,而是使用更通用的earlycon。
earlycon是通用的,其驱动源码位于:drivers/tty/serial/earlycon.c
可以通过cmdline或devicetree指定earlycon的设备。
在device tree中指定earlycon的方法是:
bootargs = "earlycon=xxx,yyy,zzz"
启用early printk(boot阶段串口控制台)的方法
1、uboot的bootargs 加入 earlyprintk 参数;
2、Linux make menuconfig中选择“Early printk”,
因为arch/arm/kernel/Makefile中 obj-$(CONFIG_EARLY_PRINTK) += early_printk.o
3、arch/arm/configs/s3c2410_defconfig 中 CONFIG_DEBUG_KERNEL=y 、 CONFIG_DEBUG_LL=y
CONFIG_DEBUG_LL 依赖 CONFIG_DEBUG_KERNEL
DEBUG_S3C_UART0 依赖 CONFIG_DEBUG_LL
DEBUG_S3C_UART0 使能 DEBUG_S3C24XX_UART
DEBUG_S3C24XX_UART 依赖 ARCH_S3C24XX
二、分析early_printk
arch/arm/kernel/early_printk.c
extern void printascii(const char *);
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,
};
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);
流程简介:
在 bootargs 中添加 earlyprintk;
在内核启动进入C语言阶段,start_kernel->parse_early_param 就会第一时间解析early_param(“earlyprintk”, setup_early_printk);
然后调用 register_console(&early_console_dev);
在xxx_defconfig中定义CONFIG_DEBUG_LL=y
那么arch/arm/kernel/setup.c 的 early_print 也能实现早期打印
void __init early_print(const char *str, ...)
{
extern void printascii(const char *);
char buf[256];
va_list ap;
va_start(ap, str);
vsnprintf(buf, sizeof(buf), str, ap);
va_end(ap);
#ifdef CONFIG_DEBUG_LL
printascii(buf);
#endif
printk("%s", buf);
}
early_param 设置bootargs传参与调用函数的关系
简单来说,它定义了一个结构体:
static struct obs_kernel_param __setup_setup_early_printk {
.str = "earlyprintk",
.setup_func = setup_early_printk,
.early = 1,
};
kernel/printk/printk.c
asmlinkage __visible void early_printk(const char *fmt, ...)
{
va_list ap;
char buf[512];
int n;
if (!early_console)
return;
va_start(ap, fmt);
n = vscnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
early_console->write(early_console, buf, n);
}
do_early_param 根据uboot的参数来调用setup_func
start_kernel()
...
setup_arch()//从命令行或者设备树中获取bootargs参数。
...
parse_early_param()//开始分析 bootargs参数,这里面会判断如果命令行中有earlyprintk关键字,并且对应的存在struct obs_kernel_param 结构体与之匹配,那么回调用对应结构体的setup_func函数
strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);//复制一份命令行参数
parse_early_options(tmp_cmdline);//开始分析
parse_args("early options", cmdline, NULL, 0, 0, 0, NULL, do_early_param);
static int __init do_early_param(char *param, char *val, const char *unused, void *arg)//回调do_early_param函数
bootargs中有earlyprintk,就会调用do_early_param:
extern const struct obs_kernel_param __setup_start[], __setup_end[];
static int __init do_early_param(char *param, char *val,
const char *unused, void *arg)
{
const struct obs_kernel_param *p;
//以下的for循环会从obs_kernel_param结构体列表中遍历,有两种情况会执行该结构体的setup_func函数:
//1.如果成员early=1并且bootargs中存在该结构体的str字符串;
//2.如果该结构体的str为earlycon,并且bootargs中有console关键字;
for (p = __setup_start; p < __setup_end; p++) {
if ((p->early && parameq(param, p->str)) ||
(strcmp(param, "console") == 0 &&
strcmp(p->str, "earlycon") == 0)
) {
//如果符合以上中的任意一个,就会执行对应的setup_func函数
if (p->setup_func(val) != 0)
pr_warn("Malformed early option '%s'\n", param);
}
}
/* We accept everything at this stage. */
return 0;
}
注册前期的console(后期正式注册串口console会使这个console失效)
//这个early_console_dev只有write接口,没有setup接口,所以就只是实现简单的写串口操作
static struct console early_console_dev = {
.name = "earlycon",
.write = early_console_write,
.flags = CON_PRINTBUFFER | CON_BOOT,
.index = -1,
};
static int __init setup_early_printk(char *buf)
{
early_console = &early_console_dev;
//调用register_console函数,注册了一个console,
//提前说下,在串口没有被真正初始化之前,printk使用的是early_console_dev这个console
register_console(&early_console_dev);
return 0;
}
其中write方法用于实现console输出的入口,也early console的核心。
CON_PRINTBUFFER标识,表示注册这个console的时候,需要把printk的buf中的log通过这个console进行输出。
CON_BOOT标识,表示这是一个boot console(bcon)。当启动过程了注册其他非boot console的时候,需要先卸载掉这个console。
printk调用.write
DEFINE_PER_CPU(printk_func_t, printk_func) = vprintk_default;
printk(const char *fmt, ...)
vprintk_func = this_cpu_read(printk_func);//这句话等价vprintk_func=vprintk_default;我把这段分析放在文章最后。
r = vprintk_func(fmt, args);//实际调用的是函数vprintk_default(fmt, args)
vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, 0, fmt, args)
...
void console_unlock(void)
.../在函数call_console_drivers调用了early_console_dev的write接口函数
static void call_console_drivers(int level, const char *ext_text, \
size_t ext_len, const char *text, size_t len) {
...
for_each_console(con) {
if (exclusive_console && con != exclusive_console)
continue;//之前已经设置exclusive_console=early_console_dev;所以只会匹配early_console_dev设备
if (!(con->flags & CON_ENABLED))
continue;//在register_console中使能过了
if (!con->write)
continue;//early_console_dev存在write接口函数
if (!cpu_online(smp_processor_id()) &&
!(con->flags & CON_ANYTIME))
continue;
if (con->flags & CON_EXTENDED)
con->write(con, ext_text, ext_len);
else
con->write(con, text, len);//调用该接口函数
}
}
...//
所以,在执行完parse_early_param函数之后,如果使能了earlyprintk,那么就可以通过printk看到串口输出了。
汇编实现 printch/printascii
掉用关系 early_printk -> early_console_dev.write -> early_console_write -> early_write -> printch/printascii
arch/arm/kernel/debug.S
ENTRY(printascii)
addruart_current r3, r1, r2
b 2f
1: waituart r2, r3
senduart r1, r3
busyuart r2, r3
teq r1, #'\n'
moveq r1, #'\r'
beq 1b
2: teq r0, #0
ldrneb r1, [r0], #1
teqne r1, #0
bne 1b
ret lr
ENDPROC(printascii)
ENTRY(printch)
addruart_current r3, r1, r2
mov r1, r0
mov r0, #0
b 1b
ENDPROC(printch)
.macro busyuart, rd, rx
3201: ldrb \rd, [\rx, #0x7C] /* \rd = *(\rx + 0x7c) */
and \rd, \rd, #0x01 /* \rd = \rd & 0x01 */
teq \rd, #0x0 /* \rd = 0 ?? */
beq 3201b
.endm
addruart_current 、waituart 、senduart 、busyuart就是往寄存器赋值控制了。
注意:直接往寄存器写值实现串口打印,是基于我们在uboot中已经把串口初始化过了。
addruart
arch/arm/include/debug/s3c24xx.S
作用是将串口的虚拟地址和物理地址传递给内核
arch/arm/Kconfig.debug
config DEBUG_UART_PHYS
default 0x50000000 if DEBUG_S3C24XX_UART && (DEBUG_S3C_UART0 || \
DEBUG_S3C2410_UART0)
config DEBUG_UART_VIRT
default 0xf7000000 if DEBUG_S3C24XX_UART && (DEBUG_S3C_UART0 || \
DEBUG_S3C2410_UART0)
再在head.S 实现串口(用来打印输出)的物理地址和虚拟地址的映射,并保存在r3和r7寄存器中。这点尤其重要,要是没实现,在MMU打开后串口无法输出调试。
测试一
bootargs不加earlyprintk,early_print也会打印几句话:
init/main.c start_kernel 538
init/main.c start_kernel 556
__atags_pointer = 33afb000
dt_phys =33afb000
phys_to_virt(dt_phys) =c3afb000
devtree.c of_flat_dt_match_machine
Get mdesc from dtb!
测试证明是 early_print 中的printascii(buf) 打印出来的, 而不是printk("%s", buf) ;
测试证明去掉arch/arm/include/debug/s3c24xx.S 的 .macro addruart, rp, rv, tmp
,无打印 ;
测试证明去掉arch/arm/include/debug/samsung.S的.macro senduart,rd,rx
,无打印;
测试证明去掉arch/arm/include/debug/samsung.S的.macro waituart,rd,rx
,打印丢失部分;
测试证明去掉arch/arm/include/debug/samsung.S的.macro busyuart, rd, rx
,打印无影响;
奇怪点:为什么不加earlyprintk能打印上述几句话?
测试二
bootargs去掉earlyprintk,menuconfig中选中 Early printk选项:同一位置,early_print有打印, early_printk 无打印, printk有正常打印 ;
bootargs去掉earlyprintk,menuconfig中去掉 Early printk选项:同一位置,early_print有打印, early_printk 无打印, printk有正常打印 ;