Ada bug yang memori-nya sah tetapi hasil hitungnya salah: int yang meluap melewati batas, operasi shift yang melebihi lebar tipe, atau pembacaan lewat pointer yang tidak sejajar. Standar C++ menyebut semuanya undefined behavior — compiler bebas menghasilkan apa pun — dan pada build optimasi, perilakunya bisa berbeda total dari yang Anda amati saat debugging. UndefinedBehaviorSanitizer (UBSan) menyisipkan pemeriksaan pada operasi-operasi itu sehingga pelanggaran pertama langsung dilaporkan dengan berkas, baris, dan penjelasan yang manusiawi. Bandingkan dengan laporan ASan: output UBSan pendek, tidak memuat alamat memori yang berubah-ubah, dan karena itu juga mudah dikutip di dokumentasi maupun log CI.

Program dengan Dua Pelanggaran Tersembunyi #

Program ini mengompilasi dengan mulus, berjalan tanpa runtuh, dan tetap mengandung dua UB:

cpp luapan.cpp — dua UB yang lolos dari compiler
// luapan.cpp — mengandung UB; jalankan dengan -fsanitize=undefined
#include <iostream>

int naikkanSatu(int nilai) {
    return nilai + 1;   // UB saat nilai = INT_MAX
}

int main() {
    int geser = 40;                        // anggap hasil parsing input log
    std::cout << (1 << geser) << "\n";     // UB: shift melebihi lebar int
    std::cout << naikkanSatu(2147483647) << "\n";  // UB: signed overflow
    return 0;
}

Kompilasi biasa lalu jalankan, dan kita mendapat dua angka yang tampak wajar:

text Output versi tanpa UBSan (perilaku mesin ini — bukan jaminan standar)
256
-2147483648

Kedua angka itu salah secara logika: 1 << 40 di mesin ini “menjadi” 1 << 8 karena hardware memotong jumlah geseran modulo 32, dan 2147483647 + 1 meluap menjadi -2147483648. Nilai seperti ini bisa menular ke seluruh perhitungan di hilirnya tanpa satu pun gejala. Sekarang kompilasi ulang dengan UBSan:

bash
g++ -std=c++23 -Wall -Wextra -fsanitize=undefined luapan.cpp -o luapan
./luapan

Pesan runtime berikut muncul di stderr saat program berjalan — output nyata dari g++ 14.2, yang tidak memuat alamat memori sehingga tampil persis begitu di mesin Anda:

text
luapan.cpp:10:21: runtime error: shift exponent 40 is too large for 32-bit type 'int'
luapan.cpp:5:20: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'

Baca struktur pesannya: berkas:baris:kolom: runtime error: lalu penjelasan dengan istilah standar — shift exponent terlalu besar untuk tipe 32-bit, dan 2147483647 + 1 tidak dapat diwakili oleh int. Perhatikan juga urutannya: pesan pertama dicetak saat baris 10 dieksekusi, pesan kedua menyusul saat baris 5 dijalankan — program melanjutkan eksekusi setelah tiap laporan.

Melanjutkan atau Berhenti? #

Perilaku bawaan UBSan disebut recoverable: lapor, lalu teruskan eksekusi. Untuk pipeline otomatis, itu kurang ideal — pelanggaran seharusnya menggagalkan proses. Flag -fno-sanitize-recover=all mengubahnya:

bash
g++ -std=c++23 -Wall -Wextra -fsanitize=undefined -fno-sanitize-recover=all luapan.cpp -o luapan-keras
./luapan-keras

Kali ini program berhenti pada pelanggaran pertama dengan kode akhir tidak nol:

text Output dengan -fno-sanitize-recover=all (dihentikan pada pelanggaran pertama)
luapan.cpp:10:21: runtime error: shift exponent 40 is too large for 32-bit type 'int'

Polanya penting diingat: pesan UBSan tampil di stderr, bukan stdout, dan program bisa tetap sukses secara tampak luar. Test suite yang “lulus” di bawah UBSan hanya lulus bila stderr bersih — pasangkan UBSan dengan -fno-sanitize-recover=all agar kegagalan tidak bisa tersembunyi.

Katalog Pelanggaran yang Dideteksi #

