深入理解Linux进程与内存 学习笔记#3

第四章 进程加载启动原理

可执行文件格式

  • 首先简单编译一个helloworld的C程序来看一下里面的格式

    gcc -o helloworld helloworld.c

使用file可以查看这个文件的格式

ELF文件格式

ELF文件由ELF文件头、Program Header Table、Section、Section Header Table四部分组成

ELF文件 头

复制代码
  readelf --file-header helloworld

输出如下所示

各部分信息

Magic: 一串特殊的识别码,主要用于外部程序快速地对这个文件进行识别,快速地判断文件类型是不是ELF。

Class: 表示这是ELF64文件。

Type: 为EXEC表示是可执行文件,其他文件类型还有REL(可重定位的目标文件)、DYN(动态链接库)、CORE(系统调试coredump文件)。

Entny pointaddress: 程序入口地址,这里显示入口在0x1060位置。

Size of this header:ELF文件头的大小,这里显示占用了64字节。

以上几个字段是ELF头中对ELF的整体描述。另外,ELF头中还有关于programheaders和section headers的描述信息:

Start of program headers: Program header的位置。

Size of program headers: 每一个Program header的大小。

Number of program headers: 总共有多少个Program header。

Start of section headers: Section header的开始位置。

Size of section headers: 每一个Section header的大小。

Number of section headers: 总共有多少个Section header。

Program Header Table

Program Header Table就是所有Segment的头信息,是用来描述所有的Segment的

复制代码
read --program-headers helloworld 

以下是部分输出

  • Section Header Table

    复制代码
    readelf --section-headers helloworld

    以下是输出结果

  • 进入入口查看

    nm -n helloworld

shell启动用户进程

shell进程先通过fork系统调用创建一个进程。然后在子进程中调用execve加载执行的程序文件,然后就可以跳过程序文件运行入口处运行这个程序了。这里的fork系统调用只能根据shell进程复制一个新的进程。这个新进程里的代码,数据都还和原来的shell进程一模一样。要想实现加载并运行另外一个程序,比如刚才的helloworld程序,那还需要用到execve系统调用。

Linux可执行文件加载器

Linux中支持的可执行文件格式有如下几种:

ELF:Executabie and Linkable Format,是Linux上最常用的可执行文件格式。

aout:主要为了和以前兼容,由于不支持动态链接,所以被ELF取代。

EM86:主要作用是在Apha的主机上运行iIniel的Linux二进制文件。

以下是linux加载器的头文件定义

复制代码
struct linux_binfmt {
	struct list_head lh;
	struct module *module;
	int (*load_binary)(struct linux_binprm *);
	int (*load_shlib)(struct file *);
#ifdef CONFIG_COREDUMP
	int (*core_dump)(struct coredump_params *cprm);
	unsigned long min_coredump;	/* minimal dump size */
#endif
} __randomize_layout;

elf加载器注册定义如下

复制代码
// ELF可执行文件格式处理器
static struct linux_binfmt elf_format = {
    .module     = THIS_MODULE,       // 所属内核模块
    .load_binary    = load_elf_binary,  // 加载可执行文件
    .load_shlib = load_elf_library,     // 加载动态库
#ifdef CONFIG_COREDUMP
    .core_dump  = elf_core_dump,    // 生成core dump
    .min_coredump   = ELF_EXEC_PAGESIZE, // core dump最小对齐大小
#endif
};

初始化如下,会通过register_binfmt进行注册

复制代码
static int __init init_elf_binfmt(void)
{
	register_binfmt(&elf_format);
	return 0;
}

而register_binfmt会通过__register_binfmt会将加载器挂载到formats全局链表中

复制代码
void __register_binfmt(struct linux_binfmt * fmt, int insert)
{
	write_lock(&binfmt_lock);
	insert ? list_add(&fmt->lh, &formats) :    //挂载到列表中
		 list_add_tail(&fmt->lh, &formats);
	write_unlock(&binfmt_lock);
}

之后在Linux加载二进制文件时会遍历formats链表。根据要加载的文件格式来查询合适的加载器

execve加载用户程序

shell程序使用fork系统调用创建新进程后,下一步加载可执行文件的工作是由execve系统调用来完成的,以下是系统调用和的do_execve定义

复制代码
SYSCALL_DEFINE3(execve,
		const char __user *, filename,
		const char __user *const __user *, argv,
		const char __user *const __user *, envp)
{
	return do_execve(getname(filename), argv, envp);
}


static int do_execve(struct filename *filename,
	const char __user *const __user *__argv,
	const char __user *const __user *__envp)
{
	struct user_arg_ptr argv = { .ptr.native = __argv };
	struct user_arg_ptr envp = { .ptr.native = __envp };
	return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
}

之后进入的这个do_execveat_common就是真正的处理函数,源码和解析如下

