Using a C compiler to create programs for an Owl-2820 VM.
Part 8 Using a C compiler to create Owl-2820 programs
In the previous post, we completed the implementation of the Owl-2820 instruction set.
I finished the post with a suggestion that we could write programs for our Owl-2820 VM in a language such as C, C++, Rust or Swift, and that we could do this by first compiling them for a RISC-V RV32I target, then somehow running the result on our Owl VM.
From our point of view, it would be ideal if a compiler such as clang
or gcc
could build for an Owl-2820 target. But, given that the Owl-2820 is a virtual CPU that exists for the purpose of this series of blog posts, it would be somewhat presumptuous to ask compiler authors to add an Owl-2820 back end to their compilers.
How, then, are we going to compile C programs for the Owl-2820 CPU? The answer is that we aren’t - at least not directly. Instead, we’re going to do this in a two stage process.
- Compiling C for a RISC-V target.
- Translating the result for the Owl-280 CPU.
Compiling C for a RISC-V target
Both clang
and gcc
support compiling C for a RISC-V target. In this post we’re going to use clang
, simply because it supports cross-compilation out of the box. In other words, once we’ve installed clang
and some of the tools that go with it, then we’ll be ready to go.
For a simple project like this it is very easy to use clang
as a cross-compiler. At the time of writing I’ve successfully compiled C programs for a RISC-V target using clang
on Windows, on Linux, on a Raspberry Pi, and on termux
on an Android tablet. The command line was almost identical in each case.
But where do we start? What C program can we compile that will run on our Owl-2820 VM?
Fibonacci in C
It will probably be of no surprise to you that we’re going to compile our original Fibonacci implementation from part 0. However, as we don’t yet have an implementation for printf()
, we will lean on the PrintFib
system call from part 6 to help us out, and call it from a print_fib()
function implemented in C. For the time being we’ll skip over its implementation, but we will return to it later in this post.
Here’s the code.
#include <stdint.h>
// We don't have an implementation of print_fib() yet. We'll come back to this.
void print_fib(uint32_t i, uint32_t result);
static uint32_t fib(uint32_t n)
{
if (n < 2)
{
return n;
}
uint32_t previous = 0;
uint32_t current = 1;
for (; n > 1; --n)
{
uint32_t next = current + previous;
previous = current;
current = next;
}
return current;
}
int main(void)
{
for (uint32_t i = 0; i < 48; i++)
{
uint32_t result = fib(i);
print_fib(i, result);
}
return 0;
}
Compiling to RISC-V assembly language
Command line
Let’s compile that to RISC-V assembly language using clang
.
clang -S -Os --target=riscv32 -march=rv32i -mabi=ilp32 -ffreestanding -ffunction-sections \
-fdata-sections ./source/fib.c
Compiler options
Here’s a breakdown of the command line options, with links to their full descriptions if you want more detail.
Option | Description |
---|---|
-S |
only run preprocess and compile steps (i.e., don’t assemble, don’t link). More… |
-Os |
enables -O2 level optimization except those optimizations that could increase code size. More… |
--target=riscv32 |
generate code for a 32-bit RISC-V target. |
-march=rv32i |
specifically target the rv32i architecture without any extensions. More… |
-mabi=ilp32 |
specifies that integer types (int, long, pointer) are all 32-bit and that there is no floating point. More… |
-ffreestanding |
assert that the compilation takes place in a freestanding environment. More… |
-ffunction-sections |
place each function in its own section. More… |
-fdata-sections |
place each data in its own section. More… |
Assembly language output
That gives us some assembly language output in a file fib.s
. I’ve shaved off a few lines from the top and bottom, but you can see that it’s very similar to versions of fib
that we’ve seen previously.
main: # @main
# %bb.0:
addi sp, sp, -32
sw ra, 28(sp) # 4-byte Folded Spill
sw s0, 24(sp) # 4-byte Folded Spill
sw s1, 20(sp) # 4-byte Folded Spill
sw s2, 16(sp) # 4-byte Folded Spill
sw s3, 12(sp) # 4-byte Folded Spill
li s0, 0
li s1, 2
li s2, 48
li s3, 1
.LBB0_1: # =>This Loop Header: Depth=1
# Child Loop BB0_3 Depth 2
mv a1, s0
bltu s0, s1, .LBB0_4
# %bb.2: # in Loop: Header=BB0_1 Depth=1
li a0, 0
li a1, 1
mv a2, s0
.LBB0_3: # Parent Loop BB0_1 Depth=1
# => This Inner Loop Header: Depth=2
mv a3, a1
addi a2, a2, -1
add a1, a0, a1
mv a0, a3
bltu s3, a2, .LBB0_3
.LBB0_4: # in Loop: Header=BB0_1 Depth=1
mv a0, s0
call print_fib
addi s0, s0, 1
bne s0, s2, .LBB0_1
# %bb.5:
li a0, 0
lw ra, 28(sp) # 4-byte Folded Reload
lw s0, 24(sp) # 4-byte Folded Reload
lw s1, 20(sp) # 4-byte Folded Reload
lw s2, 16(sp) # 4-byte Folded Reload
lw s3, 12(sp) # 4-byte Folded Reload
addi sp, sp, 32
ret
Compiling to an object file
Command line
If we replace the -S
flag with -c
then the compiler will output an object file with a .o
extension.
clang -c -Os --target=riscv32 -march=rv32i -mabi=ilp32 -ffreestanding -ffunction-sections \
-fdata-sections ./source/fib.c
Compiler options
Here’s a description of the -c
command line option.
Option | Description |
---|---|
-c |
only run the preprocess, compile and assemble steps (i.e., don’t link). More… |
Disassembling
We can disassemble the resulting object file, fib.o
, with llvm-objdump
.
llvm-objdump -d fib.o
The resulting disassembly also shows us the offset of each instruction, and how it has been encoded.
For example, the 8th instruction in the output, “li s1, 0x2
” is at offset 0x1c
and is written into memory as 93 04 20 00
which is the little-endian representation of 0x00200493
.
fib.o: file format elf32-littleriscv
Disassembly of section .text.main:
00000000 <main>:
0: 13 01 01 fe addi sp, sp, -0x20
4: 23 2e 11 00 sw ra, 0x1c(sp)
8: 23 2c 81 00 sw s0, 0x18(sp)
c: 23 2a 91 00 sw s1, 0x14(sp)
10: 23 28 21 01 sw s2, 0x10(sp)
14: 23 26 31 01 sw s3, 0xc(sp)
18: 13 04 00 00 li s0, 0x0
1c: 93 04 20 00 li s1, 0x2
20: 13 09 00 03 li s2, 0x30
24: 93 09 10 00 li s3, 0x1
00000028 <.LBB0_1>:
28: 93 05 04 00 mv a1, s0
2c: 63 60 94 00 bltu s0, s1, 0x2c <.LBB0_1+0x4>
30: 13 05 00 00 li a0, 0x0
34: 93 05 10 00 li a1, 0x1
38: 13 06 04 00 mv a2, s0
0000003c <.LBB0_3>:
3c: 93 86 05 00 mv a3, a1
40: 13 06 f6 ff addi a2, a2, -0x1
44: b3 05 b5 00 add a1, a0, a1
48: 13 85 06 00 mv a0, a3
4c: 63 e0 c9 00 bltu s3, a2, 0x4c <.LBB0_3+0x10>
00000050 <.LBB0_4>:
50: 13 05 04 00 mv a0, s0
54: 97 00 00 00 auipc ra, 0x0
58: e7 80 00 00 jalr ra <.LBB0_4+0x4>
5c: 13 04 14 00 addi s0, s0, 0x1
60: 63 10 24 01 bne s0, s2, 0x60 <.LBB0_4+0x10>
64: 13 05 00 00 li a0, 0x0
68: 83 20 c1 01 lw ra, 0x1c(sp)
6c: 03 24 81 01 lw s0, 0x18(sp)
70: 83 24 41 01 lw s1, 0x14(sp)
74: 03 29 01 01 lw s2, 0x10(sp)
78: 83 29 c1 00 lw s3, 0xc(sp)
7c: 13 01 01 02 addi sp, sp, 0x20
80: 67 80 00 00 ret
Linking
Command line
All we have so far is an object file, not an executable. Let’s take it further by removing the -c
flag so that the compiler will also run the link step to generate a binary executable.
clang -Os --target=riscv32 -march=rv32i -mabi=ilp32 -ffreestanding -ffunction-sections \
-fdata-sections ./source/fib.c
Output
The linker complains about not being able to find certain libraries.
ld.lld: error: unable to find library -lc
ld.lld: error: unable to find library -lm
ld.lld: error: unable to find library -lclang_rt.builtins-riscv32
clang: error: ld.lld command failed with exit code 1 (use -v to see invocation)
Fixing the build
We’re not using any of these libraries, so we can inform it of that with more command line options.
clang -Os --target=riscv32 -march=rv32i -mabi=ilp32 -ffreestanding -ffunction-sections \
-fdata-sections ./source/fib.c -nostdlib -nodefaultlibs
Command line options
Here’s what those options mean.
Option | Description |
---|---|
-nostdlib |
do not use the standard system library startup files or libraries when linking. More… |
-nodefaultlibs |
do not use the standard system libraries when linking. More… |
Output
However, the linker still complains because we don’t have an implementation of print_fib()
.
ld.lld: error: undefined symbol: print_fib
>>> referenced by fib.c
>>> C:\Users\myname\AppData\Local\Temp\fib-751f97.o:(main)
clang: error: ld.lld command failed with exit code 1 (use -v to see invocation)
Implementing print_fib()
We need an implementation of print_fib()
. In part 6, we added a PrintFib
system call which we called from a print_fib
subroutine.
System calls are implemented in the Owl-2820 VM by putting the syscall number in the a7 register, placing up to seven arguments in the a0 to a6 registers, then invoking the ecall
instruction.
For PrintFib
, we have two arguments, passed in the a0 and a1 registers. Here’s a reminder of its implementation.
while (!done)
{
// ...
switch (opcode)
{
case Opcode::Ecall: {
const auto syscall = Syscall(x[a7]);
switch (syscall)
{
// ...
case Syscall::PrintFib:
std::cout << std::format("fib({}) = {}\n", x[a0], x[a1]);
break;
}
break;
}
And here’s how we implemented the print_fib
subroutine in Owl-2820 assembly language. The calling convention from part 4 means that we can pass the arguments to print_fib
in registers a0 and a1 and it doesn’t have to do anything more beyond putting the syscall number into register a7.
print_fib:
li a7, 1 # set a7 to 1, i.e., PrintFib
ecall # invoke the syscall
ret # return from print_fib
How can we do the same from C?
Inline assembly language
We could write print_fib
in assembly language, for example, by putting the above code into a file print_fib.s
and compiling it alongside our C program. That would probably work, some of the time… maybe?
In reality, we should be very reluctant to do that because the C compiler would have no way of knowing which registers were required by our assembly language print_fib
implementation, nor would it know which values will be overwritten.
However, if we write print_fib()
in terms of inline assembly language in C, then we can provide that information to the compiler.
Let’s do that now, and add the following implementation of print_fib()
to the top of our C program.
#include <stdint.h>
static inline void print_fib(uint32_t i, uint32_t result)
{
// Define local register variables and associate them with specific RISC-V registers.
// See: https://gcc.gnu.org/onlinedocs/gcc/Local-Register-Variables.html
//
register uint32_t a7 __asm__("a7") = 1; // a7 = syscall number
register uint32_t a0 __asm__("a0") = i; // a0 = first argument to syscall
register uint32_t a1 __asm__("a1") = result; // a1 = second argument to syscall
// Invoke the `PrintFib` syscall.
//
// See: https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
// https://gcc.gnu.org/onlinedocs/gcc/Simple-Constraints.html
// https://gcc.gnu.org/onlinedocs/gcc/Modifiers.html
// and: https://www.felixcloutier.com/documents/gcc-asm.html
//
__asm__ __volatile__("ecall /* Do a syscall */" // assembler template: ecall (with a comment)
: "+r"(a0) // output operands: reads and writes a0
: "r"(a7), "r"(a0), "r"(a1) // input operands: reads a7, a0, a1
: "memory"); // clobbers: this instruction may modify memory
}
That seems quite verbose for a couple of lines of assembly language, but it is ultimately necessary to tell the C compiler exactly how we’re going to use the registers. Rather than explaining this in extreme detail, I’ve put some explanatory links in the code.
Compiling to assembly language
Let’s compile the program with the -S
flag so that we can see the resulting assembly language and prove to ourselves that print_fib()
is implemented as a syscall.
clang -S -Os --target=riscv32 -march=rv32i -mabi=ilp32 -ffreestanding -ffunction-sections \
-fdata-sections ./source/fib.c
Output
Here’s the assembly language output containing our print_fib
implementation.
main: # @main
# %bb.0:
li a2, 0
li a3, 2
li a4, 1
li a5, 48
.LBB0_1: # =>This Loop Header: Depth=1
# Child Loop BB0_3 Depth 2
mv a1, a2
bltu a2, a3, .LBB0_4
# %bb.2: # in Loop: Header=BB0_1 Depth=1
li a0, 0
li a1, 1
mv a6, a2
.LBB0_3: # Parent Loop BB0_1 Depth=1
# => This Inner Loop Header: Depth=2
mv a7, a1
addi a6, a6, -1
add a1, a0, a1
mv a0, a7
bltu a4, a6, .LBB0_3
.LBB0_4: # in Loop: Header=BB0_1 Depth=1
li a7, 1
mv a0, a2
#APP
ecall # Do a syscall
#NO_APP
addi a2, a2, 1
bne a2, a5, .LBB0_1
# %bb.5:
li a0, 0
ret
It hasn’t changed much from the assembly language output that we saw earlier, except that call print_fib
has been replaced with an inlined version of our print_fib()
function, i.e., this piece of code.
li a7, 1 # set a7 to 1, i.e., PrintFib
mv a0, a2 # copy `i` into `a0`
ecall # invoke the syscall
Those of you with a keen eye may have spotted that it doesn’t actually set register a1 to anything, and that’s because a1 already contains the correct value. Optimizing compilers are clever.
The _start entry point
Command line
So, having added print_fib()
, let’s try linking again.
clang -Os --target=riscv32 -march=rv32i -mabi=ilp32 -ffreestanding -ffunction-sections \
-fdata-sections ./source/fib.c -nostdlib -nodefaultlibs
Output
This time we get a warning, but nothing more.
ld.lld: warning: cannot find entry symbol _start; not setting start address
A simple runtime
At this point, some of you are nodding your heads wisely, and others are asking, “What? I thought C programs start at main()
”.
Well, yes, main()
may well be the entry point as far as your C program is concerned, but that’s not the entry point for an ELF binary, which is what we have here.
By default the entry point for an ELF binary is named _start
. It is responsible, among other things, for calling main()
and providing it with argc
and argv
, and (assuming that there is one) returning control back to the operating system when done. When we added the --nostdlib
option to the command line then we effectively told the compiler that we’ll be doing that ourselves.
We’ll create our own _start
in assembly language and put it in a file named crt0.s
. Our implementation of _start
is very cut down; it calls main()
then it exits via the Exit
syscall. And because we’re calling into a C program, we can control exactly which registers are used, so there’s no need to embed this in C.
.section .init
.global _start
.type _start, @function
_start:
.cfi_startproc
.cfi_undefined ra
# Call main().
li a0,0 # a0 = argc = 0
li a1,0 # a1 = argv = NULL
li a2,0 # a2 = envp = NULL
call main
# Exit.
li a0,0 # a0 = exit code
li a7,0 # a7 = syscall number (0 is exit)
ecall # do a syscall. There's no coming back from this one.
.cfi_endproc
.size _start, .-_start
.end
Command line
Let’s add crt0.s
to the command line and try again.
clang -Os --target=riscv32 -march=rv32i -mabi=ilp32 -ffreestanding -ffunction-sections \
-fdata-sections ./source/crt0.s ./source/fib.c -nostdlib -nodefaultlibs
Disassembling
The good news is that it compiles without warning. Let’s disassemble it with llvm-objdump
, using the -f
flag to display the ELF file headers so that we can see what the program’s start address is.
llvm-objump -f -d a.out
The program’s start address, 0x00011154
, is the same as that of _start
.
a.out: file format elf32-littleriscv
architecture: riscv32
start address: 0x00011154
Disassembly of section .text:
00011100 <main>:
11100: 13 06 00 00 li a2, 0x0
11104: 93 06 20 00 li a3, 0x2
11108: 13 07 10 00 li a4, 0x1
1110c: 93 07 00 03 li a5, 0x30
11110: 93 05 06 00 mv a1, a2
11114: 63 62 d6 02 bltu a2, a3, 0x11138 <main+0x38>
11118: 13 05 00 00 li a0, 0x0
1111c: 93 05 10 00 li a1, 0x1
11120: 13 08 06 00 mv a6, a2
11124: 93 88 05 00 mv a7, a1
11128: 13 08 f8 ff addi a6, a6, -0x1
1112c: b3 05 b5 00 add a1, a0, a1
11130: 13 85 08 00 mv a0, a7
11134: e3 68 07 ff bltu a4, a6, 0x11124 <main+0x24>
11138: 93 08 10 00 li a7, 0x1
1113c: 13 05 06 00 mv a0, a2
11140: 73 00 00 00 ecall
11144: 13 06 16 00 addi a2, a2, 0x1
11148: e3 14 f6 fc bne a2, a5, 0x11110 <main+0x10>
1114c: 13 05 00 00 li a0, 0x0
11150: 67 80 00 00 ret
Disassembly of section .init:
00011154 <_start>:
11154: 13 05 00 00 li a0, 0x0
11158: 93 05 00 00 li a1, 0x0
1115c: 13 06 00 00 li a2, 0x0
11160: ef f0 1f fa jal 0x11100 <main>
11164: 13 05 00 00 li a0, 0x0
11168: 93 08 00 00 li a7, 0x0
1116c: 73 00 00 00 ecall
This is a problem, because our Owl-2820 VM assumes that all programs start at address 0x00000000
. We need to find a way to move _start
to that address, and to place main
somewhere following it.
Specifying section addresses
If you look at the disassembly above, you’ll see that _start
is in a section named .init
, and main
is in a section named .text
. So we need the .init
section which contains _start
to start at address 0x00000000
, and the .text
section which contains main
to start a bit later in memory, e.g., at address 0x00000100
.
Command line
We can use the the linker option --section-start
to specify the starting addresses of the .init
and .text
sections.
clang -Os --target=riscv32 -march=rv32i -mabi=ilp32 -ffreestanding -ffunction-sections \
-fdata-sections ./source/crt0.s ./source/fib.c -nostdlib -nodefaultlibs \
"-Wl,--section-start=.init=0" "-Wl,--section-start=.text=100"
Compiler options
The -Wl
option is used to pass options to the linker.
Option | Description |
---|---|
-Wl, |
pass a comma separated list of options to the linker. More… |
Linker options
The --section-start
linker option specifies the start address, in hex, of a named section.
Linker Option | Description |
---|---|
--section-start=<name>=<addr> |
set the start address of section <name> to hexadecimal address <addr> . More… |
Disassembling
Let’s disassemble the resulting ELF binary.
llvm-objump -f -d a.out
This time we have the result that we want. The program’s start address is now 0x00000000
. The .init
section which contains _start
is also at address 0x00000000
. Similarly, the .text
section which contains main
is at address 0x00000100
.
a.out: file format elf32-littleriscv
architecture: riscv32
start address: 0x00000000
Disassembly of section .init:
00000000 <_start>:
0: 13 05 00 00 li a0, 0x0
4: 93 05 00 00 li a1, 0x0
8: 13 06 00 00 li a2, 0x0
c: ef 00 40 0f jal 0x100 <main>
10: 13 05 00 00 li a0, 0x0
14: 93 08 00 00 li a7, 0x0
18: 73 00 00 00 ecall
Disassembly of section .text:
00000100 <main>:
100: 13 06 00 00 li a2, 0x0
104: 93 06 20 00 li a3, 0x2
108: 13 07 10 00 li a4, 0x1
10c: 93 07 00 03 li a5, 0x30
110: 93 05 06 00 mv a1, a2
114: 63 62 d6 02 bltu a2, a3, 0x138 <main+0x38>
118: 13 05 00 00 li a0, 0x0
11c: 93 05 10 00 li a1, 0x1
120: 13 08 06 00 mv a6, a2
124: 93 88 05 00 mv a7, a1
128: 13 08 f8 ff addi a6, a6, -0x1
12c: b3 05 b5 00 add a1, a0, a1
130: 13 85 08 00 mv a0, a7
134: e3 68 07 ff bltu a4, a6, 0x124 <main+0x24>
138: 93 08 10 00 li a7, 0x1
13c: 13 05 06 00 mv a0, a2
140: 73 00 00 00 ecall
144: 13 06 16 00 addi a2, a2, 0x1
148: e3 14 f6 fc bne a2, a5, 0x110 <main+0x10>
14c: 13 05 00 00 li a0, 0x0
150: 67 80 00 00 ret
Extracting an image
Finally, we have an ELF format executable with our code at the addresses that we want it to be. At this point, we could write an ELF loader that will parse the ELF file and load the relevant parts of the program into our VM’s memory.
It is reasonably straightforward to write a rudimentary ELF loader, but it takes a lot more effort to write a secure one, as you have to be very careful with the values that you accept from the file. If you’re not careful, then your ELF loader could end up reading a maliciously crafted ELF file that subverts it in some way. For example, one of the fields might contain an offset that’s beyond the length of the image. If your ELF loader accepts that field without checking it, or adds it to an integer which then overflows, then you’re on a slippery slope to memory corruption.
Fortunately, our use case is very simple, so we don’t need to write an ELF loader. Instead, we can use llvm-objcopy
to extract a binary image from the ELF file. We can then load that binary image straight into our VM’s memory.
Command line
Here we have to tell llvm-objcopy
which sections to extract, otherwise it will include several sections that are of no interest to us.
llvm-objcopy a.out -O binary a.bin -j .init -j .text
The -j
flag tells llvm-objcopy
which sections we want to include in the image.
At this point we just want the .init
section and the .text
section. When we develop larger programs then we’ll also want to add other sections, such as .rodata
for read-only data, but we have no need of them at this point.
Viewing the image
Running llvm-objcopy
created an image, a.bin
. If we dump that in hex then we can see that the image contains the hex values from the .init
section starting at offset 00000000
, followed by the hex values from the .text
section starting at offset 00000100
.
Here’s some Powershell to dump the image. If you’re on Linux then hd
will do the trick.
format-hex a.bin
Output
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000000 13 05 00 00 93 05 00 00 13 06 00 00 EF 00 40 0F ...........ï.@.
00000010 13 05 00 00 93 08 00 00 73 00 00 00 00 00 00 00 .......s.......
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000B0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000D0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000100 13 06 00 00 93 06 20 00 13 07 10 00 93 07 00 03 ..... ........
00000110 93 05 06 00 63 62 D6 02 13 05 00 00 93 05 10 00 ...cbÖ........
00000120 13 08 06 00 93 88 05 00 13 08 F8 FF B3 05 B5 00 ........ø.³.µ.
00000130 13 85 08 00 E3 68 07 FF 93 08 10 00 13 05 06 00 ...ãh.........
00000140 73 00 00 00 13 06 16 00 E3 14 F6 FC 13 05 00 00 s.......ã.öü....
00000150 67 80 00 00 g..
And there we have it. One binary image, ready to load into our Owl-2820 VM’s memory. Or is it?
Translating the result for the Owl-2820 CPU
Well, there is the small matter of instruction encoding.
Owl-2820 shares the same instruction set as RV32I, but it encodes the instructions differently so that they are easy to decode and dispatch in software.
We either need to re-encode the instructions from RISC-V instruction encoding into Owl-2820 instruction encoding, or we need to modify our Owl-2820 VM to understand the RISC-V instruction encoding.
That will be the topic for the next post.