Минимальный пример перемещения адреса
Перемещение адреса - одна из важнейших функций связывания.
Итак, давайте посмотрим, как это работает, на минимальном примере.
0) Введение
Резюме: перемещение редактирует .text
раздел объектных файлов для перевода:
- адрес объектного файла
- в окончательный адрес исполняемого файла
Это должно быть сделано компоновщиком, потому что компилятор видит только один входной файл за раз, но мы должны знать обо всех объектных файлах сразу, чтобы решить, как:
- разрешить неопределенные символы, такие как объявленные неопределенные функции
- не конфликтовать с несколькими
.text
и .data
разделами нескольких объектных файлов
Предпосылки: минимальное понимание:
Связывание не имеет ничего общего с C или C ++ конкретно: компиляторы просто генерируют объектные файлы. Затем компоновщик принимает их в качестве входных данных, даже не зная, на каком языке они были скомпилированы. С таким же успехом это мог быть Фортран.
Итак, чтобы уменьшить корку, давайте изучим привет мир 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
регистр, который передается в системный вызов write.
Но ждать! Как компилятор может знать, где "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
, который указывает, какие именно вычисления должны быть выполнены для перевода адреса.
Это поле фактически зависит от процессора и, таким образом, задокументировано в разделе 4.4 «Перемещение» расширения ABI AMD64 System V.
В этом документе говорится, что 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
с прямым порядком байтов) вместо 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 world.
Бонусный уровень