ARM Cortex-Mx에서 예외 진입 및 종료 디코딩

발행: (2025년 12월 27일 오후 11:59 GMT+9)
14 min read
원문: Dev.to

Source: Dev.to

번역을 진행하려면 번역하고자 하는 전체 텍스트를 제공해 주세요. 현재는 소스 링크만 포함되어 있어 번역할 내용이 없습니다. 텍스트를 알려 주시면 바로 한국어로 번역해 드리겠습니다.

소개 – 이 게시물이 존재하는 이유

ARM Cortex‑M x에서 인터럽트 처리는 이론적으로는 간단해 보이지만, 디버거를 열면 혼란스러워집니다.

  • PC 값이 이유 없이 변함
  • 레지스터가 스택 메모리에 나타남
  • LR이 0xFFFFFFFD와 같은 이상한 값을 가짐
  • 일부 레지스터는 스택에 전혀 나타나지 않음

이 게시물은 실제 디버깅 스크린샷과 메모리 검사를 통해 하드웨어가 실제로 하는 일, 컴파일러가 하는 일, 그리고 디버거가 숨기는 내용을 단계별로 설명합니다.

스택 덤프와 레지스터를 살펴보기 전에 반드시 이해해야 할 한 가지:
코어는 인터럽트가 발생했을 때를 결정하고, 고정된 아키텍처 컨텍스트를 저장하며, 모드와 스택을 자동으로 전환합니다. 소프트웨어는 그 이후에만 개입합니다.

STIR이란?

  • STIRISR로 직접 점프하지 않습니다.
  • 해당 인터럽트에 대해 보류 비트만 설정합니다.
  • STIR은 하드웨어 인터럽트 라인이 HIGH로 전환되는 것과 정확히 동일하게 동작하며 — 단순히 인터럽트를 보류 상태로 표시합니다.

단계별 예외 진입

  1. NVICNVIC_ISER 레지스터에서 pending 비트를 설정합니다.
  2. CPU가 현재 실행 중인 명령을 완료합니다.
  3. 스태킹(레지스터 내용을 스택에 푸시) 및 벡터 페칭(벡터 테이블에서 핸들러 주소를 읽기).
  4. CPU:
    • 핸들러 모드로 전환합니다.
    • NVIC_IABR에서 Active 비트를 설정합니다.
    • Pending 비트를 클리어합니다.
  5. ISR이 실행을 시작합니다.
  6. 핸들러 내부의 모든 스택 작업에 MSP가 사용됩니다.

레지스터의 스태킹, 모드 전환 및 벡터 페칭은 두 명령 사이에 코어 내부에서 수행되므로, 이러한 단계는 소스 레벨 단일 스텝 디버깅 시에 보이지 않습니다.

STIR에 쓰기 할 때 흔히 하는 오해

ObservationReality
“STIR에 쓰면 ISR로 바로 점프한다.”쓰기는 오직 pending 비트를 설정할 뿐이다.
“인터럽트가 쓰기 직후 바로 발생한다.”코어는 현재 명령을 반드시 완료해야 한다.
“Pending = taken.”Pending가능함을 의미하고, taken은 코어가 ISR에 진입했음을 의미한다.

Pending 비트를 설정한 후

  • Cortex‑M 코어는 항상 현재 실행 중인 명령을 완료한다.
  • 인터럽트는 명령 경계에서만 인식되며, 명령 중간에서는 인식되지 않는다.
  • 이는 정밀하고 결정론적인 프로그램 실행을 보장한다.

결과

  • **Program Counter (PC)**는 이미 진행 중이던 명령에 대해 계속 업데이트된다.
  • 디버거는 소스 뷰에서 다음 C 문장을 강조 표시할 수 있어, 실행이 정상적으로 진행되는 것처럼 보일 수 있다.
  • 현재 명령이 끝난 후에야 예외 진입이 발생한다.

디버거 예제

스크린샷에서 디버거가 printf 문에서 멈춰 있습니다:

  1. 인터럽트가 여전히 대기 중입니다.
  2. CPU가 현재 명령을 완료하고, PC가 업데이트된 후 예외 진입이 발생합니다.

