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()开始执行
相关推荐
IT 乔峰14 小时前
脚本部署MHA集群
linux·shell
Mr_Xuhhh14 小时前
博客标题:深入理解Shell:从进程控制到自主实现一个微型Shell
linux·运维·服务器
JoyCheung-14 小时前
Free底层是怎么释放内存的
linux·c语言
旖旎夜光15 小时前
Linux(9)
linux·学习
喵了meme16 小时前
Linux学习日记24:Linux网络编程基础
linux·网络·学习
whlqjn_121116 小时前
linux下使用SHC对Shell脚本进行封装和源码隐藏
linux·centos
weixin_4624462317 小时前
K8s 集群部署基础:Linux 三节点 SSH 互信(免密登录)配置指南
linux·kubernetes·ssh
Hard but lovely17 小时前
Linux: 线程同步-- 基于条件变量 &&生产消费模型
linux·开发语言·c++
m0_7381207217 小时前
应急响应——知攻善防靶场Linux-1详细应急过程
linux·运维·服务器·网络·web安全·ssh