Troubleshooting

ARM Cortex-M 하드폴트(Hard Fault) 디버깅: 레지스터 추적으로 원인 코드 찾는 법

임베디드 친구 2026. 6. 8. 21:01
반응형

Quick Summary (TL;DR) - For Global Developers

  • Symptom: The MCU suddenly stops executing normal code and jumps to an infinite loop inside the HardFault_Handler(), often causing the watchdog timer to reset the system.
  • Cause: Execution of illegal instructions, memory access violations (such as null pointer dereferencing or unaligned memory access), or stacking failures during exception entry.
  • Solution: Trace the Link Register (LR) to identify the active stack pointer (MSP or PSP), read the stacked CPU registers (R0-R3, R12, LR, PC, xPSR), and locate the exact crash address using the Program Counter (PC).

ARM Cortex-M HardFault_Handler 발생 증상 및 디버깅의 어려움

ARM Cortex-M 기반 MCU(Cortex-M0, M3, M4, M7 등)를 타깃으로 펌웨어를 개발하다 보면, 시스템이 아무런 동작을 하지 않고 멈추는 현상을 보게 됩니다. 디버거(J-Link, ST-Link 등)를 연결해 보면 예외 처리 벡터인 HardFault_Handler에서 무한 루프에 빠져 있는것을 보게 되지요.

에러 문맥이나 키워드를 검색하면 다음과 같은 내용이 검색됩니다.

  • Cortex-M Hard Fault debugging raw registers
  • How to find PC from HardFault_Handler
  • BusFault, UsageFault escalated to HardFault
  • Unaligned memory access crash on Cortex-M0

문제는 HardFault_Handler 내부에서는 이미 크래시가 발생한 시점의 컨텍스트(Context)가 덮어써졌거나 사라진 것처럼 보인다는 것입니다. Call Stack을 상위로 추적해도 인터럽트 진입점만 표시되고, 어떤 Task의 어떤 코드에서 예외가 발생했는지 찾기 어렵습니다.

하드폴트(Hard Fault)의 근본 원인과 MCU 내부 동작 메커니즘

ARM Cortex-M 아키텍처에서 하드폴트가 발생하는 근본 원인은 크게 세 가지가 있습니다.

  1. Usage Fault (사용 오류): 정의되지 않은 인스트럭션(Undefined Instruction)을 실행하려고 하거나, Cortex-M0/M3 등 하드웨어적으로 미지원하는 환경에서 정렬되지 않은 메모리(Unaligned Access)에 접근 할 때 발생합니다. 0으로 나누기(Divide by Zero) 설정을 켠 상태에서 나눗셈 오류가 나도 해당합니다.
  2. Bus Fault / MemManage Fault (메모리 접근 오류): 유효하지 않은 메모리 주소(예: 0x00000000 널 포인터, 혹은 주변장치 클럭이 꺼진 상태의 레지스터 주소)에 읽기/쓰기를 시도할 때 발생합니다.
  3. Fault Escalation (폴트 하이재킹): BusFault, UsageFault, MemManageFault 가 각각의 제어 레지스터(SHCSR)에서 비활성화되어 있거나, 해당 인터럽트 서비스 루틴을 처리하는 도중 또 다른 폴트가 발생하면 모두 Hard Fault로 격상(Escalation)됩니다.

ARM Core 로직은 예외가 발생한 순간 고유의 하드웨어 스태킹(Hardware Stacking) 메커니즘을 가동합니다. 현재 가동 중이던 핵심 레지스터 8개(R0-R3, R12, LR, PC, xPSR)를 메인 스택 포인터(MSP) 또는 프로세스 스택 포인터(PSP)에 자동으로 push합니다. 따라서 하드폴트 직후 스택에 저장된 이 Context Frame을 역추적하면 정확히 어떤 주소(PC)에서 크래시가 났는지 찾아낼 수 있습니다.

문제를 유발하는 잘못된 포인터 및 데이터 정렬 C 코드 (Bad Case)

아래 코드는 임베디드 환경에서 하드폴트를 유발하는 가장 대표적인 두 가지 패턴(Null Pointer Dereference, Unaligned Memory Access)을 보여줍니다.

#include <stdint.h>

typedef struct {
    uint8_t  status;
    uint32_t payload; /* Might be unaligned if the struct is packed or cast raw */
} __attribute__((packed)) Packet_t;

void trigger_hardfault_example(void) {
    /* Case 1: Null Pointer Dereferencing (Causes BusFault / HardFault) */
    volatile uint32_t *invalid_ptr = (volatile uint32_t *)0x00000000;
    *invalid_ptr = 0xDEADBEEF; 

    /* Case 2: Unaligned Access on strict hardware (e.g., Cortex-M0) */
    uint8_t buffer[8] = {0, 1, 2, 3, 4, 5, 6, 7};

    /* Forcing an unaligned 32-bit read from an odd byte address */
    volatile uint32_t *unaligned_ptr = (volatile uint32_t *)&buffer[1]; 
    volatile uint32_t value = *unaligned_ptr; 

    (void)value;
}

