29 November, 2024

How is data represented in an ELF file? And what is this cryptically named bss section?

Part 12 Dealing with data

In the last couple of posts we looked at running RISC-V instructions directly and then we improved the instruction handler by using C++ concepts.

Until now, the only C programs that we’ve compiled for the Owl-2820 VM have been various incarnations of fib.c that generated the first few results in the Fibonacci sequence. It is time to move on from Fibonacci and compile and run other programs, but to do so, we need to find a way to deal with data.

It may not be immediately obvious, but fib.c doesn’t use data so we’ve been able to get away with creating an image from its text sections only. In other words, we’ve been loading and running an image that contains only code, not data.

In the next few posts we’re going to look at how we can load and run programs that contain both code and data. In this post, we’re going to look at different types of data, then focus on read-only data.

Let’s start this journey with a reminder of how we build and run programs.

Recap - building and running fib.c

To recap, we’ve built and run programs as follows:

  • use clang to compile and link the code into an RV32I ELF binary
  • extract the .init and .text sections into a binary image using llvm-objcopy
  • create a hex dump of the binary image and embed it in the source code of the VM so that it can be run without having to load anything
  • transcode the image from RISC-V encoded instructions to Owl-2820 encoded instructions

Let’s go through each stage and take a closer look at what is going on.

Compiling with clang

This command line tells clang to compile the startup code in crt0.S and the main program in fib.c, and link them into an 32-bit RISC-V ELF binary named a.out.

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"

It uses the --section-start linker option to place the .init section, which contains the instructions for the startup code, at address 0x00000000, and the .text section, which contains the instructions for the main program, at address 0x00000100.

The .init and .text sections

Here we use llvm-objdump with the -h flag to view the section headers in the resulting ELF binary, a.out.

llvm-objdump -h a.out

In the output below, the .init and .text sections are both of type TEXT, meaning that they contain executable instructions.

The .eh_frame section is of type DATA, meaning that it contains data. However, as .eh_frame describes call frames to be unwound during C++ exception handling, the data that it contains is not very interesting to us at this point.

a.out:  file format elf32-littleriscv

Sections:
Idx Name              Size     VMA      Type
  0                   00000000 00000000
  1 .init             0000001c 00000000 TEXT
  2 .text             00000054 00000100 TEXT
  3 .eh_frame         0000002c 00001154 DATA
  4 .riscv.attributes 0000001c 00000000
  5 .comment          00000029 00000000
  6 .symtab           000000c0 00000000
  7 .shstrtab         0000004c 00000000
  8 .strtab           00000031 00000000

The .init section is shockingly small as it contains just 28 bytes (0000001c in the Size column), and the .text section isn’t much bigger, at 84 bytes (00000054). The total size of the combined executable sections is only 112 bytes. Given the size of most programs these days, you’d be forgiven for thinking that 112 bytes could be a typo or a miscalculation, but it is correct as fib.c contains no data, uses no libraries, and relies on syscalls to do the heavy lifting.

The sections in the file are described in this table.

Section Name Description
.init the executable instructions for the startup code
.text the executable instructions for the program
.eh_frame call frames to be unwound in C++ exception processing
.riscv.attributes RISC-V specific attributes
.comment version control information
.symtab a symbol table
.shstrtab a blob of zero-terminated section names, indexed by the section header
.strtab strings, usually associated with symbol table entries

An ELF file can have many different types of section. For a more comprehensive list take a look at the Linux Standard Base Core Specification.

Disassembling the ELF binary

The next command line uses llvm-objdump with the -f flag to display the file header, and the -d flag to disassemble the instructions in the ELF binary, a.out.

llvm-objump -f -d a.out

By default llvm-objdump will only disassemble sections that contain executable instructions, i.e., those whose type is TEXT. As the only TEXT sections in a.out are .init and .text, these are the only sections that are disassembled.

The disassembly output confirms that the startup code in the .init section is at address 0x00000000 and that main, in the .text section, 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 the code with llvm-objcopy

The llvm-objcopy is used to create a binary image named a.bin by extracting the .init section and .text section from the ELF binary, a.out. As fib.c doesn’t contain any data, there is no need to extract any other sections.

llvm-objcopy a.out -O binary a.bin -j .init -j .text

When llvm-objcopy extracts the image, it fills gaps between sections with zeroes. There’s a gap between the end of the 28 byte .init section and the start of the 84 byte .text section, so the size of the extracted file, a.bin, is 340 bytes rather than 112 bytes.

Loading the binary image

We’ve been loading the image by taking a hex dump of a.bin then pasting the output into the VM’s source code where it can be treated as array of bytes.

