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

Project: báo cáo săn lỗi

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

  • Lập báo cáo đầy đủ cho từng lỗi tìm được
  • Giải thích nguyên nhân chứ không chỉ cách sửa
  • Chứng minh cả ba công cụ đều sạch sau khi vá
  • Viết bộ kiểm thử hồi quy cho mọi lỗi đã sửa

Một dự án C từ năm trăm tới một nghìn dòng, có ít nhất mười lăm lỗi thuộc đủ các loại đã học. Bạn nộp bốn thứ: báo cáo, bản vá, bằng chứng, và bộ kiểm thử hồi quy. Đây là bài tập gói lại toàn bộ Phần 13.

#Đề bài

Cây thư mục dự án
quanly/
  include/
    sinhvien.h        khai bao kieu va ham
    danhsach.h        danh sach dong
    tepdulieu.h       doc ghi tep
    chuoi.h           tien ich chuoi

  src/
    sinhvien.c
    danhsach.c
    tepdulieu.c
    chuoi.c
    main.c

  test/
    test.h            khung kiem thu ba macro, tu Bai 32.4
    test.c
    test_hoi_quy.c    ban se viet tep nay

  du_lieu/
    hop_le.csv
    hong.csv          du lieu vao co van de, de kich hoat cac loi

  Makefile
Nhóm lỗiSố lượng tối thiểuBài đã học
Truy cập ngoài biên, heap và ngăn xếp333.3, 34.2
Rò rỉ bộ nhớ234.4
Dùng sau khi giải phóng hoặc giải phóng hai lần233.3, 34.2
Biến chưa khởi tạo134.4
Tràn số hoặc dịch bit sai233.4, 34.3
Vi phạm strict aliasing133.6
Lỗi logic thuần túy, không công cụ nào bắt235.4
Giả định implementation-defined133.7
Không kiểm tra giá trị trả về của malloc hoặc fopen118.2

#Quy trình bắt buộc

  1. Dịch sạch trước đã

    Chạy với đủ cờ cảnh báo và sửa hết. Ghi lại số cảnh báo ban đầu và số lỗi mà chúng bắt được. Ở Bài 34.6 con số đó là hai trên mười.

  2. Chạy ba lượt sanitizer

    ASan cộng UBSan, rồi Valgrind. Ghi lại từng báo cáo nguyên văn trước khi sửa bất cứ thứ gì.

  3. Sửa một lỗi, chạy lại toàn bộ

    Không sửa nhiều lỗi cùng lúc. Một số lỗi che lấp lỗi khác, và sửa lỗi đầu có thể làm lộ ra lỗi thứ hai ở cùng chỗ.

  4. Với mỗi lỗi, viết kiểm thử trước khi vá

    Chạy kiểm thử trên bản chưa vá và chép lại dòng HONG. Đó là bằng chứng kiểm thử của bạn thật sự bắt được lỗi.

  5. Hai lỗi cuối tìm bằng cách đọc mã

    Sau khi ba công cụ đã im lặng mà kết quả vẫn sai, bạn còn lại lỗi logic. Dùng GDB và watchpoint, và cuối cùng là đọc mã.

#Bộ công cụ và kịch bản

Makefile
CC        = gcc
CHUNG     = -std=c11 -Iinclude \
            -Wall -Wextra -Wpedantic -Wshadow -Wconversion \
            -Wcast-qual -Wstrict-prototypes -Wmissing-prototypes \
            -Wnull-dereference -Wdouble-promotion -Wwrite-strings \
            -Wformat=2 -Wstack-usage=8192

