Linkers and Loaders in Linux Userland
Một chương trình thường nằm trên đĩa dưới dạng một file thực thi nhị phân (executable). Để chạy được trên CPU, chương trình phải được đưa vào bộ nhớ và đặt trong bối cảnh của một tiến trình. Quá trình này bắt đầu từ các file mã nguồn. Trình biên dịch (compiler) sẽ xử lý các file mã nguồn này để tạo ra các file đối tượng (object files). Các file đối tượng này được thiết kế để có thể nạp vào bất kỳ vị trí nào trong bộ nhớ vật lý, vì vậy chúng có định dạng được gọi là file đối tượng có thể tái định vị (relocatable object file).
Preprocessing
Một trong những bước đầu tiên trước khi biên dịch là giai đoạn tiền xử lý (preprocessing). Ở giai đoạn này, một chỉ thị mà chúng ta thường gặp là #include. Ngoài ra còn có các chỉ thị quan trọng khác như #define để tạo macro và hằng số, hay các chỉ thị biên dịch có điều kiện như #if và #ifdef. Cần lưu ý rằng tất cả chúng đều là chỉ thị tiền xử lý (preprocessor directive), không phải là câu lệnh trong C, và chúng hoạt động trước khi compiler bắt đầu biên dịch mã nguồn. Quay lại với #include, cơ chế của nó như sau: bộ tiền xử lý sao chép toàn bộ nội dung của file được include và dán vào đúng vị trí của dòng #include. Vì vậy, có thể hình dung nó như một lệnh COPY-PASTE văn bản thuần túy.
Giả sử chúng ta có một file header lib1.h:
void foo();
int add(int a, int b);Và một file main.c sử dụng file header này:
#include "lib1.h"
int main() {
foo();
return 0;
}Khi bộ tiền xử lý chạy, nó sẽ thấy dòng #include "lib1.h" và thay thế hoàn toàn dòng đó bằng nội dung của file lib1.h. Kết quả là, file main.c mà trình biên dịch thực sự “nhìn thấy” sẽ có nội dung như sau:
// --- Nội dung copy từ lib1.h ---
void foo();
int add(int a, int b);
// --- Hết nội dung lib1.h ---
int main() {
foo();
return 0;
}Dòng #include "lib1.h" đã biến mất và được thay thế hoàn toàn.
Chúng ta có thể xem kết quả của giai đoạn tiền xử lý bằng cách yêu cầu gcc chỉ chạy bộ tiền xử lý mà không biên dịch, thông qua tùy chọn -E.
gcc -E main.c -o main.iLệnh trên sẽ tạo ra file main.i. Nếu xem nội dung file này, ta sẽ thấy kết quả đúng như mô tả:
$ gcc -E main.c -o main.i
$ cat main.i
# 0 "main.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "main.c"
# 1 "lib1.h" 1
void foo();
int add(int a, int b);
# 2 "main.c" 2
int main() {
foo();
return 0;
}Compiling
Sau khi giai đoạn tiền xử lý hoàn tất, chúng ta có các file mã nguồn đã sẵn sàng để được biên dịch. Quay trở lại ví dụ, file lib1.h chỉ cung cấp các khai báo (declarations) cho các hàm foo() và add(). Tuy nhiên, phần triển khai (implementation) của chúng lại nằm trong file lib1.c.
Vì vậy, chỉ có file header là không đủ để trình liên kết (linker) làm việc. Chúng ta bắt buộc phải biên dịch file lib1.c chứa mã nguồn thực thi đó. Bước tiếp theo là biên dịch từng file .c thành các file đối tượng (object files), thường có đuôi là .o. Các file này chứa mã máy nhưng chưa phải là một chương trình hoàn chỉnh. Chúng ta sử dụng tùy chọn -c của gcc để yêu cầu trình biên dịch chỉ thực hiện bước này và dừng lại trước khi liên kết:
$ gcc -c main.c -o main.o
$ gcc -c lib1.c -o lib1.oTùy chọn -c chỉ biên dịch mã nguồn thành file đối tượng chứ không thực hiện liên kết. Kết quả chúng ta nhận được là:
main.o: Chứa mã máy đã được biên dịch từmain.c. Bên trong file này, các lời gọi hàmfoo()vàadd()vẫn là các tham chiếu chưa được giải quyết (unresolved symbols), vì mã nguồn của chúng không có ở đây.lib1.o: Chứa mã máy của các hàmfoo()vàadd()được biên dịch từlib1.c. Đây chính là nơi định nghĩa các ký hiệu (symbols) màmain.ođang tìm kiếm.
Linking
Sau khi đã có các file đối tượng (.o), bước tiếp theo là liên kết (linking). Trình liên kết (linker) sẽ kết hợp các file đối tượng được cung cấp (như main.o, lib1.o) và các thư viện hệ thống cần thiết để tạo thành một executable file duy nhất. Tại bước này, linker sẽ giải quyết các tham chiếu chéo, ví dụ như kết nối lời gọi hàm foo() trong main.o với định nghĩa của nó trong lib1.o. Có hai phương pháp chính để thực hiện việc liên kết này: liên kết tĩnh và liên kết động.
Static Linking
Với liên kết tĩnh, linker sao chép toàn bộ mã máy cần thiết vào file thực thi tại thời điểm biên dịch. Kết quả là một file duy nhất, độc lập, nhưng kích thước lớn.
Linker làm việc với ba loại file chính:
.o(object file): Kết quả biên dịch từ một file.c. Chứa mã máy nhưng chưa được linked..a(static library): Archive chứa nhiều file.o, tạo bằng lệnhar. Ví dụ:libc.a..so(shared library): File dùng cho dynamic linking. Ví dụ:libc.so.6vàld-linux.so.2(dynamic linker).
Trong ví dụ đơn giản này, ta chỉ cần truyền tất cả các file object cho linker. Toàn bộ mã máy từ lib1.o sẽ được sao chép vào file program_static. Tuy nhiên, libc vẫn được link động:
$ gcc main.o lib1.o -o program_staticNếu muốn link tĩnh toàn bộ (full static), bao gồm cả libc.a, cần thêm flag -static:
$ gcc -static main.o lib1.o -o program_static_fullVề mặt bộ nhớ, khi một chương trình liên kết tĩnh được chạy, toàn bộ thư viện sẽ được nạp vào RAM. Nếu nhiều tiến trình cùng chạy chương trình này, mỗi tiến trình sẽ có một bản sao riêng của thư viện, gây lãng phí bộ nhớ.
Dynamic Linking
Đây là phương pháp mặc định trên hầu hết các hệ điều hành hiện đại. Với liên kết động, thay vì sao chép mã từ thư viện, trình liên kết chỉ ghi lại tên của thư viện chia sẻ (shared library, thường có đuôi .so trên Linux) và tên các hàm mà chương trình sẽ cần vào file thực thi.
Quá trình liên kết thực sự sẽ được trì hoãn cho đến khi chương trình được chạy. Lúc đó, một trình liên kết động của hệ điều hành sẽ tìm các file .so cần thiết, nạp chúng vào bộ nhớ và giải quyết các tham chiếu. Vì không chứa mã thư viện, file thực thi tạo ra nhỏ gọn.
Để tạo ví dụ liên kết động, trước tiên chúng ta cần tạo một thư viện chia sẻ từ lib1.c.
# -fPIC - Position Independent Code - code có thể chạy ở bất kỳ địa chỉ nào trong bộ nhớ mà không cần chỉnh sửa
$ gcc -fPIC -c lib1.c -o lib1.o
# Tạo thư viện chia sẻ lib1.so từ lib1.o
$ gcc -shared -o lib1.so lib1.o
# Liên kết main.o với thư viện chia sẻ
$ gcc main.o ./lib1.so -o program_dynamicKhi chạy program_dynamic, hệ điều hành sẽ cần tìm và nạp lib1.so vào bộ nhớ. Có thể cần chỉ định đường dẫn cho trình liên kết động bằng biến môi trường LD_LIBRARY_PATH: export LD_LIBRARY_PATH=. && ./program_dynamic
Ưu điểm chính của liên kết động là tiết kiệm bộ nhớ. Phần mã nguồn của thư viện chia sẻ (vùng .text chỉ đọc) chỉ được nạp vào RAM một lần duy nhất. Tất cả các tiến trình cùng sử dụng thư viện này sẽ cùng tham chiếu đến một vùng nhớ chung đó. Mỗi tiến trình vẫn có vùng dữ liệu ghi được (.data, .bss) của riêng mình để đảm bảo chúng không can thiệp lẫn nhau.
Bây giờ chúng ta sang một ví dụ khác để theo dõi quá trình dynamic linking, một chương trình C sử dụng hàm printf từ thư viện chuẩn (stdio).
Đây là mã nguồn main.c:
#include <stdio.h>
int main() {
printf("Hello World\n");
return 0;
}Chúng ta sẽ biên dịch file này thành một file đối tượng (.o) bằng lệnh sau:
$ gcc -g -fno-builtin -c main.c
# -g: Thêm thông tin debug
# -fno-builtin: Tắt tối ưu hóa built-in functions (giữ nguyên printf thay vì tự động chuyển thành puts)Sau khi chạy lệnh biên dịch, ta có file main.o. Dùng lệnh file để xem thông tin về nó, ta sẽ thấy sự có mặt của debug_info do cờ -g tạo ra:
$ file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), with debug_info, not strippedFile main.o này chứa mã máy của hàm main, nhưng các tham chiếu đến dữ liệu (chuỗi “Hello World”) và hàm bên ngoài (printf) vẫn chưa được giải quyết. Chúng ta có thể thấy rõ điều này bằng cách xem mã Assembly đã được dịch ngược của nó.
$ objdump -M intel -d -S main.o
main.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
#include <stdio.h>
int main() {
0: f3 0f 1e fa endbr64
4: 55 push rbp
5: 48 89 e5 mov rbp,rsp
printf("Hello World\n");
8: 48 8d 05 00 00 00 00 lea rax,[rip+0x0] # f <main+0xf>
f: 48 89 c7 mov rdi,rax
12: b8 00 00 00 00 mov eax,0x0
17: e8 00 00 00 00 call 1c <main+0x1c>
return 0;
1c: b8 00 00 00 00 mov eax,0x0
}
21: 5d pop rbp
22: c3 retTrong đoạn mã trên, hãy chú ý hai dòng:
lea rax,[rip+0x0]: Lệnh này nạp địa chỉ của chuỗi “Hello World” vàorax. Tuy nhiên, tại thời điểm này, địa chỉ thực sự chưa được xác định, nên trình biên dịch tạm thời dùng một giá trị placeholder (rip+0x0).call 1c <main+0x1c>: Tương tự, lệnh này gọi đến hàmprintf. Địa chỉ củaprintfcũng chưa được biết, nên một giá trị placeholder khác được sử dụng.
Vậy làm thế nào để linker biết cần phải sửa những gì? Câu trả lời nằm trong các bản ghi tái định vị (relocation records). File object chứa một danh sách các chỉ dẫn cho linker.
$ objdump -r main.o
main.o: file format elf64-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
000000000000000b R_X86_64_PC32 .rodata-0x0000000000000004
0000000000000018 R_X86_64_PLT32 printf-0x0000000000000004
...Bảng trên cho linker biết:
- Tại offset
0xbtrong section.text(nơi lệnhlealưu địa chỉ chuỗi), hãy sửa nó bằng địa chỉ thực của chuỗi trong.rodata. - Tại offset
0x18(nơi lệnhcallgọi hàm), hãy sửa nó bằng địa chỉ thực của hàmprintf.
Đây chính là ý nghĩa của một file “relocatable” (có thể tái định vị). Khi biên dịch, compiler giả định tất cả các section đều bắt đầu tại địa chỉ 0. Ta có thể thấy điều này bằng cách xem header của các section (VMA là 0).
$ objdump -h main.o
main.o: file format elf64-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000023 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000063 2**0
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000063 2**0
ALLOC
3 .rodata 0000000d 0000000000000000 0000000000000000 00000063 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
...Nhờ có các bản ghi tái định vị, linker có thể lấy các đoạn mã và dữ liệu này, đặt chúng vào vị trí cuối cùng trong file thực thi, và “vá” lại tất cả các địa chỉ placeholder bằng địa chỉ thực.
Bây giờ ta sẽ sử dụng linker để link file main.o với các thư viện cần thiết:
$ gcc main.o -o mainLệnh trên liên kết file main.o với các thư viện hệ thống (chủ yếu là libc) và tạo ra file thực thi main. Ta có thể kiểm tra loại file này bằng lệnh file:
$ file main
main: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=d8fbd6d651f0968effd2552ca4e6d4338367db9f, for GNU/Linux 3.2.0, with debug_info, not strippedFile này là một executable động (dynamically linked), có nghĩa nó sẽ cần tải các thư viện chia sẻ vào bộ nhớ khi chạy. Để xem những thư viện nào sẽ được tải, ta dùng lệnh ldd:
$ ldd main
linux-vdso.so.1 (0x00007ffde35ee000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000758ea8e00000)
/lib64/ld-linux-x86-64.so.2 (0x0000758ea9203000)
$ ldd --version
ldd (Ubuntu GLIBC 2.39-0ubuntu8.6) 2.39
Copyright (C) 2024 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.Phiên bản GLIBC hiện tại là 2.39.
Vì file đã được linked, các tham chiếu đã được giải quyết nên sẽ không còn relocation records:
$ objdump -r main
main: file format elf64-x86-64Bây giờ hãy xem mã Assembly đã được linked của hàm main. Lưu ý rằng các địa chỉ giờ đã là địa chỉ thực, không còn là placeholder:
$ objdump -M intel -d -S --disassemble=main main
main: file format elf64-x86-64
Disassembly of section .init:
Disassembly of section .plt:
Disassembly of section .plt.got:
Disassembly of section .plt.sec:
Disassembly of section .text:
0000000000001149 <main>:
#include <stdio.h>
int main() {
1149: f3 0f 1e fa endbr64
114d: 55 push rbp
114e: 48 89 e5 mov rbp,rsp
printf("Hello World\n");
1151: 48 8d 05 ac 0e 00 00 lea rax,[rip+0xeac] # 2004 <_IO_stdin_used+0x4>
1158: 48 89 c7 mov rdi,rax
115b: b8 00 00 00 00 mov eax,0x0
1160: e8 eb fe ff ff call 1050 <printf@plt>
return 0;
1165: b8 00 00 00 00 mov eax,0x0
}
116a: 5d pop rbp
116b: c3 ret
Disassembly of section .fini:So với mã Assembly trong file object lúc trước, ta nhận thấy:
- Địa chỉ của lệnh
leađã được cập nhật từrip+0x0thànhrip+0xeac, giờ nó trỏ đến vị trí thực của chuỗi. - Lệnh
callgiờ gọi đến địa chỉ1050, đây là entry point của PLT (Procedure Linkage Table) dành cho hàmprintf, thay vì địa chỉ placeholder1c.
Ta cũng có thể xem toàn bộ nội dung của tất cả các section trong file thực thi như sau:
$ objdump -M intel -s main
main: file format elf64-x86-64
Contents of section .interp:
0318 2f6c6962 36342f6c 642d6c69 6e75782d /lib64/ld-linux-
0328 7838362d 36342e73 6f2e3200 x86-64.so.2.
Contents of section .note.gnu.property:
0338 04000000 20000000 05000000 474e5500 .... .......GNU.
0348 020000c0 04000000 03000000 00000000 ................
0358 028000c0 04000000 01000000 00000000 ................
Contents of section .note.gnu.build-id:
0368 04000000 14000000 03000000 474e5500 ............GNU.
0378 d8fbd6d6 51f0968e ffd2552c a4e6d433 ....Q.....U,...3
0388 8367db9f .g..
Contents of section .note.ABI-tag:
038c 04000000 10000000 01000000 474e5500 ............GNU.
039c 00000000 03000000 02000000 00000000 ................
Contents of section .gnu.hash:
03b0 02000000 06000000 01000000 06000000 ................
03c0 00008100 00000000 06000000 00000000 ................
03d0 d165ce6d .e.m
Contents of section .dynsym:
03d8 00000000 00000000 00000000 00000000 ................
03e8 00000000 00000000 01000000 12000000 ................
03f8 00000000 00000000 00000000 00000000 ................
0408 4a000000 20000000 00000000 00000000 J... ...........
0418 00000000 00000000 22000000 12000000 ........".......
0428 00000000 00000000 00000000 00000000 ................
0438 66000000 20000000 00000000 00000000 f... ...........
0448 00000000 00000000 75000000 20000000 ........u... ...
0458 00000000 00000000 00000000 00000000 ................
0468 13000000 22000000 00000000 00000000 ...."...........
0478 00000000 00000000 ........
Contents of section .dynstr:
0480 005f5f6c 6962635f 73746172 745f6d61 .__libc_start_ma
0490 696e005f 5f637861 5f66696e 616c697a in.__cxa_finaliz
04a0 65007072 696e7466 006c6962 632e736f e.printf.libc.so
04b0 2e360047 4c494243 5f322e32 2e350047 .6.GLIBC_2.2.5.G
04c0 4c494243 5f322e33 34005f49 544d5f64 LIBC_2.34._ITM_d
04d0 65726567 69737465 72544d43 6c6f6e65 eregisterTMClone
04e0 5461626c 65005f5f 676d6f6e 5f737461 Table.__gmon_sta
04f0 72745f5f 005f4954 4d5f7265 67697374 rt__._ITM_regist
0500 6572544d 436c6f6e 65546162 6c6500 erTMCloneTable.
Contents of section .gnu.version:
0510 00000200 01000300 01000100 0300 ..............
Contents of section .gnu.version_r:
0520 01000200 29000000 10000000 00000000 ....)...........
0530 751a6909 00000300 33000000 10000000 u.i.....3.......
0540 b4919606 00000200 3f000000 00000000 ........?.......
Contents of section .rela.dyn:
0550 b83d0000 00000000 08000000 00000000 .=..............
0560 40110000 00000000 c03d0000 00000000 @........=......
0570 08000000 00000000 00110000 00000000 ................
0580 08400000 00000000 08000000 00000000 .@..............
0590 08400000 00000000 d83f0000 00000000 .@.......?......
05a0 06000000 01000000 00000000 00000000 ................
05b0 e03f0000 00000000 06000000 02000000 .?..............
05c0 00000000 00000000 e83f0000 00000000 .........?......
05d0 06000000 04000000 00000000 00000000 ................
05e0 f03f0000 00000000 06000000 05000000 .?..............
05f0 00000000 00000000 f83f0000 00000000 .........?......
0600 06000000 06000000 00000000 00000000 ................
Contents of section .rela.plt:
0610 d03f0000 00000000 07000000 03000000 .?..............
0620 00000000 00000000 ........
Contents of section .init:
1000 f30f1efa 4883ec08 488b05d9 2f000048 ....H...H.../..H
1010 85c07402 ffd04883 c408c3 ..t...H....
Contents of section .plt:
1020 ff359a2f 0000ff25 9c2f0000 0f1f4000 .5./...%./....@.
1030 f30f1efa 68000000 00e9e2ff ffff6690 ....h.........f.
Contents of section .plt.got:
1040 f30f1efa ff25ae2f 0000660f 1f440000 .....%./..f..D..
Contents of section .plt.sec:
1050 f30f1efa ff25762f 0000660f 1f440000 .....%v/..f..D..
Contents of section .text:
1060 f30f1efa 31ed4989 d15e4889 e24883e4 ....1.I..^H..H..
1070 f0505445 31c031c9 488d3dca 000000ff .PTE1.1.H.=.....
1080 15532f00 00f4662e 0f1f8400 00000000 .S/...f.........
1090 488d3d79 2f000048 8d05722f 00004839 H.=y/..H..r/..H9
10a0 f8741548 8b05362f 00004885 c07409ff .t.H..6/..H..t..
10b0 e00f1f80 00000000 c30f1f80 00000000 ................
10c0 488d3d49 2f000048 8d35422f 00004829 H.=I/..H.5B/..H)
10d0 fe4889f0 48c1ee3f 48c1f803 4801c648 .H..H..?H...H..H
10e0 d1fe7414 488b0505 2f000048 85c07408 ..t.H.../..H..t.
10f0 ffe0660f 1f440000 c30f1f80 00000000 ..f..D..........
1100 f30f1efa 803d052f 00000075 2b554883 .....=./...u+UH.
1110 3de22e00 00004889 e5740c48 8b3de62e =.....H..t.H.=..
1120 0000e819 ffffffe8 64ffffff c605dd2e ........d.......
1130 0000015d c30f1f00 c30f1f80 00000000 ...]............
1140 f30f1efa e977ffff fff30f1e fa554889 .....w.......UH.
1150 e5488d05 ac0e0000 4889c7b8 00000000 .H......H.......
1160 e8ebfeff ffb80000 00005dc3 ..........].
Contents of section .fini:
116c f30f1efa 4883ec08 4883c408 c3 ....H...H....
Contents of section .rodata:
2000 01000200 48656c6c 6f20576f 726c640a ....Hello World.
2010 00 .
Contents of section .eh_frame_hdr:
2014 011b033b 30000000 05000000 0cf0ffff ...;0...........
2024 64000000 2cf0ffff 8c000000 3cf0ffff d...,.......<...
2034 a4000000 4cf0ffff 4c000000 35f1ffff ....L...L...5...
2044 bc000000 ....
Contents of section .eh_frame:
2048 14000000 00000000 017a5200 01781001 .........zR..x..
2058 1b0c0708 90010000 14000000 1c000000 ................
2068 f8efffff 26000000 00440710 00000000 ....&....D......
2078 24000000 34000000 a0efffff 20000000 $...4....... ...
2088 000e1046 0e184a0f 0b770880 003f1a39 ...F..J..w...?.9
2098 2a332422 00000000 14000000 5c000000 *3$"........\...
20a8 98efffff 10000000 00000000 00000000 ................
20b8 14000000 74000000 90efffff 10000000 ....t...........
20c8 00000000 00000000 1c000000 8c000000 ................
20d8 71f0ffff 23000000 00450e10 8602430d q...#....E....C.
20e8 065a0c07 08000000 00000000 .Z..........
Contents of section .init_array:
3db8 40110000 00000000 @.......
Contents of section .fini_array:
3dc0 00110000 00000000 ........
Contents of section .dynamic:
3dc8 01000000 00000000 29000000 00000000 ........).......
3dd8 0c000000 00000000 00100000 00000000 ................
3de8 0d000000 00000000 6c110000 00000000 ........l.......
3df8 19000000 00000000 b83d0000 00000000 .........=......
3e08 1b000000 00000000 08000000 00000000 ................
3e18 1a000000 00000000 c03d0000 00000000 .........=......
3e28 1c000000 00000000 08000000 00000000 ................
3e38 f5feff6f 00000000 b0030000 00000000 ...o............
3e48 05000000 00000000 80040000 00000000 ................
3e58 06000000 00000000 d8030000 00000000 ................
3e68 0a000000 00000000 8f000000 00000000 ................
3e78 0b000000 00000000 18000000 00000000 ................
3e88 15000000 00000000 00000000 00000000 ................
3e98 03000000 00000000 b83f0000 00000000 .........?......
3ea8 02000000 00000000 18000000 00000000 ................
3eb8 14000000 00000000 07000000 00000000 ................
3ec8 17000000 00000000 10060000 00000000 ................
3ed8 07000000 00000000 50050000 00000000 ........P.......
3ee8 08000000 00000000 c0000000 00000000 ................
3ef8 09000000 00000000 18000000 00000000 ................
3f08 1e000000 00000000 08000000 00000000 ................
3f18 fbffff6f 00000000 01000008 00000000 ...o............
3f28 feffff6f 00000000 20050000 00000000 ...o.... .......
3f38 ffffff6f 00000000 01000000 00000000 ...o............
3f48 f0ffff6f 00000000 10050000 00000000 ...o............
3f58 f9ffff6f 00000000 03000000 00000000 ...o............
3f68 00000000 00000000 00000000 00000000 ................
3f78 00000000 00000000 00000000 00000000 ................
3f88 00000000 00000000 00000000 00000000 ................
3f98 00000000 00000000 00000000 00000000 ................
3fa8 00000000 00000000 00000000 00000000 ................
Contents of section .got:
3fb8 c83d0000 00000000 00000000 00000000 .=..............
3fc8 00000000 00000000 30100000 00000000 ........0.......
3fd8 00000000 00000000 00000000 00000000 ................
3fe8 00000000 00000000 00000000 00000000 ................
3ff8 00000000 00000000 ........
Contents of section .data:
4000 00000000 00000000 08400000 00000000 .........@......
Contents of section .comment:
0000 4743433a 20285562 756e7475 2031332e GCC: (Ubuntu 13.
0010 332e302d 36756275 6e747532 7e32342e 3.0-6ubuntu2~24.
0020 30342920 31332e33 2e3000 04) 13.3.0.
Contents of section .debug_aranges:
0000 2c000000 02000000 00000800 00000000 ,...............
0010 49110000 00000000 23000000 00000000 I.......#.......
0020 00000000 00000000 00000000 00000000 ................
Contents of section .debug_info:
0000 b0000000 05000108 00000000 022f0000 ............./..
0010 001d0000 00000700 00004911 00000000 ..........I.....
0020 00002300 00000000 00000000 00000108 ..#.............
0030 07000000 00010407 05000000 010108ca ................
0040 00000001 02071200 00000101 06cc0000 ................
0050 00010205 25000000 03040569 6e740001 ....%......int..
0060 0805d800 00000101 06d30000 00046600 ..............f.
0070 00000508 6d000000 06720000 0007e600 ....m....r......
0080 0000026b 010c5800 00009500 00000878 ...k..X........x
0090 00000009 000ae100 00000103 05580000 .............X..
00a0 00491100 00000000 00230000 00000000 .I.......#......
00b0 00019c00 ....
Contents of section .debug_abbrev:
0000 0124000b 0b3e0b03 0e000002 1101250e .$...>........%.
0010 130b031f 1b1f1101 12071017 00000324 ...............$
0020 000b0b3e 0b030800 00042600 49130000 ...>......&.I...
0030 050f000b 0b491300 00063700 49130000 .....I....7.I...
0040 072e013f 19030e3a 0b3b0539 0b271949 ...?...:.;.9.'.I
0050 133c1901 13000008 05004913 00000918 .<........I.....
0060 0000000a 2e003f19 030e3a0b 3b0b390b ......?...:.;.9.
0070 49131101 12074018 7c190000 00 I.....@.|....
Contents of section .debug_line:
0000 58000000 05000800 33000000 010101fb X.......3.......
0010 0e0d0001 01010100 00000100 00010101 ................
0020 1f020700 00001900 00000201 1f020f03 ................
0030 00000000 00000000 00002600 00000105 ..........&.....
0040 0c000902 49110000 00000000 14050583 ....I...........
0050 050c083d 05015902 02000101 ...=..Y.....
Contents of section .debug_str:
0000 6c6f6e67 20756e73 69676e65 6420696e long unsigned in
0010 74007368 6f727420 756e7369 676e6564 t.short unsigned
0020 20696e74 0073686f 72742069 6e740047 int.short int.G
0030 4e552043 31372031 332e332e 30202d6d NU C17 13.3.0 -m
0040 74756e65 3d67656e 65726963 202d6d61 tune=generic -ma
0050 7263683d 7838362d 3634202d 67202d66 rch=x86-64 -g -f
0060 6e6f2d62 75696c74 696e202d 66617379 no-builtin -fasy
0070 6e636872 6f6e6f75 732d756e 77696e64 nchronous-unwind
0080 2d746162 6c657320 2d667374 61636b2d -tables -fstack-
0090 70726f74 6563746f 722d7374 726f6e67 protector-strong
00a0 202d6673 7461636b 2d636c61 73682d70 -fstack-clash-p
00b0 726f7465 6374696f 6e202d66 63662d70 rotection -fcf-p
00c0 726f7465 6374696f 6e00756e 7369676e rotection.unsign
00d0 65642063 68617200 6c6f6e67 20696e74 ed char.long int
00e0 006d6169 6e007072 696e7466 00 .main.printf.
Contents of section .debug_line_str:
0000 6d61696e 2e63002f 686f6d65 2f6e6774 main.c./home/ngt
0010 756f6e67 68756e67 002f7573 722f696e uonghung./usr/in
0020 636c7564 65007374 64696f2e 6800 clude.stdio.h._start chính là entry point đầu tiên mà OS gọi khi chương trình được load vào bộ nhớ. Sau đó gọi đến __libc_start_main.
$ objdump -M intel -d -S -j .text --disassemble=_start main
main: file format elf64-x86-64
Disassembly of section .text:
0000000000001060 <_start>:
1060: f3 0f 1e fa endbr64
1064: 31 ed xor ebp,ebp
1066: 49 89 d1 mov r9,rdx
1069: 5e pop rsi
106a: 48 89 e2 mov rdx,rsp
106d: 48 83 e4 f0 and rsp,0xfffffffffffffff0
1071: 50 push rax
1072: 54 push rsp
1073: 45 31 c0 xor r8d,r8d
1076: 31 c9 xor ecx,ecx
1078: 48 8d 3d ca 00 00 00 lea rdi,[rip+0xca] # 1149 <main>
107f: ff 15 53 2f 00 00 call QWORD PTR [rip+0x2f53] # 3fd8 <__libc_start_main@GLIBC_2.34>
1085: f4 hltTrước khi hàm main() được gọi, runtime cần thực hiện một số công việc khởi tạo. Có hai cơ chế chính để làm việc này: _init() function và .init_array section.
_init() là một hàm duy nhất được linker tự động tạo ra và đặt trong section .init. Đây là hàm khởi tạo đầu tiên được gọi bởi __libc_start_main() trước khi main() chạy. Hàm này dùng để khởi tạo các thành phần hệ thống cơ bản. Không thể can thiệp trực tiếp vào _init() từ mã nguồn:
$ objdump -M intel -S -j .init main
main: file format elf64-x86-64
Disassembly of section .init:
0000000000001000 <_init>:
1000: f3 0f 1e fa endbr64
1004: 48 83 ec 08 sub rsp,0x8
1008: 48 8b 05 d9 2f 00 00 mov rax,QWORD PTR [rip+0x2fd9] # 3fe8 <__gmon_start__@Base>
100f: 48 85 c0 test rax,rax
1012: 74 02 je 1016 <_init+0x16>
1014: ff d0 call rax
1016: 48 83 c4 08 add rsp,0x8
101a: c3 ret.init_array là một mảng chứa các con trỏ hàm (function pointers). Sau khi _init() chạy xong, runtime sẽ duyệt qua mảng này và gọi từng hàm theo thứ tự từ đầu đến cuối. Đây là cơ chế cho phép thêm các hàm khởi tạo tùy chỉnh vào chương trình:
$ objdump -s -j .init_array main
main: file format elf64-x86-64
Contents of section .init_array:
3db8 40110000 00000000 @.......Ví dụ về một hàm thuộc .init_array:
$ objdump -M intel -j .text --disassemble=frame_dummy main
main: file format elf64-x86-64
Disassembly of section .text:
0000000000001140 <frame_dummy>:
1140: f3 0f 1e fa endbr64
1144: e9 77 ff ff ff jmp 10c0 <register_tm_clones>Tương tự với khởi tạo, khi chương trình kết thúc cũng có hai cơ chế dọn dẹp chính: _fini() function và .fini_array section.
_fini() là một hàm duy nhất được linker tự động tạo ra và đặt trong section .fini. Đây là hàm dọn dẹp cuối cùng được gọi sau khi chương trình kết thúc. Hàm này dùng để dọn dẹp các thành phần hệ thống cơ bản:
$ objdump -M intel -S -j .fini main
main: file format elf64-x86-64
Disassembly of section .fini:
000000000000116c <_fini>:
116c: f3 0f 1e fa endbr64
1170: 48 83 ec 08 sub rsp,0x8
1174: 48 83 c4 08 add rsp,0x8
1178: c3 ret.fini_array là một mảng chứa các con trỏ hàm. Trước khi _fini() được gọi, runtime sẽ duyệt qua mảng này và gọi từng hàm theo thứ tự ngược từ cuối lên đầu. Đây là cơ chế cho phép thêm các hàm dọn dẹp tùy chỉnh vào chương trình:
$ objdump -s -j .fini_array main
main: file format elf64-x86-64
Contents of section .fini_array:
3dc0 00110000 00000000 ........Ví dụ về một hàm thuộc .fini_array:
$ objdump -M intel -j .text --disassemble=__do_global_dtors_aux main
main: file format elf64-x86-64
Disassembly of section .text:
0000000000001100 <__do_global_dtors_aux>:
1100: f3 0f 1e fa endbr64
1104: 80 3d 05 2f 00 00 00 cmp BYTE PTR [rip+0x2f05],0x0 # 4010 <__TMC_END__>
110b: 75 2b jne 1138 <__do_global_dtors_aux+0x38>
110d: 55 push rbp
110e: 48 83 3d e2 2e 00 00 cmp QWORD PTR [rip+0x2ee2],0x0 # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
1115: 00
1116: 48 89 e5 mov rbp,rsp
1119: 74 0c je 1127 <__do_global_dtors_aux+0x27>
111b: 48 8b 3d e6 2e 00 00 mov rdi,QWORD PTR [rip+0x2ee6] # 4008 <__dso_handle>
1122: e8 19 ff ff ff call 1040 <__cxa_finalize@plt>
1127: e8 64 ff ff ff call 1090 <deregister_tm_clones>
112c: c6 05 dd 2e 00 00 01 mov BYTE PTR [rip+0x2edd],0x1 # 4010 <__TMC_END__>
1133: 5d pop rbp
1134: c3 ret
1135: 0f 1f 00 nop DWORD PTR [rax]
1138: c3 ret
1139: 0f 1f 80 00 00 00 00 nop DWORD PTR [rax+0x0]Loading
Loader là thành phần chịu trách nhiệm nạp file thực thi nhị phân vào bộ nhớ để chương trình có thể chạy trên CPU. Đảm nhiệm quá trình gán địa chỉ cuối cùng cho các phần của chương trình và điều chỉnh code cũng như data để chúng khớp với những địa chỉ đó. Nhờ đó, code có thể gọi các hàm thư viện và truy cập biến trong khi thực thi.
Examine execution flow with pwndbg
fork() and execve()

