# efi-rt-wrapper.S — 말로 풀어 읽기
Linux v6.18.37 · arch/arm64/kernel/efi-rt-wrapper.S

커널이 펌웨어 함수를 부를 때에도 커널이 돌아올 자리는 남아 있어야 합니다. 이 래퍼는 원래 커널 스택의 위치를 기록하고 EFI 전용 스택으로 바꿉니다. 펌웨어가 정상적으로 반환하면 원래 스택으로 돌아오며, 실행 중 예외가 발생한 경우에는 별도의 복구 코드가 저장 기록을 사용합니다.

## 처음 읽을 때
SP를 바꾸면 앞으로 메모리에 저장할 위치가 달라집니다. 기존 스택 내용이 사라지는 것은 아닙니다. 돌아올 주소를 어디에 남겼는지 찾으세요.

## 더 깊이 살펴볼 때
정상 경로는 callee-saved 규약을 믿고, 예외 복구 경로는 저장한 레지스터를 명시적으로 다시 읽습니다. x18처럼 플랫폼 규약의 의미를 가진 레지스터가 왜 별도 검사를 받는지도 살펴보세요.

![단계별 개념 그림](../media/efi-rt-wrapper-overview.png)
화살표는 상태 저장·복원 및 스택 선택 관계입니다. 두 스택은 별도 메모리이며 높낮이가 실제 주소 순서는 아닙니다.

## 두 스택의 연결
원본 1–35행

### 6행
```asm
#include <linux/linkage.h>
```
C 호출용 EFI 래퍼와 예외 복구 코드의 시작·끝을 선언할 linkage 매크로를 가져옵니다. 두 경로의 진입 방식이 달라 FUNC와 CODE 선언을 구분합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 7행
```asm
#include <asm/assembler.h>
```
전역 변수 값을 PC 상대 방식으로 읽는 ldr_l 등 ARM64 공통 매크로를 가져옵니다. 이 파일은 EFI 스택 상단 변수의 값을 찾을 때 이를 사용합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 9행
```asm
SYM_FUNC_START(__efi_rt_asm_wrapper)
```
커널에서 UEFI 런타임 서비스를 호출할 때 스택과 보존 레지스터를 관리하는 진입점입니다. x0는 펌웨어 함수 주소, x1은 오류 메시지에 쓸 함수 이름이며 x2~x6이 실제 서비스 인자입니다. 이 선언은 함수 심볼과 정렬을 정하고, ARM64 정의에서는 간접 호출이 도착할 BTI C 명령도 진입부에 넣습니다. 여기서는 전역 심벌로 정의하므로 다른 오브젝트에서도 이 함수 이름을 참조할 수 있습니다. 모듈용 EXPORT_SYMBOL 등록은 별도입니다.

### 10행
```asm
	stp	x29, x30, [sp, #-112]!
```
112바이트의 커널 스택 프레임을 만들고 x29·LR을 저장합니다. 나머지 공간에는 함수 이름, x18, x19~x28을 보관하여 펌웨어 예외가 발생해도 커널 호출자로 복귀할 수 있게 합니다. 메모리에서는 sp + (-112) 위치가 첫 레지스터 x29에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x30에 대응합니다. 대괄호 뒤의 !는 주소 갱신을 뜻합니다. sp에 오프셋 -112을 더한 주소로 접근하고 그 주소를 sp에도 남깁니다.
확보한 112바이트의 시작에 FP/LR을 저장합니다. SP는 낮은 주소로 112 이동합니다.

### 11행
```asm
	mov	x29, sp
```
새 커널 스택 프레임 주소를 x29에 남깁니다. 잠시 EFI 전용 스택으로 바꿨다가 돌아올 때 이 값으로 원래 스택을 찾습니다. x29에 담기는 것은 SP의 주소값이며, SP와 스택 메모리의 내용은 그대로입니다.

### 18행
```asm
	stp	x1, x18, [sp, #16]
```
오류를 알릴 때 사용할 함수 이름 x1과 호출 전 x18을 보관합니다. x18은 플랫폼 전용 레지스터이므로 펌웨어가 바꾸면 안 되며, 호출 뒤 비교할 기준이 됩니다. 메모리에서는 sp + (16) 위치가 첫 레지스터 x1에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x18에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.
첫 슬롯은 오류 처리용 x1, 두 번째 슬롯은 원래 x18입니다.

