Fun with QEMU Arguments and Character Devices
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 typehelp
to see the commands you can use.serial0
andparallel0
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!