Linker Script (Beginner)

Relocatable Object Files

The running examples for this part will be the following two .c codes:

// main.c
extern swap();
int buf[2] = {1,2};
 
int main() {
	swap();
	return 0;
}
// swap.c
extern int buf[];
 
int* bufp0 = &buf[0]
int* bufp1;
 
void swap() {
	int temp;
	
	bufp1 = &buf[1];
	temp = *bufp0;
	*bufp0 = *bufp1;
	*bufp1 = temp;
}

Then we compile the source files using gcc tool. In order to do the compilation only and no linking, we need to use the -c flag.

gcc -c main.c swap.c

We can inspect the symbol table section of each object file as follows:

# readelf -s main.o
Symbol table '.symtab' contains 6 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     8 OBJECT  GLOBAL DEFAULT    3 buf
     4: 0000000000000000    25 FUNC    GLOBAL DEFAULT    1 main
     5: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap
 

and similarly for the swap.o

# readelf -s swap.o
Symbol table '.symtab' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS swap.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000     8 OBJECT  GLOBAL DEFAULT    5 bufp0
     4: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND buf
     5: 0000000000000000     8 OBJECT  GLOBAL DEFAULT    4 bufp1
     6: 0000000000000000    67 FUNC    GLOBAL DEFAULT    1 swap

Apart from the extra symbols related to the compilation chain, we have

object filesymbol table
main.omain.c, .text, buf, main, swap
swap.oswap.c, .text, bufp0, buf, bufp, swap
Note that since the variable int temp is a local variables, it is allocated on stack in the run time, so it does not appear in the symbols table.

Also, we can inspect the relocation entries of these object files with the command readelf -r.

# readelf -r main.o
 
Relocation section '.rela.text' at offset 0x198 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000e  000500000004 R_X86_64_PLT32    0000000000000000 swap - 4

And similarly for swap.o we will have

# readelf -r swap.o
 
Relocation section '.rela.text' at offset 0x1e0 contains 6 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000b  000400000002 R_X86_64_PC32     0000000000000000 buf + 0
000000000012  000500000002 R_X86_64_PC32     0000000000000000 bufp1 - 4
000000000019  000300000002 R_X86_64_PC32     0000000000000000 bufp0 - 4
000000000025  000500000002 R_X86_64_PC32     0000000000000000 bufp1 - 4
00000000002c  000300000002 R_X86_64_PC32     0000000000000000 bufp0 - 4
000000000037  000500000002 R_X86_64_PC32     0000000000000000 bufp1 - 4
 
Relocation section '.rela.data.rel' at offset 0x270 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000000  000400000001 R_X86_64_64       0000000000000000 buf + 0

The information above signals that these object files are not linked yet, and are not ready for execution.

Un-linked Object Files

Un-linked object files are not ready for execution as their .data and .text regions are full of patches (placeholders) that needs to be completed by a linker.

Linking and Linker Script

Linker script has two main jobs

  • To place sections like .text and .data in appropriate memory layout, and finalize their addresses
  • To patch the place holders corresponding to the global variables and function calls according to their finalized address.

In our exposition here, we will use the GNU linker (ld command). The official document for this software is very well written and easy to follow (see References), and we will follow a very similar exploration path.

A linker scrip *.ld is written in the linker command syntax and specifies all required information by the linker to do the linking. The default linker scrip is compiled into the binary, and can be accessed in running the linker command with --verbose command. A custom linker script can be provided by -T flag.

A Simple Linker Script

I will not be discussing the anatomy and syntax for a linker scrip (see the GNU linker document for that). Instead I will walk through a simple example to highlight important aspects of the linking. Consider the following linker script:

/* linker.ld */
SECTIONS 
{
	. = 0xDEADBEEF;
	.text : {
		*(.text)
	}
	. = 0xBAADF00D;
	.data : {
		*(.data)
	}
	. = 0xFEE1DEAD;
	.bss : {
		*(.bss)
	}
}

linking the object files with this linker script with the command
ld -T linker.ld main.o swap.o -o main we will obtain the main executable.
Inspecting the contents of the symbols table we will bet

# readelf -s main
 
Symbol table '.symtab' contains 8 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS swap.c
     3: 00000000baadf018     8 OBJECT  GLOBAL DEFAULT    5 bufp0
     4: 00000000deadbf08    67 FUNC    GLOBAL DEFAULT    1 swap
     5: 00000000deadbeef    25 FUNC    GLOBAL DEFAULT    1 main
     6: 00000000baadf010     8 OBJECT  GLOBAL DEFAULT    4 buf
     7: 00000000fee1deb0     8 OBJECT  GLOBAL DEFAULT    6 bufp1

where we see that the symbol tables of the previously separate object files are now combined, and also they have their finalized values .
We can also inspect the .data and .text parts of the elf file to observe that they indeed sit in correct memory locations:

# readelf -x .data main
Hex dump of section '.data':
  0xbaadf010 01000000 02000000 
  
# readelf -x .text main
Hex dump of section '.text':
  0xdeadbeef f30f1efa 554889e5 b8000000 00e80700 ....UH..........
  0xdeadbeff 0000b800 0000005d c3f30f1e fa554889 .......].....UH.
  0xdeadbf0f e5488d05 fd3000dc 48890592 1f342048 .H...0..H....4 H
  0xdeadbf1f 8b05f330 00dc8b00 8945fc48 8b157f1f ...0.....E.H....
  0xdeadbf2f 3420488b 05e03000 dc8b1289 10488b05 4 H...0......H..
  0xdeadbf3f 6d1f3420 8b55fc89 10905dc3          m.4 .U....].
 

One interesting fact from above is that while in the linker script we have specified for the .data section to sit at 0xBAADF00D, the inspection above shows that it actually sits in the memory location 0xBAADF010. The reason is that the memory location 0xBAADFOOD is not 8 byte aligned memory address (it is not a multiple of 8). That is why the linker decides to put it in the nearest aligned memory address which is 0xBAADF010.

References

Most of the content here is a combination of stuff I learned from the following resources:

  1. STM32CubeIDE user guide: Section 2.2
  2. Computer Systems: A Programmer’s Perspective by Bryant: Chapter 7
  3. The GNU linker official document: Chapter 3,