复制代码
/*
 * 执行新程序的核心函数
 * 参数:
 *   fd - 可执行文件的文件描述符(AT_EMPTY_PATH时使用)
 *   filename - 可执行文件名
 *   argv - 参数列表
 *   envp - 环境变量
 *   flags - 执行标志
 * 返回值: 
 *   成功时不会返回(跳转到新程序),失败返回错误码
 */
static int do_execveat_common(int fd, struct filename *filename,
              struct user_arg_ptr argv,
              struct user_arg_ptr envp,
              int flags)
{
    struct linux_binprm *bprm;  // 二进制程序信息结构体
    int retval;

    if (IS_ERR(filename))
        return PTR_ERR(filename);

    /* 检查进程数限制 */
    if ((current->flags & PF_NPROC_EXCEEDED) &&
        is_rlimit_overlimit(current_ucounts(), UCOUNT_RLIMIT_NPROC, rlimit(RLIMIT_NPROC))) {
        retval = -EAGAIN;
        goto out_ret;
    }
    current->flags &= ~PF_NPROC_EXCEEDED;  // 清除超额标志

    bprm = alloc_bprm(fd, filename);  // 分配bprm结构体
    if (IS_ERR(bprm)) {
        retval = PTR_ERR(bprm);
        goto out_ret;
    }

    /* 处理参数和环境变量 */
    retval = count(argv, MAX_ARG_STRINGS);  // 计算参数个数
    bprm->argc = retval;
    
    retval = count(envp, MAX_ARG_STRINGS);  // 计算环境变量个数
    bprm->envc = retval;

    /* 设置栈限制并拷贝各种字符串 */
    retval = bprm_stack_limits(bprm);  // 设置栈限制
    retval = copy_string_kernel(bprm->filename, bprm);  // 拷贝文件名
    retval = copy_strings(bprm->envc, envp, bprm);  // 拷贝环境变量
    retval = copy_strings(bprm->argc, argv, bprm);  // 拷贝参数

    /* 处理空参数的特殊情况 */
    if (bprm->argc == 0) {
        retval = copy_string_kernel("", bprm);  // 添加空字符串
        bprm->argc = 1;
    }

    /* 真正执行程序 */
    retval = bprm_execve(bprm, fd, filename, flags);
    
out_free:
    free_bprm(bprm);  // 释放bprm结构体
out_ret:
    putname(filename);  // 释放文件名
    return retval;
}

以下是图示

  1. 参数检查:检查文件名的有效性,检查进程数限制
  2. 准备执行环境 :分配并初始化linux_binprm结构体 alloc_bprm分配bprm结构体
  3. 处理参数:计算参数和环境变量数量,并将它们拷贝到内核空间
  4. 执行程序 :最终调用bprm_execve执行新程序

alloc_bprm

复制代码
/*
 * 分配并初始化 linux_binprm 结构体
 * 参数:
 *   fd - 可执行文件的文件描述符
 *   filename - 可执行文件名
 * 返回值:
 *   成功返回初始化好的bprm指针,失败返回错误码指针
 */
static struct linux_binprm *alloc_bprm(int fd, struct filename *filename)
{
    // 分配内存并初始化为0
    struct linux_binprm *bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
    int retval = -ENOMEM;
    if (!bprm)
        goto out;  // 内存分配失败

    /* 设置可执行文件名路径 */
    if (fd == AT_FDCWD || filename->name[0] == '/') {
        // 如果是绝对路径或当前目录
        bprm->filename = filename->name;
    } else {
        // 处理相对路径或空文件名情况
        if (filename->name[0] == '\0')
            bprm->fdpath = kasprintf(GFP_KERNEL, "/dev/fd/%d", fd);
        else
            bprm->fdpath = kasprintf(GFP_KERNEL, "/dev/fd/%d/%s", fd, filename->name);
        
        if (!bprm->fdpath)
            goto out_free;  // 路径格式化失败
        
        bprm->filename = bprm->fdpath;
    }
    bprm->interp = bprm->filename;  // 设置解释器路径

    /* 初始化内存管理结构 */
    retval = bprm_mm_init(bprm);
    if (retval)
        goto out_free;  // 初始化失败

    return bprm;  // 返回成功初始化的bprm

out_free:
    free_bprm(bprm);  // 释放已分配的资源
out:
    return ERR_PTR(retval);  // 返回错误码
}

这个函数是execve执行流程中的关键准备步骤,负责为程序执行创建并初始化必要的控制结构

  1. 内存分配 :为linux_binprm结构体分配内核内存
  2. 路径处理
    • 处理绝对路径(/开头)或当前目录(AT_FDCWD)情况
    • 处理相对路径时转换为/dev/fd/格式
  3. 初始化工作
    • 设置解释器路径(初始设为可执行文件路径)
    • 初始化内存管理结构(bprm_mm_init)
  4. 错误处理
    • 内存分配失败时返回ENOMEM
    • 路径格式化失败时释放已分配资源
    • 内存管理初始化失败时回滚操作
