作者
pengdonglin137@163.com
现象
在一台ARM64的Centos7虚拟机里加载 https://github.com/504ensicsLabs/LiME 编译出的内核模块时发生宕机:
insmod lime.ko path=/root/allmem.dump format=raw
上面的目的是把机器物理内存的内容全部dump到文件中,大致的实现过程是,遍历系统中所有的"System RAM",然后处理每一个物理页:根据物理页帧获取对应的page,然后调用kmap_atomic得到虚拟地址,最后将这个虚拟页的数据读取出来存放到文件中。
分析
宕机的调用栈如下:
如果对ARM64的页表属性很熟悉的话,应该可以看出PTE的bit0是0,说明这是一个无效的PTE,虽然其他的bit看上去很正常。
如果对页表熟悉不熟的话,当然也可以分析,就是麻烦一些,下面按不熟的方法来。
在源码中加调试语句,把每次访问的物理也的信息打印出来:
反复几次,发现每次都是这个地址出错0xffff80009fe80000,对应的物理地址是0xdfe80000。
为了测试这个问题,我单独写了一个demo模块,单独去访问这个地址,发现确实会宕机。
查看代码,发现驱动中使用kmap_atomic
获取page对应的虚拟地址:
看上去直接返回的是这个page对应的64KB物理内存在直接映射区的虚拟地址,而且是在开机时就映射好的,没有道理不能访问呀:
ESR的内容记录了发生异常的原因,在读的时候发生了DARA ABORT异常。
查看这段物理地址空间在crash kernel的范围内:(/proc/iomem)
难道跟crash kernel有关?暂时放下这个。
那是不是可以把之前可以访问的物理页的映射信息也打出来比较一下呢?紧接着需要思考如何将某个虚拟地址的页表映射信息输出呢?
内核提供了show_pte这个函数:arch/arm64/mm/fault.c
void show_pte(unsigned long addr) { struct mm_struct *mm; pgd_t *pgdp; pgd_t pgd; if (is_ttbr0_addr(addr)) { /* TTBR0 */ mm = current->active_mm; if (mm == &init_mm) { pr_alert("[%016lx] user address but active_mm is swapper\n", addr); return; } } else if (is_ttbr1_addr(addr)) { /* TTBR1 */ mm = &init_mm; } else { pr_alert("[%016lx] address between user and kernel address ranges\n", addr); return; } pr_alert("%s pgtable: %luk pages, %llu-bit VAs, pgdp=%016lx\n", mm == &init_mm ? "swapper" : "user", PAGE_SIZE / SZ_1K, vabits_actual, (unsigned long)virt_to_phys(mm->pgd)); pgdp = pgd_offset(mm, addr); pgd = READ_ONCE(*pgdp); pr_alert("[%016lx] pgd=%016llx", addr, pgd_val(pgd)); do { pud_t *pudp, pud; pmd_t *pmdp, pmd; pte_t *ptep, pte; if (pgd_none(pgd) || pgd_bad(pgd)) break; pudp = pud_offset(pgdp, addr); pud = READ_ONCE(*pudp); pr_cont(", pud=%016llx", pud_val(pud)); if (pud_none(pud) || pud_bad(pud)) break; pmdp = pmd_offset(pudp, addr); pmd = READ_ONCE(*pmdp); pr_cont(", pmd=%016llx", pmd_val(pmd)); if (pmd_none(pmd) || pmd_bad(pmd)) break; ptep = pte_offset_map(pmdp, addr); pte = READ_ONCE(*ptep); pr_cont(", pte=%016llx", pte_val(pte)); pte_unmap(ptep); } while(0); pr_cont("\n"); }
但是函数并没有调用EXPORT_SYMBOL_GPL
导出给模块用,怎么办呢?
可以使用内核提供的kallsyms_lookup_name
来获取这个函数的地址:
void (*func)(unsigned long addr); func = kallsyms_lookup_name("show_pte"); func(addr);
如果内核连kallsyms_lookup_name都没有导出怎么办?
可以使用kprobe。在调用register_kprobe注册kprobe的时候,会根据设置的函数名称得到函数地址,然后存放到kprobe->addr中,那么我们可以先只设置kprobe->symbol_name,当注册成功可以访问kprobe->addr得到函数的地址。目前在最新的6.5版本的内核里,register_kprobe也是导出的。
有了show_pte,那么可以输出之前几个地址的PTE的内容:
对比发现PTE的值排除物理地址占用的bit外,属性部分只有bit0的内容不同。
既然kmap_atomic直接返回了物理页的线性地址,那么可不可以通过ioremap把这个有问题的物理地址重新映射一下呢? 我测试了一下,不行,在ioremap时会检查要映射的物理地址是否是合法的系统物理内存地址,更明确地说是DDR内存,这里要跟设备内存地址区别开来。如果是系统物理内存,那么直接返回0. 这么处理也好理解,既然是ioremap,当然应该针对的是io memory,如寄存器地址。下面是ARM64上ioreamp的定义:
#define ioremap(addr, size) __ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE)) #define ioremap_nocache(addr, size) __ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE)) #define ioremap_wc(addr, size) __ioremap((addr), (size), __pgprot(PROT_NORMAL_NC)) #define ioremap_wt(addr, size) __ioremap((addr), (size), __pgprot(PROT_DEVICE_nGnRE) void __iomem *__ioremap(phys_addr_t phys_addr, size_t size, pgprot_t prot) { return __ioremap_caller(phys_addr, size, prot, __builtin_return_address(0)); } static void __iomem *__ioremap_caller(phys_addr_t phys_addr, size_t size, pgprot_t prot, void *caller) { unsigned long last_addr; unsigned long offset = phys_addr & ~PAGE_MASK; int err; unsigned long addr; struct vm_struct *area; /* * Page align the mapping address and size, taking account of any * offset. */ phys_addr &= PAGE_MASK; size = PAGE_ALIGN(size + offset); /* * Don't allow wraparound, zero size or outside PHYS_MASK. */ last_addr = phys_addr + size - 1; if (!size || last_addr < phys_addr || (last_addr & ~PHYS_MASK)) return NULL; /* * Don't allow RAM to be mapped. */ if (WARN_ON(pfn_valid(__phys_to_pfn(phys_addr)))) return NULL; area = get_vm_area_caller(size, VM_IOREMAP, caller); if (!area) return NULL; addr = (unsigned long)area->addr; area->phys_addr = phys_addr; err = ioremap_page_range(addr, addr + size, phys_addr, prot); if (err) { vunmap((void *)addr); return NULL; } return (void __iomem *)(offset + addr); }
可以看到,上面的内存属性都是DEVICE MEMORY,其中pfn_valid(__phys_to_pfn(phys_addr))
就是用来判断是否是系统物理内存的,如果是的话,返回true,那么ioremap就会直接返回0.
下面分析PTE是怎么构造的呢?以缺页异常中中构造PTE的部分为例:
handle_pte_fault |- do_anonymous_page |- entry = mk_pte(page, vma->vm_page_prot);
这里vm_page_prot存放的就是PTE中属性部分,这些属性是通过vm_get_page_prot根据vm_flags转换而来:
/* description of effects of mapping type and prot in current implementation. * this is due to the limited x86 page protection hardware. The expected * behavior is in parens: * * map_type prot * PROT_NONE PROT_READ PROT_WRITE PROT_EXEC * MAP_SHARED r: (no) no r: (yes) yes r: (no) yes r: (no) yes * w: (no) no w: (no) no w: (yes) yes w: (no) no * x: (no) no x: (no) yes x: (no) yes x: (yes) yes * * MAP_PRIVATE r: (no) no r: (yes) yes r: (no) yes r: (no) yes * w: (no) no w: (no) no w: (copy) copy w: (no) no * x: (no) no x: (no) yes x: (no) yes x: (yes) yes */ pgprot_t protection_map[16] __ro_after_init = { __P000, __P001, __P010, __P011, __P100, __P101, __P110, __P111, __S000, __S001, __S010, __S011, __S100, __S101, __S110, __S111 }; pgprot_t vm_get_page_prot(unsigned long vm_flags) { pgprot_t ret = __pgprot(pgprot_val(protection_map[vm_flags & (VM_READ|VM_WRITE|VM_EXEC|VM_SHARED)]) | pgprot_val(arch_vm_get_page_prot(vm_flags))); return arch_filter_pgprot(ret); } EXPORT_SYMBOL(vm_get_page_prot);
上面这些宏定义在arch/arm64/include/asm/pgtable-prot.h中,
#define PAGE_NONE __pgprot(((_PAGE_DEFAULT) & ~PTE_VALID) | PTE_PROT_NONE | PTE_RDONLY | PTE_NG | PTE_PXN | PTE_UXN) #define PAGE_SHARED __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_NG | PTE_PXN | PTE_UXN | PTE_WRITE) #define PAGE_SHARED_EXEC __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_NG | PTE_PXN | PTE_WRITE) #define PAGE_READONLY __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PTE_NG | PTE_PXN | PTE_UXN) #define PAGE_READONLY_EXEC __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_RDONLY | PTE_NG | PTE_PXN) #define __P000 PAGE_NONE #define __P001 PAGE_READONLY #define __P010 PAGE_READONLY #define __P011 PAGE_READONLY #define __P100 PAGE_READONLY_EXEC #define __P101 PAGE_READONLY_EXEC #define __P110 PAGE_READONLY_EXEC #define __P111 PAGE_READONLY_EXEC #define __S000 PAGE_NONE #define __S001 PAGE_READONLY #define __S010 PAGE_SHARED #define __S011 PAGE_SHARED #define __S100 PAGE_READONLY_EXEC #define __S101 PAGE_READONLY_EXEC #define __S110 PAGE_SHARED_EXEC #define __S111 PAGE_SHARED_EXEC
其中BIT0对应的是宏是PTE_VALID
,有问题的PTE的BIT0确实是0.
然后搜索一下这个宏在内核中的用法,发现使用这个宏的函数还不少:
int set_memory_valid(unsigned long addr, int numpages, int enable) { if (enable) return __change_memory_common(addr, PAGE_SIZE * numpages, __pgprot(PTE_VALID), __pgprot(0)); else return __change_memory_common(addr, PAGE_SIZE * numpages, __pgprot(0), __pgprot(PTE_VALID)); } /* * This function is used to determine if a linear map page has been marked as * not-valid. Walk the page table and check the PTE_VALID bit. This is based * on kern_addr_valid(), which almost does what we need. * * Because this is only called on the kernel linear map, p?d_sect() implies * p?d_present(). When debug_pagealloc is enabled, sections mappings are * disabled. */ bool kernel_page_present(struct page *page); static inline pte_t pte_mkpresent(pte_t pte) { return set_pte_bit(pte, __pgprot(PTE_VALID)); } static inline int pte_protnone(pte_t pte) { return (pte_val(pte) & (PTE_VALID | PTE_PROT_NONE)) == PTE_PROT_NONE; }
接着看到arch_kexec_protect_crashkres调用了set_memory_valid,这个函数是给crash_kernel所在的内存设置属性的,将那段内存映射的属性设置为无效,防止被破坏。
void arch_kexec_protect_crashkres(void) { int i; kexec_segment_flush(kexec_crash_image); for (i = 0; i < kexec_crash_image->nr_segments; i++) set_memory_valid( __phys_to_virt(kexec_crash_image->segment[i].mem), kexec_crash_image->segment[i].memsz >> PAGE_SHIFT, 0); }
结合之前看到的iomem的内容,基本可以确认就是这导致的。
验证
下面验证了一下,将/etc/default/grub
中配置的crashkernel=auto
删除,然后重新生成grub.cfg,重启后再次加载lime模块就可以正常运行了。
补充
最后补充一点ARM64的页表属性和ESR的知识,参考ARMv8手册。
TABLE和BLOCK级的页表项的格式(Table和Block描述符)
可以看到,BIT0如果是0,那么就是无效的。Block描述符的bit1是0,Table描述符的bit1是1.
PTE级的页表项格式(Page描述符)
对于Page描述符,bit0和bit1都必须是1才有效,否则的话访问会异常。
64KB页情况下的页表遍历
内存属性
在页表中除了包含物理内存地址外,还包含用于描述这段内存地址属性的字段,在ARMv8中页表中分为Table描述符、Block描述符和Page描述符,在虚拟化场景,还分为Stage1和Stage2两个阶段,每个阶段的地址翻译都有前面3中类型,根据PAGE_SIZE的不同,每种类型都有各自的格式。
其中说,Stage-2的Table描述符中没有内存属性字段,其他的描述符都有内存属性字段,但是Stage1和Stage2的有所区别。
Stage1的Table描述符中属性字段
可以看到,这里只有Upper Attributes。
Bit | Name | Meaning |
63 | NSTable | 只对Secure状态下的内存访问有效,在Non-secure状态下忽略 |
62:61 | APTable | Access permissions limit for subsequent levels of lookup |
60 | UXNTable or XNTable | XN limit for subsequent levels of lookup. This bit is called UXNTable in the EL1&0 translation regime, where it only determines whether execution at EL0 of instructions fetched from the region identified at a lower level of lookup permitted. In the other translation regimes the bit is called XNTable. |
59 | PXNTable | PXN limit for subsequent levels of lookup |
Stage1的Block和Page描述符中的属性字段
上面Lower attributes是从bit2开始的。
Bit | Name | Meaning |
54 | UXN or XN | The Execute-never bit. Determines whether the region is executable. This bit is called UXN in the EL1&0 translation regime, where it only determines whether execution at EL0 of instructions fetched from the region is permitted. In the other translation regimes the bit is called XN |
53 | PXN | The Privileged execute-never bit. Determines whether the region is executable at EL1 |
52 | Contiguous | A hint bit indicating that the translation table entry is one of a contiguous set or entries, that might be cached in a single TLB entry |
11 | nG | The not global bit. Determines whether the TLB entry applies to all ASID values, or only to the current ASID value. Valid only to the EL1&0 translation regime. |
10 | AF | The Access flag |
9:8 | SH | Shareability field |
7:6 | AP[2:1] | Data Access Permissions bits. The ARMv8 translation table descriptor format defines AP[2:1] as the Access Permissions bits, and does not define an AP[0] bit. |
5 | NS | Non-secure bit. For memory accesses from Secure state, specifies whether the output address is in the Secure or Non-secure address map |
4:2 | AttrIndx[2:0] | Stage 1 memory attributes index field, for the MAIR_ELx |
Stage2的Block和Page描述符的属性字段
上面Lower attributes是从bit2开始的。
Bit | Name | Meaning |
54 | XN | The Execute-never bit. Determines whether the region is executable |
52 | Contiguous | A hint bit indicating that the translation table entry is one of a contiguous set or entries, that might be cached in a single TLB entry |
10 | AF | The Access flag |
9:8 | SH | Shareability field |
7:6 | S2AP | Stage 2 data Access Permissions bits |
5:2 | MemAttr | Stage 2 memory attributes |
主要的几个bit的含义
The Access flag
下面是Linux内核中的用法:
static inline pte_t pte_mkold(pte_t pte) { return clear_pte_bit(pte, __pgprot(PTE_AF)); }
函数ptep_test_and_clear_young会调用上面的函数,主要用于检测page的活跃程度。
Data Access Permissions bits
- 在Stage1的Block和Page描述符中的属性字段中的AP[2:1]
- 在Stage2的Block和Page描述符的属性字段中的S2AP:
从这里看到,AP[1]用于控制用户是否可以访问,AP[2]用于控制是否只读,内核里是这么用的:
#define PTE_USER (_AT(pteval_t, 1) << 6) /* AP[1] */ #define PTE_RDONLY (_AT(pteval_t, 1) << 7) /* AP[2] */
- PTE_USER
可以通过下面的宏判断是否是用户映射:
#define pte_user(pte) (!!(pte_val(pte) & PTE_USER)) static inline bool pte_user_accessible_page(pte_t pte) { return pte_present(pte) && (pte_user(pte) || pte_user_exec(pte)); }
- PTE_RDONLY
用于控制是否只读,内核是这么用的:
// 当page是clean时,为了检测写访问时变脏,会设置为只读 static inline pte_t pte_mkclean(pte_t pte) { pte = clear_pte_bit(pte, __pgprot(PTE_DIRTY)); pte = set_pte_bit(pte, __pgprot(PTE_RDONLY)); return pte; } // 写保护 static inline pte_t pte_wrprotect(pte_t pte) { /* * If hardware-dirty (PTE_WRITE/DBM bit set and PTE_RDONLY * clear), set the PTE_DIRTY bit. */ if (pte_hw_dirty(pte)) pte = pte_mkdirty(pte); pte = clear_pte_bit(pte, __pgprot(PTE_WRITE)); pte = set_pte_bit(pte, __pgprot(PTE_RDONLY)); return pte; }
Access permissions limit for subsequent levels of lookup
在Stage1的Table描述符中属性字段中的APTable:
Shareability field
在"Stage1的Block和Page描述符中的属性字段"和"Stage2的Block和Page描述符的属性字段"中的SH:
需要注意的是:
The shareability field is only relevant if the memory is a Normal Cacheable memory type. All Device and Normal Non-cacheable memory regions are always treated as Outer Shareable, regardless of the translation table shareability attributes.
Linux内核中提供了下面的宏:
#define PTE_SHARED (_AT(pteval_t, 3) << 8) /* SH[1:0], inner shareable */
即默认Linux的内存映射用的是InnerShareable。
MAIR_EL1
截取部分手册中对Attr的约定:
- Device Memory
- Normal Memory
在内核启动时会在arch/arm64/mm/proc.S中设置MAIR_EL1寄存器,预制内核可能会用到的各种内存属性
其中相关宏的定义是:
#define MAIR(attr, mt) ((attr) << ((mt) * 8))
可以看到上面对Inner和Outer配置的Cache策略是相同的。
在映射的时候,会用到上面MT_DEVICE*/MT_NORMAL*
,这些宏表示属性在MAIR_EL1寄存器中的索引。
以下面的函数为例:
void __iomem *ioremap_cache(phys_addr_t phys_addr, size_t size) { /* For normal memory we already have a cacheable mapping. */ if (pfn_valid(__phys_to_pfn(phys_addr))) return (void __iomem *)__phys_to_virt(phys_addr); return __ioremap_caller(phys_addr, size, __pgprot(PROT_NORMAL), __builtin_return_address(0)); }
表示将指定的物理地址映射为PORT_NORMAL属性的,这种属性的内存是带cache的。这个宏定义在arch/arm64/include/asm/pgtable-prot.h中:
#define PROT_DEVICE_nGnRnE (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRINDX(MT_DEVICE_nGnRnE)) #define PROT_DEVICE_nGnRE (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRINDX(MT_DEVICE_nGnRE)) #define PROT_NORMAL_NC (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRINDX(MT_NORMAL_NC)) #define PROT_NORMAL_WT (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRINDX(MT_NORMAL_WT)) #define PROT_NORMAL (PROT_DEFAULT | PTE_PXN | PTE_UXN | PTE_DIRTY | PTE_WRITE | PTE_ATTRINDX(MT_NORMAL))
后面用的是PTE_ATTRINDX(MT_NORMAL)
,属性索引是MT_NORMAL,在MAIR_EL1中的属性值是0xFF
ESR_EL1
- 当发生异常时,ESR会记录发生异常的原因,对于陷入到EL1的情况,参考ESR_EL1
以上面报错为例,ESR_EL1的值为0x96000007:
- EC的值是:0b100101
表示Data Abort,具体细节还需要查看ISS。
- IL的值:1
表示出异常的指令是一条32bit的指令。
- ISS的值是:0b111
下面挑几个必要重要的bit位说明一下:
Bit | Name | Val | Meaning |
15 | FnP, FAR not Precise. | 0 | The FAR holds the faulting virtual address that generated the Data Abort |
7 | S1PTW, For a stage 2 fault, indicates whether the fault was a stage 2 fault on an access made for a stage 1 translation table walk | 0 | Fault not on a stage 2 translation for a stage 1 translation table walk |
6 | WnR, Write not Read. Indicates whether a synchronous abort was caused by an instruction writing to a memory location, or by an instruction reading from a memory location | 0 | Abort caused by an instruction reading from a memory location |
5:0 | DFSC, Data Fault Status Code | 0b000111 | Translation fault, level 3 |