← Lập trình C nâng cao

Bài 6 · Nâng cao · 24 phút· Cập nhật 11/06/2026

Bố cục bộ nhớ (Stack/Heap/Data)

Biên soạn bởi Nguyễn Anh Tuấn

Bố cục bộ nhớ chương trình C (memory layout): vùng text, data, bss, stack và heap - mô phỏng chạy từng dòng code và xem biến rơi vào vùng nhớ nào.

Khi chạy, bộ nhớ của một chương trình C được chia thành các vùng riêng, mỗi vùng một nhiệm vụ. Cùng một dòng khai báo, biến sẽ “rơi” vào vùng khác nhau tuỳ nó là gì:

cac-vung.c

#include <stdio.h>

int g_init = 42;     // .data  (global, da khoi tao)
int g_zero;          // .bss   (global, chua khoi tao -> 0)

int main() {         // .text  (ma lenh)
    int x = 5;       // stack  (bien cuc bo)
    printf("%d %d %d\n", g_init, g_zero, x);
    return 0;
}

Kết quả khi chạy

42 0 5
  • .text - mã lệnh & dữ liệu chỉ đọc (gồm chuỗi hằng).
  • .data / .bss - biến global/static (đã / chưa khởi tạo).
  • Stack - khung hàm & biến cục bộ (tự động).
  • Heap - vùng cấp phát động bằng malloc (thủ công).

Đây là phần thú vị nhất. Chọn một ví dụ (xếp từ đơn giản đến phức tạp), rồi bấm Bước tiếp / Lùi để chạy từng dòng và quan sát mỗi vùng bộ nhớ thay đổi: biến cục bộ vào stack, khung hàm được đẩy/pop, malloc tạo khối trên heap, ghi vào .bss… Bấm thẳng vào một dòng mã để nhảy tới bước đó:

.data, .bss và một biến cục bộ trên stack - không hàm, không heap.

Bước 1/4
chuong-trinh.c
▲ địa chỉ cao
Stack khung hàm · biến cục bộ · tham số
- stack trống -
↓ lớn dần xuống↑ lớn dần lên
- heap trống -
Heap cấp phát động (malloc / free)
Data segment
.bss - global/static chưa khởi tạo (mặc định 0)
int g0= 0
.data - global/static đã khởi tạo
int g= 42
Text segment
.text - mã lệnh & dữ liệu chỉ đọc
mã: main()chỉ đọc
▼ địa chỉ thấp
// chương trình được nạp - Trước khi main chạy: g ở .data (= 42), g0 ở .bss (= 0), mã lệnh ở .text.
  • Ví dụ 1 - Toàn cục & cục bộ: thấy .data, .bss và stack.
  • Ví dụ 2 - Gọi hàm: khung được đẩy lên rồi pop khi trả về.
  • Ví dụ 3 - Heap: malloc xin và free trả bộ nhớ động.
  • Ví dụ 4 - Đệ quy: nhiều khung chồng nhau rồi tháo ngược (LIFO).
  • Ví dụ 5 - Đủ mọi vùng: gộp tất cả lại trong một chương trình.

Đọc sơ đồ

Địa chỉ cao ở trên (Stack), thấp ở dưới (Text). Stack lớn dần xuống, heap lớn dần lên - hai bên tiến về khoảng trống ở giữa. Ô sáng xanh là thứ vừa thay đổi ở bước hiện tại.

Ba vùng này được dựng trước khi main chạy và tồn tại suốt chương trình:

  • .text: mã máy + chuỗi hằng - CHỈ ĐỌC (ghi vào → segfault). Vì thế sửa string literal là sai.
  • .data: global/static ĐÃ khởi tạo - giá trị nằm sẵn trong file thực thi.
  • .bss: global/static CHƯA khởi tạo - OS đặt 0 khi nạp, không tốn dung lượng file.
  • Biến static trong hàm cũng nằm ở .data/.bss → sống suốt chương trình.

Vì sao .bss tiết kiệm file

Khai báo int bang[1000000]; ở phạm vi global chỉ ghi vào file “cần 4 MB, toàn 0” - không lưu 4 MB số 0. Hệ điều hành cấp và xoá trắng vùng đó khi nạp. Đó là lý do .bss tách khỏi .data.

Mỗi lần gọi hàm, một khung (stack frame) chứa tham số và biến cục bộ được đẩy lên stack; khi hàm trả về, khung đó bị pop và mọi biến trong nó biến mất. Đệ quy cho thấy rõ điều này:

stack-de-quy.c

#include <stdio.h>

void dem(int n) {
    if (n == 0) return;
    printf("%d ", n);
    dem(n - 1);        // moi lan goi: them 1 khung tren stack
}

int main() {
    dem(3);            // stack: main -> dem(3) -> dem(2) -> dem(1)
    printf("\n");
    return 0;
}

Kết quả khi chạy

3 2 1

Liên hệ: con trỏ treo

