최근에 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에서 실행됐던 리눅스의 루틴을 따르도록 모드를 변경하는 작업인 것이다. 대충 분석했으면 모드 변경 코드를 찾지 못했을 것 같은데. 이렇게 깜쪽같이 슬쩍 넣어놓다니. 원래 이런 방식으로 했던걸까?


정리하다보니 내용이 너무 길다. 나머지 내용은 다음 포스트에!