5.execve()到底干了啥?

article/2025/8/12 16:07:51

       

导语

很多童鞋有分析阅读Linux源代码的强烈愿望,可是Linux内核代码量庞大,大部分人不知道如何下手,以下是我分析Linux源代码的一些经验,仅供参考,有不实之处请大神指正!


1.要想阅读内核首先要进入内核,其中用户态程序进入内核态的主要方式是int 0x80中断,搞懂这条指令的执行过程是我们学习内核的第一步;


2.Linux中最重要的结构体莫过于task_struct,没错,这就是大名鼎鼎的进程描述符(PCB,process control block),task_struct是Linux这个大轮子能转起来的关键,对task_struct的掌握程度基本上反应了你对内核的掌握程度,task_struct中包含了内存管理,IO管理,文件系统等操作系统的基本模块。task_struct位于linux-3.18.6/include/linux/sched.h中,约400行。


3.读万卷书不如行万里路,光是读内核代码是不够的,有精力的童鞋可以试着打断点看看内核中一个函数是怎么执行的,而Linux下的调试神器就是gdb,在Linux下开发过应用程序的童鞋肯定或多或少用过gdb,经常使用图形化IDE调试工具的童鞋初涉gdb可能会有些不适应。具体怎么用gdb调试Linux内核,网上这方面的教程不少,请自行Google;


4.开gdb调试时我认为有一个很重要的方法就是搞懂函数栈,Linux内核中函数不停的调用和跳转,很容易让你迷失其中,调试时清楚知晓函数调用堆栈这点很重要~


5.打蛇打七寸,擒贼先擒王,Linux代码中有不少错误处理之类的分支,调试时千万不要陷入其中,陷进去往往不能自拔。我们要抓住主要矛盾,忽略次要矛盾。错误处理一般是Linux hacker关注的重点,hacker期望从错误处理中找到漏洞以便对内核发起攻击,而我们作为Linux 内核的reader看看函数实现就足矣;

 正题

execve系统调用的作用是执行一个新的程序,可执行程序的文件格式有许多种,这里我们就分析的对象是ELF文件格式,关于ELF格式请看4讲(传送门:第4讲)。

execve系统调用进入内核后调用的是do_execve()这个函数,do_execve()被调用的地方出现在linux-3.18.6\fs\exec.c文件中。我们一起来看一下调用do_execve()它的代码。

         SYSCALL_DEFINE3(execve,constchar __user *, filename,constchar __user *const __user *, argv,constchar __user *const __user *, envp)
{returndo_execve(getname(filename), argv, envp);
}


getname(filename)获得可执行文件的文件名,argv和envp是shell命令行传递过来的命令行参数和shell上下文环境变量。

我们深入到do_execve()一探究竟。do_execve()位于linux-3.18.6\fs\exec.c文件中。进入do_execve()我们的函数栈样子是:execve-> do_execve()

intdo_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);
}


const char__user *const __user *表示用户态指针,这里我们也可以知道__argv和__envp是由用户态传递进来的执行条件。

structuser_arg_ptr argv = { .ptr.native = __argv }; // 把命令行参数转换为相应的结构体

structuser_arg_ptr envp = { .ptr.native = __envp }; // 把shell上下文环境转换为结构体

以上代码可以看出do_execve()的主要作用是封装好执行条件(argv和envp),接着继续调用do_execve_common(),do_execve_common()位于linux-3.18.6\fs\exec.c文件中。进入do_execve_common()后的函数栈样子是:execve -> do_execve() –> do_execve_common()。

static intdo_execve_common(struct filename *filename,structuser_arg_ptr argv,struct user_arg_ptrenvp)
{struct linux_binprm *bprm;struct file *file;struct files_struct *displaced;int retval;if (IS_ERR(filename)) // 判断文件名是否合法return PTR_ERR(filename);…………………………………..// 主要是错误检查,不用管file = do_open_exec(filename);
……………………………………..bprm->file = file;bprm->filename = bprm->interp =filename->name;……………………………………………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;…………………………..out_ret:putname(filename);return retval;
}


do_execve_common()稍微复杂一点了,do_open_exec(filename)打开要加载的可执行文件,file结构体包含了打开的可执行文件信息。do_open_exec(filename)之后就是对bprm结构体的初始化了,每做一项初始化都要检查成功与否,初始化错误就要及时处理。要初始化的东西很多,不一一列出来,说几个重要的。

