Static link details

1, Foreword

  1. First understand the contents of the target file( File details of ELF object file)

  2. Fact: the code and data segments in the executable file are merged from the input target file

  3. The problem to be solved by static linking: how to combine multiple object files to form a usable program or a larger module

2, Allocation of space and address

How to merge into the output file

  1. Stack in order: stack the input target files in order
  • Defect: it wastes space, and the alignment unit of the segment in the address and space is page. If it is less than the size of a page, it will occupy one page, which will cause a lot of internal fragments of memory space
  1. Similar segment merging: now the linker space allocation strategy basically adopts this method

Source code and compilation

  1. a.c
extern int shared;
extern void swap(int *a, int* b);

int main(void)
{
        int a = 100;
        swap(&a, &shared);
}
  1. b.c
int shared = 1;

void swap(int *a, int* b)
{
        *a ^= *b ^= *a ^= *b;
}
  1. Compilation source code and notes
# Compilation environment: 64 bit Ubuntu 16.04
$ gcc -c a.c -fno-stack-protector -o a.o  //The parameter - fno stack protector must be added during compilation, otherwise the link will report an error
$ gcc -c b.c -fno-stack-protector -o b.o
$ ld a.o b.o  -e main -o ab

# If you want to compile a 32-bit file on a 64 bit machine, add the parameter - m32 after gcc

# Baidu answer:
# When compiling the source code to the target file, be sure to add "- fno stack protector", otherwise the function "_stack_chk_fail" will be called by default to check the stack,
# However, it is manually linked without linking to the library file where "_stack_chk_fail", so an error will be reported during the linking process: undefined reference to ` _stack_chk_fail '
# The solution is not to add this parameter during the linking process, but to force gcc not to check the stack during compilation. In addition, add the parameter "- e main" when ld, which means that the main function is used as the program entry, and the default program entry of ld is _start.

Link process analysis

Step 1: allocation of space and address

  1. Work: scan all target files, obtain the segment length of the target file, merge them, calculate the merged length and position of each segment in the output file, and map them

  2. Target file analysis

$ objdump -h a.o 

a.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000002c  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  0000000000000000  0000000000000000  0000006c  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  0000006c  2**0
                  ALLOC
  3 .comment      00000036  0000000000000000  0000000000000000  0000006c  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000a2  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  000000a8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
$ objdump -h b.o 

b.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0000004b  0000000000000000  0000000000000000  00000040  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000004  0000000000000000  0000000000000000  0000008c  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  0000000000000000  0000000000000000  00000090  2**0
                  ALLOC
  3 .comment      00000036  0000000000000000  0000000000000000  00000090  2**0
                  CONTENTS, READONLY
  4 .note.GNU-stack 00000000  0000000000000000  0000000000000000  000000c6  2**0
                  CONTENTS, READONLY
  5 .eh_frame     00000038  0000000000000000  0000000000000000  000000c8  2**3
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA
  1. Executable analysis
$ objdump -h ab 

ab:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         00000077  00000000004000e8  00000000004000e8  000000e8  2**0  # Size: 0x77 = 0x2c + 0x4b (merging. text segment sizes of a.o and b.o)
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .eh_frame     00000058  0000000000400160  0000000000400160  00000160  2**3
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .data         00000004  00000000006001b8  00000000006001b8  000001b8  2**2  # Size: 0x04 = 0x0 + 0x04 (merging. data segment sizes of a.o and b.o)
                  CONTENTS, ALLOC, LOAD, DATA
  3 .comment      00000035  0000000000000000  0000000000000000  000001bc  2**0
                  CONTENTS, READONLY
#explain:
#Before linking, the VMA of all segments of a.o and b.o is 0. Because the virtual space has not been allocated, it is 0 by default
#After linking into ab, each segment is assigned to the corresponding virtual address
#The allocated address of the. text section is 0x004000e8 and the size is 0x77 bytes, and the allocated address of the. data section is 0x006001b8 and the size is 0x04 bytes
#Under linux, i386 ELF executable files are allocated from the address (. text) 0x08048000 by default, while x64 is 0x400000
  1. Under linux, why is the i386 ELF executable assigned from the address (. text) 0x08048000 by default, while x64 is 0x400000 (Reprint)

Step 2: symbol parsing and relocation

  1. Relocation source code analysis
$ objdump -d a.o 

a.o:     file format elf64-x86-64  

Disassembly of section .text:

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	c7 45 fc 64 00 00 00 	movl   $0x64,-0x4(%rbp)
   f:	48 8d 45 fc          	lea    -0x4(%rbp),%rax
  13:	be 00 00 00 00       	mov    $0x0,%esi  # The starting address in the target file code segment starts with 0x0 before space is allocated
  18:	48 89 c7             	mov    %rax,%rdi
  1b:	b8 00 00 00 00       	mov    $0x0,%eax
  20:	e8 00 00 00 00       	callq  25 <main+0x25> #The offset of the near address relative displacement call relative to the next instruction (0x25), where the offset is 0, so the actual call address is 0x25
  25:	b8 00 00 00 00       	mov    $0x0,%eax
  2a:	c9                   	leaveq 
  2b:	c3                   	retq

$ objdump -d ab 

ab:     file format elf64-x86-64

Disassembly of section .text:

00000000004000e8 <main>:
  4000e8:	55                   	push   %rbp
  4000e9:	48 89 e5             	mov    %rsp,%rbp
  4000ec:	48 83 ec 10          	sub    $0x10,%rsp
  4000f0:	c7 45 fc 64 00 00 00 	movl   $0x64,-0x4(%rbp)
  4000f7:	48 8d 45 fc          	lea    -0x4(%rbp),%rax
  4000fb:	be b8 01 60 00       	mov    $0x6001b8,%esi  #Address correction: the shared variable is in the. data section, and the address is 0x006001b8 (calculation formula: S+A)
  400100:	48 89 c7             	mov    %rax,%rdi
  400103:	b8 00 00 00 00       	mov    $0x0,%eax
  400108:	e8 07 00 00 00       	callq  400114 <swap>  # The near address relative displacement call is offset by 0x7 relative to the next instruction (0x40010d), so the call address is 0x400114
  40010d:	b8 00 00 00 00       	mov    $0x0,%eax
  400112:	c9                   	leaveq 
  400113:	c3                   	retq   

0000000000400114 <swap>:
  400114:	55                   	push   %rbp
....

# For 0x7 of 400108 address: if the virtual address of the known function < swap > is 0x400114, the calculation formula is 0x400114 + 0 - (0x4000e8 + 0x25) = 0x7 (calculation formula: S+A-P)
  1. Instruction correction mode

  2. Relocation table (relocation section)

  • In ELF files, there are often one or more segments. For example, the. rel.text above contains the relocation segment of. Text

  • View relocation table

$ objdump -r a.o 

a.o:     file format elf64-x86-64

RELOCATION RECORDS FOR [.text]:  # Active segment
OFFSET           TYPE              VALUE 
0000000000000014 R_X86_64_32       shared
0000000000000021 R_X86_64_PC32     swap-0x0000000000000004


RELOCATION RECORDS FOR [.eh_frame]:
OFFSET           TYPE              VALUE 
0000000000000020 R_X86_64_PC32     .text

# R_X86_64_32: absolute addressing mode correction
# R_X86_64_PC32: relative addressing correction
  • Related structure
$ cat /usr/include/elf.h
...
typedef struct
{
  Elf32_Addr    r_offset;               /* Address */  # For relocatable files, it is the offset, and for executable files or shared files, it is the virtual address of the first byte
  Elf32_Word    r_info;                 /* Relocation type and symbol index */
} Elf32_Rel;

typedef struct
{
  Elf64_Addr    r_offset;               /* Address */
  Elf64_Xword   r_info;                 /* Relocation type and symbol index */
} Elf64_Rel;
...
  1. Symbolic analysis
  • The reason why missing symbols will lead to link errors (i.e. link symbols are not defined at ordinary times) : each target file may define some symbols or reference symbols defined in other target files. During relocation, each relocation entry is a reference to a symbol. When the linker needs to reference and relocate a symbol, it must determine the target address of the symbol. At this time, the linker will find all the input symbols The global symbol table is composed of the symbol table of the target file. After finding the corresponding symbol, it will be relocated.

  • View the symbol table of a.o

$ readelf -s a.o 

Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS a.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     8: 0000000000000000    44 FUNC    GLOBAL DEFAULT    1 main
     9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND shared  # Undefined types. After the linker finishes scanning, these symbols must be found in the global symbol table, otherwise an undefined error will be reported
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND swap

3, COMMON block mechanism

Reasons for existence

  1. The compiler and linker allow different types of weak symbols to exist, but the most essential reason is that the linker does not support symbol types, that is, the linker cannot judge whether the types of each symbol are consistent (but the size can be known)

  2. Early C language programmers often forgot to "extern" to declare variables, making multiple targets produce the definition of the same variable. In order to solve this problem, uninitialized variables are directly treated as COMMON blocks

rule

  1. When dealing with weak symbols (uninitialized global variable definitions), the modern link mechanism adopts the mechanism of COMMON block

  2. Case 1: when there are multiple weak symbols and there are multiple COMMON blocks with inconsistent space sizes, the largest block shall prevail

  3. Case 2: when there is a strong symbol and multiple weak symbols, the space occupied by the symbol of the output result is the same as that of the strong symbol. When the size of a weak symbol is larger than that of the strong symbol during the linking process, the ld linker will issue a warning

//a.c 
#include <stdio.h>

int value = 1;

int main(void)
{
	printf("value:%d\n", value);
	return 0;
}

//b.c
#include <stdio.h>

long value;

//Compilation and warning information
$ gcc a.c b.c 
/usr/bin/ld: Warning: alignment 4 of symbol `value' in /tmp/cckJJ0X1.o is smaller than 8 in /tmp/ccIQn1MA.o

Relationship between uninitialized global variables and BSS segments

  1. Uninitialized global variables are finally placed in the BSS segment

  2. The space occupied by weak symbols after compiler compilation is uncertain, because other compilation units may also use this symbol, and the space occupied is larger, so space cannot be allocated in the BSS section of the target file. However, after the linker reads all input target files, the final size of any weak symbol can be determined, so in the final output Allocate space for BSS segment of file

Extension of GCC

  1. GCC's "fno COMMON" allows us to treat all uninitialized global variables as COMMON blocks

  2. When defining variables, use "_attribute extension", such as int global attribute((nocommon)), which is equivalent to a strong symbol

4, Summary

  1. Each segment of the input target file is merged into the output file by similar segment merging

  2. The linker allocates the space and address in the output file for them. Once the final address of the input segment is determined, the symbol can be parsed and relocated (instruction correction mode)

  3. The linker will parse the references to external symbols in each input target file, and patch the instructions and data to be relocated in each segment to make them point to the correct location

Posted on Sun, 31 Oct 2021 11:09:32 -0400 by skyriders