作者

pengdonglin137@163.com

现象

在一台ARM64的Centos7虚拟机里加载 https://github.com/504ensicsLabs/LiME 编译出的内核模块时发生宕机:

insmod limi.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,重启后再次加载limi模块就可以正常运行了。

最后补充一点ARM64的页表属性的只是,参考ARMv8手册。

  • 中间级和BLOCK级的页表项的格式

可以看到,BIT0如果是0,那么就是无效的。

  • PTE级的页表项格式

其中bit0如果是0,表示invalid,访问的话会异常。

使用LiME收集主机物理内存的内容时发生宕机的更多相关文章

  1. (转)HttpWebRequest以UTF-8编码写入内容时发生“Bytes to be written to the stream exceed the Content-Length bytes size specified.”错误

    from:http://www.cnblogs.com/Gildor/archive/2010/12/13/1904060.html HttpWebRequest以UTF-8编码写入内容时发生“Byt ...

  2. ActiveMQ producer 提交事务时突然宕机,会发生什么

    producer 在提交事务时,发生宕机,commit 的命令没有发送到 broker,这时会发生什么? ActiveMQ 开启事务发送消息的步骤: session.getTransactionCon ...

  3. [转]在 .NET 中远程请求 https 内容时,发生错误:根据验证过程,远程证书无效

    该文原网址:http://www.cnblogs.com/xwgli/p/5487930.html 在 .NET 中远程请求 https 内容时,发生错误:根据验证过程,远程证书无效.   当访问 h ...

  4. Excel自文本导入内容时如何做到单元格内换行

    前言:今天在处理数据的时候,在数据库中用到了\n换行符号,目的是在同表格内做到数据多行显示,比如  字段名1  字段名2  字段名3  1 数据一行 数据二行 数据三行 例子是在sql查询后的结果  ...

  5. jquery+php实现用户输入搜索内容时自动提示

    index.html <html> <head>     <meta charset=;} #search_auto li a:hover{background:#D8D ...

  6. 关于jquery html()方法获取带有OBJECT标签的元素内容时,出现“类型不匹配。”的解决办法

    关于jquery html()方法获取带有OBJECT标签的元素内容时,出现“类型不匹配.”的解决办法 解决办法: $("selector").clone().html()

  7. DEDECMS织梦全站动态化访问(包括自由列表freelist)及发布内容时自动动态化设置

    DEDECMS织梦 - 全站已有内容全部设置为动态化访问(包括自由列表freelist),以及发布内容时自动为动态化,设置分为三个步骤: 1.将所有文档设置为“仅动态”:执行以下mysql语句:upd ...

  8. V9发布内容时保留框架<iframe></iframe>

    有些时候,发布文章内容的时候需要用到<iframe></iframe>框架站外内容最近在发布内容时就遇到这个问题,<iframe></iframe>给转 ...

  9. LR回放https协议脚本失败: 错误 -27778: 在尝试与主机“www.baidu.com”connect 时发生 SSL 协议错误

    今天用LR录制脚本协议为https协议,回放脚本时出现报错: Action.c(14): 错误 -27778: 在尝试与主机"www.baidu.com"connect 时发生 S ...

  10. 弹窗查看内容时 内容滚动区域设置为body区

    看到渣浪的查看文章或者查看大图有个效果:弹窗查看内容时,如果内容过长有滚动条,则滚动条会被放到body区滚动 什么意思呢? 看个图片,一般正常弹窗是有宽高限制的,如果内容过长则直接在弹窗中进行滚动 点 ...

随机推荐

  1. CPython, Pypy, MicroPython...还在傻傻分不清楚?

    哈喽大家好,我是咸鱼 当我们说 Python 时,通常指的是官方实现的 CPython 但还有很多比如 Pypy.Jython.MicroPython.Brython.RustPython 等 &qu ...

  2. Java中AQS的原理与实现

    目录 1:什么是AQS? 2:AQS都有那些用途? 3:我们如何使用AQS 4:AQS的实现原理 5:对AQS的设计与实现的一些思考 1:什么是AQS ​ 随着计算机的算力越来越强大,各种各样的并行编 ...

  3. 即构自研海量有序数据网络MSDN,构建全球可靠的多云通讯链路

    2020是实时音视频技术应用大爆发的一年,电商直播.视频会议.在线课堂等多个场景获得了广泛关注.即构科技作为全球领先的云通讯商,截止目前已服务超过4000家企业客户,每日音视频通话时长超过20亿分钟, ...

  4. 360OS张焰:AI视觉在教育中的应用

    11月24日,由即构科技主办的2020GET大会教育科技分论坛在北京成功召开,来自叮咚课堂.小冰.360OS.蕃茄田艺术.即构科技的6位资深教育/科技大咖,在论坛上进行深度分享. 以下为360OSAI ...

  5. Centos7 安装部署 Kubernetes(k8s) 高可用集群

    目录 一.系统环境 二.前言 三.Kubernetes(k8s)高可用简介 四.配置机器基本环境 五.部署haproxy负载均衡器 六.部署etcd集群 七.部署Kubernetes(k8s) mas ...

  6. Win32API中的宽字符

    4.1了解什么是Win32API Win32API就是windows操作系统提供给我们的函数(应用程序接口),其主要存放在C:\Windows\System32 (存储的DLL是64位).C:\Win ...

  7. Flutter系列文章-Flutter进阶

    在前两篇文章中,我们已经了解了Flutter的基础知识,包括Flutter的设计理念.框架结构.Widget系统.基础Widgets以及布局.在本文中,我们将进一步探讨Flutter的高级主题,包括处 ...

  8. 类WPF跨平台模仿TIM

    类WPF跨平台模仿TIM Avalonia是什么? Avalonia 是一个功能强大的框架,使开发人员能够使用 .NET 创建跨平台应用程序.它使用自己的渲染引擎来绘制UI控件,确保在各种平台上保持一 ...

  9. 质效提升 | QA不做业务需求测试,你怎么看?

    ​因为有的小伙伴看到公司的QA不测试业务需求,只搞流程.卡点.规范.技术创新.QA平台,行业洞察,让研发自测.研发担责上线bug和风险,所以问我,你怎么看QA不做业务需求测试这件事.其实我怎么看不重要 ...

  10. SpringMVC配置web.xml文件详解(列举常用的配置)

    常用的web.xml的配置 1.Spring 框架解决字符串编码问题:过滤器 CharacterEncodingFilter(filter-name) 2.在web.xml配置监听器ContextLo ...