当前位置: 首页 > news >正文

Linux execve系统调用深度解析:从用户空间到进程替换的完整旅程

前言

在Linux系统中,进程执行新程序是一个看似简单却蕴含复杂内核机制的过程。当我们敲下exec ls命令时,背后究竟发生了什么?这个看似瞬间完成的动作,实际上经历了层层安全检查、资源分配、格式识别和上下文切换的精密协作。

本文将以execve系统调用为核心,深入剖析Linux内核如何将磁盘上的可执行文件转化为运行中的进程。我们将跟随内核代码的执行路径,从用户空间调用开始,穿越系统调用入口、文件打开、权限验证、二进制格式识别,直至最终的程序执行。每一行代码都承载着操作系统的设计哲学:安全、效率和可扩展性

execve系统调用sys_execve

asmlinkage int sys_execve(struct pt_regs regs)
{int error;char * filename;filename = getname((char __user *) regs.ebx);error = PTR_ERR(filename);if (IS_ERR(filename))goto out;error = do_execve(filename,(char __user * __user *) regs.ecx,(char __user * __user *) regs.edx,&regs);if (error == 0) {task_lock(current);current->ptrace &= ~PT_DTRACE;task_unlock(current);/* Make sure we don't return using sysenter.. */set_thread_flag(TIF_IRET);}putname(filename);
out:return error;
}

函数功能概述

sys_execve 函数是execve系统调用的内核实现,负责加载并执行新的程序,替换当前进程的地址空间

代码详细解析

函数声明

asmlinkage int sys_execve(struct pt_regs regs)
{
  • asmlinkage: 表示参数通过栈传递,这是系统调用的标准约定
  • int: 返回类型,0表示成功,负值表示错误
  • sys_execve: 系统调用实现函数名
  • struct pt_regs regs: 包含所有寄存器值的结构体,通过栈传递

变量定义

	int error;char * filename;
  • int error: 用于存储操作结果和错误代码
  • char * filename: 用于存储从用户空间复制的文件名

获取文件名

	filename = getname((char __user *) regs.ebx);error = PTR_ERR(filename);if (IS_ERR(filename))goto out;
  • filename = getname((char __user *) regs.ebx):

    • regs.ebx: 从寄存器结构体中获取ebx寄存器的值(第一个参数)
    • (char __user *): 转换为用户空间指针类型
    • getname(): 从用户空间安全复制文件名到内核空间
  • error = PTR_ERR(filename): 如果getname失败,将错误指针转换为错误代码

  • if (IS_ERR(filename)): 检查文件名获取是否失败

  • goto out: 如果失败,跳转到清理路径

执行程序加载

	error = do_execve(filename,(char __user * __user *) regs.ecx,(char __user * __user *) regs.edx,&regs);
  • do_execve(): 实际执行程序加载的核心函数

    • filename: 程序文件名(内核空间)
    • (char __user * __user *) regs.ecx: 参数数组(用户空间指针的指针)
    • (char __user * __user *) regs.edx: 环境变量数组(用户空间指针的指针)
    • &regs: 寄存器结构体指针,用于保存执行上下文
  • x86系统调用寄存器约定:

  • ebx: 文件名指针(第一个参数)

  • ecx: 命令行参数数组(第二个参数)

  • edx: 环境变量数组(第三个参数)

执行成功处理

	if (error == 0) {task_lock(current);current->ptrace &= ~PT_DTRACE;task_unlock(current);/* Make sure we don't return using sysenter.. */set_thread_flag(TIF_IRET);}
  • task_lock(current): 锁定当前任务的锁,防止并发访问

  • current->ptrace &= ~PT_DTRACE: 清除PT_DTRACE跟踪标志

    • PT_DTRACE: 用于DTrace动态跟踪的标志
    • 新程序开始执行时清除旧的跟踪状态
  • task_unlock(current): 释放任务锁

  • set_thread_flag(TIF_IRET): 设置线程标志,强制使用IRET指令返回

  • sysenter vs IRET:

    • sysenter是快速系统调用指令,但不能用于execve后的返回
    • IRET是传统的中断返回,可以正确处理新程序的上下文

资源清理

	putname(filename);
  • putname(filename): 释放由getname()分配的文件名缓冲区
  • 必须配对调用,防止内存泄漏

返回错误处理

out:return error;
}
  • out:: 标签,用于错误处理和成功路径的汇合点
  • return error: 返回最终的操作结果

系统调用执行流程

用户空间调用execve()↓
通过int 0x80进入内核↓
sys_execve(regs) ← 寄存器状态↓
do_execve() ← 核心加载逻辑↓
成功: 进程被替换,不返回
失败: 返回错误代码

加载并执行可执行文件do_execve

int do_execve(char * filename,char __user *__user *argv,char __user *__user *envp,struct pt_regs * regs)
{struct linux_binprm *bprm;struct file *file;int retval;int i;retval = -ENOMEM;bprm = kmalloc(sizeof(*bprm), GFP_KERNEL);if (!bprm)goto out_ret;memset(bprm, 0, sizeof(*bprm));file = open_exec(filename);retval = PTR_ERR(file);if (IS_ERR(file))goto out_kfree;sched_exec();bprm->p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);bprm->file = file;bprm->filename = filename;bprm->interp = filename;bprm->mm = mm_alloc();retval = -ENOMEM;if (!bprm->mm)goto out_file;retval = init_new_context(current, bprm->mm);if (retval < 0)goto out_mm;bprm->argc = count(argv, bprm->p / sizeof(void *));if ((retval = bprm->argc) < 0)goto out_mm;bprm->envc = count(envp, bprm->p / sizeof(void *));if ((retval = bprm->envc) < 0)goto out_mm;retval = security_bprm_alloc(bprm);if (retval)goto out;retval = prepare_binprm(bprm);if (retval < 0)goto out;retval = copy_strings_kernel(1, &bprm->filename, bprm);if (retval < 0)goto out;bprm->exec = bprm->p;retval = copy_strings(bprm->envc, envp, bprm);if (retval < 0)goto out;retval = copy_strings(bprm->argc, argv, bprm);if (retval < 0)goto out;retval = search_binary_handler(bprm,regs);if (retval >= 0) {free_arg_pages(bprm);/* execve success */security_bprm_free(bprm);kfree(bprm);return retval;}out:/* Something went wrong, return the inode and free the argument pages*/for (i = 0 ; i < MAX_ARG_PAGES ; i++) {struct page * page = bprm->page[i];if (page)__free_page(page);}if (bprm->security)security_bprm_free(bprm);out_mm:if (bprm->mm)mmdrop(bprm->mm);out_file:if (bprm->file) {allow_write_access(bprm->file);fput(bprm->file);}out_kfree:kfree(bprm);out_ret:return retval;
}

函数功能概述

do_execve 函数负责加载并执行可执行文件,替换当前进程的地址空间,是execve系统调用的核心实现

代码详细解析

函数声明

