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.
- ▸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ơ đồ
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
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
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.
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ớ.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Khác biệt giữa vùng .data và .bss là gì?
- 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:
mainvàtinh. Khungtinhđược đẩy lên trên (xuống dưới về địa chỉ) khi gọi hàm. - 2
Global ở vùng nào?
int g = 7;(ngoài mọi hàm) nằm ở vùng nào? Cònint 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
Vòng đời biến cục bộ
Biến
int xtrongmainnằm ở vùng nào? Khimainkết thúc thìxra sao?Hoàn thành khi: Stack (khung
main). Khimaintrả về, khung bị pop →xbiến mất. - 4
malloc trả về vùng nào
malloccấ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
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
Đệ 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.