Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 14.316 phút đọc

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ùngChứa gìCó trong file thực thi không
.dataBiến tĩnh có giá trị khởi tạo khác 0Có, giá trị được ghi thẳng vào file
.bssBiến tĩnh không khởi tạo hoặc khởi tạo bằng 0Không, chỉ ghi lại cần bao nhiêu byte
.rodataHằng chuỗi, biến const, bảng tra chỉ đọcCó, và được đánh dấu không cho ghi
ba-vung.c
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 */
terminal
gcc -c ba-vung.c && size ba-vung.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ướcAi làmChuyện gì xảy ra
Biên dịchTrình biên dịchGhi vào file: vùng .bss cần bao nhiêu byte
Nạp chương trìnhHệ điều hànhCấp các trang nhớ đã được xóa sạch về 0
Trước khi main chạyMã khởi động của thư việnKhông cần làm gì với .bss, nó đã sạch
Dựa vào biến cục bộ tự bằng 0
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 */
}
Chỉ biến tĩnh mới được bảo đảm
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.

static-ham.c
#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;
}
terminal
./static-ham
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àmstatic int x trong hàm
Nằm ở đâuNgăn xếp.data hoặc .bss
Cấp phát khi nàoMỗi lần vào hàmMột lần lúc nạp chương trình
Giá trị giữa hai lần gọiMấtGiữ nguyên
Giá trị mặc địnhRác0
Nhiều luồng gọi cùng lúcMỗi luồng một bản riêngDùng chung, phải khóa
Đệ quyMỗi tầng một bảnCả 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ệcAi làm
1Hệ điều hành nạp các vùng từ file thực thiNhân hệ điều hành
2Cấp và xóa sạch vùng .bssNhân hệ điều hành
3Nạp và liên kết các thư viện độngTrình nạp động
4Chạy mã khởi động của thư viện chuẩnHàm _start
5Chạy các hàm được đánh dấu constructorMã khởi động
6Gọi mainMã khởi động
7Chạy các hàm đăng ký bằng atexit, theo thứ tự ngượcHàm exit
8Chạy các hàm được đánh dấu destructorMã kết thúc
truoc-main.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;
}
terminal
./truoc-main
chay truoc main
main
atexit
chay sau main

#Đọc kích thước từng vùng

terminal
# Kích thước từng vùng của file thực thi
size ./chuong-trinh
   text    data     bss     dec     hex filename
   2891     628       8    3527     dc7 ./chuong-trinh
# Chi tiết hơn, kèm mọi vùng
size -A ./chuong-trinh | head
section              size      addr
.text                1837      4096
.rodata               128      8192
.data                  16     16384
.bss                    8     16400
CộtNghĩa là
textMã máy cộng dữ liệu chỉ đọc
dataBiến tĩnh có giá trị khởi tạo khác 0
bssBiến tĩnh bằng 0, không chiếm chỗ trong file
decTổng ba cột trên, hệ mười
hexCùng tổng, hệ mười sáu

Thí nghiệm: theo dõi từng vùng thay đổi

terminal
# Thêm mảng 1 MB không khởi tạo
size a.out
   text    data     bss     dec     hex
   1837     628 1000008 1002473   f4ba9
# Đổi thành khởi tạo { 1 }
size a.out
   text    data     bss     dec     hex
   1837 1000636       8 1002481   f4bb1
# Kích thước file thực thi trên đĩa
ls -l a.out
-rwxr-xr-x 1 an an 1017544 a.out      # phình lên gần 1 MB

Tự làm thử

  1. Khai báo bốn biến tĩnh khác nhau, dùng nm để xác định biến nào ở .data và biến nào ở .bss.
  2. 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.
  3. 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ả.
  4. 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.
  5. Viết chương trình có cả hàm constructor, destructor và atexit, xác nhận đúng thứ tự chạy.
  6. 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 ở .data và chiếm chỗ trong file thực thi. Biến bằng 0 nằm ở .bss và 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 static trong 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 sizenm --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.