← KernelDUJINLABS.COM

Linux 6.18.37 LTS · RISC-V 64 head.S

RISC-V 64 head.S의 hart·DTB 수신과 SATP 전환

Image header, _start_kernel, setup_vm(), trampoline page table, relocate_enable_mmu()start_kernel()까지 실제 RISC-V 64 원본으로 읽습니다.

Series
03 / 4
Baseline
Linux 6.18.37 LTS
Architecture
RISC-V 64
Source
arch/riscv/kernel/head.S

firmware가 넘긴 a0=hartid, a1=DTB가 언제 kernel virtual address, 정상 trap vector와 C stack을 얻는가?

RISC-V Linux의 entry contract는 register 두 개로 시작한다. a0에는 boot hart ID, a1에는 DTB의 물리 주소가 들어온다. S-mode kernel이라면 보통 OpenSBI 같은 firmware가 이 계약을 만들고, M-mode kernel은 mhartid와 PMP를 직접 다룬다.

setup_vm()은 MMU가 꺼진 물리 주소 실행 중 early page table을 작성한다. 이어 relocate_enable_mmu()가 return address와 trap vector를 virtual address로 보정하고, 먼저 trampoline_pg_dir, 그 다음 실제 kernel page table을 SATP에 넣는다.

첫 SATP write 직후 fetch가 가상 주소로 바뀔 수 있으므로 trampoline은 현재 실행 superpage를 물리/가상 양쪽에서 같은 내용으로 보이게 만든다. 이 겹침이 없으면 정상 trap handler도 console도 준비되기 전에 멈춘다.

들어오는 상태와 내보내는 상태

경계입력이 경계가 완성하는 상태
Firmware handoffa0=hartid, a1=DTB PAinterrupt masked entry
setup_vmMMU off, writable early page tablestrampoline_pg_dir와 early_pg_dir
relocate_enable_mmupage-table root PAvirtual RA, GP, SATP
start_kerneltp=init_task, kernel stack, trap vectorgeneric Linux init

주소와 register를 먼저 그려 본다

그림 1. SATP를 두 번 바꾸는 이유
MMU offPC와 DTB는 물리 주소
setup_vmPTE 작성
trampoline SATP현재 superpage의 VA/PA overlap
virtual PC보정된 RA로 재개
kernel SATP전체 kernel mapping
start_kernel정상 C 환경

trampoline은 주소 공간을 건너는 한 발짜리 다리이고, early/kernel page table이 이후 실행 범위를 담당한다.

그림 2. RISC-V boot register의 소유권 변화

firmware가 넘김

a0boot hart ID
a1DTB physical address
privilegeS-mode 또는 M-mode

kernel이 재배치

boot_cpu_hartid대표 hart 기록
a0 → setup_vmDTB 주소
tp/spinit_task와 init stack

firmware argument는 page-table 생성과 C 환경 복구 사이에 보존되어야 한다.

그림 3. MMU 전환 전후 주소 배치
높은 주소
kernel virtual mappingKERNEL_LINK_ADDR부터 text/data
trampoline overlap첫 superpage, PA와 VA 동시 유효
early page tablestrampoline_pg_dir / early_pg_dir
physical kernel imagefirmware가 적재한 RAM
DTB physical rangea1, setup_vm 입력
낮은 주소

kernel image의 physical load range와 link-time virtual range는 page table에서 같은 byte로 연결된다.

RISC-V Image header와 loader-visible entry

첫 instruction과 64-byte header는 UEFI와 일반 bootloader가 image 크기, load offset, magic을 판별하는 ABI다.

원본: arch/riscv/kernel/head.S 21-70줄

21SYM_CODE_START(_start)
22	/*
23	 * Image header expected by Linux boot-loaders. The image header data
24	 * structure is described in asm/image.h.
25	 * Do not modify it without modifying the structure and all bootloaders
26	 * that expects this header format!!
27	 */
28#ifdef CONFIG_EFI
29	/*
30	 * This instruction decodes to "MZ" ASCII required by UEFI.
31	 */
32	c.li s4,-13
33	j _start_kernel
34#else
35	/* jump to start kernel */
36	j _start_kernel
37	/* reserved */
38	.word 0
39#endif
40	.balign 8
41#ifdef CONFIG_RISCV_M_MODE
42	/* Image load offset (0MB) from start of RAM for M-mode */
43	.dword 0
44#else
45#if __riscv_xlen == 64
46	/* Image load offset(2MB) from start of RAM */
47	.dword 0x200000
48#else
49	/* Image load offset(4MB) from start of RAM */
50	.dword 0x400000
51#endif
52#endif
53	/* Effective size of kernel image */
54	.dword _end - _start
55	.dword __HEAD_FLAGS
56	.word RISCV_HEADER_VERSION
57	.word 0
58	.dword 0
59	.ascii RISCV_IMAGE_MAGIC
60	.balign 4
61	.ascii RISCV_IMAGE_MAGIC2
62#ifdef CONFIG_EFI
63	.word pe_head_start - _start
64pe_head_start:
65 
66	__EFI_PE_HEADER
67#else
68	.word 0
69#endif
70 

21-70줄 주석

21SYM_CODE_START(_start)

linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.

22/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

23* Image header expected by Linux boot-loaders. The image header data

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

24* structure is described in asm/image.h.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

25* Do not modify it without modifying the structure and all bootloaders

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

26* that expects this header format!!

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

27*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

28#ifdef CONFIG_EFI

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

