Bộ nhớ, địa chỉ và giá trị
Sau bài này bạn sẽ làm được
- Mô tả bộ nhớ như một dãy byte có địa chỉ
- Phân biệt địa chỉ với giá trị chứa tại địa chỉ đó
- Giải thích vì sao địa chỉ ảo khác nhau giữa các lần chạy
- Đọc được bản đồ bộ nhớ của tiến trình
Con trỏ là chỗ nhiều người bỏ cuộc với C. Lý do gần như luôn là bắt đầu từ cú pháp thay vì từ bộ nhớ. Bài này không có một dấu sao nào. Nó chỉ xây cho bạn mô hình bộ nhớ đúng, và khi mô hình đó rõ, cú pháp ở các bài sau trở thành chuyện nhỏ.
#Mô hình bộ nhớ phẳng
địa chỉ: 0 1 2 3 4 5 6 7 ...
nội dung: [??] [2A] [00] [00] [00] [??] [??] [??]| Tính chất | Chi tiết |
|---|---|
| Đơn vị nhỏ nhất có địa chỉ | Một byte, tức tám bit |
| Địa chỉ là số nguyên | Trên máy sáu tư bit, rộng 64 bit, tức 8 byte |
| Số ô tối đa về lý thuyết | Hai mũ sáu tư, khoảng 18 tỷ tỷ, nhiều hơn mọi máy thật |
| Ô nhớ không có kiểu | Mỗi ô chỉ là tám bit. Kiểu là do trình biên dịch áp lên |
| Một biến chiếm nhiều ô liền nhau | Số ô đúng bằng sizeof của kiểu |
#Địa chỉ và giá trị
Đây là phân biệt quan trọng nhất của cả chương. Mỗi biến có hai thứ hoàn toàn khác nhau gắn với nó.
| Địa chỉ | Giá trị | |
|---|---|---|
| Là gì | Vị trí trong bộ nhớ | Nội dung tại vị trí đó |
| Lấy thế nào | Toán tử và, viết là &x | Viết thẳng tên biến, x |
| Đổi được không | Không, trong suốt vòng đời biến | Có |
| Kích thước | 8 byte trên máy sáu tư bit | Bằng sizeof của kiểu |
| Ví dụ | 0x7ffd3a10 | 42 |
#include <stdio.h>
int main(void)
{
int n = 42;
double d = 3.14;
char c = 'A';
printf("n: gia tri %d, dia chi %p, rong %zu byte\n",
n, (void *)&n, sizeof n);
printf("d: gia tri %.2f, dia chi %p, rong %zu byte\n",
d, (void *)&d, sizeof d);
printf("c: gia tri %c, dia chi %p, rong %zu byte\n",
c, (void *)&c, sizeof c);
return 0;
}n: gia tri 42, dia chi 0x7ffd3a2c1e4c, rong 4 byte d: gia tri 3.14, dia chi 0x7ffd3a2c1e40, rong 8 byte c: gia tri A, dia chi 0x7ffd3a2c1e3f, rong 1 byte
Một biến nhiều byte thì địa chỉ là byte nào
Địa chỉ của một biến là địa chỉ của byte đầu tiên nó chiếm. Kiểu quyết định số byte tiếp theo cũng thuộc về nó. Đó là toàn bộ ý nghĩa của việc con trỏ có kiểu.
double d; /* chiếm 8 byte, từ &d tới &d + 7 */
char c; /* chiếm 1 byte, đúng tại &c */#Địa chỉ ảo
Địa chỉ bạn thấy trong chương trình không phải địa chỉ trên thanh nhớ thật. Hệ điều hành cho mỗi tiến trình một không gian địa chỉ riêng, gọi là địa chỉ ảo, rồi tự dịch sang địa chỉ vật lý.
| Lợi ích của địa chỉ ảo | Giải thích |
|---|---|
| Cách ly tiến trình | Chương trình của bạn không đọc được bộ nhớ chương trình khác |
| Mỗi tiến trình thấy bộ nhớ liên tục | Dù bộ nhớ vật lý bị phân mảnh khắp nơi |
| Bảo vệ theo vùng | Vùng mã chỉ đọc và chạy được, vùng dữ liệu không chạy được |
| Bộ nhớ ảo lớn hơn bộ nhớ thật | Phần ít dùng được đẩy ra đĩa |
n: dia chi 0x7ffd3a2c1e4c n: dia chi 0x7ffe91b40dcc n: dia chi 0x7ffc5e8a2f9c
#Bản đồ bộ nhớ của tiến trình
ma ham : 0x55d3f4a01169 hang chuoi : 0x55d3f4a02004 bien toan cuc : 0x55d3f4a04010 cap phat dong : 0x55d3f52b62a0 bien cuc bo : 0x7ffd3a2c1e4c
#include <stdio.h>
#include <stdlib.h>
int g_toan_cuc = 1;
int main(void)
{
int cuc_bo = 2;
char *heap = malloc(16);
printf("ma ham : %p\n", (void *)main);
printf("hang chuoi : %p\n", (void *)"xin chao");
printf("bien toan cuc : %p\n", (void *)&g_toan_cuc);
printf("cap phat dong : %p\n", (void *)heap);
printf("bien cuc bo : %p\n", (void *)&cuc_bo);
free(heap);
return 0;
}55d3f4a00000-55d3f4a01000 r--p /usr/bin/cat 55d3f4a01000-55d3f4a05000 r-xp /usr/bin/cat 55d3f4a05000-55d3f4a07000 r--p /usr/bin/cat 55d3f52b6000-55d3f52d7000 rw-p [heap] 7ffd3a2ab000-7ffd3a2cc000 rw-p [stack]
Cột quyền hạn rất đáng chú ý: r-xp nghĩa là đọc và chạy được nhưng không ghi được, đó là vùng mã. rw-p nghĩa là đọc ghi được nhưng không chạy được. Sự tách bạch này là một lớp bảo vệ quan trọng chống lại việc chạy mã do kẻ tấn công đưa vào.
#Vì sao con trỏ là tất yếu
Nếu địa chỉ chỉ là số, thì con trỏ chỉ là biến chứa số. Câu hỏi thật là: vì sao ta cần biến như vậy. Có năm lý do, và mỗi lý do sẽ thành một chương sau này.
- Hàm sửa được dữ liệu của người gọi. C truyền theo giá trị, nên cách duy nhất là đưa cho hàm địa chỉ. Bài 12.6.
- Không phải sao chép dữ liệu lớn. Truyền địa chỉ của một struct một megabyte tốn 8 byte thay vì một triệu. Bài 12.5.
- Cấp phát bộ nhớ lúc chạy. Khi chưa biết trước cần bao nhiêu,
malloctrả về một địa chỉ. Chương 14. - Cấu trúc dữ liệu động. Danh sách liên kết và cây được nối với nhau bằng con trỏ. Chương 21 tới 24.
- Truyền hành vi như dữ liệu. Con trỏ hàm cho phép quyết định gọi hàm nào lúc chạy. Bài 13.5.
Tự làm thử
- In địa chỉ và kích thước của năm biến kiểu khác nhau, sắp xếp kết quả theo địa chỉ tăng dần.
- Chạy chương trình in địa chỉ năm lần liên tiếp, ghi lại chênh lệch và giải thích.
- Viết chương trình in địa chỉ của hàm, hằng chuỗi, biến toàn cục, vùng cấp phát động và biến cục bộ. Sắp xếp theo địa chỉ.
- Dùng
memcpyđể xem bốn byte thô của một sốint, thử với các giá trị 1, 256, 65536 và âm một. - Trên Linux, đọc
/proc/self/mapstừ trong chương trình của bạn và in ra vùng nào chứa biến cục bộ. - Khai báo mảng
int a[3], in địa chỉ ba phần tử và xác nhận chúng cách nhau đúng bốn byte.
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
- Bộ nhớ là một dãy byte, mỗi byte có một địa chỉ là số nguyên đếm từ 0.
- Địa chỉ và giá trị là hai thứ khác nhau. Địa chỉ không đổi trong vòng đời biến, giá trị thì đổi được.
- Địa chỉ của một biến là địa chỉ byte đầu tiên nó chiếm, và kiểu quyết định nó chiếm mấy byte.
- Địa chỉ bạn thấy là địa chỉ ảo, đổi mỗi lần chạy do cơ chế ngẫu nhiên hóa bố cục.
- Nhìn dạng của một địa chỉ thường đoán được nó thuộc vùng nào, và từ đó đoán được loại lỗi. Ép về
(void *)khi in bằng%p.