Bỏ qua điều hướng, tới nội dung chính
Học C
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
    ret
Ba 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);
             ~^   ~~~~
             %f

16. 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 UBCảnh báo lúc dịchCông cụ bắt đượcCách chữa
Ngoài biên mảngĐôi khiASan, ValgrindKiểm tra chỉ số
Giải tham chiếu NULLHiếmASan, UBSanKiểm tra trước khi dùng
Dùng sau khi giải phóngKhôngASan, ValgrindGán NULL sau free
Giải phóng hai lầnKhôngASan, ValgrindGán NULL sau free
Biến chưa khởi tạoCó, từ -O1Valgrind, MSanLuôn khởi tạo
Tràn số có dấuKhôngUBSanKiểm tra trước khi tính
Chia cho khôngĐôi khiUBSanKiểm tra mẫu số
Dịch bit saiChỉ với hằng sốUBSanDùng kiểu không dấu
Sửa biến nhiều lầnCó, -Wsequence-pointKhôngTách ra nhiều câu lệnh
Con trỏ tới biến cục bộCó, -Wreturn-local-addrASanCấp phát động
So sánh con trỏ lạKhôngKhôngChỉ dùng == và !=
Ghi vào chuỗi hằngCó, -Wwrite-stringsSập ngayDùng const char *
Vi phạm strict aliasingCó, -Wstrict-aliasing=2KhôngDùng memcpy
Thiếu returnCó, -Wreturn-typeUBSanThêm return
Sai định dạngCó, -WformatKhôngSửa đặc tả
memcpy chồng nhauKhôngASan, ValgrindDùng memmove

Tự làm thử

  1. Chạy chương trình heap và xác nhận p và q trùng địa chỉ.
  2. Chạy hàm dịch bit với n bằng 32, 33 và 64 rồi giải thích kết quả từ hợp ngữ.
  3. Bật -Wwrite-strings trên một dự án cũ và đếm số cảnh báo.
  4. 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.
  5. Viết hàm cong_an_toan(int a, int b, int *ra) trả về mã lỗi khi tràn.
  6. 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à malloc tiế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 free sau đó, cách chỗ lỗi rất xa.
  • 1u << 32 lúc chạy cho ra 1 trên x86 vì lệnh sal chỉ 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.