ARM Cortex-Mx에서 예외 진입 및 종료 디코딩
Source: Dev.to
번역을 진행하려면 번역하고자 하는 전체 텍스트를 제공해 주세요. 현재는 소스 링크만 포함되어 있어 번역할 내용이 없습니다. 텍스트를 알려 주시면 바로 한국어로 번역해 드리겠습니다.
소개 – 이 게시물이 존재하는 이유
ARM Cortex‑M x에서 인터럽트 처리는 이론적으로는 간단해 보이지만, 디버거를 열면 혼란스러워집니다.
- PC 값이 이유 없이 변함
- 레지스터가 스택 메모리에 나타남
- LR이
0xFFFFFFFD와 같은 이상한 값을 가짐 - 일부 레지스터는 스택에 전혀 나타나지 않음
이 게시물은 실제 디버깅 스크린샷과 메모리 검사를 통해 하드웨어가 실제로 하는 일, 컴파일러가 하는 일, 그리고 디버거가 숨기는 내용을 단계별로 설명합니다.
스택 덤프와 레지스터를 살펴보기 전에 반드시 이해해야 할 한 가지:
코어는 인터럽트가 발생했을 때를 결정하고, 고정된 아키텍처 컨텍스트를 저장하며, 모드와 스택을 자동으로 전환합니다. 소프트웨어는 그 이후에만 개입합니다.
STIR이란?
- STIR은 ISR로 직접 점프하지 않습니다.
- 해당 인터럽트에 대해 보류 비트만 설정합니다.
- STIR은 하드웨어 인터럽트 라인이 HIGH로 전환되는 것과 정확히 동일하게 동작하며 — 단순히 인터럽트를 보류 상태로 표시합니다.
단계별 예외 진입
- NVIC가
NVIC_ISER레지스터에서 pending 비트를 설정합니다. - CPU가 현재 실행 중인 명령을 완료합니다.
- 스태킹(레지스터 내용을 스택에 푸시) 및 벡터 페칭(벡터 테이블에서 핸들러 주소를 읽기).
- CPU:
- 핸들러 모드로 전환합니다.
NVIC_IABR에서 Active 비트를 설정합니다.- Pending 비트를 클리어합니다.
- ISR이 실행을 시작합니다.
- 핸들러 내부의 모든 스택 작업에 MSP가 사용됩니다.
레지스터의 스태킹, 모드 전환 및 벡터 페칭은 두 명령 사이에 코어 내부에서 수행되므로, 이러한 단계는 소스 레벨 단일 스텝 디버깅 시에 보이지 않습니다.
STIR에 쓰기 할 때 흔히 하는 오해
| Observation | Reality |
|---|---|
| “STIR에 쓰면 ISR로 바로 점프한다.” | 쓰기는 오직 pending 비트를 설정할 뿐이다. |
| “인터럽트가 쓰기 직후 바로 발생한다.” | 코어는 현재 명령을 반드시 완료해야 한다. |
| “Pending = taken.” | Pending은 가능함을 의미하고, taken은 코어가 ISR에 진입했음을 의미한다. |
Pending 비트를 설정한 후
- Cortex‑M 코어는 항상 현재 실행 중인 명령을 완료한다.
- 인터럽트는 명령 경계에서만 인식되며, 명령 중간에서는 인식되지 않는다.
- 이는 정밀하고 결정론적인 프로그램 실행을 보장한다.
결과
- **Program Counter (PC)**는 이미 진행 중이던 명령에 대해 계속 업데이트된다.
- 디버거는 소스 뷰에서 다음 C 문장을 강조 표시할 수 있어, 실행이 정상적으로 진행되는 것처럼 보일 수 있다.
- 현재 명령이 끝난 후에야 예외 진입이 발생한다.
디버거 예제
스크린샷에서 디버거가 printf 문에서 멈춰 있습니다:
- 인터럽트가 여전히 대기 중입니다.
- CPU가 현재 명령을 완료하고, PC가 업데이트된 후 예외 진입이 발생합니다.
이 시점에서:
- 예외 진입이 완료되었습니다.
- 프로세서는 이제 핸들러 모드에서 **인터럽트 서비스 루틴 (ISR)**을 실행하고 있습니다.
- PC가 벡터 테이블에서 로드되었습니다.
- LR에
EXC_RETURN값이 들어 있습니다. - MSP가 활성화되어 있습니다.
- 인터럽트가 더 이상 대기 중이 아니며, 해당 NVIC 활성 비트가 설정되었습니다.
스택 프레임 검사
ISR이 실행될 때 스택 메모리를 확인하여 프로세서가 자동으로 저장한 컨텍스트를 볼 수 있습니다.
자동으로 스택에 저장되는 레지스터
xPSR, PC, LR, R12, R3, R2, R1, R0
초기 스택 포인터
- 인터럽트가 처리되기 전, SP = 0x2001FFE8.
- 스택은 Full Descending 방식이며(주소가 낮은 쪽으로 성장하고, SP는 항상 마지막으로 스택에 저장된 항목을 가리킴).
하드웨어 스택핑 순서
| 단계 | 감소 후 SP | 저장된 레지스터 |
|---|---|---|
| 1 | 0x2001FFE4 | xPSR |
| 2 | 0x2001FFE0 | PC |
| 3 | 0x2001FFDC | LR |
| 4 | 0x2001FFD8 | R12 |
| 5 | 0x2001FFD4 | R3 |
| 6 | 0x2001FFD0 | R2 |
| 7 | 0x2001FFCC | R1 |
| 8 | 0x2001FFC8 | R0 |
- 예외 진입 후, 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
- STIR → pending 비트 (즉시 점프 없음).
- 코어가 현재 명령을 완료한 후, 예외 진입을 수행합니다 (하드웨어 스택, 모드 전환, 벡터 가져오기).
- 하드웨어가 자동으로 저장 R0‑R3, R12, LR, PC, xPSR.
- 소프트웨어(컴파일러)가 R4‑R11 필요한 경우에만 저장.
- 디버거가 이러한 내부 단계를 숨길 수 있어 흐름이 이상하게 보일 수 있지만, 순서는 결정적이며 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_RETURN을 LR에 저장합니다.
EXC_RETURN 인코딩
모든 EXC_RETURN 값은 비트 [31:5] = 1 입니다.
하위 몇 비트만이 복귀 동작을 설명하며, 프로세서는 이를 자동으로 해석합니다.
| 비트 | 설명 | 값 / 의미 |
|---|---|---|
| [31:5] | EXC_RETURN 서명 | 항상 1 → 예외 복귀 값임을 식별 |
| 4 | 부동소수점 컨텍스트 | 1 → FP 컨텍스트가 스택에 저장되지 않음 0 → FP 컨텍스트가 스택에 저장됨 (FPU가 존재할 경우) |
| 3 | 복귀 모드 | 1 → Thread 모드로 복귀 0 → Handler 모드로 복귀 |
| 2 | 스택 포인터 선택 | 1 → PSP(Process Stack Pointer) 사용 0 → MSP(Main Stack Pointer) 사용 |
| 1 | 예약 | 항상 0 |
| 0 | 예약 | 항상 1 |
예외 진입 시 일어나는 일
- Cortex‑M 프로세서는 스택킹과 벡터 페칭을 하드웨어 수준에서 수행합니다.
- 프로그램 카운터(PC) 가 갑자기 바뀐 것처럼 보이며, 개별 스택킹 단계는 보이지 않습니다.
- 메모리 뷰에서는 저장된 레지스터들을 볼 수 있지만, 소스 뷰에서는 그렇지 않습니다.
요컨대, 디버거는 예외 진입 결과를 보여줄 뿐, 이를 일으킨 하드웨어 단계들을 각각 보여주지는 않습니다.
동작을 확인한 방법
| 방법 | 관찰 내용 |
|---|---|
STIR 로 인터럽트 트리거 | 하드웨어가 시작한 예외 진입을 확인 |
| 디버거 레지스터 뷰 | 레지스터가 올바르게 저장됨 |
| 스택 메모리 검사 | 고정된 예외 프레임만 저장되고, SP 가 정확히 32 바이트 이동 |
| 컴파일러 출력 검사 | R4–R11 은 필요할 때만 저장됨 |
핵심 요약: EXC_RETURN 은 Cortex‑M 코어에게 예외를 어떻게 풀어야 하는지(어떤 스택 포인터를 사용할지, 어떤 모드로 복귀할지, 부동소수점 상태가 있는지) 알려 주는 작고 하드웨어가 만든 토큰입니다. 프로세서는 모든 저수준 스택킹/언스택킹을 자동으로 처리하고, 디버거는 개별 하드웨어 단계가 아니라 최종 스택된 상태를 반영합니다.