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.
Bài tập làm thử
Bài 1 (đọc hiểu code Go). Cho đoạn code sau, giải thích chính xác vì sao nó crash và tại dòng nào:
func rutTien(tk *TaiKhoan, tien int) int {
tk.SoDu -= tien
return tk.SoDu
}
func xuLyGiaoDich(ds map[string]*TaiKhoan, ten string, tien int) {
tk := ds[ten]
con := rutTien(tk, tien)
}
Đáp án
Nếu ten (ví dụ "binh") không tồn tại trong map ds, truy cập ds[ten] trả về giá trị zero của kiểu *TaiKhoan, tức là nil, nên tk bằng nil. Khi rutTien được gọi với tk == nil, dòng tk.SoDu -= tien cố dereference một con trỏ nil để đọc/ghi trường SoDu, gây crash SIGSEGV (nil pointer dereference).
Bài 2 (đọc hiểu). Giải thích ý nghĩa của hai lệnh sau và vì sao cả hai đều cần thiết để sinh core dump khi panic:
$ ulimit -c unlimited
$ GOTRACEBACK=crash ./chuongtrinh
Đáp án
ulimit -c unlimited cho phép hệ điều hành ghi core dump không giới hạn kích thước (mặc định trên nhiều hệ thống là tắt/giới hạn 0, nên core dump sẽ không được ghi nếu thiếu bước này). GOTRACEBACK=crash bảo runtime Go, thay vì thoát sạch sau panic, tự gửi tín hiệu SIGABRT cho chính nó, khiến hệ điều hành ghi ảnh chụp toàn bộ bộ nhớ ra file core. Thiếu một trong hai, core dump sẽ không được sinh ra đầy đủ.
Bài 3 (đọc hiểu / vận dụng). Bài đo thật cho exit code: 134 khi chương trình crash với GOTRACEBACK=crash. Giải thích ý nghĩa con số 134 này.
Đáp án
134 = 128 + 6. Theo quy ước Unix, exit code của tiến trình bị giết bởi tín hiệu là 128 cộng với số hiệu tín hiệu; tín hiệu số 6 là SIGABRT. Vì vậy exit code 134 xác nhận Go đã tự gửi SIGABRT cho chính nó (theo cơ chế GOTRACEBACK=crash) để hệ điều hành ghi core dump, chứ không phải một lỗi khác.
Bài 4 (vận dụng thực tế). Sau khi có file core dump và binary, bạn chạy dlv core ./cbin /tmp/core rồi gõ bt. Lệnh này cho bạn thông tin gì, và bạn cần làm tiếp gì để xem đúng dòng code gây lỗi?
Đáp án
bt (backtrace) dựng lại toàn bộ ngăn xếp lời gọi tại thời điểm crash, ví dụ chỉ ra main.rutTien ở main.go:12 được gọi từ main.xuLyGiaoDich ở main.go:18. Để xem đúng dòng và biến tại một điểm trong ngăn xếp đó, dùng lệnh frame <số> để nhảy tới khung tương ứng (ví dụ frame 11 để xem dòng tk.SoDu -= tien), sau đó có thể print các biến tại frame đó (lưu ý một số biến cục bộ có thể bị "optimized out" nếu binary build với tối ưu, muốn xem đầy đủ hơn cần build lại với -gcflags="all=-N -l").
Bài 5 (đánh đổi / rủi ro bảo mật). Một đồng nghiệp đề xuất lưu mọi core dump từ production vào cùng thư mục backup log thông thường để tiện tra cứu về sau. Nêu vấn đề với đề xuất này.
Đáp án
Core dump là ảnh chụp toàn bộ bộ nhớ tiến trình tại thời điểm crash, nên nó chứa mọi thứ đang nằm trong bộ nhớ lúc đó: mật khẩu, token, dữ liệu người dùng, khóa mã hóa. Lưu chung với log/backup thường (nơi thường có quyền truy cập rộng hơn và giữ lâu) là rủi ro bảo mật nghiêm trọng. Core dump production cần đượ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 xong, không để lẫn vào log hay backup thông thường — nhiều nơi thậm chí cấm sinh core trên production vì lý do nà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.