int do_execve(char * filename,char __user *__user *argv,char __user *__user *envp,struct pt_regs * regs)
{
  • int: 返回类型,0表示成功,负值表示错误
  • filename: 可执行文件路径(内核空间)
  • argv: 命令行参数数组(用户空间指针的指针)
  • envp: 环境变量数组(用户空间指针的指针)
  • regs: 寄存器状态,用于设置新程序的执行上下文

变量定义

	struct linux_binprm *bprm;struct file *file;int retval;int i;
  • struct linux_binprm *bprm: 二进制程序参数结构,存储执行所需的所有信息
  • struct file *file: 可执行文件对象
  • int retval: 操作返回值
  • int i: 循环计数器

二进制参数结构分配

	retval = -ENOMEM;bprm = kmalloc(sizeof(*bprm), GFP_KERNEL);if (!bprm)goto out_ret;memset(bprm, 0, sizeof(*bprm));
  • retval = -ENOMEM: 预设错误码为内存不足
  • bprm = kmalloc(sizeof(*bprm), GFP_KERNEL): 分配linux_binprm结构内存
  • if (!bprm) goto out_ret: 如果分配失败,跳转到返回
  • memset(bprm, 0, sizeof(*bprm)): 清空结构体,确保初始状态

打开可执行文件

	file = open_exec(filename);retval = PTR_ERR(file);if (IS_ERR(file))goto out_kfree;
  • file = open_exec(filename): 以可执行方式打开文件
    • 检查文件权限(必须有执行权限)
    • 验证文件类型
  • retval = PTR_ERR(file): 保存可能的错误代码
  • if (IS_ERR(file)) goto out_kfree: 如果打开失败,跳转到清理路径

调度执行准备

	sched_exec();
  • sched_exec(): 通知调度器进程即将执行新程序
    • 可能进行负载均衡决策
    • 准备进程调度上下文

参数页面空间计算

	bprm->p = PAGE_SIZE*MAX_ARG_PAGES-sizeof(void *);
  • bprm->p: 参数栈指针位置
  • PAGE_SIZE*MAX_ARG_PAGES: 参数栈总大小(通常128KB)
  • -sizeof(void *): 为栈顶的NULL指针预留空间

二进制参数结构填充

	bprm->file = file;bprm->filename = filename;bprm->interp = filename;
  • bprm->file = file: 存储文件对象
  • bprm->filename = filename: 存储文件名
  • bprm->interp = filename: 设置解释器名(初始为文件名自身)

内存管理结构分配

	bprm->mm = mm_alloc();retval = -ENOMEM;if (!bprm->mm)goto out_file;
  • bprm->mm = mm_alloc(): 分配新的内存描述符(mm_struct)
  • 为新程序的地址空间准备内存管理结构
  • 如果分配失败,跳转到文件清理路径

初始化新上下文

	retval = init_new_context(current, bprm->mm);if (retval < 0)goto out_mm;
  • init_new_context(current, bprm->mm): 初始化新内存上下文
  • 设置LDT、TSS等架构特定的内存管理数据

参数计数

	bprm->argc = count(argv, bprm->p / sizeof(void *));if ((retval = bprm->argc) < 0)goto out_mm;bprm->envc = count(envp, bprm->p / sizeof(void *));if ((retval = bprm->envc) < 0)goto out_mm;
  • bprm->argc = count(argv, bprm->p / sizeof(void *)): 计算参数个数
    • 验证参数数量不超过栈空间限制
  • bprm->envc = count(envp, bprm->p / sizeof(void *)): 计算环境变量个数
  • 如果计数失败(如参数太多),跳转到内存清理路径

安全模块分配

	retval = security_bprm_alloc(bprm);if (retval)goto out;
  • security_bprm_alloc(bprm): 为安全模块(如SELinux)分配资源
  • 允许安全模块为bprm结构分配安全上下文

准备二进制程序

	retval = prepare_binprm(bprm);if (retval < 0)goto out;
  • prepare_binprm(bprm): 读取可执行文件头,准备执行
    • 读取文件头128字节
    • 设置执行权限和身份
    • 处理setuid/setgid

复制文件名

	retval = copy_strings_kernel(1, &bprm->filename, bprm);if (retval < 0)goto out;
  • copy_strings_kernel(1, &bprm->filename, bprm): 将文件名复制到参数栈
  • 文件名需要在新程序的地址空间中可用

设置执行指针和复制字符串

	bprm->exec = bprm->p;retval = copy_strings(bprm->envc, envp, bprm);if (retval < 0)goto out;retval = copy_strings(bprm->argc, argv, bprm);if (retval < 0)goto out;
  • bprm->exec = bprm->p: 记录当前栈位置
  • copy_strings(bprm->envc, envp, bprm): 复制环境变量到栈中
  • copy_strings(bprm->argc, argv, bprm): 复制命令行参数到栈中

搜索并执行二进制处理程序

	retval = search_binary_handler(bprm,regs);if (retval >= 0) {free_arg_pages(bprm);/* execve success */security_bprm_free(bprm);kfree(bprm);return retval;}
  • search_binary_handler(bprm,regs): 搜索匹配的二进制格式处理程序
    • 尝试ELF、a.out、脚本等格式
    • 调用对应的加载函数
  • 如果成功(retval >= 0):
    • free_arg_pages(bprm): 释放参数页面
    • security_bprm_free(bprm): 释放安全模块资源
    • kfree(bprm): 释放bprm结构
    • 返回成功(实际上进程已被替换,不会返回)

错误处理路径

out:/* Something went wrong, return the inode and free the argument pages*/for (i = 0 ; i < MAX_ARG_PAGES ; i++) {struct page * page = bprm->page[i];if (page)__free_page(page);}if (bprm->security)security_bprm_free(bprm);out_mm:if (bprm->mm)mmdrop(bprm->mm);out_file:if (bprm->file) {allow_write_access(bprm->file);fput(bprm->file);}out_kfree:kfree(bprm);out_ret:return retval;
}

out标签(参数页面清理):

  • 循环释放所有参数页面
  • __free_page(page): 释放物理页面

安全资源清理:

  • security_bprm_free(bprm): 释放安全模块资源

out_mm标签(内存清理):

  • mmdrop(bprm->mm): 释放内存描述符

out_file标签(文件清理):

  • allow_write_access(bprm->file): 恢复文件写权限
  • fput(bprm->file): 释放文件引用

out_kfree标签(结构清理):

  • kfree(bprm): 释放bprm结构

out_ret标签(最终返回):

  • return retval: 返回错误代码

函数功能总结

主要功能:加载可执行文件并替换当前进程的地址空间

  1. 资源准备阶段

    • 分配二进制参数结构
    • 打开可执行文件
    • 分配内存管理结构
  2. 参数处理阶段

    • 计算参数和环境变量数量
    • 复制所有字符串到内核栈
    • 准备执行环境
  3. 二进制加载阶段

    • 识别可执行文件格式
    • 调用对应的加载处理器
    • 设置新程序执行上下文
  4. 清理阶段

    • 成功:释放临时资源,进程被替换
    • 失败:分级释放所有已分配资源

以执行模式打开一个可执行文件open_exec

struct file *open_exec(const char *name)
{struct nameidata nd;int err;struct file *file;nd.intent.open.flags = FMODE_READ;err = path_lookup(name, LOOKUP_FOLLOW|LOOKUP_OPEN, &nd);file = ERR_PTR(err);if (!err) {struct inode *inode = nd.dentry->d_inode;file = ERR_PTR(-EACCES);if (!(nd.mnt->mnt_flags & MNT_NOEXEC) &&S_ISREG(inode->i_mode)) {int err = permission(inode, MAY_EXEC, &nd);if (!err && !(inode->i_mode & 0111))err = -EACCES;file = ERR_PTR(err);if (!err) {file = dentry_open(nd.dentry, nd.mnt, O_RDONLY);if (!IS_ERR(file)) {err = deny_write_access(file);if (err) {fput(file);file = ERR_PTR(err);}}
out:return file;}}path_release(&nd);}goto out;
}

函数功能概述

open_exec 函数用于以"执行"模式打开一个可执行文件,进行必要的权限检查和安全验证,确保文件适合作为程序执行

代码逐段解析

变量声明和初始化

struct nameidata nd;
int err;
struct file *file;nd.intent.open.flags = FMODE_READ;
  • struct nameidata nd:用于存储路径查找结果的数据结构
  • int err:错误码变量
  • struct file *file:返回的文件指针
  • nd.intent.open.flags = FMODE_READ:设置打开标志为只读模式,因为可执行文件在执行时只需要读权限

路径查找

err = path_lookup(name, LOOKUP_FOLLOW|LOOKUP_OPEN, &nd);
file = ERR_PTR(err);
  • path_lookup(name, LOOKUP_FOLLOW|LOOKUP_OPEN, &nd):根据文件名查找路径
    • LOOKUP_FOLLOW:如果路径包含符号链接,则跟随链接
    • LOOKUP_OPEN:表示这是为了打开文件而进行的查找
  • file = ERR_PTR(err):如果查找失败,将错误码转换为指针格式存储

路径查找成功处理

