head.S 커널의 시작을 trigger하는 부트로더 코드이다. 기본적인 CPU 등을 초기화하고 커널을 본격적으로 start 한다.

 

가장먼저 코드는 아래와 같이 시작한다.

 

__HEAD
/*
 * DO NOT MODIFY. Image header expected by Linux boot-loaders.
 */
efi_signature_nop                        // special NOP to identity as PE/COFF executable
b        primary_entry                        // branch to kernel start, magic
.quad        0                                // Image load offset from start of RAM, little-endian
le64sym        _kernel_size_le                        // Effective size of kernel image, little-endian
le64sym        _kernel_flags_le                // Informative flags, little-endian
.quad        0                                // reserved
.quad        0                                // reserved
.quad        0                                // reserved
.ascii        ARM64_IMAGE_MAGIC                // Magic number
.long        .Lpe_header_offset                // Offset to the PE header.

 

__HEAD

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

.head.text 메모리 section임을 얘기한다. 해당 section 대해 아래 코드를 진행한다.

 

#define ARM64_IMAGE_MAGIC        "ARM\x64"

ARM64_IMGAE_MAGIC ARM64 사용을 의미하기위한 정의 값이다.

 

SYM_CODE_START(primary_entry)
        
bl        preserve_boot_args
        
bl        init_kernel_el                        // w0=cpu_boot_mode
        
adrp        x23, __PHYS_OFFSET
        
and        x23, x23, MIN_KIMG_ALIGN - 1        // KASLR offset, defaults to 0
        
bl        set_cpu_boot_mode_flag
        
bl        __create_page_tables
        
/*
         * The following calls CPU setup code, see arch/arm64/mm/proc.S for
         * details.
         * On return, the CPU will be ready for the MMU to be turned on and
         * the TCR will have been set.
         */
        
bl        __cpu_setup                        // initialise processor
        
b        __primary_switch
SYM_CODE_END(primary_entry)

kernel start를위한 어셈블리어 코드이다 가장 먼저 SYM_CODE_START(primary_entry) 통해서 primary_entry 심볼을 링커에게 제공하고 정렬한다.

결과적으로 primary_entry 대하여 SYM_CODE_START, SYM_CODE_END 통해 명확히 시작과 끝을 명시한다. 그리고 linker 해당 macro값을 바탕으로 prmiary_entry 연결할 있다.(그냥 하나의 section의시작과 끝정도로 생각하자)

 

bl        preserve_boot_args
SYM_CODE_START_LOCAL(preserve_boot_args)
mov        x21, x0                                // x21=FDT

