# sleep.S — 말로 풀어 읽기
Linux v6.18.37 · arch/arm64/kernel/sleep.S

절전 전에는 실행을 나중에 이어 갈 수 있도록 상태를 저장해야 합니다. 복귀 후에는 전혀 다른 진입점에서 시작하더라도 저장된 스택과 LR을 되살려 원래 C 호출 지점으로 돌아갑니다. 그래서 같은 함수 호출이 처음에는 1, 복귀 뒤에는 0을 돌려준 것처럼 보입니다.

## 처음 읽을 때
저장은 “현재 계산을 기억해 두는 일”, 복원은 “CPU가 그 계산을 다시 이어 갈 상태를 만드는 일”로 생각해 보세요. 메모리 기록과 CPU 레지스터는 서로 다릅니다.

## 더 깊이 살펴볼 때
MMU가 꺼진 복귀 진입에서 저장 포인터를 VA로 읽기까지 필요한 초기화 순서와 CPU별 MPIDR 해시를 추적해 보세요.

## MPIDR 해시
원본 1–52행

### 2행
```asm
#include <linux/errno.h>
```
커널 오류 번호 정의를 포함합니다. 다만 이 파일의 저장·복원 반환값은 아래에서 정하는 1과 0이며, errno 값을 반환하는 실제 절전 실패 처리는 호출자인 suspend.c에서 확인해야 합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 3행
```asm
#include <linux/linkage.h>
```
SYM_FUNC_START·SYM_CODE_START 같은 심볼 선언 매크로를 가져옵니다. C에서 호출하는 저장 함수와 특수한 재개 진입점의 심볼 종류·정렬·크기를 빌드 도구에 알리는 데 사용합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 4행
```asm
#include <asm/asm-offsets.h>
```
C 구조체 배치에서 생성한 어셈블리용 오프셋을 가져옵니다. sleep_stack_data의 일반 레지스터·시스템 레지스터 영역과 SP 필드를 C 코드와 같은 위치로 읽고 쓰기 위해 필요합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 5행
```asm
#include <asm/assembler.h>
```
adr_l·ldr_l처럼 먼 커널 심볼의 주소나 값을 구하는 ARM64 공통 매크로를 가져옵니다. 이 파일은 이를 이용해 저장 배열과 MPIDR 해시 구조체를 찾습니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 6행
```asm
#include <asm/smp.h>
```
ARM64의 여러 CPU 지원에 관한 공통 선언을 포함합니다. 이 파일에서 CPU별 상태를 구분하는 실제 계산은 아래 compute_mpidr_hash와 부팅 때 준비한 mpidr_hash 데이터가 담당합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 8행
```asm
	.text
```
상태 저장 함수와 일반 가상 주소 복원 코드를 실행 코드 섹션 .text에 배치합니다. MMU를 켜는 cpu_resume 구간은 뒤에서 별도의 idmap 섹션으로 잠시 바뀝니다. 이는 빌드 중 출력할 바이트의 섹션을 선택하는 지시문이며, 실행 중 PC나 SP를 옮기지 않습니다.

### 39행
```asm
	.macro compute_mpidr_hash dst, rs0, rs1, rs2, rs3, mpidr, mask
```
CPU의 하드웨어 식별값 MPIDR을 sleep_save_stash 배열의 인덱스로 바꾸는 매크로입니다. 부팅 때 계산한 mask와 네 affinity 단계의 이동량을 받아, CPU마다 달라지는 비트만 겹치지 않게 모아 dst에 넣습니다. 암호학적 해시가 아니라 저장 상태를 다시 찾기 위한 충돌 없는 인덱스 계산입니다. 이 줄은 어셈블러 매크로 정의의 시작입니다. 본문은 매크로를 사용하는 위치에 펼쳐지며, 정의 자체가 런타임 함수 호출은 아닙니다.

### 40행
```asm
	and	\mpidr, \mpidr, \mask		// mask out MPIDR bits
```
MPIDR에서 부팅 때 mask로 골라 둔 비트만 남깁니다. 모든 CPU에서 같은 비트는 배열 인덱스에 필요하지 않으므로 제거합니다. 입력 mpidr 레지스터는 이 과정에서 바뀝니다. AND는 두 피연산자의 같은 위치 비트가 모두 1인 곳만 결과에 남깁니다.

### 41행
```asm
	and	\dst, \mpidr, #0xff		// mask=aff0
```
MPIDR의 Aff0 필드인 하위 8비트를 dst에 꺼냅니다. CPU를 구별하는 가장 낮은 affinity 단계부터 배열 인덱스를 구성합니다. AND는 두 피연산자의 같은 위치 비트가 모두 1인 곳만 결과에 남깁니다.