pwndbg> proc # Hiển thị thông tin chi tiết về process hiện tại
exe '/bin/bash'
cmdline -bash ''
cwd '/home/ngtuonghung'
pid 65304
tid 65304
selinux kernel
ppid 65303
uid [1000, 1000, 1000, 1000]
gid [1000, 1000, 1000, 1000]
groups [4, 24, 27, 30, 46, 100, 993, 1000, 1001]
fd[0] /dev/pts/4
fd[1] /dev/pts/4
fd[2] /dev/pts/4
fd[7] /dev/ptmx
fd[255] /dev/pts/4Trên Linux, khi chạy một chương trình bằng cách nhập tên file thực thi trên command line (ví dụ ./main), shell tạo một process mới bằng syscall fork().
pwndbg> set follow-fork-mode child # Theo dõi child process sau khi fork
pwndbg> catch fork # Break ngay sau khi fork
pwndbg> ni
pwndbg> proc # Xem lại thông tin process sau khi fork
exe '/bin/bash'
cmdline -bash ''
cwd '/home/ngtuonghung'
pid 65644
tid 65644
selinux kernel
ppid 65304
uid [1000, 1000, 1000, 1000]
gid [1000, 1000, 1000, 1000]
groups [4, 24, 27, 30, 46, 100, 993, 1000, 1001]
fd[0] /dev/pts/4
fd[1] /dev/pts/4
fd[2] /dev/pts/4
fd[3] pipe:[407169]
fd[4] pipe:[407169]
fd[7] /dev/ptmx
fd[255] /dev/pts/4Sau đó gọi execve() trong process con để thay thế hoàn toàn address space bằng chương trình mới. Kernel đóng vai trò loader thực sự, nạp file thực thi và khởi động chương trình.
pwndbg> b *execve # Breakpoint tại hàm execve
pwndbg> c
Syscall execve() yêu cầu kernel đọc ELF file và map các segments (code, data) vào address space của process thông qua mmap(). Nếu executable là dynamically linked, kernel đọc segment PT_INTERP để biết đường dẫn của dynamic linker (thường là /lib64/ld-linux.so.2), sau đó cũng load dynamic linker vào memory bằng mmap(). Khi execve() return về user-mode, RIP không trỏ vào _start trong ./main, mà trỏ vào entry point của dynamic linker.
Loader’s _start()
pwndbg> catch exec # Break ngay sau khi thực thi syscall execve
pwndbg> c
Dynamic linker (đã được kernel load) bây giờ chạy. Đầu tiên nó tự relocate địa chỉ của chính nó. Sau đó nó load các shared libraries cần thiết (như libc.so.6) thông qua mmap(). Tiếp theo nó resolve symbols bằng cách điền các địa chỉ vào GOT và PLT tables.
pwndbg> b *mmap # Breakpoint tại hàm mmap
Cuối cùng, sau khi hoàn tất, dynamic linker mới transfer control đến _start trong ./main.
./main’s _start()
_start là entry point đầu tiên trong ./main. Hàm này gọi __libc_start_main() và không bao giờ return.
pwndbg> b *_start # Breakpoint tại entry point của ./main
pwndbg> c

