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
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.
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.
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ò.
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
#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;
}/* 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
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]
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ử
chep : xin chao tong : 14 tb : 0 diem : 16750 Segmentation fault
139
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ỗi | Loại | Nguyên nhân | Bản vá |
|---|---|---|---|
| 1, dòng 13 | Tràn heap một byte | strlen không tính ký tự kết thúc chuỗi | malloc(strlen(s) + 1), và kiểm tra NULL |
| 2, dòng 20 | Biến chưa khởi tạo | int t; thiếu = 0 | int t = 0; |
| 3, dòng 27 | Chia cho không | Không kiểm tra n | if (n <= 0) return 0; |
| 4, dòng 32 | Tràn bộ đệm ngăn xếp | strcpy không giới hạn độ dài | snprintf(sv->ten, sizeof sv->ten, "%s", ten) |
| 5, dòng 39 | Con 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 44 | Tràn số có dấu | Kiể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ột | Vòng lặp bắt đầu từ n, tức đọc cả ký tự kết thúc | for (int i = n - 1; i >= 0; --i) |
| 8, dòng 57 | Dịch quá độ rộng | n có thể bằng hoặc lớn hơn 32 | return n >= 32 ? 0u : (~0u << n); |
| 9, dòng 62 | Lỗi logic | Khi a và b trùng nhau, dòng hai làm *b bằng 0 | if (a == b) return; hoặc dùng biến tạm |
| 10, dòng 94 | Giải phóng hai lần | free(t) gọi hai lần | Xóa dòng thừa, và gán t = NULL sau free |
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 */
}chep : xin chao tong : 14 tb : 0 diem : 100 ten : NguyenVad max : 5 nhan : -1 cba matna: 0 doicho: x=1
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
## 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ử
- Chép
bugs.c, dịch với-Wall -Wextravà ghi lại hai cảnh báo. - Chạy chương trình và chỉ ra dòng kết xuất nào nhìn thấy được là sai.
- 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.
- Chạy ASan và UBSan trên WSL2 rồi liệt kê thứ tự các báo lỗi.
- Viết báo cáo đầy đủ theo mẫu cho cả mười lỗi.
- 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.