Ketika program berhenti di breakpoint, melempar exception, atau runtuh karena pelanggaran memori, runtime menyimpan sesuatu yang sangat berharga: susunan function yang sedang aktif saat itu. Susunan ini disebut stack trace atau backtrace — daftar berbingkai dari titik kejadian sampai ke main(). Membacanya dengan terampil sering memangkas waktu debugging dari berjam-jam menjadi berhitung-menit, karena stack trace bukan hanya menunjukkan baris yang runtuh, melainkan juga seluruh rantai pemanggil yang membawa kita ke sana. Chapter ini melatih Anda membaca stack trace dari empat sumber: debugger, shell, exception, dan <stacktrace> dari C++23.
Anatomi Sebuah Stack Frame #
Setiap kali function dipanggil, runtime menaruh satu frame baru di atas call stack: berisi parameter, variabel lokal, dan alamat kembali ke pemanggil. Frame main() selalu berada di dasar; frame function yang sedang dieksekusi selalu di puncak. Stack trace adalah potret tumpukan itu pada satu momen. Karena frame tertua ada di bawah dan frame terbaru ada di atas, penomoran dalam laporan biasanya dibalik: #0 adalah frame terdalam (tempat kejadian), #1 pemanggil langsungnya, dan seterusnya sampai main(). Pada program yang memakai library, bagian bawah trace juga memuat frame internal seperti __libc_start_main — frame itu normal dan bisa kita abaikan saat membaca.
Backtrace dari Debugger saat Program Runtuh #
Program berikut runtuh karena men-dereference pointer nullptr — bug klasik yang lolos dari compiler:
#include <iostream>
int bagiTerakhir(const int* angka, int jumlah) {
return 100 / angka[jumlah - 1];
}
int main() {
const int* data = nullptr;
std::cout << bagiTerakhir(data, 3) << "\n";
return 0;
}Dijalankan lewat shell, program berhenti seketika tanpa penjelasan apa pun. Menjalankannya di dalam gdb (atau menempelkan gdb ke proses yang runtuh dengan gdb ./demo-null core) memberi kita potret lengkapnya:
(gdb) run
Starting program: /home/budi/latihan/demo-null
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555169 in bagiTerakhir (angka=0x0, jumlah=3)
at demo-null.cpp:5
5 return 100 / angka[jumlah - 1];
(gdb) backtrace
#0 0x0000555555555169 in bagiTerakhir (angka=0x0, jumlah=3)
at demo-null.cpp:5
#1 0x00005555555551bd in main () at demo-null.cpp:9Cara membacanya, baris demi baris:
Program received signal SIGSEGV: sistem operasi mengirim sinyal segmentation fault karena program menyentuh memori yang bukan miliknya — di sini alamat0x0.- Frame
#0: kejadian terjadi dibagiTerakhir, baris 5, dan gdb bahkan menampilkan nilai parameter:angka=0x0. Satu lihat langsung terjawab — pointer yang dibaca adalah null. - Frame
#1:main()memanggilbagiTerakhirdari baris 9 dengan mengoperdatayang ternyatanullptr. Perbaikannya ada di pemanggil: periksa pointer sebelum mengoperkannya.
Pola membaca yang perlu dibiasakan: mulai dari #0 untuk melihat apa yang runtuh, lalu turun ke #1, #2, dst. untuk melihat siapa yang mengundang kejadian itu.
Laporan Crash dari Shell #
Tanpa debugger, shell tetap memberi petunjuk pertama. Saat program di atas dijalankan, terminal menampilkan pesan khas berikut dan kode akhir yang tidak nol:
$ ./demo-null
[1] 31039 segmentation fault ./demo-null
$ echo $?
139Pesan segmentation fault memberi tahu jenis pelanggaran (akses memori di luar wilayah proses), sedangkan kode akhir 139 bermakna: 128 + nomor sinyal SIGSEGV (11). Angka ini penting untuk skrip otomatis — skrip CI yang melihat kode 139 tahu programnya runtuh, bukan sekadar gagal dengan return 1. Kebalikannya, pesan seperti abort (sinyal 6 → kode 134) biasanya muncul dari assert yang gagal atau exception yang tidak tertangkap.
std::stacktrace di C++23 #
Sejak C++23, stack trace bisa direkam dari dalam program sendiri tanpa debugger, melalui header <stacktrace>. GCC 14 mendukungnya; hanya perlu flag -g (agar nama berkas dan baris tersedia) dan flag linker -lstdc++exp:
#include <iostream>
#include <stacktrace>
#include <string>
void bacaBaris(const std::string& namaFile, int nomorBaris) {
std::cout << std::stacktrace::current() << "\n";
}
void prosesFile(const std::string& namaFile) {
bacaBaris(namaFile, 7);
}
void muatData() {
prosesFile("data-uji.csv");
}
int main() {
muatData();
return 0;
}Hasil eksekusinya di lingkungan penulis (path dan baris mengikuti tempat Anda mengompilasi; signature std::string tampil panjang karena dipakai nama tipe lengkapnya):
0# bacaBaris(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, int) at /tmp/debug-latihan/stacktrace-demo.cpp:8
1# prosesFile(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) at /tmp/debug-latihan/stacktrace-demo.cpp:12
2# muatData() at /tmp/debug-latihan/stacktrace-demo.cpp:16
3# main at /tmp/debug-latihan/stacktrace-demo.cpp:20
4# at :0
5# __libc_start_main at :0
6# _start at :0
7# std::stacktrace::current() merekam potret stack pada baris pemanggilannya — persis seperti backtrace di gdb, tetapi sebagai nilai std::basic_stacktrace yang bisa dicetak, disimpan, atau dikirim bersama log error. Perhatikan frame #4 ke bawah: runtime C meluncurkan main, dan frame itu tidak relevan bagi kita. Untuk logging error yang rapi, tandai batas baca: berhentilah di frame main.
Stack Trace dari Exception #
Exception yang tidak tertangkap juga membawa informasi bingkai (di Linux: terminate called after throwing an instance of ...), tetapi yang sering dibutuhkan adalah jejak di titik exception dilempar, bukan saat program mati. C++23 belum menyediakan std::stacktrace otomatis di dalam throw, jadi kita bisa meniru idiomnya dengan RAII kecil: setiap function mencatat namanya saat masuk dan menghapusnya saat keluar — persis perilaku frame sungguhan:
// jejak-panggil.cpp
// Simulasi stack trace manual: setiap fungsi mencatat namanya saat masuk
// dan menghapus catatan itu saat keluar (RAII). Stack trace dibekukan di
// titik kegagalan — persis seperti prinsip kerja std::stacktrace.
#include <exception>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
class JejakPanggil {
public:
explicit JejakPanggil(std::string nama) {
jejak.push_back(std::move(nama));
}
~JejakPanggil() {
jejak.pop_back();
}
static void cetak() {
for (std::size_t i = 0; i < jejak.size(); ++i) {
std::cout << " #" << i << " " << jejak[i] << "\n";
}
}
inline static std::vector<std::string> jejak;
};
void bacaBaris(const std::string& namaFile, int nomorBaris) {
JejakPanggil bingkai("bacaBaris");
std::cout << "jejak pemanggilan di titik kegagalan:\n";
JejakPanggil::cetak();
throw std::runtime_error(namaFile + " baris " + std::to_string(nomorBaris)
+ ": format tidak valid");
}
void prosesFile(const std::string& namaFile) {
JejakPanggil bingkai("prosesFile");
bacaBaris(namaFile, 7);
}
void muatData() {
JejakPanggil bingkai("muatData");
prosesFile("data-uji.csv");
}
int main() {
try {
muatData();
} catch (const std::exception& e) {
std::cout << "tertangkap: " << e.what() << "\n";
}
return 0;
}Output yang Dihasilkan:
jejak pemanggilan di titik kegagalan:
#0 muatData
#1 prosesFile
#2 bacaBaris
tertangkap: data-uji.csv baris 7: format tidak validMari kita bedah mekanismenya:
- Baris 19-27 (class
JejakPanggil): constructor mendorong nama ke vektor statis, destructor mengeluarkannya. Karena objek hidup sebatas scope function, isi vektor selalu mencerminkan tumpukan panggilan yang sedang aktif. - Baris 31-38 (
bacaBaris): jejak dibekukan dan dicetak sebelumthrow. Urutannya penting — saat exception merambat naik, semua destructor berjalan dan jejak akan kosong pada saatcatchmenangkapnya. Inilah sebabnya stack trace asli direkam di titik lempar. - Urutan cetak: vektor kita mengisi dari luar ke dalam, jadi
#0adalah frame terluar — kebalikan konvensi gdb yang menomori frame terdalam sebagai#0. Keduanya sah; yang penting konsisten saat membaca.
Dengan tiga sumber stack trace di tangan, bug yang menyebabkan program runtuh kini selalu bisa dilacak sampai ke barisnya. Namun tidak semua bug runtuh — banyak yang hanya mencetak peringatan dari compiler jauh sebelum program dijalankan. Chapter berikutnya mengajak kita membaca dan menambal peringatan-peringatan itu dengan serius.
Referensi: cppreference — std::stacktrace , GCC — Debugging Options (-g) .