Chapter 6: Debug Output - COMPLETE
This commit is contained in:
parent
9e2483d78e
commit
d0b946f327
288
doc/6_Debug_Output.md
Normal file
288
doc/6_Debug_Output.md
Normal file
@ -0,0 +1,288 @@
|
|||||||
|
# Debug Output
|
||||||
|
|
||||||
|
In this chapter we'll be looking at some ways of getting status information out
|
||||||
|
of the kernel.
|
||||||
|
|
||||||
|
## The printf function and TDD
|
||||||
|
|
||||||
|
I really like having access to a printf-like function when developing. It just
|
||||||
|
makes debugging so much easier.
|
||||||
|
|
||||||
|
For further convenience, I want the debug printer to print both to screen and
|
||||||
|
to a serial port.
|
||||||
|
|
||||||
|
Now, I'm doing this as a learning experience, and one thing I've been meaning
|
||||||
|
to practice is Test-Driven Development. So let's write the debug printing that
|
||||||
|
way.
|
||||||
|
|
||||||
|
Let's start very simple. A function that outputs a character to VGA and to
|
||||||
|
serial. Assuming (slightly prematurely) that we'll have two functions `void
|
||||||
|
vga_write(char c)` and `void serial_write(uint16_t port, uint8_t c)` The test
|
||||||
|
could look like this:
|
||||||
|
|
||||||
|
`src/kernel/boot/debug.tt`
|
||||||
|
```c
|
||||||
|
char vga_recv;
|
||||||
|
char serial_recv;
|
||||||
|
|
||||||
|
void vga_write(char c)
|
||||||
|
{
|
||||||
|
vga_recv = c;
|
||||||
|
}
|
||||||
|
void serial_write(uint16_t port, uint8_t c)
|
||||||
|
{
|
||||||
|
serial_recv = (char)c;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(putch_sends_character_to_vga)
|
||||||
|
{
|
||||||
|
debug_putch('a');
|
||||||
|
ASSERT_EQ_CHR(vga_recv, 'a');
|
||||||
|
}
|
||||||
|
TEST(putch_sends_character_to_serial)
|
||||||
|
{
|
||||||
|
debug_putch('a');
|
||||||
|
ASSERT_EQ_CHR(serial_recv, 'a');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
... which can be made to pass with a simple function like:
|
||||||
|
|
||||||
|
`src/kernel/boot/debug.c`
|
||||||
|
```c
|
||||||
|
void debug_putch(char c)
|
||||||
|
{
|
||||||
|
vga_write(c);
|
||||||
|
serial_write(PORT_COM1, c);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
... with some `#define`s and `#include`s and such, of course.
|
||||||
|
|
||||||
|
As I understand it, in efficient TDD, you should write one test at a time, and
|
||||||
|
then make that pass - so that's what I tried to do, but I'm presenting more
|
||||||
|
than one test at a time for brevity.
|
||||||
|
|
||||||
|
Next is writing strings, which requires an update to the mock functions:
|
||||||
|
|
||||||
|
`src/kernel/boot/debug.tt`
|
||||||
|
```c
|
||||||
|
char vga_recv[BUFFER_SIZE];
|
||||||
|
char serial_recv[BUFFER_SIZE];
|
||||||
|
|
||||||
|
BEFORE()
|
||||||
|
{
|
||||||
|
for(int i= 0; i < BUFFER_SIZE; i++)
|
||||||
|
{
|
||||||
|
vga_recv[i] = '\0';
|
||||||
|
serial_recv[i] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void vga_write(char c)
|
||||||
|
{
|
||||||
|
static int i = 0;
|
||||||
|
vga_recv[i++] = c;
|
||||||
|
}
|
||||||
|
void serial_write(uint16_t port, uint8_t c)
|
||||||
|
{
|
||||||
|
static int i = 0;
|
||||||
|
serial_recv[i++] = (char)c;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(putsn_writes_string)
|
||||||
|
{
|
||||||
|
char *str = "hello";
|
||||||
|
debug_putsn(str, 5);
|
||||||
|
ASSERT_EQ_STR(vga_recv, str, BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
TEST(putsn_writes_correct_number_of_characters)
|
||||||
|
{
|
||||||
|
char *str = "1234567890";
|
||||||
|
debug_putsn(str, 5);
|
||||||
|
ASSERT_EQ_STR(vga_recv, "12345", BUFFER_SIZE);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
`src/kernel/boot/debug.c`
|
||||||
|
```c
|
||||||
|
debug_putsn(char *s, size_t n)
|
||||||
|
{
|
||||||
|
while(n--)
|
||||||
|
debug_putch(*s++);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then there's `debug_printf`, which is left as an exercise for the reader - or
|
||||||
|
you could just look through my code.
|
||||||
|
|
||||||
|
> You'll probably find that my implementation, among many other things, is
|
||||||
|
> influenced by [musl libc](https://www.musl-libc.org/) which is a very clean
|
||||||
|
> standard library implementation. We'll take a much closer look at musl later...
|
||||||
|
|
||||||
|
The names of the tests are:
|
||||||
|
- `printf_prints_string`
|
||||||
|
- `printf_does_not_print_percent`
|
||||||
|
- `printf_prints_binary_number`
|
||||||
|
- `printf_prints_octal_number`
|
||||||
|
- `printf_prints_decimal_number`
|
||||||
|
- `printf_prints_hexadecimal_number`
|
||||||
|
- `printf_prints_char`
|
||||||
|
- `printf_prints_passed_string`
|
||||||
|
- `printf_keeps_printing_after_number`
|
||||||
|
- `printf_prints_text_around_number`
|
||||||
|
- `printf_keeps_going_for_unknown_format_specifier`
|
||||||
|
- `printf_pads_value`
|
||||||
|
- `printf_pads_more_than_9`
|
||||||
|
- `printf_zero_pads`
|
||||||
|
|
||||||
|
## Printing to screen
|
||||||
|
|
||||||
|
Next, let's look at `vga_write()`.
|
||||||
|
|
||||||
|
I tried to use TDD for this as well, and that had an unexpected outcome.
|
||||||
|
|
||||||
|
In order to test VGA printing, I had to mock up VGA hardware somehow - or at
|
||||||
|
least the VGA memory mapping.
|
||||||
|
|
||||||
|
This brought me two realizations.
|
||||||
|
|
||||||
|
First of all, each character on screen is described by a 16 bit value - one
|
||||||
|
byte for the character and one for the color attribute.
|
||||||
|
|
||||||
|
This can be implemented as a structure:
|
||||||
|
|
||||||
|
`src/kernel/drivers/vga.c`
|
||||||
|
```c
|
||||||
|
struct vga_cell {
|
||||||
|
uint8_t c;
|
||||||
|
uint8_t f;
|
||||||
|
}__attribute__((packed));
|
||||||
|
|
||||||
|
struct vga_cell buffer[24*80] = (void *)0xB8000;
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
The second realization was that each screen line comes right after the previous
|
||||||
|
one in memory.
|
||||||
|
|
||||||
|
This seems obvious, of course - but stay with me.
|
||||||
|
|
||||||
|
You've probably gone through some kernel development tutorial at
|
||||||
|
some point (I'd assume [Brandon Friesen's](http://www.osdever.net/bkerndev/Docs/title.htm),
|
||||||
|
[James Molloy's](http://www.jamesmolloy.co.uk/tutorial_html/) or
|
||||||
|
[The osdev.org Bare Bones ](http://wiki.osdev.org/Bare_Bones)).
|
||||||
|
If so, you may have a `putch()`, `monitor_put()` or
|
||||||
|
`terminal_putchar()` function which writes a character to screen,
|
||||||
|
increases a counter, and if it has reached the end of a line, goes
|
||||||
|
to the next.
|
||||||
|
|
||||||
|
See where I'm going with this? There's no point in keeping track on the line
|
||||||
|
and column separately. All you need is a single counter.
|
||||||
|
|
||||||
|
That makes my corresponding function very short and simple:
|
||||||
|
|
||||||
|
`src/kernel/drivers/vga.c`
|
||||||
|
```c
|
||||||
|
...
|
||||||
|
uint64_t cursor = 0;
|
||||||
|
|
||||||
|
void vga_write(char c)
|
||||||
|
{
|
||||||
|
switch(c)
|
||||||
|
{
|
||||||
|
case '\n':
|
||||||
|
cursor += 80 - (cursor%80);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
buffer[cursor++] = (struct vga_cell){.c = c, .f=7};
|
||||||
|
}
|
||||||
|
scroll();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`scroll()` scrolls the screen if we pass beyond the last line, and is also very simple:
|
||||||
|
|
||||||
|
```c
|
||||||
|
...
|
||||||
|
void scroll()
|
||||||
|
{
|
||||||
|
memmove(buffer, &buffer[80], 80*(24-1)*sizeof(struct vga_cell));
|
||||||
|
memset(&buffer[80*(24-1)], 0, 80*sizeof(struct vga_cell));
|
||||||
|
cursor -= 80;
|
||||||
|
}
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
As a final note, the line `struct vga_cell buffer[24*80] = (void *)0xB8000;` is
|
||||||
|
a really bad one. You should make sure to use double buffering when dealing
|
||||||
|
with VGA memory. The reason for this is that *reading* from VGA memory (such as
|
||||||
|
when scrolling the screen) is really really slow.
|
||||||
|
|
||||||
|
## Bonus: PANIC
|
||||||
|
|
||||||
|
Sometimes, things go wrong.
|
||||||
|
|
||||||
|
When they do, you probably want to know, and perhaps get a chance to find out
|
||||||
|
why, or even to put things right.
|
||||||
|
|
||||||
|
For this reason, I made a `PANIC()` macro. The definition looks like this:
|
||||||
|
|
||||||
|
`src/kernel/include/debug.h`
|
||||||
|
```c
|
||||||
|
...
|
||||||
|
#define S(x) #x
|
||||||
|
#define S_(x) S(x)
|
||||||
|
#define S__LINE__ S_(__LINE__)
|
||||||
|
|
||||||
|
#define PANIC(...) \
|
||||||
|
do{ \
|
||||||
|
debug_printf("\n\nKernel panic!\n%s:%d\n", __FILE__, __LINE__); \
|
||||||
|
debug_printf(__VA_ARGS__); \
|
||||||
|
volatile int _override = 0; \
|
||||||
|
while(1){ \
|
||||||
|
asm("panic_breakpoint_" S__LINE__ ":"); \
|
||||||
|
if(_override) break; \
|
||||||
|
} \
|
||||||
|
}while(0)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
It's pretty simple actually. It just prints an error message, and then loops
|
||||||
|
infinitely.
|
||||||
|
|
||||||
|
However, the loop can be broken out of by changing the value of `_override` by
|
||||||
|
e.g. breaking in gdb and running
|
||||||
|
|
||||||
|
```gdb
|
||||||
|
(gdb) set _override=1
|
||||||
|
```
|
||||||
|
|
||||||
|
The `asm` line creates a label in the code, which you can add as a breakpoint
|
||||||
|
in gdb. The `S__LINE__` stuff and related macros is so that the labels will be
|
||||||
|
unique if you call `PANIC()` twice in the same source file (gdb will happily
|
||||||
|
break at any of several labels with the same name, but gcc doesn't like two
|
||||||
|
equal labels in the same file).
|
||||||
|
|
||||||
|
Gdb has a neat command called `rbreak` which sets breakpoints based on a
|
||||||
|
regular expression. Unfortunately that only works for functions - not labels.
|
||||||
|
Therefore, I put the following function in gdbinit, to automatically find all
|
||||||
|
`panic_breakpoint`s and add them to gdb.
|
||||||
|
|
||||||
|
`toolchain/gdbinit`
|
||||||
|
```gdb
|
||||||
|
...
|
||||||
|
python
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
dump = subprocess.Popen(("objdump", "-t", os.environ['BUILDROOT'] + "sysroot/kernel"), stdout=subprocess.PIPE)
|
||||||
|
lines = subprocess.check_output(('grep', 'panic_breakpoint'), stdin=dump.stdout)
|
||||||
|
dump.wait()
|
||||||
|
for line in lines.split('\n'):
|
||||||
|
name = line.split(' ')[-1]
|
||||||
|
if name:
|
||||||
|
gdb.execute('b ' + name, to_string=True)
|
||||||
|
end
|
||||||
|
...
|
||||||
|
```
|
@ -8,4 +8,5 @@
|
|||||||
[Chapter 3: Activate Long Mode](3_Activate_Long_Mode.md)<br>
|
[Chapter 3: Activate Long Mode](3_Activate_Long_Mode.md)<br>
|
||||||
[Chapter 4: "Higher Half" Kernel](4_Higher_Half_Kernel.md)<br>
|
[Chapter 4: "Higher Half" Kernel](4_Higher_Half_Kernel.md)<br>
|
||||||
[Chapter 5: Unit Testing Framework](5_Unit_Testing.md)<br>
|
[Chapter 5: Unit Testing Framework](5_Unit_Testing.md)<br>
|
||||||
|
[Chapter 6: Debug output](6_Debug_Output.md)<br>
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user