2020-06-11

RISC-V. Part 3: The Test Program

1 The Source (src)

1.1 System API

To set input parameters and to get the results from a program, we require I/O. Peripherals and CPU share one address space. Therefore, a CPU can only access hardware of its own:

CPUHardware Access toBasic Program
PCTarget
PC hello Prints “hello world”.
Target blinkyBlinks a LED periodically.

There are techniques to circumvent the limitation. But they require some effort and, in particular if there is analog I/O, they introduce nasty side-effects. In other words: They are far beyond the scope of this blog. Because the simulator (riscv32-unknown-elf-run) is limited to the PC peripherals, we use a hello-type program.

1.2 Program hello

Jim Wilson set up a variant of a hello-type program (https://github.com/riscv/riscv-gnu-toolchain/pull/295). It’s in the file hello.c:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int
main (void)
{
  char *string = malloc (1000);
  if (string == 0)
    return 1;
  strcpy (string, "Hello world\n");
  printf ("%s", string);
  return 0;
}

2 Compile the Test Program (make)

make is the utility which almost always does what it is meant to do. E.g., if we type make hello, it finds the file hello.c in the working directory and invokes the C-compiler to build an executable named “hello” which can be run on the PC locally. To run the cross-compiler instead of the native gcc, we could define the environment variable CC=/opt/riscv/bin/riscv32-unknown-elf-gcc beforehand and type make hello afterwards. This would do the trick.

However, it is easier to write a Makefile. This file contains everything which differs from the default rules. In our case the (preliminary) Makefile reads:

CC        :=   /opt/riscv/bin/riscv32-unknown-elf-gcc

%.elf: %.c
               $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@

CFLAGS    +=   --specs=nano.specs
LDFLAGS   +=   -Wl,--gc-sections,-Map,$*.map

hello.elf:

The specifications are for:

CC := /opt/riscv/bin/riscv32-unknown-elf-gcc
Invoke the RISC-V cross-compiler.
%.elf: %.c
     $(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
Because bare metal programming sometimes requires binary files in addition to elf-files, I prefer to use an explicit elf extension. The price for that is this rule. $@ is the name of the target (here: hello.elf) and $^ is the name of all prerequisites (here: hello.c).
CFLAGS += --specs=nano.specs
Use include directories for nano and link against libc_nano.a. This is a variant of newlib for small embedded systems.
--gc-sections
Run a garbage collection for unsued sections to remove redundant modules.
-Map,$*.map
Write a linker map. $* is a placeholder for the stem name of the target which eventually expands to “hello”.
hello.elf:
This is the primary target of the file which is built if no other target has been given.

The program compiles effortless by entering make:

/opt/riscv/bin/riscv32-unknown-elf-gcc --specs=nano.specs  -Wl,--gc-sections,-Map,hello.map  hello.c   -o hello.elf

3 Run the Test Program

3.1 Traced Run

When starting the program in the simulator I added two trace options:

--trace=on
Print all executed assembler instructions.
--trace-register
Prints all changed register values.

We’ll look into the trace file later.

/opt/riscv/bin/riscv32-unknown-elf-run --trace=on --trace-register hello.elf 2> hello.trace

yields

Hello world

“Yawn!” you might think. But there is more to it than meets the eye.

3.2 Single Stepping

3.2.1 Startup Code

C programs start with crt0 (C runtime 0 very early). The module can be found with

find /opt/riscv/ -type f -name 'crt0*'
/opt/riscv/riscv32-unknown-elf/lib/crt0.o

The symbols in this tiny module are:

/opt/riscv/bin/riscv32-unknown-elf-nm -a /opt/riscv/riscv32-unknown-elf/lib/crt0.o
         w atexit
00000000 b .bss
00000000 d .data
         U _edata
         U _end
         U exit
         U __global_pointer$
00000008 t .L0 
00000010 t .L0 
00000024 t .L0 
0000002e t .L0 
00000000 t .L11
         w __libc_fini_array
         U __libc_init_array
0000003e t .Lweak_atexit
         U main
         U memset
00000000 n .riscv.attributes
00000000 T _start
00000000 t .text

_start is the only exported symbol (T) from the text segment. It must be the start address.

The references to _edata, _end, and memset look like a bss initialization (zeroing global variables). That’s fine.

However, there is no sign of a variable pointing to the start of the data segment, neither is there a reference to memcpy. Presumably the binary assumes that the data segment (initialized data) lives in writable memory. This is fine in an OS-based environment but infeasible in a bare-metal system, where initialized data must be copied from FLASH to SRAM upon start. Put it on the to-do list (item 1).

There is no reference to the end of the SRAM. This is required to initialize the stack pointer. In an OS-based environment the OS sets the stack pointer before the program is started. But again, this is infeasible for an bare metal system. Put it on the to-do list (item 2).

Connect gdb and the simulator

The simulator can be connected with gdb. I prefer to start gdb from emacs because, just like overweighted IDEs but much more agile, the editor synchronizes debugging information to the sources. Within emacs the debugger is started by typing M-x gdb. emacs proposes gdb -i=mi hello.elf. The binary (hello.elf) is correct, the native version of gdb is wrong and must be replaced by /opt/riscv/bin/riscv32-unknown-elf-gdb once. Confirmed with ↵ Enter, gdb fires up. To connect with the simulator, type at the (gdb) prompt:

target sim
Connected to the simulator.
load

This loads the sections of the file hello.elf into the simulator:

Loading section .text, size 0x1410 lma 0x10074
Loading section .rodata, size 0x108 lma 0x11484
Loading section .sdata2, size 0x4 lma 0x1158c
Loading section .eh_frame, size 0x4 lma 0x12590
Loading section .init_array, size 0x4 lma 0x12594
Loading section .fini_array, size 0x4 lma 0x12598
Loading section .data, size 0x60 lma 0x1259c
Loading section .sdata, size 0x4 lma 0x125fc
Start address 0x1009e
Transfer rate: 44128 bits in <1 sec.

The start address is “_start”. Set a breakpoint there …

b _start
Breakpoint 1 at 0x100ae

… and start the program

r
Starting program: /home/h/cvs/risc_v/src/testcase/hello.elf 

Breakpoint 1, 0x000100ae in _start ()

Because the newlib-library has no debugging information yet(!), emacs does not display the source code automatically. Instead we must type:

disass
Dump of assembler code for function _start:
   0x0001009e <+0>:	auipc	gp,0x3
   0x000100a2 <+4>:	addi	gp,gp,-770 # 0x12d9c
   0x000100a6 <+8>:	addi	a0,gp,-1948
   0x000100aa <+12>:	addi	a2,gp,-1904
=> 0x000100ae <+16>:	sub	a2,a2,a0
   0x000100b0 <+18>:	li	a1,0
   0x000100b2 <+20>:	jal	0x101e6 <memset>
   0x000100b4 <+22>:	li	a0,0
   0x000100b8 <+26>:	beqz	a0,0x100c6 <_start+40>
   0x000100ba <+28>:	li	a0,0
   0x000100be <+32>:	auipc	ra,0x0
   0x000100c2 <+36>:	jalr	zero # 0x0
   0x000100c6 <+40>:	jal	0x1016a <__libc_init_array>
   0x000100c8 <+42>:	lw	a0,0(sp)
   0x000100ca <+44>:	addi	a1,sp,4
   0x000100cc <+46>:	li	a2,0
   0x000100ce <+48>:	jal	0x1011e <main>
   0x000100d0 <+50>:	j	0x10074 <exit>
End of assembler dump.

Except for the missing debug information, this looks quite promising, just like a real debug session. We are running RISC-V code and we can now check the sp (Stack Pointer) register for the location of the stack segment.

p/x $sp
$1 = 0x3fff170

4 Debriefing

Though the program is the classical entry for an OS (operating system) based application, it poses some challenges for a bare-metal system. So, let us see how the program looks from the bare-metal perspective.

4.1 Program Size

Type

/opt/riscv/bin/riscv32-unknown-elf-size hello.elf

to list the program size:

text    data     bss     dec     hex filename
5404     112      44    5560    15b8 hello.elf

This is quite handy. The GD32VF103VBT6 has 128 kiB FLASH (for text and data) and 32 kiB SRAM (for data and bss). The data segment appears in FLASH and SRAM because it is copied from FLASH to SRAM during run time.

4.2 Segment Locations

The simulator has memory in abundance.

fgrep -e'Memory Configuration' -A3 hello.map

yields:

Memory Configuration

Name             Origin             Length             Attributes
*default*        0x0000000000000000 0xffffffffffffffff

This is not too bad for a 32-bit system ☺. But where are the segments? to find out, type

/opt/riscv/bin/riscv32-unknown-elf-readelf --sections hello.elf
There are 16 section headers, starting at offset 0x2408:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00010074 000074 001410 00  AX  0   0  2
  [ 2] .rodata           PROGBITS        00011484 001484 000108 00   A  0   0  4
  [ 3] .sdata2           PROGBITS        0001158c 00158c 000004 00   A  0   0  4
  [ 4] .eh_frame         PROGBITS        00012590 001590 000004 00  WA  0   0  4
  [ 5] .init_array       INIT_ARRAY      00012594 001594 000004 04  WA  0   0  4
  [ 6] .fini_array       FINI_ARRAY      00012598 001598 000004 04  WA  0   0  4
  [ 7] .data             PROGBITS        0001259c 00159c 000060 00  WA  0   0  4
  [ 8] .sdata            PROGBITS        000125fc 0015fc 000004 00  WA  0   0  4
  [ 9] .sbss             NOBITS          00012600 001600 000010 00  WA  0   0  4
  [10] .bss              NOBITS          00012610 001600 00001c 00  WA  0   0  4
  [11] .comment          PROGBITS        00000000 001600 000012 01  MS  0   0  1
  [12] .riscv.attributes RISCV_ATTRIBUTE 00000000 001612 00002b 00      0   0  1
  [13] .symtab           SYMTAB          00000000 001640 000860 10     14  68  4
  [14] .strtab           STRTAB          00000000 001ea0 0004e1 00      0   0  1
  [15] .shstrtab         STRTAB          00000000 002381 000086 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)

The memory areas of the GD32VF103VBT6 are:

TypeStartEnd
SRAM 0x0800 00000x0800 7fff
FLASH0x2000 00000x2001 ffff

That’s quite a mismatch which must be resolved: Point 1 on the to-do list (item 3).

4.3 System API

How did the program allocate memory? And how did it print “Hello world”. Spoiler: The simulator uses ecall (Environment call) instructions. Let us look at the trace hello.trace and type fgrep -B1 ecall hello.trace:

reg:      0x0113ba ---   _sbrk     -wrote a7 = 0xd6
insn:     0x0113be ---   _sbrk     -ecall;
--
reg:      0x0113ec ---   _sbrk     -wrote a7 = 0xd6
insn:     0x0113f0 ---   _sbrk     -ecall;
--
reg:      0x0113ec ---   _sbrk     -wrote a7 = 0xd6
insn:     0x0113f0 ---   _sbrk     -ecall;
--
reg:      0x0113ec ---   _sbrk     -wrote a7 = 0xd6
insn:     0x0113f0 ---   _sbrk     -ecall;
--
reg:      0x0112fc ---   _fstat    -wrote a7 = 0x50
insn:     0x011300 ---   _fstat    -ecall;
--
reg:      0x0113ec ---   _sbrk     -wrote a7 = 0xd6
insn:     0x0113f0 ---   _sbrk     -ecall;
--
reg:      0x01140e ---   _write    -wrote a7 = 0x40
insn:     0x011412 ---   _write    -ecall;
--
reg:      0x0112c8 ---   _exit     -wrote a7 = 0x5d
insn:     0x0112cc ---   _exit     -ecall;

a7 contains the system call number which is specific to Linux. The numbers are defined in the file /usr/include/asm-generic/unistd.h. As a bottom line the program contains 4 different system calls:

Newlib
Function
System Call
Number
unistd.h
Symbol
_sbrk 0xd6__NR_brk
_fstat0x50__NR3264_fstat
_write0x40__NR_write
_exit 0x5d__NR_exit

This unmasks the magic and adds another issue to the to-do list (item 4).

4.4 The Stack

Where is the stack? How much stack is used? The trace file answers the questions:

fgrep 'wrote sp = ' hello.trace  | awk '{print $NF;}' | sort | uniq -c
      1 0x3fffd60
      4 0x3fffdf0
      4 0x3fffe00
      4 0x3fffe10
      1 0x3fffe20
      4 0x3fffe30
      4 0x3fffe40
      4 0x3fffe50
      6 0x3fffe70
      2 0x3fffe90
      3 0x3fffea0
      1 0x3fffee0
      2 0x3fffef0
      2 0x3ffff00
      2 0x3ffff20
      7 0x3ffff30
      7 0x3ffff40
      3 0x3ffff50
      4 0x3ffff60
      4 0x3ffff70
      6 0x3ffff80
      3 0x3ffffa0
      3 0x3ffffb0
      2 0x3ffffc0

Again, this tells us that the stack is not located in the SRAM area. The stack size is below 0.75 kiB.

5 To-do List

  1. Initialize the data segment.
  2. Load the SP-register.
  3. Fix the segment offsets to match the memory areas of the MCU.
  4. Replace the system calls.

In this blog we have colored src and make. Because we did not care about lib we cannot debug the library code. And because we did not define the linker script (ld) the segments are not in the MCU memory.

Developing embedded systems
GNU Toolchain Provided

No comments:

Post a Comment