Troubleshooting

[임베디드 C] 스택 오버플로우(Stack Overflow) 원인 분석과 MAP 파일 활용한 해결 방법

임베디드 친구 2026. 6. 5. 21:31
반응형

[Quick Summary (TL;DR) - For Global Developers]

  • Symptom: MCU enters HardFault_Handler or suddenly resets, causing memory corruption and unpredictable runtime behavior.
  • Cause: A thread or function exceeds its allocated stack space due to large local variables, deep recursion, or high interrupt nesting.
  • Solution: Optimize local memory allocation using static or dynamic memory, increase stack size in the linker script, and analyze the MAP file to audit stack usage.

스택 오버플로우(Stack Overflow) 무작위 시스템 크래시 증상 (Introduction)

임베디드 시스템 개발 중 멀쩡히 동작하던 펌웨어가 특정 함수를 호출하는 순간 먹통이 되거나, 디버거가 HardFault_Handler 또는 UsageFault_Handler 가리키며 멈추는 현상을 겪어보셨을 겁니다.

이러한 무작위적 시스템 크래시(Random System Crash)의 주범 중 하나가 바로 Stack Overflow입니다. 특히 실시간 운영체제(RTOS) 환경에서는 특정 태스크(Task)가 할당받은 스택 크기를 넘어서는 순간, 인접한 다른 태스크의 TCB(Task Control Block)나 시스템 힙(Heap) 영역을 훼손시켜 원인 추적이 아주 어려워집니다. 해외 포럼에서도 MCU random reset after function call, ARM Cortex-M HardFault stack pointer와 같은 키워드로 자주 검색되는 대표적인 런타임 에러입니다.

메모리 하강 구조와 스택 오버플로우(Stack Overflow)의 근본적인 원인 분석 (Why it happens)

임베디드 c에서 스택(Stack)은 함수 호출 시 Local Variables(지역 변수), Function Parameters(매개변수), 그리고 함수 종료 후 돌아갈 Return Address(복귀 주소)를 저장하는 메모리 공간입니다.

ARM Cortex-M Core를 기준으로 설명하면, 스택은 상위 주소에서 하위 주소로 자라나는 Descending Stack 방식을 사용합니다. 함수가 호출될 때마다 SP(Stack Pointer) 레지스터 값이 감소하며 메모리가 할당됩니다.

Generated by Gemini AI.

위 다이어그램처럼 PUSH 동작이 수행되면 SP가 하위 주소 방향으로 이동합니다. 스택 오버플로우는 이 Current SP가 링커 스크립트(Linker Script, .ld/.sct)에서 정의한 Stack Limit 경계를 넘어설 때 발생합니다.

[High Address]
  +-----------------------+ <--- Stack Top (Initial SP)
  | Function A Frame      |
  +-----------------------+
  | Function B Frame      |
  +-----------------------+
  |                       | |
  |     Valid Stack       | | Stack Grows Downward
  |                       | v
  +-----------------------+ <--- Current SP
  |     Unallocated       |
  +=======================+ <--- Stack Limit (Boundary)
  |  Overflown / Corrupt  | 
  +-----------------------+
  | Heap / Global Variables
[Low Address]

경계를 넘어선 상태에서 데이터 쓰기(Write operation)가 실행되면 인접한 메모리 영역의 변수나 LR(Link Register) 값이 덮어씌워져 시스템이 통제 불능 상태에 빠지게 됩니다. 주요 발생 원인은 다음과 같습니다.

  • 과도한 크기의 지역 변수(Large Local Arrays): 버퍼 메모리를 함수 내부 변수로 크게 선언하는 경우.
  • 깊은 재귀 호출(Deep Recursion): 종료 조건이 모호하거나 depth가 깊은 재귀 함수를 호출하는 경우.
  • 인터럽트 중첩(Interrupt Nesting): ISR(Interrupt Service Routine)이 실행되는 와중에 우선순위가 더 높은 인터럽트가 지속적으로 중첩되어 스택에 콘텍스트(Context)를 누적하는 경우.

문제를 유발하는 잘못된 지역 버퍼 스택(Stack) 할당 C 코드 예시 (Bad Case)

