Memtest86+ contd.

Code

In this section we take a closer look at the few interesting parts of the code itself. The sources are located in my git repository1 and so I will describe here only the interesting parts. The git repository starts at point that corresponds to version 5.01b from the Debian upstream. I would prefer to clone the original repository, but after I contacted the maintainer he informed me that the main devel repo is private and works on making it public are ongoing.

Running and testing

I tested results of my work on the qemu emulator with KVM enabled. I wanted to boot the binary with multiboot strategy so I had two options for this:

  • QEMU offers the -kernel and -append options that can be used to provide the kernel in the multiboot1 or bzImage format and the kernel command line respectively.
  • Create an ISO image with the binary and GRUB2 on it and boot from the image with the -cdrom option of qemu.

I decided to go with the second option for two reasons. First, it is the same way the memtest will be booted on a real hardware, which is great because possible bugs will be discovered early. And second, the memtest claims to be both multiboot and bzImage at the same time and I was not sure which one would be used by the qemu if provided to the -kernel. This is nothing that could not be easily discovered but there was no reason to use this anyway. To make things easier, I already had the required script to build the image from our school hypervisor.

Assume that the memtest is already built, then we create the file bootable.iso as following:

# file:grub.cfg
set default=0
set timeout=30
insmod all_video
insmod efi_gop

menuentry "memtest" {
    multiboot /boot/memtest console=ttyS0
	set gfxpayload=auto
    boot
}
mkdir -p iso/boot
cp memtest iso/boot/memtest
cp grub.cfg iso/
grub-mkrescue -o bootable.iso iso/

The grub.cfg file instructs the GRUB2 to load proper modules and then start the multiboot file with given command-line.

I also wanted to test in UEFI conditions from the beginning, so I used the OVMF2 firmware to boot the emulator.

The whole qemu commad line then looked like this:

qemu-system-x86_64 -cpu host --accel kvm -m 1G -bios /usr/share/ovmf/OVMF.fd -cdrom bootable.iso -serial stdio -s -display vnc=:1

Bootstrap

The first issue I encountered was pretty early one. The existing multiboot patches already added the code that stored the pointer to the multiboot info structure as it was used to query the memory layout. This seemed to work, but only by accident. It turns out that the memory layout is on the beginning of the info structure, but then it overlapped the .bss section and rest of the struct was zeroed out as part of the initialization.

To workaround this I just made it to skip the .bss cleanup unconditionally.

/*
 *  Zero BSS
 */
    /* The zeroing overwrites the multiboot structure and also multiboot loader
	should clean the bss section for us */
    jmp zerobss_done
	cmpl	$1, zerobss@GOTOFF(%ebx)
	jnz	zerobss_done
	xorl	%eax, %eax
	leal	_bss@GOTOFF(%ebx), %edi
	leal	_end@GOTOFF(%ebx), %ecx
	subl	%edi, %ecx
1:	movl	%eax, (%edi)
	addl	$4, %edi
	subl	$4, %ecx
	jnz	1b
	movl	$0, zerobss@GOTOFF(%ebx)
zerobss_done:

This is certainly not a robust solution, as the C code expects that the .bss will be cleaned up. It works, but probably also only by accident.

I admit that after I investigated the memtests build system I wasn’t brave enough to dive in it. The linker script is pretty complex and I didn’t feel like I understand the purpose of everything it does and so I would probably break it.

Huge drawback of the build process is the fact that custom linker script is used very early and there is no mention about debug symbols sections in it. All my efforts to produce an intermediate binary that could at least be used with the great addr2line utility were doomed in the early stages. Even If I succeeded, the benefit of this would be very limited, because the memtest is linked as position independent executable and it even relocates itself in the memory. This sadly makes the whole debugging very unfriendly. I used qemu for testing so the best what I thought of was disassembling the memory around the instruction pointer and then look for the instruction sequence in the disassembled executable.

Later in the C code, the multiboot structure is examined for the valid framebuffer info and when found, the graphical output code will be initialized.

Graphical output

