DUJINLABS.COM

Linux v6.18.37 · ARM64

sleep.S — CPU 상태 저장과 절전 복귀

__cpu_suspend_enter는 CPU 상태를 저장하고 1을 반환합니다. 나중에 cpu_resume가 저장 상태를 복원하면 같은 호출 지점으로 돌아가되 0을 반환합니다. 저장한 LR 덕분에 C 호출자는 두 경로를 반환값으로 구분합니다.

역할과 입력

  • __cpu_suspend_enter: x0=sleep_stack_data 주소
  • cpu_resume: MMU를 켜고 MPIDR 해시로 저장 상태를 찾습니다.

원본 .S · 빌드 조건: Makefile · 명령·주소 표기 읽는 법

155줄 · SHA-256: 46ab72e110dd481b118963e0c851cca4d2102699bdea29b1450137db6fa05251

구조와 흐름

CPU 상태 저장과 절전 복귀
화살표: 저장부터 복귀까지의 시간 순서입니다. 실제 절전 진입은 이 파일 밖의 플랫폼 코드가 수행합니다.

MPIDR 해시

aff0·aff1·aff2·aff3의 필요한 비트만 남기고 각 shift 값으로 오른쪽 이동한 뒤 OR합니다. 해시는 sleep_save_stash 배열의 인덱스입니다. 매크로는 mpidr와 mask를 덮어쓰므로 입력과 출력 레지스터가 겹치지 않아야 합니다.

