RISC-V Meaty Skeleton with QEMU virt board: Difference between revisions

m
Bot: Replace deprecated source tag with syntaxhighlight
[unchecked revision][unchecked revision]
m (Bot: Replace deprecated source tag with syntaxhighlight)
 
(18 intermediate revisions by 5 users not shown)
Line 1:
{{BeginnersWarning}}
{{Rating|1}}
{{Template:Kernel designs}}
 
This tutorial assumes you have completed [[RISC-V_Bare_Bones|RISC-V Bare Bones]] on the QEMU <code>virt</code> board, or alternatively, [[HiFive-1_Bare_Bones|HiFive-1 Bare Bones]]. If not, you should complete them first for an overview of how to boot your own operating system on RISC-V. This tutorial is deliberately brief on concepts that have already been covered in the bare bones tutorials and their transitive prerequisites.
 
Line 36 ⟶ 40:
Fetch the latest version of GDB through [https://ftp.gnu.org/gnu/gdb/ https://ftp.gnu.org/gnu/gdb/]. The latest version at the time of writing is 12.1:
 
<sourcesyntaxhighlight lang="bash">
export GDB_VERSION="12.1"
wget https://ftp.gnu.org/gnu/gdb/gdb-${GDB_VERSION}.tar.xz
</syntaxhighlight>
</source>
 
Unpack the archive:
 
<sourcesyntaxhighlight lang="bash">
tar xvf gdb-${GDB_VERSION}.tar.xz
</syntaxhighlight>
</source>
 
Now move into the source directory:
 
<sourcesyntaxhighlight lang="bash">
pushd gdb-${GDB_VERSION}/
</syntaxhighlight>
</source>
 
Create a build directory and move into it:
 
<sourcesyntaxhighlight lang="bash">
mkdir build
pushd build/
</syntaxhighlight>
</source>
 
Now export a few variables as with building a cross-GCC or cross-binutils:
 
<sourcesyntaxhighlight lang="bash">
export PREFIX="$HOME/opt/cross"
export TARGET=riscv64-elf
export PATH="$PREFIX/bin:$PATH"
</syntaxhighlight>
</source>
 
Configuration options are mostly the same as with building cross-GCC or cross-binutils. In particular, you may wish to enable the following features:
Line 73 ⟶ 77:
* <code>--with-expat</code>: Build with Expat, a library for XML parsing. Requires development headers for Expat to be installed on the build host
 
<sourcesyntaxhighlight lang="bash">
../configure --target=$TARGET \
--prefix="$PREFIX" \
Line 83 ⟶ 87:
--enable-tui=yes \
--with-expat
</syntaxhighlight>
</source>
 
Now build the source code (this can take a while):
 
<sourcesyntaxhighlight lang="bash">
make -j$(nproc)
</syntaxhighlight>
</source>
 
And install:
 
<sourcesyntaxhighlight lang="bash">
make -C gdb install
</syntaxhighlight>
</source>
 
If you wish to keep the source tree available for conveniently re-building GDB in the future (e.g. with a different set of options), clean up the build files now:
 
<sourcesyntaxhighlight lang="bash">
make clean
</syntaxhighlight>
</source>
 
Move one level up to the project root:
 
<sourcesyntaxhighlight lang="bash">
popd
</syntaxhighlight>
</source>
 
Remove the <code>build/</code> directory we created:
 
<sourcesyntaxhighlight lang="bash">
rm -rf build/
</syntaxhighlight>
</source>
 
Move up one more level:
 
<sourcesyntaxhighlight lang="bash">
popd
</syntaxhighlight>
</source>
 
Double check our cross-debugger is properly installed:
 
<sourcesyntaxhighlight lang="bash">
which -- $TARGET-gdb || echo $TARGET-gdb is not in the PATH
</syntaxhighlight>
</source>
 
Now you may safely remove the source tree and archive, if you wish:
 
<sourcesyntaxhighlight lang="bash">
rm -rf gdb-${GDB_VERSION}/
rm gdb-${GDB_VERSION}.tar.xz
</syntaxhighlight>
</source>
 
If <code>$PREFIX/bin</code> is not in your path already, you may wish to persist it by writing it to your profile:
 
<sourcesyntaxhighlight lang="bash">
echo "export PATH=\"\$HOME/opt/cross/bin:\$PATH\"" >> $HOME/.profile
source $HOME/.profile
</syntaxhighlight>
</source>
 
==Dependencies==
Line 160 ⟶ 164:
Fetch the source code from the <code>v0.0.1</code> release of [https://github.com/DonaldKellett/marvelos https://github.com/DonaldKellett/marvelos] code-named "Meaty Skeleton", using [https://git-scm.com/ Git]:
 
<sourcesyntaxhighlight lang="bash">
git clone --branch v0.0.1 https://github.com/DonaldKellett/marvelos.git
</syntaxhighlight>
</source>
 
Operating systems development is about being an expert. Take the time to read the code carefully through and understand it. Please seek further information and help if you don't understand aspects of it. This code is minimal and almost everything is done deliberately, often to pre-emptively solve future problems.
Line 189 ⟶ 193:
Run <code>make $TARGET</code> with <code>TARGET</code> set to your desired target to execute the specified target. For example, to build the kernel image and run it in QEMU:
 
<sourcesyntaxhighlight lang="bash">
make run
</syntaxhighlight>
</source>
 
Additionally, the default target is <code>all</code> if not specified:
 
<sourcesyntaxhighlight lang="bash">
make
</syntaxhighlight>
</source>
 
<sourcesyntaxhighlight lang="make">
# Build
CC=riscv64-elf-gcc
Line 242 ⟶ 246:
rm -vf *.o
rm -vf $(KERNEL_IMAGE)
</syntaxhighlight>
</source>
 
===Kernel source===
 
====src/common/common.h====
 
<syntaxhighlight lang="c">
#ifndef COMMON_H
#define COMMON_H
 
int toupper(int);
void panic(const char *, ...);
 
#endif
</syntaxhighlight>
 
====src/common/common.c====
 
<syntaxhighlight lang="c">
#include <stdarg.h>
#include "common.h"
#include "../uart/uart.h"
 
int toupper(int c) {
return 'a' <= c && c <= 'z' ? c + 'A' - 'a' : c;
}
 
void panic(const char *format, ...) {
kputs("Kernel panic!");
kputs("Reason:");
va_list arg;
va_start(arg, format);
kvprintf(format, arg);
va_end(arg);
asm volatile ("wfi");
}
</syntaxhighlight>
 
====src/syscon/syscon.h====
 
<syntaxhighlight lang="c">
#ifndef SYSCON_H
#define SYSCON_H
 
// "test" syscon-compatible device is at memory-mapped address 0x100000
// according to our device tree
#define SYSCON_ADDR 0x100000
 
void poweroff(void);
void reboot(void);
 
#endif
</syntaxhighlight>
 
====src/syscon/syscon.c====
 
<syntaxhighlight lang="c">
#include <stdint.h>
#include "syscon.h"
#include "../uart/uart.h"
 
void poweroff(void) {
kputs("Poweroff requested");
*(uint32_t *)SYSCON_ADDR = 0x5555;
}
 
void reboot(void) {
kputs("Reboot requested");
*(uint32_t *)SYSCON_ADDR = 0x7777;
}
</syntaxhighlight>
 
====src/uart/uart.h====
 
<syntaxhighlight lang="c">
#ifndef UART_H
#define UART_H
 
#include <stddef.h>
#include <stdarg.h>
 
// 0x10000000 is memory-mapped address of UART according to device tree
#define UART_ADDR 0x10000000
 
void uart_init(size_t);
int kputchar(int);
int kputs(const char *);
void kvprintf(const char *, va_list);
void kprintf(const char *, ...);
 
#endif
</syntaxhighlight>
 
====src/uart/uart.c====
 
<syntaxhighlight lang="c">
#include <stddef.h>
#include <stdint.h>
#include <stdarg.h>
#include <limits.h>
#include "uart.h"
#include "../common/common.h"
 
#define to_hex_digit(n) ('0' + (n) + ((n) < 10 ? 0 : 'a' - '0' - 10))
 
/*
* Initialize NS16550A UART
*/
void uart_init(size_t base_addr) {
volatile uint8_t *ptr = (uint8_t *)base_addr;
 
// Set word length to 8 (LCR[1:0])
const uint8_t LCR = 0b11;
ptr[3] = LCR;
 
// Enable FIFO (FCR[0])
ptr[2] = 0b1;
 
// Enable receiver buffer interrupts (IER[0])
ptr[1] = 0b1;
 
// For a real UART, we need to compute and set the baud rate
// But since this is an emulated UART, we don't need to do anything
//
// Assuming clock rate of 22.729 MHz, set signaling rate to 2400 baud
// divisor = ceil(CLOCK_HZ / (16 * BAUD_RATE))
// = ceil(22729000 / (16 * 2400))
// = 592
//
// uint16_t divisor = 592;
// uint8_t divisor_least = divisor & 0xFF;
// uint8_t divisor_most = divisor >> 8;
// ptr[3] = LCR | 0x80;
// ptr[0] = divisor_least;
// ptr[1] = divisor_most;
// ptr[3] = LCR;
}
 
static void uart_put(size_t base_addr, uint8_t c) {
*(uint8_t *)base_addr = c;
}
 
int kputchar(int character) {
uart_put(UART_ADDR, (uint8_t)character);
return character;
}
 
static void kprint(const char *str) {
while (*str) {
kputchar((int)*str);
++str;
}
}
 
int kputs(const char *str) {
kprint(str);
kputchar((int)'\n');
return 0;
}
 
// Limited version of vprintf() which only supports the following specifiers:
//
// - d/i: Signed decimal integer
// - u: Unsigned decimal integer
// - o: Unsigned octal
// - x: Unsigned hexadecimal integer
// - X: Unsigned hexadecimal integer (uppercase)
// - c: Character
// - s: String of characters
// - p: Pointer address
// - %: Literal '%'
//
// None of the sub-specifiers are supported for the sake of simplicity.
// The `n` specifier is not supported since that is a major source of
// security vulnerabilities. None of the floating-point specifiers are
// supported since floating point operations don't make sense in kernel
// space
//
// Anyway, this subset should suffice for printf debugging
void kvprintf(const char *format, va_list arg) {
while (*format) {
if (*format == '%') {
++format;
if (!*format)
return;
switch (*format) {
case 'd':
case 'i':
{
int n = va_arg(arg, int);
if (n == INT_MIN) {
kprint("-2147483648");
break;
}
if (n < 0) {
kputchar('-');
n = ~n + 1;
}
char lsh = '0' + n % 10;
n /= 10;
char buf[9];
char *p_buf = buf;
while (n) {
*p_buf++ = '0' + n % 10;
n /= 10;
}
while (p_buf != buf)
kputchar(*--p_buf);
kputchar(lsh);
}
break;
case 'u':
{
unsigned n = va_arg(arg, unsigned);
char lsh = '0' + n % 10;
n /= 10;
char buf[9];
char *p_buf = buf;
while (n) {
*p_buf++ = '0' + n % 10;
n /= 10;
}
while (p_buf != buf)
kputchar(*--p_buf);
kputchar(lsh);
}
break;
case 'o':
{
unsigned n = va_arg(arg, unsigned);
char lsh = '0' + n % 8;
n /= 8;
char buf[10];
char *p_buf = buf;
while (n) {
*p_buf++ = '0' + n % 8;
n /= 8;
}
while (p_buf != buf)
kputchar(*--p_buf);
kputchar(lsh);
}
break;
case 'x':
{
unsigned n = va_arg(arg, unsigned);
char lsh = to_hex_digit(n % 16);
n /= 16;
char buf[7];
char *p_buf = buf;
while (n) {
*p_buf++ = to_hex_digit(n % 16);
n /= 16;
}
while (p_buf != buf)
kputchar(*--p_buf);
kputchar(lsh);
}
break;
case 'X':
{
unsigned n = va_arg(arg, unsigned);
char lsh = to_hex_digit(n % 16);
n /= 16;
char buf[7];
char *p_buf = buf;
while (n) {
*p_buf++ = to_hex_digit(n % 16);
n /= 16;
}
while (p_buf != buf)
kputchar(toupper(*--p_buf));
kputchar(toupper(lsh));
}
break;
case 'c':
kputchar(va_arg(arg, int));
break;
case 's':
kprint(va_arg(arg, char *));
break;
case 'p':
{
kprint("0x");
size_t ptr = va_arg(arg, size_t);
char lsh = to_hex_digit(ptr % 16);
ptr /= 16;
char buf[15];
char *p_buf = buf;
while (ptr) {
*p_buf++ = to_hex_digit(ptr % 16);
ptr /= 16;
}
while (p_buf != buf)
kputchar(*--p_buf);
kputchar(lsh);
}
break;
case '%':
kputchar('%');
break;
default:
kputchar('%');
kputchar(*format);
}
} else
kputchar(*format);
++format;
}
}
 
void kprintf(const char *format, ...) {
va_list arg;
va_start(arg, format);
kvprintf(format, arg);
va_end(arg);
}
</syntaxhighlight>
 
====src/kmain.c====
 
<syntaxhighlight lang="c">
#include "uart/uart.h"
#include "syscon/syscon.h"
#include "common/common.h"
 
#define ARCH "RISC-V"
#define MODE 'M'
 
void kmain(void) {
uart_init(UART_ADDR);
 
kprintf("Hello %s World!\n", ARCH);
kprintf("We are in %c-mode!\n", MODE);
 
poweroff();
}
</syntaxhighlight>
 
==Running the project==
 
Invoke <code>make run</code> in the project root. You should see the following output:
 
<pre>
Hello RISC-V World!
We are in M-mode!
Poweroff requested
</pre>
 
==Final remarks and going further==
 
This is by no means an end to your OSDev adventures on RISC-V. Make the OS kernel your own! Add support for memory management, interrupt handling, porting newlib ... you name it. Give your own RISC-V OS a creative name! "maRVelOS" / <code>marvelos</code> is already taken by [[User:Donaldsebleung]] though ;-)
 
Also note that this example RISC-V OS runs in M-mode usually reserved for firmware, rather than the S-mode recommended for RISC-V supervisors (OSes). If you wish to follow the RISC-V conventions closely, you may want to look into RISC-V privilege modes and [https://github.com/riscv-software-src/opensbi OpenSBI] early on and port your OS kernel accordingly. More information about RISC-V privilege modes available on [[RISC-V#Privileges|our wiki]].
 
==See also==
 
* [[RISC-V|RISC-V]]
* [[RISC-V_Bare_Bones|RISC-V Bare Bones]]
* [[HiFive-1_Bare_Bones|HiFive-1 Bare Bones]]
* [[Meaty_Skeleton|Meaty Skeleton]]
 
[[Category:RISC-V]]
[[Category:Bare_bones_tutorials]]
[[Category:C]]
[[Category:QEMU]]
[[Category:Level_1_Tutorials]]
 
===External links===
 
* [https://github.com/twilco/riscv-from-scratch RISC-V from scratch]
* [https://github.com/sgmarz/osblog The Adventures of OS]
* [https://linuxfromscratch.org/ Linux From Scratch]
* [https://github.com/riscv-software-src/opensbi OpenSBI]