### 42행
```asm
	lsr	\dst ,\dst, \rs0		// dst=aff0>>rs0
```
Aff0에서 실제로 사용하는 비트를 rs0만큼 오른쪽으로 옮겨 인덱스의 아래쪽에 배치합니다. 이동량은 실행 중 임의로 정하지 않고 부팅 때 CPU 구성에서 계산합니다. LSR은 오른쪽으로 밀려 나온 비트를 버리고 빈 상위 비트를 0으로 채웁니다.

### 43행
```asm
	and	\mask, \mpidr, #0xff00		// mask = aff1
```
이번에는 Aff1 필드인 비트 8~15를 꺼냅니다. mask 레지스터는 이미 필터링에 썼으므로 이제 중간 계산용으로 재사용합니다. AND는 두 피연산자의 같은 위치 비트가 모두 1인 곳만 결과에 남깁니다.

### 44행
```asm
	lsr	\mask ,\mask, \rs1
```
Aff1의 유효 비트를 rs1만큼 옮깁니다. 앞서 배치한 Aff0 비트와 겹치지 않도록 계산된 이동량입니다. LSR은 오른쪽으로 밀려 나온 비트를 버리고 빈 상위 비트를 0으로 채웁니다.

### 45행
```asm
	orr	\dst, \dst, \mask		// dst|=(aff1>>rs1)
```
옮겨 놓은 Aff1 비트를 dst에 합칩니다. 여기까지 Aff0와 Aff1이 CPU별 인덱스의 일부를 이룹니다. ORR은 어느 한쪽이 1인 비트를 결과에 세우므로 원래 1이던 비트도 유지됩니다.

### 46행
```asm
	and	\mask, \mpidr, #0xff0000	// mask = aff2
```
Aff2 필드인 비트 16~23을 꺼냅니다. 여러 affinity 단계가 있는 CPU 배치도 같은 인덱스 계산에 포함하기 위한 단계입니다. AND는 두 피연산자의 같은 위치 비트가 모두 1인 곳만 결과에 남깁니다.

### 47행
```asm
	lsr	\mask ,\mask, \rs2
```
Aff2의 유효 비트를 rs2만큼 옮겨 Aff0·Aff1 뒤에 들어갈 자리에 맞춥니다. LSR은 오른쪽으로 밀려 나온 비트를 버리고 빈 상위 비트를 0으로 채웁니다.

### 48행
```asm
	orr	\dst, \dst, \mask		// dst|=(aff2>>rs2)
```
Aff2 비트를 dst에 합쳐 세 affinity 단계의 식별 정보를 모읍니다. ORR은 어느 한쪽이 1인 비트를 결과에 세우므로 원래 1이던 비트도 유지됩니다.

### 49행
```asm
	and	\mask, \mpidr, #0xff00000000	// mask = aff3
```
Aff3은 앞선 필드와 연속하지 않은 비트 32~39에 있으므로 0xff00000000으로 따로 꺼냅니다. AND는 두 피연산자의 같은 위치 비트가 모두 1인 곳만 결과에 남깁니다.

### 50행
```asm
	lsr	\mask ,\mask, \rs3
```
Aff3의 유효 비트를 rs3만큼 옮깁니다. 빈 비트 구간을 제거하여 저장 배열이 불필요하게 커지지 않게 합니다. LSR은 오른쪽으로 밀려 나온 비트를 버리고 빈 상위 비트를 0으로 채웁니다.

### 51행
```asm
	orr	\dst, \dst, \mask		// dst|=(aff3>>rs3)
```
마지막 Aff3까지 합친 dst가 해당 CPU의 저장 슬롯 번호입니다. suspend와 resume 양쪽에서 같은 계산을 해야 같은 상태를 찾을 수 있습니다. ORR은 어느 한쪽이 1인 비트를 결과에 세우므로 원래 1이던 비트도 유지됩니다.

### 52행
```asm
	.endm
```
공통 작업의 정의를 여기서 마칩니다. RET가 아니라 빌드 도구가 읽는 경계입니다. 

![단계별 개념 그림](../media/sleep-overview.png)
화살표는 상태 기록과 복원 방향입니다. 장치의 실제 전원 상태나 시간 간격을 재현한 애니메이션은 아닙니다.

## 상태 저장
원본 53–98행

### 65행
```asm
SYM_FUNC_START(__cpu_suspend_enter)
```
호출자가 x0로 넘긴 sleep_stack_data에 CPU 상태를 저장하는 진입점입니다. 처음 저장을 마친 경로는 1을 반환하고, 나중에 cpu_resume으로 복원한 경로는 이 호출에서 돌아온 것처럼 0을 반환합니다. 이 선언은 함수 심볼과 정렬을 정하고, ARM64 정의에서는 간접 호출이 도착할 BTI C 명령도 진입부에 넣습니다. 여기서는 전역 심벌로 정의하므로 다른 오브젝트에서도 이 함수 이름을 참조할 수 있습니다. 모듈용 EXPORT_SYMBOL 등록은 별도입니다.