29/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

30* This instruction decodes to "MZ" ASCII required by UEFI.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

31*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

32c.li s4,-13

compressed instruction byte가 ASCII MZ로 해석되도록 만들어 UEFI PE/COFF image signature를 만족하면서 실제로는 무해한 register load를 수행한다.

33j _start_kernel

현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.

34#else

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

35/* jump to start kernel */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

36j _start_kernel

현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.

37/* reserved */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

38.word 0

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

39#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

40.balign 8

linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.

41#ifdef CONFIG_RISCV_M_MODE

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

42/* Image load offset (0MB) from start of RAM for M-mode */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

43.dword 0

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

44#else

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

45#if __riscv_xlen == 64

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

46/* Image load offset(2MB) from start of RAM */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

47.dword 0x200000

64-bit S-mode kernel이 RAM 시작에서 2MiB offset에 적재되기를 기대하는 Image header field다.

48#else

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

49/* Image load offset(4MB) from start of RAM */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

50.dword 0x400000

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

51#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

52#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

53/* Effective size of kernel image */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

54.dword _end - _start

최종 image의 유효 크기를 linker symbol 차이로 기록한다.

55.dword __HEAD_FLAGS

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

56.word RISCV_HEADER_VERSION

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

57.word 0

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

58.dword 0

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

59.ascii RISCV_IMAGE_MAGIC

bootloader가 RISC-V Linux Image인지 확인하는 magic field다.

60.balign 4

linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.

61.ascii RISCV_IMAGE_MAGIC2

bootloader가 RISC-V Linux Image인지 확인하는 magic field다.

62#ifdef CONFIG_EFI

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

63.word pe_head_start - _start

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

64pe_head_start:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