Vì khung bị pop khi hàm trả về, trả về địa chỉ một biến cục bộ (return &local;) tạo ra con trỏ treo - nó trỏ tới ô stack đã bị thu hồi. Đây chính là lỗi “dangling pointer” ở bài Con trỏ & hàm, giờ bạn thấy lý do trong sơ đồ bộ nhớ.

Khi cần bộ nhớ sống lâu hơn một hàm hoặc kích thước chỉ biết lúc chạy, ta xin trên heap bằng malloc. Khối heap tồn tại tới khi bạn free - không tự biến mất khi hàm trả về:

heap-malloc.c

#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = malloc(sizeof(int));  // xin 4 byte tren HEAP
    *p = 100;
    printf("%d\n", *p);            // 100
    free(p);                       // tra khoi heap lai cho he thong
    return 0;
}

Kết quả khi chạy

100
  • malloc → trả về địa chỉ một khối trên heap; con trỏ giữ địa chỉ đó nằm trên stack.
  • Khối heap sống tới khi free (hoặc chương trình kết thúc).
  • Quên free = rò rỉ bộ nhớ (memory leak) - chủ đề bài Cấp phát động.

Câu hỏi thường gặp

Mỗi vùng có vai trò và QUYỀN khác nhau: .text chỉ đọc (bảo vệ mã khỏi bị ghi đè), .data/.bss cho biến toàn cục, stack tự động cho lời gọi hàm, heap thủ công cho cấp phát động. Tách vùng giúp quản lý vòng đời và bảo vệ bộ nhớ.

.data chứa global/static ĐÃ khởi tạo - giá trị được lưu sẵn trong file thực thi. .bss chứa global/static CHƯA khởi tạo - file chỉ ghi “cần bao nhiêu byte”, hệ điều hành tự đặt 0 khi nạp. Nhờ vậy file thực thi nhỏ hơn (mảng global triệu phần tử = 0 không tốn dung lượng file).

Stack: trình biên dịch quản lý tự động theo khung hàm (LIFO), rất nhanh, nhưng nhỏ và có giới hạn cứng. Heap: bạn tự xin/trả bằng malloc/free, linh hoạt và lớn, sống bao lâu tuỳ bạn - nhưng quên free thì rò rỉ. Hai vùng mọc về phía nhau.

Bài Cấp phát động (malloc/free) →

Quy ước của phần cứng/OS: stack bắt đầu ở địa chỉ CAO, mỗi khung mới đẩy xuống địa chỉ thấp hơn; heap bắt đầu ở địa chỉ THẤP và lớn lên. Hai bên tiến về khoảng trống ở giữa, tận dụng tối đa vùng nhớ.

Stack có giới hạn (thường vài MB). Đệ quy quá sâu (mỗi lần gọi thêm một khung) hoặc khai báo mảng cục bộ khổng lồ làm stack vượt giới hạn → chương trình sập. Tên website Stack Overflow chính là lấy từ lỗi này!

KHÔNG nằm trên stack mà ở .data/.bss (như biến global), nên nó SỐNG suốt chương trình và giữ giá trị giữa các lần gọi hàm - dù được khai báo bên trong hàm. Đó là điểm đặc biệt của từ khoá static.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Khác biệt giữa vùng .data.bss là gì?

  1. 1

    Đếm khung stack

    Dùng mô phỏng ở Bước 2, dừng ở bước “tinh(x)”. Lúc này có mấy khung trên stack? Tên các khung?

    Hoàn thành khi: 2 khung: maintinh. Khung tinh được đẩy lên trên (xuống dưới về địa chỉ) khi gọi hàm.

  2. 2

    Global ở vùng nào?

    int g = 7; (ngoài mọi hàm) nằm ở vùng nào? Còn int g2; (ngoài hàm, chưa gán)?

    Hoàn thành khi: g.data (đã khởi tạo). g2.bss (chưa khởi tạo, mặc định 0).

  3. 3

    Vòng đời biến cục bộ

    Biến int x trong main nằm ở vùng nào? Khi main kết thúc thì x ra sao?

    Hoàn thành khi: Stack (khung main). Khi main trả về, khung bị pop → x biến mất.

  4. 4

    malloc trả về vùng nào

    malloc cấp bộ nhớ ở vùng nào? Khối đó sống tới khi nào?

    Hoàn thành khi: Heap. Sống tới khi bạn gọi free (hoặc chương trình kết thúc) - KHÔNG tự biến mất khi hàm trả về.

  5. 5

    Vì sao sập

    Liên hệ bài chuỗi: vì sao char *s = "hi"; s[0] = 'H'; có thể làm sập chương trình?

    Hoàn thành khi: Chuỗi hằng "hi" nằm ở vùng chỉ đọc (.text/.rodata). Ghi vào đó là vi phạm quyền → segfault.

  6. 6

    Đệ quy sâu

    Hàm đệ quy dem(100000) không có điều kiện dừng hợp lý có thể gây lỗi gì? Vì sao?

    Hoàn thành khi: Stack overflow - mỗi lần gọi đẩy thêm một khung; quá nhiều khung làm stack vượt giới hạn.