Flat binaries are program executables that do not contain an executable header such as ELF or PE. Flat binaries are loaded directly into memory and executed. Boot loaders and kernels are examples of software that are created as flat binaries for loading.

This tutorial will show how to create a flat ARM binary using the Plan 9 toolchain. We based our example code from a tutorial here.

/* test.c */

#define UART0 ((void*) 0x101f1000)

typedef unsigned long u32;

#define nelem(x) (sizeof(x)/sizeof(x[0]))

void
println(char *s)
{
	volatile u32 *p = UART0;
	for (; *s; s++)
		*p = *s;
	*p = '\n';
}

/* 
 * Test that we set everything up correctly in startup.s
 * We should be able to access strings if we did
 */

static char *str1 = "str1";

char str2[] = "str2";

char *strtab[] = {
	"tab1",
	"tab2",
	"tab3",
};

void
test(void)
{
	int i;

	println("Hello from Plan 9");
	println(str1);
	println(str2);
	for (i = 0; i < nelem(strtab); i++)
		println(strtab[i]);
}
/* startup.s */

TEXT start(SB), 1, $0
	/* 
	 * 5c will generate accesses to global strings using R12 (IP) 
	 * as a lookup register, so the linker has pre-defined this 
	 * address for us that we need to set for R12 on startup
	 */
	MOVW    $setR12(SB), R12

	/* setup stack and jump to C */
	MOVW    $0x20000, SP
	BL      test(SB)

	/* loop forever doing nothing */
loop:
	B       loop

Now we can compile and link test.c and startup.s using the following commands:

# Plan 9 commands

# compile the C code into an object file
5c test.c

# assemble the assembly code into an object file
5a startup.s

# link the object files together, the obj order passed to the linker is important here,
# since the link order starts at the text address and increases orderly
# -l:       don't load in the startup linkage code and other libraries
# -H n:     6 means create a flat binary with no header with padding, alternatively 0 can be used also
# -T addr:  the addresses that the code generated will be relative to this, since qemu loads the code at 0x10000, put it relative to there
# -R pad :  align the text segment to 4096 bytes, the data section will come after this padding
5l -o plan9.bin -l -H6 -T0x10000 -R4096 startup.5 test.5

# generate a standard a.out format so we can disassemble it to verify correctness
5l -o plan9.out -l -T0x10000 -R4096 startup.5 test.5

Now that we have generated the plan9.bin binary, the next step is to check if it is valid assembly. Since Plan 9 does not have a simple command to dump the assembly, we will resort to a sleazy hack to get the disassembly. We use aout2elf to convert it to an ELF binary for objdump to understand.

# Unix commands

$ aout2elf plan9.out
$ arm-none-eabi-objdump -D plan9.elf 

plan9.elf:     file format elf32-littlearm


Disassembly of section .text:

00010000 <start>:
   10000:	e52de004 	push	{lr}		; (str lr, [sp, #-4]!)
   10004:	e59fc08c 	ldr	ip, [pc, #140]	; 10098 <test+0x4c>
   10008:	e3a0d802 	mov	sp, #131072	; 0x20000
   1000c:	eb00000e 	bl	1004c <test>
   10010:	eafffffe 	b	10010 <start+0x10>

00010014 <println>:
   10014:	e52de008 	str	lr, [sp, #-8]!
   10018:	e59f507c 	ldr	r5, [pc, #124]	; 1009c <test+0x50>
   1001c:	e1a04000 	mov	r4, r0
   10020:	e1d430d0 	ldrsb	r3, [r4]
   10024:	e3530000 	cmp	r3, #0
   10028:	0a000004 	beq	10040 <println+0x2c>
   1002c:	e0d420d1 	ldrsb	r2, [r4], #1
   10030:	e5852000 	str	r2, [r5]
   10034:	e1d430d0 	ldrsb	r3, [r4]
   10038:	e3530000 	cmp	r3, #0
   1003c:	1afffffa 	bne	1002c <println+0x18>
   10040:	e3a0100a 	mov	r1, #10
   10044:	e5851000 	str	r1, [r5]
   10048:	e49df008 	ldr	pc, [sp], #8

0001004c <test>:
   1004c:	e52de00c 	str	lr, [sp, #-12]!
   10050:	e59f0048 	ldr	r0, [pc, #72]	; 100a0 <test+0x54>
   10054:	ebffffee 	bl	10014 <println>
   10058:	e51c0fe8 	ldr	r0, [ip, #-4072]	; 0xfffff018
   1005c:	ebffffec 	bl	10014 <println>
   10060:	e59f003c 	ldr	r0, [pc, #60]	; 100a4 <test+0x58>
   10064:	ebffffea 	bl	10014 <println>
   10068:	e3a04000 	mov	r4, #0
   1006c:	e3540003 	cmp	r4, #3
   10070:	aa000006 	bge	10090 <test+0x44>
   10074:	e58d4008 	str	r4, [sp, #8]
   10078:	e59f2028 	ldr	r2, [pc, #40]	; 100a8 <test+0x5c>
   1007c:	e7920104 	ldr	r0, [r2, r4, lsl #2]
   10080:	ebffffe3 	bl	10014 <println>
   10084:	e59d4008 	ldr	r4, [sp, #8]
   10088:	e2844001 	add	r4, r4, #1
   1008c:	eafffff6 	b	1006c <test+0x20>
   10090:	e49df00c 	ldr	pc, [sp], #12
   10094:	eafffffe 	b	10094 <test+0x48>
   10098:	00011ffc 	strdeq	r1, [r1], -ip
   1009c:	101f1000 	andsne	r1, pc, r0
   100a0:	0001102c 	andeq	r1, r1, ip, lsr #32
   100a4:	0001100c 	andeq	r1, r1, ip
   100a8:	00011000 	andeq	r1, r1, r0
   100ac:	00000000 	andeq	r0, r0, r0

Disassembly of section .data:

00011000 <bdata>:
   11000:	0001101d 	andeq	r1, r1, sp, lsl r0
   11004:	00011022 	andeq	r1, r1, r2, lsr #32
   11008:	00011027 	andeq	r1, r1, r7, lsr #32

0001100c <str2>:
   1100c:	32727473 	rsbscc	r7, r2, #1929379840	; 0x73000000
   11010:	00000000 	andeq	r0, r0, r0

00011014 <str1>:
   11014:	00011018 	andeq	r1, r1, r8, lsl r0

00011018 <.string>:
   11018:	31727473 	cmncc	r2, r3, ror r4
   1101c:	62617400 	rsbvs	r7, r1, #0, 8
   11020:	61740031 	cmnvs	r4, r1, lsr r0
   11024:	74003262 	strvc	r3, [r0], #-610	; 0xfffffd9e
   11028:	00336261 	eorseq	r6, r3, r1, ror #4
   1102c:	6c6c6548 	cfstr64vs	mvdx6, [ip], #-288	; 0xfffffee0
   11030:	7266206f 	rsbvc	r2, r6, #111	; 0x6f
   11034:	50206d6f 	eorpl	r6, r0, pc, ror #26
   11038:	206e616c 	rsbcs	r6, lr, ip, ror #2
   1103c:	00000039 	andeq	r0, r0, r9, lsr r0

Everything seems correct, so we run it through qemu and we should see the expected output.

# Unix commands
$ qemu-system-arm -M versatilepb -m 128M -nographic -kernel plan9.bin
Hello from Plan 9
str1
str2
tab1
tab2
tab3
# 'Ctrl-A' and then 'x' to exit
QEMU: Terminated