### 66행
```asm
	stp	x29, lr, [x0, #SLEEP_STACK_DATA_CALLEE_REGS]
```
호출 당시 프레임 포인터 x29와 복귀 주소 LR을 상태 구조체의 callee_saved_regs 시작에 보관합니다. 복원 때 이 LR로 돌아가야 cpu_suspend의 같은 호출 지점에서 실행을 이어갈 수 있습니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS) 위치가 첫 레지스터 x29에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 lr에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.

### 67행
```asm
	stp	x19, x20, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+16]
```
호출 규약상 호출자가 그대로 돌아오기를 기대하는 x19·x20를 callee_saved_regs의 +16 위치에 저장합니다. CPU가 재개될 때 이 쌍을 복원하여 suspend 이전 C 코드의 지역 상태를 이어갑니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS+16) 위치가 첫 레지스터 x19에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x20에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.

### 68행
```asm
	stp	x21, x22, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+32]
```
호출 규약상 호출자가 그대로 돌아오기를 기대하는 x21·x22를 callee_saved_regs의 +32 위치에 저장합니다. CPU가 재개될 때 이 쌍을 복원하여 suspend 이전 C 코드의 지역 상태를 이어갑니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS+32) 위치가 첫 레지스터 x21에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x22에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.

### 69행
```asm
	stp	x23, x24, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+48]
```
호출 규약상 호출자가 그대로 돌아오기를 기대하는 x23·x24를 callee_saved_regs의 +48 위치에 저장합니다. CPU가 재개될 때 이 쌍을 복원하여 suspend 이전 C 코드의 지역 상태를 이어갑니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS+48) 위치가 첫 레지스터 x23에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x24에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.

### 70행
```asm
	stp	x25, x26, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+64]
```
호출 규약상 호출자가 그대로 돌아오기를 기대하는 x25·x26를 callee_saved_regs의 +64 위치에 저장합니다. CPU가 재개될 때 이 쌍을 복원하여 suspend 이전 C 코드의 지역 상태를 이어갑니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS+64) 위치가 첫 레지스터 x25에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x26에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.

### 71행
```asm
	stp	x27, x28, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+80]
```
호출 규약상 호출자가 그대로 돌아오기를 기대하는 x27·x28를 callee_saved_regs의 +80 위치에 저장합니다. CPU가 재개될 때 이 쌍을 복원하여 suspend 이전 C 코드의 지역 상태를 이어갑니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS+80) 위치가 첫 레지스터 x27에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x28에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.

### 74행
```asm
	mov	x2, sp
```
suspend 진입 시점의 SP를 x2로 복사합니다. 재개 후에는 부팅용 임시 스택이 아니라 이 태스크가 사용하던 스택으로 돌아가야 합니다. x2에 담기는 것은 SP의 주소값이며, SP와 스택 메모리의 내용은 그대로입니다.

### 75행
```asm
	str	x2, [x0, #SLEEP_STACK_DATA_SYSTEM_REGS + CPU_CTX_SP]
```
저장 구조체 안의 system_regs.sp 필드에 SP를 기록합니다. CPU_CTX_SP와 SLEEP_STACK_DATA_SYSTEM_REGS는 C 구조체의 실제 배치로부터 빌드 때 생성되는 오프셋입니다. 실제로 x0 + (SLEEP_STACK_DATA_SYSTEM_REGS + CPU_CTX_SP) 주소의 메모리 8바이트를 x2의 값으로 씁니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.

### 78행
```asm
	ldr_l	x1, sleep_save_stash
```
sleep_save_stash 전역 변수에 들어 있는 배열 시작 주소를 x1에 읽습니다. 이 배열에는 CPU별로 저장한 sleep_stack_data의 주소가 들어갑니다. ldr_l은 ADRP로 심벌이 속한 페이지를 구한 뒤 LDR로 그 위치의 값을 읽는 매크로입니다. 주소를 구하는 adr_l과 달리 실제 메모리 접근이 있습니다.

### 79행
```asm
	mrs	x7, mpidr_el1
```
현재 CPU의 MPIDR_EL1을 읽습니다. 재개 초기에 태스크나 per-CPU 환경을 모두 복원하기 전에도 하드웨어 CPU 식별값으로 저장 상태를 찾을 수 있습니다. 이 접근 대상은 CPU의 시스템 레지스터이며, 일반 RAM에 대한 load/store가 아닙니다.

### 80행
```asm
	adr_l	x9, mpidr_hash
```
CPU 식별 비트의 mask와 이동량이 담긴 mpidr_hash 구조체 주소를 x9에 구합니다. 이 값들은 부팅 때 CPU 배치에 맞춰 준비됩니다. 여기서 얻는 것은 레이블의 주소이며, 그 위치의 메모리 내용은 아직 읽지 않습니다. adr_l 매크로는 ADRP와 ADD로 주소의 페이지 부분과 하위 오프셋을 합칩니다.