### 26행
```asm
	stp	x19, x20, [sp, #32]
```
커널의 x19·x20를 저장 프레임 +32에 보관합니다. 정상적인 펌웨어는 이를 보존하지만, 실행 도중 예외가 나면 정상 복귀 절차를 건너뛰므로 별도 복구 경로가 이 저장본을 사용합니다. 메모리에서는 sp + (32) 위치가 첫 레지스터 x19에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x20에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.

### 27행
```asm
	stp	x21, x22, [sp, #48]
```
커널의 x21·x22를 저장 프레임 +48에 보관합니다. 정상적인 펌웨어는 이를 보존하지만, 실행 도중 예외가 나면 정상 복귀 절차를 건너뛰므로 별도 복구 경로가 이 저장본을 사용합니다. 메모리에서는 sp + (48) 위치가 첫 레지스터 x21에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x22에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.

### 28행
```asm
	stp	x23, x24, [sp, #64]
```
커널의 x23·x24를 저장 프레임 +64에 보관합니다. 정상적인 펌웨어는 이를 보존하지만, 실행 도중 예외가 나면 정상 복귀 절차를 건너뛰므로 별도 복구 경로가 이 저장본을 사용합니다. 메모리에서는 sp + (64) 위치가 첫 레지스터 x23에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x24에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.

### 29행
```asm
	stp	x25, x26, [sp, #80]
```
커널의 x25·x26를 저장 프레임 +80에 보관합니다. 정상적인 펌웨어는 이를 보존하지만, 실행 도중 예외가 나면 정상 복귀 절차를 건너뛰므로 별도 복구 경로가 이 저장본을 사용합니다. 메모리에서는 sp + (80) 위치가 첫 레지스터 x25에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x26에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.

### 30행
```asm
	stp	x27, x28, [sp, #96]
```
커널의 x27·x28를 저장 프레임 +96에 보관합니다. 정상적인 펌웨어는 이를 보존하지만, 실행 도중 예외가 나면 정상 복귀 절차를 건너뛰므로 별도 복구 경로가 이 저장본을 사용합니다. 메모리에서는 sp + (96) 위치가 첫 레지스터 x27에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x28에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.

### 32행
```asm
	ldr_l	x16, efi_rt_stack_top
```
부팅 때 할당한 EFI 런타임 전용 스택의 최상단 주소를 읽습니다. 펌웨어의 스택 사용을 현재 태스크의 일반 커널 스택에서 분리합니다. ldr_l은 ADRP로 심벌이 속한 페이지를 구한 뒤 LDR로 그 위치의 값을 읽는 매크로입니다. 주소를 구하는 adr_l과 달리 실제 메모리 접근이 있습니다.
심볼의 주소가 아니라 efi_rt_stack_top 변수에 저장된 포인터 값을 읽습니다.

### 33행
```asm
	mov	sp, x16
```
SP를 EFI 전용 스택으로 바꿉니다. 이후 펌웨어 함수와 그 내부 호출은 이 공간에 프레임을 만듭니다. 바뀌는 것은 SP가 담은 주소입니다. 원래 스택의 데이터가 새 주소로 복사되는 것은 아닙니다.

### 34행
```asm
	stp	x18, x29, [sp, #-16]!
```
EFI 스택 상단에 원래 x18과 커널 프레임 주소 x29를 기록합니다. 예외 처리 코드는 이 두 값을 통해 원래 태스크 스택과 shadow call stack 상태를 되찾습니다. 메모리에서는 sp + (-16) 위치가 첫 레지스터 x18에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x29에 대응합니다. 대괄호 뒤의 !는 주소 갱신을 뜻합니다. sp에 오프셋 -16을 더한 주소로 접근하고 그 주소를 sp에도 남깁니다.
EFI 스택의 top−16에 x18, top−8에 커널 스택 프레임 주소 x29를 기록합니다.

## 정상 호출과 복귀
원본 36–57행

### 41행
```asm
	mov	x8, x0
```
펌웨어 함수 주소를 x8에 보관합니다. 곧 x0를 첫 번째 서비스 인자로 바꿔야 하므로 분기 주소를 미리 옮겨 둡니다. MOV는 x8의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

### 42행
```asm
	mov	x0, x2
```
래퍼의 x2를 펌웨어의 첫 인자 x0로 옮깁니다. 함수 포인터와 이름이라는 래퍼 전용 인자 두 개를 제외한 실제 인자를 전달합니다. MOV는 x0의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

### 43행
```asm
	mov	x1, x3
```
래퍼의 x3를 펌웨어의 두 번째 인자 x1로 옮깁니다. MOV는 x1의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

### 44행
```asm
	mov	x2, x4
```
래퍼의 x4를 펌웨어의 세 번째 인자 x2로 옮깁니다. MOV는 x2의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

