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

최대 절전 복귀에서는 저장해 둔 메모리 내용을 현재 메모리에 다시 써야 합니다. 문제는 복사할 목적지에 지금 실행 중인 커널 코드도 들어 있다는 점입니다. 자기 자신을 덮어쓰지 않을 안전한 코드 페이지와 작업용 매핑을 먼저 준비한 다음 복사합니다.

## 처음 읽을 때
페이지를 복사하는 일과 그 페이지를 명령으로 실행할 준비를 하는 일은 구분됩니다. 메모리에 쓴 데이터가 명령어 캐시에도 올바르게 보이도록 정리가 필요합니다.

## 더 깊이 살펴볼 때
TTBR1을 바꾸는 동안 코드가 TTBR0로 실행되는 구조, break-before-make의 빈 테이블, PoU와 PoC의 범위를 따로 설명해 보세요.

![단계별 개념 그림](../media/hibernate-asm-overview.png)
화살표는 페이지 내용 복사와 CPU 상태 복원을 구분합니다. 전체 절전 이미지 읽기는 이 어셈블리 밖의 단계도 포함합니다.

## 안전한 실행 페이지와 테이블 전환
원본 1–59행

### 8행
```asm
#include <linux/linkage.h>
```
복사해서 실행할 저수준 코드의 시작·끝을 심볼로 표시하는 linkage 매크로를 가져옵니다. 호출·복귀 명령 자체는 아래에 별도로 작성됩니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 9행
```asm
#include <linux/errno.h>
```
커널 오류 번호 정의를 포함합니다. 다만 이 어셈블리 본문은 복원 성공 시 cpu_resume으로 이동하는 경로이며 errno 반환을 수행하는 명령은 없습니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 11행
```asm
#include <asm/asm-offsets.h>
```
복원 페이지 목록 구조체에서 원본 주소·목적지 주소·다음 항목이 위치한 오프셋을 가져옵니다. HIBERN_PBE_ADDR·ORIG·NEXT가 C 구조체와 일치하도록 빌드 과정에서 생성됩니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 12행
```asm
#include <asm/assembler.h>
```
페이지 복사, 데이터 캐시 라인 크기 계산, 안전한 TTBR 전환 매크로를 가져옵니다. 복원 도중 외부 함수 본문이 덮어써질 수 있어 필요한 작업을 여기의 매크로로 펼칩니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 13행
```asm
#include <asm/cputype.h>
```
CPU 식별·기능에 관한 ARM64 정의를 포함합니다. 이 본문에서 CPU별 캐시 문제 대응은 alternative_insn과 ARM64_WORKAROUND_CLEAN_CACHE를 통해 선택됩니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 14행
```asm
#include <asm/memory.h>
```
ARM64 커널 메모리 배치와 크기 관련 정의를 포함합니다. 이 코드는 임시 linear map에서 복원할 페이지 주소를 다루므로 현재 주소 변환 환경과 함께 읽어야 합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 15행
```asm
#include <asm/page.h>
```
커널 페이지 크기 등의 정의를 가져옵니다. 한 항목이 복원하는 크기와 캐시 처리 끝 주소는 PAGE_SIZE를 사용하며 4KiB로 고정해 읽으면 안 됩니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 16행
```asm
#include <asm/virt.h>
```
ARM64의 EL2 stub 호출 번호와 부팅 모드 등 가상화 공통 정의를 포함합니다. 실제 EL2 재설정은 아래 HVC와 C 쪽 임시 벡터 준비를 함께 봐야 합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.

### 47행
```asm
.pushsection    ".hibernate_exit.text", "ax"
```
복원 도중 덮어써질 커널 본문과 구분하여, 안전한 페이지로 복사할 실행 코드를 이 섹션에 모읍니다. 복사본은 TTBR0의 별도 매핑에서 실행되므로 복원 대상 페이지를 덮어써도 계속 동작할 수 있습니다. 이는 빌드 중 출력할 바이트의 섹션을 선택하는 지시문이며, 실행 중 PC나 SP를 옮기지 않습니다. 이전 출력 섹션을 함께 기억하므로 뒤의 .popsection으로 되돌릴 수 있습니다.

### 48행
```asm
SYM_CODE_START(swsusp_arch_suspend_exit)
```
하이버네이션 이미지의 메모리를 원래 위치에 덮어쓰는 마지막 복원 단계입니다. x0~x5로 임시 페이지 테이블, 복원할 커널의 페이지 테이블·재개 주소, 복사 목록, EL2 벡터, 빈 페이지 주소를 받습니다. 이 매크로는 정렬된 코드 시작 심벌을 정의하고 다른 오브젝트에서도 참조할 수 있게 합니다.