이 시점에서:

  • 예외 진입이 완료되었습니다.
  • 프로세서는 이제 핸들러 모드에서 **인터럽트 서비스 루틴 (ISR)**을 실행하고 있습니다.
  • PC가 벡터 테이블에서 로드되었습니다.
  • LREXC_RETURN 값이 들어 있습니다.
  • MSP가 활성화되어 있습니다.
  • 인터럽트가 더 이상 대기 중이 아니며, 해당 NVIC 활성 비트가 설정되었습니다.

스택 프레임 검사

ISR이 실행될 때 스택 메모리를 확인하여 프로세서가 자동으로 저장한 컨텍스트를 볼 수 있습니다.

자동으로 스택에 저장되는 레지스터

xPSR, PC, LR, R12, R3, R2, R1, R0

초기 스택 포인터

  • 인터럽트가 처리되기 전, SP = 0x2001FFE8.
  • 스택은 Full Descending 방식이며(주소가 낮은 쪽으로 성장하고, SP는 항상 마지막으로 스택에 저장된 항목을 가리킴).

하드웨어 스택핑 순서

단계감소 후 SP저장된 레지스터
10x2001FFE4xPSR
20x2001FFE0PC
30x2001FFDCLR
40x2001FFD8R12
50x2001FFD4R3
60x2001FFD0R2
70x2001FFCCR1
80x2001FFC8R0
  • 예외 진입 후, SP = 0x2001FFC8, 마지막으로 스택에 저장된 레지스터(R0)를 가리킵니다.
  • 예시: R0 = 0x0A – 레지스터 뷰와 메모리 주소 0x2001FFC8 모두에서 확인되었습니다.
  • 0x2001FFE4 위치의 값은 xPSR에 해당하며, 레이아웃이 ARM Cortex‑M 사양과 일치함을 확인시켜 줍니다.

왜 스택에서 R0–R3, R12, LR, PC, 그리고 xPSR만 보이는가?

일견 보면 뭔가 빠진 것처럼 보이지만, ARM은 누가 레지스터를 보존할 책임이 있는가에 따라 레지스터를 다르게 취급합니다.

휘발성 (Caller‑Saved) 레지스터

레지스터일반적인 사용
R0–R3, R12함수 인자, 임시 계산, 단기간에 사용되는 값
  • 이 레지스터들은 자주 변할 것으로 기대됩니다.
  • 인터럽트가 발생하면, 이러한 값들은 일시적인 경우가 많으므로 하드웨어가 보존해야 합니다.
  • 따라서 Cortex‑M 코어는 예외 진입 시 자동으로 이 레지스터들을 저장합니다.

비휘발성 (Callee‑Saved) 레지스터

레지스터일반적인 사용
R4–R11지역 변수, 루프 카운터, 포인터, 구조체, 여러 명령어에 걸쳐 살아야 하는 값
  • 소프트웨어가 이 레지스터들을 보존할 책임이 있습니다.
  • 컴파일러는 ISR이 실제로 이 레지스터들을 사용할 경우에만 R4–R11을 푸시/팝하는 코드를 생성합니다.
  • ISR이 필요로 하지 않으면 푸시되지 않아 스택 공간과 시간이 절약됩니다.

왜 ARM은 이렇게 설계했을까

  • 낮은 인터럽트 지연 – 자동으로 수행되는 작업이 최소화됩니다.
  • 최소 스택 사용 – 필수 레지스터만 저장됩니다.
  • 예측 가능한 타이밍 – 하드웨어‑정의 스택 프레임이 고정되고 빠릅니다.
  • 빠른 컨텍스트 전환 – 코어가 몇 사이클만에 ISR에 진입/종료할 수 있습니다.
  • 컴파일러 유연성 – 컴파일러가 나머지를 처리하며, ISR에 실제로 필요한 것만 푸시합니다.

