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
'OS > Linux' 카테고리의 다른 글
[Linux Kernel] Kernel 분석(v5.14.16) - Device tree (0) | 2021.11.25 |
---|---|
[Linux Kernel] Kernel 분석(v5.14.16) - entry.S (0) | 2021.11.14 |
[Linux Kernel] DT (디바이스 트리) (0) | 2021.01.09 |
[Linux] 메모리 영역 (2) - Heap (0) | 2020.12.26 |
[Linux] 메모리 영역 (1) - Code, Data, Stack (0) | 2020.12.20 |