sleep.S · L1–L52
1/* SPDX-License-Identifier: GPL-2.0 */
2#include <linux/errno.h>
3#include <linux/linkage.h>
4#include <asm/asm-offsets.h>
5#include <asm/assembler.h>
6#include <asm/smp.h>
7
8	.text
9/*
10 * Implementation of MPIDR_EL1 hash algorithm through shifting
11 * and OR'ing.
12 *
13 * @dst: register containing hash result
14 * @rs0: register containing affinity level 0 bit shift
15 * @rs1: register containing affinity level 1 bit shift
16 * @rs2: register containing affinity level 2 bit shift
17 * @rs3: register containing affinity level 3 bit shift
18 * @mpidr: register containing MPIDR_EL1 value
19 * @mask: register containing MPIDR mask
20 *
21 * Pseudo C-code:
22 *
23 *u32 dst;
24 *
25 *compute_mpidr_hash(u32 rs0, u32 rs1, u32 rs2, u32 rs3, u64 mpidr, u64 mask) {
26 *	u32 aff0, aff1, aff2, aff3;
27 *	u64 mpidr_masked = mpidr & mask;
28 *	aff0 = mpidr_masked & 0xff;
29 *	aff1 = mpidr_masked & 0xff00;
30 *	aff2 = mpidr_masked & 0xff0000;
31 *	aff3 = mpidr_masked & 0xff00000000;
32 *	dst = (aff0 >> rs0 | aff1 >> rs1 | aff2 >> rs2 | aff3 >> rs3);
33 *}
34 * Input registers: rs0, rs1, rs2, rs3, mpidr, mask
35 * Output register: dst
36 * Note: input and output registers must be disjoint register sets
37         (eg: a macro instance with mpidr = x1 and dst = x1 is invalid)
38 */
39	.macro compute_mpidr_hash dst, rs0, rs1, rs2, rs3, mpidr, mask
40	and	\mpidr, \mpidr, \mask		// mask out MPIDR bits
41	and	\dst, \mpidr, #0xff		// mask=aff0
42	lsr	\dst ,\dst, \rs0		// dst=aff0>>rs0
43	and	\mask, \mpidr, #0xff00		// mask = aff1
44	lsr	\mask ,\mask, \rs1
45	orr	\dst, \dst, \mask		// dst|=(aff1>>rs1)
46	and	\mask, \mpidr, #0xff0000	// mask = aff2
47	lsr	\mask ,\mask, \rs2
48	orr	\dst, \dst, \mask		// dst|=(aff2>>rs2)
49	and	\mask, \mpidr, #0xff00000000	// mask = aff3
50	lsr	\mask ,\mask, \rs3
51	orr	\dst, \dst, \mask		// dst|=(aff3>>rs3)
52	.endm
L2
커널 오류 번호 정의를 포함합니다. 다만 이 파일의 저장·복원 반환값은 아래에서 정하는 1과 0이며, errno 값을 반환하는 실제 절전 실패 처리는 호출자인 suspend.c에서 확인해야 합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.
L3
SYM_FUNC_START·SYM_CODE_START 같은 심볼 선언 매크로를 가져옵니다. C에서 호출하는 저장 함수와 특수한 재개 진입점의 심볼 종류·정렬·크기를 빌드 도구에 알리는 데 사용합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.
L4
C 구조체 배치에서 생성한 어셈블리용 오프셋을 가져옵니다. sleep_stack_data의 일반 레지스터·시스템 레지스터 영역과 SP 필드를 C 코드와 같은 위치로 읽고 쓰기 위해 필요합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.
L5
adr_l·ldr_l처럼 먼 커널 심볼의 주소나 값을 구하는 ARM64 공통 매크로를 가져옵니다. 이 파일은 이를 이용해 저장 배열과 MPIDR 해시 구조체를 찾습니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.
L6
ARM64의 여러 CPU 지원에 관한 공통 선언을 포함합니다. 이 파일에서 CPU별 상태를 구분하는 실제 계산은 아래 compute_mpidr_hash와 부팅 때 준비한 mpidr_hash 데이터가 담당합니다. #include는 전처리 단계에서 헤더 내용을 가져옵니다. CPU가 이 줄에서 함수를 호출하는 동작은 없습니다.
L8
상태 저장 함수와 일반 가상 주소 복원 코드를 실행 코드 섹션 .text에 배치합니다. MMU를 켜는 cpu_resume 구간은 뒤에서 별도의 idmap 섹션으로 잠시 바뀝니다. 이는 빌드 중 출력할 바이트의 섹션을 선택하는 지시문이며, 실행 중 PC나 SP를 옮기지 않습니다.
L39
CPU의 하드웨어 식별값 MPIDR을 sleep_save_stash 배열의 인덱스로 바꾸는 매크로입니다. 부팅 때 계산한 mask와 네 affinity 단계의 이동량을 받아, CPU마다 달라지는 비트만 겹치지 않게 모아 dst에 넣습니다. 암호학적 해시가 아니라 저장 상태를 다시 찾기 위한 충돌 없는 인덱스 계산입니다. 이 줄은 어셈블러 매크로 정의의 시작입니다. 본문은 매크로를 사용하는 위치에 펼쳐지며, 정의 자체가 런타임 함수 호출은 아닙니다.
L40
MPIDR에서 부팅 때 mask로 골라 둔 비트만 남깁니다. 모든 CPU에서 같은 비트는 배열 인덱스에 필요하지 않으므로 제거합니다. 입력 mpidr 레지스터는 이 과정에서 바뀝니다. AND는 두 피연산자의 같은 위치 비트가 모두 1인 곳만 결과에 남깁니다.
L41
MPIDR의 Aff0 필드인 하위 8비트를 dst에 꺼냅니다. CPU를 구별하는 가장 낮은 affinity 단계부터 배열 인덱스를 구성합니다. AND는 두 피연산자의 같은 위치 비트가 모두 1인 곳만 결과에 남깁니다.
L42
Aff0에서 실제로 사용하는 비트를 rs0만큼 오른쪽으로 옮겨 인덱스의 아래쪽에 배치합니다. 이동량은 실행 중 임의로 정하지 않고 부팅 때 CPU 구성에서 계산합니다. LSR은 오른쪽으로 밀려 나온 비트를 버리고 빈 상위 비트를 0으로 채웁니다.
L43
이번에는 Aff1 필드인 비트 8~15를 꺼냅니다. mask 레지스터는 이미 필터링에 썼으므로 이제 중간 계산용으로 재사용합니다. AND는 두 피연산자의 같은 위치 비트가 모두 1인 곳만 결과에 남깁니다.
L44
Aff1의 유효 비트를 rs1만큼 옮깁니다. 앞서 배치한 Aff0 비트와 겹치지 않도록 계산된 이동량입니다. LSR은 오른쪽으로 밀려 나온 비트를 버리고 빈 상위 비트를 0으로 채웁니다.
L45
옮겨 놓은 Aff1 비트를 dst에 합칩니다. 여기까지 Aff0와 Aff1이 CPU별 인덱스의 일부를 이룹니다. ORR은 어느 한쪽이 1인 비트를 결과에 세우므로 원래 1이던 비트도 유지됩니다.
L46
Aff2 필드인 비트 16~23을 꺼냅니다. 여러 affinity 단계가 있는 CPU 배치도 같은 인덱스 계산에 포함하기 위한 단계입니다. AND는 두 피연산자의 같은 위치 비트가 모두 1인 곳만 결과에 남깁니다.
L47
Aff2의 유효 비트를 rs2만큼 옮겨 Aff0·Aff1 뒤에 들어갈 자리에 맞춥니다. LSR은 오른쪽으로 밀려 나온 비트를 버리고 빈 상위 비트를 0으로 채웁니다.
L48
Aff2 비트를 dst에 합쳐 세 affinity 단계의 식별 정보를 모읍니다. ORR은 어느 한쪽이 1인 비트를 결과에 세우므로 원래 1이던 비트도 유지됩니다.
L49
Aff3은 앞선 필드와 연속하지 않은 비트 32~39에 있으므로 0xff00000000으로 따로 꺼냅니다. AND는 두 피연산자의 같은 위치 비트가 모두 1인 곳만 결과에 남깁니다.
L50
Aff3의 유효 비트를 rs3만큼 옮깁니다. 빈 비트 구간을 제거하여 저장 배열이 불필요하게 커지지 않게 합니다. LSR은 오른쪽으로 밀려 나온 비트를 버리고 빈 상위 비트를 0으로 채웁니다.
L51
마지막 Aff3까지 합친 dst가 해당 CPU의 저장 슬롯 번호입니다. suspend와 resume 양쪽에서 같은 계산을 해야 같은 상태를 찾을 수 있습니다. ORR은 어느 한쪽이 1인 비트를 결과에 세우므로 원래 1이던 비트도 유지됩니다.
L52
— 매크로 정의를 끝냅니다.

상태 저장

x19–x29와 LR을 sleep_stack_data에 저장하고 현재 SP도 기록합니다. MPIDR 해시×8 위치에 상태 포인터를 저장한 뒤 cpu_do_suspend로 시스템 레지스터를 보존합니다. 이 함수 자체는 전원을 끄지 않으며, 최초 경로에서 x0=1로 반환합니다.

