Der Linker ist ein Programm das bei der Erstellung einer ausführbaren Datei eine zentrale Rolle einnimmt. Ein Linkerscript funkiert dabei als Anleitung für dieses Programm.
Wenn Quellcode durch einen Compiler oder Assembler übersetzt wird, entsteht als Zwischenergebnis eine sogenannte Objektdatei. Diese enthält den maschinencode, der jedoch noch nicht ausführbar ist. Das liegt daran, dass der Code in verschiedene Abschnitte unterteilt ist und bestimmte Speicheradressen oder Symbole (z.B. Funktionen oder Variablen) noch unbestimmt bleiben. Diese unbestimmten Verweise werden als “unaufgelöste Symbole” bezeichnet, da ihre exakten Positionen erst beim finalen Zusammenfügen der Dateien festgelegt werden.
Hier kommt der Linker ins Spiel. Seine Hauptaufgabe besteht darin, mehrere Objektdateien zu einer ausführbaren Datei zu verbinden. Dabei löst der Linker die unaufgelösten Symbole auf, indem er die endgültigen Speicheradressen zuweist. Das Ergebnis dieses Prozesses ist eine Datei, in der alle Adressverweise korrekt gesetzt sind. Zudem legt der Linker eine Einstiegsadresse fest, die dem Betriebssystem oder Bootloader als Startpunkt dient, um das Programm auszuführen.
Alle Objektdateien sowie die finale Ausgabedatei bestehen unter anderem aus einer Liste von Sektionen. Jede dieser Sektionen hat einen eindeutigen Namen und eine bestimmte Größe und repräsentiert einen Bereich im Speicher, der für bestimmte Daten oder Programmcode vorgesehen ist. Zu den typischen Sektionen gehören etwa der Bereich für ausführbaren Code und der für globale Daten. Zudem besitzen Objektdateien eine Symboltabelle, die sowohl definierte als auch undefinierte Symbole, wie Funktionen oder Variablen, aufführt. Beim Assemblieren werden Symbole für alle definierten Elemente erzeugt, während Verweise auf externe Funktionen oder Variablen als undefinierte Symbole markiert bleiben. Diese werden erst beim Linken aufgelöst, wenn der genaue Speicherort bekannt ist.
Mit dem tool readelf
lassen sich sowohl die Sektionen, als auch die Symboltabelle einer ausführbaren Datei anzeigen:
readelf -S <dateiname>
zeigt die Sektionen der Datei an:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00008000 008000 0019c4 00 AX 0 0 32
[ 2] .data PROGBITS 00010000 010000 010694 00 WA 0 0 65536
[ 3] .ARM.attributes ARM_ATTRIBUTES 00000000 020694 00002d 00 0 0 1
[ 4] .heap NOBITS 00000000 100000 100000 00 A 0 0 1048576
[ 5] .debug_line PROGBITS 00000000 1206c1 000cd2 00 0 0 1
[ 6] .debug_info PROGBITS 00000000 121393 000344 00 0 0 1
[ 7] .debug_abbrev PROGBITS 00000000 1216d7 0001b8 00 0 0 1
[ 8] .debug_aranges PROGBITS 00000000 121890 0002c0 00 0 0 8
[ 9] .debug_str PROGBITS 00000000 121b50 000292 01 MS 0 0 1
[10] .symtab SYMTAB 00000000 121de4 002970 10 11 610 4
[11] .strtab STRTAB 00000000 124754 001775 00 0 0 1
[12] .shstrtab STRTAB 00000000 125ec9 00007d 00 0 0 1
[Nr]: Die laufende Nummer der Sektion, beginnend bei 0.
Name: Der Name der Sektion (z.B. .text
, .data
).
Type: Der Sektionstyp, der die Art der Daten angibt (z.B. PROGBITS
für ausführbaren Code oder NOBITS
für uninitialisierte Daten).
Addr: Die Adresse im Speicher, an der die Sektion geladen wird.
Off: Der Offset der Sektion innerhalb der Datei, d.h. die Position der Sektion im Dateistream.
Size: Die Größe der Sektion in Bytes.
ES: Die Größe eines Eintrags innerhalb der Sektion (für Sektionen mit Tabelleneinträgen relevant).
Flg: Flags, die verschiedene Eigenschaften der Sektion angeben (z.B. ALLOC
, EXEC
).
Lk: Der Index einer verlinkten Sektion, falls zutreffend (z.B. Verweis auf die Symboltabelle).
Inf: Zusätzliche Information, die je nach Sektionstyp variiert (z.B. Anzahl von Einträgen in einer Tabelle).
Al: Die Ausrichtungsanforderung der Sektion im Speicher (z.B. 4-Byte-Ausrichtung).
readelf -s <dateiname>
zeigt die Symboltabelle an:
Symbol table '.symtab' contains 663 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00008000 0 SECTION LOCAL DEFAULT 1 .text
2: 00010000 0 SECTION LOCAL DEFAULT 2 .data
3: 00000000 0 SECTION LOCAL DEFAULT 3 .ARM.attributes
4: 00000000 0 SECTION LOCAL DEFAULT 4 .heap
5: 00000000 0 SECTION LOCAL DEFAULT 5 .debug_line
6: 00000000 0 SECTION LOCAL DEFAULT 6 .debug_info
...
15: 00007000 0 NOTYPE LOCAL DEFAULT ABS STACK_IRQ
16: 00008000 0 NOTYPE LOCAL DEFAULT 1 $a
17: 00008038 0 NOTYPE LOCAL DEFAULT 1 which_core
18: 00008048 0 NOTYPE LOCAL DEFAULT 1 check_pl
19: 000080fc 0 NOTYPE LOCAL DEFAULT 1 sleep
20: 000080bc 0 NOTYPE LOCAL DEFAULT 1 kernel_entry
21: 00008100 0 NOTYPE LOCAL DEFAULT 1 $d
22: 00000000 0 FILE LOCAL DEFAULT ABS vektor.o
23: 00008120 0 NOTYPE LOCAL DEFAULT 1 $a
...
31: 0000815c 0 NOTYPE LOCAL DEFAULT 1 fiq_handler
32: 00008140 0 NOTYPE LOCAL DEFAULT 1 $d
...
Num: Symbolnummer in der Tabelle, eindeutiger Index.
Value: Adresse oder Wert des Symbols (z.B. Speicherposition).
Size: Größe des Symbols in Bytes.
Type: Symboltyp, z.B. Funktion (FUNC), Variable (OBJECT).
Bind: Bindung, z.B. GLOBAL (in anderen Dateien verfügbar), LOCAL (nur in dieser Datei).
Vis: Sichtbarkeit, z.B. DEFAULT (standard), HIDDEN (versteckt).
Ndx: gibt den Status oder Kontext des Symbols an
Name: Name des Symbols, z.B. Funktions- oder Variablenname.
Ein Linker-Skript gibt dem Linker vor, wie Code und Daten eines Programms im Speicher angeordnet werden.
Ein Linker-Skript besteht aus mehreren Hauptbestandteilen:
Speicherlayout: Definiert, welcher Speicherbereich wo verfügbar ist.
Sektionen: Bestimmt, welche Teile des Programms in welchen Speicherbereich geladen werden sollen.
Optionen: Befehle zur Spezifikation der Architektur, des Einstiegspunkts und anderer Parameter, falls nötig.
Diese Struktur sorgt dafür, dass der Linker das Programm korrekt im Speicher anordnet und bestimmte Symbole oder Optionen an festgelegten Stellen platziert werden können.
Um Speicherplatz für ein Programm zu reservieren, muss der Linker wissen, wie viel Speicher verfügbar ist und an welchen Adressen er sich befindet. Dafür dient die MEMORY-Definition im Linkerskript.
Die Syntax für MEMORY sieht folgendermaßen aus:
MEMORY
{
name [(attr)] : ORIGIN = origin, LENGTH = len
…
}
Dabei bedeuten:
Der Befehl SECTIONS ist das Herzstück eines Linker-Skripts. Er gibt vor, welche Teile des Programms (die sogenannten Sektionen) in welche Speicherbereiche geladen werden sollen. Sektionen im Speicher definieren zusammenhängende Bereiche, in denen Code und Daten organisiert werden. Symbole werden in denselben Abschnitt gelegt, wenn sie zusammen initialisiert werden oder sich im gleichen Speicherbereich befinden sollen.
Im Linker-Skript können diese Sektionen wie folgt zugeordnet werden:
SECTIONS {
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
}
In diesem Beispiel werden alle .text
-Sektionen des Programms zusammengeführt und an der gleichen Speicheradresse platziert. Das Gleiche gilt für die .data
- und .bss
-Sektionen. Das *
-Symbol fungiert als Platzhalter und sorgt dafür, dass alle entsprechenden Sektionen aus den Eingabedateien (z.B. Objektdateien) in den definierten Bereich der Ausgabedatei übernommen werden.
Neben der Zuweisung der Sektionen können im Linker-Skript auch explizit Speicheradressen festgelegt werden. Dadurch wird definiert, wo die Sektionen physisch im Speicher abgelegt werden. Ein Beispiel dafür:
SECTIONS {
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
Hier wird die .text
-Sektion ab der Adresse 0x10000
und die .data
-Sektion ab der Adresse 0x8000000
im Speicher platziert. Durch die Zuweisung von Adressen können Entwickler sicherstellen, dass der Code und die Daten an den richtigen Speicherstellen abgelegt werden, insbesondere bei komplexen Systemen mit unterschiedlichen Speicherbereichen.
Der Einstiegspunkt eines Programms ist die Adresse, an der die Ausführung beginnt, typischerweise die erste Funktion, die nach dem Start des Programms aufgerufen wird. Der Einstiegspunkt wird im Linker-Skript mit dem ENTRY-Befehl definiert:
ENTRY(main)
In diesem Beispiel wird die Ausführung des Programms bei der Funktion main
begonnen. Der Einstiegspunkt ist besonders wichtig für die Initialisierung des Systems und den Start des Programmablaufs.
In einem Linker-Skript spielen Eingabe- und Ausgabesektionen eine zentrale Rolle, wenn es darum geht, wie Code und Daten in der finalen ausführbaren Datei organisiert werden. Eingabesektionen sind Bereiche in Objektdateien, die spezifische Inhalte wie ausführbaren Code (z.B. in .text
) oder Daten (z.B. in .data
) enthalten. Diese Sektionen werden vom Linker verarbeitet und in Ausgabesektionen überführt, welche die Struktur der fertigen Datei bestimmen.
Eine Ausgabesektion, wie etwa .text
, definiert, wo im Speicher die entsprechenden Eingabesektionen aus den Objektdateien abgelegt werden. Der Ausdruck * (.text)
im Linker-Skript weist den Linker an, alle .text
-Sektionen der Eingabedateien in die Ausgabesektion .text
zu packen. Das Sternchen (*
) fungiert dabei als Platzhalter, der sicherstellt, dass alle passenden Eingabesektionen zusammengeführt werden.
Die Ausrichtung der Ausgabesektionen wird von der größten Ausrichtungsanforderung der enthaltenen Eingabesektionen bestimmt. Dies ist wichtig, um sicherzustellen, dass alle Daten korrekt im Speicher platziert und zugänglich sind. Beispielsweise könnte eine .text
-Ausgabesektion so organisiert werden, dass sie den ausführbaren Code bündelt, während .data
den Bereich für initialisierte Daten repräsentiert und .bss
für nicht initialisierte Daten steht.
Im Linker-Skript ist der Lagezähler .
eine zentrale Variable, die die aktuelle Speicheradresse repräsentiert und dabei hilft, Eingabesektionen korrekt in Ausgabesektionen zu platzieren. Zu Beginn hat dieser Zähler den Wert 0, wird jedoch bei jeder neuen Sektion um deren Größe erhöht. Der Lagezähler bestimmt also, wo die nächste Sektion im Speicher platziert wird, sofern keine expliziten Adressen festgelegt wurden.
Der Lagezähler kann jedoch nicht nur die Platzierung von Sektionen steuern, sondern auch dafür sorgen, dass der Code auf bestimmte Speichergrenzen ausgerichtet ist. Besonders bei Architekturen wie ARM ist es wichtig, dass der Code auf 2- oder 4-Byte-Grenzen ausgerichtet ist. Hierfür bietet das Linker-Skript die Funktion ALIGN
, mit der der Lagezähler angepasst werden kann, um das nötige Padding einzufügen und so die Speichergrenzen korrekt zu setzen.
Ein Beispiel für die Ausrichtung der .text
-Sektion auf 4 Bytes sieht wie folgt aus:
SECTIONS
{
.isr_vector : {
exceptions.o (.isr_vector) /* 1023 Bytes */
}
.text : {
. = ALIGN(4);
*(.text)
*(.text*)
}
}
Alternativ können die Speicheradressen auch explizit im Skript festgelegt werden. In diesem Fall würde man beispielsweise die Sektion .isr_vector
auf Adresse 0x08000000
und die .text
-Sektion auf 0x08000400
setzen:
SECTIONS
{
. = 0x08000000;
.isr_vector : {
exceptions.o (.isr_vector)
}
. = 0x08000400;
.text : {
. = ALIGN(4);
*(.text)
*(.text*)
}
}
Eine weitere Methode zur Platzierung von Ausgabesektionen ist die Nutzung von Speicherregionen. Statt jeder Sektion direkt eine Adresse zuzuweisen, definiert man Speicherregionen, die den verfügbaren Speicher des Systems beschreiben:
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
SRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 256K
SDRAM (xrw) : ORIGIN = 0x90000000, LENGTH = 8M
}
Jede Region hat Berechtigungen wie r (lesen), w (schreiben) oder x (ausführen). Diese Attribute werden mit denen der Sektionen abgeglichen. So würden .text
und .rodata
in dem FLASH-Speicher landen, da sie nicht schreibbar sind.
In der Speicherverwaltung von Programmen spielen zwei Schlüsselbegriffe eine wichtige Rolle: die Load Memory Address (LMA) und die Virtual Memory Address (VMA). Diese Begriffe helfen zu definieren, wie und wo im Speicher bestimmte Sektionen des Programms abgelegt und ausgeführt werden.
In den meisten einfachen Fällen sind LMA und VMA identisch – das heißt, der Code wird an der gleichen Adresse geladen und ausgeführt. In komplexeren Szenarien können LMA und VMA jedoch unterschiedlich sein. Ein häufiges Beispiel dafür ist, wenn ein Programm von einem nicht flüchtigen Speicher (wie Flash) in den Arbeitsspeicher (RAM) geladen wird, aber an einer anderen virtuellen Adresse ausgeführt werden muss.
Beispiel: Unterschiedliche LMA und VMA
Mit einem Linker-Skript kann man festlegen, ob LMA und VMA gleich oder unterschiedlich sein sollen. Dies geschieht durch das AT
-Schlüsselwort, das die Ladeadresse (LMA) explizit festlegt.
Ein Beispiel für ein Linker-Skript, das LMA und VMA definiert:
SECTIONS {
.text 0x08000000 : AT(0x08000000) { *(.text) }
.data 0x20000000 : AT(0x08001000) { *(.data) }
}
In diesem Beispiel wird der .text
-Abschnitt sowohl bei der LMA 0x08000000
als auch bei der VMA 0x08000000
platziert, d.h., er wird geladen und zur Laufzeit an derselben Adresse ausgeführt. Der .data
-Abschnitt wird jedoch bei der LMA 0x08001000
geladen, während er zur Laufzeit bei der VMA 0x20000000
ausgeführt wird.
Weitere Informationen zu Linkern, inklusive detailierter Beschreibung aller Linkerskript-Befehle
zurück | Hauptmenü | weiter |
1.3 Assembler und Linker |
---|
1.3.1 Intro |
1.3.2 Grundkonzepte von Linker-Scripten |