-
ARM64 리눅스 부팅 초기 어셈블리 코드 분석(head.S) (1/2)개발/컴퓨터사이언스 2018. 1. 17. 23:54
최근에 ARM 64bit 리눅스의 초기 부팅 어셈블리 코드(arch/arm64/kernel/head.S)를 분석할 일이 있었다. 학교 다닐 때 x86 어셈블리로 코딩을 해본적도 있고 예전에 ARM 32bit miniOS 초기 부분도 분석해본 경험이 있어서 금방 할 줄 알았는데... ldr 같은 기본적인 명령어도 오랜만에 보니 생소했고 ARM 64bit만의 고유한 레지스터가 있어 레퍼런스를 뒤적거리면서 찾게 되다 보니 생각보다 오랜 시간이 소요됐다. ARM32와 ARM64가 원래 겹치는 영역이 별로 없는건지 아니면 내 머릿속에 남아 있는게 별로 없어서 겹칠게 없어진건지.
고생한 만큼 쉽게 잊혀질 수 있기 때문에(응?) 포스트로 이번에 공부한 내용들을 짧게나마 정리해보려한다.
1. ENTRY(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)
stext에 있는 영역은 커널 바이너리 이미지가 압축 해제된 후 PC값이 첫번째로 가리키는 영역이다. 아키텍처와 무관한 리눅스 코드를 실행하기 전에 ARM에서 필수적으로 해야하는 작업을 여기서 처리한다. 브랜치의 이름만으로도 대략 무슨일을 하는지 짐작 할 수 있다.
2. 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
Bootloader에서 kernel을 올려주면서 x0~x3에는 특정한 argument를 전달한다. 그런데 x0~x3 레지스터는 연산 할 때 자주 사용되는 레지스터이므로 값이 변경될 소지가 있다. 그래서 넘어 온 값들은 boot_args의 주소 영역에다가 값을 저장하도록 한다. 나중에 common 폴더에 있는 리눅스 초기 작업을 실행하기 전에 복구해야한다.
3. el2_setup
만약 리눅스가 EL2에서 시작이 됐다면 기존에 EL1에서 돌아가도록 짠 리눅스 코드가 실행되는데 문제가 없도록 미리 몇몇 작업을 처리할 필요가 있다. 구체적으로 무슨일을 하는지는 코드를 차근차근 따라가보자.
ENTRY(el2_setup) msr SPsel, #1 // We want to use SP_EL{1,2} mrs x0, CurrentEL cmp x0, #CurrentEL_EL2 b.eq 1f 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 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 #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
mrs/msr 은 co-processor의 primary register 값을 읽고 쓰는데 사용하는 명령어다. 주로 p13,14,15 의 레지스터값을 읽는다. ARM 32bit 버전에서는 인자가 5개일 정도로 명령어가 복잡했는데 ARM64 들어오면서 각각에 특정 레지스터 값을 입력하는 것으로 바뀌었나보다. 그래선지 예전에 분석 할 때 보다는 쉬웠던 것 같다. 레지스터가 저장되는 방향은 꼭 기억하자. 오른쪽에 있는 레지스터 값이 왼쪽 레지스터에 저장된다!
상단부 ret 명령어 전까지는 리눅스가 어떤 모드에서 실행됐는지를 확인한다. EL2로 실행되면 1: 로 PC 값이 이동하고 그렇지 않으면 Endian bit를 세팅하고 넘어간다. 현재 EL1에서 실행되고 있는 것이 확실하므로 EL1에서 사용할 수 있는 System Control Register에 현재 바이너리 형식이 Little Endian인지 Big Endian인지 세팅한다.
하단부는 VHE 유무를 세팅한다. VHE인 경우는 뭔가를 좀더 많이 하는데... 현재 VHE를 사용 할 수 있는 보드는 이세상에 없으므로 패쓰. 더 밑으로 내려가보자
/* Hyp configuration. */ 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 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
Hyp configuration 이라고 되어 있는데.. 사실 앞에서 VHE를 사용하는게 아니라면 하이퍼바이저를 사용하기 위한 작업은 아니라고 봐도 될 것 같다. x0 레지스터에 현재 EL1은 64bit로 돌아가고 있다고 세팅한 후 VHE 모드가 켜져 잇는지 확인하고(x2는 앞서 세팅 했었다) set_hcr로 넘어간다. 그리고 hcr_el2, Hypervisor Configuration Register의 값을 세팅한다. 그리고 그 아래엔 cnthctl_el2 레지스터의 값을 바꿔서 Non-secure EL1에서 physical counter, timer를 읽을 때 EL2로 trap이 되지 않도록 한다. 리눅스는 EL1으로 돌아가야 하니까 그런듯 하다. 그 아래 CONFIG_ARM_GIC_V3 지시어가 있는 부분도 동일하다. EL2로 트랩되지 않도록 처리하고 넘어가버린다. ICC_SRE_EL2는 GIC에 있는 레지스터다.
install_el2_stub: /* * When VHE is not in use, early init of EL2 and EL1 needs to be * done here. * When VHE _is_ in use, EL1 will not be used in the host and * requires no configuration, and all non-hyp-specific EL2 setup * will be done via the _EL1 system register aliases in __cpu_setup. */ /* 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 /* Coprocessor traps. */ mov x0, #0x33ff msr cptr_el2, x0 // Disable copro. traps to EL2 /* SVE register access */ mrs x1, id_aa64pfr0_el1 ubfx x1, x1, #ID_AA64PFR0_SVE_SHIFT, #4 cbz x1, 7f bic x0, x0, #CPTR_EL2_TZ // Also disable SVE traps msr cptr_el2, x0 // Disable copro. traps to EL2 isb mov x1, #ZCR_ELx_LEN_MASK // SVE: Enable full vector msr_s SYS_ZCR_EL2, x1 // length for EL1. /* Hypervisor stub */ 7: adr_l x0, __hyp_stub_vectors msr vbar_el2, x0 /* 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
install_el2_stub 로 오기 전에 debug 옵션을 켜주는 코드가 있는데 그 부분은 생략했다. 내가 분석한 코드랑은 조금 달랐다.. 버전상의 차이가 있는 건가. 아마 맥락은 비슷할 것이다. "PMU가 있는지 확인해보고 있으면 디버그를 키고 아니면 스킵한다." 이 정도만 확인하면 될 것 같다.
사실 요 브랜치에서 주의깊게 봐야하는 코드는 install_el2_stub 의 작업이다. ARM에서 가상화가 지원되면서 EL1에서 실행되는 리눅스가 초기화 작업중 EL2로 모드를 변경 할 때 어떻게 trap을 잡아낼 것인가가 주요 관심사였다. 당시 논문에서는 arm 초기화 부분에서 임시로 vector table을 두어 처리 한다고 했었는데 실제로도 vbar_el2(Vector base address register) 레지스터를 hyp_stub_vector로 세팅하도록 해서 처리하고 있다. vector table의 코드를 보면 부팅시에만 임시로 사용하는 테이블이어서 EL1에서 오는 trap 말고는 모두 invalid하게 처리해뒀다. 실제로 사용할 vector table을 세팅하기 전까지만 사용하는 코드라 그런것 같다.
그 아래는 spsr_el2, elr_e2 값을 세팅하는 작업이다. spsr은 Saved Program Status Register로 프로세스 실행중 Exception이 일어 날 때 현 프로세스의 상태(pstate)를 저장해둬 Exception 처리 후 원래 상태를 복구 할 때 사용하는 레지스터고 elr 레지스터는 exception 처리후 이동할 pc값을 저장하고 있다. 음 그런데 뭔가 이상하다. 지금 Exception이 일어날 상황이 아닌데 왜 spsr 값과 elr 값을 지정하는거지?
eret 명령어를 확인하면 왜 spsr, elr 값을 변경했는지 알 수 있다. eret는 exception 처리후 원래 상태로 돌아갈 때 사용하는 명령어다. eret가 실행되면 spsr값이 pstate 값으로 바뀌고 pc 값은 elr을 가리키게 된다. lr은 원래 el2_setup으로 넘어오기 전에 다음에 실행할 명령어를 세팅해뒀을 것이다. 즉 eret가 실행되면서 el2_setup 다음의 명령어로 이동하게 된다.
프로그램 상태는 spsr에 저장한 값으로 세팅한다. 오 그런데 spsr 값 마지막에 PSR_MODE_EL1h라고 세팅해뒀다. 즉 원래 프로그램이 EL1로 실행되고 있었다고 속이는 것이다. 이렇게 하면 eret가 실행되면 CPU는 EL1 모드로 변경돼 실행하게 된다. 즉 원래 EL1에서 실행됐던 리눅스의 루틴을 따르도록 모드를 변경하는 작업인 것이다. 대충 분석했으면 모드 변경 코드를 찾지 못했을 것 같은데. 이렇게 깜쪽같이 슬쩍 넣어놓다니. 원래 이런 방식으로 했던걸까?
정리하다보니 내용이 너무 길다. 나머지 내용은 다음 포스트에!
'개발 > 컴퓨터사이언스' 카테고리의 다른 글
tasklet과 workqueue의 차이점 (1) 2018.06.15 ARM64 리눅스 부팅 초기 어셈블리 코드 분석(head.S) (2/2) (0) 2018.01.27 입출력제어(ioctl) (0) 2017.02.11 디바이스트리(Device Tree) (2) 2017.01.04 모놀리식(Monolithic) kernel과 마이크로(Micro) 커널 (4) 2016.10.08