### 45행
```asm
	mov	x3, x5
```
래퍼의 x5를 펌웨어의 네 번째 인자 x3로 옮깁니다. MOV는 x3의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

### 46행
```asm
	mov	x4, x6
```
래퍼의 x6를 펌웨어의 다섯 번째 인자 x4로 옮깁니다. 이 구현이 다루는 런타임 서비스는 인자가 다섯 개 이하라 추가 스택 인자 이동이 필요하지 않습니다. MOV는 x4의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

### 47행
```asm
	blr	x8
```
x8의 UEFI 런타임 서비스를 EFI 전용 스택에서 호출합니다. 서비스의 efi_status_t 결과는 x0로 돌아오며, 래퍼는 이후 검사에서도 이 값을 보존합니다. BLR은 다음 명령의 주소를 LR(x30)에 기록하고 레지스터에 든 목적지로 분기합니다.

### 49행
```asm
	mov	x16, sp
```
돌아온 EFI 스택 주소를 x16에 남깁니다. 원래 태스크 스택으로 돌아간 뒤 EFI 스택에 기록한 복구용 포인터를 지우기 위해 사용합니다. x16에 담기는 것은 SP의 주소값이며, SP와 스택 메모리의 내용은 그대로입니다.

### 50행
```asm
	mov	sp, x29
```
x29에 보관했던 커널 프레임 주소로 SP를 복원합니다. 이후 프레임 해제와 함수 이름 읽기는 원래 태스크 스택에서 수행합니다. 바뀌는 것은 SP가 담은 주소입니다. 원래 스택의 데이터가 새 주소로 복사되는 것은 아닙니다.
ABI에 따라 보존된 x29가 112바이트 커널 프레임의 시작을 가리킵니다.

### 51행
```asm
	str	xzr, [x16, #8]			// clear recorded task SP value
```
EFI 스택에 남긴 태스크 SP 값을 0으로 지웁니다. 현재 태스크가 아직 EFI 펌웨어를 실행 중이라는 잘못된 판정을 막기 위한 정리입니다. 실제로 x16 + (8) 주소의 메모리 8바이트를 xzr의 값으로 씁니다. 이 주소 형식은 기준 레지스터 x16 자체를 갱신하지 않습니다.

### 53행
```asm
	ldp	x1, x2, [sp, #16]
```
원래 함수 이름을 x1에, 호출 전 x18을 x2에 읽습니다. x0에는 펌웨어 결과를 그대로 남겨 오류를 보고하더라도 반환할 수 있게 합니다. 메모리에서는 sp + (16) 위치가 첫 레지스터 x1에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x2에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.
저장한 x1과 원래 x18을 x1/x2로 읽습니다. EFI 반환값 x0는 건드리지 않습니다.

### 54행
```asm
	cmp	x2, x18
```
호출 전 x18과 펌웨어에서 돌아온 x18을 비교합니다. 플랫폼 레지스터 보존 규약을 어긴 펌웨어인지 확인합니다. CMP는 두 피연산자의 뺄셈 결과로 NZCV 조건 플래그만 갱신하고 원래 레지스터 값은 유지합니다.

### 55행
```asm
	ldp	x29, x30, [sp], #112
```
호출자의 프레임 포인터·LR을 복원하고 112바이트 스택 프레임을 해제합니다. 정상 반환과 오류 보고 양쪽이 같은 호출자에게 돌아갈 준비를 합니다. 메모리에서는 sp 위치가 첫 레지스터 x29에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x30에 대응합니다. 대괄호 밖의 오프셋 112은 후위 갱신입니다. 기존 sp 주소로 접근한 뒤 sp에 이 값을 더합니다.
LDP는 CMP의 NZCV를 바꾸지 않아 다음 B.NE는 54행 비교 결과를 사용합니다.

### 56행
```asm
	b.ne	0f
```
x18이 바뀌었다면 복원·오류 보고 경로로 갑니다. 같으면 바로 펌웨어 결과 x0를 반환합니다. 앞서 계산한 NZCV 중 Z=0 조건으로 분기합니다. 이 줄은 두 값을 새로 비교하지 않으며 조건이 맞지 않으면 다음 명령으로 진행합니다.

### 57행
```asm
	ret
```
펌웨어의 반환 상태 x0를 바꾸지 않고 커널 호출자에게 돌려줍니다. 피연산자가 없는 RET는 LR(x30)에 들어 있는 주소에서 실행을 이어갑니다.

## x18이 바뀐 경우
원본 58–72행