65(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

66__EFI_PE_HEADER

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

67#else

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

68.word 0

assembler가 section, symbol 크기 또는 반복 생성을 제어하는 지시자다. 최종 ELF와 disassembly에서 결과를 확인한다.

69#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

70(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

return address를 보정하고 trampoline에서 kernel SATP로 교체

relocate_enable_mmu()는 page-table root를 받아 현재 물리 실행을 link-time virtual 실행으로 바꾼다.

원본: arch/riscv/kernel/head.S 73-126줄

73	.global relocate_enable_mmu
74relocate_enable_mmu:
75	/* Relocate return address */
76	la a1, kernel_map
77	XIP_FIXUP_OFFSET a1
78	REG_L a1, KERNEL_MAP_VIRT_ADDR(a1)
79	la a2, _start
80	sub a1, a1, a2
81	add ra, ra, a1
82 
83	/* Point stvec to virtual address of intruction after satp write */
84	la a2, 1f
85	add a2, a2, a1
86	csrw CSR_TVEC, a2
87 
88	/* Compute satp for kernel page tables, but don't load it yet */
89	srl a2, a0, PAGE_SHIFT
90	la a1, satp_mode
91	XIP_FIXUP_OFFSET a1
92	REG_L a1, 0(a1)
93	or a2, a2, a1
94 
95	/*
96	 * Load trampoline page directory, which will cause us to trap to
97	 * stvec if VA != PA, or simply fall through if VA == PA.  We need a
98	 * full fence here because setup_vm() just wrote these PTEs and we need
99	 * to ensure the new translations are in use.
100	 */
101	la a0, trampoline_pg_dir
102	XIP_FIXUP_OFFSET a0
103	srl a0, a0, PAGE_SHIFT
104	or a0, a0, a1
105	sfence.vma
106	csrw CSR_SATP, a0
107.align 2
1081:
109	/* Set trap vector to spin forever to help debug */
110	la a0, .Lsecondary_park
111	csrw CSR_TVEC, a0
112 
113	/* Reload the global pointer */
114	load_global_pointer
115 
116	/*
117	 * Switch to kernel page tables.  A full fence is necessary in order to
118	 * avoid using the trampoline translations, which are only correct for
119	 * the first superpage.  Fetching the fence is guaranteed to work
120	 * because that first superpage is translated the same way.
121	 */
122	csrw CSR_SATP, a2
123	sfence.vma
124 
125	ret
126#endif /* CONFIG_MMU */

73-126줄 주석

73.global relocate_enable_mmu

linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.

74relocate_enable_mmu:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

75/* Relocate return address */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

76la a1, kernel_map

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

77XIP_FIXUP_OFFSET a1

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

78REG_L a1, KERNEL_MAP_VIRT_ADDR(a1)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

79la a2, _start

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

80sub a1, a1, a2

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

81add ra, ra, a1

함수 복귀 주소를 physical-to-virtual offset만큼 옮긴다. MMU enable 뒤 ret가 virtual code로 돌아가게 한다.

82(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

83/* Point stvec to virtual address of intruction after satp write */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

84la a2, 1f

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

85add a2, a2, a1

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

86csrw CSR_TVEC, a2

첫 SATP write가 예상대로 fall-through하지 않을 때도 virtual continuation으로 제어를 옮길 임시 trap target을 둔다.

87(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

88/* Compute satp for kernel page tables, but don't load it yet */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

89srl a2, a0, PAGE_SHIFT

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

90la a1, satp_mode

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

91XIP_FIXUP_OFFSET a1

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

92REG_L a1, 0(a1)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

93or a2, a2, a1

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

94(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

95/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

96* Load trampoline page directory, which will cause us to trap to

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

97* stvec if VA != PA, or simply fall through if VA == PA. We need a

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

98* full fence here because setup_vm() just wrote these PTEs and we need

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

99* to ensure the new translations are in use.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

100*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

101la a0, trampoline_pg_dir

현재 instruction이 있는 첫 superpage를 주소 전환 양쪽에서 접근 가능하게 하는 최소 page table이다.

102XIP_FIXUP_OFFSET a0

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

103srl a0, a0, PAGE_SHIFT

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

104or a0, a0, a1

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

105sfence.vma

새 SATP와 PTE를 이후 fetch/load가 확실히 사용하도록 translation cache를 정리한다.

106csrw CSR_SATP, a0

trampoline root를 활성화하는 첫 주소 공간 전환이다.

107.align 2

linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.

1081:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

109/* Set trap vector to spin forever to help debug */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

110la a0, .Lsecondary_park

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

111csrw CSR_TVEC, a0

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

112(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

113/* Reload the global pointer */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

114load_global_pointer

virtual address 실행으로 바뀐 뒤 gp를 새 주소 기준으로 다시 적재한다.

115(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

116/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

117* Switch to kernel page tables. A full fence is necessary in order to

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

118* avoid using the trampoline translations, which are only correct for

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

119* the first superpage. Fetching the fence is guaranteed to work

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

120* because that first superpage is translated the same way.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

121*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

122csrw CSR_SATP, a2

최소 trampoline을 실제 kernel page table로 교체한다.

123sfence.vma

새 SATP와 PTE를 이후 fetch/load가 확실히 사용하도록 translation cache를 정리한다.

124(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

125ret

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

126#endif /* CONFIG_MMU */

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

boot hart의 BSS, stack, setup_vm과 start_kernel 진입

boot hart를 고른 뒤 BSS, tp, stack과 DTB argument를 준비하고 page table을 만든다. 최종적으로 trap vector를 설치한 뒤 generic start_kernel로 tail call한다.

원본: arch/riscv/kernel/head.S 203-340줄

203SYM_CODE_START(_start_kernel)
204	/* Mask all interrupts */
205	csrw CSR_IE, zero
206	csrw CSR_IP, zero
207 
208#ifdef CONFIG_RISCV_M_MODE
209	/* flush the instruction cache */
210	fence.i
211 
212	/* Reset all registers except ra, a0, a1 */
213	call reset_regs
214 
215	/*
216	 * Setup a PMP to permit access to all of memory.  Some machines may
217	 * not implement PMPs, so we set up a quick trap handler to just skip
218	 * touching the PMPs on any trap.
219	 */
220	la a0, .Lpmp_done
221	csrw CSR_TVEC, a0
222 
223	li a0, -1
224	csrw CSR_PMPADDR0, a0
225	li a0, (PMP_A_NAPOT | PMP_R | PMP_W | PMP_X)
226	csrw CSR_PMPCFG0, a0
227.align 2
228.Lpmp_done:
229 
230	/*
231	 * The hartid in a0 is expected later on, and we have no firmware
232	 * to hand it to us.
233	 */
234	csrr a0, CSR_MHARTID
235#else
236	/* Enable time CSR */
237	li t0, 0x2
238	csrw CSR_SCOUNTEREN, t0
239#endif /* CONFIG_RISCV_M_MODE */
240 
241	/* Load the global pointer */
242	load_global_pointer
243 
244	/*
245	 * Disable FPU & VECTOR to detect illegal usage of
246	 * floating point or vector in kernel space
247	 */
248	li t0, SR_FS_VS
249	csrc CSR_STATUS, t0
250 
251#ifdef CONFIG_RISCV_BOOT_SPINWAIT
252	li t0, CONFIG_NR_CPUS
253	blt a0, t0, .Lgood_cores
254	tail .Lsecondary_park
255.Lgood_cores:
256 
257	/* The lottery system is only required for spinwait booting method */
258#ifndef CONFIG_XIP_KERNEL
259	/* Pick one hart to run the main boot sequence */
260	la a3, hart_lottery
261	li a2, 1
262	amoadd.w a3, a2, (a3)
263	bnez a3, .Lsecondary_start
264 
265#else
266	/* hart_lottery in flash contains a magic number */
267	la a3, hart_lottery
268	mv a2, a3
269	XIP_FIXUP_OFFSET a2
270	XIP_FIXUP_FLASH_OFFSET a3
271	lw t1, (a3)
272	amoswap.w t0, t1, (a2)
273	/* first time here if hart_lottery in RAM is not set */
274	beq t0, t1, .Lsecondary_start
275 
276#endif /* CONFIG_XIP */
277#endif /* CONFIG_RISCV_BOOT_SPINWAIT */
278 
279#ifdef CONFIG_XIP_KERNEL
280	la sp, _end + THREAD_SIZE
281	XIP_FIXUP_OFFSET sp
282	mv s0, a0
283	mv s1, a1
284	call __copy_data
285 
286	/* Restore a0 & a1 copy */
287	mv a0, s0
288	mv a1, s1
289#endif
290 
291#ifndef CONFIG_XIP_KERNEL
292	/* Clear BSS for flat non-ELF images */
293	la a3, __bss_start
294	la a4, __bss_stop
295	ble a4, a3, .Lclear_bss_done
296.Lclear_bss:
297	REG_S zero, (a3)
298	add a3, a3, RISCV_SZPTR
299	blt a3, a4, .Lclear_bss
300.Lclear_bss_done:
301#endif
302	la a2, boot_cpu_hartid
303	XIP_FIXUP_OFFSET a2
304	REG_S a0, (a2)
305 
306	/* Initialize page tables and relocate to virtual addresses */
307	la tp, init_task
308	la sp, init_thread_union + THREAD_SIZE
309	XIP_FIXUP_OFFSET sp
310	addi sp, sp, -PT_SIZE_ON_STACK
311	scs_load_init_stack
312#ifdef CONFIG_BUILTIN_DTB
313	la a0, __dtb_start
314	XIP_FIXUP_OFFSET a0
315#else
316	mv a0, a1
317#endif /* CONFIG_BUILTIN_DTB */
318	/* Set trap vector to spin forever to help debug */
319	la a3, .Lsecondary_park
320	csrw CSR_TVEC, a3
321	call setup_vm
322#ifdef CONFIG_MMU
323	la a0, early_pg_dir
324	XIP_FIXUP_OFFSET a0
325	call relocate_enable_mmu
326#endif /* CONFIG_MMU */
327 
328	call .Lsetup_trap_vector
329	/* Restore C environment */
330	la tp, init_task
331	la sp, init_thread_union + THREAD_SIZE
332	addi sp, sp, -PT_SIZE_ON_STACK
333	scs_load_current
334 
335#ifdef CONFIG_KASAN
336	call kasan_early_init
337#endif
338	/* Start the kernel */
339	call soc_early_init
340	tail start_kernel

203-340줄 주석

203SYM_CODE_START(_start_kernel)

linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.

204/* Mask all interrupts */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

205csrw CSR_IE, zero

초기 trap vector와 per-hart stack이 준비될 때까지 interrupt enable을 모두 내린다.

206csrw CSR_IP, zero

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

207(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

208#ifdef CONFIG_RISCV_M_MODE

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

209/* flush the instruction cache */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

210fence.i

메모리 또는 instruction fetch의 관찰 순서를 강제한다. 단순 compiler barrier가 아니라 hart가 보는 상태를 바꾸는 명령이다.

211(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

212/* Reset all registers except ra, a0, a1 */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

213call reset_regs

하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.

214(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

215/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

216* Setup a PMP to permit access to all of memory. Some machines may

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

217* not implement PMPs, so we set up a quick trap handler to just skip

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

218* touching the PMPs on any trap.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

219*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

220la a0, .Lpmp_done

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

221csrw CSR_TVEC, a0

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

222(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

223li a0, -1

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

224csrw CSR_PMPADDR0, a0

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

225li a0, (PMP_A_NAPOT | PMP_R | PMP_W | PMP_X)

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

226csrw CSR_PMPCFG0, a0

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

227.align 2

linker와 assembler가 진입 symbol, section 또는 정렬을 확정하는 지시자다. runtime 명령은 아니지만 entry address와 배치를 결정한다.

228.Lpmp_done:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

229(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

230/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

231* The hartid in a0 is expected later on, and we have no firmware

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

232* to hand it to us.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

233*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

234csrr a0, CSR_MHARTID

M-mode kernel은 firmware argument 대신 hardware CSR에서 현재 hart ID를 직접 읽는다.

235#else

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

236/* Enable time CSR */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

237li t0, 0x2

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

238csrw CSR_SCOUNTEREN, t0

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

239#endif /* CONFIG_RISCV_M_MODE */

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

240(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

241/* Load the global pointer */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

242load_global_pointer

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

243(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

244/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

245* Disable FPU & VECTOR to detect illegal usage of

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

246* floating point or vector in kernel space

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

247*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

248li t0, SR_FS_VS

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

249csrc CSR_STATUS, t0

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

250(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

251#ifdef CONFIG_RISCV_BOOT_SPINWAIT

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

252li t0, CONFIG_NR_CPUS

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

253blt a0, t0, .Lgood_cores

직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.

254tail .Lsecondary_park

현재 함수로 돌아오지 않는 제어 이전이다. 다음 진입점이 요구하는 register와 주소 공간을 이 줄 직전에 모두 완성해야 한다.

255.Lgood_cores:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

256(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

257/* The lottery system is only required for spinwait booting method */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

258#ifndef CONFIG_XIP_KERNEL

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

259/* Pick one hart to run the main boot sequence */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

260la a3, hart_lottery

spinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.

261li a2, 1

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

262amoadd.w a3, a2, (a3)

원자적 read-modify-write로 여러 hart 사이의 선택 또는 게시를 직렬화한다. 반환값은 갱신 전 값이라는 점이 분기 조건에 중요하다.

263bnez a3, .Lsecondary_start

직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.

264(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

265#else

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

266/* hart_lottery in flash contains a magic number */

spinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.

267la a3, hart_lottery

spinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.

268mv a2, a3

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

269XIP_FIXUP_OFFSET a2

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

270XIP_FIXUP_FLASH_OFFSET a3

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

271lw t1, (a3)

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

272amoswap.w t0, t1, (a2)

원자적 read-modify-write로 여러 hart 사이의 선택 또는 게시를 직렬화한다. 반환값은 갱신 전 값이라는 점이 분기 조건에 중요하다.

273/* first time here if hart_lottery in RAM is not set */

spinwait boot 방식에서 단 하나의 hart만 global 초기화를 수행하도록 원자적 lottery를 사용한다.

274beq t0, t1, .Lsecondary_start

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

275(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

276#endif /* CONFIG_XIP */

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

277#endif /* CONFIG_RISCV_BOOT_SPINWAIT */

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

278(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

279#ifdef CONFIG_XIP_KERNEL

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

280la sp, _end + THREAD_SIZE

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

281XIP_FIXUP_OFFSET sp

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

282mv s0, a0

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

283mv s1, a1

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

284call __copy_data

하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.

285(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

286/* Restore a0 & a1 copy */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

287mv a0, s0

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

288mv a1, s1

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

289#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

290(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

291#ifndef CONFIG_XIP_KERNEL

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

292/* Clear BSS for flat non-ELF images */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

293la a3, __bss_start

flat Image에서 zero-init global의 C 언어 계약을 assembly가 직접 만족시킨다.

294la a4, __bss_stop

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

295ble a4, a3, .Lclear_bss_done

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

296.Lclear_bss:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

297REG_S zero, (a3)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

298add a3, a3, RISCV_SZPTR

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

299blt a3, a4, .Lclear_bss

직전에 만든 값이나 flag를 검사해 경로를 나눈다. 실패 분기가 어디에서 멈추는지까지 따라가야 조건의 의미가 완성된다.

300.Lclear_bss_done:

분기 대상 label이다. 이 지점에 들어올 수 있는 모든 선행 경로가 같은 register와 stack 조건을 만족하는지 비교한다.

301#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

302la a2, boot_cpu_hartid

generic SMP 코드가 사용할 대표 boot hart ID를 전역 위치에 게시한다.

303XIP_FIXUP_OFFSET a2

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

304REG_S a0, (a2)

XLEN에 맞는 폭으로 register를 저장하거나 읽는다. frame offset과 구조체 정의가 같은 layout을 전제해야 한다.

305(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

306/* Initialize page tables and relocate to virtual addresses */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

307la tp, init_task

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

308la sp, init_thread_union + THREAD_SIZE

boot task의 kernel stack top을 만들고 pt_regs 예약분을 뺀다.

309XIP_FIXUP_OFFSET sp

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

310addi sp, sp, -PT_SIZE_ON_STACK

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

311scs_load_init_stack

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

312#ifdef CONFIG_BUILTIN_DTB

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

313la a0, __dtb_start

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

314XIP_FIXUP_OFFSET a0

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

315#else

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

316mv a0, a1

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

317#endif /* CONFIG_BUILTIN_DTB */

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

318/* Set trap vector to spin forever to help debug */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

319la a3, .Lsecondary_park

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

320csrw CSR_TVEC, a3

RISC-V CSR을 읽거나 갱신한다. privilege mode와 write side effect가 일반 register 연산과 다르다.

321call setup_vm

MMU-off C helper가 DTB와 kernel 배치를 바탕으로 early page table을 작성한다.

322#ifdef CONFIG_MMU

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

323la a0, early_pg_dir

작성된 초기 page-table root를 relocate_enable_mmu에 넘긴다.

324XIP_FIXUP_OFFSET a0

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

325call relocate_enable_mmu

하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.

326#endif /* CONFIG_MMU */

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

327(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

328call .Lsetup_trap_vector

하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.

329/* Restore C environment */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

330la tp, init_task

주소 또는 상태를 다음 계산에 사용할 register로 옮긴다. 이 시점의 값이 물리 주소인지 가상 주소인지 구분해야 한다.

331la sp, init_thread_union + THREAD_SIZE

boot task의 kernel stack top을 만들고 pt_regs 예약분을 뺀다.

332addi sp, sp, -PT_SIZE_ON_STACK

주소, 정렬 또는 bit field를 계산한다. overflow와 정렬 단위, inclusive/exclusive end를 앞뒤 줄과 함께 본다.

333scs_load_current

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

334(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

335#ifdef CONFIG_KASAN

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

336call kasan_early_init

하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.

337#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

338/* Start the kernel */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

339call soc_early_init

하위 루틴으로 진입한다. 호출 직전 argument register, stack 정렬, 보존 register가 해당 ABI를 만족하는지 확인한다.

340tail start_kernel

정상 virtual address, stack, task pointer, trap vector를 갖춘 뒤 generic kernel init으로 비복귀 전환한다.

setup_vm이 kernel PA·VA offset과 early page-table 한계를 계산하는 구간

head.S의 첫 C 호출은 MMU-off 제약 아래에서 kernel mapping을 수치로 만든다. KASLR offset, physical load address, virtual link address와 early PUD 범위를 한 함수에서 결정한다.

원본: arch/riscv/mm/init.c 1119-1204줄

1119asmlinkage void __init setup_vm(uintptr_t dtb_pa)
1120{
1121	pmd_t __maybe_unused fix_bmap_spmd, fix_bmap_epmd;
1122 
1123#ifdef CONFIG_RANDOMIZE_BASE
1124	if (!__pi_set_nokaslr_from_cmdline(dtb_pa)) {
1125		u64 kaslr_seed = __pi_get_kaslr_seed_zkr(dtb_pa);
1126		u32 kernel_size = (uintptr_t)(&_end) - (uintptr_t)(&_start);
1127		u32 nr_pos;
1128 
1129		if (kaslr_seed == 0)
1130			kaslr_seed = __pi_get_kaslr_seed(dtb_pa);
1131		/*
1132		 * Compute the number of positions available: we are limited
1133		 * by the early page table that only has one PUD and we must
1134		 * be aligned on PMD_SIZE.
1135		 */
1136		nr_pos = (PUD_SIZE - kernel_size) / PMD_SIZE;
1137 
1138		kernel_map.virt_offset = (kaslr_seed % nr_pos) * PMD_SIZE;
1139	}
1140#endif
1141 
1142	kernel_map.virt_addr = KERNEL_LINK_ADDR + kernel_map.virt_offset;
1143 
1144#ifdef CONFIG_XIP_KERNEL
1145	kernel_map.xiprom = (uintptr_t)CONFIG_XIP_PHYS_ADDR;
1146	kernel_map.xiprom_sz = (uintptr_t)(&_exiprom) - (uintptr_t)(&_xiprom);
1147 
1148	phys_ram_base = CONFIG_PHYS_RAM_BASE;
1149#ifdef CONFIG_SPARSEMEM_VMEMMAP
1150	vmemmap_start_pfn = round_down(phys_ram_base, VMEMMAP_ADDR_ALIGN) >> PAGE_SHIFT;
1151#endif
1152	kernel_map.phys_addr = (uintptr_t)CONFIG_PHYS_RAM_BASE;
1153	kernel_map.size = (uintptr_t)(&_end) - (uintptr_t)(&_start);
1154 
1155	kernel_map.va_kernel_xip_text_pa_offset = kernel_map.virt_addr - kernel_map.xiprom;
1156	kernel_map.va_kernel_xip_data_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr
1157						+ (uintptr_t)&_sdata - (uintptr_t)&_start;
1158#else
1159	kernel_map.phys_addr = (uintptr_t)(&_start);
1160	kernel_map.size = (uintptr_t)(&_end) - kernel_map.phys_addr;
1161	kernel_map.va_kernel_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr;
1162#endif
1163 
1164#if defined(CONFIG_64BIT) && !defined(CONFIG_XIP_KERNEL)
1165	set_satp_mode(dtb_pa);
1166	set_mmap_rnd_bits_max();
1167#endif
1168 
1169	/*
1170	 * In 64-bit, we defer the setup of va_pa_offset to setup_bootmem,
1171	 * where we have the system memory layout: this allows us to align
1172	 * the physical and virtual mappings and then make use of PUD/P4D/PGD
1173	 * for the linear mapping. This is only possible because the kernel
1174	 * mapping lies outside the linear mapping.
1175	 * In 32-bit however, as the kernel resides in the linear mapping,
1176	 * setup_vm_final can not change the mapping established here,
1177	 * otherwise the same kernel addresses would get mapped to different
1178	 * physical addresses (if the start of dram is different from the
1179	 * kernel physical address start).
1180	 */
1181	kernel_map.va_pa_offset = IS_ENABLED(CONFIG_64BIT) ?
1182				0UL : PAGE_OFFSET - kernel_map.phys_addr;
1183 
1184	memory_limit = KERN_VIRT_SIZE;
1185 
1186	/* Sanity check alignment and size */
1187	BUG_ON((PAGE_OFFSET % PGDIR_SIZE) != 0);
1188	BUG_ON((kernel_map.phys_addr % PMD_SIZE) != 0);
1189 
1190#ifdef CONFIG_64BIT
1191	/*
1192	 * The last 4K bytes of the addressable memory can not be mapped because
1193	 * of IS_ERR_VALUE macro.
1194	 */
1195	BUG_ON((kernel_map.virt_addr + kernel_map.size) > ADDRESS_SPACE_END - SZ_4K);
1196#endif
1197 
1198#ifdef CONFIG_RELOCATABLE
1199	/*
1200	 * Early page table uses only one PUD, which makes it possible
1201	 * to map PUD_SIZE aligned on PUD_SIZE: if the relocation offset
1202	 * makes the kernel cross over a PUD_SIZE boundary, raise a bug
1203	 * since a part of the kernel would not get mapped.
1204	 */

1119-1204줄 주석

1119asmlinkage void __init setup_vm(uintptr_t dtb_pa)

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1120{

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1121pmd_t __maybe_unused fix_bmap_spmd, fix_bmap_epmd;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1122(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1123#ifdef CONFIG_RANDOMIZE_BASE

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1124if (!__pi_set_nokaslr_from_cmdline(dtb_pa)) {

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1125u64 kaslr_seed = __pi_get_kaslr_seed_zkr(dtb_pa);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1126u32 kernel_size = (uintptr_t)(&_end) - (uintptr_t)(&_start);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1127u32 nr_pos;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1128(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1129if (kaslr_seed == 0)

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1130kaslr_seed = __pi_get_kaslr_seed(dtb_pa);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1131/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1132* Compute the number of positions available: we are limited

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1133* by the early page table that only has one PUD and we must

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1134* be aligned on PMD_SIZE.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1135*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1136nr_pos = (PUD_SIZE - kernel_size) / PMD_SIZE;

early page table 한 PUD 안에서 kernel 전체가 PMD 경계를 넘지 않고 이동할 수 있는 후보 개수를 계산한다.

1137(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1138kernel_map.virt_offset = (kaslr_seed % nr_pos) * PMD_SIZE;

seed를 후보 수로 나눈 나머지에 PMD_SIZE를 곱해 2MiB 단위 virtual KASLR offset을 만든다.

1139}

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1140#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1141(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1142kernel_map.virt_addr = KERNEL_LINK_ADDR + kernel_map.virt_offset;

seed를 후보 수로 나눈 나머지에 PMD_SIZE를 곱해 2MiB 단위 virtual KASLR offset을 만든다.

1143(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1144#ifdef CONFIG_XIP_KERNEL

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1145kernel_map.xiprom = (uintptr_t)CONFIG_XIP_PHYS_ADDR;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1146kernel_map.xiprom_sz = (uintptr_t)(&_exiprom) - (uintptr_t)(&_xiprom);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1147(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1148phys_ram_base = CONFIG_PHYS_RAM_BASE;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1149#ifdef CONFIG_SPARSEMEM_VMEMMAP

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1150vmemmap_start_pfn = round_down(phys_ram_base, VMEMMAP_ADDR_ALIGN) >> PAGE_SHIFT;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1151#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1152kernel_map.phys_addr = (uintptr_t)CONFIG_PHYS_RAM_BASE;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1153kernel_map.size = (uintptr_t)(&_end) - (uintptr_t)(&_start);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1154(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1155kernel_map.va_kernel_xip_text_pa_offset = kernel_map.virt_addr - kernel_map.xiprom;

link address에 virtual KASLR offset을 더한 실제 kernel VA 시작점이다.

1156kernel_map.va_kernel_xip_data_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr

link address에 virtual KASLR offset을 더한 실제 kernel VA 시작점이다.

1157+ (uintptr_t)&_sdata - (uintptr_t)&_start;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1158#else

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1159kernel_map.phys_addr = (uintptr_t)(&_start);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1160kernel_map.size = (uintptr_t)(&_end) - kernel_map.phys_addr;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1161kernel_map.va_kernel_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr;

link address에 virtual KASLR offset을 더한 실제 kernel VA 시작점이다.

1162#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1163(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1164#if defined(CONFIG_64BIT) && !defined(CONFIG_XIP_KERNEL)

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1165set_satp_mode(dtb_pa);

DTB와 hardware capability를 바탕으로 Sv39/Sv48/Sv57 후보를 검증한다. 지원되지 않는 mode를 SATP에 남기면 WARL 동작으로 write가 무시될 수 있다.

1166set_mmap_rnd_bits_max();

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1167#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1168(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1169/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1170* In 64-bit, we defer the setup of va_pa_offset to setup_bootmem,

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1171* where we have the system memory layout: this allows us to align

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1172* the physical and virtual mappings and then make use of PUD/P4D/PGD

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1173* for the linear mapping. This is only possible because the kernel

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1174* mapping lies outside the linear mapping.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1175* In 32-bit however, as the kernel resides in the linear mapping,

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1176* setup_vm_final can not change the mapping established here,

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1177* otherwise the same kernel addresses would get mapped to different

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1178* physical addresses (if the start of dram is different from the

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1179* kernel physical address start).

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1180*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1181kernel_map.va_pa_offset = IS_ENABLED(CONFIG_64BIT) ?

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

11820UL : PAGE_OFFSET - kernel_map.phys_addr;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1183(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1184memory_limit = KERN_VIRT_SIZE;

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1185(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1186/* Sanity check alignment and size */

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1187BUG_ON((PAGE_OFFSET % PGDIR_SIZE) != 0);

이 줄이 읽는 register와 memory, 그리고 다음 줄에 남기는 값을 기록한다. 단독으로 해석하지 말고 바로 앞의 생산자와 뒤의 소비자를 연결해 본다.

1188BUG_ON((kernel_map.phys_addr % PMD_SIZE) != 0);

초기 superpage mapping의 전제인 2MiB physical alignment를 명시적으로 검사한다.

1189(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1190#ifdef CONFIG_64BIT

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1191/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1192* The last 4K bytes of the addressable memory can not be mapped because

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1193* of IS_ERR_VALUE macro.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1194*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1195BUG_ON((kernel_map.virt_addr + kernel_map.size) > ADDRESS_SPACE_END - SZ_4K);

link address에 virtual KASLR offset을 더한 실제 kernel VA 시작점이다.

1196#endif

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1197(빈 줄)

빈 줄은 명령 묶음의 경계를 보존한다. 위에서 만든 상태와 아래에서 소비하는 상태를 나누어 읽는 자리다.

1198#ifdef CONFIG_RELOCATABLE

빌드 설정에 따라 실제 binary에 포함되는 경로가 달라지는 전처리 경계다. 분석 대상의 .config와 objdump 결과로 어느 가지가 남았는지 확인한다.

1199/*

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1200* Early page table uses only one PUD, which makes it possible

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1201* to map PUD_SIZE aligned on PUD_SIZE: if the relocation offset

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1202* makes the kernel cross over a PUD_SIZE boundary, raise a bug

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1203* since a part of the kernel would not get mapped.

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

1204*/

원본 주석이다. 현재 진입 계약이나 바로 아래 명령 묶음의 이유를 밝히므로 실행 코드와 같은 비중으로 읽는다.

숫자를 넣어 끝까지 계산한다

01

Sv39 SATP 값을 실제 PPN으로 조립한다

early_pg_dir physical address가 0x80204000, ASID가 0이고 Sv39 mode를 선택했다고 가정한다.

  1. PPN0x80204000 >> 12 = 0x80204가 SATP.PPN에 들어간다.
  2. MODERV64 SATP.MODE의 Sv39 값 8을 bit 63:60에 두므로 mode 부분은 0x8000000000000000이다.
  3. SATPASID 0이면 최종 값은 0x8000000000080204다.
  4. orderingPTE store 뒤 sfence.vma, SATP write, 필요한 sfence.vma를 거쳐 이전 translation을 제거한다.

판정SATP dump는 16진수 한 줄로 끝내지 말고 MODE, ASID, PPN으로 분해하고 PPN을 다시 12bit 이동해 실제 root page 주소와 대조한다.

02

kernel PA와 link VA 사이의 relocation offset

kernel load PA=0x80200000, KERNEL_LINK_ADDR=0xffffffff80000000, virtual KASLR offset=0x04000000을 예로 든다.

  1. runtime VA0xffffffff80000000 + 0x04000000 = 0xffffffff84000000
  2. VA-PA offset0xffffffff84000000 - 0x80200000 = 0xffffffff03e00000
  3. symbol conversionlink symbol에 virtual KASLR offset을 반영한 뒤 이 offset으로 PA와 VA를 왕복한다.
  4. superpage conditionload PA 0x80200000은 2MiB 정렬이므로 PMD leaf mapping 전제를 만족한다.

판정첫 SATP 전환 전에는 PA, 전환 후에는 VA가 같은 instruction byte를 가리키는지 PTE physical frame까지 확인한다.

코드에서 놓치기 쉬운 경계

01

RISC-V Image header는 실행 코드와 metadata가 같은 64-byte 안에 공존한다

첫 instruction은 CPU가 실행할 수 있어야 하고 UEFI가 볼 때는 MZ signature여야 한다. 뒤 필드는 load offset, image size, flags, magic, PE header offset을 고정 layout으로 제공한다.

header를 바꿀 때는 kernel만 rebuild해 보는 것으로 부족하다. U-Boot booti, EFI stub, image parser가 같은 offset과 endian을 기대하는지 확인해야 한다.

02

S-mode와 M-mode kernel의 책임이 다르다

S-mode kernel은 machine-mode firmware가 timer, IPI, hart start 같은 서비스를 SBI로 제공한다고 본다. M-mode kernel은 PMP를 열고 mhartid를 직접 읽는 코드가 포함된다.

bring-up 문서에는 현재 privilege mode, OpenSBI version, delegation된 exception/interrupt를 먼저 적어야 CSR fault의 위치를 해석할 수 있다.

03

setup_vm은 일반 C 함수처럼 보여도 실행 제약이 크다

MMU-off에서 호출되므로 absolute addressing, sanitizer, ftrace instrumentation과 일반 allocator를 사용할 수 없다. page table은 statically reserved early arrays에 기록되고 DTB pointer도 물리 주소다.

compiler option 변화로 instrumentation이 들어가면 source는 같아도 첫 C call에서 virtual address를 참조할 수 있으므로 disassembly 검사가 필요하다.

04

trampoline page table은 첫 instruction fetch를 보장한다

SATP에 새 root를 쓰는 순간부터 다음 fetch 주소 해석이 바뀐다. 현재 physical PC와 보정된 virtual PC가 같은 code byte를 가리키는 mapping을 첫 superpage에 만들어 전환을 연속적으로 보이게 한다.

sfence.vma는 PTE write의 가시성을, 두 번째는 실제 kernel root 전환 뒤 trampoline translation 제거를 보장한다.

05

gp와 tp는 일반 임시 register가 아니다

gp는 small data addressing의 기준이고 tp는 Linux에서 current task/per-CPU 접근의 기반으로 쓰인다. virtual relocation 뒤 gp를 다시 적재하고 boot stack 준비 시 tp를 init_task에 맞춰야 C 코드가 정상 global/task state를 본다.

GDB에서 함수 call이 시작되기 전 gp/tp가 symbol table의 예상 범위에 있는지 확인하면 초기 C fault를 빠르게 구분할 수 있다.

06

DTB는 setup_vm에서 읽은 뒤에도 수명이 계속된다

a1의 물리 DTB는 page-table mode와 memory topology를 정하는 입력이며 이후 early DT scan까지 보존되어야 한다. kernel image, BSS clear, initrd와 겹치면 MMU 전환은 성공해도 setup_arch에서 깨질 수 있다.

부트로더 memory map 그림에는 DTB의 start/end와 reservation 여부를 반드시 넣는다.

멈춘 위치보다 먼저 볼 값

조건관찰되는 증상첫 확인 값
a0/a1 계약 위반boot hart가 잘못 선택되거나 DTB magic 검사 전에 fault가 난다.firmware 직전 register dump
trampoline mapping 누락첫 SATP write 직후 instruction page fault 또는 park loop로 들어간다.PC superpage의 PA/VA PTE
early_pg_dir 정렬/주소 오류두 번째 SATP 전환 뒤 global 또는 stack 접근이 실패한다.SATP PPN과 PTE flags
gp/tp 복구 오류첫 C helper나 current 접근에서 엉뚱한 global을 읽는다.gp, tp, init_task symbol
DTB overlapsetup_vm 이후 machine model 또는 memory scan이 비정상이다.DTB start/end reservation

직접 확인할 실험

  1. 01
    OpenSBI 직전과 Linux _start에서 a0, a1, privilege mode, satp, stvec를 기록한다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

  2. 02
    QEMU GDB에서 첫/두 번째 csrw satp에 breakpoint를 두고 PC의 physical/virtual PTE를 직접 walk한다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

  3. 03
    readelf -lW vmlinux_start, _end, page-table arrays의 section과 load address를 확인한다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

  4. 04
    부팅 옵션 earlycon을 사용하되 첫 출력 이전 구간은 SBI debug console 또는 GDB checkpoint로 따로 계측한다.

    명령을 실행한 시점의 PC, stack, address-space root와 CPU/hart 번호를 함께 기록한다.

기준 원문