Bài 1 · Nâng cao · 22 phút· Cập nhật 11/06/2026
Hành trình biên dịch
Biên soạn bởi Nguyễn Anh Tuấn
Bốn giai đoạn biên dịch C (compile): tiền xử lý → dịch → hợp dịch → liên kết; cờ gcc -E/-S/-c và cách đọc lỗi undefined reference của linker.
Khi bạn gõ gcc main.c -o app, một dây chuyền bốn giai đoạn âm thầm chạy: tiền xử lý → dịch → hợp dịch → liên kết. Bấm Giai đoạn tiếp để xem mã nguồn biến đổi qua từng bước:
- ▸Tiền xử lý (-E): xử lý #include, #define, #if → mã C “phẳng” (.i).
- ▸Dịch (-S): mã C → hợp ngữ cho CPU đích (.s); nơi tối ưu hoá diễn ra.
- ▸Hợp dịch (-c): hợp ngữ → mã máy trong file đối tượng (.o) + bảng ký hiệu.
- ▸Liên kết: ghép các .o + thư viện, giải quyết ký hiệu → chương trình chạy được.
Bộ tiền xử lý không hiểu C - nó dán và thay văn bản: chèn nội dung header của #include, bung #define và macro, xử lý #if. Xem kết quả bằng gcc -E:
truoc-tien-xu-ly.c
#define MAX 100
#define GAP(x) ((x) + (x))
int bang[MAX];
int y = GAP(5); sau gcc -E (đã phẳng)
int bang[100];
int y = ((5) + (5)); Vì sao macro cần nhiều ngoặc
gcc -S dịch sang hợp ngữ (.s) - bạn đọc được tên thanh ghi, lệnh CPU. gcc -c hợp dịch tiếp thành file đối tượng (.o): mã máy nhị phân + bảng ký hiệu (định nghĩa gì, còn thiếu gì). Đây vẫn chưa phải chương trình chạy được:
- ▸.s - hợp ngữ: dạng người-đọc-được của lệnh máy (gcc -S).
- ▸.o - đối tượng: mã máy của MỘT file nguồn + bảng ký hiệu (gcc -c).
- ▸Một .o có thể “thiếu” printf, sqrt… - sẽ được lấp ở bước liên kết.
Trình liên kết (linker) ghép các .o lại, nối thư viện chuẩn (libc), và giải quyết ký hiệu: mỗi lời gọi hàm tìm đúng định nghĩa. Thiếu một định nghĩa ⇒ lỗi undefined reference - và đây là lỗi của LIÊN KẾT, không phải trình dịch:
loi-lien-ket
// dung sqrt nhung quen -lm
gcc -std=c17 main.c -o app
// /usr/bin/ld: undefined reference to 'sqrt'
// Sua: noi thu vien math khi LIEN KET
gcc -std=c17 main.c -lm -o app Quy tắc nhớ
Bình thường bạn để gcc chạy cả bốn giai đoạn một lần. Hiểu các cờ để điều khiển nó như dân chuyên:
lenh-thuc-te
gcc -Wall -Wextra -Werror -g -O2 -std=c17 main.c util.c -o app
# -Wall -Wextra : bat canh bao (dung NGAY tu dau)
# -Werror : coi canh bao la loi (ky luat)
# -g : them thong tin go loi cho gdb
# -O2 : toi uu cho ban phat hanh
# -std=c17 : chot chuan C17 - chuan dung XUYEN SUOT moi khoa C cua Meo Tự tay đưa một file qua từng giai đoạn biên dịch, xem .i/.s/.o, và tái tạo lỗi 'undefined reference' rồi sửa.
$ # Tai lab bang nut “📦 Tai ma nguon (.zip)” o tren, roi: $ unzip 01-hanh-trinh-bien-dich.zip && cd 01-hanh-trinh-bien-dich $ gcc -E hello.c -o hello.i # tien xu ly $ gcc -S hello.c -o hello.s # dich ra assembly $ gcc -c hello.c -o hello.o # hop dich ra .o $ gcc hello.o -o hello && ./hello
Xem từng file (3) ↡
Câu hỏi thường gặp
Tách giai đoạn cho phép (1) build TĂNG DẦN: chỉ dịch lại file .c thay đổi rồi liên kết lại, nhanh hơn nhiều; (2) tái dùng .o trong nhiều chương trình; (3) gỡ lỗi từng bước. gcc thật ra gọi lần lượt cpp (tiền xử lý), cc1 (dịch), as (hợp dịch), ld (liên kết).
File đối tượng (.o) chứa mã máy của MỘT file nguồn, kèm bảng ký hiệu (symbol table): những hàm/biến nó định nghĩa và những thứ nó còn THIẾU (vd printf). Nó CHƯA chạy được - phải qua liên kết để ghép các .o và nối thư viện, lấp mọi ký hiệu còn thiếu.
Lỗi của trình LIÊN KẾT (không phải trình dịch): một ký hiệu (hàm/biến) được dùng nhưng không tìm thấy định nghĩa ở .o nào hay thư viện nào. Thường do quên thêm file .c/.o, hay quên link thư viện (vd -lm cho math). Khác với lỗi cú pháp - đó là của trình dịch.
Chỉ thao tác văn bản: dán nội dung file của #include vào, thay #define và bung macro, giữ/bỏ khối #if/#ifdef. Nó KHÔNG hiểu cú pháp C. Sau bước này không còn dòng nào bắt đầu bằng #. Xem được bằng gcc -E.
Không. Header chỉ được DÁN vào các .c qua #include rồi biên dịch cùng. Vì thế header thường chỉ chứa khai báo (prototype, struct, macro), còn định nghĩa hàm nằm trong .c. Dùng include guard (#ifndef) để tránh dán hai lần.
-Wall -Wextra (bật cảnh báo), -Werror (coi cảnh báo là lỗi), -g (thông tin gỡ lỗi cho gdb), -O2 (tối ưu cho bản phát hành), -std=c17 (chuẩn ngôn ngữ). Khi học nên thêm -fsanitize=address để bắt lỗi bộ nhớ (bài Sanitizers).
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Khi gcc dịch một file .c thành chương trình chạy được, bốn giai đoạn diễn ra theo thứ tự nào?
- 1
Dừng ở từng giai đoạn
Với một file
hello.c, chạygcc -E,-S,-clần lượt. Mỗi lệnh sinh ra file gì?Hoàn thành khi:
-E→.i(hoặc in ra màn hình);-S→.s(assembly);-c→.o(đối tượng). Không cờ → chương trình chạy được. - 2
Đọc pipeline
Trong mô phỏng ở Bước 1, sau giai đoạn “Hợp dịch” đã có chương trình chạy được chưa? Vì sao?
Hoàn thành khi: Chưa - mới có
.o(mã máy + bảng ký hiệu). Phải qua LIÊN KẾT để ghép.ovà nốilibcmới ra chương trình. - 3
undefined reference
Bạn dùng
sqrt()nhưng gcc báo “undefined reference to sqrt”. Đây là lỗi giai đoạn nào, sửa sao?Hoàn thành khi: Lỗi LIÊN KẾT - thiếu thư viện math. Sửa: thêm
-lmvào lệnh link (gcc main.c -lm). - 4
Tiền xử lý
Cho
#define GAP(x) ((x)+(x)). Sau tiền xử lý,GAP(3*2)bung thành gì? Vì sao cần ngoặc?Hoàn thành khi:
((3*2)+(3*2)). Ngoặc quanh(x)và quanh cả macro tránh sai thứ tự ưu tiên khi thay văn bản. - 5
Build tăng dần
Dự án có
main.cvàutil.c. Bạn chỉ sửautil.c. Về lý thuyết cần dịch lại file nào?Hoàn thành khi: Chỉ
util.c→util.o, rồi liên kết lại.main.ocũ dùng lại được - đó là ý tưởng của build tăng dần (Make). - 6
Đọc lệnh gcc
gcc -Wall -Wextra -g -std=c17 main.c -o applàm gì? Liệt kê vai trò từng cờ.Hoàn thành khi:
-Wall -Wextrabật cảnh báo;-gthêm thông tin gỡ lỗi;-std=c17chọn chuẩn;-o appđặt tên chương trình ra làapp.