스택 오버플로우 감지

EmbeddedMemory

임베디드 시스템에서 스택이 할당된 영역을 넘어 자라면 인접 메모리를 덮어쓴다. 전역 변수 손상, 다른 태스크 스택 침범, 하드폴트 등 원인 추적이 어려운 장애로 이어진다.

1. 스택 페인팅 (Stack Painting)

스택 영역 전체를 알려진 패턴(보통 0xDEADBEEF)으로 초기화한 뒤, 패턴이 남아있는 최하위 주소를 찾아 사용량을 측정한다.

#define STACK_PAINT 0xDEADBEEFU

void paint_stack(uint32_t *bottom, size_t words) {
    for (size_t i = 0; i < words; i++) {
        bottom[i] = STACK_PAINT;
    }
}

size_t stack_used(uint32_t *bottom, size_t words) {
    size_t unused = 0;
    while (unused < words && bottom[unused] == STACK_PAINT) {
        unused++;
    }
    return (words - unused) * sizeof(uint32_t);
}
  • 런타임 감지가 아니라 사후 측정이다. 오버플로우가 발생한 뒤에야 알 수 있다
  • high watermark를 기록해두면 전체 탐색을 반복하지 않아도 된다
  • FreeRTOS의 uxTaskGetStackHighWaterMark()가 이 방식이다

2. 스택 카나리 (Stack Canary)

가드 패턴 방식

스택 끝에 센티널 값을 배치하고, 주기적으로 또는 컨텍스트 스위치 시 값이 변조되었는지 확인한다.

#define CANARY_VALUE 0xCAFEBABEU

// 태스크 스택 하단에 카나리 배치
task_stack[0] = CANARY_VALUE;

// 컨텍스트 스위치 훅에서 검사
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) {
    // 카나리 파괴 감지 → 에러 처리
    error_handler(STACK_OVERFLOW, pcTaskName);
}

FreeRTOS의 configCHECK_FOR_STACK_OVERFLOW를 2로 설정하면 컨텍스트 스위치마다 스택 하단 패턴을 검사한다.

GCC -fstack-protector

함수 진입 시 스택 프레임에 카나리 값을 삽입하고, 함수 리턴 전에 검증한다. 버퍼 오버런에 의한 리턴 주소 변조를 감지한다.

// 링커 또는 초기화 코드에서 카나리 값 정의
unsigned long __stack_chk_guard = 0xDEADBEEF;

// 카나리 변조 시 호출되는 핸들러
void __stack_chk_fail(void) {
    __disable_irq();
    // 에러 로깅, 시스템 리셋 등
    NVIC_SystemReset();
}
  • -fstack-protector: 8바이트 이상 버퍼가 있는 함수만 계측
  • -fstack-protector-all: 모든 함수 계측. 오버헤드가 크다
  • 스택 자체의 오버플로우가 아니라 함수 내 버퍼 오버런을 감지하는 메커니즘이다

3. MPU (Memory Protection Unit)

스택 끝에 접근 불가 영역(guard region)을 설정한다. 스택이 이 영역에 닿으면 하드웨어가 즉시 예외를 발생시킨다.

// Cortex-M MPU 설정: 스택 가드 영역
void configure_stack_guard(uint32_t *stack_bottom) {
    MPU->RNR = 0;  // Region 0
    MPU->RBAR = (uint32_t)stack_bottom;
    MPU->RASR = (0 << 28)  // XN: 실행 불가
              | (0 << 24)  // AP: 접근 불가
              | (4 << 1)   // SIZE: 32B (2^(4+1))
              | (1 << 0);  // ENABLE
    MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk;
}

void MemManage_Handler(void) {
    uint32_t cfsr = SCB->CFSR;
    if (cfsr & SCB_CFSR_DACCVIOL_Msk) {
        // 스택 오버플로우로 인한 메모리 접근 위반
        uint32_t fault_addr = SCB->MMFAR;
        error_handler(STACK_OVERFLOW, fault_addr);
    }
}
  • 오버플로우 발생 즉시 감지한다. 사후 검사가 아니다
  • 가드 영역 크기만큼 RAM을 소비한다 (보통 32~256B)
  • RTOS에서 컨텍스트 스위치 시 가드 영역을 현재 태스크의 스택으로 재설정해야 한다
  • Cortex-M0에는 MPU가 없는 경우가 많다

비교

방식감지 시점오버헤드하드웨어 의존
페인팅사후 측정초기화 시 + 주기적 검사없음
카나리 (가드 패턴)컨텍스트 스위치 시매 스위치마다 검사없음
GCC stack-protector함수 리턴 시함수 진입/종료마다없음
MPU 가드즉시하드웨어 처리MPU 필요

정적 분석으로 예방

런타임 감지 외에 컴파일 타임에 최대 스택 사용량을 계산하는 방법이 있다.

  1. GCC -fstack-usage: 함수별 스택 사용량을 .su 파일로 출력한다
  2. 콜 트리를 따라 최대 경로의 합을 구한다
  3. 인터럽트: 컨텍스트 저장 + ISR 내 최대 콜 체인을 합산한다
  4. 산출된 worst case에 여유(보통 25~50%)를 더해 스택 크기를 결정한다

메모

  • 테스트에서 관측된 high watermark는 worst case가 아니다. 모든 인터럽트가 동시에 중첩되는 상황은 테스트에서 거의 재현되지 않는다
  • Cortex-M DWT(Data Watchpoint and Trace)를 사용하면 디버거 없이도 하드웨어 워치포인트로 감지할 수 있다
  • FreeRTOS configCHECK_FOR_STACK_OVERFLOW = 1은 스택 포인터만 검사하고, 2는 가드 패턴까지 검사한다