if (!err) {struct inode *inode = nd.dentry->d_inode;file = ERR_PTR(-EACCES);
  • if (!err):如果路径查找成功(err == 0)
  • struct inode *inode = nd.dentry->d_inode:从目录项获取文件的inode
  • file = ERR_PTR(-EACCES):预设错误为"权限不足"

文件系统和执行权限检查

if (!(nd.mnt->mnt_flags & MNT_NOEXEC) &&S_ISREG(inode->i_mode)) {
  • !(nd.mnt->mnt_flags & MNT_NOEXEC):检查文件系统是否允许执行
    • MNT_NOEXEC 标志表示该文件系统挂载时设置了noexec选项
  • S_ISREG(inode->i_mode):检查文件是否是普通文件(不是目录、设备文件等)
  • 只有同时满足这两个条件,文件才可能被执行

文件权限检查

int err = permission(inode, MAY_EXEC, &nd);
if (!err && !(inode->i_mode & 0111))err = -EACCES;
file = ERR_PTR(err);
  • permission(inode, MAY_EXEC, &nd):检查当前进程是否有执行该文件的权限
  • !(inode->i_mode & 0111):检查文件是否有任何执行权限位设置(owner/group/other)
    • 0111 是八进制,对应二进制的001 001 001,表示执行权限
  • 如果权限检查通过但文件没有执行权限位,设置错误为 -EACCES
  • 将错误码转换为文件指针

成功打开文件

if (!err) {file = dentry_open(nd.dentry, nd.mnt, O_RDONLY);if (!IS_ERR(file)) {err = deny_write_access(file);if (err) {fput(file);file = ERR_PTR(err);}}
  • file = dentry_open(nd.dentry, nd.mnt, O_RDONLY):以只读方式打开文件
  • !IS_ERR(file):检查文件是否成功打开
  • deny_write_access(file):拒绝写访问,防止在执行过程中文件被修改
    • 这是重要的安全措施,确保执行的代码不会被篡改
  • 如果拒绝写访问失败,释放文件并返回错误

成功返回

out:return file;
}
  • out 标签:统一的返回点
  • 返回文件指针(成功或错误)

失败情况处理

path_release(&nd);
}
goto out;
  • path_release(&nd):释放路径查找过程中占用的资源
  • goto out:跳转到返回点

函数功能总结

主要功能:安全地打开一个可执行文件以供执行,进行完整的安全性验证

  1. 路径有效性:验证文件路径是否存在且可访问
  2. 文件系统检查:确保文件系统没有设置 noexec 挂载选项
  3. 文件类型验证:确认目标是普通文件,不是目录或设备文件
  4. 权限验证
    • 进程是否有执行权限(MAY_EXEC
    • 文件是否设置了执行权限位(至少一个x位)
  5. 写保护:打开后立即拒绝写访问,防止代码被篡改

创建并初始化一个完整的文件对象dentry_open

struct file *dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags)
{struct file * f;struct inode *inode;int error;error = -ENFILE;f = get_empty_filp();if (!f)goto cleanup_dentry;f->f_flags = flags;f->f_mode = ((flags+1) & O_ACCMODE) | FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;inode = dentry->d_inode;if (f->f_mode & FMODE_WRITE) {error = get_write_access(inode);if (error)goto cleanup_file;}f->f_mapping = inode->i_mapping;f->f_dentry = dentry;f->f_vfsmnt = mnt;f->f_pos = 0;f->f_op = fops_get(inode->i_fop);file_move(f, &inode->i_sb->s_files);if (f->f_op && f->f_op->open) {error = f->f_op->open(inode,f);if (error)goto cleanup_all;}f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);/* NB: we're sure to have correct a_ops only after f_op->open */if (f->f_flags & O_DIRECT) {if (!f->f_mapping->a_ops || !f->f_mapping->a_ops->direct_IO) {fput(f);f = ERR_PTR(-EINVAL);}}return f;cleanup_all:fops_put(f->f_op);if (f->f_mode & FMODE_WRITE)put_write_access(inode);file_kill(f);f->f_dentry = NULL;f->f_vfsmnt = NULL;
cleanup_file:put_filp(f);
cleanup_dentry:dput(dentry);mntput(mnt);return ERR_PTR(error);
}

函数功能概述

dentry_open 函数是VFS层的核心函数,用于根据dentryvfsmount创建并初始化一个文件对象,建立应用程序与文件系统之间的桥梁

代码逐段解析

变量声明和获取空文件对象

struct file * f;
struct inode *inode;
int error;error = -ENFILE;
f = get_empty_filp();
if (!f)goto cleanup_dentry;
  • f:要返回的文件对象指针
  • inode:文件的inode指针
  • error:错误码,初始设为-ENFILE
  • get_empty_filp():从文件对象缓存中获取一个空的file结构
  • 如果获取失败(内存不足),跳转到cleanup_dentry清理路径

设置文件标志和模式

f->f_flags = flags;
f->f_mode = ((flags+1) & O_ACCMODE) | FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
  • f->f_flags = flags:设置用户传入的打开标志(如O_RDONLY、O_WRONLY等)
  • f->f_mode:计算文件的访问模式
    • (flags+1) & O_ACCMODE:将打开标志转换为访问模式
    • FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE:默认启用lseekpreadpwrite功能

写访问权限检查

inode = dentry->d_inode;
if (f->f_mode & FMODE_WRITE) {error = get_write_access(inode);if (error)goto cleanup_file;
}
  • inode = dentry->d_inode:从dentry获取对应的inode
  • 如果文件以写模式打开(FMODE_WRITE):
    • get_write_access(inode):检查并增加inode的写访问计数
    • 如果失败(如文件系统只读),跳转到cleanup_file清理文件对象

设置文件对象的基本属性

f->f_mapping = inode->i_mapping;
f->f_dentry = dentry;
f->f_vfsmnt = mnt;
f->f_pos = 0;
f->f_op = fops_get(inode->i_fop);
file_move(f, &inode->i_sb->s_files);
  • f->f_mapping = inode->i_mapping:设置文件的地址空间映射
  • f->f_dentry = dentry:关联dentry
  • f->f_vfsmnt = mnt:关联挂载点
  • f->f_pos = 0:初始化文件位置为0(文件开头)
  • f->f_op = fops_get(inode->i_fop):确保操作函数相关的模块已加载
  • file_move(f, &inode->i_sb->s_files):将文件对象加入到超级块的文件列表中

调用文件系统的open方法

if (f->f_op && f->f_op->open) {error = f->f_op->open(inode,f);if (error)goto cleanup_all;
}
  • 如果文件系统提供了open操作:
    • 调用具体文件系统的open()方法
    • 如果open失败,跳转到cleanup_all进行完整清理

清理不必要的标志和初始化预读

f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);
  • 清除临时使用的打开标志:
    • O_CREATO_EXCL:创建相关标志,在打开后不再需要
    • O_NOCTTY:控制终端标志
    • O_TRUNC:截断标志
  • file_ra_state_init():初始化文件的预读状态

O_DIRECT直接I/O检查

if (f->f_flags & O_DIRECT) {if (!f->f_mapping->a_ops || !f->f_mapping->a_ops->direct_IO) {fput(f);f = ERR_PTR(-EINVAL);}
}
  • 如果使用直接I/O模式(O_DIRECT):
    • 检查地址空间操作是否支持direct_IO
    • 如果不支持,释放文件并返回-EINVAL错误

成功返回

return f;
  • 返回初始化完成的文件对象指针

错误清理路径

cleanup_all:
fops_put(f->f_op);
if (f->f_mode & FMODE_WRITE)put_write_access(inode);
file_kill(f);
f->f_dentry = NULL;
f->f_vfsmnt = NULL;
  • cleanup_all:文件系统open失败时的清理
    • fops_put(f->f_op):释放文件操作表引用
    • 如果之前获取了写访问,现在释放它
    • file_kill(f):从文件列表中移除
    • 清空dentryvfsmnt指针
cleanup_file:
put_filp(f);
  • cleanup_file:文件对象分配成功但后续失败的清理
    • put_filp(f):释放文件对象
cleanup_dentry:
dput(dentry);
mntput(mnt);
return ERR_PTR(error);
  • cleanup_dentry:最外层的清理
    • dput(dentry):减少dentry引用计数
    • mntput(mnt):减少挂载点引用计数
    • 返回错误指针

函数功能总结

主要功能:根据dentryvfsmount创建并初始化一个完整的文件对象,建立文件访问的桥梁

  1. 资源分配:分配和初始化file结构体
  2. 权限验证:检查写访问权限(如果需要)
  3. 关联建立:将file与dentry、vfsmount、inode关联
  4. 操作表设置:获取具体文件系统的操作函数表
  5. 文件系统回调:调用文件系统特定的open方法
  6. 功能初始化:设置预读、清理标志等

从内核缓存中分配一个空的文件对象get_empty_filp

struct file *get_empty_filp(void)
{
static int old_max;struct file * f;/** Privileged users can go above max_files*/if (files_stat.nr_files < files_stat.max_files ||capable(CAP_SYS_ADMIN)) {f = kmem_cache_alloc(filp_cachep, GFP_KERNEL);if (f) {memset(f, 0, sizeof(*f));if (security_file_alloc(f)) {file_free(f);goto fail;}eventpoll_init_file(f);atomic_set(&f->f_count, 1);f->f_uid = current->fsuid;f->f_gid = current->fsgid;f->f_owner.lock = RW_LOCK_UNLOCKED;/* f->f_version: 0 */INIT_LIST_HEAD(&f->f_list);return f;}}/* Ran out of filps - report that */if (files_stat.max_files >= old_max) {printk(KERN_INFO "VFS: file-max limit %d reached\n",files_stat.max_files);old_max = files_stat.max_files;} else {/* Big problems... */printk(KERN_WARNING "VFS: filp allocation failed\n");}
fail:return NULL;
}

函数功能概述

get_empty_filp 函数用于从内核缓存中分配一个空的文件对象,并进行初始化。它是文件对象分配的核心函数

代码逐段解析

静态变量和局部变量声明

static int old_max;
struct file * f;
  • static int old_max:静态变量,用于记录上次报告的文件数量限制值
  • struct file * f:要返回的文件对象指针

文件数量限制检查

