The last post explained how EL2 per cpu variables are defined and used. To review: EL2 per cpu variables are accessed by first acquiring the base address in the .hyp.data..percpu section, then add the per cpu offset stored in tpidr_el2 to get the final address. There are two questions to be answered:
How are the EL2 per cpu area allocated?
How are the per cpu offsets calculated and installed into tpidr_el2?
This post will answer these questions.
Memory Allocation
EL2 per cpu area’s memory is allocated in the KVM ARM initialization function init_hyp_mode :
/* * Allocate and initialize pages for Hypervisor-mode percpu regions. */ for_each_possible_cpu(cpu) { structpage *page; void *page_addr; // allocate memory with alloc_pages // nvhe_per_cpu_order() calculates how large the per cpu area is in pages, // then take log2 e.g. 8 pages = 2^3 pages,returns 3. page = alloc_pages(GFP_KERNEL, nvhe_percpu_order()); if (!page) { err = -ENOMEM; goto out_err; } // turn the struct page returned into linear address page_addr = page_address(page); // copy the base values in .hyp.data..percpu into the allocated area // in case some per cpu variables are initialized // CHOOSE_NVHE_SYM is used to change the symbol's name into an nvhe symbol name // CHOOSE_NVHE_SYM(__per_cpu_start) can be thought as the start of .hyp.data..percpu memcpy(page_addr, CHOOSE_NVHE_SYM(__per_cpu_start), nvhe_percpu_size()); // store the allcated linear address into an EL1 array kvm_arm_hyp_percpu_base[cpu] = (unsignedlong)page_addr; }
Map the allocated area to EL2 after allocation (same, in init_hyp_mode):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
for_each_possible_cpu(cpu) { // get the EL1 linear address that should be mapped to EL2 for this CPU char *percpu_begin = (char *)kvm_arm_hyp_percpu_base[cpu]; char *percpu_end = percpu_begin + nvhe_percpu_size();
/* Prepare the CPU initialization parameters */ // see the next section for explanation cpu_prepare_hyp_mode(cpu); }
Setting up tpidr_el2
We have discussed how the EL2 per cpu areas are allocated and how they are mapped to EL2. Next up is the offset calculation and tpidr_el2 installation.
Calculating per cpu offset
cpu_prepare_hyp_mode(cpu) is responsible for filling up a struct kvm_nvhe_init_params . This structure stores the initial values of EL2 system registers, including tpidr_el2 . The relevant parts are listed below:
staticvoidcpu_prepare_hyp_mode(int cpu) { // use per_cpu_ptr_nvhe_sym to get the linear address of the symbol // for the current cpu // in this case, the symbol kvm_init_params structkvm_nvhe_init_params *params = per_cpu_ptr_nvhe_sym(kvm_init_params, cpu); [...]
/* * Calculate the raw per-cpu offset without a translation from the * kernel's mapping to the linear mapping, and store it in tpidr_el2 * so that we can use adr_l to access per-cpu variables in EL2. * Also drop the KASAN tag which gets in the way... */ // here we calculate the per cpu offset // formula: A - B where: // A: the start of the per cpu area allocated using alloc_pages (linear address) // B: start of the base per cpu area (linear address) params->tpidr_el2 = (unsignedlong)kasan_reset_tag(per_cpu_ptr_nvhe_sym(__per_cpu_start, cpu)) - (unsignedlong)kvm_ksym_ref(CHOOSE_NVHE_SYM(__per_cpu_start));
[...] }
Installing tpidr_el2
After saving the per cpu offset in params→tpidr_el2 , the next step is to install the value in tpidr_el2 , let’s first check out the call stack of the initialization of KVM ARM:
hyp_install_host_vector runs in EL1, it calls a hypercall to enter EL2, passing in a physical pointer to a struct kvm_nvhe_init_param to initialize EL2. ___kvm_hyp_init does the actual initialization.
[...] // get the EL1 linear address of the current cpu's `kvm_init_params` params = this_cpu_ptr_nvhe_sym(kvm_init_params); // this does an `hvc`, passing in // 1. function ID for `__kvm_hyp_init` // 2. physical address of `kvm_init_params` of the current cpu // 3. address of the local variable `res`, used to output exit code arm_smccc_1_1_hvc(KVM_HOST_SMCCC_FUNC(__kvm_hyp_init), virt_to_phys(params), &res); WARN_ON(res.a0 != SMCCC_RET_SUCCESS); }
We’ll skip how that line arm_smccc… actually calls hvc , in short, EL2 can check x0 for __kvm_hyp_init ‘s ID to confirm the reason why EL1 called hvc , and receive the pointer to kvm_init_params in x1 . Then the execution goes to:
// arch/arm64/kvm/hyp/nvhe/hyp-init.S /* * Only uses x0..x3 so as to not clobber callee-saved SMCCC registers. * * x0: SMCCC function ID * x1: struct kvm_nvhe_init_params PA */ __do_hyp_init: /* Check for a stub HVC call */// not in this case cmp x0, #HVC_STUB_HCALL_NR b.lo __kvm_handle_stub_hvc // check for __kvm_hyp_init (yes) mov x3, #KVM_HOST_SMCCC_FUNC(__kvm_hyp_init) cmp x0, x3 // jump to 1: b.eq 1f mov x0, #SMCCC_RET_NOT_SUPPORTED eret // move param's physical address to x0 1: mov x0, x1 // move return address to x3, in case ___kvm_hyp_init overwrites it // (___kvm_hyp_init) does not change x3 mov x3, lr // jump to the processing function bl ___kvm_hyp_init // Clobbers x0..x2 // recover lr mov lr, x3 /* Hello, World! */ // put value indicating success into x0 mov x0, #SMCCC_RET_SUCCESS // return back to EL1 eret
Lastly let’s check out ___kvm_hyp_init :
Actually tpidr_el2 is set up in the very first two instructions, I’ll let you see for your self how it does it, you can also see how other EL2’s system registers are initialized in this function.
Note that EL2 MMU is not enabled yet when this function is executed, therefore all memory accesses use physical addresses here.