### 53행
```asm
	break_before_make_ttbr_switch	x5, x0, x6, x8
```
TTBR1을 잠시 빈 페이지 x5로 바꾸고 기존 TLB를 무효화한 뒤 임시 페이지 테이블 x0를 설치합니다. 복원 중 원래 테이블까지 덮어쓰므로 별도로 복사한 linear map이 필요하며, x6·x8은 이 전환의 작업 레지스터입니다.

### 55행
```asm
	mov	x21, x1
```
메모리 복원이 끝난 뒤 설치할 원래 커널의 페이지 테이블 물리 주소를 x21에 보관합니다. 복사 루프에서 x1을 다른 용도로 쓰기 전에 남겨 둡니다. MOV는 x21의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

### 56행
```asm
	mov	x30, x2
```
최종 재개 주소를 LR인 x30에 넣습니다. 마지막 RET은 이 함수를 호출한 복원용 커널로 돌아가는 대신 저장된 커널의 cpu_resume으로 이동하게 됩니다. MOV는 x30의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.
cpu_resume를 LR에 보존합니다. 마지막 RET가 여기로 이동합니다.

### 57행
```asm
	mov	x24, x4
```
복원할 커널의 EL2 벡터 물리 주소를 x24에 보관합니다. 0이면 EL2 벡터 복원이 필요하지 않은 경로입니다. MOV는 x24의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

### 58행
```asm
	mov	x25, x5
```
빈 페이지 물리 주소를 x25에 보관합니다. 루프가 x5를 작업용으로 덮어쓴 뒤에도 원래 페이지 테이블로 바꾸는 break-before-make에 다시 필요합니다. MOV는 x25의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

## 복원 목록 순회
원본 60–83행

### 61행
```asm
	mov	x19, x3
```
복원할 페이지 목록 restore_pblist의 첫 항목을 x19에 둡니다. 각 항목에는 임시 보관 위치, 원래 위치, 다음 항목의 포인터가 들어 있습니다. MOV는 x19의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.

### 63행
```asm
1:	ldr	x10, [x19, #HIBERN_PBE_ORIG]
```
현재 항목의 orig_address를 x10에 읽습니다. 이미지에서 되살릴 한 페이지의 원래 주소이며 복사 후 캐시 처리에도 다시 사용합니다. 실제로 x19 + (HIBERN_PBE_ORIG) 주소의 메모리 8바이트를 x10에 읽습니다. 이 주소 형식은 기준 레지스터 x19 자체를 갱신하지 않습니다.

### 64행
```asm
	mov	x0, x10
```
copy_page의 첫 번째 인자인 목적지 레지스터 x0에 원래 페이지 주소를 넣습니다. MOV는 x0의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.
x10에 목적지 시작을 따로 남겨 copy_page가 x0를 증가시켜도 캐시 처리 시작을 유지합니다.

### 65행
```asm
	ldr	x1, [x19, #HIBERN_PBE_ADDR]
```
현재 항목의 address를 x1에 읽습니다. 디스크에서 읽은 내용을 임시로 보관한 페이지가 복사 원본이 됩니다. 실제로 x19 + (HIBERN_PBE_ADDR) 주소의 메모리 8바이트를 x1에 읽습니다. 이 주소 형식은 기준 레지스터 x19 자체를 갱신하지 않습니다.

### 67행
```asm
	copy_page	x0, x1, x2, x3, x4, x5, x6, x7, x8, x9
```
x1의 임시 페이지를 x0의 원래 위치로 PAGE_SIZE만큼 복사합니다. x2~x9는 데이터를 운반하는 작업 레지스터이며 x0·x1도 페이지 끝까지 증가합니다. 외부 함수를 호출하지 않는 매크로이므로 복원 중 다른 커널 코드가 덮어써져도 사용할 수 있습니다.

### 69행
```asm
	add	x1, x10, #PAGE_SIZE
```
원래 주소 x10에 PAGE_SIZE를 더해 캐시 처리 범위의 끝 주소를 x1에 구합니다. 복사된 데이터가 나중에 코드로 실행될 수도 있으므로 페이지 전체를 처리합니다. 이 ADD 형식은 NZCV 조건 플래그를 바꾸지 않습니다.

