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.

Ảnh chụp đoạn mã Go nền tối minh hoạ core dump và phân tích sự cố trong Go mổ xẻ tiến trình đã chết, vì sao panic trace mất trạng thái core dump giữ lại khi crash panic trace cho biết ở đâu nhưng mất trạng thái giá trị biến nội dung struct map lúc đó biến mất theo tiến trình core dump bằng ảnh chụp toàn bộ bộ nhớ tiến trình lúc chết mổ xẻ nó post-mortem bằng dlv core soi stack cộng biến dù đã chết, code có bug nil pointer func rutTien tk trỏ TaiKhoan tien int int tk.SoDu trừ bằng tien BUG tk có thể nil crash SIGSEGV return tk.SoDu func xuLyGiaoDich ds map string trỏ TaiKhoan ten string tien int tk bằng ds ten binh không có trong map tk bằng nil con bằng rutTien tk tien crash tại đây, bật sinh core dump khi crash ulimit c unlimited cho phép sinh core dump GOTRACEBACK crash chuongtrinh Go sinh core khi panic GOTRACEBACK crash thay vì exit sạch Go gửi SIGABRT cho chính nó HĐH ghi core dump ảnh chụp bộ nhớ ra file core, mổ xẻ core dump bằng dlv core dlv core chuongtrinh core nạp binary cộng core dump bt backtrace toàn bộ ngăn xếp lúc crash frame 11 chuyển tới frame của hàm ứng dụng print tk xem biến tại frame đó nếu còn goroutines mọi goroutine lúc crash tìm deadlock rò rỉ

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.)

Ảnh chụp bảng kết quả đo thật nền tối core dump và phân tích sự cố trong Go chạy bằng GOTRACEBACK crash cộng ulimit cộng dlv core Go 1.23 arm64, 1 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 bằng 128 cộng 6 SIGABRT Go tự gửi để sinh core ls la tmp core 40325120 tmp core core dump 40 MB ảnh chụp bộ nhớ, 2 dlv core dựng lại ngăn xếp đầy đủ lúc crash dlv core ./cbin tmp core bt 11 0xb3be8 in main rutTien at work t128 main.go 12 13 0xb3c74 in main xuLyGiaoDich at work t128 main.go 18 đúng nơi crash rutTien dòng 12 tk.SoDu gọi từ xuLyGiaoDich 18, 3 điều hướng frame tới đúng dòng lỗi frame 13 dòng 18 con bằng rutTien tk tien ds ten trả nil frame 11 dòng 12 tk.SoDu trừ bằng tien nil dereference crash từ core dump dựng lại đường đi tới lỗi dù tiến trình đã chết build crash tối ưu một số biến có thể optimized out báo trung thực, cốt lõi vì sao panic trace mất trạng thái core dump giữ toàn bộ bộ nhớ bật ulimit c unlimited cộng GOTRACEBACK crash sinh core khi panic phân tích dlv core binary core bt frame print goroutines đo thật core 40 MB dlv dựng stack tới main rutTien 12 nơi crash dùng khi crash hiếm khó tái hiện điều tra sau sự cố post-mortem đánh đổi core dump lớn cộng chứa bí mật trong bộ nhớ xử lý cẩn thận

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ề

  1. 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ớ.
  2. dlv core mổ xẻ tiến trình đã chết: đo thật, bt dựng lại ngăn xếp tới đúng main.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).
  3. 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.