When you learned to write C, you might have been in touch with compiling already. In case you’re a master at compiling and cross compiling, this section is a refresher. In the other case (which also includes those that only clicked the Compile-button in an IDE), this section will guide you through the most import aspects (for this course).
Compiling is typically used to describe the proces that converts the C sources into a binary that can be executed. When looking into detail what happens, it turns out to be a bit more complex than simply invoking gcc.
cli@hwswcd:~/$> gcc -o main.bin main.c
A more accurate use of Compiling is to indicate the part of the toolchain that converts source code into machine code. (Although this is not entirely correct, going into the details falls out of the scope of this course.) The output of the compilers are called object files (.o) which need to be stiched together by Linking.
cli@hwswcd:~/$> gcc -c main.c
cli@hwswcd:~/$> gcc -o main.bin main.o
When your entire program only consists out of a single file, it is (probably) not worth going into this much detail. Luckily tools exists that nicely hide this away for the programmer. In this course Make will be used. To use make, the programmer (as in: YOU) need to work with a Makefile. (Trust me, you’ll be thankful for it.)
The linker is the tool that connects everything together and fills in the dots. This example shows a very simple C program that prints the classical message to the output. However there is not many code written, the linker has work to do. For example, what is this printf() function? Where is it coming from and what does it do exactly? As you might know already, this comes from libraries that have done some boring, heavy lifting for you. This is just a simple example of where the linker jumps in to hook-up everything together.
#include <stdio.h>
int main(void) {
printf("Hello world!\n");
return 0;
}
If the machine on which the compiler and linker are executed differs from the target machine that is to execute the program, the term cross-compilation is used. Probably you have already used a cross compiler before (unknowingly), or haven’t you programmed an Arduino yet? Although you compiled-and-linked the code on your laptop, the program was executed by another processor.
All the code in this course will also be cross-compiled. The toolchain that is used is official RISC-V C and C++ cross-compiler. You can download and install it for yourself from GitHub. For more information on installing this toolchain please use Google (e.g. here).
Ofcourse you are warmly invited to get the toolchain up and running on your own machines. However, this is not always easy and it can be a dive into a rabbit hole. This cross-compiler is installed on Roger, especially for you!
Bare metal programming is writing software that is not running on an Operating System (OS). You probably have done this (maybe unknowingly) while programming an Arduino or another microprocessor. Running without an OS has one major disadvantage: there is no OS. This implies that everything a programmer needs, has to be provided. In the C-example of the previous section, one more function has been used: print_hex(x, y);
. Code for this function has to be provided too.
#ifndef PRINT_H
#define PRINT_H
#define OUTPORT 0x80000000
void print_chr(char ch);
void print_str(const char *p);
void print_hex(unsigned int val, int digits);
#endif
#include "print.h"
void print_chr(char ch) {
*((volatile unsigned int*)OUTPORT) = ch;
}
void print_str(const char *p) {
while (*p != 0)
*((volatile unsigned int*)OUTPORT) = *(p++);
}
void print_hex(unsigned int val, int digits) {
unsigned int index, max;
int i; /* !! must be signed, because of the check 'i>=0' */
char x;
if(digits == 0)
return;
max = digits << 2;
for (i = max-4; i >= 0; i -= 4) {
index = val >> i;
index = index & 0xF;
x="0123456789ABCDEF"[index];
*((volatile unsigned int*)OUTPORT) = x;
}
print_str("\n");
}
With these three functions, there is an opportunity to print a character, a string (as in: a list of characters), or a hexadecimal value. A logical question would be: “Where is my character (or other variable) printed ?”. The answer lies in this line:
*((volatile uint32_t*)OUTPORT) = ch;
Let’s break this down for those whose C-skills are a bit rusty. The define OUTPORT makes sure that, everywhere in the code this define is substituted by the 32-bit number 0x80000000. During assignment, this value is type-cast to an unsigned 32-bit pointer ((volatile uint32_t*)). The keyword volatile states that the content of a variable can also be altered from another source. This is important!! Otherwise the optimisation of the C-compiler might optimise-out certain lines of C-code. Finally, that address is dereferenced ( *(<address>) ) to target the memory that is located at the provided address.
To make the code even more readable, another define can be made that hides this low level C-code.
#ifndef PRINT_H
#define PRINT_H
#define HWSWCD_PRINT_BASE_ADDRESS 0x80000000
#define HWSWCD_PRINT *((volatile unsigned int*)HWSWCD_PRINT_BASE_ADDRESS)
void print_chr(char ch);
void print_str(const char *p);
void print_hex(unsigned int val, int digits);
#endif
#include "print.h"
void print_chr(char ch) {
HWSWCD_PRINT = ch;
}
void print_str(const char *p) {
while (*p != 0)
HWSWCD_PRINT = *(p++);
}
void print_hex(unsigned int val, int digits) {
unsigned int index, max;
int i; /* !! must be signed, because of the check 'i>=0' */
char x;
if(digits == 0)
return;
max = digits << 2;
for (i = max-4; i >= 0; i -= 4) {
index = val >> i;
index = index & 0xF;
HWSWCD_PRINT="0123456789ABCDEF"[index];
}
print_str("\n");
}
When it comes to compiling, more files are involved. When the linker looks at the generated object files, it can now find all the necessary functions.
Note that also the object files can be inspected. The compiler typically includes a tool for this. In case of the RISC-V crosscompiler this tool is invoked like this: riscv32-unknown-elf-objdump -D build/print.o
.
print.o: file format elf32-littleriscv
Disassembly of section .text:
00000000 <print_chr>:
0: 800007b7 lui a5,0x80000
4: 00a7a023 sw a0,0(a5) # 80000000 <.L17+0x7ffffeb8>
8: 00008067 ret
0000000c <print_str>:
c: 80000737 lui a4,0x80000
00000010 <.L3>:
10: 00054783 lbu a5,0(a0)
14: 00079463 bnez a5,1c <.L4>
18: 00008067 ret
0000001c <.L4>:
1c: 00150513 addi a0,a0,1
20: 00f72023 sw a5,0(a4) # 80000000 <.L17+0x7ffffeb8>
24: fedff06f j 10 <.L3>
00000028 <_print_dec>:
28: ff010113 addi sp,sp,-16
2c: 00812423 sw s0,8(sp)
30: 00112623 sw ra,12(sp)
34: 00050413 mv s0,a0
38: 00900793 li a5,9
3c: 00000513 li a0,0
00000040 <.L6>:
40: 0287ec63 bltu a5,s0,78 <.L7>
44: 04a7e063 bltu a5,a0,84 <.L8>
48: 03050513 addi a0,a0,48
4c: 0ff57513 zext.b a0,a0
50: 800007b7 lui a5,0x80000
54: 00a7a023 sw a0,0(a5) # 80000000 <.L17+0x7ffffeb8>
00000058 <.L9>:
58: 03040413 addi s0,s0,48
5c: 0ff47413 zext.b s0,s0
60: 800007b7 lui a5,0x80000
64: 00c12083 lw ra,12(sp)
68: 0087a023 sw s0,0(a5) # 80000000 <.L17+0x7ffffeb8>
6c: 00812403 lw s0,8(sp)
70: 01010113 addi sp,sp,16
74: 00008067 ret
00000078 <.L7>:
78: 00150513 addi a0,a0,1
7c: ff640413 addi s0,s0,-10
80: fc1ff06f j 40 <.L6>
00000084 <.L8>:
84: 00000097 auipc ra,0x0
88: 000080e7 jalr ra # 84 <.L8>
8c: fcdff06f j 58 <.L9>
00000090 <print_dec>:
90: ff010113 addi sp,sp,-16
94: 00812423 sw s0,8(sp)
98: 00112623 sw ra,12(sp)
9c: 00050413 mv s0,a0
a0: 00900793 li a5,9
a4: 00000513 li a0,0
000000a8 <.L12>:
a8: 0487e063 bltu a5,s0,e8 <.L13>
ac: 04a7e463 bltu a5,a0,f4 <.L14>
b0: 03050513 addi a0,a0,48
b4: 0ff57513 zext.b a0,a0
b8: 800007b7 lui a5,0x80000
bc: 00a7a023 sw a0,0(a5) # 80000000 <.L17+0x7ffffeb8>
000000c0 <.L15>:
c0: 03040413 addi s0,s0,48
c4: 0ff47413 zext.b s0,s0
c8: 800007b7 lui a5,0x80000
cc: 0087a023 sw s0,0(a5) # 80000000 <.L17+0x7ffffeb8>
d0: 00c12083 lw ra,12(sp)
d4: 00812403 lw s0,8(sp)
d8: 00a00713 li a4,10
dc: 00e7a023 sw a4,0(a5)
e0: 01010113 addi sp,sp,16
e4: 00008067 ret
000000e8 <.L13>:
e8: 00150513 addi a0,a0,1
ec: ff640413 addi s0,s0,-10
f0: fb9ff06f j a8 <.L12>
000000f4 <.L14>:
f4: 00000097 auipc ra,0x0
f8: 000080e7 jalr ra # f4 <.L14>
fc: fc5ff06f j c0 <.L15>
00000100 <print_hex>:
100: 04058463 beqz a1,148 <.L17>
104: 00259593 slli a1,a1,0x2
108: ffc58593 addi a1,a1,-4
10c: 000006b7 lui a3,0x0
110: 80000637 lui a2,0x80000
00000114 <.L19>:
114: 0005da63 bgez a1,128 <.L20>
118: 00000537 lui a0,0x0
11c: 00050513 mv a0,a0
120: 00000317 auipc t1,0x0
124: 00030067 jr t1 # 120 <.L19+0xc>
00000128 <.L20>:
128: 00b55733 srl a4,a0,a1
12c: 00068793 mv a5,a3
130: 00f77713 andi a4,a4,15
134: 00e787b3 add a5,a5,a4
138: 0007c783 lbu a5,0(a5)
13c: ffc58593 addi a1,a1,-4
140: 00f62023 sw a5,0(a2) # 80000000 <.L17+0x7ffffeb8>
144: fd1ff06f j 114 <.L19>
00000148 <.L17>:
148: 00008067 ret
Disassembly of section .rodata.str1.4:
00000000 <.LC1>:
0: 000a .insn 2, 0x000a
...
00000004 <.LC0>:
4: 3130 .insn 2, 0x3130
6: 3332 .insn 2, 0x3332
8: 3534 .insn 2, 0x3534
a: 3736 .insn 2, 0x3736
c: 3938 .insn 2, 0x3938
e: 4241 .insn 2, 0x4241
10: 46454443 .insn 4, 0x46454443
...
Disassembly of section .comment:
00000000 <.comment>:
0: 4700 .insn 2, 0x4700
2: 203a4343 .insn 4, 0x203a4343
6: 6728 .insn 2, 0x6728
8: 3430 .insn 2, 0x3430
a: 3936 .insn 2, 0x3936
c: 6436 .insn 2, 0x6436
e: 3066 .insn 2, 0x3066
10: 2939 .insn 2, 0x2939
12: 3120 .insn 2, 0x3120
14: 2e34 .insn 2, 0x2e34
16: 2e32 .insn 2, 0x2e32
18: 0030 .insn 2, 0x0030
Disassembly of section .riscv.attributes:
00000000 <.riscv.attributes>:
0: 1b41 .insn 2, 0x1b41
2: 0000 .insn 2, 0x
4: 7200 .insn 2, 0x7200
6: 7369 .insn 2, 0x7369
8: 01007663 bgeu zero,a6,14 <.riscv.attributes+0x14>
c: 0011 .insn 2, 0x0011
e: 0000 .insn 2, 0x
10: 1004 .insn 2, 0x1004
12: 7205 .insn 2, 0x7205
14: 3376 .insn 2, 0x3376
16: 6932 .insn 2, 0x6932
18: 7032 .insn 2, 0x7032
1a: 0031 .insn 2, 0x0031
firmware.o: file format elf32-littleriscv
Disassembly of section .text.startup:
00000000 <main>:
0: ff010113 addi sp,sp,-16
4: 00200593 li a1,2
8: 00100513 li a0,1
c: 00112623 sw ra,12(sp)
10: 00000097 auipc ra,0x0
14: 000080e7 jalr ra # 10 <main+0x10>
18: 00200593 li a1,2
1c: 00100513 li a0,1
20: 00000097 auipc ra,0x0
24: 000080e7 jalr ra # 20 <main+0x20>
28: 00200593 li a1,2
2c: 00058513 mv a0,a1
30: 00000097 auipc ra,0x0
34: 000080e7 jalr ra # 30 <main+0x30>
38: 00200593 li a1,2
3c: 00300513 li a0,3
40: 00000097 auipc ra,0x0
44: 000080e7 jalr ra # 40 <main+0x40>
48: 00200593 li a1,2
4c: 00500513 li a0,5
50: 00000097 auipc ra,0x0
54: 000080e7 jalr ra # 50 <main+0x50>
58: 00200593 li a1,2
5c: 00800513 li a0,8
60: 00000097 auipc ra,0x0
64: 000080e7 jalr ra # 60 <main+0x60>
68: 00200593 li a1,2
6c: 00d00513 li a0,13
70: 00000097 auipc ra,0x0
74: 000080e7 jalr ra # 70 <main+0x70>
78: 00200593 li a1,2
7c: 01500513 li a0,21
80: 00000097 auipc ra,0x0
84: 000080e7 jalr ra # 80 <main+0x80>
00000088 <.L2>:
88: 0000006f j 88 <.L2>
Disassembly of section .comment:
00000000 <.comment>:
0: 4700 .insn 2, 0x4700
2: 203a4343 .insn 4, 0x203a4343
6: 6728 .insn 2, 0x6728
8: 3430 .insn 2, 0x3430
a: 3936 .insn 2, 0x3936
c: 6436 .insn 2, 0x6436
e: 3066 .insn 2, 0x3066
10: 2939 .insn 2, 0x2939
12: 3120 .insn 2, 0x3120
14: 2e34 .insn 2, 0x2e34
16: 2e32 .insn 2, 0x2e32
18: 0030 .insn 2, 0x0030
Disassembly of section .riscv.attributes:
00000000 <.riscv.attributes>:
0: 1b41 .insn 2, 0x1b41
2: 0000 .insn 2, 0x
4: 7200 .insn 2, 0x7200
6: 7369 .insn 2, 0x7369
8: 01007663 bgeu zero,a6,14 <.riscv.attributes+0x14>
c: 0011 .insn 2, 0x0011
e: 0000 .insn 2, 0x
10: 1004 .insn 2, 0x1004
12: 7205 .insn 2, 0x7205
14: 3376 .insn 2, 0x3376
16: 6932 .insn 2, 0x6932
18: 7032 .insn 2, 0x7032
1a: 0031 .insn 2, 0x0031
Typically, the one function that has to be present in every C-program is the main() function. This function is called by the OS to start of the program. As in bare metal programming there is no OS, some form of booting-process needs to be defined. Assembly to the rescue!!
A short assembly file can jump in and do that job. This assembly code will do the following:
One thing to note is the line .global start
. This line makes the start function visible outside this assembly file. As this function is going to be called from outside, this line is NOT optional.
Then there is the line .section .init
which is needed. This tells the compiler that the code that follows has to be compiled under a section with the name init. This will later be necessary for the linking.
This assembly file is compiled in a similar way as the c-files. An object file is generated which can be inspected. Note that 0 bytes are allocated to the .text section and 88 bytes are present in the .init section.
build/start.o: file format elf32-littleriscv
build/start.o
architecture: riscv:rv32, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000000 00000000 00000000 00000034 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 00000034 2**0
ALLOC
3 .init 00000088 00000000 00000000 00000034 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
4 .riscv.attributes 0000001a 00000000 00000000 000000bc 2**0
CONTENTS, READONLY
SYMBOL TABLE:
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .init 00000000 .init
00000084 l .init 00000000 done
00000000 l d .riscv.attributes 00000000 .riscv.attributes
00000000 g .init 00000000 start
00000000 *UND* 00000000 main
RELOCATION RECORDS FOR [.init]:
OFFSET TYPE VALUE
00000080 R_RISCV_JAL main
00000084 R_RISCV_JAL done
.global start
.section .init, "ax"
start:
/* zero-initialize all registers */
addi x1, zero, 0
addi x2, zero, 0
addi x3, zero, 0
addi x4, zero, 0
addi x5, zero, 0
addi x6, zero, 0
addi x7, zero, 0
addi x8, zero, 0
addi x9, zero, 0
addi x10, zero, 0
addi x11, zero, 0
addi x12, zero, 0
addi x13, zero, 0
addi x14, zero, 0
addi x15, zero, 0
addi x16, zero, 0
addi x17, zero, 0
addi x18, zero, 0
addi x19, zero, 0
addi x20, zero, 0
addi x21, zero, 0
addi x22, zero, 0
addi x23, zero, 0
addi x24, zero, 0
addi x25, zero, 0
addi x26, zero, 0
addi x27, zero, 0
addi x28, zero, 0
addi x29, zero, 0
addi x30, zero, 0
addi x31, zero, 0
/* set stack pointer */
lui sp, %hi(4*1024)
/* call main */
jal ra, main
j done
done:
j done