🌱 Memory Layout trong chương trình C/C++
Memory layout là một khái niệm quan trọng trong ngôn ngữ lập trình C/C++. Nó liên quan đến cách các biến và đối tượng được tổ chức và lưu trữ trong bộ nhớ. Trong bài viết này, chúng ta sẽ tìm hiểu về memory layout và tầm quan trọng của nó trong lập trình C/C++.
👉 Tại sao cần hiểu Memory Layout?
Trong lập trình nhúng và embedded systems, việc hiểu rõ memory layout là rất quan trọng vì một số lý do sau:
- Tối ưu bộ nhớ: Vi điều khiển thường có RAM/Flash hạn chế (vd: STM32F103 chỉ có 20KB RAM), Cần tránh lãng phí bộ nhớ với các biến không cần thiết
- Debug hiệu quả: Hiểu được lỗi stack overflow, memory leak, dangling pointer
- Thiết kế driver: Biết cách đặt biến vào đúng vùng nhớ (const vào Flash, buffer vào RAM)
👉 Kiến trúc Memory Layout
Trong C/C++, bộ nhớ được chia thành các vùng khác nhau, bao gồm:
- vùng dữ liệu (data segment), chứa các biến toàn cục và static.
- vùng stack (stack segment), lưu trữ các biến cục bộ và thông tin trong quá trình function call.
- vùng heap (heap segment), được sử dụng để cấp phát bộ nhớ động.
- vùng text (text segment), chứa mã máy của chương trình.
Sơ đồ memory layout:
Bảng tóm tắt các vùng nhớ:
| Vùng nhớ | Địa chỉ | Nội dung | Lifetime | Kích thước |
|---|---|---|---|---|
| Text Segment | Thấp nhất | Machine code, hằng số | Toàn bộ chương trình | Cố định (Flash) |
| Data Segment (Initialized) |
Sau Text | Global/static đã khởi tạo | Toàn bộ chương trình | Cố định |
| BSS Segment (Uninitialized) |
Sau Data | Global/static chưa khởi tạo | Toàn bộ chương trình | Cố định |
| Heap | Tăng dần ↑ | malloc/new động | Đến khi free/delete | Linh động |
| Stack | Cao nhất Giảm dần ↓ |
Biến local, function call | Trong scope của hàm | Linh động |
Ví dụ thực tế với STM32F103C8T6:
// STM32F103C8T6: 64KB Flash, 20KB RAM
// Memory Map:
// Flash: 0x08000000 - 0x0800FFFF (64KB)
// SRAM: 0x20000000 - 0x20004FFF (20KB)
// --- TEXT SEGMENT (Flash) ---
const char firmware_version[] = "v1.0.0"; // 0x08000xxx
void setup() { /* code here */ } // 0x08000xxx
// --- DATA SEGMENT (RAM - initialized) ---
int sensor_count = 5; // 0x20000000
static uint32_t error_code = 0xFF; // 0x20000004
// --- BSS SEGMENT (RAM - uninitialized) ---
uint8_t uart_buffer[256]; // 0x20000008
static int readings[100]; // 0x20000108
void process_data() {
// --- STACK ---
int local_var = 10; // 0x20004FFC (top of stack)
char temp[32]; // 0x20004FDC
// --- HEAP ---
uint8_t* dynamic_buf = (uint8_t*)malloc(512); // Somewhere in heap
// ...
free(dynamic_buf);
}
👉 Text Segment (Code Segment)
Text segment chứa mã máy của chương trình và các hằng số (const). Vùng này thường được đặt ở vùng nhớ chỉ đọc (read-only) để ngăn chặn việc sửa đổi mã máy trong quá trình chạy. Điều này giúp bảo vệ chương trình khỏi các lỗi và tấn công.
Đặc điểm của Text Segment:
- Vị trí: Nằm trong Flash memory (non-volatile) trên MCU
- Quyền truy cập: Read-only (R-X: Read + Execute)
- Kích thước: Cố định sau khi compile
- Nội dung: Instructions (machine code), const variables, string literals
Ví dụ chi tiết:
// ===== Text Segment =====
// 1. Machine code của functions
void LED_Toggle() {
GPIOC->ODR ^= GPIO_PIN_13; // Assembly instructions here
}
// 2. Const variables (stored in Flash)
const uint32_t UART_BAUDRATE = 115200;
const char* device_name = "STM32F103";
// 3. Lookup tables (LUT) - efficient for embedded
const uint8_t sin_table[256] = {
128, 131, 134, /* ... precomputed sine values ... */
};
// 4. String literals
printf("Hello World"); // "Hello World" stored in text segment
// ERROR: Cố gắng modify text segment
char* str = "Test";
str[0] = 'B'; // CRASH! Segmentation fault hoặc Hard Fault
// OK: Copy to RAM first
char str[] = "Test"; // Copied to stack
str[0] = 'B'; // OK!
Tại sao cần const trong embedded?
| Khai báo | Vị trí | RAM tiêu tốn | Flash tiêu tốn |
|---|---|---|---|
uint8_t table[1000]; |
RAM (BSS) | 1000 bytes | 0 bytes |
const uint8_t table[1000]; |
Flash (Text) | 0 bytes 🎉 | 1000 bytes |
const để tiết kiệm RAM!
👉 Data Segment
Data segment chứa các biến global và static đã được khởi tạo giá trị. Các biến trong vùng này tồn tại trong suốt thời gian chạy của chương trình.
Data segment được chia thành 2 phần:
◉ Initialized Data Segment
Phân đoạn dữ liệu đã khởi tạo chứa các biến global và static được khởi tạo một cách tường minh (explicit) bởi lập trình viên.
Cách hoạt động của Data Segment:
- Compile time: Giá trị khởi tạo được lưu vào file .bin/.hex
- Startup code: Copy từ Flash → RAM trước khi main() chạy
- Runtime: Biến tồn tại suốt đời chương trình, có thể đọc/ghi
// ===== Initialized Data Segment =====
// Global variables với giá trị khởi tạo
int device_id = 0x1234; // Data segment (4 bytes RAM)
float temperature = 25.5; // Data segment (4 bytes RAM)
char status = 'A'; // Data segment (1 byte RAM)
// Static variables trong hàm
void count_calls() {
static int counter = 0; // Data segment
counter++;
printf("Called %d times\n", counter);
}
// NOT OK: Tốn RAM không cần thiết!
uint8_t error_messages[10][32] = {
"No error",
"Timeout",
// ... 320 bytes RAM!
};
// OK: Tiết kiệm RAM - dùng const
const char* const error_messages[] = {
"No error", // Text segment
"Timeout", // Text segment
// ... chỉ tốn Flash!
};
◉ Uninitialized Data Segment (BSS)
BSS (Block Started by Symbol) chứa các biến toàn cục và static không được khởi tạo hoặc được khởi tạo bằng 0.
Đặc điểm quan trọng của BSS:
- Tự động zero: Startup code sẽ gán = 0 cho toàn bộ BSS
- Không chiếm Flash: Chỉ lưu size, không lưu data
- Tiết kiệm file size: .bin/.hex nhỏ hơn vì không chứa giá trị 0
// ===== BSS Segment =====
// Không khởi tạo → BSS
int sensor_data; // BSS, giá trị = 0
uint8_t rx_buffer[512]; // BSS, tất cả = 0
// Khởi tạo = 0 → Cũng vào BSS
int error_count = 0; // BSS (tối ưu!)
static uint32_t flags = 0x00; // BSS
// Static trong function
void init_module() {
static int initialized; // BSS, = 0
if (!initialized) {
// First time init
initialized = 1;
}
}
// So sánh file size:
uint8_t big_array_data[4096] = {1, 2, 3}; // Data → +4096 bytes trong .bin
uint8_t big_array_bss[4096]; // BSS → +0 bytes trong .bin!
Ví dụ startup code (chuẩn ARM Cortex-M):
// File: startup_stm32f103xb.s (Assembly)
Reset_Handler:
// 1. Copy Data segment từ Flash → RAM
ldr r0, =_sdata ; Start address of .data in RAM
ldr r1, =_edata ; End address of .data
ldr r2, =_sidata ; Source in Flash
CopyDataLoop:
ldr r3, [r2], #4 ; Load and increment
str r3, [r0], #4 ; Store and increment
cmp r0, r1
bcc CopyDataLoop
// 2. Zero fill BSS segment
ldr r0, =_sbss ; Start of BSS
ldr r1, =_ebss ; End of BSS
movs r2, #0
FillBssLoop:
str r2, [r0], #4
cmp r0, r1
bcc FillBssLoop
// 3. Call main()
bl main
// Example so sánh
int s_global = 10; // init data segment, value = 10
int s_global; // bss segment, value = 0
int bss_global; // bss segment, value = 0
int main()
{
static int number = 10; // init data segment
}
👉 Heap Segment
Bộ nhớ trong vùng heap nằm ngoài phạm vi của các hàm và tồn tại đến khi chúng được giải phóng bằng cách sử dụng toán tử delete.
Bộ nhớ Heap dùng cho khái niệm cấp phát động - dynamic memory allocation.
Đặc điểm của Heap:
- Quản lý thủ công: Phải malloc/free hoặc new/delete
- Kích thước linh động: Tăng lên về phía stack
- Tốc độ chậm: Allocation/deallocation mất thời gian
- Fragmentation: Bộ nhớ bị phân mảnh sau nhiều lần malloc/free
// ===== Heap Allocation =====
// C style
void process_large_data() {
// Allocate 1KB buffer dynamically
uint8_t* buffer = (uint8_t*)malloc(1024);
if (buffer == NULL) {
// Out of memory!
return;
}
// Use buffer...
memset(buffer, 0, 1024);
// Must free!
free(buffer);
}
// C++ style
void create_objects() {
// Single object
Sensor* sensor = new Sensor();
sensor->init();
delete sensor;
// Array of objects
Motor* motors = new Motor[4];
// ...
delete[] motors; // Note the []
}
// NOT OK: Common mistakes
void memory_leaks() {
// 1. Memory leak - forgot to free
uint8_t* data = (uint8_t*)malloc(100);
// ... use data ...
// Oops, forgot free(data)!
// 2. Double free - crash!
int* ptr = (int*)malloc(sizeof(int));
free(ptr);
free(ptr); // CRASH!
// 3. Use after free
char* str = (char*)malloc(50);
free(str);
strcpy(str, "test"); // Undefined behavior!
}
// OK!
void safe_allocation() {
uint8_t* buffer = (uint8_t*)malloc(256);
if (buffer != NULL) {
// Use buffer...
free(buffer);
buffer = NULL; // Prevent double free
}
}
- Non-deterministic: Không biết chính xác mất bao lâu
- Fragmentation: Bộ nhớ bị phân mảnh sau nhiều lần malloc/free
- Out of memory: Không handle được nếu malloc trả về NULL
👉 Stack Segment
Các biến được khai báo trong một hàm (biến local) sẽ được lưu trữ trong vùng stack. Khi hàm kết thúc, các biến trong stack sẽ được tự động giải phóng. Điều này đảm bảo việc quản lý và giải phóng bộ nhớ tự động và tiết kiệm tài nguyên.
Ngoài ra, Stack Segment còn được dùng để lưu trữ thông tin trong quá trình Function Call.
Stack hoạt động dựa theo cơ chế LIFO (Last In - First Out), tức là thông tin được đẩy vào trước, sẽ được lấy ra sau, và ngược lại, giống như cách chúng ta để sách vào thùng, quyển sách cho vào đầu tiên, sẽ là quyển được lấy ra cuối cùng.
➤ Như hình trên, dễ thấy stack sẽ có chiều hướng đi xuống về mặt địa chỉ, nếu như chúng ta đẩy data vào, và loại stack được sử dụng thông dụng nhất (trong máy tính cũng như các dòng vi điều khiển), là stack loại Full Descending - tức là Stack Pointer (SP) luôn trỏ đến vị trí data mới nhất được đẩy vào stack, và khi Push data vào, SP sẽ giảm đi (descending).
Cấu trúc Stack Frame:
Mỗi lần gọi function, một stack frame mới được tạo chứa:
- Return address: Địa chỉ quay lại sau khi hàm kết thúc
- Function parameters: Tham số truyền vào
- Local variables: Biến cục bộ
- Saved registers: Backup các thanh ghi cần bảo toàn
// ===== Stack Visualization =====
void function_C(int z) {
char buffer[16]; // 16 bytes
int local_c = 30; // 4 bytes
}
void function_B(int y) {
int local_b = 20;
function_C(3);
}
void function_A(int x) {
int local_a = 10;
function_B(2);
}
int main() {
function_A(1);
return 0;
}
/*
Stack layout khi đang ở function_C():
High Address (0x20005000)
┌─────────────────────────┐
│ main's stack frame │
├─────────────────────────┤
│ Return address to main │
│ Parameter x = 1 │
│ local_a = 10 │
├─────────────────────────┤
│ Return addr to func_A │
│ Parameter y = 2 │
│ local_b = 20 │
├─────────────────────────┤
│ Return addr to func_B │
│ Parameter z = 3 │
│ local_c = 30 │
│ buffer[16] │ ← SP (Stack Pointer)
└─────────────────────────┘
Low Address
*/
Ví dụ thực tế về Stack:
// Example: UART data processing
void UART_SendString(const char* str) {
// Local variables on stack
uint16_t len = strlen(str); // 2 bytes
uint16_t i; // 2 bytes
for (i = 0; i < len; i++) {
while (!(USART1->SR & USART_SR_TXE));
USART1->DR = str[i];
}
// Stack tự động giải phóng khi return
}
void process_data() {
// Tạo buffer tạm trên stack
char temp_buffer[64]; // 64 bytes
// Format string
snprintf(temp_buffer, 64, "Temp: %d C\n", read_temperature());
// Send
UART_SendString(temp_buffer);
// temp_buffer tự động giải phóng ở đây
}
// DANGER: Returning local variable address!
char* get_message() {
char msg[] = "Hello";
return msg; // Dangling pointer! Stack đã bị giải phóng!
}
// CORRECT: Use static or malloc
const char* get_message_safe() {
static const char msg[] = "Hello"; // Static - vẫn tồn tại
return msg;
}
Stack Overflow
- Recursive functions quá sâu
- Khai báo mảng local quá lớn
- Nested function calls quá nhiều
// Stack overflow examples
// 1. Large local array
void bad_function() {
uint8_t huge_buffer[8192]; // 8KB on stack! Crash on STM32F103!
}
// 2. Infinite recursion
void factorial(int n) {
if (n <= 0) return;
factorial(n - 1); // Mỗi call tốn ~20 bytes
} // factorial(1000) → stack overflow!
// 3. Deep call stack
void func_a() { char buf[512]; func_b(); }
void func_b() { char buf[512]; func_c(); }
void func_c() { char buf[512]; func_d(); }
// Tổng: 1536 bytes + call overhead → có thể overflow!
// Solutions
// 1. Use static or global for large buffers
static uint8_t rx_buffer[8192]; // BSS segment, not stack!
// 2. Use malloc (if heap available)
uint8_t* buffer = (uint8_t*)malloc(8192);
// 3. Increase stack size in startup file
// In startup_stm32xxx.s:
// Stack_Size EQU 0x800 ; 2KB → increase to 0x1000 (4KB)
Cách kiểm tra Stack Usage:
// Method 1: Stack painting (STM32)
void stack_monitor_init() {
// Fill stack with pattern
extern uint32_t _estack, _sstack;
uint32_t* p = &_sstack;
while (p < &_estack) {
*p++ = 0xDEADBEEF; // Magic pattern
}
}
uint32_t stack_get_usage() {
extern uint32_t _estack, _sstack;
uint32_t* p = &_sstack;
uint32_t unused = 0;
// Count untouched stack
while (*p == 0xDEADBEEF) {
unused += 4;
p++;
}
uint32_t total = (uint32_t)&_estack - (uint32_t)&_sstack;
return total - unused; // Used stack
}
// Method 2: Real-time check
void check_stack_overflow() {
uint32_t current_sp;
__asm volatile ("mov %0, sp" : "=r" (current_sp));
extern uint32_t _sstack;
if (current_sp < (uint32_t)&_sstack + 128) {
// Less than 128 bytes left!
printf("WARNING: Stack almost full!\n");
}
}
- Giữ local variables nhỏ gọn (<256 bytes)
- Tránh large arrays trên stack
- Hạn chế recursion depth
- Monitor stack usage trong development
- Để 20-30% stack margin cho worst case
👉 Memory Optimization trong Embedded
Kỹ thuật tối ưu bộ nhớ:
// 1. Sử dụng đúng kiểu dữ liệu
uint8_t counter; // 1 byte - đủ cho 0-255
// Không cần: uint32_t counter; // Lãng phí 3 bytes!
// 2. Bit fields cho flags
struct {
uint8_t is_ready : 1; // 1 bit
uint8_t is_error : 1; // 1 bit
uint8_t mode : 3; // 3 bits (0-7)
uint8_t reserved : 3; // Total: 1 byte instead of 3!
} status;
// 3. Pack structures
// NOT OK: Unaligned (12 bytes due to padding)
struct bad_struct {
uint8_t a; // 1 byte + 3 padding
uint32_t b; // 4 bytes
uint8_t c; // 1 byte + 3 padding
};
// OK: Aligned (8 bytes)
struct good_struct {
uint32_t b; // 4 bytes
uint8_t a; // 1 byte
uint8_t c; // 1 byte + 2 padding
};
// 4. Shared buffers
static uint8_t shared_buffer[512];
void uart_process() {
// Reuse shared_buffer
UART_Receive(shared_buffer, 512);
}
void spi_process() {
// Reuse same buffer (not concurrent!)
SPI_Read(shared_buffer, 512);
}
// 5. Use const for read-only data
const uint8_t gamma_table[256] = { /* ... */ }; // Flash, not RAM!
Kiểm tra Memory Usage sau compile:
# Dùng arm-none-eabi-size
$ arm-none-eabi-size firmware.elf
text data bss dec hex filename
12456 124 2048 14628 3924 firmware.elf
# Giải thích:
# text = Code + const data (trong Flash)
# data = Initialized global/static (Flash → RAM)
# bss = Uninitialized global/static (chỉ RAM)
# Flash usage = text + data = 12456 + 124 = 12580 bytes
# RAM usage = data + bss = 124 + 2048 = 2172 bytes
# Xem chi tiết với .map file
$ cat firmware.map | grep -A 20 "Memory Configuration"
>>>>>> Follow ngay <<<<<<<
Để nhận được những bài học miễn phí mới nhất nhé 😊
Chúc các bạn học tập tốt 😊
