Мінімальний приклад переміщення адреси
Переїзд адреси - одна з найважливіших функцій зв'язку.
Тож давайте подивимось, як це працює з мінімальним прикладом.
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) .текст .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
, в якому вказано, що саме потрібно зробити, щоб обчислити адресу.
Це поле фактично залежить від процесора, і таким чином задокументоване в розділі 4.4 "Переміщення" AMD64 System V ABI .
У цьому документі сказано, що 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
.
І єдине, що в розділі даних - це наш привіт світовий рядок.
Рівень бонусу