retval =copy_strings(bprm->argc, argv, bprm); // 把传入的命令行参数拷贝到bprm中

retval =copy_strings(bprm->envc, envp, bprm); //把传入的shell上下文拷贝到bprm中

retval = exec_binprm(bprm);// 对可执行文件的处理,比较关键的一句

我们跳入到exec_binprm(bprm)中,看看内核是怎么处理可执行文件的,exec_binprm()同样位于linux-3.18.6\fs\exec.c文件中,进入exec_binprm()我们的函数栈变为:execve -> do_execve() –> do_execve_common() -> exec_binprm()。

static intexec_binprm(struct linux_binprm *bprm)
{pid_t old_pid, old_vpid;int ret;/* Need to fetch pid before load_binarychanges it */old_pid = current->pid;rcu_read_lock();old_vpid = task_pid_nr_ns(current,task_active_pid_ns(current->parent));rcu_read_unlock();ret = search_binary_handler(bprm);if (ret >= 0) {audit_bprm(bprm);trace_sched_process_exec(current,old_pid, bprm);ptrace_event(PTRACE_EVENT_EXEC,old_vpid);proc_exec_connector(current);}return ret;
}


exec_binprm()中关键的代码是ret =search_binary_handler(bprm);寻找可执行文件的处理函数(可执行文件的类型不止一种),从search_binary_handler的名字不难发现,我们的可执行文件都是二进制文件(这不废话吗~)。

我们去看看search_binary_handler()发生了什么,search_binary_handler()位于linux-3.18.6\fs\exec.c文件中,跳入search_binary_handler()后我们函数栈的样子为:execve-> do_execve() –> do_execve_common() -> exec_binprm() -> search_binary_handler()。

intsearch_binary_handler(struct linux_binprm *bprm)
{bool need_retry =IS_ENABLED(CONFIG_MODULES);struct linux_binfmt *fmt;int retval;……………………………………..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);read_lock(&binfmt_lock);put_binfmt(fmt);bprm->recursion_depth--;if (retval < 0 &&!bprm->mm) {/* we got toflush_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;}}
……………………………return retval;
}


关键代码为list_for_each_entry这个循环,在循环体内部寻找可执行文件的解析函数,如果找到了就加载。

retval =fmt->load_binary(bprm); // 加载可执行文件的处理函数

load_binary()是一个函数指针,以ELF格式的可执行文件为例,load_binary()实际上调用的是load_elf_binary(),load_elf_binary这个函数指针被包含在一个名为elf_format的结构体中,而elf_format在linux-3.18.6\fs\binfmt_elf.c文件中定义。

到linux-3.18.6\fs\binfmt_elf.c中找到load_elf_binary:

static structlinux_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,
};


elf_format结构体由init_elf_binfmt(void)函数注册到文件解析链表中。init_elf_binfmt(void)函数位于linux-3.18.6\fs\binfmt_elf.c文件中,代码为:

static int__init init_elf_binfmt(void)
{register_binfmt(&elf_format);return 0;
}


search_binary_handler()函数的工作就是用list_for_each_entry遍历文件解析链表,找到文件的解析函数。

接下来我们可以全文检索一下Linux下的register_binfmt()函数,打开网址:http://codelab.shiyanlou.com/search?q=register_binfmt&project=linux-3.18.6


可以看到register_binfmt()函数被调用9次,注册了9种不同的文件解析函数。

前面说了文件解析函数的注册,似乎有些跑题了,赶紧拉回来,回到search_binary_handler()函数,在search_binary_handler()的list_for_each_entry循环中找到ELF文件的解析函数load_elf_binary(),我们进入load_elf_binary()看看内核是怎么解析ELF文件的。load_elf_binary()位于/linux-3.18.6/fs/binfmt_elf.c文件中,进入load_elf_binary()后函数栈的样子为:execve-> do_execve() –> do_execve_common() -> exec_binprm() -> search_binary_handler()-> load_elf_binary()。

static intload_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;}}…………………………………..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;……………………………………start_thread(regs, elf_entry,bprm->p);retval = 0;……………………………………
}


load_elf_binary()的作用不仅是解析ELF文件,更重要的是把ELF文件映射到进程空间中去。

         current->mm->end_code = end_code;current->mm->start_code =start_code;current->mm->start_data =start_data;current->mm->end_data = end_data;


以上四句话把当前进程的代码段、数据段起始和终止位置改为ELF文件中指明的数据段和代码段位置,execve系统调用返回用户态后进程就拥有了新的代码段、数据段。

if(elf_interpreter),如果需要依赖动态库,要做动态链接,需要执行if中的代码,这里我们不考虑动态链接的执行过程,只考虑静态链接。如果是静态链接的话执行else中的代码。

一般来说ELF文件中的ELFHeader中的Entry point address字段(ELF的格式见第4讲,传送门:第4讲)指明了程序入口地址(main函数的地址),这个地址一般是0x8048000(0x8048000以上的是内核段内存)。该入口地址被解析后存放在elf_ex.e_entry中,elf_entry = loc->elf_ex.e_entry;就是把ELF文件中的入口地址赋值给elf_entry变量。所以静态链接程序的起始位置一般是0x8048000。

我们接着往下读,来到start_thread(regs,elf_entry, bprm->p);,这是关键的一个函数,位于linux-3.18.6\arch\x86\kernel\ process_32.c文件中,我们跳进去看看。进入start_thread()后函数张的样子为:execve -> do_execve() –> do_execve_common() -> exec_binprm()-> search_binary_handler() -> load_elf_binary() -> start_thread()。

start_thread(structpt_regs *regs, unsigned long new_ip, unsigned long new_sp)
{set_user_gs(regs, 0);regs->fs             = 0;regs->ds            = __USER_DS;regs->es            = __USER_DS;regs->ss            = __USER_DS;regs->cs            = __USER_CS;regs->ip             = new_ip;regs->sp            = new_sp;regs->flags                = X86_EFLAGS_IF;/** force it to the iret return path by makingit look as if there was* some work pending.*/set_thread_flag(TIF_NOTIFY_RESUME);
}


