🌱 Memory Layout trong chương trình C/C++

🌱 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)
⚠️ Lưu ý: Trong embedded, mỗi byte đều quý giá! Một con STM32F030 entry-level chỉ có 4KB RAM - tương đương 4096 bytes. Nếu bạn khai báo 1 mảng buffer 2KB, bạn đã dùng hết 50% RAM của nó!

    👉 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:

Embedded C 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);
}
💡 Tip: Dùng file .map sau khi compile để xem chính xác địa chỉ của từng biến trong memory layout!

    👉 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!
⚠️ Lưu ý: Trên ARM Cortex-M, cố gắng write vào Flash/Text segment sẽ gây ra Hard Fault exception! Vì vậy, bạn nên kiểm tra pointer có trỏ vào Flash hay RAM trước khi write.

    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
Với MCU có RAM nhỏ, đặt tất cả lookup tables, config data, và strings vào const để tiết kiệm RAM!

    👉 Data Segment

    Data segment chứa các biến globalstatic đã đượ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:

  1. Compile time: Giá trị khởi tạo được lưu vào file .bin/.hex
  2. Startup code: Copy từ Flash → RAM trước khi main() chạy
  3. 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
💡 Tip: Nếu biến không cần giá trị khởi tạo cụ thể, đừng gán gì cả hoặc gán = 0. Điều này giảm kích thước file .bin và tốc độ startup!
// 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
    }
}
IMPORTANT NOTE: Heap allocation rất NGUY HIỂM trong RTOS và real-time systems vì:
  • 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
👉 Tránh dùng malloc/free trong embedded! Dùng static buffers hoặc memory pools thay thế.

    👉 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:

  1. Return address: Địa chỉ quay lại sau khi hàm kết thúc
  2. Function parameters: Tham số truyền vào
  3. Local variables: Biến cục bộ
  4. 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

Stack Overflow xảy ra khi:
  • Recursive functions quá sâu
  • Khai báo mảng local quá lớn
  • Nested function calls quá nhiều
Hậu quả: Hard Fault, system crash, corrupt data!
// 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");
    }
}
💡 Một số lưu ý cho Stack:
  • 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 😊

Nguyễn Văn Nghĩa

Mình là một người thích học hỏi và chia sẻ các kiến thức về Nhúng IOT.

Đăng nhận xét

Mới hơn Cũ hơn
//
Avatar

Đăng nhập

Người dùng mới? Đăng ký ngay

hoặc

Bằng việc tạo tài khoản, bạn đồng ý với Chính sách bảo mật & Chính sách Cookie.