구조체 패딩 보존 및 방어적 코딩을 적용한 C 코드 (Good Case)

하드폴트를 방지하기 위해서는 원시 포인터 캐스팅을 지양하고, 하드웨어 특성에 맞춰 메모리 정렬(Alignment)을 유지하거나 널 체크를 선행해야 합니다.

#include <stdint.h>
#include <string.h>

typedef struct {
    uint8_t  status;
    uint32_t payload; 
} SafePacket_t; /* Compiler aligns 'payload' to a 4-byte boundary by default */

void safe_execution_example(void) {
    /* Mitigation 1: Defensive Null Pointer Check */
    volatile uint32_t *ptr = (volatile uint32_t *)0x00000000;
    if (ptr != NULL) {
        *ptr = 0xDEADBEEF;
    }

    /* Mitigation 2: Handling Unaligned Data safely using memcpy */
    uint8_t buffer[8] = {0, 1, 2, 3, 4, 5, 6, 7};
    uint32_t safe_value = 0;

    /* memcpy safely handles unaligned byte-to-word copy on any architecture */
    memcpy((void *)&safe_value, &buffer[1], sizeof(uint32_t));

    (void)safe_value;
}

핵심 수정 포인트

  • attribute((packed)) 제거 및 memcpy 활용: Cortex-M0 아키텍처나 특정 버스 환경에서는 패킹된 구조체의 32비트 멤버에 직접 접근할 때 하드폴트가 발생합니다. 바이트 단위로 복사하는 memcpy를 사용하면 컴파일러가 정렬 오류가 나지 않도록 인스트럭션을 안전하게 쪼개서 생성합니다.
  • 명시적 포인터 유효성 검증: 하드코딩된 주소나 가리키는 대상이 모호한 포인터는 반드시 NULL 또는 가용 RAM 범위 내에 있는지 검증 로직을 거칩니다.

Cortex-M 하드폴트 디버깅 및 트러블슈팅 가이드

디버거 툴(IAR EWARM, Keil MDK, STM32CubeIDE 등) 상에서 HardFault_Handler에 걸렸을 때, 레지스터 분석을 통해 PC(Program Counter)를 뽑아내는 실무 트러블슈팅 절차입니다.

Step 1. EXC_RETURN (Link Register) 값 확인을 통한 스택 판별

하드폴트 핸들러 내에 브레이크포인트가 걸렸을 때 제일 먼저 Core Register 윈도우에서 LR(Link Register) 값을 확인합니다. 인터럽트 진입 시 LR은 일반 주소가 아닌 예외 복귀 플래그인 EXC_RETURN 값으로 채워집니다.

  • 0xFFFFFFF9: 예외 발생 전 시스템이 Main Stack(MSP)을 사용 중이었음.
  • 0xFFFFFFFD: 예외 발생 전 시스템이 Process Stack(PSP)을 사용 중이었음(RTOS 환경의 Task 내부에서 터졌을 때 주로 발생).

Step 2. 해당 스택 포인터 주소로 이동하여 Context Frame 추출

만약 LR이 0xFFFFFFFD라면 디버거의 레지스터 창에서 PSP 값을 확인하고, Memory View 창에 해당 PSP 주소를 입력합니다. 하드웨어 스태킹 규칙에 따라 메모리에 다음과 같은 순서(Low Address에서 High Address 방향)로 레지스터가 저장되어 있습니다.

Offset Stacked Register Description
SP + 0x00 R0 Argument / Result
SP + 0x04 R1 Argument / Result
SP + 0x08 R2 Argument / Parameter
SP + 0x0C R3 Argument / Parameter
SP + 0x10 R12 Intra-Procedure-call scratch register
SP + 0x14 LR Link Register (Crash를 유발한 함수를 호출한 곳)
SP + 0x18 PC Program Counter (실제 에러를 유발한 정확한 코드 주소)
SP + 0x1C xPSR Execution Program Status Register

Step 3. PC 주소 추적 및 MAP 파일 비교

SP + 0x18 위치에 기록된 32비트 헥사 주소(예: 0x08002A34)를 확보합니다. 이 주소가 바로 크래시를 일으킨 인스트럭션 위치입니다.

  • 디버거의 Disassembly Window 창에 해당 주소를 입력하면 문제가 된 C 코드 라인과 어셈블리 명령어를 즉시 확인할 수 있습니다.
  • 디버거를 연결할 수 없는 양산 제품 검증 환경이라면, 빌드 결과물로 나온 .map 파일 또는 디스어셈블리 파일(.list, *.asm)을 열어 해당 주소가 어떤 함수 영역에 포함되어 있는지 맵핑하여 원인을 검거합니다.
반응형