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:
| CPU | Hardware Access to | Basic Program |
| PC | Target |
| PC | ✔ | ✘ | hello | Prints “hello world”. |
| Target | ✘ | ✔ | blinky | Blinks 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:
| Type | Start | End |
| SRAM | 0x0800 0000 | 0x0800 7fff |
| FLASH | 0x2000 0000 | 0x2001 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 |
| _fstat | 0x50 | __NR3264_fstat |
| _write | 0x40 | __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
- Initialize the data segment.
- Load the
SP-register.
- Fix the segment offsets to match the memory areas of the MCU.
- 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.
 |
| GNU Toolchain Provided |