← C Thực Chiến · Toolchain & Kiểm soát chất lượng

Bài 2 · Nâng cao · 22 phút· Cập nhật 11/06/2026

Thư viện & liên kết

Biên soạn bởi Nguyễn Anh Tuấn

Thư viện tĩnh (.a) vs thư viện động (.so) trong C: symbol, linker giải quyết ký hiệu, include guard - hiểu lỗi undefined reference & multiple definition.

Mỗi file .c là một đơn vị dịch (translation unit), biên dịch độc lập thành một .o. Để dùng hàm ở file khác, bạn cần khai báo (prototype) nó - thường đặt trong header:

ba-file.c

/* util.h  - KHAI BAO: "co ham nay, kieu the nay" */
int tinh(int x);

/* util.c  - DINH NGHIA: than ham */
int tinh(int x) { return x * x; }

/* main.c  - DUNG: can khai bao qua #include */
#include "util.h"
int main(void) { return tinh(5); }
  • Khai báo: “có ký hiệu này, kiểu thế này” (prototype) - được phép lặp.
  • Định nghĩa: thân hàm hoặc storage của biến - chỉ MỘT lần (ODR).
  • Mỗi .o ghi rõ: ký hiệu nó ĐỊNH NGHĨA và ký hiệu nó còn THIẾU.

Trình liên kết (linker) ghép các .o và thư viện, rồi tìm định nghĩa cho mọi ký hiệu còn thiếu. Bật/tắt các thành phần để xem ký hiệu được giải quyết - hay thất bại với undefined reference:

Chọn thành phần đưa vào lệnh liên kết:
main.o libc
$ gcc main.o util.o -lm -o app
Ký hiệu cầnĐược dùng bởiNơi cung cấp
tinh main.o ✓ util.o
printf main.o, util.o ✓ libc
sqrt main.o ✓ libm
✓ Liên kết thành công → sinh ra chương trình app. Mọi ký hiệu đã tìm được định nghĩa.

Thử bỏ util.o → mất tinh; bỏ libm → mất sqrt. Đây chính là lỗi “undefined reference” ở Bài 1, giờ bạn thấy trình liên kết ghép ký hiệu thế nào.

  • Mỗi ký hiệu cần (tinh, printf, sqrt) phải có ĐÚNG MỘT nơi cung cấp.
  • Không nơi nào cung cấp → undefined reference (lỗi liên kết).
  • Hai nơi cùng định nghĩa → multiple definition (cũng lỗi liên kết).

Header chia sẻ khai báo giữa các file. Nhưng nếu một header bị dán hai lần (qua include lồng nhau), bạn gặp lỗi định nghĩa lặp - nên luôn bọc include guard:

util.h

#ifndef UTIL_H        /* neu chua dinh nghia UTIL_H */
#define UTIL_H        /* thi dinh nghia no, va... */

int tinh(int x);      /* ...dan noi dung nay (chi 1 lan) */

extern int dem;       /* bien toan cuc DUNG CHUNG: khai bao */

#endif                /* het guard */
  • #ifndef/#define/#endif (hoặc #pragma once) → mỗi header chỉ dán một lần.
  • static ở phạm vi file = internal linkage: riêng tư, linker không thấy.
  • Biến toàn cục dùng chung: extern trong header, định nghĩa MỘT lần trong .c.

Thư viện tĩnh là một “kho” các .o gộp bằng ar. Khi liên kết, mã cần thiết được copy thẳng vào chương trình:

tao-dung-thu-vien-tinh

gcc -std=c17 -c util.c -o util.o
ar rcs libutil.a util.o          # dong goi .o thanh thu vien tinh
gcc -std=c17 main.c -L. -lutil -o app   # -L. tim trong thu muc nay; -lutil = libutil.a
./app                            # chay doc lap, khong can libutil.a nua

Đặc điểm liên kết tĩnh

Chương trình chạy độc lập (không cần file thư viện lúc chạy), nhưng exe to hơn và khi thư viện vá lỗi thì phải build lại chương trình. Thứ tự quan trọng: thư viện đứng sau file dùng nó.

Thư viện động được liên kết lúc chạy, không copy vào exe. Nhiều chương trình dùng chung một bản trong RAM:

tao-dung-thu-vien-dong

gcc -std=c17 -fPIC -c util.c -o util.o   # -fPIC: ma doc lap vi tri
gcc -shared util.o -o libutil.so         # tao thu vien dong
gcc -std=c17 main.c -L. -lutil -o app    # link DONG
./app                            # can libutil.so luc CHAY
ldd app                          # xem cac thu vien dong app can
  • Liên kết lúc chạy: exe nhỏ, nhiều app dùng chung một .so trong bộ nhớ.
  • Vá lỗi thư viện: thay .so là xong, không cần build lại app.
  • Đổi lại: .so PHẢI có mặt lúc chạy (ldd, LD_LIBRARY_PATH, rpath).
Chạy trên máy bạn - kỹ năng thật cần gõ lệnh thật

Tách dự án thành nhiều file, tự tạo thư viện tĩnh (.a) và động (.so), và tái tạo 'undefined reference' bằng cách bỏ một file khi liên kết.