### 71행
```asm
	raw_dcache_line_size x2, x3
```
현재 CPU의 CTR_EL0에서 최소 데이터 캐시 라인 크기를 읽어 바이트 단위로 x2에 구합니다. x3은 계산용이며, 다음 루프는 이 크기씩 주소를 이동합니다. CTR_EL0.DminLine은 바이트 수 자체가 아닙니다. 4를 이 필드 값만큼 왼쪽으로 이동하여 바이트 수를 구합니다.

### 72행
```asm
	sub	x3, x2, #1
```
캐시 라인 크기에서 1을 빼 정렬 마스크를 만듭니다. 예를 들어 64바이트 라인이면 63이지만 실제 값은 앞서 읽은 CPU 정보에 따릅니다. 이 SUB 형식은 NZCV 조건 플래그를 바꾸지 않습니다.

### 73행
```asm
	bic	x4, x10, x3
```
페이지 시작 주소의 하위 정렬 비트를 지워 첫 캐시 라인 시작 주소를 x4에 만듭니다. 페이지의 첫 바이트를 포함하는 라인부터 처리합니다. BIC는 마스크에서 1인 위치만 0으로 만들고 나머지 비트는 유지합니다.

### 74행
```asm
2:	/* clean D line / unified line */
```
복사한 페이지에서 캐시 라인 하나를 정리하는 루프의 시작입니다. 뒤의 B.LO 2b가 이 위치로 돌아오며 숫자 라벨 자체는 명령을 추가하지 않습니다. 레이블은 이 위치에 붙인 이름이며, 이름을 적는 것만으로 CPU 명령이 추가되지는 않습니다.

### 75행
```asm
alternative_insn "dc cvau, x4",  "dc civac, x4",  ARM64_WORKAROUND_CLEAN_CACHE
```
일반 CPU에서는 DC CVAU로 복사한 내용을 명령·데이터가 만나는 PoU까지 정리합니다. ARM64_WORKAROUND_CLEAN_CACHE가 필요한 CPU에는 DC CIVAC로 바꿔 PoC까지 clean하고 invalidate합니다. 이 선택은 부팅 때 CPU 기능에 따른 alternatives 패치로 이루어집니다. 이 매크로는 기본·교체 명령의 패치 정보를 만들며, CPU가 이 줄을 매번 if 조건처럼 실행하지 않습니다.
CPU 우회가 없으면 PoU clean인 CVAU, 해당 우회에서는 PoC clean+invalidate인 CIVAC로 패치됩니다.

### 76행
```asm
	add	x4, x4, x2
```
현재 주소를 데이터 캐시 라인 크기만큼 증가시켜 다음 라인을 준비합니다. 이 ADD 형식은 NZCV 조건 플래그를 바꾸지 않습니다.

### 77행
```asm
	cmp	x4, x1
```
다음 캐시 라인 주소와 페이지 끝을 비교합니다. 두 주소의 대소로 아직 처리할 라인이 남았는지 판단합니다. CMP는 두 피연산자의 뺄셈 결과로 NZCV 조건 플래그만 갱신하고 원래 레지스터 값은 유지합니다.

### 78행
```asm
	b.lo	2b
```
끝 주소보다 작으면 같은 페이지의 다음 캐시 라인을 처리합니다. 메모리 주소 비교이므로 unsigned 조건인 LO를 사용합니다. 앞서 계산한 NZCV 중 C=0 조건으로 분기합니다. 이 줄은 두 값을 새로 비교하지 않으며 조건이 맞지 않으면 다음 명령으로 진행합니다.

### 80행
```asm
	ldr	x19, [x19, #HIBERN_PBE_NEXT]
```
복원을 마친 항목에서 다음 페이지 항목의 포인터를 읽습니다. 실제로 x19 + (HIBERN_PBE_NEXT) 주소의 메모리 8바이트를 x19에 읽습니다.

### 81행
```asm
	cbnz	x19, 1b
```
다음 항목이 NULL이 아니면 다시 페이지 복사부터 수행합니다. NULL이면 모든 복원 페이지의 데이터 저장과 캐시 정리 요청이 끝난 상태입니다. 이 분기는 지정 레지스터의 값이나 비트를 직접 검사하며 CMP가 남긴 NZCV를 읽거나 바꾸지 않습니다. 조건이 맞지 않으면 바로 다음 명령으로 진행합니다.

