Getting Rust on the GBA 1
I've seen a bunch of excellent posts about kernels recently, which has inspired me to give it a try too. I've decided on trying to get Rust running on the Game Boy Advance. For an introduction to GBA programming, I really cannot recommend [TONC][TONC] any higher.
Lets take a look at what happens when we compile a C program for the GBA. I'm using devkitARM as my cross-compiler and toolchain. I've set $DEVKITARM
to point into the root of it, and will refer to things in it that way.
And I'm just going to be compiling the first example from TONC. This is an intensionally silly example, to flaunt how low level GBA programming is.
// First demo. You are not expected to understand it
// (don't spend too much time trying and read on).
// But if you do understand (as a newbie): wow!
int main()
{unsigned int*)0x04000000 = 0x0403;
*(
unsigned short*)0x06000000)[120+80*240] = 0x001F;
((unsigned short*)0x06000000)[136+80*240] = 0x03E0;
((unsigned short*)0x06000000)[120+96*240] = 0x7C00;
((
while(1);
return 0;
}
This just puts 3 coloured dots on the screen. It'll be easy to port to rust, since it really doesn't depend on anything, which is why I chose to use it.
Lets look at what commands get run when I make
the project.
arm-none-eabi-gcc -c first.c -mthumb-interwork -mthumb -O2 -Wall \
-fno-strict-aliasing -o first.o
arm-none-eabi-gcc first.o -mthumb-interwork -mthumb -specs=gba.specs -o first.elf
arm-none-eabi-objcopy -v -O binary first.elf first.gba
copy from `first.elf' [elf32-littlearm] to `first.gba' [binary]
gbafix first.gba
ROM fixed!
This produces first.gba
, which runs nicely on my emulator.
It's using the cross compiled gcc toolchain living in $DEVKITARM/bin
. That's also where the gbafix
tool lives.
Lets understand what all these flags are doing.
-mthumb-interwork -mthumb
is telling gcc to use the 16 bit Thumb intstructions as well as arm instructions. (ARM chips have both 16 and 32 bit instructions, with different trade offs)-O2 -Wall
are just turning on optimizations and warnings.-fno-strict-aliasing
is a little confusing to me, but I think we need to tell the compiler that we might pull pointers to things out of nowhere, and not to assume otherwise. If anyone else knows what this does, I'd be happy to hear about it.
Then, when we are linking, we have this -specs=gba.specs
flag. Lets find this gba.specs
file and see what it's doing. Ah, it's in $DEVKITARM/arm-none-eabi/lib/
. Lots of other interesting things in there too. Here's what's in the gba.specs file:
%rename link old_link
*link:
%(old_link) -T gba_cart.ld%s
*startfile:
gba_crt0%O%s crti%O%s crtbegin%O%s
So, gcc uses .specs
files to re-write some rules. I'll summarize what this means.
When we link everything together, use the flag
-T gba_cart.ld
, and search forgba_card.ld
as if it were a library.There is a
gba_cart.ld
file in the same folder asgba.specs
, which is loaded. This is a linker script, and quite important. I'll copy it to the local project directory, for easy access.startfile
is just adding some.o
files to be linked in, we'll also just copy those to the local directory, and add them ourselves. I take the source version ofgba_crt0.s
, since we might need to amke some modifications.crti.o
actually lives in$DEVKITARM/lib/gcc/arm-none-eabi/4.9.2/crti.o
.
Lets try and build without the specs
file now.
arm-none-eabi-gcc -nostdlib -mthumb-interwork -mthumb -c first.c -o first.o
arm-none-eabi-gcc -nostdlib -mthumb-interwork -mthumb -c gba_crt0.s -o gba_crt0.o
arm-none-eabi-gcc -nostdlib -mthumb-interwork -mthumb -T gba_cart.ld \
first.o gba_crt0.o crti.o -o first.elf
arm-none-eabi-objcopy -O binary first.elf first.gba
gbafix first.gba
Blarg, errors
crtbegin.o: In function `deregister_tm_clones':
crtstuff.c:(.text+0x20): undefined reference to `__TMC_END__'
crtbegin.o: In function `register_tm_clones':
crtstuff.c:(.text+0x54): undefined reference to `__TMC_END__'
Looks like there's some more symbols, we can fix this by also linking in crtn.o
and crtend.o
(Not shown, lots of time in gobjdump
and gnm
looking at what symbols are defined where).
arm-none-eabi-gcc -nostdlib -mthumb-interwork -mthumb -c first.c -o first.o
arm-none-eabi-gcc -nostdlib -mthumb-interwork -mthumb -T gba_cart.ld \
first.o gba_crt0.s crti.o crtbegin.o crtend.o crtn.o -o first.elf
arm-none-eabi-objcopy -O binary first.elf first.gba
gbafix first.gba
(.init+0x200): undefined reference to `fake_heap_end'
Ah no, I must have missed something. Poking around with ag
and gobjdump
leads me to find that
$ ag --all-types fake
...
Binary file libsysbase.a matches.
...
$ gnm --target=elf32-littlearm libsysbase.a
...
sbrk.o:
U __end__
00000000 T _sbrk_r
U fake_heap_end
U fake_heap_start
00000000 b heap_start.5558
...
Well, I'm not going to using system memory anyways, or malloc, so I'll just comment out that part of gba_crt0.s
and see if it works.
It does. Nice.
These crt*
files are the C Runtime. They handle things such as initializing global variables and calling main
. The gbacrt0.s
has some GBA specific code, for example it has the Nintendo header at the start:
section ".init"
.
.global _startalign
.
.arm
@----------------------------------------------------------------------------_start:
@----------------------------------------------------------------------------
b rom_header_end
156,1,0 @ Nintendo Logo Character Data (8000004h)
.fill 16,1,0 @ Game Title
.fill byte 0x30,0x31 @ Maker Code (80000B0h)
.byte 0x96 @ Fixed Value (80000B2h)
.byte 0x00 @ Main Unit Code (80000B3h)
.byte 0x00 @ Device Type (80000B4h)
.7,1,0 @ unused
.fill byte 0x00 @ Software Version No (80000BCh)
.byte 0xf0 @ Complement Check (80000BDh)
.byte 0x00,0x00 @ Checksum (80000BEh)
.
@----------------------------------------------------------------------------rom_header_end:
So at the very start of the code, we actually just skip forward over Nintendo's header. This header has 0x00,0x00
for it's checksum, which get overwritten when we run gbafix
.
We'll look at the rest of gba_crt0.s
later, right now I want to drop the extra files we don't need. Since we loop forever and don't return from main
, I want to stop using crtn
and crtend
, since we will never use them. However crtbegin
uses a symbol from them. Eh, we'll cut that too, see if it runs.
arm-none-eabi-gcc -nostdlib -mthumb-interwork -mthumb -c first.c -o first.o
arm-none-eabi-gcc -nostdlib -mthumb-interwork -mthumb -T gba_cart.ld \
first.o gba_crt0.s crti.o -o first.elf
arm-none-eabi-objcopy -O binary first.elf first.gba
gbafix first.gba
Yes, it does work!
(TODO: talk about gba_cart.ld
?)
Now, since Rust uses LLVM, I wanted to try and compile this using clang. This is actually the reason I expanded the gba.specs
file, since clang does accept it.
I'm going to skip over lots of the trial and error, and point out a few highlights.
First of all, gba_crt0.s
is using the pre-AUL instructions. This basically boils down to needing to add a bunch of s
suffixes to commands, and there's also one straight up wrong instruction
85: mov r6, r2
which requires ARMv6
or latter. Now, if you were past-me, you might think "Oh, but the GBA's chip is called ARM7, and 7 > 6, so there's no problem, right?". Wrong! Confusingly, the ARM7 chip supports ARMv4
instructions. That 7 is totally unrelated.
This took me going to #llvm
on irc to figure out.
I have decided for now to keep compiling gba_crt0.s
with arm-none-eabi-gcc
, and not worry about fixing everything up so clang likes it for now, since clang still depends on arm-none-eabi-gcc
for the linking step anyways.
Here's the script I ended up with for compiling things using clang.
#!/bin/bash -ex
# -ex will stop on the first error, and print every command
# as it gets run.
clang -nostdlib -target arm-none-eabi -mcpu=arm7tdmi -c first.c -o first.clang.o
# Clang is a cross compiler, so we don't need to rebuild it from source.
# We do need to pass it the target triple (arm-none-eabi), and what cpu
# we are using.
# Also, since we aren't using libc, we pass -nostdlib
arm-none-eabi-gcc -nostdlib -mthumb-interwork -mthumb \
-c gba_crt0.s -o gba_crt0.o
# Compile gba_crt0.s using gcc still.
clang -nostdlib -target arm-none-eabi -Tgba_cart.ld \
first.clang.o gba_crt0.o crti.o -o first.clang.elf
# Call clang to link everything. Actually, clang will just call out to
# arm-none-eabi-gcc to link everything properly. It needs to be in the path.
arm-none-eabi-objcopy -O binary first.clang.elf first.clang.gba
gbafix first.clang.gba
For comparison, here is the final gcc script:
#!/bin/bash -ex
arm-none-eabi-gcc -nostdlib -mthumb-interwork -mthumb -c first.c -o first.gcc.o
arm-none-eabi-gcc -nostdlib -mthumb-interwork -mthumb -c gba_crt0.s -o gba_crt0.o
arm-none-eabi-gcc -nostdlib -mthumb-interwork -mthumb -T gba_cart.ld \
first.gcc.o gba_crt0.o crti.o -o first.gcc.elf
arm-none-eabi-objcopy -O binary first.gcc.elf first.gcc.gba
gbafix first.gcc.gba
That took a few days to get correct.
Lets take a break for now, and I'll cover using Rust code in the next post. See you next time.