bprm_mm_init
复制代码
/*
 * 初始化二进制程序的内存管理结构
 * 参数:
 *   bprm - 二进制程序信息结构体(包含执行参数、文件信息等)
 * 返回值:
 *   成功返回0,失败返回错误码(如-ENOMEM)
 */
static int bprm_mm_init(struct linux_binprm *bprm)
{
    int err;
    struct mm_struct *mm = NULL;  // 新进程的内存描述符指针

    /* 1. 分配新的mm_struct结构体 */
    bprm->mm = mm = mm_alloc();   // 调用内存分配器获取新内存描述符
    err = -ENOMEM;
    if (!mm)
        goto err;  // 内存不足时跳转错误处理

    /* 2. 保存当前进程的栈大小限制(rlimit) */
    task_lock(current->group_leader);  // 加锁避免竞态条件
    bprm->rlim_stack = current->signal->rlim[RLIMIT_STACK];  // 记录栈限制
    task_unlock(current->group_leader);

    /* 3. 调用底层内存初始化函数 */
    err = __bprm_mm_init(bprm);  // 初始化内存布局、栈等
    if (err)
        goto err;  // 初始化失败时跳转错误处理

    return 0;  // 成功返回

err:
    /* 错误处理:释放已分配的mm_struct */
    if (mm) {
        bprm->mm = NULL;  // 清除bprm中的引用
        mmdrop(mm);       // 释放内存描述符
    }
    return err;  // 返回错误码
}
__bprm_mm_init

__bprm_mm_init 为新程序的内存布局奠定基础,其核心是为用户态栈建立初始的虚拟内存区域(VMA),确保后续加载程序、参数传递和栈扩展能安全进行。这种分阶段初始化的设计体现了Linux内核在内存管理上的谨慎和灵活性。

复制代码
/*
 * 初始化二进制程序执行的栈内存区域
 * 参数:
 *   bprm - 二进制程序信息结构体(包含内存描述符等)
 * 返回值:
 *   成功返回0,失败返回错误码
 */
static int __bprm_mm_init(struct linux_binprm *bprm)
{
    int err;
    struct vm_area_struct *vma = NULL;  // 虚拟内存区域指针
    struct mm_struct *mm = bprm->mm;    // 获取新进程的内存描述符

    /* 1. 分配并初始化VMA结构体 */
    bprm->vma = vma = vm_area_alloc(mm);  // 从slab分配器分配VMA
    if (!vma)
        return -ENOMEM;  // 内存分配失败
    
    vma_set_anonymous(vma);  // 设置为匿名映射(无文件背景)

    /* 2. 获取内存描述符的写锁 */
    if (mmap_write_lock_killable(mm)) {
        err = -EINTR;  // 如果被信号中断则返回
        goto err_free;
    }

    /* 3. 设置栈VMA的初始属性 */
    BUILD_BUG_ON(VM_STACK_FLAGS & VM_STACK_INCOMPLETE_SETUP);  // 编译时检查标志位冲突
    
    // 初始栈范围设为STACK_TOP_MAX向下1页(临时占位)
    vma->vm_end = STACK_TOP_MAX;          // 栈顶初始为架构最大地址
    vma->vm_start = vma->vm_end - PAGE_SIZE;  // 栈大小初始为1页
    vma->vm_flags = VM_SOFTDIRTY | VM_STACK_FLAGS | VM_STACK_INCOMPLETE_SETUP;  // 栈标志
    vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);  // 设置页保护属性

    /* 4. 将VMA插入进程地址空间 */
    err = insert_vm_struct(mm, vma);
    if (err)
        goto err;  // 插入失败跳转错误处理

    /* 5. 更新内存统计信息 */
    mm->stack_vm = mm->total_vm = 1;  // 初始内存计数设为1页
    mmap_write_unlock(mm);  // 释放内存写锁

    /* 6. 设置初始栈指针位置 */
    bprm->p = vma->vm_end - sizeof(void *);  // 预留指针大小的返回地址空间
    return 0;  // 初始化成功

/* 错误处理路径 */
err:
    mmap_write_unlock(mm);  // 释放锁
err_free:
    bprm->vma = NULL;  // 清除bprm中的VMA引用
    vm_area_free(vma);  // 释放VMA内存
    return err; 

bprm_execve

复制代码
/*
 * 执行二进制程序的核心函数
 * 参数:
 *   bprm - 包含程序加载信息的结构体
 *   fd - 可执行文件描述符
 *   filename - 可执行文件名
 *   flags - 执行标志
 * 返回值:
 *   成功时不返回(跳转到新程序),失败返回错误码
 */
