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
| assert | Kiểm tra lúc chạy | |
|---|---|---|
| Bắt loại lỗi nào | Lỗi lập trình của bạn | Dữ liệu xấu từ bên ngoài |
| Có thể xảy ra trong bản phát hành không | Không, nếu mã đúng | Có, đó là chuyện thường |
| Còn trong bản phát hành không | Không, NDEBUG xoá nó | Có, luôn luôn |
| Khi thất bại thì | Dừng chương trình | Trả về lỗi cho người gọi |
| Ví dụ | Con trỏ nội bộ NULL | Người dùng nhập chữ vào ô số |
| Nếu bỏ đi | Lỗi khó tìm hơn | Lỗ 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ệu | Tin được không | Kiểm tra ở đâu |
|---|---|---|
| Đối số dòng lệnh | Không | Hàm phân tích đối số |
| Biến môi trường | Không | Chỗ đọc chúng |
| Nội dung tệp | Không, kể cả tệp của chính bạn | Hàm phân tích tệp |
| Gói tin mạng | Không, tuyệt đối không | Hàm giải khung |
| Đầu vào bàn phím | Không | Hàm đọc |
| Kết quả của thư viện bên thứ ba | Một phần | Ngay sau lời gọi |
| Hàm static trong cùng tệp | Có | assert là đủ |
| Hằng số trong mã của bạn | Có | 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ử
- Chạy
khang_dinh.cở cả hai chế độ và giải thích vì saoso_lan_goikhác nhau. - Tìm trong mã của bạn một
assertcó tác dụng phụ và sửa nó. - 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.
- Đổi
>=thành>trongranh_gioi.cvà tìm hậu quả bằng công cụ phát hiện lỗi bộ nhớ. - Cài macro
KIEM_TRAluôn bật và dùng nó cho ba bất biến quan trọng trong dự án của bạn. - 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".NDEBUGxoá cả biểu thức bên trongassert, 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.