if (files_stat.nr_files < files_stat.max_files ||capable(CAP_SYS_ADMIN)) {
  • 检查两个条件之一是否满足:
    1. files_stat.nr_files < files_stat.max_files:当前已分配文件数小于最大限制
    2. capable(CAP_SYS_ADMIN):当前进程具有系统管理员权限
  • 这是重要的资源限制检查,防止系统文件对象耗尽

从缓存分配文件对象

f = kmem_cache_alloc(filp_cachep, GFP_KERNEL);
  • kmem_cache_alloc(filp_cachep, GFP_KERNEL):从文件对象的slab缓存中分配内存
  • filp_cachep:专门用于file结构的slab缓存指针
  • GFP_KERNEL:分配标志,表示在内核上下文中可睡眠等待内存

内存分配成功处理

if (f) {memset(f, 0, sizeof(*f));
  • 如果成功分配到内存:
  • memset(f, 0, sizeof(*f)):将整个file结构体清零初始化
  • 确保所有字段都处于已知的初始状态

安全模块初始化

if (security_file_alloc(f)) {file_free(f);goto fail;
}
  • security_file_alloc(f):调用Linux安全模块(如SELinux)的文件分配钩子函数
  • 如果安全模块初始化失败:
    • file_free(f):释放刚分配的文件对象
    • goto fail:跳转到失败处理

事件轮询初始化

eventpoll_init_file(f);
  • eventpoll_init_file(f):初始化文件的事件轮询相关字段
  • epoll机制准备文件对象,设置事件等待队列

设置基本属性

atomic_set(&f->f_count, 1);
f->f_uid = current->fsuid;
f->f_gid = current->fsgid;
f->f_owner.lock = RW_LOCK_UNLOCKED;
/* f->f_version: 0 */
INIT_LIST_HEAD(&f->f_list);
  • atomic_set(&f->f_count, 1):设置引用计数为1
  • f->f_uid = current->fsuid:设置文件所有者UID为当前进程的文件系统UID
  • f->f_gid = current->fsgid:设置文件所有者GID为当前进程的文件系统GID
  • f->f_owner.lock = RW_LOCK_UNLOCKED:初始化文件锁为未锁定状态
  • INIT_LIST_HEAD(&f->f_list):初始化文件链表头,用于将文件连接到超级块的文件列表中

成功返回

return f;
  • 返回初始化完成的文件对象指针

文件数量限制警告(if语句外部)

/* Ran out of filps - report that */
if (files_stat.max_files >= old_max) {printk(KERN_INFO "VFS: file-max limit %d reached\n",files_stat.max_files);old_max = files_stat.max_files;
} else {/* Big problems... */printk(KERN_WARNING "VFS: filp allocation failed\n");
}
  • 这个代码块在初始if条件不满足时执行(文件数超限且非特权用户)
  • files_stat.max_files >= old_max:检查当前限制值是否大于等于上次记录的值
    • 如果成立:打印信息级消息,报告达到文件数量限制,更新old_max
    • 如果不成立:打印警告级消息,表示文件分配失败,文件数量限制值动态减小且造成了文件对象分配失败

失败返回

fail:
return NULL;
  • fail标签:统一的失败返回点
  • 返回NULL表示分配失败

函数功能总结

主要功能:从内核缓存中安全地分配并初始化一个文件对象

  1. 资源限制管理

    • 检查系统文件数量限制
    • 特权进程可以突破限制
    • 避免系统资源耗尽
  2. 内存管理

    • 使用slab分配器高效分配file结构
    • 自动清零初始化确保状态一致
  3. 安全初始化

    • 集成Linux安全模块(LSM)
    • 设置正确的UID/GID身份
  4. 功能初始化

    • 初始化引用计数
    • 设置事件轮询机制
    • 初始化文件锁和链表

get_write_access/deny_write_access

int get_write_access(struct inode * inode)
{spin_lock(&inode->i_lock);if (atomic_read(&inode->i_writecount) < 0) {spin_unlock(&inode->i_lock);return -ETXTBSY;}atomic_inc(&inode->i_writecount);spin_unlock(&inode->i_lock);return 0;
}
int deny_write_access(struct file * file)
{struct inode *inode = file->f_dentry->d_inode;spin_lock(&inode->i_lock);if (atomic_read(&inode->i_writecount) > 0) {spin_unlock(&inode->i_lock);return -ETXTBSY;}atomic_dec(&inode->i_writecount);spin_unlock(&inode->i_lock);return 0;
}

函数功能概述

这两个函数共同管理文件的写访问计数,用于实现文本忙保护机制,防止在执行文件的同时修改文件

get_write_access 代码解析

spin_lock(&inode->i_lock);
if (atomic_read(&inode->i_writecount) < 0) {spin_unlock(&inode->i_lock);return -ETXTBSY;
}
  • spin_lock(&inode->i_lock):获取inode的自旋锁,保护并发访问
  • atomic_read(&inode->i_writecount) < 0:检查写计数是否小于0
  • 如果小于0,说明文件正在被执行(已被deny_write_access),立即:
    • 释放锁
    • 返回 -ETXTBSY(Text file busy)错误

增加写计数

atomic_inc(&inode->i_writecount);
spin_unlock(&inode->i_lock);
  • atomic_inc(&inode->i_writecount):原子性地增加写访问计数
  • spin_unlock(&inode->i_lock):释放inode
  • 这表示现在有一个写者准备写入该文件

成功返回

return 0;
  • 返回0表示成功获取写访问权限

deny_write_access 代码解析

获取inode和加锁

struct inode *inode = file->f_dentry->d_inode;spin_lock(&inode->i_lock);
  • inode = file->f_dentry->d_inode:从文件对象获取对应的inode
  • spin_lock(&inode->i_lock):获取inode锁,保护并发访问

检查活跃写入者

if (atomic_read(&inode->i_writecount) > 0) {spin_unlock(&inode->i_lock);return -ETXTBSY;
}
  • atomic_read(&inode->i_writecount) > 0:检查写计数是否大于0
  • 如果大于0,说明有进程正在写入该文件,立即:
    • 释放锁
    • 返回 -ETXTBSY 错误
  • 这防止在文件被执行时还有写入操作

减少写计数

atomic_dec(&inode->i_writecount);
spin_unlock(&inode->i_lock);
  • atomic_dec(&inode->i_writecount):原子性地减少写访问计数
  • spin_unlock(&inode->i_lock):释放inode

成功返回

return 0;
  • 返回0表示成功拒绝写访问

写计数语义详解

i_writecount 的含义:

  • > 0:表示有N个进程正在写入文件
  • = 0:没有写入者,也没有执行保护
  • < 0:文件正在被执行,拒绝所有写入

实际应用场景

场景1:正常文件写入

// 进程要写入文件
get_write_access(inode);  // i_writecount: 0 → 1
// 执行写入操作...
// 完成后会调用对应的put_write_access

场景2:执行文件保护

// 执行execve时
open_exec(filename);      // 打开可执行文件
deny_write_access(file);  // i_writecount: 0 → -1
// 现在文件受到保护,无法被写入
// 执行程序...

场景3:冲突检测

// 场景:尝试修改正在执行的文件
情况1: 文件正在执行 → i_writecount = -1get_write_access() → 返回ETXTBSY情况2: 文件正在被写入 → i_writecount > 0  deny_write_access() → 返回ETXTBSY

copy_strings_kernel

int copy_strings_kernel(int argc,char ** argv, struct linux_binprm *bprm)
{int r;mm_segment_t oldfs = get_fs();set_fs(KERNEL_DS);r = copy_strings(argc, (char __user * __user *)argv, bprm);set_fs(oldfs);return r;
}
int copy_strings(int argc,char __user * __user * argv, struct linux_binprm *bprm)
{struct page *kmapped_page = NULL;char *kaddr = NULL;int ret;while (argc-- > 0) {char __user *str;int len;unsigned long pos;if (get_user(str, argv+argc) ||!(len = strnlen_user(str, bprm->p))) {ret = -EFAULT;goto out;}if (bprm->p < len)  {ret = -E2BIG;goto out;}bprm->p -= len;/* XXX: add architecture specific overflow check here. */pos = bprm->p;while (len > 0) {int i, new, err;int offset, bytes_to_copy;struct page *page;offset = pos % PAGE_SIZE;i = pos/PAGE_SIZE;page = bprm->page[i];new = 0;if (!page) {page = alloc_page(GFP_HIGHUSER);bprm->page[i] = page;if (!page) {ret = -ENOMEM;goto out;}new = 1;}if (page != kmapped_page) {if (kmapped_page)kunmap(kmapped_page);kmapped_page = page;kaddr = kmap(kmapped_page);}if (new && offset)memset(kaddr, 0, offset);bytes_to_copy = PAGE_SIZE - offset;if (bytes_to_copy > len) {bytes_to_copy = len;if (new)memset(kaddr+offset+len, 0,PAGE_SIZE-offset-len);}err = copy_from_user(kaddr+offset, str, bytes_to_copy);if (err) {ret = -EFAULT;goto out;}pos += bytes_to_copy;str += bytes_to_copy;len -= bytes_to_copy;}}ret = 0;
out:if (kmapped_page)kunmap(kmapped_page);return ret;
}

函数功能概述

这两个函数负责在内核空间和用户空间之间复制命令行参数和环境变量,是 execve 系统调用中关键的数据准备环节

copy_strings_kernel 代码解析

设置内核数据段

int r;
mm_segment_t oldfs = get_fs();
set_fs(KERNEL_DS);
  • mm_segment_t oldfs = get_fs():保存当前地址空间限制,表示是在用户空间还是内核空间
  • set_fs(KERNEL_DS):临时设置addr_limit为内核空间地址限制,允许当前用户空间访问内核空间地址
  • 这是必要的,因为内核通常不能直接访问用户空间指针

执行字符串复制

r = copy_strings(argc, (char __user * __user *)argv, bprm);
  • 调用实际的字符串复制函数
  • 参数转换:(char __user * __user *)argv 确保类型正确

恢复段设置

set_fs(oldfs);
return r;
  • 恢复原来的FS设置
  • 返回复制操作的结果

copy_strings 代码解析

变量声明

struct page *kmapped_page = NULL;
char *kaddr = NULL;
int ret;
  • kmapped_page:当前映射的内核页面指针
  • kaddr:页面在内核空间的虚拟地址
  • ret:返回码

参数循环处理

while (argc-- > 0) {char __user *str;int len;unsigned long pos;
  • 遍历所有参数(从最后一个到第一个)
  • str:用户空间字符串指针
  • len:字符串长度
  • pos:在bprm中的写入位置

获取用户空间字符串

    if (get_user(str, argv+argc) ||!(len = strnlen_user(str, bprm->p))) {ret = -EFAULT;goto out;}
  • get_user(str, argv+argc):从用户空间获取字符串指针
  • strnlen_user(str, bprm->p):安全地获取字符串长度,不超过剩余空间
  • 如果任一操作失败,返回 -EFAULT

空间检查

    if (bprm->p < len)  {ret = -E2BIG;goto out;}
  • 检查剩余空间是否足够存放当前字符串
  • 如果不够,返回 -E2BIG(参数列表过长)

位置计算

    bprm->p -= len;/* XXX: add architecture specific overflow check here. */pos = bprm->p;
  • 更新bprm中的剩余空间
  • pos 记录当前字符串的起始位置

分页复制循环

    while (len > 0) {int i, new, err;int offset, bytes_to_copy;struct page *page;
  • 循环直到复制完整个字符串
  • i:页面索引
  • new:是否是新分配的页面
  • offset:在页面内的偏移量
  • bytes_to_copy:本次要复制的字节数

页面分配和映射

        offset = pos % PAGE_SIZE;i = pos/PAGE_SIZE;page = bprm->page[i];new = 0;if (!page) {page = alloc_page(GFP_HIGHUSER);bprm->page[i] = page;if (!page) {ret = -ENOMEM;goto out;}new = 1;}
  • 计算页面索引和偏移量
  • 检查页面是否已分配,如果没有则分配新页面
  • GFP_HIGHUSER:从高端内存分配,适合用户空间数据
  • 设置 new 标志表示是新分配的页面

内核映射管理

        if (page != kmapped_page) {if (kmapped_page)kunmap(kmapped_page);kmapped_page = page;kaddr = kmap(kmapped_page);}
  • 如果切换到新页面,取消旧页面映射,建立新页面映射
  • kmap():将高端内存页面映射到内核虚拟地址空间

页面初始化

        if (new && offset)memset(kaddr, 0, offset);
  • 如果是新页面且有偏移量,用0填充页面开始到偏移量的部分
  • 确保参数之间的正确对齐

计算复制长度

        bytes_to_copy = PAGE_SIZE - offset;if (bytes_to_copy > len) {bytes_to_copy = len;if (new)memset(kaddr+offset+len, 0,PAGE_SIZE-offset-len);}
  • 计算本次能复制的最大字节数
  • 如果剩余字符串长度小于页面剩余空间:
    • 调整复制长度
    • 如果是新页面,用0填充剩余部分

执行复制操作

        err = copy_from_user(kaddr+offset, str, bytes_to_copy);if (err) {ret = -EFAULT;goto out;}
  • 从用户空间复制数据到内核页面
  • copy_from_user 在启用 set_fs(KERNEL_DS) 后可以正常工作
  • 如果复制失败,返回 -EFAULT

更新位置指针

        pos += bytes_to_copy;str += bytes_to_copy;len -= bytes_to_copy;
  • 更新目标位置、源指针和剩余长度
  • 准备下一次循环

清理和返回

ret = 0;
out:
if (kmapped_page)kunmap(kmapped_page);
return ret;
  • 成功完成所有复制操作
  • 清理时取消页面映射
  • 返回操作结果

函数功能总结

主要功能:安全高效地在内核空间和用户空间之间复制命令行参数和环境变量

  1. 安全性保障

    • 严格的边界检查防止缓冲区溢出
    • 安全的用户空间访问
    • 错误处理的完整性
  2. 内存效率

    • 按需分配页面,避免内存浪费
    • 智能的页面映射管理
    • 零填充确保数据对齐

计算用户空间字符串的长度strnlen_user

long strnlen_user(const char __user *s, long n)
{unsigned long mask = -__addr_ok(s);unsigned long res, tmp;might_sleep();__asm__ __volatile__("	testl %0, %0\n""	jz 3f\n""	andl %0,%%ecx\n""0:	repne; scasb\n""	setne %%al\n""	subl %%ecx,%0\n""	addl %0,%%eax\n""1:\n"".section .fixup,\"ax\"\n""2:	xorl %%eax,%%eax\n""	jmp 1b\n""3:	movb $1,%%al\n""	jmp 1b\n"".previous\n"".section __ex_table,\"a\"\n""	.align 4\n""	.long 0b,2b\n"".previous":"=r" (n), "=D" (s), "=a" (res), "=c" (tmp):"0" (n), "1" (s), "2" (0), "3" (mask):"cc");return res & mask;
}

函数功能概述

strnlen_user 函数用于安全地计算用户空间字符串的长度,最多计算n个字符,防止访问无效的用户空间地址导致内核崩溃

代码逐段解析

变量声明和初始化

unsigned long mask = -__addr_ok(s);
unsigned long res, tmp;
  • __addr_ok(s):检查用户空间地址s是否有效
    • 检查地址是否处于当前线程可以访问的地址空间中
    • 用户空间:0 - USER_DS
    • 内核空间:0 - KERNEL_DS
  • mask = -__addr_ok(s)
    • 如果地址有效:__addr_ok(s) = 1mask = -1 = 0xFFFFFFFF(全1)
    • 如果地址无效:__addr_ok(s) = 0mask = 0(全0)
  • res:存储结果长度
  • tmp:临时变量

地址有效性检查

	"	testl %0, %0\n""	jz 3f\n"
  • testl %0, %0:测试第0个操作数,即参数n
    • 进行n & n操作,如果结果为0,设置ZF=0,否则,ZF=1
  • jz 3f:如果n==0,跳转到标签3(直接返回),即已经没有剩余空间

设置ECX寄存器

	"	andl %0,%%ecx\n"
  • andl %0,%%ecxecx = ecx & n
  • 这里ecx初始值是mask,所以:
    • 如果地址有效:ecx = mask & n = 0xFFFFFFFF & n = n
    • 如果地址无效:ecx = mask & n = 0x0 & n = 0
    • ecx用于循环计数

字符串扫描循环

	"0:	repne; scasb\n"
  • repne:重复执行下条指令直到条件满足
  • scasb:比较AL寄存器与[EDI]指向的字节,然后EDI++
    • 初始置eax为0,所以AL为0,循环比较[EDI]指向的字节是否是0
  • 组合效果:从s开始扫描,寻找NULL字节(‘\0’),最多扫描ECX次

计算长度

	"	setne %%al\n""	subl %%ecx,%0\n""	addl %0,%%eax\n"
  • setne %%al:如果未找到NULL,设置AL=1;找到NULL,设置AL=0
    • 未找到NULL说明剩余空间不够存放字符串
    • 专门地加1,让调用者可以通过res > n判断空间不足
  • subl %%ecx,%0n = n - ecx(计算扫描的字节数)
  • addl %0,%%eaxeax = eax + n(加上扫描的字节数)

正常返回标签

	"1:\n"
  • 正常执行路径的出口点

修复代码段 - 页错误处理

".section .fixup,\"ax\"\n"
"2:	xorl %%eax,%%eax\n"
"	jmp 1b\n"
  • .section .fixup,"ax":定义修复代码段
  • xorl %%eax,%%eax:清空eax(返回0长度)
  • jmp 1b:跳转回正常出口

修复代码段 - n==0处理

"3:	movb $1,%%al\n"
"	jmp 1b\n"
".previous\n"
  • movb $1,%%al:设置eax=1(当n==0时)
  • .previous:恢复之前的代码段

异常表

".section __ex_table,\"a\"\n"
"	.align 4\n"
"	.long 0b,2b\n"
".previous"
  • 定义异常表:如果标签0b(扫描循环)发生页错误,跳转到2b(修复代码)
  • 这是内核的页错误修复机制
:"=r" (n), "=D" (s), "=a" (res), "=c" (tmp)
:"0" (n), "1" (s), "2" (0), "3" (mask)
:"cc");

输出操作数

  • "=r" (n):n在寄存器中,会被修改
  • "=D" (s):s在EDI寄存器中,会被修改
  • "=a" (res):结果在EAX寄存器中
  • "=c" (tmp)tmp在ECX寄存器中,会被修改

输入操作数

  • "0" (n):n输入到第一个寄存器
  • "1" (s):s输入到EDI寄存器
  • "2" (0):0输入到EAX寄存器(AL=0,查找NULL字节)
  • "3" (mask):mask输入到ECX寄存器

返回结果

return res & mask;
  • 如果地址有效:res & 0xFFFFFFFF = res(返回实际长度)
  • 如果地址无效:res & 0 = 0(返回0)

关键算法详解

初始状态

  • EDI = 字符串地址s
  • ECX = 最大扫描长度(mask & n)
  • AL = 0(查找NULL字节)
  • EAX = 0

repne scasb 执行后

  • ECX = 剩余可扫描长度
  • EDI = 扫描结束位置
  • ZF标志:如果找到NULL则置位

长度计算

实际扫描长度 = 初始ECX - 结束ECX
如果未找到NULL:长度 = 实际扫描长度 + 1
如果找到NULL:长度 = 实际扫描长度

函数功能总结

主要功能:安全地计算用户空间字符串长度,具有防崩溃保护

  1. 地址有效性检查

    • 通过掩码机制防止无效地址访问
    • 页错误异常修复
  2. 边界保护

    • 限制最大扫描长度为n
    • 防止缓冲区溢出
  3. 错误处理

    • 无效地址返回0
    • 页错误返回0

读取可执行文件的头部信息prepare_binprm

int prepare_binprm(struct linux_binprm *bprm)
{int mode;struct inode * inode = bprm->file->f_dentry->d_inode;int retval;mode = inode->i_mode;/** Check execute perms again - if the caller has CAP_DAC_OVERRIDE,* generic_permission lets a non-executable through*/if (!(mode & 0111))	/* with at least _one_ execute bit set */return -EACCES;if (bprm->file->f_op == NULL)return -EACCES;bprm->e_uid = current->euid;bprm->e_gid = current->egid;if(!(bprm->file->f_vfsmnt->mnt_flags & MNT_NOSUID)) {/* Set-uid? */if (mode & S_ISUID) {current->personality &= ~PER_CLEAR_ON_SETID;bprm->e_uid = inode->i_uid;}/* Set-gid? *//** If setgid is set but no group execute bit then this* is a candidate for mandatory locking, not a setgid* executable.*/if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {current->personality &= ~PER_CLEAR_ON_SETID;bprm->e_gid = inode->i_gid;}}/* fill in binprm security blob */retval = security_bprm_set(bprm);if (retval)return retval;memset(bprm->buf,0,BINPRM_BUF_SIZE);return kernel_read(bprm->file,0,bprm->buf,BINPRM_BUF_SIZE);
}

函数概述

这个函数用于准备二进制程序加载的初始参数,包括权限检查、设置执行身份和读取可执行文件头

变量声明和初始检查

int mode;
struct inode * inode = bprm->file->f_dentry->d_inode;
int retval;
  • 声明局部变量:mode 用于存储文件模式,inode 指向文件对应的 inoderetval 用于存储返回值
mode = inode->i_mode;
  • inode 中获取文件的权限模式位
if (!(mode & 0111))	/* with at least _one_ execute bit set */return -EACCES;
  • 检查文件是否具有任何执行权限位(用户、组或其他)
  • 八进制 0111 对应二进制 001 001 001,检查任一执行位是否被设置
  • 如果没有执行权限,返回权限拒绝错误
if (bprm->file->f_op == NULL)return -EACCES;
  • 检查文件操作结构体是否存在
  • 如果为 NULL,说明该文件不支持文件操作,返回权限错误

身份信息设置

bprm->e_uid = current->euid;
bprm->e_gid = current->egid;
  • 初始化设置有效用户ID和组ID为当前进程的有效UID和GID
if(!(bprm->file->f_vfsmnt->mnt_flags & MNT_NOSUID)) {
  • 检查文件所在的文件系统是否设置了 MNT_NOSUID 标志
  • 如果设置了该标志,表示忽略文件的setuid/setgid

SetUID 处理

/* Set-uid? */
if (mode & S_ISUID) {current->personality &= ~PER_CLEAR_ON_SETID;bprm->e_uid = inode->i_uid;
}
  • 检查文件是否设置了setuid位(S_ISUID)
  • 如果设置了:
    • 清除当前进程的 PER_CLEAR_ON_SETID 个性标志
    • 将执行的有效用户ID设置为文件所有者的UID
  • 当文件系统不可信,如U盘,挂载时可以指定标志MNT_NOSUID,这样可以避免通过setuid提权
  • 当文件系统不指定标志MNT_NOSUID,可以通过setuid提权,在这里的效果:
    • 如果执行进程的权限是普通用户,执行文件的权限是root,那么可以通过setuid提权
    • 将二进制文件bprm的有效权限指定为执行文件的root权限,而不是一开始的执行进程的普通用户权限

SetGID 处理

/* Set-gid? */
if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {current->personality &= ~PER_CLEAR_ON_SETID;bprm->e_gid = inode->i_gid;
}
  • 检查文件是否同时设置了 setgid 位(S_ISGID)和组执行位(S_IXGRP)
  • 这个条件确保只有当文件可被组执行时才应用 setgid
  • 如果只有一个S_ISGID标志,可能只是文件锁定
  • 如果条件满足:
    • 清除个性标志
    • 将执行的有效组ID设置为文件所属组的GID

安全模块和文件读取

/* fill in binprm security blob */
retval = security_bprm_set(bprm);
if (retval)return retval;
  • 调用安全模块的钩子函数来设置二进制程序的安全信息
  • 如果安全模块返回错误,立即返回该错误
memset(bprm->buf,0,BINPRM_BUF_SIZE);
  • 将二进制参数缓冲区清零,确保没有残留数据
return kernel_read(bprm->file,0,bprm->buf,BINPRM_BUF_SIZE);
  • 从文件开头读取 BINPRM_BUF_SIZE 字节的数据到缓冲区
  • 这通常用于读取可执行文件的魔数和其他头部信息
  • 返回读取的字节数或错误码

函数功能

主要功能:

  1. 权限验证 - 检查文件是否具有执行权限
  2. 身份设置 - 根据 setuid/setgid 位设置执行时的有效用户ID和组ID
  3. 安全初始化 - 调用安全模块进行二进制程序的安全检查
  4. 文件头读取 - 读取可执行文件的头部信息用于后续的格式识别和加载

在内核空间安全地读取文件内容到内核缓冲区kernel_read

int kernel_read(struct file *file, unsigned long offset,char *addr, unsigned long count)
{mm_segment_t old_fs;loff_t pos = offset;int result;old_fs = get_fs();set_fs(get_ds());/* The cast to a user pointer is valid due to the set_fs() */result = vfs_read(file, (void __user *)addr, count, &pos);set_fs(old_fs);return result;
}
ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{struct inode *inode = file->f_dentry->d_inode;ssize_t ret;if (!(file->f_mode & FMODE_READ))return -EBADF;if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))return -EINVAL;ret = locks_verify_area(FLOCK_VERIFY_READ, inode, file, *pos, count);if (!ret) {ret = security_file_permission (file, MAY_READ);if (!ret) {if (file->f_op->read)ret = file->f_op->read(file, buf, count, pos);elseret = do_sync_read(file, buf, count, pos);if (ret > 0)dnotify_parent(file->f_dentry, DN_ACCESS);}}return ret;
}

kernel_read 函数分析

变量声明和初始化

mm_segment_t old_fs;
loff_t pos = offset;
int result;
  • mm_segment_t old_fs: 保存当前的内存段设置,用于后续恢复
  • loff_t pos = offset: 将 offset 参数转换为 loff_t 类型(64位偏移量)
  • int result: 存储读取操作的返回值

内存段设置

old_fs = get_fs();
set_fs(get_ds());
  • get_fs(): 获取当前线程的地址空间限制(用户空间 vs 内核空间)
  • get_ds(): 获取内核数据段描述符
  • set_fs(get_ds()): 临时将地址空间限制设置为内核空间

为什么需要这个操作?

  • vfs_read函数的addr参数默认是在用户空间地址范围内的
  • 通过 set_fs(KERNEL_DS),即便addr的地址在内核空间也可以通过验证
  • 这样 vfs_read 就可以直接使用 addr 参数而不产生页错误

实际读取操作

/* The cast to a user pointer is valid due to the set_fs() */
result = vfs_read(file, (void __user *)addr, count, &pos);
  • 类型转换: (void __user *)addr 将内核地址转换为用户空间指针类型
  • 调用 vfs_read: 执行实际的文件读取操作
  • 传递 &pos: 传递位置指针,vfs_read 会更新文件位置

恢复环境

set_fs(old_fs);
return result;
  • set_fs(old_fs): 恢复原来的地址空间限制设置
  • return result: 返回读取的字节数或错误码

vfs_read 函数分析

变量声明和基础检查

struct inode *inode = file->f_dentry->d_inode;
ssize_t ret;if (!(file->f_mode & FMODE_READ))return -EBADF;
  • 获取 inode: 从文件结构体获取对应的 inode
  • 权限检查: 检查文件是否以读模式打开,如果没有则返回 -EBADF(错误的文件描述符)

文件操作检查

if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))return -EINVAL;
  • 检查文件操作表: 确保文件有对应的操作函数表
  • 检查读函数: 确保至少有一种读取方法可用(同步读或异步读)
  • 如果不满足条件,返回 -EINVAL(无效参数)

