Linking for STM32F4


I’m trying to get some Rust code running on a STM32F4 based board we are using at my university. As a first step I want to transform some low-level C code into the Rust equivalent, just to make sure I get the tooling correct. The only thing this code does is to initialize the GPIO D port, and then continuously writing the bits read on the pins 8-15 to pins 0-7.

Following piece of C works well when compiled with gcc:

#define GPIO_D 0x40020C00
#define GPIO_D_IDR_H GPIO_D + 0x11
#define GPIO_D_ODR_L GPIO_D + 0x14

void startup(void) __attribute__((naked)) __attribute__((section(".start_section")));

void startup(void) {
  asm volatile(" NOP\n"
	       " LDR SP,=0x2001C000\n"
	       " BL main\n"
	       ".L1: B .L1\n"

int main(void) {
  *((volatile unsigned int *) GPIO_D_MODER) = 0x00005555;
  while(1) {
    *((volatile unsigned char *) GPIO_D_ODR_L) = *((volatile unsigned char *) GPIO_D_IDR_H);
  return 0;

This is the makefile used for the compiling, linking and conversion to appropriate loadfile (somewhat shortened, but should contain all relevant parts):

# Compiler
CC      = $(TCPATH)arm-none-eabi-gcc
CFLAGS 	= -Wall -O0 -g \
          -mthumb -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard \

# Linker
LD	= $(T)arm-none-eabi-ld
LDFLAGS	= -nostartfiles -Tmd407-ram.x

# Object copy
OC 	= $(TCPATH)arm-none-eabi-objcopy
OCFLAGS = -S -O srec

### Compile, link and create load file
$(OUT)/$(PROJ).s19 : $(OUT)/$(PROJ).elf
	$(OC) $(OCFLAGS) $(OUT)/$(PROJ).elf $(OUT)/$(PROJ).s19

$(OUT)/$(PROJ).elf : $(OBJS)
	$(LD) $(LDFLAGS) $(OBJS) -o $(OUT)/$(PROJ).elf

And this is the linker script used:

   Default linker script for MD407 (STM32F407)
   All code and data goes to RAM

/* Memory Spaces Definitions */
  RAM (xrw)  :  ORIGIN = 0x20000000, LENGTH = 112K

    .text :
        . = ALIGN(4);
        *(.start_section) /* startup code */
        *(.text)          /* remaining code */
        *(.data)          /* initialised data */
        *(.rodata)        /* read-only data (constants) */
        *(.bss)           /* uninitialised data */
        . = ALIGN(4);
    }  >RAM

Now, I’ve tried to translate the C code above quite literally into Rust. This is my attempt:

#![feature(asm, naked_functions, lang_items, used)]

use core::ptr;

#[link_section = ".start_section"]
fn startup() {
    unsafe {
	      LDR SP, =0X2001C000
	      BL  main
	      .L1: B   .L1"
             : : : : "volatile"

pub extern fn main() -> ! {
    const GPIO_D_MODER: u32 = 0x4002C000;
    const GPIO_D_IDR_H: u32 = 0x4002C011;
    const GPIO_D_ODR_L: u32 = 0x4002C014;
    unsafe {
        ptr::write_volatile(GPIO_D_MODER as *mut u32, 0x00005555);
    loop {
        unsafe {
            let data: u8 = ptr::read_volatile(GPIO_D_IDR_H as *mut u8);
            ptr::write_volatile(GPIO_D_ODR_L as *mut u8, data);

#[lang = "eh_personality"] extern fn eh_personality() {}
#[lang = "panic_fmt"] fn panic_fmt() -> ! { loop {} }

… and this is my .cargo/config

target = "thumbv7em-none-eabi"

rustflags = [
            "-C", "link-arg=-nostartfiles",
            "-C", "link-arg=-Tmd407-ram.x",
	    "--emit asm"

Compiling this with xargo build works just fine, and a resulting 352 byte target/thumbv7em-none-eabi/debug/bargraph is generated. The problem is the following:

$ arm-none-eabi-size target/thumbv7em-none-eabi/debug/bargraph
   text	   data	    bss	    dec	    hex	filename
      0	      0	      0	      0	      0	target/thumbv7em-none-eabi/debug/bargraph

I guess this has something to do with the linking step, but I can’t figure out how to proceed. Any help is most appreciated!


Have you seen the blog post from japric? I am not 100% shure, but maybe your linker can’t find the linker script. Maybe this part of the quickstart code helps:


Thanks for your suggestions. I’ve seen japaric’s blog posts (they are hard to miss when researching the embedded Rust topic :-)). They seam great but a bit too high level for my current needs - I want to do the bit-flipping myself and build my own abstractions.

Anyway, the linker script is in the right place, if I remove it, the build process do not succeed. I have seen japaric’s and other’s linker scripts but am not sure how to adopt them for my needs. For some reason my code must go to RAM (the course material tells me this) which is not the case in the script in the blog… I guess? My linker script knowledge is not that great. From japaric’s blog:

    /* NOTE K = KiBi = 1024 bytes */ 
    FLASH : ORIGIN = 0x08000000, LENGTH = 256K 
    RAM : ORIGIN = 0x20000000, LENGTH = 40K 

/* NOTE Do NOT modify `_stack_start` unless you know what you are doing */ 
_stack_start = ORIGIN(RAM) + LENGTH(RAM);

The script I’m using do not have the _stack_start definition. How is this used?


Ok, so I managed to solve some of these issues and do post it here in case anyone should ever stumble across this.

First, for Rust to use the custom linker section .start_section used in the linker script, I had to throw in the attribute #[no_mangle] and, as a consequence, make the startup function public. I guess this should not strictly be necessary (the startup function is merely a placeholder for the assembler code and never called), but a side-effect of something I don’t really understand (see #31758).

Second, if I make rustc emit the object files (--emit obj in .cargo/confg) and link them manually using arm-none-eabi-ld and the link options provided in .cargo/config, I obtain a correctly linked ELF-file. This should of course be handled by Cargo, and I will have to look further into this.