$ unzip 02-thu-vien-lien-ket.zip && cd 02-thu-vien-lien-ket   # tai zip bang nut o tren
$ gcc -c util.c -o util.o
$ ar rcs libutil.a util.o            # thu vien tinh
$ gcc main.c -L. -lutil -o app_tinh  # link tinh
$ gcc -fPIC -shared util.c -o libutil.so  # thu vien dong
$ gcc -c main.c -o main.o
$ gcc main.o -o broken               # tai tao undefined reference
Tải mã nguồn (.zip) Xem trên GitHub bài lab: 02-thu-vien-lien-ket
Xem từng file (4) ↡

Câu hỏi thường gặp

Khai báo (declaration) nói “có thứ này tồn tại, kiểu thế này” - vd prototype int tinh(int);. Định nghĩa (definition) cung cấp thân hàm hoặc storage - int tinh(int x) { … }. Một ký hiệu được khai báo nhiều lần, nhưng chỉ định nghĩa MỘT lần (Quy tắc Một Định Nghĩa - ODR).

Bài C biên dịch như thế nào? - khoá C cơ bản →

Khi nhiều file (hoặc header lồng nhau) cùng #include một header, nội dung bị dán nhiều lần → định nghĩa lặp, lỗi biên dịch. Include guard (#ifndef X / #define X / #endif) hoặc #pragma once đảm bảo mỗi header chỉ được dán MỘT lần cho mỗi đơn vị dịch.

Đó là internal linkage: hàm/biến chỉ nhìn thấy trong file .c đó, KHÔNG lộ ra cho trình liên kết. Dùng để đóng gói (hàm phụ trợ riêng tư) và tránh đụng tên với file khác. Ngược lại, mặc định hàm/biến toàn cục có external linkage (lộ ra cho linker).

Thư viện TĨNH (.a): tập hợp các .o, liên kết lúc BUILD - mã được COPY vào chương trình. Exe chạy độc lập nhưng to; vá lỗi thư viện phải build lại. Thư viện ĐỘNG (.so/.dll/.dylib): liên kết lúc CHẠY - nhiều chương trình DÙNG CHUNG một bản trong RAM; exe nhỏ, vá thư viện không cần build lại app - nhưng phải có .so lúc chạy.

Với liên kết tĩnh: CÓ. Trình liên kết quét trái → phải; một thư viện phải đứng SAU file dùng nó: gcc main.c -lutil (đúng), không phải gcc -lutil main.c (có thể “undefined reference” vì lúc gặp lib chưa biết cần tinh).

Cùng một ký hiệu được ĐỊNH NGHĨA ở hai nơi - thường do định nghĩa hàm/biến trong header rồi include vào nhiều .c. Sửa: header chỉ KHAI BÁO; định nghĩa nằm trong đúng một .c. Với biến toàn cục: extern int g; trong header, int g; định nghĩa một lần trong .c.

Bài Header & module - khoá C nâng cao →

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Theo Quy tắc Một Định Nghĩa (ODR), một ký hiệu được phép xuất hiện thế nào?

  1. 1

    Bỏ util.o

    Trong công cụ ở Bước 2, bỏ chọn util.o. Ký hiệu nào thành undefined? Lỗi này nói gì?

    Hoàn thành khi: tinh → "undefined reference to tinh": linker không tìm thấy định nghĩa của tinh (nó nằm trong util.o).

  2. 2

    Bỏ libm

    Bỏ chọn libm (-lm). Ký hiệu nào mất? Cách sửa trên dòng lệnh?

    Hoàn thành khi: sqrt → thêm -lm vào lệnh liên kết để nối thư viện math.

  3. 3

    Header chứa gì

    util.h nên chứa khai báo hay định nghĩa của tinh? Vì sao?

    Hoàn thành khi: Khai báo (prototype). Nếu định nghĩa trong header, nhiều .c include sẽ gây "multiple definition". Định nghĩa đặt trong util.c.

  4. 4

    Tạo thư viện tĩnh

    ar rcs libfoo.a a.o b.o làm gì? Rồi link nó vào main thế nào?

    Hoàn thành khi: Đóng gói a.o, b.o thành thư viện tĩnh libfoo.a. Link: gcc -std=c17 main.c -L. -lfoo -o app.

  5. 5

    Lỗi runtime của link động

    Chương trình link động báo "error while loading shared libraries: cannot open shared object file". Vì sao và sửa sao?

    Hoàn thành khi: Thiếu .so lúc CHẠY (linker động không tìm thấy). Đặt .so vào đường chuẩn, hoặc set LD_LIBRARY_PATH, hoặc nhúng rpath khi build.

  6. 6

    Tĩnh hay động?

    Khi nào nên link tĩnh, khi nào động? Nêu một lý do mỗi bên.

    Hoàn thành khi: Tĩnh: triển khai đơn giản, exe độc lập (không phụ thuộc .so). Động: nhiều chương trình chung lib → tiết kiệm RAM/đĩa, vá lib không cần build lại app.