pt_regs结构体在在linux-3.18.6\arch\x86\include\asm\ptrace.h中定义:

struct pt_regs {unsignedlong r15;unsignedlong r14;unsignedlong r13;unsignedlong r12;unsignedlong bp;unsignedlong bx;
/* arguments:non interrupts/non tracing syscallsonly save up to here*/unsignedlong r11;unsignedlong r10;unsignedlong r9;unsignedlong r8;unsignedlong ax;unsignedlong cx;unsignedlong dx;unsignedlong si;unsignedlong di;unsignedlong orig_ax;
/* end ofarguments */
/* cpu exceptionframe or undefined */unsignedlong ip;unsignedlong cs;unsignedlong flags;unsignedlong sp;unsignedlong ss;
/* top of stackpage */
};


进程执行execve系统调用,CPU往进程的内核堆栈压入了很多寄存器值。struct pt_regs表示进程内核堆栈的系统调用时SAVE_ALL宏(传送门:第一讲)压入内核栈的部分。

egs->ip     = new_ip;


从start_thread()的实参可以得知new_ip的值是我们新加载的可执行文件的elf_entry的位置,也就是ELF文件中main函数的位置。egs->ip          = new_ip;把ELF文件中定义的main函数起始地址赋值给eip寄存器,进程返回到用户态时的执行位置从原来的int 0x80的下一条指令变成了new_ip的位置。

regs->sp            = new_sp;


修改内核堆栈的栈顶指针。

当系统调用返回后,CPU拿到新的ip指针和新的用户态堆栈,新的用户态堆栈中包含新程序的命令行参数和shell上下文环境,就可以放心的执行新程序啦~

 

总结execve系统调用的过程:

1.      execve系统调用陷入内核,并传入命令行参数和shell上下文环境

2.      execve陷入内核的第一个函数:do_execve,do_execve封装命令行参数和shell上下文

3.      do_execve调用do_execve_common,do_execve_common打开ELF文件并把所有的信息一股脑的装入linux_binprm结构体

4.      do_execve_common中调用search_binary_handler,寻找解析ELF文件的函数

5.      search_binary_handler找到ELF文件解析函数load_elf_binary

6.      load_elf_binary解析ELF文件,把ELF文件装入内存,修改进程的用户态堆栈(主要是把命令行参数和shell上下文加入到用户态堆栈),修改进程的数据段代码段

7.      load_elf_binary调用start_thread修改进程内核堆栈(特别是内核堆栈的ip指针)