### 81행
```asm
	ldr	x10, [x9, #MPIDR_HASH_MASK]
```
mpidr_hash.mask를 읽어 CPU마다 달라지는 MPIDR 비트만 선택할 준비를 합니다. 실제로 x9 + (MPIDR_HASH_MASK) 주소의 메모리 8바이트를 x10에 읽습니다. 이 주소 형식은 기준 레지스터 x9 자체를 갱신하지 않습니다.

### 86행
```asm
	ldp	w3, w4, [x9, #MPIDR_HASH_SHIFTS]
```
Aff0와 Aff1을 배열 인덱스에 배치할 이동량 두 개를 w3·w4에 읽습니다. 구조체 필드가 32비트이므로 W 레지스터로 읽으며 X 레지스터의 상위 비트는 0이 됩니다. 메모리에서는 x9 + (MPIDR_HASH_SHIFTS) 위치가 첫 레지스터 w3에 대응하고, 그보다 4바이트 높은 위치가 두 번째 레지스터 w4에 대응합니다. 이 주소 형식은 기준 레지스터 x9 자체를 갱신하지 않습니다.

### 87행
```asm
	ldp	w5, w6, [x9, #(MPIDR_HASH_SHIFTS + 8)]
```
Aff2와 Aff3에 대한 이동량을 w5·w6에 읽어 네 affinity 단계의 계산 재료를 모두 준비합니다. 메모리에서는 x9 + ((MPIDR_HASH_SHIFTS + 8)) 위치가 첫 레지스터 w5에 대응하고, 그보다 4바이트 높은 위치가 두 번째 레지스터 w6에 대응합니다. 이 주소 형식은 기준 레지스터 x9 자체를 갱신하지 않습니다.

### 88행
```asm
	compute_mpidr_hash x8, x3, x4, x5, x6, x7, x10
```
x7의 MPIDR, x10의 mask, x3~x6의 이동량으로 현재 CPU의 슬롯 번호를 x8에 계산합니다. 매크로 내부에서 x7과 x10은 작업용으로 바뀝니다.
결과 x8은 CPU별 저장 슬롯 인덱스입니다.

### 89행
```asm
	add	x1, x1, x8, lsl #3
```
배열 시작 주소에 슬롯 번호×8을 더합니다. 각 슬롯이 64비트 포인터 하나이므로 x1은 이 CPU의 저장 상태 주소를 넣을 위치가 됩니다. 이 ADD 형식은 NZCV 조건 플래그를 바꾸지 않습니다.
포인터 배열 원소가 8바이트이므로 인덱스를 3비트 왼쪽 이동합니다.

### 91행
```asm
	str	x0, [x1]
```
호출자가 넘긴 sleep_stack_data 주소를 해당 CPU의 슬롯에 기록합니다. CPU가 전원을 잃고 다시 들어와도 MPIDR로 이 포인터를 되찾을 수 있습니다. 실제로 x1 주소의 메모리 8바이트를 x0의 값으로 씁니다. 이 주소 형식은 기준 레지스터 x1 자체를 갱신하지 않습니다.
이 CPU가 나중에 복귀할 때 사용할 sleep_stack_data 포인터를 게시합니다.

### 92행
```asm
	add	x0, x0, #SLEEP_STACK_DATA_SYSTEM_REGS
```
x0를 구조체 전체 주소에서 system_regs 영역의 주소로 바꿉니다. 다음 cpu_do_suspend가 요구하는 인자는 이 시스템 레지스터 저장 영역입니다. 이 ADD 형식은 NZCV 조건 플래그를 바꾸지 않습니다.

### 93행
```asm
	stp	x29, lr, [sp, #-16]!
```
cpu_do_suspend를 호출하기 전에 현재 x29·LR을 임시 스택 프레임에 보관합니다. BL이 LR을 바꾸더라도 저장을 끝낸 뒤 원래 C 호출자로 돌아갈 수 있게 합니다. 메모리에서는 sp + (-16) 위치가 첫 레지스터 x29에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 lr에 대응합니다. 대괄호 뒤의 !는 주소 갱신을 뜻합니다. sp에 오프셋 -16을 더한 주소로 접근하고 그 주소를 sp에도 남깁니다.

### 94행
```asm
	bl	cpu_do_suspend
```
x0가 가리키는 cpu_suspend_ctx에 TPIDR, TCR, VBAR, SCTLR, per-CPU 오프셋, SP_EL0 등 재개에 필요한 시스템 상태를 저장합니다. 이 함수 자체가 전원을 끄는 것은 아닙니다. 복귀 뒤 1을 받은 C 코드가 별도의 suspend finisher를 호출합니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.

### 95행
```asm
	ldp	x29, lr, [sp], #16
```
임시로 보관한 프레임 포인터와 복귀 주소를 되찾고 SP를 16바이트 되돌립니다. cpu_do_suspend 호출 때문에 추가한 프레임만 해제합니다. 메모리에서는 sp 위치가 첫 레지스터 x29에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 lr에 대응합니다. 대괄호 밖의 오프셋 16은 후위 갱신입니다. 기존 sp 주소로 접근한 뒤 sp에 이 값을 더합니다.

