43
 44 #define __PHYS_OFFSET   (KERNEL_START - TEXT_OFFSET)

  • KERNEL_START = _text
    • 커널 이미지가 시작되는 가상 주소(Virtual Address)입니다. 
    • _text = 0xFFFF 0000 0808 0000
  • TEXT_OFFSET = 0x80000 (512KB)
    • 부트로더가 물리 메모리에 커널 이미지를 로드할 때, RAM 시작점으로부터 이 오프셋만큼 비워두고 로드해야 하는 규칙(Boot Protocol)이 있습니다. (5.x 버전에서 보안 및 최적화를 위해 Boot Protocol이 변경되서 0으로 설정)
  • __PHYS_OFFSET   = 0xFFFF 0000 0808 0000 - 0x8 0000 = 0xFFFF 0000 0800 0000

 

주의 : __PHYS_OFFSET 는 실제로 0xFFFF 0000 0800 0000 값으로 사용되지 않습니다.

 

__PHYS_OFFSET  은 아래 216라인에서 adrp 명령에 의해 _text 의 물리 주소를 획득해 커널이미지의 물리시작 주소를 나타냅니다.

 

예시 )  adrp    x24, __PHYS_OFFSET


arch/arm64/Makefile

 61 # The byte offset of the kernel image in RAM from the start of RAM.
 62 ifeq ($(CONFIG_ARM64_RANDOMIZE_TEXT_OFFSET), y)
 63 TEXT_OFFSET := $(shell awk 'BEGIN {srand(); printf "0x%03x000\n", int(512 * rand())}')
 64 else
 65 TEXT_OFFSET := 0x00080000
 66 endif


arch/arm64/kernel/vmlinux.lds.S

 93         . = KIMAGE_VADDR + TEXT_OFFSET;

  • KIMAGE_VADDR  = 0xFFFF 0000 0800 0000
  • TEXT_OFFSET= 0x80000
  • . = 0xFFFF 0000 0808 0000

 

 94
 95         .head.text : {
 96                 _text = .;

  • _text = 0xFFFF 0000 0808 0000


 97                 HEAD_TEXT

  • 480 #define HEAD_TEXT  *(.head.text)

 

 98         }


arch/arm64/include/asm/memory.h

 42 /*
 43  * PAGE_OFFSET - the virtual address of the start of the kernel image (top
 44  *       (VA_BITS - 1))

  • 주석 오류 : 4.7에서 PAGE_OFFSET - the virtual address of the start of the linear map 으로 수정됨
  • Linear Mapping 영역의 시작점 (물리주소와 1:1 매핑)


 45  * VA_BITS - the maximum number of bits for virtual addresses.
 46  * VA_START - the first kernel virtual address.
 47  * TASK_SIZE - the maximum size of a user space task.
 48  * TASK_UNMAPPED_BASE - the lower boundary of the mmap VM area.
 49  */
 50 #define VA_BITS         (CONFIG_ARM64_VA_BITS)
 51 #define VA_START        (UL(0xffffffffffffffff) << VA_BITS)
 52 #define PAGE_OFFSET     (UL(0xffffffffffffffff) << (VA_BITS - 1))
 53 #define KIMAGE_VADDR        (MODULES_END)
 54 #define MODULES_END     (MODULES_VADDR + MODULES_VSIZE)
 55 #define MODULES_VADDR       (VA_START + KASAN_SHADOW_SIZE)
 56 #define MODULES_VSIZE       (SZ_128M)

 

CONFIG_ARM64_VA_BITS = 48 기준


|
|0xFFFF FFFF FFFF FFFF
|
|
|
|
|0xFFFF 8000 0000 0000 = PAGE_OFFSET     Linear Mapping 영역의 시작점 (물리주소와 1:1 매핑)
|
|
|
|
|0xFFFF 0000 0808 0000 = KERNEL_START, _text
|
|                  +512KB (0x8 0000)
|
|0xFFFF 0000 0800 0000 = KIMAGE_VADDR, MODULES_END, __PHYS_OFFSET
|
|                  +128MB (0x800 0000)
|
|0xFFFF 0000 0000 0000  = MODULES_VADDR = VA_START ( KASAN_SHADOW_SIZE는 0으로 계산 : 분석의 편의를 위해)
|
|
|
|
|0x0000 0000 0000 0000
|


 45
 46 #if (TEXT_OFFSET & 0xfff) != 0
 47 #error TEXT_OFFSET must be at least 4KB aligned
 48 #elif (PAGE_OFFSET & 0x1fffff) != 0
 49 #error PAGE_OFFSET must be at least 2MB aligned
 50 #elif TEXT_OFFSET > 0x1fffff
 51 #error TEXT_OFFSET must be less than 2MB
 52 #endif

  • 부팅 프로토콜에서 TEXT_OFFSET 의 최대크기는 2MB 미만으로 제한하고 있습니다.


 53
 54 #define KERNEL_START    _text
 55 #define KERNEL_END      _end
 56
 57 /*
 58  * Kernel startup entry point.
 59  * ---------------------------
 60  *
 61  * The requirements are:
 62  *   MMU = off, D-cache = off, I-cache = on or off,
 63  *   x0 = physical address to the FDT blob.
 64  *
 65  * This code is mostly position independent so you call this at
 66  * __pa(PAGE_OFFSET + TEXT_OFFSET).
 67  *
 68  * Note that the callee-saved registers are used for storing variables
 69  * that are useful before the MMU is enabled. The allocations are described
 70  * in the entry routines.
 71  */
 72         __HEAD

  • 103 #define __HEAD      .section    ".head.text","ax"


 73 _head:

 74         /*
 75          * DO NOT MODIFY. Image header expected by Linux boot-loaders.
 76          */
 77 #ifdef CONFIG_EFI
 78         /*
 79          * This add instruction has no meaningful effect except that
 80          * its opcode forms the magic "MZ" signature required by UEFI.
 81          */
 82         add     x13, x18, #0x16
 83         b       stext
 84 #else
 85         b       stext                           // branch to kernel start, magic
 86         .long   0                               // reserved
 87 #endif

 

212 ENTRY(stext)
213         bl      preserve_boot_args

  • boot_args[] 에 부트로더에서 넘어온 x0 ~ x3 까지 저장


214         bl      el2_setup                       // Drop to EL1, w20=cpu_boot_mode

  • el2에서 부팅된경우 하이퍼바이저 환경 setup


215         mov     x23, xzr                        // KASLR offset, defaults to 0
216         adrp    x24, __PHYS_OFFSET

  • adrp 명령을 사용함으로써 _text 의 물리주소에서 TEXT_OFFSET 뺀값 즉 커널이미지가 올라온 물리주소의 시작 주소를 획득하여 x24에 보관합니다.
  • 컴파일시 adrp 명령은 현재위치(0xFFFF 0000 0808 nnnn) -  _text(0xFFFF 0000 0808 0000) - TEXT_OFFSET(0x8 0000) 을 계산한 상대주소(즉 현재 위치에서 얼마나 떨어져있나(예 xxxx))값을 갖고 있다가, 실행시에 현재위치(0x4008 yyyy) + xxxx(상대주소) 한 곳의 주소를 x24에 보관한다. (상대주소는 페이지 단위임)
  • adrp 현재 위치에서 n 페이지 만큼 떨어진 페이지의 주소를 획득합니다.


217         bl      set_cpu_boot_mode_flag

  • __boot_cpu_mode 에 부팅시의 익셉션레벨 저장


218         bl      __create_page_tables            // x25=TTBR0, x26=TTBR1

  • ttbr0 = idmap_pd_dir 을 PGD로 해서 __idmap_text_start ~ __idmap_text_end 까지 Section Mapping
  • ttbr1 = swapper_pd_dir을 PGD로 해서 KIMAGE_VADDR 시작해서 kernel_img_size 만큼 Section Mapping

219         /*
220          * The following calls CPU setup code, see arch/arm64/mm/proc.S for
221          * details.
222          * On return, the CPU will be ready for the MMU to be turned on and
223          * the TCR will have been set.
224          */
225         ldr     x27, 0f                         // address to jump to after
226                                                 // MMU has been enabled
227         adr_l   lr, __enable_mmu                // return (PIC) address
228         b       __cpu_setup                     // initialise processor
229 ENDPROC(stext)
230         .align  3
231 0:      .quad   __mmap_switched - (_head - TEXT_OFFSET) + KIMAGE_VADDR

  • __mmap_switched - (_head - TEXT_OFFSET)  = RAM의 시작 물리주소 부터 __mmap_switched 까지의 offset
  • + KIMAGE_VADDR : 에 커널이미지 시작 가상주소를 더해서 __mmap_switched 의 가상주소 계산

316 __create_page_tables:
317         adrp    x25, idmap_pg_dir
318         adrp    x26, swapper_pg_dir
319         mov     x28, lr

  • 유저영역의 PGD로 idmap_pg_dir, 커널영역의 PGD로 swapper_pg_dir 지정

320
321         /*
322          * Invalidate the idmap and swapper page tables to avoid potential
323          * dirty cache lines being evicted.
324          */
325         mov     x0, x25
326         add     x1, x26, #SWAPPER_DIR_SIZE
327         bl      __inval_cache_range
328
329         /*
330          * Clear the idmap and swapper page tables.
331          */
332         mov     x0, x25
333         add     x6, x26, #SWAPPER_DIR_SIZE
334 1:      stp     xzr, xzr, [x0], #16
335         stp     xzr, xzr, [x0], #16
336         stp     xzr, xzr, [x0], #16
337         stp     xzr, xzr, [x0], #16
338         cmp     x0, x6
339         b.lo    1b

  • idmap과 swapper page table 초기화


340
341         ldr     x7, =SWAPPER_MM_MMUFLAGS

  • #define SWAPPER_MM_MMUFLAGS (PMD_ATTRINDX(MT_NORMAL) | SWAPPER_PMD_FLAGS)
  • MT_NORMAL = 캐쉬가능한 일반메모리
  • SWAPPER_PMD_FLAGS = Section Mapping (2MB단위)


342
343         /*
344          * Create the identity mapping.
345          */
346         mov     x0, x25                         // idmap_pg_dir
347         adrp    x3, __idmap_text_start          // __pa(__idmap_text_start)
348

 

 

388         create_pgd_entry x0, x3, x5, x6
389         mov     x5, x3                          // __pa(__idmap_text_start)
390         adr_l   x6, __idmap_text_end            // __pa(__idmap_text_end)
391         create_block_map x0, x7, x3, x5, x6

  • __idmap_text_start     ~      __idmap_text_end     까지 Section Mapping
  • idmap_pg_dir 이 PGD


392
393         /*
394          * Map the kernel image (starting with PHYS_OFFSET).
395          */
396         mov     x0, x26                         // swapper_pg_dir
397         ldr     x5, =KIMAGE_VADDR
398         add     x5, x5, x23                     // add KASLR displacement
399         create_pgd_entry x0, x5, x3, x6
400         ldr     w6, kernel_img_size
401         add     x6, x6, x5
402         mov     x3, x24                         // phys offset
403         create_block_map x0, x7, x3, x5, x6

  • KIMAGE_VADDR + kernel_img_size 까지 swapper_pg_dir을 PGD로 하는 Section Mapping


404
405         /*
406          * Since the page tables have been populated with non-cacheable
407          * accesses (MMU disabled), invalidate the idmap and swapper page
408          * tables again to remove any speculatively loaded cache lines.
409          */
410         mov     x0, x25
411         add     x1, x26, #SWAPPER_DIR_SIZE
412         dmb     sy
413         bl      __inval_cache_range
414
415         ret     x28
416 ENDPROC(__create_page_tables)

417
418 kernel_img_size:
419         .long   _end - (_head - TEXT_OFFSET)
420         .ltorg

  • _end - _head : 커널이미지 크기
  • 여기에 TEXT_OFFSET을 더해서 부팅 프로토콜에 의해서 추가 확보된 공간도 커널 이미지 크기에 추가

 


 

 

 

183 /*
184  *      __cpu_setup
185  *
186  *      Initialise the processor for turning the MMU on.  Return in x0 the
187  *      value of the SCTLR_EL1 register.
188  */
189 ENTRY(__cpu_setup)
190         tlbi    vmalle1                         // Invalidate local TLB
191         dsb     nsh
192
193         mov     x0, #3 << 20
194         msr     cpacr_el1, x0                   // Enable FP/ASIMD
195         mov     x0, #1 << 12                    // Reset mdscr_el1 and disable
196         msr     mdscr_el1, x0                   // access to the DCC from EL0
197         reset_pmuserenr_el0 x0                  // Disable PMU access from EL0
198         /*
199          * Memory region attributes for LPAE:
200          *
201          *   n = AttrIndx[2:0]
202          *                      n       MAIR
203          *   DEVICE_nGnRnE      000     00000000
204          *   DEVICE_nGnRE       001     00000100
205          *   DEVICE_GRE         010     00001100
206          *   NORMAL_NC          011     01000100
207          *   NORMAL             100     11111111
208          *   NORMAL_WT          101     10111011
209          */
210         ldr     x5, =MAIR(0x00, MT_DEVICE_nGnRnE) | \
211                      MAIR(0x04, MT_DEVICE_nGnRE) | \
212                      MAIR(0x0c, MT_DEVICE_GRE) | \
213                      MAIR(0x44, MT_NORMAL_NC) | \
214                      MAIR(0xff, MT_NORMAL) | \
215                      MAIR(0xbb, MT_NORMAL_WT)
216         msr     mair_el1, x5

  • mair 에 페이지들이 갖을수 있는 속성 및 이에 따른 번호를 미리 정하고 각 페이지변환테이블의 AttrIndx[2:0] 비트에서 해당 페이지의 속성번호를 지정

217         /*
218          * Prepare SCTLR
219          */

220         adr     x5, crval
221         ldp     w5, w6, [x5]
222         mrs     x0, sctlr_el1
223         bic     x0, x0, x5                      // clear bits
224         orr     x0, x0, x6                      // set bits
225         /*
226          * Set/prepare TCR and TTBR. We use 512GB (39-bit) address range for
227          * both user and kernel.
228          */
229         ldr     x10, =TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
230                         TCR_TG_FLAGS | TCR_ASID16 | TCR_TBI0
231         tcr_set_idmap_t0sz      x10, x9

 

 

TCR (Translation Control Register) 

1. TCR_TxSZ(VA_BITS): 가상 주소창 크기 조절 스위치 (주석의 핵심)
가상 주소를 몇 비트까지 쓸 것인지 MMU에게 알려주는 비트입니다.
  • ARM64에서 주소는 최대 64비트이지만, 실제로는 자원 낭비를 막기 위해 39비트, 48비트 등 원하는 크기만큼만 가상 주소를 사용합니다. 
  • TxSZ 공식은 64 - 가상 주소 비트 수로 계산합니다. 주석대로 가상 주소를 39비트(VA_BITS = 39)만 쓰겠다면, 하드웨어 레지스터에는 64 - 39 = 25라는 값을 기록하게 됩니다. 이 값을 통해 MMU는 딱 512GB 크기를 지원하는 페이지 변환 테이블을 만들수 있고 탐색합니다.

2. 나머지 조립 플래그들의 하드웨어적 역할
  • TCR_CACHE_FLAGS
    • 역할: TCR_IRGN_WBWA | TCR_ORGN_WBWA 플래그가 바로 이 안에 패킹되어 있습니다.
    • 상세: 페이지 변환 테이블을 탐색할 때 캐시를 사용하여 탐색하라는 지시입니다. TLB Miss 인경우 캐시에 페이지 변환 테이블을 읽어오고 사용합니다. 이 플래그가 설정되어있지 않으면 RAM에서 읽어 옮니다.
  • TCR_SMP_FLAGS
    • 역할: 멀티코어(SMP) 환경을 위한 설정입니다.
    • 상세: SMP에서 페이지 변환 테이블의 변경 후 한 Core 에서 TLB Invalidate 를을 하면 나머지 Core 에도 TLB Invalidate 신호를 보냅니다. 이 플래그 미설정시 가 Core 별로 TLB Invalidate 명령을 줘야 합니다.
  • TCR_TG_FLAGS
    • 역할: Translation Granule, 즉 1 Page의 크기를 지정합니다.
    • 상세: 리눅스의 가장 표준적인 규격인 4KB 크기를 기본 페이지 단위로 설정하도록 지정합니다.
  • TCR_ASID16
    • 역할: ASID( Address Space Identifier)의 크기를 16비트로 설정합니다. (아니면 8비트)
    • 상세: 각 프로세스마다 고유의 ASID를 붙여서, 이 ASID 를 주소Mapping 데이터에 포함시켜서 프로세스가 바뀔 때마다 TLB를 매번 통째로 지우지 않고, 해당 프로세스로 복귀시 재사용할 수 있게 만들어 주는 설정입니다. (ASID를 사용하지 않으면 프로세스 전환시마다 가상주소가 중복될수 있으므로 전체 TLB를 지우고 시작해야합니다.)
  • TCR_TBI0 (Top Byte Ignore)
    • 역할: 64비트 가상 주소의 맨 상위 1바이트(Bits [63:56])를 주소 변환할 때 하드웨어적으로 무시하라는 플래그입니다.
    • 상세: 이 비트를 켜두면, 개발자나 커널이 64비트 포인터 변수의 맨 앞자리에 주소와 상관없는 '커스텀 태그 데이터(보안 마킹 등)'를 넣어도 CPU가 주소 에러를 내지 않고 정상 작동합니다. ttbr0, ttbr1 중 무엇을 사용할지는 55번 비트로 합니다.

232
233         /*
234          * Read the PARange bits from ID_AA64MMFR0_EL1 and set the IPS bits in
235          * TCR_EL1.
236          */
237         mrs     x9, ID_AA64MMFR0_EL1
238         bfi     x10, x9, #32, #3
239 #ifdef CONFIG_ARM64_HW_AFDBM
240         /*
241          * Hardware update of the Access and Dirty bits.
242          */
243         mrs     x9, ID_AA64MMFR1_EL1
244         and     x9, x9, #0xf
245         cbz     x9, 2f
246         cmp     x9, #2
247         b.lt    1f
248         orr     x10, x10, #TCR_HD               // hardware Dirty flag update
249 1:      orr     x10, x10, #TCR_HA               // hardware Access flag update
250 2:
251 #endif  /* CONFIG_ARM64_HW_AFDBM */
252         msr     tcr_el1, x10
253         ret                                     // return to head.S

  • 설정된 플래그들을 tcr 레지스터에 반영합니다.


254 ENDPROC(__cpu_setup)

255
256         /*
257          * We set the desired value explicitly, including those of the
258          * reserved bits. The values of bits EE & E0E were set early in
259          * el2_setup, which are left untouched below.
260          *
261          *                 n n            T
262          *       U E      WT T UD     US IHBS
263          *       CE0      XWHW CZ     ME TEEA S
264          * .... .IEE .... NEAI TE.I ..AD DEN0 ACAM
265          * 0011 0... 1101 ..0. ..0. 10.. .0.. .... < hardware reserved
266          * .... .1.. .... 01.1 11.1 ..01 0.01 1101 < software settings
267          */
268         .type   crval, #object
269 crval:
270         .word   0xfcffffff                      // clear
271         .word   0x34d5d91d                      // set

 

0x34d5d91d 비트별 설명

1. 가상 메모리와 캐시를 켜는 핵심 비트 (Bit 0 ~ 12)
  • Bit 0: M (MMU enable) ➡️ 1
    • 역할: 가상 메모리 시스템(MMU)을 전면 활성화합니다.
    • 상세: 이 비트가 1이 되는 순간부터 CPU는 모든 주소를 물리 주소가 아닌 '가상 주소'로 해석하기 시작합니다. 우리가 한 땀 한 땀 만든 페이지 테이블 주소록이 드디어 하드웨어에 의해 작동하는 순간입니다.
  • Bit 1: A (Alignment check enable) ➡️ 0
    • 역할: 엄격한 메모리 주소 정렬 검사를 끕니다. (부팅 초기에는 완화된 정렬을 사용합니다).
  • Bit 2: C (Cacheability control) ➡️ 1
    • 역할: 데이터 캐시(Data Cache)를 전면 활성화합니다.
    • 상세: 이 비트가 1이 되어야만 앞서 공부했던 MT_NORMAL 메모리 영역이나 TCR_IRGN_WBWA 같은 초고속 캐시 정책(WBWA)이 하드웨어적으로 완전히 가동됩니다. 메모리 읽고 쓰기 속도가 수십 배 빨라집니다.
  • Bit 3: SA (SP Alignment check enable) ➡️ 1
    • 역할: 스택 포인터(SP) 레지스터를 사용할 때, 주소가 16바이트 단위로 예쁘게 정렬되어 있는지 하드웨어적으로 강제 검사합니다. 정렬되지 않은 잘못된 스택 접근으로 인한 메모리 오염을 원천 차단합니다.
  • Bit 4: SA0 (SP0 Alignment check enable) ➡️ 1
    • 역할: 유저 모드(EL0)에서 사용하는 스택 포인터에 대해서도 정렬 검사를 강제합니다.
  • Bit 12: I (Instruction cacheability control) ➡️ 1
    • 역할: 명령어 캐시(Instruction Cache)를 전면 활성화합니다.
    • 상세: CPU가 앞으로 실행할 리눅스 커널 코드(명령어)들을 메인 RAM에서 매번 가져오지 않고, 초고속 명령어 캐시에 미리 담아두고 실행하게 만들어 CPU 파이프라인의 연산 효율을 극대화합니다.

2. 하드웨어 호환성 및 예약을 위한 고정 비트 (Bit 11, 20, 22, 23, 28, 29)
ARM64 아키텍처 규경상, 하드웨어의 안정적인 동작을 위해 이유를 불문하고 무조건 1로 채워두어야 하는 고정(Reserved) 비트들입니다. 이 비트들이 1로 셋팅되지 않으면 CPU가 오작동하거나 예외를 일으킬 수 있습니다.
  • Bit 11: EOS (Exception Exit Context Synchronizing) ➡️ 1 (ARMv8.0에서는 RES1 고정)
  • Bit 20: TSCXT ➡️ 1 (RES1 고정)
  • Bit 22: EIS (Exception Entry Context Synchronizing) ➡️ 1 (RES1 고정)
  • Bit 23: SPAN ➡️ 1 (RES1 고정 - Privileged Access Never 상태 제어)
  • Bit 28: NTWE ➡️ 1 (RES1 고정)
  • Bit 29: NTWI ➡️ 1 (RES1 고정)

3. 시스템 안정성과 예외 처리를 위한 비트 (Bit 14 ~ 26)
  • Bit 14: V (Vector table location) ➡️ 1
    • 역할: 예외 벡터 테이블의 하위 호환성 위치를 지정하는 비트이며, AArch64에서는 기본 규칙(RES1)을 따르도록 1로 셋팅합니다.
  • Bit 16: nTWE (Not Trap WFE) ➡️ 1
    • 역할: 유저 모드(EL0) 프로그램이 WFE (Wait For Event, 잠시 대기) 명령을 내렸을 때, 커널(EL1)이 이를 가로채서 방해하지 못하도록 락을 해제(Trap 방지)합니다. 유저 프로그램이 하드웨어 절전 명령을 자연스럽게 쓸 수 있게 합니다.
  • Bit 18: nTWI (Not Trap WFI) ➡️ 1
    • 역할: 위 설정과 마찬가지로 유저 모드에서 WFI (Wait For Interrupt, 인터럽트 대기) 명령을 썼을 때 커널이 가로채지 못하게(Trap 방지) 막아줍니다.
  • Bit 24: E0E (EL0 Endianness) ➡️ 1
    • 역할: 이 비트는 커널의 엔디안 설정과 결합하여 유저 공간(EL0)의 기본 엔디안 환경을 조율하는 데 사용됩니다.
  • Bit 25: EE (Exception Endianness) ➡️ 1
    • 역할: 인터럽트나 예외(Exception)가 발생하여 커널 코드로 진입하는 순간, 하드웨어가 데이터를 읽는 엔디안 규칙을 정합니다. (리눅스는 기본적으로 리틀 엔디안을 확실히 보장하기 위해 정밀하게 컨트롤합니다).

📌 비트 맵 전체 요약표 (0x34d5d91d)
비트 위치필드 이름값하드웨어 설정 의미
Bit 0 M 1 MMU 가상 메모리 활성화 (핵심)
Bit 1 A 0 주소 정렬 검사 비활성화
Bit 2 C 1 데이터 캐시(D-Cache) 활성화 (핵심)
Bit 3 SA 1 커널 스택 포인터 정렬 검사 활성화
Bit 4 SA0 1 유저 스택 포인터 정렬 검사 활성화
Bit 11 EOS 1 아키텍처 필수 고정 비트 (RES1)
Bit 12 I 1 명령어 캐시(I-Cache) 활성화 (핵심)
Bit 14 V 1 벡터 테이블 RES1 고정 비트
Bit 16 nTWE 1 유저 모드 WFE 명령어 가로채기 방지
Bit 18 nTWI 1 유저 모드 WFI 명령어 가로채기 방지
Bit 20, 22, 23 RES1 1 아키텍처 필수 고정 비트 세트
Bit 24, 25 E0E, EE 1 엔디안 제어 및 예외 처리 비트 조율
Bit 28, 29 nTWE/I 1 아키텍처 필수 고정 비트 세트

 


 

 

759 /*
760  * Enable the MMU.
761  *
762  *  x0  = SCTLR_EL1 value for turning on the MMU.
763  *  x27 = *virtual* address to jump to upon completion
764  *
765  * Other registers depend on the function called upon completion.
766  *
767  * Checks if the selected granule size is supported by the CPU.
768  * If it isn't, park the CPU
769  */
770         .section        ".idmap.text", "ax"
771 __enable_mmu:
772         mrs     x22, sctlr_el1                  // preserve old SCTLR_EL1 value
773         mrs     x1, ID_AA64MMFR0_EL1
774         ubfx    x2, x1, #ID_AA64MMFR0_TGRAN_SHIFT, 4
775         cmp     x2, #ID_AA64MMFR0_TGRAN_SUPPORTED
776         b.ne    __no_granule_support
777         update_early_cpu_boot_status 0, x1, x2
778         msr     ttbr0_el1, x25                  // load TTBR0
779         msr     ttbr1_el1, x26                  // load TTBR1
780         isb
781         msr     sctlr_el1, x0
782         isb

  • ttbr0_el1 = idmap_pg_dir
  • ttrb1_el1 = swapper_pg_dir
  • mmu on

 


783         /*
784          * Invalidate the local I-cache so that any instructions fetched
785          * speculatively from the PoC are discarded, since they may have
786          * been dynamically patched at the PoU.
787          */
788         ic      iallu
789         dsb     nsh
790         isb

791 #ifdef CONFIG_RANDOMIZE_BASE
792         mov     x19, x0                         // preserve new SCTLR_EL1 value
793         blr     x27
794
795         /*
796          * If we return here, we have a KASLR displacement in x23 which we need
797          * to take into account by discarding the current kernel mapping and
798          * creating a new one.
799          */
800         msr     sctlr_el1, x22                  // disable the MMU
801         isb
802         bl      __create_page_tables            // recreate kernel mapping
803
804         msr     sctlr_el1, x19                  // re-enable the MMU
805         isb
806         ic      iallu                           // flush instructions fetched
807         dsb     nsh                             // via old mapping
808         isb
809         add     x27, x27, x23                   // relocated __mmap_switched
810 #endif
811         br      x27
812 ENDPROC(__enable_mmu)

813
814 __no_granule_support:
815         /* Indicate that this CPU can't boot and is stuck in the kernel */
816         update_early_cpu_boot_status CPU_STUCK_IN_KERNEL, x1, x2
817 1:
818         wfe
819         wfi
820         b 1b
821 ENDPROC(__no_granule_support)

 


422 /*
423  * The following fragment of code is executed with the MMU enabled.
424  */
425         .set    initial_sp, init_thread_union + THREAD_START_SP
426 __mmap_switched:
427         mov     x28, lr                         // preserve LR
428         adr_l   x8, vectors                     // load VBAR_EL1 with virtual
429         msr     vbar_el1, x8                    // vector table address
430         isb

  • 인터럽트 벡터 테이블 설정


431
432         // Clear BSS
433         adr_l   x0, __bss_start
434         mov     x1, xzr
435         adr_l   x2, __bss_stop
436         sub     x2, x2, x0
437         bl      __pi_memset
438         dsb     ishst                           // Make zero page visible to PTW

  • BSS영역 초기화


439
440 #ifdef CONFIG_RELOCATABLE
441
442         /*
443          * Iterate over each entry in the relocation table, and apply the
444          * relocations in place.
445          */
446         adr_l   x8, __dynsym_start              // start of symbol table
447         adr_l   x9, __reloc_start               // start of reloc table
448         adr_l   x10, __reloc_end                // end of reloc table
449

450 0:      cmp     x9, x10
451         b.hs    2f
452         ldp     x11, x12, [x9], #24
453         ldr     x13, [x9, #-8]
454         cmp     w12, #R_AARCH64_RELATIVE
455         b.ne    1f
456         add     x13, x13, x23                   // relocate
457         str     x13, [x11, x23]
458         b       0b
459
460 1:      cmp     w12, #R_AARCH64_ABS64
461         b.ne    0b
462         add     x12, x12, x12, lsl #1           // symtab offset: 24x top word
463         add     x12, x8, x12, lsr #(32 - 3)     // ... shifted into bottom word
464         ldrsh   w14, [x12, #6]                  // Elf64_Sym::st_shndx
465         ldr     x15, [x12, #8]                  // Elf64_Sym::st_value
466         cmp     w14, #-0xf                      // SHN_ABS (0xfff1) ?
467         add     x14, x15, x23                   // relocate
468         csel    x15, x14, x15, ne
469         add     x15, x13, x15
470         str     x15, [x11, x23]
471         b       0b
472
473 2:      adr_l   x8, kimage_vaddr                // make relocated kimage_vaddr
474         dc      cvac, x8                        // value visible to secondaries
475         dsb     sy                              // with MMU off
476 #endif

  • 재배치가 필요한 항목들 주소계산해서 적용


477
478         adr_l   sp, initial_sp, x4
479         mov     x4, sp
480         and     x4, x4, #~(THREAD_SIZE - 1)
481         msr     sp_el0, x4                      // Save thread_info

  • 커널에서 사용할 스택을 initial_sp로 설정
  • sp_el0 에 thread_info 위치 기록

 initial_sp = init_thread_union + THREAD_START_SP (THREAD_SIZE - 16)

THREAD_SIZE 에서 16을 뺀것은 스택포인터는 16바이트 정렬되어있어야 되서입니다. 또한 init_thread_unionTHREAD_SIZE 가 THREAD_SIZE 로 정렬되어 있어서 480 라인의 THREAD_SIZE 정렬 수식이 정상적으로 동작하지 않습니다. 그래서 16을 뺀것입니다. 그럼 1,2,3... 등 다른 값을 차감할수있는데 16을 뺏을가요.. 그건 앞에서 말한대로 스택포인터는 16바이트로 정렬되어 있어야 하기 때문입니다.


include/linux/sched.h

2438 union thread_union {
2439     struct thread_info thread_info;
2440     unsigned long stack[THREAD_SIZE/sizeof(long)];
2441 };

 

arch/arm64/include/asm/thread_info.h

 47 struct thread_info {
 48     unsigned long       flags;      /* low level flags */
 49     mm_segment_t        addr_limit; /* address limit */
 50     struct task_struct  *task;      /* main task structure */
 51     int         preempt_count;  /* 0 => preemptable, <0 => bug */
 52     int         cpu;        /* cpu */
 53 };

 

| 0xFFFF FFFF FFFF FFFF
|
| initial_sp = sp
|
|
|  + THREAD_SIZE - 16
|
|
| init_thread_union(.stack), sp_el0
|
|0x0000 0000 0000 0000


482         str_l   x21, __fdt_pointer, x5          // Save FDT pointer

  • preserve_boot_args 에서 부터 x21 보관해온 FDT 포인터를 __fdt_pointer에 저장합니다.


483
484         ldr_l   x4, kimage_vaddr                // Save the offset between
485         sub     x4, x4, x24                     // the kernel virtual and
486         str_l   x4, kimage_voffset, x5          // physical mappings

  • kimage_voffset = kimage_vaddr - x24
  • kimage_vaddr : 커널이미지의 시작 가상주소
  • x24 : 커널이미지의 물리 시작주소
  • kimage_voffset 는 커널이미지의 물리 주소와 가상주소의 차이값을 갖는다.

487

488         mov     x29, #0

  • fp 레지스터 0으로 초기화 하여 백트레이스시 종결자(Terminator)임을 나타냅니다.


489 #ifdef CONFIG_KASAN
490         bl      kasan_early_init
491 #endif
492 #ifdef CONFIG_RANDOMIZE_BASE
493         cbnz    x23, 0f                         // already running randomized?
494         mov     x0, x21                         // pass FDT address in x0
495         bl      kaslr_early_init                // parse FDT for KASLR options
496         cbz     x0, 0f                          // KASLR disabled? just proceed
497         mov     x23, x0                         // record KASLR offset
498         ret     x28                             // we must enable KASLR, return
499                                                 // to __enable_mmu()
500 0:
501 #endif
502         b       start_kernel

  • start_kernel 로 분기


503 ENDPROC(__mmap_switched)

504
505 /*
506  * end early head section, begin head code that is also used for
507  * hotplug and needs to have the same protections as the text region
508  */
509         .section ".text","ax"
510
511 ENTRY(kimage_vaddr)
512         .quad           _text - TEXT_OFFSET
513

  • 커널이미지 시작 가상주소에서 커널이미지 앞의 여유공간(부팅 프로토콜에 지정된)까지 포함하는 주소를 kimage_vaddr 로 설정

 

 

'linux' 카테고리의 다른 글

2분동안 배운 Arm 어셈블리어로 Head.S 분석  (0) 2026.06.21
6.12/debug_objects_early_init()  (0) 2026.06.18
6.12/smp_setup_processor_id();  (0) 2026.06.17
6.12/set_task_stack_end_magic()  (0) 2026.06.17
6.18/head.S  (0) 2025.12.07

2분 투자로 배우는 Arm 어셈블리 

0. 배우지 않았지만 이미 알고 있는 명령어

ADD(더하기)          예) ADD X0, X1, X2  → X0 = X1 + X2
SUB(빼기)           예) SUB X0, X1, #5  → X0 = X1 - 5
AND(bit 연산 AND)   예) AND X0, X1, X2  → X0 = X1 & X2
ORR(bit 연산 OR)    예) ORR X0, X1, #1  → X0 = X1 | 1



1. 1분이면 입문 (명령어의 첫 알파벳으로 유추 가능)

B (Branch, 분기) : 어디로 가라   
                 예) B    label ; label로 가라 
                     BL    label ; label로 갔다가 RET 명령으로 돌아와라
                      (LR 레지스터에 복귀 주소보관)
                     BR    x1    ; x1 레지스터가 가리키는 주소로 가라
                     B.EQ (Branch if Equal) ; 결과가 같을 때 (Z == 1) 점프
                     B.NE (Branch if Not Equal); 결과가 다를 때 (Z == 0) 점프
                     

C (Compare, 비교) : 두 레지스터의 값을 뺀(Sub) 결과를 PSTATE 레지스터에 반영
                  (두 레지스터 값 변경 없음)
                   예) CMP R0, R1 ; R0 - R1 결과(0 인지, +, -, Carry 발생여부 등)

                        CBZ  R1, L3   ; R1의 값이 0 이면 L3로 

                        CBNZ R1, L3   ; R1의 값이 0 이 아니면 L3로



