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有正常打印 ;

posted @ 2022-07-27 17:21  solonj  阅读(1843)  评论(0编辑  收藏  举报