가장 흔하게 발생하는 실수는 네트워크 패킷이나 센서 데이터를 처리하기 위해 함수 내부에서 대용량 버퍼를 지역 변수로 선언하는 경우입니다.

#define BUFFER_SIZE 2048 // 2KB Buffer

void Process_Network_Data(void) {
    /* BAD: Allocating a 2KB array on the stack.
     * If the total allocated stack for this task/system is only 1KB or 2KB,
     * this single declaration will immediately cause a Stack Overflow. */
    uint8_t local_buffer[BUFFER_SIZE]; 

    /* Simulate data processing */
    for(int i = 0; i < BUFFER_SIZE; i++) {
        local_buffer[i] = Read_Hardware_Register();
    }

    Parse_Payload(local_buffer);
}

스택 오버플로우(Stack Overflow) 해결을 위한 올바른 정적 메모리 할당 C 코드 (Good Case)

스택 공간을 절약하기 위해 대용량 버퍼는 static 키워드를 사용하여 .bss/.data (정적 메모리 영역)로 빼거나, 필요한 경우에만 힙(Heap) 메모리를 활용해야 합니다. 다음은 정적 할당으로 스택 부하를 제거한 코드입니다.

#define BUFFER_SIZE 2048

/* GOOD: Allocated in the .bss section (RAM), not on the stack.
 * Thread-safe caution: If this function is called concurrently by multiple 
 * RTOS tasks, use mutex/semaphores or consider dynamic allocation (malloc). */
static uint8_t global_static_buffer[BUFFER_SIZE];

void Process_Network_Data(void) {
    /* No large allocation on the stack; only pointer/index variables are used */

    for(int i = 0; i < BUFFER_SIZE; i++) {
        global_static_buffer[i] = Read_Hardware_Register();
    }

    Parse_Payload(global_static_buffer);
}

핵심 수정 포인트

  • static 키워드를 추가하여 변수의 저장 위치를 스택 프레임 외부의 정적 데이터 영역인 .bss 섹션으로 변경했습니다. 이를 통해 함수가 호출되어도 SP(Stack Pointer)가 급격하게 감소하지 않아 오버플로우 위험을 원천 차단합니다.

런타임 하드폴트 및 스택 디버깅 가이드 (Debugging Tips)

빌드 결과물인 MAP 파일 (.map) 분석

  • 컴파일 후 생성되는 MAP 파일을 열어 전체 시스템의 스택 크기와 전역 변수 배치를 수시로 확인해야 합니다.툴체인(Keil, IAR, GCC)에서 제공하는 MAP 파일에서 STACK 또는 __stack_limit 키워드를 검색하여, 설계한 스택 사이즈가 메모리 맵 상에 안전하게 정렬되어 있는지 확인합니다. GCC 기준 -fstack-usage 플래그를 컴파일러 옵션에 추가하면, 각 함수가 소비하는 정확한 스택 크기가 .su 파일로 출력되므로 어떤 함수가 범인인지 정량적으로 추적할 수 있습니다.

스택 가드 패턴 (Stack Canary / Guard Pattern) 활용

  • 스택의 최하단(종료 경계면) 메모리에 특정 매직 넘버(예: 0xDEADBEEF)를 써두고, 시스템 소프트웨어나 타이머 ISR에서 이 값이 유지되고 있는지 주기적으로 감킹(Monitoring)하는 기법입니다. RTOS 환경이라면 Stack Overflow Hook 기능(configCHECK_FOR_STACK_OVERFLOW)을 활성화하여 런타임에 경계를 넘는 순간 즉시 트랩(Trap)을 걸 수 있도록 설정합니다.

디버거 레지스터 윈도우 (SP 레지스터 추적)

  • J-Link 또는 ST-Link를 연결한 상태에서 HardFault가 발생했다면, IDE의 Register View를 열어 SP(R13) 값을 확인합니다. 링크 스크립트에서 정의한 스택 범위를 벗어난 주소값이 SP에 찍혀있다면 100% 스택 오버플로우입니다. 복귀 주소가 저장되는 LR(Link Register, R14) 값을 역추적하여 하드폴트 직전에 실행 중이던 함수를 찾아내야 합니다.
반응형