static int bprm_execve(struct linux_binprm *bprm,
               int fd, struct filename *filename, int flags)
{
    struct file *file;
    int retval;

    retval = prepare_bprm_creds(bprm);  // 准备执行凭证
    if (retval)
        return retval;

    check_unsafe_exec(bprm);  // 检查不安全执行状态
    current->in_execve = 1;   // 标记进程正在执行execve

    file = do_open_execat(fd, filename, flags);  // 打开可执行文件
    if (IS_ERR(file)) {
        retval = PTR_ERR(file);
        goto out_unmark;
    }

    sched_exec();  // 调度相关准备

    bprm->file = file;  // 设置bprm中的文件指针
    
    // 处理O_CLOEXEC文件描述符情况
    if (bprm->fdpath && get_close_on_exec(fd))
        bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;

    retval = security_bprm_creds_for_exec(bprm);  // 安全模块检查
    if (retval)
        goto out;

    retval = exec_binprm(bprm);  // 实际执行二进制程序
    if (retval < 0)
        goto out;

    // 执行成功后的清理工作
    current->fs->in_exec = 0;
    current->in_execve = 0;
    rseq_execve(current);      // 处理restartable sequences
    acct_update_integrals(current);  // 更新进程统计信息
    task_numa_free(current, false);  // 释放NUMA相关资源
    return retval;

out:
    // 错误处理: 如果已经过了不可返回点,强制终止进程
    if (bprm->point_of_no_return && !fatal_signal_pending(current))
        force_fatal_sig(SIGSEGV);

out_unmark:
    current->fs->in_exec = 0;
    current->in_execve = 0;

    return retval;
}

这个函数是execve系统调用链中的关键一环,负责将新程序加载到当前进程的地址空间并开始执行。

  1. 准备工作
    • 准备执行凭证(credentials)
    • 检查执行环境安全性
    • 打开可执行文件
  2. 实际执行
    • 通过exec_binprm()加载并执行程序
    • 处理安全模块检查
  3. 执行后处理
    • 清理执行状态标记
    • 更新进程统计信息
    • 释放相关资源
  4. 错误处理
    • 特殊处理"不可返回点"的情况
    • 确保失败时不会意外返回用户空间

ELF(Executable Linkable Format)文件加载过程

对于ELF文件加载器elf_format来所,load_binary函数指针指向的是load_elf_binary,之后会进入这个函数进行加载工作。

读取ELF文件头

复制代码
/* 检查ELF魔数(e_ident)和类型 */
if (memcmp(elf_ex->e_ident, ELFMAG, SELFMAG) != 0)
    goto out;
if (elf_ex->e_type != ET_EXEC && elf_ex->e_type != ET_DYN)
    goto out;
if (!elf_check_arch(elf_ex))
    goto out;

这里主要是验证ELF文件的魔数(0x7F 'E' 'L' 'F')、类型(可执行/动态库)和架构兼容性。

复制代码
interp_elf_ex = kmalloc(sizeof(*interp_elf_ex), GFP_KERNEL);
if (!interp_elf_ex) {
    retval = -ENOMEM;
    goto out_free_file;
}

先将ELF文件头复制保存起来。文件头中包含当前文件格式类型的数据,在读取完文件头后会进行一些合法性判断,如果不合法,则退出返回

读取Program Header

复制代码
/* 加载Program Headers */
elf_phdata = load_elf_phdrs(elf_ex, bprm->file);
if (!elf_phdata)
    goto out;

/* 遍历Headers处理解释器和特殊段 */
for(i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
    if (elf_ppnt->p_type == PT_INTERP) {
        /* 加载解释器路径(如/lib64/ld-linux-x86-64.so.2) */
        elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
        elf_read(bprm->file, elf_interpreter, elf_ppnt->p_filesz, elf_ppnt->p_offset);
        interpreter = open_exec(elf_interpreter);
    }
    else if (elf_ppnt->p_type == PT_GNU_STACK) {
        /* 处理栈权限(如NX位) */
        executable_stack = (elf_ppnt->p_flags & PF_X) ? EXSTACK_ENABLE_X : EXSTACK_DISABLE_X;
    }
}

读取Program Headers,定位解释器和关键段(如栈权限段)。 其中Program Headers的读取是在load_elf_phdrs中完成的

load_elf_phdrs
复制代码
/**
 * load_elf_phdrs - 加载ELF文件的程序头表(Program Header Table)
 * @elf_ex: ELF文件头结构体指针
 * @elf_file: 要读取的ELF文件
 * 
 * 返回值: 成功返回程序头表指针,失败返回NULL
 */
static struct elf_phdr *load_elf_phdrs(const struct elfhdr *elf_ex,
				       struct file *elf_file)
{
	struct elf_phdr *elf_phdata = NULL;  // 程序头表指针
	int retval = -1;                     // 返回值,默认失败
	unsigned int size;                   // 程序头表总大小

	/*
	 * 检查程序头表项大小是否与当前系统定义一致,
	 * 如果不一致说明ELF文件格式不兼容
	 */
	if (elf_ex->e_phentsize != sizeof(struct elf_phdr))
		goto out;

	/* 对程序头表数量进行合理性检查 */
	/* 并计算程序头表总大小 */
	size = sizeof(struct elf_phdr) * elf_ex->e_phnum;
	/* 检查大小是否有效(非零、不超过65536字节、不超过最小对齐要求) */
	if (size == 0 || size > 65536 || size > ELF_MIN_ALIGN)
		goto out;

