Porting

From llvm-mos
Jump to navigation Jump to search

We've designed the LLVM-MOS SDK to be as possible to port to new platforms (but no easier). This is a tutorial-style guide on how to do so.

Imaginary Target[edit | edit source]

For the purposes of this guide, we'll need a target to port to. Rather than use a real target (which may have an official port by the next time this guide is updated), we'll invent a new one.

We'll make the target as simple as possible. (Real targets are complicated, but they're all complicated in different ways). Let's say the target has 64KiB of RAM available, with no banking. We'll also imagine that the target has an emulator that's capable of loading programs in some file format. Let's say the file format is very simple: a 64KiB image to load into RAM, followed by two bytes indicating the start address, little-endian.

The Simplest Program[edit | edit source]

First, make sure the latest SDK release is extracted somewhere, and make a directory to work in. You can do most of this tutorial without the SDK sources; you only need the SDK sources if you're looking to contribute your port to the SDK. (But please do!)

Next, create the simplest possible C program: main.c

int main(void) { return 0; }

Parent Target[edit | edit source]

The SDK's targets are hierarchical: a target can have an incomplete target as a parent. The parent is called incomplete because the child fills in missing pieces of it. An incomplete target can also have a different incomplete target as a parent, forming a tree. The complete targets form the leaves of this tree; only these can produce binaries.

For porting a real target, take a look at the SDK; there may already be an incomplete target for the family of devices or boards you're porting to. Completing an incomplete target is much much easier than building one from scratch.

Along those lines, the tree of targets is rooted at the common target. This provides functionality that is essential to running C on a 6502; it can be reasonably shared by all targets.

Since we're porting to fake target, we should select common as our parent target. This means to compile our code, we should invoke clang as mos-common-clang.

Compiling: First Attempt[edit | edit source]

Let's compile:

$ mos-common-clang -o main -Os main.c
ld.lld: error: cannot find linker script link.ld

This fails because the linker has no earthly idea how to layout out code for our platform. For this, we have to provide a linker script, link.ld.

Linker Script[edit | edit source]

The linker scripts are based on GCC linker scripts (reference), which is extended by LLD (reference), which is further extended by LLVM-MOS (reference).

There's a lot of functionality packed behind these little scripts; it can take time to learn the language thoroughly. However, you don't need very much to get started.

Here's a minimal linker script for our platform: link.ld

MEMORY { ram (rw) : ORIGIN = 0x200, LENGTH = 0xfe00 }
SECTIONS { INCLUDE c.ld }

__rc0 = 0x00;
INCLUDE imag-regs.ld
ASSERT(__rc0 == 0x00, "Inconsistent zero page map.")
ASSERT(__rc31 == 0x1f, "Inconsistent zero page map.")

The MEMORY section describes the layout of the RAM available for the linker to put linked sections in. This typically excludes the zero page and stack; these are usually handled by other mechanisms. This MEMORY section states that there's a memory region named ram suitable to be assigned both read-only and writable sections. It starts at 0x200, and it ends at the end of RAM.

The SECTIONS directive states which sections from input files the linker should place in which output sections, as well as symbols relating to section placement. The linker will automatically place all sections in the ram region, which is what we want.

The next bit of linker script assigns symbols __rc0 through __rc31 to addresses 0 through 31. This defines the "imaginary registers" in the zero page that are reserved for compiler use (and that form the C calling convention). INCLUDE imag-regs.ld is a helper script that automatically assigns each unset register to the register before it + 1. Thus, you only need to set the first register to zero, and the script takes care of the rest. Note that you can only specify the locations of even registers; the odd registers are fixed, since they must immediately follow the preceding register for the pair to work as a pointer.

Compiling: Second Attempt[edit | edit source]

$ mos-common-clang -o main -Os main.c
$ ls -l
...
main
...
$ file main
main: ELF 32-bit LSB executable, *unknown arch 0x1966* version 1 (SYSV), statically linked, not stripped

That compiled, but it's an ELF file, which isn't at all what the target takes. LLVM-MOS uses ELF for its object files, libraries, and executables (by default). ELF provides rich information about the contents; this is what allows the full suite of LLVM tools to work. For example, you can use llvm-nm to dump all the symbols in the generated file:

$ llvm-nm main
00000209 B __bss_end
00000000 A __bss_size
00000209 B __bss_start
00000209 T __data_end
00000209 A __data_load_start
00000000 A __data_size
00000209 T __data_start
00000209 T __fini_array_end
00000209 T __fini_array_start
00000209 B __heap_start
00000209 T __init_array_end
00000209 T __init_array_start
00000000 A __rc0
00000001 A __rc1
...
00000009 A __rc9
00000203 T _fini
00000200 T _init
00000200 T _start
00000204 T main

Most of these symbols were generated by the INCLUDE c.ld call, but you can also see our main function was placed at 0x204, and that the imaginary registers were set up appropriately. You can get a disassembly too:

$ llvm-objdump -d --print-imm-hex main

main:	file format elf32-mos

Disassembly of section .text:

00000200 <_start>:
     200: 20 04 02     	jsr	$204

00000203 <_fini>:
     203: 60           	rts

00000204 <main>:
     204: a2 00        	ldx	#$0
     206: a9 00        	lda	#$0
     208: 60           	rts

However, none of this helps make an output file that our emulator can actually load.

Output Format[edit | edit source]

To make our object file, we return to our linker script: link.ld

MEMORY {
  ram : ORIGIN = 0x0000, LENGTH = 0x10000
  user_ram (rw) : ORIGIN = 0x0200, LENGTH = 0xfe00
}
SECTIONS { INCLUDE c.ld }

__rc0 = 0x00;
INCLUDE imag-regs.ld
ASSERT(__rc0 == 0x00, "Inconsistent zero page map.")
ASSERT(__rc31 == 0x1f, "Inconsistent zero page map.")

OUTPUT_FORMAT { FULL(ram) SHORT(_start) }

The new line, OUTPUT_FORMAT { FULL(ram) SHORT(_start) }, is an LLVM-MOS extension to the linker script language that describes what our actual output file should look like. We want the full contents of RAM to be output, padded with zeros. Afterwards, we want a little-endian short containing the starting point of the program, set up by the common target: _start.

Additionally, the ram section was renamed to user_ram; this is more appropriate, since it doesn't actually span the full 64KiB address range, which is what we want to write to the file. Instead, we create an overlapping ram section that starts at 0 and ends at 0xffff. This is the section we write in the OUTPUT_FORMAT.

Compiling: Third Attempt[edit | edit source]

$ mos-common-clang -o main -Os main.c
$ ls -l
...
main
main.elf
...
$ file main
main:data
$ file main.elf
main: ELF 32-bit LSB executable, *unknown arch 0x1966* version 1 (SYSV), statically linked, not stripped
$ hexdump -C main
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00000200  20 04 02 60 a2 00 a9 00  60 00 00 00 00 00 00 00  | ..`....`.......|
00000210  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
00010000  00 02                                             |..|
00010002

This time two files were produced: main, and main.elf. main.elf is the ELF file produced previously, while main contains the contents as described by the OUTPUT_FORMAT script.

Looking at the contents of main, we have 64KiB of data, with the interesting stuff beginning at 0x200, as expected. Afterwards, we have 00 02, which is the little-endian word 0x200, which is indeed the value of _start.

Optional Libraries[edit | edit source]

Take another look at the disassembly:

main:	file format elf32-mos

Disassembly of section .text:

00000200 <_start>:
     200: 20 04 02     	jsr	$204

00000203 <_fini>:
     203: 60           	rts

00000204 <main>:
     204: a2 00        	ldx	#$0
     206: a9 00        	lda	#$0
     208: 60           	rts

Several things are amiss already. There's a _start routine that contains JSR main, that's good. But _fini doesn't look hooked up to anything. And once it's finished, main just returns back to _start, which runs right into _fini, which returns into never-never land.

This is all because the common target's libraries don't by default include any functionality that isn't absolutely necessary for the C runtime. In this case, that's _start, which contains pre-main functionality (e.g., __attribute__((constructor))) and C++ constructors, and _fini, which contains post-main functionality (e.g., __attribute__((destructor)) and C++ destructors).