L (Load, 읽어오기) : 메모리의 값을 읽어서 레지스터에 보관
                   예) LDR R0, [R1] : R1의 값을 주소로 참조하여 R0에 저장
                       R0 = *R1
                   
M (Move, 이동X, 복사O) : 
                   예) MOV  x21, x0  ;   x21 = x0
                      MOV  x19, xzr ;   x19 = 0
                            xzr(zero 레지스터 : 항상 0값을 갖음)
                      MSR  sctlr_el1, x19 ;   sctlr_el1 = x19 
                            sctlr_el1(특수목적 레지스터)
                      MRS  x19, sctlr_el1 ;   x19 = sctlr_el1

S (Store, 저장하기) : 레지스터 값을 메모리에 저장                          
                   예) STR r0, [r1] : r0의 값을 r1의 값을 주소로 참조하여 저장
                         *r1 = r0
                      STR r0, [r1], #4 : r0의 값을 r1의 값을 주소로 참조하여 저장하고 r1에 4를 더함
                         *r1 = r0, r1 = r1 + 4 

T (Test, 비교) : 두 레지스터의 값을 AND연산을 해서 결과를 flag 레지스터에 반영
                (두 레지스터 값 변경 없음)
                    예)  TST   X19, #1                  ; x19 & 1 결과를 flag 레지스터에 반영 (x19 값 변동 없음)
                          TBZ   X0, #63, Lpositive ; X0의 63번째 비트가 0(양수)이면 Lpositive 라벨로 점프

                          TBNZ X0, #63, Lnegative ; X0의 63번째 비트가 1(음수)이면 Lnegative 라벨로 점프


2. 2분이면 초급

상태 플래그(PSTATE)
N : 연산 결과가 음의 값을 가질 경우    → Set 1
Z : 연산 결과가 Zero인 경우         → Set 1
C : 연산 결과가 Carry를 발생시킨 경우 → Set 1
V : 연산 결과 Overflow가 발생한 경우 → Set 1


위에서 언급한 ADD, SUB, CMP, TST 등의 명령이 상태 플래그 변경


예)
TST   X0, #4    
B.EQ  bit_is_zero  ; 연산 결과가 0이면(Z==1)     bit_is_zero    라벨로 점프
B.NE  bit_is_notzero   ; 연산 결과가 0이 아니면(Z==0) bit_is_notzero 라벨로 점프


이글은 어셈블리를 처음 접하는 사람들의 거부감을 덜기 위함이며 수많은 예외의 경우가 있을 수 있습니다.
오류가 있으면 댓글 부탁드립니다.
도움주신 구글 검색 서비스에 감사드립니다.

'linux' 카테고리의 다른 글

4.6/head.S  (0) 2026.08.09
6.12/debug_objects_early_init()  (0) 2026.06.18
6.12/smp_setup_processor_id();  (0) 2026.06.17
6.12/set_task_stack_end_magic()  (0) 2026.06.17
6.18/head.S  (0) 2025.12.07

커널의 핵심 디버깅 기능인 Debug Objects(오브젝트 추적 서브시스템)의 초기화 코드입니다.

 

리눅스 커널은 수많은 타이머(Timer), 워크큐(Workqueue), 뮤텍스(Mutex) 같은 내부 오브젝트들을 사용합니다. 만약 개발자가 실수로 이미 해제된 타이머를 다시 해제하거나, 초기화되지 않은 오브젝트를 사용하면 커널 전체가 크래시될 수 있습니다.

이러한 치명적인 버그를 잡아내기 위해 오브젝트들의 생성, 사용, 소멸 과정을 감시하는 'Debug Objects'라는 것을 둡니다.

커널이 부팅되는 Early Boot에 일할 수 있도록 최소한의 메모리 공간과 잠금 장치을 처리하는 역할을 합니다. 부팅 극초기에는 메모리를 동적으로 할당해 주는 kmalloc() 같은 기능이 아직 켜지지 않았기 때문에, 컴파일 시점에 미리 만들어둔 정적 메모리(Static Pool)를 준비를 합니다.

 

1286 /*
1287  * Called during early boot to initialize the hash buckets and link
1288  * the static object pool objects into the poll list. After this call
1289  * the object tracker is fully operational.
1290  */
1291 void __init debug_objects_early_init(void)
1292 {
1293         int i;
1294
1295         for (i = 0; i < ODEBUG_HASH_SIZE; i++)
1296                 raw_spin_lock_init(&obj_hash[i].lock);

 

  • 추적 중인 커널 오브젝트들은 추후 빠른 검색을 위해 obj_hash라는 전역 해시 테이블에서 관리됩니다.
  • 동작: 이 루프는 해시 테이블의 모든 버킷(방)을 돌며 동기화를 위한 스핀락을 초기화합니다.
  • 왜 raw_spin_lock인가?: 리눅스 커널의 일반 스핀락은 실시간(RT) 커널 설정에서 상황에 따라 선점될 수 있습니다. 반면 raw_spin_lock은 무슨 일이 있어도 절대 멈추지 않는 최하위 레벨의 잠금 장치입니다. 인터럽트 처럼 1분 1초가 급한 순간에도 오동작 없이 오브젝트를 추적해야 하므로 이 잠금 장치를 사용합니다.

 


1297
1298         for (i = 0; i < ODEBUG_POOL_SIZE; i++)
1299                 hlist_add_head(&obj_static_pool[i].node, &obj_pool);

 

  • 오브젝트 하나를 감시할 때마다 이를 기록할 struct debug_obj가 필요합니다. 동적 메모리 할당이 안 되니, 컴파일할 때 미리  준비해 둔 배열(obj_static_pool)을 사용합니다.
  • 동작: 배열 형태로 쌓여있던 obj_static_pool[]을 하나씩 꺼내어, 전역 obj_pool에 연결 리스트 형태로 매달아 둡니다.
  • hlist_add_head: 리스트의 맨 앞에 새로운 노드를 삽입하는 리눅스 커널 내부의 이중 연결 리스트 매크로입니다.

1300 }

'linux' 카테고리의 다른 글

4.6/head.S  (0) 2026.08.09
2분동안 배운 Arm 어셈블리어로 Head.S 분석  (0) 2026.06.21
6.12/smp_setup_processor_id();  (0) 2026.06.17
6.12/set_task_stack_end_magic()  (0) 2026.06.17
6.18/head.S  (0) 2025.12.07

 920         smp_setup_processor_id();

 

 89 void __init smp_setup_processor_id(void)

  • __init 매크로는 이 함수가 커널 부팅 시에만 사용되고 이후에는 메모리에서 해제될 코드임을 나타냅니다.


 90 {
 91         u64 mpidr = read_cpuid_mpidr() & MPIDR_HWID_BITMASK;

  • 시스템 레지스터 읽기 함수를 통해  MPIDR(Multiprocessor Affinity Register) 값을 가져옵니다. 이 레지스터는 멀티코어 시스템에서 각 코어가 가진 고유한 물리적 하드웨어 ID 정보를 담고 있습니다.


 92         set_cpu_logical_map(0, mpidr);

 

  • 하드웨어가 인식하는 물리적 CPU ID(mpidr)를 Linux 커널이 관리하는 논리적(Logical) CPU ID 0번에 매핑합니다.
  • 현재 이 코드를 실행 중인 코어가 시스템을 깨운 첫 번째 코어(Boot CPU / 사전에 결정된 주 프로세서)이므로, 커널 내부적으로 항상 0번 인덱스를 부여하는 것입니다

 


 93
 94         pr_info("Booting Linux on physical CPU 0x%010lx [0x%08x]\n",
 95                 (unsigned long)mpidr, read_cpuid_id());

  • 커널 로그(dmesg)에 부팅 메시지를 출력합니다.


 96 }

 

현재 시스템을 부팅하고 있는 주 프로세서(부트 CPU, 보통 CPU 0)의 물리적 ID를 확인하고 이를 커널의 논리적 CPU ID와 매핑하는 아주 초기 단계의 설정 함수입니다.

 

 

63      40 39    32 31 30 29    25 24 23    16 15     8 7      0
+---------+--------+--+--+--------+--+--------+--------+--------+

|   RES0  |  Aff3  |R1|U |  RES0  |MT|  Aff2  |  Aff1  |  Aff0  |
+---------+--------+--+--+--------+--+--------+--------+--------+

 

  • Aff0 ~ Aff3 (Affinity Levels): 시스템의 계층적 구조를 나타내며, 칩 설계사(SoC)가 정의합니다. 일반적인 다중 클러스터 구조에서의 표준 의미는 다음과 같습니다.
    • Aff3 [39:32]: 다중 다이(Multi-die) 또는 복수 소켓 시스템 구분
    • Aff2 [23:16]: 클러스터 그룹 (Cluster Group) 구분
    • Aff1 [15:8]: 단일 클러스터 내의 코어 ID (Core ID) 구분
    • Aff0 [7:0]: 코어 내의 하드웨어 스레드 ID (Thread/SMT ID) 구분 
  • MT (Multi-threading, 비트 24): 해당 코어가 하인퍼스레딩(SMT) 같은 멀티스레딩을 지원하는지 여부를 나타냅니다 (1이면 Aff0가 스레드를 의미함). 
  • U (Uniprocessor, 비트 30): 시스템이 단일 코어(Single-core) 시스템인지 여부를 나타냅니다 (1이면 단일 코어). 
  • RES1 (비트 31): 항상 1로 고정된 예약 비트입니다.

 

 

'linux' 카테고리의 다른 글

2분동안 배운 Arm 어셈블리어로 Head.S 분석  (0) 2026.06.21
6.12/debug_objects_early_init()  (0) 2026.06.18
6.12/set_task_stack_end_magic()  (0) 2026.06.17
6.18/head.S  (0) 2025.12.07
6.12/head.S  (0) 2025.06.14

919         set_task_stack_end_magic(&init_task);

 

1111 void set_task_stack_end_magic(struct task_struct *tsk)
1112 {
1113         unsigned long *stackend;
1114
1115         stackend = end_of_stack(tsk);
1116         *stackend = STACK_END_MAGIC;    /* for overflow detection */
1117 }

 

 28 static __always_inline unsigned long *end_of_stack(const struct task_struct *task)
 29 {
 30 #ifdef CONFIG_STACK_GROWSUP
 31     return (unsigned long *)((unsigned long)task->stack + THREAD_SIZE) - 1;
 32 #else
 33     return task->stack;
 34 #endif
 35 }

 

스택의 맨 밑바닥(end_of_stack)에 STACK_END_MAGIC (0x57ac6e9d)라는 데이터를 미리 적어둡니다.

스택은 메모리의 높은 주소에서 낮은 주소 방향(아래 방향)으로 자란다는 것입니다. 따라서 스택의 "끝(최하단)"은 주소상으로 가장 낮은 위치가 됩니다.

컨텍스트 스위칭이 일어나거나 중요한 순간마다 이 맨 밑바닥 주소를 검사합니다. 만약 이 값이 엉뚱한 값으로 바뀌어 있다면? "스택이 넘쳤구나!" 하고 감지합니다. 매직 넘버가 바뀐 것을 감지하면 커널은 시스템을 안전하게 보호하기 위해 즉시 커널 패닉(Kernel Panic)을 발생시키고 BUG: handle_stack_overflow 같은 에러 메시지를 뿌리며 시스템을 멈춥니다.

 

'linux' 카테고리의 다른 글

6.12/debug_objects_early_init()  (0) 2026.06.18
6.12/smp_setup_processor_id();  (0) 2026.06.17
6.18/head.S  (0) 2025.12.07
6.12/head.S  (0) 2025.06.14
6.1/early_ioremap_init(void)  (0) 2025.03.29

 41 #if (PAGE_OFFSET & 0x1fffff) != 0
 42 #error PAGE_OFFSET must be at least 2MB aligned
 43 #endif

 

https://www.kernel.org/doc/Documentation/arm64/booting.rst

  • PAGE_OFFSET 이 2MB 정렬이 아니면 오류
  • PMD (Section) 단위로 매핑하기 위해 2MB정렬
 

PAGE_OFFSET 의 의미 변경 내역

더보기
 
PAGE_OFFSET는 리눅스 커널에서 물리 메모리(RAM) 전체가 커널 가상 주소 공간에 1:1로 직접 매핑되는 '선형 매핑(Linear Mapping/Direct Mapping)' 영역의 시작 주소를 의미합니다.
이 매핑 덕분에 커널은 __va(phys)나 __pa(virt) 같은 단순 사칙연산 매크로만으로 물리 주소와 가상 주소를 빠르게 변환해 왔습니다. 
역사적으로 리눅스 커널의 아키텍처(특히 AArch64 / ARM64)가 발전하면서 PAGE_OFFSET의 위치와 의미에 큰 변화가 있었습니다. 주요 변경 이력을 정리하면 다음과 같습니다. 

 


 

1. 32비트 시절 (x86, ARM32): 고정된 유저/커널 공간 분할
  • 특징: 전체 4GB 가상 주소 공간을 유저 공간과 커널 공간으로 쪼개어 사용했습니다 (예: VMSPLIT_3G 기준 3:1 비율).
  • 역할: PAGE_OFFSET은 커널 가상 주소의 절대적인 시작점이자 선형 매핑의 시작 주소인 0xC0000000으로 고정되었습니다. 커널 이미지 자체도 이 주소 근처에 로드되었습니다. 
 
2. AArch64 (ARM64) 도입 초기 (커널 3.x ~ 4.x) 
64비트 아키텍처로 오면서 가상 주소 공간이 거대해져 유저 공간(TTBR0)과 커널 공간(TTBR1)이 완전히 분리되었습니다.
  • 역할: PAGE_OFFSET은 커널 공간(TTBR1)의 가장 첫 번째 주소(가장 밑바닥)로 정의되었습니다.
  • 공식: 가상 주소 비트 수(VA_BITS)에 따라 결정되었습니다.
    • 예: VA_BITS = 48일 때, PAGE_OFFSET = 0xFFFF_0000_0000_0000 
  • 구조: 커널 이미지(vmlinux)가 이 PAGE_OFFSET 바로 위에 위치하는 형태를 가졌습니다. 
 
3. 커널 이미지의 분리 및 KASLR 도입 (커널 4.6 ~)
보안 강화를 위해 커널 주소 공간을 무작위화하는 KASLR(Kernel Address Space Layout Randomization)이 도입되면서 큰 구조 변화가 생겼습니다.
  • 변경점: 커널 이미지가 위치하는 가상 주소(KIMAGE_VADDR)와 물리 메모리가 선형 매핑되는 주소(PAGE_OFFSET)가 완전히 분리되었습니다.
  • 의미 변경: 이전까지 PAGE_OFFSET은 "커널 가상 주소 공간의 시작이자 커널 이미지의 시작"이었으나, 이때부터 "커널 이미지는 다른 곳에 독립적으로 배치되고, PAGE_OFFSET은 오직 순수한 RAM 선형 매핑 영역의 시작점"으로 의미가 좁혀졌습니다. 
 
4. 선형 매핑 영역의 하단 이동 (커널 5.4)
ARM64 아키텍처의 메모리 맵 재배치(Memory Layout Rework)가 이루어진 중요한 시점입니다.
  • 변경점: 기존에는 PAGE_OFFSET(선형 매핑)이 커널 공간의 밑바닥에 있고 그 위에 vmalloc, modules 등이 있었습니다. 하지만 커널 5.4부터 선형 매핑 영역이 커널 주소 공간의 가장 윗부분(끝자락)으로 이동했습니다.
  • 이유: 선형 매핑 영역은 가용 RAM 용량에 따라 크기가 동적으로 변합니다. 이를 주소 공간 최상단으로 옮김으로써, 시스템에 장착된 RAM 크기에 맞춰 PAGE_OFFSET 시작 주소를 유연하게 조절(동적 계산)할 수 있게 하기 위함이었습니다. 
 
5. 가상 주소 크기(52비트 LVA) 지원 및 고정화 (커널 5.11 ~ 현재 6.12.y)
ARMv8.2-LVA 확장으로 하드웨어가 최대 52비트 가상 주소(VA)를 지원하게 되면서 또 한 번의 전환기를 맞이합니다. 
  • 문제점: 하나의 리눅스 커널 바이너리가 48비트 하드웨어와 52비트 하드웨어를 모두 지원해야 했습니다. 만약 하드웨어 비트 수에 따라 PAGE_OFFSET 값이 동적으로 바뀌면, 커널이 주소 변환 매크로(virt_to_phys)를 호출할 때마다 매번 변수를 읽어야 하므로 성능 오버헤드가 발생합니다. 
  • 최종 변경: 성능 최적화를 위해 PAGE_OFFSET 값을 상수가 처리할 수 있는 최대 범위인 52비트 기준의 고정 값(0xFFF0000000000000)으로 박아두었습니다. 48비트 장치든 52비트 장치든 상관없이 컴파일 타임 상수로 고정되어 컴파일러가 주소 변환 코드를 극도로 최적화할 수 있게 되었습니다. 

 

 


 44
 45 /*
 46  * Kernel startup entry point.
 47  * ---------------------------
 48  *
 49  * The requirements are:
 50  *   MMU = off, D-cache = off, I-cache = on or off,
 51  *   x0 = physical address to the FDT blob.
 52  *

MMU = off, D-cache = off, I-cache = on or off

x0 = FDT 의 물리주소

 

시작시 MMU가 off 되어야 하는 이유?

MMU는 CPU가 명령어를 수행할 때 메모리에 접근할 vaddr(Virtual Address)를 paddr(Physical Address)로 변환하는 작업을 수행합니다. 그리고 이때 사용되는 자료구조가 주소 매핑 테이블인데 이는 커널이 생성합니다. Startup 코드가 수행되는 시점에서는 매핑 테이블이 존재하지 않는데 MMU가 on 되어 있다면 메모리 주소가 vaddr로 해석됩니다. 결국 MMU가 수행되고 이는 paddr로 변환되어 의도치 않은 동작이 수행됩니다. 이와 같은 이유로 매핑 테이블이 생성되기 이전에는 MMU를 off 해야합니다.

 

D-Cashe 가 off 되어야 하는 이유?

D-Cache는 CPU와 메인 메모리 사이에서 데이터를 빠르게 접근하기 위한 저장소 역할을 수행합니다. 대부분의 시스템 프로그래머들은 알다시피 종종 캐시와 메모리간 데이터 싱크(Sync) 문제들을 마주하게 되는데 startup 시점에서는 paddr에서 데이터를 직접 읽거나 써야 합니다. 그러나 D-Cache가 on 되어 있다면 paddr에서 데이터를 읽는 대신 캐시 메모리에서 읽으므로 의도치 않은 동작을 수행하게 됩니다. 이와 같은 이유로 D-Cache도 off 해야합니다.

 

https://www.ooseel.net/2021/07/why-mmu-and-d-cache-must-be-off-at-startup-point-in-arm64/


 53  * Note that the callee-saved registers are used for storing variables
 54  * that are useful before the MMU is enabled. The allocations are described
 55  * in the entry routines.
 56  */
 57         __HEAD

95 #define __HEAD .section ".head.text","ax"

 

  • .head.text 섹션 a(allocation) 및 x(execution) 속성으로 시작
  • a (SHF_ALLOC) : 실행시 메모리에 차지한다. (.comment 등 "a" 속성이 없으면 실행시 메모리에 올라 오지 않음)
  • x (SHF_EXECINSTR) : 실행가능한 기계어

https://developer.arm.com/documentation/dui0774/i/armclang-Integrated-Assembler-Directives/Section-directives

https://man7.org/linux/man-pages/man5/elf.5.html

https://sourceware.org/binutils/docs/as/Section.html

https://refspecs.linuxfoundation.org/elf/elf.pdf


 58         /*
 59          * DO NOT MODIFY. Image header expected by Linux boot-loaders.
 60          */
 61         efi_signature_nop                       // special NOP to identity as PE/COFF executable

 

  • uefi인경우 "MZ" 서명, 아니면 nop

 

 62         b       primary_entry                   // branch to kernel start, magic

 

  • primary_entry 로 분기

 

 63         .quad   0                               // Image load offset from start of RAM, little-endian
 64         le64sym _kernel_size_le                 // Effective size of kernel image, little-endian
 65         le64sym _kernel_flags_le                // Informative flags, little-endian
 66         .quad   0                               // reserved
 67         .quad   0                               // reserved
 68         .quad   0                               // reserved
 69         .ascii  ARM64_IMAGE_MAGIC               // Magic number
 70         .long   .Lpe_header_offset              // Offset to the PE header.
 71
 72         __EFI_PE_HEADER

 

  • uefi 인경우 uefi 헤더위치


 73
 74         .section ".idmap.text","a"

 

  • .idmap.text 섹션 a(allocation) 속성으로 시작
  • 실행코드인데 "x" flag가 빠져도 되는가?
    1. 아직 MMU가 동작하지 않기 때문에 영향이 없습니다.
    2. create_init_idmap의  map_range( ..., _stext, (u64)__initdata_begin,  PAGE_KERNEL_ROX, ...); 에서 실행권한을 부여합니다.
    3. idmap.text 섹션은 극초반에 사용되는 물리주소와 가상주소가 같은 특수한 섹션입니다. 이를 다른 커널 영역들과 구별하여 관리하기 위함으로 보입니다.


 75

 76         /*
 77          * The following callee saved general purpose registers are used on the
 78          * primary lowlevel boot path:
 79          *
 80          *  Register   Scope                      Purpose
 81          *  x19        primary_entry() .. start_kernel()        whether we entered with the MMU on
 82          *  x20        primary_entry() .. __primary_switch()    CPU boot mode
 83          *  x21        primary_entry() .. start_kernel()        FDT pointer passed at boot in x0
 84          */
 85 SYM_CODE_START(primary_entry)
 86         bl      record_mmu_state

 


133         __INIT

 

 93 #define __INIT      .section    ".init.text","ax"

 

  • .init.text 섹션 a(allocation) 및 x(execution) 속성으로 시작


134 SYM_CODE_START_LOCAL(record_mmu_state)
135         mrs     x19, CurrentEL
136         cmp     x19, #CurrentEL_EL2
137         mrs     x19, sctlr_el1
138         b.ne    0f
139         mrs     x19, sctlr_el2

 

  • CurrentEL 레지스터를 읽어서 현재 실행 모드가 EL2이면 x19에 sctlr_el2 레지스터 를 EL1이면 sctlr_el1 레지스터을 읽어오기

https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/CurrentEL--Current-Exception-Level

https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/SCTLR-EL1--System-Control-Register--EL1-

http://jake.dothome.co.kr/registers64/

 

140 0:


