Bài trước, Delve gỡ lỗi một tiến trình đang chạy. Nhưng nhiều sự cố nghiêm trọng nhất là những cú crash hiếm, khó tái hiện — nó xảy ra một lần trên production lúc 3 giờ sáng rồi tiến trình chết, và bạn không thể gắn debugger vào cái đã không còn. Panic trace trong log cho biết crash ở đâu (dòng nào), nhưng mất trạng thái: giá trị biến, nội dung struct/map tại thời điểm đó biến mất theo tiến trình.
Core dump giải bài toán này: nó là ảnh chụp toàn bộ bộ nhớ của tiến trình tại thời điểm chết. Sau đó bạn mổ xẻ nó (phân tích post-mortem) bằng dlv core — dựng lại ngăn xếp, xem biến, kiểm goroutine — dù tiến trình đã chết từ lâu. Bài này gây một crash thật, sinh core dump, và phân tích nó để tìm nguyên nhân.
Code có bug nil pointer
func rutTien(tk *TaiKhoan, tien int) int {
tk.SoDu -= tien // BUG: tk có thể nil -> crash SIGSEGV
return tk.SoDu
}
func xuLyGiaoDich(ds map[string]*TaiKhoan, ten string, tien int) {
tk := ds[ten] // "binh" không có trong map -> tk = nil
con := rutTien(tk, tien) // crash tại đây
}
Truy cập một key không có trong map trả về giá trị zero (ở đây là nil con trỏ), rồi rutTien dereference nil → crash SIGSEGV. Kinh điển.
Bật sinh core dump khi crash
$ ulimit -c unlimited # cho phép sinh core dump
$ GOTRACEBACK=crash ./chuongtrinh # Go sinh core khi panic
Hai điều kiện: ulimit -c unlimited cho phép hệ điều hành ghi core dump (mặc định thường tắt, giới hạn 0), và GOTRACEBACK=crash bảo Go — thay vì thoát sạch sau panic — tự gửi SIGABRT cho chính nó, khiến HĐH ghi ảnh chụp bộ nhớ ra file core.

Hình 1: Core dump là ảnh chụp toàn bộ bộ nhớ tiến trình lúc crash (bật bằng ulimit -c unlimited + GOTRACEBACK=crash); mổ xẻ post-mortem bằng dlv core — bt, frame, print, goroutines — dù tiến trình đã chết.
Đo thật: sinh core dump và dựng lại ngăn xếp
Crash sinh core dump:
$ ulimit -c unlimited; GOTRACEBACK=crash ./cbin
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV ...]
exit code: 134 // 128+6 = SIGABRT (Go tự gửi để sinh core)
$ ls -la /tmp/core
-rw------- 40325120 /tmp/core // core dump 40 MB - ảnh chụp bộ nhớ
Exit code 134 (= 128 + 6, tức SIGABRT) xác nhận Go đã tự gửi tín hiệu để sinh core. File core 40 MB chứa toàn bộ ảnh chụp bộ nhớ. Mổ xẻ bằng dlv core:
$ dlv core ./cbin /tmp/core
(dlv) bt
...
11 0xb3be8 in main.rutTien at /work/t128/main.go:12
13 0xb3c74 in main.xuLyGiaoDich at /work/t128/main.go:18
bt (backtrace) dựng lại toàn bộ ngăn xếp lúc crash — chỉ đúng main.rutTien dòng 12 (tk.SoDu -= tien), được gọi từ main.xuLyGiaoDich dòng 18. Điều hướng tới frame ứng dụng:
(dlv) frame 13
=> 18: con := rutTien(tk, tien) // ds[ten] trả nil
(dlv) frame 11
=> 12: tk.SoDu -= tien // nil dereference -> crash
Từ một core dump, ta dựng lại chính xác đường đi tới lỗi — ds["binh"] trả nil, truyền vào rutTien, dereference nil ở dòng 12 — dù tiến trình đã chết. (Trung thực: binary này build với cờ crash tối ưu nên một số biến cục bộ có thể bị "optimized out"; để xem biến đầy đủ, build với -gcflags="all=-N -l" như bài Delve. Nhưng thông tin ngăn xếp và vị trí dòng thì luôn dựng lại được, và đó thường là mấu chốt.)

Hình 2: Đo thật — crash nil pointer với GOTRACEBACK=crash sinh core dump 40 MB (exit 134/SIGABRT); dlv core dựng lại ngăn xếp tới đúng main.rutTien:12 (nơi crash) gọi từ xuLyGiaoDich:18, điều hướng frame tới đúng dòng lỗi dù tiến trình đã chết.
Đánh đổi cần cân nhắc
Core dump rất LỚN và chứa bí mật — xử lý cẩn thận. Core dump là ảnh chụp toàn bộ bộ nhớ tiến trình, nên nó lớn (40 MB cho một chương trình tí hon; hàng GB cho dịch vụ thật) và chứa mọi thứ trong bộ nhớ lúc đó: mật khẩu, token, dữ liệu người dùng, khóa mã hóa. Đây là rủi ro bảo mật nghiêm trọng — core dump production phải được lưu trữ an toàn (mã hóa, quyền truy cập hạn chế) và xóa sau khi điều tra, không để lẫn vào log hay backup thường. Nhiều nơi cấm sinh core trên production vì lý do này.
Cần binary khớp chính xác để phân tích. dlv core cần đúng binary đã sinh ra core dump (cùng bản build, cùng symbol) để giải mã địa chỉ thành tên hàm và dòng. Nếu bạn deploy binary đã strip symbol (để nhỏ gọn) mà không giữ bản có symbol, core dump gần như vô dụng. Thực hành tốt: lưu binary có symbol (hoặc file debug riêng) cho mỗi bản release, khớp với version đang chạy, để khi có core dump thì phân tích được.
Core dump là biện pháp cuối, không thay quan sát chủ động. Core dump mạnh cho crash khó tái hiện mà log không đủ. Nhưng với phần lớn bug, log có cấu trúc, metrics, tracing (các bài quan sát trước) và test bắt được rẻ hơn và sớm hơn nhiều. Đừng dựa vào core dump như phòng tuyến đầu — nó là công cụ pháp y (forensic) cho những ca hiếm mà mọi thứ khác đã bó tay, không phải quy trình gỡ lỗi hàng ngày.
Ba ý mang về
- Core dump giữ toàn bộ trạng thái lúc crash mà panic trace mất: bật bằng
ulimit -c unlimited+GOTRACEBACK=crash(Go tự gửi SIGABRT, exit 134) — đo thật, một crash nil pointer sinh core dump 40 MB chứa ảnh chụp bộ nhớ. dlv coremổ xẻ tiến trình đã chết: đo thật,btdựng lại ngăn xếp tới đúngmain.rutTien:12(nơi dereference nil) gọi từxuLyGiaoDich:18, điều hướng frame tới đúng dòng lỗi — điều tra post-mortem dù tiến trình chết từ lâu (build-N -lđể xem biến đầy đủ hơn).- Nêu rõ rủi ro và giới hạn: core dump rất lớn và chứa bí mật trong bộ nhớ (mã hóa/hạn chế truy cập/xóa sau điều tra), cần đúng binary có symbol để phân tích, và là công cụ pháp y cho crash hiếm — không thay log/metrics/test cho gỡ lỗi hàng ngày.
Phần sau ta xét một loại sự cố âm thầm nhưng chết người: phát hiện rò rỉ goroutine trong sản xuất — cách nhận ra số goroutine tăng mãi không giảm, dùng pprof goroutine để tìm chỗ rò, và các mẫu gây rò phổ biến.