1, Foreword
-
First understand the contents of the target file( File details of ELF object file)
-
Fact: the code and data segments in the executable file are merged from the input target file
-
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
- 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
- Similar segment merging: now the linker space allocation strategy basically adopts this method
Source code and compilation
- a.c
extern int shared; extern void swap(int *a, int* b); int main(void) { int a = 100; swap(&a, &shared); }
- b.c
int shared = 1; void swap(int *a, int* b) { *a ^= *b ^= *a ^= *b; }
- 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
-
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
-
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
- 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
- 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
- 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)
-
Instruction correction mode
-
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; ...
- 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
-
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)
-
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
-
When dealing with weak symbols (uninitialized global variable definitions), the modern link mechanism adopts the mechanism of COMMON block
-
Case 1: when there are multiple weak symbols and there are multiple COMMON blocks with inconsistent space sizes, the largest block shall prevail
-
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
-
Uninitialized global variables are finally placed in the BSS segment
-
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
-
GCC's "fno COMMON" allows us to treat all uninitialized global variables as COMMON blocks
-
When defining variables, use "_attribute extension", such as int global attribute((nocommon)), which is equivalent to a strong symbol
4, Summary
-
Each segment of the input target file is merged into the output file by similar segment merging
-
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)
-
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