Bài 33.332 phút đọc
Mười sáu hành vi không xác định phải thuộc
Sau bài này bạn sẽ làm được
- Nhận ra mười sáu dạng UB thường gặp nhất khi đọc mã
- Phân biệt tràn số có dấu với quấn vòng của số không dấu
- Nêu ba dạng UB liên quan tới con trỏ đã giải phóng
- Viết lại từng đoạn sai thành đoạn đúng
Mười sáu dạng, chia làm bốn nhóm. Đây không phải danh sách đầy đủ, phụ lục J.2 của chuẩn C11 liệt kê hơn hai trăm dạng. Nhưng mười sáu dạng này chiếm gần hết những lỗi bạn sẽ thật sự gặp.
#Nhóm một: bộ nhớ
1. Truy cập ngoài biên mảng
int a[5];
a[5] = 1; /* UB: chi so hop le la 0..4 */
a[-1] = 1; /* UB */
a[10] = 1; /* UB */
int *p = &a[5]; /* HOP LE: dia chi ngay sau phan tu cuoi
duoc phep TINH RA de lam moc dung vong lap */
*p = 1; /* UB: nhung KHONG duoc doc hay ghi qua no */
int *q = &a[6]; /* UB: ngay ca viec TINH dia chi nay cung sai */2. Giải tham chiếu con trỏ NULL
int *p = NULL;
*p = 10; /* UB, gan nhu chac chan sap */
if (p != NULL) *p = 10; /* dung */
/* Chu y: kiem tra PHAI dat truoc, khong duoc dat sau.
Bai 33.2 da cho thay kiem tra dat sau bi xoa. */3 và 4. Dùng sau khi giải phóng, giải phóng hai lần
heap.c
unsigned char *p = malloc(16);
memcpy(p, "xinchao!", 8);
hien("truoc free :", p);
free(p);
hien("sau free :", p); /* 1. dung sau khi giai phong */
unsigned char *q = malloc(16);
memcpy(q, "AAAAAAAA", 8);
hien("q moi :", q);
hien("p doc lai :", p); /* 2. p va q co trung dia chi khong? */
printf("p = %p, q = %p, trung nhau: %s\n",
(void *)p, (void *)q, p == q ? "CO" : "KHONG");
free(p); /* 3. giai phong hai lan */
printf("qua duoc double free\n");
free(q);terminal
gcc -std=c11 -O1 -Wall -Wextra -o heap.exe heap.c && ./heap.exe
truoc free : 78 69 6E 63 68 61 6F 21 sau free : 20 68 C0 00 00 00 00 00 q moi : 41 41 41 41 41 41 41 41 p doc lai : 41 41 41 41 41 41 41 41 p = 0000000000C024E0, q = 0000000000C024E0, trung nhau: CO qua duoc double free
echo $?
127
5. Đọc biến chưa khởi tạo
int x;
printf("%d", x); /* UB */
int a[10];
int t = 0;
for (int i = 0; i < 10; ++i) t += a[i]; /* UB */
/* Khong phai "gia tri rac". Chuan goi day la gia tri BAT DINH,
va doc no la UB neu bien co the nam trong thanh ghi.
Hau qua that: trinh bien dich duoc phep cho x mang hai gia tri
khac nhau o hai cho doc khac nhau trong cung mot ham. */
int x;
if (x == x) puts("bang"); /* KHONG bao dam in ra */
/* Cach dung: luon khoi tao. */
int x = 0;
int a[10] = { 0 };#Nhóm hai: số học và bit
6. Tràn số có dấu
INT_MAX + 1 /* UB */
INT_MIN - 1 /* UB */
-INT_MIN /* UB: |INT_MIN| khong bieu dien duoc */
INT_MIN / -1 /* UB: ket qua 2147483648 khong lot vao int.
Tren x86, lenh idiv sinh ngoai le phan cung. */
abs(INT_MIN) /* UB, cung ly do */
UINT_MAX + 1u /* HOP LE: = 0, so khong dau quan vong */
/* Do la ly do dung size_t va unsigned cho chi so va kich thuoc:
khong co UB tran. Nhung phai canh giac voi phep tru: */
size_t n = 3, m = 5;
if (n - m > 0) { } /* LUON dung: 3 - 5 quan vong thanh so rat lon */
if (n > m) { } /* dung */7. Chia và chia dư cho không
x / 0; x % 0; /* UB */
/* Voi so THUC thi khac: 1.0 / 0.0 cho ra inf, va do la hanh vi
duoc dinh nghia neu cai dat theo IEEE 754. */
double d = 1.0 / 0.0; /* inf, hop le tren gan nhu moi may */
double n = 0.0 / 0.0; /* NaN, hop le */8. Dịch bit sai
dich.c
__attribute__((noinline)) unsigned dich(unsigned x, int n) { return x << n; }
int main(void) {
printf("dich(1, 32) luc chay = %u\n", dich(1, 32));
printf("dich(1, 33) luc chay = %u\n", dich(1, 33));
printf("dich(1, 64) luc chay = %u\n", dich(1, 64));
printf("(-1) << 1 = %d\n", -1 << 1);
return 0;
}terminal
gcc -std=c11 -O2 -Wall -Wextra -o dich dich.c && ./dich
dich(1, 32) luc chay = 1 dich(1, 33) luc chay = 2 dich(1, 64) luc chay = 1 (-1) << 1 = -2
gcc -std=c11 -O2 -S -masm=intel -o - dich.c
dich:
mov eax, ecx
mov ecx, edx
sal eax, cl ; CPU chi dung 5 bit thap cua cl
retBa cách dịch bit hay sai
unsigned mat_na = ~0u << n; /* UB neu n >= 32 */
int co = 1 << 31; /* UB: tran int co dau */
int x = -1 << 1; /* UB truoc C99, gio la
implementation-defined */Cách viết đúng
unsigned mat_na = n >= 32 ? 0u : (~0u << n);
unsigned co = 1u << 31; /* u: so khong dau, khong tran */
unsigned x = (unsigned)-1 << 1; /* chuyen sang khong dau truoc */
/* Quy tac: MOI phep dich bit deu lam tren kieu KHONG DAU. */9. Sửa một biến nhiều lần trong một điểm tuần tự
i = i++ + ++i; /* UB */
a[i] = i++; /* UB */
printf("%d %d", i++, i++); /* UB */
/* Bai 33.5 danh ca bai cho chuyen nay. */#Nhóm ba: con trỏ
10. Trả về con trỏ tới biến cục bộ
terminal
gcc -std=c11 -O2 -Wall -Wextra -o lung lung.c && ./lung
lung.c:9:12: warning: function returns address of local variable [-Wreturn-local-addr] con tro cuc bo = 0000000000000000
int *f(void) {
int x = 42;
return &x; /* UB: x chet khi ham tra ve */
}
/* GCC o -O2 thay luon dia chi do bang NULL. Ban mong doi mot dia chi
rac va nhan duoc 0.
Cach dung: cap phat dong, hoac de nguoi goi cap bo dem. */
int *f(void) {
int *p = malloc(sizeof *p);
if (p) *p = 42;
return p; /* nguoi goi phai free */
}
void f(int *ra) { *ra = 42; } /* thuong tot hon: khong ai phai free */11. So sánh con trỏ vào hai đối tượng khác nhau
int a[5], b[5];
if (a < b) { } /* UB: hai mang khong lien quan */
if (a == b) { } /* HOP LE: == va != luon dung duoc */
if (&a[0] < &a[3]) { } /* HOP LE: cung mot mang */
/* Trong thuc te thi so sanh < gan nhu luon "chay dung" vi no
thanh mot lenh cmp tren dia chi. Nhung chuan khong bao dam,
va tren may co bo nho phan doan thi no that su sai. */12. Ghi vào chuỗi hằng
terminal
gcc -std=c11 -O2 -Wall -Wextra -o lit lit.c && ./lit
truoc: abc Segmentation fault
# Cờ bắt được lỗi này ngay lúc dịch
gcc -std=c11 -O2 -Wwrite-strings -c lit.c
lit.c:4:15: warning: initialization discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
char *s = "abc";
^~~~~char *s = "abc";
s[0] = 'A'; /* UB: chuoi hang nam trong .rodata */
const char *s = "abc"; /* dung: kieu noi ro la chi doc */
char s[] = "abc"; /* dung: day la MANG, sao chep tu chuoi hang */
s[0] = 'A'; /* hop le */
/* Bat -Wwrite-strings de trinh bien dich bat ho ban. No khong nam
trong -Wall vi no gay on voi ma cu. */13. Vi phạm strict aliasing
float f = 1.0f;
int i = *(int *)&f; /* UB. Bai 33.6 noi ky */
/* Cach dung: memcpy hoac union. Ca hai deu KHONG ton them lenh nao. */#Nhóm bốn: hàm và thư viện
14. Hàm không void mà không return
terminal
./lung
thieu_return(5) = 1 thieu_return(-5) = 1
int f(int x) {
if (x) return 1;
} /* UB khi x == 0 */
/* GCC rut ca ham thanh "mov eax, 1; ret". Nen f(-5) tra ve 1.
Ngoai le duy nhat: main khong can return, chuan quy dinh
no ngam tra ve 0. */15. Sai đặc tả định dạng
printf("%d", 3.14); /* UB */
printf("%s", 42); /* UB, gan nhu chac chan sap */
printf("%d", (long)5); /* UB tren may co long 64 bit */
printf("%s"); /* UB: thieu doi so */
scanf("%d", x); /* UB: quen dau & */
/* GCC bat gan het nhung loi nay voi -Wformat, nam trong -Wall.
Bat -Wformat=2 de nghiem hon. */terminal
gcc -std=c11 -Wall -c fmt.c
fmt.c:4:14: warning: format '%d' expects argument of type 'int', but argument 2 has type 'double' [-Wformat=]
printf("%d", 3.14);
~^ ~~~~
%f16. memcpy với vùng chồng nhau
char a[20] = "0123456789";
memcpy(a, a + 1, 9); /* UB: hai vung chong nhau */
memmove(a, a + 1, 9); /* dung */
/* Nguyen mau noi het:
void *memcpy(void *restrict d, const void *restrict s, size_t n);
void *memmove(void *d, const void *s, size_t n);
memcpy co restrict, tuc no HUA hai vung khong chong. memmove khong co,
nen no ho tro chong nhau. Bai 32.3 da noi ky.
Trong thuc te memcpy voi vung chong nhau thuong van "chay dung"
voi n nho, roi hong khi n du lon de trinh bien dich vector hoa. */#Bảng tra nhanh
| Dạng UB | Cảnh báo lúc dịch | Công cụ bắt được | Cách chữa |
|---|---|---|---|
| Ngoài biên mảng | Đôi khi | ASan, Valgrind | Kiểm tra chỉ số |
| Giải tham chiếu NULL | Hiếm | ASan, UBSan | Kiểm tra trước khi dùng |
| Dùng sau khi giải phóng | Không | ASan, Valgrind | Gán NULL sau free |
| Giải phóng hai lần | Không | ASan, Valgrind | Gán NULL sau free |
| Biến chưa khởi tạo | Có, từ -O1 | Valgrind, MSan | Luôn khởi tạo |
| Tràn số có dấu | Không | UBSan | Kiểm tra trước khi tính |
| Chia cho không | Đôi khi | UBSan | Kiểm tra mẫu số |
| Dịch bit sai | Chỉ với hằng số | UBSan | Dùng kiểu không dấu |
| Sửa biến nhiều lần | Có, -Wsequence-point | Không | Tách ra nhiều câu lệnh |
| Con trỏ tới biến cục bộ | Có, -Wreturn-local-addr | ASan | Cấp phát động |
| So sánh con trỏ lạ | Không | Không | Chỉ dùng == và != |
| Ghi vào chuỗi hằng | Có, -Wwrite-strings | Sập ngay | Dùng const char * |
| Vi phạm strict aliasing | Có, -Wstrict-aliasing=2 | Không | Dùng memcpy |
| Thiếu return | Có, -Wreturn-type | UBSan | Thêm return |
| Sai định dạng | Có, -Wformat | Không | Sửa đặc tả |
| memcpy chồng nhau | Không | ASan, Valgrind | Dùng memmove |
Tự làm thử
- Chạy chương trình heap và xác nhận
pvàqtrùng địa chỉ. - Chạy hàm dịch bit với
nbằng 32, 33 và 64 rồi giải thích kết quả từ hợp ngữ. - Bật
-Wwrite-stringstrên một dự án cũ và đếm số cảnh báo. - Viết mười sáu đoạn mã, mỗi đoạn một dạng UB, rồi thử từng cờ cảnh báo xem cái nào bắt được.
- Viết hàm
cong_an_toan(int a, int b, int *ra)trả về mã lỗi khi tràn. - Với ba dạng không công cụ nào bắt được, viết ra quy tắc mã hóa của riêng bạn để tránh chúng.
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
- Sau
free, bộ cấp phát ghi sổ sách vào chính khối vừa trả, vàmalloctiếp theo thường trả lại đúng địa chỉ đó. - Giải phóng hai lần không sập ngay: chương trình chết ở lần
freesau đó, cách chỗ lỗi rất xa. 1u << 32lúc chạy cho ra 1 trên x86 vì lệnhsalchỉ dùng năm bit thấp, còn trên ARM thì cho ra 0.- Mọi phép dịch bit nên làm trên kiểu không dấu, và mọi kiểm tra tràn nên đặt trước phép tính.
- Ba dạng UB không cảnh báo lúc dịch và cũng không công cụ nào bắt được: so sánh con trỏ lạ, sửa biến nhiều lần, và một phần của strict aliasing.