스택 오버플로우 감지
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 필요 |
정적 분석으로 예방
런타임 감지 외에 컴파일 타임에 최대 스택 사용량을 계산하는 방법이 있다.
- GCC
-fstack-usage: 함수별 스택 사용량을.su파일로 출력한다 - 콜 트리를 따라 최대 경로의 합을 구한다
- 인터럽트: 컨텍스트 저장 + ISR 내 최대 콜 체인을 합산한다
- 산출된 worst case에 여유(보통 25~50%)를 더해 스택 크기를 결정한다
메모
- 테스트에서 관측된 high watermark는 worst case가 아니다. 모든 인터럽트가 동시에 중첩되는 상황은 테스트에서 거의 재현되지 않는다
- Cortex-M DWT(Data Watchpoint and Trace)를 사용하면 디버거 없이도 하드웨어 워치포인트로 감지할 수 있다
- FreeRTOS
configCHECK_FOR_STACK_OVERFLOW = 1은 스택 포인터만 검사하고,2는 가드 패턴까지 검사한다