Bài 34.528 phút đọc
Năm nguyên nhân của lỗi phân đoạn
Sau bài này bạn sẽ làm được
- Nhận ra nguyên nhân từ triệu chứng và từ vết gọi
- Bật core dump và mở nó bằng GDB
- Giải thích vì sao ghi ngoài biên gần thường không sập
- Biết vì sao tràn ngăn xếp cho vết gọi dài bất thường
Lỗi phân đoạn nghĩa là chương trình chạm vào một trang nhớ mà hệ điều hành không cho phép. Chỉ có năm nguyên nhân thật sự, và mỗi nguyên nhân có một dấu vết riêng trong backtrace giúp bạn nhận ra ngay.
#Năm nguyên nhân
| Nguyên nhân | Dấu hiệu trong GDB | Công cụ tìm nhanh nhất |
|---|---|---|
| Con trỏ NULL | Tham số hoặc biến hiện giá trị 0x0 | GDB, thấy ngay ở dòng đầu |
| Ngoài biên xa | Địa chỉ trông vô lý, ví dụ 0x7f3a hoặc số rất lớn | ASan |
| Ghi vào vùng chỉ đọc | Địa chỉ nằm trong .rodata, và lệnh là lệnh ghi | Cờ -Wwrite-strings lúc dịch |
| Tràn ngăn xếp | Backtrace dài hàng nghìn khung, cùng một hàm lặp lại | GDB, nhìn là biết |
| Dùng sau khi giải phóng | Địa chỉ hợp lý nhưng dữ liệu là rác | ASan |
#Một: con trỏ NULL
sap.c
#include <stdio.h>
typedef struct { int gia_tri; } Muc;
static Muc *tim(Muc *ds, int n, int can) {
for (int i = 0; i < n; ++i)
if (ds[i].gia_tri == can) return &ds[i];
return NULL;
}
static void in_muc(Muc *m) {
printf("gia tri = %d\n", m->gia_tri); /* khong kiem tra NULL */
}
int main(void) {
Muc ds[3] = { { 1 }, { 2 }, { 3 } };
in_muc(tim(ds, 3, 2));
in_muc(tim(ds, 3, 99)); /* tra ve NULL */
return 0;
}terminal
gcc -std=c11 -g3 -O0 -o sap.exe sap.c && ./sap.exe
Segmentation fault
echo $?
139
terminal
gdb -q -batch -x g4.gdb ./sap.exe
Thread 1 received signal SIGSEGV, Segmentation fault.
0x00000000004015c4 in in_muc (m=0x0) at sap.c:14
14 printf("gia tri = %d\n", m->gia_tri);
#0 0x00000000004015c4 in in_muc (m=0x0) at sap.c:14
#1 0x000000000040163b in main () at sap.c:21/* Ba cach chua, theo thu tu uu tien: */
/* 1. Kiem tra o noi goi, va xu ly truong hop khong tim thay */
Muc *m = tim(ds, 3, 99);
if (m == NULL) {
fprintf(stderr, "khong tim thay\n");
return 1;
}
in_muc(m);
/* 2. Kiem tra trong ham, neu NULL la dau vao hop le */
static void in_muc(const Muc *m) {
if (m == NULL) { printf("(khong co)\n"); return; }
printf("gia tri = %d\n", m->gia_tri);
}
/* 3. Khang dinh, neu NULL la loi lap trinh chu khong phai dau vao hop le */
#include <assert.h>
static void in_muc(const Muc *m) {
assert(m != NULL); /* sap voi thong bao ro rang, o ban go loi */
printf("gia tri = %d\n", m->gia_tri);
}
/* Cach 3 KHONG thay the cach 1: assert bien mat khi dinh nghia NDEBUG. */#Hai: ngoài biên xa
int a[5];
a[5] = 1; /* thuong KHONG sap: van trong trang nho cua ngan xep */
a[1000] = 1; /* co the sap */
a[1000000] = 1; /* gan nhu chac chan sap */#Ba: ghi vào vùng chỉ đọc
terminal
gcc -std=c11 -O2 -Wall -Wextra -o lit lit.c && ./lit
truoc: abc Segmentation fault
gcc -std=c11 -O2 -c lit.c -o lit.o && objdump -h lit.o
0 .text 00000000 ... 3 .rdata 00000020 ... 4 .text.startup 00000050 ... 7 .rdata$zzz 00000040 ...
char *s = "abc";
s[0] = 'A'; /* chuoi hang nam trong .rdata, tuc trang chi doc */
/* Dau hieu nhan ra trong GDB:
- lenh la lenh GHI
- dia chi hop ly va nam trong vung ma cua chuong trinh
- "info proc mappings" cho thay trang do khong co quyen w
Cach chua: bat -Wwrite-strings, no bat duoc ngay luc dich. */terminal
gcc -std=c11 -O2 -Wwrite-strings -c lit.c
lit.c:4:15: warning: initialization discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
char *s = "abc";
^~~~~#Bốn: tràn ngăn xếp
tran_stack.c
#include <stdio.h>
static int sau(int n) {
char dem[1024];
dem[0] = (char)n;
return sau(n + 1) + dem[0]; /* khong co diem dung */
}
int main(void) {
printf("bat dau\n");
fflush(stdout);
printf("%d\n", sau(0));
return 0;
}terminal
gcc -std=c11 -g3 -O0 -o ts.exe tran_stack.c && ./ts.exe
bat dau Segmentation fault
gdb -q -batch -x g10.gdb ./ts.exe
Thread 1 received signal SIGSEGV, Segmentation fault. sau (n=1940) at tran_stack.c:5 5 dem[0] = (char)n; #1934 0x000000000040157f in sau (n=6) at tran_stack.c:7 #1935 0x000000000040157f in sau (n=5) at tran_stack.c:7 #1936 0x000000000040157f in sau (n=4) at tran_stack.c:7 #1937 0x000000000040157f in sau (n=3) at tran_stack.c:7 #1938 0x000000000040157f in sau (n=2) at tran_stack.c:7 #1939 0x000000000040157f in sau (n=1) at tran_stack.c:7 #1940 0x000000000040157f in sau (n=0) at tran_stack.c:7 #1941 0x00000000004015cc in main () at tran_stack.c:14
#Năm: dùng sau khi giải phóng
terminal
gcc -std=c11 -O1 -Wall -Wextra -o heap.exe heap.c && ./heap.exe
truoc free : 78 69 6E 63 68 61 6F 21 sau free : 20 68 C0 00 00 00 00 00 q moi : 41 41 41 41 41 41 41 41 p doc lai : 41 41 41 41 41 41 41 41 p = 0000000000C024E0, q = 0000000000C024E0, trung nhau: CO qua duoc double free
/* Ba thoi quen phong ngua: */
/* 1. Gan NULL ngay sau free */
free(p);
p = NULL; /* free(NULL) hop le, nen double free thanh vo hai.
Va *p sau do sap NGAY, de tim hon nhieu. */
/* Dong goi thanh macro: */
#define GIAI_PHONG(p) do { free(p); (p) = NULL; } while (0)
/* 2. Quy uoc so huu ro rang trong ten ham */
SinhVien *sv_tao(void); /* tra ve: nguoi goi so huu, phai huy */
void sv_huy(SinhVien *sv); /* nhan: ham nay so huu, se giai phong */
const char *sv_ten(const SinhVien *sv); /* muon: KHONG duoc giai phong */
/* 3. Mot chu so huu cho moi khoi, ghi vao tai lieu cua header */#Mở core dump
# Tren Linux
ulimit -c unlimited # cho phep sinh core dump
./prog # Segmentation fault (core dumped)
# Tim tep core: tuy cau hinh he thong
cat /proc/sys/kernel/core_pattern
ls core* # hoac coredumpctl list, neu dung systemd
gdb ./prog core
(gdb) bt full # vet goi kem bien cuc bo moi khung
(gdb) frame 1
(gdb) info locals
(gdb) print *p
# Voi systemd:
coredumpctl list
coredumpctl gdb 12345 # mo core cua tien trinh 12345Tự làm thử
- Viết chương trình gây segfault theo cả năm nguyên nhân và mở từng cái bằng GDB.
- Xác nhận dòng
printftrước chỗ sập không in ra, rồi thêmfflushvà chạy lại. - Đếm số khung trong backtrace của chương trình đệ quy vô hạn và tính ra kích thước ngăn xếp.
- Bật
-Wstack-usage=4096trên một dự án và tìm hàm ngốn ngăn xếp nhất. - Chứng minh
malloctrả lại đúng khối vừafree. - Bật core dump trên Linux, gây sập, rồi mở tệp core bằng GDB.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Năm nguyên nhân: NULL, ngoài biên xa, ghi vào vùng chỉ đọc, tràn ngăn xếp, dùng sau khi giải phóng.
- GDB in giá trị tham số ngay trên dòng đầu của mỗi khung, nên
btthường đủ để nhận ra lỗi NULL. - Ngoài biên gần nguy hiểm hơn ngoài biên xa vì nó không sập và không cảnh báo.
- Backtrace 1941 khung với cùng một hàm lặp lại là dấu hiệu tràn ngăn xếp không thể nhầm.
- Bộ đệm
stdoutmất khi chương trình bị giết, nên dòngprintfcuối cùng không định vị được lỗi.