学习内容来自庖丁解牛,仅作为个人学习研究用途,如作者认为侵权请联系第一时间删除。
1. execve系统调用上下文环境
int execlp(const char *filename, const char *arg, ...)
说明
execlp函数会从PATH环境变量所指得目录中查找符合参数file的文件名,找到后便执行该文件,
然后将第二个以后的参数当作该文件的argv[0]、argv[1]…,最后一个参数必须用空指针(NULL)结束。
对于main函数int main(int argc, char *argv[])生成的lab可执行文件,在命令行中执行lab -a,
则main中argc=2,argv[0]=lab, argv[1]=-a。
那么对于execlp("/bin/ls","ls",NULL);
就是去找到/bin/ls这个程序,从这个程序的地方开始执行。
然后把ls,null传进去
那么在ls这个程序的main函数里,argc=1,argv[0]=ls。
execve系统调用说明
编程使用的库函数 exec 及类似函数都是 execve 的封装例程。
调用exec并不创建新进程,exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈段。
int execve(const char *filename, char *const argv[],char *const envp[]);
filename 为可执行文件的名字,argv 是以 NULL 结尾的命令行参数数组,
envp 同样是以 NULL 结尾的环境变量数组。
调用execlp时shell的堆栈
注意父进程在调用 execve 这个命令行时,只是压在了 shell 程序当前进程的堆栈上,堆栈在加载完新的可执行程序之后已经被清空了。所以是内核帮我们创建了一个新的进程执行堆栈和新的进程的用户态堆栈。
2. 静态、动态链接的装载区别
对于静态链接的可执行程序,等到系统调用返回后就直接执行程序入口点处的代码即可。
对于动态链接的程序,从内核态返回时还需要先执行.interp
节指向的动态连接器。(动态链接的可执行文件会比静态链接多出.interp
这个节以及其他 ld 需要用到的节)。动态链接的过程主要是动态链接器在起作用,而不是内核完成的。
3. execve系统调用过程
sys_execve()
-> do_execve()
-> do_execve_common()
-> exec_binprm()
-> search_binary_handler()
-> load_elf_binary()
-> start_thread()
sys_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);
}
调用do_execve()
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_execve_common(filename, argv, envp);
}
do_execve()只是对参数进行了类型转换,并传递给do_execve_common()
/*
* sys_execve() executes a new program.
*/
static int do_execve_common(struct filename *filename,
struct user_arg_ptr argv,
struct user_arg_ptr envp)
{
......
file = do_open_exec(filename);//打开要加载的可执行文件,加载它的文件头部,以判断文件类型
......
bprm->file = file;
bprm->filename = bprm->interp = filename->name;
//创建了一个结构体bprm,把环境变量和命令行参数都复制到结构体中
retval = bprm_mm_init(bprm);
if (retval)
goto out_unmark;
retval = copy_strings(bprm->envc, envp, bprm);//把传入的shell上下文复制到bprm中
if (retval < 0)
goto out;
......
retval = copy_strings(bprm->argc, argv, bprm);// 把传入的命令行参数复制到bprm中
if (retval < 0)
goto out;
......
retval = exec_binprm(bprm); //准备交给真正的可执行文件加载器
if (retval < 0)
goto out;
......
return retval;
}
do_execve_common函数把从shell传入的参数构造了一个结构体bprm,然后调用exec_binprm()
static int exec_binprm(struct linux_binprm *bprm)
{
......
ret = search_binary_handler(bprm);//根据读入的文件头部,寻找此可执行文件的处理函数
......
return ret;
}
search_binary_handler()函数关键代码
int search_binary_handler(struct linux_binprm *bprm)
{
......
list_for_each_entry(fmt, &formats, lh){
......
retval = fmt->load_binary(bprm);
......
}
......
}
在这个循环中寻找能够解析当前可执行文件的代码并加载出来,实际调用的是load_elf_binary() 函数。
static int load_elf_binary(struct linux_binprm *bprm)
{
...
if (elf_interpreter) {
...... // 动态链接的处理
} else { // 静态链接的处理
elf_entry =loc->elf_ex.e_entry;
if (BAD_ADDR(elf_entry)) {
retval = -EINVAL;
gotoout_free_dentry;
}
}
...
/* Now we do a little grungy work by mmapping the ELF image into
the correct location in memory. */
...
start_thread(regs, elf_entry,bprm->p);
retval = 0;
...
}
load_elf_binary中根据静态、动态链接的不同设置不同的 elf_entry,按照 ELF 文件布局加载到内存中,然后启动新的进程调用start_thread()。
void
start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{
regs->ip = new_ip;
regs->sp = new_sp;
}
进程切换到内核态前的堆栈位置和返回地址就存储在pt_regs结构里。
这里设置为 new_ip,其他如前文代码所示是 elf_entry。等该进程返回用户态时,就转而执行 elf_entry 指向的代码。
4. execve系统调用过程总结
execve 在执行时陷入内核态,用 execve 中加载的程序把当前正在执行的进程覆盖掉,当系统调用返回时也就返回到新的可执行程序起点。
execve()的系统调用实质是运行在内核态的 sys_execve()函数,大致处理过程简要总结如下。
(1)sys_execve 中的 do_execve()读取 128 个字节的文件头部,以此判断可执行文件的类型。
(2)调用 search_binary_handle()去搜索和匹配合适的可执行文件装载处理过程。
(3)ELF 文件由 load_elf_binary()函数负责装载。load_elf_binary 函数调用了 start_thread函数,创建新进程的堆栈,其中有 pt_regs 栈底指针。修改了中断现场中保存的EIP 寄存器,这里分静态链接和动态链接两种情况。
-静态链接:elf_entry 指向可执行文件的头部,一般是 main 函数,是新程序执行的起点。
-动态链接:elf_entry 指向 ld(动态链接器)的起点 load_elf_interp。
5. 补充elf_format全局变量
search_binary_handler函数中,在链表中寻找能够解析这种文件格式的内核模块。
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
bprm->recursion_depth++;
retval = fmt->load_binary(bprm);
解开宏定义如下
for (fmt=__container_of((&formats)->next, fmt, lh); &fmt->lh != (&formats); fmt=__container_of(fmt->lh.next, fmt, lh))
fmt是迭代器指针,struct linux_binfmt *fmt;
formats是链表头
static struct linux_binfmt elf_format = {
.module = THIS_MODULE,
.load_binary = load_elf_binary,
.load_shlib = load_elf_library,
.core_dump = elf_core_dump,
.min_coredump = ELF_EXEC_PAGESIZE,
};
结合fmt变量的类型可以看出他是elf_format结构体指针,它会遍历formats链表,
那么这个for就是在formats链表中找可以解析当前二进制文件的迭代器。
再看elf_format这个变量的注册。
static int __init init_elf_binfmt(void)
{
register_binfmt(&elf_format);//把变量注册进内核链表,在链表里查找文件的格式
return 0;
}
在 init_elf_binfmt 函数中,把 elf_format 变量注册在内核的 formats链表中,所以可以在链表里找到对应模块来解析 ELF 文件格式的头部。