adr_l        x0, boot_args                        // record the contents of
stp        x21, x1, [x0]                        // x0 .. x3 at kernel entry
stp        x2, x3, [x0, #16]

dmb        sy                                // needed before dc ivac with
                                                
// MMU off

add        x1, x0, #0x20                        // 4 x 8 bytes
b        dcache_inval_poc                // tail call
SYM_CODE_END(preserve_boot_args)

부트로더에의해 전달된 파라미터 값을 저장한다.

init_kernel_el는 커널을 EL2 시작하겠다는 의미이다. Kernel 참고로 EL1 EL2 시작할 가있는데 여기서는 EL2 시작하는 것이다. 기본적으로 AArch64에서 하이퍼바이저로 동작할 때는 EL2 시작하고 아닌 경우에는 EL1으로 Kernel 시작된다.

 

adrp        x23, __PHYS_OFFSET
SYM_FUNC_START(init_kernel_el)
        
mrs        x0, CurrentEL
        
cmp        x0, #CurrentEL_EL2
        
b.eq        init_el2

__PHYS_OFFSET x23 레지스터에 저장하는 주소값 저장 명령문이다 여기서 __PHYS_OFFSET KERNEL_START 정의되어있다. 커널 이미지의 주소를 x23 레지스터에 저장해준다. 커널 이미지는 아래와 같이 커널 start 위한 코드가 들어있다.

Kernel start 위해 CurrentEL 시스템 값을 x0 register 저장하고 비교하는데 이는 CUrrentEL_EL2 x0 레지스터에 정상적으로 EL2 모드로 setup됬는지 확인하는 부분이다.

 

and        x23, x23, MIN_KIMG_ALIGN - 1        // KASLR offset, defaults to 0

KERNEL image 2M base Address 가지기 때문에 0x00200000 AND 연산을 진행해서 x23 다시 저장한다.

SYM_FUNC_START_LOCAL(set_cpu_boot_mode_flag)
        
adr_l        x1, __boot_cpu_mode
        
cmp        w0, #BOOT_CPU_MODE_EL2
        
b.ne        1f
        
add        x1, x1, #4
1:        str        w0, [x1]                        // Save CPU boot mode
        
dmb        sy
        
dc        ivac, x1                        // Invalidate potentially stale cache line
        
ret
SYM_FUNC_END(set_cpu_boot_mode_flag)

다음으로는 cpu 관련 정보를 setup하는 부분이다. 부트로더에서는 처음 언급한것과 같이 kernel image 로드와 cpu등의 기본적인 부분의 setup 기본이다.

먼저 __boot_cpu_mode 해당 커널이 EL2 부팅했는지 EL1으로 부팅했는지에 대한 정보를 가지고 있다. x1레지스터에 해당 값을 저장해준다.

마찬가지로 EL2이냐 EL1이냐에따라서 add x1 x1 수해앟고 결과적으로 wo 저장한다. str(레지스터 x1 값을 메모리 w0 저장)

dmb sy 캐시 버퍼를 clear해주는 역할을 진행한다.

 

SYM_FUNC_START_LOCAL(__create_page_tables)

SYM_FUNC_END(_create_page_tables)

부분은 MMU memory mapping 하는 작업이다. 기본적으로 부트로더가 시작될 때는 MMU 꺼져있고 physical Address 바로 연결된다. 그리고 과정을 거쳐서 virtual address maaping이된다.

가장먼저 이후 exception발생을 막기위해 MMU 진행하기전에 전체적으로 clear 해준다.

이후 identity mapping 시작하게되는데 identity mapping 바로 physical virtual 같은주소로 1:1 매핑해주는 동작이다. 이러한 매핑방법을 idmap이라고 한다.

 

여기서 잠깐 ARM 메모리 매핑 방법에 대해서 알아보면 3가지로 구분된다

가장 먼저 linear 방법으로 virtual + N = physical 같은 방법으로 기본적으로 page table없이도 N값만알면 매핑을 있다.

그리고 Non-linear 방법으로 page table방법이다. page table(virtual) = physical 얻는 방법이다.

마지막으로 idmap방법인다 virtual=physical 방법으로 MMU 사용되지 않는 boot 초반 같은 경우에 사용한다.

 

 

mov        x7, SWAPPER_MM_MMUFLAGS

Page Table dirty cache 모두 초기화해주고 x7 레지스터에 미리 정의된 MMU flag set 저장한다. flag값이 너무 용량이 커지게되면 mov명령으로 레지스터에 저장하는 것이 불가능하고 ldr 사용해야 한다.

 

#if ARM64_KERNEL_USES_PMD_MAPS
#define SWAPPER_MM_MMUFLAGS        (PMD_ATTRINDX(MT_NORMAL) | SWAPPER_PMD_FLAGS)
#else
#define SWAPPER_MM_MMUFLAGS        (PTE_ATTRINDX(MT_NORMAL) | SWAPPER_PTE_FLAGS)
#endif

SWAP FLAG값은 PMD MAP 사용하냐 안하냐에 따라 위와같이 다르게 정의된다.

여기서 PMD, PTE, PGD같은 값은 리눅스 페이지 테이블이 어떤 구조로되어있는지를 말한다. 최근 리눅스 페이지 메이핑의 경우 4단계로 구분되어져있으며 각각의 단계를 거쳐서 physical address 찾아간다(2단계로 설정할 수도 있다)

pmd_table = pgd_table[pgd_offset] & PAGE_MASK
pte_table = pmd_table[pmd_offset] & PAGE_MASK
phys_page = pte_table[pte_offset] & PAGE_MASK
phys_addr = phys_page[page_offset]

리눅스 physical address 찾는 과정이다. 흔히 process 다루는 주소 가상 논리주소 값이 pgd address값이고 값을 통해 pgd_table 거쳐 pmd 값을 pmd 거쳐 pte, pte 거쳐서 physical_page 얻고 마지막으로 physical addresss 얻는 것이다.

 

 

#ifdef CONFIG_ARM64_VA_BITS_52
        
mrs_s        x6, SYS_ID_AA64MMFR2_EL1
        
and        x6, x6, #(0xf << ID_AA64MMFR2_LVA_SHIFT)
        
mov        x5, #52
        
cbnz        x6, 1f
#endif
        
mov        x5, #VA_BITS_MIN

virtual address사용 bit 얼마인지 체크하고 bit 맞춰 RAM영역을 mapping 있도록 설정해주는 부분이다.

bit52 지원하는 경우 x5 레지스터에 52 값을 넣어주고 아닌 경우 사용하는 bit값을 x5레지스터에 저장해주고 있다.

 

         * Calculate the maximum allowed value for TCR_EL1.T0SZ so that the
         * entire ID map region can be mapped. As T0SZ == (64 - #bits used),
         * this number conveniently equals the number of leading zeroes in
         * the physical address of __idmap_text_end.
         */
        
adrp        x5, __idmap_text_end
        
clz        x5, x5
        
cmp        x5, TCR_T0SZ(VA_BITS_MIN) // default T0SZ small enough?
        
b.ge        1f                        // .. then skip VA range extension

adr_l        x6, idmap_t0sz
        
str        x5, [x6]
        
dmb        sy
        
dc        ivac, x6                // Invalidate potentially stale cache line

다음으로 메모리 변환이 정상적으로 이루어질 있도록 TCR 레지스터에 대한 설정을 진행하는 부분이다. TCR_T0SZ 통해서 현재 변환레지스터 부분이 정상적으로 메모리 변환이 일어날 있는 만큼 공간이 충분한지 확인하고 만약 불가능한 경우 idmap_t0sz 통해서 레지스터에 다시 값을 넣어주고 있다.

 

map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14
/*

 * Map memory for specified virtual address range. Each level of page table needed supports
 * multiple entries. If a level requires n entries the next page table level is assumed to be
 * formed from n pages.
 *
 *        tbl:        location of page table
 *        rtbl:        address to be used for first level page table entry (typically tbl + PAGE_SIZE)
 *        vstart:        virtual address of start of range
 *        vend:        virtual address of end of range - we map [vstart, vend - 1]
 *        flags:        flags to use to map last level entries
 *        phys:        physical address corresponding to vstart - physical memory is contiguous
 *        pgds:        the number of pgd entries
 *
 * Temporaries:        istart, iend, tmp, count, sv - these need to be different registers
 * Preserves:        vstart, flags
 * Corrupts:        tbl, rtbl, vend, istart, iend, tmp, count, sv
 */
        
.macro map_memory, tbl, rtbl, vstart, vend, flags, phys, pgds, istart, iend, tmp, count, sv
        
sub \vend, \vend, #1
        
add \rtbl, \tbl, #PAGE_SIZE
        
mov \sv, \rtbl
        
mov \count, #0
        
compute_indices \vstart, \vend, #PGDIR_SHIFT, \pgds, \istart, \iend, \count
        
populate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmp
        
mov \tbl, \sv
        
mov \sv, \rtbl

map_memory 위와 같이 실제적으로 va 범위, phys 값등을 인자로 MMU 생성하는 매크로이다.

지금까지 계산한 TCR 레지스터, 그리고 idmap 시작주소 주소 등을 통해서 MMU 생성하고 있다. 여기서 idmap으로 생성하기 때문에 실제물리주소와 가상주소는 1:1 매핑으로 같은 시작 주소 값을 가지는 것을 있다.

 

        adrp        x0, init_pg_dir
        
mov_q        x5, KIMAGE_VADDR                // compile time __va(_text)
        
add        x5, x5, x23                        // add KASLR displacement
        
mov        x4, PTRS_PER_PGD
        
adrp        x6, _end                        // runtime __pa(_end)
        
adrp        x3, _text                        // runtime __pa(_text)
        
sub        x6, x6, x3                        // _end - _text
        
add        x6, x6, x5                        // runtime __va(_end)

map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14

Kernel image 매핑 해주는 코드이다. 해당 코드는 Swapper Page Table이라고도 한다 Swapper page table 커널이미지를 위한 매핑 테이블이다.

KIMGAE_VADDR 커널이미지의주소 값을 가리키는 값이다.

 

 

bl        __cpu_setup
...
SYM_FUNC_START(__cpu_setup)
mov_q        mair, MAIR_EL1_SET
mov_q        tcr, TCR_TxSZ(VA_BITS) | TCR_CACHE_FLAGS | TCR_SMP_FLAGS | \
                        
TCR_TG_FLAGS | TCR_KASLR_FLAGS | TCR_ASID16 | \
                        
TCR_TBI0 | TCR_A1 | TCR_KASAN_SW_FLAGS

다음으로는 cpu setup부분인데 부분은 cpu 셋업과 MMU Table 매핑을위한 설정 값들을 설정하는 동작을 수행한다.

mair 통해서 페이지 테이블 매핑 영역 특성에 따른 속성 세트 인덱스를 기록하고 tcr 통해서 MMU 주소 변환 기능을 제어하는 TCR 레지스터의 설정 값을 만든다.

이렇게 되면 MMU 활성화를 위한 모든 작업이 마무리 것이다.

 

b        __primary_switch
#ifdef CONFIG_RANDOMIZE_BASE
        
mov        x19, x0                                // preserve new SCTLR_EL1 value
        
mrs        x20, sctlr_el1                        // preserve old SCTLR_EL1 value
#endif

bl        __enable_mmu

RANDOMIZE 지원하는지 체크하고 지원하는 경우 위왁 같이 SCTLR_EL1 값을 x19, x20 저장해준다.

이후 MMU 드디어 활성화 하기위해 __enable_mmu 코드로 점프한다.

 

        mrs        x2, ID_AA64MMFR0_EL1
        
ubfx        x2, x2, #ID_AA64MMFR0_TGRAN_SHIFT, 4
        
cmp     x2, #ID_AA64MMFR0_TGRAN_SUPPORTED_MIN
        
b.lt    __no_granule_support
        
cmp     x2, #ID_AA64MMFR0_TGRAN_SUPPORTED_MAX
        
b.gt    __no_granule_support

MMFR레지스터의 Translation Granule필드르 보고 커널에서 설정한 페이지 크기가 지원 가능한지 체크하는 부분이다. 만약 지원이 불가능하다면 아래 코드로 점프가 되고 부팅 실패하게 된다.
SYM_FUNC_START_LOCAL(__no_granule_support)
        
/* Indicate that this CPU can't boot and is stuck in the kernel */
        
update_early_cpu_boot_status \
                
CPU_STUCK_IN_KERNEL | CPU_STUCK_REASON_NO_GRAN, x1, x2
1:
        
wfe
        
wfi
        
b        1b
SYM_FUNC_END(__no_granule_support)
update_early_cpu_boot_status 0, x2, x3

만약 위의 MMFR 레지스터 검증 결과가 정상이면 매크로가 실행되게되고 이렇게되면 CPU 정상 부팅했음을 기록하게된다.

        adrp        x2, idmap_pg_dir
        
phys_to_ttbr x1, x1
        
phys_to_ttbr x2, x2
        
msr        ttbr0_el1, x2                        // load TTBR0
        
offset_ttbr1 x1, x3
        
msr        ttbr1_el1, x1                        // load TTBR1
        
isb

마지막으로 page table 접근할 있도록 page table 주소 값을 레지스터에 set up해주는 작업이다.

page table physical 주소 값을 phys_to_ttbr 매크로를 통해서 얻어오고 ttbr_el0, ttbr_el1 각각의 table 주소 값을 저장한다.

마지막으로 isb arm 명령어로 pipe line clear해주는 역할을 한다.

SYM_FUNC_START_LOCAL(__primary_switched)
        
adr_l        x4, init_task
        
init_cpu_task x4, x5, x6

adr_l        x8, vectors                        // load VBAR_EL1 with virtual
        
msr        vbar_el1, x8                        // vector table address
        
isb

stp        x29, x30, [sp, #-16]!
        
mov        x29, sp

str_l        x21, __fdt_pointer, x5                // Save FDT pointer
ldr_l        x4, kimage_vaddr                // Save the offset between
        
sub        x4, x4, x0                        // the kernel virtual and
        
str_l        x4, kimage_voffset, x5                // physical mappings

// Clear BSS
        
adr_l        x0, __bss_start
        
mov        x1, xzr
        
adr_l        x2, __bss_stop
        
sub        x2, x2, x0
        
bl        __pi_memset
        
dsb        ishst                                // Make zero page visible to PTW

마지막으로 kernel 시작하기 전에 메모리 영역 레지스터 값들을 초기화 하는 동작이다. 해당 동작은 반드시 MMU활성화 진행해야 한다.

가장 먼저 VBAR_EL1 값을 설정해준다. 해당 값은 EL1 exception 발생하면 VBAR_EL1 찾아가서 Exception상황에 맞는 동작을 수행한다. EL1 exception 대한 Table주소 값이 VBAR_EL1 저장된다. (EL 레벨에 따른 Exception table주소 값을 저장한 벡터들이 각각 존재한다)

 

다음으로는 MMU이전에 physical address값으로 저장했던 커널이미지 주소 값을 MMU활성화 되었기 때문에 계산해서 virtual address 변경해주고 bss 영역을 clear 할당한다.

#if defined(CONFIG_KASAN_GENERIC) || defined(CONFIG_KASAN_SW_TAGS)
        
bl        kasan_early_init
#endif

kasan_early_init() 경우 kasan 메모리를 할당 매핑 해준다. kasan kernel addresss sanitizer kernel에서 커널 개발자가 런타임 관련 메모리 버그를 찾을 있게 도와주는 역할을 해주는 툴이다. 기본적으로 성능저하 때문에 release 커널에는 사용되지 않으나 디버깅 테스트용으로는 유용하다. 위와 같이 KASAN관련 feature on해줌으로써 진행할 있다.

        mov        x0, x21                                // pass FDT address in x0
        
bl        early_fdt_map                        // Try mapping the FDT early
        
bl        init_feature_override                // Parse cpu feature overrides
#ifdef CONFIG_RANDOMIZE_BASE
        
tst        x23, ~(MIN_KIMG_ALIGN - 1)        // already running randomized?
        
b.ne        0f
        
bl        kaslr_early_init                // parse FDT for KASLR options
        
cbz        x0, 0f                                // KASLR disabled? just proceed
        
orr        x23, x23, x0                        // record KASLR offset
        
ldp        x29, x30, [sp], #16                // we must enable KASLR, return
        
ret                                        // to __primary_switch()
0:
#endif

FDT device tree라고 생각하면된다. device접근을위해 FDT 주소 값을 초기화 해준다. 맨 처음에 파라미터로 받아와서 x21레지스터에 저장했던 FDT값을 다시 x0로 옮기고 매핑한다. 여기서 만약 RANDOMIZE feature on이라면 KASLR 이용한다 KASLR Base Addresss Random화를 통해서 주소 값을 보호해주는 역할이다.

        bl        switch_to_vhe                        // Prefer VHE if possible
        
ldp        x29, x30, [sp], #16
        
bl        start_kernel
        
ASM_BUG()
SYM_FUNC_END(__primary_switched)

마지막으로 bl start_kernel 통해서 C언어 kernel 코드로 진입한다.

 

 

 

  • ref

https://elixir.bootlin.com/linux/v5.14.16/source/arch/arm64/kernel/head.S

https://blog.daum.net/tlos6733/174

https://elixir.bootlin.com/linux/v5.14.16

https://github.com/lorenzo-stoakes/linux-gorman-book-notes/blob/master/3.md>

코드로 알아보는 ARM 리눅스 커널

https://source.android.google.cn/devices/tech/debug/kasan-kcov?hl=ko

+ Recent posts