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

Lab: săn mười lỗi trong một tệp

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

  • Tìm đủ mười lỗi mà không đọc mã trước
  • Ghi lại dòng lỗi, loại lỗi, công cụ phát hiện và kết xuất gốc
  • Sửa từng lỗi và giải thích nguyên nhân
  • Chứng minh sau khi sửa cả ba công cụ đều sạch

Mười lỗi được đánh số trong một tệp chín mươi bảy dòng. Luật chơi: không đọc mã trước. Tìm bằng công cụ, ghi lại kết xuất gốc, rồi mới mở tệp ra để hiểu vì sao.

#Đề bài

  1. Tìm đủ mười lỗi bằng công cụ

    Dùng cảnh báo lúc dịch, ASan, UBSan, Valgrind và GDB. Không mở tệp nguồn cho tới khi bạn đã liệt kê được ít nhất bảy lỗi.

  2. Ghi lại cho mỗi lỗi

    Số dòng, loại lỗi, công cụ nào phát hiện, và kết xuất gốc của công cụ, chép nguyên văn.

  3. Giải thích nguyên nhân

    Vì sao lỗi xảy ra, chứ không phải sửa thế nào. Nếu bạn không giải thích được thì bản vá của bạn là đoán mò.

  4. Vá và chứng minh

    Sau khi vá, cả ba công cụ phải im lặng và chương trình phải cho ra kết quả đúng.

#Tệp bugs.c

bugs.c, phần hàm
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

typedef struct {
    char ten[8];
    int  diem;
} SinhVien;

/* LOI 1 */
static char *sao_chep(const char *s) {
    char *p = malloc(strlen(s));
    strcpy(p, s);
    return p;
}

/* LOI 2 */
static int tong(const int *a, int n) {
    int t;
    for (int i = 0; i < n; ++i) t += a[i];
    return t;
}

/* LOI 3 */
static int trung_binh(const int *a, int n) {
    return tong(a, n) / n;
}

/* LOI 4 */
static void dat_ten(SinhVien *sv, const char *ten) {
    strcpy(sv->ten, ten);
}

/* LOI 5 */
static int *lon_nhat(int *a, int n) {
    int max = a[0];
    for (int i = 1; i < n; ++i) if (a[i] > max) max = a[i];
    return &max;
}

/* LOI 6 */
static int nhan_an_toan(int a, int b) {
    if (a * b < 0) return -1;
    return a * b;
}

/* LOI 7 */
static void in_nguoc(const char *s) {
    int n = (int)strlen(s);
    for (int i = n; i >= 0; --i) putchar(s[i]);
    putchar('\n');
}

/* LOI 8 */
static unsigned mat_na(int n) {
    return ~0u << n;
}

/* LOI 9 */
static void doi_cho(int *a, int *b) {
    *a = *a + *b;
    *b = *a - *b;
    *a = *a - *b;
}
bugs.c, hàm main
/* LOI 10 */
int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);

    int a[5] = { 3, 1, 4, 1, 5 };

    char *t = sao_chep("xin chao");
    printf("chep : %s\n", t);

    printf("tong : %d\n", tong(a, 5));
    printf("tb   : %d\n", trung_binh(a, 0));

    SinhVien sv = { "", 100 };
    dat_ten(&sv, "NguyenVanA");
    printf("diem : %d\n", sv.diem);

    printf("max  : %d\n", *lon_nhat(a, 5));
    printf("nhan : %d\n", nhan_an_toan(100000, 100000));

    in_nguoc("abc");
    printf("matna: %u\n", mat_na(32));

    int x = 1, y = 2;
    doi_cho(&x, &x);
    printf("doicho: x=%d\n", x);

    free(t);
    free(t);

    return 0;
}

#Bước một: cảnh báo lúc dịch

terminal
gcc -std=c11 -g -O1 -Wall -Wextra -o bugs.exe bugs.c
bugs.c:39:12: warning: function returns address of local variable [-Wreturn-local-addr]
bugs.c:87:16: warning: unused variable 'y' [-Wunused-variable]
bugs.c:21:35: warning: 't' is used uninitialized in this function [-Wuninitialized]
bugs.c:27:23: warning: 't' is used uninitialized in this function [-Wuninitialized]
# Thêm mọi cờ nghiêm ngặt vẫn không ra thêm gì
gcc -std=c11 -g -O2 -Wall -Wextra -Wpedantic -Wshadow -Wconversion -D_FORTIFY_SOURCE=2 -c bugs.c -o /dev/null
bugs.c:39:12: warning: function returns address of local variable [-Wreturn-local-addr]
bugs.c:87:16: warning: unused variable 'y' [-Wunused-variable]
bugs.c:21:35: warning: 't' is used uninitialized in this function [-Wuninitialized]

