21.10.3.1. 마이크로컨트롤러 크래시 덤프 구조 파악
드론이 하드 폴트(Hard Fault)를 일으키며 쇳덩이로 변해버린 직후, SD 카드 최상단에 생성된 fault_2023_10_26_15_30_00.log 파일을 메모장으로 열어보라.
그 안에는 C++ 코드나 친절한 에러 메시지 따위는 단 한 줄도 없고, 오직 16진수 숫자로 도배된 암호문만이 가득할 것이다.
[Hard Fault Log]
...
R0: 0x20005C80 R1: 0x00000000 R2: 0x12345678 R3: 0x00000001
...
PC: 0x08154A32
LR: 0x08092B14
SP: 0x2001F820
...
이 암호문은 당신의 코드가 죽기 직전, ARM Cortex-M 마이크로컨트롤러 내부의 물리적 레지스터(Register)에 저장되어 있던 모든 데이터를 순간 동결시킨 스냅샷(Snapshot)이다.
이 수많은 숫자 꾸러미 속에서 우리가 범인을 색출하기 위해 집중해야 할 핵심 단서는 정확히 세 개뿐이다: PC, LR, SP.
1. PC (Program Counter): 살해 현장의 주소
**PC (프로그램 카운터)**는 CPU가 “지금 당장 실행하고 있던 명령어(Instruction)의 메모리 주소“를 담고 있는 방이다.
위 로그에서 PC: 0x08154A32 라는 값이 찍혀있다면?
이 말은 즉슨, 펌웨어가 쭉쭉 잘 실행되다가 메모리의 저 0x08154A32 주소에 적혀있는 코드를 실행하려는 그 찰나의 순간에 펑! 하고 터져버렸다는 뜻이다.
이 16진수 주소가 바로 우리가 C++ 소스 코드에서 몇 번 줄(Line)에 널 포인터(Null Pointer)나 배열 초과 오류를 냈는지 가리키는 궁극적인 스모킹 건(Smoking Gun)이다. (이 주소를 C++ 코드로 역추적하는 addr2line 기법은 21.10.3.1.2장에서 상세히 다룬다.)
2. LR (Link Register): 나를 사지로 내몬 배후 세력
경찰이 살인 사건을 조사할 때 범행 현장(PC)만 보지 않는다. “이 사람이 죽기 직전에 누구를 만났는가?“를 알아야 한다.
**LR (링크 레지스터)**는 함수가 호출되었을 때, “이 함수가 끈나고 나서 되돌아갈 부모 함수의 주소“를 저장하는 방이다.
위 로그에서 LR: 0x08092B14 라는 값이 찍혔다.
이것은 내 코드를 죽인 직접적인 범인은 PC 주소에 있는 함수지만, **그 문제의 함수를 호출하도록 명령을 내린 상위 함수(배후 세력)**가 바로 저 LR 주소에 있다는 뜻이다.
콜 스택(Call Stack)이 완전히 깨져서 추적이 불가능할 때, 이 LR 값은 논리적 흐름(누가 누구를 불렀나)을 추적하는 데 결정적인 단서가 된다.
3. SP (Stack Pointer): 내 밥그릇의 깊이
**SP (스택 포인터)**는 현재 이 스레드가 전체 스택 메모리의 어디까지 파고 내려갔는지를 나타내는 주소다.
우리가 21.10.2.1.1장에서 스택 마진(Stack Margin) 부족이 코드를 폭파시킬 수 있다고 경고했었다. 만약 이 SP 값이 나타내는 주소가 커널이 내 모듈에게 허락한 스택 영역(예: STACK_MAIN 1200 바이트 구간)의 끝단을 아득히 넘어서 다른 구역까지 침범해 있다면, 이 추락 원인은 100% 스택 오버플로우다. 별도의 로직 에러가 아니라 지나치게 깊은 재귀 함수나 거대한 로컬 배열이 원인이라는 뜻이다.
이 세 가지 숫자(PC, LR, SP)를 SD 카드에서 캐내었다면 절반은 성공한 셈이다.
하지만 인간의 눈으로 0x08154A32 라는 16진수가 PayloadAutoDrop.cpp의 몇 번째 줄인지 알 길은 없다.
이 차가운 16진수를 부검하여 소스 코드 라인 번호로 번역해 내는 검시관의 칼날, arm-none-eabi-addr2line 유틸리티의 강력한 마법을 21.10.3.1.2장에서 휘둘러보자.