Bỏ qua điều hướng, tới nội dung chính
Học C
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ầngChi phíBắt được gìBỏ sót gì
1. Cảnh báo trình biên dịchKhông, đã dịch rồiLỗ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ĩnhVài phútRò 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ầnTrà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 địnhChậm 1.2 tới 2 lầnTrà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âyKế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_3
Tầ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. */
SanitizerBắt gìChậm bao nhiêuKết hợp được với
-fsanitize=addressTràn heap và stack, dùng sau khi thả, rò rỉ2 tới 3 lần, RAM gấp 3undefined
-fsanitize=undefinedTràn số, dịch bit, căn chỉnh, NULL1.2 tới 2 lầnaddress
-fsanitize=threadTranh chấp dữ liệu giữa các luồng5 tới 15 lần, RAM gấp 10KHÔNG với address
-fsanitize=memoryĐọc bộ nhớ chưa khởi tạo3 lầnKHÔNG với address, chỉ Clang
-fsanitize=leakChỉ rò rỉ, nhẹ hơn addressRất ítundefined

#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ử

  1. Dịch và chạy nam.c trên máy bạn và xác nhận nó báo thành công.
  2. Chạy từng tầng và ghi lại tầng nào bắt được lỗi nào.
  3. 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.
  4. Thử -ftrapv trên máy bạn với hai biến volatile.
  5. Viết Makefile năm tầng cho dự án của bạn.
  6. Đ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.