Bài 58.326 phút đọc
Quy trình phân tích hoàn chỉnh
Sau bài này bạn sẽ làm được
- Xếp năm tầng theo thứ tự chi phí tăng dần
- Biết mỗi tầng bắt được gì và bỏ sót gì
- Đưa cả năm tầng vào một Makefile
- Đưa dần vào một dự án cũ mà không bị ngợp
Không có một công cụ nào bắt được mọi lỗi. Bài này lấy một chương trình có đúng năm lỗi, mỗi lỗi chỉ bị một tầng công cụ bắt được, và cho thấy vì sao bạn cần cả năm.
#Năm lỗi, năm tầng
nam.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* TANG 1 - trinh bien dich: sai kieu dinh dang */
static void loi_1(void) {
long long v = 42;
printf("loi_1: %d\n", v); /* %d cho long long */
}
/* TANG 2 - phan tich tinh: ro ri tren nhanh loi */
char *loi_2(int co_loi) {
char *p = malloc(64);
if (!p) return NULL;
if (co_loi) return NULL; /* quen free(p) */
strcpy(p, "ok");
return p;
}
/* TANG 3 - ve sinh luc chay: tran bo dem tren dong */
static void loi_3(void) {
char *p = malloc(8);
memset(p, 'x', 9); /* ghi 9 byte vao 8 */
printf("loi_3: %c\n", p[0]);
free(p);
}
/* TANG 4 - UBSan: tran so nguyen co dau */
static int loi_4(int a, int b) {
return a * b; /* 100000 * 100000 tran int */
}
/* TANG 5 - kiem thu: ket qua SAI nhung khong co UB nao */
int loi_5(int diem) {
if (diem > 90) return 'A'; /* dung: >= 90 */
return 'B';
}
int main(void) {
loi_1();
char *p = loi_2(1);
free(p);
loi_3();
printf("loi_4: %d\n", loi_4(100000, 100000));
printf("loi_5: %c (mong doi A)\n", (char)loi_5(90));
return 0;
}terminal
# Dich binh thuong, tat canh bao, roi chay
gcc -std=c11 -O2 -w nam.c -o nam.exe
./nam.exe
loi_1: 42 loi_3: x loi_4: 1410065408 loi_5: B (mong doi A)
echo $?
0
| Tầng | Chi phí | Bắt được gì | Bỏ sót gì |
|---|---|---|---|
| 1. Cảnh báo trình biên dịch | Không, đã dịch rồi | Lỗi cú pháp mở rộng, kiểu, định dạng, mẫu đáng ngờ | Mọi thứ cần nhìn qua nhiều hàm |
| 2. Phân tích tĩnh | Vài phút | Rò rỉ, NULL, dùng sau khi thả, trên đường đi cụ thể | Lỗi phụ thuộc dữ liệu lúc chạy |
| 3. Vệ sinh bộ nhớ | Chậm 2 tới 3 lần | Tràn heap và stack, dùng sau khi thả, rò rỉ | Đường không được chạy tới |
| 4. Vệ sinh hành vi không xác định | Chậm 1.2 tới 2 lần | Tràn số, dịch bit quá, căn chỉnh, NULL | Đường không được chạy tới |
| 5. Kiểm thử | Vài chục giây | Kết quả sai mà hoàn toàn hợp lệ về mặt ngôn ngữ | Trường hợp bạn không nghĩ ra |
#Tầng 1 và 2
terminal
# TANG 1: bo co day du cua Bai 58.1
gcc -std=c11 -Wall -Wextra -Wconversion -Wformat=2 -O2 -c nam.c -o /dev/null
nam.c:9:21: warning: format '%d' expects argument of type 'int', but argument 2 has type 'long long int' [-Wformat=]
terminal
# TANG 2: clang-tidy
clang-tidy nam.c --checks='-*,clang-analyzer-*,bugprone-*' -- -std=c11
nam.c:16:24: warning: Potential leak of memory pointed to by 'p' [clang-analyzer-unix.Malloc] nam.c:17:5: warning: Call to function 'strcpy' is insecure as it does not provide bounding of the memory buffer [clang-analyzer-security.insecureAPI.strcpy] nam.c:24:5: warning: Call to function 'memset' is insecure as it does not provide security checks introduced in the C11 standard [clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling]
#Tầng 3 và 4
Tầng 3: AddressSanitizer
gcc -std=c11 -g -O1 -fsanitize=address -fno-omit-frame-pointer nam.c -o nam_asan
./nam_asan
=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x602000000018 at pc 0x7f3a1c thread T0
WRITE of size 9 at 0x602000000018 thread T0
#0 0x7f3a1c in memset
#1 0x401186 in loi_3 nam.c:24
#2 0x4011f8 in main nam.c:42
0x602000000018 is located 0 bytes to the right of 8-byte region
[0x602000000010,0x602000000018) allocated by thread T0 here:
#0 0x7f2b40 in malloc
#1 0x401171 in loi_3 nam.c:23
SUMMARY: AddressSanitizer: heap-buffer-overflow nam.c:24 in loi_3Tầng 4: UndefinedBehaviorSanitizer
gcc -std=c11 -g -O1 -fsanitize=undefined \
-fno-sanitize-recover=all nam.c -o nam_ubsan
./nam_ubsan
nam.c:30:14: runtime error: signed integer overflow:
100000 * 100000 cannot be represented in type 'int'
/* -fno-sanitize-recover=all la BAT BUOC trong CI.
Mac dinh, UBSan IN ra loi roi CHAY TIEP, va ma thoat van la 0.
Nghia la CI cua ban xanh du co loi. Voi co nay, no dung ngay
va tra ve ma thoat khac 0. */| Sanitizer | Bắt gì | Chậm bao nhiêu | Kết hợp được với |
|---|---|---|---|
| -fsanitize=address | Tràn heap và stack, dùng sau khi thả, rò rỉ | 2 tới 3 lần, RAM gấp 3 | undefined |
| -fsanitize=undefined | Tràn số, dịch bit, căn chỉnh, NULL | 1.2 tới 2 lần | address |
| -fsanitize=thread | Tranh chấp dữ liệu giữa các luồng | 5 tới 15 lần, RAM gấp 10 | KHÔNG với address |
| -fsanitize=memory | Đọc bộ nhớ chưa khởi tạo | 3 lần | KHÔNG với address, chỉ Clang |
| -fsanitize=leak | Chỉ rò rỉ, nhẹ hơn address | Rất ít | undefined |
#Tầng 5
Bốn tầng đầu không bắt được lỗi thứ năm
int loi_5(int diem) {
if (diem > 90) return 'A'; /* dung: >= 90 */
return 'B';
}
/* Ham nay:
- khong co canh bao bien dich nao
- khong co canh bao phan tich tinh nao
- khong dung bo nho sai, nen ASan im
- khong co hanh vi khong xac dinh nao, nen UBSan im
- chay nhanh, tra ve mot gia tri hop le
No chi don gian TRA VE SAI. Va khong mot cong cu tu dong nao
biet duoc y dinh cua ban la >= chu khong phai >.
Chi co MOT thu bat duoc no: mot bai kiem thu noi ro
ket qua mong doi. */terminal
./nam.exe
loi_1: 42 loi_3: x loi_4: 1410065408 loi_5: B (mong doi A)
Bài kiểm thử bắt được nó
static void thu_loi_5(void) {
KT_NHOM("loi_5: gia tri bien 90");
KT_BANG_INT(loi_5(89), 'B');
KT_BANG_INT(loi_5(90), 'A'); /* <- bai nay do */
KT_BANG_INT(loi_5(91), 'A');
}#Cả năm tầng trong một Makefile
Makefile
CC := gcc
SRC := $(wildcard src/*.c)
KT_SRC := $(wildcard kiem-thu/kt_*.c)
INC := -Iinclude -Ikiem-thu
CANH_BAO := -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion \
-Wshadow -Wstrict-prototypes -Wmissing-prototypes \
-Wold-style-definition -Wcast-qual -Wcast-align \
-Wwrite-strings -Wundef -Wdouble-promotion -Wformat=2 \
-Wnull-dereference -Wswitch-enum
CFLAGS := -std=c11 $(CANH_BAO) $(INC) -g
.PHONY: kiem-tra t1 t2 t3 t4 t5 sach
kiem-tra: t1 t2 t5 t3 t4
@echo "ca nam tang deu sach"
## Tang 1: canh bao trinh bien dich, la loi
t1:
$(CC) $(CFLAGS) -Werror -fsyntax-only $(SRC) $(KT_SRC)
## Tang 2: phan tich tinh
t2:
clang-tidy $(SRC) -- $(CFLAGS)
cppcheck --enable=warning,style,portability --std=c11 \
--inline-suppr --error-exitcode=1 $(INC) src/
## Tang 3: ve sinh bo nho
t3:
$(CC) $(CFLAGS) -O1 -fsanitize=address -fno-omit-frame-pointer \
$(KT_SRC) $(SRC) -o kt-asan
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 ./kt-asan
## Tang 4: ve sinh hanh vi khong xac dinh
t4:
$(CC) $(CFLAGS) -O1 -fsanitize=undefined \
-fno-sanitize-recover=all $(KT_SRC) $(SRC) -o kt-ubsan
UBSAN_OPTIONS=print_stacktrace=1 ./kt-ubsan
## Tang 5: kiem thu, ban thuong
t5:
$(CC) $(CFLAGS) -O2 $(KT_SRC) $(SRC) -o kt
./kt
sach:
rm -f kt kt-asan kt-ubsan *.gcda *.gcno *.gcov#Đưa vào dự án cũ
Cách làm cả đội bỏ cuộc
# Thu hai: bat het moi thu
CFLAGS := -Wall -Wextra -Wconversion ... -Werror
make
# 3847 errors
# Thu ba: ca doi ngung lam viec de sua canh bao
# Thu tu: ai do them -Wno-everything
# Thu sau: quay ve nhu cu, va khong ai nhac lai nua
Cách làm được
# Tuan 1: chay VA DEM, khong sua gi, khong bat -Werror
make canh-bao 2>&1 | grep -c warning: > .moc.txt
# 3847
# Tuan 1: chan viec TANG len
kiem-canh-bao:
@n=$$(make canh-bao 2>&1 | grep -c 'warning:'); \
cu=$$(cat .moc.txt); \
echo "canh bao: $$cu -> $$n"; \
[ $$n -le $$cu ] || { echo "TANG"; exit 1; }; \
echo $$n > .moc.txt
# Tuan 2: bat -Werror cho cac tep MOI
src/mo-dun-moi.o: CFLAGS += -Werror
# Tuan 3: bat tang 5, du chi 10 bai kiem thu
# Thang 2: bat tang 3 va 4 tren bo kiem thu do
# Thang 3: bat tang 2, tat cac nhom on ao
# Con so 3847 giam dan, va ai cung nhin thay no giam.
Tự làm thử
- Dịch và chạy
nam.ctrên máy bạn và xác nhận nó báo thành công. - Chạy từng tầng và ghi lại tầng nào bắt được lỗi nào.
- Trên Linux hoặc WSL, chạy ASan và UBSan và so báo cáo với định dạng trong bài.
- Thử
-ftrapvtrên máy bạn với hai biếnvolatile. - Viết Makefile năm tầng cho dự án của bạn.
- Đo thời gian của từng tầng trên dự án của bạn và sắp lại thứ tự cho hợp lý.
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
- Năm lỗi, năm tầng, và chương trình vẫn báo thành công khi không có tầng nào chạy.
- Tầng 1 và 2 là phân tích tĩnh: phủ hết mã nguồn nhưng chỉ thấy được những gì suy ra được.
- Tầng 3 và 4 là phân tích động: chính xác hơn nhiều nhưng chỉ thấy đường mà bộ kiểm thử đi qua.
- Tầng 5 là thứ duy nhất bắt được lỗi ngữ nghĩa, vì chỉ bạn biết đáp án đúng là gì.
- Đưa vào theo thứ tự kiểm thử, cảnh báo, ASan, UBSan, phân tích tĩnh; chặn tăng thay vì bắt về không ngay.