20 August, 2024

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.

  1. Compiling C for a RISC-V target.
  2. 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.