Add the RISC-V entry point assembly to libtock_runtime, and a workflow to verify it is included correctly.
diff --git a/.github/workflows/assembly.yml b/.github/workflows/assembly.yml new file mode 100644 index 0000000..3ac8ceb --- /dev/null +++ b/.github/workflows/assembly.yml
@@ -0,0 +1,51 @@ +# Builds the libtock_runtime entry point assembly, making sure the source code +# and compiled library match. Outputs the library file as an artifact in case +# users have a different toolchain version that produces a slightly different +# library. + +name: assembly + +# We want to run this on all pull requests. Additionally, Bors needs workflows +# to run on the `staging` and `trying` branches to block merges on them. +on: + pull_request: + push: + branches: + - staging + - trying + +jobs: + assembly: + # Using ubuntu-latest can cause breakage when ubuntu-latest is updated to + # point at a new Ubuntu version. Instead, explicitly specify the version, so + # we can update when we need to. This *could* break if we don't update it + # until support for 20.04 is dropped, but it is likely we'll have a reason + # to update to a newer Ubuntu before then anyway. + runs-on: ubuntu-20.04 + + steps: + # Clones a single commit from the libtock-rs repository. The commit cloned + # is a merge commit between the PR's target branch and the PR's source. + # We'll later add another commit (the pre-merge target branch) to the + # repository. + - name: Clone repository + uses: actions/checkout@v2.3.0 + + # Build the entry point assembly. + - name: Build assembly + run: | + sudo apt-get install binutils-riscv64-linux-gnu + core/runtime/asm/assemble.sh + + # Publish the built assembly library. + - name: Upload assembly library + uses: actions/upload-artifact@v2 + with: + name: assembly + path: core/runtime/asm/*.a + + # The main goal of this is to verify the built library matches the PR's + # library, although this check has the secondary benefit of catching any + # intermediate files left behind by assemble.sh + - name: Check assembly library diff + run: git diff --quiet
diff --git a/core/runtime/asm/asm_riscv32.S b/core/runtime/asm/asm_riscv32.S new file mode 100644 index 0000000..c3f42c9 --- /dev/null +++ b/core/runtime/asm/asm_riscv32.S
@@ -0,0 +1,57 @@ +.section .start, "ax" +.globl start +start: + /* First, verify the process binary was loaded at the correct address. The + * check is performed by comparing the program counter at the start to the + * address of `start`, which is stored in rt_header. */ + auipc s0, 0 /* s0 = pc */ + mv a5, a0 /* Save rt_header so syscalls don't overwrite it */ + lw s1, 0(a5) /* s1 = rt_header.start */ + beq s0, s1, .Lset_brk /* Skip error handling code if pc is correct */ + /* If the beq on the previous line did not jump, then the binary is not at + * the correct location. Report the error via LowLevelDebug then exit. */ + li a0, 8 /* LowLevelDebug driver number */ + li a1, 1 /* Command: Print alert code */ + li a2, 2 /* Alert code 2 (incorrect location) */ + li a4, 2 /* `command` class */ + ecall + li a0, 0 /* exit-terminate */ + /* TODO: Set a completion code, once completion codes are decided */ + li a4, 6 /* `exit` class */ + ecall + + .Lset_brk: + /* memop(): set brk to rt_header's initial break value */ + li a0, 0 /* operation: set break */ + lw a1, 4(a5) /* rt_header's initial process break */ + li a4, 5 /* `memop` class */ + ecall + + /* Set the stack pointer */ + lw sp, 8(a5) /* sp = rt_header._stack_top */ + + /* Copy .data into place. */ + lw a0, 12(a5) /* remaining = rt_header.data_size */ + beqz a0, .Lzero_bss /* Jump to zero_bss if remaining is zero */ + lw a1, 16(a5) /* src = rt_header.data_flash_start */ + lw a2, 20(a5) /* dest = rt_header.data_ram_start */ + .Ldata_loop_body: + lw a3, 0(a1) /* a3 = *src */ + sw a3, 0(a2) /* *dest = a3 */ + c.addi a0, -4 /* remaining -= 4 */ + c.addi a1, 4 /* src += 4 */ + c.addi a2, 4 /* dest += 4 */ + bnez a0, .Ldata_loop_body /* Iterate again if remaining != 0 */ + + .Lzero_bss: + lw a0, 24(a5) /* remaining = rt_header.bss_size */ + beqz a0, .Lcall_rust_start /* Jump to call_Main if remaining is zero */ + lw a1, 28(a5) /* dest = rt_header.bss_start */ + .Lbss_loop_body: + sb zero, 0(a1) /* *dest = zero */ + c.addi a0, -1 /* remaining -= 1 */ + c.addi a1, 1 /* dest += 1 */ + bnez a0, .Lbss_loop_body /* Iterate again if remaining != 0 */ + + .Lcall_rust_start: + jal rust_start
diff --git a/core/runtime/asm/assemble.sh b/core/runtime/asm/assemble.sh new file mode 100755 index 0000000..d07e2ab --- /dev/null +++ b/core/runtime/asm/assemble.sh
@@ -0,0 +1,30 @@ +#!/bin/bash + +set -e + +# Switch into the directory this script is in, in case it was run from another +# location. +cd "$(dirname "$0")" + +# Our CI runs in GitHub Actions' Ubuntu 20.04 image. The only RISC-V toolchain +# in Ubuntu 20.04's repositories is for riscv64-linux-gnu. Fortunately, this +# toolchain can output 32-bit RISC-V assembly using the -march= option, and the +# fact it is targeted at GNU/Linux doesn't matter for our short handwritten +# assembly segment. +# +# Although we also support rv32imac targets, we do not need to separately +# assemble for it, as asm_riscv32.S does not use atomic instructions. +riscv64-linux-gnu-as -march=rv32imc asm_riscv32.S -o riscv32.o +# For some reason, riscv64-linux-gnu-as includes local symbols in its output. +# This pollutes the output of `objdump`, making debugging more difficult. This +# strips the extra symbols to keep the disassembly readable. +riscv64-linux-gnu-strip -K start -K rust_start riscv32.o + +# Remove the archive file in case there is something unexpected in it (so that +# issues cannot persist across calls to this script). +rm -f libriscv32.a +# c == do not complain if archive needs to be created +# r == insert or replace file in archive +riscv64-linux-gnu-ar cr libriscv32.a riscv32.o +# Remove riscv32.o as it is an intermediate build artifact. +rm riscv32.o
diff --git a/core/runtime/asm/libriscv32.a b/core/runtime/asm/libriscv32.a new file mode 100644 index 0000000..f6139a7 --- /dev/null +++ b/core/runtime/asm/libriscv32.a Binary files differ
diff --git a/core/runtime/layout_generic.ld b/core/runtime/layout_generic.ld index 0bbef87..38624a0 100644 --- a/core/runtime/layout_generic.ld +++ b/core/runtime/layout_generic.ld
@@ -1,171 +1,124 @@ -/* Userland Generic Layout +/* Layout file for Tock process binaries that use libtock-rs. This currently + * implements static linking, because we do not have a working + * position-independent relocation solution. This layout works for all + * platforms libtock-rs supports (ARM and RISC-V). * - * Currently, due to incomplete ROPI-RWPI support in rustc (see - * https://github.com/tock/libtock-rs/issues/28), this layout implements static - * linking. An application init script must define the FLASH and SRAM address - * ranges as well as MPU_MIN_ALIGN before including this layout file. - * - * Here is a an example application linker script to get started: + * This layout should be included by a script that defines the FLASH and RAM + * regions for the board as well as TBF_HEADER_SIZE. Here is a an example + * process binary linker script to get started: * MEMORY { - * /* FLASH memory region must start immediately *after* the Tock - * * Binary Format headers, which means you need to offset the - * * beginning of FLASH memory region relative to where the - * * application is loaded. - * FLASH (rx) : ORIGIN = 0x10030, LENGTH = 0x0FFD0 - * SRAM (RWX) : ORIGIN = 0x20000, LENGTH = 0x10000 + * FLASH (X) : ORIGIN = 0x10000, LENGTH = 0x10000 + * RAM (W) : ORIGIN = 0x20000, LENGTH = 0x10000 * } - * MPU_MIN_ALIGN = 8K; + * TBF_HEADER_SIZE = 0x40; * INCLUDE ../libtock-rs/layout.ld + * + * FLASH refers to the area the process binary occupies in flash, including TBF + * headers. RAM refers to the area the process will have access to in memory. + * STACK_SIZE is the size of the process' stack (this layout file may round the + * stack size up for alignment purposes). TBF_HEADER_SIZE must correspond to the + * --protected-region-size flag passed to elf2tab. + * + * This places the flash sections in the following order: + * 1. .rt_header -- Constants used by runtime initialization. + * 2. .text -- Executable code. + * 3. .rodata -- Read-only global data (e.g. most string constants). + * 4. .data -- Read-write data, copied to RAM at runtime. + * + * This places the RAM sections in the following order: + * 1. .stack -- The stack grows downward. Putting it first gives us + * MPU-based overflow detection. + * 2. .data -- Read-write data, initialized by copying from flash. + * 3. .bss -- Zero-initialized read-write global data. + * 4. Heap -- The heap (optional) comes after .bss and grows upwards to + * the process break. */ -ENTRY(_start) +/* GNU LD looks for `start` as an entry point by default, while LLVM's LLD looks + * for `_start`. To be compatible with both, we manually specify an entry point. + */ +ENTRY(start) SECTIONS { - /* Section for just the app crt0 header. - * This must be first so that the app can find it. + /* Sections located in FLASH at runtime. */ - .crt0_header : - { - _beginning = .; /* Start of the app in flash. */ - /** - * Populate the header expected by `crt0`: - * - * struct hdr { - * uint32_t got_sym_start; - * uint32_t got_start; - * uint32_t got_size; - * uint32_t data_sym_start; - * uint32_t data_start; - * uint32_t data_size; - * uint32_t bss_start; - * uint32_t bss_size; - * uint32_t reldata_start; - * uint32_t stack_size; - * }; - */ - /* Offset of GOT symbols in flash */ - LONG(LOADADDR(.got) - _beginning); - /* Offset of GOT section in memory */ - LONG(_got); - /* Size of GOT section */ - LONG(SIZEOF(.got)); - /* Offset of data symbols in flash */ - LONG(LOADADDR(.data) - _beginning); - /* Offset of data section in memory */ - LONG(_data); - /* Size of data section */ + + /* Add a section where elf2tab will place the TBF headers, so that the rest + * of the FLASH sections are in the right locations. */ + .tbf_header (NOLOAD) : { + . = . + TBF_HEADER_SIZE; + } > FLASH + + /* Runtime header. Contains values the linker knows that the runtime needs + * to look up. + */ + .rt_header : { + rt_header = .; + LONG(start); + LONG(ADDR(.bss) + SIZEOF(.bss)); /* Initial process break */ + LONG(_stack_top); LONG(SIZEOF(.data)); - /* Offset of BSS section in memory */ - LONG(_bss); - /* Size of BSS section */ + LONG(LOADADDR(.data)); + LONG(ADDR(.data)); LONG(SIZEOF(.bss)); - /* First address offset after program flash, where elf2tab places - * .rel.data section */ - LONG(LOADADDR(.endflash) - _beginning); - /* The size of the stack requested by this application */ - LONG(_stack_top_aligned - _sstack); - /* Pad the header out to a multiple of 32 bytes so there is not a gap - * between the header and subsequent .data section. It's unclear why, - * but LLD is aligning sections to a multiple of 32 bytes. */ - . = ALIGN(32); - } > FLASH =0xFF + LONG(ADDR(.bss)); + } > FLASH - /* Text section, Code! */ - .text : - { - . = ALIGN(4); - _text = .; - KEEP (*(.start)) - *(.text*) - *(.rodata*) - KEEP (*(.syscalls)) - *(.ARM.extab*) - . = ALIGN(4); /* Make sure we're word-aligned here */ - _etext = .; - } > FLASH =0xFF + /* Text section -- the application's code. */ + .text ALIGN(4) : { + *(.start) + *(.text) + } > FLASH - /* Application stack */ - .stack (NOLOAD) : - { - /* elf2tab requires that the `_sram_origin` symbol be present to - * mark the first address in the SRAM memory. Since ELF files do - * not really need to specify this address as they only care about - * loading into flash, we need to manually mark this address for - * elf2tab. elf2tab will use it to add a fixed address header in the - * TBF header if needed. + /* Read-only data section. Contains strings and other global constants. */ + .rodata ALIGN(4) : { + *(.rodata) + /* .data is placed after .rodata in flash. data_flash_start is used by + * AT() to place .data in flash as well as in rt_header. */ - _sram_origin = .; - _sstack = .; + _data_flash_start = .; + } > FLASH + + /* Sections located in RAM at runtime. + */ + + /* Reserve space for the stack. Aligned to a multiple of 16 bytes for the + * RISC-V calling convention: + * https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf + */ + .stack (NOLOAD) : { KEEP(*(.stack_buffer)) - _stack_top_unaligned = .; - . = ALIGN(8); - _stack_top_aligned = .; - } > SRAM + _stack_top = ALIGN(16); /* Used in rt_header */ + } > RAM - /* Data section, static initialized variables - * Note: This is placed in Flash after the text section, but needs to be - * moved to SRAM at runtime + /* Read-write data section. This is deployed as part of FLASH but is copied + * into RAM at runtime. */ - .data : AT (_etext) - { - . = ALIGN(4); /* Make sure we're word-aligned here */ - _data = .; - KEEP(*(.data*)) - *(.sdata*) /* RISC-V small-pointer data section */ - . = ALIGN(4); /* Make sure we're word-aligned at the end of flash */ - } > SRAM - - /* Global Offset Table */ - .got : - { - . = ALIGN(4); /* Make sure we're word-aligned here */ - _got = .; - *(.got*) - *(.got.plt*) + .data ALIGN(4) : AT(_data_flash_start) { + data_ram_start = .; + /* .sdata is the RISC-V small data section */ + *(.sdata .data) + /* Pad to word alignment so the relocation loop can use word-sized + * copies. + */ . = ALIGN(4); - } > SRAM + } > RAM - /* BSS section, static uninitialized variables */ - .bss : - { - . = ALIGN(4); /* Make sure we're word-aligned here */ - _bss = .; - KEEP(*(.bss* .sbss*)) - *(COMMON) - . = ALIGN(4); - } > SRAM - - /* End of flash. */ - .endflash : - { - } > FLASH - - /* ARM Exception support - * - * This contains compiler-generated support for unwinding the stack, - * consisting of key-value pairs of function addresses and information on - * how to unwind stack frames. - * https://wiki.linaro.org/KenWerner/Sandbox/libunwind?action=AttachFile&do=get&target=libunwind-LDS.pdf - * - * .ARM.exidx is sorted, so has to go in its own output section. - * - * __NOTE__: It's at the end because we currently don't actually serialize - * it to the binary in elf2tbf. If it was before the RAM sections, it would - * through off our calculations of the header. + /* BSS section. These are zero-initialized static variables. This section is + * not copied from FLASH into RAM but rather directly initialized, and is + * mainly put in this linker script so that we get an error if it overflows + * the RAM region. */ - PROVIDE_HIDDEN (__exidx_start = .); - .ARM.exidx : - { - /* (C++) Index entries for section unwinding */ - *(.ARM.exidx* .gnu.linkonce.armexidx.*) - } > FLASH - PROVIDE_HIDDEN (__exidx_end = .); + .bss ALIGN(4) (NOLOAD) : { + /* .sbss is the RISC-V small data section */ + *(.sbss .bss) + } > RAM + _heap_start = ADDR(.bss) + SIZEOF(.bss); /* Used by rt_header */ + + /* Sections we do not need. */ /DISCARD/ : { - *(.eh_frame) + *(.ARM.exidx .eh_frame) } } - -ASSERT((_stack_top_aligned - _stack_top_unaligned) == 0, " -STACK_SIZE must be 8 byte multiple")