#Bước hai: chạy thử

terminal
./bugs.exe
chep : xin chao
tong : 14
tb   : 0
diem : 16750
Segmentation fault
echo $?
139
terminal
gdb -q -batch -x gb.gdb ./bugs.exe
chep : xin chao
tong : 14
tb   : 0
diem : 16750

Thread 1 received signal SIGSEGV, Segmentation fault.
main () at bugs.c:83
83	    printf("max  : %d\n", *lon_nhat(a, 5));
#0  main () at bugs.c:83

#Bước ba: sanitizer

# Chay tren Linux hoac WSL2, vi MinGW khong co sanitizer.
# Bai 34.1 da noi ve gioi han nay.

gcc -std=c11 -g3 -O1 -fno-omit-frame-pointer \
    -fsanitize=address,undefined \
    -o bugs_san bugs.c

./bugs_san

#Đáp án và bản vá

LỗiLoạiNguyên nhânBản vá
1, dòng 13Tràn heap một bytestrlen không tính ký tự kết thúc chuỗimalloc(strlen(s) + 1), và kiểm tra NULL
2, dòng 20Biến chưa khởi tạoint t; thiếu = 0int t = 0;
3, dòng 27Chia cho khôngKhông kiểm tra nif (n <= 0) return 0;
4, dòng 32Tràn bộ đệm ngăn xếpstrcpy không giới hạn độ dàisnprintf(sv->ten, sizeof sv->ten, "%s", ten)
5, dòng 39Con trỏ tới biến cục bộmax chết khi hàm trả vềTrả về giá trị, hoặc trả về con trỏ vào chính mảng a
6, dòng 44Tràn số có dấuKiểm tra đặt sau phép nhân, xem Bài 33.4__builtin_mul_overflow trước khi nhân
7, dòng 51Đọc lệch mộtVòng lặp bắt đầu từ n, tức đọc cả ký tự kết thúcfor (int i = n - 1; i >= 0; --i)
8, dòng 57Dịch quá độ rộngn có thể bằng hoặc lớn hơn 32return n >= 32 ? 0u : (~0u << n);
9, dòng 62Lỗi logicKhi a và b trùng nhau, dòng hai làm *b bằng 0if (a == b) return; hoặc dùng biến tạm
10, dòng 94Giải phóng hai lầnfree(t) gọi hai lầnXóa dòng thừa, và gán t = NULL sau free
bugs_da_va.c, chín hàm sau khi vá
static char *sao_chep(const char *s) {
    char *p = malloc(strlen(s) + 1);        /* +1 cho ky tu ket thuc */
    if (p == NULL) return NULL;
    strcpy(p, s);
    return p;
}

static int tong(const int *a, int n) {
    int t = 0;                              /* khoi tao */
    for (int i = 0; i < n; ++i) t += a[i];
    return t;
}

static int trung_binh(const int *a, int n) {
    if (n <= 0) return 0;                   /* chan chia cho khong */
    return tong(a, n) / n;
}

static void dat_ten(SinhVien *sv, const char *ten) {
    snprintf(sv->ten, sizeof sv->ten, "%s", ten);   /* luon ket thuc chuoi */
}

static int lon_nhat(const int *a, int n) {  /* tra ve GIA TRI, khong tra con tro */
    int max = a[0];
    for (int i = 1; i < n; ++i) if (a[i] > max) max = a[i];
    return max;
}

static int nhan_an_toan(int a, int b) {
    int r;
    if (__builtin_mul_overflow(a, b, &r)) return -1;   /* kiem tra TRUOC */
    return r;
}

static void in_nguoc(const char *s) {
    int n = (int)strlen(s);
    for (int i = n - 1; i >= 0; --i) putchar(s[i]);    /* bat dau tu n - 1 */
    putchar('\n');
}