### 96행
```asm
	mov	x0, #1
```
처음 상태를 저장한 경로임을 알리는 1을 x0에 넣습니다. cpu_suspend는 이 값을 보고 실제 저전력 진입 함수 fn(arg)를 실행합니다. MOV는 x0의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.
저장을 처음 마친 경로는 1을 반환합니다. 플랫폼의 실제 절전 작업은 호출자에게 남아 있습니다.

### 97행
```asm
	ret
```
상태 저장을 요청한 C 코드로 돌아갑니다. 이후 CPU 재개 경로는 저장해 둔 같은 복귀 주소를 사용하되 반환값을 0으로 바꿉니다. 피연산자가 없는 RET는 LR(x30)에 들어 있는 주소에서 실행을 이어갑니다.

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

## MMU가 꺼진 복귀 진입
원본 99–115행

### 100행
```asm
	.pushsection ".idmap.text", "a"
```
재개 직후 MMU 설정을 바꾸는 코드를 idmap 영역에 둡니다. 이 영역은 가상 주소와 물리 주소가 같아 주소 변환을 켜는 도중에도 같은 명령을 계속 실행할 수 있습니다. 이는 빌드 중 출력할 바이트의 섹션을 선택하는 지시문이며, 실행 중 PC나 SP를 옮기지 않습니다. 이전 출력 섹션을 함께 기억하므로 뒤의 .popsection으로 되돌릴 수 있습니다.

### 101행
```asm
SYM_CODE_START(cpu_resume)
```
전원 차단 뒤 CPU가 다시 들어오는 재개 진입점입니다. 아직 일반 커널 함수처럼 스택과 MMU가 준비됐다고 가정할 수 없습니다. 이 매크로는 정렬된 코드 시작 심벌을 정의하고 다른 오브젝트에서도 참조할 수 있게 합니다.

### 102행
```asm
	mov	x0, xzr
```
init_kernel_el에 MMU가 꺼진 상태라는 값 0을 넘깁니다. 일반 부팅의 입력과 달리 여기서는 저장된 CPU 상태를 복원하기 위한 초기 환경을 만듭니다. MOV는 x0의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

### 103행
```asm
	bl	init_kernel_el
```
현재 EL에 맞춰 최소 실행 환경을 구성합니다. EL2에서 들어왔다면 필요한 EL2 상태를 설정하고 EL1로 내려오며, 출발 EL을 나타내는 값을 x0로 돌려줍니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.

### 104행
```asm
	mov	x19, x0			// preserve boot mode
```
출발 EL 정보가 이후 호출로 사라지지 않도록 x19에 보관합니다. 가상 주소 영역에 들어간 뒤 finalise_el2가 이 값으로 EL2 후속 설정 여부를 판단합니다. MOV는 x19의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.
init_kernel_el의 반환 부트 모드를 다음 초기화 호출 동안 x19에 보존합니다.

### 105행
```asm
	bl	__cpu_setup
```
페이지 변환 제어와 메모리 속성 등 MMU를 켜기 위한 CPU 설정을 준비합니다. __cpu_setup은 MMU를 켤 SCTLR 값을 x0로 돌려주며, 다음 __enable_mmu가 이를 사용합니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.

### 107행
```asm
	adrp	x1, swapper_pg_dir
```
일반 커널 가상 주소를 변환할 swapper_pg_dir의 페이지 주소를 x1에 준비합니다. __enable_mmu가 TTBR1 쪽 페이지 테이블로 사용합니다. 여기서 얻는 것은 레이블의 주소이며, 그 위치의 메모리 내용은 아직 읽지 않습니다. ADRP가 만드는 주소는 4KiB 경계까지만입니다. 심벌의 하위 12비트가 0이면 그 자체가 심벌 주소이고, 그렇지 않으면 별도 오프셋 계산이 필요합니다.

### 108행
```asm
	adrp	x2, idmap_pg_dir
```
현재 실행 구간을 그대로 변환할 idmap_pg_dir의 페이지 주소를 x2에 준비합니다. MMU를 켠 직후에도 이 코드의 주소가 유효하도록 TTBR0 쪽에 사용합니다. 여기서 얻는 것은 레이블의 주소이며, 그 위치의 메모리 내용은 아직 읽지 않습니다. ADRP가 만드는 주소는 4KiB 경계까지만입니다. 심벌의 하위 12비트가 0이면 그 자체가 심벌 주소이고, 그렇지 않으면 별도 오프셋 계산이 필요합니다.

### 109행
```asm
	bl	__enable_mmu
```
x1의 커널 페이지 테이블과 x2의 idmap을 설치하고 x0의 SCTLR 설정으로 MMU를 켭니다. 호출 이후에도 잠시 idmap에서 실행한 뒤 일반 커널 가상 주소로 넘어갑니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.