	/* 分配内存来存储程序头表 */
	elf_phdata = kmalloc(size, GFP_KERNEL);
	if (!elf_phdata)
		goto out;

	/* 从ELF文件中读取程序头表数据 */
	retval = elf_read(elf_file, elf_phdata, size, elf_ex->e_phoff);

out:
	/* 错误处理:如果失败则释放内存并返回NULL */
	if (retval) {
		kfree(elf_phdata);
		elf_phdata = NULL;
	}
	return elf_phdata;
}

该函数用于加载ELF文件中的程序头表(Program Header Table),程序头表描述了ELF文件中的各个段(segment)信息。

清空父进程继承来的资源

复制代码
/* 清除父进程的地址空间等资源 */
retval = begin_new_exec(bprm);
if (retval)
    goto out_free_dentry;

/* 设置新进程的个性化配置(如地址随机化) */
SET_PERSONALITY2(*elf_ex, &arch_state);
setup_new_exec(bprm);
begin_new_exec
复制代码
/**
 * begin_new_exec - 准备执行新程序的主要处理函数
 * @bprm: 二进制程序参数结构体指针
 * 
 * 返回值: 成功返回0,失败返回错误码
 * 
 * 该函数是execve系统调用的核心部分,负责准备新程序的执行环境
 */
int begin_new_exec(struct linux_binprm *bprm)
{
    struct task_struct *me = current;  // 当前任务结构体
    int retval;

    /* 从文件获取新的凭证(credentials) */
    retval = bprm_creds_from_file(bprm);
    if (retval)
        return retval;

    /* 标记为不可返回点,后续错误将直接导致进程终止 */
    bprm->point_of_no_return = true;

    /* 确保当前是线程组中的唯一线程 */
    retval = de_thread(me);
    if (retval)
        goto out;

    /* 取消所有io_uring活动 */
    io_uring_task_cancel();

    /* 确保文件表不被共享 */
    retval = unshare_files();
    if (retval)
        goto out;

    /* 
     * 设置新的可执行文件到内存管理结构(mm)中
     * 必须在exec_mmap()之前调用
     */
    retval = set_mm_exe_file(bprm->mm, bprm->file);
    if (retval)
        goto out;

    /* 检查文件可读性并设置dumpable标志 */
    would_dump(bprm, bprm->file);
    if (bprm->have_execfd)
        would_dump(bprm, bprm->executable);

    /* 释放旧的地址空间映射 */
    acct_arg_size(bprm, 0);
    retval = exec_mmap(bprm->mm);
    if (retval)
        goto out;

    bprm->mm = NULL;

    /* 处理命名空间 */
    retval = exec_task_namespaces();
    if (retval)
        goto out_unlock;

#ifdef CONFIG_POSIX_TIMERS
    /* 处理POSIX定时器 */
    spin_lock_irq(&me->sighand->siglock);
    posix_cpu_timers_exit(me);
    spin_unlock_irq(&me->sighand->siglock);
    exit_itimers(me);
    flush_itimer_signals();
#endif

    /* 使信号处理表私有化 */
    retval = unshare_sighand(me);
    if (retval)
        goto out_unlock;

    /* 清除进程标志 */
    me->flags &= ~(PF_RANDOMIZE | PF_FORKNOEXEC | PF_NOFREEZE | PF_NO_SETAFFINITY);
    flush_thread();
    me->personality &= ~bprm->per_clear;

    /* 清除系统调用工作 */
    clear_syscall_work_syscall_user_dispatch(me);

    /* 关闭执行时关闭的文件描述符 */
    do_close_on_exec(me->files);

    /* 安全执行处理 */
    if (bprm->secureexec) {
        me->pdeath_signal = 0;  // 禁止父进程发送信号

        /* 重置栈限制 */
        if (bprm->rlim_stack.rlim_cur > _STK_LIM)
            bprm->rlim_stack.rlim_cur = _STK_LIM;
    }

    me->sas_ss_sp = me->sas_ss_size = 0;  // 清除信号栈设置

    /* 设置dumpable标志 */
    if (bprm->interp_flags & BINPRM_FLAGS_ENFORCE_NONDUMP ||
        !(uid_eq(current_euid(), current_uid()) &&
          gid_eq(current_egid(), current_gid())))
        set_dumpable(current->mm, suid_dumpable);
    else
        set_dumpable(current->mm, SUID_DUMP_USER);

    /* 性能事件和任务名处理 */
    perf_event_exec();
    __set_task_comm(me, kbasename(bprm->filename), true);

    /* 更新exec ID并刷新信号处理程序 */
    WRITE_ONCE(me->self_exec_id, me->self_exec_id + 1);
    flush_signal_handlers(me, 0);

    /* 设置新的用户计数 */
    retval = set_cred_ucounts(bprm->cred);
    if (retval < 0)
        goto out_unlock;

    /* 安全模块回调 */
    security_bprm_committing_creds(bprm);

    /* 提交新的凭证 */
    commit_creds(bprm->cred);
    bprm->cred = NULL;

    /* 对于setuid二进制文件,禁用普通用户的监控 */
    if (get_dumpable(me->mm) != SUID_DUMP_USER)
        perf_event_exit_task(me);

    /* 安全模块回调 */
    security_bprm_committed_creds(bprm);

    /* 处理execfd情况 */
    if (bprm->have_execfd) {
        retval = get_unused_fd_flags(0);
        if (retval < 0)
            goto out_unlock;
        fd_install(retval, bprm->executable);
        bprm->executable = NULL;
        bprm->execfd = retval;
    }
    return 0;

out_unlock:
    up_write(&me->signal->exec_update_lock);
out:
    return retval;
}

