文章目录
-
- 引言
- [一、应用层入口: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()的作用
- 设置程序计数器 :
regs->pc = elf_entry - 设置堆栈指针 :
regs->sp = bprm->p - 切换到用户模式 :
regs->pstate = PSR_MODE_EL0t - 启用安全防护:Spectre缓解措施
- 准备从新地址开始执行
八、完整执行流程总结
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()开始执行