Understanding the linker scripts for STM32F103C8T6

1

I have recently got into STM32F103C8T6 bare-metal programming, and the linker script implementation seems a little confusing. I found two versions of linker scripts online, and surprisingly both work as expected, despite the huge difference in their contents.

Version 1, as used here

SECTIONS
{
    .  = 0x0;         /* From 0x00000000 */
    .text : 
    {
        *(isr_vector) /* Interrupt Service Routine Vector table */
        *(.text)      /* Program code */
    }
}

and the resulting firmware is flashed with flash write_bank 0 add.bin 0

Version 2, as used here

MEMORY {
    FLASH (rw): ORIGIN = 0x8000000, LENGTH = 64K
    RAM (rwx): ORIGIN = 0x20000000, LENGTH = 20K
}

ENTRY(Reset_Handler)
SECTIONS
{   .text : {
        KEEP(* (.isr_vector))
        * (.text*)
    } > FLASH

    __StackTop = ORIGIN(RAM) + LENGTH(RAM);
}

and the resulting firmware is flashed with flash write_image erase main.bin 0x8000000

As you can see, in both linker script and the OpenOCD command to flash the firmware, version 1 flashes .text to 0x00000000, whereas version 2 flashes to 0x8000000. Firstly, I'm not sure what these addresses refer to: are they LMA or VMA? Secondly, why does flashing to different addresses have the same effect?

I did some research, but the Programming manual doesn't seem to address my confusion.

linker
firmware
bare-metal
linker-scripts
asked on Stack Overflow Feb 4, 2020 by nalzok • edited Feb 4, 2020 by glts

1 Answer

2

The processor boots looking for a vector table at address 0x00000000 in arms address space. ST has implemented their part such that application flash is at address 0x08000000 in the arms address space.

Depending on the boot mode ST can if you will mirror either the built in bootloader program to address 0x00000000 or the application. Such that arm accesses to address 0x00000000 will return with values found in a flash. If mirroring the application then both 0x00000000 and 0x08000000 will read values from the same physical flash device and return them. There is no magic here, the ARM bus has an address and amount of data it wants to read, the logic has masks and matches to determine which address space it is, then if 0x00000000 to some number of Kbytes, then if strap is one way read from one flash bank else read from another. Likewise with writes.

Search for BOOT0 in the reference manual for the part.

Ideally you want to link for 0x08000000 (or 0x00200000 for some of the parts but not all) so that the vector table entries read at 0x00000004, 0x00000008 and so on return 0x0800xxxx addresses, that way only the vector table is actually read from 0x00000000 and then the rest of the program in the application address space. You will see in the docs that the 0x00000000 address space for parts with a lot of flash does not support the whole size of the flash so you couldnt use all of the flash if you link for 0x00000000

now these linker scripts are interesting, first off if you see this in a vector table for a cortex-m

.word       _start + 1
.word       _nmi_handler + 1
.word       _hard_fault + 1

find another example.

.thumb

.section isr_vector
.word 0x20001000
.word one
.word two

.text
.thumb_func
one:
    b one
.thumb_func
two:
    b two
.thumb_func

First linker script

Disassembly of section .text:

00000000 <one-0xc>:
   0:   20001000    andcs   r1, r0, r0
   4:   0000000d    andeq   r0, r0, sp
   8:   0000000f    andeq   r0, r0, pc

0000000c <one>:
   c:   e7fe        b.n c <one>

0000000e <two>:
   e:   e7fe        b.n e <two>

in this case when the arm does a read of the vector table to find the reset vector at address 0x00000004 the ST part will return the value in the second word of the application flash (think address 0x08000004)

in this case it finds 0000000d which means start fetching instructions at address 0x0000000c.

With the second linker script

.thumb

.section .isr_vector
.globl _isr_vector
_isr_vector:
.word 0x20001000
.word Reset_Handler

.text
.globl Reset_Handler
.thumb_func
Reset_Handler:
    b .


Disassembly of section .text:

08000000 <_isr_vector>:
 8000000:   20001000    andcs   r1, r0, r0
 8000004:   08000009    stmdaeq r0, {r0, r3}

08000008 <Reset_Handler>:
 8000008:   e7fe        b.n 8000008 <Reset_Handler>

In this case when the arm looks for the reset vector at 0x00000004 it will get 0x08000009 which means fetch the first instructions at address 0x08000008 which is in the address space for the part for the application flash, this is preferred.

You will find some ST parts have a small window at 0x00200000 that can read some of that flash faster (its an arm thing ITCM vs AXIM, read the cortex-m7 docs).

By an msp432 and I think application flash is 0x01000000 same mirroring thing, just a different address.

.thumb_func
.align  2
.global Reset_Handler
.type   Reset_Handler, %function
Reset_Handler:

