이번에는 primary_entry함수 안에 있는 set_cpu_boot_mode_flag 함수에 대해서 알아보겠다.

/*
 * Sets the __boot_cpu_mode flag depending on the CPU boot mode passed
 * in w0. See arch/arm64/include/asm/virt.h for more info.
 */
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]			// This CPU has booted in EL1
	dmb	sy
	dc	ivac, x1			// Invalidate potentially stale cache line
	ret
SYM_FUNC_END(set_cpu_boot_mode_flag)

일단 이 함수는 w0에 (첫번째 register를 의미하며, 32bit만 읽고 싶을때는 w0, 64bit를 모두 읽고 싶을 때는 x0로 읽어 올 수 있다.) 현재 CPU에서 동작하는 Exception Level이 인자로 전달되었다고 가정한다.

결론 부터 이야기 하면 이 함수의 목적은, 커널이 소유하고있는 CPU들이 같은 Exception Level에서 부팅되었음을 확인하고자 만들어졌으며, 알고리즘은 쓰고 같은지 확인 이다.

 

__boot_cpu_mode 배열은 아래와 같이 선언되었다고 했을 때, 이 함수를 수행하고 있는 CPU가 EL1에서 booting되었다고 한다면, 함수는 아래 배열에서 첫번째 index에 BOOT_CPU_MODE_EL1 값을 쓸 것이며, 결과로는 배열의 element들이 모두 같은 값을 갖게 될 것이다. 반대로 CPU가 EL2에서 booting되었다고 한다면, 함수는 아래 배열에서 두 번째 index에 BOOT_CPU_MODE_EL2 값을 쓸것이고, 배열의 모든 element들이 모두 같은 값을 갖게된다.

/*
 * We need to find out the CPU boot mode long after boot, so we need to
 * store it in a writable variable.
 *
 * This is not in .bss, because we set it sufficiently early that the boot-time
 * zeroing of .bss would clobber it.
 */
SYM_DATA_START(__boot_cpu_mode)
	.long	BOOT_CPU_MODE_EL2
	.long	BOOT_CPU_MODE_EL1
SYM_DATA_END(__boot_cpu_mode)

이는 boot CPU 이후에 secondary CPU들에 대해서도 booting exception level에 따라 배열의 element에 값을 쓰고 배열의 element들이 같은 값을 유지하고 있는지 여부에 따라 모든 CPU들의 booting exception level들이 같은 level에서 부팅되었는지 다른 level을 갖고 부팅 되었는지를 확인할 수 있게 된다.

  • dc ivac, x1:이 함수가 호출 되었을 때는 아직 CPU의 dcache가 활성화 되어있지 않은 상태로써, dcache에 배열의 주소에 해당하는 data가 있다면 나중에 예기치 못한 상황이 생길 수도있어서, 해당 배열에 대해서 dcache invalidation operation을 해주고 있다.

 

예전에 kernel code를 공부할 때 버전보다 한참 upgrade되어서, 전에 보던 코드랑 조금 다르지만 현재 코드버전 (v5.9)를 기준으로 다시 공부를 시작한다. 예전코드에서 _stext symbole이 v5.9부터는 primary_entry라는 symbol이름으로 변경되었다. 확인해보지는 않았지만, secondary core들의 entry point는 아마 secondary_entry 가 되지 않을까 싶다.

 

먼저 SYM_CODE_START라는 macro에 대해서 좀 살펴볼 필요가 있는데, 여기(TODO)에서 다루어 보려고 한다.

SYM_CODE_START(primary_entry)
	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
SYM_CODE_END(primary_entry)

예전 글에서 kernel을 booting시키기 위해서 register들이 가져야 할 값들이 있다고 설정했는데, 이 값들을 실제로 사용하기 전까지 어딘가 저장해놓고 X0~X3 register들을 최대한 사용하려는 목적으로 preserve_boot_args 함수를 호출한다.

 

/*
 * Preserve the arguments passed by the bootloader in x0 .. x3
 */
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

	mov	x1, #0x20			// 4 x 8 bytes
	b	__inval_dcache_area		// tail call
SYM_CODE_END(preserve_boot_args)

kernel에서 초기화 중 제일 중요한 device tree blob이 load된 메모리의 시작 주소를 포함하여 X0~X3을 boot_args라는 버퍼에 저장해 놓는다. 이 코드에서 알아두어야 할 macro가 하가 있는데  adr_l macro 이다.

 

/*
 * @dst: destination register (64 bit wide)
 * @sym: name of the symbol
 */
 .macro	adr_l, dst, sym
 adrp	\dst, \sym
 add	\dst, \dst, :lo12:\sym
 .endm

