
안녕하세요, 10년 차 개발자 김현수입니다.
임베디드 개발, 특히 펌웨어 레벨을 만지다 보면 정말 등골이 서늘해지는 순간들이 있습니다.
코드는 분명 완벽한데 보드가 먹통이 되거나, 특정 메모리 영역에 접근하자마자 하드폴트(HardFault)가 뜨면서 죽어버리는 상황이죠.
최근에 저도 후배랑 Arm Cortex-M33 기반의 MCU를 다루다가 비슷한 일을 겪었습니다.
"선배님, 이거 왜 자꾸 리셋되나요? 데이터시트대로 다 했는데..."
옆에서 울상이 된 후배를 보는데, 문득 예전 제 모습이 겹쳐 보이더라고요.
이럴 때 가장 먼저 의심해야 할 범인이 하나 있습니다.
바로 TrustZone, 그러니까 보안 상태(Security State)입니다.
Armv8-M 아키텍처가 보편화되면서 이제는 MCU에서도 보안이 정말 중요해졌죠.
문제는 내가 지금 실행하고 있는 코드가 '보안(Secure)' 상태인지, '비보안(Non-Secure)' 상태인지 눈에 보이지 않는다는 겁니다.
마치 안개 속에서 운전하는 기분이랄까요.
TrustZone이라는 거대한 벽
Arm Cortex-M33 같은 최신 코어들은 메모리를 두 개의 세상으로 나눕니다.
Secure World와 Non-Secure World.
이걸 TrustZone이라고 부르죠.
간단히 말하면, 중요한 비밀번호나 암호화 키는 Secure 구역에 숨겨두고, 일반적인 애플리케이션 코드는 Non-Secure 구역에서 돌리는 겁니다.

그런데 디버깅을 하다 보면 지금 코어(PE, Processing Element)가 어느 세상에 있는지 헷갈릴 때가 정말 많습니다.
Secure 상태에서만 접근 가능한 레지스터를 Non-Secure 상태에서 건드리면?
바로 뻗어버립니다.
그래서 우리는 디버거, 그중에서도 개발자들의 영원한 친구 GDB를 통해 현재 상태를 확인해야 합니다.
하지만 GDB 명령어가 한두 개도 아니고, 매번 레지스터 비트 하나하나 세어가며 확인하기엔 우리 눈이 너무 아프잖아요?
스택 포인터(SP)는 답을 알고 있다
자, 여기서 10년 짬바의 꿀팁, 아니 사실은 Arm 아키텍처의 기본 원리를 이용한 트릭 하나 들어갑니다.
우리가 흔히 쓰는 스택 포인터(SP, R13 레지스터)를 자세히 들여다보는 겁니다.
Armv8-M에서는 보안 확장이 적용되면서 스택 포인터도 분신술을 씁니다.
원래 있던 MSP(Main Stack Pointer), PSP(Process Stack Pointer)가 각각 보안(Secure)과 비보안(Non-Secure) 버전으로 쪼개집니다.
- MSP_S: Secure용 메인 스택 포인터
- MSP_NS: Non-Secure용 메인 스택 포인터
- PSP_S: Secure용 프로세스 스택 포인터
- PSP_NS: Non-Secure용 프로세스 스택 포인터
이렇게 4가지나 생겨버리죠. 복잡하죠?
하지만 핵심은 '현재 활성화된 SP가 누구냐'입니다.
현재 코어가 Non-Secure 상태라면, 현재 SP는 무조건 `*_NS`로 끝나는 녀석과 값이 같아야 합니다.
반대로 Secure 상태라면, `*_S`로 끝나는 녀석과 값이 같겠죠.
GDB로 1초 만에 확인하는 법
자, 이제 터미널을 열고 GDB에 딱 한 줄만 쳐보세요.
i r sp psp_ns msp_ns psp_s msp_s
(해석하자면: info registers 명령어로 현재 SP와 4가지 스택 포인터 변형들의 값을 다 보여줘!)
그럼 결과가 이렇게 쫘라락 나옵니다.
sp 0x20004000 0x20004000
psp_ns 0x2001ad40 0x2001ad40
msp_ns 0x0 0x0
psp_s 0x0 0x0
msp_s 0x20004000 0x20004000자, 숨은그림찾기 시작입니다.
맨 위의 `sp` 값(0x20004000)과 똑같은 값을 가진 녀석이 누구인가요?
네, 맨 아래 `msp_s`입니다.
그렇다면 결론은?
"아, 지금 이 프로세서는 Secure 상태(Secure State)에 있구나!"
단박에 알 수 있죠.
이 상황은 보통 리셋 직후에 많이 봅니다. Cortex-M33은 리셋되면 기본적으로 Secure 상태로 시작하거든요.
조금 더 깊이: RTOS가 돌고 있다면?
만약 Zephyr나 FreeRTOS 같은 RTOS 위에서 애플리케이션 코드가 돌고 있는 상황이라면 어떨까요?
보통 `main` 함수 쯤에서 브레이크포인트를 걸고 똑같이 확인해 봅니다.
sp 0x2001ad40 0x2001ad40
psp_ns 0x2001ad40 0x2001ad40
msp_ns 0x20019c30 0x20019c30
psp_s 0x20001eb8 0x20001eb8
msp_s 0x20000bf8 0x20000bf8이번에는 `sp`가 `psp_ns`랑 값이 똑같네요?
그럼 "Non-Secure 상태이면서, 스레드 모드(Thread Mode)에서 돌고 있구나"라고 해석하면 됩니다.
(참고로 `CONTROL` 레지스터의 `SPSEL` 비트가 1이면 PSP를 쓴다는 뜻인데, 보통 RTOS의 태스크들은 PSP를 쓰니까요.)
도구보다는 원리를 이해하는 힘
사실 `xPSR` 레지스터를 까보거나 다른 복잡한 명령어를 써서 확인할 수도 있습니다.
하지만 급박한 디버깅 현장에서는 이렇게 직관적인 비교가 훨씬 빠르고 강력합니다.
저도 신입 시절에는 무조건 복잡한 장비, 비싼 디버거가 있어야만 해결할 수 있다고 믿었습니다.
하지만 시간이 지날수록 느끼는 건, 결국 기본기가 튼튼해야 도구도 제대로 부릴 수 있다는 점입니다.
스택 포인터가 왜 나뉘었는지, 프로세서가 언제 어떤 스택을 쓰는지 이해하고 있다면, GDB 명령어 한 줄로도 수십 분의 삽질을 줄일 수 있으니까요.
오늘도 영문 모를 하드폴트와 싸우고 계신 모든 임베디드 개발자분들.
커피 한 잔 하시면서, 침착하게 `sp` 값을 한번 찍어보는 건 어떨까요?
의외로 범인은 아주 가까운 곳에 숨어있을지도 모릅니다.