note in the second example the author has extra code, both .thumb_func and the .type mark that label as a function (which does the ORR with 1 to set the lsbit of the address so you don't have to have the ugly vector table, use the tools)

For example:

.thumb

.section .isr_vector
.globl _isr_vector
_isr_vector:
.word 0x20001000
.word Reset_Handler
.word Something_Else

.text
.globl Reset_Handler
.thumb_func
Reset_Handler:
    b .

.type Something_Else, %function
Something_Else:
    b .
    

Disassembly of section .text:

08000000 <_isr_vector>:
 8000000:   20001000    andcs   r1, r0, r0
 8000004:   0800000d    stmdaeq r0, {r0, r2, r3}
 8000008:   0800000f    stmdaeq r0, {r0, r1, r2, r3}

0800000c <Reset_Handler>:
 800000c:   e7fe        b.n 800000c <Reset_Handler>

0800000e <Something_Else>:
 800000e:   e7fe        b.n 800000e <Something_Else>

both worked, which one you use is your personal preference

.thumb_func

is clean and simple and doesn't require matching the label, but it is position dependent, the next label is the one that is marked as a function. Also there is no .arm_func

.type labelname, %function

works for both arm and thumb code it is arguably a little more typing and you have to match the label name, but there are pros to that as you clearly are stating the label you want to be identified as a function address and this habit works for both arm and thumb modes.

Both authors (or is it the same person twice?) have created unnecessary work.

Consider these

so.s

.thumb
.word 0x20001000
.word one
.word two

.thumb_func
one:
    b .

.thumb_func
two:
    b .
    

next.s

add r1,r2,r3
add r2,r3,r4
add r3,r4,r5

so.ld

MEMORY
{
    xyz : ORIGIN = 0x08000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > xyz
}

arm-none-eabi-as so.s -o so.o
arm-none-eabi-as next.s -o next.o
arm-none-eabi-ld -T so.ld so.o next.o -o so.elf
arm-none-eabi-objdump -D so.elf

so.elf:     file format elf32-littlearm


Disassembly of section .text:

08000000 <one-0xc>:
 8000000:   20001000    andcs   r1, r0, r0
 8000004:   0800000d    stmdaeq r0, {r0, r2, r3}
 8000008:   0800000f    stmdaeq r0, {r0, r1, r2, r3}

0800000c <one>:
 800000c:   e7fe        b.n 800000c <one>

0800000e <two>:
 800000e:   e7fe        b.n 800000e <two>
 8000010:   e0821003    add r1, r2, r3
 8000014:   e0832004    add r2, r3, r4
 8000018:   e0843005    add r3, r4, r5

That's good, but if you

arm-none-eabi-ld -T so.ld next.o so.o -o so.elf
arm-none-eabi-objdump -D so.elf

so.elf:     file format elf32-littlearm


Disassembly of section .text:

08000000 <one-0x18>:
 8000000:   e0821003    add r1, r2, r3
 8000004:   e0832004    add r2, r3, r4
 8000008:   e0843005    add r3, r4, r5
 800000c:   20001000    andcs   r1, r0, r0
 8000010:   08000019    stmdaeq r0, {r0, r3, r4}
 8000014:   0800001b    stmdaeq r0, {r0, r1, r3, r4}

08000018 <one>:
 8000018:   e7fe        b.n 8000018 <one>

0800001a <two>:
 800001a:   e7fe        b.n 800001a <two>

that's bad.

if not called out in the linker script then the command line position of the link determines order of the .text items in the file. When providing an example or making a project shouldn't you have a makefile or build instructions to go with the code? Do you really need to do the extra work in the linker script? YMMV.

Also note the memory labels in the linker script are just labels to connect the dots between MEMORY and SECTIONS. You can use whatever strings you want within limits and with some exceptions.

I used to use the (rw) stuff but it became problematic had to re-architect my less is more linker script between two versions of binutils. (looks to me like that example has a bug in it) If the linker complains about a missing section (.rodata) just add it.

The beauty of bare metal is you are free to choose how you want to do this, unless you use libraries then you have to play in their sandbox. And often you will find over engineered linker scripts and bootstraps and such trying to cover all kinds of possible use cases and features. Just make one that covers your use case.

You can write your code such that you don't need to zero .bss nor copy .data, or you can choose to support .data but not .bss or you can fully support both with a relatively simple linker script and bootstrap. You own the space on this mcu do you really need to design the stack into the linker script? You know how big the part is just make the stack come down from the top. (granted you only need to type the size in once if you do the linker magic) Your choice. If you want to support a heap for some reason (why would you ever have a heap on an mcu? not a good thing) you can add that into the linker script too.

Again the beauty of bare-metal, so long as it works you are free to do whatever you want. I recommend going beyond what these folks have done and learn the tools a little better (primarily on the assembly language side) so that you have more choices as to how you do it.

Why both work is because the part mirrors some/all of the 0x08000000 arm address space to 0x00000000. so linking for either will work within the window.

You can choose what to label these I use the term arm address space, I would also use the term physical address, few if any of these cores have virtual memory space, so that wouldn't make sense. The ARM bus from the ARM IP (arm does not make chips it makes cores that companies like ST buy and put their own or other folks logic around to make a chip) will send out a read with one of these physical addresses on it, how the chip responds is based on how they designed it. The vector table entries are at known addresses meaning the logic is going to use a known address to read each item, and the chip and the programmer need to respond, if there were an mmu that could change the address space, the vector table would still need to be at one of those physical addresses if the handler logic went through the mmu.

So given the multiple choice test I would say Logical Memory Address, LMA.

The next thing here is that openocd supports writes to 0x00000000. openocd supporting flash writes at all is a bonus as it is chip specific and someone has to put in the time. Because of the nature of 0x00000000 for these parts and how you powered it up (strap pins) you could have it strapped wrong, and/or the openocd implementation has an assumption or a definition to always turn 0x00000000 into 0x08000000 or something like that, might be in the .cfg file itself which I looked at for you recently but didn't look for this detail.

answered on Stack Overflow Feb 4, 2020 by old_timer • edited Sep 14, 2020 by halfer

User contributions licensed under CC BY-SA 3.0