Linux内核之struct pt_regs结构
前沿
项目开发最近进行系统hook功能实现相关业务,主要在centos7和8系列环境开发下关功能。调研了相关知识点,发现在系统7和8上内核版本差别比较大,7-3.10.x系列版本,8-4.18.x系列版本。依据两个系统的内核情况根对应的内核符号表进行数据业务的钩子功能开发。开发过程发现两个系统虽然都定义对应的钩子目标函数定义,但是实际过程发现在7系统上直接定义于目标函数相同的函数进行hook即可,而系统8系列可以直接将参数定义成pt_regs进行处理。那么问题来了,这个结构是个什么?有什么用?如何使用?带着这么几个问题来逐个来介绍。
1.struct pt_regs是什么
pt_regs内核定义一个结构,其文件定义路径通常为arch/x86/include/asm/ptrace.h,如下结构定义:
(__i386__)
struct pt_regs {unsigned long bx;// bx %ebx 通用寄存器,通常用作基址寄存器(Base Register),保存数据或地址。unsigned long cx;// cx %ecx 通用寄存器,常用作计数器(Counter),如循环计数或字符串操作。unsigned long dx;// dx %edx 通用寄存器,通常与 %eax 配合使用,存放数据或 I/O 端口地址。unsigned long si;// si %esi 源索引寄存器(Source Index),用于内存操作的源地址(如 movs 指令)。unsigned long di;// di %edi 目的索引寄存器(Destination Index),用于内存操作的目的地址。unsigned long bp;// bp %ebp 基址指针寄存器(Base Pointer),指向当前栈帧的基地址,用于函数调用。unsigned long ax;// ax %eax 累加寄存器(Accumulator),用于算术运算和系统调用的返回值。unsigned long ds;// ds %ds 数据段寄存器(Data Segment),指向当前数据段的段选择子。unsigned long es;// es %es 附加段寄存器(Extra Segment),用于某些内存操作(如字符串操作)。unsigned long fs;// fs %fs 附加段寄存器,Linux 内核中通常用于线程本地存储(TLS)或特定内核用途。unsigned long gs;// gs %gs 附加段寄存器,用途与 %fs 类似,可能用于特定扩展。unsigned long orig_ax;// orig_ax - 原始系统调用号或中断错误码:- 系统调用时保存系统调用号(如 __NR_read)。- 中断或异常时保存错误码或中断向量号。unsigned long ip;// ip %eip 指令指针寄存器(Instruction Pointer),指向下一条要执行的指令地址。unsigned long cs;// cs %cs 代码段寄存器(Code Segment),保存当前代码段的段选择子。unsigned long flags;// flags %eflags 标志寄存器,保存 CPU 状态标志(如中断使能、方向标志、溢出标志等)。unsigned long sp;// sp %esp 栈指针寄存器(Stack Pointer),指向当前栈顶地址。unsigned long ss;// ss %ss 栈段寄存器(Stack Segment),指向当前栈段的段选择子。
};
x86_64位:
struct pt_regs {unsigned long r15; // r15 %r15 通用寄存器,通常用于保存临时数据或地址。在系统调用中可能作为第 6 个参数(若需要)。unsigned long r14; // r14 %r14 通用寄存器,用途同上。unsigned long r13; // r13 %r13 通用寄存器,用途同上。unsigned long r12; // r12 %r12 通用寄存器,用途同上。unsigned long bp; // bp %rbp 基址指针寄存器,指向当前栈帧的基地址,用于函数调用栈回溯。unsigned long bx; // bx %rbx 基址寄存器,常用于保存数据或地址(如内存操作的基地址)。unsigned long r11; // r11 %r11 通用寄存器,可能用于临时存储或特定指令(如 syscall 指令会破坏 %r11)。unsigned long r10; // r10 %r10 通用寄存器,在系统调用中作为第 4 个参数(若需要)。unsigned long r9; // r9 %r9 通用寄存器,在系统调用中作为第 5 个参数(若需要)。unsigned long r8; // r8 %r8 通用寄存器,在系统调用中作为第 6 个参数(若需要)。unsigned long ax; // ax %rax 累加寄存器,用于系统调用的返回值(ret 前内核会设置 regs->ax)。unsigned long cx; // cx %rcx 计数器寄存器,在 syscall 指令中保存返回地址(被破坏),某些场景下可能作为第 4 个参数。unsigned long dx; // dx %rdx 数据寄存器,通常用于系统调用的第 3 个参数。unsigned long si; // si %rsi 源索引寄存器,通常用于系统调用的第 2 个参数。unsigned long di; // di %rdi 目的索引寄存器,通常用于系统调用的第 1 个参数。unsigned long orig_ax; // orig_ax-原始系统调用号或中断错误码:- 系统调用时保存系统调用号(如 __NR_read),- 中断或异常时保存中断向量号或错误码。unsigned long ip; // ip %rip 指令指针寄存器,指向触发中断/异常/系统调用的下一条指令地址(即用户空间的返回地址)。unsigned long cs; // cs %cs 代码段寄存器,保存当前代码段的段选择子(用户态为 0x33,内核态为 0x10)。unsigned long flags; // flags %rflags 标志寄存器,保存 CPU 状态标志(如中断使能、方向标志、溢出标志等)。unsigned long sp; // sp %rsp 栈指针寄存器,指向用户空间的栈顶地址。unsigned long ss; // ss %ss 栈段寄存器,保存当前栈段的段选择子(用户态为 0x2b)。
};
上述两个结构均为x86架构定义,但是实际次做中发现在arm重有偏差,这里简要列出结构,后有文章详细介绍相关用途,结构如下:
arm64架构pt_regs结构定义:
/** This struct defines the way the registers are stored on the stack during an* exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for* stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.*此结构定义了异常期间寄存器在堆栈上的存储方式。需要注意的是,sizeof(struct pt_regs)必须是16的倍数(用于堆栈对齐)。*另外,结构user_pt_regs必须构成结构pt_regs的前缀。*/struct pt_regs {union {struct user_pt_regs user_regs;struct {u64 regs[31]; // X0-X30(通用寄存器)系统调用参数通过 regs[0]-regs[7](X0-X7)传递。u64 sp; // 栈指针 (SP_EL0) 保存用户空间栈顶地址,用于恢复用户态执行。u64 pc; // 程序计数器 (PC)保存触发中断/异常/系统调用的指令地址(即返回地址)。u64 pstate; // 处理器状态 (PSTATE)};};u64 orig_x0; //保存系统调用 第一个参数的原始值。某些系统调用(如 restart_syscall)需要恢复原始参数
#ifdef __AARCH64EB__u32 unused2;s32 syscallno; //在传统 ARM64 系统调用中,系统调用号通过 X8 寄存器传递,但内核可能将其复制到 syscallno 字段。
#elses32 syscallno;u32 unused2;
#endifu64 orig_addr_limit; //保存用户态进程的地址访问限制(如 USER_DS)。在内核态执行时临时扩大地址限制(如 KERNEL_DS),执行完毕后恢复/* Only valid when ARM64_HAS_IRQ_PRIO_MASKING is enabled. */u64 pmr_save; //中断优先级掩码保存:仅当启用 ARM64_HAS_IRQ_PRIO_MASKING 时有效。保存中断处理前的 PMR 值,处理完成后恢复。u64 stackframe[2]; //父函数的帧指针(FP)。父函数的返回地址(LR)。
};
2.pt_regs有什么用
通过该结构,当需要进行内核函数hook时,可以简化对应目标钩子函数的定义以及接口函数的标准化,避免大量的函数指针来制定具体的函数定义(参数个数和参数类型差别)。从而统一接口类型,使钩子函数实现简化执行。例如:
原函类型:
typedef asmlinkage long (*sys_call_file_mkdirat_t)(int dfd, const char __user * pathname, umode_t mode);
static sys_call_file_mkdirat_t old_sys_mkdirat;typedef asmlinkage long (*sys_call_file_unlinkat_t)(int dfd, const char __user * pathname, int flag);
static sys_call_file_unlinkat_t old_sys_unlinkat;
pt_regs:
typedef asmlinkage long (*sys_call_file_t)(const struct pt_regs *);static sys_call_file_t[2];// 统一hook函数接口,具体参数依据实际函数定义从regs结构中按照寄存器位获取。// 例如
static sys_call_file_t[2]; // 初始化是会将需要hook的函数地址进行存储处理asmlinkage long mkdir_hook(const struct pt_regs *regs)
{int dfd = regs->di;char *filename = (char *)regs->si;umode_t mode = regs->dx;sys_call_file_t origin_mkdirat = sys_call_file_t[__NR_mkdirat]; //获取hook后的原函数地址//todo ...return origin_mkdirat(regs); // 原函数}asmlinkage long unlinkat_hook(const struct pt_regs *regs)
{int dfd = regs->di;char *filename = (char *)regs->si;int flag = regs->dx;sys_call_file_t origin_unlinkat = sys_call_file_t[__NR_unlinkat]; //获取hook后的原函数地址//todo ...return origin_unlinkat(regs); // 原函数
}
3.pt_regs如何使用
例如__NR_write对应32位机器调用内核函数原型如下:
asmlinkage long sys_write(unsigned int fd, const char __user *buf,size_t count);
通过自定义函数结合pt_regs进行hook处理:
asmlinkage long self_write_hook(const struct pt_regs* regs)
{int fd = regs->bx; // 第一个参数 fd 通过bx 传递char *buf = (char *)regs->cx;// 第二个参数 buf 通过ecxsize_t count = regs->dx; // 第三个参数 count 通过edx// to ...return origin_read(regs); // origin_read为hook时存储的原始函数地址
}
上述为针对sys_write函数的hook调用时通过regs结构来实现相关参数的获取处理。
4.实际使用案例
通过对openat为例子进行hook,具体代码如下:
my_openat_hook.c:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/unistd.h>
#include <linux/sched.h>
#include <linux/syscalls.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/file.h>
#include <linux/fdtable.h>
#include <linux/uaccess.h>
#include <linux/kallsyms.h>
#include <linux/vmalloc.h>
#include <linux/slab.h>
#include <linux/kprobes.h>
#include <linux/fs_struct.h>
#include <linux/mm.h>
#include <linux/delay.h>
#include <linux/namei.h>
#include <asm/syscall.h>
#include <uapi/linux/mount.h>//符号表获取
typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
kallsyms_lookup_name_t get_kallsyms_lookup_name(void)
{int ret;kallsyms_lookup_name_t pfun;static struct kprobe kp ={.symbol_name = "kallsyms_lookup_name",};ret = register_kprobe(&kp);if (ret < 0){printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);return NULL;}pfun = (kallsyms_lookup_name_t)kp.addr;unregister_kprobe(&kp);return pfun;
}static int obtain_sys_call_table_addr(unsigned long *sys_call_table_addr)
{unsigned long temp_sys_call_table_addr;kallsyms_lookup_name_t fn_kallsyms_lookup_name = 0;fn_kallsyms_lookup_name = get_kallsyms_lookup_name();if (fn_kallsyms_lookup_name == NULL){printk("Fail to get_allsyms_lookup_name\n");return -1;}temp_sys_call_table_addr = fn_kallsyms_lookup_name("sys_call_table");/* Return error if the symbol doesn't exist */if (0 == temp_sys_call_table_addr){printk("Can not found sys_call_table\n");return -1;}printk("Found sys_call_table: %p", (void *)temp_sys_call_table_addr);*sys_call_table_addr = temp_sys_call_table_addr;return 0;
}static unsigned long sys_call_table_ptr;
typedef asmlinkage long (*self_openat_hook_t)(const struct pt_regs*);
static self_openat_hook_t old_self_openat;
unsigned int disable_cr0(void)
{unsigned int cr0 = 0;unsigned int ret;asm volatile ("movq %%cr0, %%rax": "=a"(cr0));ret = cr0;cr0 &= 0xfffeffff;asm volatile ("movq %%rax, %%cr0"::"a"(cr0));return ret;
}
void enable_cr0(unsigned int val)
{asm volatile ("movq %%rax, %%cr0": : "a"(val));
}asmlinkage long self_openat_hook(const struct pt_regs* regs)
{int dfd = regs->di;char* filename = (char*)regs->si;int flag = regs->dx;umode_t mode = regs->r10;printk(KERN_INFO "dfd = %d,filename = %s,flag = %d,mode = %d",dfd,filename,flag,mode);return old_self_openat(regs);
}static int __init self_init(void)
{int cr0;obtain_sys_call_table_addr(&sys_call_table_ptr);cr0 = disable_cr0();old_self_openat = (self_openat_hook_t)((unsigned long*)sys_call_table_ptr)[__NR_openat];// 保留旧函数((unsigned long*)sys_call_table_ptr)[__NR_openat] = (unsigned long)self_openat_hook; // 设置新函数enable_cr0(cr0);printk("Success to hook openat func !!!");return 0;
}static void __exit self_exit(void) {int cr0;cr0 = disable_cr0();((unsigned long*)sys_call_table_ptr)[__NR_openat] = old_self_openat;enable_cr0(cr0);printk("Exit to hook openat func !!!");return;
}module_init(self_init)
module_exit(self_exit)
MODULE_LICENSE("GPL");
Makefile:
obj-m := my_openat_hook.oPWD := $(shell pwd)KERNEL_DIR := "/lib/modules/$(shell uname -r)/build"EXTRA_CFLAGS += -I$(src)/includemodules:@$(MAKE) -C $(KERNEL_DIR) M=$(PWD) modulesclean:@rm -rf *.ko *.o *.mod.c *symvers *order .*cmd *cmd
将ko文件写入到系统:
insmod my_openat.ko,通过dmesg -w查看系统日志:
以上为使用pt_retgs结构实现的内核函数hook功能案例。