Пример минимального перемещения адреса
Перемещение адреса является одной из важнейших функций связывания.
Итак, давайте посмотрим, как это работает, с минимальным примером.
0) Введение
Резюме: перемещение редактирует секцию .text
объектных файлов для перевода:
- адрес объектного файла
- в окончательный адрес исполняемого файла
Это должно сделать компоновщик, поскольку компилятор видит только один входной файл за раз, но мы должны знать обо всех объектных файлах одновременно, чтобы решить, как:
- разрешить неопределенные символы, такие как объявленные неопределенные функции
- не конфликтовать с несколькими
.text
и .data
разделами нескольких объектных файлов
Предварительные условия: минимальное понимание:
Связывание не имеет ничего общего с C или 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) .текст .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
, которые должны переместить адрес строки приветствия в регистр rsi
, который передается в записьсистемный вызов.
Но подождите!Как может компилятор узнать, где "Hello world!"
окажется в памяти при загрузке программы?
Ну, не может, особенно после того, как мы связали кучу файлов .o
вместе с несколькими .data
section.
Это может сделать только компоновщик, поскольку только у него будут все эти объектные файлы.
Так что компилятор просто:
- помещает значение-заполнитель
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
Формат этого раздела зафиксирован документальноat: 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 , раздел 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). Текст из .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
в младшем порядке) вместо 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
.
И единственное, что есть в разделе данных, - это наша строка приветствия.
Бонусный уровень