Liên kết động trong Linux và Windows (Phần 1)
Phần I
Bài báo này thảo luận về khái niệm thư viện chia sẻ trong cả Windows và
Linux. Đồng thời lướt qua các kiểu cấu trúc dữ liệu để giải thích liên kết
động làm việc như thế nào trong các hệ điều hành này. Bài này rất hữu ích
cho các nhà phát triển hứng thú nghiên cứu vấn đề về các hàm ẩn bảo mật,
liên quan tới tốc độ liên kết động. Và cũng khẳng định một số kiến thức cơ
bản về liên kết động đã được đưa ra trước đây.
Phần một giới thiệu các khái niệm cho cả Linux và Windows, nhưng cơ bản
tập trung trên Linux. Lần tới, trong phần hai chúng ta sẽ thảo luận chúng làm
việc trong Windows như thế nào và sau đó là so sánh hai môi trường với
nhau.
Thư viện tĩnh và thư viện động
Thư viện là một tập hợp các chương trình con cho phép mã chương trình
được chia sẻ và thay đổi theo kiểu modul. Các chương trình chạy và thư viện
liên hệ với nhau theo một tiến trình gọi là linking (liên kết), làm việc qua
một cầu nối (linker).
Thư viện có thể chia thành hai loại: thư viện tĩnh và thư viện chia sẻ.
Thư viện tĩnh là một tập hợp các file kiểu đối tượng. Theo quy ước, các file
này có đuôi kết thúc là “.a” trong UNIX và “.lib” trong Windows. Khi một
chương trình được liên kết ngược với một thư viện tĩnh, mã máy từ các file
đối tượng cho bất kì hàm mở rộng dùng trong chương trình sẽ được sao chép
từ thư viện vào chương trình chạy cuối cùng.
Ngược lại với thư viện tĩnh, mã lệnh trong thư viện chia sẻ không giới hạn
Code – mã độc lập vị trí). Các đối tượng chia sẻ trong Linux thường có PIC
để tránh phải định vị lại vị trí thư viện trong thời gian tải. Tất cả các trang
mã lệnh có thể được chia sẻ giữa toàn bộ tiến trình dùng cùng một thư viện
và có thể được lập trang tới (hoặc từ) hệ thống file. Trong dòng x86, không
có cách đơn giản nào để định địa chỉ dữ liệu liên quan tới khu vực hiện tại,
kể từ khi tất cả các jump và các call là kiểu liên hệ cấu trúc con trỏ. Do đó,
tất cả các tham chiếu tới khu vực địa chỉ tĩnh mở rộng được thực hiện trực
tiếp qua một bảng, gọi là bảng GOT (Global Offset Table).
Liên kết động trong Linux
Cấu trúc dữ liệu ELF
Vì đây không phải là bài báo đặe tả định dạng kiểu ELF, chúng ta sẽ chỉ
thảo luận về các cấu trúc dữ liệu, liên quan tới nội dung mà chúng ta đang
xem xét. Đối với liên kết động, các cầu nối ELF cơ bản dùng hai bảng đặc
trưng theo bộ xử lý: Global Offset Table (GOT) và Procedure Linkage Table
(PLT).
Global Offset Table (GOT) - Bảng địa chỉ Offset mở rộng
Các mối liên kết ELF hỗ trợ mã PIC qua bảng GOT trong từng thư viện
chia sẻ. GOT chỉ chứa địa chỉ của tất cả các dữ liệu tĩnh dùng trong chương
trình. Địa chỉ của GOT thông thường được lưu trữ trong một thanh ghi
(EBX), trong đó một địa chỉ quan hệ của mã lệnh được dùng.
Procedure Linkage Table (PLT) - Bảng liên kết các chương trình con
Cả chương trình chạy sử dụng thư viện chia sẻ và chính bản thân thư viện
chia sẻ đều có một bảng PLT. Tương tự như cách bảng GOT gửi lại các tính
vẫn với giá trị băm như thê, và dùng chỉ mục lấy ra từ Chain
[symindx].
Thực hiện như thế nào
Trong Linux, tự bản thân mối liên kết động ld.so đã là một thư viện chia sẻ
ELF. Khi chương trình khởi động, hệ thống vẽ bản đồ ld.so thành một phần
của không gian địa chỉ và chạy mã lệnh bootstrap của nó. Đường vào chính
của bộ nạp được định nghĩa trong dl_main(elf/rtld.c). Cầu nối định vị và giải
quyết, tham chiếu tới thường trình riêng của nó. Đó là việc cần thiết để tải
mọi thứ về.
Phân đoạn tĩnh (nằm ở phần tiêu đề chương trình) trong file ELF bao gồm
một con trỏ, trỏ tới bảng xâu của file (DT_STRTAB), cũng như là mục vào
DT_NEEDED. Mỗi một phần tử đó bao gồm phần offset trong bảng xâu tên
của thư viện yêu cầu. Mối liên kết động tạo ra một danh sách phạm vi
chương trình chạy, bao gồm cả các thư viện để tải.
Chúng ta có hai cách đặc tả đối tượng trước khi tải. Hoặc là qua môi trường
biến LD_PRELOAD, hoặc là qua file /etc/ld.so.preload. Cách sau có thể
được dùng khi hàng rào an toàn ngăn cản dùng qua môi trường biến. Bộ nạp
thêm mục vào DT_NEEDED của chương trình chạy, cũng như là phạm vi
sau các đường vào trước khi tải.
Với mỗi một đường vào trong phạm vi, mối liên kết sẽ tìm file chứa thư
viện. Mỗi khi thư viện được tìm ra, mối liên kết sẽ đọc tiêu dề ELF để tìm
tiêu đề chương trình, được trỏ bởi phân đoạn động. Mối liên kết sẽ nạp sơ đồ
thư viện vào không gian địa chỉ chương trình. Từ phân đoạn động, nó thêm
bảng biểu tượng của thư viện vào chain của các bảng biểu tưọng. Và nếu các
thư viện phụ thuộc nhiều hơn, nó sẽ thêm vào một danh sách được tải, và
Điạ chỉ trong lệnh gọi (0x80483a4) là một đường vào trong bảng PLT. Bốn
đường vào đầu tiên trong PLT (nhờ đó lối vào thứ 3 và thứ 4 được duy trì) là
phổ biến cho tất cả các hàm cuộc gọi. Phần còn lại của đường vào được
nhóm lại thành một khối, mỗi khối gồm 3 đường vào và tương ứng cho một
hàm. Điều này được chỉ ra trong hình 2.
Hình 2. Đường vào đầu tiên trong PLT.
Hình 3. Bảng GOT trên đĩa.
Câu lệnh bao gồm một bước nhảy tới địa chỉ trong đường vào của bảng
GOT *(GOT + 0x14), và lại trỏ vào đường vào tiếp theo trong bảng PLT
viz 0x80483aa (như đã được chỉ ra trong hình 2).
Các câu lệnh tiếp theo thực hiện với các địa chỉ dùng mối liên kết động. Câu
lệnh nhảy đẩy một khoảng chứa trống (0x10). Đây là một khoảng trống
trong bảng định vị lại file, trỏ tới biểu tượng được yêu cầu trong bảng biểu
tượng, và địa chỉ trỏ tới đường vào GOT (0x804963c).
Hình 4. Đường vào định vị lại vị trí 8 byte (RELSZ).
Như đã thấy trên hình 4, kích thước của đường vào định vị lại vị trí là 8 byte
(RELSZ). Khoảng trống 0x10 cho chúng ta đường vào số 3 trong bảng
.rel.plt, với đường vào là cho m. Lối vào khoảng trống trong bảng tương ứng
với địa chỉ GOT, phải được cập nhật.
Đoạn mã lệnh sau nhảy tới đường vào đầu tiên trong PLT, là phần chung.
Hình 5. Điểm ngắt 1.
Đường vào trong PLT lại nhảy tới địa chỉ trong GOT + 8. Bộ nạp tại thời
gian tải cập nhật đường vào trong GOT + 4 và GOT + 8 (sớm hơn
0x000000 như đã thấy trong hình 3). Bây giờ GOT + 8 (0x 4000bcb0) trỏ
Chúng có thể thay đổi mà không cần thông báo */
/* Con trỏ chỉ mục trỏ tới khu vực động*/
ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM +
DT_VERSIONTAGNUM
+ DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
const ElfW(Phdr) *l_phdr; /* Con trỏ trỏ tới bảng tiêu
đề chương trình trong lõi */
ElfW(Addr) l_entry; /* đường vào trỏ khu vực */
ElfW(Half) l_phnum; /* Số đường vào tiêu đề chương
trình */
ElfW(Half) l_ldnum; /* Số đường vào phân đoạn động */ /* Mảng phụ thuộc DT_NEEDED và các phụ thuộc của nó, trong
câu lện phụ thuộc để tra tìm biểu tượng (có hoặc không có bản sao).
Không có đường vào trước phụ thuộc được tải */
struct r_scope_elem l_searchlist; /* Symbol hash table */
Elf_Symndx l_nbuckets;
const Elf_Symndx *l_buckets, *l_chain;
Tại thời gian việc thực thi nhảy vào thường trình phân giải biểu tượng,
chúng ta có địa chỉ của bản đồ liên kết và khoảng trống định vị lại trong
ngăn xếp. Khoảng trống định vị lại đã được nói ở trên đưa ra chỉ mục trong
bảng biểu tượng cho tên biểu tưọng và tưong ứng với địa chỉ GOT, nơi các
địa chỉ phân giải sẽ được ghi. Điạ chỉ phân giải đối tượng (GOT+8) trỏ tới
một trampoline ELF_MACHINE_RUNTIME_TRAMPOLINE.
Thành phần _dl_lookup_symbol() khi trả lại lại gọi do_lookup() cho từng
đường vào trong mảng scope. Mảng scope bao gồm các phần tử thuộc kiểu
cấu trúc r_scope_elem cho các thư viện. Nó được xác định như là một phần
của phạm vi tìm kiếm mở rộng. Cấu trúc này được làm đầy tại thời gian tải.
struct r_scope_elem
{
/* Array of maps for the scope. */
struct link_map **r_list;
/* Number of entries in the scope. */
unsigned int r_nlist;
};
Lời gọi do_lookup được định nghĩa cho FCT trong do-lookup.h. Bây giờ hãy
xem xét một chút về nó từ một phối cảnh logic trong tiếng Anh thuần tuý, để
làm cho nó dễ hiểu hơn. Thực hiện như sau:
Do_lookup algorithm()
{
For each of the link_map structures in scope->r_list
do{
Get the symtable address from link_map->l_info
Get the strtable address from link_map->l_info
Tìm kiếm hash buket thích hợp trong các đối tượng bảng biểu tượng dùng
thủ tục băm tên biểu tượng trong _dl_lookup_symbol().
Dùng đường dẫn chỉ mục trong chuỗi link_map -> l_chain
Do {
Tra tìm đường dẫn bảng biểu tượng dùng chỉ mục. So ánh tên biểu tượng với
(strtab + sym->st_name).
Nếu tìm thấy, trả lại đường vào bảng biểu tượng với cấu trúc link_map;
}