Advanced Debugging & Observability

코어 덤프 분석 및 레지스터 스택 프레임 복원 방법: ARM Cortex-M 하드폴트 역추적 가이드

임베디드 친구 2026. 6. 30. 21:04
반응형

내용 요약

현상: 예기치 못한 HardFault 크래시 혹은 HardFault_Handler에서 멈추는 현상.

원인: 유효하지 않은 메모리 참조 또는 잘못된 함수 포인터 호출로 Stack Frame을 Push한 상태로 실시간 연결이 끊어지는 문제.

해결: 스택에 저장된 PC(Program Counter)와 LR(Link Register) 레지스터의 컨텍스트를 Core Dump 메모리 버퍼에서 역추적.

펌웨어 크래시와 코어 덤프 메모리 유실 증상 (Firmware Crash & Core Dump Memory Loss Symptoms)

임베디드 장치가 필드에 배포된 이후 발생하는 간헐적인 시스템 다운(System Lock-up) 현상은 실시간 디버거(J-Link, ST-Link)를 연결할 수 없어 원인 파악이 어렵습니다. 시스템이 예기치 않게 HardFault_Handler 무한 루프에 진입하거나, 구글 및 해외 포럼에서 빈번하게 검색되는 Imprecise Data Bus Error, UsageFault: UNALIGNED, 또는 Kernel Panic 증상을 마주할 때가 대표적입니다.

디버거 없이 독립 구동(Standalone) 중인 MCU에서 크래시가 발생하면, 코어는 먹통이 되고 실시간 변수나 콜 스택(Call Stack)을 확인할 길이 없습니다. 이때 유일한 해결책은 크래시 순간의 내부 RAM 상태를 플래시 메모리나 파일 시스템에 기록하는 Core Dump 기법입니다. 그러나 파일에 기록된 이진 데이터(Raw Binary Data)에서 Stack Frame의 구조를 올바르게 복원하지 못하면, 덤프 파일 파일 자체가 무용지물이 되어 간헐성 메모리 오염 버그를 영구적으로 놓치게 됩니다.

ARM Cortex-M Stack Frame 구조와 코어 덤프 복원의 근본적인 원인 분석 (Why Core Dump Stack Frame Recovery Matters)

필드에서 수집한 Core Dump 파일로 원인을 찾으려면 ARM Cortex-M 코어의 하드웨어 예외 처리(Hardware Exception Processing) 메커니즘을 이해해야 합니다.
Cortex-M Core는 HardFaultBusFault 같은 하드웨어 예외가 발생하는 순간, 현재 수행 중이던 실행 컨텍스트를 보호하기 위해 하드웨어적으로 특정 레지스터 세트를 스택(MSP 또는 PSP)에 자동 푸시합니다. 이를 Stack Frame이라고 하며, 구조는 다음과 같이 고정밀 표준 규격(AAPCS)을 따릅니다.
$$\text{Stack Frame} = {R0, R1, R2, R3, R12, LR, PC, xPSR}$$

  • PC (Program Counter): 하드폴트를 유발한 직전 또는 해당 지점의 실제 명령어 주소.
  • LR (Link Register): 예외가 발생한 함수를 호출한 부모 함수의 복귀 주소.
  • xPSR (Execution Program Status Register): 크래시 당시의 프로세서 상태 및 인터럽트 번호.

문제는 Compiler Optimization이 활성화되면, 로컬 변수들이 최적화되어 레지스터에만 상주하다가 스택 프레임이 푸시될 때 원본 값이 왜곡되거나 유실될 수 있습니다. 또한, 스택 포인터(SP) 자체가 오염되는 경우 하드웨어는 엉뚱한 메모리에 스택 프레임을 쌓게 됩니다.
따라서 Core Dump 파싱 시, 단순 메모리 덤프에서 현재 SP의 위치를 찾아내고 역으로 8개 워드(32바이트) 밀려 들어간 데이터 블록 속에서 PCLR 값을 바이너리 레벨에서 정확히 추출해 내야만 컴파일러가 생성한 빌드 기호 파일(.MAP 또는 .elf)과 대조하여 오류 발생 코드를 역산할 수 있습니다.

잘못된 함수 포인터 참조로 하드폴트를 유발하는 C 코드 예시 (Bad Case)

아래 코드는 초기화되지 않은 함수 포인터를 호출하여 잘못된 분기(Branch)를 발생시키고, 시스템을 즉시 HardFault 상태로 진입하게 만드는 전형적인 불량 소스 코드입니다.

#include <stdint.h>
#include <stddef.h>

/* Type definition for a volatile task function pointer */
typedef void (*TaskFunction_t)(void);

typedef struct {
    uint32_t task_id;
    TaskFunction_t run_func; /* Can be NULL if not properly initialized */
} DeviceTask_t;

void process_system_task(DeviceTask_t* task) {
    /* BAD CASE: Missing NULL pointer verification before jumping to the address */
    /* This will force the PC to branch into an invalid memory or zero address */
    task->run_func(); 
}

int main(void) {
    DeviceTask_t my_faulty_task;
    my_faulty_task.task_id = 0x10203040;
    my_faulty_task.run_func = NULL; /* Explicitly uninitialized */

    while(1) {
        process_system_task(&my_faulty_task);
    }
}