该函数是Linux内核中执行新程序的核心函数,处理execve系统调用的主要工作,负责准备新程序的执行环境,包括凭证、内存映射、信号处理等

exec_mmap
复制代码
/**
 * exec_mmap - 切换进程到新的内存地址空间
 * @mm: 新的内存描述符指针
 *
 * 返回值: 成功返回0,失败返回错误码
 * 
 * 该函数是execve操作的核心部分,负责将进程的内存环境切换到新的地址空间,
 * 并妥善处理旧地址空间的资源释放。
 */
static int exec_mmap(struct mm_struct *mm)
{
    struct task_struct *tsk;       // 当前任务指针
    struct mm_struct *old_mm;      // 旧的内存描述符
    struct mm_struct *active_mm;   // 当前活动的内存描述符
    int ret;                       // 返回值

    /* 1. 准备工作:获取当前任务和内存信息 */
    tsk = current;                 // 获取当前任务结构
    old_mm = current->mm;          // 保存当前内存描述符
    
    /* 通知父进程不再追踪旧的VM状态 */
    exec_mm_release(tsk, old_mm);
    
    /* 同步旧内存的RSS统计信息 */
    if (old_mm)
        sync_mm_rss(old_mm);

    /* 2. 获取exec更新锁,保护整个exec操作 */
    ret = down_write_killable(&tsk->signal->exec_update_lock);
    if (ret)
        return ret;

    /* 3. 处理旧内存映射(如果存在) */
    if (old_mm) {
        /*
         * 获取旧mm的读锁,期间检查是否有致命信号,
         * 避免在应该退出的情况下继续执行
         */
        ret = mmap_read_lock_killable(old_mm);
        if (ret) {
            up_write(&tsk->signal->exec_update_lock);
            return ret;
        }
    }

    /* 4. 开始关键的内存映射切换操作 */
    task_lock(tsk);  // 锁定任务结构,防止并发修改
    
    /* 处理内存屏障,确保多核间内存操作顺序 */
    membarrier_exec_mmap(mm);

    /* 禁用中断保证原子性 */
    local_irq_disable();
    
    /* 保存当前活动内存描述符 */
    active_mm = tsk->active_mm;
    
    /* 切换到新的内存描述符 */
    tsk->active_mm = mm;
    tsk->mm = mm;

    /*
     * 根据架构决定中断启用时机:
     * 某些架构需要在activate_mm期间保持中断禁用
     */
    if (!IS_ENABLED(CONFIG_ARCH_WANT_IRQS_OFF_ACTIVATE_MM))
        local_irq_enable();

    /* 激活新的内存映射 */
    activate_mm(active_mm, mm);

    /* 确保中断最终被重新启用 */
    if (IS_ENABLED(CONFIG_ARCH_WANT_IRQS_OFF_ACTIVATE_MM))
        local_irq_enable();

    /* 5. 更新内存管理相关数据结构 */
    lru_gen_add_mm(mm);    // 将新mm加入LRU生成管理系统
    task_unlock(tsk);      // 解锁任务结构
    lru_gen_use_mm(mm);    // 标记开始使用新mm

    /* 6. 清理旧内存资源 */
    if (old_mm) {
        mmap_read_unlock(old_mm);  // 释放旧mm的读锁
        
        /* 检查active_mm一致性 */
        BUG_ON(active_mm != old_mm);
        
        /* 更新最大RSS水线统计 */
        setmax_mm_hiwater_rss(&tsk->signal->maxrss, old_mm);
        
        /* 更新内存描述符所有者信息 */
        mm_update_next_owner(old_mm);
        
        /* 释放对旧mm的引用 */
        mmput(old_mm);
        return 0;
    }

    /* 如果没有旧mm,释放active_mm */
    mmdrop(active_mm);
    return 0;
}

在清空父进程继承来的虚拟地址后将前面在临时变量bprm中保存的新的地址空间拿来用上,这样新进程的虚拟内存就准备好了

接下来再调用setup_arg_pages,为新进程也设置上新的栈备用。

