태스크나 인터럽트가 할당된 스택 범위를 넘어 다른 메모리를 덮어쓰는 현상. 임베디드에서 가장 찾기 어려운 버그 유형이다.
왜 최악인가
스택이 넘치면 → 인접한 다른 태스크의 스택이나 전역 변수를 덮어쓴다
→ 전혀 관계없는 곳에서 이상한 값이 나온다
→ 증상과 원인의 거리가 멀어 추적이 극히 어렵다
→ 재현도 잘 안 된다 (특정 실행 경로에서만 깊어짐)
"어제까지 잘 되던 게 오늘 이상하다" 의 단골 원인이다.
무엇이 스택을 먹나
-
① 지역 변수 — 특히 배열
- uint8_t buf[1024]; ← 1KB를 한 번에
-
② 함수 호출 깊이
- a() → b() → c() → d() 각 프레임이 누적
-
③ 재귀 — 깊이가 데이터에 좌우되면 예측 불가
-
④ 인터럽트 — 자동 스태킹 32바이트 (FPU면 104바이트)
- 중첩되면 배수로
-
⑤ printf 계열 — 내부에서 수백 바이트를 쓴다 ★
- 특히 %f 포매팅은 1KB 이상 쓰는 구현도 있다
★ printf가 의외의 범인인 경우가 많다.
디버깅용으로 넣은 printf 때문에 스택이 넘치는 일이 실제로 흔하다.
검출 방법 ① — 스택 워터마크 (RTOS)
// 태스크 스택을 생성 시 특정 패턴(0xA5)으로 채워 둔다
// 나중에 남아 있는 패턴의 길이 = 한 번도 안 쓴 깊이
UBaseType_t remaining = uxTaskGetStackHighWaterMark(handle);
if (remaining < 50) { /* 위험 */ }
"역대 최저 여유량" 을 알려 준다. 현재가 아니라 지금까지의 최악이다.
configCHECK_FOR_STACK_OVERFLOW = 2
void vApplicationStackOverflowHook(TaskHandle_t t, char *name) {
// 여기서 로그를 남기고 리셋
}
방법 2는 스택 끝의 패턴이 손상됐는지 검사하므로 방법 1보다 확실하다. 다만 컨텍스트 스위칭 시점에만 확인하므로, 그 사이에 넘쳤다가 복구되면 놓친다.
검출 방법 ② — MPU 가드 (가장 확실)
- 스택 바로 아래에 접근 금지 구역을 설정
- 넘치는 순간 MemManage Fault
- 즉시, 정확히 잡힌다
소프트웨어 검사와 달리 실시간으로 잡는다. MPU가 있는 MCU라면 개발 단계에서 반드시 켜는 것이 좋다.
검출 방법 ③ — 카나리 값
#define CANARY 0xDEADBEEF
uint32_t stack_canary = CANARY; // 스택 끝에 배치
void check(void) {
if (stack_canary != CANARY) { /* 오버플로 */ }
}
RTOS가 없어도 쓸 수 있는 간단한 방법이다.
검출 방법 ④ — 정적 분석
arm-none-eabi-gcc -fstack-usage … # 함수별 스택 사용량을 .su 파일로 출력
- main.c:42:6:process_data — 528 static
호출 그래프와 결합하면 최악 스택 깊이를 계산할 수 있다. 재귀나 함수 포인터가 없으면 정확한 상한을 구할 수 있어, 안전 인증에서 요구되기도 한다.
예방
✅ 큰 배열은 static 으로 (.bss 로 이동) ✅ 재귀 사용 금지 (MISRA C 규칙 17.2) ✅ 스택 크기를 워터마크 기준으로 산정 — 실측 + 여유 30~50% ✅ ISR에서 큰 지역 변수 금지 ✅ printf 계열을 임베디드용 경량 구현으로 교체 ✅ 인터럽트 중첩 깊이를 고려해 MSP 크기 결정
크기 산정 방법 — 실무 절차
- 넉넉하게 시작한다 (예: 2KB) 2. 최악의 시나리오로 오래 돌린다
-
- 모든 기능을 실행하고, 에러 경로도 태운다
- 워터마크를 확인해 실제 최대 사용량을 파악
- 실측값 + 30~50% 여유로 확정
- 양산 후에도 워터마크를 주기적으로 로깅
"돌아가니까 괜찮다"가 가장 위험하다. 평소 경로에서는 안 넘치다가 드문 에러 처리 경로에서 넘치는 일이 많다.
힙과 스택의 충돌
[.data][.bss][힙 ↓ ][빈 공간][ ↑ 스택]
둘이 만나면 → 조용히 서로를 덮어쓴다
하드웨어가 알려 주지 않는다. 링커 스크립트에서 최소 여유 공간을 검사하게 할 수 있다.
_Min_Stack_Size = 0x400; ASSERT(. + _Min_Stack_Size <= ORIGIN(RAM) + LENGTH(RAM), "스택 부족") 링크 시점에 검사하므로 빌드가 실패해 미리 알 수 있다.