Flag -fsanitize=undefined adalah bundel beberapa pemeriksa. Yang paling sering menyelamatkan kode nyata:

PemeriksaPelanggaran yang ditandai
signed-integer-overflowaritmetika int/long bertanda meluap (perkalian, penjumlahan, negasi INT_MIN)
shiftgeser melebihi lebar tipe atau geser nilai bertanda negatif
alignmentpointer yang tidak sejajar dengan syarat tipenya
nulldereference atau pembentukan reference dari pointer null
object-sizeakses yang melebihi ukuran objek yang diketahui compiler
float-divide-by-zeropembagian floating-point oleh nol (jika diaktifkan)

Pemeriksa alignment memberi laporan yang sedikit berbeda — ia memuat alamat, sehingga tampilannya bervariasi antar mesin:

text Contoh output UBSan untuk pointer tidak sejajar (alamat bervariasi)
baca-sejajar.cpp:10:19: runtime error: load of misaligned address 0x7ffd30a9c3f1 for type 'int', which requires 4 byte alignment
0x7ffd30a9c3f1: note: pointer points here

Catatan soal batas: dereference nullptr yang benar-benar dibaca biasanya tetap runtuh di hardware — UBSan melaporkan saat pointer null dihitung atau di-dereference, lalu segfault bisa menyusul; itulah sebabnya laporan null-deref paling sering kita lihat lewat ASan atau debugger. UBSan tidak mendeteksi buffer overflow — pekerjaan itu milik ASan; keduanya saling melengkapi dan lazim dipasangkan dengan -fsanitize=address,undefined.

Versi Aman: Periksa Batas Sebelum Menghitung #

Program berikut melakukan operasi yang sama dengan penjagaan eksplisit di depan gerbang:

cpp
// aman-ubsan.cpp — operasi aritmetika yang sama, tetapi dijaga sebelum UB terjadi.

#include <iostream>
#include <limits>
#include <stdexcept>

int naikkanSatu(int nilai) {
    if (nilai == std::numeric_limits<int>::max()) {
        throw std::overflow_error("naikkanSatu: melebihi batas int");
    }
    return nilai + 1;
}

int geserKiri(int basis, int jumlah) {
    if (jumlah < 0 || jumlah >= 32) {
        throw std::invalid_argument("geserKiri: jumlah harus 0..31");
    }
    return basis << jumlah;
}

int main() {
    try {
        std::cout << geserKiri(1, 8) << "\n";
        std::cout << naikkanSatu(2147483647) << "\n";
    } catch (const std::exception& e) {
        std::cout << "tertangkap: " << e.what() << "\n";
    }
    return 0;
}

Output yang Dihasilkan:

text
256
tertangkap: naikkanSatu: melebihi batas int

Mari kita bedah pertahanannya:

  1. Baris 8-10 (naikkanSatu): sebelum nilai + 1 dieksekusi, kita bandingkan dengan std::numeric_limits<int>::max(). Bila sama, penjumlahan pasti meluap — lempar std::overflow_error dengan pesan yang jelas, sehingga pemanggil memutuskan apa artinya bagi domainnya.
  2. Baris 13-15 (geserKiri): jumlah geser divalidasi terhadap lebar int (32 bit) sebelum operasi; input 40 dari versi bermasalah kini ditolak sebagai std::invalid_argument.
  3. Alur output: geserKiri(1, 8) sah dan mencetak 256 — nilai yang benar secara standar, bukan hasil kebetulan hardware. Panggilan naikkanSatu(2147483647) ditangkap catch, program berakhir dengan kode akhir 0 dan UBSan diam total.

Pola “validasi di gerbang, lempar exception” ini mengubah bug diam menjadi error yang eksplisit — dan bila satu gerbang terlewat, UBSan yang berjalan di test suite tetap menangkap sisa pelanggarannya.

Dengan memori dan aritmetika terjaga, satu wilayah bug besar tersisa: program yang dirancang berjalan bersamaan. Chapter berikutnya membawa ThreadSanitizer ke meja kerja kita untuk mengendus data race.

Referensi: Clang — UndefinedBehaviorSanitizer , GCC — Instrumentation Options .