复制代码
/**
 * setup_arg_pages - 设置进程的用户态栈空间
 * @bprm: 二进制程序参数结构体指针
 * @stack_top: 用户栈顶地址
 * @executable_stack: 栈是否可执行标志
 *
 * 返回值: 成功返回0,失败返回错误码
 *
 * 该函数负责在execve()执行过程中设置新的用户态栈空间,
 * 包括栈大小计算、权限设置和空间扩展等操作。
 */
int setup_arg_pages(struct linux_binprm *bprm,
                   unsigned long stack_top,
                   int executable_stack)
{
    unsigned long ret;
    unsigned long stack_shift;      // 栈偏移量
    struct mm_struct *mm = current->mm;  // 当前进程的内存管理结构
    struct vm_area_struct *vma = bprm->vma;  // 栈对应的虚拟内存区域
    struct vm_area_struct *prev = NULL;  // 前一个VMA
    unsigned long vm_flags;        // VMA标志位
    unsigned long stack_base;      // 栈基地址
    unsigned long stack_size;      // 栈大小
    unsigned long stack_expand;    // 栈扩展大小
    unsigned long rlim_stack;      // 栈资源限制
    struct mmu_gather tlb;         // TLB操作结构体

#ifdef CONFIG_STACK_GROWSUP
    /* 处理栈向上增长的情况 */
    stack_base = bprm->rlim_stack.rlim_max;  // 获取栈大小限制
    stack_base = calc_max_stack_size(stack_base);  // 计算最大栈大小

    /* 增加栈随机化所需空间 */
    stack_base += (STACK_RND_MASK << PAGE_SHIFT);

    /* 检查参数数组是否超过栈大小限制 */
    if (vma->vm_end - vma->vm_start > stack_base)
        return -ENOMEM;

    /* 计算栈基地址并页面对齐 */
    stack_base = PAGE_ALIGN(stack_top - stack_base);
    stack_shift = vma->vm_start - stack_base;

    /* 调整参数指针位置 */
    mm->arg_start = bprm->p - stack_shift;
    bprm->p = vma->vm_end - stack_shift;
#else
    /* 默认处理栈向下增长的情况 */
    stack_top = arch_align_stack(stack_top);  // 架构相关的栈对齐
    stack_top = PAGE_ALIGN(stack_top);       // 页面对齐

    /* 检查栈地址是否合法 */
    if (unlikely(stack_top < mmap_min_addr) ||
        unlikely(vma->vm_end - vma->vm_start >= stack_top - mmap_min_addr))
        return -ENOMEM;

    /* 计算栈偏移量并调整参数指针 */
    stack_shift = vma->vm_end - stack_top;
    bprm->p -= stack_shift;
    mm->arg_start = bprm->p;
#endif

    /* 调整加载器和执行文件指针 */
    if (bprm->loader)
        bprm->loader -= stack_shift;
    bprm->exec -= stack_shift;

    /* 获取内存写锁,可被信号中断 */
    if (mmap_write_lock_killable(mm))
        return -EINTR;

    /* 设置基本的栈VMA标志 */
    vm_flags = VM_STACK_FLAGS;

    /*
     * 根据参数设置栈的可执行权限:
     * EXSTACK_ENABLE_X - 允许执行
     * EXSTACK_DISABLE_X - 禁止执行
     * 其他情况 - 使用架构默认设置
     */
    if (unlikely(executable_stack == EXSTACK_ENABLE_X))
        vm_flags |= VM_EXEC;
    else if (executable_stack == EXSTACK_DISABLE_X)
        vm_flags &= ~VM_EXEC;
    
    /* 合并默认标志并设置临时标志 */
    vm_flags |= mm->def_flags;
    vm_flags |= VM_STACK_INCOMPLETE_SETUP;

    /* 修改VMA保护标志 */
    tlb_gather_mmu(&tlb, mm);  // 准备TLB刷新
    ret = mprotect_fixup(&tlb, vma, &prev, vma->vm_start, vma->vm_end,
                        vm_flags);
    tlb_finish_mmu(&tlb);     // 完成TLB操作

    if (ret)
        goto out_unlock;
    BUG_ON(prev != vma);  // 确保VMA未被合并或拆分

    /* 对可执行栈发出警告 */
    if (unlikely(vm_flags & VM_EXEC)) {
        pr_warn_once("process '%pD4' started with executable stack\n",
                    bprm->file);
    }

    /* 如果需要,移动栈页面 */
    if (stack_shift) {
        ret = shift_arg_pages(vma, stack_shift);
        if (ret)
            goto out_unlock;
    }

    /* 清除临时设置标志 */
    vma->vm_flags &= ~VM_STACK_INCOMPLETE_SETUP;

    /* 准备栈扩展参数 */
    stack_expand = 131072UL; /* 默认扩展128KB */
    stack_size = vma->vm_end - vma->vm_start;
    
    /* 根据页面对齐栈资源限制 */
    rlim_stack = bprm->rlim_stack.rlim_cur & PAGE_MASK;
    
    /* 计算实际扩展大小(不超过资源限制) */
    stack_expand = min(rlim_stack, stack_size + stack_expand);

    /* 计算扩展后的栈基地址 */
#ifdef CONFIG_STACK_GROWSUP
    stack_base = vma->vm_start + stack_expand;
#else
    stack_base = vma->vm_end - stack_expand;
#endif

    /* 设置栈起始指针并扩展栈空间 */
    current->mm->start_stack = bprm->p;
    ret = expand_stack(vma, stack_base);
    if (ret)
        ret = -EFAULT;

out_unlock:
    /* 释放内存写锁并返回 */
    mmap_write_unlock(mm);
    return ret;
}
  1. 栈方向处理
    • 同时支持向上(CONFIG_STACK_GROWSUP)和向下(默认)增长的栈
    • 根据配置采用不同的计算方式
  2. 安全性检查
    • 检查栈大小限制
    • 验证参数数组大小
    • 确保栈地址不低于mmap_min_addr
  3. 权限控制
    • 根据executable_stack参数设置栈的可执行权限
    • 对可执行栈发出警告
  4. 内存管理
    • 使用mmap_write_lock保护内存操作
    • 正确处理TLB刷新
    • 使用VM_STACK_INCOMPLETE_SETUP标志标记临时状态
  5. 栈空间调整
    • 计算合适的栈大小
    • 移动栈页面(shift_arg_pages)
    • 扩展栈空间(expand_stack)
  6. 错误处理
    • 多处检查并返回错误
    • 使用goto实现集中错误处理
    • 确保锁的释放