__libc_start_main() thực hiện nhiều công việc khởi tạo. Đầu tiên nó đăng ký rtld_fini với atexit(). rtld_fini (runtime linker finalization) là function cleanup của dynamic linker, được gọi sau cùng khi chương trình thoát để dọn dẹp các shared libraries đã load.
Tiếp theo nó đăng ký fini (tức _fini() và .fini_array) với atexit(). Do atexit hoạt động theo cơ chế LIFO (Last In First Out), fini được đăng ký sau sẽ chạy trước rtld_fini.
Sau đó nó gọi _init() function để khởi tạo các thành phần hệ thống.

Tiếp theo nó chạy các functions trong .init_array theo thứ tự từ đầu đến cuối. Lưu ý rằng _init() chạy trước, sau đó mới đến .init_array:
_init() ← Chạy trước
↓
.init_array[0]
.init_array[1]
...
.init_array[n] ← Theo thứ tự forward
↓
main()Cụ thể ở đây là frame_dummy():

Đối với glibc version ≤ 2.33, __libc_start_main() gọi main() trực tiếp. Từ glibc ≥ 2.34, __libc_start_main() gọi __libc_start_call_main(), rồi mới gọi main().


./main’s main()
Và đây chính là lời gọi hàm printf() của chúng ta:

Hàm main() là nơi code chương trình chạy. Có thể return một giá trị hoặc gọi exit() trực tiếp để kết thúc chương trình.
Nếu main() return, __libc_start_main() sẽ nhận return value và pass nó cho exit(). Nếu gọi exit() trực tiếp trong main() hoặc ở bất kỳ đâu, nó sẽ bypass __libc_start_main() và nhảy thẳng vào exit().

