스택이 지금까지 최대 어디까지 사용됐는지 측정하는 기법 (stack high-water mark).
원리 — 패턴을 채우고 남은 것을 센다
① 스택 영역 전체를 특정 패턴으로 채운다 (예: 0xA5A5A5A5)
[0xA5][0xA5][0xA5][0xA5][0xA5][0xA5][0xA5][0xA5]
↑ 스택 바닥 스택 꼭대기 ↑
② 프로그램을 충분히 실행한다
[0xA5][0xA5][0xA5][데이터][데이터][데이터][데이터][데이터]
↑ 여기까지 사용됐다
③ 바닥부터 세어 패턴이 그대로 남아 있는 길이 = 한 번도 안 쓴 여유
"현재 사용량"이 아니라 "역대 최대 사용량" 을 알려 준다는 것이 핵심이다. 스택은 함수가 반환하면 값이 남아 있어도 논리적으로는 비므로, 현재 SP만 보면 최악을 알 수 없다.
FreeRTOS에서
UBaseType_t remaining = uxTaskGetStackHighWaterMark(task_handle);
// 반환값 단위: 워드 (4바이트). 남은 여유 공간
if (remaining < 50) {
log_warn("태스크 스택 부족: %lu 워드", remaining);
}
// 모든 태스크를 주기적으로 점검
void monitor_task(void *p) {
for (;;) {
for (int i = 0; i < TASK_COUNT; i++) {
UBaseType_t hw = uxTaskGetStackHighWaterMark(tasks[i]);
if (hw < THRESHOLD) report(i, hw);
}
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
양산 펌웨어에도 남겨 두고 원격으로 수집하면 현장에서의 실제 사용량을 알 수 있다.
베어메탈에서 직접 구현
extern uint32_t _sstack, _estack;
#define PATTERN 0xA5A5A5A5
// 부팅 직후 (main 진입 전이 이상적)
void stack_paint(void) {
uint32_t *p = &_sstack;
uint32_t *sp = (uint32_t *)__get_MSP();
while (p < sp - 16) *p++ = PATTERN; // 현재 SP 아래까지만
}
uint32_t stack_unused(void) {
uint32_t *p = &_sstack;
while (*p == PATTERN && p < &_estack) p++;
return (uint32_t)(p - &_sstack) * 4; // 바이트
}
주의 — 칠하는 시점에 이미 스택을 쓰고 있으므로 현재 SP 아래만 칠해야 한다. 자기 자신의 스택 프레임을 덮으면 죽는다.
왜 필요한가 — 스택 크기 산정의 유일한 실증 방법
- 정적 분석(-fstack-usage) — : 이론적 상한. 함수 포인터·재귀가 있으면 부정확
- 실측(워터마크) — : 실제로 일어난 최악. 테스트하지 않은 경로는 못 잡는다
둘 다 필요하다. 정적 분석으로 상한을 구하고, 워터마크로 실제를 확인한 뒤 여유를 더한다.
산정 절차
- 넉넉하게 시작 (2KB) 2. 모든 기능을 실행하는 테스트를 오래 돌린다
- ★ 정상 경로뿐 아니라 에러 처리 경로까지 반드시 태운다
- 워터마크 확인 → 실제 최대 사용량 파악
- 실제 사용량 + 30~50% 여유로 확정
- 양산 후에도 주기적으로 로깅
★ 2번이 중요하다. 평소에는 안 넘치다가
드물게 실행되는 에러 처리에서 넘치는 경우가 많다.
printf("%f") 같은 것이 에러 경로에만 있으면 놓치기 쉽다.
놓칠 수 있는 것 — 인터럽트 중첩
- 워터마크는 그 태스크가 실행 중일 때 발생한 인터럽트만 반영한다
- 최악의 중첩 조합이 테스트 중에 발생하지 않았다면 잡히지 않는다
인터럽트 중첩 깊이 × 32바이트(FPU면 104바이트) 를 계산으로 더해 두는 것이 안전하다.
MPU 가드와 함께 쓴다
워터마크 : 얼마나 여유가 있는지 알려 준다 (예방) MPU 가드 : 넘치는 순간 잡는다 (검출)
둘은 역할이 다르다. 워터마크는 사후 통계이므로 실제로 넘치는 순간을 놓칠 수 있다. MPU 가드가 확정적 검출을 담당한다.
힙에도 같은 개념
xPortGetMinimumEverFreeHeapSize(); // 역대 최저 여유 힙
"역대 최저"를 보는 것이 핵심이라는 점이 동일하다. 현재 여유가 넉넉해도 피크에서 바닥을 쳤다면 위험하다.