Other functionality is contained in optional libraries that must be explicitly included by the targets. This is either because the nature of the target and it's OS may make the functionality unnecessary, or because there is more than one best way to accomplish it, again depending on the target.

The canonical reference for these optional libraries are the various CMakeLists.txt files in the common target.

One of the things that a target needs to decide is how a program is exited. This happens whenever exit is called, or implicitly when main returns. When either occurs, _fini must be called first. The common target provides optional libraries corresponding to the usual possibilities:

Exit Library Description
exit-custom Run some custom code (e.g., execute a special BRK, which goes into an underlying OS).
exit-loop Go into an infinite loop.
exit-return Return from _start

Our target doesn't have an underlying OS, so exit-loop seems reasonable. Specifying this library on the command line produces a more sensible behavior:

$ mos-common-clang -o main -Os main.c -lexit-loop
$ llvm-objdump -d --print-imm-hex main.elf

main.elf:	file format elf32-mos

Disassembly of section .text:

00000200 <_start>:
     200: 20 07 02     	jsr	$207

00000203 <__after_main>:
     203: 4c 0c 02     	jmp	$20c

00000206 <_fini>:
     206: 60           	rts

00000207 <main>:
     207: a2 00        	ldx	#$0
     209: a9 00        	lda	#$0
     20b: 60           	rts

0000020c <exit>:
     20c: 20 06 02     	jsr	$206
     20f: 4c 0f 02     	jmp	$20f

Now, after main returns back to _start, _start falls through to __after_main, which jumps to exit. exit in turn calls _fini then enters an infinite loop, as desired. exit can then be called by any C function to run _fini and loop, not just by returning from main.

There are some other optional libraries that are necessary to get full C language functionality on our target. Notably, we need to set up the stack. The init-stack optional library can do this; we just need to provide a symbol, __stack, that is the top of stack at program start. Since there's nothing on the stack, for this target the top of stack should actually be 0x10000; since that's not a real address should counterintuitively be 0. You could also use 0xffff and waste a byte; no-one would judge you.

We can set this in our linker script, link.ld:

MEMORY {
  ram : ORIGIN = 0x0000, LENGTH = 0x10000
  user_ram (rw) : ORIGIN = 0x0200, LENGTH = 0xfe00
}
SECTIONS { INCLUDE c.ld }

__rc0 = 0x00;
INCLUDE imag-regs.ld
ASSERT(__rc0 == 0x00, "Inconsistent zero page map.")
ASSERT(__rc31 == 0x1f, "Inconsistent zero page map.")

/* Top of stack would be 0x10000, but wrap around. */
__stack = 0

OUTPUT_FORMAT { FULL(ram) SHORT(_start) }

Finally, we can compile again and link against init-stack too:

$ mos-common-clang -o main -Os main.c -lexit-loop -linit-stack
$ llvm-objdump -d --print-imm-hex main.elf

main.elf:	file format elf32-mos

Disassembly of section .text:

00000200 <_start>:
     200: 20 07 02     	jsr	$207

00000203 <__after_main>:
     203: 4c 0c 02     	jmp	$20c

00000206 <_fini>:
     206: 60           	rts

00000207 <main>:
     207: a2 00        	ldx	#$0
     209: a9 00        	lda	#$0
     20b: 60           	rts

0000020c <exit>:
     20c: 20 06 02     	jsr	$206
     20f: 4c 0f 02     	jmp	$20f

Nothing changed! Is there a problem? Nope!

Since the C program doesn't actually use a stack, the linker doesn't include any code to set it up. That's the purpose of _start and _fini; these sections collect snippets of code to set things up, depending on what's actually used in the final binary. So, with this change, the target is complete for freestanding C/C++, and it even has an exit routine.'

Extending the SDK[edit | edit source]

After porting LLVM-MOS to a platform, please consider submitting your port for inclusion in the LLVM-MOS SDK. We'd like the SDK to be a nice out-of-the-box way to write code for any 6502 platform, and contributions along those lines are greatly appreciated.

To do so, continue onward to the Extending SDK guide.