그라운드 임베디드 EN 유튜브 구독

노트 / 컴퓨터구조

스택과 힙: 함수 호출이 쌓이는 곳

함수를 부를 때마다 쌓이는 스택, 런타임에 빌려 쓰는 힙. 임베디드에서 스택 넘침과 힙 사용을 조심하는 이유.

GROUND EMBEDDED · 정리 노트컴퓨터구조 07스택과 힙 1void b(void) { uint8_t buf[64]; /* ... …// 64… 2void a(void) { uint32_t x; b(); } 3int main(void) { a(); }

스택

함수를 호출하면 돌아갈 주소, 저장해야 할 레지스터, 지역 변수가 스택에 쌓이고 함수가 끝나면 걷힙니다. Cortex-M의 스택은 높은 주소에서 낮은 주소로 자랍니다.

  • 지역 배열 uint8_t buf[512];는 스택 512바이트를 씁니다.
  • 인터럽트가 걸리면 레지스터 8개(32바이트, FPU 사용 시 더 많이)가 자동으로 쌓입니다.
  • 함수 호출이 깊어지거나 인터럽트가 중첩되면 스택이 더 깊어집니다.

스택 넘침

스택이 정해진 영역을 넘으면 그 아래에 있는 전역 변수나 힙을 덮어씁니다. 증상은 "가끔 변수 값이 이상해진다", "특정 기능을 켜면 하드폴트"처럼 원인과 멀리 떨어져 나타납니다. 스택 영역을 0xA5 같은 패턴으로 채워 두고 나중에 얼마나 지워졌는지 보면 최대 사용량을 잴 수 있습니다.

힙

malloc으로 런타임에 빌리는 영역입니다. 임베디드에서는 단편화, 실패 처리, 실행 시간이 예측되지 않는 문제 때문에 초기화 때만 쓰거나 아예 쓰지 않고, 대신 정적 배열이나 메모리 풀을 씁니다.

영역 크기 결정 위험
스택 링커 스크립트 넘침
힙 링커 스크립트 단편화, 할당 실패
정적(.data/.bss) 컴파일 시 없음 (맵 파일로 확인)

그림으로 보기: RAM은 이렇게 나뉜다

0x20000000.data .bss전역 변수그 위힙 ↑malloc빈 공간여유여기가 0이 되면 충돌0x20002000스택 ↓지역 변수, 복귀 주소
Cortex-M의 일반적인 RAM 배치. 스택은 높은 주소에서 아래로, 힙은 낮은 주소에서 위로 자랍니다. 둘이 만나면 메모리가 깨집니다.

함수 호출 한 번에 쌓이는 것

void b(void) { uint8_t buf[64]; /* ... */ }   // 64바이트 + 저장할 레지스터
void a(void) { uint32_t x; b(); }
int main(void) { a(); }

main → a → b로 들어갈 때마다 복귀 주소, 보존해야 할 레지스터, 지역 변수가 스택에 쌓였다가 돌아올 때 사라집니다. 여기에 인터럽트가 겹치면 그 순간의 스택 위에 핸들러 몫이 더 쌓입니다. 그래서 필요한 스택 크기는 "가장 깊은 호출 경로 + 중첩될 수 있는 인터럽트 전부"입니다.

무료 전자책이 주제를 한 권으로 이어 읽기 (PDF 3권)→

스택이 얼마나 쓰였는지 재는 법

시작할 때 스택 영역을 정해진 값으로 채워 두고, 나중에 그 값이 남아 있는 곳까지를 세는 방법이 가장 간단합니다.

extern uint32_t _sstack, _estack;            // 링커 스크립트의 심볼

void stack_paint(void) {
    for (uint32_t *p = &_sstack; p < (uint32_t *)__get_MSP() - 8; p++) *p = 0xDEADBEEF;
}
uint32_t stack_free_bytes(void) {
    uint32_t *p = &_sstack;
    while (*p == 0xDEADBEEF) p++;
    return (uint32_t)((uint8_t *)p - (uint8_t *)&_sstack);
}

한참 돌린 뒤 남은 양이 전체의 20%보다 적으면 스택을 늘립니다. GCC의 -fstack-usage 옵션은 함수마다 쓰는 스택 크기를 파일로 내 줍니다.

힙을 피하는 이유

  • 단편화: 할당과 해제를 반복하면 빈 공간이 잘게 쪼개져, 합계는 남는데 큰 덩어리를 못 잡는 순간이 옵니다. 몇 달씩 켜 두는 장치에서는 치명적입니다.
  • 시간이 일정하지 않음: malloc이 걸리는 시간은 그때그때 다릅니다.
  • 실패 처리: NULL이 돌아왔을 때 할 수 있는 일이 마땅치 않습니다.

그래서 임베디드에서는 전역이나 정적 버퍼, 고정 크기 메모리 풀을 주로 쓰고, 힙은 초기화 때 한 번만 잡거나 아예 쓰지 않습니다.

스택 넘침의 증상

전역 변수가 이유 없이 바뀌거나, 특정 함수에 들어가면 하드폴트가 나거나, 인터럽트가 잦을 때만 죽습니다. 큰 지역 배열과 깊은 재귀가 주범입니다.

영상으로 이어 보기회로와 하드웨어 시리즈 8편전자제품 속 회로 소자는 어떻게 동작할까? · 저항 · 커패시터 · 인덕터 완벽 정리FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →