Although the story is about a CPU that's older than many readers, the steps are still relevant to anybody wanting to bring up a GNU gcc toolchain on a fresh MCU architecture. Sometimes the ld script directives are issued as command line arguments, for example if you peer behind the scenes at the linker step in the command line history on the Arduino IDE.
OUTPUT_FORMAT(elf32-i386)
MEMORY {
ram (!rx) : o = 0x0, l = 32k
rom (rx) : o = 0x8000, l = 32k
}
REGION_ALIAS("region_ivt", ram);
REGION_ALIAS("region_data", ram);
REGION_ALIAS("region_bss", ram);
REGION_ALIAS("region_text", rom);
REGION_ALIAS("region_rodata", rom);
REGION_ALIAS("region_bootvec", rom);
SECTIONS {
.text : {
_rom_begin = .;
crt0.o(.text*) *(.text*)
} >region_text =0xFF
.rodata : {
*(.rodata)
rodata_end = .;
} >region_rodata
.bootvec 0xFFF0 : {
crt0.o(.bootvec)
FILL(0xFF)
. = ALIGN(16);
} >region_bootvec
.ivt 0x0000 : {
KEEP(*(.ivt))
. = . + 0x400;
} >region_ivt
.data : AT(rodata_end) {
data_start = .;
*(.data)
data_end = .;
} >region_data
data_load_start = LOADADDR(.data);
.bss : {
bss_begin = .;
*(.bss)
*(COMMON)
bss_end = .;
} >region_bss
/DISCARD/ : {
*(.comment)
}
}
Right, so I thought I'd scare you off with a bit of GNU ld scripting.
I found myself a little indisposed this week so I thought I'd turn to a software project for a change. One of my round tuits is to make an SBC for the 8088s I have lying around. Not to run DOS or play games but as a general purpose MCU board. It's a bit both exhilarating and depressing how it's actually cheaper to buy a modern MCU dev board that can outperform the heart of a PC or XT computer, and even emulate at the same or higher speed. And no need for peripheral chips either. Still I can't stand seeing those chips idle so some time back I got TK Chia's ia-16 gcc for the 16-bit x86 CPUs working in a couple of ways: converting a Ubuntu DEB package to RPM, and building a Flatpak.
But to run bare silicon embedded programs on an SBC, we need a C runtime startup routine, written in assembler and usually called crt0, as well as a linker script to put all the program regions in the right memory areas.
First off the linker script. My SBC design has 32 kB RAM at the bottom and 32 kB ROM at the top of the 16-bit address space. (No segments, 64 kB should be enough for me 😉.) ROM is at the top because of the boot vector.
The order of the ROM segments from 0x8000 onwards is: code, read-only data (initialised constant data), initialisation data for writable data, e.g. the initialiser for char string[10] = "0123456789" which has to be copied from ROM to RAM, then lots of unused ROM bits until the boot vector at 0xFFF0 at the top.
The order of the RAM segments is: interrupt vectors up to 0x400 per 8086 architecture, writable data as above, BSS area which has to be zeroed before main(), the heap growing upwards from there, and the stack growing downwards from 0x7FFF.
You can actually ask AI to explain the ld script to you. The explanation was very good and it was a useful check that I had grokked ld script language.
As for the startup routine, here it is:
// Startup code
.code16
.text
.global _rom_begin
_rom_begin:
cli
// Setup segment registers in tiny memory model
xor %ax,%ax
mov %ax,%ds
mov %ax,%es
// Stack at end of RAM
mov %ax,%ss
mov $0x7fff,%sp
// Copy .data from ROM to RAM (writable initialised data)
// Const initialised data is in .rodata
mov $data_load_start,%si
mov $data_start,%di
mov $data_end,%cx
sub %di,%cx
cld
rep movsb
// Zero BSS for zero or uninitialised global variables
mov $bss_begin,%di
mov $bss_end,%cx
sub %di,%cx
xor %al,%al
rep stosb
sti
// Main C function
call main
// If main() returns, halt
1: hlt
jmp 1b
.section .bootvec,"axT"
.intel_syntax
jmp 0x0000:_rom_begin
It follows the specification of the ld script.
Finally a Makefile that builds a test program. Notice the use of srec_cat from srecord to get the top 32 kB for the ROM image.
CC = ia16-elf-gcc
# ES only for segment please!
CFLAGS = -ffreestanding -mcmodel=tiny -mprotected-mode
AS = ia16-elf-as
LD = ia16-elf-ld
LDFLAGS = -T sbc.ld
OBJCOPY = ia16-elf-objcopy
OBJDUMP = ia16-elf-objdump
ELF = hello.elf
HEX = hello.hex
ROM = hello.rom
OBJS= \
crt0.o \
arch.o \
hello.o \
# end of list
.PHONY : all clean
all: $(ROM)
$(ELF): $(OBJS) sbc.ld
$(LD) $(LDFLAGS) -M -o $(ELF) $(OBJS) > hello.map
$(HEX): $(ELF)
$(OBJCOPY) -O ihex $(ELF) $(HEX)
$(ROM): $(HEX)
srec_cat $(HEX) -intel -offset -0x8000 -output $(ROM) -binary
%.s: %.c
$(CC) -S $(CFLAGS) $<
objdump:
$(OBJDUMP) -m i8086 -d $(ELF)
clean:
rm -f $(OBJS)
Ok, I've managed to find a suitable 8086 emulator (so many cannot load a raw memory image), and simulated the processor starting at 0xFFF0:0000, doing a far jump to 0x0000:_rom_begin, copying .data to RAM, clearing BSS, through to calling main, returning, then entering the halt loop. This gives me confidence that a bare silicon ROM will run on the SBC.
Ken Yap
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.