Introduction Link to heading

I was trying to boot linux with qemu and it didn’t work until i added console=ttyS0.So, I decided to take a deep dive into the kernel boot sequence to understand it.

I looked into kernel docs,and found console supports several console types like ttyS, tty, ttyUSB and few others. but i was interested in ttyS0 only for now.

Boot options registration Link to heading

Linux has infrastructure to register boot options and parsers will iterate boot options and call the registered functions.

in init/main.c, the chain of calls will eventually call do_early_param which specifically looks for "console" and calls setup_func() through obs_kernel_param structure.

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

So, what are __setup_start, __setup_end and setup_func?

__setup_start are __setup_end pointers built into the elf at compile time with super linker scripts and macros. anyway, that’s topic for another post.

In kernel/printk/printk.c, there is the registration of the boot option

__setup("console=", console_setup);

tracing it down the rabbit hole

#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 }

#define __setup(str, fn)						\
	__setup_param(str, fn, fn, 0)

so, in our case, setup_func is console_setup.

and in include/asm-generic/vmlinux.lds.h

#define INIT_SETUP(initsetup_align)					\
		. = ALIGN(initsetup_align);				\
		VMLINUX_SYMBOL(__setup_start) = .;			\
		KEEP(*(.init.setup))					\
		VMLINUX_SYMBOL(__setup_end) = .;

back to the console, console_setup does basic parsing of console= options

static int __init console_setup(char *str)
{
	if (str[0] >= '0' && str[0] <= '9') {
		strcpy(buf, "ttyS");
		strncpy(buf + 4, str, sizeof(buf) - 5);
	} else {
		strncpy(buf, str, sizeof(buf) - 1);
	}

	for (s = buf; *s; s++)
		if (isdigit(*s) || *s == ',')
			break;
	idx = simple_strtoul(s, NULL, 10);
	*s = 0;

	__add_preferred_console(buf, idx, options, brl_options);
	console_set_on_cmdline = 1;
	return 1;
}
__setup("console=", console_setup);

The real work is done by __add_preferred_console. it will put the boot option in global array console_cmdline. Here is the declaration from printk.c

#define MAX_CMDLINECONSOLES 8

static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];

Note that __add_preferred_console will use an existing entry if found. And it will be marked as preferred_console.

Normally, it wouldn’t match anything and c pointer will point to the next empty entry in the array. in my case, loop didn’t even start because nothing was in console_cmdline.

So, using console=ttyS0 will populate the first entry in the console_cmdline only.

static int __add_preferred_console(char *name, int idx, char *options,
				   char *brl_options)
{
	struct console_cmdline *c;
	int i;

	/*
	 *	See if this tty is not yet registered, and
	 *	if we have a slot free.
	 */
	for (i = 0, c = console_cmdline;
	     i < MAX_CMDLINECONSOLES && c->name[0];
	     i++, c++) {
		if (strcmp(c->name, name) == 0 && c->index == idx) {
			if (!brl_options)
				preferred_console = i;
			return 0;
		}
	}

	if (i == MAX_CMDLINECONSOLES)
		return -E2BIG;
	if (!brl_options)
		preferred_console = i;
	strlcpy(c->name, name, sizeof(c->name));
	c->options = options;
	braille_set_options(c, brl_options);
	c->index = idx;
	return 0;
}

Driver Registration Link to heading

Now we have console_cmdline populated with boot options, something needs to handle these consoles. well, here comes the driver :)

in kernel/printk/printk.c, the console_cmdline array is accessed through register_console which matches the name of console to driver and does some magic to print the messages in printk buffer.

for uart serial console, i have 8259 driver compiled as part of kernel. from drivers/tty/serial/8250/8250_core.c

static int __init univ8250_console_init(void)
{
	if (nr_uarts == 0)
		return -ENODEV;

	serial8250_isa_init_ports();
	register_console(&univ8250_console);
	return 0;
}
console_initcall(univ8250_console_init);

Note that console_initcall has to be called after __setup. So, that driver can find the console device.

The driver will define needed function for setup/write

static struct console univ8250_console = {
	.name		= "ttyS",
	.write		= univ8250_console_write,
	.device		= uart_console_device,
	.setup		= univ8250_console_setup,
	.match		= univ8250_console_match,
	.flags		= CON_PRINTBUFFER | CON_ANYTIME,
	.index		= -1,
	.data		= &serial8250_reg,
};

The interesting part is in register_console, the comments says that console_unlock will flush the buffer.

		/*
		 * console_unlock(); will print out the buffered messages
		 * for us.
		 */
		logbuf_lock_irqsave(flags);
		console_seq = syslog_seq;
		console_idx = syslog_idx;
		logbuf_unlock_irqrestore(flags);
		/*
		 * We're about to replay the log buffer.  Only do this to the
		 * just-registered console to avoid excessive message spam to
		 * the already-registered consoles.
		 */
		exclusive_console = newcon;
	}
	console_unlock();

So, console_unlock will call call_console_drivers which will call write registered in the driver.

static void call_console_drivers(const char *ext_text, size_t ext_len,
				 const char *text, size_t len)
{
	struct console *con;

	trace_console_rcuidle(text, len);

	if (!console_drivers)
		return;

	for_each_console(con) {
		if (exclusive_console && con != exclusive_console)
			continue;
		if (!(con->flags & CON_ENABLED))
			continue;
		if (!con->write)
			continue;
		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);
	}
}

Reference Link to heading