임베디드 면접 용어 사전
디버깅·신뢰성

HardFault

처리되지 않은 오류로 진입하는 Cortex-M 예외. CFSR·HFSR과 스택 프레임의 복귀 주소로 원인을 추적한다.

Cortex-M에서 처리할 수 없는 오류가 발생했을 때 진입하는 예외. 임베디드 디버깅의 최전선이다.

언제 발생하나

  • ① 잘못된 메모리 접근 — NULL 포인터, 매핑되지 않은 주소
  • ② 정렬되지 않은 접근 — M0/M0+ 에서 (M3+ 는 설정에 따라)
  • 잘못된 명령어 — — 함수 포인터가 깨짐, Thumb 비트 누락
  • 0으로 나누기 — — SCB->CCR 의 DIV_0_TRP 활성 시
  • 스택 오버플로 — — 스택이 유효하지 않은 영역으로
  • 에스컬레이션 — — 다른 폴트 핸들러가 비활성이거나
    • 폴트 처리 중 또 폴트

원인 파악 — 상태 레지스터부터

SCB->HFSR    // HardFault Status
  FORCED     — 다른 폴트가 에스컬레이션됨 → CFSR 을 봐야 한다  ★
  VECTTBL    — 벡터 테이블 읽기 실패

SCB->CFSR    // Configurable Fault Status (3개가 합쳐진 32비트)
  ├ MMFSR (하위 8비트)  — MemManage: MPU 위반
  │   MMARVALID → MMFAR 에 위반 주소
  ├ BFSR  (중간 8비트)  — BusFault: 존재하지 않는 메모리
  │   BFARVALID → BFAR 에 위반 주소
  │   PRECISERR — 정확한 주소를 안다
  │   IMPRECISERR — 쓰기 버퍼 때문에 주소를 모른다  ★
  └ UFSR  (상위 16비트) — UsageFault
      UNDEFINSTR — 잘못된 명령어
      INVSTATE   — Thumb 비트 문제
      UNALIGNED  — 정렬 위반
      DIVBYZERO  — 0 나누기

★ FORCED 비트가 켜져 있으면 원래는 MemManage/BusFault/UsageFault인데 그 핸들러가 비활성이라 HardFault로 승격된 것이다. CFSR을 봐야 진짜 원인을 안다.

개별 폴트 핸들러를 활성화하면 더 정확한 진단이 가능하다.

SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk
            | SCB_SHCSR_BUSFAULTENA_Msk
            | SCB_SHCSR_USGFAULTENA_Msk;

스택 프레임에서 정보 꺼내기 — 필수 기법

void HardFault_Handler(void) {
    __asm volatile (
        "TST   LR, #4          \n"     // EXC_RETURN bit2: 0=MSP, 1=PSP
        "ITE   EQ              \n"
        "MRSEQ R0, MSP         \n"
        "MRSNE R0, PSP         \n"
        "B     hard_fault_c    \n"
    );
}

void hard_fault_c(uint32_t *sp) {
    volatile uint32_t r0  = sp[0];
    volatile uint32_t r1  = sp[1];
    volatile uint32_t r2  = sp[2];
    volatile uint32_t r3  = sp[3];
    volatile uint32_t r12 = sp[4];
    volatile uint32_t lr  = sp[5];    // 어디서 이 함수를 불렀나
    volatile uint32_t pc  = sp[6];    // ★ 폴트를 낸 명령의 주소
    volatile uint32_t psr = sp[7];

    volatile uint32_t cfsr = SCB->CFSR;
    volatile uint32_t bfar = SCB->BFAR;   // 접근하려던 주소

    __asm volatile ("BKPT #0");       // 디버거 연결 시 여기서 멈춘다
    while (1) { }
}

★ PC 값을 맵 파일이나 addr2line과 대조하면 어느 소스 줄에서 죽었는지 정확히 알 수 있다.

arm-none-eabi-addr2line -e firmware.elf 0x08001234
# → main.c:142

어느 스택인지 판단하는 것이 중요하다. LR(EXC_RETURN)의 비트 2가 0이면 MSP, 1이면 PSP다. 잘못된 스택을 뒤지면 엉뚱한 값이 나온다.

원인별 전형적 패턴

증상의심
BFAR ≈ 0x00000000NULL 포인터 역참조
BFAR가 이상한 큰 값초기화 안 된 포인터, 배열 범위 초과
PC가 이상한 값함수 포인터 손상, 스택 오버플로
INVSTATEThumb 비트 누락 (직접 계산한 주소로 점프)
UNALIGNEDpacked 구조체 멤버 접근
IMPRECISERR쓰기 버퍼 때문에 주소 불명 ★

★ IMPRECISERR 대응

// 쓰기 버퍼를 비활성화하면 정확한 주소를 얻을 수 있다 (느려지지만 디버깅용)
SCB->ACTLR |= SCB_ACTLR_DISDEFWBUF_Msk;

또는 폴트 지점 근처에 __DSB()를 넣어 쓰기를 강제 완료시킨다.

운영 중 대응 — 로그를 남기고 리셋

void hard_fault_c(uint32_t *sp) {
    // .noinit 영역에 저장 (소프트 리셋으로는 지워지지 않는다)
    fault_log.magic = 0xFA017;
    fault_log.pc    = sp[6];
    fault_log.lr    = sp[5];
    fault_log.cfsr  = SCB->CFSR;
    fault_log.bfar  = SCB->BFAR;

    NVIC_SystemReset();     // 리셋
}

// 부팅 시
if (fault_log.magic == 0xFA017) {
    report_to_server(&fault_log);
    fault_log.magic = 0;
}

현장에서 발생한 폴트를 나중에 분석할 수 있다. 디버거를 연결할 수 없는 양산 기기에서 유일한 단서가 된다.

예방

✅ MPU로 NULL 영역·스택 가드 설정 ✅ 포인터 사용 전 NULL 검사 ✅ 배열 인덱스 범위 검사 ✅ 스택 크기를 워터마크로 검증 ✅ 개별 폴트 핸들러 활성화 (더 정확한 정보) ✅ 폴트 핸들러에서 로그를 남기고 리셋

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 디버깅·신뢰성 — JTAG/SWD·HardFault(fault 레지스터)·트레이스·watchdog·EMC