Ví dụ tối thiểu về việc di dời địa chỉ
Chuyển địa chỉ là một trong những chức năng quan trọng của liên kết.
Vì vậy, hãy xem nó hoạt động như thế nào với một ví dụ tối thiểu.
0) Giới thiệu
Tóm tắt: việc tái định cư sẽ chỉnh sửa .text
phần tệp đối tượng cần dịch:
- địa chỉ tệp đối tượng
- vào địa chỉ cuối cùng của tệp thực thi
Điều này phải được thực hiện bởi trình liên kết vì trình biên dịch chỉ xem một tệp đầu vào tại một thời điểm, nhưng chúng ta phải biết về tất cả các tệp đối tượng cùng một lúc để quyết định cách:
- giải quyết các ký hiệu không xác định như các hàm không xác định đã khai báo
- không đụng độ nhiều
.text
và nhiều .data
phần của nhiều tệp đối tượng
Điều kiện tiên quyết: hiểu biết tối thiểu về:
Liên kết không liên quan gì đến C hoặc C ++ cụ thể: trình biên dịch chỉ tạo các tệp đối tượng. Sau đó, trình liên kết lấy chúng làm đầu vào mà không bao giờ biết ngôn ngữ nào đã biên dịch chúng. Nó cũng có thể là Fortran.
Vì vậy, để giảm bớt lớp vỏ, chúng ta hãy nghiên cứu một NASM x86-64 ELF Linux xin chào thế giới:
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
được biên dịch và lắp ráp với:
nasm -o hello_world.o hello_world.asm
ld -o hello_world.out hello_world.o
với NASM 2.10.09.
1) .text của .o
Đầu tiên, chúng tôi dịch ngược .text
phần của tệp đối tượng:
objdump -d hello_world.o
mang lại:
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
những dòng quan trọng là:
a: 48 be 00 00 00 00 00 movabs $0x0,%rsi
11: 00 00 00
mà sẽ chuyển địa chỉ của chuỗi hello world vào thanh rsi
ghi, địa chỉ này được chuyển đến lệnh gọi hệ thống ghi.
Nhưng đợi đã! Làm thế nào trình biên dịch có thể biết vị trí "Hello world!"
sẽ kết thúc trong bộ nhớ khi chương trình được tải?
Chà, nó không thể, đặc biệt là sau khi chúng tôi liên kết một loạt các .o
tệp với nhiều .data
phần.
Chỉ trình liên kết mới có thể làm điều đó vì chỉ anh ta mới có tất cả các tệp đối tượng đó.
Vì vậy, trình biên dịch chỉ:
- đặt giá trị giữ chỗ
0x0
trên đầu ra đã biên dịch
- cung cấp thêm một số thông tin cho trình liên kết về cách sửa đổi mã đã biên dịch với các địa chỉ tốt
"Thông tin bổ sung" này được chứa trong .rela.text
phần của tệp đối tượng
2) .rela.text
.rela.text
viết tắt của "chuyển vị trí của phần .text".
Từ chuyển vị trí được sử dụng vì trình liên kết sẽ phải chuyển địa chỉ từ đối tượng vào tệp thực thi.
Chúng tôi có thể tháo rời .rela.text
phần bằng:
readelf -r hello_world.o
trong đó chứa;
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
Định dạng của phần này được cố định thành tài liệu tại: http://www.sco.com/developers/gabi/2003-12-17/ch4.reloc.html
Mỗi mục nhập cho trình liên kết biết về một địa chỉ cần được di dời, ở đây chúng tôi chỉ có một địa chỉ cho chuỗi.
Đơn giản hóa một chút, đối với dòng cụ thể này, chúng tôi có thông tin sau:
Offset = C
: byte đầu tiên của .text
mục này thay đổi là gì.
Nếu chúng ta nhìn lại văn bản đã dịch ngược, nó chính xác nằm bên trong lệnh quan trọng movabs $0x0,%rsi
và những người biết mã hóa lệnh x86-64 sẽ nhận thấy rằng đoạn văn bản này mã hóa phần địa chỉ 64-bit của lệnh.
Name = .data
: địa chỉ trỏ đến .data
phần
Type = R_X86_64_64
, chỉ định chính xác những gì tính toán phải được thực hiện để dịch địa chỉ.
Trường này thực sự phụ thuộc vào bộ xử lý, và do đó được ghi lại trên phần mở rộng AMD64 System V ABI 4.4 "Chuyển vị trí".
Tài liệu đó nói rằng R_X86_64_64
:
Field = word64
: 8 byte, do đó 00 00 00 00 00 00 00 00
địa chỉ tại0xC
Calculation = S + A
S
là giá trị tại địa chỉ được di dời, do đó00 00 00 00 00 00 00 00
A
là addend 0
ở đây. Đây là một trường của mục nhập tái định cư.
Vì vậy, S + A == 0
và chúng tôi sẽ được chuyển đến địa chỉ đầu tiên của .data
phần.
3) .text của .out
Bây giờ, hãy xem vùng văn bản của tệp thực thi ld
được tạo cho chúng tôi:
objdump -d hello_world.out
cho:
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
Vì vậy, điều duy nhất thay đổi từ tệp đối tượng là các dòng quan trọng:
4000ba: 48 be d8 00 60 00 00 movabs $0x6000d8,%rsi
4000c1: 00 00 00
mà bây giờ trỏ tới địa chỉ 0x6000d8
( d8 00 60 00 00 00 00 00
bằng little-endian) thay vì 0x0
.
Đây có phải là vị trí thích hợp cho hello_world
chuỗi không?
Để quyết định, chúng ta phải kiểm tra các tiêu đề chương trình, tiêu đề này cho Linux biết nơi tải từng phần.
Chúng tôi tháo rời chúng bằng:
readelf -l hello_world.out
mang lại:
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
Điều này cho chúng ta biết rằng .data
phần, là phần thứ hai, bắt đầu tại VirtAddr
= 0x06000d8
.
Và thứ duy nhất trên phần dữ liệu là chuỗi hello world của chúng tôi.
Mức thưởng