8.      进程从execve返回到用户态后ip指向ELF文件的main函数地址,用户态堆栈中包含了命令行参数和shell上下文环境


http://chatgpt.dhexx.cn/article/kpisDbVl.shtml

相关文章

Linux系统编程(再论execve)

文章目录 前言一、execve的第三个参数二、进程空间三、命令行参数规范四、optstring规则的扩展定义总结 前言 本篇文章我们继续来研究一下execve这个系统调用&#xff0c;上篇文章已经讲解了前两个参数的意义了&#xff0c;那么这篇文章就来讲解一下第三个参数的具体含义。 一…

Linux0.11 execve函数(六)

系列文章目录 Linux 0.11启动过程分析&#xff08;一&#xff09; Linux 0.11 fork 函数&#xff08;二&#xff09; Linux0.11 缺页处理&#xff08;三&#xff09; Linux0.11 根文件系统挂载&#xff08;四&#xff09; Linux0.11 文件打开open函数&#xff08;五&#xff09…

C语言 execve()函数使用方法

1.君の名は execve() – 叫做执行程序函数 就像Python中的os.system(cmd)这个函数&#xff0c;我们可以用这个函数来执行我们的shell脚本&#xff0c;单独的shell命令&#xff0c;或者是调用其他的程序&#xff0c;我们的execve()这个函数就和Python中的os.system函数类似&am…

SPSS-因子分析

因子分析 有可能用较少的综合指标分析存在于各变量中的各类信息&#xff0c;而各综合指标之间彼此是不相关的&#xff0c;代表各类信息的综合指标称为因子。定义&#xff1a;因子分析就是用少数几个因子来描述许多指标或因素之间的联系&#xff0c;以较少几个因子反映原资料的 …

spss进行主成分分析

什么是主成分分析 简而概之, 就是一组数据受太多因素影响, 选出几个能代表他们的因素,并进行线性组合得到一组比原维度小的因素组合, 作为新的因素集 用spss操作 随手拿出一组数据 1.数据统一标准化 因为我们得到的原始数据大小,类型不一, 一起分析会不准确, 所以将数据全部…

实用干货!因子分析超全步骤总结

因子分析是统计数据分析方法之一&#xff0c;因子分析包括探索性因子分析和验证性因子分析。本文主要讨论探索性因子分析。 一、研究背景 关于工作满意度有14个问题&#xff0c;调研得到215份问卷结果。希望通过因子分析&#xff0c;用少量因子反映14个题目的信息&#xff0c;…

NLP | 朴素贝叶斯法的学习与分类

朴素贝叶斯法的学习与分类 《统计学习方法》李航第四章 1、概述 书上对朴素贝叶斯的描述如下&#xff1a; 朴素贝叶斯法时基于贝叶斯定理与特征条件独立假设的分类方法。对于给定的训练数据集&#xff0c;首先基于特征条件独立假设学习输入/输出的联合概率分布&#xff1b;然…

标定相机参数-张正友方法

一、实验原理 1.计算外参 设三维世界坐标的点为M=[X,Y,Z,1]T,二维相机平面像素坐标为m=[u,v,1]T,所以标定用的棋盘格平面到图像平面的单应性关系为: sm=A[R,t]M 其中 不妨设棋盘格位于Z = 0,定义旋转矩阵R的第i列为 ri, 则有: 令H=[h1 h2 h3]=λA[r1 r2 t] 于是空间到图…

《统计学习方法》——朴素贝叶斯法

引言 朴素贝叶斯法(Naive Bayes)是基于贝叶斯定理与特征条件独立假设的分类方法。朴素贝叶斯法实现简单&#xff0c;学习与预测的效率都很高&#xff0c;是一种常用的方法。 这一章需要大量的概率论知识&#xff0c;忘记了的同学建议先参阅人工智能数学基础之概率论。 朴素贝…

数据挖掘十大算法之 naïve Bayes

朴素贝叶斯法是基于贝叶斯定理和特征条件独立假设的分类方法。朴素贝叶斯法实现简单&#xff0c;学习与预测的效率都很高&#xff0c;被广泛应用于文本分类、垃圾邮件过滤、自然语言处理等场景。下面我们来介绍贝叶斯定理&#xff0c;在介绍贝叶斯定理之前&#xff0c;先介绍下…

专题:深度神经网络基本问题的原理详细分析和推导

