Linux - 解读aarch64_insn_(read/write/patch_text)

AArch64架构(ARM 64位架构)中,以 aarch64_insn_xxx 命名的一系列函数(例如 aarch64_insn_readaarch64_insn_writeaarch64_insn_patch 等)主要用于直接操作或分析 机器指令(machine instructions)。它们是底层系统编程的核心工具,常见于操作系统内核、调试器、虚拟机监控程序(Hypervisor)或即时编译(JIT)引擎中。典中典的用途就是bpf啦!

我们先从最开始的源代码去看,额就是看头文件啦:

// May 28, 2021 Linux Kernel 5.13版本开始支持以下操作 
#ifndef __ASSEMBLY__
int aarch64_insn_read(void *addr, u32 *insnp);
int aarch64_insn_write(void *addr, u32 insn);
int aarch64_insn_patch_text_nosync(void *addr, u32 insn);
int aarch64_insn_patch_text(void *addrs[], u32 insns[], int cnt);
#endif /* __ASSEMBLY__ */

// Linux Kernel 6.2-rc2加入
int aarch64_insn_write_literal_u64(void *addr, u64 val);

// Linux Kernel 6.8-rc7加入
void *aarch64_insn_set(void *dst, u32 insn, size_t len);
void *aarch64_insn_copy(void *dst, void *src, size_t len);

头文件路径为arch/arm64/include/asm/patching.h

其实也不算加入,低版本内核这几个函数的位置不在同一个地方而已

先初略的说一下这几个玩意的用途,不按顺序:

  • aarch64_insn_write_literal_u64安全且原子地写入一个自然对齐的 64 位字面量(literal)到内核代码段中,所谓原子就是要满足点击跳转过去的特性。至于什么是字面量?你当我是宝宝巴士快乐启蒙呢!什么都给你说,爬一边问Deepseek去!

  • aarch64_insn_set|aarch64_insn_copy在 ARM64 架构的内核开发中,text_poke 是一个新实现的 动态代码修改接口,其核心目标是提供一种安全、通用的方式,对内核的 只读且可执行(RO+X)的代码段 进行读写操作(如批量写入或填充指令)。该接口的灵感来源于 x86 架构的同名实现,旨在支持诸如 BPF JIT 编译器等需要动态生成或修改机器码的场景。(什么?你说没看见text_poke?哦,那玩意在patching.c里面,嘻嘻嘻)

aarch64_insn_read

这个东西怎么说呢?就是字面意思读一下就完事了,hhh(((

int __kprobes aarch64_insn_read(void *addr, u32 *insnp)
{
	int ret;
	__le32 val;

	ret = copy_from_kernel_nofault(&val, addr, AARCH64_INSN_SIZE);
	if (!ret)
		*insnp = le32_to_cpu(val);

	return ret;
}

真就读一下,给你看源代码喏!

aarch64_insn_write

在看之前,我们先说一下patch_map ,显而易见

static void __kprobes *patch_map(void *addr, int fixmap)
{
	phys_addr_t phys;

	if (is_image_text((unsigned long)addr)) { // 用断某个地址是否位于内核镜像的.text section中
		phys = __pa_symbol(addr);
	} else {
		struct page *page = vmalloc_to_page(addr);
		BUG_ON(!page);
		phys = page_to_phys(page) + offset_in_page(addr);
	}

	return (void *)set_fixmap_offset(fixmap, phys); // 设置可读可写,就是把pte改成PAGE_KERNEL原本应该是KERNEL_RO?
}

static void __kprobes patch_unmap(int fixmap)
{
	clear_fixmap(fixmap);
}

你是不是有什么小算盘说那这个patch_map到处乱改?其实这个patch_map使用的是内核里面的那个叫fixmap的东西,恰恰好,这玩意有限制的,我们看看,它可以修改哪些地址的权限

enum fixed_addresses {
	FIX_EARLYCON_MEM_BASE, // 早期控制台的内存映射
	__end_of_permanent_fixed_addresses, // 永久固定映射区域的结束标记

    // 高端内存映射区域的起始和结束
	FIX_KMAP_BEGIN = __end_of_permanent_fixed_addresses,
	FIX_KMAP_END = FIX_KMAP_BEGIN + (KM_MAX_IDX * NR_CPUS) - 1,

	/* Support writing RO kernel text via kprobes, jump labels, etc. */
    // 用于内核代码修改的临时映射槽位
	FIX_TEXT_POKE0,
	FIX_TEXT_POKE1,

	__end_of_fixmap_region,

	/*
	 * Share the kmap() region with early_ioremap(): this is guaranteed
	 * not to clash since early_ioremap() is only available before
	 * paging_init(), and kmap() only after.
	 */
    // 早期 IO 映射的配置
#define NR_FIX_BTMAPS      32   // 每个槽位组的映射数量
#define FIX_BTMAPS_SLOTS   7    // 槽位组的数量
#define TOTAL_FIX_BTMAPS	(NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)

	FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
	FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,
	__end_of_early_ioremap_region
};

掏出我们的主角↓

static int __kprobes __aarch64_insn_write(void *addr, __le32 insn)
{
	void *waddr = addr;
	unsigned long flags = 0;
	int ret;

	raw_spin_lock_irqsave(&patch_lock, flags); // 嗝,奇怪的锁
	waddr = patch_map(addr, FIX_TEXT_POKE0);

	ret = copy_to_kernel_nofault(waddr, &insn, AARCH64_INSN_SIZE); // 字面意思就是不会panic而已

	patch_unmap(FIX_TEXT_POKE0);
	raw_spin_unlock_irqrestore(&patch_lock, flags);

	return ret;
}

aarch64_insn_set

aarch64_insn_write大差不差,区别就是改完之后他会同步icache