Luckily the memtest also provides an option that enables to print output on serial line. This is achieved by passing console=ttyS0,115200. The ttyS0 is identifier of the first UART3 on the machine (at I/O port 0x3f8 and above) and the 115200 specifies the baudrate 4.

The baudrate is in fact not necessary in QEMU, but got used to always specify it because then it saves time when switching to a real hardware.

With the serial output enabled there is at least some reference information on how the output should look at certain moment.

The output itself was done via the cprint(int y, int x, const char *text) function that accepts coordinates and text to be printed. This function primarily prints to the text framebuffer located at 0xB8000 and optionally redirects its output to the serial line if configured. I added another procedure call here that calls to my code and if this is initialized, prints character on the screen.

The font is provided in a file font_8x16.c and dimensions of one glyph are (surprise!) 8x16 pixels. The font is stored as array of bytes, each byte represents single row of pixels, with ones being active pixels an zeroes being inactive pixels. Each 16 consecutive rows form a glyph. Visual representation therefore begins at offset ASCII code of char * 16.

Below is representation of letter P. Can you see it?

    /* P */
    0x00,     /* ........ */
    0x00,     /* ........ */
    0xfc,     /* ******.. */
    0xc6,     /* **...**. */
    0xc6,     /* **...**. */
    0xc6,     /* **...**. */
    0xc6,     /* **...**. */
    0xfc,     /* ******.. */
    0xc0,     /* **...... */
    0xc0,     /* **...... */
    0xc0,     /* **...... */
    0xc0,     /* **...... */
    0x00,     /* ........ */
    0x00,     /* ........ */
    0x00,     /* ........ */
    0x00,     /* ........ */

Drawing a letter with this knowledge is matter of two nested for cycles.

Colours

When I sent the first working version to my colleague who gave me the inspiration for this project, the active pixels were printed white and background was black.

He pointed out that the text mode output always had a blue background. In my opinion black background is fine, but it was great opportunity to dive in the framebuffers colour format.

The framebuffer (or at least according to the multiboot) can have two colour modes. In the first one, there is provided a colour palette and one chooses colour by indexing to it. I don’t know exactly how that works, because the specification does not provide more details and in qemu is reported the second mode. My guess is that this option applies only to some legacy hardware, but I didn’t find more information on this.

The second mode is called direct RGB and it is what one would expect to see. There is a structure describing what bits in the pixel express amount of Red, Green and Blue. Interesting might be that qemu reports bpp 20 (which is 20 bits per pixel), but 20 doesn’t seem very multiple of three. There are either some unused bits, or the colors have different number of bits dedicated.

Results

With my code I was able to mirror most of the console output to the QEMU emulated VGA.

The first image captures the output of GRUB2 bootloader. On the left side is the output printed on serial console (qemu -serial stdio) and the right window is output from the emulated VGA (qemu -display vnc=:1).

GRUB2 Boot screen. Left on serial console, right VGA.

The second image is finally output from the memtest86+ itself. Left side is again serial console output, right window is emulated VGA.

Twice the same output from memtest. Left on serial console, right VGA.

Real hardware

At this point, the patches provides working graphical output in qemu only. Real hardware has its own set of bugs and the debugging is harder because I do not own even the serial adapter. There are certainly ways of going around this, but this remains open for some future post.

Conclusion

In this project I implemented support for graphical framebuffers in the memtest86 binary. The work took me only about two days of work and it was my Christmas Holiday project for year 2021.

Although I had been involved in writing a little operating system from scratch, there was no support for graphical output. I therefore took this as an opportunity to put my hands on the graphical output.

I am also happy that I could experience the font rendering on my own so that now I much more appreciate this omnipresent piece of UI.


  1. https://git.kabele.me/vitkabele/memtest86plus ↩︎

  2. https://github.com/tianocore/tianocore.github.io/wiki/OVMF ↩︎

  3. https://wiki.osdev.org/Serial_Ports ↩︎

  4. https://en.wikipedia.org/wiki/Baud ↩︎