TL;DR

  1. STIR → pending 비트 (즉시 점프 없음).
  2. 코어가 현재 명령을 완료한 후, 예외 진입을 수행합니다 (하드웨어 스택, 모드 전환, 벡터 가져오기).
  3. 하드웨어가 자동으로 저장 R0‑R3, R12, LR, PC, xPSR.
  4. 소프트웨어(컴파일러)가 R4‑R11 필요한 경우에만 저장.
  5. 디버거가 이러한 내부 단계를 숨길 수 있어 흐름이 이상하게 보일 수 있지만, 순서는 결정적이며 ARM Cortex‑M 아키텍처 매뉴얼에 문서화되어 있습니다.

Source:

Exception Return on Cortex‑M x

왜 하드웨어가 매번 모든 레지스터를 저장하지 않을까

하드웨어가 예외가 발생할 때마다 모든 레지스터를 저장한다면 Cortex‑M x는 훨씬 느려지고 실시간 시스템에 적합하지 않게 됩니다.

EXC_RETURN 이란?

  • 예외 진입 시 LR(링크 레지스터)에 넣어지는 특수 값.
  • 이 값을 PC에 쓰면 예외 복귀가 트리거됩니다.
  • 일반적인 복귀 주소가 아니라, 프로세서에게 예외 복귀 방법을 알려 주는 값입니다.

LR에 있는 값을 이용하는 전형적인 복귀 명령:

BX   LR
POP {PC}
LDR  PC, [addr]

중요한 참고 – 일반 C 함수 호출과 달리, 예외 메커니즘은 특수 값 EXC_RETURNLR에 저장합니다.

EXC_RETURN 인코딩

모든 EXC_RETURN 값은 비트 [31:5] = 1 입니다.
하위 몇 비트만이 복귀 동작을 설명하며, 프로세서는 이를 자동으로 해석합니다.

비트설명값 / 의미
[31:5]EXC_RETURN 서명항상 1 → 예외 복귀 값임을 식별
4부동소수점 컨텍스트1 → FP 컨텍스트가 스택에 저장되지 않음
0 → FP 컨텍스트가 스택에 저장됨 (FPU가 존재할 경우)
3복귀 모드1 → Thread 모드로 복귀
0 → Handler 모드로 복귀
2스택 포인터 선택1PSP(Process Stack Pointer) 사용
0MSP(Main Stack Pointer) 사용
1예약항상 0
0예약항상 1

예외 진입 시 일어나는 일

  • Cortex‑M 프로세서는 스택킹벡터 페칭을 하드웨어 수준에서 수행합니다.
  • 프로그램 카운터(PC) 가 갑자기 바뀐 것처럼 보이며, 개별 스택킹 단계는 보이지 않습니다.
  • 메모리 뷰에서는 저장된 레지스터들을 볼 수 있지만, 소스 뷰에서는 그렇지 않습니다.

요컨대, 디버거는 예외 진입 결과를 보여줄 뿐, 이를 일으킨 하드웨어 단계들을 각각 보여주지는 않습니다.

동작을 확인한 방법

방법관찰 내용
STIR 로 인터럽트 트리거하드웨어가 시작한 예외 진입을 확인
디버거 레지스터 뷰레지스터가 올바르게 저장됨
스택 메모리 검사고정된 예외 프레임만 저장되고, SP 가 정확히 32 바이트 이동
컴파일러 출력 검사R4–R11 은 필요할 때만 저장됨

핵심 요약: EXC_RETURN 은 Cortex‑M 코어에게 예외를 어떻게 풀어야 하는지(어떤 스택 포인터를 사용할지, 어떤 모드로 복귀할지, 부동소수점 상태가 있는지) 알려 주는 작고 하드웨어가 만든 토큰입니다. 프로세서는 모든 저수준 스택킹/언스택킹을 자동으로 처리하고, 디버거는 개별 하드웨어 단계가 아니라 최종 스택된 상태를 반영합니다.

Back to Blog

관련 글

더 보기 »

Vim

1. 커서 아래 단어와 일치하는 모든 발생 찾기 vim