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

Tràn số và cách kiểm tra đúng

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

  • Giải thích vì sao kiểm tra tràn sau khi tràn là vô nghĩa
  • Viết kiểm tra tràn bằng cách so với INT_MAX trước khi cộng
  • Dùng __builtin_add_overflow và họ hàm cùng nhóm
  • Biết khi nào chuyển sang số không dấu là cách chữa đúng

Tràn số có dấu là dạng UB gây nhiều lỗ hổng bảo mật nhất, vì nó xuất hiện đúng ở chỗ người ta tưởng mình đang kiểm tra: tính kích thước bộ đệm, cộng chỉ số, nhân số phần tử với kích thước phần tử.

#Vì sao kiểm tra sau khi tràn là vô nghĩa

Tràn số có dấu
Khi kết quả một phép tính trên kiểu có dấu không biểu diễn được trong kiểu đó. Chuẩn C gọi đây là hành vi không xác định, chứ không phải quấn vòng. Chỉ số không dấu mới được bảo đảm quấn vòng theo modulo.
terminal
# Bản kiểm tra sai, ở mức tối ưu thấp nhất
gcc -O0 -S -masm=intel -o - tran.c
kiem_tra_tran:
    mov  DWORD PTR 16[rbp], ecx
    mov  eax, 0          ; ca cau if bien mat
    pop  rbp
    ret

#Cách một: so trước khi tính

Bốn phép tính, bốn cách kiểm tra
#include <limits.h>
#include <stdbool.h>

/* Cong */
bool cong_tran(int a, int b) {
    if (b > 0 && a > INT_MAX - b) return true;
    if (b < 0 && a < INT_MIN - b) return true;
    return false;
}

/* Tru */
bool tru_tran(int a, int b) {
    if (b < 0 && a > INT_MAX + b) return true;
    if (b > 0 && a < INT_MIN + b) return true;
    return false;
}

/* Nhan */
bool nhan_tran(int a, int b) {
    if (a == 0 || b == 0) return false;
    if (a == -1) return b == INT_MIN;
    if (b == -1) return a == INT_MIN;

    if (a > 0) {
        if (b > 0) return a > INT_MAX / b;
        return b < INT_MIN / a;
    }
    if (b > 0) return a < INT_MIN / b;
    return a < INT_MAX / b;
}

/* Chia */
bool chia_tran(int a, int b) {
    return b == 0 || (a == INT_MIN && b == -1);
}
terminal
# Đoạn kiểm tra đúng thì còn nguyên trong hợp ngữ
gcc -O2 -S -masm=intel -o - tran.c
kiem_tra_dung:
    xor  eax, eax
    cmp  ecx, 2147483647
    sete al
    ret

#Cách hai: hàm dựng sẵn

Ba hàm của GCC và Clang
bool __builtin_add_overflow(a, b, ket_qua);
bool __builtin_sub_overflow(a, b, ket_qua);
bool __builtin_mul_overflow(a, b, ket_qua);

/* Chung la macro tong quat: nhan moi kieu so nguyen, ke ca hai
   toan hang khac kieu nhau. Tra ve true neu TRAN, va van ghi
   ket qua quan vong vao *ket_qua. */

int r;

if (__builtin_mul_overflow(so_phan_tu, co_phan_tu, &r)) {
    return -1;                    /* tran, tu choi cap phat */
}

void *p = malloc((size_t)r);
terminal
gcc -O2 -S -masm=intel -o - tran.c
kiem_tra_builtin:
    xor  eax, eax
    add  ecx, 1
    seto al          ; doc thang co OF cua CPU
    ret
Bản đa nền tảng
#include <stdbool.h>
#include <limits.h>

#if defined(__GNUC__) || defined(__clang__)

#define CONG_TRAN(a, b, r) __builtin_add_overflow((a), (b), (r))
#define NHAN_TRAN(a, b, r) __builtin_mul_overflow((a), (b), (r))

#else

static inline bool CONG_TRAN(int a, int b, int *r) {
    if (b > 0 && a > INT_MAX - b) return true;
    if (b < 0 && a < INT_MIN - b) return true;
    *r = a + b;
    return false;
}

static inline bool NHAN_TRAN(int a, int b, int *r) {
    long long t = (long long)a * b;
    if (t > INT_MAX || t < INT_MIN) return true;
    *r = (int)t;
    return false;
}

#endif

#Cách ba: đổi sang kiểu rộng hơn

/* Neu ban co kieu rong gap doi thi cach nay don gian nhat: */

int32_t a, b;
int64_t t = (int64_t)a * b;        /* KHONG the tran: 32 x 32 lot trong 64 */

if (t > INT32_MAX || t < INT32_MIN) return -1;

int32_t r = (int32_t)t;

/* Chu y ep kieu o VE PHAI cua phep nhan, khong phai o ket qua: */