### 82행
```asm
	dsb	ish		/* wait for PoU cleaning to finish */
```
Inner Shareable 영역의 앞선 메모리·캐시 작업 완료를 기다립니다. 복원한 내용을 이후 실행 단계에서 볼 수 있도록, 페이지 테이블 전환 전에 복사 결과를 확정합니다.

## 복원된 커널의 실행 상태로 이동
원본 84–95행

### 85행
```asm
	break_before_make_ttbr_switch	x25, x21, x6, x8
```
x25의 빈 페이지를 거쳐 x21에 저장해 둔 원래 커널의 TTBR1 테이블로 바꿉니다. 이제 임시 복사 테이블 대신 하이버네이션 이미지와 함께 복원된 주소 공간을 사용합니다.

### 87행
```asm
	ic	ialluis
```
Inner Shareable 영역의 명령 캐시를 무효화합니다. 같은 주소에 예전과 다른 명령 바이트를 덮어썼으므로 낡은 명령을 실행하지 않게 해야 합니다. 무효화 요청과 이후 명령 실행에 대한 동기화는 구분되며, 뒤의 DSB·ISB가 그 순서를 마무리합니다.

### 88행
```asm
	dsb	ish
```
앞서 요청한 명령 캐시 무효화가 완료될 때까지 기다립니다.

### 89행
```asm
	isb
```
이후 명령을 갱신된 실행 상태에서 다시 가져오게 합니다. 복원한 코드로 분기하기 전에 캐시 동기화 순서를 마무리합니다.

### 91행
```asm
	cbz	x24, 3f		/* Do we need to re-initialise EL2? */
```
보관한 EL2 벡터 주소 x24가 0이면 HVC를 건너뜁니다. 이 하이버네이션 이미지가 EL2 재설정을 필요로 하는지에 따라 실행 중 선택하는 분기입니다. 이 분기는 지정 레지스터의 값이나 비트를 직접 검사하며 CMP가 남긴 NZCV를 읽거나 바꾸지 않습니다. 조건이 맞지 않으면 바로 다음 명령으로 진행합니다.

### 92행
```asm
	hvc	#0
```
주석상 목적은 복원된 커널에 맞게 EL2를 다시 설정하는 것입니다. HVC #0의 0은 호출 번호가 아니며 실제 서비스는 x0로 구분됩니다. 다만 이 v6.18.37 함수에는 HVC_SET_VECTORS와 새 벡터 주소를 x0·x1에 준비하는 코드가 보이지 않으므로, 이 한 줄을 두고 벡터 설치가 완료된다고 설명할 수는 없습니다. 호출 인자를 요구하는 trans_pgd_stub_vectors와 함께 확인해야 하는 부분입니다. 일반 함수 호출처럼 BL로 이동하는 대신 동기 예외를 통해 요청을 전달합니다.
x24에는 복원할 EL2 벡터 주소가 남아 있지만, HVC_SET_VECTORS 처리기는 x0의 요청 번호와 x1의 새 벡터 주소를 사용합니다. 이 함수의 표시된 경로에는 그 두 인자를 준비하는 명령이 없으므로 HVC 실행과 벡터 설치 성공을 구분해야 합니다.

### 93행
```asm
3:	ret
```
앞서 x30에 저장해 둔 cpu_resume 주소로 이동합니다. 복원용 커널의 호출 지점으로 돌아가는 것이 아니라, 복원된 커널이 CPU 레지스터와 스택을 되살리는 절차를 시작합니다.
56행에서 기록한 cpu_resume로 이동합니다.

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

### 95행
```asm
.popsection
```
별도 복사용 .hibernate_exit.text의 생성을 끝내고 이전 섹션으로 돌아갑니다. 실행 중 cpu_resume에서 돌아온다는 뜻은 아닙니다. 이는 빌드 중 출력할 바이트의 섹션을 선택하는 지시문이며, 실행 중 PC나 SP를 옮기지 않습니다.

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

### 복사 도중 이 함수가 덮어써지면 어떻게 되나요?
다음 명령을 잃게 됩니다. 그래서 안전한 페이지에 옮겨 놓고 그 페이지를 통해 실행합니다.

### cpu_resume는 마지막에 어떻게 호출되나요?
앞에서 그 주소를 x30에 보관합니다. 마지막 RET가 저장해 둔 cpu_resume로 이동합니다.

### 데이터를 복사했는데 왜 I-cache도 정리하나요?
복원된 코드가 새 데이터로 쓰였기 때문입니다. CPU가 이전 명령 캐시 내용을 실행하지 않도록 동기화해야 합니다.
