进程模型8-进程加载
内核版本 | 架构 | 作者 | GitHub | CSDN |
---|---|---|---|---|
Linux-4.1.5 | armv7-A | Lux1206 |
进程管理是个复杂的过程,包括进程PID管理、进程优先级、创建、启动、销毁、进程调度/抢占、进程切换、调度策略、负载均衡等等。本篇主要介绍进程的进程的创建,梳理内核中创建一个进程涉及的重要步骤。
进程加载意义
在 linux 中除了 0号进程是被手工“捏造”出来的之外,其他所有进程的创建都是通过 fork 的方式从父进程复制而来,这种复制使得创建进程变得简化,同时进程创建过程中使用“写时拷贝”避免立即进行物理内存的创建和映射,使创建过程更加高效。大多数情况下fork 和 exec 配合使用,先调用 fork 创建子进程,然后子进程执行 exec 族调用进行程序加载,创建新的代码段、数据段、bss、heap、stack、mmap区域。如果没有fork,exec的用途有限。而没有exec,fork几乎没有什么用途。
ELF 数据格式
在了解进程加载之前必须得对 ELF 格式有所有了解,因为加载过程中重要的步骤之一就是从 ELF 文件中将必要信息映射到用户空间的制定区域。
ELF 头部信息
typedef struct elf32_hdr {
/*ELF的一些标识信息,固定值*/
unsigned char e_ident[EI_NIDENT]; /* ELF identification */
/*目标文件类型:1-可重定位文件,2-可执行文件,3-共享目标文件等*/
Elf32_Half e_type; /* object file type */
/*文件的目标体系结构类型:ARM,X86-64*/
Elf32_Half e_machine; /* machine */
/*目标文件版本:1-当前版本*/
Elf32_Word e_version; /* object file version */
/*程序入口的虚拟地址,重定位文件中一定为0*/
Elf32_Addr e_entry; /* virtual entry point */
/*程序头表(segment header table)的偏移量,没有可为0*/
Elf32_Off e_phoff; /* program header table offset */
/*节区头表(section header table)的偏移量,没有可为0*/
Elf32_Off e_shoff; /* section header table offset */
/*与文件相关的,特定于处理器的标志*/
Elf32_Word e_flags; /* processor-specific flags */
/*ELF头部的大小,单位字节*/
Elf32_Half e_ehsize; /* ELF header size */
/*程序头表每个表项的大小,单位字节*/
Elf32_Half e_phentsize; /* program header entry size */
/*程序头表表项的个数*/
Elf32_Half e_phnum; /* number of program header entries */
/*节区头表每个表项的大小,单位字节*/
Elf32_Half e_shentsize; /* section header entry size */
/*节区头表表项的数目*/
Elf32_Half e_shnum; /* number of section header entries */
/*.shstrtab 段在段表中的索引位置*/
Elf32_Half e_shstrndx; /* index of ".shstrtab" in segment table */
} Elf32_Ehdr;
> readelf -h app.elf
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x43e0
Start of program headers: 64 (bytes into file)
Start of section headers: 1694872 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 37
Section header string table index: 36
ELF 程序段信息
/* ELF32 Program Header */
typedef struct elf32_phdr {
/*段类型:PT_LOAD=1 可加载的段*/
Elf32_Word p_type; /* segment type */
/*从文件头到该段的偏移*/
Elf32_Off p_offset; /* segment offset */
/*该段被放到进程中内存中的虚拟地址*/
Elf32_Addr p_vaddr; /* virtual address of segment */
/*在linux中这个成员没有任何意义,值与p_vaddr相同*/
Elf32_Addr p_paddr; /* physical address - ignored? */
/*该段在文件映像中所占的字节数,一般p_memsz>=p_filesz,.bss段在内存中占用空间*/
Elf32_Word p_filesz; /* number of bytes in file for seg. */
/*该段在内存映像中占用的字节数*/
Elf32_Word p_memsz; /* number of bytes in mem. for seg. */
/*段标志*/
Elf32_Word p_flags; /* flags */
/*p_vaddr是否对齐*/
Elf32_Word p_align; /* memory alignment */
} Elf32_Phdr;
> readelf -l ./out/app.bin
Elf file type is EXEC (Executable file)
Entry point 0x405710
There are 9 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R 0x8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000008e3b8 0x000000000008e3b8 R E 0x200000
LOAD 0x000000000008e870 0x000000000068e870 0x000000000068e870
0x0000000000002130 0x0000000000017738 RW 0x200000
DYNAMIC 0x000000000008fdb8 0x000000000068fdb8 0x000000000068fdb8
0x0000000000000220 0x0000000000000220 RW 0x8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 0x4
GNU_EH_FRAME 0x000000000007e7d0 0x000000000047e7d0 0x000000000047e7d0
0x0000000000003204 0x0000000000003204 R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x000000000008e870 0x000000000068e870 0x000000000068e870
0x0000000000001790 0x0000000000001790 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .data.rel.ro .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .data.rel.ro .dynamic .got
可加载程序类型
在 linux 内核中除了支持 elf 格式的程序意外,还支持了其他格式的可执行程序,针对每一种格式由专门的加载器检查可执行文件的格式和解析,完成可执行程序的载入。主要支持的程序格式如下:
格式 | 说明 |
---|---|
a.out | 早期UNIX系统中的默认可执行文件格式 |
elf | 目前广泛应用的可执行文件和共享库文件格式 |
script | 支持脚本类文件的加载 |
misc | 能够动态添加对任意二进制格式的支持,适用于广泛的非标准二进制格式 |
em86 | 主要用于在 Alpha 处理器上运行 x86 可执行文件 |
elf_fdpic | 是一种变种的 ELF 格式,主要用于嵌入式系统和没有内存管理单元 (MMU) 的系统,常用于位置无关代码 (PIC) 的系统,如某些实时操作系统(RTOS)和嵌入式 Linux 环境 |
flat style exe | 是一种用于嵌入式系统的轻量级可执行文件格式,通常用于没有文件系统的裸机环境。 由uClinux 项目引入,特别适用于没有 MMU 的嵌入式处理器 |
exec 簇函数
函数申明
#include <unistd.h>
int execl(const char *pathname, const char *arg, ...);
int execv(const char *pathname, char *const argv[]);
int execle(const char *path, const char *arg, ..., char *const envp[])
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg, ...);
int execvp(const char *filename, char *const argv[]);
参数
pathname:执行文件的绝对路径
filename:可执行文件名,将在PATH
环境变量中搜索文件
arg:参数化的程序参数,一般第一个参数为要执行命令名,以NULL结束
argv:数组化的程序参数,一般第一个数组成员为要执行命令名,以NULL结束
envp:数组化的环境变量,以NULL结束
使用示例
execl("/bin/ls", "ls", "-l", (char *)NULL);
char *argv[] = { "ls", "-l", (char *)NULL };
execv("/bin/ls", argv);
char *envp[] = { "HOME=/usr/home", "LOGNAME=home", (char *)NULL };
execle("/bin/ls", "ls", "-l", (char *)NULL, envp);
char *argv[] = { "ls", "-l", (char *)NULL };
char *envp[] = { "HOME=/usr/home", "LOGNAME=home", (char *)NULL };
execve("/bin/ls", argv, envp);
execlp("ls", "ls", "-l", (char *)NULL);
char *argv[] = { "ls", "-l", (char *)NULL };
execvp("ls", argv);
简单记忆法
- l 用参数列表的方式,最后一个参数时NULL
- v 把参数放在数组内argv[],数组最后一个值是NULL
- e 用新的环境变量,最后一个是存放新的环境变量的字符串数组
- p 用文件名,非p用的时完整路径
程序参数的保存
系统调用
exec 簇中所有的函数都是通过 execve 和 execveat 系统调用实现,主要区别是 execveat 多了一个 flags 标志位,flags 参数为 execveat 系统调用提供了额外的功能,控制了程序的加载和路径解析方式。
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);
}
SYSCALL_DEFINE5(execveat,
int, fd, /* 已经打开的目录,可以与filename组合成完整文件路径 */
const char __user *, filename, /* 程序路径 */
const char __user *const __user *, argv,/* 参数列表 */
const char __user *const __user *, envp,/* 环境变量列表 */
int, flags)
{
int lookup_flags = (flags & AT_EMPTY_PATH) ? LOOKUP_EMPTY : 0;
return do_execveat(fd,
getname_flags(filename, lookup_flags, NULL),
argv, envp, flags);
}
进程布局
随机地址映射 ASLR 关闭的情况下程序加载完成后的用户空间布局如下所示:
区域 | 范围 | 说明 |
---|---|---|
内核空间 | 0xFFFFFFFF ~ 0xC0000000 | 所有进程共享 |
内核隔离空间 | 0xC0000000 ~ 0xC0000000-16M | 内核和用户区的隔离区间 |
用户空间 | 0xC0000000-16M ~ 0x00000000 | 用户虚拟空间 |
默认栈空间 | 0xC0000000-16M ~ 0xC0000000-16M-132K | 默认进程栈空间为132K |
栈扩展区 | 0xC0000000-16M-132K ~ 0xC0000000-16M-132K-8M | 栈的最大可以扩展到8M |
satck guard | 0xC0000000-16M-132K-8M ~ 0xC0000000-16M-128M | 防止栈溢出等问题预留了较大的区域 |
匿名映射区 | 0xC0000000-16M-128M ~ | 用于文件映射和匿名映射区间 |
堆空间 | ~ mm->start_brk | 堆空间,起始位置与程序段大小有关 |
程序段空间 | mm->start_brk ~ 0x08048000 | 程序段空间的总大小,开始地址与默认链接地址有关 |
保留区 | 0x08048000 ~ 0x00000000 | 保留区 |
代码分析
数据结构
加载参数描述符
在 execv 加载新程序的过程中使用 linux_binprm 结构保存所需的临时数据,例如新程序首部128字节、程序参数和程序环境变量等,程序加载完成后 linux_binprm 构建的实体将会被销毁。
struct linux_binprm {
char buf[BINPRM_BUF_SIZE]; /* 可执行文件的头128字节 */
#ifdef CONFIG_MMU
struct vm_area_struct *vma; /* 表示新程序使用的虚拟内存区域信息 */
unsigned long vma_pages; /* 表示新程序占用的虚拟内存页数 */
#else
# define MAX_ARG_PAGES 32 /* 用于存储程序参数和环境变量的最大页面数(根据需要来申请page,然后挂载到对应位置) */
struct page *page[MAX_ARG_PAGES];/* 32个页地址指针数组 */
#endif
struct mm_struct *mm; /* 表示新进程的内存映射信息(开始只有一个page用作参数区和临时栈区,后期进行扩容) */
unsigned long p; /* current top of mem. 表示新程序的栈顶位置,开始指向用户进程地址高位置(3G-16M的位置),之后随着参数的存入和随机化地址偏移的加入,最后指向用户栈栈顶 */
unsigned int
cred_prepared:1,/* true if creds already prepared (multiple
* preps happen for interpreters) */
cap_effective:1;/* true if has elevated effective capabilities,
* false if not; except for init which inherits
* its parent's caps anyway */
#ifdef __alpha__
unsigned int taso:1;
#endif
unsigned int recursion_depth; /* only for search_binary_handler() */
struct file * file; /* 要执行的新程序文件 */
struct cred *cred; /* new credentials. 表示新程序的执行凭证 */
int unsafe; /* how unsafe this exec is (mask of LSM_UNSAFE_*). 表示执行新程序的安全级别 */
unsigned int per_clear; /* bits to clear in current->personality. 表示在执行新程序时需要清除的当前进程的 personality 标志位 */
int argc, envc; /* 命令行参数和环境变量数目 */
const char * filename; /* Name of binary as seen by procps. 新程序文件的名称 */
const char * interp; /* Name of the binary really executed. Most
of the time same as filename, but could be
different for binfmt_{misc,script}. 程序解释器的名称 */
unsigned interp_flags; /* 表示解释器的标志位 */
unsigned interp_data;
unsigned long loader, exec; /* 分别表示解释器和新程序的入口地址 */
};
程序加载器描述符
Linux 内核在初始化时将不同类型可执行程序加载器统一注册到 formats 链表上,在使用 execv 簇函数加载新程序时,先获取程序头部128字节内容,然后遍历加载器链表查找可以用于加载当前程序的加载器,完成程序加载。
struct linux_binfmt {
struct list_head lh; /* 用于加入二进制处理程序链表的节点 */
struct module *module;
int (*load_binary)(struct linux_binprm *); /* 读取可执行文件的头部信息为进程建立一个新的执行环境 */
int (*load_shlib)(struct file *); /* 态的把一个共享库捆绑到一个已经在运行的进程 */
int (*core_dump)(struct coredump_params *cprm);/* 在core文件中存放当前进程的执行上下文. 在进程接收到一个缺省操作为”dump”的信号时创建的, 其格式取决于被执行程序的可执行类型 */
unsigned long min_coredump; /* minimal dump size */
};
函数分析
execve
execve/execveat
└── do_execve
└── do_execveat_common
├── unshare_files
├── do_open_execat
├── bprm_mm_init
├── prepare_binprm
├── copy_strings_kernel
├── copy_strings
└── exec_binprm
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);
}
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);/* AT_FDCWD:路径名解释为相对于当前工作目录的路径 */
}
/*
* sys_execve() executes a new program.
*/
static int do_execveat_common(int fd, /* 要执行的程序的文件描述符。如果fd为负数,则是以当前相对路径使用 filename指定的文件名 */
struct filename *filename,/* 要执行的程序的文件。如果fd为负数或filename为绝对路径,则使用 filename指定的文件名;否则需要组合产生文件名 */
struct user_arg_ptr argv, /* 要传递给新程序的参数列表 */
struct user_arg_ptr envp, /* 新程序将使用的环境变量列表 */
int flags) /* 执行的程序的标志 */
{
char *pathbuf = NULL;
struct linux_binprm *bprm;
struct file *file;
struct files_struct *displaced;
int retval;
...
retval = unshare_files(&displaced);/* fock后的子进程与父进程共享task->files,此时子进程重新加载需要为子进程单独复制一份已经打开的文件表 */
if (retval)
goto out_ret;
retval = -ENOMEM;
bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);/* 申请linux_binprm结构体 */
if (!bprm)
goto out_files;
retval = prepare_bprm_creds(bprm); /* 基于当进程创建新程序的执行凭证 */
if (retval)
goto out_free;
check_unsafe_exec(bprm);
current->in_execve = 1; /* 设置当前进程正在进行execve加载中 */
file = do_open_execat(fd, filename, flags);/* 查找并打开新程序的二进制文件 */
retval = PTR_ERR(file);
if (IS_ERR(file))
goto out_unmark;
sched_exec(); /* 寻找最轻负载的cpu来执行进程文件 */
bprm->file = file;
if (fd == AT_FDCWD || filename->name[0] == '/') {/* fd 为负数或为绝对路径,则从filename提取新程序文件名 */
bprm->filename = filename->name;
} else {
if (filename->name[0] == '\0') /* 未指定文件名是使用 fd 组合完成的文件名 */
pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d", fd);
else /* filename->name 指定的文件名是相对相对路径, 则组合生成完整文件名 */
pathbuf = kasprintf(GFP_TEMPORARY, "/dev/fd/%d/%s",
fd, filename->name);
if (!pathbuf) {
retval = -ENOMEM;
goto out_unmark;
}
/*
* Record that a name derived from an O_CLOEXEC fd will be
* inaccessible after exec. Relies on having exclusive access to
* current->files (due to unshare_files above).
*/
if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))/* 设置在执行exec函数时自动关闭该文件描述符 */
bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
bprm->filename = pathbuf;
}
bprm->interp = bprm->filename; /* 程序解析器设置为文件名,后期修改为真正的解释器路径 */
retval = bprm_mm_init(bprm); /* 创建进程的内存描述符bprm->mm, 用于存放程序的参数、环境变量、程序名等信息,大小为一个page,此时为临时映射,不能直接访问,未分配物理内存 */
if (retval)
goto out_unmark;
bprm->argc = count(argv, MAX_ARG_STRINGS);/* 解析参数个数 */
if ((retval = bprm->argc) < 0)
goto out;
bprm->envc = count(envp, MAX_ARG_STRINGS);/* 解析环境变量个数 */
if ((retval = bprm->envc) < 0)
goto out;
retval = prepare_binprm(bprm); /* 检查该二进制文件的可执行权限,并读取文件头128字节到bprm->buf */
if (retval < 0)
goto out;
retval = copy_strings_kernel(1, &bprm->filename, bprm);/* 将新程序的文件名拷贝到新进程的临时栈上,从bprm->p位置开始向低字节存储 */
if (retval < 0)
goto out;
bprm->exec = bprm->p;/* 程序入口先设置为临时栈相同的位置,后期在 setup_arg_pages 中会修改 */
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 = exec_binprm(bprm); /* 执行可执行程序(环境准备、程序载入、用户栈扩展、设置用户进程用户空间上下文) */
if (retval < 0)
goto out;
/* execve succeeded. 新程序准备完成,释放临时资源 */
...
return retval;
}
所有类型的程序的载都是在这个函数中完成,主要工作分为3大部分:
- 待加载程序的打开、写权限的取消以及文件名的组合;
- 加载参数描述符 bprm 的申请,4K 临时参数栈的申请以及文件名、用户参数、环境变量保存到参数栈;
- 开始加载程序,包括加载器的寻找,待加载程序段和解析器程序段的读取,用户空间分区划分,用户栈扩展等,最后重新设置进程描述符内核栈上的用户上下文,用于在内核返回时直接进入解析器或新程序中执行。
bprm_mm_init
bprm_mm_init
└── mm_alloc
└── __bprm_mm_init
static int bprm_mm_init(struct linux_binprm *bprm)
{
int err;
struct mm_struct *mm = NULL;
bprm->mm = mm = mm_alloc();/* 分配一个新的 mm_struct 结构体 */
err = -ENOMEM;
if (!mm)
goto err;
/* 在执行 execve() 系统调用时,内核会使用该4K临时栈保存参数和环境变量,之后会将其再扩展128K,作真正的用户栈 */
err = __bprm_mm_init(bprm);/* 为新的 mm_struct 初始化一个 vm_area_struct 结构体, 标记大小为4K,目前还没有映射物理地址,不可用 */
if (err)
goto err;
return 0;
...
return err;
}
static int __bprm_mm_init(struct linux_binprm *bprm)
{
int err;
struct vm_area_struct *vma = NULL;
struct mm_struct *mm = bprm->mm;
bprm->vma = vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);/* 申请虚拟内存对象 */
if (!vma)
return -ENOMEM;
down_write(&mm->mmap_sem);
vma->vm_mm = mm;
/* 这里只是设置参数边界还没有实际申请内存 */
BUILD_BUG_ON(VM_STACK_FLAGS & VM_STACK_INCOMPLETE_SETUP);
vma->vm_end = STACK_TOP_MAX; /* 位置为用户空间的最高地址(3G-16M的位置),大小为一个page(4K) */
vma->vm_start = vma->vm_end - PAGE_SIZE; /* 参数开始位置在距离顶端一个page大小的位置下 */
vma->vm_flags = VM_SOFTDIRTY | VM_STACK_FLAGS | VM_STACK_INCOMPLETE_SETUP;/* 设置vm标志位, VM_STACK_INCOMPLETE_SETUP 表示vm是未完成的 */
vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
INIT_LIST_HEAD(&vma->anon_vma_chain);
err = insert_vm_struct(mm, vma); /* 将虚拟内存区域插入到内存管理单元中的红黑树中 */
if (err)
goto err;
mm->stack_vm = mm->total_vm = 1;
arch_bprm_mm_init(mm, vma);
up_write(&mm->mmap_sem);
bprm->p = vma->vm_end - sizeof(void *); /* 临时栈顶开始位置从用户空间最高位置空出一个NULL大小 */
return 0;
...
return err;
}
函数中为临时参数描述符 bprm 申请了内存管理单元 mm 内存,并划分了一个 4K 虚拟内存区域,将其边界设置为 3G-16M ~ 3G-16M-4K 位置,但是没有映射物理页,这个区域后期将用于存储文件名称、用于参数、环境变量等参数信息,并将 bprm->p 指向 vma->vm_end 位置。
copy_strings
static int copy_strings(int argc, struct user_arg_ptr argv,
struct linux_binprm *bprm)
{
struct page *kmapped_page = NULL;
char *kaddr = NULL;
unsigned long kpos = 0;
int ret;
while (argc-- > 0) {/* 逐个拷贝多个用户态字符串 */
const char __user *str;
int len;
unsigned long pos;
ret = -EFAULT;
str = get_user_arg_ptr(argv, argc);/* 获取被拷贝字符串的用户态首地址 */
if (IS_ERR(str))
goto out;
len = strnlen_user(str, MAX_ARG_STRLEN);/* 获取被拷贝字符串的长度 */
if (!len)
goto out;
ret = -E2BIG;
if (!valid_arg_len(bprm, len))
goto out;
/* We're going to work our way backwords. */
pos = bprm->p;/* 计算“参数区”中存储位置的偏移 */
str += len; /* 字符串从后向前存储 */
bprm->p -= len;
while (len > 0) {/* 以后退方式逐个字符拷贝字符串 */
int offset, bytes_to_copy;
if (fatal_signal_pending(current)) {
ret = -ERESTARTNOHAND;
goto out;
}
cond_resched();/* 检查是否需要进行进程调度,防止拷贝大量字符串时阻塞调度 */
offset = pos % PAGE_SIZE;
if (offset == 0)
offset = PAGE_SIZE;
bytes_to_copy = offset;
if (bytes_to_copy > len)
bytes_to_copy = len;
offset -= bytes_to_copy;
pos -= bytes_to_copy;
str -= bytes_to_copy;
len -= bytes_to_copy;
if (!kmapped_page || kpos != (pos & PAGE_MASK)) {
struct page *page;
page = get_arg_page(bprm, pos, 1);/* 获取“参数区”对应的物理内存page,如果还没有则新分配page */
if (!page) {
ret = -E2BIG;
goto out;
}
if (kmapped_page) {
flush_kernel_dcache_page(kmapped_page);
kunmap(kmapped_page);
put_arg_page(kmapped_page);
}
kmapped_page = page;
kaddr = kmap(kmapped_page);/* 给“参数区”物理page映射到内核虚拟地址,此时才能存储 */
kpos = pos & PAGE_MASK;
flush_arg_page(bprm, kpos, kmapped_page);
}
if (copy_from_user(kaddr+offset, str, bytes_to_copy)) {/* 拷贝用户态字符串到“参数区”中的存储位置 */
ret = -EFAULT;
goto out;
}
}
}
ret = 0;
...
return ret;
}
加载过程中的字符串拷贝到临时栈上的操作都是次函数完成的,函数中在拷贝字符串前会先检查当前的虚拟内存是否已经映射到真实物理页上,如果没有则进行物理页映射,主要是因为在 __bprm_mm_init 中只申请了虚拟内存描述符和设置了边界,并没有映射物理地址,此处完成映射。
exec_binprm
exec_binprm
└── search_binary_handler
└── load_binary -> load_elf_binary
static int exec_binprm(struct linux_binprm *bprm)
{
int ret;
...
ret = search_binary_handler(bprm);/* 搜索二进制执行文件的加载工具并执行加载 */
if (ret >= 0) {
// 内核追踪设置
...
}
return ret;
}
int search_binary_handler(struct linux_binprm *bprm)
{
bool need_retry = IS_ENABLED(CONFIG_MODULES);
struct linux_binfmt *fmt;
int retval;
/* This allows 4 levels of binfmt rewrites before failing hard. */
if (bprm->recursion_depth > 5)// 执行文件递归不能超过5层,例如C语言中调用python脚本,python又调用shell等
return -ELOOP;
retval = security_bprm_check(bprm);
if (retval)
return retval;
retval = -ENOENT;
retry:
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {/* 遍历 formats 链表上所有二进制格式程序处理实体 */
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
bprm->recursion_depth++;
retval = fmt->load_binary(bprm); /* 调用对应加载器的载入函数来加载新程序 */
read_lock(&binfmt_lock);
put_binfmt(fmt);
bprm->recursion_depth--;
if (retval < 0 && !bprm->mm) { /* 程序加载失败或没有分配mm信息 */
/* we got to flush_old_exec() and failed after it */
read_unlock(&binfmt_lock);
force_sigsegv(SIGSEGV, current);
return retval;
}
if (retval != -ENOEXEC || !bprm->file) {
read_unlock(&binfmt_lock);
return retval;
}
}
read_unlock(&binfmt_lock);
// 尝试判断
...
return retval;
}
函数的主要功能是遍历内核初始化时已经注册到 formats 为全局链表上的各种加载器,通过之前流程中读取到 bprm->buf 中的128字节程序头,就可以在加载器中的 load_binary() 函数中确定是否能够进行加载,同时加载程序支持嵌套但最大不超过5层,例如:C语言中调用 python 脚本,python又调用 shell 等,本文主要分析 ELF 格式加载。
load_elf_binary
load_elf_binary
├── load_elf_phdrs
├── flush_old_exec
├── setup_new_exec
├── setup_arg_pages
├── elf_map
├── set_brk
├── load_elf_interp
├── create_elf_tables
└── start_thread
static int load_elf_binary(struct linux_binprm *bprm)
{
struct file *interpreter = NULL; /* to shut gcc up */
unsigned long load_addr = 0, load_bias = 0;
int load_addr_set = 0;
char * elf_interpreter = NULL;
unsigned long error;
struct elf_phdr *elf_ppnt, *elf_phdata, *interp_elf_phdata = NULL;
unsigned long elf_bss, elf_brk;
int retval, i;
unsigned long elf_entry;
unsigned long interp_load_addr = 0;
unsigned long start_code, end_code, start_data, end_data;
unsigned long reloc_func_desc __maybe_unused = 0;
int executable_stack = EXSTACK_DEFAULT;
struct pt_regs *regs = current_pt_regs();/* 获取当前进程的用户空间上下文信息,需要重新设置新程序入口和用户栈指针 */
struct {
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
} *loc;
struct arch_elf_state arch_state = INIT_ARCH_ELF_STATE;
loc = kmalloc(sizeof(*loc), GFP_KERNEL); /* 申请可执行文件和解析器ELF首部信息结构体 */
if (!loc) {
retval = -ENOMEM;
goto out_ret;
}
/* Get the exec-header */
loc->elf_ex = *((struct elfhdr *)bprm->buf); /* 获取可执行文件首部128字节信息转成elf固定结构 */
retval = -ENOEXEC;
/* First of all, some simple consistency checks */
if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0) /* 比较首部魔数 */
goto out;
if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN) /* 仅支持ELF可执行文件和动态库文件 */
goto out;
if (!elf_check_arch(&loc->elf_ex)) /* 检查架构相关标志 */
goto out;
if (!bprm->file->f_op->mmap) /* 文件mmap函数非空 */
goto out;
elf_phdata = load_elf_phdrs(&loc->elf_ex, bprm->file); /* 读取文件的所有程序段信息 */
if (!elf_phdata)
goto out;
elf_ppnt = elf_phdata;
elf_bss = 0;
elf_brk = 0;
start_code = ~0UL;
end_code = 0;
start_data = 0;
end_data = 0;
/* 1.准备阶段:
* 加载可执行执行文件或动态库文件的程序头表,如果是可执行文件还需要加载解析器文件的程序头表,
* 遍历待加载文件的程序头表,找到PT_INTERP segment 用 `readelf -l xxx`可以读出 interpreter 文件的路径,
* 一般为"/lib64/ld-linux-x86-64.so.2",并且读出 interpreter 的`elf header`保存到loc->interp_elf_ex
*/
for (i = 0; i < loc->elf_ex.e_phnum; i++) { /* 遍历所有程序的头表*/
if (elf_ppnt->p_type == PT_INTERP) { /* 找到类型为解析器的头表,这种类型的程序头表只有在可执行文件中存在 */
/* This is the program interpreter used for
* shared libraries - for now assume that this
* is an a.out format binary
*/
retval = -ENOEXEC;
if (elf_ppnt->p_filesz > PATH_MAX || /* 解析器程序段在文件中的大小(就是解析器路径,最大长度为4096) */
elf_ppnt->p_filesz < 2)
goto out_free_ph;
retval = -ENOMEM;
elf_interpreter = kmalloc(elf_ppnt->p_filesz,
GFP_KERNEL);
if (!elf_interpreter)
goto out_free_ph;
retval = kernel_read(bprm->file, elf_ppnt->p_offset, /* 读取解析器的完整路径名称如"/lib64/ld-linux-x86-64.so.2" */
elf_interpreter,
elf_ppnt->p_filesz);
if (retval != elf_ppnt->p_filesz) {
if (retval >= 0)
retval = -EIO;
goto out_free_interp;
}
/* make sure path is NULL terminated */
retval = -ENOEXEC;
if (elf_interpreter[elf_ppnt->p_filesz - 1] != '\0')
goto out_free_interp;
interpreter = open_exec(elf_interpreter); /* 打开解析器文件,返回解析器文件句柄 */
retval = PTR_ERR(interpreter);
if (IS_ERR(interpreter))
goto out_free_interp;
/*
* If the binary is not readable then enforce
* mm->dumpable = 0 regardless of the interpreter's
* permissions.
*/
would_dump(bprm, interpreter);
retval = kernel_read(interpreter, 0, bprm->buf, /* 读取解析器的elf头部128字节长度 */
BINPRM_BUF_SIZE);
if (retval != BINPRM_BUF_SIZE) {
if (retval >= 0)
retval = -EIO;
goto out_free_dentry;
}
/* Get the exec headers */
loc->interp_elf_ex = *((struct elfhdr *)bprm->buf); /* 获取ELF 解析器的ELF头部信息 */
break;
}
elf_ppnt++;
}
elf_ppnt = elf_phdata;
for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++) /* 第二次遍历程序头表做一些特殊处理 */
switch (elf_ppnt->p_type) {
case PT_GNU_STACK:
if (elf_ppnt->p_flags & PF_X)/* 检查 PT_GNU_STACK 段是否需要增加执行权限 */
executable_stack = EXSTACK_ENABLE_X;
else
executable_stack = EXSTACK_DISABLE_X;
break;
case PT_LOPROC ... PT_HIPROC:/* arm不使用此类型 */
retval = arch_elf_pt_proc(&loc->elf_ex, elf_ppnt,
bprm->file, false,
&arch_state);
if (retval)
goto out_free_dentry;
break;
}
/* Some simple consistency checks for the interpreter */
if (elf_interpreter) { /* ELF 解析器的头部检查 */
retval = -ELIBBAD;
/* Not an ELF interpreter */
if (memcmp(loc->interp_elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
goto out_free_dentry;
/* Verify the interpreter has a valid arch */
if (!elf_check_arch(&loc->interp_elf_ex))
goto out_free_dentry;
/* Load the interpreter program headers */
interp_elf_phdata = load_elf_phdrs(&loc->interp_elf_ex, /* 读取ELF解析器文件的所有程序头表信息 */
interpreter);
if (!interp_elf_phdata)
goto out_free_dentry;
/* Pass PT_LOPROC..PT_HIPROC headers to arch code */
elf_ppnt = interp_elf_phdata;
for (i = 0; i < loc->interp_elf_ex.e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_LOPROC ... PT_HIPROC:
retval = arch_elf_pt_proc(&loc->interp_elf_ex,
elf_ppnt, interpreter,
true, &arch_state);
if (retval)
goto out_free_dentry;
break;
}
}
/* 2.资源准备:
* 所有新进程(除init进程)的都是通过 fork 由父进程复制而来,继承了父进程的很多资源,此时重新加载新的程序,
* 需要释放从父进程继承而来的部分资源,创造一个全新的进程(进程号不变)。主要工作如下:
* 释放从父节点继承来的相关资源;创建自己的信号量表;重新设置 tsk->comm 可执行文件名称;
* current->mm 结构替换成 bprm->mm 结构;扩展进程用户栈到132K;重新设置用户进程栈开始地址;
*/
retval = arch_check_elf(&loc->elf_ex, !!interpreter, &arch_state);
if (retval)
goto out_free_dentry;
/* Flush all traces of the currently running executable */
retval = flush_old_exec(bprm); /* 释放当前进程旧的exe文件相关资源,建立新exe文件相关资源,信号量表,和mm替换 */
if (retval)
goto out_free_dentry;
/* Do this immediately, since STACK_TOP as used in setup_arg_pages
may depend on the personality. */
SET_PERSONALITY2(loc->elf_ex, &arch_state);
if (elf_read_implies_exec(loc->elf_ex, executable_stack)) /* 配置current->personality属性,一般默认是关闭的 */
current->personality |= READ_IMPLIES_EXEC;
if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)/* 地址随机化能力标志设置 */
current->flags |= PF_RANDOMIZE;
setup_new_exec(bprm); /* 设置新程序用户空间匿名映射区 mmap_base 地址和进程程序名称, 设置mm->task_size = TASK_SIZE */
/* Do this so that we can load the interpreter, if need be. We will
change some of these later */
retval = setup_arg_pages(bprm, randomize_stack_top(STACK_TOP), /* 将用户进程的栈再扩展128K,总大小:4K(临时参数区)+128k(扩展部分) */
executable_stack);
if (retval < 0)
goto out_free_dentry;
current->mm->start_stack = bprm->p; /* 设置进程用户栈位置为(STACK_TOP_MAX-sizeof(void*)-sizeof(参数区)-随机化偏移大小)的位置 */
/* 3.程序载入:
* 将 PT_LOAD 类型的程序段描述的程序映射到当前用户进程的虚拟内存上;
* 设置好 .code .data .bss .brk 段在用户空间内存布局
*/
for(i = 0, elf_ppnt = elf_phdata;
i < loc->elf_ex.e_phnum; i++, elf_ppnt++) { /* 遍历在准备阶段获取到的可执行文件的所有程序头表 */
int elf_prot = 0, elf_flags;
unsigned long k, vaddr;
unsigned long total_size = 0;
/* 仅载入类型为PT_LOAD的头表下的内容,一般只存在2个PT_LOAD,第一个PT_LOAD存放只读数据例如.text .rodata 段等,
* 第二个PT_LOAD存放可读可写数据,例如.data .bss 段等,PT_LOAD 段按照地址增序放置 */
if (elf_ppnt->p_type != PT_LOAD)
continue;
if (unlikely (elf_brk > elf_bss)) { /* 这里正常情况不会进入,第一次时都为0,第二次是二者相等,除非有第三个PT_LOAD段 */
// 正常情况不会进入
...
}
if (elf_ppnt->p_flags & PF_R) /* 将可载入程序表权限转换为elf对应的权限 */
elf_prot |= PROT_READ;
if (elf_ppnt->p_flags & PF_W)
elf_prot |= PROT_WRITE;
if (elf_ppnt->p_flags & PF_X)
elf_prot |= PROT_EXEC;
elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;
vaddr = elf_ppnt->p_vaddr; /* 获取程序表的虚拟地址 */
if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) { /* 可执行文件 */
elf_flags |= MAP_FIXED;
} else if (loc->elf_ex.e_type == ET_DYN) { /* 动态库文件 */
load_bias = ELF_ET_DYN_BASE - vaddr; /* 32bit中ELF_ET_DYN_BASE载入地址为用户进程空间大小的2/3处(位于匿名和文件映射区)位于开始存放.code.data.bss,此处是计算一个偏移量 */
if (current->flags & PF_RANDOMIZE)
load_bias += arch_mmap_rnd(); /* 如果开启地址随机化偏移则加上一个随机偏移 */
load_bias = ELF_PAGESTART(load_bias);
total_size = total_mapping_size(elf_phdata, /* 计算ET_DYN文件中所有PT_LOAD程序头表项表示的程序大小 */
loc->elf_ex.e_phnum);
if (!total_size) {
retval = -EINVAL;
goto out_free_dentry;
}
}
/* 重点:将当前PT_LOAD程序表项描述的数据区域,使用vm_mmap映射到当前进程虚拟内存的(load_bias + vaddr)位置上,
* 此时只是映射还没有拷贝到物理内存上,函数的返回值就是实际映射的起始地址 */
error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,
elf_prot, elf_flags, total_size);
if (BAD_ADDR(error)) {
retval = IS_ERR((void *)error) ?
PTR_ERR((void*)error) : -EINVAL;
goto out_free_dentry;
}
if (!load_addr_set) { /* 基于第一个PT_LOAD类型的程序表项计算出可执行文件/动态库文件开始位置对应的虚拟地址(载入地址) */
load_addr_set = 1;
load_addr = (elf_ppnt->p_vaddr - elf_ppnt->p_offset); /* 计算可执行文件的载入地址??? */
if (loc->elf_ex.e_type == ET_DYN) {
load_bias += error -
ELF_PAGESTART(load_bias + vaddr);
load_addr += load_bias;
reloc_func_desc = load_bias;
}
}
k = elf_ppnt->p_vaddr;
if (k < start_code)/* 只会在处理第一个PT_LOAD类型的程序表项时进入,处理第二个时 k>start_code */
start_code = k;
if (start_data < k)/* 两次处理都会进入,但是以处理第二个PT_LOAD类型的程序表项时的虚拟地址为准 */
start_data = k;
/* 第二个PT_LOAD类型的程序表项中描述了bss段,但是不包含在文件中,bss在载入时占用内存,所以只可能是 p_memsz >= p_filesz */
if (BAD_ADDR(k) || elf_ppnt->p_filesz > elf_ppnt->p_memsz ||
elf_ppnt->p_memsz > TASK_SIZE ||
TASK_SIZE - elf_ppnt->p_memsz < k) {
/* set_brk can never work. Avoid overflows. */
retval = -EINVAL;
goto out_free_dentry;
}
k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;
if (k > elf_bss) /* 两次处理都会进入,但是以处理第二个PT_LOAD类型的程序表项时的(虚拟地址+文件内大小)的地址为准 */
elf_bss = k;
if ((elf_ppnt->p_flags & PF_X) && end_code < k) /* 只有第一次处理会进入,因为第一个PT_LOAD类型的程序表项具有执行权限 */
end_code = k;
if (end_data < k) /* 两次处理都会进入,但是以处理第二个PT_LOAD类型的程序表项时的(虚拟地址+文件内大小)的地址为准 */
end_data = k;
k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz; /* 在第二次处理时p_memsz-p_filesz就是bss段的大小,而第一次时p_memsz==p_filesz */
if (k > elf_brk) /* 两次处理都会进入,但是以处理第二个PT_LOAD类型的程序表项时的(虚拟地址+所需内存大小)的地址为准 */
elf_brk = k;
}
/* 针对动态库文件需要加上偏移,针对可执行文件load_bias为0 */
loc->elf_ex.e_entry += load_bias;
elf_bss += load_bias;
elf_brk += load_bias;
start_code += load_bias;
end_code += load_bias;
start_data += load_bias;
end_data += load_bias;
/* elf_map()中并没有映射bss区域,此时需要创建bss对应的匿名映射vma, 同时设置堆的current->mm->start_brk = current->mm->brk = elf_brk; */
retval = set_brk(elf_bss, elf_brk);
if (retval)
goto out_free_dentry;
if (likely(elf_bss != elf_brk) && unlikely(padzero(elf_bss))) {
retval = -EFAULT; /* Nobody gets to see this, but.. */
goto out_free_dentry;
}
/* 4.程序入口地址寻找:
* 如果有解析器则寻找解析器加载到当前进程后得地址,加上解析器的入口地址,先跳转到动态解析器执行;
* 如果是静态程序,则不依赖解析器,只需要寻找到程序的入口地址即可。
*/
if (elf_interpreter) { /* 可执行文件依赖解析器时的代码入口是解析器的代码入口(动态链接的程序) */
unsigned long interp_map_addr = 0;
/* 完成解析器在当前进程空间的映射,返回解析器载入进程空间的开始地址。
* execve系统调用执行完成之后,不是马上执行可执行文件中的代码,而是执行动态链接器的启动代码 */
elf_entry = load_elf_interp(&loc->interp_elf_ex,
interpreter,
&interp_map_addr,
load_bias, interp_elf_phdata);
if (!IS_ERR((void *)elf_entry)) {
interp_load_addr = elf_entry;
elf_entry += loc->interp_elf_ex.e_entry;/* 解析器运行时的代码入口=解析器的载入地址+解析器的代码入口 */
}
if (BAD_ADDR(elf_entry)) {
retval = IS_ERR((void *)elf_entry) ?
(int)elf_entry : -EINVAL;
goto out_free_dentry;
}
reloc_func_desc = interp_load_addr;
allow_write_access(interpreter);/* 设置写权限 */
fput(interpreter);
kfree(elf_interpreter);
} else { /* 可执行文件不依赖解析器时的代码入口是可执行文件代码入口(静态链接的程序) */
elf_entry = loc->elf_ex.e_entry;/* 设置可执行文件的代码入口 */
if (BAD_ADDR(elf_entry)) {
retval = -EINVAL;
goto out_free_dentry;
}
}
kfree(interp_elf_phdata);
kfree(elf_phdata);
set_binfmt(&elf_format); /* 设置当前进程的 mm->binfmt 为新的加载器 */
...
install_exec_creds(bprm);
retval = create_elf_tables(bprm, &loc->elf_ex, /* 将构造的 elf_info 结构数据和每条参数、环境变量的地址保存到用户栈上 */
load_addr, interp_load_addr);
if (retval < 0)
goto out;
/* N.B. passed_fileno might not be initialized? 设置当前用户进程程序的内存布局*/
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->start_data = start_data;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p; /* 真正程序可用的用户栈位置 */
if ((current->flags & PF_RANDOMIZE) && (randomize_va_space > 1)) {/* 堆结束地址计算(随机化) */
current->mm->brk = current->mm->start_brk =
arch_randomize_brk(current->mm);
#ifdef compat_brk_randomized
current->brk_randomized = 1;
#endif
}
if (current->personality & MMAP_PAGE_ZERO) {
/* 在0地址上创建一个全零的匿名映射vma */
error = vm_mmap(NULL, 0, PAGE_SIZE, PROT_READ | PROT_EXEC,
MAP_FIXED | MAP_PRIVATE, 0);
}
...
/* 5. 启动进程新程序:
* 修改保存在内核栈上的用户态上下文寄存器:ip = elf_entry,sp = bprm->p
* 当系统调用完成后,返回用户空间时,会跳转到interpreter或者新程序的入口开始执行,
* 一般会跳转到动态解析中进行动态链接,之后在进入用户进程
*/
start_thread(regs, elf_entry, bprm->p);
retval = 0;
out:
kfree(loc);
out_ret:
return retval;
...
}
ELF 格式的动态库文件和可执行文件的加载由 load_elf_binary() 完成,主要工作分为以下几部分:
- 准备阶段:
读取解析器 PT_INTERP 程序段,获取解析器路径一般为 "/lib64/ld-linux-x86-64.so.2",读取解析器ELF头部信息和程序段信息。 - 资源准备:
所有新进程(除 init 进程)的都是通过 fork 由父进程复制而来,继承了父进程的很多资源,此时重新加载新的程序,需要释放从父进程继承而来的部分资源,创造一个全新的进程(进程号pid不变),此时在内核中释放用户进程资源不会有任何影响。
主要工作:释放从父节点继承来的资源;创建自己的信号量表;重新设置 tsk->comm 可执行文件名称;current->mm 替换成 bprm->mm 结构;扩展进程用户栈到132K;重新设置用户进程栈开始地址; - 程序载入:
将 PT_LOAD 可载入类型的程序段映射到当前用户进程的虚拟内存上,设置好 .code .data .bss .brk 在用户空间内存上的布局。 - 程序入口寻找和参数保存到用户栈:
如果有解析器则寻找,解析入口地址 = 解析器加载到内存的地址+解析器的入口地址,先跳转到动态解析器执行,如果是静态程序,则不依赖解析器,只需要寻找到程序的入口地址即可。
将 elf_info 等信息压栈到用户进程栈上,会体现在用户进程的 /proc/PID/auxv 文件中。 - 修改进程内核栈上的用户态上下文:
进程内核栈上的用户态上下文中包含了 18 个寄存器的值,这些值在从内核返回用户层的时候决定了返回到什么位置,此处已经将新进程的环境准备好了,可以修改之前进入内核时的用户上下文,修改到新的进程入口地址或是解析器的入口地址,当系统调用完成后,返回用户空间时,会跳转到 interpreter 或者新程序的入口开始执行。
flush_old_exec
flush_old_exec
└── exec_mmap
int flush_old_exec(struct linux_binprm * bprm)
{
int retval;
retval = de_thread(current);/* 创建独立信号量表 */
if (retval)
goto out;
set_mm_exe_file(bprm->mm, bprm->file);/* exe_file指向新的可执行文件bprm->mm->exe_file = bprm->file */
acct_arg_size(bprm, 0);
/* 重点:
* 首先释放掉进程旧的用户地址空间映射, 这个是可以的,应为目前是在内核空间中,使用不到永不空间的虚拟地址
* 然后设置新的用户地址空间映射,bprm->mm正式转正了:tsk->mm = bprm->mm
* 从现在开始可以通过“正常地址访问+缺页异常”来访问新exe的地址空间了,不过现在还是映射了一个page,即“参数区”
*/
retval = exec_mmap(bprm->mm);
if (retval)
goto out;
bprm->mm = NULL; /* We're using it now. bprm->mm已经完成使命此时设置为0, 后面直接使用 tsk->mm */
set_fs(USER_DS);
current->flags &= ~(PF_RANDOMIZE | PF_FORKNOEXEC | PF_KTHREAD |
PF_NOFREEZE | PF_NO_SETAFFINITY);
flush_thread();
current->personality &= ~bprm->per_clear;
return 0;
out:
return retval;
}
static int exec_mmap(struct mm_struct *mm)
{
struct task_struct *tsk;
struct mm_struct *old_mm, *active_mm;
/* Notify parent that we're no longer interested in the old VM */
tsk = current;
old_mm = current->mm;
mm_release(tsk, old_mm);
...
task_lock(tsk);
active_mm = tsk->active_mm;
tsk->mm = mm; /* 设置当前任务的用户空间描述符, 后期直接使用 tsk->mm */
tsk->active_mm = mm; /* 设置激活 mm */
activate_mm(active_mm, mm); /* 新用户空间描述符替换旧的用户空间描述符 */
tsk->mm->vmacache_seqnum = 0;
vmacache_flush(tsk);
task_unlock(tsk);
...
mmdrop(active_mm);
return 0;
}
主要是释放当前进程持有的一些旧地址空间,设置进程新的名称,并将进程的 tsk->mm 替换为 bprm->mm,后期直接使用 tsk->mm 即可。
setup_new_exec
setup_new_exec
├── arch_pick_mmap_layout
├ └── mmap_base
├── __set_task_comm
└── do_close_on_exec
void setup_new_exec(struct linux_binprm * bprm)
{
arch_pick_mmap_layout(current->mm); /* 计算mmap区域的基地址:tsk->mm->mmap_base, 基地址在用户栈stack区域之下,在这个区域存存放动态库文件 */
/* This is the point of no return */
current->sas_ss_sp = current->sas_ss_size = 0;
...
__set_task_comm(current, kbasename(bprm->filename), true); /* 设置 tsk->comm 进程新的可执行文件名称为 bprm->filename */
current->mm->task_size = TASK_SIZE; /* 设置用户进程空间大小约为3G-16M */
...
current->self_exec_id++;
flush_signal_handlers(current, 0);
do_close_on_exec(current->files); /* 扫描并关闭所有标记为 FD_CLOEXEC 的文件描述符的函 */
}
void arch_pick_mmap_layout(struct mm_struct *mm)
{
unsigned long random_factor = 0UL;
if (current->flags & PF_RANDOMIZE)
random_factor = arch_mmap_rnd();/* 32bit 时为128内的随机值 */
if (mmap_is_legacy()) { /* 经典布局下,文件映射与匿名映射区的地址增长方向是从低地址到高地址增长 */
mm->mmap_base = TASK_UNMAPPED_BASE + random_factor;
mm->get_unmapped_area = arch_get_unmapped_area;
} else { /* 新布局下,文件映射与匿名映射区的地址增长方向是从高地址到低地址增长 */
mm->mmap_base = mmap_base(random_factor);
mm->get_unmapped_area = arch_get_unmapped_area_topdown;
}
}
static unsigned long mmap_base(unsigned long rnd)
{
unsigned long gap = rlimit(RLIMIT_STACK);/* 获取当前任务用户栈的大小 */
if (gap < MIN_GAP)/* 根据用户栈的大小设置gap大小(gap包括了用户栈大小,gap-stackSize的空间用于栈向下扩展 */
gap = MIN_GAP;/* 用户空间开始位置-128M位置向下 */
else if (gap > MAX_GAP)
gap = MAX_GAP;/* 用户空间5/6位置开始向下 */
return PAGE_ALIGN(TASK_SIZE - gap - rnd);/* 匿名映射区位于进程用户空间总大小-gap大小(包括栈)-rnd */
}
主要是设置新执行文件的 mm 参数,包括 mm->task_size = TASK_SIZE,关闭 exec 执行时需要关闭的文件描述符,并根据设置按照新式布局还是传统布局设置,设备匿名映射区基地址 mm->mmap_base 的开始位置。
在新式布局下,文件映射与匿名映射区的地址是从高地址到低地址增长(朝着堆方向增长),具有更大的可扩展空间,同时下面的堆也具有了更多的扩展空间。
在传统布局下,文件映射与匿名映射区的地址增长是从低地址到高地址(背靠heap,朝着栈的方向生长),这样会导致可供堆进行扩展的空间是小于 1G。
setup_arg_pages
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;
unsigned long vm_flags;
unsigned long stack_base;
unsigned long stack_size;
unsigned long stack_expand;
unsigned long rlim_stack;
#ifdef CONFIG_STACK_GROWSUP
...
#else
stack_top = arch_align_stack(stack_top);/* 第一个page上的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;/* 计算栈顶到用户空间最高地址(3G-16M的位置)位置的距离, 这里的偏移是随机值的大小 */
bprm->p -= stack_shift; /* 因为随机化地址,所以需要修改 p 的位置,向下偏移,p上第一个page存储的数据在 shift_arg_pages 中移动 */
mm->arg_start = bprm->p;/* 同步设置参数开始位置,这里也是参数区域的分割线,向上就是参数区域 */
#endif
if (bprm->loader)
bprm->loader -= stack_shift;
bprm->exec -= stack_shift;/* 同步修改程序的入口地址 */
down_write(&mm->mmap_sem);
vm_flags = VM_STACK_FLAGS;
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;
ret = mprotect_fixup(vma, &prev, vma->vm_start, vma->vm_end,/* 此时vma->vm_start, vma->vm_end之间是参数区+堆栈区,设置其属性 */
vm_flags);
if (ret)
goto out_unlock;
BUG_ON(prev != vma);
/* Move stack pages down in memory. */
if (stack_shift) {
ret = shift_arg_pages(vma, stack_shift);/* 因为有随机化,所以需要同步调整vm区域位置,包括之前放置的envp 和argv */
if (ret)
goto out_unlock;
}
/* mprotect_fixup is overkill to remove the temporary stack flags */
vma->vm_flags &= ~VM_STACK_INCOMPLETE_SETUP;/* 表示vm区域已经可以使用,只是现在还比较小只有不到一个page大小 */
stack_expand = 131072UL; /* randomly 32*4k (or 2*64k) pages 进程用户栈大小再扩展128K */
stack_size = vma->vm_end - vma->vm_start; /* 计算__bprm_mm_init中申请的第一个物理page的大小 */
/* rlimit(RLIMIT_STACK)表示进程栈大小,默认为8M,在 INIT_RLIMITS 中设置为 #define _STK_LIM (8*1024*1024),INIT_RLIMITS被0号进程设置,后期进程都是继承 */
rlim_stack = rlimit(RLIMIT_STACK) & PAGE_MASK;
#ifdef CONFIG_STACK_GROWSUP
...
#else
if (stack_size + stack_expand > rlim_stack)
stack_base = vma->vm_end - rlim_stack;
else
stack_base = vma->vm_start - stack_expand;/* 将用户栈的大小继续向下扩展128K,如果后期不够用的话会触发缺页异常自动进行栈扩展(8M以内) */
#endif
current->mm->start_stack = bprm->p; /* 进程用户栈的起始位置位于参数区之下 */
ret = expand_stack(vma, stack_base); /* vma->vm_start 位置更新为stack_base */
...
return ret;
}
函数中主要是将用户栈进行扩展,之前申请的 “参数栈” 的大小只有 4K,所以需要进行扩展,默认扩展到 132K = 4k+128k,后期可以继续进行扩展,但是并不是说栈区可以无限增长,它也有最大限制_STK_LIM
(一般为 8M)。当压栈耗尽栈所对应的内存区域,这将触发一个 缺页异常 (page fault)。通过异常陷入内核态后,异常会被内核的 expand_stack() 函数处理,进而调用 acct_stack_growth() 来检查是否还有合适的地方用于栈的增长。如果此时栈的总大小没有超过 8M栈会被加长,否则会导致栈溢出内核会产生 段错误(segmentation fault)。
create_elf_tables
static int
create_elf_tables(struct linux_binprm *bprm, struct elfhdr *exec,
unsigned long load_addr, unsigned long interp_load_addr)
{
unsigned long p = bprm->p;/* 替换程序后的新进程的用户栈 */
int argc = bprm->argc;
int envc = bprm->envc;
elf_addr_t __user *argv;
elf_addr_t __user *envp;
elf_addr_t __user *sp;
elf_addr_t __user *u_platform;
elf_addr_t __user *u_base_platform;
elf_addr_t __user *u_rand_bytes;
const char *k_platform = ELF_PLATFORM;
const char *k_base_platform = ELF_BASE_PLATFORM;
unsigned char k_rand_bytes[16];
int items;
elf_addr_t *elf_info;
int ei_index = 0;
const struct cred *cred = current_cred();
struct vm_area_struct *vma;
...
u_platform = NULL;
if (k_platform) {
size_t len = strlen(k_platform) + 1;
u_platform = (elf_addr_t __user *)STACK_ALLOC(p, len);/* 在用户栈上压入当前平台的能力字符串 */
if (__copy_to_user(u_platform, k_platform, len))
return -EFAULT;
}
/*
* If this architecture has a "base" platform capability
* string, copy it to userspace.
*/
u_base_platform = NULL;
if (k_base_platform) {
size_t len = strlen(k_base_platform) + 1;
u_base_platform = (elf_addr_t __user *)STACK_ALLOC(p, len);/* 在用户栈上压入当前平台基础能力字符 */
if (__copy_to_user(u_base_platform, k_base_platform, len))
return -EFAULT;
}
/*
* Generate 16 random bytes for userspace PRNG seeding.
*/
get_random_bytes(k_rand_bytes, sizeof(k_rand_bytes));
u_rand_bytes = (elf_addr_t __user *)STACK_ALLOC(p, sizeof(k_rand_bytes));/* 在用户栈上压入16个字节的随机数 */
if (__copy_to_user(u_rand_bytes, k_rand_bytes, sizeof(k_rand_bytes)))
return -EFAULT;
/* Create the ELF interpreter info */
elf_info = (elf_addr_t *)current->mm->saved_auxv;/* 获取辅助向量的条目,用于/proc/PID/auxv */
/* update AT_VECTOR_SIZE_BASE if the number of NEW_AUX_ENT() changes */
#define NEW_AUX_ENT(id, val) \
do { \
elf_info[ei_index++] = id; \
elf_info[ei_index++] = val; \
} while (0)
#ifdef ARCH_DLINFO
...
#endif
/* 将系统、平台、elf信息进行整理传递到mm->saved_auxv中,并保存到用户栈上,体现在/proc/PID/auxv */
NEW_AUX_ENT(AT_HWCAP, ELF_HWCAP);
NEW_AUX_ENT(AT_PAGESZ, ELF_EXEC_PAGESIZE);
NEW_AUX_ENT(AT_CLKTCK, CLOCKS_PER_SEC);
NEW_AUX_ENT(AT_PHDR, load_addr + exec->e_phoff);
NEW_AUX_ENT(AT_PHENT, sizeof(struct elf_phdr));
NEW_AUX_ENT(AT_PHNUM, exec->e_phnum);
NEW_AUX_ENT(AT_BASE, interp_load_addr);
NEW_AUX_ENT(AT_FLAGS, 0);
NEW_AUX_ENT(AT_ENTRY, exec->e_entry);
NEW_AUX_ENT(AT_UID, from_kuid_munged(cred->user_ns, cred->uid));
NEW_AUX_ENT(AT_EUID, from_kuid_munged(cred->user_ns, cred->euid));
NEW_AUX_ENT(AT_GID, from_kgid_munged(cred->user_ns, cred->gid));
NEW_AUX_ENT(AT_EGID, from_kgid_munged(cred->user_ns, cred->egid));
NEW_AUX_ENT(AT_SECURE, security_bprm_secureexec(bprm));
NEW_AUX_ENT(AT_RANDOM, (elf_addr_t)(unsigned long)u_rand_bytes);
#ifdef ELF_HWCAP2
NEW_AUX_ENT(AT_HWCAP2, ELF_HWCAP2);
#endif
NEW_AUX_ENT(AT_EXECFN, bprm->exec);
if (k_platform) {
NEW_AUX_ENT(AT_PLATFORM,
(elf_addr_t)(unsigned long)u_platform);
}
if (k_base_platform) {
NEW_AUX_ENT(AT_BASE_PLATFORM,
(elf_addr_t)(unsigned long)u_base_platform);
}
if (bprm->interp_flags & BINPRM_FLAGS_EXECFD) {
NEW_AUX_ENT(AT_EXECFD, bprm->interp_data);
}
#undef NEW_AUX_ENT
/* AT_NULL is zero; clear the rest too */
memset(&elf_info[ei_index], 0,
sizeof current->mm->saved_auxv - ei_index * sizeof elf_info[0]);
/* And advance past the AT_NULL entry. */
ei_index += 2;
sp = STACK_ADD(p, ei_index); /* 在用户进程的用户空间栈预留出ei_index参数大小,后面压栈 */
items = (argc + 1) + (envc + 1) + 1;
bprm->p = STACK_ROUND(sp, items); /* 在堆栈上预留 argv 和 envp 指针位置,后面压栈,此处更新 bprm->p 到真正可用的栈位置 */
/* Point sp at the lowest address on the stack */
#ifdef CONFIG_STACK_GROWSUP
...
#else
sp = (elf_addr_t __user *)bprm->p; /* 记录空出 elf_info 和 argc 以及 envc 空间的位置*/
#endif
/*
* Grow the stack manually; some architectures have a limit on how
* far ahead a user-space access may be in order to grow the stack.
*/
vma = find_extend_vma(current->mm, bprm->p);
if (!vma)
return -EFAULT;
/* Now, let's put argc (and argv, envp if appropriate) on the stack */
if (__put_user(argc, sp++))/* 先在参数预留位置放置参数个数 */
return -EFAULT;
argv = sp;
envp = argv + argc + 1;
/* Populate argv and envp */
p = current->mm->arg_end = current->mm->arg_start; /* 获取栈上之前保存参数的地址 */
while (argc-- > 0) { /* 参数在do_execveat_common中已经拷贝到"参数区"了此处是进行每个参数开始地址的保存 */
size_t len;
if (__put_user((elf_addr_t)p, argv++)) /* 将栈上参数区的参数存放的地址 p,逐个向上入栈到指定的位置 argv (上面已经空出来) */
return -EFAULT;
len = strnlen_user((void __user *)p, MAX_ARG_STRLEN); /* 检查原先p 位置的参数长度合法性 */
if (!len || len > MAX_ARG_STRLEN)
return -EINVAL;
p += len; /* 向上寻找之前存放的参数字符串 */
}
if (__put_user(0, argv))
return -EFAULT;
current->mm->arg_end = current->mm->env_start = p;/* 设置参数结束地址 */
while (envc-- > 0) { /* 环境变量在do_execveat_common中已经拷贝到"参数区"了此处是进行每个环境变量开始地址的保存 */
size_t len;
if (__put_user((elf_addr_t)p, envp++))
return -EFAULT;
len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
if (!len || len > MAX_ARG_STRLEN)
return -EINVAL;
p += len;
}
if (__put_user(0, envp))
return -EFAULT;
current->mm->env_end = p;/* 记录环境变量结束位置 */
/* Put the elf_info on the stack in the right place. */
sp = (elf_addr_t __user *)envp + 1;
if (copy_to_user(sp, elf_info, ei_index * sizeof(elf_addr_t)))/* 将elf_info写入到用户进程用户栈上 */
return -EFAULT;
return 0;
}
在之前的流程中先申请了 4K 的“参数栈”,然后又将栈扩展了 128K,此函数中继续在之前参数栈存放环境变量、用户参数的位置后面继续存放一些参数信息,然后完成 mm->arg_end、mm->env_start、mm->env_end的设置,其中 mm->arg_start 已经在 setup_arg_pages() 中设置完成。
start_thread
#define start_thread(regs,pc,sp) \
({ \
memset(regs->uregs, 0, sizeof(regs->uregs));/* 清除所有用户上下文寄存器 */ \
if (current->personality & ADDR_LIMIT_32BIT) \
regs->ARM_cpsr = USR_MODE; /* 进入用户模式 */ \
else \
regs->ARM_cpsr = USR26_MODE; \
if (elf_hwcap & HWCAP_THUMB && pc & 1) \
regs->ARM_cpsr |= PSR_T_BIT;/* 设置Thumb or arm模式 */ \
regs->ARM_cpsr |= PSR_ENDSTATE; /* 设置大小端字节序 */ \
regs->ARM_pc = pc & ~1; /* 设置解析或新进程的 pc */ \
regs->ARM_sp = sp; /* 设置新进程堆栈 sp */ \
nommu_start_thread(regs); \
})
对新进程的内核栈上的用户上下文的18个寄存器进行修改,主要修改返回用户空间时的模式、PC值(新程序或解析器的入口地址),设置新程序的用户栈地址。完成这些设置后,整个程序加载的第一阶段已经完成,从内核返回用户层的时候再返回的地方将用户上下文进行出栈,就会跳转到新程序或者解析器中执行,如果进入解析器入口,还会有第二阶段动态链接等操作,之后才会真正进入用户进程程序中。
缺页中断加载
execve 的实现并不负责将文件内容加载到物理页中,它只建立了程序文件与内存区域的映射关系就结束了。真正负责加载文件内容的是缺页中断,内核在加载的过程中为进程创建了 vma 结构体实例,然后将的 vm_file 属性设成新程序文件,建立起内存区域和文件的映射关系。当 CUP 执行代码访问对应的虚拟内存是就会产生缺页异常,缺页异常的处理路径:do_page_fault() -> __do_page_fault() -> do_user_addr_fault() -> handle_mm_fault()
,发生缺页中断的虚拟地址所在的内存区域 vma(虚拟地址落在该内存区域的 vm_start 和 vm_end 之间)存在文件映射 (vm_file 不为空),那就可以通过虚拟内存地址计算文件中的偏移,就定位到了内存所缺的页对应到文件的哪一段,然后内核将对应的物理页加载到进内存中。通过多次缺页异常,会逐渐将文件从物理页上加载到内存中。
🌀路西法 的CSDN博客拥有更多美文等你来读。