拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

init mark_rodata_ro

init mark_rodata_ro

mark_rodata_ro是 x86-64 架构中用于将内核的只读数据段(.rodata)和代码段(.text)在页表级别设置为只读的初始化函数。它的核心目的是利用硬件的内存保护机制,防止内核启动后意外或恶意的修改。

核心作用:硬件级写保护

在内核启动早期,所有内存页都是可写的,因为内核需要修改页表、初始化数据结构。当初始化基本完成、即将启动第一个用户进程之前,内核会调用mark_rodata_ro,将那些“本不该被修改”的内存区域(内核代码和只读数据)在页表项中清除写权限。

这个函数由mark_readonly()在CONFIG_STRICT_KERNEL_RWX=y时调用。值得注意的是,CONFIG_STRICT_KERNEL_RWX在 x86、arm64 和 s390 上是强制启用的,无法通过make menuconfig关闭。

实现逻辑

该函数在 x86-64 上的典型实现位于arch/x86/mm/init_64.c,核心步骤如下:

  1. 确定保护范围:通常从_stext(内核代码段起始)开始,到__end_rodata(只读数据段结束)为止。在某些实现中,起始地址会跳过第一页(_stext + PAGE_SIZE),因为内核页全局目录(kernel_pg_dir)位于该页中。

  2. 设置页表属性:通过set_memory_ro()或类似机制,将指定范围内的页面标记为只读。在早期版本中,使用的是change_page_attr_addr()配合PAGE_KERNEL_RO。

  3. 刷新 TLB:修改页表后,必须刷新 TLB(转换检测缓冲区),以确保 CPU 使用新的权限。通常通过global_flush_tlb()或flush_tlb_all()完成。

  4. 非可执行标记:对于.rodata段之后的数据区域,还会调用set_memory_nx()将其标记为不可执行,防止数据被当作代码执行。

与其他机制的关系

mark_rodata_ro与内核的动态代码修改机制存在交互:

  • ftrace/kprobes:这些机制需要临时修改内核代码。因此,内核引入了kernel_set_to_readonly标志和set_kernel_text_rw()等函数,允许在需要时临时将代码段恢复为可写,完成修改后再重新保护。

  • CONFIG_DEBUG_RODATA:在较新的内核中,这个配置选项已被CONFIG_STRICT_KERNEL_RWX取代。旧版的mark_rodata_ro就是由CONFIG_DEBUG_RODATA控制的。

安全意义

这个函数是内核自我加固(Self-Hardening)的关键一步。通过将代码和只读数据标记为只读,它显著增加了利用内核漏洞的难度——攻击者无法直接覆写内核代码或关键数据结构来执行恶意代码。这也是现代内核安全基线的核心组成部分。

调用时机与调用者

mark_rodata_ro()的调用关系非常清晰,它位于free_initmem()之后、启动第一个用户进程之前,是内核从“初始化阶段”切换到“运行阶段”的标志性步骤之一。

这个函数由kernel_init()(PID 1,即 init 进程的内核线程部分)在启动流程中调用。

典型的调用顺序可以简化为:

rest_init() 创建 kernel_init (PID 1) ↓ kernel_init() 等待 kthreadd (PID 2) 完成 ↓ kernel_init_freeable() 执行 do_basic_setup() 等初始化 ↓ free_initmem() 释放 __init 代码和数据段 ↓ mark_rodata_ro() 将 .text 和 .rodata 设为只读 ↓ run_init_process() 启动第一个用户态进程 (/sbin/init)

内核文档注释明确写道:“Linux calls mark_rodata_ro() after freeing .init code and prior to calling the first init userspace process”。这句话精炼地概括了它的位置。

为什么必须在这个时机调用?

这个时机是精心选择的,有三个关键原因:

  1. __init代码已释放:在free_initmem()之后,所有标记为__init的函数(仅在初始化期间运行一次)所占用的内存已经被归还给伙伴系统。mark_rodata_ro()此时再将.text和.rodata设为只读,不会影响任何还在运行的初始化代码。

  2. __ro_after_init数据已定型:内核中有一类特殊数据(如phys_base),它们在初始化期间需要被写入,但初始化完成后就永不改变。这些数据被放在专门的段中,在mark_rodata_ro()执行时一并被设为只读,从而在运行期间受到硬件写保护。

  3. 第一个用户进程尚未启动:此时系统仍处于纯粹的内核态,没有用户态代码运行。在用户态程序开始执行前完成只读保护,可以确保内核的攻击面在最早的时刻就被最小化。

架构相关的实现

mark_rodata_ro()是一个架构相关的函数,每个支持严格内核内存保护(CONFIG_STRICT_KERNEL_RWX)的架构都需要实现它。在 x86 上,它的实现位于arch/x86/mm/init_64.c,通过set_memory_ro()和set_memory_nx()修改页表项,然后刷新 TLB 使新的权限生效。

返回列表