<linux/arch/arm64/include/asm/assembler.h>

adrp instruction을 통해서 심볼(\sym)의 주소를 PC 기준 offset를 통해 심볼의 상대 주소로 가져 오는 내용이다. 그리고 그 offset은 page aligned offset이기 때문에 심볼의 하위 12bit는 :lo12:\sym 값을 더해줌으로써 실제 심볼의 상대주소를 가져온다는 부분이다. 이 부분에 대한 상세내용은 여기에서 다루려고 한다.

 

사실 preserve_boot_args 함수 이름으로만 본다면 2개의 stp instruction들로만 끝나는게 맞다고 생각한다. 그치만 이 함수에서는 나중에 d-cache invalidation도 해주고 있다. 추측컨데, bootloader에서 d-cache를 사용했다면 나중에 d-cache를 enable했을 때 (kernel의 one of the requirements로 d-cache는 꺼져 있어야 한다), 혹시 compulsory cache miss가 생겨야 할 부분에서 안 생기고 unexpected result를 갖게 될까 invalide해 주고 있다고 추측된다.

 

Barrier instruction 중 하나인 dmb는 barrier instruction 전과 후를 기점으로 memory access instruction들의 순서가 변하지 않음을 보장하는 역할을 하고, 다른 memory system master들이 dmb 명령어 전의 memory access들에 대해서 같은 값을 얻을 수 있게 해준다. 위에 있는 dmb sy는 사실 여기서 어떠한 이유로 사용되었는지 알수가 없다.

 

저장해 놓아야할 값들은 X0~x3 register들의 값들로 8byte*4 = 32byte이다. 이 부분에 대해서 d-cache를 invalidation해 주기 위해 __inval_dcache_area 함수의 인자로 x0에는 boot_args의 시작 주소, x1에는 size (0x20)을 저장하고 __inval_dcache_area 함수를 부르고 있다.

 

SYM_FUNC_START_LOCAL(__dma_inv_area)
SYM_FUNC_START_PI(__inval_dcache_area)
	/* FALLTHROUGH */

/*
 *	__dma_inv_area(start, size)
 *	- start   - virtual start address of region
 *	- size    - size in question
 */
	add	x1, x1, x0
	dcache_line_size x2, x3
	sub	x3, x2, #1
	tst	x1, x3				// end cache line aligned?
	bic	x1, x1, x3
	b.eq	1f
	dc	civac, x1			// clean & invalidate D / U line
1:	tst	x0, x3				// start cache line aligned?
	bic	x0, x0, x3
	b.eq	2f
	dc	civac, x0			// clean & invalidate D / U line
	b	3f
2:	dc	ivac, x0			// invalidate D / U line
3:	add	x0, x0, x2
	cmp	x0, x1
	b.lo	2b
	dsb	sy
	ret
SYM_FUNC_END_PI(__inval_dcache_area)
SYM_FUNC_END(__dma_inv_area)

<linux/arch/arm64/mm/cache.S>

이 함수는 invalide해야할 메모리에 대해서 cache line size 단위로 (그리고 cache line size align 기준으로) invalidate해 주고 있다. x0가 cache line size aligned되어 있는지 확인하여 아니라면 아닌 부분(아래 그림에서 A에 해당)만 한번 invalidate해주고, 중간에 cache line size aligned된 chunk(아래 그림에서 B에 해당)들에 대해서 loop를 돌아가며 cache invalidate해주고 있다. 그리고 아래 부분이 cache line size aligned되어 있지 않다면, 안된 부분(아래 그림에서 C에 해당)만 한번 cache invalidation해주고 있다.

 

 

위 함수에서 알아두어야 할 점은:

  • dcache_line_size macro
  • dc [civac|ivac] instruction

dcache_line_size함수는 2개의 인자를 필요로 하는데 첫 번째 인자인 reg는 dcache line size를 return할 register를 그리고 두 번째로는 연산시 필요한 임시의 register를 받는다.

/*
 * dcache_line_size - get the safe D-cache line size across all CPUs
 */
	.macro	dcache_line_size, reg, tmp
	read_ctr	\tmp
	ubfm		\tmp, \tmp, #16, #19	// cache line size encoding
	mov		\reg, #4		// bytes per word
	lsl		\reg, \reg, \tmp	// actual cache line size
	.endm

read_ctr macro는 해당 architecture에서 d-cache line size를 얻을 수 있는 system register를 읽어오는 macro로써 ARM64 architecture에서는 "mrs \reg, ctr_el0" 이 명령어로 치환된다.

 

 

