Basics
Let's program some stuff to run on the GBA.
Basic Compilation
As usual with any new Rust project we'll need a Cargo.toml
file:
# Cargo.toml
[package]
name = "gba_from_scratch"
version = "0.1.0"
edition = "2021"
And we want some sort of program to run so let's make an example called ex1.rs
in the examples/
directory.
It can just be a classic "Hello, World" type program to start.
// examples/ex1.rs fn main() { println!("hello"); }
Since we're not running the compiler on the GBA itself, then we'll need to "cross-compile" our program. It's called "cross compilation" when you build a program for some system other than the system that you're running the compiler on. The system running the compiler is called the "host" system, and the system you're building for is called the "target" system. In our case, the host system can be basically anything that can run a Rust toolchain. I've had success on Windows, Linux, and Mac, there's no big difficulties.
To do a cross compile, we pass --target to cargo
.
If we look up the Game Boy Advance on wikipedia, we can see that it has an ARM7TDMI CPU.
The "ARM7T" part means that it uses the "ARMv4T" CPU architecture.
Now we go the Platform Support page and use "ctrl+F" to look for "ARMv4T".
We can see three(-ish) entries that might(?) be what we want.
armv4t-none-eabi
armv4t-unknown-linux-gnueabi
thumbv4t-none-eabi
This is the part where my "teach like you're telling a story" style breaks down a bit.
What should happen next is that we pick the thumbv4t-none-eabi
target.
Except there's not an easy to find document that tells you this step that I can just link to and have you read a few lines.
The shortest version of the full explanation is something like "Many ARM CPUs support two code 'states', and one of them is called 'thumb', and that's the better default on the GBA."
We can certainly talk more about that later, but for now you just gotta go with it.
Let's see what happens when we pass --target thumbv4t-none-eabi
as part of a call to cargo
:
>cargo build --example ex1 --target thumbv4t-none-eabi
Compiling gba_from_scratch v0.1.0 (D:\dev\gba-from-scratch)
error[E0463]: can't find crate for `std`
|
= note: the `thumbv4t-none-eabi` target may not be installed
= help: consider downloading the target with `rustup target add thumbv4t-none-eabi`
= help: consider building the standard library from source with `cargo build -Zbuild-std`
error: requires `sized` lang_item
For more information about this error, try `rustc --explain E0463`.
error: could not compile `gba_from_scratch` (lib) due to 2 previous errors
Well we seem to have already configured something wrong, somehow. The trouble with a wrong project configuration is that the compiler can't always guess what you meant to do. This means that the error message suggestions might be helpful, but they also might lead you down the wrong path.
One suggested way to fix the problem is to add the thumbv4t-none-eabi
target with rustup
.
It seems pretty low risk to just try installing that, so let's see.
>rustup target add thumbv4t-none-eabi
error: toolchain 'nightly-x86_64-pc-windows-msvc' does not contain component 'rust-std' for target 'thumbv4t-none-eabi'; did you mean 'thumbv6m-none-eabi'?
note: not all platforms have the standard library pre-compiled: https://doc.rust-lang.org/nightly/rustc/platform-support.html
help: consider using `cargo build -Z build-std` instead
Ah, dang.
If we double check the Platform Support page we might see that thumbv4t-none-eabi
is in the "Tier 3" section.
Tier 3 targets don't have a standard library available in rustup
.
How about this build-std
thing?
The -Z
flags are all unstable flags, so we can check the unstable section of the cargo manual.
Looks like build-std lets us build our own standard library.
We're going to need Nightly rust, so set that up how you want if you need to.
You can use rustup default nightly
(which sets the system global default), or you can use a toolchain file if you want to use Nightly on just this one project.
Once we've set for Nightly use, we need to get the rust-src
component from rustup
too.
rustup default nightly
rustup component add rust-src
Okay let's try again
> cargo build --example ex1 --target thumbv4t-none-eabi -Z build-std
Compiling compiler_builtins v0.1.89
Compiling core v0.0.0 (/Users/dg/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/core)
Compiling libc v0.2.140
Compiling cc v1.0.77
Compiling memchr v2.5.0
Compiling std v0.0.0 (/Users/dg/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/std)
Compiling unwind v0.0.0 (/Users/dg/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/unwind)
Compiling rustc-std-workspace-core v1.99.0 (/Users/dg/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/rustc-std-workspace-core)
Compiling alloc v0.0.0 (/Users/dg/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/alloc)
Compiling cfg-if v1.0.0
Compiling adler v1.0.2
Compiling rustc-demangle v0.1.21
Compiling rustc-std-workspace-alloc v1.99.0 (/Users/dg/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/rustc-std-workspace-alloc)
Compiling panic_abort v0.0.0 (/Users/dg/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/panic_abort)
Compiling panic_unwind v0.0.0 (/Users/dg/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/panic_unwind)
Compiling gimli v0.26.2
Compiling miniz_oxide v0.5.3
Compiling hashbrown v0.12.3
Compiling object v0.29.0
Compiling std_detect v0.1.5 (/Users/dg/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/stdarch/crates/std_detect)
error[E0432]: unresolved import `alloc::sync`
--> /Users/dg/.cargo/registry/src/index.crates.io-6f17d22bba15001f/gimli-0.26.2/src/read/dwarf.rs:2:12
|
2 | use alloc::sync::Arc;
| ^^^^ could not find `sync` in `alloc`
For more information about this error, try `rustc --explain E0432`.
error: could not compile `gimli` (lib) due to previous error
warning: build failed, waiting for other jobs to finish...
Whoa... that's way too much. We didn't mean for all of that to happen. Let's check that cargo manual again. Ah, it says we need to pass an argument to our command line argument if we don't want as much stuff to be build
> cargo build --example ex1 --target thumbv4t-none-eabi -Z build-std=core
Compiling gba_from_scratch v0.1.0 (/Users/dg/gba-from-scratch)
error[E0463]: can't find crate for `std`
|
= note: the `thumbv4t-none-eabi` target may not support the standard library
= note: `std` is required by `gba_from_scratch` because it does not declare `#![no_std]`
= help: consider building the standard library from source with `cargo build -Zbuild-std`
For more information about this error, try `rustc --explain E0463`.
error: could not compile `gba_from_scratch` (lib) due to previous error
That's different from before at least.
Well, we told to to only build core
and not std
, and then it said we couldn't use std
.
Makes sense.
Lets change the example.
// ex1.rs
#![no_std]
fn main() {
println!("hello");
}
And we need to fix our lib.rs
to also be no_std
.
It doesn't do anything else for now, it's just blank beyond being no_std.
#![allow(unused)] fn main() { // lib.rs #![no_std] }
Now rust-analyzer is telling me we can't use println in our example.
Also, we're missing a #[panic_handler]
.
Here's the error.
> cargo build --example ex1 --target thumbv4t-none-eabi -Z build-std=core
Compiling gba_from_scratch v0.1.0 (/Users/dg/gba-from-scratch)
error: cannot find macro `println` in this scope
--> examples/ex1.rs:4:3
|
4 | println!("hello");
| ^^^^^^^
error: `#[panic_handler]` function required, but not found
error: could not compile `gba_from_scratch` (example "ex1") due to 2 previous errors
Well, we can comment out the println!
.
For the panic handler, we go to the Attributes part of the rust reference.
That links us to panic_handler, which sets what function gets called in event of panic.
// ex1.rs #![no_std] fn main() { // } #[panic_handler] fn panic_handler(_: &core::panic::PanicInfo) -> ! { loop {} }
Now we get a new, different error when we try to build:
> cargo build --example ex1 --target thumbv4t-none-eabi -Z build-std=core
Compiling gba_from_scratch v0.1.0 (/Users/dg/gba-from-scratch)
error: requires `start` lang_item
error: could not compile `gba_from_scratch` (example "ex1") due to previous error
Alright so what's this start
lang item deal?
Well it has to do with the operating system being able to run your executable.
The details aren't important for us, because there's no operating system on the GBA.
Instead of trying to work with the start
thing, we'll declare our program as #![no_main]
.
This prevents the compiler from automatically generating the main
entry fn, which is what's looking to call that start fn.
Note that this generated main
fn is separate from the main
fn that we normally think of as being the start of the program.
Because, as always, programmers are very good at naming things.
// ex1.rs #![no_std] #![no_main] fn main() { // } #[panic_handler] fn panic_handler(_: &core::panic::PanicInfo) -> ! { loop {} }
Okay let's try another build.
> cargo build --example ex1 --target thumbv4t-none-eabi -Z build-std=core
Compiling gba_from_scratch v0.1.0 (/Users/dg/gba-from-scratch)
warning: function `main` is never used
--> examples/ex1.rs:4:4
|
4 | fn main() {
| ^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: `gba_from_scratch` (example "ex1") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.64s
Okay. It builds.
Using mGBA
Let's see if it works I guess.
Personally I like to use mGBA as my emulator of choice, but any GBA emulator should be fine.
If you're on Windows then your executable will be called mgba.exe
by default, and if you're on Mac or Linux you'll get both mgba
(no UI) and mgba-qt
(has a menu bar and such around the video frame).
On my Windows machine I just made a copy of mgba.exe
that's called mgba-qt.exe
so that both names work on all of my devices.
> mgba target/thumbv4t-none-eabi/debug/examples/ex1
The emulator starts and then... shows a dialog box. "An error occurred." says the box's title bar. "Could not load game. Are you sure it's in the correct format?" Well, sorry mgba, but we're not sure it's in the correct format. In fact, we're pretty sure it's not the correct format right now. I guess we'll have to inspect the compilation output.
ARM Binutils
If we go to ARM's developer website we can fine the ARM Toolchain Downloads page.
This lets us download the tools for working with executables for the arm-none-eabi
family of targets.
This includes our thumbv4t
program, as well as other variants of ARM code.
You can get it from their website, or if you're on a Linux you can probably get it from your package manager.
The binutils package for a target family has many individual tools.
The ones we'll be using will all be named arm-none-eabi-
to start, to distinguish them from the same tool for other targets.
So if we want to use "objdump" we call it with arm-none-eabi-objdump
and so on.
That's exactly what we want to use right now.
We pass the name of the compiled executable, and then whichever other options we want.
For now let's look at the --section-headers
> arm-none-eabi-objdump target/thumbv4t-none-eabi/debug/examples/ex1 --section-headers
target/thumbv4t-none-eabi/debug/examples/ex1: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .debug_abbrev 000000f4 00000000 00000000 00000094 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
1 .debug_info 000005a6 00000000 00000000 00000188 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
2 .debug_aranges 00000020 00000000 00000000 0000072e 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
3 .debug_str 00000495 00000000 00000000 0000074e 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
4 .debug_pubnames 000000c0 00000000 00000000 00000be3 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
5 .debug_pubtypes 00000364 00000000 00000000 00000ca3 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
6 .ARM.attributes 00000030 00000000 00000000 00001007 2**0
CONTENTS, READONLY
7 .debug_frame 00000028 00000000 00000000 00001038 2**2
CONTENTS, READONLY, DEBUGGING, OCTETS
8 .debug_line 00000042 00000000 00000000 00001060 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
9 .comment 00000013 00000000 00000000 000010a2 2**0
CONTENTS, READONLY
There's a few columns of note:
Size
is the number of bytes for the section.VMA
is the Virtual Memory Address. On the GBA this means the intended address when the main program is running. All of our data starts in ROM, and some of it we will copy into RAM just after boot. When a section is intended to be copied into RAM, it will have a VMA separate from the LMA.LMA
is the Logical Memory Address. On the GBA this means the address in ROM.
Which means... according to the chart... none of this data would end up in the ROM? I guess that means that, if we extracted our raw program from the ELF container file that the compiler uses, we would end up with a totally blank ROM. That certainly doesn't sound like what mgba would call the "correct format".
Linker Scripts
What's wrong is that we need to adjust the linker script.
That link goes to the documentation for the binutils linker (called ld
), and technically we're actually using the linker that ships with the compiler (called rust-lld
).
rust-lld
is the Rust version of lld
, which is LLVM's linker that's intended to be a "drop in" replacement for GNU's ld
.
Both linkers use a linker script system, and they both even use the same linker script format.
I tried to find an in depth manual for lld
specifically, but all I could find was the top level "man page" explanations.
Referring to the the GNU ld
manual will have to do.
You don't have to read the whole manual, the short story goes like this: linkers take one or more "object" files and "link" them into a single "executable" file. The linker script is what guides the linker in exactly what to do. If you don't say what script to use then the linker will use a default linker script that it keeps wherever. When the target is a "normal" target like Windows or Mac then using a default linker script is just fine. When the target is something a little more esoteric, like most embedded devices, including the GBA, then the default won't be good enough. We'll have to write our own script and make the linker use that.
One complexity here is that the linker script to use is an argument passed to the linker.
And the way you pass args to the linker is that you tell rustc
to do it.
Except with cargo build
there's no way to tell rustc
an extra argument.
We could use cargo rustc
, but it's a pain to have to remember an alternate command.
As much as possible we'd like cargo build
to work.
We could use a build.rs
file to pass an arg to the linker, but making a build script just to pass one argument seems like maybe overkill.
Probably we should just set it as part of our the RUSTFLAGS
environment variable.
The catch with RUSTFLAGS
is that any time you change it you have to build the entire crate graph again.
We want to "write it down" (so to speak) and have it automatically be the same every time.
This can be done with a cargo configuration file.
First let's make a blank normal_boot.ld
file in a linker_scripts/
folder.
Then in the .cargo
folder we fill in config.toml
# .cargo/config.toml
[target.thumbv4t-none-eabi]
rustflags = ["-Clink-arg=-Tlinker_scripts/normal_boot.ld"]
while we're at it, we can even set a default target (which is used when we don't specify --target
, and we can configure for build-std
to be automatically be used, all in the same file.
# .cargo/config.toml
[unstable]
build-std = ["core"]
[build]
target = "thumbv4t-none-eabi"
[target.thumbv4t-none-eabi]
rustflags = ["-Clink-arg=-Tlinker_scripts/normal_boot.ld"]
Great, let's try it out
> cargo build --example ex1
warning: function `main` is never used
--> examples\ex1.rs:4:4
|
4 | fn main() {
| ^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: `gba_from_scratch` (example "ex1") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.10s
Cool. It's a lot less to type, and we're ready to fill in our linker script.
Our linker script is called normal_boot.ld
because there's two ways for the GBA to boot up.
One of them is the "normal" style with a program running off of the game pak.
The other is "multiboot" where the GBA can download a program over the link cable.
Since we might want to do multiboot some day, we might as well give our linker script a specific name to start with.
Once things are set up we won't really have to think about it on a regular basis, so it's fine.
There's three things we'll have to concern ourselves with:
- The entry point
- The memory locations
- The sections
Picking an entry point is easy, it's just the name of a symbol.
The traditional entry point name is just _start
, so we'll go with that.
ENTRY(_start)
Having an entry point set doesn't really matter for running the program on actual GBA hardware. Still when the entry point ends up at one of the usual address values, it helps the heuristic system mgba uses to determine if it should run our program as a normal game or a multiboot game, so it's not entirely useless.
Which brings us to the memory portion.
The GBA has three main chunks of memory: Read-Only Memory (ROM), Internal Work RAM (IWRAM), and External Work RAM (EWRAM). We can cover more of the fine differences later, for now it's enough to write them down into our linker script. For each one we have to specify the base address and the size in bytes.
MEMORY {
ewram (w!x) : ORIGIN = 0x2000000, LENGTH = 256K
iwram (w!x) : ORIGIN = 0x3000000, LENGTH = 32K
rom (rx) : ORIGIN = 0x8000000, LENGTH = 32M
}
Finally, we have to tell the linker which output section to assign all of the input sections it finds. This uses a glob-matching sort of system. We specify an output section that we want to have created, and then in the braces for it we list matchers that are checked against each input section the linker sees. When an input section fits one of the matchers, it goes with that output section.
Program code is supposed to end up in the .text
section, so we can start with just that.
SECTIONS {
.text : {
*(.text .text.*);
} >rom
}
Here we've got one matcher listed, *(.text .text.*);
.
The *
at the start means it applies to any input file.
We could limit what files it applies to, if we wanted, but generally we shouldn't.
Inside the parenthesis is a space separated list of globs.
We've got two: .text
and .text.*
.
The first is for the exact match .text
, and the second is for anything that starts with .text.
.
The convention for section names is to start with a .
, and they can't have spaces.
Rust will default to having every function in its own section, all with the prefix .text.
.
Unused code can only be removed one entire input section at a time, so having every function in a distinct input section keeps our output as small as possible.
The >rom
part after tha braces allocates the entire output section into the rom
memory that we declared before.
All together, we've got this:
/* normal_boot.ld */
/* THIS LINKER SCRIPT FILE IS RELEASED TO THE PUBLIC DOMAIN (SPDX: CC0-1.0) */
ENTRY(_start)
MEMORY {
ewram (w!x) : ORIGIN = 0x2000000, LENGTH = 256K
iwram (w!x) : ORIGIN = 0x3000000, LENGTH = 32K
rom (rx) : ORIGIN = 0x8000000, LENGTH = 32M
}
SECTIONS {
.text : {
*(.text._start);
*(.text .text.*);
} >rom
}
This isn't a complete and "final" linker script, but for now it's enough to let us proceed.
If we rebuild the program right now we still won't get anything in the output .text
section.
Remember that dead code warning we keep getting on our main
function?
Nothing in our program ever calls main
, and it's not public for outsiders to call, so it gets discarded during linking.
Since no code can call main
then no code can panic either, and the panic_handler
function gets removed as well.
We end up with nothing at all.
Writing A _start
We need to add some code to our progam so that there will be something to output.
Might as well define the _start
function.
_start
doesn't work like a normal function.
The way the very start of the GBA's ROM works is special.
When the GBA first boots the BIOS (which is part of the GBA itself, not part of our ROM) takes control.
It and plays the boot animation and sound that you're probably familiar with, then does a checksum on our ROM's header data.
If the checksum passes the BIOS jumps control to 0x0800_0000
(the start of ROM).
That's where our _start
will be.
The first instruction can be "anything" but immediateley after that is the rest of the header data.
That means that in practice the very first instruction of _start
has to be a jump past the rest of the header data, since the header data isn't executable code.
Sticking non-executable data into the middle of a function isn't something that the compiler is really capable of dealing with, so we'll have to take direct control of the situation.
We could do this using either global_assembly! or a #[naked] function.
One might think that we should pick the Stable option (global assembly), over the Nightly option (a naked function).
However, naked functions are basically much easier to work with.
Since using build-std
means that we have to use Nightly anyway, it's not that bad to also use naked functions as well.
If naked functions were the very last thing that required us to use Nightly we could move to global assembly instead.
At the top of ex1.rs
we need to add #![feature(naked_functions)]
.
Then we add our _start
function.
In addition to marking it as #[naked]
, we also mark it #[no_mangle]
.
We need to use #[instruction_set(arm::a32)]
as well.
This is part of that arm/thumb thing from before.
Because the BIOS jumps to the start of the ROM with the CPU in a32 mode, our function must be encoded appropriately.
Since _start
has got to specifically at the very start of the ROM we'll use #[link_section = ".text._start"]
to assign our function a specific section name we can use in our linker script.
Since _start
is going to be "called" by the outside world we have to assign it the extern "C"
ABI.
Since it should never return we will mark the return type as -> !
.
So far it all looks like this:
#![allow(unused)] fn main() { // ex1.rs #[naked] #[no_mangle] #[instruction_set(arm::a32)] #[link_section = ".text._start"] unsafe extern "C" fn _start() -> ! { todo!() } }
Inside of the _start
function, because it's a naked function, we must put an asm!
block as the only statement.
Our assembly will be very simple for now.
Let's look at it on its own.
b 1f
.space 0xE0
1:
b 1b
In the first line we branch (b
) to the label 1
that is "forward" from the instruction (1f
).
Then with .space
we put 0xE0 blank bytes.
This is called a "directive", it doesn't emit an instruction directly, instead it tells the assembler to do a special action.
We can tell it's a directive because it has a .
at the beginning.
The blank space is where the header data can go when we need to fill it in.
mgba doesn't check the header, so during development it's fine to leave the header blank.
We can always fix the header data after compilation using a special tool called gbafix
when we need to.
The 1:
is a label.
We know it's a label because it ends with :
.
Unlike with function names, a label can be just a number.
In fact, it's preferred to only use numberic labels whenever possible.
When a non-numeric label is defined more than once it causes problems (that's why function names are mangled by default, and we had to use no_mangle
).
When a numeric label is defined more than once, all instances of that label can co-exist just fine.
When you jump to a numbered label (forward or back), it just jumps to the closest instance of that number (in whichever direction).
Note that a label can have something else on the same line following the :
.
Usually a label will be on a line of its own so that it stands out a little more in the code, but that's just a code style thing.
Something can follow a label on the same line as well.
If a label is on a line of its own, the label "points to" the next line that has a non-label thing on it.
You can also have more than one label point at the same line, if necessary.
Finally, our second actual instruction is that we want to branch backward to the label 1
.
Since that 1
label points at the branch itself, this instruction causes an infinite loop.
The same as if we'd written loop {}
in rust.
At the end of our assembly we have to put options(noreturn)
.
That's just part of how #[naked]
functions work.
So when we put it all together we get this:
#![allow(unused)] fn main() { // ex1.rs #[naked] #[no_mangle] #[instruction_set(arm::a32)] #[link_section = ".text._start"] unsafe extern "C" fn _start() -> ! { core::arch::asm! { "b 1f", ".space 0xE0", "1:", "b 1b", options(noreturn) } } }
And we also want to adjust the linker script.
Since _start
is now in .text._start
, we'll put a special matcher for that to make sure it stays at the start of the ROM, no matter what order the linker sees our files in.
/* normal_boot.ld */
SECTIONS {
.text : {
*(.text._start);
*(.text .text.*);
} >rom
}
And after all of this, we can build our example and see that something shows up in the .text
section of the executable.
> cargo build --example ex1 && arm-none-eabi-objdump target/thumbv4t-none-eabi/debug/examples/ex1 --section-headers
Compiling core v0.0.0 (C:\Users\Daniel\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\core)
Compiling rustc-std-workspace-core v1.99.0 (C:\Users\Daniel\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\rustc-std-workspace-core)
Compiling compiler_builtins v0.1.89
Compiling gba_from_scratch v0.1.0 (D:\dev\gba-from-scratch)
Finished dev [unoptimized + debuginfo] target(s) in 9.98s
target/thumbv4t-none-eabi/debug/examples/ex1: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 000000e6 08000000 08000000 00010000 2**1
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .ARM.exidx 00000010 080000e8 080000e8 000100e8 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .debug_abbrev 0000010a 00000000 00000000 000100f8 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
3 .debug_info 000005b7 00000000 00000000 00010202 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
4 .debug_aranges 00000028 00000000 00000000 000107b9 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
5 .debug_ranges 00000018 00000000 00000000 000107e1 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
6 .debug_str 0000049c 00000000 00000000 000107f9 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
7 .debug_pubnames 000000cb 00000000 00000000 00010c95 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
8 .debug_pubtypes 00000364 00000000 00000000 00010d60 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
9 .ARM.attributes 00000030 00000000 00000000 000110c4 2**0
CONTENTS, READONLY
10 .debug_frame 00000038 00000000 00000000 000110f4 2**2
CONTENTS, READONLY, DEBUGGING, OCTETS
11 .debug_line 00000056 00000000 00000000 0001112c 2**0
CONTENTS, READONLY, DEBUGGING, OCTETS
12 .comment 00000013 00000000 00000000 00011182 2**0
CONTENTS, READONLY
I think we're ready to test the program.
Obviously we just use cargo run
and...
> cargo run --example ex1
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target\thumbv4t-none-eabi\debug\examples\ex1`
error: could not execute process `target\thumbv4t-none-eabi\debug\examples\ex1` (never executed)
Caused by:
%1 is not a valid Win32 application. (os error 193)
Ah, right, Windows doesn't know how to run GBA programs, of course.
Instead, let's adjust the .cargo/config.toml
to set a "runner" value in our target confituration.
When we have a runner set, cargo run
will call the runner program and pass the program we picked as the first argument.
# .cargo/config.toml
[target.thumbv4t-none-eabi]
rustflags = ["-Clink-arg=-Tlinker_scripts/normal_boot.ld"]
runner = "mgba-qt" #remove the -qt part if you're on Windows!
And so we try again
> cargo run --example ex1
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `mgba-qt target\thumbv4t-none-eabi\debug\examples\ex1`
If everything is right so far, mGBA should launch and show a white screen. Congrats, it didn't crash.
Checking With objdump
If we want to double check that our code is showing up in the executable properly we can even use objdump
to check that.
If we pass --disassemble
we can get a printout of the assembly.
There's a bunch of other options for how to configure that output too, so check the --help
output to see what you can do.
I like to use --demangle --architecture=armv4t --no-show-raw-insn -Mreg-names-std
, and you get output like this:
> arm-none-eabi-objdump target/thumbv4t-none-eabi/debug/examples/ex1 --disassemble --demangle --architecture=armv4t --no-show-raw-insn -Mreg-names-std
target/thumbv4t-none-eabi/debug/examples/ex1: file format elf32-littlearm
Disassembly of section .text:
08000000 <_start>:
8000000: b 80000e4 <_start+0xe4>
...
80000e4: b 80000e4 <_start+0xe4>
80000e8: udf #65006 ; 0xfdee
Disassembly is a tricky thing sometimes.
It's not always clear to the disassembler what is code and what's data.
Or when it should decode a32
code (4 bytes each) or t32
code (2 bytes each).
In this case, the disassembler did notice that enough bytes in a row are all zero, and it just cuts that from the output with a ...
.
That's cool, but it doesn't always work.
Every once in a while the disassembler will interpret things wrong and a chunk of the display will be nonsense.
It's kinda just how it goes, try not to worry if you see it happen.
Also, at the end of our function we can see there's an undefined instruction.
Those will happen sometimes at the end functions.
I'm unclear on why.
It doesn't seem to be for alignment, because going 4 bytes past 0x0800_00E8
to 0x0800_00EC
would make things less aligned.
Still, I guess it's not really a big deal when it happens.
We've got so much ROM space available that an occasional 2 or 4 bytes extra won't really break the bank.
Proving Our Program Is Doing Something
It's all nice and well to see a white screen, but let's finish up this section by having our program do something, anything at all, which lets us see that we're really having an effect on the GBA.
The simplest thing to do would be to make the screen turn on black instead of white. When the BIOS transfers control to our program a thing called the "forced blank" mode is active. This makes the display draw all pixels as white. If we turn off the forced blank bit we'll get a black screen instead.
All we have to do is add a few more lines of assembly to our _start
function:
#![allow(unused)] fn main() { // in `main` of ex1.rs core::arch::asm! { "b 1f", ".space 0xE0", "1:", "mov r0, #0x04000000", "mov r1, #0", "strh r1, [r0]", "2:", "b 2b", options(noreturn) } }
This part after the header data is what's new:
mov r0, #0x04000000
mov r1, #0
strh r1, [r0]
mov
will "move" a value into a register.
This shares the usual assignment syntax of Rust and most other programming languages:
the destination register is on the left,
and the source data to move into that register is on the right.
So you could think of it being similar to
#![allow(unused)] fn main() { let r0 = 0x04000000; }
The #
means that the value is an "immediate" value.
It gets encoded into the instruction itself, so it doesn't have to "come from" anywhere else.
With LLVM's assembler it seems like actually putting the #
before an immediate value is optional (that is: the program will compile the same without it),
but on some assemblers putting the #
is required, so I'll be putting it in the tutorial code.
After we move values into r0
and r1
we have a strh
.
This will "store(half)" the data in the first argument to the address in the second argument.
In other words, it writes the lower 16 bits of the register to the address, as if the address was a *mut u16
.
The argument order for single loads and stores on ARM is that the address is always last, and in square brackets.
The square brackets make it fairly easy to spot when skimming through a big pile of assembly.
After doing that strh
we have an "empty loop" like we had before, but just using the label 2
instead of 1
this time.
And if we turn on the program...
cargo run --example ex1
Instead of a totally white screen, we'll see a totally black screen. We've had some effect on the GBA.
Which is enough to call this article over. In the next article we'll actually learn more details about what we just did, as well as more details about how else we can affect the screen.
This is the exact state of the repo when I finished this article.