Operasi aritmetika pada tipe integer memiliki batas: nilai yang dihasilkan harus muat dalam rentang tipe. Jika hasil operasi melampaui batas tersebut, terjadilah integer overflow. Pada C, overflow tipe signed adalah undefined behavior, sedangkan overflow tipe unsigned terdefinisi sebagai wrap-around modular. Perbedaan ini menjadi sumber bug paling klasik di C—program bisa berjalan benar di mesin uji tetapi rusak di mesin produksi, atau bisa dimanfaatkan penyerang untuk eksploitasi keamanan. Pada Chapter ini kita akan melihat fenomena overflow secara konkret dan cara mendeteksinya sebelum operasi dilakukan.

Perilaku signed Overflow vs unsigned Wrap-Around #

Standar C menetapkan dua aturan berbeda untuk overflow tipe signed dan unsigned:

  • signed overflow: undefined behavior. Compiler bebas melakukan apa pun, termasuk optimisasi dengan asumsi overflow tidak pernah terjadi.
  • unsigned overflow: terdefinisi sebagai operasi modulo 2^N di mana N adalah jumlah bit tipe. Hasilnya adalah wrap-around.
c
#include <stdio.h>
#include <limits.h>

int main(void) {
    int a = INT_MAX;       /* 2147483647 */
    int b = 1;
    int hasil = a + b;     /* UB: signed overflow */

    printf("a       = %d\n", a);
    printf("b       = %d\n", b);
    printf("a + b   = %d (UB!)\n", hasil);

    /* unsigned: wrap-around well-defined */
    unsigned int u = UINT_MAX;
    unsigned int u_hasil = u + 1;
    printf("u       = %u\n", u);
    printf("u + 1   = %u (wrap-around, terdefinisi)\n", u_hasil);
    return 0;
}

Output yang Dihasilkan:

text
a       = 2147483647
b       = 1
a + b   = -2147483648 (UB!)
u       = 4294967295
u + 1   = 0 (wrap-around, terdefinisi)

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (int a = INT_MAX;): variabel a diisi dengan nilai int positif terbesar.
  2. Baris 8 (int hasil = a + b;): ekspresi a + b secara matematis adalah 2.147.483.648, yang melampaui INT_MAX. Karena int bertanda, hasil ini undefined behavior.
  3. Baris 12 (a + b = -2147483648): pada GCC x86_64, output kebetulan adalah INT_MIN karena wrap-around di hardware. Tetapi standar C tidak menjamin ini; compiler bebas menghasilkan nilai lain atau menghapus kode yang bergantung pada hasil ini.
  4. Baris 16 (unsigned int u = UINT_MAX;): variabel unsigned int diisi dengan nilai maksimumnya.
  5. Baris 17 (u + 1): hasil penjumlahan 4.294.967.295 + 1 meluap, tetapi karena tipe unsigned, hasilnya terdefinisi sebagai 0 (modulo 2^32).

Pemeriksaan Overflow Sebelum Operasi #

Cara aman menangani signed overflow adalah dengan memeriksa apakah operasi akan meluap sebelum operasi tersebut dilakukan. Strateginya: hitung batas yang masih aman dengan operasi terbalik, lalu bandingkan dengan operan. Untuk penjumlahan a + b, kita periksa b > 0 && a > INT_MAX - b (overflow positif) atau b < 0 && a < INT_MIN - b (overflow negatif).

c
#include <stdio.h>
#include <limits.h>

/* Pemeriksaan overflow sebelum operasi: penjumlahan */
int safe_add(int a, int b, int *result) {
    if (b > 0 && a > INT_MAX - b) return 0;   /* akan overflow */
    if (b < 0 && a < INT_MIN - b) return 0;   /* akan underflow */
    *result = a + b;
    return 1;
}