std::span<uint32_t> LoadRv32iImage()
{
    // The RISC-V binary image from: https://badlydrawnrod.github.io/posts/2024/08/20/lbavm-008/
    alignas(alignof(uint32_t)) static uint8_t image[] = {
        // Address 0x0000 (from the .init section)
        0x13, 0x05, 0x00, 0x00, 0x93, 0x05, 0x00, 0x00, 0x13, 0x06, 0x00, 0x00, 0xEF, 0x00, 0x40, 0x0F,
        0x13, 0x05, 0x00, 0x00, 0x93, 0x08, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

        // --- snip - lots of zeroes inserted by `llvm-obcopy` ---
        
        // Address 0x0100 (from the .text section)
        0x13, 0x06, 0x00, 0x00, 0x93, 0x06, 0x20, 0x00, 0x13, 0x07, 0x10, 0x00, 0x93, 0x07, 0x00, 0x03,
        0x93, 0x05, 0x06, 0x00, 0x63, 0x62, 0xD6, 0x02, 0x13, 0x05, 0x00, 0x00, 0x93, 0x05, 0x10, 0x00,
        0x13, 0x08, 0x06, 0x00, 0x93, 0x88, 0x05, 0x00, 0x13, 0x08, 0xF8, 0xFF, 0xB3, 0x05, 0xB5, 0x00,
        0x13, 0x85, 0x08, 0x00, 0xE3, 0x68, 0x07, 0xFF, 0x93, 0x08, 0x10, 0x00, 0x13, 0x05, 0x06, 0x00,
        0x73, 0x00, 0x00, 0x00, 0x13, 0x06, 0x16, 0x00, 0xE3, 0x14, 0xF6, 0xFC, 0x13, 0x05, 0x00, 0x00,
        0x67, 0x80, 0x00, 0x00
    };

    uint32_t* imageBegin = reinterpret_cast<uint32_t*>(image);
    uint32_t* imageEnd = imageBegin + sizeof(image) / sizeof(uint32_t);

    return std::span<uint32_t>(imageBegin, imageEnd);
}

This approach was just about workable for compiling and running a program as small as fib.c, but having to recompile the VM to accommodate different programs is not a long term solution.

Running the binary image

To run the program in the binary image as RISC-V encoded instructions, the program copies the loaded image into VM memory then runs it using RunRv32i().

        // Create a 4K memory image.
        constexpr size_t memorySize = 4096;
        std::vector<uint32_t> image(memorySize / sizeof(uint32_t));

        auto rv32iImage = LoadRv32iImage();
        std::ranges::copy(rv32iImage, image.begin());

        std::cout << "Running RISC-V encoded instructions...\n";
        RunRv32i(image);

To run the program in the binary image as Owl-2820 encoded instructions, the program transcodes the entire image from RISC-V encoding to Owl-2820 encoding, then copies the result into VM memory before running it with Run().

This is possible only because the image consists of instructions only. If the image also contained data then we’d need a way to tell the VM which parts of the image are code and which are data.

        // Transcode it to Owl-2820.
        auto owlImage = Rv32iToOwl(rv32iImage);
        DisassembleOwl(owlImage);
        std::ranges::copy(owlImage, image.begin());

        std::cout << "\nRunning Owl-2820 encoded instructions...\n";
        Run(image);

Summary

That was an overview of what it takes to be able to compile and load an image so that it can be run on the Owl VM. There are several shortcomings to this approach.

In particular:

  • It doesn’t handle data.
  • Section addresses have to be specified manually during compilation.
  • The image has to be hard-coded into the VM in order to run it.
  • Transcoding only works because it can assume that the entire image is code.

Programs with data

For the Owl-2820 VM to be of any use, it needs to be able to run programs that contain data.

To explore this further, we’ll move away from fib.c to some simple programs that contain different types of data. We’ll also add more syscalls so that the Owl-2820 VM can do more than just printing Fibonacci numbers and exiting.

Before going into details, let’s have a brief introduction to the different types of data.

Types of data

There are essentially 3 types of data can appear in a RISC-V ELF binary that has been compiled from C source code.

  1. read-only data such as string literals
  2. initialized data, i.e., variables, both global and static, that have a value when the program starts
  3. uninitialized data, uninitialized variables, typically zeroed when the program starts

Data of each type is placed in its own section in the ELF binary.

Section Name Description
.rodata read-only data, such as string literals
.data and .sdata initialized data
.bss and .sbss uninitialized data

What is bss?

The names .rodata and .data are self-explanatory, but what is .bss? How does that mean uninitialized data?

For the uninitiated, bss stands for “Block Started by Symbol”. That’s almost as cryptic as bss, so the reality is that it is nothing more than a historical convention that means uninitialized data.

A historical note on bss

