Linking¶
Linker (ld) nhận một danh sách object file và library, trả về một executable hoặc shared library. Bài này đi qua ba việc nó làm, các quy tắc khi gặp symbol trùng/thiếu, và hai cách đưa library vào chương trình. Nên đọc bài 2 trước.
Ba việc của linker¶
- Symbol resolution: ghép mỗi symbol
U(undefined) với đúng một định nghĩa. - Section merging và gán địa chỉ: gộp các
.textcủa từng.othành một.text, tương tự.data,.bss, rồi gán địa chỉ cho từng thứ. - Relocation: duyệt các relocation entry và điền địa chỉ thật vào chỗ đã đánh dấu.
hello.o: .text .data .rodata ┌─────────── executable ───────────┐
math.o : .text .data ──ld──▶ │ .text (hello.o + math.o + crt*) │
libc : (qua dynamic link) │ .rodata .data .bss │
crt1.o : _start └──────────────────────────────────┘
Kết quả của relocation: lệnh call square trong hello.o có 4 byte operand 00 00 00 00; trong executable nó đã là 39 00 00 00.
Với S = 0x1194 (địa chỉ square), A = -4 (addend) và P = 0x1157 (vị trí operand): S + A - P = 0x39.
Symbol resolution¶
Strong và weak¶
Mỗi symbol global là strong hoặc weak. Hàm và biến global thông thường là strong; symbol đánh dấu __attribute__((weak)) là weak. (Với GCC cũ trước 10, biến global chưa khởi tạo cũng là weak/common; từ GCC 10 mặc định -fno-common nên chúng là strong.) Quy tắc:
- Hai strong trùng tên: lỗi
multiple definition. - Một strong và nhiều weak: chọn strong.
- Chỉ có weak: chọn một trong số đó.
Rule 1 trong thực tế (hai file cùng định nghĩa int counter, có khởi tạo):
$ nm a.o b.o
a.o: 0000000000000000 D counter
b.o: 0000000000000000 D counter
$ gcc a.o b.o -o ab
/usr/bin/ld: b.o:(.data+0x0): multiple definition of `counter'; a.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status
Rule 2 là cách nhiều hệ thống embedded cung cấp "hook" mặc định, mà người dùng có thể ghi đè bằng cách định nghĩa hàm cùng tên:
#include <stdio.h>
__attribute__((weak)) void hook(void) { puts("default hook"); }
int main(void) { hook(); return 0; }
$ gcc w.c -o w1 && ./w1
default hook
$ gcc w.c h2.c -o w2 && ./w2 # h2.c định nghĩa hook() bình thường (strong)
custom hook
Pattern này xuất hiện nhiều trong Linux kernel và bootloader (ví dụ các hàm __weak trong U-Boot, handler mặc định trong vector table của MCU).
static và extern¶
staticở file scope đổi symbol từGLOBALthànhLOCAL: linker không thấy, nên hai file có thể cùng cóstatic int hiddenmà không xung đột (d hiddenở outputnmtrên).- Khai báo
extern int counter;không tạo symbol, chỉ báo "có biến này ở file khác". Định nghĩa thật (không cóextern) chỉ được xuất hiện một lần trong toàn chương trình.
Xem thêm cách tổ chức header/source ở bài 5.
Library¶
Static library .a¶
.a là archive: một tập hợp các .o do ar đóng gói.
mm.c chỉ gọi f1(). Linker không chép cả libf.a vào chương trình, chỉ lấy member nào cần để thoả symbol đang U. Có thể kiểm chứng trong map file:
f1.o được kéo vào vì mm.o cần f1; f2.o không xuất hiện. Đơn vị nhỏ nhất là một .o, vì vậy khi viết library nên tách mỗi hàm/nhóm hàm liên quan thành một file .c, để chương trình không phải mang theo code không dùng.
Thứ tự -l quan trọng
Linker xử lý các file từ trái sang phải. Khi gặp một archive, nó chỉ lấy những member giải quyết symbol đang U tại thời điểm đó. Vì vậy:
Quy tắc an toàn: đặt -l... sau các file nguồn/object. Nếu hai library phụ thuộc vòng nhau, có thể lặp lại hoặc dùng -Wl,--start-group -la -lb -Wl,--end-group.
Shared library .so¶
Linker không chép code vào executable, chỉ ghi lại tên library cần (DT_NEEDED). Việc nạp xảy ra lúc chạy.
Build một shared library:
$ gcc -fPIC -shared -Wl,-soname,libf.so.1 f1.c f2.c -o libf.so.1.0.0
$ ln -sf libf.so.1.0.0 libf.so.1 # soname: dùng lúc chạy
$ ln -sf libf.so.1 libf.so # linker name: dùng lúc link (-lf)
-fPIC(Position-Independent Code): sinh code không phụ thuộc địa chỉ nạp, để nhiều process cùng chia sẻ một bản.sotrong memory ở các địa chỉ khác nhau.- soname (
libf.so.1) là tên mà executable ghi lại. Nó đại diện cho ABI: sửa lỗi bên trong giữ nguyên soname (libf.so.1.0.1), đổi ABI thì tăng soname (libf.so.2).
Chuỗi ba tên cho thấy ba vai trò khác nhau:
| Tên | Vai trò | Ai dùng |
|---|---|---|
libf.so.1.0.0 |
file thật | |
libf.so.1 |
soname | dynamic loader, lúc chạy |
libf.so |
linker name | ld, qua -lf lúc build |
Link và chạy:
$ gcc mm.c -Llib -lf -o ms
$ readelf -d ms | grep -E "NEEDED|SONAME"
0x0000000000000001 (NEEDED) Shared library: [libf.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
$ ./ms
./ms: error while loading shared libraries: libf.so.1: cannot open shared object file: No such file or directory
-L chỉ có tác dụng lúc link. Lúc chạy, loader tìm theo thứ tự: RPATH/RUNPATH trong executable, LD_LIBRARY_PATH, cache /etc/ld.so.cache (cập nhật bằng ldconfig), rồi thư mục mặc định /lib, /usr/lib. Có ba cách để ./ms chạy được:
$ LD_LIBRARY_PATH=./lib ./ms # (1) tạm thời, để thử
$ gcc mm.c -Llib -lf -Wl,-rpath,'$ORIGIN/lib' -o ms2 # (2) ghi đường dẫn vào executable
$ sudo cp libf.so.1* /usr/local/lib && sudo ldconfig # (3) cài vào hệ thống
$ ldd ms2 | grep libf
libf.so.1 => /.../lib/libf.so.1 (0x00007fed8bb02000)
$ readelf -d ms2 | grep -i runpath
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN/lib]
$ORIGIN là thư mục chứa executable, nên có thể đặt binary và lib/ cạnh nhau rồi copy đi đâu cũng chạy được, rất tiện khi triển khai lên target. Nhớ đặt trong dấu nháy đơn để shell không expand.
Static vs shared¶
Static (libx.a) |
Shared (libx.so) |
|
|---|---|---|
| Bản chất | linker chép các .o cần thiết vào executable |
linker ghi lại tên; nạp lúc chạy |
| Kích thước executable | lớn | nhỏ |
| Memory | mỗi chương trình một bản | nhiều process chia sẻ phần code |
| Phụ thuộc lúc chạy | không | cần đúng .so trên target |
| Cập nhật library | build lại chương trình | thay .so |
| Option | -static (toàn bộ) hoặc -l:libx.a |
mặc định |
Với hello trên ARM, cùng source:
Executable static chạy được mà không cần libc.so trên target, thuận tiện cho initramfs tối giản hoặc khi không kiểm soát được môi trường chạy; đổi lại kích thước lớn và phải build lại khi libc có bản vá bảo mật. Với glibc, một số tính năng (NSS, dlopen, locale) hoạt động kém khi link static; musl thường được chọn nếu cần static thật sự.
Linker script¶
Mặc định ld dùng một linker script nội bộ để quyết định section nào đặt ở đâu. Xem bằng ld --verbose. Với chương trình user space ta hiếm khi cần sửa. Trong bare-metal/bootloader/kernel, linker script là công cụ chính để đặt code vào đúng vùng nhớ của chip (Flash, SRAM, ...), ví dụ arch/arm/kernel/vmlinux.lds.S trong kernel hay u-boot.lds trong U-Boot. Dạng tối giản:
SECTIONS
{
. = 0x08000000; /* địa chỉ bắt đầu (Flash) */
.text : { *(.text*) }
.rodata : { *(.rodata*) }
. = 0x20000000; /* SRAM */
.data : { *(.data*) }
.bss : { *(.bss*) *(COMMON) }
}
Tóm tắt¶
- Linker làm ba việc: resolve symbol, merge section + gán địa chỉ, relocation.
- Symbol strong/weak: hai strong trùng tên là lỗi; weak cho phép ghi đè mặc định.
.alà archive các.o, linker chỉ lấy member cần; thứ tự-lquan trọng..sođược nạp lúc chạy;-Lcho lúc link,-rpath/LD_LIBRARY_PATH/ldconfigcho lúc chạy.- Linker script quyết định layout section, quan trọng với bare-metal và bootloader.
Tiếp theo: Từ executable đến process.
References¶
man ld,man ar,man ld.so- GNU ld manual
- Ulrich Drepper - How To Write Shared Libraries
- Randal E. Bryant, David R. O'Hallaron - Computer Systems: A Programmer's Perspective, mục 7.6-7.12