int64_t sai  = a * b;              /* SAI: a * b tinh o 32 bit ROI moi mo rong.
                                      Da tran truoc khi gan */

int64_t dung = (int64_t)a * b;     /* DUNG: a duoc mo rong truoc,
                                      nen ca phep nhan lam o 64 bit */

#Số không dấu: quấn vòng hợp lệ, nhưng vẫn là bẫy

unsigned int u = 0;
u = u - 1;                /* HOP LE: quan vong thanh UINT_MAX = 4294967295 */

size_t n = 0;
n = n - 1;                /* HOP LE: SIZE_MAX */

/* Khong co UB. Nhung ket qua gan nhu chac chan KHONG phai
   thu ban muon. */
Trừ trên số không dấu
size_t n = so_phan_tu();      /* co the la 0 */

for (size_t i = 0; i <= n - 1; ++i)
    xu_ly(a[i]);

/* Neu n == 0 thi n - 1 = SIZE_MAX, va vong lap chay
   mot ty ty lan cho toi khi sap. */
Không bao giờ trừ
size_t n = so_phan_tu();

for (size_t i = 0; i < n; ++i)
    xu_ly(a[i]);

/* Dung < voi n thay vi <= voi n - 1. Khi n == 0 thi
   vong lap khong chay lan nao, dung nhu mong doi. */
Phép tínhCó dấuKhông dấuNên dùng
Vượt giá trị lớn nhấtUBQuấn vòng, hợp lệKhông dấu, có kiểm tra
Đi dưới 0UBQuấn vòng, hợp lệKhông dấu, có kiểm tra
Dịch trái ra khỏi kiểuUBBỏ bit tràn, hợp lệKhông dấu
Dịch phảiImplementation-definedXác địnhKhông dấu
Chia làm trònVề không, xác địnhXác địnhCả hai đều ổn
Chỉ số và kích thướcDễ tràn thành UBDễ quấn vòng thầm lặngsize_t, kiểm tra biên

#Nhân, chia và ép kiểu

Mẫu cấp phát an toàn

Ba tầng bảo vệ cho một lần cấp phát
#include <stdint.h>
#include <stdlib.h>

void *cap_mang(size_t so, size_t co) {
    /* 1. Chan nhan tran */
    if (co != 0 && so > SIZE_MAX / co) return NULL;

    size_t tong = so * co;

    /* 2. Chan cap phat vo ly lon, thuong la dau hieu du lieu vao hong */
    if (tong > 64u * 1024u * 1024u) return NULL;

    /* 3. calloc tu kiem tra tran mot lan nua, va quet sach bo nho */
    return calloc(so, co);
}

/* Chuan bat buoc calloc phai tra ve NULL neu so * co tran.
   malloc thi khong co bao dam do, vi no chi nhan MOT doi so.

   Do la mot ly do rat tot de dung calloc cho mang. */

Ép kiểu thu hẹp

long long lon = 5000000000LL;

int nho = (int)lon;       /* implementation-defined truoc C23,
                             xac dinh tu C23: quan vong theo bu hai.
                             Gia tri o day la 705032704 */

/* Trinh bien dich khong canh bao vi ban da ep TUONG MINH.
   Bat -Wconversion de no canh bao cac ep NGAM: */

int a = lon;              /* warning: conversion from 'long long int'
                             to 'int' may change value [-Wconversion] */

/* Cach dung: kiem tra truoc khi thu hep. */

#include <limits.h>

if (lon > INT_MAX || lon < INT_MIN) return -1;

int nho = (int)lon;

Tự làm thử

  1. Viết ba hàm kiểm tra tràn cho cộng, trừ và nhân, rồi kiểm thử với mọi giá trị biên.
  2. So hợp ngữ của bản viết tay và bản __builtin_add_overflow.
  3. Viết vòng lặp for (size_t i = 0; i <= n - 1; ++i) với n bằng 0 và xem điều gì xảy ra.
  4. Chứng minh if (i < u) với i = -1 và u = 1 không in ra gì.
  5. Viết hàm cap_mang và kiểm thử nó với so = SIZE_MAX.
  6. Bật -Wconversion trên một dự án cũ và phân loại các cảnh báo thành thật và giả.

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

  • Mọi biểu thức kiểm tra tràn có chứa chính phép tính tràn đều bị xóa, ngay cả ở -O0.
  • Cách đúng là so với INT_MAX và INT_MIN trước khi tính, hoặc dùng __builtin_add_overflow chỉ tốn một lệnh seto.
  • Ép kiểu phải đặt ở vế phải phép nhân: (int64_t)a * b chứ không phải (int64_t)(a * b).
  • Số không dấu quấn vòng hợp lệ nhưng vẫn sai: n - 1 với n = 0 cho ra SIZE_MAX.
  • calloc bắt buộc phải kiểm tra tràn khi nhân hai đối số, còn malloc thì không.