While researching for this post, I found my old copy of The Magic Garden Explained, by Berny Goodheart and James Cox. This book, first published in 1994, explains the internal workings of UNIX System V Release 4. The book says that “bss came from the IBM 7090 assembly language and stood for ‘block started by symbol’”.

This clue led to a manual for the Fortran Assembly Program for the IBM 709/7090, first published in 1960, which describes the “storage-allocating pseudo-operation” BSS as a way “to reserve blocks of memory for data storage or working space”.

In other words, the original BSS was just an assembler directive that reserved a block of space. It even had a counterpart, BES, which stood for “Block Ended by Symbol”.

If you’re curious to learn more then follow the link to the manual and look at the pages numbered 29-30. The entire manual is fascinating. If you thought that computers have always worked with bytes, or that ASCII was ubiquitous in the days before Unicode, then think again. You might even change your mind about the meaning of “operating system”.

Adding syscalls

To make it easier to demonstrate the different types of data, we’re going to add 3 new syscalls: randomize, random and puts. Here’s how they’re implemented in the Owl-2820 VM.

random

The randomize syscall sets the seed of the random number generator.

        case Syscall::randomize:
            std::srand(time(nullptr));
            break;

random

The random syscall returns a random number between 0 and the value passed to it in register a0.

        case Syscall::random:
            if (x[a0] != 0)
            {
                x[a0] = std::rand() % x[a0];
            }
            break;

puts

The puts syscall prints a zero-terminated string, whose address is passed in register a0, to the host’s stdout.

        case Syscall::puts:
            // We really shouldn't do this without at least a bounds check. Who knows what horrors
            // the caller is giving us.
            std::puts(reinterpret_cast<const char*>(memory.data()) + x[a0]);
            break;

Read-only data

A fortune program

We’ll demonstrate the use of read-only data by looking at a simple fortune program that displays a random aphorism when it runs. To do this, it uses the newly added randomize, random and puts syscalls.

In fib.c, the syscall interfaces were part of fib.c itself, but for fortune.c I’ve moved them into a new header, syscalls.h.

#include "syscalls.h"

#include <stddef.h>
#include <stdint.h>

static const char* aphorisms[] = {
        "Absence makes the heart grow fonder.",
        "A chain is only as strong as its weakest link.",
        "Actions speak louder than words.",
        "Better late than never.",
        "Experience is the name everyone gives to their mistakes.",
        "If it ain't broke, don't fix it.",
        "If you lie down with dogs, you get up with fleas.",
        "Ignorance is bliss.",
        "Measure twice. Cut once.",
        "The road to hell is paved with good intentions.",
};

int main()
{
    randomize();

    const size_t numAphorisms = sizeof(aphorisms) / sizeof(char*);
    puts(aphorisms[random(numAphorisms)]);

    return 0;
}

The fortune.c program contains read-only data, in the form of the aphorisms array and the strings that it points to. As we saw earlier, read-only data is placed in the .rodata section of the ELF binary.

Section Name Description
.rodata read-only data

Compiling fortune

Let’s compile the program, then examine the resulting ELF file.

clang -Os --target=riscv32 -march=rv32i -mabi=ilp32 -ffreestanding -ffunction-sections \
      -fdata-sections ./source/crt0.S ./source/fortune.c -nostdlib -nodefaultlibs \
      "-Wl,--section-start=.init=0" \
      "-Wl,--section-start=.text=100" \
      "-Wl,--section-start=.rodata=800" \
      "-g"

As before, the --section-start linker option is used to place the .init section at address 0x0000, and the .text section at address 0x0100.

There’s an additional section, .rodata, which I’ve placed at address 0x0800 to leave a gap between the end of the .text section and the start of .rodata so that we can add more code later without having to change the command line.

The -g flag generates debug information in the ELF file. This will be useful when disassembling.

Viewing fortune’s section headers

We can run llvm-objdump -h to view the section headers.

llvm-objdump -h a.out

The section headers show that the read-only data has indeed gone into the .rodata section, and that its type is DATA.

a.out:  file format elf32-littleriscv

Sections:
Idx Name              Size     VMA      Type
  0                   00000000 00000000
  1 .init             0000001c 00000000 TEXT
  2 .text             00000038 00000100 TEXT
  3 .rodata           0000019e 00000800 DATA
  4 .eh_frame         0000002c 000009a0 DATA
  5 .riscv.attributes 0000001c 00000000
  6 .comment          00000029 00000000
  7 .symtab           00000100 00000000
  8 .shstrtab         00000054 00000000
  9 .strtab           00000052 00000000

Finding the data

We can run llvm-objcopy to extract the sections of interest into a single binary image, a.bin, this time including the .rodata section.

llvm-objcopy a.out -O binary a.bin -j .init -j .text -j .rodata