### 110행
```asm
	ldr	x8, =_cpu_resume
```
다음 복원 함수 _cpu_resume의 커널 가상 주소를 x8에 읽습니다. MMU가 켜졌으므로 이제 이 주소를 실행할 수 있습니다. =심벌 형식은 어셈블러가 만든 리터럴 풀에서 심벌의 주소값을 읽는 의사 명령입니다. 그 심벌이 가리키는 코드 본문을 읽는 것은 아닙니다.

### 111행
```asm
	br	x8
```
x8의 _cpu_resume으로 직접 넘어갑니다. 원래 재개 진입점으로 돌아갈 호출이 아니므로 BLR로 복귀 주소를 만들지 않습니다. 레지스터가 담은 주소로 실행 위치를 바꾸며, 그 주소에서 값을 읽어 다른 레지스터에 복사하는 load 명령은 아닙니다.
MMU 활성화 후 최종 가상 주소의 _cpu_resume로 이동합니다.

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

### 113행
```asm
	.ltorg
```
_cpu_resume 주소를 읽는 LDR 리터럴 등이 참조할 상수 풀을 이 위치에 내보냅니다. 앞에서 BR로 이동했으므로 정상 경로는 이 데이터에 떨어져 실행되지 않습니다.

### 114행
```asm
	.popsection
```
idmap 구간의 출력을 끝내고 이전 .text 섹션으로 돌아갑니다. 다음 _cpu_resume은 MMU를 켠 뒤 사용하는 일반 커널 코드입니다. 이는 빌드 중 출력할 바이트의 섹션을 선택하는 지시문이며, 실행 중 PC나 SP를 옮기지 않습니다.

## 저장된 호출 상태 복구
원본 116–155행

### 116행
```asm
SYM_FUNC_START(_cpu_resume)
```
MMU가 켜진 커널 가상 주소에서 저장 상태 복원을 이어가는 함수입니다. CPU별 상태를 찾고 원래 스택과 레지스터를 복구하여 suspend 호출자로 돌아갑니다. 이 선언은 함수 심볼과 정렬을 정하고, ARM64 정의에서는 간접 호출이 도착할 BTI C 명령도 진입부에 넣습니다. 여기서는 전역 심벌로 정의하므로 다른 오브젝트에서도 이 함수 이름을 참조할 수 있습니다. 모듈용 EXPORT_SYMBOL 등록은 별도입니다.

### 117행
```asm
	mov	x0, x19
```
앞서 x19에 보관한 출발 EL 정보를 finalise_el2의 인자 x0로 넘깁니다. MOV는 x0의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

### 118행
```asm
	bl	finalise_el2
```
EL2에서 출발했고 현재 EL1인 경우 HVC로 EL2의 기능 설정을 마무리합니다. 하드웨어·설정 조건이 맞으면 커널을 VHE의 EL2h로 옮기며, EL1 출발 CPU는 이 전환을 시도하지 않습니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.

### 120행
```asm
	mrs	x1, mpidr_el1
```
깨어난 CPU의 MPIDR_EL1을 읽어 suspend 때 사용한 저장 슬롯을 다시 계산할 준비를 합니다. 이 접근 대상은 CPU의 시스템 레지스터이며, 일반 RAM에 대한 load/store가 아닙니다.

### 121행
```asm
	adr_l	x8, mpidr_hash		// x8 = struct mpidr_hash virt address
```
suspend 때와 같은 mpidr_hash 구조체를 찾습니다. 저장할 때와 다른 계산을 사용하면 다른 CPU의 스택을 복원하게 됩니다. 여기서 얻는 것은 레이블의 주소이며, 그 위치의 메모리 내용은 아직 읽지 않습니다. adr_l 매크로는 ADRP와 ADD로 주소의 페이지 부분과 하위 오프셋을 합칩니다.

### 124행
```asm
	ldr	x2, [x8, #MPIDR_HASH_MASK]
```
저장 시 사용했던 CPU 식별 비트 mask를 다시 읽습니다. 실제로 x8 + (MPIDR_HASH_MASK) 주소의 메모리 8바이트를 x2에 읽습니다. 이 주소 형식은 기준 레지스터 x8 자체를 갱신하지 않습니다.

### 125행
```asm
	ldp	w3, w4, [x8, #MPIDR_HASH_SHIFTS]
```
Aff0·Aff1의 이동량을 읽어 저장 시와 같은 슬롯 번호를 재현합니다. 메모리에서는 x8 + (MPIDR_HASH_SHIFTS) 위치가 첫 레지스터 w3에 대응하고, 그보다 4바이트 높은 위치가 두 번째 레지스터 w4에 대응합니다. 이 주소 형식은 기준 레지스터 x8 자체를 갱신하지 않습니다.

