The Page Fault Process After Address Access in Arm-Linux
Address Access Example
Taking the access to the user-space virtual address 0x0000007000003000 as an example, consider the following code.
#include <sys/mman.h>
#include <stdio.h>
int main()
{
unsigned long long *x = (unsigned long long)0x0000007000003000;
int *p = (int*)mmap(0x0000007000003000, sizeof(unsigned long long) * 10, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
printf("before write, x = %llu\n", *x);
*x = 1;
printf("after write, x = %llu\n", *x);
}If the user-space virtual address 0x0000007000003000 is not mapped in advance via mmap with read and write permissions, a Segmentation fault (core dumped) will occur, because 0x0000007000003000 is not managed as a vma (virtual memory area) allocated through mmap or brk in the process's virtual address space, and thus the virtual address is not accessible at this point. The output after compilation is as follows.
before write, x = 0
after write, x = 1
As can be seen, a new value was successfully written to the user-space virtual address 0x0000007000003000.
What Happens After Accessing an Address
After performing mmap on the virtual address 0x0000007000003000, from the process's perspective, a vma has been established to manage the address, but at this point no actual physical address has been associated with this virtual address yet. This association is achieved through a page fault.
Let me introduce the MMU (Memory Management Unit) hardware that is now commonly integrated inside the CPU. Its role is to translate from VA (virtual address) to PA (physical address). The translation process can be simply understood as PA=MMU(VA) or PA=page_table(VA). Currently, MMUs basically complete the address translation process through multi-level page tables. After mmap maps a virtual address, the page table establishment process has not yet occurred, so when the CPU attempts to access the virtual address 0x0000007000003000, its MMU cannot find the corresponding physical address through the page table.
In short, the essence of the page fault process is to tell the MMU how to access the corresponding physical address through the page table after receiving a virtual address as input.
The Page Fault Process
The current process is at EL0 exception level, and the base address of the page table is stored in the TTBR0_EL1 register. Note that TTBR0_EL1 holds a physical address, which is precisely the base address of the PGD page table for the user-space address space of this process. The relative offsets of other PGD page tables within this address space (i.e., pgd_offset(mm,addr)) are all calculated based on the physical address stored in TTBR0_EL1.
During the execution of the printf("before write, x = %llu\n", *x) code, an access to the content at virtual address 0x0000007000003000 triggers a page fault exception (such as a data abort or instruction abort), which is caught by the exception vector table. The process then enters the page table establishment flow through the pre-registered page fault exception handler functions. The assembly code for exception handling can be found in arch\arm64\kernel\entry.S, and a code snippet is shown below.
el0_da:
/*
* Data abort handling
*/
mrs x26, far_el1
enable_daif
ct_user_exit
clear_address_tag x0, x26
mov x1, x25
mov x2, sp
bl do_mem_abort
b ret_to_user
el0_ia:
/*
* Instruction abort handling
*/
mrs x26, far_el1
enable_da_f
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_off
#endif
ct_user_exit
mov x0, x26
mov x1, x25
mov x2, sp
bl do_el0_ia_bp_hardening
b ret_to_userAs can be seen, for a data abort (e.g., accessing a variable), the do_mem_abort function handles it, and for an instruction abort (e.g., executing code at a certain address in the code segment), the do_el0_ia_bp_hardening function handles it. Subsequently, the registered handler within fault_info performs page fault handling for the specific virtual address, with corresponding functions do_translation_fault and do_page_fault. The relevant code is located at arch\arm64\mm\fault.c.
The logic after entering __do_page_fault is basically similar to the traditional cascading page table establishment process. The main operations during this process include setting the appropriate permissions based on the vma where the address resides, and sequentially establishing pgd\pud\pmd\pte page tables. Finally, the process reaches the anonymous page fault logic do_anonymous_page and the file page fault logic do_fault, where appropriate pages for storing data are allocated through kernel memory allocation interfaces such as alloc_pages from the physical memory managed by the buddy allocator or slab allocator. Finally, set_pte_at sets the address of the allocated page into the content of the corresponding pte entry, completing the entire page fault process. In addition to the page address stored in the pte entry, there are also upper attributes and lower attributes used to indicate some permission and flag bits. For details, please refer to the chapter on the virtual address hierarchy in the Arm official manual. Finally, the saved context is restored, and execution returns to the location where the page fault occurred.
The Page Table Access Process
Taking a three-level page table access as an example, the virtual address 0x0000007000003000 is divided by bits in the following table:
| 0x0000 | 0x0070 | 0x0000 | 0x3000 |
|---|---|---|---|
6348 |
4732 |
3116 |
150 |
| 0000 0000 0000 0000 | 0000 0000 0111 0000 | 0000 0000 0000 0000 | 0011 0000 0000 0000 |
The result of the address division by page table for address calculation is shown in the following table:
| ttbr-sensitive | pgd | pmd | pte | offset |
|---|---|---|---|---|
6339 |
3830 |
2921 |
2012 |
110 |
| 0000 0000 0000 0000 0000 0 | 000 0111 00 | 00 0000 000 | 0 0000 0011 | 0000 0000 0000 |
| The row shows the result after appending three trailing 0s | 0xE0 | 0x0 | 0x18 | 0x0 |
Based on the address calculation process at each level shown in the diagram below, the address calculation process will be briefly explained.

First, based on the TTBR0-EL1 register, the base address of the process's pgd page table is obtained, and a bitwise OR is performed with the offset corresponding to the pgd page table, i.e.
Here, the address is the pgd entry for this address, which stores the base address of the pmd entry pointed to by the pgd entry.
Accessing the content at the physical address yields (corresponding to the level 1 table descriptor), i.e. . The trailing 0b11 of indicates that the page table size is 4K. Different trailing bits represent different page table sizes, and the corresponding offset is 0, so the position of the pmd page table is
Accessing the address yields (corresponding to the level 2 table descriptor), i.e. . It can be seen that the offset corresponding to the pte is 0x18, so after removing the trailing 0b11 and performing a bitwise OR with the offset, the position of the pte page table can be calculated as
The corresponding pte content is . Since the last 12 bits of the virtual address are all 0, the content in at the corresponding [47:12] bits is the address of the physical page. Therefore, the final physical address of the page is
At this point, through the content stored at the physical address , i.e. , the content stored at virtual address 0x0000007000003000 can be found.
References
[1] https://www.cnblogs.com/LoyenWang/p/11406693.html [2] https://armv8-ref.codingbelief.com/zh/