### 58행
```asm
0:
```
펌웨어가 x18을 손상시킨 경우 들어오는 오류 보고 경로의 시작입니다. shadow call stack을 사용하는 빌드는 먼저 x18을 복구합니다. 레이블은 이 위치에 붙인 이름이며, 이름을 적는 것만으로 CPU 명령이 추가되지는 않습니다.

### 66행
```asm
#ifdef CONFIG_SHADOW_CALL_STACK
```
Shadow Call Stack은 함수의 복귀 주소를 일반 스택과 별도 공간에도 보관하여 덮어쓰기 공격에 대비하는 기능입니다. ARM64에서는 x18이 그 별도 스택의 포인터이므로, 이 설정이 켜졌다면 C 오류 보고 코드로 가기 전에 손상된 x18부터 복구해야 합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 고릅니다. CPU가 실행 중 전처리 조건을 검사하지는 않습니다.

### 67행
```asm
	ldr_l	x18, efi_rt_stack_top
```
x18이 손상된 경로에서 EFI 전용 스택 상단 주소를 다시 읽습니다. 기존 x18 값은 믿을 수 없으므로 저장본을 찾을 주소부터 새로 구합니다. ldr_l은 ADRP로 심벌이 속한 페이지를 구한 뒤 LDR로 그 위치의 값을 읽는 매크로입니다. 주소를 구하는 adr_l과 달리 실제 메모리 접근이 있습니다.

### 68행
```asm
	ldr	x18, [x18, #-16]
```
EFI 스택 상단의 16바이트 아래에 저장한 원래 x18을 되찾습니다. 이후 계측된 C 함수가 올바른 shadow call stack에 복귀 주소를 기록하도록 합니다.

### 69행
```asm
#endif
```
Shadow Call Stack용 포인터 복구는 여기까지입니다. 설정과 관계없이 x18 보존 규약 위반에 대한 오류 보고는 이어서 수행합니다.

### 71행
```asm
	b	efi_handle_corrupted_x18	// tail call
```
x0의 펌웨어 상태와 x1의 함수 이름으로 efi_handle_corrupted_x18을 꼬리 호출합니다. 이 함수는 펌웨어가 x18을 손상시켰다는 메시지를 남기고 원래 상태값을 그대로 반환합니다. B를 사용하므로 래퍼로 재복귀하지 않습니다. 이 분기는 LR에 새 복귀 주소를 기록하지 않습니다.
x0의 EFI 결과와 복구한 x1을 유지한 tail call입니다.

### 72행
```asm
SYM_FUNC_END(__efi_rt_asm_wrapper)
```
도구에 이 코드 범위의 끝을 알려 크기와 심볼 정보를 기록합니다. CPU가 이 줄 때문에 자동으로 반환하지는 않습니다.

## 동기 예외 복구
원본 73–87행

### 74행
```asm
SYM_CODE_START(__efi_rt_asm_recover)
```
펌웨어 실행 중 동기 예외를 커널이 복구하기로 결정했을 때 들어오는 별도 경로입니다. efi_runtime_fixup_exception이 PC를 여기로 바꾸고 x0에 EFI_ABORTED, x30에 저장된 태스크 SP를 마련합니다. 이 매크로는 정렬된 코드 시작 심벌을 정의하고 다른 오브젝트에서도 참조할 수 있게 합니다.

### 75행
```asm
	mov	sp, x30
```
여기서 x30은 보통의 복귀 주소가 아니라 예외 처리 코드가 넣어 준 태스크 스택 프레임 주소입니다. 이를 SP에 넣어 펌웨어 스택을 벗어납니다. 바뀌는 것은 SP가 담은 주소입니다. 원래 스택의 데이터가 새 주소로 복사되는 것은 아닙니다.
efi.c가 예외 프레임의 x30에 원래 커널 SP를 넣어 이 경로로 보냅니다. 여기의 x30은 정상 LR 용도가 아닙니다.

### 77행
```asm
	ldr_l	x16, efi_rt_stack_top		// clear recorded task SP value
```
EFI 런타임 스택 상단을 다시 찾아, 이제 펌웨어 실행을 벗어났다는 표시를 정리할 준비를 합니다. ldr_l은 ADRP로 심벌이 속한 페이지를 구한 뒤 LDR로 그 위치의 값을 읽는 매크로입니다. 주소를 구하는 adr_l과 달리 실제 메모리 접근이 있습니다.