### 126행
```asm
	ldp	w5, w6, [x8, #(MPIDR_HASH_SHIFTS + 8)]
```
Aff2·Aff3의 이동량까지 읽어 MPIDR 전체의 유효 식별 비트를 같은 방식으로 모읍니다. 메모리에서는 x8 + ((MPIDR_HASH_SHIFTS + 8)) 위치가 첫 레지스터 w5에 대응하고, 그보다 4바이트 높은 위치가 두 번째 레지스터 w6에 대응합니다. 이 주소 형식은 기준 레지스터 x8 자체를 갱신하지 않습니다.

### 127행
```asm
	compute_mpidr_hash x7, x3, x4, x5, x6, x1, x2
```
현재 CPU의 MPIDR로 저장 배열 인덱스를 x7에 계산합니다. 이 값은 Linux의 논리 CPU 번호 자체가 아니라 suspend 저장 배열을 찾기 위한 해시 인덱스입니다.

### 130행
```asm
	ldr_l	x0, sleep_save_stash
```
CPU별 저장 상태 포인터 배열 sleep_save_stash의 시작 주소를 x0에 읽습니다. ldr_l은 ADRP로 심벌이 속한 페이지를 구한 뒤 LDR로 그 위치의 값을 읽는 매크로입니다. 주소를 구하는 adr_l과 달리 실제 메모리 접근이 있습니다.

### 131행
```asm
	ldr	x0, [x0, x7, lsl #3]
```
x7×8 위치에서 이 CPU의 sleep_stack_data 주소를 꺼냅니다. 저장 당시의 태스크 스택 안에 남겨 둔 구조체를 찾는 단계입니다. 실제로 x0 + (x7 × 8) 주소의 메모리 8바이트를 x0에 읽습니다.

### 132행
```asm
	add	x29, x0, #SLEEP_STACK_DATA_CALLEE_REGS
```
나중에 일반 레지스터를 복구할 수 있도록 callee_saved_regs 시작 주소를 x29에 보관합니다. 그동안 x0는 시스템 레지스터 복원 함수의 인자로 재사용됩니다. 이 ADD 형식은 NZCV 조건 플래그를 바꾸지 않습니다.

### 133행
```asm
	add	x0, x0, #SLEEP_STACK_DATA_SYSTEM_REGS
```
x0를 system_regs 영역으로 옮겨 cpu_do_resume에 넘길 문맥 포인터를 준비합니다. 이 ADD 형식은 NZCV 조건 플래그를 바꾸지 않습니다.

### 135행
```asm
	ldr	x2, [x0, #CPU_CTX_SP]
```
system_regs.sp에 저장해 둔 suspend 당시 SP를 x2에 읽습니다. 실제로 x0 + (CPU_CTX_SP) 주소의 메모리 8바이트를 x2에 읽습니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.

### 136행
```asm
	mov	sp, x2
```
SP를 저장된 태스크 스택 위치로 되돌립니다. 이후의 함수 호출과 최종 복귀는 이 스택 위에서 진행됩니다. 바뀌는 것은 SP가 담은 주소입니다. 원래 스택의 데이터가 새 주소로 복사되는 것은 아닙니다.
cpu_do_resume를 호출하기 전에 원래 커널 스택을 복구합니다.

### 140행
```asm
	bl	cpu_do_resume
```
x0가 가리키는 저장 영역에서 시스템 레지스터, per-CPU 오프셋, SP_EL0, x18 등을 복구합니다. TCR의 T0SZ는 현재 idmap을 유지하도록 처리하고, 마지막 ISB로 설정을 적용한 뒤 돌아옵니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.

### 142행
```asm
#if defined(CONFIG_KASAN) && defined(CONFIG_KASAN_STACK)
```
KASAN은 커널의 범위 밖 접근과 해제 후 접근을 검사하는 기능이고, KASAN_STACK은 스택 변수에도 검사 코드를 넣는 설정입니다. 둘 다 켜진 빌드에서만 아래 스택 검사 정보 정리를 수행합니다. v6.18.37의 KASAN_STACK은 Generic 또는 소프트웨어 태그 방식에 해당하며 모든 KASAN 방식에서 쓰이는 설정은 아닙니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 고릅니다. CPU가 실행 중 전처리 조건을 검사하지는 않습니다.

### 143행
```asm
	mov	x0, sp
```
방금 복원한 SP를 함수 인자 x0에 넣습니다. 이 값은 정리할 범위의 위쪽 경계인 watermark입니다. AArch64 스택은 낮은 주소 쪽으로 사용 범위가 늘어나므로, SP보다 낮은 주소는 앞으로 새 함수 프레임이 사용할 공간입니다. x0에 담기는 것은 SP의 주소값이며, SP와 스택 메모리의 내용은 그대로입니다.

