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:
1 |
$ ulimit -c unlimited |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <cstdlib> #include <stdio.h> void foo() { int a; int *p = &a; *p = 1; printf("*p = %d\n", *p); free(p); } int main() { foo(); return 0; } |
File main.cpp nằm trong folder /home/tuanpm3/linux_debug_tips.
Build ra file chạy và chạy chương trình →
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
$ gcc main.cpp -o test $ ./test *p = 1 *** Error in `./test': free(): invalid pointer: 0x00007ffc156de68c *** ======= Backtrace: ========= /lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7fd163f407e5] /lib/x86_64-linux-gnu/libc.so.6(+0x8037a)[0x7fd163f4937a] /lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7fd163f4d53c] ./test[0x400622] ./test[0x400642] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7fd163ee9830] ./test[0x400509] ======= Memory map: ======== 00400000-00401000 r-xp 00000000 fc:00 58593205 /home/tuanpm3/linux_debug_tips/test 00600000-00601000 r--p 00000000 fc:00 58593205 /home/tuanpm3/linux_debug_tips/test 00601000-00602000 rw-p 00001000 fc:00 58593205 /home/tuanpm3/linux_debug_tips/test 00ff8000-01019000 rw-p 00000000 00:00 0 [heap] 7fd15c000000-7fd15c021000 rw-p 00000000 00:00 0 7fd15c021000-7fd160000000 ---p 00000000 00:00 0 7fd163cb3000-7fd163cc9000 r-xp 00000000 fc:00 8917379 /lib/x86_64-linux-gnu/libgcc_s.so.1 7fd163cc9000-7fd163ec8000 ---p 00016000 fc:00 8917379 /lib/x86_64-linux-gnu/libgcc_s.so.1 7fd163ec8000-7fd163ec9000 rw-p 00015000 fc:00 8917379 /lib/x86_64-linux-gnu/libgcc_s.so.1 7fd163ec9000-7fd164089000 r-xp 00000000 fc:00 8917241 /lib/x86_64-linux-gnu/libc-2.23.so 7fd164089000-7fd164289000 ---p 001c0000 fc:00 8917241 /lib/x86_64-linux-gnu/libc-2.23.so 7fd164289000-7fd16428d000 r--p 001c0000 fc:00 8917241 /lib/x86_64-linux-gnu/libc-2.23.so 7fd16428d000-7fd16428f000 rw-p 001c4000 fc:00 8917241 /lib/x86_64-linux-gnu/libc-2.23.so 7fd16428f000-7fd164293000 rw-p 00000000 00:00 0 7fd164293000-7fd1642b9000 r-xp 00000000 fc:00 8912993 /lib/x86_64-linux-gnu/ld-2.23.so 7fd16449d000-7fd1644a0000 rw-p 00000000 00:00 0 7fd1644b7000-7fd1644b8000 rw-p 00000000 00:00 0 7fd1644b8000-7fd1644b9000 r--p 00025000 fc:00 8912993 /lib/x86_64-linux-gnu/ld-2.23.so 7fd1644b9000-7fd1644ba000 rw-p 00026000 fc:00 8912993 /lib/x86_64-linux-gnu/ld-2.23.so 7fd1644ba000-7fd1644bb000 rw-p 00000000 00:00 0 7ffc156bf000-7ffc156e0000 rw-p 00000000 00:00 0 [stack] 7ffc1575d000-7ffc15760000 r--p 00000000 00:00 0 [vvar] 7ffc15760000-7ffc15762000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] Aborted (core dumped) |
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 →
1 |
$ ulimit -c unlimited |
Nếu chưa install gdb thì chạy lệnh sau để install →
1 |
$ sudo apt-get install gdb -y |
Build lại chương trình ở mode debug →
1 |
$ gcc main.cpp -o test -g |
-g là option dùng để bật chế độ debug với gdb. Chạy lại chương trình →
1 |
$ ./test |
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 →
1 2 |
$ ls core main.cpp test |
Chạy lại chương trình phát nữa sử dụng gdb kết hợp với file “core” →
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$ gdb test core GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1 Copyright (C) 2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from test...done. warning: exec file is newer than core file. [New LWP 15381] Core was generated by `./test'. Program terminated with signal SIGABRT, Aborted. #0 0x00007f63cf521428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54 54 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory. (gdb) |
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 →
1 2 3 4 5 6 7 8 9 10 |
(gdb) backtrace #0 0x00007f16c7478428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54 #1 0x00007f16c747a02a in __GI_abort () at abort.c:89 #2 0x00007f16c74ba7ea in __libc_message (do_abort=do_abort@entry=2, fmt=fmt@entry=0x7f16c75d3ed8 "*** Error in `%s': %s: 0x%s ***\n") at ../sysdeps/posix/libc_fatal.c:175 #3 0x00007f16c74c337a in malloc_printerr (ar_ptr=<optimized out>, ptr=<optimized out>, str=0x7f16c75d0caf "free(): invalid pointer", action=3) at malloc.c:5006 #4 _int_free (av=<optimized out>, p=<optimized out>, have_lock=0) at malloc.c:3867 #5 0x00007f16c74c753c in __GI___libc_free (mem=<optimized out>) at malloc.c:2968 #6 0x0000000000400622 in foo () at main.cpp:10 #7 0x0000000000400642 in main () at main.cpp:15 (gdb) |
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 →
1 |
free(p); |
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) →
1 2 3 4 5 6 7 |
void foo() { int a; int *p = &a; *p = 1; printf("*p = %d\n", *p); } |
Build và chạy lại chương trình →
1 2 3 |
$ gcc main.cpp -o test -g $ ./test *p = 1 |
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) —