execve() 系统调用深度解析:从用户空间到内核的完整加载过程

文章目录

    • 引言
    • [一、应用层入口:execveat() 函数](#一、应用层入口:execveat() 函数)
      • [1.1 execveat() 函数原型](#1.1 execveat() 函数原型)
      • [1.2 execveat() 的优势](#1.2 execveat() 的优势)
      • [1.3 libc中的实现](#1.3 libc中的实现)
    • 二、系统调用入口:进入内核
      • [2.1 系统调用号定义](#2.1 系统调用号定义)
      • [2.2 双版本支持的原因](#2.2 双版本支持的原因)
      • [2.3 兼容层处理](#2.3 兼容层处理)
    • 三、核心处理:do_execveat_common()
      • [3.1 主要工作流程](#3.1 主要工作流程)
      • [3.2 linux_binprm结构](#3.2 linux_binprm结构)
    • 四、二进制执行:bprm_execve()
      • [4.1 执行前准备](#4.1 执行前准备)
      • [4.2 打开可执行文件](#4.2 打开可执行文件)
      • [4.3 调度优化](#4.3 调度优化)
    • 五、二进制格式识别:search_binary_handler()
      • [5.1 遍历格式处理器](#5.1 遍历格式处理器)
      • [5.2 常见的二进制格式处理器](#5.2 常见的二进制格式处理器)
      • [5.3 魔数识别机制](#5.3 魔数识别机制)
    • 六、ELF文件加载:load_elf_binary()
      • [6.1 ELF验证和解析](#6.1 ELF验证和解析)
      • [6.2 处理程序头表](#6.2 处理程序头表)
      • [6.3 内存映射关键步骤](#6.3 内存映射关键步骤)
      • [6.4 动态链接器处理](#6.4 动态链接器处理)
    • 七、最终执行:start_thread()
      • [7.1 ARM64架构的实现](#7.1 ARM64架构的实现)
      • [7.2 start_thread()的作用](#7.2 start_thread()的作用)
    • 八、完整执行流程总结
      • [8.1 动态链接程序的执行流程](#8.1 动态链接程序的执行流程)
      • [8.2 静态链接程序的执行流程](#8.2 静态链接程序的执行流程)

引言

在Linux系统中,程序的执行是一个复杂而精妙的过程。当我们运行一个可执行文件时,背后涉及到用户空间库函数调用、系统调用进入内核、二进制格式识别、内存映射、动态链接等多个环节。本文将从应用层的execveat()函数开始,深入追踪到内核的start_thread(),完整解析Linux程序加载的全过程。

一、应用层入口:execveat() 函数

1.1 execveat() 函数原型

c 复制代码
int execveat(int dirfd,           // 目录文件描述符
             const char *path,    // 路径名(相对或绝对)
             char *const argv[],  // 参数数组
             char *const envp[],  // 环境变量数组
             int flags);          // 标志位

1.2 execveat() 的优势

  • 避免TOCTOU竞态条件:原子性操作防止程序在检查和执行之间被替换
  • 灵活的路径指定:支持相对目录文件描述符的路径
  • 直接执行已打开文件 :通过AT_EMPTY_PATH标志可以直接执行文件描述符

1.3 libc中的实现

c 复制代码
// glibc中的execveat实现非常简单
execveat (int dirfd, const char *path, char *const argv[], 
          char *const envp[], int flags)
{
    return INLINE_SYSCALL_CALL (execveat, dirfd, path, &argv[0], 
                                &envp[0], flags);
}

二、系统调用入口:进入内核

2.1 系统调用号定义

c 复制代码
#define __NR_execveat 281
__SC_COMP(__NR_execveat, sys_execveat, compat_sys_execveat)

2.2 双版本支持的原因

  • sys_execveat():64位程序原生调用
  • compat_sys_execveat():32位程序兼容调用
  • 区别在于32位和64位指针大小不同(4字节 vs 8字节)

2.3 兼容层处理

c 复制代码
// fs/compat.c
COMPAT_SYSCALL_DEFINE5(execveat, int, fd,
                      const char __user *, filename,
                      const compat_uptr_t __user *, argv,
                      const compat_uptr_t __user *, envp,
                      int, flags)
{
    return compat_do_execveat(fd,
        getname_flags(filename, lookup_flags, NULL), // 复制到内核空间
        argv, envp, flags);
}

三、核心处理:do_execveat_common()

3.1 主要工作流程

c 复制代码
static int do_execveat_common(int fd, struct filename *filename,
                             struct user_arg_ptr argv,
                             struct user_arg_ptr envp,
                             int flags)
{
    // 1. 检查进程数限制(RLIMIT_NPROC)
    // 2. 分配linux_binprm结构(二进制加载上下文)
    // 3. 计算参数和环境变量个数
    // 4. 复制参数字符串到内核空间
    // 5. 调用bprm_execve()执行
}

3.2 linux_binprm结构

c 复制代码
struct linux_binprm {
    char buf[BINPRM_BUF_SIZE];      // 文件头部缓冲区(128字节)
    struct file *file;              // 打开的可执行文件
    struct file *interpreter;       // 解释器文件(如动态链接器)
    unsigned int argc, envc;        // 参数和环境变量计数
    const char *filename;           // 程序文件名
    int point_of_no_return;         // "不返回点"标志
    // ... 更多字段
};

四、二进制执行:bprm_execve()

4.1 执行前准备

c 复制代码
static int bprm_execve(struct linux_binprm *bprm,
                      int fd, struct filename *filename, int flags)
{
    // 1. 清理io_uring活动
    io_uring_task_cancel();
    
    // 2. 分离文件描述符表
    retval = unshare_files(&displaced);
    
    // 3. 准备执行凭证
    retval = prepare_bprm_creds(bprm);
    
    // 4. 安全检查
    check_unsafe_exec(bprm);
    
    // 5. 标记正在execve
    current->in_execve = 1;
}

4.2 打开可执行文件

c 复制代码
static struct file *do_open_execat(int fd, struct filename *name, int flags)
{
    // 设置打开标志:只读、可执行
    struct open_flags open_exec_flags = {
        .open_flag = O_LARGEFILE | O_RDONLY | __FMODE_EXEC,
        .acc_mode = MAY_EXEC,
    };
    
    // 实际打开文件
    file = do_filp_open(fd, name, &open_exec_flags);
    
    // 验证文件类型和权限
    if (!S_ISREG(file_inode(file)->i_mode) || path_noexec(&file->f_path))
        goto exit;
    
    // 拒绝写访问(防止执行中被修改)
    err = deny_write_access(file);
}

4.3 调度优化

c 复制代码
void sched_exec(void)
{
    // execve是负载均衡的好时机
    // 此时任务内存占用最小,缓存最冷
    
    // 选择最适合的CPU
    dest_cpu = p->sched_class->select_task_rq(p, task_cpu(p), 
                                              SD_BALANCE_EXEC, 0);
    
    // 如果需要,迁移到新CPU
    if (dest_cpu != smp_processor_id() && cpu_active(dest_cpu)) {
        stop_one_cpu(task_cpu(p), migration_cpu_stop, &arg);
    }
}

五、二进制格式识别:search_binary_handler()

5.1 遍历格式处理器

c 复制代码
static int search_binary_handler(struct linux_binprm *bprm)
{
    // 读取文件头部到bprm->buf(128字节)
    retval = prepare_binprm(bprm);
    
    // 遍历所有注册的二进制格式处理器
    list_for_each_entry(fmt, &formats, lh) {
        // 尝试加载
        retval = fmt->load_binary(bprm);
        
        // 如果成功或已过"不返回点",直接返回
        if (bprm->point_of_no_return || (retval != -ENOEXEC)) {
            return retval;
        }
        // 否则继续尝试下一个格式
    }
}

5.2 常见的二进制格式处理器

格式处理器文件 处理格式 魔数检查
fs/binfmt_elf.c ELF格式 0x7f 'E' 'L' 'F'
fs/binfmt_script.c 脚本 '#!'
fs/binfmt_aout.c a.out格式 特定魔数
fs/binfmt_misc.c 杂项格式 可配置魔数

5.3 魔数识别机制

每个格式处理器在load_binary()函数中自己检查bprm->buf

  • ELF:检查前4字节 0x7f 'E' 'L' 'F'
  • 脚本:检查前2字节 '#!'
  • a.out:检查特定的魔数值

六、ELF文件加载:load_elf_binary()

6.1 ELF验证和解析

c 复制代码
static int load_elf_binary(struct linux_binprm *bprm)
{
    // 1. 从bprm->buf获取ELF头
    struct elfhdr *elf_ex = (struct elfhdr *)bprm->buf;
    
    // 2. 验证魔数和架构
    if (memcmp(elf_ex->e_ident, ELFMAG, SELFMAG) != 0)
        goto out;
    if (!elf_check_arch(elf_ex))
        goto out;
    
    // 3. 读取程序头表
    elf_phdata = load_elf_phdrs(elf_ex, bprm->file);
}

6.2 处理程序头表

c 复制代码
// 遍历所有程序头
for(i = 0; i < elf_ex->e_phnum; i++, elf_ppnt++) {
    switch (elf_ppnt->p_type) {
    case PT_INTERP:  // 动态链接器段
        // 读取解释器路径,如"/lib64/ld-linux-x86-64.so.2"
        break;
        
    case PT_LOAD:    // 可加载段
        // 映射.text、.data等段到内存
        error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
                       elf_prot, elf_flags, total_size);
        break;
        
    case PT_GNU_STACK:  // 堆栈段
        // 设置堆栈可执行性
        break;
    }
}

6.3 内存映射关键步骤

c 复制代码
// 映射ELF段到内存
static unsigned long elf_map(struct file *filep, unsigned long addr,
                            struct elf_phdr *eppnt, int prot, int type)
{
    // 调用底层mmap
    map_addr = do_mmap(filep, addr, eppnt->p_filesz,
                       prot, type, eppnt->p_offset);
    
    // 处理.bss段(内存大小 > 文件大小)
    if (eppnt->p_memsz > eppnt->p_filesz) {
        // 零初始化剩余部分
        unsigned long start = addr + eppnt->p_filesz;
        do_brk(start, eppnt->p_memsz - eppnt->p_filesz);
    }
}

6.4 动态链接器处理

c 复制代码
// 如果程序需要动态链接
if (interpreter) {  // 有PT_INTERP段
    // 1. 打开动态链接器
    interpreter = open_exec(elf_interpreter);
    
    // 2. 加载动态链接器到内存
    load_elf_interp(&interp_elf_ex, interpreter, &interp_load_addr);
    
    // 3. 设置入口点为动态链接器的入口
    elf_entry = interp_load_addr + interp_elf_ex.e_entry;
}

七、最终执行:start_thread()

7.1 ARM64架构的实现

c 复制代码
// arch/arm64/include/asm/processor.h
static inline void start_thread(struct pt_regs *regs, unsigned long pc,
                                unsigned long sp)
{
    start_thread_common(regs, pc);
    regs->pstate = PSR_MODE_EL0t;  // 切换到用户模式
    spectre_v4_enable_task_mitigation(current);  // 安全缓解
    regs->sp = sp;  // 设置堆栈指针
}

static inline void start_thread_common(struct pt_regs *regs, unsigned long pc)
{
    // 保存系统调用号
    s32 previous_syscall = regs->syscallno;
    
    // 清零所有寄存器
    memset(regs, 0, sizeof(*regs));
    
    // 恢复系统调用号,设置程序计数器
    regs->syscallno = previous_syscall;
    regs->pc = pc;
}

7.2 start_thread()的作用

  1. 设置程序计数器regs->pc = elf_entry
  2. 设置堆栈指针regs->sp = bprm->p
  3. 切换到用户模式regs->pstate = PSR_MODE_EL0t
  4. 启用安全防护:Spectre缓解措施
  5. 准备从新地址开始执行

八、完整执行流程总结

8.1 动态链接程序的执行流程

复制代码
用户执行:./program
    ↓
libc:execveat() → 系统调用
    ↓
内核:sys_execveat() → compat_do_execveat()
    ↓
do_execveat_common():参数处理和准备
    ↓
bprm_execve():打开文件、安全检查
    ↓
exec_binprm():执行二进制
    ↓
search_binary_handler():遍历格式处理器
    ↓
load_elf_binary():加载ELF
    ├── 发现PT_INTERP段(动态链接器)
    ├── 加载程序段到内存
    ├── 加载动态链接器
    └── 设置入口点 = 动态链接器入口
    ↓
start_thread():设置寄存器状态
    ↓
动态链接器(ld.so)执行:
    ├── 加载依赖库(libc.so.6等)
    ├── 符号重定位
    ├── 调用程序初始化函数
    └── 跳转到program的main()
    ↓
程序:main()开始执行

8.2 静态链接程序的执行流程

复制代码
用户执行:./static_program
    ↓
内核:load_elf_binary()
    ├── 没有PT_INTERP段
    ├── 直接映射.text/.data段
    ├── 设置入口点 = 程序入口
    └── start_thread()
    ↓
程序:直接从main()开始执行
相关推荐
HLC++13 小时前
Linux的进程间通信
android·linux·服务器
groundhappy16 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕17 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫18 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生19 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.1120 小时前
linux基础IO
linux·运维·服务器
BelongPanda1 天前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯1 天前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_777998341 天前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器
yunwei371 天前
eBPF 教程:检查 exec 后真正安装的可执行镜像
linux·开源