Bài 14.520 phút đọc
Bản đồ bộ nhớ
Sau bài này bạn sẽ làm được
- Vẽ lại đầy đủ bản đồ bộ nhớ của một tiến trình
- Xem bản đồ thật bằng công cụ hệ điều hành
- Đoán được vùng nhớ từ dạng của một địa chỉ
- Hiểu quyền đọc ghi chạy của từng vùng
Bốn bài trước đã đi qua từng vùng riêng. Bài này ghép chúng lại thành một bản đồ hoàn chỉnh, rồi dạy bạn một kỹ năng gỡ lỗi rất thực dụng: nhìn một địa chỉ là đoán được nó thuộc vùng nào, và từ đó đoán được loại lỗi.
#Sáu vùng của một tiến trình
| Vùng | Chứa gì | Ai cấp | Vòng đời |
|---|---|---|---|
| Ngăn xếp | Biến cục bộ, tham số, khung hàm | Trình biên dịch | Theo khối lệnh |
| Vùng cấp phát động | Thứ bạn xin bằng malloc | Bạn | Từ malloc tới free |
| .bss | Biến tĩnh bằng 0 | Hệ điều hành lúc nạp | Suốt chương trình |
| .data | Biến tĩnh khác 0 | Nạp từ file thực thi | Suốt chương trình |
| .rodata | Hằng chuỗi, bảng chỉ đọc | Nạp từ file thực thi | Suốt chương trình |
| .text | Mã máy của mọi hàm | Nạp từ file thực thi | Suốt chương trình |
ban-do.c
#include <stdio.h>
#include <stdlib.h>
int g_data = 1; /* .data */
int g_bss; /* .bss */
const char *g_chuoi = "abc"; /* con trỏ ở .data, nội dung ở .rodata */
int main(void)
{
int cuc_bo = 0;
void *heap = malloc(16);
printf(".text : %p (dia chi ham main)\n", (void *)main);
printf(".rodata : %p (noi dung hang chuoi)\n", (void *)g_chuoi);
printf(".data : %p (bien khac 0)\n", (void *)&g_data);
printf(".bss : %p (bien bang 0)\n", (void *)&g_bss);
printf("heap : %p (malloc)\n", heap);
printf("stack : %p (bien cuc bo)\n", (void *)&cuc_bo);
free(heap);
return 0;
}terminal
./ban-do
.text : 0x5581e4a01189 (dia chi ham main) .rodata : 0x5581e4a02008 (noi dung hang chuoi) .data : 0x5581e4a04010 (bien khac 0) .bss : 0x5581e4a04018 (bien bang 0) heap : 0x5581e57b52a0 (malloc) stack : 0x7ffe3b2c1e44 (bien cuc bo)
#Quyền đọc, ghi và chạy
| Vùng | Đọc | Ghi | Chạy | Vi phạm thì sao |
|---|---|---|---|---|
| .text | Có | Không | Có | Ghi vào mã: tín hiệu phân đoạn |
| .rodata | Có | Không | Không | Ghi vào hằng chuỗi: sập |
| .data và .bss | Có | Có | Không | Chạy dữ liệu: bị chặn |
| Vùng cấp phát động | Có | Có | Không | Chạy dữ liệu: bị chặn |
| Ngăn xếp | Có | Có | Không | Chạy mã trên ngăn xếp: bị chặn |
vi-pham.c
#include <stdio.h>
int main(void)
{
char *hang = "abc"; /* trỏ vào .rodata */
hang[0] = 'X'; /* ghi vào vùng chỉ đọc */
printf("%s\n", hang);
return 0;
}terminal
gcc -std=c17 -Wall -Wwrite-strings vi-pham.c -o t
vi-pham.c:5:18: warning: initialization discards 'const' qualifier from pointer target type [-Wdiscarded-qualifiers]
./t
Segmentation fault (core dumped)
gcc -fsanitize=address -g vi-pham.c -o t && ./t
ERROR: AddressSanitizer: SEGV on unknown address 0x5581e4a02008 The signal is caused by a WRITE memory access.
#Xem bản đồ thật
terminal
# Trên Linux: bản đồ đầy đủ của một tiến trình đang chạy
cat /proc/self/maps
5581e4a00000-5581e4a01000 r--p 00000000 08:02 1234 /usr/bin/cat 5581e4a01000-5581e4a04000 r-xp 00001000 08:02 1234 /usr/bin/cat 5581e4a04000-5581e4a06000 r--p 00004000 08:02 1234 /usr/bin/cat 5581e4a06000-5581e4a07000 rw-p 00005000 08:02 1234 /usr/bin/cat 5581e57b5000-5581e57d6000 rw-p 00000000 00:00 0 [heap] 7f2a1c000000-7f2a1c028000 r--p 00000000 08:02 5678 /usr/lib/libc.so.6 7ffe3b2a1000-7ffe3b2c2000 rw-p 00000000 00:00 0 [stack] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
| Cột | Nghĩa là |
|---|---|
| 5581e4a00000-5581e4a01000 | Khoảng địa chỉ, từ đầu tới ngay sau cuối |
| r-xp | Quyền đọc, ghi, chạy, và p là riêng tư |
| 00001000 | Độ lệch trong file được ánh xạ |
| 08:02 | Thiết bị chứa file |
| 1234 | Số hiệu nút của file |
| /usr/bin/cat | File được ánh xạ, hoặc tên vùng đặc biệt |
terminal
# Trong chương trình của bạn, tự đọc bản đồ của chính mình
man 5 proc
# Trên macOS
vmmap $(pgrep chuong-trinh)
# Trên Windows, dùng công cụ VMMap của Sysinternals
vmmap.exe -p chuong-trinh.exe
# Trong gdb, xem vùng của tiến trình đang gỡ lỗi
(gdb) info proc mappings
tu-doc.c
#include <stdio.h>
#include <string.h>
/* Tìm xem một địa chỉ nằm ở vùng nào, bằng cách đọc bản đồ của chính mình. */
static void tim_vung(const void *dia_chi)
{
FILE *f = fopen("/proc/self/maps", "r");
if (f == NULL) { perror("maps"); return; }
char dong[512];
unsigned long muc_tieu = (unsigned long)dia_chi;
while (fgets(dong, sizeof dong, f) != NULL) {
unsigned long dau, cuoi;
if (sscanf(dong, "%lx-%lx", &dau, &cuoi) == 2
&& muc_tieu >= dau && muc_tieu < cuoi) {
printf("%p nam trong: %s", dia_chi, dong);
break;
}
}
fclose(f);
}#Đọc địa chỉ để đoán loại lỗi
Đây là kỹ năng thực dụng nhất của bài. Khi chương trình sập, thông báo lỗi luôn kèm một địa chỉ. Nhìn dạng của nó là bạn thu hẹp được nguyên nhân ngay lập tức.
| Địa chỉ trông như | Vùng nào | Nguyên nhân nhiều khả năng nhất |
|---|---|---|
| 0x0 hoặc dưới 0x1000 | Không được ánh xạ | Con trỏ rỗng. Thường do quên kiểm tra malloc hoặc fopen |
| Số nhỏ như 0x8 hoặc 0x10 | Không được ánh xạ | Con trỏ rỗng cộng độ lệch trường struct, ví dụ p->truong với p là NULL |
| 0x55... hoặc 0x40... | .text, .rodata, .data | Ghi vào hằng chuỗi, hoặc ghi vào biến const |
| 0x55... cao hơn một chút | Vùng cấp phát động | Dùng sau khi giải phóng, hoặc ghi ra ngoài khối |
| 0x7f... | Thư viện động | Ghi đè dữ liệu của thư viện, thường do tràn bộ đệm |
| 0x7ff... rất cao | Ngăn xếp | Trả về địa chỉ biến cục bộ, hoặc tràn ngăn xếp |
| 0x4141414141414141 | Không được ánh xạ | Đã bị ghi đè bằng dữ liệu. Chuỗi 41 là ký tự A lặp lại |
| Giá trị lẻ như 0x7fff...45 | Bất kỳ | Con trỏ bị lệch, thường do ép kiểu sai hoặc đọc rác |
Quy trình gỡ lỗi khi chương trình sập
terminal
# Bước 1: biên dịch lại với thông tin gỡ lỗi và trình dò lỗi
gcc -std=c17 -Wall -Wextra -g -fsanitize=address,undefined bai.c -o bai
# Bước 2: chạy lại, thường là đã có câu trả lời ngay
./bai
# Bước 3: nếu vẫn chưa rõ, chạy trong gdb
gdb -q ./bai -ex run -ex bt -ex 'info locals'
# Bước 4: bật lưu ảnh bộ nhớ để phân tích sau
ulimit -c unlimited && ./bai && gdb ./bai core
#Thư viện động cũng nằm trong bản đồ
terminal
# Xem chương trình của bạn dùng những thư viện nào
ldd ./chuong-trinh
linux-vdso.so.1 (0x00007ffe3b3f2000) libc.so.6 => /usr/lib/libc.so.6 (0x00007f2a1c000000) /lib64/ld-linux-x86-64.so.2 (0x00007f2a1c2a1000)
| Thành phần | Là gì |
|---|---|
| libc.so.6 | Thư viện chuẩn C, chứa printf, malloc, strlen |
| ld-linux-x86-64.so.2 | Trình nạp động, nó chạy trước cả mã của bạn |
| linux-vdso.so.1 | Thư viện ảo do nhân cung cấp, tăng tốc vài lời gọi hệ thống |
Liên kết tĩnh gộp mọi thứ vào một file
terminal
gcc bai.c -o dong && ls -l dong && ldd dong
-rwxr-xr-x 1 an an 16064 dong libc.so.6 => /usr/lib/libc.so.6
gcc -static bai.c -o tinh && ls -l tinh && ldd tinh
-rwxr-xr-x 1 an an 892456 tinh not a dynamic executable
| Liên kết động | Liên kết tĩnh | |
|---|---|---|
| Kích thước file | Nhỏ, vài chục kilobyte | Lớn, gần một megabyte |
| Bộ nhớ khi nhiều tiến trình chạy | Dùng chung một bản thư viện | Mỗi tiến trình một bản riêng |
| Cập nhật vá lỗi thư viện | Chỉ cần thay file thư viện | Phải biên dịch lại chương trình |
| Thời gian khởi động | Chậm hơn chút, phải nạp và nối | Nhanh hơn |
| Mang sang máy khác | Cần đúng phiên bản thư viện | Chạy được ngay |
Chương 39 sẽ dạy cách tự tạo thư viện tĩnh và thư viện động, cùng cách chọn giữa hai kiểu.
Tự làm thử
- Chạy chương trình in địa chỉ sáu vùng, sắp xếp kết quả theo địa chỉ tăng dần và đối chiếu với bản đồ.
- Đọc
/proc/self/mapstrên máy bạn, đếm xem có bao nhiêu vùng và phân loại chúng. - Cài hàm
tim_vungtrong bài, dùng nó để xác định vùng của năm địa chỉ khác nhau. - Ghi vào hằng chuỗi để gây lỗi, ghi lại địa chỉ trong thông báo và đối chiếu với bảng đoán lỗi.
- Tạo struct nhiều trường, gán qua con trỏ rỗng vào trường thứ ba, xác nhận địa chỉ lỗi bằng đúng độ lệch của trường đó.
- Biên dịch cùng một chương trình theo cả hai kiểu liên kết, so kích thước file và kết quả của
ldd.
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
- Sáu vùng theo thứ tự địa chỉ tăng dần: .text, .rodata, .data, .bss, vùng cấp phát động, rồi ngăn xếp ở rất cao.
- Mỗi vùng có quyền hạn riêng. Vùng mã chạy được nhưng không ghi được, vùng dữ liệu ghi được nhưng không chạy được.
- Trên Linux,
/proc/self/mapscho bạn thấy bản đồ thật với đầy đủ khoảng địa chỉ và quyền hạn. - Nhìn dạng của địa chỉ trong thông báo lỗi là đoán được vùng, và từ đó đoán được loại lỗi.
- Địa chỉ nhỏ bằng đúng độ lệch của một trường struct nghĩa là con trỏ rỗng cộng độ lệch.