<ctr_el0 register bit-fields>

 

이 register value에서 [19:16] bit field의 설명은 아래와 같다.

 

 

따라서 Dminline 값을 통해 우리는 d-cache line size를 읽어 올 수 있으며 위 코드에서는

ubfm \tmp, \tmp, #16, #19 // cache line size encoding를 통해 Dminline의 값만을 얻어 오고 있다.

* 이 명령어에 대한 설명은 developer.arm.com/documentation/dui0802/a/UBFM를 참조하기 바랍니다.

얻어 온 값이 log2를 취한 값이라는 점과 byte가 아니라 word의 갯수라는 점을 참고해 본다면 위 코드처럼 4<<\tmp을 통해 d-cache line size를 얻어오는 것이 당연해 보인다.

 

dc [civac|ivac]명령어 들에 대해서 보자면, 타켓 메모리가 아닌 부분과 같은 캐쉬라인에 속해 있는 부분은 혹시 모를 상황에 대비하여 단순 invalidate 뿐만 아니라 clean operation도 해줌으로써 메모리에 태쉬 내용을 반영시켜주고 있다.

  • dc civac: 는 "DC CIVAC, Data or unified Cache line Clean and Invalidate by VA to PoC"
  • dc ivac: 는 "DC IVAC, Data or unified Cache line Invalidate by VA to PoC"

다음으로는 primary_entry의 두 번째 함수인 el2_setup에 대해서 보자면,

/*
 * If we're fortunate enough to boot at EL2, ensure that the world is
 * sane before dropping to EL1.
 *
 * Returns either BOOT_CPU_MODE_EL1 or BOOT_CPU_MODE_EL2 in w0 if
 * booted in EL1 or EL2 respectively.
 */
SYM_FUNC_START(el2_setup)
	msr	SPsel, #1			// We want to use SP_EL{1,2}
	mrs	x0, CurrentEL
	cmp	x0, #CurrentEL_EL2
	b.eq	1f
	mov_q	x0, (SCTLR_EL1_RES1 | ENDIAN_SET_EL1)
	msr	sctlr_el1, x0
	mov	w0, #BOOT_CPU_MODE_EL1		// This cpu booted in EL1
	isb
	ret
    
    ...
    
    /* 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
SYM_FUNC_END(el2_setup)

이 함수는 mrs x0, CurrentEL을 통해서 현재 kernel이 동작하고 있는 Exception Level을 읽어온다. 그리고 만약 현재 kernel이 동작하고 있는 CPU Exception Level이 EL1라고 한다면 SCTLR_EL1 시스템 레지스터에서 RES1 와 ENDIAN bitfield들을 1로 set해주고 w0 에 현재 동작하는 CPU mode가 EL1에 해당한다는 BOOT_CPU_MODE_EL1 상수 값을 return해주며 이 함수를 끝낸다. 만약 현재 CPU가 EL2에서 동작하고 있다면, 일단 커널 입장에서는 본인이 KVM을 위해서 동작하고 있을 수도 있다는 가정으로 아래 코드들을 수행한다. 여기서는 kernel이 KVM hypervisor로 동작하는 부분은 생략하기로 한다.

 

커널이 설정을 확인하며 자신이 EL2에서 동작하고는 있지만 이는 사실 bootloader가 별 의도 없이 올려주었고, 자신은 KVM hypervisor가 아니라 linux kernel로만 동작할 것이라는게 확실해지면 위에 코드처럼 spsr_el2 레지스터에 "PSR_MODE_EL1H"로, elr_el2를 이 함수의 caller로 그리고 eret (exception return)을 해주면서 커널 자신을 EL1으로 level drop 해주면서 이 함수를 끝낸다.

 

'전공공부 > Linux' 카테고리의 다른 글

setup_arch: arm64_memblock_init  (0) 2021.09.12
function: set_cpu_boot_mode_flag  (0) 2020.11.09
[Line by line] Linux Kernel, SYM_CODE_START macro  (0) 2020.10.21
Linux Kernel Image Header - arm64  (4) 2019.02.16
[Line by line] Linux Kernel - 1  (0) 2019.02.09

TODO.

'전공공부 > Linux' 카테고리의 다른 글

setup_arch: arm64_memblock_init  (0) 2021.09.12
function: set_cpu_boot_mode_flag  (0) 2020.11.09
[2020.10.17] Linux Kernel, primary_entry - 1  (0) 2020.10.21
Linux Kernel Image Header - arm64  (4) 2019.02.16
[Line by line] Linux Kernel - 1  (0) 2019.02.09

+ Recent posts