Table of Contents
What is a linker script
Linker scripts are files written in a scripting language which is tailored specifically for the purpose of directing a compiler's to map data and code in explicitly specified memory locations.
This code and data comes in the form of sections which are named self-contained blocks that either the compiler or the programmer has defined to logically partition parts of a program, the programmer can instruct the compiler to generate a non-default section using a compiler directive as follows:
#pragma section mySection
This would then have all following code and variables declared to be found inside of this section.
Using the linkerscript we can map these input sections to new output sections which are defined within the linkerscript and will be then be mapped to memory regions.
How a linker script works
Linker scripts typically have two sections, the first section of the script which is called the MEMORY section is typically used to describe the memory layout both of the chip and of the software architecture. This works by assigning addresses to identifiers of which can either represent the chips physical memory layout or a software architecture layout with memory regions laid out for specific sets of code or data.
The second section which is called the SECTIONS section takes the memory regions described in the first section and assigned code and data to these memory regions.
Here we can see the two sections of the linker script, the first section begins at line 8 and the second section begins at line 15.
figure 1
One more thing to note is that these linker scripts also have preprocessor macros or something similar as can be seen on line's 1 through to line 5. Lines 3 to 5 are actually symbol definition but the nuance will be discussed later.
Pre-section
Figure 2 shows the beginning of a linker script, at this point a number of things can or must be declared before any of the sections are written, this is the pre-section and we will be discussing what is put here below.
figure 2
ENTRY
Before the two main sections are written, at least one macro needs to be used which is the ENTRY macro. This tells the linker where the start address of the program is, this could be a start address that the hardware expects which will be described in the hardware's programming manual or could be a location that a bootloader it expecting to jump to.
The ENTRY macro can reference any existing symbol in the program including the symbol name of the main function or other startup code.
We can see in the linker script the ENTRY macro is used as the following: ENTRY(_START)
OUTPUT_FORMAT
The macro OUTPUT_FORMAT should be used to tell the compiler about the file type to link the final executable into. In the above example for a tricore target the format elf32-tricore is used. This tells the compiler to generate a 32-bit elf file specifically for a tricore target.
This is written in figure 2 as: OUTPUT_FORMAT("elf32-tricore").
OUTPUT_ARCH
The macro OUTPUT_ARCH is used to tell the compiler what architecture is being compiled and linked for, this may not be required if the compiler is being invoked with the target specified as arguments however if the compilation is done separate to the linking then this is required. This is written in figure 2 as: OUTPUT_ARCH(tricore).
Symbol definitions
Symbols may also be defined here before the MEMORY section is written, defining symbols in the pre-section part of the linker script allows the MEMORY section to reference these symbols to define the addresses and sizes of regions in memory as can be seen in figure 1 with the symbols _MainAddress, _OtherStuff and _OtherStuffLen. Keep in mind that symbol definitions end with a semicolon (;).
Note: All symbols and numerical values can be used in arithmetic expressions to calculate new values based off of previously defined symbols, this includes ternary operators as can be seen in figure 2. This can be done both when defining a symbol's value or when using the symbol. This is usually used to calculate offsets in memory.
MEMORY Section
figure 3
The MEMORY section of the linkerscript as briefly described previously allows the definition of memory regions. It begins with the keyword MEMORY followed by a set of open-close curly braces of which all specified memory regions are described within. These memory regions can either represent physical hardware as can be seen in figure 3 with the region PMU_PFLASH0 being mapped directly to the address corresponding to the Tricores PFLASH 0 and/or these memory regions may map logically to code/data regions that have been partitioned in the software architecture as can be seen in figure 1 where a region has been created specifically for the main function to exist in and everything else is to be allocated in another memory region.
Memory regions are defined following this format: name (attribute) : org = origin, len = length
First the symbol/region name is written typically in all capital letters with underscores representing spaces, this follows the convention of using the C preprocessor symbol definitions. Following the region name are the region attributes which tells the compiler what operations are allowed to be performed on this memory region, these are as follows:
r - read only
w - read and write
x - regions containing executable code
a - allocated regions
i - initialized regions
l - same as i
! - inverts the behavior of the following attribute
NOTE: attributes are not case sensitive and they are also optional.
Next the origin of the memory region is specified, this uses the keyword org followed by either a written address, a symbol-named address or a calculated address using arithmetic expressions.
Lastly the length of the memory region is specified using the keyword len followed by again either a written address, a symbol-named address or a calculated address using arithmetic expressions.
Examples may be seen in figure 3.
Note: The linker script is sensitive to punctuation such as a colon (:) following the attributes and comma (,) following the org specification. Also note that both decimal numbers and hexidecimal numbers can be used, hexidecimal numbers are specified with the 0x prefix otherwise decimal will be assumed. Also note that numerical postfixes can be used such as k,m and g to indicate to the compiler that the size is speicifed in Kilobytes, Megabytes and Gigabytes and that these postfixes are not case sensitive. Lastly note that the linkerscript supports C style comments for multi-line comments or a hash (#) symbol is used for single line comments.
SECTIONS Section
figure 4
The SECTIONS section begins with the keyword SECTIONS followed by a set of open-close curly braces in which all output sections are described.
The output sections are defined by the following rules:
Their name is specified with a dot (.) followed by their name and then a colon as can be seen in figure 4.
Then comes a set of open-close curly braces in which input section's contents are mapped to the output sections.
Lastly this output section is mapped to a memory region previously described in the MEMORY section of the linker script.
An example can be seen in figure 4.
Input sections are mapped to output sections in the following way, an asterisk (*) is used as a wild card which allows us to not have to concern ourselves with parts of an input-section's path. This is because sections can and will contain child sections which are accessed akin to path as follows:
RootSection.ParentSection.ChildSection....
Using the wildcard in this manor allows us to not concern ourselves with the path to the child section we wish to map like this:
*(.ChildSection)
Or even:
*(.ParentSection.*)
All input sections will be mapped to this output section in the written order that they appear in the linker script, this is because the linker script has an address counter variable which increments as input sections are mapped sequentially down the script. This variable can be accessed directly and used within the script, the variable name is the dot (.) which can be seen in figure 4. Taking a look at figure 4 we will see the following within the section definition:
Firstly a symbol _startOfSillySection is defined, as this is a symbol it can be accessed in c code using the extern keyword along with the symbol name, this symbol is then given an address which is the value of the address counter at that point in the script and as it's the first thing in the section mySillySection we can know that this symbol has the address of the beginning of mySillySection. This can be used to determine the location of mapped code during runtime through reading the address of the symbol or after compilation by finding the symbol in a generated mem-map file.
Note that this symbol has no value, it is not a variable it is a name for an address in the programs symbol table therefore to read this address you de-reference this symbol in the C code.
At the end of the definition of mySillySection we can see a second symbol is defined which also captures the address at the end of the section, using these two symbols we can find the size of the output section at runtime.
Also keep in mind it is possible to assign a value to the address counter within the linker script, this can be done to force alignment of memory before code/data is mapped which can be done with the ALIGN macro as can be seen here:
In the screenshot above we can also see the KEEP keyword, this keyword is used to force the compiler to keep the symbols even if they're not specifically referenced which could be used when perform jumps or read/writes to something we know is in memory such as other peoples code separate compiled (Eg firmware) or to registers which are accessed through unnamed memory addresses.
After the input sections have been mapped to the output section we need to specify which of the previously defined memory region(s) this output section must go into which comes after the closing curly brace. It looks something like this:
.myOutputSection:
{
*(*) #everything goes here
} > VMA AT > LMA
or
.myOutputSection:
{
*(*) #everything goes here
} > VMA
This is often done only with a single greater than sign (>) and then a previously declared memory region however sometimes we have to specify two regions as one region will specify where in the executable the code/data can be found and the other will specify where on the chip the code/data must be located.
This is because we may end up flashing data to flash that then needs copying to the RAM to be modified or code that must be copied to RAM to be executed to do this we use a greater than sign followed by the memory region, this is called the virtual memory address (VMA). The VMA contains the memory region that is going to be seen in the program during execution but does not describe where in the flashed executable file the data/code is going to be found.
After the VMA is described the keyword AT is used followed by a second greater than sign (>) which contains the Load memory address (LMA). This address is the address of the code/data found within the flashed executable file itself. The reason for two address definitions is in the case that code/data has to be copied from one place to another in order for the program to run, this could be to put config in a specific hardware-dependent address, move code/data RAM or cache for better performance, to move code/data to memory where the debugger/flashing tool can't reach (such as RAM or cache).
Demo Example
Now we're going to go through a simple example of using a linker script with GCC which can be downloaded from the bottom of this page. This demo doubles as an explanation of the stages of compilation as we will be running a C file through GCC one stage at a time and looking at the output of these stages to see how the sections are formed and eventually linked.
Here we can see the C file that we will be working with. We can see stdio.h is included at the top, then two variables are declared statically, one is an int which is 4 bytes and is initialised to the value of 10 and the second is an unitialised double therefore is 8 bytes.
We then define TRUE and FALSE as 1 and 0 respectively to the preprocessor before implementing the main function.
Inside of the main function we have 3 conditionals. The first we can see will always run as the preprocessor will resolve this into the following:
if(1)
As we will see later. The second if statement will never run because the preprocessor will resolve as the following:
if(0)
Again as we will see later.
The last conditional will never make it past the preprocessor.
Lastly note that the preprocessor will strip out all of the comments.
Invoking the preprocessor
To invoke the preprocessor; go into the respective folder inside the project for your operating system and run the script preproc(.bat). This will invoke the preprocessor on the c file ans generate an output preprocessed file called demo_preprocessed.i in the root of the project.
Looking at the top of the file we can see the preprocessor has resolved all of the includes required to include stdio.h and at the bottom of the file we can see the following:
This shows us the preprocessor has resolved TRUE and FALSE and replaced them with the declared values, it has also stripped out the commments and removed the third conditional. This is the code that will go through the compiler.
Generating the assembly
After preprocessing the code file goes through a generation stage which parses the C code and generates an abstract syntax tree (AST) which is a logical tree structure representing the code, this tree will be parsed by the language rules and then be turned into generated assembly. This can be invoked by running the gen(.bat) script which will take the preprocessed file and generate file called demo_genned.s.
At line 5 and 6 we can see the definition of the symbol _x which is declared as a 32 bit long-integer with the value of 10. Note that the variable name (identifier) we gave this in the C code was just x yet the symbol has been translated into _x, the C compiler prepends underscored onto identifiers as a form of name mangling.
On line 7 we can see the definition of the variable y as _y using the directive .lcomm which allocates memory in a section called .bss, we will discuss this section later. The allocation rules look like this:
As we can see _y is allocated as an 8 byte aligned block that is 8 bytes in size, perfect for a double.
On line 15 we can see the definition of the main function as _main.
Next on line 11 we can see the string used in the first conditional is declared using the .ascii directive. Note that the other strings are not present as previous stages have optimised them out after recognising they will never run.
First the stack is set up and then on line 26 we can see that pointer to LC0 (containing the string) is copied to the stack and then a call to _printf is made before _main completes by moving 0 to register A and calling return.
Assembling the assembly
Now that we have seen the assembly and understand why it is made up as it is, lets assemble it and look at the output, to do this we invoke the assembler on the assembly, this can be done by running the script called assemble(.bat).
This generates us an object file which is not longer a textual representation and we're going to need some tools to inspect farther, luckily with GCC we already have these tools, the first on we will use is called objdump. Run the script called objdmp(.bat) in the command line and you should see lots of text output.
Objdump dumps data from object files such as the sections and their data and the symbol table of the program so it would be a good idea to explain these two.
Sections
A section is a named block of data or code, some of the sections are given names and purpose due to legacy support and some of them are given names by programmers/compilers. Historically there are 3 main sections
- .text - This is where code historically went and we will see main is in here.
- .bss - This is a section for all uninitialised data, remember that _y went into .bss as it was uninitialised where as _x actually went in a section called .data
- .data - This section is where initialised read/writable data goes.
- rdata - This section is where only read only data goes and is protected against writes, this includes strings by default and as we will see the string in the first conditional will be placed here.
Symbol table
The symbol table is a map that maps symbols (names for identifiers such as variables, functions or even nothing as we will see) to addresses, this is what gives us variables. Function names are also found in the symbol table with addresses to the code within the .text section. It is in the symbol table that the names are prefixed with underscores. Even section names are found in the symbol table.
Note here that we can see the symbols/names for our variables x and y as well as the function main and printf and even the section names.
Now all of these sections and their contents are located in an object file which is more properly called a relocatable object file, this is because everything inside of this file is addressed relative to the sections in the file, there are no absolute addresses here and therefor cannot be executed directly, the process of cementing the object file into an absolute address is called linking.
Linking
We are now at the final stage of the compilation, the object file(s) are linked together and an executable binary is outputted. To Invoke the linker we run the script link(.bat) which will give us either an elf file or an exe file but to do this we must instruct the compiler on how to arrange the input sections (sections coming from the object file) and arrange their contents into new output sections which will be found within the executable. To do this we use something called a linkerscript.
This is the linkerscript we are using the the demo:
The Linker Script
Pre Section
Using this we can now see why ENTRY is fed the symbol _main. this is because of the name mangling that went on translating the function name into it's symbol within the symbol table.
We also have a symbol called _MainAddr which is given the address 0xABBA, this is where in memory we are going to put the main function. We then have another symbol called _OtherStuff which is given the address 0xF00D which is where everything else will go. lastly for the symbols we have one called _OtherStuffLen which is given the address 0x4000, note that in this case this symbol is referring to an address and if we look at the symbol table we will see a symbol called _OtherStuffLen pointing to that address, this symbol is not used to point into memory and is used purely as a named value.
MEMORY Section
After the symbols have been defined we get to the MEMORY section of the script where we have a memory region called myMain which can be read and executed but may not be written to, it's origin is to be the address found at _MainAddr in the symbol table and is going to be 10Kb long.
The second region is called otherStuff which is where everything else will go therefore is has the permissions to read, write and execute as there is both data and code located there. It is given the address found at _OtherStuff in the symbol table and the length found at _OtherStuffLen in the symbol table. Note that the values are not at the address in the symbol table, the values are the addresses in the symbol table. Do not dereference _OtherStuffLen as it is not a valid address.
SECTIONS Section
In the SECTIONS section we take the all of the input sections (.text, .bss, .rdata...) from the object files and we create the output sections for the executable and assign them to the memory regions. Here we can see two output sections have been defined: .text and .otherThings.
In the .text section we use the wild card (*) symbol to search for any section in any object file or in any other sections called .text and we put it in the region myMain.
In the .otherThings section we use the wild card (*) symbol to put everything else that hasn't been allocated into the memory region otherStuff.
Invoking the linker with this script should give us an executable with two sections.
Running objdump on the output like this
will show the two sections with the specified addresses and access attributes.