NGUON     = $(wildcard src/*.c)
NGUON_THU = $(wildcard test/*.c)

# --- ban thuong, chi de xem chuong trinh chay the nao ---
thuong:
	$(CC) $(CHUNG) -g -O1 -o quanly $(NGUON)

# --- ban sanitizer: ASan va UBSan ---
san:
	$(CC) $(CHUNG) -g3 -O1 -fno-omit-frame-pointer \
	      -fsanitize=address,undefined \
	      -fsanitize-address-use-after-scope \
	      -fno-sanitize-recover=all \
	      -o quanly_san $(NGUON)

# --- ban cho Valgrind ---
vg:
	$(CC) $(CHUNG) -g -O1 -o quanly_vg $(NGUON)
	valgrind --leak-check=full --show-leak-kinds=all \
	         --track-origins=yes --error-exitcode=1 \
	         ./quanly_vg du_lieu/hong.csv

# --- ban kiem thu hoi quy ---
thu:
	$(CC) $(CHUNG) -g3 -O1 -fsanitize=address,undefined \
	      -fno-sanitize-recover=all \
	      -o chay_thu $(filter-out src/main.c,$(NGUON)) $(NGUON_THU)
	ASAN_OPTIONS=detect_stack_use_after_return=1 ./chay_thu

# --- chay het ---
kiem: san thu vg
	ASAN_OPTIONS=detect_stack_use_after_return=1 \
	UBSAN_OPTIONS=print_stacktrace=1 \
	MALLOC_PERTURB_=42 \
	./quanly_san du_lieu/hong.csv

sach:
	rm -f quanly quanly_san quanly_vg chay_thu *.o

.PHONY: thuong san vg thu kiem sach
kiem_tra.sh, dùng cho git bisect
#!/bin/sh
# Tra ve 0 neu MOI THU sach, khac 0 neu co loi.
# Dung voi: git bisect run ./kiem_tra.sh

set -e

make sach  > /dev/null
make san   > /dev/null 2>&1 || exit 125    # 125 = khong dich duoc, bo qua

ASAN_OPTIONS=detect_leaks=1 \
UBSAN_OPTIONS=halt_on_error=1 \
MALLOC_PERTURB_=42 \
./quanly_san du_lieu/hong.csv > /dev/null 2>&1 || exit 1

make thu > /dev/null 2>&1 || exit 1

exit 0

#Mẫu báo cáo

bao_cao/loi-07.md
# Loi 07: ro ri bo nho khi doc dong CSV hong

| | |
|---|---|
| **Tep va dong** | src/tepdulieu.c:64 |
| **Loai** | Ro ri bo nho, definitely lost |
| **Cong cu** | Valgrind, va LeakSanitizer xac nhan |
| **Muc do** | Cao: ro ri moi dong hong, tep lon lam het bo nho |
| **Trang thai** | Da va, co kiem thu hoi quy |

## Ket xuat goc

    ==9871== 5,600 bytes in 100 blocks are definitely lost in loss record 4 of 4
    ==9871==    at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
    ==9871==    by 0x4009A1: sv_tao (sinhvien.c:18)
    ==9871==    by 0x400B23: doc_dong (tepdulieu.c:58)
    ==9871==    by 0x400C05: doc_tep (tepdulieu.c:81)
    ==9871==    by 0x400E12: main (main.c:34)
    ==9871==
    ==9871== LEAK SUMMARY:
    ==9871==    definitely lost: 5,600 bytes in 100 blocks

## Nguyen nhan

doc_dong cap phat mot SinhVien bang sv_tao o dong 58, roi phan tich
cac truong. Neu mot truong khong hop le thi ham tra ve NULL o dong 64
ma KHONG giai phong doi tuong da cap.

Tep du_lieu/hong.csv co 100 dong hong, moi dong ro ri 56 byte,
tong 5600 byte, dung nhu Valgrind bao.

Day la mau loi kinh dien: duong THANH CONG co giai phong, duong LOI
thi khong. Loi loai nay khong bao gio lo ra trong kiem thu chi dung
du lieu hop le.

## Ban va

    @@ -60,7 +60,10 @@ SinhVien *doc_dong(const char *dong) {
         if (phan_tich_diem(truong[2], &sv->diem) != 0) {
    -        return NULL;
    +        sv_huy(sv);
    +        return NULL;
         }

## Bang chung sau khi va

    $ make vg
    ==9903== LEAK SUMMARY:
    ==9903==    definitely lost: 0 bytes in 0 blocks
    ==9903==    indirectly lost: 0 bytes in 0 blocks
    ==9903== ERROR SUMMARY: 0 errors from 0 contexts

    $ make san
    $ ./quanly_san du_lieu/hong.csv
    doc 900 dong hop le, bo qua 100 dong hong
    (khong co bao cao nao cua ASan)

## Kiem thu hoi quy

Xem test/test_hoi_quy.c ham thu_loi_07.

Truoc khi va:  1 kiem tra, 1 hong
Sau khi va:    1 kiem tra, 0 hong

#Bộ kiểm thử hồi quy

test/test_hoi_quy.c
#include <stdlib.h>
#include <string.h>

#include "test.h"
#include "sinhvien.h"
#include "tepdulieu.h"
#include "danhsach.h"
#include "chuoi.h"

/* Loi 01: ten qua dai ghi de len truong ke ben */
static void thu_loi_01(void) {
    NHOM("loi 01: tran bo dem ten");

    SinhVien sv = { 0 };
    sv.diem = 100;

    sv_dat_ten(&sv, "MotCaiTenRatDaiVuotQuaGioiHan");

    KIEM_BANG(sv.diem, 100);
    KIEM_BANG(sv.ten[sizeof sv.ten - 1], 0);
}

/* Loi 07: ro ri tren duong loi cua doc_dong */
static void thu_loi_07(void) {
    NHOM("loi 07: ro ri khi dong CSV hong");

    /* Doc mot dong hong 1000 lan. Neu con ro ri thi ASan bao
       khi chuong trinh thoat. */
    for (int i = 0; i < 1000; ++i) {
        SinhVien *sv = doc_dong("1,Nguyen Van A,KHONG_PHAI_SO");
        KIEM(sv == NULL);
    }
}

/* Loi 09: tran so khi tinh kich thuoc mang */
static void thu_loi_09(void) {
    NHOM("loi 09: tran khi tinh kich thuoc");

    DanhSach *ds = ds_tao();
    KIEM(ds != NULL);

    /* So phan tu vo ly phai bi tu choi, khong duoc cap phat sai */
    KIEM_BANG(ds_dat_suc_chua(ds, (size_t)-1), -1);
    KIEM_BANG(ds_dat_suc_chua(ds, 1000000000000u), -1);

    ds_huy(ds);
}

/* Loi 12: dung sau khi giai phong trong ds_xoa */
static void thu_loi_12(void) {
    NHOM("loi 12: dung sau khi giai phong");

    DanhSach *ds = ds_tao();

    for (int i = 0; i < 10; ++i) ds_them(ds, sv_tao(i, "x", 5));

    ds_xoa(ds, 5);

    KIEM_BANG(ds_so(ds), 9);
    KIEM(ds_tai(ds, 5) != NULL);
    KIEM_BANG(ds_tai(ds, 5)->ma, 6);      /* phan tu sau da don len */

    ds_huy(ds);
}

/* Loi 14: doi_cho hong khi hai con tro trung nhau */
static void thu_loi_14(void) {
    NHOM("loi 14: hoan doi voi hai con tro trung");

    SinhVien *a = sv_tao(1, "A", 10);

    ds_doi_cho(a, a);                     /* truong hop bien */

    KIEM_BANG(a->ma, 1);
    KIEM_BANG(a->diem, 10);
    KIEM_CHUOI(a->ten, "A");

    sv_huy(a);
}

int main(void) {
    thu_loi_01();
    thu_loi_07();
    thu_loi_09();
    thu_loi_12();
    thu_loi_14();
    /* ... du 15 ham ... */

    printf("\n%d kiem tra, %d hong\n", test_so_kiem, test_so_hong);

    return test_so_hong == 0 ? 0 : 1;
}

#Danh sách nghiệm thu

Chương 33, hành vi không xác định

  • Phân loại đúng mỗi lỗi thành undefined, unspecified hay implementation-defined.
  • Giải thích được vì sao ít nhất một lỗi chỉ lộ ra ở -O2.
  • Không dùng -fwrapv hay -fno-strict-aliasing làm bản vá.
  • Mọi kiểm tra tràn đặt trước phép tính, không đặt sau.

Chương 34, gỡ lỗi bộ nhớ

  • Chạy đủ ba lượt: ASan cộng UBSan, TSan nếu có luồng, Valgrind.
  • Mọi báo cáo chép nguyên văn vào tài liệu, kể cả phần vết gọi.
  • Dùng --error-exitcode=1 và -fno-sanitize-recover=all, để tích hợp liên tục phát hiện được.
  • Nêu được ít nhất một lỗi mà chỉ Valgrind bắt, và một lỗi mà chỉ ASan bắt.

Chương 35, GDB

  • Dùng ít nhất một watchpoint để tìm ra một lỗi ghi đè, kèm backtrace.
  • Dùng ít nhất một điểm dừng có điều kiện trên một vòng lặp dài.
  • Viết ít nhất một lệnh tự định nghĩa trong .gdbinit của dự án.
  • Với hai lỗi logic, ghi lại quá trình thu hẹp phạm vi từng bước.

Chất lượng chung

  • Dịch sạch với bộ cờ nghiêm ngặt đầy đủ, không một cảnh báo nào.
  • Cả ba công cụ im lặng sau khi vá.
  • Đủ mười lăm bài kiểm thử hồi quy, và mỗi bài đều thất bại trên bản chưa vá.
  • make sach && make kiem chạy từ đầu và trả về mã thoát 0.
  • Mỗi báo cáo có đủ sáu phần: vị trí, kết xuất gốc, nguyên nhân, bản vá, bằng chứng, kiểm thử.
  • Ghi lại tổng kết: bao nhiêu lỗi mỗi công cụ bắt được, và bao nhiêu lỗi phải đọc mã mới ra.

Tự làm thử

  1. Cài mười lăm lỗi vào một dự án và đổi mã nguồn với một bạn học.
  2. Chạy đủ ba lượt công cụ và ghi lại kết xuất gốc trước khi sửa gì.
  3. Viết mười lăm báo cáo theo mẫu sáu phần.
  4. Viết mười lăm bài kiểm thử hồi quy và chứng minh từng bài thất bại trên bản chưa vá.
  5. Viết kiem_tra.sh và chạy git bisect run trên lịch sử phiên bản của bạn.
  6. Lập bảng tổng kết công cụ nào bắt được bao nhiêu lỗi.

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

  • Bốn thứ phải nộp: báo cáo, bản vá, bằng chứng sau khi vá, và bộ kiểm thử hồi quy.
  • Ghi lại kết xuất gốc trước khi sửa, vì sau khi vá thì bằng chứng biến mất.
  • Phần nguyên nhân viết cho tử tế thường cho bạn một mẫu để tìm ra những lỗi cùng loại ở chỗ khác.
  • Bộ kiểm thử hồi quy phải dịch với sanitizer, nếu không nó bỏ sót cả nhóm lỗi rò rỉ và dùng sau khi giải phóng.
  • Sanitizer giải quyết khoảng tám mươi phần trăm số lỗi với chi phí gần bằng không, phần còn lại vẫn đòi đọc mã.