exit()
exit() thực hiện dọn dẹp và không bao giờ return. Đầu tiên nó chạy các atexit handlers theo thứ tự LIFO (Last In First Out). Sau đó nó chạy các functions trong .fini_array theo thứ tự ngược từ cuối lên đầu.
Cụ thể là __do_global_dtors_aux():
pwndbg> b *__do_global_dtors_aux # Breakpoint tại hàm cleanup trong .fini_array
pwndbg> c
Sau đó nó gọi _fini() function, rồi tiếp là rtld_fini().
exit()
↓
User atexit handlers (nếu có)
↓
.fini_array[n]
.fini_array[n-1]
...
.fini_array[0] ← Theo thứ tự ngược
↓
_fini() ← Cleanup của chương trình
↓
rtld_fini() ← Cleanup của dynamic linker (đăng ký đầu tiên, chạy sau cùng)
↓
_exit() syscallThứ tự này đảm bảo resources được cleanup đúng theo phụ thuộc. Trong __libc_start_main(), rtld_fini được đăng ký trước, fini được đăng ký sau. Do atexit hoạt động theo LIFO, fini chạy trước rtld_fini, đảm bảo chương trình cleanup xong trước khi dynamic linker cleanup shared libraries.
pwndbg> b *_fini # Breakpoint tại hàm _fini
pwndbg> c
Sau khi gọi _fini(), exit() flush tất cả buffered I/O streams và close file descriptors. Cuối cùng nó gọi _exit() syscall. _exit() là syscall trực tiếp đến kernel. Kernel terminate process hoàn toàn. Đây là điểm kết thúc cuối cùng và không bao giờ return.
pwndbg> b *_exit # Breakpoint tại hàm _exit
pwndbg> c
Next instruction và chương trình sẽ kết thúc.
pwndbg> ni
[Inferior 2 (process 66299) exited normally]References
- Operating System Concepts 10th Edition - Abraham Silberschatz, Greg Gagne, and Peter Baer Galvin.
- Perplexity.