int main(void) {
    int hasil;
    if (safe_add(INT_MAX, 1, &hasil)) {
        printf("Hasil: %d\n", hasil);
    } else {
        printf("Overflow terdeteksi: operasi ditolak\n");
    }

    if (safe_add(100, 200, &hasil)) {
        printf("Hasil: %d\n", hasil);
    } else {
        printf("Overflow terdeteksi\n");
    }
    return 0;
}

Output yang Dihasilkan:

text
Overflow terdeteksi: operasi ditolak
Hasil: 300

Pada baris 6, fungsi safe_add menerima dua int dan pointer result untuk menampung hasil. Fungsi mengembalikan 1 jika operasi aman, 0 jika akan overflow. Pada baris 7, kondisi b > 0 && a > INT_MAX - b memeriksa apakah a + b akan melebihi INT_MAX. Ekspresi INT_MAX - b aman karena b > 0 sehingga INT_MAX - b masih dalam rentang. Pada baris 8, kondisi serupa memeriksa underflow ke arah INT_MIN. Pada baris 17, pemanggilan safe_add(INT_MAX, 1, &hasil) mendeteksi overflow dan mengembalikan 0. Pada baris 23, pemanggilan safe_add(100, 200, &hasil) aman dan mengembalikan 300.

Pemeriksaan Overflow untuk Perkalian #

Perkalian lebih sulit diperiksa daripada penjumlahan karena magnitudo hasilnya tumbuh jauh lebih cepat. Strategi yang umum: bandingkan salah satu operan dengan INT_MAX / operan_lainnya. Jika a > INT_MAX / b dan b > 0, maka a * b akan meluap.

c
#include <stdio.h>
#include <limits.h>

int main(void) {
    /* Multiplication overflow check */
    int a = 1000000;
    int b = 3000;
    if (a > INT_MAX / b) {
        printf("%d * %d akan overflow (INT_MAX = %d)\n", a, b, INT_MAX);
    } else {
        printf("%d * %d = %d\n", a, b, a * b);
    }

    /* Safe multiply yang benar */
    int c = 100;
    int d = 200;
    if (c > INT_MAX / d) {
        printf("Overflow!\n");
    } else {
        printf("%d * %d = %d\n", c, d, c * d);
    }
    return 0;
}

Output yang Dihasilkan:

text
1000000 * 3000 akan overflow (INT_MAX = 2147483647)
100 * 200 = 20000

Pada baris 8, ekspresi INT_MAX / b menghasilkan 2.147.483.647 / 3000 = 715.827 (pembagian integer). Karena a = 1.000.000 > 715.827, perkalian a * b dipastikan akan meluap. Pada baris 17, c = 100 < INT_MAX / 200 = 10.737.418, sehingga perkalian aman dan menghasilkan 20.000.

Konsekuensi Optimisasi Compiler #

Karena signed overflow adalah undefined behavior, compiler modern melakukan optimisasi berdasarkan asumsi bahwa hal itu tidak pernah terjadi. Akibatnya, loop yang bergantung pada wrap-around signed integer bisa berperilaku tak terduga. Sebagai contoh, loop for (int i = 1; i > 0; i++) seharusnya berhenti ketika i membungkus ke nilai negatif, tetapi compiler dengan optimisasi -O2 bisa saja menganggap kondisi i > 0 selalu benar (karena overflow diasumsikan tidak terjadi) dan menghasilkan loop tak berhingga.

Untuk kode yang sengaja memanfaatkan wrap-around (misalnya algoritma hash atau checksum), gunakan tipe unsigned agar perilaku terdefinisi. Untuk memaksa GCC memperlakukan signed overflow sebagai wrap-around, gunakan flag -fwrapv, tetapi ini tidak portabel dan tidak menjamin perilaku di compiler lain.

Dengan pola pemeriksaan rentang sebelum operasi dan kesadaran akan perbedaan signed/unsigned overflow, kita siap menulis kode integer yang aman. Selanjutnya, kita akan melihat batasan presisi serupa pada tipe floating point.