Ответ 1
EDIT: я переместил этот ответ в дубликат: fooobar.com/questions/6282/...
В этом ответе основное внимание уделяется перемещению адресов, что является одной из важнейших функций связывания.
Минимальный пример будет использован для прояснения концепции.
0) Введение
Сводка: перемещение редактирует раздел .text
объектных файлов для перевода:
- адрес объектного файла
- в конечный адрес исполняемого файла
Это должно выполняться компоновщиком, потому что компилятор видит только один входной файл за раз, но мы должны знать обо всех объектных файлах сразу, чтобы решить, как:
- Разрешить undefined символы, как объявленные undefined функции
- не разбивать несколько разделов
.text
и.data
нескольких объектных файлов
Предпосылки: минимальное понимание:
- x86-64 или сборка IA-32
- глобальная структура файла ELF. Я сделал учебник для этого
Связывание не имеет ничего общего с C или С++: компиляторы просто генерируют объектные файлы. Затем компоновщик берет их как входные данные, даже не зная, какой язык их компилировал. Это может быть и Fortran.
Итак, чтобы уменьшить кору, давайте изучим мир приветствия NASM x86-64 ELF Linux:
section .data
hello_world db "Hello world!", 10
section .text
global _start
_start:
; sys_write
mov rax, 1
mov rdi, 1
mov rsi, hello_world
mov rdx, 13
syscall
; sys_exit
mov rax, 60
mov rdi, 0
syscall
скомпилирован и собран с помощью:
nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o
с NASM 2.10.09.
1).text.o
Сначала мы декомпилируем раздел .text
объектного файла:
objdump -d hello_world.o
который дает:
0000000000000000 <_start>:
0: b8 01 00 00 00 mov $0x1,%eax
5: bf 01 00 00 00 mov $0x1,%edi
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
14: ba 0d 00 00 00 mov $0xd,%edx
19: 0f 05 syscall
1b: b8 3c 00 00 00 mov $0x3c,%eax
20: bf 00 00 00 00 mov $0x0,%edi
25: 0f 05 syscall
ключевыми строками являются:
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
который должен переместить адрес строки hello world в регистр rsi
, который передается системному вызову записи.
Но подождите! Как компилятор может узнать, где "Hello world!"
закончится в памяти при загрузке программы?
Ну, это невозможно, особенно после того, как мы связали кучу файлов .o
вместе с несколькими разделами .data
.
Только компоновщик может это сделать, поскольку только у него будут все эти объектные файлы.
Итак, компилятор просто:
- помещает значение-заполнитель
0x0
на скомпилированный вывод - дает дополнительную информацию компоновщику о том, как изменить скомпилированный код с хорошими адресами.
Эта "дополнительная информация" содержится в разделе .rela.text
объектного файла
2).rela.text
.rela.text
означает "перемещение раздела .text".
Перемещение слов используется, потому что компоновщик должен будет переместить адрес из объекта в исполняемый файл.
Мы можем разделить раздел .rela.text
на:
readelf -r hello_world.o
который содержит;
Relocation section '.rela.text' at offset 0x340 contains 1 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000c 000200000001 R_X86_64_64 0000000000000000 .data + 0
Формат этого раздела фиксируется документированным по адресу: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
Каждая запись сообщает компоновщику об одном адресе, который нужно переместить, здесь у нас есть только одна строка.
Упрощая бит, для этой конкретной строки мы имеем следующую информацию:
-
Offset = C
: что является первым байтом.text
, который изменяется в этой записи.Если мы оглянемся на декомпилированный текст, это точно внутри критического
movabs $0x0,%rsi
, а те, которые знают кодировку команд x86-64, заметят, что это кодирует 64-разрядную адресную часть инструкции. -
Name = .data
: адрес указывает на раздел.data
-
Type = R_X86_64_64
, который указывает, что именно нужно сделать для перевода адреса.Это поле фактически зависит от процессора и, таким образом, документировано на AMD64 System V ABI extension в разделе 4.4 "Перемещение".
В этом документе говорится, что
R_X86_64_64
делает:-
Field = word64
: 8 байтов, поэтому00 00 00 00 00 00 00 00
по адресу0xC
-
Calculation = S + A
-
S
- значение на перемещаемом адресе, таким образом00 00 00 00 00 00 00 00
-
A
- это сложение, которое здесь0
. Это поле записи перемещения.
Итак
S + A == 0
, и мы переместимся в самый первый адрес раздела.data
. -
-
3).text.out
Теперь давайте посмотрим на текстовую область исполняемого ld
, сгенерированного для нас:
objdump -d hello_world.out
дает:
00000000004000b0 <_start>:
4000b0: b8 01 00 00 00 mov $0x1,%eax
4000b5: bf 01 00 00 00 mov $0x1,%edi
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
4000c4: ba 0d 00 00 00 mov $0xd,%edx
4000c9: 0f 05 syscall
4000cb: b8 3c 00 00 00 mov $0x3c,%eax
4000d0: bf 00 00 00 00 mov $0x0,%edi
4000d5: 0f 05 syscall
Таким образом, единственное, что изменилось из объектного файла, - это критические строки:
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
которые теперь указывают на адрес 0x6000d8
(d8 00 60 00 00 00 00 00
в little-endian) вместо 0x0
.
Является ли это правильным местом для строки hello_world
?
Чтобы решить, мы должны проверить заголовки программ, которые сообщают Linux, где загружать каждый раздел.
Мы разбираем их с помощью:
readelf -l hello_world.out
который дает:
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000000d7 0x00000000000000d7 R E 200000
LOAD 0x00000000000000d8 0x00000000006000d8 0x00000000006000d8
0x000000000000000d 0x000000000000000d RW 200000
Section to Segment mapping:
Segment Sections...
00 .text
01 .data
Это говорит нам, что раздел .data
, который является вторым, начинается с VirtAddr
= 0x06000d8
.
И единственное, что в разделе данных - это наша мировая строка hello.