Bài 34.326 phút đọc
UndefinedBehaviorSanitizer
Sau bài này bạn sẽ làm được
- Dịch và chạy với -fsanitize=undefined
- Đọc thông báo UBSan và tra về đúng dòng mã
- Bật -fno-sanitize-recover để dừng ngay ở lỗi đầu tiên
- Kết hợp UBSan với ASan trong một lần dịch
UBSan gần như miễn phí: chậm thêm khoảng hai mươi phần trăm. Nó bắt đúng nhóm lỗi mà ASan không thấy và trình biên dịch không cảnh báo được, tức là tràn số, dịch bit sai và chia cho không.
#Chạy thử
ub.c
#include <stdio.h>
#include <limits.h>
__attribute__((noinline)) int cong(int a, int b) { return a + b; }
__attribute__((noinline)) unsigned dich(unsigned x, int n) { return x << n; }
__attribute__((noinline)) int chia(int a, int b) { return a / b; }
int main(void) {
printf("cong = %d\n", cong(INT_MAX, 1));
printf("dich = %u\n", dich(1, 32));
printf("chia = %d\n", chia(10, 0));
return 0;
}terminal
# Không có UBSan: hai dòng đầu chạy êm, dòng ba mới sập
gcc -std=c11 -g -O1 -Wall -Wextra -o ub ub.c && ./ub
cong = -2147483648 dich = 1 Floating point exception (core dumped)
terminal
# Có UBSan
gcc -std=c11 -g -O1 -fsanitize=undefined -o ub ub.c && ./ub
ub.c:4:44: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int' cong = -2147483648 ub.c:5:56: runtime error: shift exponent 32 is too large for 32-bit type 'unsigned int' dich = 1 ub.c:6:44: runtime error: division by zero Floating point exception (core dumped)
#Mười kiểm tra hay dùng
| Tên nhóm | Bắt gì | Ví dụ |
|---|---|---|
| signed-integer-overflow | Tràn số có dấu | INT_MAX + 1 |
| shift | Dịch quá độ rộng, dịch số âm | 1u << 32 |
| integer-divide-by-zero | Chia số nguyên cho không | x / 0 |
| null | Giải tham chiếu con trỏ NULL | *(int *)0 |
| alignment | Con trỏ lệch căn chỉnh | *(int *)(buf + 1) |
| bounds | Ngoài biên mảng khi biết kích thước | int a[5]; a[i] |
| object-size | Ghi vượt kích thước đối tượng | memcpy quá lớn |
| float-cast-overflow | Ép số thực sang nguyên bị tràn | (int)1e20 |
| return | Hàm không void thiếu return | Bài 33.3 mục 14 |
| vla-bound | Kích thước VLA âm hoặc bằng không | int a[n] với n = 0 |
| unreachable | Chạy vào __builtin_unreachable | Nhánh bạn thề không xảy ra |
| pointer-overflow | Tính địa chỉ bị tràn | p + n quá lớn |
Ba kiểm tra đáng chú ý nhất
/* 1. alignment: quan trong tren ARM, noi truy cap lech lam sap chuong trinh */
char bo_dem[16];
int *p = (int *)(bo_dem + 1); /* lech 1 byte */
*p = 42;
/* UBSan: runtime error: store to misaligned address 0x... for type 'int',
which requires 4 byte alignment */
/* 2. bounds: chi hoat dong khi kich thuoc BIET LUC DICH */
int a[5];
int i = doc_so();
a[i] = 1; /* UBSan bat neu i ngoai 0..4 */
int *b = malloc(5 * sizeof *b);
b[i] = 1; /* UBSan KHONG bat: kich thuoc khong biet.
Cai nay la viec cua ASan. */
/* 3. return: bat duoc dung cho ma UB o Bai 33.2 */
int f(int x) { if (x) return 1; }
/* UBSan: runtime error: execution reached the end of a value-returning
function without returning a value */#Chọn nhóm kiểm tra
Bốn cách gọi
# 1. Tat ca cac kiem tra mac dinh
gcc -fsanitize=undefined prog.c
# 2. Chi mot vai nhom
gcc -fsanitize=signed-integer-overflow,shift,null prog.c
# 3. Tat ca tru mot nhom
gcc -fsanitize=undefined -fno-sanitize=alignment prog.c
# 4. Them nhom KHONG nam trong "undefined"
gcc -fsanitize=undefined,unsigned-integer-overflow prog.c| Nhóm | Có trong GCC | Có trong Clang | Ghi chú |
|---|---|---|---|
| undefined (nhóm chính) | Có | Có | Dùng cái này |
| unsigned-integer-overflow | Không | Có | Không phải UB, hay báo giả |
| implicit-conversion | Không | Có | Bắt ép kiểu ngầm mất dữ liệu |
| local-bounds | Không | Có | Mảng cục bộ, cần -O |
| nullability | Không | Có | Dùng với _Nonnull |
| float-divide-by-zero | Có | Có | Không phải UB nếu theo IEEE 754 |
#Dừng ngay hay chạy tiếp
# Mac dinh: bao roi chay tiep
gcc -fsanitize=undefined prog.c
# Dung han o loi dau tien, va tra ve ma thoat khac 0
gcc -fsanitize=undefined -fno-sanitize-recover=all prog.c
# Hoac dat luc chay
UBSAN_OPTIONS=halt_on_error=1 ./prog
# In ca vet goi cho moi bao loi
UBSAN_OPTIONS=print_stacktrace=1 ./progterminal
gcc -std=c11 -g -O1 -fsanitize=undefined -fno-sanitize-recover=all -o ub ub.c && ./ub
ub.c:4:44: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
echo $?
1
#Kết hợp với ASan
gcc -std=c11 -g3 -O1 \
-fno-omit-frame-pointer \
-fsanitize=address,undefined \
-fno-sanitize-recover=all \
-Wall -Wextra \
-o prog prog.c
# Hai sanitizer nay dung chung duoc, va nen dung chung.
# Chi phi cong lai van chi khoang 2.2 lan.
# KHONG dung chung voi thread:
gcc -fsanitize=address,thread prog.c
# error: -fsanitize=address is incompatible with -fsanitize=thread| Cặp | Dùng chung được | Ghi chú |
|---|---|---|
| address + undefined | Có | Cặp nên dùng mặc định |
| address + leak | Có | Leak đã nằm sẵn trong address |
| thread + undefined | Có | Lượt chạy thứ hai |
| address + thread | Không | Lỗi ngay lúc dịch |
| address + memory | Không | Lỗi ngay lúc dịch |
| memory + undefined | Có | Chỉ Clang, và cần dịch cả thư viện |
#Giới hạn
Tự làm thử
- Viết chương trình có ba lỗi UB rồi chạy có và không có UBSan.
- Xác nhận mã thoát là 0 nếu không dùng
-fno-sanitize-recover. - Thử kiểm tra
alignmentbằng cách ép con trỏ lệch một byte. - Chứng minh
boundsbắt được mảng cố định còn ASan thì không. - Bật
unsigned-integer-overflowtrên một dự án có hàm băm và đếm số báo giả. - Viết Makefile ba lượt và chạy nó trên một dự án của bạn.
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
- UBSan chỉ chậm thêm khoảng hai mươi phần trăm và bắt đúng nhóm lỗi ASan không thấy.
- Báo lỗi nói cả phép tính lẫn kiểu:
2147483647 + 1 cannot be represented in type 'int'. - Mặc định nó báo rồi chạy tiếp và trả về mã thoát 0, nên trong CI phải dùng
-fno-sanitize-recover=all. boundsbắt được mảng biết kích thước lúc dịch, chỗ mà ASan bỏ qua, nên hai công cụ bổ sung cho nhau.- Nó không bắt strict aliasing, không bắt biến chưa khởi tạo, và chỉ thấy đường mã thật sự chạy.