1.2 - Compiling Bare Metal

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

gcc_1

gcc_1

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.

main.c
#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

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.

print.h
#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
print.c
#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.

print.h
#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
print.c
#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.

gcc_1

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.

objdump of 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
objdump of firmware.o

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

Booting

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:

  • initialise all the registers of the register file to 0x0
  • initialise the stack pointer (x2 - sp) to 4096 (=0x00001000)
  • jump to the main function and backup the program counter (x1 - ra)
  • create and endless loop to catch a returning main function

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.

objdump of start.o

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
start.S
.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