文章目录 **写在最前面****1 神经网络算法的直观了解****1.1 神经网络过程描述**&#xff1a;**1.2 神经网络相关的几个问题****1.2.1 表征假设和激活函数** **1.2.2 结构设计(Architecture Design)****1.2.3 代价函数(Cost Function)和优化目标(Optimization objective)****1.…

第四章 朴素贝叶斯法

文章目录 朴素贝叶斯法的学习与分类基本方法数据定义学习联合概率分布如何求出条件概率分布&#xff1f;如何分类&#xff1f; 后验概率最大化的含义 朴素贝叶斯的参数估计法极大似然估计学习分类算法贝叶斯估计 朴素贝叶斯法&#xff08;与贝叶斯估计是不同的概念&#xff09;…

GAN生成对抗式神经网络数学推导

由上面一篇文章我们已经知道了&#xff0c;如果我们从真实数据分布里面取n个样本&#xff0c;根据给定样本我们可以列出其出现概率的表达式&#xff0c;那么生成这N个样本数据的似然(likelihood)就是 l ( θ ) ∏ i 1 N p ( x i ∣ θ ) l ( \theta ) \prod _ { i 1 } ^ { …

《统计学习方法》学习笔记(三)之 朴素贝叶斯法

朴素贝叶斯法 总述 朴素贝叶斯法是基于贝叶斯定理与特征条件独立性假设的分类方法。对于给定的训练数据集&#xff0c;首先基于特征独立性假设学习输入/输出的联合概率分布&#xff1b;然后基于此模型&#xff0c;对给定的输入 x x x&#xff0c;利用贝叶斯定理求出后验概率最…

朴素贝叶斯(二)|极大似然估计+学习与分类算法+贝叶斯估计| 《统计学习方法》学习笔记(十六)

朴素贝叶斯法的参数估计 1. 极大似然估计 在朴素贝叶斯法中&#xff0c;学习意味着估计 P ( Y c k ) P(Yc_k) P(Yck​)和 P ( X ( j ) x ( j ) ∣ Y c k ) P(X^{(j)}x^{(j)}|Yc_k) P(X(j)x(j)∣Yck​)。可以应用极大似然估计法估计相应的概率。先验概率 P ( Y c k ) P(Yc…

一文看懂 “极大似然估计” 与 “最大后验估计” —— 最大后验估计篇

本文历次修订后全长 2万8000余字&#xff0c;受到 CSDN 博文字数限制&#xff0c;故切分两篇发布&#xff0c;所以现在是两文看懂了… 前篇介绍参数估计背景和极大似然估计&#xff1b;本篇介绍最大后验估计和两种方法对比请务必先看前文&#xff1a;一文看懂 “极大似然估计”…

【生成模型】极大似然估计,你必须掌握的概率模型

上一期为大家说明了什么是无监督生成模型。在无监督生成模型中&#xff0c;极大似然法一直扮演着非常核心的位置&#xff0c;我们必须对它有深刻的理解&#xff0c;本期小米粥将为大家讲一下极大似然法的那些事情。 作者&编辑 | 小米粥 1 一个小游戏&#xff1a;取球猜概率…

透彻理解机器学习中极大似然估计MLE的原理(附3D可视化代码)

文章目录 相关资料一、什么是概率&#xff0c;什么是似然二、极大似然估计 Maximum Likelihood Estimation (MLE) 的含义2.1 机器学习中的极大化似然函数2.2 极大似然估计和损失函数的关系VAE最大化似然函数推导出损失函数 三、代码可视化&#xff1a;极大似然估计3.1 似然函数…

C#RSA密码以及利用欧几里得算法实现两数互质的判断

最近做课程设计,想到以前看过RSA密码的相关内容&#xff0c;于是就想用刚学的C#做一个数字加密系统。RSA加密的流程如下&#xff1a; 来看一个“玩具式”的例子&#xff1a; (1)选取两个素数p2,q11,于是N22. (2)构造数,这是小于22且不含因数2和11的自然数的个数。 (3)选一个…

判断两数互质,java实现

数组下标i和j值互质时&#xff0c;a[i][j] true,反之false Write a program to create an n * n Boolean array. If I and j are coprime, a [i] [J] is true, otherwise it is false /** * When Array index Mutuality ,a[i][j] true,else is false * 数组i和j值互质时&…