这个函数展示了Linux内核如何精细地管理进程栈空间,包括权限控制、空间分配和安全性考虑,是进程执行环境设置的重要组成部分。

执行Segment加载

接下来加载器会将ELF文件中的LOAD类型的Segment都加载到内存。只有LOAD类型的Segment是需要被映射到内存的

复制代码
/* 遍历所有PT_LOAD段并映射到内存 */
for(i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
    if (elf_ppnt->p_type != PT_LOAD)
        continue;

    /* 计算加载地址和权限 */
    elf_prot = make_prot(elf_ppnt->p_flags, &arch_state, !!interpreter, false);
    elf_flags = MAP_PRIVATE | (first_pt_load ? MAP_FIXED_NOREPLACE : MAP_FIXED);

    /* 映射段到内存 */
    error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt, elf_prot, elf_flags, total_size);
    if (BAD_ADDR(error))
        goto out_free_dentry;

    /* 更新代码/数据段边界 */
    if (elf_ppnt->p_flags & PF_X) {
        start_code = min(start_code, (unsigned long)elf_ppnt->p_vaddr);
        end_code = max(end_code, (unsigned long)elf_ppnt->p_vaddr + elf_ppnt->p_filesz);
    }
}

数据内存申请和堆初始化

复制代码
/* 初始化BSS和堆空间 */
retval = set_brk(elf_bss, elf_brk, bss_prot);
if (retval)
    goto out_free_dentry;

/* 清零BSS段 */
if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) {
    retval = -EFAULT;
    goto out_free_dentry;
}

跳转到程序入口执行

复制代码
/* 动态链接:加载解释器并重定位入口地址 */
if (interpreter) {
    elf_entry = load_elf_interp(interp_elf_ex, interpreter, load_bias, interp_elf_phdata, &arch_state);
    elf_entry += interp_elf_ex->e_entry;
}

/* 静态链接:直接使用ELF入口地址 */
else {
    elf_entry = e_entry;
}

/* 设置寄存器状态并跳转到入口点 */
finalize_exec(bprm);
START_THREAD(elf_ex, regs, elf_entry, bprm->p);
相关推荐
千里马-horse2 小时前
Linux 系统中安装 ktlint
linux·运维·服务器
-Springer-2 小时前
STM32 学习 —— 个人学习笔记10-1(I2C 通信协议及 MPU6050 简介 & 软件 I2C 读写 MPU6050)
笔记·stm32·学习
feng_you_ying_li2 小时前
linux攻略计划启动,首先是linux的基本介绍(1)
linux·运维·服务器
张3蜂2 小时前
Ubuntu Linux 与 Ubuntu with Rosetta:深入解析两者的区别与适用场景
linux·运维·ubuntu
廿一夏2 小时前
搭建Ubuntu 虚拟机与部署docker
linux·ubuntu·docker
历程里程碑2 小时前
43. TCP -2实现英文查中文功能
java·linux·开发语言·c++·udp·c#·排序算法
小陈phd2 小时前
多模态大模型学习笔记(二十二)——大模型微调全解:从全量调参到LoRA的参数高效训练实战
笔记·学习
千里马-horse2 小时前
ubuntu 电脑安装protoc-gen-grpc-kotlin
linux·运维·ubuntu
Engineer邓祥浩2 小时前
JVM学习笔记(3) 第二部分 自动内存管理 第2章 Java内存区域与内存溢出异常
jvm·笔记·学习