Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 20.426 phút đọc

Biên dịch, liên kết và build lại từng phần

Sau bài này bạn sẽ làm được

  • Phân biệt lỗi biên dịch với lỗi liên kết qua thông báo
  • Biên dịch từng tệp thành .o rồi liên kết lại
  • Giải thích vì sao build lại từng phần nhanh hơn build lại tất cả
  • Viết một Makefile tối thiểu chạy được

Cho tới giờ bạn chỉ chạy gcc a.c b.c -o app. Nó vẫn đúng. Nhưng với dự án ba mươi tệp, cách đó dịch lại toàn bộ mỗi lần bạn sửa một dấu chấm phẩy, và đó là khác biệt giữa nửa giây và nửa phút cho mỗi lần thử.

#Một lệnh hay hai bước

/* Cach 1: mot lenh */
gcc -Wall -Wextra -Iinclude src/*.c -o app

/* Cach 2: bien dich rieng roi lien ket */
gcc -Wall -Wextra -Iinclude -c src/main.c     -o build/main.o
gcc -Wall -Wextra -Iinclude -c src/sinhvien.c -o build/sinhvien.o
gcc -Wall -Wextra -Iinclude -c src/quanly.c   -o build/quanly.o
gcc build/main.o build/sinhvien.o build/quanly.o -o app
Một lệnhHai bước
Số lệnh phải gõ1n cộng 1
Sửa một tệp thì dịch lại mấy tệpTất cảĐúng một
Dịch song song nhiều tệpKhôngĐược, mỗi tệp một tiến trình
Cần công cụ hỗ trợKhôngNên có Makefile
Hợp choDự án dưới 5 tệpMọi dự án lớn hơn

#Đơn vị dịch

Đơn vị dịch
Một tệp .c sau khi preprocessor đã dán hết mọi header vào. Đây là thứ trình biên dịch thật sự nhìn thấy, và mỗi đơn vị dịch cho ra đúng một tệp .o.
main.c  +  moi header no include  ->  mot don vi dich  ->  main.o
quanly.c + moi header no include  ->  mot don vi dich  ->  quanly.o

/* Hai don vi dich KHONG BIET GI VE NHAU.
   Trinh bien dich dich quanly.c ma khong he nhin thay main.c.

   Do la ly do:
     - quanly.c phai include header de biet kieu SinhVien
     - ham o main.c goi duoc tu quanly.c chi khi co nguyen mau
     - loi khong khop chi lo ra o buoc lien ket */

#Build lại từng phần

  1. Lần đầu: dịch mọi tệp

    terminal
    make
    gcc -Wall -Wextra -Iinclude -c src/main.c -o build/main.o
    gcc -Wall -Wextra -Iinclude -c src/sinhvien.c -o build/sinhvien.o
    gcc -Wall -Wextra -Iinclude -c src/quanly.c -o build/quanly.o
    gcc -Wall -Wextra -Iinclude -c src/luutru.c -o build/luutru.o
    gcc -Wall -Wextra -Iinclude -c src/ui.c -o build/ui.o
    gcc build/main.o build/sinhvien.o build/quanly.o build/luutru.o build/ui.o -o build/app
  2. Sửa một tệp .c: dịch lại một tệp

    terminal
    touch src/quanly.c && make
    gcc -Wall -Wextra -Iinclude -c src/quanly.c -o build/quanly.o
    gcc build/main.o build/sinhvien.o build/quanly.o build/luutru.o build/ui.o -o build/app
  3. Sửa một header: dịch lại mọi tệp include nó

    terminal
    touch include/sinhvien.h && make
    gcc -Wall -Wextra -Iinclude -c src/main.c -o build/main.o
    gcc -Wall -Wextra -Iinclude -c src/sinhvien.c -o build/sinhvien.o
    gcc -Wall -Wextra -Iinclude -c src/quanly.c -o build/quanly.o
    gcc -Wall -Wextra -Iinclude -c src/luutru.c -o build/luutru.o
    gcc build/... -o build/app
  4. Không sửa gì: không làm gì

    terminal
    make
    make: 'build/app' is up to date.

