Bộ nhớ tĩnh
Sau bài này bạn sẽ làm được
- Phân biệt vùng .data với .bss và biết cái nào chiếm chỗ trong file
- Giải thích vì sao biến tĩnh tự bằng 0
- Biết thứ tự khởi tạo trước khi main chạy
- Dùng lệnh size để đọc kích thước từng vùng
Bộ nhớ tĩnh là vùng thứ ba, bên cạnh ngăn xếp và vùng cấp phát động. Nó được cấp một lần lúc nạp chương trình và không bao giờ thay đổi kích thước. Hiểu nó giải thích được vì sao biến toàn cục tự bằng 0 và vì sao một mảng lớn khai báo static lại không làm sập chương trình.
#Hai vùng: .data và .bss
| Vùng | Chứa gì | Có trong file thực thi không |
|---|---|---|
| .data | Biến tĩnh có giá trị khởi tạo khác 0 | Có, giá trị được ghi thẳng vào file |
| .bss | Biến tĩnh không khởi tạo hoặc khởi tạo bằng 0 | Không, chỉ ghi lại cần bao nhiêu byte |
| .rodata | Hằng chuỗi, biến const, bảng tra chỉ đọc | Có, và được đánh dấu không cho ghi |
int a = 42; /* .data, vì có giá trị khác 0 */
int b = 0; /* .bss, vì giá trị là 0 */
int c; /* .bss, vì không khởi tạo */
const int d = 7; /* .rodata, vì là hằng */
static int e = 100; /* .data, static không đổi vùng */
static int f; /* .bss */
char g[1000000] = { 1 }; /* .data: chiếm 1 MB trong FILE thực thi */
char h[1000000]; /* .bss: file không lớn thêm chút nào */ text data bss dec hex filename
4 1000008 1000012 2000024 1e8498 ba-vung.o#Vì sao biến tĩnh tự bằng 0
Không phải trình biên dịch chăm chỉ sinh mã gán. Chuyện xảy ra ở tầng hệ điều hành, và nó có lý do bảo mật.
| Bước | Ai làm | Chuyện gì xảy ra |
|---|---|---|
| Biên dịch | Trình biên dịch | Ghi vào file: vùng .bss cần bao nhiêu byte |
| Nạp chương trình | Hệ điều hành | Cấp các trang nhớ đã được xóa sạch về 0 |
| Trước khi main chạy | Mã khởi động của thư viện | Không cần làm gì với .bss, nó đã sạch |
void f(void)
{
int dem; /* trên ngăn xếp, chứa rác từ lời gọi trước */
dem++; /* hành vi không xác định */
}int g_dem; /* toàn cục: chắc chắn 0 */
void f(void)
{
static int s_dem; /* static: chắc chắn 0 */
int dem = 0; /* cục bộ: phải tự khởi tạo */
dem++;
}#Biến static trong hàm
Bài 8.9 đã giới thiệu. Ở đây ta nhìn nó từ góc độ bộ nhớ: biến static trong hàm nằm ở .data hoặc .bss chứ không nằm trên ngăn xếp, dù tên của nó chỉ nhìn thấy được trong hàm.
#include <stdio.h>
void f(void)
{
int cuc_bo = 0;
static int tinh = 0;
printf("cuc bo tai %p, tinh tai %p\n",
(void *)&cuc_bo, (void *)&tinh);
++cuc_bo;
++tinh;
printf("cuc bo = %d, tinh = %d\n", cuc_bo, tinh);
}
int main(void)
{
f();
f();
f();
return 0;
}cuc bo tai 0x7ffd8c2a1e44, tinh tai 0x55d3f4a04010 cuc bo = 1, tinh = 1 cuc bo tai 0x7ffd8c2a1e44, tinh tai 0x55d3f4a04010 cuc bo = 1, tinh = 2 cuc bo tai 0x7ffd8c2a1e44, tinh tai 0x55d3f4a04010 cuc bo = 1, tinh = 3
Hai địa chỉ nằm ở hai vùng hoàn toàn khác nhau: biến cục bộ ở khoảng 0x7ffd, tức ngăn xếp, còn biến tĩnh ở khoảng 0x55d3, tức vùng dữ liệu gần mã chương trình.
| int x trong hàm | static int x trong hàm | |
|---|---|---|
| Nằm ở đâu | Ngăn xếp | .data hoặc .bss |
| Cấp phát khi nào | Mỗi lần vào hàm | Một lần lúc nạp chương trình |
| Giá trị giữa hai lần gọi | Mất | Giữ nguyên |
| Giá trị mặc định | Rác | 0 |
| Nhiều luồng gọi cùng lúc | Mỗi luồng một bản riêng | Dùng chung, phải khóa |
| Đệ quy | Mỗi tầng một bản | Cả cây đệ quy dùng chung một bản |
Giá trị khởi tạo phải là hằng
void f(int x)
{
static int a = 5; /* hợp lệ */
static int b = 2 * 3 + 1; /* hợp lệ, tính được lúc biên dịch */
static int c = sizeof(int);/* hợp lệ */
static int d = x; /* LỖI: initializer element is not constant */
}Lý do đã rõ từ mục trên: giá trị đó được ghi thẳng vào file thực thi lúc biên dịch, chứ không có lệnh gán nào chạy lúc thi hành. Mà lúc biên dịch thì x chưa tồn tại.
#Thứ tự khởi tạo trước khi main chạy
| Thứ tự | Việc | Ai làm |
|---|---|---|
| 1 | Hệ điều hành nạp các vùng từ file thực thi | Nhân hệ điều hành |
| 2 | Cấp và xóa sạch vùng .bss | Nhân hệ điều hành |
| 3 | Nạp và liên kết các thư viện động | Trình nạp động |
| 4 | Chạy mã khởi động của thư viện chuẩn | Hàm _start |
| 5 | Chạy các hàm được đánh dấu constructor | Mã khởi động |
| 6 | Gọi main | Mã khởi động |
| 7 | Chạy các hàm đăng ký bằng atexit, theo thứ tự ngược | Hàm exit |
| 8 | Chạy các hàm được đánh dấu destructor | Mã kết thúc |
#include <stdio.h>
#include <stdlib.h>
__attribute__((constructor))
static void truoc(void)
{
printf("chay truoc main\n");
}
__attribute__((destructor))
static void sau(void)
{
printf("chay sau main\n");
}
static void khi_thoat(void)
{
printf("atexit\n");
}
int main(void)
{
atexit(khi_thoat);
printf("main\n");
return 0;
}chay truoc main main atexit chay sau main
#Đọc kích thước từng vùng
text data bss dec hex filename 2891 628 8 3527 dc7 ./chuong-trinh
section size addr .text 1837 4096 .rodata 128 8192 .data 16 16384 .bss 8 16400
| Cột | Nghĩa là |
|---|---|
| text | Mã máy cộng dữ liệu chỉ đọc |
| data | Biến tĩnh có giá trị khởi tạo khác 0 |
| bss | Biến tĩnh bằng 0, không chiếm chỗ trong file |
| dec | Tổng ba cột trên, hệ mười |
| hex | Cùng tổng, hệ mười sáu |
Thí nghiệm: theo dõi từng vùng thay đổi
text data bss dec hex 1837 628 1000008 1002473 f4ba9
text data bss dec hex 1837 1000636 8 1002481 f4bb1
-rwxr-xr-x 1 an an 1017544 a.out # phình lên gần 1 MB
Tự làm thử
- Khai báo bốn biến tĩnh khác nhau, dùng
nmđể xác định biến nào ở.datavà biến nào ở.bss. - Khai báo mảng một megabyte theo ba cách trong bài, so sánh kích thước file thực thi và kết quả của
size. - In địa chỉ của một biến cục bộ và một biến static trong cùng một hàm, gọi ba lần và giải thích kết quả.
- Cài hàm đếm nút của cây bằng biến static, gọi hai lần và quan sát kết quả sai ở lần thứ hai.
- Viết chương trình có cả hàm constructor, destructor và
atexit, xác nhận đúng thứ tự chạy. - Cài mẫu khởi tạo trễ cho một bảng tra, đo thời gian lần gọi đầu tiên và lần gọi thứ hai.
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
- Biến tĩnh khác 0 nằm ở
.datavà chiếm chỗ trong file thực thi. Biến bằng 0 nằm ở.bssvà chỉ chiếm một con số. - Biến tĩnh tự bằng 0 vì hệ điều hành bắt buộc phải xóa sạch mọi trang nhớ mới, và đó là quy định bảo mật.
- Biến
statictrong hàm nằm ở vùng tĩnh chứ không trên ngăn xếp, nên nó dùng chung cho mọi lời gọi kể cả trong đệ quy. - Giá trị khởi tạo của biến tĩnh phải là hằng biết được lúc biên dịch.
- Dùng
sizevànm --size-sort -Sđể biết chương trình đang chiếm bao nhiêu bộ nhớ tĩnh và biến nào chiếm nhiều nhất.