Phương pháp debug khi chương trình bị Segmentation fault, Core dumped trên Linux

    Một trong số các vấn đề làm đau đầu các developer khi lập trình trên Linux là điều tra các lỗi chết chương trình bất thình lình – crash. Rất nhiều anh em dev tỏ ra khá hoảng loạn và lúng túng khi bỗng dưng chương trình lăn ra chết với thông báo kiểu như thế này.

Segmentation fault hoặc Aborted (core dumped)

Trong bài này mình muốn chia sẻ với anh em một số kỹ năng debug để đối mặt với các lỗi chết chương trình như thế này. Bình tĩnh, tự tin và “giết bug”.

 “Segmentation fault” là gì ?

Khi chương trình của chúng ta chạy, nó truy cập đến các phần khác nhau của bộ nhớ. Đầu tiên, chúng ta có các biến local nằm trong “stack”. Thứ hai, chúng ta cũng có thể có các vùng nhớ được cấp phát trong quá trình chạy (runtime) sử dụng malloc/calloc/realloc (trong C), new (trong C++) và nằm trong “heap”. Chương trình chỉ được phép truy cập đến vùng nhớ thuộc quyền quản lý của nó mà thôi. Bất cứ truy cập vào vùng nhớ nào nằm phạm vi cho phép của chương trình sẽ dẫn đến lỗi “Segmentation fault”.

Có 5 lỗi phổ biến dẫn đến lỗi “segmentation fault” đó là

  • Dereferencing con trỏ NULL
  • Dereferencing con trỏ chưa được khởi tạo
  • Dereferencing con trỏ đã bị free hoặc delete
  • Ghi giá trị vượt quá giới hạn của mảng
  • Hàm đệ quy sử dụng hết vùng bộ dành cho stack – còn gọi là “stack overflow”

“Core dump” là gì ?

Bất cứ khi nào một ứng dụng gặp sự cố gây ra crash (gọi nôm nà là chết chương trình), hệ điều hành sẽ lưu trữ (hoặc gửi) báo cáo về lỗi đó. Trên Windows, chúng ta sẽ nhận được một hộp thoại thông báo lỗi và chúng ta có thể click vào button [Debug] để debug lỗi (với điều kiện có source code và app được biên dịch ở chế độ debug).

Trên Linux, bất cứ khi nào một ứng dụng bị crash (thông thường nhất là gây ra bởi “Segmentation fault”), nó có tùy chọn tạo ra một file lưu vết lỗi gọi là “core dump” (trong hầu hết các trường hợp, cài đặt mặc định của Linux sẽ tắt tính năng này). Core dump là một file lưu lại trạng thái của chương trình tại thời điểm mà nó chết. Nó cũng là bản sao lưu lại tất cả các vùng bộ nhớ ảo đã được truy cập bởi chương trình.

Làm thế nào bật tính năng tạo file “core dump” khi app crash, và file này nằm ở đâu ?

Việc này phụ thuộc vào bản phân phối và cấu hình con Linux của bạn. Để cho đơn giản thì ở đây mình lấy ví dụ trên Ubuntu desktop. Các phiên bản khác của Linux cũng tương tự thôi, anh em có thể search thêm trên google. Để bật tính năng tự động tạo file core dump, chúng ta cần phải cho Linux biết kích thước cho phép của file core dump là bao nhiêu. Sử dụng lệnh ulimit để thiết lập:

Theo mặc định, giá trị này là 0, đó là lý do tại file core dump không được tạo ra theo mặc định. Việc chạy dòng lệnh ulimit trong một Terminal sẽ cho phép tạo file core dump cho phiên Terminal đó. Tham số unlimited có nghĩa là không hạn chế kích thước của file core dump. Bây giờ, nếu có chương trình bị tèo, bạn hãy chạy ứng dụng đó trong phiên Terminal này và chờ nó tèo.

Ví dụ có chương trình đơn giản như sau, file main.cpp

File main.cpp nằm trong folder /home/tuanpm3/linux_debug_tips.

Build ra file chạy và chạy chương trình →

Như vậy chương trình đã bị “core dumped”. Để tìm ra nguyên nhân gây ra lỗi này, chúng ta sẽ làm như sau.

Đầu tiên là chạy lệnh ulimit →

Nếu chưa install gdb thì chạy lệnh sau để install →

Build lại chương trình ở mode debug →

-g là option dùng để bật chế độ debug với gdb. Chạy lại chương trình →

Chương trình vẫn tèo giống lúc đầu nhưng bây giờ sẽ có thêm file tên là “core” được tạo ra và nằm trong directory hiện tại của terminal. Tức là trong trường hợp này /home/tuanpm3/linux_debug_tips sẽ chứa file core lưu thông tin về trạng thái của chương trình tại thời điểm mà nó chết. Dùng lệnh ls trong /home/tuanpm3/linux_debug_tips để kiểm tra có file “core” không →

Chạy lại chương trình phát nữa sử dụng gdb kết hợp với file “core” →

Thông tin ở frame #0 có vẻ như chưa có gì hữu ích lắm, đó là do dòng code trực tiếp dẫn đến tèo chương trình là dòng code nằm trong thư viện chứ không phải dòng code nằm trong code logic của chương trình. Tuy nhiên, chắc chắn nguyên nhân gốc nằm ở code logic của chương trình. Trong trường hợp này hãy dùng lệnh “backtrace” của gdb để xem thêm các frame khác trong callstack →

Trong callstack thì các frame được thực thi trước sẽ ở bên dưới và ngược lại. Vì vậy hãy nhìn từ dưới lên trên để xem frame cuối cùng thuộc phạm vi source code của mình (chưa đi vào hàm trong thư viện) là frame nào. Trong ví dụ này là frame #6 và line code có vấn đề là line số 10 nằm trong file main.cpp.

Soi lại file main.cpp ta thấy rằng line 10 là dòng code sau →

hàm free ở đây là hàm thư viện của C, chắc chắn là hàm này là chuẩn rồi, không có vấn đề gì với hàm này, như vậy vấn đề ở đây là tham số truyền vào cho hàm free. Tham số truyền vào hàm free đang là p – địa chỉ của biến a, mà biến a là biến local nằm trong stack, vùng nhớ của a sẽ tự động được giải phóng khi hàm foo() kết thúc chứ không thể giải phóng bằng hàm free được. Sửa lại hàm foo như sau (xóa lệnh gọi hàm free đi) →

Build và chạy lại chương trình →

Done. Bug đã bị tiêu diệt đẹp 😀

Lời kết

Trên đây chỉ là một vài khái niệm và ví dụ nhỏ để giới thiệu cho anh em hướng tiếp cận và phương pháp debug khi gặp lỗi Segmentation fault, Core dumped trên Linux. Trên thực tế các lỗi gặp phải sẽ muôn hình vạn trạng, thiên biến vạn hóa và khó lường hơn rất nhiều. Tuy nhiên, các tools toys sử dụng để debug và quy trình debug cũng sẽ tương tự như vậy.

— Phạm Minh Tuấn (Shun) —