#Makefile tối thiểu

Makefile
CC      = gcc
CFLAGS  = -std=c11 -Wall -Wextra -Wpedantic -g -Iinclude
LDFLAGS =

NGUON = $(wildcard src/*.c)
DOI   = $(NGUON:src/%.c=build/%.o)
DICH  = build/ql-sinhvien

all: $(DICH)

$(DICH): $(DOI)
	$(CC) $(DOI) $(LDFLAGS) -o $@

build/%.o: src/%.c | build
	$(CC) $(CFLAGS) -c $< -o $@

build:
	mkdir -p build

clean:
	rm -rf build

.PHONY: all clean
Ký hiệuNghĩaTrong ví dụ trên
$@Tên mục tiêubuild/main.o
$<Phụ thuộc đầu tiênsrc/main.c
$^Mọi phụ thuộcToàn bộ danh sách .o
%Ký tự đại diện trong quy tắc mẫuKhớp main, quanly, ...
|Phụ thuộc chỉ theo thứ tựThư mục build, không tính thời gian sửa

#Cờ nên dùng

CFLAGS = -std=c11 -Wall -Wextra -Wpedantic -g -Iinclude
CờBắt được gì
-std=c11Từ chối phần mở rộng của trình biên dịch, mã khả chuyển hơn
-WallKhoảng bốn mươi cảnh báo cơ bản, không hề là tất cả
-WextraThêm hai chục cảnh báo nữa, đáng bật
-WpedanticCảnh báo mọi chỗ đi lệch khỏi chuẩn
-gThông tin gỡ lỗi, không ảnh hưởng tốc độ chạy
-IincludeThêm thư mục tìm header

Quy trình build hoàn chỉnh cho dự án Phần 9

terminal
make clean && make
gcc -std=c11 -Wall -Wextra -Wpedantic -g -Iinclude -c src/sinhvien.c -o build/sinhvien.o
... (bon tep nua) ...
gcc build/*.o -o build/ql-sinhvien
# Kiểm không cảnh báo với bộ cờ nghiêm ngặt
make clean && make CFLAGS='-std=c11 -Wall -Wextra -Wpedantic -Wshadow -Wconversion -Werror -Iinclude'
(khong co dong nao)
# Kiểm bộ nhớ
make asan && ./build/ql-sinhvien du-lieu.svdb < kich-ban.txt
(khong co dong nao)
make clean && make && valgrind --leak-check=full ./build/ql-sinhvien du-lieu.svdb < kich-ban.txt
==12345== All heap blocks were freed -- no leaks are possible
==12345== ERROR SUMMARY: 0 errors from 0 contexts

Tự làm thử

  1. Biên dịch dự án của bạn bằng một lệnh, rồi bằng hai bước, và so sánh các tệp sinh ra.
  2. Sửa một tệp .c rồi chạy make, đếm xem có mấy lệnh gcc chạy.
  3. Sửa một header rồi chạy make, so sánh với lần trên.
  4. Viết Makefile ở trên, cố ý dùng dấu cách thay tab, rồi đọc thông báo lỗi.
  5. Thêm mục tiêu asan và xác nhận cờ -fsanitize có ở cả hai bước.
  6. Đo thời gian make -j1 và make -j8 trên dự án của bạn.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Cờ -c dừng lại trước bước liên kết và sinh ra tệp .o. Đó là toàn bộ khác biệt giữa một lệnh và hai bước.
  • Mỗi tệp .c cùng mọi header của nó là một đơn vị dịch, và các đơn vị dịch không biết gì về nhau.
  • Sửa một tệp .c thì chỉ dịch lại tệp đó, còn sửa header thì phải dịch lại mọi tệp include nó.
  • Makefile không theo dõi header sẽ để lại tệp .o cũ và sinh ra lỗi rất khó tìm. Trong lúc chưa có, hãy make clean.
  • Cờ -fsanitize phải có ở cả bước dịch lẫn bước liên kết, còn -lm chỉ cần ở bước liên kết.