Chương 7 · Liên kết Chapter 7 · Linking
Các mảnh mã rời được gom lại thành một chương trình chạy được như thế nào Nhiều mảnh mã được ghép lại thành một chương trình chạy được How separate pieces of code become one runnable program
Liên kết (linking) là quá trình thu thập và kết hợp nhiều mảnh mã và dữ liệu thành một tệp duy nhất có thể được nạp vào bộ nhớ và chạy. Bryant và O'Hallaron nhấn mạnh rằng việc này có thể xảy ra ở thời điểm biên dịch, thời điểm nạp, và thậm chí ngay trong thời điểm chạy; trên hệ thống hiện đại nó được thực hiện tự động bởi các chương trình gọi là trình liên kết (linker).1 Hiểu nó cho phép xây dựng chương trình lớn, tránh những lỗi lập trình nguy hiểm và âm thầm, hiểu cách luật phạm vi (scoping) được hiện thực, và khai thác được thư viện chia sẻ.1
Khi bạn viết một chương trình lớn, bạn chia nó thành nhiều tệp nhỏ cho dễ quản lý. Nhưng máy cần một tệp chạy được duy nhất. “Liên kết” chính là bước ghép các mảnh ấy lại: nối mã, nối dữ liệu, và điền đúng địa chỉ cho từng chỗ gọi hàm hay dùng biến. Nhớ nhé: liên kết là bước khâu các mảnh mã rời thành một chương trình hoàn chỉnh.
Linking is the process of collecting and combining various pieces of code and data into a single file that can be loaded into memory and executed. Bryant and O'Hallaron stress that it can happen at compile time, at load time, and even at run time; on modern systems it is performed automatically by programs called linkers.1 Understanding it lets you build large programs, avoid dangerous and silent programming errors, understand how scoping rules are implemented, and exploit shared libraries.1
Điểm tinh tế: trình liên kết “hiểu rất ít” về máy đích — phần lớn công việc đã do trình biên dịch và trình hợp dịch làm xong. Nhiệm vụ của nó gói gọn trong hai việc: phân giải ký hiệu (symbol resolution), gắn mỗi tham chiếu ký hiệu với đúng một định nghĩa; và định vị lại (relocation), gán địa chỉ bộ nhớ thực cho các phần mã–dữ liệu rồi sửa mọi tham chiếu trỏ về đúng chỗ.1 Cả chương dùng một ví dụ chạy xuyên suốt — hai tệp nguồn main.c và sum.c trên hệ x86-64 chạy Linux với định dạng tệp đối tượng ELF-64 — để làm mọi khái niệm trở nên cụ thể.1
Luận đề của chương: liên kết không phải một bước phụ tầm thường, mà là một quá trình tinh vi gồm hai nhiệm vụ cốt lõi — phân giải ký hiệu và định vị lại — chi phối cả tính đúng đắn lẫn cấu trúc của chương trình; nắm được nó mở ra việc lập trình với tệp đối tượng, thư viện tĩnh và thư viện chia sẻ, mã độc lập vị trí, và xen phủ thư viện.1
Ý chính: liên kết là bước ghép các mảnh mã rời thành một chương trình chạy được, làm hai việc chính — tìm đúng định nghĩa cho mỗi cái tên, và điền đúng địa chỉ cho mỗi tham chiếu. Nhớ nhé: hiểu liên kết thì bạn gỡ được những lỗi “khó hiểu” nhất và dùng được thư viện chia sẻ.
The chapter's thesis: linking is not a trivial afterthought but a sophisticated process of two core tasks — symbol resolution and relocation — that governs both the correctness and the structure of programs; mastering it opens up object files, static and shared libraries, position-independent code, and library interpositioning.1
Cụm khái niệm 01 Concept cluster 01
Trình điều khiển biên dịch, liên kết tĩnh, và các dạng tệp đối tượng Một lệnh gcc, bốn chương trình, và các loại tệp đối tượng The compiler driver, static linking, and the forms of object files
Phần lớn hệ thống biên dịch cung cấp một trình điều khiển biên dịch (compiler driver) thay người dùng gọi lần lượt tiền xử lý, trình biên dịch, trình hợp dịch và trình liên kết. Với ví dụ của sách gồm main.c và sum.c, lệnh gcc -Og -o prog main.c sum.c lần lượt chạy cpp (sinh main.i), cc1 (sinh main.s), as (sinh main.o), rồi ld ghép các tệp đối tượng cùng các tệp đối tượng hệ thống thành tệp chạy được prog.1
Khi bạn gõ gcc -o prog main.c sum.c, thật ra một “người điều phối” âm thầm gọi bốn chương trình: một cái dọn dẹp mã, một cái dịch sang hợp ngữ, một cái đổi thành mã máy, và cuối cùng trình liên kết ghép tất cả thành tệp chạy được. Nhớ nhé: một lệnh gcc che giấu cả một dây chuyền bốn bước.
Most compilation systems provide a compiler driver that invokes the preprocessor, compiler, assembler, and linker on the user's behalf. For the book's example of main.c and sum.c, the command gcc -Og -o prog main.c sum.c runs cpp (producing main.i), cc1 (producing main.s), as (producing main.o), then ld, which combines the object files with system object files into the executable prog.1
Định nghĩa. Để dựng tệp chạy được, trình liên kết làm đúng hai việc. Phân giải ký hiệu: tệp đối tượng định nghĩa và tham chiếu các ký hiệu (symbol) — mỗi ký hiệu ứng với một hàm, một biến toàn cục, hoặc một biến static; mục đích là gắn mỗi tham chiếu với đúng một định nghĩa. Định vị lại: trình liên kết đổi các phần bắt đầu ở địa chỉ 0 thành các địa chỉ bộ nhớ thực và sửa mọi tham chiếu.1
Nói đơn giản. Trình liên kết làm hai việc: tìm đúng “chủ nhân” cho mỗi cái tên được gọi, và điền đúng địa chỉ cho mỗi chỗ dùng tên đó. Nhớ nhé: gắn tên với định nghĩa, rồi điền địa chỉ — hai việc, thế thôi.
Definition. To build the executable, the linker performs exactly two tasks. Symbol resolution: object files define and reference symbols — each a function, a global variable, or a static variable; the goal is to associate every reference with exactly one definition. Relocation: the linker turns sections that start at address 0 into real memory addresses and modifies all references accordingly.1
main.c --[cpp]--> main.i
--[cc1]--> main.s
--[as]--> main.o
--[ld + sum.o]--> prog
nguồn --> (cpp, cc1, as)
--> .o --[ld]--> prog
main.c --[cpp]--> main.i
--[cc1]--> main.s
--[as]--> main.o
--[ld + sum.o]--> prog
-
compiler driver
Trình điều khiển biên dịch — một lệnh, cả dây chuyền Trình điều khiển biên dịch — một lệnh gọi bốn chương trình The compiler driver — one command, a whole pipeline
Trình điều khiển biên dịch là “mặt tiền” ẩn giấu bốn pha. Bạn chỉ gõ một lệnh
gcc, nhưng bên dưới là tiền xử lý, biên dịch, hợp dịch và liên kết nối tiếp nhau; có thể thấy các bước này bằng cờ-v.1 Trình liên kếtldnhận vào một tập tệp đối tượng khả định vị (relocatable object files) cùng đối số dòng lệnh và sinh ra tệp chạy được đã liên kết đầy đủ.1Bạn gõ một lệnh ngắn, nhưng máy làm bốn việc liền mạch: dọn mã, dịch, đổi sang mã máy, rồi ghép lại. Muốn xem từng bước, thêm cờ
-vvào lệnh gcc. Nhớ nhé: một lệnh gcc = bốn chương trình chạy nối đuôi nhau.The compiler driver is a façade hiding four phases. You type one
gcccommand, but underneath come preprocessing, compilation, assembly, and linking in sequence; you can watch them with the-vflag.1 The linkerldtakes a collection of relocatable object files plus command-line arguments and produces a fully linked executable.1Bạn gặp nó ở đâuWhere you meet it Mỗi lần biên dịch nhiều tệp một lúc, trình điều khiển lặng lẽ sinh ra các tệp.otạm rồi gọildghép chúng lại.1 Khi dự án của bạn có nhiều tệp mã nguồn mà vẫn chạy được như một chương trình. Every time you compile several files at once, the driver quietly produces temporary.ofiles and then callsldto merge them.1Vì sao tách mô-đun
Trình liên kết cho phép biên dịch riêng lẻ (separate compilation): thay vì một tệp nguồn khổng lồ, ta chia thành nhiều mô-đun dịch độc lập; sửa một mô-đun chỉ cần dịch lại nó rồi liên kết lại, không phải dịch lại tất cả.1
Ba thời điểm liên kết
Liên kết có thể xảy ra ở thời điểm biên dịch (khi dịch mã nguồn), thời điểm nạp (khi loader đưa chương trình vào bộ nhớ), và thời điểm chạy (do chính ứng dụng yêu cầu).1
-
two linker tasks
Hai nhiệm vụ của trình liên kết — phân giải và định vị Hai việc của trình liên kết — gắn tên và điền địa chỉ The linker's two tasks — resolve and relocate
Tệp đối tượng chỉ là những khối byte: một số chứa mã, một số chứa dữ liệu, một số chứa cấu trúc dữ liệu hướng dẫn trình liên kết và loader. Trình liên kết nối các khối, quyết định vị trí chạy cho chúng, rồi sửa các chỗ trong khối mã và dữ liệu. Nó hiểu rất ít về máy đích — trình biên dịch và trình hợp dịch đã làm phần lớn công việc.1
Tệp đối tượng giống những mảnh ghép: có mảnh là mã, có mảnh là dữ liệu. Trình liên kết nối chúng lại, chọn chỗ đặt, rồi sửa lại các “mũi tên” sao cho trỏ đúng. Nhớ nhé: trình liên kết nối mảnh, chọn chỗ, rồi chỉnh mũi tên cho đúng.
Object files are just blocks of bytes: some hold code, some hold data, some hold data structures that guide the linker and loader. The linker concatenates blocks, decides their run-time locations, and modifies locations within the code and data blocks. It has minimal understanding of the target machine — the compiler and assembler did most of the work.1
Bạn gặp nó ở đâuWhere you meet it Khi trình liên kết báo “undefined reference to …” — đó là bước phân giải ký hiệu thất bại vì không tìm thấy định nghĩa.1 Khi bạn thấy lỗi “undefined reference”: máy không tìm được định nghĩa cho một cái tên. When the linker reports “undefined reference to …” — that is symbol resolution failing because it found no definition.1 -
three object-file forms
Ba dạng tệp đối tượng — và định dạng ELF Ba loại tệp đối tượng, và chuẩn chung ELF Three forms of object file — and the ELF format
Tệp đối tượng có ba dạng: khả định vị (relocatable) — kết hợp được với tệp khác lúc biên dịch; chạy được (executable) — chép thẳng vào bộ nhớ và chạy; và đối tượng chia sẻ (shared object) — một loại khả định vị đặc biệt, nạp và liên kết động lúc nạp hoặc lúc chạy. Hệ x86-64 Linux hiện đại dùng định dạng ELF (Executable and Linkable Format).12
Tệp đối tượng có ba kiểu: kiểu ghép được với kiểu khác, kiểu chạy thẳng, và kiểu chia sẻ cho nhiều chương trình dùng chung. Trên Linux ngày nay, cả ba dùng một chuẩn chung tên là ELF. Nhớ nhé: ba loại tệp đối tượng — ghép được, chạy được, chia sẻ — cùng một chuẩn ELF.
Object files come in three forms: relocatable — combinable with others at compile time; executable — copied straight into memory and run; and shared object — a special relocatable kind, loaded and linked dynamically at load or run time. Modern x86-64 Linux uses the ELF (Executable and Linkable Format).12
Bạn gặp nó ở đâuWhere you meet it Mỗi tệp.olà khả định vị, mỗi tệpa.out/proglà chạy được, mỗi tệp.solà đối tượng chia sẻ — bạn thấy cả ba mỗi ngày. Tệp.o, tệp chạy được, và tệp.sochính là ba loại đó. Each.ofile is relocatable, eacha.out/progis executable, and each.sois a shared object — you meet all three daily.Lịch sử định dạng
Unix đời đầu ở Bell Labs dùng định dạng
a.out(tới nay tệp chạy được vẫn hay được gọi làa.out). Windows dùng PE (Portable Executable), macOS dùng Mach-O. Khái niệm thì giống nhau, chỉ chi tiết khác.1Mô-đun đối tượng và tệp đối tượng
Về mặt kỹ thuật, mô-đun đối tượng (object module) là một chuỗi byte, còn tệp đối tượng (object file) là mô-đun đối tượng được lưu trên đĩa; sách dùng hai thuật ngữ này thay thế cho nhau.1
Cụm khái niệm 02 Concept cluster 02
Ký hiệu, bảng ký hiệu, và phân giải ký hiệu Những cái tên trong chương trình, và cách chúng được gắn với định nghĩa Symbols, symbol tables, and symbol resolution
Một tệp đối tượng ELF khả định vị được chia thành các phần (section): .text chứa mã máy, .rodata chứa dữ liệu chỉ đọc, .data chứa biến toàn cục/tĩnh đã khởi tạo, và .bss dành cho biến toàn cục/tĩnh chưa khởi tạo. Mỗi mô-đun còn mang một bảng ký hiệu ở phần .symtab, mô tả các ký hiệu nó định nghĩa và tham chiếu.1
Một tệp đối tượng được chia thành nhiều ngăn: ngăn chứa mã, ngăn chứa dữ liệu chỉ đọc, ngăn chứa biến đã có giá trị, và ngăn dành cho biến chưa có giá trị. Kèm theo đó là một “danh bạ” tên gọi để trình liên kết tra cứu. Nhớ nhé: tệp đối tượng chia thành ngăn mã, ngăn dữ liệu, và một danh bạ tên.
A relocatable ELF object file is divided into sections: .text holds machine code, .rodata holds read-only data, .data holds initialized global/static variables, and .bss is for uninitialized global/static variables. Each module also carries a symbol table in its .symtab section, describing the symbols it defines and references.1
Định nghĩa. Phần .bss giữ các biến chưa khởi tạo (và biến khởi tạo bằng 0): nó không chiếm chỗ thực trong tệp đối tượng, chỉ là một chỗ giữ chỗ (placeholder), lúc chạy mới được cấp bộ nhớ và đặt về 0. Tên .bss có từ assembly IBM 704 (“block started by symbol”), và một cách nhớ vui là “Better Save Space”.1
Nói đơn giản. Biến chưa gán giá trị không cần lưu gì trong tệp — máy chỉ ghi chú “sẽ cần chừng này ô nhớ, đặt bằng 0 khi chạy”. Nhờ vậy tệp gọn hơn. Nhớ nhé: biến chưa khởi tạo không tốn chỗ trong tệp, chỉ được ghi chú lại.
Definition. The .bss section holds uninitialized variables (and variables initialized to zero): it occupies no actual space in the object file, being merely a placeholder, and is allocated and zeroed at run time. The name .bss comes from IBM 704 assembly (“block started by symbol”), and a fun mnemonic is “Better Save Space”.1
Ký hiệu mạnh (strong): hàm, biến toàn cục đã khởi tạo
Ký hiệu yếu (weak): biến toàn cục chưa khởi tạo
Luật 1: cấm nhiều ký hiệu mạnh trùng tên
Luật 2: mạnh thắng yếu
Luật 3: nhiều yếu → chọn tuỳ ý
tên “mạnh”: hàm, biến có giá trị
tên “yếu”: biến chưa có giá trị
mạnh thắng yếu
Strong: functions, initialized globals
Weak: uninitialized globals
Rule 1: no two strong with same name
Rule 2: strong beats weak
Rule 3: many weak → pick arbitrarily
-
kinds of symbols
Ba loại ký hiệu — toàn cục, ngoại, và cục bộ Ba loại tên — dùng chung, vay mượn, và riêng tư Three kinds of symbols — global, external, local
Với trình liên kết có ba loại ký hiệu: toàn cục (global) do mô-đun này định nghĩa và mô-đun khác dùng được (hàm và biến toàn cục không
static); ngoại (external) được mô-đun này dùng nhưng do mô-đun khác định nghĩa; và cục bộ (local) chỉ hiện và dùng trong mô-đun của nó (hàm và biến khai báostatic).1Có ba loại tên: tên dùng chung mà ai cũng gọi được, tên “vay” từ tệp khác, và tên riêng chỉ dùng trong tệp của mình. Từ khoá
staticlàm một tên trở thành riêng tư. Nhớ nhé: dùng chung, vay mượn, hay riêng tư — đó là ba loại tên.For the linker there are three kinds of symbols: global, defined by this module and usable by others (non-
staticfunctions and global variables); external, used by this module but defined elsewhere; and local, visible and used only within its own module (staticfunctions and variables).1Bạn gặp nó ở đâuWhere you meet it Khi bạn khai báostaticcho một biến hay hàm để “giấu” nó khỏi các mô-đun khác — một thói quen lập trình tốt mà sách khuyến khích.1 Khi bạn viếtstatictrước một biến để nó chỉ dùng được trong tệp hiện tại. When you mark a variable or functionstaticto “hide” it from other modules — a good practice the book encourages.1Ký hiệu cục bộ ≠ biến cục bộ
Bảng
.symtabkhông chứa biến cục bộ khôngstatic— chúng nằm trên ngăn xếp lúc chạy, trình liên kết không quan tâm. Nhưng biến cục bộ khai báostaticthì được cấp chỗ trong.data/.bssvà có một ký hiệu cục bộ tên duy nhất (ví dụx.1,x.2).1Mục trong bảng ký hiệu
Mỗi mục
Elf64_Symbolgồm:name(độ dời vào bảng chuỗi),type(thường là data hoặc function),binding(cục bộ hay toàn cục),section(chỉ số phần),value(độ dời hoặc địa chỉ tuyệt đối), vàsize.1 -
strong vs weak
Ký hiệu mạnh và yếu — ba luật dễ gây lỗi âm thầm Tên “mạnh” và “yếu” — và vì sao chúng gây lỗi khó thấy Strong vs weak symbols — three silently dangerous rules
Khi nhiều mô-đun định nghĩa ký hiệu toàn cục trùng tên, Linux xử lý bằng quy ước mạnh/yếu. Hàm và biến toàn cục đã khởi tạo là mạnh; biến toàn cục chưa khởi tạo là yếu. Luật 1: cấm hai ký hiệu mạnh trùng tên. Luật 2: một mạnh và nhiều yếu → chọn mạnh. Luật 3: nhiều yếu trùng tên → chọn tuỳ ý một cái.1
Nếu hai tệp cùng đặt một tên toàn cục, máy phải chọn. Tên đã có giá trị thì “mạnh”, tên chưa có giá trị thì “yếu”. Hai tên mạnh trùng nhau là lỗi; mạnh luôn thắng yếu; nhiều yếu thì chọn bừa. Nhớ nhé: hai tên mạnh trùng là lỗi, còn mạnh luôn thắng yếu.
When several modules define global symbols with the same name, Linux uses a strong/weak convention. Functions and initialized globals are strong; uninitialized globals are weak. Rule 1: no two strong symbols with the same name. Rule 2: one strong and several weak → choose the strong. Rule 3: several weak with the same name → choose any.1
Bạn gặp nó ở đâuWhere you meet it Khi một biến vô tình được định nghĩa làintở mô-đun này vàdoubleở mô-đun kia: luật 2 có thể âm thầm ghi đè, tạo lỗi hiểm hóc. Dùng cờ-fno-commonhoặc-Werrorđể trình liên kết báo lỗi thay vì im lặng.1 Khi hai tệp lỡ dùng chung một tên biến với kiểu khác nhau — máy có thể âm thầm làm sai mà không báo. When a variable is accidentally anintin one module and adoublein another: rule 2 can silently overwrite, creating insidious bugs. Use-fno-commonor-Werrorto make the linker complain instead of staying silent.1COMMON và .bss
Trình biên dịch gán biến toàn cục chưa khởi tạo vào giả-phần COMMON (vì chưa biết mô-đun nào định nghĩa thật), còn biến khởi tạo bằng 0 và biến
staticthì gán vào.bss— vì chúng chắc chắn là duy nhất.1Giả-phần ABS, UNDEF, COMMON
Trường
sectioncó ba giá trị đặc biệt: ABS cho ký hiệu không được định vị lại, UNDEF cho ký hiệu tham chiếu nhưng định nghĩa ở nơi khác, và COMMON cho dữ liệu chưa khởi tạo chưa được cấp phát. Chúng chỉ tồn tại trong tệp khả định vị.1 -
static libraries
Thư viện tĩnh — và trật tự dòng lệnh có ý nghĩa Thư viện tĩnh — và vì sao thứ tự trên dòng lệnh lại quan trọng Static libraries — and command-line order matters
Thư viện tĩnh (static library) đóng gói nhiều mô-đun đối tượng liên quan vào một tệp lưu trữ (archive) đuôi
.a, tạo bằng công cụar. Lúc liên kết, trình liên kết chỉ chép những mô-đun thực sự được tham chiếu. Nó quét các tệp và thư viện từ trái sang phải, duy trì ba tập:E(mô-đun sẽ gộp),U(ký hiệu chưa phân giải),D(ký hiệu đã định nghĩa).1Thư viện tĩnh là một “túi” nhiều hàm gộp lại. Khi liên kết, máy chỉ lấy đúng những hàm bạn cần, bỏ phần còn lại. Nó đọc dòng lệnh từ trái sang phải, nên đặt sai thứ tự có thể làm nó bỏ sót. Nhớ nhé: để thư viện SAU tệp cần nó, vì máy đọc trái sang phải.
A static library packages related object modules into one archive file with suffix
.a, built with theartool. At link time the linker copies only the modules actually referenced. It scans files and libraries left to right, keeping three sets:E(modules to merge),U(unresolved symbols),D(defined symbols).1Bạn gặp nó ở đâuWhere you meet it Khigcc ./libvector.a main2.cbáo “undefined reference to ‘addvec’”: thư viện đặt trước tệp cần nó nên lúc quét, tậpUrỗng và mô-đun không được lấy. Quy tắc chung: đặt thư viện ở cuối dòng lệnh.1 Khi bạn gặp lỗi “undefined reference” dù hàm có trong thư viện — thường chỉ vì đặt thư viện sai chỗ. Whengcc ./libvector.a main2.creports “undefined reference to ‘addvec’”: the library came before the file needing it, soUwas empty at the scan and the module was not pulled in. Rule of thumb: put libraries at the end of the command line.1Vì sao có thư viện
Nếu gộp cả thư viện C chuẩn vào một mô-đun
libc.o, mỗi tệp chạy được sẽ chứa một bản sao đầy đủ — rất lãng phí. Trên hệ của sách,libc.akhoảng 5 MB vàlibm.akhoảng 2 MB; thư viện tĩnh cho phép chỉ chép những mô-đun cần dùng.1Thư viện phụ thuộc nhau
Nếu các thư viện phụ thuộc lẫn nhau, phải xếp sao cho với mỗi ký hiệu, định nghĩa đứng sau tham chiếu; có khi phải lặp lại một thư viện trên dòng lệnh để thoả các ràng buộc phụ thuộc.1
Cụm khái niệm 03 Concept cluster 03
Định vị lại, nạp tệp chạy được, và liên kết động với thư viện chia sẻ Điền địa chỉ, nạp chương trình, và dùng chung thư viện Relocation, loading executables, and dynamic linking with shared libraries
Định vị lại (relocation) gồm hai bước: gộp các phần cùng loại từ mọi mô-đun thành một phần tổng rồi gán địa chỉ chạy, và sửa mọi tham chiếu trong thân mã và dữ liệu để chúng trỏ đúng địa chỉ chạy. Để làm được, trình liên kết dựa vào các mục định vị lại (relocation entries) mà trình hợp dịch sinh ra — đặt ở .rel.text cho mã và .rel.data cho dữ liệu.1
Định vị lại là bước điền địa chỉ: máy gom các mảnh cùng loại lại, chọn chỗ đặt trong bộ nhớ, rồi sửa từng “mũi tên” trong mã cho trỏ đúng. Nó biết chỗ nào cần sửa nhờ một danh sách ghi chú sẵn. Nhớ nhé: định vị lại = gom mảnh, chọn địa chỉ, rồi sửa mọi mũi tên.
Relocation has two steps: merge same-type sections from all modules into one aggregate section and assign run-time addresses, then modify every reference in the code and data bodies so they point to the right run-time address. To do this, the linker relies on relocation entries the assembler generates — placed in .rel.text for code and .rel.data for data.1
Định nghĩa. ELF định nghĩa 32 kiểu định vị lại; sách chỉ bàn hai kiểu cơ bản nhất. R_X86_64_PC32 định vị một tham chiếu dùng địa chỉ tương đối-PC 32 bit (ví dụ đích của lệnh call). R_X86_64_32 định vị một tham chiếu dùng địa chỉ tuyệt đối 32 bit. Hai kiểu này phục vụ mô hình mã nhỏ (small code model) của x86-64, giả định tổng mã và dữ liệu nhỏ hơn 2 GB.1
Nói đơn giản. Có hai cách ghi địa chỉ: “cách chỗ hiện tại bao xa” (tương đối) và “địa chỉ chính xác” (tuyệt đối). Mỗi chỗ cần sửa được ghi chú là thuộc kiểu nào. Nhớ nhé: địa chỉ có thể tính theo khoảng cách, hoặc ghi thẳng con số.
Definition. ELF defines 32 relocation types; the book discusses only the two most basic. R_X86_64_PC32 relocates a reference using a 32-bit PC-relative address (e.g. the target of a call). R_X86_64_32 relocates a reference using a 32-bit absolute address. These serve the x86-64 small code model, which assumes total code and data under 2 GB.1
Mục định vị lại (Elf64_Rela):
offset, type, symbol, addend
PC-tương đối:
*refptr = ADDR(sym) + addend − refaddr
Tuyệt đối:
*refptr = ADDR(sym) + addend
mỗi chỗ cần sửa ghi:
vị trí · kiểu · tên · phần bù
Relocation entry (Elf64_Rela):
offset, type, symbol, addend
PC-relative:
*refptr = ADDR(sym) + addend − refaddr
Absolute:
*refptr = ADDR(sym) + addend
-
relocation
Định vị lại — gán địa chỉ và sửa từng tham chiếu Định vị lại — điền địa chỉ đúng cho mỗi chỗ gọi Relocation — assign addresses and fix each reference
Khi trình hợp dịch gặp một tham chiếu tới đối tượng mà vị trí cuối cùng chưa biết, nó sinh một mục định vị lại mô tả cách sửa tham chiếu khi gộp tệp vào tệp chạy được. Mục ELF gồm bốn trường:
offset(vị trí cần sửa),symbol(ký hiệu đích),type(kiểu định vị), vàaddend(hằng số bù). Trình liên kết lặp qua từng phần và từng mục để tính địa chỉ mới.1Khi dịch mã, máy chưa biết hàm hay biến sẽ nằm ở đâu, nên nó để lại một chỗ trống kèm ghi chú “sửa chỗ này sau”. Lúc liên kết, trình liên kết đọc ghi chú và điền địa chỉ thật vào. Nhớ nhé: chỗ trống cộng ghi chú, sửa sau — đó là định vị lại.
When the assembler meets a reference to an object whose final location is unknown, it generates a relocation entry describing how to fix the reference when the file is merged into an executable. The ELF entry has four fields:
offset(the spot to fix),symbol(the target symbol),type(the relocation kind), andaddend(a bias constant). The linker iterates over each section and each entry to compute the new address.1Bạn gặp nó ở đâuWhere you meet it Khiobjdump -dxhiện một dòng nhưR_X86_64_PC32 sum-0x4cạnh một lệnhcall— đó chính là mục định vị lại cho lời gọisum.1 Khi bạn xem mã máy bằng công cụ và thấy những dòng ghi chú “sửa chỗ này” kèm tên hàm. Whenobjdump -dxshows a line likeR_X86_64_PC32 sum-0x4beside acallinstruction — that is the relocation entry for the call tosum.1Một ví dụ cụ thể
Trong ví dụ của sách, lời gọi
sumcó mụcr.offset=0xf, r.symbol=sum, r.type=R_X86_64_PC32, r.addend=-4. VớiADDR(.text)=0x4004d0vàADDR(sum)=0x4004e8, trình liên kết tínhrefaddr=0x4004dfrồi*refptr = 0x4004e8 + (-4) - 0x4004df = 0x5.1Hai bước của định vị lại
Bước 1 gộp mọi phần cùng loại (ví dụ các
.data) thành một phần tổng và gán địa chỉ chạy. Bước 2 sửa từng tham chiếu trong thân mã–dữ liệu để trỏ đúng địa chỉ đó.1 -
executable & loading
Tệp chạy được và nạp — từ đĩa vào bộ nhớ Tệp chạy được và bước nạp — đưa chương trình vào bộ nhớ Executable & loading — from disk into memory
Tệp chạy được ELF đã liên kết đầy đủ nên không còn phần
.rel. ELF header chứa điểm vào (entry point) — địa chỉ lệnh đầu tiên khi chạy. Một bảng tiêu đề chương trình (program header table) ánh xạ các khối liền kề của tệp thành các phân đoạn (segment) bộ nhớ: phân đoạn mã chỉ-đọc ở0x400000, phân đoạn dữ liệu đọc–ghi ngay sau đó.1Tệp chạy được là tệp đã sẵn sàng: mọi địa chỉ đã điền xong. Nó có một “mục lục” cho biết đưa phần nào vào vùng nhớ nào, và một điểm bắt đầu để máy chạy từ đó. Nhớ nhé: tệp chạy được đã điền đủ địa chỉ và chỉ chờ được nạp vào bộ nhớ.
A fully linked ELF executable no longer needs
.relsections. The ELF header holds the entry point — the address of the first instruction to run. A program header table maps contiguous chunks of the file to memory segments: a read-only code segment at0x400000, a read/write data segment just after.1Bạn gặp nó ở đâuWhere you meet it Khi bạn gõ./prog, shell gọi loader quaexecve; loader chép mã và dữ liệu vào bộ nhớ rồi nhảy tới điểm vào, luôn là hàm_starttrongcrt1.o, cuối cùng gọi__libc_start_mainrồimain.1 Khi bạn chạy./prog, một “người bốc xếp” chép chương trình vào bộ nhớ rồi khởi động nó. When you type./prog, the shell invokes the loader viaexecve; the loader copies code and data into memory and jumps to the entry point, always the_startfunction incrt1.o, which eventually calls__libc_start_mainand thenmain.1Ảnh bộ nhớ lúc chạy
Trên Linux x86-64, phân đoạn mã bắt đầu ở
0x400000, rồi tới phân đoạn dữ liệu, heap (domallocmở rộng lên), vùng cho thư viện chia sẻ, ngăn xếp người dùng bắt đầu ngay dưới2^48 − 1, và vùng nhân ở trên2^48.1Nạp thực chất là ánh xạ
Mô tả “chép từ đĩa” chỉ đúng về khái niệm: thực tế loader ánh xạ (map) các trang của tệp vào không gian địa chỉ ảo, và việc chép dữ liệu bị hoãn tới khi CPU chạm vào trang — cơ chế phân trang của bộ nhớ ảo (Chương 9).1
-
dynamic linking + PIC
Liên kết động, thư viện chia sẻ, và mã độc lập vị trí Thư viện chia sẻ — một bản dùng chung cho nhiều chương trình Dynamic linking, shared libraries, and position-independent code
Thư viện chia sẻ (shared library) — tệp
.sotrên Linux, DLL trên Windows — là mô-đun có thể nạp ở một địa chỉ bất kỳ và liên kết với chương trình lúc nạp hoặc lúc chạy, do một trình liên kết động (dynamic linker) nhưld-linux.sothực hiện. Trong hệ tệp chỉ có một bản.socho mỗi thư viện, và một bản.texttrong bộ nhớ có thể được nhiều tiến trình dùng chung.15Thay vì chép cả thư viện vào từng chương trình, Linux giữ một bản chung tên đuôi
.sovà cho nhiều chương trình dùng lại. Nhờ vậy tiết kiệm bộ nhớ và dễ cập nhật. Nhớ nhé: thư viện chia sẻ là một bản dùng chung, không chép vào mỗi chương trình.A shared library — a
.sofile on Linux, a DLL on Windows — is a module that can be loaded at an arbitrary address and linked with a program at load or run time by a dynamic linker such asld-linux.so. The file system holds just one.soper library, and a single copy of its.textin memory can be shared by many processes.15Bạn gặp nó ở đâuWhere you meet it Để chia sẻ được, mã phải là mã độc lập vị trí (position-independent code, PIC), dịch bằng cờ-fpic; tham chiếu biến toàn cục đi qua bảng độ dời toàn cục (global offset table, GOT), còn lời gọi hàm ngoài đi qua bảng liên kết thủ tục (procedure linkage table, PLT) với kỹ thuật liên kết trễ (lazy binding).1 Ứng dụng còn có thể tự nạp thư viện lúc đang chạy bằngdlopen, rồi lấy hàm bằngdlsym— như máy chủ web nạp thêm chức năng mà không cần dừng. To be shareable, code must be position-independent code (PIC), compiled with-fpic; global-variable references go through the global offset table (GOT), and external function calls go through the procedure linkage table (PLT) using lazy binding.1GOT và PLT
GOT nằm đầu phân đoạn dữ liệu, mỗi mục 8 byte cho một đối tượng toàn cục; PLT là mảng các mục mã 16 byte. Hai bảng phối hợp theo kiểu liên kết trễ: lần đầu gọi một hàm ngoài mới tốn công phân giải địa chỉ, các lần sau chỉ còn một lệnh nhảy gián tiếp qua GOT.1
Xen phủ thư viện và công cụ
Xen phủ thư viện (library interpositioning) cho phép chặn lời gọi hàm thư viện và chạy mã riêng: ở thời điểm biên dịch (macro +
-I), thời điểm liên kết (cờ--wrapvới__wrap_f/__real_f), hoặc thời điểm chạy (biến môi trườngLD_PRELOAD). Bộ công cụ GNU binutils gồmar,strings,strip,nm,size,readelf,objdump, vàlddđể liệt kê thư viện chia sẻ mà một tệp chạy được cần.15
Ba ngộ nhận thường gặp Three common misconceptions
“Liên kết chỉ là bước cuối không quan trọng” “Linking is just an unimportant final step”
Ngộ nhận: liên kết diễn ra lặng lẽ, học làm gì.
Thực tế: các quyết định phân giải ký hiệu có thể âm thầm ảnh hưởng tính đúng đắn, và hiểu liên kết là chìa khoá để dùng thư viện chia sẻ và hiểu loading, bộ nhớ ảo, ánh xạ bộ nhớ.1
Myth: linking happens quietly, why learn it.
Reality: symbol-resolution decisions can silently affect correctness, and understanding linking is the key to shared libraries and to grasping loading, virtual memory, and memory mapping.1
“Biến chưa khởi tạo vẫn chiếm chỗ trong tệp” “Uninitialized variables still take space in the file”
Ngộ nhận: mọi biến toàn cục đều tốn byte trên đĩa.
Thực tế: biến chưa khởi tạo nằm ở .bss, không chiếm chỗ thực trong tệp đối tượng — chỉ là chỗ giữ chỗ, lúc chạy mới được cấp và đặt về 0.1
Myth: every global variable costs bytes on disk.
Reality: uninitialized variables live in .bss, which occupies no actual space in the object file — it is a placeholder, allocated and zeroed only at run time.1
“Thứ tự thư viện trên dòng lệnh không quan trọng” “Library order on the command line doesn't matter”
Ngộ nhận: đặt thư viện ở đâu cũng được.
Thực tế: trình liên kết quét trái→phải; nếu thư viện đứng trước tệp cần nó, tập ký hiệu chưa phân giải còn rỗng và mô-đun bị bỏ qua, gây lỗi “undefined reference”. Quy tắc: thư viện đặt ở cuối.1
Myth: put libraries anywhere.
Reality: the linker scans left→right; if a library precedes the file needing it, the unresolved-symbol set is still empty and the module is skipped, causing “undefined reference”. Rule: put libraries at the end.1
Tự kiểm Self-check
Năm câu hỏi để biết bạn đã nắm quá trình liên kết Năm câu hỏi kiểm tra nhanh Five questions to check you grasped linking
Trả lời được những câu này nghĩa là bạn đã theo trọn hành trình từ nhiều tệp nguồn rời tới một chương trình chạy được, qua phân giải ký hiệu, định vị lại và liên kết động.
Nếu trả lời được, nghĩa là bạn đã hiểu chương này.
Answering these means you followed the whole journey from separate source files to a runnable program, through symbol resolution, relocation, and dynamic linking.
-
Trình điều khiển biên dịch gọi những chương trình nào theo thứ tự, và hai nhiệm vụ cốt lõi của trình liên kết là gì?
Gợi ý sâu hơn: nhiệm vụ nào thất bại thì sinh ra lỗi “undefined reference”?
-
Kể ba loại ký hiệu với trình liên kết, và cho biết từ khoá
staticlàm một ký hiệu thành loại nào.Gợi ý sâu hơn: vì sao
.symtabkhông chứa biến cục bộ khôngstatic? -
Ký hiệu mạnh và yếu khác nhau ra sao, và ba luật phân giải ký hiệu trùng tên là gì?
Gợi ý sâu hơn: cờ nào giúp biến lỗi im lặng do luật 2 thành lỗi báo ra?
-
Vì sao
.bsskhông chiếm chỗ trong tệp đối tượng, và trật tự thư viện trên dòng lệnh quan trọng thế nào?Gợi ý sâu hơn: ba tập
E,U,Dthay đổi ra sao khi quét một thư viện tĩnh? -
Thư viện chia sẻ khác thư viện tĩnh ở đâu, và GOT với PLT phục vụ việc gì trong mã độc lập vị trí?
Gợi ý sâu hơn: liên kết trễ (lazy binding) tiết kiệm công sức ở điểm nào?
Khi bạn gõ một lệnh gcc, máy âm thầm chạy mấy chương trình? Trình liên kết làm gì ở cuối?
Từ khoá
staticlàm một cái tên trở nên thế nào — dùng chung hay riêng tư?Nếu hai tệp cùng đặt một tên toàn cục, máy chọn ra sao?
Vì sao đặt thư viện sai chỗ trên dòng lệnh lại gây lỗi “undefined reference”?
Thư viện chia sẻ giúp tiết kiệm gì so với chép thư viện vào mỗi chương trình?
Which programs does the compiler driver invoke, in order, and what are the linker's two core tasks?
Name the three kinds of linker symbols, and say which kind
staticmakes a symbol.How do strong and weak symbols differ, and what are the three rules for duplicate names?
Why does
.bsstake no space in the object file, and why does library order on the command line matter?How do shared libraries differ from static ones, and what do the GOT and PLT do in position-independent code?
Khép lại chương Closing the chapter
Liên kết không phải một bước phụ lặng lẽ, mà là quá trình tinh vi gắn mỗi tham chiếu với đúng một định nghĩa rồi đặt mọi mảnh vào đúng địa chỉ thật. Nắm được phân giải ký hiệu và định vị lại, bạn gỡ được những lỗi khó hiểu nhất và mở khoá cả thế giới thư viện chia sẻ.1
Liên kết không chỉ là bước cuối cho có; nó ghép các mảnh mã rời và điền đúng địa chỉ cho từng chỗ. Hiểu nó là bạn gỡ được lỗi khó và dùng được thư viện chia sẻ. Nhớ nhé: khâu đúng các mảnh và điền đúng địa chỉ — đó là cả nghề liên kết.
Linking is not a quiet afterthought but a sophisticated process that binds each reference to exactly one definition and then places every piece at its real address. Master symbol resolution and relocation, and you can debug the most baffling errors and unlock the whole world of shared libraries.1