### 144행
```asm
	bl	kasan_unpoison_task_stack_below
```
kasan_unpoison_task_stack_below는 x0에서 태스크 스택 시작 주소를 계산하고 [스택 시작, 복원한 SP) 범위의 KASAN shadow 정보를 접근 가능한 상태로 바꿉니다. suspend finisher에서 평범하게 되돌아오지 않고 저장 시점으로 복귀하므로 남아 있을 수 있는 스택 poison 표시를 정리하는 것입니다. 스택의 실제 변수 값을 지우거나 복사하는 함수는 아니며 반환값도 없습니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다.

### 145행
```asm
#endif
```
KASAN의 스택 검사 정보 정리는 여기까지입니다. 두 설정을 함께 켜지 않은 빌드에서는 이 호출 없이 바로 일반 레지스터 복원으로 이어집니다.

### 147행
```asm
	ldp	x19, x20, [x29, #16]
```
x29가 가리키는 callee_saved_regs의 +16에서 x19·x20를 복원합니다. 재개 초기화 중 이 레지스터를 작업용으로 사용했어도 suspend 호출자는 이전 값을 그대로 이어서 사용합니다. 메모리에서는 x29 + (16) 위치가 첫 레지스터 x19에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x20에 대응합니다. 이 주소 형식은 기준 레지스터 x29 자체를 갱신하지 않습니다.

### 148행
```asm
	ldp	x21, x22, [x29, #32]
```
x29가 가리키는 callee_saved_regs의 +32에서 x21·x22를 복원합니다. 재개 초기화 중 이 레지스터를 작업용으로 사용했어도 suspend 호출자는 이전 값을 그대로 이어서 사용합니다. 메모리에서는 x29 + (32) 위치가 첫 레지스터 x21에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x22에 대응합니다. 이 주소 형식은 기준 레지스터 x29 자체를 갱신하지 않습니다.

### 149행
```asm
	ldp	x23, x24, [x29, #48]
```
x29가 가리키는 callee_saved_regs의 +48에서 x23·x24를 복원합니다. 재개 초기화 중 이 레지스터를 작업용으로 사용했어도 suspend 호출자는 이전 값을 그대로 이어서 사용합니다. 메모리에서는 x29 + (48) 위치가 첫 레지스터 x23에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x24에 대응합니다. 이 주소 형식은 기준 레지스터 x29 자체를 갱신하지 않습니다.

### 150행
```asm
	ldp	x25, x26, [x29, #64]
```
x29가 가리키는 callee_saved_regs의 +64에서 x25·x26를 복원합니다. 재개 초기화 중 이 레지스터를 작업용으로 사용했어도 suspend 호출자는 이전 값을 그대로 이어서 사용합니다. 메모리에서는 x29 + (64) 위치가 첫 레지스터 x25에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x26에 대응합니다. 이 주소 형식은 기준 레지스터 x29 자체를 갱신하지 않습니다.

### 151행
```asm
	ldp	x27, x28, [x29, #80]
```
x29가 가리키는 callee_saved_regs의 +80에서 x27·x28를 복원합니다. 재개 초기화 중 이 레지스터를 작업용으로 사용했어도 suspend 호출자는 이전 값을 그대로 이어서 사용합니다. 메모리에서는 x29 + (80) 위치가 첫 레지스터 x27에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x28에 대응합니다. 이 주소 형식은 기준 레지스터 x29 자체를 갱신하지 않습니다.

### 152행
```asm
	ldp	x29, lr, [x29]
```
마지막으로 저장된 프레임 포인터와 suspend 호출자의 복귀 주소를 복원합니다. x29를 다른 값으로 바꾸기 전에 나머지 callee-saved 레지스터를 모두 읽어 둔 이유입니다. 메모리에서는 x29 위치가 첫 레지스터 x29에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 lr에 대응합니다.
이 LDP가 FP와 저장 시점의 LR을 복원합니다.

### 153행
```asm
	mov	x0, #0
```
x0를 0으로 만들어 재개 경로임을 C 코드에 알립니다. 처음 저장할 때 반환한 1과 구별되어 finisher를 다시 실행하지 않고 __cpu_suspend_exit 쪽으로 이어집니다. MOV는 x0의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.
복귀 경로는 0을 반환하므로 호출자가 최초 저장 경로와 구분합니다.

### 154행
```asm
	ret
```
복원한 LR로 돌아갑니다. C 코드에서는 예전 __cpu_suspend_enter 호출이 이번에는 0을 반환한 것처럼 보입니다.

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

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

### 저장 함수 안에 CPU 전원을 끄는 명령이 있나요?
이 함수는 상태를 저장하고 반환합니다. 실제 절전 진입은 호출자가 플랫폼 경로에서 수행합니다.

### 복귀한 CPU는 자기 상태를 어떻게 찾나요?
MPIDR의 필요한 affinity 비트를 해시 인덱스로 만들고 sleep_save_stash의 해당 슬롯에서 포인터를 읽습니다.

### 왜 0과 1을 다르게 반환하나요?
C 호출자가 최초 저장 후의 경로와 절전에서 돌아온 경로를 구분하도록 하기 위해서입니다.