### 78행
```asm
	str	xzr, [x16, #-8]
```
EFI 스택 끝의 복구용 태스크 SP 기록을 0으로 지웁니다. 정상 반환 경로와 동일하게 EFI 실행 중 상태를 해제합니다. 실제로 x16 + (-8) 주소의 메모리 8바이트를 xzr의 값으로 씁니다. 이 주소 형식은 기준 레지스터 x16 자체를 갱신하지 않습니다.

### 80행
```asm
	ldp	x19, x20, [sp, #32]
```
정상적인 펌웨어 복귀를 거치지 못한 경로이므로, 호출 전에 프레임 +32에 보관한 x19·x20를 직접 되살립니다. 커널 호출자가 기대하는 callee-saved 상태를 복원하는 단계입니다. 메모리에서는 sp + (32) 위치가 첫 레지스터 x19에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x20에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.

### 81행
```asm
	ldp	x21, x22, [sp, #48]
```
정상적인 펌웨어 복귀를 거치지 못한 경로이므로, 호출 전에 프레임 +48에 보관한 x21·x22를 직접 되살립니다. 커널 호출자가 기대하는 callee-saved 상태를 복원하는 단계입니다. 메모리에서는 sp + (48) 위치가 첫 레지스터 x21에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x22에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.

### 82행
```asm
	ldp	x23, x24, [sp, #64]
```
정상적인 펌웨어 복귀를 거치지 못한 경로이므로, 호출 전에 프레임 +64에 보관한 x23·x24를 직접 되살립니다. 커널 호출자가 기대하는 callee-saved 상태를 복원하는 단계입니다. 메모리에서는 sp + (64) 위치가 첫 레지스터 x23에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x24에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.

### 83행
```asm
	ldp	x25, x26, [sp, #80]
```
정상적인 펌웨어 복귀를 거치지 못한 경로이므로, 호출 전에 프레임 +80에 보관한 x25·x26를 직접 되살립니다. 커널 호출자가 기대하는 callee-saved 상태를 복원하는 단계입니다. 메모리에서는 sp + (80) 위치가 첫 레지스터 x25에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x26에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.

### 84행
```asm
	ldp	x27, x28, [sp, #96]
```
정상적인 펌웨어 복귀를 거치지 못한 경로이므로, 호출 전에 프레임 +96에 보관한 x27·x28를 직접 되살립니다. 커널 호출자가 기대하는 callee-saved 상태를 복원하는 단계입니다. 메모리에서는 sp + (96) 위치가 첫 레지스터 x27에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x28에 대응합니다. 이 주소 형식은 기준 레지스터 sp 자체를 갱신하지 않습니다.

### 85행
```asm
	ldp	x29, x30, [sp], #112
```
예외 전 커널 호출자의 프레임 포인터와 LR을 복원하고 전체 저장 프레임을 해제합니다. EFI 내부의 복귀 주소로 돌아가는 것이 아닙니다. 메모리에서는 sp 위치가 첫 레지스터 x29에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x30에 대응합니다. 대괄호 밖의 오프셋 112은 후위 갱신입니다. 기존 sp 주소로 접근한 뒤 sp에 이 값을 더합니다.

### 86행
```asm
	ret
```
예외 처리 코드가 준비한 EFI_ABORTED를 x0에 둔 채 원래 커널 호출자로 돌아갑니다. 커널의 C 예외 처리 쪽에서는 이후 EFI 런타임 서비스 사용도 비활성화합니다. 피연산자가 없는 RET는 LR(x30)에 들어 있는 주소에서 실행을 이어갑니다.

### 87행
```asm
SYM_CODE_END(__efi_rt_asm_recover)
```
도구에 이 코드 범위의 끝을 알려 크기와 심볼 정보를 기록합니다. CPU가 이 줄 때문에 자동으로 반환하지는 않습니다.

## 설명한 뒤 함께 생각해 볼 질문

### 다른 스택으로 가면 원래 함수로 못 돌아오지 않나요?
원래 프레임 주소를 x29와 EFI 스택에 남겨 둡니다. 그 값을 SP로 되돌리면 원래 프레임을 다시 읽을 수 있습니다.

### 복구 함수에서 x30을 SP로 쓰는 것은 이상하지 않나요?
C 예외 처리 코드가 복구용 원래 SP를 예외 프레임의 x30에 넣어 보냅니다. 이 경로에서 x30은 잠시 다른 의미로 사용됩니다.

### 정상 복귀에도 x19–x28을 모두 다시 읽나요?
이 래퍼의 정상 경로는 ABI에 따른 펌웨어의 보존을 전제로 합니다. 저장본을 모두 읽는 경로는 동기 예외 복구 부분입니다.
