2.2 Processor initialization (Linux)
2.2: Processor initialization (Linux)
리눅스 커널의 설명을 stext 함수에서 멈췄었습니다. stext 함수는 arm64
아키텍처의 엔트리 포인트 입니다. 이번에는 좀더 깊게 들어가 이전 레슨에서 이미 수행한 코드와 비슷한 점을 찾아보겠습니다.
이번 챕터에서는 대부분 다른 ARM 시스템 레지스터를 논의하고 그들이 리눅스 커널에서 어떻게 사용되는지 논의하기 때문에 조금 지루하다고 생각할 수 있습니다. 그러나 다음의 이유로 이번 챕터가 매우 중요하다고 생각합니다:
- 하드웨어가 소프트웨어에게 제공하는 인터페이스를 이해하는 것이 필요하다. 이러한 인터페이스를 아는 것으로 많은 경우에서 특정 커널 기능이 어떻게 구현되는지 분해해보고 소프트웨어와 하드웨어가 이 기능을 구현하기 위해 어떻게 연합하는지 분해가 가능합니다.
- 시스템 레지스터에서 다른 옵션들은 다양한 하드웨어 기능을 활성화/비활성화 하는 것과 연관이 있습니다. 만약 ARM 프로세서가 가지는 시스템 레지스터의 차이를 배운다면 어떤 종류의 기능들을 지원하는지 이미 알고 있는 것이 됩니다.
이제 stext
함수를 다시 살펴봅시다.
ENTRY(stext)
bl preserve_boot_args
bl el2_setup // Drop to EL1, 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
ENDPROC(stext)
preserve_boot_args
preserve_boot_args 함수는 부트로더에 의해서 커널로 전달되는 매개 변수들을 저장하는 역할을 맡습니다.
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
mov x1, #0x20 // 4 x 8 bytes
b __inval_dcache_area // tail call
ENDPROC(preserve_boot_args)
kernel boot protocol 에 따르면 매개변수들은 레지스터 x0 - x3
안에서 커널로 전달된다. x0
은 시스템 RAM에서 디바이스 트리 블랍(.dtb
)의 물리적 주소를 가지고 있습니다. x1 - x3
는 나중에 사용을 위해 예약되 있습니다. 이 함수가 하는 일은 x0 - x3
레지스터의 내용을 boot_args 배열로 복사하고 데이터 캐시에서 해당하는 캐시 라인을 invalidate합니다. 멀티 프로세서 시스템에서 캐시 관리는 자체로 큰 주제이고 지금은 생략할 것입니다. 이 주제에 관심있는 분들에게 ARM Programmer's Guide
의 챕터 Caches와 Multi-core processors 를 읽어보기를 권장합니다.
el2_setup
arm64boot protocol 에 따르면 커널은 EL1과 EL2 둘다에서 부팅될 수 있습니다. 두 번째 경우에서 커널은 가상화 확장에 접근을 하고 호스트 운영체제로 동작할 수 있습니다. 만약 EL2로 부팅될 만큼 운이 좋다면, el2_setup 함수가 호출될 것입니다. 오로지 EL2에서만 접근 가능한 다른 매개변수를 설정하는 역할을 맡고 EL1으로 떨어트립니다. 이제 이 함수를 작은 부분으로 나누고 각 부분을 하나씩 설명할 것입니다.
msr SPsel, #1 // We want to use SP_EL{1,2}
할당된 스택 포인터는 EL1과 EL2를 위해 사용될 것입니다. 다른 옵션은 EL0에서 스택 포인터를 재사용하는 것입니다.
mrs x0, CurrentEL
cmp x0, #CurrentEL_EL2
b.eq 1f
오로지 현재 EL이 EL2인 경우에만 라벨 1
로 뻗어나가고 다른 경우에는 EL2 셋업을 할 수 없고 이 함수를 수행하게 됩니다.
mrs x0, sctlr_el1
CPU_BE( orr x0, x0, #(3 << 24) ) // Set the EE and E0E bits for EL1
CPU_LE( bic x0, x0, #(3 << 24) ) // Clear the EE and E0E bits for EL1
msr sctlr_el1, x0
mov w0, #BOOT_CPU_MODE_EL1 // This cpu booted in EL1
isb
ret
만약 이게 실행되면, sctlr_el1
레지스터는 갱신되고 CPU_BIG_ENDIAN 설정 값에 따라서 big-endian
또는 little-endian
에서 CPU가 동작하게 됩니다. 그 후 el2_setup
함수에서 벗어나고 BOOT_CPU_MODE_EL1 상수를 반환합니다.
ARM64 Function Calling Conventions에 따르면 반환 값은 x0
레지스터에 위치해야 합니다.(또는 우리의 경우에는 w0
. w0
레지스터를 32 비트의 x0
으로 생각할 수 있다.)
1: mrs x0, sctlr_el2
CPU_BE( orr x0, x0, #(1 << 25) ) // Set the EE bit for EL2
CPU_LE( bic x0, x0, #(1 << 25) ) // Clear the EE bit for EL2
msr sctlr_el2, x0
만약 EL2에서 부팅되는 것으로 보이면 EL2를 위한 비슷한 종류의 설정을 해줍니다.(sctlr_el2
레지스터는 sctlr_el1
대신에 사용됩니다.)
#ifdef CONFIG_ARM64_VHE
/*
* Check for VHE being present. For the rest of the EL2 setup,
* x2 being non-zero indicates that we do have VHE, and that the
* kernel is intended to run at EL2.
*/
mrs x2, id_aa64mmfr1_el1
ubfx x2, x2, #8, #4
#else
mov x2, xzr
#endif
만약 Virtualization Host Extensions (VHE)가 ARM64_VHE 설정 변수에 의해서 활성화 되고 호스트 머신이 이것을 지원한다면 x2
는 0이 아닌 값으로 갱신됩니다. x2
는 나중에 같은 함수에서 VHE
가 활성화 됬는지 확인하는 것에 사용됩니다.
mov x0, #HCR_RW // 64-bit EL1
cbz x2, set_hcr
orr x0, x0, #HCR_TGE // Enable Host Extensions
orr x0, x0, #HCR_E2H
set_hcr:
msr hcr_el2, x0
isb
여기서 hcr_el2
레지스터를 설정합니다. RPi OS에서 EL1을 위한 64-bit 실행 모드를 설정하기 위해 같은 레지스터를 사용합니다. 주어진 코드 샘플의 첫 번째 라인에서 수행되는 일입니다. 또한 만약 x2 != 0
, 이것은 VHE가 가능하고 커널이 사용하도록 설정된 것을 의미합니다,이면 hcr_el2
는 VHE를 활성화 하기 위해 사용될 것입니다.
/*
* Allow Non-secure EL1 and EL0 to access physical timer and counter.
* This is not necessary for VHE, since the host kernel runs in EL2,
* and EL0 accesses are configured in the later stage of boot process.
* Note that when HCR_EL2.E2H == 1, CNTHCTL_EL2 has the same bit layout
* as CNTKCTL_EL1, and CNTKCTL_EL1 accessing instructions are redefined
* to access CNTHCTL_EL2. This allows the kernel designed to run at EL1
* to transparently mess with the EL0 bits via CNTKCTL_EL1 access in
* EL2.
*/
cbnz x2, 1f
mrs x0, cnthctl_el2
orr x0, x0, #3 // Enable EL1 physical timers
msr cnthctl_el2, x0
1:
msr cntvoff_el2, xzr // Clear virtual offset
다음 코드는 위의 코멘트에서 잘 설명되 있습니다. 추가할 것이 따로 없습니다.
#ifdef CONFIG_ARM_GIC_V3
/* GICv3 system register access */
mrs x0, id_aa64pfr0_el1
ubfx x0, x0, #24, #4
cmp x0, #1
b.ne 3f
mrs_s x0, SYS_ICC_SRE_EL2
orr x0, x0, #ICC_SRE_EL2_SRE // Set ICC_SRE_EL2.SRE==1
orr x0, x0, #ICC_SRE_EL2_ENABLE // Set ICC_SRE_EL2.Enable==1
msr_s SYS_ICC_SRE_EL2, x0
isb // Make sure SRE is now set
mrs_s x0, SYS_ICC_SRE_EL2 // Read SRE back,
tbz x0, #0, 3f // and check that it sticks
msr_s SYS_ICH_HCR_EL2, xzr // Reset ICC_HCR_EL2 to defaults
3:
#endif
다음 코드는 오로지 GICv3이 가능할 때만 실행됩니다. GIC는 Generic Interrupt Controller를 의미합니다. GIC의 v3 버전은 가상화 문맥에 특히 유용한 몇가지 기능을 추가했습니다. 예를 들어 GICv3가 있으면 LPIs(Locality-specific Peripheral Interrupt)를 갖는 것이 가능해집니다. 이러한 인터럽트는 메세지 버스에 의해 전달되고 설정은 메모리에 특별한 테이블에서 유지됩니다.
주어진 코드는 SRE(System Register interface)를 활성화하는 역할을 합니다. 이 단계는 ICC_*_ELn
레지스터를 사용하기 전에 수행되어야 하고 GICv3 기능을 사용합니다.
/* Populate ID registers. */
mrs x0, midr_el1
mrs x1, mpidr_el1
msr vpidr_el2, x0
msr vmpidr_el2, x1
midair_el1
과 mpidr_el1
은 identification registers group으로 부터 읽기만 가능한 레지스터 입니다. 이것들은 프로세서 제조사, 프로세서 아키텍처 이름, 코어 수 그리고 다른 정보들에 대해 다양한 정보를 제공합니다. EL1에서 접근하려는 모든 읽는 것들을 위해서 이 정보를 수정하는 것도 가능합니다. 여기서 vpidr_el2
와 vmpidr_el2
를 mdir_el1
과 mpidr_el1
에서 가져온 값으로 덧붙여 EL1이나 더 높은 예외 레벨에서 접근하든 정보가 같게 됩니다.
#ifdef CONFIG_COMPAT
msr hstr_el2, xzr // Disable CP15 traps to EL2
#endif
프로세서가 32 비트 실행 모드에서 실행될 때 “coprocessor”의 개념이 있습니다. corprocessor는 시스템 레지스터에 의해서 보통 접근되는 64 bit 실행모드에 정보에 접근하기 위해서 사용됩니다. 공식 문서에서 corprocessor를 통해 접근 가능한 것이 정확히 어떤 것인지 읽을 수 있습니다. msr hstr_el2, xzr
명령어는 하위 예외 레벨에서 corprocessor를 사용할 수 있게 해줍니다. 오로지 compatiblity 모드가 활성화 됬을 때만 가능합니다.(이 모드에선느 커널이 64 bit 커널 위에서 32 bit 유저 프로그램을 돌릴 수 있게 됩니다)
/* EL2 debug */
mrs x1, id_aa64dfr0_el1 // Check ID_AA64DFR0_EL1 PMUVer
sbfx x0, x1, #8, #4
cmp x0, #1
b.lt 4f // Skip if no PMU present
mrs x0, pmcr_el0 // Disable debug access traps
ubfx x0, x0, #11, #5 // to EL2 and allow access to
4:
csel x3, xzr, x0, lt // all PMU counters from EL1
/* Statistical profiling */
ubfx x0, x1, #32, #4 // Check ID_AA64DFR0_EL1 PMSVer
cbz x0, 6f // Skip if SPE not present
cbnz x2, 5f // VHE?
mov x1, #(MDCR_EL2_E2PB_MASK << MDCR_EL2_E2PB_SHIFT)
orr x3, x3, x1 // If we don't have VHE, then
b 6f // use EL1&0 translation.
5: // For VHE, use EL2 translation
orr x3, x3, #MDCR_EL2_TPMS // and disable access from EL1
6:
msr mdcr_el2, x3 // Configure debug traps
이 코드들은 mdcr_el2
(Monitor Debug Configuration Register(EL2))를 설정하는 역할을 합니다. 이 레지스터는 가상화 확장과 연관된 다른 디버그 트랩을 설정하는 역할을 합니다. 디버그와 트레이스는 우리 주제에서 살짝 벗어나기 때문에 이 코드 블락의 상세한 부분은 설명하지 않고 남기겠습니다. 만약 상세한 것에 관심 있다면 AArch64-Reference-Manual의 2810
페이지에 mdcr_el2
레지스터 설명을 읽기를 권합니다.
/* Stage-2 translation */
msr vttbr_el2, xzr
운영체제가 하이퍼바이저로 사용될 때 게스트 운영체제들을 위한 완벽한 메모리 격리를 제공해야 합니다. 스테이지 2 가상 메모리 변환은 정확히 이 목적을 위해 사용됩니다.: 각 게스트 운영체제는 자체 모든 시스템 메모리를 가지고 있다고 생각합니다 비록 사실상 각 메모리 접근은 스테이지 2 변환에 의해서 물리적 메모리에 매핑됩니다. vttbr_el2
는 스테이지 2 변환을 위한 변환 테이블의 기준 주소(base address)를 가집니다. 이 시점에서 스테이즈 2 변환은 비활성화되고 vttbr_el2
는 0으로 설정되어야 합니다.
cbz x2, install_el2_stub
mov w0, #BOOT_CPU_MODE_EL2 // This CPU booted in EL2
isb
ret
VHE가 활성화 되잇는지 확인하기 위해 첫 x2
와 0을 비교합니다. 만약 결과가 사실을 반환한다면 install_el2_stub
라벨로 점프하고 아니면 CPU가 EL2에서 부팅된 것을 기록하고 el2_setup
함수를 빠져나옵니다. 후자에서 프로세서는 EL2 모드에서 수행을 이어가고 EL1은 아예 사용되지 않을 것입니다.
install_el2_stub:
/* sctlr_el1 */
mov x0, #0x0800 // Set/clear RES{1,0} bits
CPU_BE( movk x0, #0x33d0, lsl #16 ) // Set EE and E0E on BE systems
CPU_LE( movk x0, #0x30d0, lsl #16 ) // Clear EE and E0E on LE systems
msr sctlr_el1, x0
만약 이 지머에 도달한다면 VHE가 필요없고 EL1으로 곧 변경할 것임을 의미합니다. 따라서 이른 EL1 초기화는 여기서 수행되어야 합니다. 복사된 코드는 sctlr_el1
(System Control Register) 초기화를 하는 역할을 수행합니다. 이미 RPi OS를 위한 같은 작업을 여기서 수행했습니다.
/* Coprocessor traps. */
mov x0, #0x33ff
msr cptr_el2, x0 // Disable copro. traps to EL2
이 코드는 EL1이 cpacr_el1
레지스터에 접근하게 해주고 결과적으로 Trace, Floating-point, Advanced SIMD 기능에 접근을 제어할 수 있게 해줍니다.
/* Hypervisor stub */
adr_l x0, __hyp_stub_vectors
msr vbar_el2, x0
비록 몇 가지 기능이 필요로 하지만 EL2를 당장 사용할 계획이 없습니다. 예를 들어 현재 실행중인 커널에서 다른 커널로 로드하고 부팅하는 것을 가능케 하는 kexec 시스템콜을 구현하기 위해서 필요합니다.
_hyp_stub_vectors은 EL2를 위한 모든 예외 핸들러의 주소들을 가지고 있습니다. 인터럽트와 에외 핸들링을 자세히 다룬 후 다음 레슨에서 EL1를 위한 예외 핸들링 기능을 구현할 것입니다.
/* spsr */
mov x0, #(PSR_F_BIT | PSR_I_BIT | PSR_A_BIT | PSR_D_BIT |\
PSR_MODE_EL1h)
msr spsr_el2, x0
msr elr_el2, lr
mov w0, #BOOT_CPU_MODE_EL2 // This CPU booted in EL2
eret
마지막으로 EL1에서 프로세서 상태를 초기화해야 하고 예외 레벨을 바꿔야 합니다. 이미 RPi OS를 위해서는 수행했기에 이 코드를 자세히 설명하지 않겠습니다.
여기서 새로운 유일한 것은 elr_el2
가 초기화 되는 방법입니다. lr
또는 링크 레지스터는 x30
의 대체어 입니다. bl
(Branch Link) 명령어를 실행할 때 마다 x30
은 자동으로 현재 명령어의 주소로 바뀝니다. 이러한 사실은 주로 ret
명령어에 의해 사용되기에 정화히 어디로 돌아갈지 알 수 있습니다. 우리의 경우에 lr
은 여기를 가르키고 elr_el2
를 초기화 하는 방법 때문에 EL1으로 바뀐 후 실행이 재시작 될 위치이기도 합니다.
Processor initialization at EL1
이제 stext
함수로 돌아갑시다. 다음으로 몇 개의 줄들은 중요하지는 않지만 완벽함을 위해 설명하겠습니다.
adrp x23, __PHYS_OFFSET
and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0
KASLR 또는 Kernel address space layout randomization,은 커널을 메모리에서 랜덤한 주소에 위치할 수 있게 해주는 기술입니다. 이 기능은 오로지 보안적인 이유로 요구됩니다. 추가 정보를 위해서 위에 링크를 읽을 수 있습니다.
bl set_cpu_boot_mode_flag
여기서 CPU 부팅 모드는 __boot_cpu_mode 변수안에 저장됩니다. 이것을 수행하는 코드는 이전에 살펴본 preserve_boot_args
함수와 매우 유사합니다.
bl __create_page_tables
bl __cpu_setup // initialise processor
b __primary_switch
마지막 3 함수들은 매우 중요하지만 가상 메모리 관리와 관계있기에 상세한 설명을 레슨 6까지 미루겠습니다. 현재는 그냥 간단하게 의미를 설명하겠습니다.
__create_page_tables
이름이 의미하듯이 페이지 테이블을 생성하는 역할을 합니다.__cpu_setup
가상 메모리 관리에 특정한 다양한 프로세서 설정을 초기화 합니다.__primary_switch
MMU를 활성화 하고 아키텍처 독립적인 시작점인 start_kernel 함수로 점프 합니다.
Conclusion
이 챕터에서는 간단하게 리눅스 커널이 부팅될 때 프로세서가 어떻게 초기화 되는지 다뤘습니다. 다음 레슨에서는 ARM 프로세서를 자세히 계속 다루고 모든 운영체제에 핵심적인 주제를 다룰 것입니다: 인터럽트 핸들링
댓글남기기