ARMs for the smallest: subtleties of compilation and linker
- Tutorial

Continuing a series of articles about developing from scratch for ARM, today I will cover the topic of writing linker scripts for GNU ld . This topic can be useful not only for those who work with embedded systems, but also for those who want to better understand the structure of executable files. Although the examples are somehow based on the arm-none-eabi toolchain, the essence of the layout is the same with the Visual Studio linker, for example.
Previous articles:
Code examples from the article: https://github.com/farcaller/arm-demos
When we compile the source file, the output we get is an object file that typically contains several sections with data. The four most common sections are:
- .text - compiled machine code;
- .data - global and static variables;
- .rodata - an analogue
.datafor immutable data; - .bss - global and static variables that contain a null value at startup.
In the binary files that we work with as part of this cycle, two more sections will often come across:
- .comment - information about the compiler version;
- .ARM.attributes - ARM-specific file attributes.
In addition to sections, there is another important entity in the object file: a symbol table. This is a kind of hash: name - address (and additional attributes). In the symbol table, for example, all exported functions and their addresses (which will indicate somewhere in the .text section) are indicated.
After we got a few of these files, the linker is taken up, who, according to the given rules, will collect all sections, throw out the unnecessary ones and make the final executable file. For "standard" OSs, rules are defined where what should be located, but in the case of microcontrollers, we usually need to manually push everything through the flash and RAM.
Take a look inside
As a first example, we will learn the following C code
module_a.c:static int local_function();
int external_counter;
static int counter;
static int preset_counter = 5;
const int constant = 10;
int public_function()
{
volatile int i = 3 + constant;
++external_counter;
return local_function() * i;
}
static int local_function()
{
++counter;
++preset_counter;
return counter + preset_counter;
}
Compile it and see which sections we got:
% rake 'show:sections[a]'
arm-none-eabi-gcc -mthumb -O2 -mcpu=cortex-m0 -c module_a.c -o build/module_a.o
arm-none-eabi-objdump build/module_a.o -h
build/module_a.o: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000034 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000004 00000000 00000000 00000068 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000004 00000000 00000000 0000006c 2**2
ALLOC
3 .rodata 00000004 00000000 00000000 0000006c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000071 00000000 00000000 00000070 2**0
CONTENTS, READONLY
5 .ARM.attributes 00000031 00000000 00000000 000000e1 2**0
CONTENTS, READONLY
As we see, there are six sections, the purpose of which is already more or less known to us. The second line is the section attributes, they will be more interesting later, when linking. Let's see what characters are defined in these sections:
% rake 'show:symbols:text[a]'
arm-none-eabi-objdump build/module_a.o -j .text -t
build/module_a.o: file format elf32-littlearm
SYMBOL TABLE:
00000000 l d .text 00000000 .text
00000000 g F .text 00000034 public_function
Let's open man on objdump for consultation. In this section, we see two symbols:
.text- this is a debugging symbol that points to the beginning of the section, public_function- this is a symbol that points to our function. There is local_functionno symbol for it, since the function is declared as static, i.e., it is not exported outside the object file.% rake 'show:symbols:data[a]'
arm-none-eabi-objdump build/module_a.o -j .data -j .bss -t
build/module_a.o: file format elf32-littlearm
SYMBOL TABLE:
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l O .data 00000004 preset_counter
00000000 l O .bss 00000004 counter
In the sections
.dataand .bssthere are two of our counters - preset_counterand counter. They are in different sections, since u preset_counterhas an initial value, which is stored in .data:% rake 'show:contents[a,.data]'
arm-none-eabi-objdump build/module_a.o -j .data -s
build/module_a.o: file format elf32-littlearm
Contents of section .data:
0000 05000000
The
countervalue does not, so it is initialized to zero and gets into the section .bss. The section itself is .bssnot physically present in the file, since its contents are always fixed - these are zeros. If you declared char buffer[1024]in the code, the compiler would have to write kilobytes of empty space to the object file, which makes no sense. At this moment, you may ask a question - where did
external_counteryou go ?% rake 'show:symbols:all[a]'
arm-none-eabi-objdump build/module_a.o -t
build/module_a.o: file format elf32-littlearm
SYMBOL TABLE:
00000000 l df *ABS* 00000000 module_a.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .rodata 00000000 .rodata
00000000 l O .data 00000004 preset_counter
00000000 l O .bss 00000004 counter
00000000 l d .comment 00000000 .comment
00000000 l d .ARM.attributes 00000000 .ARM.attributes
00000000 g F .text 00000034 public_function
00000004 O *COM* 00000004 external_counter
00000000 g O .rodata 00000004 constant
external_counterwent to the section *COM*. In this case, this means that it may be located outside of this object file. Already at the stage of linking, ld will figure out whether the symbol is declared in another file, or he should create it himself - in this case, in the section .bss. Also pay attention to what const int constantgot into .rodata. The compiler ensures that the code does not need to change the value at this address, so that the linker can safely place it in flash memory. We can look at
.comment:% rake 'show:contents[a,.comment]'
arm-none-eabi-objdump build/module_a.o -j .comment -s
build/module_a.o: file format elf32-littlearm
Contents of section .comment:
0000 00474343 3a202847 4e552054 6f6f6c73 .GCC: (GNU Tools
0010 20666f72 2041524d 20456d62 65646465 for ARM Embedde
0020 64205072 6f636573 736f7273 2920342e d Processors) 4.
0030 372e3320 32303133 30333132 20287265 7.3 20130312 (re
0040 6c656173 6529205b 41524d2f 656d6265 lease) [ARM/embe
0050 64646564 2d345f37 2d627261 6e636820 dded-4_7-branch
0060 72657669 73696f6e 20313936 3631355d revision 196615]
0070 00
The compiler version is really written here. We can also look into
.ARM.attributes, though for this it is worth using not objdump , but readelf :% rake 'show:attrs[a]'
arm-none-eabi-readelf build/module_a.o -A
Attribute Section: aeabi
File Attributes
Tag_CPU_name: "Cortex-M0"
Tag_CPU_arch: v6S-M
Tag_CPU_arch_profile: Microcontroller
Tag_THUMB_ISA_use: Thumb-1
Tag_ABI_PCS_wchar_t: 4
Tag_ABI_FP_denormal: Needed
Tag_ABI_FP_exceptions: Needed
Tag_ABI_FP_number_model: IEEE 754
Tag_ABI_align_needed: 8-byte
Tag_ABI_align_preserved: 8-byte, except leaf SP
Tag_ABI_enum_size: small
Tag_ABI_optimization_goals: Aggressive Speed
Public tag documentation can be viewed at the ARM Info Center .
Putting it all together
Now that we have looked inside the object files, let's see how ld collects them into one successful application.
The main work of ld revolves around the memory card, which we saw in the first part. To simplify things greatly, layout is the process of tearing out sections from object files, folding them to the specified addresses and correcting cross-references. In "standard" operating systems, the kernel can read the output file and load sections into memory at the expected virtual addresses. The dynamic linker also does a similar job, loading external libraries into specific memory locations and setting up cross-references to them.
With embedded systems easier, the firmware program takes your binary file and uploads it to the USB flash drive as is. He does not care about either macho or elves, he works with binary dumps.
Take a simple linker script and take it apart.
layout.ld:MEMORY
{
rom(RX) : ORIGIN = 0x00000000, LENGTH = 0x8000
ram(WAIL) : ORIGIN = 0x10000000, LENGTH = 0x2000
}
ENTRY(public_function)
SECTIONS
{
.text : { *(.text) } > rom
_data_start = .;
.data : { *(.data) } > ram AT> rom
_bss_start = .;
.bss : { *(.bss) } > ram
_bss_end = .;
}
The default linker configuration allows it to use all available memory (somewhere around 0xFFFFFFFF bytes in the case of 32-bit ARM). To begin with, we define the memory regions that can be used:
romand ram. The letters in brackets define the attributes: read, write, execute, memory allocation. Sections that are not explicitly specified in the script will be scattered across regions with suitable attributes automatically. If a section does not find a place, the linker will refuse to work, arguing that their behavior somehow: error: no memory region specified for loadable section `.data'. The two parameters,
ORIGINand LENGTH, define the start and length of the region, respectively, one can still find the options org, o, lenandl, they are equivalent. Value - this expression, i.e., it can perform arithmetic operations or use suffixes K, Mand the like Writing LENGTH = 0x8000, for example, may alternatively execute as follows: l = 32K. The second part of the file is the section configuration. In general, this means copying
The source sections are set in the form
ИМЯ_ФАЙЛА(ИМЯ_СЕКЦИИ), the character *behaves in a standard way, so the record *(.text)means: sections .textfrom all files.The section has two addresses: LMA (Load Memory Address) - from where it is loaded, and VMA (Virtual Memory Address) - at what address it is available in virtual memory. To explain it more simply, LMA is where it will appear in the binary file, and VMA is where the characters will be redirected, i.e., the pointer to the character in the code will refer to the VMA address.
We are interested in three sections - code, data and data, which are zero by default. Thus, we copy the code (
.text) into flash memory, data ( .data) into flash memory, but on the assumption that they will be available in RAM, and .bssinto RAM. For
.bss, in general, initialization is not required ( UPD: they tell me in the jabber that what is required, we must guarantee that there are zeros, not the garbage that appeared for any reason), since at the start of the microcontroller the RAM is probably reset to zero. But here you .datahave to tinker separately, the problem is due to a twofold nature. On the one hand, specific data is stored there (starting value preset_counter), so it must be in flash memory. On the other hand, this is a writable section, so it should be in RAM. This problem is solved by different LMA and VMA, as well as additional C code, which, when launched, will copy the contents from LMA to VMA. For constant data, which is usually found in a section .rodata, such a procedure, for example, is not needed, we can safely read it directly from flash memory.The linker has a cursor concept - this is the current LMA. At the beginning of the SECTIONS block, the cursor is zero and gradually increases as new sections are added. The current cursor value is stored in a variable
.(dot). Let's run the linker and see the result of its work:
% rake 'show:map[a]'
arm-none-eabi-ld -T layout.ld -M -o build/out.elf build/module_a.o
Allocating common symbols
Common symbol size file
external_counter 0x4 build/module_a.o
Memory Configuration
Name Origin Length Attributes
rom 0x0000000000000000 0x0000000000008000 xr
ram 0x0000000010000000 0x0000000000002000 awl
*default* 0x0000000000000000 0xffffffffffffffff
First, we see how the linker puts the “common” symbol in a separate category
external_counter. Next, we see that our memory configuration has been loaded and added to the default configuration (which allocates the entire address space).Linker script and memory map
.text 0x0000000000000000 0x34
*(.text)
.text 0x0000000000000000 0x34 build/module_a.o
0x0000000000000000 public_function
0x0000000000000034 _data_start = .
Next, the linker places in memory the sections that we specified in the first place
.text..rodata 0x0000000000000034 0x4
.rodata 0x0000000000000034 0x4 build/module_a.o
0x0000000000000034 constant
.glue_7 0x0000000000000038 0x0
.glue_7 0x0000000000000000 0x0 linker stubs
.glue_7t 0x0000000000000038 0x0
.glue_7t 0x0000000000000000 0x0 linker stubs
.vfp11_veneer 0x0000000000000038 0x0
.vfp11_veneer 0x0000000000000000 0x0 linker stubs
.v4_bx 0x0000000000000038 0x0
.v4_bx 0x0000000000000000 0x0 linker stubs
.iplt 0x0000000000000038 0x0
.iplt 0x0000000000000000 0x0 build/module_a.o
.rel.dyn 0x0000000000000038 0x0
.rel.iplt 0x0000000000000000 0x0 build/module_a.o
They are followed by sections that we do not point clearly -
.rodata, .glue_7, .glue_7t, .vfp11_veneer, .v4_bx, .iplt, .rel.dyn. Since .rodataeverything is clear, our constant is stored there in four bytes constant. As for the remaining sections, their existence is due to all kinds of support for performance, for example, jumps from ARM to Thumb. All these sections are empty and do not fall into the final image..data 0x0000000010000000 0x4 load address 0x0000000000000038
*(.data)
.data 0x0000000010000000 0x4 build/module_a.o
0x0000000010000004 _data_end = .
Here is our section
.data, as you see, it is located at the address 0x10000000, although it is physically stored at the address 0x38(i.e., immediately after .rodata). Here we see the value of our variable, read from the cursor _data_end..igot.plt 0x0000000010000004 0x0 load address 0x000000000000003c
.igot.plt 0x0000000000000000 0x0 build/module_a.o
.bss 0x0000000010000004 0x8 load address 0x000000000000003c
*(.bss)
.bss 0x0000000010000004 0x4 build/module_a.o
COMMON 0x0000000010000008 0x4 build/module_a.o
0x0000000010000008 external_counter
0x000000001000000c _bss_end = .
Another empty section, followed by -
.bss.LOAD build/module_a.o
OUTPUT(build/out.elf elf32-littlearm)
.comment 0x0000000000000000 0x70
.comment 0x0000000000000000 0x70 build/module_a.o
0x71 (size before relaxing)
.ARM.attributes
0x0000000000000000 0x31
.ARM.attributes
0x0000000000000000 0x31 build/module_a.o
Finally, ld generates an output file and discards unnecessary sections. Look like that's it?
0x0000000000000034 _data_start = .
...
.data 0x0000000010000000 0x4 load address 0x0000000000000038
The variable that points to the beginning
.dataactually points to the wrong place! But the truth is, the cursor after .textindicates its end. To correctly set a variable, it must be moved inside the description of the output section:.data :
{
_data_start = .;
*(.data)
_data_end = .;
} > ram AT> rom
We compose and see what has changed:
% rake 'show:map[a]' SCRIPT=layout2.ld
arm-none-eabi-ld -T layout2.ld -M -o build/module_a.elf build/module_a.o
...
.data 0x0000000010000000 0x4 load address 0x0000000000000038
0x0000000010000000 _data_start = .
*(.data)
.data 0x0000000010000000 0x4 build/module_a.o
0x0000000010000004 _data_end = .
...
Great, now everything is in place.
You may be wondering - what is the matter to us, where is it located
.data? As you remember, the data is physically stored in a flash, and you have to work with it from the RAM. For this reason, we will have to write boot code that will be copied .datato RAM, and these variables will help us find out the specific addresses where the section should be moved.Let's complicate the task
We figured out one module. Let's add a second file and see what changes. The second file will contain a
external_counterlittle C ++ code already known to us :module_b.cppint external_counter;
extern "C" int public_function();
void function_b()
{
external_counter += public_function();
}
void function_c()
{
}
void function_d()
{
}
As you know, when compiling C ++ code, the names of functions and methods go through “mangling”, when the types of arguments, the names of classes and namespaces are encoded in the name:
% rake 'show:symbols:text[b]'
arm-none-eabi-gcc -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -mthumb -O2 -mcpu=cortex-m0 -c module_b.cpp -o build/module_b.o
arm-none-eabi-objdump build/module_b.o -j .text -t
build/module_b.o: file format elf32-littlearm
SYMBOL TABLE:
00000000 l d .text 00000000 .text
00000000 g F .text 00000014 _Z10function_bv
00000014 g F .text 00000002 _Z10function_cv
00000018 g F .text 00000002 _Z10function_dv
We compile the code with flags
-fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tablesto avoid the appearance of additional sections related to the handling of exceptional situations. Function names have been encoded accordingly. We cannot generate a map for this module, since it cannot be built on its own, it depends on the function
public_functionfrom the module a. We compose both modules at once:% rake 'show:map[a|b]' SCRIPT=layout2.ld
arm-none-eabi-ld -T layout2.ld -M -o build/out.elf build/module_a.o build/module_b.o
...
.text 0x0000000000000000 0x34 build/module_a.o
0x0000000000000000 public_function
.text 0x0000000000000034 0x1c build/module_b.o
0x0000000000000034 function_b()
0x0000000000000048 function_c()
0x000000000000004c function_d()
...
The block of common characters is gone, all characters are found in the corresponding modules. Sections
.text, as well as the others, are arranged one after another.Let's collect the trash!
For embedded applications, the size of the output file is more relevant than ever, so you should make sure that the maximum amount of unnecessary data and dead code is deleted. The linker is able to get rid of sections that no one refers to and that were not explicitly specified as necessary in the build script. This is done quite simply - using the flag
--gc-sections:% rake 'show:map[a|b]' SCRIPT=layout2.ld GC=1
arm-none-eabi-ld --gc-sections -T layout2.ld -M -o build/out.elf build/module_a.o build/module_b.o
Discarded input sections
.rodata 0x0000000000000000 0x4 build/module_a.o
COMMON 0x0000000000000000 0x0 build/module_a.o
.text 0x0000000000000000 0x1c build/module_b.o
.data 0x0000000000000000 0x0 build/module_b.o
...
.text 0x0000000000000000 0x34
*(.text)
.text 0x0000000000000000 0x34 build/module_a.o
0x0000000000000000 public_function
...
As you can see, the section
.textof build/module_b.ohas been completely removed, as contained useless functions! At the same time, the linker threw out unused constants from the first module. In fact, this optimization is not complete, as we can easily see with a simple experiment, see
module_c.cppvoid function_b();
extern "C" int public_function()
{
function_b();
}
We will replace the module
awith the module cand see if the linker can delete the section.% rake 'show:map[b|c]' SCRIPT=layout2.ld GC=1
arm-none-eabi-gcc -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -mthumb -O2 -mcpu=cortex-m0 -c module_c.cpp -o build/module_c.o
arm-none-eabi-ld --gc-sections -T layout2.ld -M -o build/out.elf build/module_b.o build/module_c.o
Discarded input sections
.data 0x0000000000000000 0x0 build/module_b.o
.data 0x0000000000000000 0x0 build/module_c.o
.bss 0x0000000000000000 0x0 build/module_c.o
...
.text 0x0000000000000000 0x24
*(.text)
.text 0x0000000000000000 0x1c build/module_b.o
0x0000000000000000 function_b()
0x0000000000000014 function_c()
0x0000000000000018 function_d()
.text 0x000000000000001c 0x8 build/module_c.o
0x000000000000001c public_function
Although it was possible to throw out part of the sections (though empty), we still lose invaluable bytes on the functions
function_c()and function_d(), which ended up in the same section as the one function_b()we need. Compiler flags will come to the rescue, which break functions and data into different sections: -ffunction-sectionsand -fdata-sections:% rake clean && rake 'show:symbols:all[b]' SPLIT_SECTIONS=1
arm-none-eabi-gcc -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -ffunction-sections -fdata-sections -mthumb -O2 -mcpu=cortex-m0 -c module_b.cpp -o build/module_b.o
arm-none-eabi-objdump build/module_b.o -t
build/module_b.o: file format elf32-littlearm
SYMBOL TABLE:
00000000 l df *ABS* 00000000 module_b.cpp
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l d .text._Z10function_bv 00000000 .text._Z10function_bv
00000000 l d .text._Z10function_cv 00000000 .text._Z10function_cv
00000000 l d .text._Z10function_dv 00000000 .text._Z10function_dv
00000000 l d .bss.external_counter 00000000 .bss.external_counter
00000000 l d .comment 00000000 .comment
00000000 l d .ARM.attributes 00000000 .ARM.attributes
00000000 g F .text._Z10function_bv 00000014 _Z10function_bv
00000000 *UND* 00000000 public_function
00000000 g F .text._Z10function_cv 00000002 _Z10function_cv
00000000 g F .text._Z10function_dv 00000002 _Z10function_dv
00000000 g O .bss.external_counter 00000004 external_counter
Now that every function and object are placed in independent sections, the linker can get rid of them:
% rake clean && rake 'show:map[b|c]' SCRIPT=layout2.ld GC=1 SPLIT_SECTIONS=1
arm-none-eabi-gcc -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -ffunction-sections -fdata-sections -mthumb -O2 -mcpu=cortex-m0 -c module_b.cpp -o build/module_b.o
arm-none-eabi-gcc -fno-exceptions -fno-unwind-tables -fno-asynchronous-unwind-tables -ffunction-sections -fdata-sections -mthumb -O2 -mcpu=cortex-m0 -c module_c.cpp -o build/module_c.o
arm-none-eabi-ld --gc-sections -T layout2.ld -M -o build/out.elf build/module_b.o build/module_c.o
Discarded input sections
.text 0x0000000000000000 0x0 build/module_b.o
.data 0x0000000000000000 0x0 build/module_b.o
.bss 0x0000000000000000 0x0 build/module_b.o
.text._Z10function_cv
0x0000000000000000 0x4 build/module_b.o
.text._Z10function_dv
0x0000000000000000 0x4 build/module_b.o
.text 0x0000000000000000 0x0 build/module_c.o
.data 0x0000000000000000 0x0 build/module_c.o
.bss 0x0000000000000000 0x0 build/module_c.o
...
Instead of a conclusion
And again, the volume of the article is growing, now it is twice as large as the first part. Unfortunately, the layout is a complex topic, and it is difficult to master the "entry". In a week, we will continue to explore the linker and make a full-fledged build script for our embedded applications.
PS As always, many thanks to pfactum for proofreading the text.