锁定和权限验证

ret = locks_verify_area(FLOCK_VERIFY_READ, inode, file, *pos, count);
if (!ret) {ret = security_file_permission(file, MAY_READ);
  • locks_verify_area(FLOCK_VERIFY_READ, ...):

    • 检查请求的读取区域是否被文件锁锁定
    • FLOCK_VERIFY_READ 表示这是读操作检查
    • 如果区域被写锁锁定,读操作会被拒绝
  • security_file_permission(file, MAY_READ):

    • Linux 安全模块(LSM)钩子函数
    • 允许安全模块(如 SELinux)进行额外的权限检查
    • MAY_READ 表示需要读权限

实际读取操作

if (!ret) {if (file->f_op->read)ret = file->f_op->read(file, buf, count, pos);elseret = do_sync_read(file, buf, count, pos);
  • 优先使用 f_op->read: 如果文件系统提供了自定义的读函数
  • 回退到 do_sync_read: 使用通用的同步读取实现

文件通知

if (ret > 0)dnotify_parent(file->f_dentry, DN_ACCESS);
  • 条件触发: 只有当成功读取到数据时(ret > 0
  • dnotify_parent: 向父目录发送文件访问通知
  • DN_ACCESS: 表示文件被访问(读取)的事件

函数功能总结

kernel_read 功能

主要目的: 在内核空间安全地读取文件内容到内核缓冲区

  1. 地址空间管理: 通过 set_fs() 临时突破用户/内核空间边界
  2. 参数转换: 将内核参数适配为 vfs_read 期望的格式
  3. 环境保护: 确保操作后恢复原始环境

vfs_read 功能

主要目的: 提供虚拟文件系统层的统一读取接口

关键特性:

  1. 统一接口: 为所有文件系统提供一致的读取API
  2. 安全检查: 多层权限和锁定验证
  3. 扩展性: 支持文件系统特定的读取实现
  4. 事件通知: 集成文件系统监控机制

强制锁定验证locks_verify_area

static inline int locks_verify_area(int read_write, struct inode *inode,struct file *filp, loff_t offset,size_t count)
{if (inode->i_flock && MANDATORY_LOCK(inode))return locks_mandatory_area(read_write, inode, filp, offset, count);return 0;
}
#define MANDATORY_LOCK(inode) \(IS_MANDLOCK(inode) && ((inode)->i_mode & (S_ISGID | S_IXGRP)) == S_ISGID)
int locks_mandatory_area(int read_write, struct inode *inode,struct file *filp, loff_t offset,size_t count)
{struct file_lock fl;int error;locks_init_lock(&fl);fl.fl_owner = current->files;fl.fl_pid = current->tgid;fl.fl_file = filp;fl.fl_flags = FL_POSIX | FL_ACCESS;if (filp && !(filp->f_flags & O_NONBLOCK))fl.fl_flags |= FL_SLEEP;fl.fl_type = (read_write == FLOCK_VERIFY_WRITE) ? F_WRLCK : F_RDLCK;fl.fl_start = offset;fl.fl_end = offset + count - 1;for (;;) {error = __posix_lock_file(inode, &fl);if (error != -EAGAIN)break;if (!(fl.fl_flags & FL_SLEEP))break;error = wait_event_interruptible(fl.fl_wait, !fl.fl_next);if (!error) {/** If we've been sleeping someone might have* changed the permissions behind our back.*/if ((inode->i_mode & (S_ISGID | S_IXGRP)) == S_ISGID)continue;}locks_delete_block(&fl);break;}return error;
}

locks_verify_area 函数

条件检查

static inline int locks_verify_area(int read_write, struct inode *inode,struct file *filp, loff_t offset,size_t count)
{if (inode->i_flock && MANDATORY_LOCK(inode))return locks_mandatory_area(read_write, inode, filp, offset, count);return 0;
}
  • inode->i_flock: 检查inode是否有文件锁链表
  • MANDATORY_LOCK(inode): 宏定义检查是否为强制锁定文件
  • 条件满足: 如果文件有锁且是强制锁定文件,调用详细检查函数
  • 条件不满足: 返回0表示无冲突

MANDATORY_LOCK 宏

#define MANDATORY_LOCK(inode) \(IS_MANDLOCK(inode) && ((inode)->i_mode & (S_ISGID | S_IXGRP)) == S_ISGID)
  • IS_MANDLOCK(inode): 检查文件系统支持强制锁定(挂载时有mand选项)
  • (inode->i_mode & (S_ISGID | S_IXGRP)) == S_ISGID:
    • 检查文件设置了setgid位但没有组执行权限
    • 这是强制锁定文件的标识条件

locks_mandatory_area 函数

变量声明和锁初始化

struct file_lock fl;
int error;locks_init_lock(&fl);
  • struct file_lock fl: 创建临时文件锁结构体
  • locks_init_lock(&fl): 初始化锁结构,设置默认值

锁参数设置

fl.fl_owner = current->files;
fl.fl_pid = current->tgid;
fl.fl_file = filp;
fl.fl_flags = FL_POSIX | FL_ACCESS;
if (filp && !(filp->f_flags & O_NONBLOCK))fl.fl_flags |= FL_SLEEP;
  • fl_owner: 锁的所有者设置为当前进程的文件结构
  • fl_pid: 进程ID
  • fl_file: 关联的文件对象
  • fl_flags:
    • FL_POSIX: POSIX风格的锁
    • FL_ACCESS: 这是访问检查,不是实际加锁
    • FL_SLEEP: 如果文件以阻塞模式打开,允许睡眠等待

锁类型和范围

fl.fl_type = (read_write == FLOCK_VERIFY_WRITE) ? F_WRLCK : F_RDLCK;
fl.fl_start = offset;
fl.fl_end = offset + count - 1;
  • fl_type: 根据操作类型设置读锁或写锁
  • fl_startfl_end: 定义要检查的字节范围

锁检查循环

for (;;) {error = __posix_lock_file(inode, &fl);if (error != -EAGAIN)break;if (!(fl.fl_flags & FL_SLEEP))break;
  • __posix_lock_file: 核心函数,检查锁冲突
  • -EAGAIN: 表示锁冲突,需要重试或等待
  • FL_SLEEP检查: 如果不能睡眠,立即退出

等待锁释放

    error = wait_event_interruptible(fl.fl_wait, !fl.fl_next);if (!error) {if ((inode->i_mode & (S_ISGID | S_IXGRP)) == S_ISGID)continue;}
  • wait_event_interruptible: 可中断的等待,在锁的等待队列上睡眠
  • 唤醒后检查: 如果等待成功(无信号中断),重新检查文件模式
  • 模式变化: 如果文件不再是强制锁定文件,不继续重试

清理和返回

    locks_delete_block(&fl);break;
}return error;
  • locks_delete_block(&fl): 从阻塞列表中删除临时锁
  • 返回错误码: 0表示成功,负值表示错误

函数功能总结

locks_verify_area 功能

主要目的: 快速检查是否需要强制锁定验证

  1. 文件有锁: inode->i_flock != NULL
  2. 强制锁定文件: 文件系统支持且文件设置了正确的模式位
  3. 执行详细检查: 满足条件时调用底层验证

locks_mandatory_area 功能

主要目的: 执行具体的强制锁定冲突检查

核心机制:

  1. 模拟加锁: 创建临时锁结构模拟请求的操作
  2. 冲突检测: 通过__posix_lock_file检查与现有锁的冲突
  3. 阻塞处理: 处理锁冲突时的等待逻辑
  4. 状态验证: 确保在等待期间文件状态没有变化

查找并执行相应二进制格式处理程序search_binary_handler

int search_binary_handler(struct linux_binprm *bprm,struct pt_regs *regs)
{int try,retval;struct linux_binfmt *fmt;retval = security_bprm_check(bprm);if (retval)return retval;/* kernel module loader fixup *//* so we don't try to load run modprobe in kernel space. */set_fs(USER_DS);retval = -ENOENT;for (try=0; try<2; try++) {read_lock(&binfmt_lock);for (fmt = formats ; fmt ; fmt = fmt->next) {int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;if (!fn)continue;if (!try_module_get(fmt->module))continue;read_unlock(&binfmt_lock);retval = fn(bprm, regs);if (retval >= 0) {put_binfmt(fmt);allow_write_access(bprm->file);if (bprm->file)fput(bprm->file);bprm->file = NULL;current->did_exec = 1;return retval;}read_lock(&binfmt_lock);put_binfmt(fmt);if (retval != -ENOEXEC || bprm->mm == NULL)break;if (!bprm->file) {read_unlock(&binfmt_lock);return retval;}}read_unlock(&binfmt_lock);if (retval != -ENOEXEC || bprm->mm == NULL) {break;
#ifdef CONFIG_KMOD}else{
#define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e))if (printable(bprm->buf[0]) &&printable(bprm->buf[1]) &&printable(bprm->buf[2]) &&printable(bprm->buf[3]))break; /* -ENOEXEC */request_module("binfmt-%04x", *(unsigned short *)(&bprm->buf[2]));
#endif}}return retval;
}

函数功能概述

search_binary_handler 是 Linux 内核中负责查找并执行相应二进制格式处理程序的核心函数。它遍历所有注册的二进制格式(如 ELF、a.out、脚本等),尝试找到能够处理当前可执行文件的格式并执行它

代码详细分析

安全检查和初始化

retval = security_bprm_check(bprm);
if (retval)return retval;/* kernel module loader fixup */
/* so we don't try to load run modprobe in kernel space. */
set_fs(USER_DS);
  • 安全检查:调用 LSM(Linux Security Module)钩子进行权限检查
  • 设置地址空间set_fs(USER_DS) 确保在内核空间使用用户空间地址限制
    • 二进制格式处理程序可能来自可加载模块
    • 某些恶意或错误的模块可能提供内核地址的函数指针
    • 通过 set_fs(USER_DS),如果尝试调用内核地址的函数,会触发页错误或保护异常

主循环 - 尝试匹配二进制格式

retval = -ENOENT;
for (try=0; try<2; try++) {read_lock(&binfmt_lock);for (fmt = formats ; fmt ; fmt = fmt->next) {int (*fn)(struct linux_binprm *, struct pt_regs *) = fmt->load_binary;if (!fn)continue;if (!try_module_get(fmt->module))continue;
  • 双重尝试机制:最多尝试两次匹配
  • 加锁:读取二进制格式链表时需要加锁
  • 遍历格式链表formats 是所有注册二进制格式的链表头
  • 获取函数指针:每个格式的 load_binary 方法是实际加载器
  • 模块引用计数:增加模块引用计数防止卸载

执行二进制加载器

        read_unlock(&binfmt_lock);retval = fn(bprm, regs);if (retval >= 0) {put_binfmt(fmt);allow_write_access(bprm->file);if (bprm->file)fput(bprm->file);bprm->file = NULL;current->did_exec = 1;return retval;}
  • 释放锁:在执行加载器前释放锁,避免长时间持有
  • 执行加载器:调用具体格式的加载函数
  • 成功处理
    • 如果返回 >=0 表示执行成功
    • 释放二进制格式对应模块的引用
    • 允许文件写入访问,和前面在open_exec函数的deny_write_access相对称
    • 释放文件引用
    • 设置进程的 did_exec 标志表示已执行

错误处理和继续尝试

        read_lock(&binfmt_lock);put_binfmt(fmt);if (retval != -ENOEXEC || bprm->mm == NULL)break;if (!bprm->file) {read_unlock(&binfmt_lock);return retval;}}read_unlock(&binfmt_lock);
  • 重新加锁:继续遍历时需要重新获取锁
  • 释放格式引用:减少模块引用计数
  • 错误判断
    • 如果不是 -ENOEXEC(格式不匹配)错误,直接退出
    • 如果内存描述符为空也退出

内核模块动态加载

    if (retval != -ENOEXEC || bprm->mm == NULL) {break;
#ifdef CONFIG_KMOD}else{
#define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e))if (printable(bprm->buf[0]) &&printable(bprm->buf[1]) &&printable(bprm->buf[2]) &&printable(bprm->buf[3]))break; /* -ENOEXEC */request_module("binfmt-%04x", *(unsigned short *)(&bprm->buf[2]));
#endif}
}
  • CONFIG_KMOD:内核模块动态加载支持配置选项
  • 只有在编译时启用模块支持时才包含此代码

printable 宏定义:

  • \t - 制表符
  • \n - 换行符
  • 0x20-0x7e - 所有可打印 ASCII 字符(空格到波浪号)
// 检查二进制文件的前4个字节
if (文件头是文本字符) {break;  // 可能是shell脚本或其他文本文件,不再尝试加载模块
}

动态模块请求

request_module("binfmt-%04x", *(unsigned short *)(&bprm->buf[2]));
  • %04x:输出4位十六进制数,不足补零
  • *(unsigned short *)(&bprm->buf[2]):取二进制格式魔数的后两个字节
  • 当二进制格式当前没有对应加载器时,尝试通过加载ko模块来进行处理这类二进制格式,便于扩展二进制格式

场景1:已知二进制格式(如ELF)

第一次循环尝试:↓
遍历 formats 链表↓
找到 elf_format → 调用 load_elf_binary↓
执行成功 → 返回 0 → 进程启动

场景2:未知二进制格式,但是是文本文件

第一次循环:所有注册格式都无法识别 → 返回 -ENOEXEC↓
进入 KMOD 处理↓
检查前4字节:'#!/b' (shell脚本)↓
可打印字符 → break → 返回 -ENOEXEC↓
上层处理:尝试作为脚本执行

场景3:未知二进制格式,非文本文件

第一次循环:返回 -ENOEXEC↓
进入 KMOD 处理  ↓
检查前4字节:非文本字符(如 0xCA 0xFE 0xBA 0xBE)↓
request_module("binfmt-%04x", 0xFECA)↓
尝试加载 binfmt_feca.ko 模块↓
第二次循环:新模块已注册 → 成功执行

实际模块加载过程

用户空间执行未知二进制文件↓
内核: search_binary_handler()↓
第一次循环: 所有格式都无法识别 → -ENOEXEC↓
检查文件头: 非文本文件↓
request_module("binfmt-XXXX")↓
内核向 kmod 子系统发送请求↓
kmod 调用用户空间 modprobe↓
modprobe 查找 /lib/modules/.../binfmt_XXXX.ko↓
加载模块到内核↓
模块注册新的二进制格式↓
第二次循环: 找到新格式 → 成功执行

函数功能总结

  1. 二进制格式识别:遍历所有注册的二进制格式处理程序
  2. 安全验证:通过 Linux 安全模块进行执行前检查
  3. 动态加载:支持运行时加载二进制格式处理模块
  4. 资源管理:正确处理文件引用、模块引用计数等资源
http://www.dtcms.com/a/566133.html

相关文章:

  • 蓝牙钥匙 第37次 企业车队管理场景下的智能化解决方案:从权限管理到访问控制
  • 福州做企业网站中山住房和建设局网站
  • 做网站活动利于优化的网站要备案吗
  • 南京网站关键词优化丫丫影院
  • auto-tracking自动埋点插件
  • 什么叫网站维护建购物网站难吗
  • 公司做网页要多少钱佛山seo
  • 美术馆网站建设概述网站如何收录快
  • 避免出现重复的属性方法:Python高级编程技巧详解
  • 营销型网站建设的五力原则包括深圳在线官网
  • 德州口碑好的网站制作公司爱站网关键词挖掘工具熊猫
  • 响应式外贸网站价格著名的wordpress网站
  • 【每日一面】实现一个深拷贝函数
  • 图标网站导航制作怎么做网站后台管理系统设计
  • 产品月报|睿本云10月产品功能迭代
  • 国外物流公司网站模板长沙专业网站制作
  • 河北邯郸建网站流量平台
  • 【文献分享】利用 GeneTEA 对基因描述进行自然语言处理以进行过表达分析
  • 开发笔记之:python集成Qt C++编写的扩展模块
  • 新野网站建设旅行社手机网站建设方案
  • 乌兰察布市建设局网站淮安网站建设推广
  • 查看数据库表某一段时间的镜像
  • 三目运算符
  • 做兼职编辑的网站网站建设配图
  • 数组——定长滑动窗口:1343. 大小为 K 且平均值大于等于阈值的子数组数目
  • Linux如何根据一个服务端口查询是二进制还是Docker容器安装
  • Ubuntu虚拟机部署Dify+Ollama搭建智能体和工作流
  • 在百度建免费网站吗网站开发总结报告
  • 【C + +】C++11 (下) | 类新功能 + STL 变化 + 包装器全解析
  • Linux的lsblk、fdisk和gdisk