방어적 예외 처리 및 코어 덤프용 스택 정보 보존 C 코드 (Good Case)

함수 호출 전 방어 코드를 추가하고, 만약 예외가 발생하더라도 Naked Assembly 구조를 통해 크래시 당시의 SP(Stack Pointer) 위치와 레지스터 컨텍스트를 명확히 보존하여 안전하게 Core Dump로 넘겨주는 구조입니다.

#include <stdint.h>
#include <stddef.h>

typedef void (*TaskFunction_t)(void);

typedef struct {
    uint32_t task_id;
    TaskFunction_t run_func;
} DeviceTask_t;

/* Global buffer reserved to preserve the exact context during a crash */
volatile uint32_t crash_dump_buffer[8];

void save_core_dump_context(uint32_t *stack_address) {
    /* Extracting the hardware stack frame values mapped by the MCU */
    uint32_t r0  = stack_address[0];
    uint32_t r1  = stack_address[1];
    uint32_t r2  = stack_address[2];
    uint32_t r3  = stack_address[3];
    uint32_t r12 = stack_address[4];
    uint32_t lr  = stack_address[5]; /* Return address of the parent function */
    uint32_t pc  = stack_address[6]; /* Precise address where the fault triggers */
    uint32_t psr = stack_address[7];

    crash_dump_buffer[0] = r0;
    crash_dump_buffer[1] = r1;
    crash_dump_buffer[2] = r2;
    crash_dump_buffer[3] = r3;
    crash_dump_buffer[4] = r12;
    crash_dump_buffer[5] = lr;
    crash_dump_buffer[6] = pc;
    crash_dump_buffer[7] = psr;

    /* Write crash_dump_buffer to Non-Volatile Memory (Flash/EEPROM) here */
    while (1) {
        __asm volatile("bkpt #0"); /* Halt debugger or trigger watchdog reset */
    }
}

/* Naked function to guarantee that the stack pointer is not altered by compiler optimization */
__attribute__((naked)) void HardFault_Handler(void) {
    __asm volatile (
        "tst lr, #4 \n"               /* Check which stack pointer was in use (MSP vs PSP) */
        "ite eq \n"
        "mrseq r0, msp \n"            /* If equal, Main Stack Pointer (MSP) was used */
        "mrsne r0, psp \n"            /* If not equal, Process Stack Pointer (PSP) was used */
        "b save_core_dump_context \n" /* Pass SP pointer as the first argument (R0) */
    );
}

void process_system_task(DeviceTask_t* task) {
    /* GOOD CASE: Defensive check to prevent invalid branch branching */
    if (task != NULL && task->run_func != NULL) {
        task->run_func();
    }
}

핵심 수정 포인트 (Key Implementation Details)

  • attribute((naked)) 사용: 컴파일러가 예외 핸들러 진입 시 자체적으로 프롤로그(Prologue) 코드를 생성하여 SP를 변경하는 것을 방지합니다. 크래시 시점의 스택을 100% 무결하게 보존합니다.
  • MSP/PSP 분기 판단 (tst lr, #4): 코어가 스레드 모드(Thread Mode)에서 PSP를 썼는지, 핸들러 모드(Handler Mode)에서 MSP를 썼는지 감지하여 정확한 덤프 타깃 스택 주소를 추출합니다.
  • 방어적 NULL 체크: 상위 어플리케이션 단에서 오염된 포인터를 사전 차단하여 HardFault 예외 발생 빈도 자체를 낮춥니다.

엔지니어를 위한 코어 덤프 가상 복원 및 트러블슈팅 가이드 (Debugging Tips)

필드에서 수집한 메모리 덤프 이진 파일 데이터만 가지고 소스 코드의 범인을 특정하는 실무 분석 노하우입니다.

  • 스택 프레임 오프셋 매핑 (Stack Frame Offset Mapping): 덤프된 메모리 파일에서 추출한 SP 주소의 시작점으로부터 $+24$바이트 오프셋 위치가 LR, $+28$바이트 오프셋 위치가 PC 레지스터 값입니다. 기호 스크립트가 없을 때는 이 헥사(Hex) 값을 수동으로 추출하십시오.
  • 포스트 모템 ELF 기호 매핑 (Post-Mortem Symbol Lookup): 획득한 PC 주소(예: 0x080041A2)를 가지고 개발 PC의 터미널에서 툴체인의 addr2line 명령어를 실행합니다.
arm-none-eabi-addr2line -e your_firmware.elf -f -e 0x080041A2

명령어를 수행하면 컴파일러 최적화(Compiler Optimization)가 걸려있더라도 에러를 발생시킨 파일명과 정확한 소스 코드 라인 넘버가 즉각 반환됩니다.

  • MAP 파일을 이용한 링커 섹션 검증 (Linker Map File Verification): 덤프된 PC 및 LR 값이 코드 영역(Flash Text Section)을 벗어나 SRAM 영역이나 베터리 백업 도메인 등의 엉뚱한 주소를 가리키고 있다면, 이는 Stack Overflow로 인해 복귀 주소(Return Address) 자체가 훼손되었음을 의미하는 강력한 증거입니다. 이때는 MAP 파일 내부의 변수 할당 주소를 확인하여 해당 스택 인접 영역의 버퍼 경계를 전수조사해야 합니다.
반응형