If you do man qemu-system and search for -serial you would come across a good explanation of how the qemu terminal emulation works. Let’s have abit of fun with it.

Playing with Character Devices

qemu-system-x86_64 -kernel ./vmlinux 

pops up a window with a few tabs, VGA, compat_monitor0, serial0, parallel0 (Ctrl-Alt M > View > Show Tabs)

  • VGA is what you would see if you connected a monitor to the system you are booting up.
  • compat_monitor0 is the qemu monitor that you can use to interact with the qemu instance. (qemu). You can type help to see the commands you can use.
  • serial0 and parallel0 are buses or channels of communication between the host and the emulated qemu instance. I like to think of parallel as the 64-bit bus and serial as RX/TX UART communication.
qemu-system-x86_64 -kernel ./vmlinux -append "console=ttyS0 meowmeow"

Just like how you can parse arguments into an executable like ./a.out param1 param2, you can also parse arguments into ./vmlinux (just that you can’t execute it directly and need to use qemu or boot it from a bootloader). In this case we pass an argument console=ttyS0 to tell the kernel to use ttyS0 as the console. We also pass meowmeow as an argument, which is just a random marker I will find in the kernel logs (when we log to a file). For now you see a lot of stuff spew out in the serial0 tab.

You could try other consoles too listed here. I tried putting a funny Baud rate but nothing happened.

qemu-system-x86_64 -kernel ./vmlinux -append "console=ttyS0" -serial file:serial.log 
cat serial.log # see kernel boot logs
cat serial.log | grep meowmeow
[    0.000000] Command line: console=ttyS0 meowmeow
[    0.058980] Kernel command line: console=ttyS0 meowmeow
[    0.059534] Unknown kernel command line parameters "meowmeow", will be passed to user space.

You can specify many -serial flags, it would correspond to serial0, serial1, etc. After each -serial, you specify a character device, which could be many things. In this example, we chose a write-only chardev which writes to a file. Read the man page or -chardev help to see all options.

We also observe that whatever we parse into -append gets sent to the kernel file vmlinux. So it comes as no surprise later we send things like console=ttyS0 panic=1 root=/dev/sda rootfstype=ext2. Basically just telling the kernel what to do when it panics and how to work with the emulated hardware we pass it.

qemu-system-x86_64 -kernel ./vmlinux -append "console=ttyS0" -serial file:serial.log -chardev file,path=mon.log,mux=on,id=char0 -mon chardev=char0,mode=readline

This binds the file mon.log to the qemu monitor. When we open it up, we see

$ cat mon.log
QEMU 4.2.1 monitor - type 'help' for more information
(qemu)

Practically speaking, it doesn’t make sense to bind a monitor interactive console to a write-only file. So let’s bind it to stdio instead.

qemu-system-x86_64 -chardev stdio,id=char0 -mon chardev=char0,mode=readline -kernel ./vmlinux -append "console=ttyS0"

In the graphical window, open the serial0 tab. Then in the monitor, you can play around

(qemu) info chardev
parallel0: filename=vc
char0: filename=stdio
serial0: filename=vc
(qemu) system_reset
(qemu) stop
(qemu) cont

If you don’t like the kernel logs to appear in graphical window, you can bind -serial chardev:char0 too. Now you can cycle between compat-monitor0 and serial0 using C-A C.

If we do this to -parallel 0 (which is currently blank anyway), and also do -display none. Then we have effectively done -nographic ourselves using:

NOGRAPHIC="-chardev stdio,mux=on,id=char0 -mon chardev=char0,mode=readline -serial chardev:char0 -parallel chardev:char0 -display none"

Here are some differences, but please DO NOT use $NOGRAPHIC because I found it causes later issues with the shell (busybox segfault). Just use -nographic and treat $NOGRAPHIC like an academic curiosity. | -nographic | $NOGRAPHIC | | ———– | ———– | | messes up wrapping (fixed with reset) | doesn’t | | prints VGA stuff to console (“Booting from ROM”) | doesn’t | | works with shell | doesn’t (OH NO BAD) |

You can have abit more fun here with some automation.

Trying to Boot

qemu-system-x86_64 -nographic -kernel ./vmlinux -append "console=ttyS0"

Anyway, enough playing with chardevs and terminals, we see that the kernel is looking for a blockdev root device

[    1.332198] /dev/root: Can't open blockdev
[    1.332485] VFS: Cannot open root device "(null)" or unknown-block(0,0): error -6
[    1.332612] Please append a correct "root=" boot option; here are the available partitions:
[    1.332827] 0b00         1048575 sr0 

So let’s try to add a root device

dd if=/dev/zero of=rootfs.ext2 bs=1024k count=256
mkfs.ext2 rootfs.ext2
qemu-system-x86_64 -nographic -kernel ./vmlinux -append "console=ttyS0 root=/dev/sda" -hda rootfs.ext2

You can pass in more drives using -hda, -hdb, -hdc, -hdd. But it will fill up /dev/sda,sdb,sdc,sdd sequentially. Then

(qemu) info block

It still crashes, but now it complains there is no init.

Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/admin-guide/init.rst for guidance.

Let’s try writing out own init to get a feel for what the kernel wants.

$ mkdir vfs && cd vfs
$ cat << EOF > hello-kernel.c
#include <stdio.h>

int main(){
        printf("Hello, kernel!\n");
        sleep(9999999999999);
}
EOF
$ gcc --static hello-kernel.c -o init
$ find . | cpio -o -H newc | gzip > root.cpio.gz
$ qemu-system-x86_64 -nographic -kernel ./vmlinux -append "console=ttyS0 root=/dev/sda" -hda rootfk.ext2 -initrd vfs/root.cpio.gz 

This shows

[    1.504610] Run /init as init process
Hello, kernel!

Yay! If you have segmentation fault after typing, make sure you are using -nographic (instead of my $NOGRAPHIC) because I think when you do your own multiplexing it confuses qemu (probably a bug, would be interesting to investigate).

Interestingly, if we have an -initrd we actually don’t need a -hda. We can just do

qemu-system-x86_64 -kernel ./vmlinux -append "console=ttyS0" -initrd vfs/root.cpio.gz

it works fine too. Instead of using our own init, we can use busybox’s init. Check out my minimal kernel blog for details.

Anyway, the main purpose of this blog was to document my understanding of character devices and the many qemu parameters, so I will stop here. Hope you found it useful! Contact me at telegram @tch1001 if you need any help!