Then we can use format-hex or hd to view the output as a hex dump.

The image

The startup code in the .init section has been written to the image at offset 0x0000.

00000000   37 81 00 00 13 05 00 00 93 05 00 00 13 06 00 00  7.............
00000010   EF 00 00 0F 93 08 00 00 73 00 00 00 00 00 00 00  ï......s.......

The program in the .text section has been written to the image at offset 0x0100.

00000100   93 08 30 00 73 00 00 00 93 08 40 00 13 05 A0 00  .0.s....@... .
00000110   73 00 00 00 13 15 25 00 B7 15 00 00 93 85 05 80  s.....%.·....
00000120   33 85 A5 00 03 25 05 00 93 08 20 00 73 00 00 00  3¥..%... .s...
00000130   13 05 00 00 67 80 00 00 00 00 00 00 00 00 00 00  ....g..........

The read-only data in the .rodata section has been written to the image at offset 0x0800.

00000800   D5 08 00 00 28 08 00 00 33 09 00 00 86 09 00 00  Õ...(...3......
00000810   FA 08 00 00 B4 08 00 00 54 09 00 00 57 08 00 00  ú...´...T...W...
00000820   6B 08 00 00 84 08 00 00 41 20 63 68 61 69 6E 20  k......A chain
00000830   69 73 20 6F 6E 6C 79 20 61 73 20 73 74 72 6F 6E  is only as stron
00000840   67 20 61 73 20 69 74 73 20 77 65 61 6B 65 73 74  g as its weakest
00000850   20 6C 69 6E 6B 2E 00 49 67 6E 6F 72 61 6E 63 65   link..Ignorance
00000860   20 69 73 20 62 6C 69 73 73 2E 00 4D 65 61 73 75   is bliss..Measu
00000870   72 65 20 74 77 69 63 65 2E 20 43 75 74 20 6F 6E  re twice. Cut on
00000880   63 65 2E 00 54 68 65 20 72 6F 61 64 20 74 6F 20  ce..The road to
00000890   68 65 6C 6C 20 69 73 20 70 61 76 65 64 20 77 69  hell is paved wi
000008A0   74 68 20 67 6F 6F 64 20 69 6E 74 65 6E 74 69 6F  th good intentio
000008B0   6E 73 2E 00 49 66 20 69 74 20 61 69 6E 27 74 20  ns..If it ain't
000008C0   62 72 6F 6B 65 2C 20 64 6F 6E 27 74 20 66 69 78  broke, don't fix
000008D0   20 69 74 2E 00 41 62 73 65 6E 63 65 20 6D 61 6B   it..Absence mak
000008E0   65 73 20 74 68 65 20 68 65 61 72 74 20 67 72 6F  es the heart gro
000008F0   77 20 66 6F 6E 64 65 72 2E 00 45 78 70 65 72 69  w fonder..Experi
00000900   65 6E 63 65 20 69 73 20 74 68 65 20 6E 61 6D 65  ence is the name
00000910   20 65 76 65 72 79 6F 6E 65 20 67 69 76 65 73 20   everyone gives
00000920   74 6F 20 74 68 65 69 72 20 6D 69 73 74 61 6B 65  to their mistake
00000930   73 2E 00 41 63 74 69 6F 6E 73 20 73 70 65 61 6B  s..Actions speak
00000940   20 6C 6F 75 64 65 72 20 74 68 61 6E 20 77 6F 72   louder than wor
00000950   64 73 2E 00 49 66 20 79 6F 75 20 6C 69 65 20 64  ds..If you lie d
00000960   6F 77 6E 20 77 69 74 68 20 64 6F 67 73 2C 20 79  own with dogs, y
00000970   6F 75 20 67 65 74 20 75 70 20 77 69 74 68 20 66  ou get up with f
00000980   6C 65 61 73 2E 00 42 65 74 74 65 72 20 6C 61 74  leas..Better lat
00000990   65 20 74 68 61 6E 20 6E 65 76 65 72 2E 00        e than never..

String literals

The strings start at offset 0x0828 and continue to the end of the output.

00000820                           41 20 63 68 61 69 6E 20          A chain
00000830   69 73 20 6F 6E 6C 79 20 61 73 20 73 74 72 6F 6E  is only as stron
00000840   67 20 61 73 20 69 74 73 20 77 65 61 6B 65 73 74  g as its weakest
00000850   20 6C 69 6E 6B 2E 00 49 67 6E 6F 72 61 6E 63 65   link..Ignorance

The aphorisms array

The 40 bytes that precede the first string are the contents of the aphorisms array, which is an array of pointers to strings.

static const char* aphorisms[] = {
        "Absence makes the heart grow fonder.",
        // ...

There are 10 pointers in the aphorisms array, each of which is 4 bytes in size as we’re compiling for a 32-bit target. Here’s how it looks in the image.

00000800   D5 08 00 00 28 08 00 00 33 09 00 00 86 09 00 00  Õ...(...3......
00000810   FA 08 00 00 B4 08 00 00 54 09 00 00 57 08 00 00  ú...´...T...W...
00000820   6B 08 00 00 84 08 00 00                          k.......

If you squint closely at this, then you’ll see a bunch of little-endian addresses. For example, the first entry, at offset 0x800 contains D5 08 00 00. When this is interpreted as a 32-bit little-endian number, it is 000008d5, which is the offset in the image, a.bin, of the string “Absence makes the heart grow fonder.

Another use for .rodata

There’s another use for .rodata that may not be immediately apparent. Let’s change main() to look like the following code, then recompile it.

int main()
{
    randomize();
    const size_t numAphorisms = sizeof(aphorisms) / sizeof(char*);
    puts(aphorisms[random(numAphorisms)]);

    switch (random(100))
    {
    case 0:
        puts(aphorisms[0]);
        break;
    case 1:
        puts(aphorisms[1]);
        break;
    case 2:
        puts(aphorisms[2]);
        break;
    case 3:
        puts(aphorisms[3]);
        break;
    case 4:
        puts(aphorisms[4]);
        break;
    case 5:
        puts(aphorisms[5]);
        break;
    case 6:
        puts(aphorisms[6]);
        break;
    case 7:
        puts(aphorisms[7]);
        break;
    case 8:
        puts(aphorisms[8]);
        break;
    case 9:
        puts(aphorisms[9]);
        break;
    default:
        puts("Never trust your inputs.");
        break;
    }

    return 0;
}

Viewing the section headers

This time, when we view the section headers with llvm-objdump, we can see that the .rodata section has grown by 65 bytes, to 0x1df bytes.

Sections:
Idx Name              Size     VMA      Type
  0                   00000000 00000000
  1 .init             0000001c 00000000 TEXT
  2 .text             0000013c 00000100 TEXT
  3 .rodata           000001df 00000800 DATA
  4 .eh_frame         0000002c 000009e0 DATA
  5 .riscv.attributes 0000001c 00000000
  6 .comment          00000029 00000000
  7 .symtab           00000110 00000000
  8 .shstrtab         00000054 00000000
  9 .strtab           00000057 00000000

Some of that growth can be accounted for by the string literal used by the switch statement’s default case, “Never trust your inputs.”, which occupies 25 bytes including its terminator.

But what are the remaining 40 bytes? And where do they come from?

Why did .rodata grow?

We can begin to find the answer by extracting a binary image with llvm-objcopy, then looking at its hex dump starting from the contents of the .rodata section at offset 0x0800.

00000800   5C 01 00 00 D4 01 00 00 98 01 00 00 AC 01 00 00  \...Ô......¬...
00000810   70 01 00 00 E8 01 00 00 FC 01 00 00 C0 01 00 00  p...è...ü...À...
00000820   24 02 00 00 84 01 00 00 16 09 00 00 50 08 00 00  $..........P...
00000830   74 09 00 00 C7 09 00 00 3B 09 00 00 F5 08 00 00  t...Ç...;...õ...
00000840   95 09 00 00 7F 08 00 00 93 08 00 00 AC 08 00 00  .........¬...
00000850   41 20 63 68 61 69 6E 20 69 73 20 6F 6E 6C 79 20  A chain is only
00000860   61 73 20 73 74 72 6F 6E 67 20 61 73 20 69 74 73  as strong as its
00000870   20 77 65 61 6B 65 73 74 20 6C 69 6E 6B 2E 00 49   weakest link..I
00000880   67 6E 6F 72 61 6E 63 65 20 69 73 20 62 6C 69 73  gnorance is blis
00000890   73 2E 00 4D 65 61 73 75 72 65 20 74 77 69 63 65  s..Measure twice
000008A0   2E 20 43 75 74 20 6F 6E 63 65 2E 00 54 68 65 20  . Cut once..The
000008B0   72 6F 61 64 20 74 6F 20 68 65 6C 6C 20 69 73 20  road to hell is
000008C0   70 61 76 65 64 20 77 69 74 68 20 67 6F 6F 64 20  paved with good
000008D0   69 6E 74 65 6E 74 69 6F 6E 73 2E 00 4E 65 76 65  intentions..Neve
000008E0   72 20 74 72 75 73 74 20 79 6F 75 72 20 69 6E 70  r trust your inp
000008F0   75 74 73 2E 00 49 66 20 69 74 20 61 69 6E 27 74  uts..If it ain't
00000900   20 62 72 6F 6B 65 2C 20 64 6F 6E 27 74 20 66 69   broke, don't fi
00000910   78 20 69 74 2E 00 41 62 73 65 6E 63 65 20 6D 61  x it..Absence ma
00000920   6B 65 73 20 74 68 65 20 68 65 61 72 74 20 67 72  kes the heart gr
00000930   6F 77 20 66 6F 6E 64 65 72 2E 00 45 78 70 65 72  ow fonder..Exper
00000940   69 65 6E 63 65 20 69 73 20 74 68 65 20 6E 61 6D  ience is the nam
00000950   65 20 65 76 65 72 79 6F 6E 65 20 67 69 76 65 73  e everyone gives
00000960   20 74 6F 20 74 68 65 69 72 20 6D 69 73 74 61 6B   to their mistak
00000970   65 73 2E 00 41 63 74 69 6F 6E 73 20 73 70 65 61  es..Actions spea
00000980   6B 20 6C 6F 75 64 65 72 20 74 68 61 6E 20 77 6F  k louder than wo
00000990   72 64 73 2E 00 49 66 20 79 6F 75 20 6C 69 65 20  rds..If you lie
000009A0   64 6F 77 6E 20 77 69 74 68 20 64 6F 67 73 2C 20  down with dogs,
000009B0   79 6F 75 20 67 65 74 20 75 70 20 77 69 74 68 20  you get up with
000009C0   66 6C 65 61 73 2E 00 42 65 74 74 65 72 20 6C 61  fleas..Better la
000009D0   74 65 20 74 68 61 6E 20 6E 65 76 65 72 2E 00     te than never..

The strings

The strings now start at offset 0x0850.

00000850   41 20 63 68 61 69 6E 20 69 73 20 6F 6E 6C 79 20  A chain is only
00000860   61 73 20 73 74 72 6F 6E 67 20 61 73 20 69 74 73  as strong as its
00000870   20 77 65 61 6B 65 73 74 20 6C 69 6E 6B 2E 00 49   weakest link..I

The aphorisms array

The string are still preceded by contents of the aphorisms array which is now at offset 0x0828.

00000820                           16 09 00 00 50 08 00 00          ...P...
00000830   74 09 00 00 C7 09 00 00 3B 09 00 00 F5 08 00 00  t...Ç...;...õ...
00000840   95 09 00 00 7F 08 00 00 93 08 00 00 AC 08 00 00  .........¬...

Some mystery data

We’ve accounted for the strings, and the aphorisms array, but we still don’t know what the remaining read-only data is, namely, the 40 bytes which start at offset 0x0800.

00000800   5C 01 00 00 D4 01 00 00 98 01 00 00 AC 01 00 00  \...Ô......¬...
00000810   70 01 00 00 E8 01 00 00 FC 01 00 00 C0 01 00 00  p...è...ü...À...
00000820   24 02 00 00 84 01 00 00                          $.......

Decoding the mystery data

If we interpret the mystery data as 32-bit little-endian values, then we get 10 values, starting with 0x015c, 0x01d4, 0x0198, and so on. These are clearly not values in the .rodata section as that starts at 0x0800.

However, when we compiled the program, we told the linker to put the .text section at address 0x0100. We just saw from the section headers that the .text section’s size is 0x013c bytes, so that means that the .text section extends from 0x0100 up to 0x023c.

The values in the mystery data are all in that range. Could they correspond to addresses in the .text section? If so, code will we find at those addresses?

Could it be code?

Let’s disassemble the .text section with llvm-objdump using the -S flag to show the source line, and the -l flag to show the line numbers.

llvm-objdump -Sl --section=.text a.out

I’ve removed some of the output, as there was a lot of it. But here’s enough to get a flavour. You can see how the compiler has inlined the syscalls, and the comments helpfully tell us which syscalls are being invoked.

00000100 <main>:
; main():
; C:\projects\c++\owl-cpu\target\./source\syscalls.h:50
;     __asm__ __volatile__(
     100: 93 08 30 00   li      a7, 0x3
     104: 73 00 00 00   ecall
; C:\projects\c++\owl-cpu\target\./source\syscalls.h:64
;     __asm__ __volatile__("ecall /* syscall(random) */" // assembler template: ecall (with a comment)
     108: 93 08 40 00   li      a7, 0x4
     10c: 13 05 a0 00   li      a0, 0xa
     110: 73 00 00 00   ecall
; C:\projects\c++\owl-cpu\target\./source\fortune.c:23
;     puts(aphorisms[random(numAphorisms)]);
     114: 13 15 25 00   slli    a0, a0, 0x2
     118: b7 15 00 00   lui     a1, 0x1
     11c: 93 85 85 82   addi    a1, a1, -0x7d8
     120: 33 85 a5 00   add     a0, a1, a0
     124: 03 25 05 00   lw      a0, 0x0(a0)
     128: 93 05 a0 00   li      a1, 0xa
; C:\projects\c++\owl-cpu\target\./source\syscalls.h:38
;     __asm__ __volatile__("ecall /* syscall(puts) */" // assembler template: ecall (with a comment)
     12c: 93 08 20 00   li      a7, 0x2
     130: 73 00 00 00   ecall
; C:\projects\c++\owl-cpu\target\./source\syscalls.h:64
;     __asm__ __volatile__("ecall /* syscall(random) */" // assembler template: ecall (with a comment)
     134: 93 08 40 00   li      a7, 0x4
     138: 13 05 40 06   li      a0, 0x64
     13c: 73 00 00 00   ecall
; C:\projects\c++\owl-cpu\target\./source\fortune.c:25
;     switch (random(100))
     140: 63 78 b5 0c   bgeu    a0, a1, 0x210 <main+0x110>
     144: 13 15 25 00   slli    a0, a0, 0x2
     148: b7 15 00 00   lui     a1, 0x1
     14c: 93 85 05 80   addi    a1, a1, -0x800
     150: 33 05 b5 00   add     a0, a0, a1
     154: 03 25 05 00   lw      a0, 0x0(a0)
     158: 67 00 05 00   jr      a0
; C:\projects\c++\owl-cpu\target\./source\syscalls.h:38
;     __asm__ __volatile__("ecall /* syscall(puts) */" // assembler template: ecall (with a comment)
     15c: 37 15 00 00   lui     a0, 0x1
     160: 13 05 65 91   addi    a0, a0, -0x6ea
     164: 93 08 20 00   li      a7, 0x2
     168: 73 00 00 00   ecall
     16c: 6f 00 80 0c   j       0x234 <main+0x134>
; C:\projects\c++\owl-cpu\target\./source\syscalls.h:38
;     __asm__ __volatile__("ecall /* syscall(puts) */" // assembler template: ecall (with a comment)
     170: 37 15 00 00   lui     a0, 0x1
     174: 13 05 b5 93   addi    a0, a0, -0x6c5
     178: 93 08 20 00   li      a7, 0x2
     17c: 73 00 00 00   ecall
     180: 6f 00 40 0b   j       0x234 <main+0x134>
     ;
     ; --- snip ---
     ;
; C:\projects\c++\owl-cpu\target\./source\syscalls.h:38
;     __asm__ __volatile__("ecall /* syscall(puts) */" // assembler template: ecall (with a comment)
     224: 37 15 00 00   lui     a0, 0x1
     228: 13 05 35 89   addi    a0, a0, -0x76d
     22c: 93 08 20 00   li      a7, 0x2
     230: 73 00 00 00   ecall
; C:\projects\c++\owl-cpu\target\./source\fortune.c:62
;     return 0;
     234: 13 05 00 00   li      a0, 0x0
     238: 67 80 00 00   ret

If you look carefully then you’ll see some of the assembly language is preceded by comments that correspond with lines of source code, such as…

;     puts(aphorisms[random(numAphorisms)]);
     114: 13 15 25 00   slli    a0, a0, 0x2
     118: b7 15 00 00   lui     a1, 0x1
     ; --- snip ---

And…

;     switch (random(100))
     140: 63 78 b5 0c   bgeu    a0, a1, 0x210 <main+0x110>
     144: 13 15 25 00   slli    a0, a0, 0x2
     ; --- snip ---

You might find yourself wondering what happened to the case statements, as there’s no obvious evidence of them in the output. However, they’re definitely present. Cast your mind back to those offsets that we found in the mystery data; 0x015c, 0x01d4, 0x0198 and so on.

Here’s the code corresponding to offset 0x015c. It’s an invocation of the puts syscall, using a combination of lui and addi to load the value 0x0916 into register a0.

; C:\projects\c++\owl-cpu\target\./source\syscalls.h:38
;     __asm__ __volatile__("ecall /* syscall(puts) */" // assembler template: ecall (with a comment)
     15c: 37 15 00 00   lui     a0, 0x1
     160: 13 05 65 91   addi    a0, a0, -0x6ea
     164: 93 08 20 00   li      a7, 0x2
     168: 73 00 00 00   ecall
     16c: 6f 00 80 0c   j       0x234 <main+0x134>

Offset 0x0916 in a.bin is a value in the .rodata section. What do we find there? The string, “Absence makes the heart grow fonder.

00000910   78 20 69 74 2E 00 41 62 73 65 6E 63 65 20 6D 61  x it..Absence ma
00000920   6B 65 73 20 74 68 65 20 68 65 61 72 74 20 67 72  kes the heart gr
00000930   6F 77 20 66 6F 6E 64 65 72 2E 00 45 78 70 65 72  ow fonder..Exper

The instructions at offset 0x015c correspond to the first case statement.

    case 0:
        puts(aphorisms[0]);
        break;

Similarly, here’s the code corresponding to offset 0x1d4. It’s another puts, this time loading the value 0x850 into register a0.

; C:\projects\c++\owl-cpu\target\./source\syscalls.h:38
;     __asm__ __volatile__("ecall /* syscall(puts) */" // assembler template: ecall (with a comment)
     1d4: 37 15 00 00   lui     a0, 0x1
     1d8: 13 05 05 85   addi    a0, a0, -0x7b0
     1dc: 93 08 20 00   li      a7, 0x2
     1e0: 73 00 00 00   ecall
     1e4: 6f 00 00 05   j       0x234 <main+0x134>

The value 0x0850 is an offset in the image to the string, “A chain is only as strong as its weakest link.

00000850   41 20 63 68 61 69 6E 20 69 73 20 6F 6E 6C 79 20  A chain is only
00000860   61 73 20 73 74 72 6F 6E 67 20 61 73 20 69 74 73  as strong as its
00000870   20 77 65 61 6B 65 73 74 20 6C 69 6E 6B 2E 00 49   weakest link..I

The instructions at offset 0x1d4 correspond to the second case statement.

    case 1:
        puts(aphorisms[1]);
        break;

If we were to disassemble the image at each of the offsets in the mystery data, then we’d find that each one corresponds to a different case in the switch statement. The mystery data is nothing more than a jump table for the switch statement.

How the switch statement selects each case

The code for the switch statement itself starts at 0x134. At the start of this block, register a1 has already been set to the number 10, which is the number of cases in the switch statement.

The code starting at address 0x134 calls the random(100) from switch (random(100)).

; C:\projects\c++\owl-cpu\target\./source\syscalls.h:64
;     __asm__ __volatile__("ecall /* syscall(random) */" // assembler template: ecall (with a comment)
     134: 93 08 40 00   li      a7, 0x4                     ; random is syscall 4
     138: 13 05 40 06   li      a0, 0x64                    ; random(100)
     13c: 73 00 00 00   ecall                               ; a0 = random(100)

The code at address 0x140 compares the value returned from random(100) to the value in register a1. This was set to 10, so what we’re seeing here is a bounds check that jumps to the default case for values that are 10 or greater.

; C:\projects\c++\owl-cpu\target\./source\fortune.c:25
;     switch (random(100))
     140: 63 78 b5 0c   bgeu    a0, a1, 0x210 <main+0x110>  ; branch to the default case if a0 >= 10

Having performed the bounds check, determining which case to invoke based on the value in a0 returned from random(100) is simply a matter of using it to compute an index into the jump table starting at address 0x800 in .rodata, then performing an indirect jump to the address stored at that index.

     144: 13 15 25 00   slli    a0, a0, 0x2                 ; multiply by 4 to get an index into
                                                            ; the jump table
     148: b7 15 00 00   lui     a1, 0x1                     ; a1 = address of jump table
     14c: 93 85 05 80   addi    a1, a1, -0x800
     150: 33 05 b5 00   add     a0, a0, a1                  ; a0 = index + address of jump table
     154: 03 25 05 00   lw      a0, 0x0(a0)                 ; a0 = *a0
     158: 67 00 05 00   jr      a0                          ; jump to the value in a0

In short, what has happened here is that the compiler has determined that the cases in the switch statement are contiguous, and has output a jump table in the .rodata section, where each address in the table corresponds to the compiled code for one of the cases in the switch statement.

If you’re familiar with C++, then this jump table in the .rodata section is similar to a vtable, but in this case the CPU is performing an indirect jump rather than an indirect call.

Summary

In this post we reviewed how to compile fib.c and load it as an image to run on the Owl-2820 VM, and identified several shortcomings with the approach.

One of the more obvious problems was that the approach didn’t cater for data. When we looked into this, we discovered that an ELF binary caters for data by placing it into different sections according to how it is used:

  1. read-only data, such as string literals goes into the .rodata section
  2. initialized data, i.e., variables that have a value when the program starts, goes into the .data section
  3. uninitialized data, goes into the enigmatically named .bss section

We introduced some new system calls, and used them to write a new program, fortune.c, which we used as a basis for examining read-only data. We learned that the read-only data section .rodata, is used both for program data such as string literals, and for data inserted by the compiler, such as a jump table used for dispatching a switch statement.

Loading ELF binaries

In the next post, we’ll look at initialized data in .data and at uninitialized data in .bss . Following that, over the course of the next few posts, we’ll gradually build things up so that our Owl-2820 VM can load and run ELF binaries.