sleep.S · L53–L98
53/*
54 * Save CPU state in the provided sleep_stack_data area, and publish its
55 * location for cpu_resume()'s use in sleep_save_stash.
56 *
57 * cpu_resume() will restore this saved state, and return. Because the
58 * link-register is saved and restored, it will appear to return from this
59 * function. So that the caller can tell the suspend/resume paths apart,
60 * __cpu_suspend_enter() will always return a non-zero value, whereas the
61 * path through cpu_resume() will return 0.
62 *
63 *  x0 = struct sleep_stack_data area
64 */
65SYM_FUNC_START(__cpu_suspend_enter)
66	stp	x29, lr, [x0, #SLEEP_STACK_DATA_CALLEE_REGS]
67	stp	x19, x20, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+16]
68	stp	x21, x22, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+32]
69	stp	x23, x24, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+48]
70	stp	x25, x26, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+64]
71	stp	x27, x28, [x0,#SLEEP_STACK_DATA_CALLEE_REGS+80]
72
73	/* save the sp in cpu_suspend_ctx */
74	mov	x2, sp
75	str	x2, [x0, #SLEEP_STACK_DATA_SYSTEM_REGS + CPU_CTX_SP]
76
77	/* find the mpidr_hash */
78	ldr_l	x1, sleep_save_stash
79	mrs	x7, mpidr_el1
80	adr_l	x9, mpidr_hash
81	ldr	x10, [x9, #MPIDR_HASH_MASK]
82	/*
83	 * Following code relies on the struct mpidr_hash
84	 * members size.
85	 */
86	ldp	w3, w4, [x9, #MPIDR_HASH_SHIFTS]
87	ldp	w5, w6, [x9, #(MPIDR_HASH_SHIFTS + 8)]
88	compute_mpidr_hash x8, x3, x4, x5, x6, x7, x10
89	add	x1, x1, x8, lsl #3
90
91	str	x0, [x1]
92	add	x0, x0, #SLEEP_STACK_DATA_SYSTEM_REGS
93	stp	x29, lr, [sp, #-16]!
94	bl	cpu_do_suspend
95	ldp	x29, lr, [sp], #16
96	mov	x0, #1
97	ret
98SYM_FUNC_END(__cpu_suspend_enter)
L65
호출자가 x0로 넘긴 sleep_stack_data에 CPU 상태를 저장하는 진입점입니다. 처음 저장을 마친 경로는 1을 반환하고, 나중에 cpu_resume으로 복원한 경로는 이 호출에서 돌아온 것처럼 0을 반환합니다. 이 선언은 함수 심볼과 정렬을 정하고, ARM64 정의에서는 간접 호출이 도착할 BTI C 명령도 진입부에 넣습니다. 여기서는 전역 심벌로 정의하므로 다른 오브젝트에서도 이 함수 이름을 참조할 수 있습니다. 모듈용 EXPORT_SYMBOL 등록은 별도입니다.
L66
호출 당시 프레임 포인터 x29와 복귀 주소 LR을 상태 구조체의 callee_saved_regs 시작에 보관합니다. 복원 때 이 LR로 돌아가야 cpu_suspend의 같은 호출 지점에서 실행을 이어갈 수 있습니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS) 위치가 첫 레지스터 x29에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 lr에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.
L67
호출 규약상 호출자가 그대로 돌아오기를 기대하는 x19·x20를 callee_saved_regs의 +16 위치에 저장합니다. CPU가 재개될 때 이 쌍을 복원하여 suspend 이전 C 코드의 지역 상태를 이어갑니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS+16) 위치가 첫 레지스터 x19에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x20에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.
L68
호출 규약상 호출자가 그대로 돌아오기를 기대하는 x21·x22를 callee_saved_regs의 +32 위치에 저장합니다. CPU가 재개될 때 이 쌍을 복원하여 suspend 이전 C 코드의 지역 상태를 이어갑니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS+32) 위치가 첫 레지스터 x21에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x22에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.
L69
호출 규약상 호출자가 그대로 돌아오기를 기대하는 x23·x24를 callee_saved_regs의 +48 위치에 저장합니다. CPU가 재개될 때 이 쌍을 복원하여 suspend 이전 C 코드의 지역 상태를 이어갑니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS+48) 위치가 첫 레지스터 x23에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x24에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.
L70
호출 규약상 호출자가 그대로 돌아오기를 기대하는 x25·x26를 callee_saved_regs의 +64 위치에 저장합니다. CPU가 재개될 때 이 쌍을 복원하여 suspend 이전 C 코드의 지역 상태를 이어갑니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS+64) 위치가 첫 레지스터 x25에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x26에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.
L71
호출 규약상 호출자가 그대로 돌아오기를 기대하는 x27·x28를 callee_saved_regs의 +80 위치에 저장합니다. CPU가 재개될 때 이 쌍을 복원하여 suspend 이전 C 코드의 지역 상태를 이어갑니다. 메모리에서는 x0 + (SLEEP_STACK_DATA_CALLEE_REGS+80) 위치가 첫 레지스터 x27에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x28에 대응합니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.
L74
suspend 진입 시점의 SP를 x2로 복사합니다. 재개 후에는 부팅용 임시 스택이 아니라 이 태스크가 사용하던 스택으로 돌아가야 합니다. x2에 담기는 것은 SP의 주소값이며, SP와 스택 메모리의 내용은 그대로입니다.
L75
저장 구조체 안의 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 자체를 갱신하지 않습니다.
L78
sleep_save_stash 전역 변수에 들어 있는 배열 시작 주소를 x1에 읽습니다. 이 배열에는 CPU별로 저장한 sleep_stack_data의 주소가 들어갑니다. ldr_l은 ADRP로 심벌이 속한 페이지를 구한 뒤 LDR로 그 위치의 값을 읽는 매크로입니다. 주소를 구하는 adr_l과 달리 실제 메모리 접근이 있습니다.
L79
현재 CPU의 MPIDR_EL1을 읽습니다. 재개 초기에 태스크나 per-CPU 환경을 모두 복원하기 전에도 하드웨어 CPU 식별값으로 저장 상태를 찾을 수 있습니다. 이 접근 대상은 CPU의 시스템 레지스터이며, 일반 RAM에 대한 load/store가 아닙니다.
L80
CPU 식별 비트의 mask와 이동량이 담긴 mpidr_hash 구조체 주소를 x9에 구합니다. 이 값들은 부팅 때 CPU 배치에 맞춰 준비됩니다. 여기서 얻는 것은 레이블의 주소이며, 그 위치의 메모리 내용은 아직 읽지 않습니다. adr_l 매크로는 ADRP와 ADD로 주소의 페이지 부분과 하위 오프셋을 합칩니다.
L81
mpidr_hash.mask를 읽어 CPU마다 달라지는 MPIDR 비트만 선택할 준비를 합니다. 실제로 x9 + (MPIDR_HASH_MASK) 주소의 메모리 8바이트를 x10에 읽습니다. 이 주소 형식은 기준 레지스터 x9 자체를 갱신하지 않습니다.
L86
Aff0와 Aff1을 배열 인덱스에 배치할 이동량 두 개를 w3·w4에 읽습니다. 구조체 필드가 32비트이므로 W 레지스터로 읽으며 X 레지스터의 상위 비트는 0이 됩니다. 메모리에서는 x9 + (MPIDR_HASH_SHIFTS) 위치가 첫 레지스터 w3에 대응하고, 그보다 4바이트 높은 위치가 두 번째 레지스터 w4에 대응합니다. 이 주소 형식은 기준 레지스터 x9 자체를 갱신하지 않습니다.
L87
Aff2와 Aff3에 대한 이동량을 w5·w6에 읽어 네 affinity 단계의 계산 재료를 모두 준비합니다. 메모리에서는 x9 + ((MPIDR_HASH_SHIFTS + 8)) 위치가 첫 레지스터 w5에 대응하고, 그보다 4바이트 높은 위치가 두 번째 레지스터 w6에 대응합니다. 이 주소 형식은 기준 레지스터 x9 자체를 갱신하지 않습니다.
L88
x7의 MPIDR, x10의 mask, x3~x6의 이동량으로 현재 CPU의 슬롯 번호를 x8에 계산합니다. 매크로 내부에서 x7과 x10은 작업용으로 바뀝니다.
L89
배열 시작 주소에 슬롯 번호×8을 더합니다. 각 슬롯이 64비트 포인터 하나이므로 x1은 이 CPU의 저장 상태 주소를 넣을 위치가 됩니다. 이 ADD 형식은 NZCV 조건 플래그를 바꾸지 않습니다.
L91
호출자가 넘긴 sleep_stack_data 주소를 해당 CPU의 슬롯에 기록합니다. CPU가 전원을 잃고 다시 들어와도 MPIDR로 이 포인터를 되찾을 수 있습니다. 실제로 x1 주소의 메모리 8바이트를 x0의 값으로 씁니다. 이 주소 형식은 기준 레지스터 x1 자체를 갱신하지 않습니다.
L92
x0를 구조체 전체 주소에서 system_regs 영역의 주소로 바꿉니다. 다음 cpu_do_suspend가 요구하는 인자는 이 시스템 레지스터 저장 영역입니다. 이 ADD 형식은 NZCV 조건 플래그를 바꾸지 않습니다.
L93
cpu_do_suspend를 호출하기 전에 현재 x29·LR을 임시 스택 프레임에 보관합니다. BL이 LR을 바꾸더라도 저장을 끝낸 뒤 원래 C 호출자로 돌아갈 수 있게 합니다. 메모리에서는 sp + (-16) 위치가 첫 레지스터 x29에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 lr에 대응합니다. 대괄호 뒤의 !는 주소 갱신을 뜻합니다. sp에 오프셋 -16을 더한 주소로 접근하고 그 주소를 sp에도 남깁니다.
L94
x0가 가리키는 cpu_suspend_ctx에 TPIDR, TCR, VBAR, SCTLR, per-CPU 오프셋, SP_EL0 등 재개에 필요한 시스템 상태를 저장합니다. 이 함수 자체가 전원을 끄는 것은 아닙니다. 복귀 뒤 1을 받은 C 코드가 별도의 suspend finisher를 호출합니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.
L95
임시로 보관한 프레임 포인터와 복귀 주소를 되찾고 SP를 16바이트 되돌립니다. cpu_do_suspend 호출 때문에 추가한 프레임만 해제합니다. 메모리에서는 sp 위치가 첫 레지스터 x29에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 lr에 대응합니다. 대괄호 밖의 오프셋 16은 후위 갱신입니다. 기존 sp 주소로 접근한 뒤 sp에 이 값을 더합니다.
L96
처음 상태를 저장한 경로임을 알리는 1을 x0에 넣습니다. cpu_suspend는 이 값을 보고 실제 저전력 진입 함수 fn(arg)를 실행합니다. MOV는 x0의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.
L97
상태 저장을 요청한 C 코드로 돌아갑니다. 이후 CPU 재개 경로는 저장해 둔 같은 복귀 주소를 사용하되 반환값을 0으로 바꿉니다. 피연산자가 없는 RET는 LR(x30)에 들어 있는 주소에서 실행을 이어갑니다.
L98
(__cpu_suspend_enter)의 심볼 타입과 크기를 기록합니다. 실행을 끝내는 RET를 생성하지 않습니다.

MMU가 꺼진 복귀 진입

cpu_resume는 .idmap.text에 있습니다. init_kernel_el의 부트 모드를 x19에 보존하고 __cpu_setup, __enable_mmu를 호출합니다. MMU를 켠 다음 LDR literal과 BR로 가상 주소의 _cpu_resume에 이동합니다.

sleep.S · L99–L115
99
100	.pushsection ".idmap.text", "a"
101SYM_CODE_START(cpu_resume)
102	mov	x0, xzr
103	bl	init_kernel_el
104	mov	x19, x0			// preserve boot mode
105	bl	__cpu_setup
106	/* enable the MMU early - so we can access sleep_save_stash by va */
107	adrp	x1, swapper_pg_dir
108	adrp	x2, idmap_pg_dir
109	bl	__enable_mmu
110	ldr	x8, =_cpu_resume
111	br	x8
112SYM_CODE_END(cpu_resume)
113	.ltorg
114	.popsection
115
L100
재개 직후 MMU 설정을 바꾸는 코드를 idmap 영역에 둡니다. 이 영역은 가상 주소와 물리 주소가 같아 주소 변환을 켜는 도중에도 같은 명령을 계속 실행할 수 있습니다. 이는 빌드 중 출력할 바이트의 섹션을 선택하는 지시문이며, 실행 중 PC나 SP를 옮기지 않습니다. 이전 출력 섹션을 함께 기억하므로 뒤의 .popsection으로 되돌릴 수 있습니다.
L101
전원 차단 뒤 CPU가 다시 들어오는 재개 진입점입니다. 아직 일반 커널 함수처럼 스택과 MMU가 준비됐다고 가정할 수 없습니다. 이 매크로는 정렬된 코드 시작 심벌을 정의하고 다른 오브젝트에서도 참조할 수 있게 합니다.
L102
init_kernel_el에 MMU가 꺼진 상태라는 값 0을 넘깁니다. 일반 부팅의 입력과 달리 여기서는 저장된 CPU 상태를 복원하기 위한 초기 환경을 만듭니다. MOV는 x0의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.
L103
현재 EL에 맞춰 최소 실행 환경을 구성합니다. EL2에서 들어왔다면 필요한 EL2 상태를 설정하고 EL1로 내려오며, 출발 EL을 나타내는 값을 x0로 돌려줍니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.
L104
출발 EL 정보가 이후 호출로 사라지지 않도록 x19에 보관합니다. 가상 주소 영역에 들어간 뒤 finalise_el2가 이 값으로 EL2 후속 설정 여부를 판단합니다. MOV는 x19의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.
L105
페이지 변환 제어와 메모리 속성 등 MMU를 켜기 위한 CPU 설정을 준비합니다. __cpu_setup은 MMU를 켤 SCTLR 값을 x0로 돌려주며, 다음 __enable_mmu가 이를 사용합니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.
L107
일반 커널 가상 주소를 변환할 swapper_pg_dir의 페이지 주소를 x1에 준비합니다. __enable_mmu가 TTBR1 쪽 페이지 테이블로 사용합니다. 여기서 얻는 것은 레이블의 주소이며, 그 위치의 메모리 내용은 아직 읽지 않습니다. ADRP가 만드는 주소는 4KiB 경계까지만입니다. 심벌의 하위 12비트가 0이면 그 자체가 심벌 주소이고, 그렇지 않으면 별도 오프셋 계산이 필요합니다.
L108
현재 실행 구간을 그대로 변환할 idmap_pg_dir의 페이지 주소를 x2에 준비합니다. MMU를 켠 직후에도 이 코드의 주소가 유효하도록 TTBR0 쪽에 사용합니다. 여기서 얻는 것은 레이블의 주소이며, 그 위치의 메모리 내용은 아직 읽지 않습니다. ADRP가 만드는 주소는 4KiB 경계까지만입니다. 심벌의 하위 12비트가 0이면 그 자체가 심벌 주소이고, 그렇지 않으면 별도 오프셋 계산이 필요합니다.
L109
x1의 커널 페이지 테이블과 x2의 idmap을 설치하고 x0의 SCTLR 설정으로 MMU를 켭니다. 호출 이후에도 잠시 idmap에서 실행한 뒤 일반 커널 가상 주소로 넘어갑니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.
L110
다음 복원 함수 _cpu_resume의 커널 가상 주소를 x8에 읽습니다. MMU가 켜졌으므로 이제 이 주소를 실행할 수 있습니다. =심벌 형식은 어셈블러가 만든 리터럴 풀에서 심벌의 주소값을 읽는 의사 명령입니다. 그 심벌이 가리키는 코드 본문을 읽는 것은 아닙니다.
L111
x8의 _cpu_resume으로 직접 넘어갑니다. 원래 재개 진입점으로 돌아갈 호출이 아니므로 BLR로 복귀 주소를 만들지 않습니다. 레지스터가 담은 주소로 실행 위치를 바꾸며, 그 주소에서 값을 읽어 다른 레지스터에 복사하는 load 명령은 아닙니다.
L112
(cpu_resume)의 심볼 타입과 크기를 기록합니다. 실행을 끝내는 RET를 생성하지 않습니다.
L113
_cpu_resume 주소를 읽는 LDR 리터럴 등이 참조할 상수 풀을 이 위치에 내보냅니다. 앞에서 BR로 이동했으므로 정상 경로는 이 데이터에 떨어져 실행되지 않습니다.
L114
idmap 구간의 출력을 끝내고 이전 .text 섹션으로 돌아갑니다. 다음 _cpu_resume은 MMU를 켠 뒤 사용하는 일반 커널 코드입니다. 이는 빌드 중 출력할 바이트의 섹션을 선택하는 지시문이며, 실행 중 PC나 SP를 옮기지 않습니다.

저장된 호출 상태 복구

finalise_el2 이후 MPIDR 해시로 상태 포인터를 얻습니다. 먼저 SP를 복구해 cpu_do_resume를 호출할 수 있게 합니다. 필요하면 KASAN 스택 상태를 정리하고 x19–x29·LR을 복구합니다. x0=0을 설정한 RET는 저장 시점의 호출자로 돌아갑니다.

sleep.S · L116–L155
116SYM_FUNC_START(_cpu_resume)
117	mov	x0, x19
118	bl	finalise_el2
119
120	mrs	x1, mpidr_el1
121	adr_l	x8, mpidr_hash		// x8 = struct mpidr_hash virt address
122
123	/* retrieve mpidr_hash members to compute the hash */
124	ldr	x2, [x8, #MPIDR_HASH_MASK]
125	ldp	w3, w4, [x8, #MPIDR_HASH_SHIFTS]
126	ldp	w5, w6, [x8, #(MPIDR_HASH_SHIFTS + 8)]
127	compute_mpidr_hash x7, x3, x4, x5, x6, x1, x2
128
129	/* x7 contains hash index, let's use it to grab context pointer */
130	ldr_l	x0, sleep_save_stash
131	ldr	x0, [x0, x7, lsl #3]
132	add	x29, x0, #SLEEP_STACK_DATA_CALLEE_REGS
133	add	x0, x0, #SLEEP_STACK_DATA_SYSTEM_REGS
134	/* load sp from context */
135	ldr	x2, [x0, #CPU_CTX_SP]
136	mov	sp, x2
137	/*
138	 * cpu_do_resume expects x0 to contain context address pointer
139	 */
140	bl	cpu_do_resume
141
142#if defined(CONFIG_KASAN) && defined(CONFIG_KASAN_STACK)
143	mov	x0, sp
144	bl	kasan_unpoison_task_stack_below
145#endif
146
147	ldp	x19, x20, [x29, #16]
148	ldp	x21, x22, [x29, #32]
149	ldp	x23, x24, [x29, #48]
150	ldp	x25, x26, [x29, #64]
151	ldp	x27, x28, [x29, #80]
152	ldp	x29, lr, [x29]
153	mov	x0, #0
154	ret
155SYM_FUNC_END(_cpu_resume)
L116
MMU가 켜진 커널 가상 주소에서 저장 상태 복원을 이어가는 함수입니다. CPU별 상태를 찾고 원래 스택과 레지스터를 복구하여 suspend 호출자로 돌아갑니다. 이 선언은 함수 심볼과 정렬을 정하고, ARM64 정의에서는 간접 호출이 도착할 BTI C 명령도 진입부에 넣습니다. 여기서는 전역 심벌로 정의하므로 다른 오브젝트에서도 이 함수 이름을 참조할 수 있습니다. 모듈용 EXPORT_SYMBOL 등록은 별도입니다.
L117
앞서 x19에 보관한 출발 EL 정보를 finalise_el2의 인자 x0로 넘깁니다. MOV는 x0의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.
L118
EL2에서 출발했고 현재 EL1인 경우 HVC로 EL2의 기능 설정을 마무리합니다. 하드웨어·설정 조건이 맞으면 커널을 VHE의 EL2h로 옮기며, EL1 출발 CPU는 이 전환을 시도하지 않습니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.
L120
깨어난 CPU의 MPIDR_EL1을 읽어 suspend 때 사용한 저장 슬롯을 다시 계산할 준비를 합니다. 이 접근 대상은 CPU의 시스템 레지스터이며, 일반 RAM에 대한 load/store가 아닙니다.
L121
suspend 때와 같은 mpidr_hash 구조체를 찾습니다. 저장할 때와 다른 계산을 사용하면 다른 CPU의 스택을 복원하게 됩니다. 여기서 얻는 것은 레이블의 주소이며, 그 위치의 메모리 내용은 아직 읽지 않습니다. adr_l 매크로는 ADRP와 ADD로 주소의 페이지 부분과 하위 오프셋을 합칩니다.
L124
저장 시 사용했던 CPU 식별 비트 mask를 다시 읽습니다. 실제로 x8 + (MPIDR_HASH_MASK) 주소의 메모리 8바이트를 x2에 읽습니다. 이 주소 형식은 기준 레지스터 x8 자체를 갱신하지 않습니다.
L125
Aff0·Aff1의 이동량을 읽어 저장 시와 같은 슬롯 번호를 재현합니다. 메모리에서는 x8 + (MPIDR_HASH_SHIFTS) 위치가 첫 레지스터 w3에 대응하고, 그보다 4바이트 높은 위치가 두 번째 레지스터 w4에 대응합니다. 이 주소 형식은 기준 레지스터 x8 자체를 갱신하지 않습니다.
L126
Aff2·Aff3의 이동량까지 읽어 MPIDR 전체의 유효 식별 비트를 같은 방식으로 모읍니다. 메모리에서는 x8 + ((MPIDR_HASH_SHIFTS + 8)) 위치가 첫 레지스터 w5에 대응하고, 그보다 4바이트 높은 위치가 두 번째 레지스터 w6에 대응합니다. 이 주소 형식은 기준 레지스터 x8 자체를 갱신하지 않습니다.
L127
현재 CPU의 MPIDR로 저장 배열 인덱스를 x7에 계산합니다. 이 값은 Linux의 논리 CPU 번호 자체가 아니라 suspend 저장 배열을 찾기 위한 해시 인덱스입니다.
L130
CPU별 저장 상태 포인터 배열 sleep_save_stash의 시작 주소를 x0에 읽습니다. ldr_l은 ADRP로 심벌이 속한 페이지를 구한 뒤 LDR로 그 위치의 값을 읽는 매크로입니다. 주소를 구하는 adr_l과 달리 실제 메모리 접근이 있습니다.
L131
x7×8 위치에서 이 CPU의 sleep_stack_data 주소를 꺼냅니다. 저장 당시의 태스크 스택 안에 남겨 둔 구조체를 찾는 단계입니다. 실제로 x0 + (x7 × 8) 주소의 메모리 8바이트를 x0에 읽습니다.
L132
나중에 일반 레지스터를 복구할 수 있도록 callee_saved_regs 시작 주소를 x29에 보관합니다. 그동안 x0는 시스템 레지스터 복원 함수의 인자로 재사용됩니다. 이 ADD 형식은 NZCV 조건 플래그를 바꾸지 않습니다.
L133
x0를 system_regs 영역으로 옮겨 cpu_do_resume에 넘길 문맥 포인터를 준비합니다. 이 ADD 형식은 NZCV 조건 플래그를 바꾸지 않습니다.
L135
system_regs.sp에 저장해 둔 suspend 당시 SP를 x2에 읽습니다. 실제로 x0 + (CPU_CTX_SP) 주소의 메모리 8바이트를 x2에 읽습니다. 이 주소 형식은 기준 레지스터 x0 자체를 갱신하지 않습니다.
L136
SP를 저장된 태스크 스택 위치로 되돌립니다. 이후의 함수 호출과 최종 복귀는 이 스택 위에서 진행됩니다. 바뀌는 것은 SP가 담은 주소입니다. 원래 스택의 데이터가 새 주소로 복사되는 것은 아닙니다.
L140
x0가 가리키는 저장 영역에서 시스템 레지스터, per-CPU 오프셋, SP_EL0, x18 등을 복구합니다. TCR의 T0SZ는 현재 idmap을 유지하도록 처리하고, 마지막 ISB로 설정을 적용한 뒤 돌아옵니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다. 호출된 코드가 x0 같은 작업 레지스터를 바꿀 수 있으므로, 호출 뒤의 값은 그 코드의 반환·보존 규칙에 따릅니다.
L142
KASAN은 커널의 범위 밖 접근과 해제 후 접근을 검사하는 기능이고, KASAN_STACK은 스택 변수에도 검사 코드를 넣는 설정입니다. 둘 다 켜진 빌드에서만 아래 스택 검사 정보 정리를 수행합니다. v6.18.37의 KASAN_STACK은 Generic 또는 소프트웨어 태그 방식에 해당하며 모든 KASAN 방식에서 쓰이는 설정은 아닙니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 고릅니다. CPU가 실행 중 전처리 조건을 검사하지는 않습니다.
L143
방금 복원한 SP를 함수 인자 x0에 넣습니다. 이 값은 정리할 범위의 위쪽 경계인 watermark입니다. AArch64 스택은 낮은 주소 쪽으로 사용 범위가 늘어나므로, SP보다 낮은 주소는 앞으로 새 함수 프레임이 사용할 공간입니다. x0에 담기는 것은 SP의 주소값이며, SP와 스택 메모리의 내용은 그대로입니다.
L144
kasan_unpoison_task_stack_below는 x0에서 태스크 스택 시작 주소를 계산하고 [스택 시작, 복원한 SP) 범위의 KASAN shadow 정보를 접근 가능한 상태로 바꿉니다. suspend finisher에서 평범하게 되돌아오지 않고 저장 시점으로 복귀하므로 남아 있을 수 있는 스택 poison 표시를 정리하는 것입니다. 스택의 실제 변수 값을 지우거나 복사하는 함수는 아니며 반환값도 없습니다. BL은 다음 명령의 주소를 LR(x30)에 기록하고 목적지로 분기합니다.
L145
KASAN의 스택 검사 정보 정리는 여기까지입니다. 두 설정을 함께 켜지 않은 빌드에서는 이 호출 없이 바로 일반 레지스터 복원으로 이어집니다.
L147
x29가 가리키는 callee_saved_regs의 +16에서 x19·x20를 복원합니다. 재개 초기화 중 이 레지스터를 작업용으로 사용했어도 suspend 호출자는 이전 값을 그대로 이어서 사용합니다. 메모리에서는 x29 + (16) 위치가 첫 레지스터 x19에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x20에 대응합니다. 이 주소 형식은 기준 레지스터 x29 자체를 갱신하지 않습니다.
L148
x29가 가리키는 callee_saved_regs의 +32에서 x21·x22를 복원합니다. 재개 초기화 중 이 레지스터를 작업용으로 사용했어도 suspend 호출자는 이전 값을 그대로 이어서 사용합니다. 메모리에서는 x29 + (32) 위치가 첫 레지스터 x21에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x22에 대응합니다. 이 주소 형식은 기준 레지스터 x29 자체를 갱신하지 않습니다.
L149
x29가 가리키는 callee_saved_regs의 +48에서 x23·x24를 복원합니다. 재개 초기화 중 이 레지스터를 작업용으로 사용했어도 suspend 호출자는 이전 값을 그대로 이어서 사용합니다. 메모리에서는 x29 + (48) 위치가 첫 레지스터 x23에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x24에 대응합니다. 이 주소 형식은 기준 레지스터 x29 자체를 갱신하지 않습니다.
L150
x29가 가리키는 callee_saved_regs의 +64에서 x25·x26를 복원합니다. 재개 초기화 중 이 레지스터를 작업용으로 사용했어도 suspend 호출자는 이전 값을 그대로 이어서 사용합니다. 메모리에서는 x29 + (64) 위치가 첫 레지스터 x25에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x26에 대응합니다. 이 주소 형식은 기준 레지스터 x29 자체를 갱신하지 않습니다.
L151
x29가 가리키는 callee_saved_regs의 +80에서 x27·x28를 복원합니다. 재개 초기화 중 이 레지스터를 작업용으로 사용했어도 suspend 호출자는 이전 값을 그대로 이어서 사용합니다. 메모리에서는 x29 + (80) 위치가 첫 레지스터 x27에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 x28에 대응합니다. 이 주소 형식은 기준 레지스터 x29 자체를 갱신하지 않습니다.
L152
마지막으로 저장된 프레임 포인터와 suspend 호출자의 복귀 주소를 복원합니다. x29를 다른 값으로 바꾸기 전에 나머지 callee-saved 레지스터를 모두 읽어 둔 이유입니다. 메모리에서는 x29 위치가 첫 레지스터 x29에 대응하고, 그보다 8바이트 높은 위치가 두 번째 레지스터 lr에 대응합니다.
L153
x0를 0으로 만들어 재개 경로임을 C 코드에 알립니다. 처음 저장할 때 반환한 1과 구별되어 finisher를 다시 실행하지 않고 __cpu_suspend_exit 쪽으로 이어집니다. MOV는 x0의 기존 값을 대체합니다. 원본이 주소값이어도 그 주소의 메모리를 읽거나 복사하지 않습니다.
L154
복원한 LR로 돌아갑니다. C 코드에서는 예전 __cpu_suspend_enter 호출이 이번에는 0을 반환한 것처럼 보입니다.
L155
(_cpu_resume)의 심볼 타입과 크기를 기록합니다. 실행을 끝내는 RET를 생성하지 않습니다.

매크로 정의와 확장

각 정의는 v6.18.37 원문입니다. 역슬래시 인자는 사용 위치의 값으로 치환되며, 안쪽 매크로와 조건부 블록은 이후 단계에서 다시 처리됩니다.

SYM_CODE_END

아래 정의의 인자 치환, 조건부 생성과 중첩 매크로를 통해 코드를 만듭니다. SYM 계열의 타입·범위·BTI 차이는 공통 표기 설명에 정리했습니다.

include/linux/linkage.h L305–L306

앞뒤 조건을 포함한 원문

#define SYM_CODE_END(name)				\
	SYM_END(name, SYM_T_NONE)

SYM_CODE_START

아래 정의의 인자 치환, 조건부 생성과 중첩 매크로를 통해 코드를 만듭니다. SYM 계열의 타입·범위·BTI 차이는 공통 표기 설명에 정리했습니다.

include/linux/linkage.h L278–L279

앞뒤 조건을 포함한 원문

#define SYM_CODE_START(name)				\
	SYM_START(name, SYM_L_GLOBAL, SYM_A_ALIGN)

SYM_FUNC_END

아래 정의의 인자 치환, 조건부 생성과 중첩 매크로를 통해 코드를 만듭니다. SYM 계열의 타입·범위·BTI 차이는 공통 표기 설명에 정리했습니다.

include/linux/linkage.h L248–L249

앞뒤 조건을 포함한 원문

#define SYM_FUNC_END(name)				\
	SYM_END(name, SYM_T_FUNC)

SYM_FUNC_START

아래 정의의 인자 치환, 조건부 생성과 중첩 매크로를 통해 코드를 만듭니다. SYM 계열의 타입·범위·BTI 차이는 공통 표기 설명에 정리했습니다.

arch/arm64/include/asm/linkage.h L18–L20

앞뒤 조건을 포함한 원문

#define SYM_FUNC_START(name)				\
	SYM_START(name, SYM_L_GLOBAL, SYM_A_ALIGN)	\
	bti c ;
include/linux/linkage.h L209–L210

앞뒤 조건을 포함한 원문

#define SYM_FUNC_START(name)				\
	SYM_START(name, SYM_L_GLOBAL, SYM_A_ALIGN)

compute_mpidr_hash

MPIDR의 affinity 필드를 마스크·이동·OR하여 CPU 상태 저장 배열 인덱스를 만듭니다. mpidr·mask 임시값이 바뀝니다.

arch/arm64/kernel/sleep.S L39–L52

앞뒤 조건을 포함한 원문

	.macro compute_mpidr_hash dst, rs0, rs1, rs2, rs3, mpidr, mask
	and	\mpidr, \mpidr, \mask		// mask out MPIDR bits
	and	\dst, \mpidr, #0xff		// mask=aff0
	lsr	\dst ,\dst, \rs0		// dst=aff0>>rs0
	and	\mask, \mpidr, #0xff00		// mask = aff1
	lsr	\mask ,\mask, \rs1
	orr	\dst, \dst, \mask		// dst|=(aff1>>rs1)
	and	\mask, \mpidr, #0xff0000	// mask = aff2
	lsr	\mask ,\mask, \rs2
	orr	\dst, \dst, \mask		// dst|=(aff2>>rs2)
	and	\mask, \mpidr, #0xff00000000	// mask = aff3
	lsr	\mask ,\mask, \rs3
	orr	\dst, \dst, \mask		// dst|=(aff3>>rs3)
	.endm
인자 치환 예: compute_mpidr_hash x8, x3, x4, x5, x6, x7, x10

1단계 치환 결과입니다. 내부 매크로·조건·고유 라벨 번호는 아직 펼치지 않았습니다.

	and	x7, x7, x10		// mask out MPIDR bits
	and	x8, x7, #0xff		// mask=aff0
	lsr	x8 ,x8, x3		// dst=aff0>>rs0
	and	x10, x7, #0xff00		// mask = aff1
	lsr	x10 ,x10, x4
	orr	x8, x8, x10		// dst|=(aff1>>rs1)
	and	x10, x7, #0xff0000	// mask = aff2
	lsr	x10 ,x10, x5
	orr	x8, x8, x10		// dst|=(aff2>>rs2)
	and	x10, x7, #0xff00000000	// mask = aff3
	lsr	x10 ,x10, x6
	orr	x8, x8, x10		// dst|=(aff3>>rs3)

상수·구조체 오프셋

S_*·THREAD_*·FREGS_* 등의 구조체 오프셋은 빌드한 C 구조체 배치에서 생성됩니다. 숫자를 고정하지 않고 원래 기호를 사용했습니다.

asm-offsets.c의 생성 식

    맨 위로 ↑