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

Lập trình phòng vệ

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

  • Phân biệt assert với kiểm tra lúc chạy
  • Xác định ranh giới tin cậy của mã bạn viết
  • Viết kiểm tra đầu vào ở đúng chỗ
  • Tránh lập trình phòng vệ quá đà

Lập trình phòng vệ không phải là kiểm tra mọi thứ ở mọi nơi. Đó là biết chính xác chỗ nào dữ liệu chuyển từ không tin được sang tin được, và dồn toàn bộ việc kiểm tra vào đúng chỗ đó.

#Hai loại kiểm tra

assertKiểm tra lúc chạy
Bắt loại lỗi nàoLỗi lập trình của bạnDữ liệu xấu từ bên ngoài
Có thể xảy ra trong bản phát hành khôngKhông, nếu mã đúngCó, đó là chuyện thường
Còn trong bản phát hành khôngKhông, NDEBUG xoá nóCó, luôn luôn
Khi thất bại thìDừng chương trìnhTrả về lỗi cho người gọi
Ví dụCon trỏ nội bộ NULLNgười dùng nhập chữ vào ô số
Nếu bỏ điLỗi khó tìm hơnLỗ hổng bảo mật
Cùng một hàm, hai loại kiểm tra
/* TAI ranh gioi: du lieu tu nguoi dung. Kiem tra THAT. */
int phan_tich_tuoi(const char *s, int *ra) {
    if (s == NULL || ra == NULL) return -1;         /* kiem tra luc chay */
    char *cuoi;
    long t = strtol(s, &cuoi, 10);
    if (cuoi == s || *cuoi != '\0') return -1;
    if (t < 0 || t >= 200)          return -1;
    *ra = (int)t;
    return 0;
}

/* BEN TRONG: doi so den tu ma cua chinh ban, da qua kiem tra. */
static void in_the(const NguoiDung *nd) {
    assert(nd != NULL);                             /* khang dinh */
    assert(nd->tuoi >= 0 && nd->tuoi < 200);
    printf("%s, %d tuoi\n", nd->ten, nd->tuoi);
}

/* Neu assert trong in_the that bai, do KHONG phai loi cua nguoi
   dung. Do la loi cua BAN: mot duong nao do vao in_the ma khong
   qua phan_tich_tuoi. Dung ngay la dung, vi ban vua phat hien
   mot lo hong trong thiet ke cua minh. */

#assert biến mất

khang_dinh.c
#include <assert.h>

static int so_lan_goi = 0;
static int tang_dem(void) { return ++so_lan_goi; }

static int chia(int a, int b) {
    assert(b != 0 && "chia cho 0");
    return a / b;
}

int main(void) {
    printf("che do: %s\n",
#ifdef NDEBUG
           "NDEBUG (assert bi tat)"
#else
           "go loi (assert bat)"
#endif
    );

    printf("chia(10, 2) = %d\n", chia(10, 2));

    /* TAC DUNG PHU trong assert: bien mat cung voi assert */
    assert(tang_dem() > 0);
    printf("so_lan_goi = %d\n", so_lan_goi);

    printf("sap goi chia(1, 0)\n");
    fflush(stdout);
    printf("chia(1, 0) = %d\n", chia(1, 0));
    return 0;
}
terminal
# Ban go loi
gcc -std=c11 -Wall -Wextra -g khang_dinh.c -o kd_go.exe
./kd_go.exe
che do: go loi (assert bat)
chia(10, 2) = 5
so_lan_goi = 1
sap goi chia(1, 0)
Assertion failed!

Program: ...\kd_go.exe
File: khang_dinh.c, Line 11

Expression: b != 0 && "chia cho 0"
echo $?
3
terminal
# Ban phat hanh
gcc -std=c11 -Wall -Wextra -O2 -DNDEBUG khang_dinh.c -o kd_ph.exe
khang_dinh.c:8:12: warning: 'tang_dem' defined but not used [-Wunused-function]
 static int tang_dem(void) { return ++so_lan_goi; }
            ^~~~~~~~
./kd_ph.exe
che do: NDEBUG (assert bi tat)
chia(10, 2) = 5
so_lan_goi = 0
sap goi chia(1, 0)
Illegal instruction
echo $?
132
Không nên
assert(tang_dem() > 0); /* tac dung phu */ assert(malloc(n) != NULL); /* ro ri, va bien mat */ assert(dong_tep(t) == 0); /* tep khong duoc dong! */ assert(so_luong-- > 0); /* gia tri thay doi */ /* Quy tac: BIEU THUC TRONG assert PHAI KHONG CO TAC DUNG PHU. Doc no nhu mot cau hoi, khong phai mot menh lenh. */
Nên
int n = tang_dem(); assert(n > 0); void *p = malloc(co); if (p == NULL) return -1; /* kiem tra that, khong assert */ int r = dong_tep(t); assert(r == 0); assert(so_luong > 0); --so_luong;

#Ranh giới tin cậy

