Computer Systems: A Programmer's Perspective Chương 7 · Linking

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

  1. 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ết ld nhậ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 đủ.1

    Bạ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ờ -v và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 gcc command, but underneath come preprocessing, compilation, assembly, and linking in sequence; you can watch them with the -v flag.1 The linker ld takes a collection of relocatable object files plus command-line arguments and produces a fully linked executable.1

    Bạ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 .o tạm rồi gọi ld ghé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 .o files and then calls ld to merge them.1

    Vì 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

  2. 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
  3. 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 .o là khả định vị, mỗi tệp a.out/prog là chạy được, mỗi tệp .so là đố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 .so chính là ba loại đó. Each .o file is relocatable, each a.out/prog is executable, and each .so is 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.1

    Mô-đ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

  1. 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áo static).1

    Có 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á static là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-static functions and global variables); external, used by this module but defined elsewhere; and local, visible and used only within its own module (static functions and variables).1

    Bạn gặp nó ở đâuWhere you meet it Khi bạn khai báo static cho 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ết static trước một biến để nó chỉ dùng được trong tệp hiện tại. When you mark a variable or function static to “hide” it from other modules — a good practice the book encourages.1

    Ký hiệu cục bộ ≠ biến cục bộ

    Bảng .symtab không chứa biến cục bộ không static — 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áo static thì được cấp chỗ trong .data/.bss và có một ký hiệu cục bộ tên duy nhất (ví dụ x.1, x.2).1

    Mục trong bảng ký hiệu

    Mỗi mục Elf64_Symbol gồ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

  2. 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-common hoặ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 an int in one module and a double in another: rule 2 can silently overwrite, creating insidious bugs. Use -fno-common or -Werror to make the linker complain instead of staying silent.1

    COMMON 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 static thì gán vào .bss — vì chúng chắc chắn là duy nhất.1

    Giả-phần ABS, UNDEF, COMMON

    Trường section có 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

  3. 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).1

    Thư 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 the ar tool. 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).1

    Bạn gặp nó ở đâuWhere you meet it Khi gcc ./libvector.a main2.c báo “undefined reference to ‘addvec’”: thư viện đặt trước tệp cần nó nên lúc quét, tập U rỗ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ỗ. When gcc ./libvector.a main2.c reports “undefined reference to ‘addvec’”: the library came before the file needing it, so U was empty at the scan and the module was not pulled in. Rule of thumb: put libraries at the end of the command line.1

    Vì 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.a khoảng 5 MB và libm.a khoảng 2 MB; thư viện tĩnh cho phép chỉ chép những mô-đun cần dùng.1

    Thư 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

  1. 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.1

    Khi 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), and addend (a bias constant). The linker iterates over each section and each entry to compute the new address.1

    Bạn gặp nó ở đâuWhere you meet it Khi objdump -dx hiện một dòng như R_X86_64_PC32 sum-0x4 cạnh một lệnh call — đó chính là mục định vị lại cho lời gọi sum.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. When objdump -dx shows a line like R_X86_64_PC32 sum-0x4 beside a call instruction — that is the relocation entry for the call to sum.1

    Một ví dụ cụ thể

    Trong ví dụ của sách, lời gọi sum có mục r.offset=0xf, r.symbol=sum, r.type=R_X86_64_PC32, r.addend=-4. Với ADDR(.text)=0x4004d0 và ADDR(sum)=0x4004e8, trình liên kết tính refaddr=0x4004df rồi *refptr = 0x4004e8 + (-4) - 0x4004df = 0x5.1

    Hai 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

  2. 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 đó.1

    Tệ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 .rel sections. 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 at 0x400000, a read/write data segment just after.1

    Bạn gặp nó ở đâuWhere you meet it Khi bạn gõ ./prog, shell gọi loader qua execve; 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 _start trong crt1.o, cuối cùng gọi __libc_start_main rồi main.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 via execve; the loader copies code and data into memory and jumps to the entry point, always the _start function in crt1.o, which eventually calls __libc_start_main and then main.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 (do malloc mở 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ưới 2^48 − 1, và vùng nhân ở trên 2^48.1

    Nạ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

  3. 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 .so trê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.so thực hiện. Trong hệ tệp chỉ có một bản .so cho mỗi thư viện, và một bản .text trong bộ nhớ có thể được nhiều tiến trình dùng chung.15

    Thay 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 .so và 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 .so file 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 as ld-linux.so. The file system holds just one .so per library, and a single copy of its .text in memory can be shared by many processes.15

    Bạ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ằng dlopen, rồi lấy hàm bằng dlsym — 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.1

    GOT 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ờ --wrap với __wrap_f/__real_f), hoặc thời điểm chạy (biến môi trường LD_PRELOAD). Bộ công cụ GNU binutils gồm ar, 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.

  1. 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”?

  2. Kể ba loại ký hiệu với trình liên kết, và cho biết từ khoá static làm một ký hiệu thành loại nào.

    Gợi ý sâu hơn: vì sao .symtab không chứa biến cục bộ không static?

  3. 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?

  4. Vì sao .bss khô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, D thay đổi ra sao khi quét một thư viện tĩnh?

  5. 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?

  1. 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?

  2. Từ khoá static làm một cái tên trở nên thế nào — dùng chung hay riêng tư?

  3. Nếu hai tệp cùng đặt một tên toàn cục, máy chọn ra sao?

  4. Vì sao đặt thư viện sai chỗ trên dòng lệnh lại gây lỗi “undefined reference”?

  5. 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?

  1. Which programs does the compiler driver invoke, in order, and what are the linker's two core tasks?

  2. Name the three kinds of linker symbols, and say which kind static makes a symbol.

  3. How do strong and weak symbols differ, and what are the three rules for duplicate names?

  4. Why does .bss take no space in the object file, and why does library order on the command line matter?

  5. 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