Bài 8 · Nâng cao · 26 phút· Cập nhật 11/06/2026
Gỡ lỗi, kiểm thử & CI
Biên soạn bởi Nguyễn Anh Tuấn
Gỡ lỗi C với gdb/lldb: breakpoint, đọc stack trace; viết unit test với assert/ctest (Unity/CMocka); dựng CI GitHub Actions chạy kiểm tra tự động.
Khi code chạy sai, đừng đoán - soi bằng trình gỡ lỗi. Biên dịch với -g rồi điều khiển từng bước, in biến, xem call stack:
phien-gdb
gcc -std=c17 -g main.c -o app # -g: gdb co ten bien & so dong
gdb ./app # (macOS: lldb ./app)
(gdb) break main # dat breakpoint o ham main (b)
(gdb) run # chay toi breakpoint (r)
(gdb) next # chay 1 dong, KHONG vao ham (n)
(gdb) step # BUOC VAO ham duoc goi (s)
(gdb) print x # in gia tri bien x (p x)
(gdb) backtrace # chuoi loi goi ham hien tai (bt)
(gdb) watch tong # dung khi 'tong' thay doi
(gdb) continue # chay tiep (c) - ▸break / run / next / step / print / backtrace / continue - bộ lệnh cốt lõi.
- ▸next = qua một dòng; step = bước vào hàm. watch = dừng khi biến đổi.
- ▸lldb (macOS) lệnh tương đương: b, r, n, s, p, bt, c.
Khi chương trình sập (segfault) hay sanitizer báo lỗi, stack trace cho biết chuỗi hàm đang chạy lúc đó - đọc từ trên (nơi sập) xuống (ai gọi):
stack-trace
(gdb) backtrace
#0 tinh_tong (a=0x0, n=5) at sum.c:7 <- noi SAP (a la NULL!)
#1 xu_ly (data=...) at app.c:22 <- ham goi tinh_tong
#2 main () at app.c:40 <- diem bat dau
# Sinh & mo core dump (anh chup luc sap):
ulimit -c unlimited
./app # sap -> sinh file 'core'
gdb ./app core # mo de xem trang thai luc sap Mẹo
Test tự động là tấm lưới an toàn: sửa code mà không sợ làm hỏng thứ khác. Mẫu Arrange-Act-Assert, chạy qua ctest:
test_util.c
#include <assert.h>
#include "util.h"
static void test_binh_phuong(void) {
assert(binh_phuong(3) == 9); // Arrange + Act + Assert
assert(binh_phuong(0) == 0);
assert(binh_phuong(-2) == 4);
}
int main(void) {
test_binh_phuong();
return 0; // exit 0 = pass; assert fail -> abort (exit khac 0)
} CMakeLists.txt (phần test)
enable_testing()
add_executable(tests test_util.c util.c)
add_test(NAME util_tests COMMAND tests)
# chay: ctest --test-dir build --output-on-failure - ▸assert.h đủ cho bài nhỏ; lớn hơn: Unity, CMocka, Criterion.
- ▸Mẫu Arrange-Act-Assert: dựng dữ liệu, gọi, kiểm tra kết quả.
- ▸ctest gom & chạy mọi test; exit code khác 0 = có test trượt.
CI (Continuous Integration) chạy CẢ chuỗi công cụ của khoá này trên mỗi lần đẩy code - và chặn merge nếu có khâu nào hỏng. Bật/tắt “lỗi” để xem pipeline dừng ở đâu:
CI chạy đúng thứ tự công cụ của cả khoá: định dạng → phân tích tĩnh → biên dịch → kiểm thử → test dưới sanitizer. Dừng ở thất bại đầu tiên - sửa từ gốc, không cần chạy phần sau.
.github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: sudo apt-get update && sudo apt-get install -y cmake clang-format clang-tidy
- run: clang-format --dry-run --Werror $(git ls-files '*.c' '*.h')
- run: cmake -S . -B build -DCMAKE_C_FLAGS="-fsanitize=address,undefined -g" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
- run: cmake --build build
- run: clang-tidy -p build $(git ls-files '*.c')
- run: ctest --test-dir build --output-on-failure Bạn vừa đi hết bộ công cụ biến một người “viết được C” thành lập trình viên C đi làm được. Quy trình hằng ngày khép kín:
- ▸Viết code → clang-format (định dạng) → clang-tidy (bắt lỗi tĩnh).
- ▸CMake build → chạy test (ctest) → chạy lại test dưới ASan/UBSan.
- ▸Đẩy lên → CI chạy hết chuỗi trên, chặn merge nếu hỏng.
- ▸Khi sập: gdb/lldb + stack trace để lần ra nguyên nhân.
Xong khoá Toolchain - bạn đã có “xưởng” của riêng mình
Build một dự án CMake có unit test, chạy ctest, gỡ một lỗi bằng gdb/lldb, và đọc workflow CI mẫu.
$ unzip 08-debug-test-ci.zip && cd 08-debug-test-ci # tai zip bang nut o tren $ cmake -S . -B build && cmake --build build $ ctest --test-dir build --output-on-failure $ gdb ./build/app # (macOS: lldb ./build/app) - break, run, bt, print $ cat .github/workflows/ci.yml
Xem từng file (7) ↡
Câu hỏi thường gặp
gdb (GNU, phổ biến trên Linux) và lldb (LLVM, mặc định macOS) cùng làm một việc. Lệnh gần như tương đương: break/b, run/r, next/n, step/s, print/p, backtrace/bt, continue/c. Học một bộ là dùng được cả hai.
next chạy hết MỘT dòng và KHÔNG đi vào hàm được gọi (nhảy qua nó). step thì BƯỚC VÀO trong hàm. Dùng next để đi nhanh trên dòng hiện tại; dùng step khi muốn xem bên trong một hàm.
breakpoint dừng khi chạy tới một DÒNG/hàm. watchpoint dừng khi một BIẾN thay đổi giá trị - rất tiện khi cần biết “ai đã sửa biến này?”. Trong gdb: break main.c:10 và watch x.
Nhỏ thì assert.h là đủ. Lớn hơn: Unity (nhẹ, hay dùng cho nhúng), CMocka (có mock), Criterion (hiện đại). Quan trọng không phải framework mà là CÓ test và chạy TỰ ĐỘNG (ctest/CI), theo mẫu Arrange-Act-Assert.
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.
Trong gdb, đang ở dòng gọi ham_phu() và muốn ĐI VÀO bên trong ham_phu để xem, dùng lệnh nào?
- 1
Lệnh gdb
Trong gdb, lệnh nào đặt breakpoint, lệnh nào in giá trị biến
x?Hoàn thành khi:
break <hàm/dòng>đặt breakpoint;print x(hoặcp x) in giá trịx. - 2
next hay step
Bạn đang ở dòng gọi
ham_phu(). Muốn ĐI VÀO trongham_phuđể xem, dùng lệnh nào?Hoàn thành khi:
step(s).nextsẽ nhảy quaham_phumà không vào trong. - 3
Viết một test
Viết một
assertkiểm trabinh_phuong(4) == 16.Hoàn thành khi:
assert(binh_phuong(4) == 16); - 4
Khâu bị skip
Trong công cụ ở Bước 4, bật “Lỗi biên dịch”. Những khâu nào bị bỏ qua? Vì sao?
Hoàn thành khi: Kiểm thử và Test+Sanitizers bị skip - pipeline dừng ngay ở khâu Biên dịch (không build được thì chưa thể test).
- 5
CI fail ở format
CI báo đỏ ở khâu clang-format. Bạn làm gì để qua khâu này?
Hoàn thành khi: Chạy
clang-format -itrên các file, commit, đẩy lại. CI chạy lại từ đầu và qua khâu format. - 6
Quy trình hoàn chỉnh
Liệt kê đúng thứ tự các khâu của một CI C chuyên nghiệp (từ bài học).
Hoàn thành khi: Định dạng → Phân tích tĩnh → Biên dịch → Kiểm thử → Test dưới Sanitizer.