크래시 시점의 메모리와 레지스터 상태를 저장해 나중에 분석하는 것.
왜 임베디드에서 필요한가
현장에 배포된 기기가 가끔 재부팅된다
→ 디버거를 붙일 수 없다
→ 재현이 안 된다
→ 로그만으로는 원인을 모른다
"그 순간에 무슨 일이 있었는지"를 남기는 유일한 방법이다.
무엇을 저장하나 — 우선순위 순
① 폴트 정보 : CFSR, HFSR, BFAR, MMFAR (16바이트)
② 스택 프레임 : R0-R3, R12, LR, PC, xPSR (32바이트)
③ 나머지 레지스터: R4-R11, SP (36바이트)
④ 메타데이터 : 펌웨어 버전, 리셋 원인, 업타임 (수십 바이트)
⑤ 스택 일부 : 콜 스택 복원용 (256~1024바이트)
⑥ 주요 전역 변수: 시스템 상태
⑦ 최근 로그 : 링 버퍼 내용
①②만으로도 대부분의 원인을 좁힐 수 있다. 저장 공간이 부족하면 여기까지만 해도 큰 도움이 된다.
어디에 저장하나
| 위치 | 장점 | 단점 |
|---|---|---|
| .noinit RAM | 빠르다(폴트 중에도 안전) | 전원이 끊기면 사라짐 |
| RTC 백업 레지스터 | VBAT로 유지 | 매우 작다(수십 바이트) |
| 백업 SRAM | VBAT로 유지 | 용량 제한 |
| 내부 플래시 | 영구 보존 | 폴트 중 쓰기가 위험 |
| 외부 플래시/EEPROM | 용량 충분 | 통신이 필요 → 폴트 중 불안정 |
실무 조합 — 폴트 핸들러에서는 .noinit RAM에 빠르게 기록하고
리셋한 뒤, 정상 부팅한 상태에서 플래시나 서버로 옮긴다.
폴트 핸들러 안에서 플래시 쓰기나 통신을 시도하면 안 된다 — 이미 시스템이 불안정한 상태라 또 죽을 수 있다.
구현
typedef struct {
uint32_t magic;
uint32_t pc, lr, psr;
uint32_t r0, r1, r2, r3, r12;
uint32_t cfsr, hfsr, bfar, mmfar;
uint32_t uptime_ms;
char fw_version[16];
uint32_t stack_snapshot[64];
} CoreDump;
__attribute__((section(".noinit"))) volatile CoreDump g_dump;
void hard_fault_c(uint32_t *sp) {
g_dump.pc = sp[6];
g_dump.lr = sp[5];
g_dump.psr = sp[7];
g_dump.cfsr = SCB->CFSR;
g_dump.bfar = SCB->BFAR;
g_dump.uptime_ms = HAL_GetTick();
for (int i = 0; i < 64; i++) g_dump.stack_snapshot[i] = sp[i];
g_dump.magic = 0xC0DEDEAD; // ★ 마지막에 쓴다
__DSB();
NVIC_SystemReset();
}
★ magic을 마지막에 쓰는 이유 — 기록 도중에 또 죽으면 불완전한 덤프가 유효한 것처럼 보인다. 매직을 마지막에 써야 "완전히 기록됐다"는 표시가 된다.
부팅 시 회수
int main(void) {
if (g_dump.magic == 0xC0DEDEAD) {
save_to_flash(&g_dump); // 또는 서버 전송
g_dump.magic = 0; // 중복 보고 방지
}
…
}
분석 — PC를 소스 줄로
arm-none-eabi-addr2line -e firmware.elf -f -C 0x08001234
# process_data
# /src/main.c:142
해당 버전의 .elf 파일을 반드시 보관해야 한다.
버전이 다르면 주소가 전혀 다른 함수를 가리킨다.
릴리스마다 elf + map 파일을 아카이브에 저장 덤프의 fw_version 으로 어느 elf 를 쓸지 결정
이 절차가 없으면 덤프를 모아도 분석할 수 없다.
콜 스택 복원
- 스택 스냅샷에서 플래시 영역(0x08xxxxxx)에 해당하는 값들을 찾는다
- 그것들이 반환 주소일 가능성이 높다
- addr2line 으로 각각을 해석하면 대략적인 호출 경로가 나온다
정확하지는 않지만(데이터가 우연히 그 범위일 수 있다) 실무에서 상당히 유용하다.
리셋 원인도 함께
uint32_t csr = RCC->CSR;
if (csr & RCC_CSR_IWDGRSTF) reason = WATCHDOG;
if (csr & RCC_CSR_SFTRSTF) reason = SOFTWARE; // 우리가 낸 리셋
if (csr & RCC_CSR_PORRSTF) reason = POWER_ON;
if (csr & RCC_CSR_PINRSTF) reason = RESET_PIN;
RCC->CSR |= RCC_CSR_RMVF;
덤프가 없는데 워치독 리셋이 기록됐다면 폴트가 아니라 무한 루프나 데드락이다. 원인 분류의 첫 갈래가 된다.
통계로 활용
같은 PC 값이 반복되면 → 특정 버그 다양한 PC 값이 흩어지면 → 메모리 손상, 스택 오버플로, 하드웨어 문제 특정 기기에서만 발생 → 하드웨어 불량 여러 기기의 덤프를 모아 패턴을 보는 것이 현장 문제 해결의 핵심이다.