Ranh giới tin cậy
Đường phân chia giữa phần dữ liệu bạn kiểm soát và phần bạn không. Mọi dữ liệu vượt qua ranh giới theo chiều vào phải được kiểm tra đầy đủ tại đúng chỗ đó, và sau đó mã bên trong được phép tin nó.
Nguồn dữ liệuTin được khôngKiểm tra ở đâu
Đối số dòng lệnhKhôngHàm phân tích đối số
Biến môi trườngKhôngChỗ đọc chúng
Nội dung tệpKhông, kể cả tệp của chính bạnHàm phân tích tệp
Gói tin mạngKhông, tuyệt đối khôngHàm giải khung
Đầu vào bàn phímKhôngHàm đọc
Kết quả của thư viện bên thứ baMột phầnNgay sau lời gọi
Hàm static trong cùng tệpCóassert là đủ
Hằng số trong mã của bạnCóstatic_assert nếu cần
ranh_gioi.c
#define TEN_TOI_DA 32
typedef struct { char ten[TEN_TOI_DA]; int tuoi; } NguoiDung;

enum { OK = 0, E_DAI = 1, E_TUOI = 2, E_RONG = 3 };

/* ==== TAI ranh gioi: du lieu tu ben ngoai, kiem tra THAT ==== */
static int phan_tich(const char *dong, NguoiDung *ra) {
    const char *phay = strchr(dong, ',');
    if (phay == NULL) return E_RONG;

    size_t ten_co = (size_t)(phay - dong);
    if (ten_co == 0)          return E_RONG;
    if (ten_co >= TEN_TOI_DA) return E_DAI;     /* >= chu khong phai > */

    memcpy(ra->ten, dong, ten_co);
    ra->ten[ten_co] = '\0';

    char *cuoi;
    long t = strtol(phay + 1, &cuoi, 10);
    if (cuoi == phay + 1) return E_TUOI;
    if (t < 0 || t >= 200) return E_TUOI;
    ra->tuoi = (int)t;
    return OK;
}

/* ==== BEN TRONG ranh gioi: doi so da duoc kiem tra ==== */
static void in_the(const NguoiDung *nd) {
    assert(nd != NULL);
    assert(nd->tuoi >= 0 && nd->tuoi < 200);
    printf("  [the] %s, %d tuoi\n", nd->ten, nd->tuoi);
}
terminal
gcc -std=c11 -Wall -Wextra -O2 ranh_gioi.c -o rg.exe
./rg.exe
dau vao: "an,20"
  [the] an, 20 tuoi
dau vao: "binh,999"
  TU CHOI: tuoi ngoai pham vi
dau vao: ",25"
  TU CHOI: ten rong
dau vao: "mot_cai_ten_rat_rat_rat_rat_rat_dai_qua_gioi_han,30"
  TU CHOI: ten qua dai
dau vao: "chi,khong-phai-so"
  TU CHOI: tuoi ngoai pham vi

#Kiểm tra ở đúng chỗ

Kiểm tra ở mọi nơi
int tinh_a(const NguoiDung *nd) { if (nd == NULL) return -1; if (nd->tuoi < 0) return -1; return tinh_b(nd); } int tinh_b(const NguoiDung *nd) { if (nd == NULL) return -1; if (nd->tuoi < 0) return -1; return tinh_c(nd); } int tinh_c(const NguoiDung *nd) { if (nd == NULL) return -1; if (nd->tuoi < 0) return -1; return nd->tuoi * 2; } /* Ba van de: 1. Chi phi: kiem tra ba lan cho mot dieu 2. Ma noi: mot nua so dong la kiem tra 3. TE NHAT: gio tinh_c PHAI tra ve int, va -1 la mot gia tri hop le cua "tuoi * 2"? Kieu tra ve bi o nhiem boi mot truong hop khong bao gio xay ra. */
Kiểm tra một lần ở ranh giới
/* Ranh gioi: kiem tra MOT lan */ int xu_ly_dong(const char *dong) { NguoiDung nd; if (phan_tich(dong, &nd) != OK) return -1; return tinh_a(&nd); /* tu day tro di, tin duoc */ } /* Ben trong: khang dinh, khong kiem tra */ static int tinh_a(const NguoiDung *nd) { assert(nd && nd->tuoi >= 0); return tinh_b(nd); } static int tinh_b(const NguoiDung *nd) { return tinh_c(nd); } static int tinh_c(const NguoiDung *nd) { return nd->tuoi * 2; } /* tinh_c gio tra ve DUNG cai no tinh, khong co gia tri loi. */

#Phòng vệ quá đà

Tự làm thử

  1. Chạy khang_dinh.c ở cả hai chế độ và giải thích vì sao so_lan_goi khác nhau.
  2. Tìm trong mã của bạn một assert có tác dụng phụ và sửa nó.
  3. Vẽ ranh giới tin cậy của một chương trình bạn đã viết, và kiểm tra xem việc kiểm tra có nằm đúng ở đó không.
  4. Đổi >= thành > trong ranh_gioi.c và tìm hậu quả bằng công cụ phát hiện lỗi bộ nhớ.
  5. Cài macro KIEM_TRA luôn bật và dùng nó cho ba bất biến quan trọng trong dự án của bạn.
  6. Tìm một chỗ nuốt lỗi trong mã của bạn và làm cho lỗi lộ ra.

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

  • assertcho lỗi lập trình, kiểm tra lúc chạy cho dữ liệu bên ngoài; câu hỏi phân loại là "nếu sai thì lỗi của ai".
  • NDEBUG xoá cả biểu thức bên trong assert, nên biểu thức đó không được có tác dụng phụ.
  • Kiểm tra đầy đủ tại ranh giới tin cậy, rồi tin dữ liệu ở bên trong.
  • Hàm công khai của thư viện luôn phải kiểm tra thật, vì người gọi ở ngoài ranh giới.
  • Ba kiểu phòng vệ có hại: nuốt lỗi, mặc định im lặng, và kiểm tra thứ không thể sai.