141 CPU_LE( tbnz    x19, #SCTLR_ELx_EE_SHIFT, 1f    )
142 CPU_BE( tbz     x19, #SCTLR_ELx_EE_SHIFT, 1f    )

 

  • CPU_LE : 컴파일시 Little-endian 설정이면 해당라인 컴파일 하고 아니면 무시
  • CPU_BE : 컴파일시 Big-endian 설정이면 해당라인 컴파일 하고 아니면 무시
  • little-endian 으로 컴파일 했는데 실행시 big-endian (EE = 1 즉 NZ) 이면 1f로 (forward에 있는  label "1" 로 분기, 즉 154라인으로 분기)
  • big-endian 으로 컴파일 했는데 실행시 little-endian (EE = 0 즉 Z) 이면 1f로 (forward에 있는  label "1" 로 분기, 즉 154라인으로 분기)



143         tst     x19, #SCTLR_ELx_C               // Z := (C == 0)

 

  • cache 활성화 여부 :
  • 0 = disable ==> Z := 1
  • 1 = enable ==> Z := 0

 

https://developer.arm.com/documentation/111107/2025-09/AArch32-Registers/SCTLR--System-Control-Register

https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/SCTLR-EL1--System-Control-Register--EL1-?lang=en#fieldset_0-25_25


144         and     x19, x19, #SCTLR_ELx_M          // isolate M bit

 

  • MMU 활성화 여부
  • x19 = ( 0 = disable, 1 = enable )


145         csel    x19, xzr, x19, eq               // clear x19 if Z

 

  • csel Xd, Xn, Xm, <cond>
  • Xd = <cond> ? Xn : Xm
  • xzr : 항상 0값을 갖는 레지스터
  • 144라인의 and 명령은 flag 레지스터를 변경하지 않음, 그래서 143 라인의 tst 명령결과가 eq (Z == 1)인지 판단
  • cache 가 비활성화 되었으면 x19에 0, 활성화 되있으면  x19 값 변경없이(MMU 활성화 여부 값을 가지고) 리턴


146         ret


147
148         /*
149          * Set the correct endianness early so all memory accesses issued
150          * before init_kernel_el() occur in the correct byte order. Note that
151          * this means the MMU must be disabled, or the active ID map will end
152          * up getting interpreted with the wrong byte order.
153          */
154 1:      eor     x19, x19, #SCTLR_ELx_EE

 

  • 실행 환경의 endian을 컴파일시 설정된 endian으로 변경


155         bic     x19, x19, #SCTLR_ELx_M

 

  • x19 = x19 & ~ #SCTLR_ELx_M    ==> M = 0
  • MMU disable (M = 0)


156         b.ne    2f

 

  • 136라인의 결과 반영.  즉,실행 level el1 이면 2 로


157         pre_disable_mmu_workaround
158         msr     sctlr_el2, x19
159         b       3f
160 2:      pre_disable_mmu_workaround
161         msr     sctlr_el1, x19
162 3:      isb
163         mov     x19, xzr
164         ret
165 SYM_CODE_END(record_mmu_state)

 

  • 실행 환경의 endian을 컴파일시 설정된 endian으로 변경 MMU disable 하고 x19에 0을 대입 후 리턴

643 /**
644  * Errata workaround prior to disable MMU. Insert an ISB immediately prior
645  * to executing the MSR that will change SCTLR_ELn[M] from a value of 1 to 0.
646  */
647     .macro pre_disable_mmu_workaround
648 #ifdef CONFIG_QCOM_FALKOR_ERRATUM_E1041
649     isb
650 #endif
651     .endm

 

  • 이전 명령어가 다음명령이 실행되기 전에  각 장치에  반영되게함

 


 

 87         bl      preserve_boot_args

 


167 /*
168  * Preserve the arguments passed by the bootloader in x0 .. x3
169  */
170 SYM_CODE_START_LOCAL(preserve_boot_args)
171         mov     x21, x0                         // x21=FDT
172
173         adr_l   x0, boot_args                   // record the contents of

  • boot_args 의 주소를 x0에

 

<arch/arm64/kernel/setup.c>

 88 u64 __cacheline_aligned boot_args[4];


174         stp     x21, x1, [x0]                   // x0 .. x3 at kernel entry
175         stp     x2, x3, [x0, #16]

 

  • boot_args[] = {x21 = x0, x1, x2, x3};


176
177         cbnz    x19, 0f                         // skip cache invalidation if MMU is on

 

  • MMU 가 enable 이면 0f 로


178         dmb     sy                              // needed before dc ivac with
179                                                 // MMU off
180
181         add     x1, x0, #0x20                   // 4 x 8 bytes
182         b       dcache_inval_poc                // tail call

 

  • data cache 를 invalidate 함
  • data cache off 상태인데 invalidate 하는 이유
    • 프로세서가 전원이 켜지거나 리셋된 직후, 혹은 부트로더가 작동하는 동안 캐시 메모리에는 임의의 쓰레기 데이터가 남아있을 수 있습니다.
    • ARM 아키텍처는 하드웨어 제조사(삼성, 퀄컴, 애플 등)마다 캐시 컨트롤러 설계가 다릅니다. 어떤 칩은 하드웨어 리셋 시 완전히 무효화(Auto-Invalidate)를 해주지만, 어떤 임베디드 칩이나 초기 아키텍처는 이를 완벽히 보장하지 않습니다.


183 0:      str_l   x19, mmu_enabled_at_boot, x0

 

  • mmu enable 상태를 mmu_enabled_at_boot에 저장
  • enable = SCTLR_ELx_M
  • diable = 0


184         ret
185 SYM_CODE_END(preserve_boot_args)



 88
 89         adrp    x1, early_init_stack
 90         mov     sp, x1

 

  • stack pointer 를 early_init_stack으로 설정 (4KB)

<vmlinux.lds.S>

342         . += SZ_4K;             /* stack for the early C runtime */
343         early_init_stack = .;

 


 91         mov     x29, xzr
 92         adrp    x0, __pi_init_idmap_pg_dir
 93         mov     x1, xzr
 94         bl      __pi_create_init_idmap

 

  • "__pi_" 는 /arch/arm64/kernel/pi/Makefile 에서 
  • __pi_ + create_init_idmap = __pi_create_init_idmap 로 함수명을 만듬.
  • 소스 분석은 create_init_idmap로

기존 어셈블리로 작성되었던 부분을 C언어로 작성

현재 MMU off 상태라 모든 주소는 물리 주소임

가상주소와 물리주소를 동일하게 매핑하면  아래 물리주소 1002 에서 가상주소 1003 으로 자연스럽게 넘어감

예)

주소 

1000  물리 주소

1001  물리 주소

1002 MMU on     ; 다음 실행 주소는 1003, 이때 가상주소와 물리 주소를 동일하게 매핑하고 메인루틴의 가상주소할당 후 분기

1003 가상주소   

1004 가상주소

...

2000 b start_kernel ; 실행하고자 하는 메인 루틴으로 분기

 

 91 asmlinkage phys_addr_t __init create_init_idmap(pgd_t *pg_dir, ptdesc_t clrmask)

 

  • pg_dir   = x0 = __pi_init_idmap_pg_dir 
  • => idmap 의 PGD 를 __pi_init_idmap_pg_dir 로 설정
  • clrmask = x1 = 0
  • => clear 시킬 flag 없음


 92 {
 93         phys_addr_t ptep = (phys_addr_t)pg_dir + PAGE_SIZE; /* MMU is off */

  • ptep = 다음 페이지 변환 테이블을 저장할곳의 위치


 94         pgprot_t text_prot = PAGE_KERNEL_ROX; // Read Only + Execution
 95         pgprot_t data_prot = PAGE_KERNEL;
 96
 97         pgprot_val(text_prot) &= ~clrmask;
 98         pgprot_val(data_prot) &= ~clrmask;
 99
100         /* MMU is off; pointer casts to phys_addr_t are safe */
101         map_range(&ptep, (u64)_stext, (u64)__initdata_begin,
102                   (phys_addr_t)_stext, text_prot, IDMAP_ROOT_LEVEL,
103                   (pte_t *)pg_dir, false, 0);

 

  • 가상주소 (_stext , __initdata_begin] 범위로 하고  _stext 를 물리 주소로 하는 읽기 전용, 실행가능한 주소공간 맵핑
  • 즉, 물리주소와 가상주소가 같은 _stext  부터 __initdata_begin 까지의 주소 공간 맵핑
  • IDMAP_ROOT_LEVEL = 0
  • pg_dir = __pi_init_idmap_pg_dir 

 


104         map_range(&ptep, (u64)__initdata_begin, (u64)_end,
105                   (phys_addr_t)__initdata_begin, data_prot, IDMAP_ROOT_LEVEL,
106                   (pte_t *)pg_dir, false, 0);

 

  • 가상주소 ( __initdata_begin , _end ] 범위로 하고  __initdata_begin 를 물리 주소로 하는읽기 전용 실행불가(data 보관용) 주소공간 확보. 즉, 물리주소와 가상주소가 같은 __initdata_begin 부터 _end  까지의 주소 공간 맵핑


107
108         return ptep;

 

  • _end 까지 매핑한 주소변환테이블의 끝위치 반환


109 }


 14 /**
 15  * map_range - Map a contiguous range of physical pages into virtual memory
 16  *
 17  * @pte:                Address of physical pointer to array of pages to
 18  *                      allocate page tables from
 19  * @start:              Virtual address of the start of the range
 20  * @end:                Virtual address of the end of the range (exclusive)
 21  * @pa:                 Physical address of the start of the range
 22  * @prot:               Access permissions of the range
 23  * @level:              Translation level for the mapping
 24  * @tbl:                The level @level page table to create the mappings in
 25  * @may_use_cont:       Whether the use of the contiguous attribute is allowed
 26  * @va_offset:          Offset between a physical page and its current mapping
 27  *                      in the VA space
 28  */
 29 void __init map_range(phys_addr_t *pte, u64 start, u64 end, phys_addr_t pa,
 30                       pgprot_t prot, int level, pte_t *tbl, bool may_use_cont,
 31                       u64 va_offset)
 32 {
 33         u64 cmask = (level == 3) ? CONT_PTE_SIZE - 1 : U64_MAX;
 34         ptdesc_t protval = pgprot_val(prot) & ~PTE_TYPE_MASK;
 35         int lshift = (3 - level) * PTDESC_TABLE_SHIFT;
 36         u64 lmask = (PAGE_SIZE << lshift) - 1;

 

해당 level 미만의 매핑테이블까지 마스크 값 생성(각 비트 1로 세트)

level ==>         0          1           2           3

start ==>   |  PGD  |  PUD  |  PMD  |  PTE  |   PAGE_SHIFT   |

size ==>         9            9           9           9                 12                     (1page = 4KB기준)

lmask ==>                    0          1            2                  3

 

level    lmask

0 ==> 0x0000 007F FFFF FFFF   (PUD + PMD + PTE + PAGE_MASK)

1  ==> 0x0000 0000 3FFF FFFF   (PMD + PTE + PAGE_MASK)

2  ==> 0x0000 0000 001F FFFF   (PTE + PAGE_MASK)

3  ==> 0x0000 0000 0000 0FFF   (PAGE_MASK)


 37
 38         start   &= PAGE_MASK;
 39         pa      &= PAGE_MASK;

 

  • start 를 page 단위 시작 주소로 변환
  • pa 를 page 단위 시작 주소로 변환


 40
 41         /* Advance tbl to the entry that covers start */
 42         tbl += (start >> (lshift + PAGE_SHIFT)) % PTRS_PER_PTE;

 

level ==>         0          1           2           3

start ==>   |  PGD  |  PUD  |  PMD  |  PTE  |   PAGE_SHIFT   |

size ==>         9            9           9           9                 12                     (1page = 4KB기준)

lmask ==>                    0          1            2                  3

 

  • start가 현재 페이지 변환 테이블의 몇번째 항목에 위치하는지 계산해서 tbl에 반영


 43
 44         /*
 45          * Set the right block/page bits for this level unless we are
 46          * clearing the mapping
 47          */
 48         if (protval)
 49                 protval |= (level == 2) ? PMD_TYPE_SECT : PTE_TYPE_PAGE;

 

  • PMD level 이면 PTE를 사용하지 않고 PMD에 물리적으로 연속된 2MB 단위(Section)로 매핑 할수있게 설정
  • 아니면 1PAGE 매핑할수 있게 설정


 50

 51         while (start < end) {
 52                 u64 next = min((start | lmask) + 1, PAGE_ALIGN(end));

 

  • (start | lmask) + 1 ==> 해당 매핑테이블의 다음 항목 계산, end의 다음페이지 시작주소를 넘어서면 end의 다음페이지 시작주소


 53
 54                 if (level < 2 || (level == 2 && (start | next | pa) & lmask)) {

 

  • level < 2 ==> PUD, PMD, PTE를 만들기 위해(즉, 다음 level 매핑 테이블을 만들기 위해)
  • level == 2 && .. ==> PMD 이면서 2MB 단위(section)가 아닌경우 PTE를 만들기 위해


 55                         /*
 56                          * This chunk needs a finer grained mapping. Create a
 57                          * table mapping if necessary and recurse.
 58                          */
 59                         if (pte_none(*tbl)) {

 

  • 다음 level 매핑테이블의 주소, 상태값이 없으면


 60                                 *tbl = __pte(__phys_to_pte_val(*pte) |
 61                                              PMD_TYPE_TABLE | PMD_TABLE_UXN);

 

  • *tbl에 인수로 넘어온 pte 즉, 다음 level 매핑테이블용 page 물리 주소와 다음 level 매핑 테이블이 있고(PMD_TYPE_TABLE),
  • 유저영역에서 해당 주소공간을 실행불가(PMD_TABLE_UXN)를 설정
  • PMD_TYPE_TABLE 값은 PGD _TYPE_TABLE , PUD _TYPE_TABLE 값과 동일해서 PGD, PUD 에서도 사용가능


 62                                 *pte += PTRS_PER_PTE * sizeof(pte_t);

 

  • 다음 매핑테이블로 사용할 주소계산 *pte + 1page(4KB)


 63                         }
 64                         map_range(pte, start, next, pa, prot, level + 1,
 65                                   (pte_t *)(__pte_to_phys(*tbl) + va_offset),
 66                                   may_use_cont, va_offset);

 

  • level + 1 => 앞에서 구성된 다음 단계의 매핑 테이블 구성 하기 위해 재귀호출 (PGD -> PUD -> PMD -> PTE)


 67                 } else {
 68                         /*
 69                          * Start a contiguous range if start and pa are
 70                          * suitably aligned
 71                          */
 72                         if (((start | pa) & cmask) == 0 && may_use_cont)
 73                                 protval |= PTE_CONT;

 

  • start와 pa 주소가 CONT_PTE_SIZE 로 align 되어있고, PTE 를 연속적으로 사용할 것이 요청왔으면 PTE_CONT 설정


 74
 75                         /*
 76                          * Clear the contiguous attribute if the remaining
 77                          * range does not cover a contiguous block
 78                          */
 79                         if ((end & ~cmask) <= start)
 80                                 protval &= ~PTE_CONT;

 

  • PTE_CONT flag를 적용할경우 end 를 넘어서면 PTE_CONT flag 제거


 81
 82                         /* Put down a block or page mapping */
 83                         *tbl = __pte(__phys_to_pte_val(pa) | protval);

 

  • 물리주소와 flag 설정값 사용하여 가상주소와 매핑


 84                 }
 85                 pa += next - start;
 86                 start = next;
 87                 tbl++;

 

  • end까지 매핑하기 위해 1단위씩 증가


 88         }
 89 }


 

  • x0 = create_init_idmap 의 return 값 ( _end  까지 매핑한 주소변환테이블의 끝위치 반환)

 

 95
 96         /*
 97          * If the page tables have been populated with non-cacheable
 98          * accesses (MMU disabled), invalidate those tables again to
 99          * remove any speculatively loaded cache lines.
100          */
101         cbnz    x19, 0f

 

  • MMU on 이면 0f 로


102         dmb     sy
103         mov     x1, x0                          // end of used region

 

  • _end 까지 매핑한 주소변환테이블의 끝위치


104         adrp    x0, __pi_init_idmap_pg_dir
105         adr_l   x2, dcache_inval_poc
106         blr     x2

107         b       1f

 

  • x0 : __pi_init_idmap_pg_dir
  • x1 : _end  까지 매핑한 주소변환테이블의 끝위치
  • dcache 을 invalidate 할 범위 (x0, x1) 로 하여 실행
  • 1f로 가기


108

109         /*
110          * If we entered with the MMU and caches on, clean the ID mapped part
111          * of the primary boot code to the PoC so we can safely execute it with
112          * the MMU off.
113          */
114 0:      adrp    x0, __idmap_text_start
115         adr_l   x1, __idmap_text_end
116         adr_l   x2, dcache_clean_poc
117         blr     x2

 

  • MMU 와 Cache 가 On 되있으면, __idmap_text_start 부터 __idmap_text_end 까지 dcache clean ( = FLUSH) 


118
119 1:      mov     x0, x19

 

  • MMU off 이면 0, on 이면 0 아닌값을 x0에 넣음


120         bl      init_kernel_el                  // w0=cpu_boot_mode

 


 

254         .section ".idmap.text","a"
255
256 /*
257  * Starting from EL2 or EL1, configure the CPU to execute at the highest
258  * reachable EL supported by the kernel in a chosen default state. If dropping
259  * from EL2 to EL1, configure EL2 before configuring EL1.
260  *
261  * Since we cannot always rely on ERET synchronizing writes to sysregs (e.g. if
262  * SCTLR_ELx.EOS is clear), we place an ISB prior to ERET.
263  *
264  * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in x0 if
265  * booted in EL1 or EL2 respectively, with the top 32 bits containing
266  * potential context flags. These flags are *not* stored in __boot_cpu_mode.
267  *
268  * x0: whether we are being called from the primary boot path with the MMU on
269  */
270 SYM_FUNC_START(init_kernel_el)
271         mrs     x1, CurrentEL
272         cmp     x1, #CurrentEL_EL2
273         b.eq    init_el2

 

  • 현재 모드가 el2이면 init_el2로 가기


274
275 SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
276         mov_q   x0, INIT_SCTLR_EL1_MMU_OFF
277         pre_disable_mmu_workaround
278         msr     sctlr_el1, x0
279         isb

 

  • el1 모드의 MMU off


280         mov_q   x0, INIT_PSTATE_EL1

 

arch/arm64/include/asm/ptrace.h

 19 #define INIT_PSTATE_EL1 \
 20     (PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT | PSR_MODE_EL1h)

 

https://developer.arm.com/documentation/ddi0487/maa/-Part-D-The-AArch64-System-Level-Architecture/-Chapter-D1-The-AArch64-System-Level-Programmers--Model/-D1-5-Process-state--PSTATE/-D1-5-1-PSTATE-fields-that-are-meaningful-in-AArch64-state

https://developer.arm.com/documentation/111107/2026-03/AArch64-Registers/DAIF--Interrupt-Mask-Bits

 

281         msr     spsr_el1, x0
282         msr     elr_el1, lr
283         mov     w0, #BOOT_CPU_MODE_EL1
284         eret

 

  • 인터럽트를 disable 하고 부팅 모드가 el1임을 저장하고 return
  • 여기서 ret가 아닌 eret로 return 한 이유는 spsp_el1에 저장된 인터럽트 disable을 반영하기 위함


285
286 SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
287         msr     elr_el2, lr

 

  • 복귀 주소 elr_el2 에 보관


288
289         // clean all HYP code to the PoC if we booted at EL2 with the MMU on
290         cbz     x0, 0f

 

  • MMU off 이면 0f로 


291         adrp    x0, __hyp_idmap_text_start
292         adr_l   x1, __hyp_text_end
293         adr_l   x2, dcache_clean_poc
294         blr     x2

 

  • 하이퍼바이저 text 섹션 ( __hyp_idmap_text_start, __hyp_text_end) dcache clean
  • invalid 를 안하고 clean을하는 이유는 dcache 에 남아있는 data가 유효할거라 생각해서..

 

295
296         mov_q   x0, INIT_SCTLR_EL2_MMU_OFF
297         pre_disable_mmu_workaround
298         msr     sctlr_el2, x0
299         isb

 

  • el2에서의 MMU off

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/SCTLR-EL2--System-Control-Register--EL2-


300 0:
301
302         init_el2_hcr    HCR_HOST_NVHE_FLAGS

  • 하이퍼바이저 제어 레지스터(HCR_EL2)를 초기화합니다.
  • HCR_HOST_NVHE_FLAGS는 nVHE(Non-virtualized Host Extensions) 모드로 하이퍼바이저를 설정하겠다는 의미입니다. 이는 호스트 운영체제(Linux)는 EL1에서 실행되고, EL2에는 가상화를 위한 최소한의 기능만 남겨두는 전통적인 방식으로 CPU를 세팅하는 것입니다.


303         init_el2_state

  • el2의  MMU off, 타이머 등을 적절한 값으로 초기화


304
305         /* Hypervisor stub */
306         adr_l   x0, __hyp_stub_vectors
307         msr     vbar_el2, x0
308         isb

 

  • el2의 인터럽트 벡터 테이블을  __hyp_stub_vectors 로 설정 


309
310         mov_q   x1, INIT_SCTLR_EL1_MMU_OFF

  • MMU off 하기위해 x1에 준비


311
312         mrs     x0, hcr_el2
313         and     x0, x0, #HCR_E2H
314         cbz     x0, 2f

 

HCR_EL2.E2H 비트는 이 시스템이 VHE(Virtualization Host Extensions) 모드로 동작 중인지를 나타내는 플래그입니다.

  • 만약 이 비트가 1이면: VHE 모드 (호스트 커널이 EL2에서 직접 실행됨)
  • 만약 이 비트가 0이면: nVHE 모드 (전통적인 방식, 호스트 커널은 EL1에서 실행됨)

0이면 2f로 이동


315
316         /* Set a sane SCTLR_EL1, the VHE way */
317         msr_s   SYS_SCTLR_EL12, x1

 

  • 위 310라인에서 MMU off 로 설정한 값을 현재 VHE 모드이므로 sctlr_el1에 반영

https://developer.arm.com/documentation/ddi0602/2026-03/Base-Instructions/MSR--register---Move-general-purpose-register-to-System-register-?lang=en#MRS_values

 

 

arch/arm64/include/asm/sysreg.h

1136     .macro  mrs_s, rt, sreg
1137      __emit_inst(0xd5200000|(\sreg)|(.L__gpr_num_\rt))
1138     .endm
1139
1140     .macro  msr_s, sreg, rt
1141     __emit_inst(0xd5000000|(\sreg)|(.L__gpr_num_\rt))
1142     .endm

 

  58 #define __emit_inst(x)          .inst(x)

  • .inst 는 GAS 문법에서 x 는 기계어이므로 x 값 그대로 위치에 반영

 


318         mov     x2, #BOOT_CPU_FLAG_E2H
319         b       3f

 

  • 현재 이 CPU가 VHE(E2H) 모드로 부팅되었음을 나타내는 플래그 플래그(BOOT_CPU_FLAG_E2H)를 x2 레지스터에 기록
  • 3f로 가기


320
321 2:

  • VHE 비트가 0일 때 즉 nVHE 인경우


322         msr     sctlr_el1, x1

  • MMU off 설정값(x1)을 실제 sctlr_el1에 반영


323         mov     x2, xzr

  • VHE 가 아님을 나타내기위해 x2를 초기화


324 3:
325         mov     x0, #INIT_PSTATE_EL1
326         msr     spsr_el2, x0

 

  • 권한 레벨(EL)을 EL1으로 세팅 (PSR_MODE_EL1h)
  • 커널이 처음 실행될 때 방해받지 않도록 모든 인터럽트(DAIF 비트)를 일시적으로 전부 차단(Mask) 
  • 전용 스택 포인터(SP_EL1)를 사용 (PSR_MODE_EL1h)

https://developer.arm.com/documentation/dui0801/l/Overview-of-AArch64-state/Stack-Pointer-register?lang=en

  • 하기위해 spsr_el2에  값을 세팅


327
328         mov     w0, #BOOT_CPU_MODE_EL2

  • 부팅 모드가 EL2임을 저장


329         orr     x0, x0, x2

  • x0 = (x0 = 부팅모드 or x2 = VHE 여부) 를 넘겨주기 위해


330         eret

  • ret 가 아니 eret를 사용하여 spsr_el2에 적용된 인터럽트 금지 및 EL1으로의 모드전환을 발생시킴


331 SYM_FUNC_END(init_kernel_el)


 

121         mov     x20, x0

  • x20에 부팅모드(EL1인지 EL2인지)와 VHE 여부를 값을 보관


122
123         /*
124          * The following calls CPU setup code, see arch/arm64/mm/proc.S for
125          * details.
126          * On return, the CPU will be ready for the MMU to be turned on and
127          * the TCR will have been set.
128          */
129         bl      __cpu_setup                     // initialise processor


456 /*
457  *      __cpu_setup
458  *
459  *      Initialise the processor for turning the MMU on.
460  *
461  * Output:
462  *      Return in x0 the value of the SCTLR_EL1 register.
463  */
464         .pushsection ".idmap.text", "a"

  • .idmap.text 섹션에 위치


465 SYM_FUNC_START(__cpu_setup)
466         tlbi    vmalle1                         // Invalidate local TLB
467         dsb     nsh

  • el1의 가상주소 전체에 대한 tlb를 invalid 하여 초기화 되었음을 보장하고 완료될때까지 기다림

 

468
469         msr     cpacr_el1, xzr                  // Reset cpacr_el1

https://developer.arm.com/documentation/ddi0601/2022-03/AArch64-Registers/CPACR-EL1--Architectural-Feature-Access-Control-Register?lang=en

  • 부동소수점(FP), SIMD(NEON), 벡터 연산(SVE) 접근 금지

 

470         mov     x1, MDSCR_EL1_TDCC              // Reset mdscr_el1 and disable
471         msr     mdscr_el1, x1                   // access to the DCC from EL0

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/MDSCR-EL1--Monitor-Debug-System-Control-Register

  • MDSCR_EL1은 하드웨어 모니터링 및 디버깅 시스템을 조작하는 레지스터입니다. 여기에 TDCC 비트를 설정하면,  EL0 공간의 프로그램들이 데이터 통신 채널(DCC)을 통해 디버그 하드웨어에 접근하는 것을  트랩합니다. 부팅 도중 유저 공간 인터페이스를 통해 시스템 디버거가 해킹당하는 것을 막기 위한 것입니다.

 


472         reset_pmuserenr_el0 x1                  // Disable PMU access from EL0

https://developer.arm.com/documentation/ddi0601/2021-12/AArch64-Registers/PMUSERENR-EL0--Performance-Monitors-User-Enable-Register

  • CPU 내부에 탑재된 성능 모니터링 유닛(PMU)은 캐시 미스 횟수, 실행된 명령어 개수 등을 정밀하게 측정합니다.
  • 이런 자원은 EL1 즉 커널에서 처리하고 승인시 조회할수있게 하는 것이 보안에 안전할거같습니다.
  • 따라서 이를 위해 유저 공간의 PMU 접근 권한 레지스터(PMUSERENR_EL0)를 0으로 강제 리셋하여 금지하는 매크로입니다.

 

473         reset_amuserenr_el0 x1                  // Disable AMU access from EL0

https://developer.arm.com/documentation/ddi0601/2023-09/AArch32-Registers/AMUSERENR--Activity-Monitors-User-Enable-Register

  • PMU와 비슷하게 CPU의 실시간 주파수, 사이클 카운트, 전력 소모 상태 등을 모니터링하는 활동 모니터링 유닛(AMU)이 있습니다.
  • 이 역시 보안상 커널에서 처리하고 승인시 조횔할수있게 하는것이 안전할거같습니다.
  • 따라서 이를 위해 유저 사용 권한(AMUSERENR_EL0)을 완전히 박탈하여 시스템을 보호합니다.


474
475         /*
476          * Default values for VMSA control registers. These will be adjusted
477          * below depending on detected CPU features.
478          */
479         mair    .req    x17
480         tcr     .req    x16
481         tcr2    .req    x15

  • mair = x17, tcr = x16, tcr2 = x15 라고 alias


482         mov_q   mair, MAIR_EL1_SET

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/MAIR-EL1--Memory-Attribute-Indirection-Register--EL1-

  • 커널이 정의한 MAIR_EL1_SET 상수를 mair(x17) 레지스터에 채워 넣습니다. MAIR(Memory Attribute Indirection Register)은 페이지 변환 테이블이 사용할 페이지 속성을 나타내는 값을 보관.
  • 즉  "장치 제어용 메모리(Device)"로 쓸지, " 일반 메모리(Normal)"로 쓸지, 캐시 정책은 어떻게 할지 등의 속성(Attribute) 테이블을 정의하는 레지스터입니다.


483         mov_q   tcr, TCR_T0SZ(IDMAP_VA_BITS) | TCR_T1SZ(VA_BITS_MIN) | TCR_CACHE_FLAGS | \
484                      TCR_SHARED | TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \
485                      TCR_TBI0 | TCR_A1 | TCR_KASAN_SW_FLAGS | TCR_MTE_FLAGS

TCR ( Translation Control Register ) 

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/TCR-EL1--Translation-Control-Register--EL1-?lang=en

https://docs.kernel.org/arch/arm64/memory-tagging-extension.html

  • TCR_T0SZ( T0SZ  ): TTBR0 영역(주로 유저 공간 및 초기 매핑용)의 가상 주소 크기를 설정합니다.
  • TCR_T1SZ( T1SZ  ): TTBR1 영역(커널 공간)의 가상 주소 크기를 설정합니다.
  • 실제 가상 주소 비트 수는 64 - TxSZ  공식으로 계산됩니다. 즉, TxSZ 값이 클수록 가상 주소 공간의 크기는 작아집니다.
  • TCR_CACHE_FLAGS: 페이지 테이블을 탐색할 때 CPU 캐시(일반적으로 캐시 내부/외부 모두 켜짐 상태인 Outer/Inner Write-Back, Write-Allocate)를 거쳐서 탐색하도록 설정합니다.
  • TCR_SHARED: 페이지 테이블이 위치한 메모리 영역을 'Inner Shareable'(코어 간 공유 가능) 영역으로 지정합니다. 멀티코어 환경에서 다른 CPU 코어가 페이지 테이블을 변경했을 때 하드웨어적으로 데이터 일관성(Coherency)을 유지하기 위해 필수적입니다. 만약 이 플래그를 설정하지 않으면 Non-shareable 이 되서 페이지 변환테이블 작업시 캐쉬 프로토콜이 작동한지 않아서 일관성이 유지 되지 않음.
  • TCR_TG_FLAGS: Translation Granule(페이지 하나의 크기)을 결정합니다. 커널 설정에 따라 4KB, 16KB, 64KB 중 어떤 크기의 기본 페이지 단위를 사용할지 하드웨어 MMU에 알려주는 역할을 합니다.
  • TCR_KASLR_FLAGS: Kernel Address Space Layout Randomization을 지원하기 위한 플래그입니다. 부팅할 때마다 커널 코드가 배치되는 가상 주소를 무작위로 바꾸어, 해커가 특정 함수(예: 취약점 공격용 ROP 가젯)의 고정 주소를 타겟팅하지 못하도록 방어합니다.
  • TCR_ASID16: Address Space Identifier를 16비트(65,536개) 크기로 사용하겠다고 선언합니다. ASID는 각 프로세스(유저 공간)마다 부여되는 고유 ID입니다. 이것이 있으면 컨텍스트 스위칭(프로세스 전환)이 일어날 때마다 무겁게 CPU의 TLB(주소 변환 캐시)를 매번 전부 비우지(Flush) 않아도 되므로 성능이 크게 향상됩니다.
  • TCR_TBI0: Top Byte Ignore 기능을 TTBR0(유저 공간) 주소에 활성화합니다. 이 기능이 켜지면, 64비트 가상 주소 포인터 중 최상위 8비트(bit[63:56])에 어떤 값이 들어있든 MMU는 이를 주소 해석에 쓰지 않고 무시합니다. 덕분에 소프트웨어는 이 상위 8비트에 메타데이터나 태그를 심어둘 수 있습니다. (아래 KASAN, MTE 기술의 기반이 됩니다.)
  • MSB로 유저공간(TTBR0)과 커널 영역 (TTBR1) 주소인지를 구별했으나 63~56 bit를 사용하므로 55 bit로 유저공간인지 커널 공간인지를 구분함, 현재 가상주소 최대 크기는 52bit 임
  • TCR_A1: ASID 보관 위치를 TTBR1(커널)이 아닌 TTBR0(유저) 레지스터 기준으로 정렬하도록 지시합니다.
  • 이 ASID 번호 데이터는 원래 TTBR0과 TTBR1의 상위 비트[63:48]에 둘 다 각각 저장되어 있습니다
  • 하드웨어 MMU는 주소 변환 결과를 TLB에 기록할 때, TTBR0와 TTBR1 양쪽에 적힌 ASID 중 하나만 선택해서 기준으로 삼아야 합니다. 이때 기준점을 잡아주는 것이 TCR_A1 비트입니다. (당연히 TTBR0를 기준으로 해야지)
  • TCR_KASLR_FLAGS: Kernel Address Space Layout Randomization을 지원하기 위한 플래그입니다. 부팅할 때마다 커널 코드가 배치되는 가상 주소를 무작위로 바꾸어, 해커가 특정 함수(예: 취약점 공격용 ROP 가젯)의 고정 주소를 타겟팅하지 못하도록 방어합니다.
  • TCR_MTE_FLAGS: ARMv8.5부터 도입된 MTE (Memory Tagging Extension) 하드웨어 기능을 활성화합니다. KASAN이 소프트웨어적으로 태그를 검사했다면, MTE는 주소의 태그와 실제 물리 메모리에 기록된 할당 태그가 일치하는지를 CPU 하드웨어 수준에서 초고속으로 비교하여 메모리 오염 공격을 원천 차단합니다.

 

  • 위의 플래그를 설정하여 tcr(x16)에 저장


486         mov     tcr2, xzr

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/TCR2-EL1--Extended-Translation-Control-Register--EL1-

  • tcr2는 tcr의 기능확장버전으로 페이지 변환 테이블 엔트리 한 칸의 크기를 기존 64비트에서 128비트로 늘리는 기능등 이 있다.

여기서는 사용하지 않기 위해 0으로 초기화

 

487
488         tcr_clear_errata_bits tcr, x9, x5

특정 CPU의 오류 우회 작업


489

490 #ifdef CONFIG_ARM64_VA_BITS_52
491         mov             x9, #64 - VA_BITS
492 alternative_if ARM64_HAS_VA52
493         tcr_set_t1sz    tcr, x9
494 #ifdef CONFIG_ARM64_LPA2
495         orr             tcr, tcr, #TCR_DS
496 #endif
497 alternative_else_nop_endif
498 #endif

 

  • 커널을 빌드할 때 52비트 가상 주소 공간을 사용하도록 설정했고, CPU가 가상주소 52비트를 지원하면 tcr 레지스터에 가상 주소 52비트를 사용할수있게 설정합니다.
  • CONFIG_ARM64_LPA2는 4KB 또는 16KB 크기의 메모리 페이지 환경에서 최대 52비트의 대용량 물리 주소 범위 및 페이지 테이블 포맷(LPA2)을 사용할 것인지 결정하는 빌드 옵션입니다.
  • 이 옵션이 켜져 있으면 orr(Bitwise OR) 연산자를 사용하여, 기존 tcr 레지스터 값에 TCR_DS 비트 플래그를 추가합니다. TCR_DS (Data Size) 비트가 1로 세팅되어야 하드웨어 MMU가 52비트 크기의 거대한 물리 메모리 주소(PA)를 다룰 수 있는 확장 모드로 진입합니다.
  • 만약 CPU가 가상주소 52비트를 지원하지 않는다면 nop 으로 덮어씁니다.
  • (실제로는 먼저 nop으로 덮어쓰여저있고 부팅 cpu가 부팅을 어느정도완료하고 세컨드 cpu 가 부팅하기전 가상주소 52비트지원이 확인되면 nop부분을 가상주소 52비트처리하는 코드로 덮어씁니다.)

 

499
500         /*
501          * Set the IPS bits in TCR_EL1.
502          */
503         tcr_compute_pa_size tcr, #TCR_IPS_SHIFT, x5, x6

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/ID-AA64MMFR0-EL1--AArch64-Memory-Model-Feature-Register-0

http://jake.dothome.co.kr/registers64/

물리주소 크기를 ID_AA64MMFR0_EL1에서 읽어 TCR.IPS에 설정

 


504 #ifdef CONFIG_ARM64_HW_AFDBM

 

  • CONFIG_ARM64_HW_AFDBM은 하드웨어 주소 접근 플래그 및 더티 비트 관리(Hardware Access Flag and Dirty Bit Management) 기능을 커널에서 사용할 것인지 묻는 빌드 옵션입니다.


505         /*
506          * Enable hardware update of the Access Flags bit.
507          * Hardware dirty bit management is enabled later,
508          * via capabilities.
509          */
510         mrs     x9, ID_AA64MMFR1_EL1

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/ID-AA64MMFR1-EL1--AArch64-Memory-Model-Feature-Register-1

http://jake.dothome.co.kr/registers64/

  • ID_AA64MMFR1_EL1  : AArch64 상태에서 구현된 메모리 모델 및 메모리 관리 지원에 대한 정보를 제공합니다.

 

511         ubfx    x9, x9, ID_AA64MMFR1_EL1_HAFDBS_SHIFT, #4

  • ubfx(Unsigned Bit Field Extract) 명령어를 사용해 x9에 저장된 ID_AA64MMFR1_EL1 레지스터 값 중 하드웨어 AF/DB 지원 여부를 나타내는 HAFDBS 필드(4비트 크기)만 쏙 뽑아냅니다.


512         cbz     x9, 1f

  • cbz(Compare and Branch on Zero) 명령어로 추출한 값이 0인지 검사합니다.
  • 만약 0이라면 이 CPU는 하드웨어 AF/DB 기능을 지원하지 않는 구형 CPU이므로, 아래 설정을 건너뛰고 라벨 1f (519라인)로 점프합니다.


513         orr     tcr, tcr, #TCR_HA               // hardware Access flag update

  • CPU가 하드웨어 AF를 지원함이 확인되었으므로, orr 명령어로 tcr 값에 TCR_HA (Hardware Access Flag update) 비트 플래그를 활성화합니다.
  • 이 비트가 켜지면 커널이 일일이 개입하지 않아도, 메모리에 접근할 때마다 페이지 테이블의 Access Flag(AF) 비트를 알아서 1로 세팅해 줍니다. 이는 OS의 페이지 교체 알고리즘(LRU 등) 성능을 크게 향상시킵니다.

 


514 #ifdef CONFIG_ARM64_HAFT
515         cmp     x9, ID_AA64MMFR1_EL1_HAFDBS_HAFT
516         b.lt    1f
517         orr     tcr2, tcr2, TCR2_EL1_HAFT
518 #endif /* CONFIG_ARM64_HAFT */

519 1:
520 #endif  /* CONFIG_ARM64_HW_AFDBM */

https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/TCR-EL2--Translation-Control-Register--EL2-?lang=en#fieldset_0-21_21-1

  • HAFT (Hardware managed Access Flag for Table Descriptors) 기능 지원 여부를 확인합니다.
  • 앞서 x9에 추출해 둔 하드웨어 지원 레벨 값을 ID_AA64MMFR1_EL1_HAFDBS_HAFT (0b0011)상수와 비교(cmp)합니다.
  • 만약 CPU의 하드웨어가 완벽히 HAFT 를 지원하지 않으면 1f 라벨로 이동합니다. 
  • 아니면 tcr2 에 TCR2_EL1_HAFT값을 사용하여 tcr2.HA, tcr2.HD비트 플래그를 설정합니다.

 

 

521         msr     mair_el1, mair

  • Move to System Register. 앞서 482번 라인 등에서 mair(x17 레지스터의 별칭)에 임시로 빌드해 두었던 메모리 속성(Memory Attribute) 마스크 값을 실제 CPU 하드웨어의 MAIR_EL1 (Memory Attribute Indirection Register, EL1) 레지스터에 직접 덮어씁니다.
  • 이 레지스터에 값이 입력되는 순간부터 CPU는 각 페이지 테이블 descriptor가 가리키는 메모리 영역이 일반 RAM(Normal Memory)인지, 캐시를 쓰지 않는 디바이스 메모리(Device Memory)인지 등의 물리적 특성을 식별할 수 있는 기준(Index)을 가지게 됩니다.


522         msr     tcr_el1, tcr

  • 준비된 가상 메모리 시스템 제어 데이터인 tcr(x16 레지스터의 별칭)의 최종 값을 실제 하드웨어 제어 레지스터인 TCR_EL1 (Translation Control Register, EL1)에 직접 덮어씁니다. 
  • 가상주소 공간의 크기(VA_BITS), 페이지 크기(4KB/16KB/64KB Granule), 캐시 공유 정책, 앞서 설정한 하드웨어 Access Flag(AF) 자동 갱신 여부 등의 모든 MMU 하드웨어 동작 매커니즘이 이 순간 CPU에 최종 주입됩니다.
  • (단, MMU가 켜지기(SCTLR_EL1.M 비트 세팅) 전까지는 실제 주소 변환이 시작되지는 않습니다.)


523
524         mrs_s   x1, SYS_ID_AA64MMFR3_EL1
525         ubfx    x1, x1, #ID_AA64MMFR3_EL1_S1PIE_SHIFT, #4
526         cbz     x1, .Lskip_indirection

https://developer.arm.com/documentation/ddi0601/2023-06/AArch64-Registers/ID-AA64MMFR3-EL1--AArch64-Memory-Model-Feature-Register-3

  • 일반적인 시스템 레지스터는 mrs 명령어로 읽지만, ID_AA64MMFR3_EL1 레지스터는 ARM 아키텍처 규격상 비교적 최근에 추가된 레지스터이기 때문에 구형 어셈블러 컴파일러가 인식하지 못할 수 있습니다.
  • 이를 방지하기 위해 커널은 매크로 형태의 특수 명령어인 mrs_s를 사용하여 해당 레지스터의 주소(Opcode)를 직접 지정하여 값을 x1 레지스터로 읽어옵니다.
  • 이 레지스터에는 CPU가 지원하는 최신 메모리 모델 기능들(S1PIE, S2PIE 등)에 대한 정보가 비트 단위로 저장되어 있습니다.
  • Unsigned Bit Field Extract (부호 없는 비트 필드 추출). x1에 담긴 시스템 레지스터 값 중에서 S1PIE 기능의 지원 여부를 나타내는 4비트 크기의 필드만 정확히 추출해 냅니다.
  • Compare and Branch on Zero. 만약 ubfx로 추출한 x1의 값이 0이라면(즉, 현재 CPU가 S1PIE 기능을 지원하지 않는 구형 칩셋이라면), 아래에 이어질 권한 설정 코드들을 실행하지 않고 앞(Forward)에 있는 로컬 라벨인 .Lskip_indirection으로 곧장 점프합니다.

 

S1PIE란 무엇인가?

기존 ARM64의 페이지 테이블 descriptor(PTE) 내에는 이 페이지가 읽기 전용인지, 쓰기가 가능한지, 유저 공간용인지 등을 결정하는 고정된 권한 비트(AP, UXN, PXN 등)가 있었습니다. 하지만 운영체제가 고도화되면서 더 세부적이고 유연한 메모리 권한 제어(예: 실행 전용 페이지 보호, 커널 공간 방어 등)가 필요해졌습니다.

  • 간접 참조(Indirection): S1PIE 기능이 활성화되면 CPU는 PTE의 권한 비트를 직접 해석하지 않고, 이를 하나의 '인덱스(Index)'로 취급합니다.
  • CPU는 이 인덱스를 바탕으로 별도의 권한 제어 레지스터(PIRE0_EL1, PIRx_EL1)를 참조하여 실제 메모리 권한을 동적이고 안전하게 결정하게 됩니다.
  • 이 기능을 쓰면 커널이 메모리 보호 정책을 바꿀 때 수많은 페이지 테이블을 일일이 순회하며 비트를 수정할 필요 없이, 시스템 레지스터의 매핑 테이블 값만 바꾸면 되므로 보안성과 성능이 대폭 향상됩니다.

https://developer.arm.com/documentation/111107/2026-03/AArch64-Registers/PIRE0-EL1--Permission-Indirection-Register-0--EL1-

 

 


527
528         mov_q   x0, PIE_E0_ASM
529         msr     REG_PIRE0_EL1, x0

  • PIE_E0_ASM은 유저 공간에서 사용될 가상 메모리 인덱스별 권한 속성(예: 유저 공간 읽기/쓰기 허용, 유저 공간 실행 불가 등)을  정의해 둔 비트맵입니다.
  • 이 값을 PIRE0_EL1 (Permission Indirection Register EL0, Exception Level 1) 시스템 레지스터에 쓰면, 이제 CPU MMU는 유저 공간(EL0)의 애플리케이션 주소를 변환할 때, 이 레지스터에 정의된 권한 매핑 테이블을 기준으로 메모리 접근을 통제(Access Control)하게 됩니다.

 

530         mov_q   x0, PIE_E1_ASM
531         msr     REG_PIR_EL1, x0

  • PIE_E1_ASM은 커널 공간에서 사용할 인덱스별 메모리 권한 정책을 담고 있습니다. 커널 공간은 유저 공간보다 훨씬 엄격한 보안 규칙(예: 커널 코드 영역은 절대 쓰기 불가, 커널 데이터 영역은 절대 실행 불가 등)이 적용된 비트맵입니다. 
  • 이 값을 PIR_EL1 (Permission Indirection Register, Exception Level 1) 시스템 레지스터에 쓰면 이제 커널 공간(EL1) 내부에서 실행되는 모든 메모리 접근은 이 레지스터의 매핑 테이블을 거쳐 권한 유효성이 검사됩니다.

 

532
533         orr     tcr2, tcr2, TCR2_EL1_PIE

  • TCR2_EL1_PIE 비트는 앞으로 주소 변환 할 때, 기존 방식처럼 페이지 테이블의 권한 비트를 직접 해석하지 말고, PIR_EL1, PIRE0_EL1 에 등록한 권한 간접 참조 매핑 테이블을 거쳐서 메모리 접근 권한을 판단해라.


534
535 .Lskip_indirection:

S1PIE 기능을 지원하지 않는 CPU일 때


536
537         mrs_s   x1, SYS_ID_AA64MMFR3_EL1
538         ubfx    x1, x1, #ID_AA64MMFR3_EL1_TCRX_SHIFT, #4
539         cbz     x1, 1f
540         msr     REG_TCR2_EL1, tcr2

  • 구형 어셈블러와의 호환성을 위해 mrs_s 매크로 명령어를 사용하여 최신 하드웨어 기능 제어 정보가 담긴 ID_AA64MMFR3_EL1 시스템 레지스터의 값을 x1로 다시 읽어옵니다.
  • Unsigned Bit Field Extract. 읽어온 x1 값 중에서 이번에는 TCRX 기능의 하드웨어 지원 여부를 나타내는 4비트 크기의 필드만 정확히 추출합니다.
  • 추출된 값이 0이 아니라면, 이 CPU가 기본 TCR_EL1 레지스터 외에 추가적인 확장 기능들을 제어할 수 있는 보조 레지스터(TCR2_EL1) 를 사용할 수 있다는 의미입니다.
  • Compare and Branch on Zero. 만약 추출한 x1 값이 0이라면(즉, 현재 CPU가 TCRX 확장 사양을 지원하지 않는 구형 칩셋이라면), 1f로 점프하여 빠져나갑니다.
  • Move to System Register. 하드웨어의 TCRX 지원이 확인되었으므로, 지금까지 열심히 빌드해 온 tcr2(x15 레지스터의 별칭)의 최종 설정 값을 실제 CPU의 하드웨어 제어 레지스터인 TCR2_EL1 (Translation Control Register 2, EL1)에 직접 써서 주입합니다.

 


541 1:
542
543         /*
544          * Prepare SCTLR
545          */

https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/SCTLR-EL1--System-Control-Register--EL1-

http://jake.dothome.co.kr/registers64/

  • SCTLR은 System Control Register (시스템 제어 레거스터)의 약자입니다. 이 주석은 MMU를 켜고 끄는 것을 포함하여, CPU의 가장 원초적이고 핵심적인 하드웨어 기능들을 제어하는 최상위 스위치(SCTLR_EL1)에 주입할 값을 최종 준비하겠다는 선언입니다.


546         mov_q   x0, INIT_SCTLR_EL1_MMU_ON

 

  • INIT_SCTLR_EL1_MMU_ON은 리눅스 커널 헤더(arch/arm64/include/asm/sysreg.h)에 정의된 매우 중요한 64비트 상수 마스크입니다. 이 상수에는 MMU를 활성화하는 SCTLR_EL1.M (비트 0) 플래그를 포함하여, 캐시 활성화, 명령어 정렬 검사 등 CPU가 정상적인 운영체제 모드로 진입하기 위해 켜져야 하는 수많은 제어 비트들이 조합되어 있습니다.
  • x0 레지스터의 중요성: ARM64 아키텍처의 함수 호출 규약(Calling Convention)에 따라, 함수의 리턴 값(반환 값)은 항상 x0 레지스터에 담겨야 합니다. 이 함수의 맨 처음 주석에 적혀 있던 "Output: Return in x0 the value of the SCTLR_EL1 register." 규격을 정확히 만족시키는 순간입니다.

547         ret                                     // return to head.S
548
549         .unreq  mair
550         .unreq  tcr
551         .unreq  tcr2

  • 가독성을 위해 x17, x16, x15 범용 레지스터에 각각 부여했던 mair, tcr, tcr2라는 별칭(Alias)을 해제하는 어셈블러 지시자입니다.


552 SYM_FUNC_END(__cpu_setup)

 



130         b       __primary_switch

131 SYM_CODE_END(primary_entry)


508 SYM_FUNC_START_LOCAL(__primary_switch)
509         adrp    x1, reserved_pg_dir

  • adrp (Address of Page): 현재 PC(Program Counter)를 기준으로 상대적인 4KB 페이지 정렬 주소를 계산하여 레지스터에 로드하는 ARM64 특유의 고속 주소 로딩 명령어입니다. MMU가 켜지기 전이므로 이 명령어로 가져오는 주소는 물리 주소(Physical Address)입니다.
  • reserved_pg_dir: 커널 빌드 시 정적으로 할당된 비어 있는 임시 페이지 디렉터리(페이지 테이블) 공간입니다. MMU를 켜는 과도기 동안 혹시나 발생할 수 있는 잘못된 메모리 접근(Fault)을 안전하게 격리하거나, 초기화 과정에서 임시 버퍼 역할을 하기 위해 준비된 0으로 채워진 메모리 영역입니다. 이 주소가 x1 레지스터에 저장됩니다.
 


510         adrp    x2, __pi_init_idmap_pg_dir

  • __pi_init_idmap_pg_dir: 부팅 시 가장 중요한 Identity Mapping 페이지 테이블의 물리 주소입니다.
  • Identity Mapping이란? 가상 주소와 물리 주소를 1:1로 똑같이 매핑하는 것을 말합니다 (예: 가상 주소 0x80000000 -> 물리 주소 0x80000000).
  • MMU가 켜지는 바로 그 순간, CPU는 가상 주소로 메모리를 바라보기 시작합니다. 만약 현재 실행 중인 코드 영역이 1:1 매핑되어 있지 않다면, MMU가 켜지자마자 다음 명령어를 엉뚱한 가상 주소에서 찾게 되어 CPU가 Panic합니다. 이를 방지하기 위한 필수 테이블 주소를 x2에 담는 것입니다.

 


511         bl      __enable_mmu

 

  • bl (Branch with Link): 함수 호출 명령입니다. 다음 실행할 명령어 주소(리턴 링크)를 x30 (LR 레지스터)에 저장한 후, __enable_mmu 함수로 점프합니다.
  •  __enable_mmu 함수로 진입할 때, 앞서 __cpu_setup이 반환했던 SCTLR_EL1 설정 값은 여전히 x0에 들어있고, 임시 테이블들은 x1, x2에 담겨 넘어가게 됩니다. __enable_mmu 안에서 마침내 msr sctlr_el1, x0가 실행되며 MMU의 스위치가 켜집니다.

 

 


445 /*
446  * Enable the MMU.
447  *
448  *  x0  = SCTLR_EL1 value for turning on the MMU.
449  *  x1  = TTBR1_EL1 value
450  *  x2  = ID map root table address
451  *
452  * Returns to the caller via x30/lr. This requires the caller to be covered
453  * by the .idmap.text section.
454  *
455  * Checks if the selected granule size is supported by the CPU.
456  * If it isn't, park the CPU
457  */
458         .section ".idmap.text","a"

  • 이 함수 역시 .idmap.text 섹션에 배치됩니다. MMU를 켜는 명령어가 실행되는 그 순간에는 가상 주소와 물리 주소가 완벽히 일치하는 공간(Identity Mapping)에 코드가 머물러 있어야만 CPU가 크래시 나지 않고 다음 명령어를 실행할 수 있기 때문입니다.

 


459 SYM_FUNC_START(__enable_mmu)
460         mrs     x3, ID_AA64MMFR0_EL1
461         ubfx    x3, x3, #ID_AA64MMFR0_EL1_TGRAN_SHIFT, 4

  • CPU의 메모리 지원 사양을 담은 ID_AA64MMFR0_EL1 시스템 레지스터를 읽어와, 커널이 빌드될 때 선택한 페이지 크기(Translation Granule, 예: 4KB, 16KB, 64KB)를 하드웨어가 물리적으로 지원하는지 나타내는 4비트 필드를 ubfx 명령어로 추출합니다.


462         cmp     x3, #ID_AA64MMFR0_EL1_TGRAN_SUPPORTED_MIN
463         b.lt    __no_granule_support
464         cmp     x3, #ID_AA64MMFR0_EL1_TGRAN_SUPPORTED_MAX
465         b.gt    __no_granule_support

  • 현재 CPU의 지원 범위가 커널이 요구하는 최소 규격(MIN)보다 작거나, 최대 규격(MAX)보다 크다면 하드웨어 불일치 오류로 판단합니다.
  • 이 경우 조건 분기문(b.lt, b.gt)을 통해 CPU를 무한 루프에 가두어 안전하게 멈추는 __no_granule_support 라벨로 점프합니다.

 

 

466         phys_to_ttbr x2, x2
467         msr     ttbr0_el1, x2                   // load TTBR0

 

  • x2에 담겨 있던 Identity Map 루트 테이블의 물리 주소를 TTBR 레지스터 포맷에 맞게 변환(phys_to_ttbr)한 후, 하드웨어 레지스터인 TTBR0_EL1 에 설정합니다.
  • 이로써 MMU가 켜졌을 때 현재 실행 중인 코드가 있는 물리 주소 영역을 안전하게 1:1로 가상 주소에 매핑할 준비가 끝납니다.


468         load_ttbr1 x1, x1, x3

  • x1 레지스터에 저장되어 있던 커널 메모리 테이블 주소를 TTBR1_EL1 레지스터에 로드하는 매크로입니다.

 


469
470         set_sctlr_el1   x0

 

  • __cpu_setup이 반환하여 x0에 저장되어 있던 INIT_SCTLR_EL1_MMU_ON 값을 바탕으로 실제 시스템 제어 레지스터(SCTLR_EL1)를 업데이트하는 매크로입니다.
  • 이 매크로 내부에서 msr sctlr_el1, x0 및 isb가 실행되는 순간, 하드웨어 MMU가 마침내 On 상태로 전환됩니다. 이 라인 직후부터 CPU는 모든 주소를 가상 주소로 해석하기 시작합니다.

 

471
472         ret
473 SYM_FUNC_END(__enable_mmu)

 

 

 

498 SYM_FUNC_START_LOCAL(__no_granule_support)
499         /* Indicate that this CPU can't boot and is stuck in the kernel */
500         update_early_cpu_boot_status \
501                 CPU_STUCK_IN_KERNEL | CPU_STUCK_REASON_NO_GRAN, x1, x2

  • __early_cpu_boot_status 에 커널이 멈춘 이유 CPU_STUCK_IN_KERNEL , CPU_STUCK_REASON_NO_GRAN 을 저장합니다.

 


502 1:
503         wfe
504         wfi
505         b       1b

 

무한 루프


506 SYM_FUNC_END(__no_granule_support)


 

512
513         adrp    x1, early_init_stack
514         mov     sp, x1

 

  • 초기 임시 스택 영역(early_init_stack)으로 SP(스택 포인터) 를 설정합니다.
  •  ARM64에서 C 언어로 작성된 함수를 호출하려면 지역 변수를 저장하고 복귀 주소를 push할 수 있는 스택이 반드시 필요합니다. 이 두 라인을 통해 C 함수를 실행할 수 있는 최소한의 환경이 구축됩니다.

 

 


515         mov     x29, xzr

 

  •  ARM64 아키텍처 규격에서 x29 레지스터는 FP(Frame Pointer, 프레임 포인터) 역할을 합니다. 이 레지스터를 xzr(Zero Register)을 사용해 완전히 초기화합니다.
  • 프레임 포인터의 값을 0으로 세팅하는 것은 "여기서부터가 최상위 스택 프레임(함수 호출의 시작점)이다"라고 마킹하는 것입니다. 나중에 커널에 문제가 생겨서 디버깅을 위해 스택을 역추적(Backtrace)할 때, 디버거가 0을 만나면 "여기가 최상단이구나" 하고 추적을 안전하게 멈추게 됩니다.

 

 


516         mov     x0, x20                         // pass the full boot status
517         mov     x1, x21                         // pass the FDT

  • 함수를 호출하기 전에 인자(Argument)를 세팅합니다. ARM64 호출 규약에 따라 첫 번째 인자는 x0, 두 번째 인자는 x1에 담아야 합니다.
  • x0 = x20: 부팅 과정 내내 x20에 보존해 왔던 현재 CPU의 부팅 상태(Boot Status) 플래그를 첫 번째 인자로 넘깁니다.
  • x1 = x21: 부트로더(u-boot 등)가 넘겨주어 x21에 보관하고 있던 FDT(디바이스 트리)의 물리 주소를 두 번째 인자로 넘깁니다.

 


518         bl      __pi_early_map_kernel           // Map and relocate the kernel

 

  • 스택과 인자가 모두 준비되었으므로, 실제 커널 이미지와 하드웨어 정보(FDT)를 가상 메모리에 매핑하는 초기화 C 함수인 __pi_early_map_kernel을 호출합니다.
  • 이 함수 내부로 진입하면 부트로더가 메모리에 대충 올려둔 리눅스 커널의 진짜 코드 영역(TEXT), 데이터 영역(DATA),  초기화 세그먼트 등을 가상 주소 공간에 매핑 및 재배치하는 본격적인 커널 맵 공사가 시작됩니다.
  • __pi_ 접두사의 의미: Position Independent(위치 독립 실행)를 뜻합니다. 커널이 메모리의 어느 주소에 로드되더라도(KASLR 보안 기능 등이 적용되어도) 정상 동작할 수 있도록 컴파일된 특수 영역임을 나타냅니다. 

 


519
520         ldr     x8, =__primary_switched

 

  • __primary_switched(과거형 단어임에 주목)라는 함수의 절대 가상 주소(Absolute Virtual Address)를 리터럴 풀(Literal Pool)에서 읽어와 x8 레지스터에 로드합니다.
  • 앞서 사용했던 adrp 명령어는 현재 PC(Program Counter) 기준의 상대 주소를 가져오기 때문에 MMU 온/오프와 상관없이 현재 위치 기반의 주소를 알아냅니다. 반면, 이 ldr 방식으로 가져오는 주소는 링커(Linker)가 빌드할 때 지정한 0xFFFF_XXXX_XXXX_XXXX 형태의 완벽한 커널 전용 가상 주소입니다.

 


521         adrp    x0, KERNEL_START                // __pa(KERNEL_START)

 

커널 파일이 메모리에 로드된 시작 지점의 물리 주소(Physical Address)를 계산하여 x0 레지스터에 담습니다. (주석의 __pa는 Physical Address의 약자입니다.)

다음 함수인 __primary_switched에게 "현재 진짜 커널 코드가 램(RAM)의 몇 번지(물리 주소)에 얹혀서 시작되었는지"를 첫 번째 인자(x0)로 넘겨주기 위한 준비 작업입니다.

 


522         br      x8

 

  • Branch to Register. x8 레지스터에 저장된 주소로 무조건 점프합니다.
  • 대전환의 순간: 이 명령어가 실행되는 순간, CPU는 지금까지 머물던 1:1 식별 매핑(Identity Mapping) 영역을 탈출하여, 0xFFFF...로 시작하는 상위 커널 가상 주소 공간 영역으로 완전히 워프(Warp)하게 됩니다. 이 시점부터 명실상부한 가상 메모리 기반의 리눅스 운영체제가 가동됩니다.

 

523 SYM_FUNC_END(__primary_switch)

 


 

215 /*
216  * The following fragment of code is executed with the MMU enabled.
217  *
218  *   x0 = __pa(KERNEL_START)
219  */


220 SYM_FUNC_START_LOCAL(__primary_switched)


221         adr_l   x4, init_task

 

  • init_task는 리눅스 커널의 0번 프로세스(idle 프로세스 또는 swapper)의 task_struct 구조체 변수입니다. 모든 프로세스의 조상이 되는 첫 번째 프로세스입니다.
  • adr_l은 ARM64 어셈블리의 매크로(Macro)로, 현재 PC(Program Counter) 기준 상대 주소(PC-relative)를 이용해 특정 심볼의 가상 주소를 레지스터에 로드하는 역할을 합니다.
  • MMU가 갓 켜진 이 시점에서는 페이지 테이블이 매핑된 가상 주소(Virtual Address)를 사용해야 하므로, 이 명령어를 통해 init_task 구조체가 위치한 가상 주소를 레지스터 x4에 저장합니다.

 


222         init_cpu_task x4, x5, x6


187         /*
188          * Initialize CPU registers with task-specific and cpu-specific context.
189          *
190          * Create a final frame record at task_pt_regs(current)->stackframe, so
191          * that the unwinder can identify the final frame record of any task by
192          * its location in the task stack. We reserve the entire pt_regs space
193          * for consistency with user tasks and kthreads.
194          */
195         .macro  init_cpu_task tsk, tmp1, tmp2
196         msr     sp_el0, \tsk

 

  • msr(Move System Register) 명령어는 일반 레지스터의 값을 시스템 제어 레지스터로 옮깁니다.
  • sp_el0는 EL0(유저 레벨)에서 사용하는 스택 포인터 레지스터이지만, 리눅스 커널(EL1)은 이 레지스터를 유저 스택 용도가 아니라 '현재 실행 중인 태스크의 task_struct 주소(current)'를 가리키는 포인터 저장소로 전용(Tweak)하여 사용합니다.
  • 따라서 x4(\tsk)에 들어있던 init_task 구조체의 가상 주소가 sp_el0 시스템 레지스터에 저장됩니다. 이후 커널 내부에서 current 매크로를 호출하면 항상 이 sp_el0 값을 읽어와 현재 프로세스가 무엇인지 알 수 있게 됩니다.

 


197
198         ldr     \tmp1, [\tsk, #TSK_STACK]

 

  • \tsk 레지스터(x4, 즉 init_task 구조체의 시작 주소)로부터 TSK_STACK만큼 떨어진 오프셋(Offset)에 있는 값을 읽어 \tmp1(x5) 레지스터에 저장합니다.
  • TSK_STACK 오프셋에 저장된 값은 이 태스크가 사용할 커널 스택의 최하단(낮은 주소) 가상 주소(task_struct->stack)입니다.


199         add     sp, \tmp1, #THREAD_SIZE

 

  • add 명령어로 스택의 기준 주소를 계산합니다.
  • ARM64(AArch64) 아키텍처에서 스택은 높은 주소에서 낮은 주소 방향으로 자라납니다(Descending Stack).
  • 198라인에서 가져온 \tmp1은 스택의 가장 낮은 주소이므로, 여기에 스택의 총 크기인 #THREAD_SIZE(보통 16KB 또는 32KB)를 더해야 스택의 가장 꼭대기(높은 주소, Stack Base)를 가리키게 됩니다.
  • 이 계산된 최상단 주소를 시스템의 실제 스택 포인터 레지스터인 sp에 대입합니다.

200         sub     sp, sp, #PT_REGS_SIZE

 

  • sub 명령어로 스택 포인터 위치를 아래로 끌어내립니다.
  • 앞선 주석(192라인)에서 *"일관성을 위해 pt_regs 공간을 예약한다"*고 했던 작업을 실제로 수행하는 라인입니다.
  • 스택 최상단(sp)에서 CPU 레지스터들을 통째로 저장할 수 있는 구조체 크기인 #PT_REGS_SIZE만큼 주소를 빼서(주소가 낮아지는 방향으로 공간을 확보) 최종적인 sp 위치를 확정합니다. 이 빈 공간은 나중에 컨텍스트 스위칭이나 예외 처리가 발생했을 때 레지스터 값을 백업하는 용도로 사용됩니다.

 


201
202         stp     xzr, xzr, [sp, #S_STACKFRAME]

 

  • stp (Store Pair) 명령어는 두 개의 레지스터 값을 메모리에 연속으로 저장합니다.
  • 제로 레지스터(xzr, 항상 0을 가짐) 2개를 현재 스택 포인터(sp)로부터 S_STACKFRAME 오프셋만큼 떨어진 위치에 저장합니다.
  •  이 위치가 바로 최초의 pt_regs 내부에 위치한 스택 프레임 공간(FP와 LR)입니다. 이곳에 0과 0을 넣음으로써 스택 체인의 연결 고리의 끝임을 나타냅니다. 디버거가 스택을 역추적하다가 FP(0)와 LR(0)을 만나는 순간 "상위 호출자가 없다"고 판단하여 오류 없이 추적을 종료하게 됩니다.

203         mov     \tmp1, #FRAME_META_TYPE_FINAL
204         str     \tmp1, [sp, #S_STACKFRAME_TYPE]
205         add     x29, sp, #S_STACKFRAME

  • 임시 레지스터 tmp1에 #FRAME_META_TYPE_FINAL 이라는 기정의된 상수 플래그 값을 넣은 뒤, 이를 스택의 S_STACKFRAME_TYPE 공간에 저장(str)합니다.
  • 현재 스택 포인터(sp)에 S_STACKFRAME 오프셋을 더한 주소(즉, 방금 0과 메타데이터를 저장한 그 지점)를 계산하여 CPU의 x29 (Frame Pointer) 레지스터에 직접 입력합니다.
  • 이제부터 이 CPU에서 실행되는 모든 후속 함수들은 이 x29 주소를 뼈대로 삼아 자신의 스택 프레임을 쌓아 올리게 됩니다. 이 줄 덕분에 최초의 함수 호출 역추적이 완벽하게 작동할 수 있는 기초 이정표가 세워집니다.

 

[ 높은 주소 ]
            |                                   |
            +-----------------------------------+ <-- 199번 줄: add sp, \tmp1, #THREAD_SIZE
            |    TYPE = FRAME_META_..._FINAL    | <-- 203-204번 줄: "진짜 끝"임을 인증하는 도장
  x29 ----> +-----------------------------------+ <-- 205번 줄: add x29, sp, #S_STACKFRAME 
            |    LR (Link Register) = 0         | <-- 202번 줄: stp xzr, xzr로 0 삽입
            |    FP (Frame Pointer) = 0         | <-- 202번 줄: 스택 체인의 절대적 끝(종착점)
            +-----------------------------------+
            |           pt_regs 나머지          |
            |                                   |
       sp-> +-----------------------------------+ <-- 200번 줄: sub sp, sp,#PT_REGS_SIZE
            |            x30                    | <-- 228번 줄
            |            x29                    | <-- 228번 줄: stp x29, x30, [sp, #-16]!
  x29, sp-> +-----------------------------------+ <-- 228번 줄, 229: mov x29, sp
            |                                   |
            |                                   |
            |                                   |
            |                                   |
init_stack->+-----------------------------------+



[ 낮은 주소 ]

  CPU 레지스터 상태:
  x29 (Frame Pointer) : 0xYYYYYYYY (S_STACKFRAME의 가상 주소)
  
  
  
  <예시>
  
  [최초 상태]  현재 함수 내부의 x29(FP) 레지스터 확인
                  |
                  v
[역추적 1단계] 상위 함수의 스택 프레임(FP) 주소로 이동 -> 함수명 출력
                  |
                  v
[역추적 2단계] 또 그 상위 함수의 스택 프레임(FP) 주소로 이동 -> 함수명 출력
                  |
                  v
                 ... (반복)
                  |
                  v
[최종 단계]   우리가 만든 최상위 `S_STACKFRAME` 주소에 도달!
              1. 메타데이터가 `FRAME_META_TYPE_FINAL` 인지 확인 (통과)
              2. 상위 FP 값을 읽으려고 보니 `0` 임을 확인!
                  |
                  v
        "아, 여기가 진짜 끝이구나! 추적 종료." -> 에러 없이 깔끔하게 멈춤

 

 

206
207         scs_load_current

Shadow Call Stack (SCS)이란?

컴퓨터 보안에서 가장 흔하고 치명적인 공격 중 하나가 바로 "스택 버퍼 오버플로우"를 이용한 ROP (Return-Oriented Programming) 공격입니다.

  • 공격 원리: 함수가 종료될 때 돌아갈 주소(Link Register 또는 Return Address)는 일반 스택 메모리에 저장됩니다. 공격자가 스택에 넘치는 데이터를 주입하여 이 반환 주소를 해커의 악성 코드 주소로 변조하면, 함수가 끝나는 순간 시스템의 제어권이 넘어갑니다.
  • SCS의 방어책: 이를 막기 위해 ARM64 Linux 커널은 진짜 스택과 별개로 '반환 주소만 따로 저장하는 비밀 스택'을 하나 더 만듭니다. 이것이 바로 Shadow Call Stack입니다. 함수가 호출될 때 반환 주소를 일반 스택과 섀도 스택 양쪽에 다 적어두고, 함수가 끝날 때는 섀도 스택에 저장된 안전한 반환 주소만 사용하여 복귀합니다. 이로 인해 일반 스택이 아무리 오염되어도 ROP 공격이 원천 차단됩니다.

CONFIG_SHADOW_CALL_STACK 옵션이 켜져 있다면 이 매크로는 다음과 같은 실제 어셈블리 명령어로 확장됩니다. (만약 이 옵션이 꺼져 있다면 아무것도 하지 않는 빈 줄이 됩니다.)

 

ldr x18, [tsk, #TSK_TI_SCS_SP]

 

현재 태스크 구조체(tsk)에 저장되어 있던 이 태스크 고유의 섀도 스택 주소를 읽어와 x18 레지스터에 설정합니다.


208
209         adr_l   \tmp1, __per_cpu_offset
210         ldr     w\tmp2, [\tsk, #TSK_TI_CPU]
211         ldr     \tmp1, [\tmp1, \tmp2, lsl #3]

 

멀티코어 시스템에서 각 CPU 코어는 자신만의 독립적인 데이터(예: 스케줄러 정보, CPU별 통계, 타이머 등)를 가집니다. 이 코드의 목적은 "현재 부팅 중인 CPU가 멀티코어 메모리 맵 전체 중에서 '자신의 고유 변수 영역'이 정확히 어디인지 가리키는 이정표(오프셋)를 하드웨어 레지스터에 세팅하는 것"입니다.

 

  • __per_cpu_offset이라는 전역 배열(테이블)의 가상 주소를 계산하여 tmp1 레지스터에 저장합니다.
  • __per_cpu_offset[NR_CPUS] = {CPU 0의 오프셋, CPU 1의 오프셋, CPU 2의 오프셋...} 처럼 각 CPU 번호에 대응하는 메모리 오프셋 주소 값들을 순서대로 들고 있는 거대한 이정표 입니다.
  • 현재 태스크 구조체(tsk)의 메모리 공간에서 TSK_TI_CPU 오프셋(구조체 내 cpu 멤버 변수 위치 )에 저장된 값을 읽어와 32비트 레지스터 변수 wtmp2에 넣습니다.
  • 현재 이 코드를 실행하고 있는 "나(CPU)의 번호가 몇 번인가?"를 확인하는 작업입니다. 부팅 초기 단계(Primary CPU)이므로 여기서는 대개 0번을 읽어오게 됩니다.
  • tmp1(배열 시작 주소)에 tmp2(CPU 번호)를 더해 정확한 내 CPU의 오프셋 값이 저장된 주소를 찾아간 뒤, 그 안에 든 진짜 오프셋 값을 읽어와 다시 tmp1에 덮어씁니다.
  • lsl #3 : 64비트 아키텍처(ARM64)에서 메모리 주소(포인터) 하나는 8바이트 크기를 가집니다. 따라서 CPU 번호에 8을 곱해야 정확한 배열의 인덱스로 접근할 수 있습니다. 컴퓨터 과학에서 8을 곱하는 가장 빠른 방법은 비트를 왼쪽으로 3칸 미는 것(lsl #3, 2^3 = 8)이기 때문에 이 연산이 사용되었습니다.

 

212         set_this_cpu_offset \tmp1

  • set_this_cpu_offset 매크로는 방금 계산한 내 CPU 전용 오프셋 값을 ARM64의 특수 시스템 레지스터인 tpidr_el1에 저장합니다.
  • 이 레지스터에 오프셋이 등록되는 순간부터, 커널 내부에서 C 언어로 this_cpu_ptr(변수명) 같은 명령을 내릴 때 CPU가 메모리를 헤매지 않고 곧바로 "내 코어 전용 변수 방"으로 직행할 수 있게 됩니다. 다른 CPU 코어들과 Race Condition 없이 안전하고 빠르게 자기만의 데이터를 다룰 수 있는 하드웨어적 기반이 완성되는 순간입니다.

 

213         .endm

 



223
224         adr_l   x8, vectors                     // load VBAR_EL1 with virtual
225         msr     vbar_el1, x8                    // vector table address
226         isb

  • vectors는 entry.S 에 정의되어 있는 최초의 예외 처리 루틴들이 모여 있는 테이블의 시작점입니다
  • vbar_el1 (Vector Base Address Register, EL1) 은 커널 공간의 예외 처리 베이스 주소를 저장하는 레지스터입니다.
  • 이 줄이 실행되는 순간부터, CPU는 커널 실행 중 인터럽트나 CPU 예외가 발생하면 vbar_el1에 저장된 주소를 기준으로 삼아 해당 예외에 맞는 핸들러로 자동 점프하게 됩니다.
  • isb는 CPU의 파이프라인을 Flush 하고 다음 명령어를 처음부터 다시 Fetch 하도록 강제하는 동기화 Barrier 명령어입니다.
  • 현대 CPU는 성능을 높이기 위해 명령어들을 미리 읽어서 실행하는 '파이프라인' 구조를 가집니다. msr vbar_el1, x8 명령어로 시스템 레지스터를 변경했더라도, CPU 파이프라인에 이미 그 전 상태를 가정하고 들어와 있는 후속 명령어들이 있을 수 있습니다.
  • isb를 배치함으로써 "방금 vbar_el1 설정을 바꿨으니, 이 뒤에 오는 모든 명령어들은 반드시 바뀐 예외 설정을 인지한 상태에서 안전하게 실행되어야 한다"는 것을 보장합니다.

 

227
228         stp     x29, x30, [sp, #-16]!

 

 

  • stp (Store Pair) 명령어로 x29(FP)와 x30(LR) 레지스터의 값을 메모리에 동시에 저장합니다.
  • 주소 지정 방식 ([sp, #-16]!): 이 문법은 ARM64의 Pre-indexed 주소 지정 방식입니다. 맨 뒤에 붙은 느낌표(!)가 핵심입니다.
    1. sp 값을 먼저 16바이트만큼 아래(낮은 주소 방향)로 내립니다. (64비트 레지스터 2개 = 8바이트 × 2 = 16바이트 공간 확보)
    2. 새로 변경된 sp 주소 위치에 x29와 x30 값을 차례대로 저장합니다.
    3. 느낌표(!)의 효과로 인해, sp 레지스터의 값 자체가 16바이트 내려간 새 주소로 최종 업데이트됩니다.
  •  현재 함수의 상위 호출자 정보를 스택에 안전하게 보관하여, 나중에 이 함수가 끝날 때 부모 함수로 정상적으로 돌아갈 수 있도록 이정표를 저장하는 것입니다.

 

229         mov     x29, sp

 

  • 현재 스택 포인터(sp)의 주소 값을 프레임 포인터 레지스터인 x29에 복사합니다.
  • 방금 x29와 x30을 저장한 그 스택 지점을 "지금부터 실행될 새로운 함수의 스택 프레임 기준점"으로 삼겠다는 선언입니다. 이 줄이 실행됨으로써 디버거나 Unwinder가 함수 호출 계층을 추적할 수 있는 새로운 스택 체인 고리가 완벽하게 연결됩니다.

 


230
231         str_l   x21, __fdt_pointer, x5          // Save FDT pointer

 

  • 이 기기의 하드웨어 정보가 담긴 FDT(Device Tree Blob)의 물리 주소가 저장되어 있는 레지스터입니다. 부트로더가 커널을 실행할 때 x20과 x21 레지스터에 각각 부트 상태와 FDT 주소를 담아서 넘겨주며, 커널은 이 값을 여기까지 계속 유지해 왔습니다.
  • 커널의 C 언어 코드에서 참조할 수 있도록 정의된 전역 포인터 변수입니다 (void *__fdt_pointer). 이 코드 라인을 통해 assembly 단계에서 보관하던 FDT 주소가 C 언어 환경의 전역 변수로 복사됩니다. 이후 커널은 __fdt_pointer를 통해 디바이스 트리를 파싱하여 CPU 코어 개수, 메모리 크기, 주변 장치(UART, 가속기 등) 정보를 알아내게 됩니다.


232
233         adrp    x4, _text                       // Save the offset between
234         sub     x4, x4, x0                      // the kernel virtual and
235         str_l   x4, kimage_voffset, x5          // physical mappings

 

 

  • _text커널 이미지의 맨 처음 시작점을 가리키는 링커 스크립트 기호(Symbol)입니다.
  • 현재 MMU가 켜진 가상 메모리 상태이므로, 이 명령어를 실행하면 _text가 위치한 가상 주소(Virtual Address)의 페이지 기준 주소가 x4 레지스터에 저장됩니다.
  • 가상 주소가 담긴 x4에서 물리 주소가 담긴 x0를 뺍니다 ($x4 = x4 - x0$).
  • 즉, [커널 가상 주소] - [커널 물리 주소] 공식을 수행하는 것입니다. 이 결과값이 바로 두 주소 공간 사이의 차이인 오프셋(Offset)이 됩니다.
  • 방금 계산한 오프셋 값(x4)을 가상 메모리에 있는 kimage_voffset이라는 전역 변수에 저장합니다.
  • x5는 주소 계산을 위해 매크로 내부에서 임시로 사용하는 스크래치 레지스터입니다.

 

왜 오프셋(voffset)을 저장해야 할까요?

ARM64 리눅스 커널은 실행 중에 가상 주소를 물리 주소로 바꾸거나, 반대로 물리 주소를 가상 주소로 변환해야 하는 일이 아주 빈번합니다. 매번 복잡한 페이지 테이블을 역추적하는 것은 성능에 불리하므로, 커널은 이 오프셋 값을 이용해 아주 단순한 산술 연산만으로 주소를 변환합니다.

Virtual Address   = Physical Address + kimage_voffset
Physical Address = Virtual Address - kimage_voffset
 

이 오프셋 덕분에 커널은 실행 중에 자신이 물리 메모리 어디에 얹혀있든 상관없이 빠르고 유연하게 메모리를 관리할 수 있게 됩니다.

 

 

 

 


236
237         mov     x0, x20
238         bl      set_cpu_boot_mode_flag

  • x20 레지스터에 보관되어 있던 부트 시점의 CPU 모드 값(boot status)을 x0 레지스터로 복사합니다.
  • set_cpu_boot_mode_flag 함수 호출합니다. 이 함수는 x0로 전달받은 부트 모드 값을 분석하여 커널의 전역 변수인 __boot_cpu_mode 배열에 저장합니다.

 왜 CPU 부트 모드 플래그를 저장해야 할까요?

리눅스 커널은 자신이 EL1(일반 OS 모드)으로 켜졌는지, 아니면 EL2(하이퍼바이저 모드)로 켜졌는지에 따라 하드웨어 제어 방식을 다르게 처리해야 합니다.

  1. KVM(가상화) 활성화 여부: 커널이 EL2 모드로 부팅되었다면, 리눅스 자체를 하이퍼바이저로 사용할 수 있는 KVM 기능인 nVHE나 VHE 모드를 활성화할 수 있습니다.
  2. 보조 CPU(Secondary CPUs) 부팅: 나중에 CPU 코어 1, 2, 3번을 깨울 때(SMP 활성화), 주 프로세서(CPU 0)와 똑같은 예외 레벨(EL)로 맞춰서 깨워야 하므로 이 플래그 정보가 기준점이 됩니다.

 


 

 57 u32 __boot_cpu_mode[] = { BOOT_CPU_MODE_EL2, BOOT_CPU_MODE_EL1 };

 

  • __boot_cpu_mode[0]: 주 프로세서(Primary CPU)의 부팅 모드를 기록하는 공간.
  • __boot_cpu_mode[1]: 나중에 깨어날 보조 프로세서들(Secondary CPUs)의 부팅 모드를 기록하는 공간.

 

 

 

414 /*
415  * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
416  * in w0. See arch/arm64/include/asm/virt.h for more info.
417  */
418 SYM_FUNC_START_LOCAL(set_cpu_boot_mode_flag)
419         adr_l   x1, __boot_cpu_mode
420         cmp     w0, #BOOT_CPU_MODE_EL2
421         b.ne    1f

 

  • 현재 부팅 상태가 EL2가 아니면 1f


422         add     x1, x1, #4

  • 다음 부팅 CPU에게 첫 부팅 CPU의 부팅 상태를 알려주기 위해 인텍스 증가 ( __boot_cpu_mode[1])


423 1:      str     w0, [x1]                        // Save CPU boot mode

 

  • 최종 결정된 x1 주소가 가리키는 메모리에 w0 값(부트 모드 플래그)을 저장합니다.
  • EL1 부팅 시 (b.ne 만족): x1이 그대로 __boot_cpu_mode[0]을 가리키므로 여기에 값이 저장됩니다.
  • EL2 부팅 시 (b.ne 불만족): 4바이트가 더해진 __boot_cpu_mode[1]에 값이 저장됩니다. 

 


424         ret
425 SYM_FUNC_END(set_cpu_boot_mode_flag)

 


 

 


239
240 #if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)

 

  • CONFIG_KASAN_GENERIC: 가장 널리 쓰이는 표준 KASAN 모드입니다. 컴파일러가 메모리 접근 코드 주변에 검사 코드를 삽입하고, Shadow Memory 라는 별도의 공간을 두어 메모리 오염을 감지합니다.
  • CONFIG_KASAN_SW_TAGS: ARM64의 TBI(Top Byte Ignore) 하드웨어 기능을 활용하는 소프트웨어 태그 기반 KASAN 모드입니다. 포인터의 상위 바이트에 태그를 숨겨 메모리 유효성을 검사합니다.

 


241         bl      kasan_early_init

  • 이 함수는 커널이 실행되면서 앞으로 발생할 메모리 접근을 감시할 수 있도록, 초기 섀도 메모리 영역을 임시 페이지 테이블(kasan_early_shadow_page)로 매핑하는 작업을 수행합니다. 이 작업이 끝나야만 이후에 켜질 복잡한 커널 코드들이 메모리를 안전하게 감시받으며 달릴 수 있습니다.


242 #endif

https://www.kernel.org/doc/html/latest/dev-tools/kasan.html

 


243         mov     x0, x20
244         bl      finalise_el2                    // Prefer VHE if possible

  • x20 레지스터에 보관되어 있던 부트 시점의 CPU 모드 값(boot status)을 x0 레지스터로 복사합니다.
  • 주석에 적힌 "Prefer VHE if possible" (가능하면 VHE를 선호함)이라는 문구 그대로, 함수 내부에서는 x0(즉, x20) 값을 검사하여 현재 시스템이 EL2 하이퍼바이저 모드로 부팅되었는지를 확인합니다.
  • 조건이 맞고 하드웨어가 ARMv8.1+ VHE 기능을 지원한다면, 시스템 제어 레지스터를 조작하여 커널이 EL2에서 직접 효율적으로 실행되도록 세팅을 마무리지어 줍니다.

왜 이 타이밍에 VHE를 결정할까?

리눅스 커널 부팅 과정에서 가상화 모드(VHE vs nVHE)를 확정 짓는 것은 향후 커널의 메모리 맵과 실행 권한을 결정하는 중대한 분수령입니다.

  • VHE 모드가 활성화되면: 커널이 EL2의 가상 주소 공간(TTBR0_EL2, TTBR1_EL2)을 직접 사용하게 되므로, 시스템 레지스터의 매핑 구조를 이에 맞게 재조정해야 합니다.
  • 이후 단계와의 연결: 이 작업이 끝나고 나면 바로 아래에서 메인 C-런타임 함수인 start_kernel()로 진입하게 되는데, 그 전에 CPU의 실행 환경(예외 레벨 및 가상화 모드)이 완벽하게 픽스되어 있어야 C 언어로 작성된 가상화 서브시스템(KVM)이 오작동 없이 초기화될 수 있습니다.

 

 

 


239 /*
240  * Entry point to finalise EL2 and switch to VHE if deemed capable
241  *
242  * w0: boot mode, as returned by init_kernel_el()
243  */
244 SYM_FUNC_START(finalise_el2)
245         // Need to have booted at EL2
246         cmp     w0, #BOOT_CPU_MODE_EL2
247         b.ne    1f

 

  • 주석에 적힌 대로, VHE 설정을 하려면 애초에 시스템이 EL2 모드로 부팅되었어야만 합니다.
  • 따라서 매개변수로 넘어온 w0의 부트 모드가 BOOT_CPU_MODE_EL2(0x0e12) 가 맞는지 비교합니다.
  • 비교 결과가 다르다면(즉, 시스템이 EL2가 아닌 일반 EL1 모드로 부팅되었다면), VHE를 적용할 수 없으므로 뒤쪽의 VHE 설정 로직들을 전부 건너뛰고 하단에 있을 로컬 라벨 1:로 즉시 점프합니다.
  • 만약 같다면(EL2 부팅이 맞다면), 점프하지 않고 아래로 계속 진행하여 하드웨어가 VHE 기능을 지원하는지 확인하고 가상화 환경 초기화를 완수하게 됩니다.

 

 

VHE (Virtualization Host Extensions)란?

ARMv8.1 아키텍처부터 도입된 기능입니다.

  • nVHE (Non-VHE, 기본 방식): 호스트 리눅스 커널은 EL1에서 돌고, 가상화를 제어하는 KVM 핵심 코드는 EL2에서 따로 돕니다. 하이퍼바이저 기능을 쓸 때마다 EL1 < --- > EL2 간의 값 비싼 CPU 모드 전환이 일어납니다.
  • VHE (활성화 방식): 호스트 리눅스 커널이 처음부터 EL2 레벨을 통째로 장악하여 실행됩니다. 주소 공간과 레지스터 구조를 재매핑하여 커널이 모드 전환 없이 직접 가상머신을 제어하므로 오버헤드가 거의 제로에 가깝게 줄어듭니다.

 

248
249         // and still be at EL1
250         mrs     x0, CurrentEL
251         cmp     x0, #CurrentEL_EL1
252         b.ne    1f

  • VHE 설정을 시도한 직후, 시스템이 예기치 않게 EL2 모드에서 EL1(커널 모드)로 Downgrade 되었는지 확인하는 방어적 예외 처리 구간입니다.
  • 현재 CPU가 실행 중인 Exception Level 정보를 담고 있는 레지스터인 CurrentEL의 값을 읽어서 x0에 복사합니다.
  • 방금 읽어온 현재 CPU의 레벨(x0)이 EL1 모드 플래그(CurrentEL_EL1, 값으로는 0x4)와 일치하는지 비교합니다.
  • 비교 결과가 다르다면(즉, 현재 CPU가 EL1으로 떨어지지 않고, 우리가 의도한 대로 EL2 모드를 안전하게 유지하고 있다면), 아래쪽에 위치한 로컬 라벨 1:로 점프하여 정상 흐름을 계속 탑니다.
  • 만약 같다면(어떤 이유에서든 현재 상태가 EL1이라면), 점프하지 않고 바로 다음 줄로 진행하여 "EL2로 유지되어야 하는데 EL1으로 떨어졌다"는 에러 처리나 대피(Fallback) 로직(예: nVHE 모드로 강제 전환 등)을 수행하게 됩니다.

 

 


253
254         mov     x0, #HVC_FINALISE_EL2

 

  • HVC_FINALISE_EL2 라는 내부 상수를 x0 레지스터에 로드합니다.
  • 이 상수는 하이퍼바이저(EL2)에게 보낼 "명령어 ID"입니다. 바로 다음 줄에서 하이퍼바이저 모드를 호출할 때, x0에 담긴 이 ID를 보고 하이퍼바이저가 무슨 작업을 처리해야 하는지 판단하게 됩니다.


255         hvc     #0

 

  • ARM64 아키텍처에서 가상화의 핵심 명령어입니다. 유저 모드(EL0)에서 커널(EL1)로 갈 때 svc(System Call)를 쓰는 것처럼, 커널(EL1)에서 하이퍼바이저(EL2)로 트랩(Trap)하여 진입할 때 hvc 명령어를 사용합니다.
  • 이 명령어가 실행되는 순간 CPU는 EL2 권한으로 전환되며, 미리 등록된 하이퍼바이저 벡터 테이블의 핸들러가 실행됩니다. 이 핸들러는 x0에 담긴 HVC_FINALISE_EL2 명령을 확인하고, CPU 시스템 레지스터들의 VHE 세팅을 최종적으로 확정지은 뒤 다시 원래 코드로 복귀시킵니다.
  • 뒤의 #0은 Immediate value 로, 대개 소프트웨어적으로 추가적인 구분을 할 때 쓰이나 리눅스에서는 보통 0으로 처리하고 레지스터(x0)로 인자를 넘깁니다. SMCCC(Secure Monitor Call Convention)라는 규격에 정의되었습니다.

 

 


256 1:
257         ret
258 SYM_FUNC_END(finalise_el2)

 

 

 



245         ldp     x29, x30, [sp], #16

 

  • 228번 라인에서 스택에 안전하게 보관(Push)해 두었던 프레임 포인터(x29)와 링크 레지스터(x30, 돌아갈 주소) 값을 복원(Pop)하는 과정입니다.
  • 뒤의 ], #16은 Post-increment 로, 값을 다 읽어온 후에 스택 포인터(sp)를 16바이트만큼 위로 올려 스택 공간을 깔끔하게 비워줍니다.

 


246         bl      start_kernel

 

  • init/main.c 파일에 정의된 리눅스 커널의 메인 C 함수인 start_kernel()로 진입합니다.


247         ASM_BUG()

 

  • 정상적인 상황이라면 start_kernel() 함수는 운영체제가 종료(Shutdown/Poweroff)될 때까지 절대로 반환(Return)되어서는 안 되는 함수입니다. 즉, 이 줄로 실행 흐름이 내려왔다는 것 자체가 시스템에 거대한 치명적 오류가 발생했음을 의미합니다.
  • 따라서 ASM_BUG() 매크로는 실행이 이곳에 도달하는 순간 시스템을 강제로 중단(Fault/Panic)시키고 개발자가 디버깅할 수 있도록 크래시 정보를 남기는 역할을 합니다.

 


248 SYM_FUNC_END(__primary_switched)
249

 




 

시스템이 처음 부팅될 때 부팅을 주도하는 주 프로세서(Boot CPU 또는 BSP) 외에, 멀티코어 시스템에서 다른 보조 프로세서(Secondary CPU 또는 AP)들이 동적으로 활성화(Hotplug 또는 시스템 부팅 시)될 때 가장 먼저 진입하는 진입점(Entry Point)을 정의하고 있습니다.

 

351         /*
352          * Secondary entry point that jumps straight into the kernel. Only to
353          * be used where CPUs are brought online dynamically by the kernel.
354          */
355 SYM_FUNC_START(secondary_entry)
356         mov     x0, xzr
357         bl      init_kernel_el                  // w0=cpu_boot_mode

 

  • 보조 CPU가 현재 어떤 Exception Level(EL, 예외 레벨)에서 실행 중인지 확인하고, 커널이 정상적으로 동작할 수 있도록 EL 설정을 초기화합니다. (ARM64에서 일반적인 리눅스 커널은 EL1에서 동작하며, 하이퍼바이저가 있다면 EL2에서 시작해 EL1으로 전환될 수 있습니다.)
  • 옆의 주석(w0=cpu_boot_mode)에서 알 수 있듯이, 이 함수를 실행하고 나면 결과값(CPU가 어떤 모드로 부팅되었는지에 대한 정보)이 w0 레지스터(x0의 하위 32비트)에 저장되어 반환됩니다.
  • 이전 270라인의 init_kernel_el 참고

 


358         b       secondary_startup

  • 예외 레벨 초기화가 끝났으므로, 보조 CPU를 본격적으로 커널 구동 상태로 만들기 위한 secondary_startup 루틴으로 넘어갑니다.
  • 이 이후 단계에서 MMU(메모리 관리 장치)를 켜고, 페이지 테이블을 설정하며, 최종적으로 C 언어로 작성된 커널 코드(secondary_start_kernel)로 진입하게 됩니다.


359 SYM_FUNC_END(secondary_entry)

360

 

361 SYM_FUNC_START_LOCAL(secondary_startup)
362         /*
363          * Common entry point for secondary CPUs.
364          */
365         mov     x20, x0                         // preserve boot mode

  • 나중에 이 부팅 모드 값을 다시 참조할 수 있도록,  x20 에 값을 백업


366
367 #ifdef CONFIG_ARM64_VA_BITS_52
368 alternative_if ARM64_HAS_VA52
369         bl      __cpu_secondary_check52bitva
370 alternative_else_nop_endif
371 #endif

 

  • CONFIG_ARM64_VA_BITS_52 52비트 가상 주소 기능을 커널 자체에서 지원하도록 설정했을 때만 실행 코드로 만듭니다.
  • alternative_if ARM64_HAS_VA52: 현재 시스템의 CPU 하드웨어가 52비트 가상 주소 기능(ARM64_HAS_VA52)을 실제로 지원하는지 부팅 시점에 판단합니다.
  • 만약 하드웨어가 52비트 VA를 지원한다면, 369 라인의 bl __cpu_secondary_check52bitva를 그대로 실행하여 보조 CPU의 52비트 가상 주소 상태를 체크합니다.
  • 만약 하드웨어가 52비트 VA를 지원하지 않는다면, 커널은 런타임에 이 메모리 영역의 명령어를 nop(No Operation, 아무것도 하지 않고 넘어감) 명령어들로 통째로 갈아 끼워 버립니다(alternative_else_nop_endif).
  • 매번 if(지원하는가?)를 CPU 분기 예측으로 검사하면 성능 손실이 발생하므로, 부팅할 때 딱 한 번 하드웨어를 검사한 뒤 안 쓰는 코드는 아예 nop으로 지워버리는 리눅스 커널 고유의 최적화 기법입니다

 


372
373         bl      __cpu_setup                     // initialise processor

 

  • 이 함수는 각 CPU 코어가 메모리 관리 장치(MMU)를 켜고 가상 주소 체계로 전환하기 전에, 주로 시스템 레지스터인 SCTLR_EL1, TCR_EL1, MAIR_EL1 등을 올바른 초기 상태로 설정하는 역할을 합니다.
  • 주 CPU(Boot CPU)도 부팅 가동 시 이 함수를 똑같이 호출하며, 보조 CPU들 역시 가상 메모리 세계로 진입하기 전에 이 과정을 반드시 거쳐야 합니다.

 

 


374         adrp    x1, swapper_pg_dir
375         adrp    x2, idmap_pg_dir
376         bl      __enable_mmu

  • swapper_pg_dir은 리눅스 커널의 가상 메모리 페이지 테이블의 최상위 디렉터리 주소입니다. 즉, 커널 공간(Kernel Space) 전체를 매핑하고 있는 시작점 주소를 x1에 담아 __enable_mmu 함수의 첫 번째 매개변수로 전달하는 것입니다.
  • idmap_pg_dir은 Identity Mapping 페이지 테이블의 주소입니다. 이는 "물리 주소 == 가상 주소"가 되도록 똑같이 1:1로 매핑해 둔 특수 페이지 테이블입니다.
  • MMU를 켜는 그 정점의 순간, 명령어 라인 몇 개는 가상 주소가 켜지기 전(물리 주소 세계)과 켜진 후(가상 주소 세계)에서 동일한 주소로 실행되어야 CPU가 꼬이지 않고 부드럽게 전환 될 수 있습니다. 이 과도기를 무사히 넘기기 위한 징검다리 지도의 주소를 x2에 담아 전달합니다.
  • __enable_mmu 내부에서는 전달받은 주소들을 시스템 레지스터(TTBR0_EL1, TTBR1_EL1)에 쓰고, 시스템 제어 레지스터(SCTLR_EL1)의 MMU Enable 비트(M 비트)를 1로 세팅합니다. 이 함수가 성공적으로 끝나고 복귀할 때, 보조 CPU는 더 이상 물리 주소가 아닌 가상 주소(Virtual Address) 모드로 동작하게 됩니다.

 

왜 두 개의 페이지 테이블(swapper와 idmap)이 필요할까?

ARM64 구조에서 MMU가 켜지면 프로세서는 두 개의 서로 다른 가상 주소 영역을 하드웨어적으로 완벽히 분리해 관리합니다.

  • TTBR0_EL1 (Translation Table Base Register 0): 주로 유저 공간이나 하위 주소 영역($0x0000_....) 변환에 쓰입니다. MMU가 켜지는 순간의 징검다리 역할을 위해 idmap_pg_dir이 여기에 설정됩니다.
  • TTBR1_EL1 (Translation Table Base Register 1): 상위 주소 영역($0xFFFF_....)인 커널 공간 변환에 쓰입니다. 리눅스 커널의 본체가 거주하는 공간의 지도인 swapper_pg_dir이 여기에 설정됩니다.

 

 


377         ldr     x8, =__secondary_switched
378         br      x8

  • _secondary_switched 루틴의 절대 주소(가상 주소 값)를 레지스터 리터럴 풀(Literal Pool)에서 읽어와 x8 레지스터에 저장합니다.
  • bl(Branch with Link)이 아닌 br(Branch to Register)을 사용했습니다. 돌아올 링크 레지스터(lr)를 남기지 않고, 이 어셈블리 루틴을 떠나 목적지로 완전히 넘어가겠다는 뜻입니다.
  • 이 명령어가 실행되는 순간, 보조 CPU는 물리 주소와 1:1 매핑되어 있던 과도기 상태(idmap)를 완전히 탈피하여, __secondary_switched 루틴으로 진입합니다. 그곳에서 스택 포인터(sp)를 설정하고 최종적으로 C 언어 함수인 secondary_start_kernel()을 호출하게 됩니다.


379 SYM_FUNC_END(secondary_startup)

 

381         .text
382 SYM_FUNC_START_LOCAL(__secondary_switched)
383         mov     x0, x20
384         bl      set_cpu_boot_mode_flag

  • x20 레지스터에 보관되어 있던 부팅 모드 값을 다시 x0 레지스터로 복사(이동)합니다.
  • 현재 보조 CPU가 어떤 모드(EL1 커널 모드 또는 EL2 하이퍼바이저 모드)로 시스템에 참여했는지 확인하여, 커널의 전역 변수나 플래그에 전역적으로 기록하는 역할을 합니다.

 


385
386         mov     x0, x20
387         bl      finalise_el2

  • x20에 들어있던 부팅 모드(cpu_boot_mode) 값을 다시 x0 레지스터로 복사합니다.
  • finalise_el2 내부에서는 이 보조 CPU가 하이퍼바이저 모드(EL2) 기능을 정상적으로 수행할 수 있도록 하드웨어 설정을 최종 확정(Finalise)하고, 실제 커널 코드가 돌아갈 EL1 상태와의 동기화를 마칩니다.


388
389         str_l   xzr, __early_cpu_boot_status, x3
390         adr_l   x5, vectors
391         msr     vbar_el1, x5
392         isb

 

  • __early_cpu_boot_status는 보조 CPU가 부팅되는 동안 자신의 상태(성공, 실패, MMU 문제 등)를 주 CPU(Boot CPU)에게 보고하기 위해 쓰이는 일종의 '통신용 플래그 변수'입니다.
  • 이제 보조 CPU가 MMU도 성공적으로 켰고 __secondary_switched 내부 진입까지 무사히 마쳤으므로, 이 플래그를 0(xzr = Zero Register)으로 깨끗이 클리어하여 "이 코어는 초기 어셈블리 단계의 부팅 단계를 에러 없이 무사히 통과했다"는 신호를 남기는 것입니다.
  • vectors는 리눅스 커널이 인터럽트(Interrupt), 시스템 콜(Syscall), 메모리 폴트(Page Fault) 등을 처리하기 위해 미리 만들어 둔 ARM64 예외 벡터 테이블의 시작 주소입니다.
  • vbar_el1 (Vector Base Address Register, EL1)은 CPU 하드웨어가 예외 상황을 맞닥뜨렸을 때 "어느 주소에 있는 코드를 실행해야 하는가?"를 가리키는 시스템 레지스터입니다.
  • 이 설정을 통해, 이제 이 보조 CPU에 타이머 인터럽트가 발생하거나 하드웨어 예외가 터지면 커널이 정의한 vectors 핸들러로 즉시 격리·처리될 수 있는 시스템적 방어벽이 완성됩니다.

 

393
394         adr_l   x0, secondary_data
395         ldr     x2, [x0, #CPU_BOOT_TASK]

  • 부팅 CPU가 새로 깨어날 세컨더리 CPU를 위해 미리 메모리에 준비해 둔 공유 데이터 영역입니다. 여기에는 이 CPU가 사용할 스택 포인터 주소, 태스크(프로세스) 정보, 페이지 테이블 정보 등이 담겨 있습니다.
  • [x0, #CPU_BOOT_TASK]: 방금 구한 secondary_data 주소(x0)로부터 CPU_BOOT_TASK라는 오프셋(거리)만큼 떨어진 위치의 데이터를 읽어 x2 레지스터에 넣습니다.
  • 의미: 이 CPU 코어가 운영체제 내에서 최초로 실행할 '태스크 구조체(task_struct)'의 주소를 가져옵니다. ARM64 리눅스 커널에서는 이 태스크 구조체 정보가 정상적으로 로드되어야만 커널 스택(sp)의 위치도 올바르게 계산해 낼 수 있습니다.

 

396         cbz     x2, __secondary_too_slow

 

  • cbz (Compare and Branch on Zero): 비교 대상 레지스터(x2)의 값이 0이면 지정된 레이블로 즉시 점프(Branch)하는 명령어입니다.
  • 의미: 만약 x2에 담긴 부팅 태스크 주소가 0(NULL)이라면, 무언가 문제가 생겨 메인 CPU가 데이터를 채워주기 전에 세컨더리 CPU가 너무 빨리 깨어났거나 동기화 타이밍을 놓친 상황입니다.
  • __secondary_too_slow 분기: 이 조건이 참이 되면 CPU는 정상 부팅을 중단하고 __secondary_too_slow라는 에러 처리 루틴으로 점프합니다. 

 


397
398         init_cpu_task x2, x1, x3

  • 방금 전 라인(395번 행)에서 로드한 부팅 태스크 정보(x2)를 바탕으로, 이 CPU 코어가 앞으로 사용할 '현재 실행 중인 프로세스 정보(Current Task)'와 '커널 스택(Kernel Stack)'을 CPU 하드웨어 레지스터에 바인딩하는 핵심 동작을 수행합니다.


399
400 #ifdef CONFIG_ARM64_PTR_AUTH
401         ptrauth_keys_init_cpu x2, x3, x4, x5
402 #endif

  • CONFIG_ARM64_PTR_AUTH: 리눅스 커널을 빌드할 때 하드웨어 포인터 인증(PAC) 보안 기능을 활성화했는지 체크합니다. 이 옵션이 켜져 있고, CPU 하드웨어가 이 기능을 지원할 때만 코드가 생성됩니다.
  • ptrauth_keys_init_cpu 매크로: 새로 깨어난 세컨더리 CPU 코어의 내부 특수 보안 레지스터에 포인터 인증용 비밀 키(Cryptographic Keys)를 주입하는 매크로입니다. 중간 주소 계산 및 데이터 로드를 위해 x2, x3, x4, x5 레지스터가 매개변수로 사용됩니다.

포인터 인증(Pointer Authentication)이란?

ARM64 아키텍처에서 포인터 인증(PAC)은 메모리 주소(포인터)의 남는 상위 비트에 암호화된 인증 코드(PAC, Pointer Authentication Code)를 심어두는 기술입니다.

  1. 인증 코드 생성: 함수가 호출될 때, CPU는 메모리에 저장될 돌아갈 주소(Return Address)와 시스템 고유의 비밀 키를 조합하여 암호화 코드를 만든 뒤 포인터 상위 비트에 숨겨둡니다.
  2. 검증: 함수가 끝나고 돌아갈 때, CPU는 이 상위 비트의 암호화 코드가 유효한지 다시 검사합니다.
  3. 공격 차단: 만약 해커가 버퍼 오버플로우 공격 등으로 메모리에 있는 리턴 주소를 강제로 위변조했다면, 이 암호화 인증 코드가 깨지게 됩니다. CPU는 이를 즉시 감지하고 시스템을 안전하게 크래시(Fault)시켜 해커가 악성 코드를 실행하지 못하도록 원천 차단합니다.

이 매크로가 실행되면서, 새로 켜진 세컨더리 CPU도 메인 CPU와 마찬가지로 커널 코드를 실행할 때 이 암호화 키를 활용해 스스로를 보호할 수 있게 됩니다.

 

 


403
404         bl      secondary_start_kernel

  • bl (Branch with Link): 이 명령어를 통해 어셈블리 코드의 세계를 뒤로하고, C 언어로 작성된 리눅스 커널의 핵심 함수인 secondary_start_kernel()을 호출합니다.


405         ASM_BUG()

  • ASM_BUG() 라인이 실행되었다는 것은 secondary_start_kernel 함수가 도중에 알 수 없는 치명적인 오류로 튕겨 나옵니다(리턴되었습니다)를 의미합니다. 즉, 커널이 도저히 손쓸 수 없는 심각한 버그 상태이므로 시스템을 즉시 Panic시키는 역할을 합니다.


406 SYM_FUNC_END(__secondary_switched)


 

408 SYM_FUNC_START_LOCAL(__secondary_too_slow)
409         wfe
410         wfi
411         b       __secondary_too_slow

 

  • wfe (Wait For Event): ARM 아키텍처의 대표적인 저전력 가동 중지 명령어입니다.
  • 동작: CPU 코어를 즉시 저전력 스탠바이(휴면) 상태로 진입시킵니다. 이 상태의 CPU는 다른 CPU가 sev(Send Event) 명령어를 날려 이벤트를 발생시키기 전까지는 깨어나지 않고 잠들게 됩니다.
  • wfi (Wait For Interrupt): 또 다른 저전력 가동 중지 명령어입니다.
  • 동작: CPU를 하드웨어 인터럽트(소프트웨어/외부 신호 등)가 발생할 때까지 잠들게 만듭니다.
  • 왜 둘 다 쓰나? wfe나 wfi 중 어떤 이유로든 CPU가 예기치 않게 잠에서 깨어날 가능성이 있습니다. 커널은 이 세컨더리 CPU가 확실하게 아무것도 하지 못하도록 두 겹의 저전력 대기 명령어를 촘촘히 배치해 둔 것입니다.

412 SYM_FUNC_END(__secondary_too_slow)

 

ARM64 세컨더리 CPU 부팅 시퀀스 총정리

[물리 주소 세계 (Physical Address)]
 1. secondary_entry       -> 부팅 모드(EL1/EL2) 확인 및 보존
 2. secondary_startup     -> 52비트 주소 검증, 내부 하드웨어 세팅 (__cpu_setup)
                             페이지 테이블 세팅 후 MMU 활성화 (__enable_mmu)
          │
          ▼ [가상 주소 세계로 점프 (Virtual Address)]
 3. __secondary_switched  -> 예외 처리 벡터 테이블 등록 (vbar_el1)
                             공유 데이터에서 나만의 스택/태스크 정보 할당 (init_cpu_task)
                             하드웨어 포인터 인증 보안 키 설정 (ptrauth)
          │
          ▼ [C 언어 커널 세계로 완전 진입]
 4. secondary_start_kernel -> 타이머/인터럽트 구동, 스케줄러 합류 후 유저 앱 실행!

 

'linux' 카테고리의 다른 글

6.12/smp_setup_processor_id();  (0) 2026.06.17
6.12/set_task_stack_end_magic()  (0) 2026.06.17
6.12/head.S  (0) 2025.06.14
6.1/early_ioremap_init(void)  (0) 2025.03.29
6.1/setup_arch(char **cmdline_p)  (0) 2024.04.02

 41 #if (PAGE_OFFSET & 0x1fffff) != 0
 42 #error PAGE_OFFSET must be at least 2MB aligned
 43 #endif

 

https://www.kernel.org/doc/Documentation/arm64/booting.rst

  • PAGE_OFFSET 이 2MB 정렬이 아니면 오류
  • PMD (Section) 단위로 매핑하기 위해 2MB정렬


 44
 45 /*
 46  * Kernel startup entry point.
 47  * ---------------------------
 48  *
 49  * The requirements are:
 50  *   MMU = off, D-cache = off, I-cache = on or off,
 51  *   x0 = physical address to the FDT blob.
 52  *

MMU = off, D-cache = off, I-cache = on or off

x0 = FDT 의 물리주소

 

시작시 MMU가 off 되어야 하는 이유?

MMU는 CPU가 명령어를 수행할 때 메모리에 접근할 vaddr(Virtual Address)를 paddr(Physical Address)로 변환하는 작업을 수행합니다. 그리고 이때 사용되는 자료구조가 주소 매핑 테이블인데 이는 커널이 생성합니다. Startup 코드가 수행되는 시점에서는 매핑 테이블이 존재하지 않는데 MMU가 on 되어 있다면 메모리 주소가 vaddr로 해석됩니다. 결국 MMU가 수행되고 이는 paddr로 변환되어 의도치 않은 동작이 수행됩니다. 이와 같은 이유로 매핑 테이블이 생성되기 이전에는 MMU를 off 해야합니다.

 

D-Cashe 가 off 되어야 하는 이유?

D-Cache는 CPU와 메인 메모리 사이에서 데이터를 빠르게 접근하기 위한 저장소 역할을 수행합니다. 대부분의 시스템 프로그래머들은 알다시피 종종 캐시와 메모리간 데이터 싱크(Sync) 문제들을 마주하게 되는데 startup 시점에서는 paddr에서 데이터를 직접 읽거나 써야 합니다. 그러나 D-Cache가 on 되어 있다면 paddr에서 데이터를 읽는 대신 캐시 메모리에서 읽으므로 의도치 않은 동작을 수행하게 됩니다. 이와 같은 이유로 D-Cache도 off 해야합니다.

 

https://www.ooseel.net/2021/07/why-mmu-and-d-cache-must-be-off-at-startup-point-in-arm64/


 53  * Note that the callee-saved registers are used for storing variables
 54  * that are useful before the MMU is enabled. The allocations are described
 55  * in the entry routines.
 56  */
 57         __HEAD

95 #define __HEAD .section ".head.text","ax"

 

  • .head.text 섹션 a(allocation) 및 x(execution) 속성으로 시작
  • a (SHF_ALLOC) : 실행시 메모리에 차지한다. (.comment 등 "a" 속성이 없으면 실행시 메모리에 올라 오지 않음)
  • x (SHF_EXECINSTR) : 실행가능한 기계어

https://developer.arm.com/documentation/dui0774/i/armclang-Integrated-Assembler-Directives/Section-directives

https://man7.org/linux/man-pages/man5/elf.5.html


 58         /*
 59          * DO NOT MODIFY. Image header expected by Linux boot-loaders.
 60          */
 61         efi_signature_nop                       // special NOP to identity as PE/COFF executable

 

  • uefi인경우 "MZ" 서명, 아니면 nop

 

 62         b       primary_entry                   // branch to kernel start, magic

 

  • primary_entry 로 분기

 

 63         .quad   0                               // Image load offset from start of RAM, little-endian
 64         le64sym _kernel_size_le                 // Effective size of kernel image, little-endian
 65         le64sym _kernel_flags_le                // Informative flags, little-endian
 66         .quad   0                               // reserved
 67         .quad   0                               // reserved
 68         .quad   0                               // reserved
 69         .ascii  ARM64_IMAGE_MAGIC               // Magic number
 70         .long   .Lpe_header_offset              // Offset to the PE header.
 71
 72         __EFI_PE_HEADER

 

  • uefi 인경우 uefi 헤더위치


 73
 74         .section ".idmap.text","a"

 

  • .idmap.text 섹션 a(allocation) 속성으로 시작


 75

 76         /*
 77          * The following callee saved general purpose registers are used on the
 78          * primary lowlevel boot path:
 79          *
 80          *  Register   Scope                      Purpose
 81          *  x19        primary_entry() .. start_kernel()        whether we entered with the MMU on
 82          *  x20        primary_entry() .. __primary_switch()    CPU boot mode
 83          *  x21        primary_entry() .. start_kernel()        FDT pointer passed at boot in x0
 84          */
 85 SYM_CODE_START(primary_entry)
 86         bl      record_mmu_state


133         __INIT

 

 93 #define __INIT      .section    ".init.text","ax"

 

  • .init.text 섹션 a(allocation) 및 x(execution) 속성으로 시작


134 SYM_CODE_START_LOCAL(record_mmu_state)
135         mrs     x19, CurrentEL
136         cmp     x19, #CurrentEL_EL2
137         mrs     x19, sctlr_el1
138         b.ne    0f
139         mrs     x19, sctlr_el2

 

  • CurrentEL 레지스터를 읽어서 현재 실행 모드가 EL2이면 x19에 sctlr_el2 레지스터 를 EL1이면 sctlr_el1 레지스터을 읽어오기

https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/CurrentEL--Current-Exception-Level

https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/SCTLR-EL1--System-Control-Register--EL1-

 

140 0:


141 CPU_LE( tbnz    x19, #SCTLR_ELx_EE_SHIFT, 1f    )
142 CPU_BE( tbz     x19, #SCTLR_ELx_EE_SHIFT, 1f    )

 

  • CPU_LE : 컴파일시 Little-endian 설정이면 해당라인 컴파일 아니면 무시
  • CPU_BE : 컴파일시 Big-endian 설정이면 해당라인 컴파일 아니면 무시
  • little-endian 으로 컴파일 했는데 실행시 big-endian 이면 1f로 (forward에 있는  label "1" 로 분기, 즉 154라인으로 분기)
  • big-endian 으로 컴파일 했는데 실행시 little-endian 이면 1f로 (forward에 있는  label "1" 로 분기, 즉 154라인으로 분기)



143         tst     x19, #SCTLR_ELx_C               // Z := (C == 0)

 

  • cache 활성화 여부 :
  • 0 = disable ==> Z := 1
  • 1 = enable ==> Z := 0

 

https://developer.arm.com/documentation/111107/2025-09/AArch32-Registers/SCTLR--System-Control-Register

https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/SCTLR-EL1--System-Control-Register--EL1-?lang=en#fieldset_0-25_25


144         and     x19, x19, #SCTLR_ELx_M          // isolate M bit

 

  • MMU 활성화 여부 : 0 = disable, 1 = enable


145         csel    x19, xzr, x19, eq               // clear x19 if Z

 

  • csel Xd, Xn, Xm, <cond>
  • Xd = <cond> ? Xn : Xm
  • xzr : 항상 0값을 갖는 레지스터
  • 144라인의 and 명령은 flag 레지스터를 변경하지 않음, 그래서 143 라인의 tst 명령결과가 eq (Z == 1)인지 판단
  • 즉 cache 가 비활성화 되었으면 x19에 0, 활성화 되있으면  x19 값 변경없이(MMU 활성화 여부 값을 가지고) 리턴


146         ret


147
148         /*
149          * Set the correct endianness early so all memory accesses issued
150          * before init_kernel_el() occur in the correct byte order. Note that
151          * this means the MMU must be disabled, or the active ID map will end
152          * up getting interpreted with the wrong byte order.
153          */
154 1:      eor     x19, x19, #SCTLR_ELx_EE

 

  • 실행 환경의 endian을 컴파일시 설정된 endian으로 변경


155         bic     x19, x19, #SCTLR_ELx_M

 

  • MMU disable


156         b.ne    2f

 

  • 실행 level el1 이면 2 로


157         pre_disable_mmu_workaround
158         msr     sctlr_el2, x19
159         b       3f
160 2:      pre_disable_mmu_workaround
161         msr     sctlr_el1, x19
162 3:      isb
163         mov     x19, xzr
164         ret
165 SYM_CODE_END(record_mmu_state)

 

  • 실행 환경의 endian을 컴파일시 설정된 endian으로 변경 MMU disable 하고 x19에 0을 대입 후 리턴

643 /**
644  * Errata workaround prior to disable MMU. Insert an ISB immediately prior
645  * to executing the MSR that will change SCTLR_ELn[M] from a value of 1 to 0.
646  */
647     .macro pre_disable_mmu_workaround
648 #ifdef CONFIG_QCOM_FALKOR_ERRATUM_E1041
649     isb
650 #endif
651     .endm

 

  • 이전 명령어가 다음명령이 실행되기 전에  각 장치에  반영되게함

 


 

 87         bl      preserve_boot_args

 


167 /*
168  * Preserve the arguments passed by the bootloader in x0 .. x3
169  */
170 SYM_CODE_START_LOCAL(preserve_boot_args)
171         mov     x21, x0                         // x21=FDT
172
173         adr_l   x0, boot_args                   // record the contents of

  • boot_args 의 주소를 x0에

 

<arch/arm64/kernel/setup.c>

 88 u64 __cacheline_aligned boot_args[4];


174         stp     x21, x1, [x0]                   // x0 .. x3 at kernel entry
175         stp     x2, x3, [x0, #16]

 

  • boot_args[] = {x21 = x0, x1, x2, x3};


176
177         cbnz    x19, 0f                         // skip cache invalidation if MMU is on

 

  • MMU 가 enable 이면 0f 로


178         dmb     sy                              // needed before dc ivac with
179                                                 // MMU off
180
181         add     x1, x0, #0x20                   // 4 x 8 bytes
182         b       dcache_inval_poc                // tail call

 

  • data cache 를 invalidate 함


183 0:      str_l   x19, mmu_enabled_at_boot, x0

 

  • mmu enable 상태를 mmu_enabled_at_boot에 저장
  • enable = SCTLR_ELx_M
  • diable = 0


184         ret
185 SYM_CODE_END(preserve_boot_args)



 88
 89         adrp    x1, early_init_stack
 90         mov     sp, x1

 

  • stack pointer 를 early_init_stack으로 설정 (4KB)

<vmlinux.lds.S>

342         . += SZ_4K;             /* stack for the early C runtime */
343         early_init_stack = .;

 


 91         mov     x29, xzr
 92         adrp    x0, __pi_init_idmap_pg_dir
 93         mov     x1, xzr
 94         bl      __pi_create_init_idmap

 

  • "__pi_" 는 /arch/arm64/kernel/pi/Makefile 에서 
  • __pi_ + create_init_idmap = __pi_create_init_idmap 로 함수명을 만듬.
  • 소스 분석은 create_init_idmap로

기존 어셈블리로 작성되었던 부분을 C언어로 작성

현재 MMU off 상태라 모든 주소는 물리 주소임

가상주소와 물리주소를 동일하게 매핑하면  아래 물리주소 1002 에서 가상주소 1003 으로 자연스럽게 넘어감

예)

주소 

1000  물리 주소

1001  물리 주소

1002 MMU on     ; 다음 실행 주소는 1003, 이때 가상주소와 물리 주소를 동일하게 매핑하고 메인루틴의 가상주소할당 후 분기

1003 가상주소   

1004 가상주소

...

2000 b start_kernel ; 실행하고자 하는 메인 루틴으로 분기

 

 91 asmlinkage phys_addr_t __init create_init_idmap(pgd_t *pg_dir, ptdesc_t clrmask)

 

pg_dir   = x0 = __pi_init_idmap_pg_dir 

=> idmap 의 PGD 를 __pi_init_idmap_pg_dir 로 설정

clrmask = x1 = 0

=> clear 시킬 flag 없음


 92 {
 93         phys_addr_t ptep = (phys_addr_t)pg_dir + PAGE_SIZE; /* MMU is off */
 94         pgprot_t text_prot = PAGE_KERNEL_ROX; // Read Only + Execution
 95         pgprot_t data_prot = PAGE_KERNEL;
 96
 97         pgprot_val(text_prot) &= ~clrmask;
 98         pgprot_val(data_prot) &= ~clrmask;
 99
100         /* MMU is off; pointer casts to phys_addr_t are safe */
101         map_range(&ptep, (u64)_stext, (u64)__initdata_begin,
102                   (phys_addr_t)_stext, text_prot, IDMAP_ROOT_LEVEL,
103                   (pte_t *)pg_dir, false, 0);

 

  • 가상주소 (_stext , __initdata_begin] 범위로 하고  _stext 를 물리 주소로 하는 읽기 전용, 실행가능한 주소공간 맵핑
  • 즉, 물리주소와 가상주소가 같은 _stext  부터 __initdata_begin 까지의 주소 공간 맵핑
  • IDMAP_ROOT_LEVEL = 0
  • pg_dir = __pi_init_idmap_pg_dir 

 


104         map_range(&ptep, (u64)__initdata_begin, (u64)_end,
105                   (phys_addr_t)__initdata_begin, data_prot, IDMAP_ROOT_LEVEL,
106                   (pte_t *)pg_dir, false, 0);

 

  • 가상주소 ( __initdata_begin , _end ] 범위로 하고  __initdata_begin 를 물리 주소로 하는읽기 전용 실행불가(data 보관용) 주소공간 확보. 즉, 물리주소와 가상주소가 같은 __initdata_begin 부터 _end  까지의 주소 공간 맵핑


107
108         return ptep;

 

  • _end 까지 매핑한 주소변환테이블의 끝위치 반환


109 }


 14 /**
 15  * map_range - Map a contiguous range of physical pages into virtual memory
 16  *
 17  * @pte:                Address of physical pointer to array of pages to
 18  *                      allocate page tables from
 19  * @start:              Virtual address of the start of the range
 20  * @end:                Virtual address of the end of the range (exclusive)
 21  * @pa:                 Physical address of the start of the range
 22  * @prot:               Access permissions of the range
 23  * @level:              Translation level for the mapping
 24  * @tbl:                The level @level page table to create the mappings in
 25  * @may_use_cont:       Whether the use of the contiguous attribute is allowed
 26  * @va_offset:          Offset between a physical page and its current mapping
 27  *                      in the VA space
 28  */
 29 void __init map_range(phys_addr_t *pte, u64 start, u64 end, phys_addr_t pa,
 30                       pgprot_t prot, int level, pte_t *tbl, bool may_use_cont,
 31                       u64 va_offset)
 32 {
 33         u64 cmask = (level == 3) ? CONT_PTE_SIZE - 1 : U64_MAX;
 34         ptdesc_t protval = pgprot_val(prot) & ~PTE_TYPE_MASK;
 35         int lshift = (3 - level) * PTDESC_TABLE_SHIFT;
 36         u64 lmask = (PAGE_SIZE << lshift) - 1;

 

해당 level 미만의 매핑테이블까지 마스크 값 생성(각 비트 1로 세트)

level ==>         0          1           2           3

start ==>   |  PGD  |  PUD  |  PMD  |  PTE  |   PAGE_SHIFT   |

size ==>         9            9           9           9                 12                     (1page = 4KB기준)

lmask ==>                    0          1            2                  3

 

level    lmask

0 ==> 0x0000 007F FFFF FFFF   (PUD + PMD + PTE + PAGE_MASK)

1  ==> 0x0000 0000 3FFF FFFF   (PMD + PTE + PAGE_MASK)

2  ==> 0x0000 0000 001F FFFF   (PTE + PAGE_MASK)

3  ==> 0x0000 0000 0000 0FFF   (PAGE_MASK)


 37
 38         start   &= PAGE_MASK;
 39         pa      &= PAGE_MASK;

 

start 를 page 단위 시작 주소로 변환

pa 를 page 단위 시작 주소로 변환


 40
 41         /* Advance tbl to the entry that covers start */
 42         tbl += (start >> (lshift + PAGE_SHIFT)) % PTRS_PER_PTE;

 

level ==>         0          1           2           3

start ==>   |  PGD  |  PUD  |  PMD  |  PTE  |   PAGE_SHIFT   |

size ==>         9            9           9           9                 12                     (1page = 4KB기준)

lmask ==>                    0          1            2                  3

 

start가 어떤 주소 맵핑 테이블의 어디에 위치하는지 계산해서 tbl 에 반영


 43
 44         /*
 45          * Set the right block/page bits for this level unless we are
 46          * clearing the mapping
 47          */
 48         if (protval)
 49                 protval |= (level == 2) ? PMD_TYPE_SECT : PTE_TYPE_PAGE;

 

  • PMD level 이면 PTE를 사용하지 않고 PMD에 물리적으로 연속된 2MB 단위(Section)로 매핑 할수있게 설정
  • 아니면 1PAGE 매핑할수 있게 설정


 50

 51         while (start < end) {
 52                 u64 next = min((start | lmask) + 1, PAGE_ALIGN(end));

 

  • (start | lmask) + 1 ==> 해당 매핑테이블의 다음 항목 계산, end의 다음페이지 시작주소를 넘어서면 end의 다음페이지 시작주소


 53

                      if (level < 3 && (start | next | pa) & lmask) {

  •  5.12 버전 원래 코드는 위와 같다. 이는 PGD, PUD 마저 align 되었있지 않으면 하위 변환테이블을 만들지 않기 때문에 아래와 같이 변경되었다.(5.15버전부터)


 54                 if (level < 2 || (level == 2 && (start | next | pa) & lmask)) {

 

  • level < 2 ==> PUD, PMD, PTE를 만들기 위해(다음 level 매핑 테이블을 만들기 위해)
  • level == 2 && .. ==> PMD 이면서 2MB 단위(section)가 아닌경우 PTE를 만들기 위해


 55                         /*
 56                          * This chunk needs a finer grained mapping. Create a
 57                          * table mapping if necessary and recurse.
 58                          */
 59                         if (pte_none(*tbl)) {

 

  • 다음 level 매핑테이블의 주소, 상태값이 없으면


 60                                 *tbl = __pte(__phys_to_pte_val(*pte) |
 61                                              PMD_TYPE_TABLE | PMD_TABLE_UXN);

 

  • *tbl에 인수로 넘어온 pte 즉, 다음 level 매핑테이블용 page 물리 주소와 다음 level 매핑 테이블이 있고(PMD_TYPE_TABLE),
  • 유저영역에서 해당 주소공간을 실행불가(PMD_TABLE_UXN)를 설정
  • PMD_TYPE_TABLE 값은 PGD _TYPE_TABLE , PUD _TYPE_TABLE 값과 동일해서 PGD, PUD 에서도 사용가능


 62                                 *pte += PTRS_PER_PTE * sizeof(pte_t);

 

  • 다음 매핑테이블로 사용할 주소계산 *pte + 1page(4KB)


 63                         }
 64                         map_range(pte, start, next, pa, prot, level + 1,
 65                                   (pte_t *)(__pte_to_phys(*tbl) + va_offset),
 66                                   may_use_cont, va_offset);

 

  • level + 1 => 앞에서 구성된 다음 단계의 매핑 테이블 구성 하기 위해 재귀호출 (PGD -> PUD -> PMD -> PTE)


 67                 } else {
 68                         /*
 69                          * Start a contiguous range if start and pa are
 70                          * suitably aligned
 71                          */
 72                         if (((start | pa) & cmask) == 0 && may_use_cont)
 73                                 protval |= PTE_CONT;

 

  • start와 pa 주소가 CONT_PTE_SIZE 로 align 되어있고, PTE 를 연속적으로 사용할 것이 요청왔으면 PTE_CONT 설정


 74
 75                         /*
 76                          * Clear the contiguous attribute if the remaining
 77                          * range does not cover a contiguous block
 78                          */
 79                         if ((end & ~cmask) <= start)
 80                                 protval &= ~PTE_CONT;

 

  • PTE_CONT flag를 적용할경우 end 를 넘어서면 PTE_CONT flag 제거


 81
 82                         /* Put down a block or page mapping */
 83                         *tbl = __pte(__phys_to_pte_val(pa) | protval);

 

  • 물리주소와 flag 설정값 사용하여 가상주소와 매핑


 84                 }
 85                 pa += next - start;
 86                 start = next;
 87                 tbl++;

 

  • end까지 매핑하기 위해 1단위씩 증가


 88         }
 89 }


 

  • x0 = create_init_idmap 의 return 값 ( _end  까지 매핑한 주소변환테이블의 끝위치 반환)

 

 95
 96         /*
 97          * If the page tables have been populated with non-cacheable
 98          * accesses (MMU disabled), invalidate those tables again to
 99          * remove any speculatively loaded cache lines.
100          */
101         cbnz    x19, 0f

 

  • MMU on 이면 0f 로


102         dmb     sy
103         mov     x1, x0                          // end of used region

 

_end 까지 매핑한 주소변환테이블의 끝위치


104         adrp    x0, __pi_init_idmap_pg_dir
105         adr_l   x2, dcache_inval_poc
106         blr     x2

107         b       1f

 

  • x0 : __pi_init_idmap_pg_dir
  • x1 : _end  까지 매핑한 주소변환테이블의 끝위치
  • dcache 을 invalidate 할 범위 (x0, x1) 로 하여 실행
  • 1f로 가기


108

109         /*
110          * If we entered with the MMU and caches on, clean the ID mapped part
111          * of the primary boot code to the PoC so we can safely execute it with
112          * the MMU off.
113          */
114 0:      adrp    x0, __idmap_text_start
115         adr_l   x1, __idmap_text_end
116         adr_l   x2, dcache_clean_poc
117         blr     x2

 

  • MMU 와 Cache 가 On 되있으면, __idmap_text_start 부터 __idmap_text_end 까지 dcache clean ( = FLUSH) 


118
119 1:      mov     x0, x19

 

  • MMU off 이면 0, on 이면 0 아닌값을 x0에 넣음


120         bl      init_kernel_el                  // w0=cpu_boot_mode

 


 

254         .section ".idmap.text","a"
255
256 /*
257  * Starting from EL2 or EL1, configure the CPU to execute at the highest
258  * reachable EL supported by the kernel in a chosen default state. If dropping
259  * from EL2 to EL1, configure EL2 before configuring EL1.
260  *
261  * Since we cannot always rely on ERET synchronizing writes to sysregs (e.g. if
262  * SCTLR_ELx.EOS is clear), we place an ISB prior to ERET.
263  *
264  * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in x0 if
265  * booted in EL1 or EL2 respectively, with the top 32 bits containing
266  * potential context flags. These flags are *not* stored in __boot_cpu_mode.
267  *
268  * x0: whether we are being called from the primary boot path with the MMU on
269  */
270 SYM_FUNC_START(init_kernel_el)
271         mrs     x1, CurrentEL
272         cmp     x1, #CurrentEL_EL2
273         b.eq    init_el2

 

  • 현재 모드가 el2이면 init_el2로 가기


274
275 SYM_INNER_LABEL(init_el1, SYM_L_LOCAL)
276         mov_q   x0, INIT_SCTLR_EL1_MMU_OFF
277         pre_disable_mmu_workaround
278         msr     sctlr_el1, x0
279         isb

 

  • el1 모드의 MMU off


280         mov_q   x0, INIT_PSTATE_EL1

 

arch/arm64/include/asm/ptrace.h

 19 #define INIT_PSTATE_EL1 \
 20     (PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT | PSR_MODE_EL1h)

 

https://developer.arm.com/documentation/ddi0487/maa/-Part-D-The-AArch64-System-Level-Architecture/-Chapter-D1-The-AArch64-System-Level-Programmers--Model/-D1-5-Process-state--PSTATE/-D1-5-1-PSTATE-fields-that-are-meaningful-in-AArch64-state

https://developer.arm.com/documentation/111107/2026-03/AArch64-Registers/DAIF--Interrupt-Mask-Bits

 

281         msr     spsr_el1, x0
282         msr     elr_el1, lr
283         mov     w0, #BOOT_CPU_MODE_EL1
284         eret

 

  • 인터럽트를 disable 하고 부팅 모드가 el1임을 저장하고 return
  • 여기서 ret가 아닌 eret로 return 한 이유는 spsp_el1에 저장된 인터럽트 disable을 반영하기 위함


285
286 SYM_INNER_LABEL(init_el2, SYM_L_LOCAL)
287         msr     elr_el2, lr

 

  • 복귀 주소 elr_el2 에 보관


288
289         // clean all HYP code to the PoC if we booted at EL2 with the MMU on
290         cbz     x0, 0f

 

  • MMU off 이면 0f로 


291         adrp    x0, __hyp_idmap_text_start
292         adr_l   x1, __hyp_text_end
293         adr_l   x2, dcache_clean_poc
294         blr     x2

 

  • 하이퍼바이저 text 섹션 ( __hyp_idmap_text_start, __hyp_text_end) dcache clean
  • invalid 를 안하고 clean을하는 이유는 dcache 에 남아있는 data가 유효할거라 생각해서..

 

295
296         mov_q   x0, INIT_SCTLR_EL2_MMU_OFF
297         pre_disable_mmu_workaround
298         msr     sctlr_el2, x0
299         isb

 

  • el2에서의 MMU off

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/SCTLR-EL2--System-Control-Register--EL2-


300 0:
301
302         init_el2_hcr    HCR_HOST_NVHE_FLAGS

  • 하이퍼바이저 제어 레지스터(HCR_EL2)를 초기화합니다.
  • HCR_HOST_NVHE_FLAGS는 nVHE(Non-virtualized Host Extensions) 모드로 하이퍼바이저를 설정하겠다는 의미입니다. 이는 호스트 운영체제(Linux)는 EL1에서 실행되고, EL2에는 가상화를 위한 최소한의 기능만 남겨두는 전통적인 방식으로 CPU를 세팅하는 것입니다.


303         init_el2_state

  • el2의  MMU off, 타이머 등을 적절한 값으로 초기화


304
305         /* Hypervisor stub */
306         adr_l   x0, __hyp_stub_vectors
307         msr     vbar_el2, x0
308         isb

 

  • el2의 인터럽트 벡터 테이블을  __hyp_stub_vectors 로 설정 


309
310         mov_q   x1, INIT_SCTLR_EL1_MMU_OFF

  • MMU off 하기위해 x1에 준비


311
312         mrs     x0, hcr_el2
313         and     x0, x0, #HCR_E2H
314         cbz     x0, 2f

 

HCR_EL2.E2H 비트는 이 시스템이 VHE(Virtualization Host Extensions) 모드로 동작 중인지를 나타내는 플래그입니다.

  • 만약 이 비트가 1이면: VHE 모드 (호스트 커널이 EL2에서 직접 실행됨)
  • 만약 이 비트가 0이면: nVHE 모드 (전통적인 방식, 호스트 커널은 EL1에서 실행됨)

0이면 2f로 이동


315
316         /* Set a sane SCTLR_EL1, the VHE way */
317         msr_s   SYS_SCTLR_EL12, x1

 

  • 위 310라인에서 MMU off 로 설정한 값을 현재 VHE 모드이므로 sctlr_el1에 반영

https://developer.arm.com/documentation/ddi0602/2026-03/Base-Instructions/MSR--register---Move-general-purpose-register-to-System-register-?lang=en#MRS_values

 

 

arch/arm64/include/asm/sysreg.h

1136     .macro  mrs_s, rt, sreg
1137      __emit_inst(0xd5200000|(\sreg)|(.L__gpr_num_\rt))
1138     .endm
1139
1140     .macro  msr_s, sreg, rt
1141     __emit_inst(0xd5000000|(\sreg)|(.L__gpr_num_\rt))
1142     .endm

 

  58 #define __emit_inst(x)          .inst(x)

  • .inst 는 GAS 문법에서 x 는 기계어이므로 x 값 그대로 위치에 반영

 


318         mov     x2, #BOOT_CPU_FLAG_E2H
319         b       3f

 

  • 현재 이 CPU가 VHE(E2H) 모드로 부팅되었음을 나타내는 플래그 플래그(BOOT_CPU_FLAG_E2H)를 x2 레지스터에 기록
  • 3f로 가기


320
321 2:

  • VHE 비트가 0일 때 즉 nVHE 인경우


322         msr     sctlr_el1, x1

  • MMU off 설정값(x1)을 실제 sctlr_el1에 반영


323         mov     x2, xzr

  • VHE 가 아님을 나타내기위해 x2를 초기화


324 3:
325         mov     x0, #INIT_PSTATE_EL1
326         msr     spsr_el2, x0

 

  • 권한 레벨(EL)을 EL1으로 세팅 (PSR_MODE_EL1h)
  • 커널이 처음 실행될 때 방해받지 않도록 모든 인터럽트(DAIF 비트)를 일시적으로 전부 차단(Mask) 
  • 전용 스택 포인터(SP_EL1)를 사용 (PSR_MODE_EL1h)

https://developer.arm.com/documentation/dui0801/l/Overview-of-AArch64-state/Stack-Pointer-register?lang=en

  • 하기위해 spsr_el2에  값을 세팅


327
328         mov     w0, #BOOT_CPU_MODE_EL2

  • 부팅 모드가 EL2임을 저장


329         orr     x0, x0, x2

  • x0 = (x0 = 부팅모드 or x2 = VHE 여부) 를 넘겨주기 위해


330         eret

  • ret 가 아니 eret를 사용하여 spsr_el2에 적용된 인터럽트 금지 및 EL1으로의 모드전환을 발생시킴


331 SYM_FUNC_END(init_kernel_el)


 

121         mov     x20, x0

  • x20에 부팅모드(EL1인지 EL2인지)와 VHE 여부를 값을 보관


122
123         /*
124          * The following calls CPU setup code, see arch/arm64/mm/proc.S for
125          * details.
126          * On return, the CPU will be ready for the MMU to be turned on and
127          * the TCR will have been set.
128          */
129         bl      __cpu_setup                     // initialise processor


456 /*
457  *      __cpu_setup
458  *
459  *      Initialise the processor for turning the MMU on.
460  *
461  * Output:
462  *      Return in x0 the value of the SCTLR_EL1 register.
463  */
464         .pushsection ".idmap.text", "a"

  • .idmap.text 섹션에 위치


465 SYM_FUNC_START(__cpu_setup)
466         tlbi    vmalle1                         // Invalidate local TLB
467         dsb     nsh

  • el1의 가상주소 전체에 대한 tlb를 invalid 하여 초기화 되었음을 보장하고 완료될때까지 기다림

 

468
469         msr     cpacr_el1, xzr                  // Reset cpacr_el1

https://developer.arm.com/documentation/ddi0601/2022-03/AArch64-Registers/CPACR-EL1--Architectural-Feature-Access-Control-Register?lang=en

  • 부동소수점(FP), SIMD(NEON), 벡터 연산(SVE) 접근 금지

 

470         mov     x1, MDSCR_EL1_TDCC              // Reset mdscr_el1 and disable
471         msr     mdscr_el1, x1                   // access to the DCC from EL0

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/MDSCR-EL1--Monitor-Debug-System-Control-Register

  • MDSCR_EL1은 하드웨어 모니터링 및 디버깅 시스템을 조작하는 레지스터입니다. 여기에 TDCC 비트를 설정하면,  EL0 공간의 프로그램들이 데이터 통신 채널(DCC)을 통해 디버그 하드웨어에 접근하는 것을  트랩합니다. 부팅 도중 유저 공간 인터페이스를 통해 시스템 디버거가 해킹당하는 것을 막기 위한 것입니다.

 


472         reset_pmuserenr_el0 x1                  // Disable PMU access from EL0

https://developer.arm.com/documentation/ddi0601/2021-12/AArch64-Registers/PMUSERENR-EL0--Performance-Monitors-User-Enable-Register

  • CPU 내부에 탑재된 성능 모니터링 유닛(PMU)은 캐시 미스 횟수, 실행된 명령어 개수 등을 정밀하게 측정합니다.
  • 이런 자원은 EL1 즉 커널에서 처리하고 승인시 조회할수있게 하는 것이 보안에 안전할거같습니다.
  • 따라서 이를 위해 유저 공간의 PMU 접근 권한 레지스터(PMUSERENR_EL0)를 0으로 강제 리셋하여 금지하는 매크로입니다.

 

473         reset_amuserenr_el0 x1                  // Disable AMU access from EL0

https://developer.arm.com/documentation/ddi0601/2023-09/AArch32-Registers/AMUSERENR--Activity-Monitors-User-Enable-Register

  • PMU와 비슷하게 CPU의 실시간 주파수, 사이클 카운트, 전력 소모 상태 등을 모니터링하는 활동 모니터링 유닛(AMU)이 있습니다.
  • 이 역시 보안상 커널에서 처리하고 승인시 조횔할수있게 하는것이 안전할거같습니다.
  • 따라서 이를 위해 유저 사용 권한(AMUSERENR_EL0)을 완전히 박탈하여 시스템을 보호합니다.


474
475         /*
476          * Default values for VMSA control registers. These will be adjusted
477          * below depending on detected CPU features.
478          */
479         mair    .req    x17
480         tcr     .req    x16
481         tcr2    .req    x15

  • mair = x17, tcr = x16, tcr2 = x15 라고 alias


482         mov_q   mair, MAIR_EL1_SET

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/MAIR-EL1--Memory-Attribute-Indirection-Register--EL1-

  • 커널이 정의한 MAIR_EL1_SET 상수를 mair(x17) 레지스터에 채워 넣습니다. MAIR(Memory Attribute Indirection Register)은 페이지 변환 테이블이 사용할 페이지 속성을 나타내는 값을 보관.
  • 즉  "장치 제어용 메모리(Device)"로 쓸지, " 일반 메모리(Normal)"로 쓸지, 캐시 정책은 어떻게 할지 등의 속성(Attribute) 테이블을 정의하는 레지스터입니다.


483         mov_q   tcr, TCR_T0SZ(IDMAP_VA_BITS) | TCR_T1SZ(VA_BITS_MIN) | TCR_CACHE_FLAGS | \
484                      TCR_SHARED | TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \
485                      TCR_TBI0 | TCR_A1 | TCR_KASAN_SW_FLAGS | TCR_MTE_FLAGS

TCR ( Translation Control Register ) 

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/TCR-EL1--Translation-Control-Register--EL1-?lang=en

https://docs.kernel.org/arch/arm64/memory-tagging-extension.html

  • TCR_T0SZ( T0SZ  ): TTBR0 영역(주로 유저 공간 및 초기 매핑용)의 가상 주소 크기를 설정합니다.
  • TCR_T1SZ( T1SZ  ): TTBR1 영역(커널 공간)의 가상 주소 크기를 설정합니다.
  • 실제 가상 주소 비트 수는 64 - TxSZ  공식으로 계산됩니다. 즉, TxSZ 값이 클수록 가상 주소 공간의 크기는 작아집니다.
  • TCR_CACHE_FLAGS: 페이지 테이블을 탐색할 때 CPU 캐시(일반적으로 캐시 내부/외부 모두 켜짐 상태인 Outer/Inner Write-Back, Write-Allocate)를 거쳐서 탐색하도록 설정합니다.
  • TCR_SHARED: 페이지 테이블이 위치한 메모리 영역을 'Inner Shareable'(코어 간 공유 가능) 영역으로 지정합니다. 멀티코어 환경에서 다른 CPU 코어가 페이지 테이블을 변경했을 때 하드웨어적으로 데이터 일관성(Coherency)을 유지하기 위해 필수적입니다. 만약 이 플래그를 설정하지 않으면 Non-shareable 이 되서 페이지 변환테이블 작업시 캐쉬 프로토콜이 작동한지 않아서 일관성이 유지 되지 않음.
  • TCR_TG_FLAGS: Translation Granule(페이지 하나의 크기)을 결정합니다. 커널 설정에 따라 4KB, 16KB, 64KB 중 어떤 크기의 기본 페이지 단위를 사용할지 하드웨어 MMU에 알려주는 역할을 합니다.
  • TCR_KASLR_FLAGS: Kernel Address Space Layout Randomization을 지원하기 위한 플래그입니다. 부팅할 때마다 커널 코드가 배치되는 가상 주소를 무작위로 바꾸어, 해커가 특정 함수(예: 취약점 공격용 ROP 가젯)의 고정 주소를 타겟팅하지 못하도록 방어합니다.
  • TCR_ASID16: Address Space Identifier를 16비트(65,536개) 크기로 사용하겠다고 선언합니다. ASID는 각 프로세스(유저 공간)마다 부여되는 고유 ID입니다. 이것이 있으면 컨텍스트 스위칭(프로세스 전환)이 일어날 때마다 무겁게 CPU의 TLB(주소 변환 캐시)를 매번 전부 비우지(Flush) 않아도 되므로 성능이 크게 향상됩니다.
  • TCR_TBI0: Top Byte Ignore 기능을 TTBR0(유저 공간) 주소에 활성화합니다. 이 기능이 켜지면, 64비트 가상 주소 포인터 중 최상위 8비트(bit[63:56])에 어떤 값이 들어있든 MMU는 이를 주소 해석에 쓰지 않고 무시합니다. 덕분에 소프트웨어는 이 상위 8비트에 메타데이터나 태그를 심어둘 수 있습니다. (아래 KASAN, MTE 기술의 기반이 됩니다.)
  • MSB로 유저공간(TTBR0)과 커널 영역 (TTBR1) 주소인지를 구별했으나 63~56 bit를 사용하므로 55 bit로 유저공간인지 커널 공간인지를 구분함, 현재 가상주소 최대 크기는 52bit 임
  • TCR_A1: ASID 보관 위치를 TTBR1(커널)이 아닌 TTBR0(유저) 레지스터 기준으로 정렬하도록 지시합니다.
  • 이 ASID 번호 데이터는 원래 TTBR0과 TTBR1의 상위 비트[63:48]에 둘 다 각각 저장되어 있습니다
  • 하드웨어 MMU는 주소 변환 결과를 TLB에 기록할 때, TTBR0와 TTBR1 양쪽에 적힌 ASID 중 하나만 선택해서 기준으로 삼아야 합니다. 이때 기준점을 잡아주는 것이 TCR_A1 비트입니다. (당연히 TTBR0를 기준으로 해야지)
  • TCR_KASLR_FLAGS: Kernel Address Space Layout Randomization을 지원하기 위한 플래그입니다. 부팅할 때마다 커널 코드가 배치되는 가상 주소를 무작위로 바꾸어, 해커가 특정 함수(예: 취약점 공격용 ROP 가젯)의 고정 주소를 타겟팅하지 못하도록 방어합니다.
  • TCR_MTE_FLAGS: ARMv8.5부터 도입된 MTE (Memory Tagging Extension) 하드웨어 기능을 활성화합니다. KASAN이 소프트웨어적으로 태그를 검사했다면, MTE는 주소의 태그와 실제 물리 메모리에 기록된 할당 태그가 일치하는지를 CPU 하드웨어 수준에서 초고속으로 비교하여 메모리 오염 공격을 원천 차단합니다.

 

  • 위의 플래그를 설정하여 tcr(x16)에 저장


486         mov     tcr2, xzr

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/TCR2-EL1--Extended-Translation-Control-Register--EL1-

  • tcr2는 tcr의 기능확장버전으로 페이지 변환 테이블 엔트리 한 칸의 크기를 기존 64비트에서 128비트로 늘리는 기능등 이 있다.

여기서는 사용하지 않기 위해 0으로 초기화

 

487
488         tcr_clear_errata_bits tcr, x9, x5

특정 CPU의 오류 우회 작업


489

490 #ifdef CONFIG_ARM64_VA_BITS_52
491         mov             x9, #64 - VA_BITS
492 alternative_if ARM64_HAS_VA52
493         tcr_set_t1sz    tcr, x9
494 #ifdef CONFIG_ARM64_LPA2
495         orr             tcr, tcr, #TCR_DS
496 #endif
497 alternative_else_nop_endif
498 #endif

 

  • 커널을 빌드할 때 52비트 가상 주소 공간을 사용하도록 설정했고, CPU가 가상주소 52비트를 지원하면 tcr 레지스터에 가상 주소 52비트를 사용할수있게 설정합니다.
  • CONFIG_ARM64_LPA2는 4KB 또는 16KB 크기의 메모리 페이지 환경에서 최대 52비트의 대용량 물리 주소 범위 및 페이지 테이블 포맷(LPA2)을 사용할 것인지 결정하는 빌드 옵션입니다.
  • 이 옵션이 켜져 있으면 orr(Bitwise OR) 연산자를 사용하여, 기존 tcr 레지스터 값에 TCR_DS 비트 플래그를 추가합니다. TCR_DS (Data Size) 비트가 1로 세팅되어야 하드웨어 MMU가 52비트 크기의 거대한 물리 메모리 주소(PA)를 다룰 수 있는 확장 모드로 진입합니다.
  • 만약 CPU가 가상주소 52비트를 지원하지 않는다면 nop 으로 덮어씁니다.
  • (실제로는 먼저 nop으로 덮어쓰여저있고 부팅 cpu가 부팅을 어느정도완료하고 세컨드 cpu 가 부팅하기전 가상주소 52비트지원이 확인되면 nop부분을 가상주소 52비트처리하는 코드로 덮어씁니다.)

 

499
500         /*
501          * Set the IPS bits in TCR_EL1.
502          */
503         tcr_compute_pa_size tcr, #TCR_IPS_SHIFT, x5, x6

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/ID-AA64MMFR0-EL1--AArch64-Memory-Model-Feature-Register-0

http://jake.dothome.co.kr/registers64/

물리주소 크기를 ID_AA64MMFR0_EL1에서 읽어 TCR.IPS에 설정

 


504 #ifdef CONFIG_ARM64_HW_AFDBM

 

  • CONFIG_ARM64_HW_AFDBM은 하드웨어 주소 접근 플래그 및 더티 비트 관리(Hardware Access Flag and Dirty Bit Management) 기능을 커널에서 사용할 것인지 묻는 빌드 옵션입니다.


505         /*
506          * Enable hardware update of the Access Flags bit.
507          * Hardware dirty bit management is enabled later,
508          * via capabilities.
509          */
510         mrs     x9, ID_AA64MMFR1_EL1

https://developer.arm.com/documentation/ddi0601/2026-03/AArch64-Registers/ID-AA64MMFR1-EL1--AArch64-Memory-Model-Feature-Register-1

http://jake.dothome.co.kr/registers64/

  • ID_AA64MMFR1_EL1  : AArch64 상태에서 구현된 메모리 모델 및 메모리 관리 지원에 대한 정보를 제공합니다.

 

511         ubfx    x9, x9, ID_AA64MMFR1_EL1_HAFDBS_SHIFT, #4

  • ubfx(Unsigned Bit Field Extract) 명령어를 사용해 x9에 저장된 ID_AA64MMFR1_EL1 레지스터 값 중 하드웨어 AF/DB 지원 여부를 나타내는 HAFDBS 필드(4비트 크기)만 쏙 뽑아냅니다.


512         cbz     x9, 1f

  • cbz(Compare and Branch on Zero) 명령어로 추출한 값이 0인지 검사합니다.
  • 만약 0이라면 이 CPU는 하드웨어 AF/DB 기능을 지원하지 않는 구형 CPU이므로, 아래 설정을 건너뛰고 라벨 1f (519라인)로 점프합니다.


513         orr     tcr, tcr, #TCR_HA               // hardware Access flag update

  • CPU가 하드웨어 AF를 지원함이 확인되었으므로, orr 명령어로 tcr 값에 TCR_HA (Hardware Access Flag update) 비트 플래그를 활성화합니다.
  • 이 비트가 켜지면 커널이 일일이 개입하지 않아도, 메모리에 접근할 때마다 페이지 테이블의 Access Flag(AF) 비트를 알아서 1로 세팅해 줍니다. 이는 OS의 페이지 교체 알고리즘(LRU 등) 성능을 크게 향상시킵니다.

 


514 #ifdef CONFIG_ARM64_HAFT
515         cmp     x9, ID_AA64MMFR1_EL1_HAFDBS_HAFT
516         b.lt    1f
517         orr     tcr2, tcr2, TCR2_EL1_HAFT
518 #endif /* CONFIG_ARM64_HAFT */

519 1:
520 #endif  /* CONFIG_ARM64_HW_AFDBM */

https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/TCR-EL2--Translation-Control-Register--EL2-?lang=en#fieldset_0-21_21-1

  • HAFT (Hardware managed Access Flag for Table Descriptors) 기능 지원 여부를 확인합니다.
  • 앞서 x9에 추출해 둔 하드웨어 지원 레벨 값을 ID_AA64MMFR1_EL1_HAFDBS_HAFT (0b0011)상수와 비교(cmp)합니다.
  • 만약 CPU의 하드웨어가 완벽히 HAFT 를 지원하지 않으면 1f 라벨로 이동합니다. 
  • 아니면 tcr2 에 TCR2_EL1_HAFT값을 사용하여 tcr2.HA, tcr2.HD비트 플래그를 설정합니다.

 

 

521         msr     mair_el1, mair

  • Move to System Register. 앞서 482번 라인 등에서 mair(x17 레지스터의 별칭)에 임시로 빌드해 두었던 메모리 속성(Memory Attribute) 마스크 값을 실제 CPU 하드웨어의 MAIR_EL1 (Memory Attribute Indirection Register, EL1) 레지스터에 직접 덮어씁니다.
  • 이 레지스터에 값이 입력되는 순간부터 CPU는 각 페이지 테이블 descriptor가 가리키는 메모리 영역이 일반 RAM(Normal Memory)인지, 캐시를 쓰지 않는 디바이스 메모리(Device Memory)인지 등의 물리적 특성을 식별할 수 있는 기준(Index)을 가지게 됩니다.


522         msr     tcr_el1, tcr

  • 준비된 가상 메모리 시스템 제어 데이터인 tcr(x16 레지스터의 별칭)의 최종 값을 실제 하드웨어 제어 레지스터인 TCR_EL1 (Translation Control Register, EL1)에 직접 덮어씁니다. 
  • 가상주소 공간의 크기(VA_BITS), 페이지 크기(4KB/16KB/64KB Granule), 캐시 공유 정책, 앞서 설정한 하드웨어 Access Flag(AF) 자동 갱신 여부 등의 모든 MMU 하드웨어 동작 매커니즘이 이 순간 CPU에 최종 주입됩니다.
  • (단, MMU가 켜지기(SCTLR_EL1.M 비트 세팅) 전까지는 실제 주소 변환이 시작되지는 않습니다.)


523
524         mrs_s   x1, SYS_ID_AA64MMFR3_EL1
525         ubfx    x1, x1, #ID_AA64MMFR3_EL1_S1PIE_SHIFT, #4
526         cbz     x1, .Lskip_indirection

https://developer.arm.com/documentation/ddi0601/2023-06/AArch64-Registers/ID-AA64MMFR3-EL1--AArch64-Memory-Model-Feature-Register-3

  • 일반적인 시스템 레지스터는 mrs 명령어로 읽지만, ID_AA64MMFR3_EL1 레지스터는 ARM 아키텍처 규격상 비교적 최근에 추가된 레지스터이기 때문에 구형 어셈블러 컴파일러가 인식하지 못할 수 있습니다.
  • 이를 방지하기 위해 커널은 매크로 형태의 특수 명령어인 mrs_s를 사용하여 해당 레지스터의 주소(Opcode)를 직접 지정하여 값을 x1 레지스터로 읽어옵니다.
  • 이 레지스터에는 CPU가 지원하는 최신 메모리 모델 기능들(S1PIE, S2PIE 등)에 대한 정보가 비트 단위로 저장되어 있습니다.
  • Unsigned Bit Field Extract (부호 없는 비트 필드 추출). x1에 담긴 시스템 레지스터 값 중에서 S1PIE 기능의 지원 여부를 나타내는 4비트 크기의 필드만 정확히 추출해 냅니다.
  • Compare and Branch on Zero. 만약 ubfx로 추출한 x1의 값이 0이라면(즉, 현재 CPU가 S1PIE 기능을 지원하지 않는 구형 칩셋이라면), 아래에 이어질 권한 설정 코드들을 실행하지 않고 앞(Forward)에 있는 로컬 라벨인 .Lskip_indirection으로 곧장 점프합니다.

 

S1PIE란 무엇인가?

기존 ARM64의 페이지 테이블 descriptor(PTE) 내에는 이 페이지가 읽기 전용인지, 쓰기가 가능한지, 유저 공간용인지 등을 결정하는 고정된 권한 비트(AP, UXN, PXN 등)가 있었습니다. 하지만 운영체제가 고도화되면서 더 세부적이고 유연한 메모리 권한 제어(예: 실행 전용 페이지 보호, 커널 공간 방어 등)가 필요해졌습니다.

  • 간접 참조(Indirection): S1PIE 기능이 활성화되면 CPU는 PTE의 권한 비트를 직접 해석하지 않고, 이를 하나의 '인덱스(Index)'로 취급합니다.
  • CPU는 이 인덱스를 바탕으로 별도의 권한 제어 레지스터(PIRE0_EL1, PIRx_EL1)를 참조하여 실제 메모리 권한을 동적이고 안전하게 결정하게 됩니다.
  • 이 기능을 쓰면 커널이 메모리 보호 정책을 바꿀 때 수많은 페이지 테이블을 일일이 순회하며 비트를 수정할 필요 없이, 시스템 레지스터의 매핑 테이블 값만 바꾸면 되므로 보안성과 성능이 대폭 향상됩니다.

https://developer.arm.com/documentation/111107/2026-03/AArch64-Registers/PIRE0-EL1--Permission-Indirection-Register-0--EL1-

 

 


527
528         mov_q   x0, PIE_E0_ASM
529         msr     REG_PIRE0_EL1, x0

  • PIE_E0_ASM은 유저 공간에서 사용될 가상 메모리 인덱스별 권한 속성(예: 유저 공간 읽기/쓰기 허용, 유저 공간 실행 불가 등)을  정의해 둔 비트맵입니다.
  • 이 값을 PIRE0_EL1 (Permission Indirection Register EL0, Exception Level 1) 시스템 레지스터에 쓰면, 이제 CPU MMU는 유저 공간(EL0)의 애플리케이션 주소를 변환할 때, 이 레지스터에 정의된 권한 매핑 테이블을 기준으로 메모리 접근을 통제(Access Control)하게 됩니다.

 

530         mov_q   x0, PIE_E1_ASM
531         msr     REG_PIR_EL1, x0

  • PIE_E1_ASM은 커널 공간에서 사용할 인덱스별 메모리 권한 정책을 담고 있습니다. 커널 공간은 유저 공간보다 훨씬 엄격한 보안 규칙(예: 커널 코드 영역은 절대 쓰기 불가, 커널 데이터 영역은 절대 실행 불가 등)이 적용된 비트맵입니다. 
  • 이 값을 PIR_EL1 (Permission Indirection Register, Exception Level 1) 시스템 레지스터에 쓰면 이제 커널 공간(EL1) 내부에서 실행되는 모든 메모리 접근은 이 레지스터의 매핑 테이블을 거쳐 권한 유효성이 검사됩니다.

 

532
533         orr     tcr2, tcr2, TCR2_EL1_PIE

  • TCR2_EL1_PIE 비트는 앞으로 주소 변환 할 때, 기존 방식처럼 페이지 테이블의 권한 비트를 직접 해석하지 말고, PIR_EL1, PIRE0_EL1 에 등록한 권한 간접 참조 매핑 테이블을 거쳐서 메모리 접근 권한을 판단해라.


534
535 .Lskip_indirection:

S1PIE 기능을 지원하지 않는 CPU일 때


536
537         mrs_s   x1, SYS_ID_AA64MMFR3_EL1
538         ubfx    x1, x1, #ID_AA64MMFR3_EL1_TCRX_SHIFT, #4
539         cbz     x1, 1f
540         msr     REG_TCR2_EL1, tcr2

  • 구형 어셈블러와의 호환성을 위해 mrs_s 매크로 명령어를 사용하여 최신 하드웨어 기능 제어 정보가 담긴 ID_AA64MMFR3_EL1 시스템 레지스터의 값을 x1로 다시 읽어옵니다.
  • Unsigned Bit Field Extract. 읽어온 x1 값 중에서 이번에는 TCRX 기능의 하드웨어 지원 여부를 나타내는 4비트 크기의 필드만 정확히 추출합니다.
  • 추출된 값이 0이 아니라면, 이 CPU가 기본 TCR_EL1 레지스터 외에 추가적인 확장 기능들을 제어할 수 있는 보조 레지스터(TCR2_EL1) 를 사용할 수 있다는 의미입니다.
  • Compare and Branch on Zero. 만약 추출한 x1 값이 0이라면(즉, 현재 CPU가 TCRX 확장 사양을 지원하지 않는 구형 칩셋이라면), 1f로 점프하여 빠져나갑니다.
  • Move to System Register. 하드웨어의 TCRX 지원이 확인되었으므로, 지금까지 열심히 빌드해 온 tcr2(x15 레지스터의 별칭)의 최종 설정 값을 실제 CPU의 하드웨어 제어 레지스터인 TCR2_EL1 (Translation Control Register 2, EL1)에 직접 써서 주입합니다.

 


541 1:
542
543         /*
544          * Prepare SCTLR
545          */

https://developer.arm.com/documentation/ddi0595/2021-06/AArch64-Registers/SCTLR-EL1--System-Control-Register--EL1-

http://jake.dothome.co.kr/registers64/

  • SCTLR은 System Control Register (시스템 제어 레거스터)의 약자입니다. 이 주석은 MMU를 켜고 끄는 것을 포함하여, CPU의 가장 원초적이고 핵심적인 하드웨어 기능들을 제어하는 최상위 스위치(SCTLR_EL1)에 주입할 값을 최종 준비하겠다는 선언입니다.


546         mov_q   x0, INIT_SCTLR_EL1_MMU_ON

 

  • INIT_SCTLR_EL1_MMU_ON은 리눅스 커널 헤더(arch/arm64/include/asm/sysreg.h)에 정의된 매우 중요한 64비트 상수 마스크입니다. 이 상수에는 MMU를 활성화하는 SCTLR_EL1.M (비트 0) 플래그를 포함하여, 캐시 활성화, 명령어 정렬 검사 등 CPU가 정상적인 운영체제 모드로 진입하기 위해 켜져야 하는 수많은 제어 비트들이 조합되어 있습니다.
  • x0 레지스터의 중요성: ARM64 아키텍처의 함수 호출 규약(Calling Convention)에 따라, 함수의 리턴 값(반환 값)은 항상 x0 레지스터에 담겨야 합니다. 이 함수의 맨 처음 주석에 적혀 있던 "Output: Return in x0 the value of the SCTLR_EL1 register." 규격을 정확히 만족시키는 순간입니다.

547         ret                                     // return to head.S
548
549         .unreq  mair
550         .unreq  tcr
551         .unreq  tcr2

  • 가독성을 위해 x17, x16, x15 범용 레지스터에 각각 부여했던 mair, tcr, tcr2라는 별칭(Alias)을 해제하는 어셈블러 지시자입니다.


552 SYM_FUNC_END(__cpu_setup)

 



130         b       __primary_switch

131 SYM_CODE_END(primary_entry)


508 SYM_FUNC_START_LOCAL(__primary_switch)
509         adrp    x1, reserved_pg_dir

  • adrp (Address of Page): 현재 PC(Program Counter)를 기준으로 상대적인 4KB 페이지 정렬 주소를 계산하여 레지스터에 로드하는 ARM64 특유의 고속 주소 로딩 명령어입니다. MMU가 켜지기 전이므로 이 명령어로 가져오는 주소는 물리 주소(Physical Address)입니다.
  • reserved_pg_dir: 커널 빌드 시 정적으로 할당된 비어 있는 임시 페이지 디렉터리(페이지 테이블) 공간입니다. MMU를 켜는 과도기 동안 혹시나 발생할 수 있는 잘못된 메모리 접근(Fault)을 안전하게 격리하거나, 초기화 과정에서 임시 버퍼 역할을 하기 위해 준비된 0으로 채워진 메모리 영역입니다. 이 주소가 x1 레지스터에 저장됩니다.
 


510         adrp    x2, __pi_init_idmap_pg_dir

  • __pi_init_idmap_pg_dir: 부팅 시 가장 중요한 Identity Mapping 페이지 테이블의 물리 주소입니다.
  • Identity Mapping이란? 가상 주소와 물리 주소를 1:1로 똑같이 매핑하는 것을 말합니다 (예: 가상 주소 0x80000000 -> 물리 주소 0x80000000).
  • MMU가 켜지는 바로 그 순간, CPU는 가상 주소로 메모리를 바라보기 시작합니다. 만약 현재 실행 중인 코드 영역이 1:1 매핑되어 있지 않다면, MMU가 켜지자마자 다음 명령어를 엉뚱한 가상 주소에서 찾게 되어 CPU가 Panic합니다. 이를 방지하기 위한 필수 테이블 주소를 x2에 담는 것입니다.

 


511         bl      __enable_mmu

 

  • bl (Branch with Link): 함수 호출 명령입니다. 다음 실행할 명령어 주소(리턴 링크)를 x30 (LR 레지스터)에 저장한 후, __enable_mmu 함수로 점프합니다.
  •  __enable_mmu 함수로 진입할 때, 앞서 __cpu_setup이 반환했던 SCTLR_EL1 설정 값은 여전히 x0에 들어있고, 임시 테이블들은 x1, x2에 담겨 넘어가게 됩니다. __enable_mmu 안에서 마침내 msr sctlr_el1, x0가 실행되며 MMU의 스위치가 켜집니다.

 

 


445 /*
446  * Enable the MMU.
447  *
448  *  x0  = SCTLR_EL1 value for turning on the MMU.
449  *  x1  = TTBR1_EL1 value
450  *  x2  = ID map root table address
451  *
452  * Returns to the caller via x30/lr. This requires the caller to be covered
453  * by the .idmap.text section.
454  *
455  * Checks if the selected granule size is supported by the CPU.
456  * If it isn't, park the CPU
457  */
458         .section ".idmap.text","a"

  • 이 함수 역시 .idmap.text 섹션에 배치됩니다. MMU를 켜는 명령어가 실행되는 그 순간에는 가상 주소와 물리 주소가 완벽히 일치하는 공간(Identity Mapping)에 코드가 머물러 있어야만 CPU가 크래시 나지 않고 다음 명령어를 실행할 수 있기 때문입니다.

 


459 SYM_FUNC_START(__enable_mmu)
460         mrs     x3, ID_AA64MMFR0_EL1
461         ubfx    x3, x3, #ID_AA64MMFR0_EL1_TGRAN_SHIFT, 4

  • CPU의 메모리 지원 사양을 담은 ID_AA64MMFR0_EL1 시스템 레지스터를 읽어와, 커널이 빌드될 때 선택한 페이지 크기(Translation Granule, 예: 4KB, 16KB, 64KB)를 하드웨어가 물리적으로 지원하는지 나타내는 4비트 필드를 ubfx 명령어로 추출합니다.


462         cmp     x3, #ID_AA64MMFR0_EL1_TGRAN_SUPPORTED_MIN
463         b.lt    __no_granule_support
464         cmp     x3, #ID_AA64MMFR0_EL1_TGRAN_SUPPORTED_MAX
465         b.gt    __no_granule_support

  • 현재 CPU의 지원 범위가 커널이 요구하는 최소 규격(MIN)보다 작거나, 최대 규격(MAX)보다 크다면 하드웨어 불일치 오류로 판단합니다.
  • 이 경우 조건 분기문(b.lt, b.gt)을 통해 CPU를 무한 루프에 가두어 안전하게 멈추는 __no_granule_support 라벨로 점프합니다.

 

 

466         phys_to_ttbr x2, x2
467         msr     ttbr0_el1, x2                   // load TTBR0

 

  • x2에 담겨 있던 Identity Map 루트 테이블의 물리 주소를 TTBR 레지스터 포맷에 맞게 변환(phys_to_ttbr)한 후, 하드웨어 레지스터인 TTBR0_EL1 에 설정합니다.
  • 이로써 MMU가 켜졌을 때 현재 실행 중인 코드가 있는 물리 주소 영역을 안전하게 1:1로 가상 주소에 매핑할 준비가 끝납니다.


468         load_ttbr1 x1, x1, x3

  • x1 레지스터에 저장되어 있던 커널 메모리 테이블 주소를 TTBR1_EL1 레지스터에 로드하는 매크로입니다.

 


469
470         set_sctlr_el1   x0

 

  • __cpu_setup이 반환하여 x0에 저장되어 있던 INIT_SCTLR_EL1_MMU_ON 값을 바탕으로 실제 시스템 제어 레지스터(SCTLR_EL1)를 업데이트하는 매크로입니다.
  • 이 매크로 내부에서 msr sctlr_el1, x0 및 isb가 실행되는 순간, 하드웨어 MMU가 마침내 On 상태로 전환됩니다. 이 라인 직후부터 CPU는 모든 주소를 가상 주소로 해석하기 시작합니다.

 

471
472         ret
473 SYM_FUNC_END(__enable_mmu)

 

 

 

498 SYM_FUNC_START_LOCAL(__no_granule_support)
499         /* Indicate that this CPU can't boot and is stuck in the kernel */
500         update_early_cpu_boot_status \
501                 CPU_STUCK_IN_KERNEL | CPU_STUCK_REASON_NO_GRAN, x1, x2

  • __early_cpu_boot_status 에 커널이 멈춘 이유 CPU_STUCK_IN_KERNEL , CPU_STUCK_REASON_NO_GRAN 을 저장합니다.

 


502 1:
503         wfe
504         wfi
505         b       1b

 

무한 루프


506 SYM_FUNC_END(__no_granule_support)


 

512
513         adrp    x1, early_init_stack
514         mov     sp, x1

 

  • 초기 임시 스택 영역(early_init_stack)으로 SP(스택 포인터) 를 설정합니다.
  •  ARM64에서 C 언어로 작성된 함수를 호출하려면 지역 변수를 저장하고 복귀 주소를 push할 수 있는 스택이 반드시 필요합니다. 이 두 라인을 통해 C 함수를 실행할 수 있는 최소한의 환경이 구축됩니다.

 

 


515         mov     x29, xzr

 

  •  ARM64 아키텍처 규격에서 x29 레지스터는 FP(Frame Pointer, 프레임 포인터) 역할을 합니다. 이 레지스터를 xzr(Zero Register)을 사용해 완전히 초기화합니다.
  • 프레임 포인터의 값을 0으로 세팅하는 것은 "여기서부터가 최상위 스택 프레임(함수 호출의 시작점)이다"라고 마킹하는 것입니다. 나중에 커널에 문제가 생겨서 디버깅을 위해 스택을 역추적(Backtrace)할 때, 디버거가 0을 만나면 "여기가 최상단이구나" 하고 추적을 안전하게 멈추게 됩니다.

 

 


516         mov     x0, x20                         // pass the full boot status
517         mov     x1, x21                         // pass the FDT

  • 함수를 호출하기 전에 인자(Argument)를 세팅합니다. ARM64 호출 규약에 따라 첫 번째 인자는 x0, 두 번째 인자는 x1에 담아야 합니다.
  • x0 = x20: 부팅 과정 내내 x20에 보존해 왔던 현재 CPU의 부팅 상태(Boot Status) 플래그를 첫 번째 인자로 넘깁니다.
  • x1 = x21: 부트로더(u-boot 등)가 넘겨주어 x21에 보관하고 있던 FDT(디바이스 트리)의 물리 주소를 두 번째 인자로 넘깁니다.

 


518         bl      __pi_early_map_kernel           // Map and relocate the kernel

 

  • 스택과 인자가 모두 준비되었으므로, 실제 커널 이미지와 하드웨어 정보(FDT)를 가상 메모리에 매핑하는 초기화 C 함수인 __pi_early_map_kernel을 호출합니다.
  • 이 함수 내부로 진입하면 부트로더가 메모리에 대충 올려둔 리눅스 커널의 진짜 코드 영역(TEXT), 데이터 영역(DATA),  초기화 세그먼트 등을 가상 주소 공간에 매핑 및 재배치하는 본격적인 커널 맵 공사가 시작됩니다.
  • __pi_ 접두사의 의미: Position Independent(위치 독립 실행)를 뜻합니다. 커널이 메모리의 어느 주소에 로드되더라도(KASLR 보안 기능 등이 적용되어도) 정상 동작할 수 있도록 컴파일된 특수 영역임을 나타냅니다. 

 


519
520         ldr     x8, =__primary_switched

 

  • __primary_switched(과거형 단어임에 주목)라는 함수의 절대 가상 주소(Absolute Virtual Address)를 리터럴 풀(Literal Pool)에서 읽어와 x8 레지스터에 로드합니다.
  • 앞서 사용했던 adrp 명령어는 현재 PC(Program Counter) 기준의 상대 주소를 가져오기 때문에 MMU 온/오프와 상관없이 현재 위치 기반의 주소를 알아냅니다. 반면, 이 ldr 방식으로 가져오는 주소는 링커(Linker)가 빌드할 때 지정한 0xFFFF_XXXX_XXXX_XXXX 형태의 완벽한 커널 전용 가상 주소입니다.

 


521         adrp    x0, KERNEL_START                // __pa(KERNEL_START)

 

커널 파일이 메모리에 로드된 시작 지점의 물리 주소(Physical Address)를 계산하여 x0 레지스터에 담습니다. (주석의 __pa는 Physical Address의 약자입니다.)

다음 함수인 __primary_switched에게 "현재 진짜 커널 코드가 램(RAM)의 몇 번지(물리 주소)에 얹혀서 시작되었는지"를 첫 번째 인자(x0)로 넘겨주기 위한 준비 작업입니다.

 


522         br      x8

 

  • Branch to Register. x8 레지스터에 저장된 주소로 무조건 점프합니다.
  • 대전환의 순간: 이 명령어가 실행되는 순간, CPU는 지금까지 머물던 1:1 식별 매핑(Identity Mapping) 영역을 탈출하여, 0xFFFF...로 시작하는 상위 커널 가상 주소 공간 영역으로 완전히 워프(Warp)하게 됩니다. 이 시점부터 명실상부한 가상 메모리 기반의 리눅스 운영체제가 가동됩니다.

 

523 SYM_FUNC_END(__primary_switch)

 


 

215 /*
216  * The following fragment of code is executed with the MMU enabled.
217  *
218  *   x0 = __pa(KERNEL_START)
219  */


220 SYM_FUNC_START_LOCAL(__primary_switched)


221         adr_l   x4, init_task

 

  • init_task는 리눅스 커널의 0번 프로세스(idle 프로세스 또는 swapper)의 task_struct 구조체 변수입니다. 모든 프로세스의 조상이 되는 첫 번째 프로세스입니다.
  • adr_l은 ARM64 어셈블리의 매크로(Macro)로, 현재 PC(Program Counter) 기준 상대 주소(PC-relative)를 이용해 특정 심볼의 가상 주소를 레지스터에 로드하는 역할을 합니다.
  • MMU가 갓 켜진 이 시점에서는 페이지 테이블이 매핑된 가상 주소(Virtual Address)를 사용해야 하므로, 이 명령어를 통해 init_task 구조체가 위치한 가상 주소를 레지스터 x4에 저장합니다.

 


222         init_cpu_task x4, x5, x6


187         /*
188          * Initialize CPU registers with task-specific and cpu-specific context.
189          *
190          * Create a final frame record at task_pt_regs(current)->stackframe, so
191          * that the unwinder can identify the final frame record of any task by
192          * its location in the task stack. We reserve the entire pt_regs space
193          * for consistency with user tasks and kthreads.
194          */
195         .macro  init_cpu_task tsk, tmp1, tmp2
196         msr     sp_el0, \tsk

 

  • msr(Move System Register) 명령어는 일반 레지스터의 값을 시스템 제어 레지스터로 옮깁니다.
  • sp_el0는 EL0(유저 레벨)에서 사용하는 스택 포인터 레지스터이지만, 리눅스 커널(EL1)은 이 레지스터를 유저 스택 용도가 아니라 '현재 실행 중인 태스크의 task_struct 주소(current)'를 가리키는 포인터 저장소로 전용(Tweak)하여 사용합니다.
  • 따라서 x4(\tsk)에 들어있던 init_task 구조체의 가상 주소가 sp_el0 시스템 레지스터에 저장됩니다. 이후 커널 내부에서 current 매크로를 호출하면 항상 이 sp_el0 값을 읽어와 현재 프로세스가 무엇인지 알 수 있게 됩니다.

 


197
198         ldr     \tmp1, [\tsk, #TSK_STACK]

 

  • \tsk 레지스터(x4, 즉 init_task 구조체의 시작 주소)로부터 TSK_STACK만큼 떨어진 오프셋(Offset)에 있는 값을 읽어 \tmp1(x5) 레지스터에 저장합니다.
  • TSK_STACK 오프셋에 저장된 값은 이 태스크가 사용할 커널 스택의 최하단(낮은 주소) 가상 주소(task_struct->stack)입니다.


199         add     sp, \tmp1, #THREAD_SIZE

 

  • add 명령어로 스택의 기준 주소를 계산합니다.
  • ARM64(AArch64) 아키텍처에서 스택은 높은 주소에서 낮은 주소 방향으로 자라납니다(Descending Stack).
  • 198라인에서 가져온 \tmp1은 스택의 가장 낮은 주소이므로, 여기에 스택의 총 크기인 #THREAD_SIZE(보통 16KB 또는 32KB)를 더해야 스택의 가장 꼭대기(높은 주소, Stack Base)를 가리키게 됩니다.
  • 이 계산된 최상단 주소를 시스템의 실제 스택 포인터 레지스터인 sp에 대입합니다.

200         sub     sp, sp, #PT_REGS_SIZE

 

  • sub 명령어로 스택 포인터 위치를 아래로 끌어내립니다.
  • 앞선 주석(192라인)에서 *"일관성을 위해 pt_regs 공간을 예약한다"*고 했던 작업을 실제로 수행하는 라인입니다.
  • 스택 최상단(sp)에서 CPU 레지스터들을 통째로 저장할 수 있는 구조체 크기인 #PT_REGS_SIZE만큼 주소를 빼서(주소가 낮아지는 방향으로 공간을 확보) 최종적인 sp 위치를 확정합니다. 이 빈 공간은 나중에 컨텍스트 스위칭이나 예외 처리가 발생했을 때 레지스터 값을 백업하는 용도로 사용됩니다.

 


201
202         stp     xzr, xzr, [sp, #S_STACKFRAME]

 

  • stp (Store Pair) 명령어는 두 개의 레지스터 값을 메모리에 연속으로 저장합니다.
  • 제로 레지스터(xzr, 항상 0을 가짐) 2개를 현재 스택 포인터(sp)로부터 S_STACKFRAME 오프셋만큼 떨어진 위치에 저장합니다.
  •  이 위치가 바로 최초의 pt_regs 내부에 위치한 스택 프레임 공간(FP와 LR)입니다. 이곳에 0과 0을 넣음으로써 스택 체인의 연결 고리의 끝임을 나타냅니다. 디버거가 스택을 역추적하다가 FP(0)와 LR(0)을 만나는 순간 "상위 호출자가 없다"고 판단하여 오류 없이 추적을 종료하게 됩니다.

203         mov     \tmp1, #FRAME_META_TYPE_FINAL
204         str     \tmp1, [sp, #S_STACKFRAME_TYPE]
205         add     x29, sp, #S_STACKFRAME

  • 임시 레지스터 tmp1에 #FRAME_META_TYPE_FINAL 이라는 기정의된 상수 플래그 값을 넣은 뒤, 이를 스택의 S_STACKFRAME_TYPE 공간에 저장(str)합니다.
  • 현재 스택 포인터(sp)에 S_STACKFRAME 오프셋을 더한 주소(즉, 방금 0과 메타데이터를 저장한 그 지점)를 계산하여 CPU의 x29 (Frame Pointer) 레지스터에 직접 입력합니다.
  • 이제부터 이 CPU에서 실행되는 모든 후속 함수들은 이 x29 주소를 뼈대로 삼아 자신의 스택 프레임을 쌓아 올리게 됩니다. 이 줄 덕분에 최초의 함수 호출 역추적이 완벽하게 작동할 수 있는 기초 이정표가 세워집니다.

 

[ 높은 주소 ]
            |                                   |
            +-----------------------------------+ <-- 199번 줄: add sp, \tmp1, #THREAD_SIZE
            |    TYPE = FRAME_META_..._FINAL    | <-- 203-204번 줄: "진짜 끝"임을 인증하는 도장
  x29 ----> +-----------------------------------+ <-- 205번 줄: add x29, sp, #S_STACKFRAME 
            |    LR (Link Register) = 0         | <-- 202번 줄: stp xzr, xzr로 0 삽입
            |    FP (Frame Pointer) = 0         | <-- 202번 줄: 스택 체인의 절대적 끝(종착점)
            +-----------------------------------+
            |           pt_regs 나머지          |
            |                                   |
       sp-> +-----------------------------------+ <-- 200번 줄: sub sp, sp,#PT_REGS_SIZE
            |            x30                    | <-- 228번 줄
            |            x29                    | <-- 228번 줄: stp x29, x30, [sp, #-16]!
  x29, sp-> +-----------------------------------+ <-- 228번 줄, 229: mov x29, sp
            |                                   |
            |                                   |
            |                                   |
            |                                   |
init_stack->+-----------------------------------+



[ 낮은 주소 ]

  CPU 레지스터 상태:
  x29 (Frame Pointer) : 0xYYYYYYYY (S_STACKFRAME의 가상 주소)
  
  
  
  <예시>
  
  [최초 상태]  현재 함수 내부의 x29(FP) 레지스터 확인
                  |
                  v
[역추적 1단계] 상위 함수의 스택 프레임(FP) 주소로 이동 -> 함수명 출력
                  |
                  v
[역추적 2단계] 또 그 상위 함수의 스택 프레임(FP) 주소로 이동 -> 함수명 출력
                  |
                  v
                 ... (반복)
                  |
                  v
[최종 단계]   우리가 만든 최상위 `S_STACKFRAME` 주소에 도달!
              1. 메타데이터가 `FRAME_META_TYPE_FINAL` 인지 확인 (통과)
              2. 상위 FP 값을 읽으려고 보니 `0` 임을 확인!
                  |
                  v
        "아, 여기가 진짜 끝이구나! 추적 종료." -> 에러 없이 깔끔하게 멈춤

 

 

206
207         scs_load_current

Shadow Call Stack (SCS)이란?

컴퓨터 보안에서 가장 흔하고 치명적인 공격 중 하나가 바로 "스택 버퍼 오버플로우"를 이용한 ROP (Return-Oriented Programming) 공격입니다.

  • 공격 원리: 함수가 종료될 때 돌아갈 주소(Link Register 또는 Return Address)는 일반 스택 메모리에 저장됩니다. 공격자가 스택에 넘치는 데이터를 주입하여 이 반환 주소를 해커의 악성 코드 주소로 변조하면, 함수가 끝나는 순간 시스템의 제어권이 넘어갑니다.
  • SCS의 방어책: 이를 막기 위해 ARM64 Linux 커널은 진짜 스택과 별개로 '반환 주소만 따로 저장하는 비밀 스택'을 하나 더 만듭니다. 이것이 바로 Shadow Call Stack입니다. 함수가 호출될 때 반환 주소를 일반 스택과 섀도 스택 양쪽에 다 적어두고, 함수가 끝날 때는 섀도 스택에 저장된 안전한 반환 주소만 사용하여 복귀합니다. 이로 인해 일반 스택이 아무리 오염되어도 ROP 공격이 원천 차단됩니다.

CONFIG_SHADOW_CALL_STACK 옵션이 켜져 있다면 이 매크로는 다음과 같은 실제 어셈블리 명령어로 확장됩니다. (만약 이 옵션이 꺼져 있다면 아무것도 하지 않는 빈 줄이 됩니다.)

 

ldr x18, [tsk, #TSK_TI_SCS_SP]

 

현재 태스크 구조체(tsk)에 저장되어 있던 이 태스크 고유의 섀도 스택 주소를 읽어와 x18 레지스터에 설정합니다.


208
209         adr_l   \tmp1, __per_cpu_offset
210         ldr     w\tmp2, [\tsk, #TSK_TI_CPU]
211         ldr     \tmp1, [\tmp1, \tmp2, lsl #3]

 

멀티코어 시스템에서 각 CPU 코어는 자신만의 독립적인 데이터(예: 스케줄러 정보, CPU별 통계, 타이머 등)를 가집니다. 이 코드의 목적은 "현재 부팅 중인 CPU가 멀티코어 메모리 맵 전체 중에서 '자신의 고유 변수 영역'이 정확히 어디인지 가리키는 이정표(오프셋)를 하드웨어 레지스터에 세팅하는 것"입니다.

 

  • __per_cpu_offset이라는 전역 배열(테이블)의 가상 주소를 계산하여 tmp1 레지스터에 저장합니다.
  • __per_cpu_offset[NR_CPUS] = {CPU 0의 오프셋, CPU 1의 오프셋, CPU 2의 오프셋...} 처럼 각 CPU 번호에 대응하는 메모리 오프셋 주소 값들을 순서대로 들고 있는 거대한 이정표 입니다.
  • 현재 태스크 구조체(tsk)의 메모리 공간에서 TSK_TI_CPU 오프셋(구조체 내 cpu 멤버 변수 위치 )에 저장된 값을 읽어와 32비트 레지스터 변수 wtmp2에 넣습니다.
  • 현재 이 코드를 실행하고 있는 "나(CPU)의 번호가 몇 번인가?"를 확인하는 작업입니다. 부팅 초기 단계(Primary CPU)이므로 여기서는 대개 0번을 읽어오게 됩니다.
  • tmp1(배열 시작 주소)에 tmp2(CPU 번호)를 더해 정확한 내 CPU의 오프셋 값이 저장된 주소를 찾아간 뒤, 그 안에 든 진짜 오프셋 값을 읽어와 다시 tmp1에 덮어씁니다.
  • lsl #3 : 64비트 아키텍처(ARM64)에서 메모리 주소(포인터) 하나는 8바이트 크기를 가집니다. 따라서 CPU 번호에 8을 곱해야 정확한 배열의 인덱스로 접근할 수 있습니다. 컴퓨터 과학에서 8을 곱하는 가장 빠른 방법은 비트를 왼쪽으로 3칸 미는 것(lsl #3, 2^3 = 8)이기 때문에 이 연산이 사용되었습니다.

 

212         set_this_cpu_offset \tmp1

  • set_this_cpu_offset 매크로는 방금 계산한 내 CPU 전용 오프셋 값을 ARM64의 특수 시스템 레지스터인 tpidr_el1에 저장합니다.
  • 이 레지스터에 오프셋이 등록되는 순간부터, 커널 내부에서 C 언어로 this_cpu_ptr(변수명) 같은 명령을 내릴 때 CPU가 메모리를 헤매지 않고 곧바로 "내 코어 전용 변수 방"으로 직행할 수 있게 됩니다. 다른 CPU 코어들과 Race Condition 없이 안전하고 빠르게 자기만의 데이터를 다룰 수 있는 하드웨어적 기반이 완성되는 순간입니다.

 

213         .endm

 



223
224         adr_l   x8, vectors                     // load VBAR_EL1 with virtual
225         msr     vbar_el1, x8                    // vector table address
226         isb

  • vectors는 entry.S 에 정의되어 있는 최초의 예외 처리 루틴들이 모여 있는 테이블의 시작점입니다
  • vbar_el1 (Vector Base Address Register, EL1) 은 커널 공간의 예외 처리 베이스 주소를 저장하는 레지스터입니다.
  • 이 줄이 실행되는 순간부터, CPU는 커널 실행 중 인터럽트나 CPU 예외가 발생하면 vbar_el1에 저장된 주소를 기준으로 삼아 해당 예외에 맞는 핸들러로 자동 점프하게 됩니다.
  • isb는 CPU의 파이프라인을 Flush 하고 다음 명령어를 처음부터 다시 Fetch 하도록 강제하는 동기화 Barrier 명령어입니다.
  • 현대 CPU는 성능을 높이기 위해 명령어들을 미리 읽어서 실행하는 '파이프라인' 구조를 가집니다. msr vbar_el1, x8 명령어로 시스템 레지스터를 변경했더라도, CPU 파이프라인에 이미 그 전 상태를 가정하고 들어와 있는 후속 명령어들이 있을 수 있습니다.
  • isb를 배치함으로써 "방금 vbar_el1 설정을 바꿨으니, 이 뒤에 오는 모든 명령어들은 반드시 바뀐 예외 설정을 인지한 상태에서 안전하게 실행되어야 한다"는 것을 보장합니다.

 

227
228         stp     x29, x30, [sp, #-16]!

 

 

  • stp (Store Pair) 명령어로 x29(FP)와 x30(LR) 레지스터의 값을 메모리에 동시에 저장합니다.
  • 주소 지정 방식 ([sp, #-16]!): 이 문법은 ARM64의 Pre-indexed 주소 지정 방식입니다. 맨 뒤에 붙은 느낌표(!)가 핵심입니다.
    1. sp 값을 먼저 16바이트만큼 아래(낮은 주소 방향)로 내립니다. (64비트 레지스터 2개 = 8바이트 × 2 = 16바이트 공간 확보)
    2. 새로 변경된 sp 주소 위치에 x29와 x30 값을 차례대로 저장합니다.
    3. 느낌표(!)의 효과로 인해, sp 레지스터의 값 자체가 16바이트 내려간 새 주소로 최종 업데이트됩니다.
  •  현재 함수의 상위 호출자 정보를 스택에 안전하게 보관하여, 나중에 이 함수가 끝날 때 부모 함수로 정상적으로 돌아갈 수 있도록 이정표를 저장하는 것입니다.

 

229         mov     x29, sp

 

  • 현재 스택 포인터(sp)의 주소 값을 프레임 포인터 레지스터인 x29에 복사합니다.
  • 방금 x29와 x30을 저장한 그 스택 지점을 "지금부터 실행될 새로운 함수의 스택 프레임 기준점"으로 삼겠다는 선언입니다. 이 줄이 실행됨으로써 디버거나 Unwinder가 함수 호출 계층을 추적할 수 있는 새로운 스택 체인 고리가 완벽하게 연결됩니다.

 


230
231         str_l   x21, __fdt_pointer, x5          // Save FDT pointer

 

  • 이 기기의 하드웨어 정보가 담긴 FDT(Device Tree Blob)의 물리 주소가 저장되어 있는 레지스터입니다. 부트로더가 커널을 실행할 때 x20과 x21 레지스터에 각각 부트 상태와 FDT 주소를 담아서 넘겨주며, 커널은 이 값을 여기까지 계속 유지해 왔습니다.
  • 커널의 C 언어 코드에서 참조할 수 있도록 정의된 전역 포인터 변수입니다 (void *__fdt_pointer). 이 코드 라인을 통해 assembly 단계에서 보관하던 FDT 주소가 C 언어 환경의 전역 변수로 복사됩니다. 이후 커널은 __fdt_pointer를 통해 디바이스 트리를 파싱하여 CPU 코어 개수, 메모리 크기, 주변 장치(UART, 가속기 등) 정보를 알아내게 됩니다.


232
233         adrp    x4, _text                       // Save the offset between
234         sub     x4, x4, x0                      // the kernel virtual and
235         str_l   x4, kimage_voffset, x5          // physical mappings

 

 

  • _text 커널 이미지의 맨 처음 시작점을 가리키는 링커 스크립트 기호(Symbol)입니다.
  • 현재 MMU가 켜진 가상 메모리 상태이므로, 이 명령어를 실행하면 _text가 위치한 가상 주소(Virtual Address)의 페이지 기준 주소가 x4 레지스터에 저장됩니다.
  • 가상 주소가 담긴 x4에서 물리 주소가 담긴 x0를 뺍니다 ($x4 = x4 - x0$).
  • 즉, [커널 가상 주소] - [커널 물리 주소] 공식을 수행하는 것입니다. 이 결과값이 바로 두 주소 공간 사이의 차이인 오프셋(Offset)이 됩니다.
  • 방금 계산한 오프셋 값(x4)을 가상 메모리에 있는 kimage_voffset이라는 전역 변수에 저장합니다.
  • x5는 주소 계산을 위해 매크로 내부에서 임시로 사용하는 스크래치 레지스터입니다.

 

왜 오프셋(voffset)을 저장해야 할까요?

ARM64 리눅스 커널은 실행 중에 가상 주소를 물리 주소로 바꾸거나, 반대로 물리 주소를 가상 주소로 변환해야 하는 일이 아주 빈번합니다. 매번 복잡한 페이지 테이블을 역추적하는 것은 성능에 불리하므로, 커널은 이 오프셋 값을 이용해 아주 단순한 산술 연산만으로 주소를 변환합니다.

Virtual Address   = Physical Address + kimage_voffset
Physical Address = Virtual Address - kimage_voffset
 

이 오프셋 덕분에 커널은 실행 중에 자신이 물리 메모리 어디에 얹혀있든 상관없이 빠르고 유연하게 메모리를 관리할 수 있게 됩니다.

 

 

 

 


236
237         mov     x0, x20
238         bl      set_cpu_boot_mode_flag

  • x20 레지스터에 보관되어 있던 부트 시점의 CPU 모드 값(boot status)을 x0 레지스터로 복사합니다.
  • set_cpu_boot_mode_flag 함수 호출합니다. 이 함수는 x0로 전달받은 부트 모드 값을 분석하여 커널의 전역 변수인 __boot_cpu_mode 배열에 저장합니다.

 왜 CPU 부트 모드 플래그를 저장해야 할까요?

리눅스 커널은 자신이 EL1(일반 OS 모드)으로 켜졌는지, 아니면 EL2(하이퍼바이저 모드)로 켜졌는지에 따라 하드웨어 제어 방식을 다르게 처리해야 합니다.

  1. KVM(가상화) 활성화 여부: 커널이 EL2 모드로 부팅되었다면, 리눅스 자체를 하이퍼바이저로 사용할 수 있는 KVM 기능인 nVHE나 VHE 모드를 활성화할 수 있습니다.
  2. 보조 CPU(Secondary CPUs) 부팅: 나중에 CPU 코어 1, 2, 3번을 깨울 때(SMP 활성화), 주 프로세서(CPU 0)와 똑같은 예외 레벨(EL)로 맞춰서 깨워야 하므로 이 플래그 정보가 기준점이 됩니다.

 


 

 57 u32 __boot_cpu_mode[] = { BOOT_CPU_MODE_EL2, BOOT_CPU_MODE_EL1 };

 

  • __boot_cpu_mode[0]: 주 프로세서(Primary CPU)의 부팅 모드를 기록하는 공간.
  • __boot_cpu_mode[1]: 나중에 깨어날 보조 프로세서들(Secondary CPUs)의 부팅 모드를 기록하는 공간.

 

 

 

414 /*
415  * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
416  * in w0. See arch/arm64/include/asm/virt.h for more info.
417  */
418 SYM_FUNC_START_LOCAL(set_cpu_boot_mode_flag)
419         adr_l   x1, __boot_cpu_mode
420         cmp     w0, #BOOT_CPU_MODE_EL2
421         b.ne    1f

 

  • 현재 부팅 상태가 EL2가 아니면 1f


422         add     x1, x1, #4

  • 다음 부팅 CPU에게 첫 부팅 CPU의 부팅 상태를 알려주기 위해 인텍스 증가 ( __boot_cpu_mode[1])


423 1:      str     w0, [x1]                        // Save CPU boot mode

 

  • 최종 결정된 x1 주소가 가리키는 메모리에 w0 값(부트 모드 플래그)을 저장합니다.
  • EL1 부팅 시 (b.ne 만족): x1이 그대로 __boot_cpu_mode[0]을 가리키므로 여기에 값이 저장됩니다.
  • EL2 부팅 시 (b.ne 불만족): 4바이트가 더해진 __boot_cpu_mode[1]에 값이 저장됩니다. 

 


424         ret
425 SYM_FUNC_END(set_cpu_boot_mode_flag)

 


 

 


239
240 #if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)

 

  • CONFIG_KASAN_GENERIC: 가장 널리 쓰이는 표준 KASAN 모드입니다. 컴파일러가 메모리 접근 코드 주변에 검사 코드를 삽입하고, Shadow Memory 라는 별도의 공간을 두어 메모리 오염을 감지합니다.
  • CONFIG_KASAN_SW_TAGS: ARM64의 TBI(Top Byte Ignore) 하드웨어 기능을 활용하는 소프트웨어 태그 기반 KASAN 모드입니다. 포인터의 상위 바이트에 태그를 숨겨 메모리 유효성을 검사합니다.

 


241         bl      kasan_early_init

  • 이 함수는 커널이 실행되면서 앞으로 발생할 메모리 접근을 감시할 수 있도록, 초기 섀도 메모리 영역을 임시 페이지 테이블(kasan_early_shadow_page)로 매핑하는 작업을 수행합니다. 이 작업이 끝나야만 이후에 켜질 복잡한 커널 코드들이 메모리를 안전하게 감시받으며 달릴 수 있습니다.


242 #endif

https://www.kernel.org/doc/html/latest/dev-tools/kasan.html

 


243         mov     x0, x20
244         bl      finalise_el2                    // Prefer VHE if possible

  • x20 레지스터에 보관되어 있던 부트 시점의 CPU 모드 값(boot status)을 x0 레지스터로 복사합니다.
  • 주석에 적힌 "Prefer VHE if possible" (가능하면 VHE를 선호함)이라는 문구 그대로, 함수 내부에서는 x0(즉, x20) 값을 검사하여 현재 시스템이 EL2 하이퍼바이저 모드로 부팅되었는지를 확인합니다.
  • 조건이 맞고 하드웨어가 ARMv8.1+ VHE 기능을 지원한다면, 시스템 제어 레지스터를 조작하여 커널이 EL2에서 직접 효율적으로 실행되도록 세팅을 마무리지어 줍니다.

왜 이 타이밍에 VHE를 결정할까?

리눅스 커널 부팅 과정에서 가상화 모드(VHE vs nVHE)를 확정 짓는 것은 향후 커널의 메모리 맵과 실행 권한을 결정하는 중대한 분수령입니다.

  • VHE 모드가 활성화되면: 커널이 EL2의 가상 주소 공간(TTBR0_EL2, TTBR1_EL2)을 직접 사용하게 되므로, 시스템 레지스터의 매핑 구조를 이에 맞게 재조정해야 합니다.
  • 이후 단계와의 연결: 이 작업이 끝나고 나면 바로 아래에서 메인 C-런타임 함수인 start_kernel()로 진입하게 되는데, 그 전에 CPU의 실행 환경(예외 레벨 및 가상화 모드)이 완벽하게 픽스되어 있어야 C 언어로 작성된 가상화 서브시스템(KVM)이 오작동 없이 초기화될 수 있습니다.

 

 

 


239 /*
240  * Entry point to finalise EL2 and switch to VHE if deemed capable
241  *
242  * w0: boot mode, as returned by init_kernel_el()
243  */
244 SYM_FUNC_START(finalise_el2)
245         // Need to have booted at EL2
246         cmp     w0, #BOOT_CPU_MODE_EL2
247         b.ne    1f

 

  • 주석에 적힌 대로, VHE 설정을 하려면 애초에 시스템이 EL2 모드로 부팅되었어야만 합니다.
  • 따라서 매개변수로 넘어온 w0의 부트 모드가 BOOT_CPU_MODE_EL2(0x0e12) 가 맞는지 비교합니다.
  • 비교 결과가 다르다면(즉, 시스템이 EL2가 아닌 일반 EL1 모드로 부팅되었다면), VHE를 적용할 수 없으므로 뒤쪽의 VHE 설정 로직들을 전부 건너뛰고 하단에 있을 로컬 라벨 1:로 즉시 점프합니다.
  • 만약 같다면(EL2 부팅이 맞다면), 점프하지 않고 아래로 계속 진행하여 하드웨어가 VHE 기능을 지원하는지 확인하고 가상화 환경 초기화를 완수하게 됩니다.

 

 

VHE (Virtualization Host Extensions)란?

ARMv8.1 아키텍처부터 도입된 기능입니다.

  • nVHE (Non-VHE, 기본 방식): 호스트 리눅스 커널은 EL1에서 돌고, 가상화를 제어하는 KVM 핵심 코드는 EL2에서 따로 돕니다. 하이퍼바이저 기능을 쓸 때마다 EL1 < --- > EL2 간의 값 비싼 CPU 모드 전환이 일어납니다.
  • VHE (활성화 방식): 호스트 리눅스 커널이 처음부터 EL2 레벨을 통째로 장악하여 실행됩니다. 주소 공간과 레지스터 구조를 재매핑하여 커널이 모드 전환 없이 직접 가상머신을 제어하므로 오버헤드가 거의 제로에 가깝게 줄어듭니다.

 

248
249         // and still be at EL1
250         mrs     x0, CurrentEL
251         cmp     x0, #CurrentEL_EL1
252         b.ne    1f

  • VHE 설정을 시도한 직후, 시스템이 예기치 않게 EL2 모드에서 EL1(커널 모드)로 Downgrade 되었는지 확인하는 방어적 예외 처리 구간입니다.
  • 현재 CPU가 실행 중인 Exception Level 정보를 담고 있는 레지스터인 CurrentEL의 값을 읽어서 x0에 복사합니다.
  • 방금 읽어온 현재 CPU의 레벨(x0)이 EL1 모드 플래그(CurrentEL_EL1, 값으로는 0x4)와 일치하는지 비교합니다.
  • 비교 결과가 다르다면(즉, 현재 CPU가 EL1으로 떨어지지 않고, 우리가 의도한 대로 EL2 모드를 안전하게 유지하고 있다면), 아래쪽에 위치한 로컬 라벨 1:로 점프하여 정상 흐름을 계속 탑니다.
  • 만약 같다면(어떤 이유에서든 현재 상태가 EL1이라면), 점프하지 않고 바로 다음 줄로 진행하여 "EL2로 유지되어야 하는데 EL1으로 떨어졌다"는 에러 처리나 대피(Fallback) 로직(예: nVHE 모드로 강제 전환 등)을 수행하게 됩니다.

 

 


253
254         mov     x0, #HVC_FINALISE_EL2

 

  • HVC_FINALISE_EL2 라는 내부 상수를 x0 레지스터에 로드합니다.
  • 이 상수는 하이퍼바이저(EL2)에게 보낼 "명령어 ID"입니다. 바로 다음 줄에서 하이퍼바이저 모드를 호출할 때, x0에 담긴 이 ID를 보고 하이퍼바이저가 무슨 작업을 처리해야 하는지 판단하게 됩니다.


255         hvc     #0

 

  • ARM64 아키텍처에서 가상화의 핵심 명령어입니다. 유저 모드(EL0)에서 커널(EL1)로 갈 때 svc(System Call)를 쓰는 것처럼, 커널(EL1)에서 하이퍼바이저(EL2)로 트랩(Trap)하여 진입할 때 hvc 명령어를 사용합니다.
  • 이 명령어가 실행되는 순간 CPU는 EL2 권한으로 전환되며, 미리 등록된 하이퍼바이저 벡터 테이블의 핸들러가 실행됩니다. 이 핸들러는 x0에 담긴 HVC_FINALISE_EL2 명령을 확인하고, CPU 시스템 레지스터들의 VHE 세팅을 최종적으로 확정지은 뒤 다시 원래 코드로 복귀시킵니다.
  • 뒤의 #0은 Immediate value 로, 대개 소프트웨어적으로 추가적인 구분을 할 때 쓰이나 리눅스에서는 보통 0으로 처리하고 레지스터(x0)로 인자를 넘깁니다. SMCCC(Secure Monitor Call Convention)라는 규격에 정의되었습니다.

 

 


256 1:
257         ret
258 SYM_FUNC_END(finalise_el2)

 

 

 



245         ldp     x29, x30, [sp], #16

 

  • 228번 라인에서 스택에 안전하게 보관(Push)해 두었던 프레임 포인터(x29)와 링크 레지스터(x30, 돌아갈 주소) 값을 복원(Pop)하는 과정입니다.
  • 뒤의 ], #16은 Post-increment 로, 값을 다 읽어온 후에 스택 포인터(sp)를 16바이트만큼 위로 올려 스택 공간을 깔끔하게 비워줍니다.

 


246         bl      start_kernel

 

  • init/main.c 파일에 정의된 리눅스 커널의 메인 C 함수인 start_kernel()로 진입합니다.


247         ASM_BUG()

 

  • 정상적인 상황이라면 start_kernel() 함수는 운영체제가 종료(Shutdown/Poweroff)될 때까지 절대로 반환(Return)되어서는 안 되는 함수입니다. 즉, 이 줄로 실행 흐름이 내려왔다는 것 자체가 시스템에 거대한 치명적 오류가 발생했음을 의미합니다.
  • 따라서 ASM_BUG() 매크로는 실행이 이곳에 도달하는 순간 시스템을 강제로 중단(Fault/Panic)시키고 개발자가 디버깅할 수 있도록 크래시 정보를 남기는 역할을 합니다.

 


248 SYM_FUNC_END(__primary_switched)
249

 




 

시스템이 처음 부팅될 때 부팅을 주도하는 주 프로세서(Boot CPU 또는 BSP) 외에, 멀티코어 시스템에서 다른 보조 프로세서(Secondary CPU 또는 AP)들이 동적으로 활성화(Hotplug 또는 시스템 부팅 시)될 때 가장 먼저 진입하는 진입점(Entry Point)을 정의하고 있습니다.

 

351         /*
352          * Secondary entry point that jumps straight into the kernel. Only to
353          * be used where CPUs are brought online dynamically by the kernel.
354          */
355 SYM_FUNC_START(secondary_entry)
356         mov     x0, xzr
357         bl      init_kernel_el                  // w0=cpu_boot_mode

 

  • 보조 CPU가 현재 어떤 Exception Level(EL, 예외 레벨)에서 실행 중인지 확인하고, 커널이 정상적으로 동작할 수 있도록 EL 설정을 초기화합니다. (ARM64에서 일반적인 리눅스 커널은 EL1에서 동작하며, 하이퍼바이저가 있다면 EL2에서 시작해 EL1으로 전환될 수 있습니다.)
  • 옆의 주석(w0=cpu_boot_mode)에서 알 수 있듯이, 이 함수를 실행하고 나면 결과값(CPU가 어떤 모드로 부팅되었는지에 대한 정보)이 w0 레지스터(x0의 하위 32비트)에 저장되어 반환됩니다.
  • 이전 270라인의 init_kernel_el 참고

 


358         b       secondary_startup

  • 예외 레벨 초기화가 끝났으므로, 보조 CPU를 본격적으로 커널 구동 상태로 만들기 위한 secondary_startup 루틴으로 넘어갑니다.
  • 이 이후 단계에서 MMU(메모리 관리 장치)를 켜고, 페이지 테이블을 설정하며, 최종적으로 C 언어로 작성된 커널 코드(secondary_start_kernel)로 진입하게 됩니다.


359 SYM_FUNC_END(secondary_entry)

360

 

361 SYM_FUNC_START_LOCAL(secondary_startup)
362         /*
363          * Common entry point for secondary CPUs.
364          */
365         mov     x20, x0                         // preserve boot mode

  • 나중에 이 부팅 모드 값을 다시 참조할 수 있도록,  x20 에 값을 백업


366
367 #ifdef CONFIG_ARM64_VA_BITS_52
368 alternative_if ARM64_HAS_VA52
369         bl      __cpu_secondary_check52bitva
370 alternative_else_nop_endif
371 #endif

 

  • CONFIG_ARM64_VA_BITS_52 52비트 가상 주소 기능을 커널 자체에서 지원하도록 설정했을 때만 실행 코드로 만듭니다.
  • alternative_if ARM64_HAS_VA52: 현재 시스템의 CPU 하드웨어가 52비트 가상 주소 기능(ARM64_HAS_VA52)을 실제로 지원하는지 부팅 시점에 판단합니다.
  • 만약 하드웨어가 52비트 VA를 지원한다면, 369 라인의 bl __cpu_secondary_check52bitva를 그대로 실행하여 보조 CPU의 52비트 가상 주소 상태를 체크합니다.
  • 만약 하드웨어가 52비트 VA를 지원하지 않는다면, 커널은 런타임에 이 메모리 영역의 명령어를 nop(No Operation, 아무것도 하지 않고 넘어감) 명령어들로 통째로 갈아 끼워 버립니다(alternative_else_nop_endif).
  • 매번 if(지원하는가?)를 CPU 분기 예측으로 검사하면 성능 손실이 발생하므로, 부팅할 때 딱 한 번 하드웨어를 검사한 뒤 안 쓰는 코드는 아예 nop으로 지워버리는 리눅스 커널 고유의 최적화 기법입니다

 


372
373         bl      __cpu_setup                     // initialise processor

 

  • 이 함수는 각 CPU 코어가 메모리 관리 장치(MMU)를 켜고 가상 주소 체계로 전환하기 전에, 주로 시스템 레지스터인 SCTLR_EL1, TCR_EL1, MAIR_EL1 등을 올바른 초기 상태로 설정하는 역할을 합니다.
  • 주 CPU(Boot CPU)도 부팅 가동 시 이 함수를 똑같이 호출하며, 보조 CPU들 역시 가상 메모리 세계로 진입하기 전에 이 과정을 반드시 거쳐야 합니다.

 

 


374         adrp    x1, swapper_pg_dir
375         adrp    x2, idmap_pg_dir
376         bl      __enable_mmu

  • swapper_pg_dir은 리눅스 커널의 가상 메모리 페이지 테이블의 최상위 디렉터리 주소입니다. 즉, 커널 공간(Kernel Space) 전체를 매핑하고 있는 시작점 주소를 x1에 담아 __enable_mmu 함수의 첫 번째 매개변수로 전달하는 것입니다.
  • idmap_pg_dir은 Identity Mapping 페이지 테이블의 주소입니다. 이는 "물리 주소 == 가상 주소"가 되도록 똑같이 1:1로 매핑해 둔 특수 페이지 테이블입니다.
  • MMU를 켜는 그 정점의 순간, 명령어 라인 몇 개는 가상 주소가 켜지기 전(물리 주소 세계)과 켜진 후(가상 주소 세계)에서 동일한 주소로 실행되어야 CPU가 꼬이지 않고 부드럽게 전환 될 수 있습니다. 이 과도기를 무사히 넘기기 위한 징검다리 지도의 주소를 x2에 담아 전달합니다.
  • __enable_mmu 내부에서는 전달받은 주소들을 시스템 레지스터(TTBR0_EL1, TTBR1_EL1)에 쓰고, 시스템 제어 레지스터(SCTLR_EL1)의 MMU Enable 비트(M 비트)를 1로 세팅합니다. 이 함수가 성공적으로 끝나고 복귀할 때, 보조 CPU는 더 이상 물리 주소가 아닌 가상 주소(Virtual Address) 모드로 동작하게 됩니다.

 

왜 두 개의 페이지 테이블(swapper와 idmap)이 필요할까?

ARM64 구조에서 MMU가 켜지면 프로세서는 두 개의 서로 다른 가상 주소 영역을 하드웨어적으로 완벽히 분리해 관리합니다.

  • TTBR0_EL1 (Translation Table Base Register 0): 주로 유저 공간이나 하위 주소 영역($0x0000_....) 변환에 쓰입니다. MMU가 켜지는 순간의 징검다리 역할을 위해 idmap_pg_dir이 여기에 설정됩니다.
  • TTBR1_EL1 (Translation Table Base Register 1): 상위 주소 영역($0xFFFF_....)인 커널 공간 변환에 쓰입니다. 리눅스 커널의 본체가 거주하는 공간의 지도인 swapper_pg_dir이 여기에 설정됩니다.

 

 


377         ldr     x8, =__secondary_switched
378         br      x8

  • _secondary_switched 루틴의 절대 주소(가상 주소 값)를 레지스터 리터럴 풀(Literal Pool)에서 읽어와 x8 레지스터에 저장합니다.
  • bl(Branch with Link)이 아닌 br(Branch to Register)을 사용했습니다. 돌아올 링크 레지스터(lr)를 남기지 않고, 이 어셈블리 루틴을 떠나 목적지로 완전히 넘어가겠다는 뜻입니다.
  • 이 명령어가 실행되는 순간, 보조 CPU는 물리 주소와 1:1 매핑되어 있던 과도기 상태(idmap)를 완전히 탈피하여, __secondary_switched 루틴으로 진입합니다. 그곳에서 스택 포인터(sp)를 설정하고 최종적으로 C 언어 함수인 secondary_start_kernel()을 호출하게 됩니다.


379 SYM_FUNC_END(secondary_startup)

 

381         .text
382 SYM_FUNC_START_LOCAL(__secondary_switched)
383         mov     x0, x20
384         bl      set_cpu_boot_mode_flag

  • x20 레지스터에 보관되어 있던 부팅 모드 값을 다시 x0 레지스터로 복사(이동)합니다.
  • 현재 보조 CPU가 어떤 모드(EL1 커널 모드 또는 EL2 하이퍼바이저 모드)로 시스템에 참여했는지 확인하여, 커널의 전역 변수나 플래그에 전역적으로 기록하는 역할을 합니다.

 


385
386         mov     x0, x20
387         bl      finalise_el2

  • x20에 들어있던 부팅 모드(cpu_boot_mode) 값을 다시 x0 레지스터로 복사합니다.
  • finalise_el2 내부에서는 이 보조 CPU가 하이퍼바이저 모드(EL2) 기능을 정상적으로 수행할 수 있도록 하드웨어 설정을 최종 확정(Finalise)하고, 실제 커널 코드가 돌아갈 EL1 상태와의 동기화를 마칩니다.


388
389         str_l   xzr, __early_cpu_boot_status, x3
390         adr_l   x5, vectors
391         msr     vbar_el1, x5
392         isb

 

  • __early_cpu_boot_status는 보조 CPU가 부팅되는 동안 자신의 상태(성공, 실패, MMU 문제 등)를 주 CPU(Boot CPU)에게 보고하기 위해 쓰이는 일종의 '통신용 플래그 변수'입니다.
  • 이제 보조 CPU가 MMU도 성공적으로 켰고 __secondary_switched 내부 진입까지 무사히 마쳤으므로, 이 플래그를 0(xzr = Zero Register)으로 깨끗이 클리어하여 "이 코어는 초기 어셈블리 단계의 부팅 단계를 에러 없이 무사히 통과했다"는 신호를 남기는 것입니다.
  • vectors는 리눅스 커널이 인터럽트(Interrupt), 시스템 콜(Syscall), 메모리 폴트(Page Fault) 등을 처리하기 위해 미리 만들어 둔 ARM64 예외 벡터 테이블의 시작 주소입니다.
  • vbar_el1 (Vector Base Address Register, EL1)은 CPU 하드웨어가 예외 상황을 맞닥뜨렸을 때 "어느 주소에 있는 코드를 실행해야 하는가?"를 가리키는 시스템 레지스터입니다.
  • 이 설정을 통해, 이제 이 보조 CPU에 타이머 인터럽트가 발생하거나 하드웨어 예외가 터지면 커널이 정의한 vectors 핸들러로 즉시 격리·처리될 수 있는 시스템적 방어벽이 완성됩니다.

 

393
394         adr_l   x0, secondary_data
395         ldr     x2, [x0, #CPU_BOOT_TASK]

  • 부팅 CPU가 새로 깨어날 세컨더리 CPU를 위해 미리 메모리에 준비해 둔 공유 데이터 영역입니다. 여기에는 이 CPU가 사용할 스택 포인터 주소, 태스크(프로세스) 정보, 페이지 테이블 정보 등이 담겨 있습니다.
  • [x0, #CPU_BOOT_TASK]: 방금 구한 secondary_data 주소(x0)로부터 CPU_BOOT_TASK라는 오프셋(거리)만큼 떨어진 위치의 데이터를 읽어 x2 레지스터에 넣습니다.
  • 의미: 이 CPU 코어가 운영체제 내에서 최초로 실행할 '태스크 구조체(task_struct)'의 주소를 가져옵니다. ARM64 리눅스 커널에서는 이 태스크 구조체 정보가 정상적으로 로드되어야만 커널 스택(sp)의 위치도 올바르게 계산해 낼 수 있습니다.

 

396         cbz     x2, __secondary_too_slow

 

  • cbz (Compare and Branch on Zero): 비교 대상 레지스터(x2)의 값이 0이면 지정된 레이블로 즉시 점프(Branch)하는 명령어입니다.
  • 의미: 만약 x2에 담긴 부팅 태스크 주소가 0(NULL)이라면, 무언가 문제가 생겨 메인 CPU가 데이터를 채워주기 전에 세컨더리 CPU가 너무 빨리 깨어났거나 동기화 타이밍을 놓친 상황입니다.
  • __secondary_too_slow 분기: 이 조건이 참이 되면 CPU는 정상 부팅을 중단하고 __secondary_too_slow라는 에러 처리 루틴으로 점프합니다. 

 


397
398         init_cpu_task x2, x1, x3

  • 방금 전 라인(395번 행)에서 로드한 부팅 태스크 정보(x2)를 바탕으로, 이 CPU 코어가 앞으로 사용할 '현재 실행 중인 프로세스 정보(Current Task)'와 '커널 스택(Kernel Stack)'을 CPU 하드웨어 레지스터에 바인딩하는 핵심 동작을 수행합니다.


399
400 #ifdef CONFIG_ARM64_PTR_AUTH
401         ptrauth_keys_init_cpu x2, x3, x4, x5
402 #endif

  • CONFIG_ARM64_PTR_AUTH: 리눅스 커널을 빌드할 때 하드웨어 포인터 인증(PAC) 보안 기능을 활성화했는지 체크합니다. 이 옵션이 켜져 있고, CPU 하드웨어가 이 기능을 지원할 때만 코드가 생성됩니다.
  • ptrauth_keys_init_cpu 매크로: 새로 깨어난 세컨더리 CPU 코어의 내부 특수 보안 레지스터에 포인터 인증용 비밀 키(Cryptographic Keys)를 주입하는 매크로입니다. 중간 주소 계산 및 데이터 로드를 위해 x2, x3, x4, x5 레지스터가 매개변수로 사용됩니다.

포인터 인증(Pointer Authentication)이란?

ARM64 아키텍처에서 포인터 인증(PAC)은 메모리 주소(포인터)의 남는 상위 비트에 암호화된 인증 코드(PAC, Pointer Authentication Code)를 심어두는 기술입니다.

  1. 인증 코드 생성: 함수가 호출될 때, CPU는 메모리에 저장될 돌아갈 주소(Return Address)와 시스템 고유의 비밀 키를 조합하여 암호화 코드를 만든 뒤 포인터 상위 비트에 숨겨둡니다.
  2. 검증: 함수가 끝나고 돌아갈 때, CPU는 이 상위 비트의 암호화 코드가 유효한지 다시 검사합니다.
  3. 공격 차단: 만약 해커가 버퍼 오버플로우 공격 등으로 메모리에 있는 리턴 주소를 강제로 위변조했다면, 이 암호화 인증 코드가 깨지게 됩니다. CPU는 이를 즉시 감지하고 시스템을 안전하게 크래시(Fault)시켜 해커가 악성 코드를 실행하지 못하도록 원천 차단합니다.

이 매크로가 실행되면서, 새로 켜진 세컨더리 CPU도 메인 CPU와 마찬가지로 커널 코드를 실행할 때 이 암호화 키를 활용해 스스로를 보호할 수 있게 됩니다.

 

 


403
404         bl      secondary_start_kernel

  • bl (Branch with Link): 이 명령어를 통해 어셈블리 코드의 세계를 뒤로하고, C 언어로 작성된 리눅스 커널의 핵심 함수인 secondary_start_kernel()을 호출합니다.


405         ASM_BUG()

  • ASM_BUG() 라인이 실행되었다는 것은 secondary_start_kernel 함수가 도중에 알 수 없는 치명적인 오류로 튕겨 나옵니다(리턴되었습니다)를 의미합니다. 즉, 커널이 도저히 손쓸 수 없는 심각한 버그 상태이므로 시스템을 즉시 Panic시키는 역할을 합니다.


406 SYM_FUNC_END(__secondary_switched)


 

408 SYM_FUNC_START_LOCAL(__secondary_too_slow)
409         wfe
410         wfi
411         b       __secondary_too_slow

 

  • wfe (Wait For Event): ARM 아키텍처의 대표적인 저전력 가동 중지 명령어입니다.
  • 동작: CPU 코어를 즉시 저전력 스탠바이(휴면) 상태로 진입시킵니다. 이 상태의 CPU는 다른 CPU가 sev(Send Event) 명령어를 날려 이벤트를 발생시키기 전까지는 깨어나지 않고 잠들게 됩니다.
  • wfi (Wait For Interrupt): 또 다른 저전력 가동 중지 명령어입니다.
  • 동작: CPU를 하드웨어 인터럽트(소프트웨어/외부 신호 등)가 발생할 때까지 잠들게 만듭니다.
  • 왜 둘 다 쓰나? wfe나 wfi 중 어떤 이유로든 CPU가 예기치 않게 잠에서 깨어날 가능성이 있습니다. 커널은 이 세컨더리 CPU가 확실하게 아무것도 하지 못하도록 두 겹의 저전력 대기 명령어를 촘촘히 배치해 둔 것입니다.

412 SYM_FUNC_END(__secondary_too_slow)

 

ARM64 세컨더리 CPU 부팅 시퀀스 총정리

[물리 주소 세계 (Physical Address)]
 1. secondary_entry       -> 부팅 모드(EL1/EL2) 확인 및 보존
 2. secondary_startup     -> 52비트 주소 검증, 내부 하드웨어 세팅 (__cpu_setup)
                             페이지 테이블 세팅 후 MMU 활성화 (__enable_mmu)
          │
          ▼ [가상 주소 세계로 점프 (Virtual Address)]
 3. __secondary_switched  -> 예외 처리 벡터 테이블 등록 (vbar_el1)
                             공유 데이터에서 나만의 스택/태스크 정보 할당 (init_cpu_task)
                             하드웨어 포인터 인증 보안 키 설정 (ptrauth)
          │
          ▼ [C 언어 커널 세계로 완전 진입]
 4. secondary_start_kernel -> 타이머/인터럽트 구동, 스케줄러 합류 후 유저 앱 실행!

 

'linux' 카테고리의 다른 글

6.12/set_task_stack_end_magic()  (0) 2026.06.17
6.18/head.S  (0) 2025.12.07
6.1/early_ioremap_init(void)  (0) 2025.03.29
6.1/setup_arch(char **cmdline_p)  (0) 2024.04.02
6.1/fixmap_remap_fdt(phys_addr_t dt_phys, int *size, pgprot_t prot)  (0) 2023.12.19

 

 

 

 21 /*
 22  * Must be called after early_fixmap_init
 23  */
 24 void __init early_ioremap_init(void)
 25 {
 26         early_ioremap_setup();
 27 }

 

 71 void __init early_ioremap_setup(void)
 72 {
 73         int i;
 74
 75         for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
 76                 if (WARN_ON(prev_map[i]))
 77                         break;
 78
 79         for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
 80                 slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
 81 }

 

위 그림의 BTMAPS 영역을 slot_virt[]에 할당

'linux' 카테고리의 다른 글

6.18/head.S  (0) 2025.12.07
6.12/head.S  (0) 2025.06.14
6.1/setup_arch(char **cmdline_p)  (0) 2024.04.02
6.1/fixmap_remap_fdt(phys_addr_t dt_phys, int *size, pgprot_t prot)  (0) 2023.12.19
6.1/early_fixmap_init(void)  (0) 2023.12.18

+ Recent posts