static unsigned mat_na(int n) {
    if (n < 0)   return 0u;
    if (n >= 32) return 0u;                 /* chan dich qua do rong */
    return ~0u << n;
}

static void doi_cho(int *a, int *b) {
    if (a == b) return;                     /* chan truong hop trung */
    int t = *a; *a = *b; *b = t;            /* bien tam: ro rang va nhanh hon */
}
terminal
# Sau khi vá, trên MinGW
gcc -std=c11 -g -O2 -Wall -Wextra -Wpedantic -o bugs_va.exe bugs_da_va.c && ./bugs_va.exe
chep : xin chao
tong : 14
tb   : 0
diem : 100
ten  : NguyenVad
max  : 5
nhan : -1
cba
matna: 0
doicho: x=1
echo $?
0

diem : 100 thay vì 16750, nhan : -1 thay vì một số tràn, cba thay vì cba kèm một ký tự rác, và x=1 thay vì x=0. Chín lỗi đã hết.

#Mẫu báo cáo

bao_cao.md
## Loi 4: tran bo dem tren ngan xep

**Dong:** bugs.c:32
**Loai:** stack-buffer-overflow
**Cong cu phat hien:** AddressSanitizer

**Ket xuat goc:**

    ==8123==ERROR: AddressSanitizer: stack-buffer-overflow on address
    0x7ffd3a1b40c8 at pc 0x7f2e8c4c9a1d bp 0x7ffd3a1b4090 sp 0x7ffd3a1b3838
    WRITE of size 11 at 0x7ffd3a1b40c8 thread T0
        #0 0x7f2e8c4c9a1c in strcpy
        #1 0x40130f in dat_ten bugs.c:32
        #2 0x4014a7 in main bugs.c:80

**Nguyen nhan:**

SinhVien.ten la mang 8 byte. Chuoi "NguyenVanA" dai 10 ky tu, cong
ky tu ket thuc la 11 byte. strcpy khong biet kich thuoc dich nen ghi
du 11 byte, tran 3 byte sang truong diem nam ngay sau trong bo nho.

Doc chay khong co ASan cho thay diem doi tu 100 thanh 16750, tuc
hai byte thap cua truong diem da bi ghi de boi "nA" cong ky tu ket thuc.

**Ban va:**

    - strcpy(sv->ten, ten);
    + snprintf(sv->ten, sizeof sv->ten, "%s", ten);

**Bang chung sau khi va:**

    ./bugs_va.exe        -> "diem : 100"
    ASan                 -> khong bao
    UBSan                -> khong bao
    valgrind             -> ERROR SUMMARY: 0 errors

**Kiem thu hoi quy:**

    KIEM_CHUOI(sv.ten, "NguyenV");     /* bi cat dung 7 ky tu */
    KIEM_BANG(sv.diem, 100);           /* khong bi ghi de */
    KIEM_BANG(sv.ten[7], 0);           /* CO ky tu ket thuc chuoi */

Tự làm thử

  1. Chép bugs.c, dịch với -Wall -Wextra và ghi lại hai cảnh báo.
  2. Chạy chương trình và chỉ ra dòng kết xuất nào nhìn thấy được là sai.
  3. Mở bằng GDB, xác định nó chết ở lỗi số mấy, và giải thích vì sao không phải lỗi số một.
  4. Chạy ASan và UBSan trên WSL2 rồi liệt kê thứ tự các báo lỗi.
  5. Viết báo cáo đầy đủ theo mẫu cho cả mười lỗi.
  6. Viết mười bài kiểm thử hồi quy, mỗi bài phải thất bại trên bản chưa vá.

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ảnh báo lúc dịch bắt được hai trên mười lỗi, kể cả khi bật đủ mọi cờ nghiêm ngặt.
  • Chương trình chết ở lỗi số năm, sau khi bốn lỗi trước đã xảy ra mà không để lại dấu vết rõ ràng.
  • ASan, UBSan và Valgrind cộng lại bắt được chín trên mười.
  • Lỗi còn lại là lỗi logic thuần túy và không công cụ nào bắt được, chỉ đọc mã và kiểm thử mới ra.
  • Một báo cáo lỗi đủ dùng có sáu phần, và phần hay bị bỏ nhất là bài kiểm thử hồi quy.