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:
// 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:
256
-2147483648Kedua 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:
g++ -std=c++23 -Wall -Wextra -fsanitize=undefined luapan.cpp -o luapan
./luapanPesan 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:
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:
g++ -std=c++23 -Wall -Wextra -fsanitize=undefined -fno-sanitize-recover=all luapan.cpp -o luapan-keras
./luapan-kerasKali ini program berhenti pada pelanggaran pertama dengan kode akhir tidak nol:
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:
| Pemeriksa | Pelanggaran yang ditandai |
|---|---|
signed-integer-overflow | aritmetika int/long bertanda meluap (perkalian, penjumlahan, negasi INT_MIN) |
shift | geser melebihi lebar tipe atau geser nilai bertanda negatif |
alignment | pointer yang tidak sejajar dengan syarat tipenya |
null | dereference atau pembentukan reference dari pointer null |
object-size | akses yang melebihi ukuran objek yang diketahui compiler |
float-divide-by-zero | pembagian floating-point oleh nol (jika diaktifkan) |
Pemeriksa alignment memberi laporan yang sedikit berbeda — ia memuat alamat, sehingga tampilannya bervariasi antar mesin:
baca-sejajar.cpp:10:19: runtime error: load of misaligned address 0x7ffd30a9c3f1 for type 'int', which requires 4 byte alignment
0x7ffd30a9c3f1: note: pointer points hereCatatan 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:
// 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:
256
tertangkap: naikkanSatu: melebihi batas intMari kita bedah pertahanannya:
- Baris 8-10 (
naikkanSatu): sebelumnilai + 1dieksekusi, kita bandingkan denganstd::numeric_limits<int>::max(). Bila sama, penjumlahan pasti meluap — lemparstd::overflow_errordengan pesan yang jelas, sehingga pemanggil memutuskan apa artinya bagi domainnya. - Baris 13-15 (
geserKiri): jumlah geser divalidasi terhadap lebarint(32 bit) sebelum operasi; input 40 dari versi bermasalah kini ditolak sebagaistd::invalid_argument. - Alur output:
geserKiri(1, 8)sah dan mencetak256— nilai yang benar secara standar, bukan hasil kebetulan hardware. PanggilannaikkanSatu(2147483647)ditangkapcatch, 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 .