百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Linux 可执行文件程序载入和执行过程

ahcoder 2024-12-20 13:17 21 浏览

今天分析下 Linux 下一个可执行文件是怎么载入和执行的。


Linux 下标准的可执行文件格式是 ELF。


ELF (Executable and Linking Format) 是一种对象文件的格式。


在 linux 系统中,一个ELF文件主要用来表示3种类型的文件


  • 可执行文件 : 被操作系统中的加载器从硬盘中读取,加载到内存中去执行。
  • 目标文件(.o) :被链接器读取,用来产生一个可执行文件或者共享文件。
  • 共享文件(.so) :在动态链接的时候,由 ld-linux.so 来读取。


今天分析一下可执行文件类型。


当我们在 Linux 下的 bash 下输入一个命令执行可执行程序时,bash 进程会调用 fork() 创建一个新的进程,然后新的进程调用 execve() 系统调用来执行指定的可执行程序。


原先的 bash 进程继续返回等待刚才启动的新进程结束,然后继续等待用户的输入。


execve() 系统调用原型如下:


int execve(const char *filename, char *const argv[], char *const envp[]);

他们的三个参数分别是被执行的程序文件名、执行参数和环境变量。


当调用 execve() 系统调用时,进入内核调用过程如下

sys_execve()
--> do_execve() // 主要根据可执行文件进行构造 linux_binprm 内核结构,该结构记录可执行文件信息,然后从formats链表中找到执行该执行文件的方法
--> load_elf_binary

do_execve 主要完成 linux_binprm 内核结构的初始化,该结构定义如下:


struct linux_binprm{
char buf[128];
/*与可执行文件路径名的处理一样,每个参数的最大长度定为一个物理页,所以设置为一个页面指针数组,最大个数为32*/
unsigned long page[MAX_ARG_PAGES];
unsigned long p;
int sh_bang; //可执行文件的性质,当时shell脚本时为1
struct inode * inode; //可执行文件的inode
int e_uid, e_gid; //可执行文件的属性
int argc, envc; //命令行参数和环境变量数目
char * filename; //可执行文件的路径名 
};

该结构主要记录了执行可执行文件所有需要的信息。


其中 page 表示的是存放参数的页面数组,而 p 表示的是在这些数组的顶部,因为这些字符串是按照栈的方式存放的,也就是说,先分配地址更高的数组,向低地址方向增长,p 就指向栈顶部。


结构如下图



最终执行可执行文件的接口为 load_elf_binary。


具体实现如下:


static int load_elf_binary(struct linux_binprm * bprm, struct pt_regs * regs)
{
struct elfhdr elf_ex;
struct elfhdr interp_elf_ex;
struct file * file;
...

status = 0;
load_addr = 0;
elf_ex = *((struct elfhdr *) bprm->buf); /* exec-header */


//比对四个字符,必须是0x7f、‘E’、‘L’、和‘F’
if (elf_ex.e_ident[0] != 0x7f || strncmp(&elf_ex.e_ident[1], "ELF",3) != 0)
return -ENOEXEC;


//映像类型必须是ET_EXEC
if(elf_ex.e_type != ET_EXEC || (elf_ex.e_machine != EM_386 && elf_ex.e_machine != EM_486) || (!bprm->inode->i_op || !bprm->inode->i_op->default_file_ops || !bprm->inode->i_op->default_file_ops->mmap)){
return -ENOEXEC;
};


elf_phdata = (struct elf_phdr *) kmalloc(elf_ex.e_phentsize * elf_ex.e_phnum, GFP_KERNEL);

old_fs = get_fs();
set_fs(get_ds());
//获取所有程序头表信息
retval = read_exec(bprm->inode, elf_ex.e_phoff, (char *) elf_phdata, elf_ex.e_phentsize * elf_ex.e_phnum);
set_fs(old_fs);

elf_ppnt = elf_phdata;

elf_bss = 0;
elf_brk = 0;

elf_exec_fileno = open_inode(bprm->inode, O_RDONLY);



file = current->files->fd[elf_exec_fileno];

elf_stack = 0xffffffff;
elf_interpreter = NULL;
start_code = 0;
end_code = 0;
end_data = 0;

old_fs = get_fs();
set_fs(get_ds());

/* 处理解释器段,通过遍历每个段,找到PT_INTERP类型段,也即是解释器段,找到说明需要运行过程中的动态链接。
“解释器”段实际上只是一个字符串,即解释器的文件名,如”/lib/ld-linux.so.2”, 或者64位机器上对应的叫做”/lib64/ld-linux-x86-64.so.2”
通过命令 readelf -l 可执行文件 获取解释器段信息 type 类型为 INTERP
*/ 
for(i=0;i < elf_ex.e_phnum; i++){
//检查是否有需要加载的解释器
if(elf_ppnt->p_type == PT_INTERP) { // 该类型表示动态连接器


elf_interpreter = (char *) kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);
//根据其位置的p_offset和大小p_filesz把整个"解释器"段的内容读入缓冲区;
//从用户程序的program header 中读取动态链接器的路径,比如 /lib64/ld-linux-x86-64.so
retval = read_exec(bprm->inode,elf_ppnt->p_offset,elf_interpreter, elf_ppnt->p_filesz);

if(retval >= 0) //获取连接器的inod
retval = namei(elf_interpreter, &interpreter_inode);
if(retval >= 0) //解释器也是一个elf格式的程序,读入解释器的前128个字节,即解释器映像的头部
retval = read_exec(interpreter_inode,0,bprm->buf,128);

if(retval >= 0){
interp_ex = *((struct exec *) bprm->buf); /* exec-header */
interp_elf_ex = *((struct elfhdr *) bprm->buf); /* exec-header */

};

};
elf_ppnt++;
};

set_fs(old_fs);


//检查并读取解释器(也可以叫动态链接器)的程序头表
if(elf_interpreter){
...
}


if (!bprm->sh_bang) {
...
}

//在此清除掉了父进程的所有相关代码 
flush_old_exec(bprm);

current->mm->end_data = 0;
current->mm->end_code = 0;
current->mm->start_mmap = ELF_START_MMAP;
current->mm->mmap = NULL;
elf_entry = (unsigned int) elf_ex.e_entry;

current->mm->rss = 0;
//建立环境变量参数的页表映射,从虚拟地址 0xC0000000UL 处开始
bprm->p += setup_arg_pages(0, bprm->page);
current->mm->start_stack = bprm->p;


old_fs = get_fs();
set_fs(get_ds());

elf_ppnt = elf_phdata;
for(i=0;i < elf_ex.e_phnum; i++){

if(elf_ppnt->p_type == PT_INTERP) {

set_fs(old_fs);
//不装入解释器,那么这个入口地址就是目标映像本身的入口地址
if(interpreter_type & 1) 
elf_entry = load_aout_interp(&interp_ex, interpreter_inode); //加载text data bss段

//如果需要装入解释器,就通过load_elf_interp装入其映像, 并把将来进入用户空间的入口地址设置成load_elf_interp()的返回值,即解释器映像的入口地址
if(interpreter_type & 2) 
elf_entry = load_elf_interp(&interp_elf_ex, interpreter_inode);

old_fs = get_fs();
set_fs(get_ds());

iput(interpreter_inode);
kfree(elf_interpreter);

if(elf_entry == 0xffffffff) { 
...
};
};

// 类型为 PT_LOAD 需要映射到进程的地址虚拟空间的
if(elf_ppnt->p_type == PT_LOAD) {
error = do_mmap(file, elf_ppnt->p_vaddr & 0xfffff000, elf_ppnt->p_filesz + (elf_ppnt->p_vaddr & 0xfff),
PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE, elf_ppnt->p_offset & 0xfffff000);

#ifdef LOW_ELF_STACK
if(elf_ppnt->p_vaddr & 0xfffff000 < elf_stack) 
elf_stack = elf_ppnt->p_vaddr & 0xfffff000;
#endif

if(!load_addr) 
load_addr = elf_ppnt->p_vaddr - elf_ppnt->p_offset;
k = elf_ppnt->p_vaddr;
if(k > start_code) start_code = k;
k = elf_ppnt->p_vaddr + elf_ppnt->p_filesz;
if(k > elf_bss) elf_bss = k;
if((elf_ppnt->p_flags | PROT_WRITE) && end_code < k)
end_code = k; 
if(end_data < k) end_data = k; 
k = elf_ppnt->p_vaddr + elf_ppnt->p_memsz;
if(k > elf_brk) elf_brk = k; 
};
elf_ppnt++;
};
set_fs(old_fs);

kfree(elf_phdata);

if(interpreter_type != INTERPRETER_AOUT) sys_close(elf_exec_fileno);

...

current->executable = bprm->inode;
bprm->inode->i_count++;
#ifdef LOW_ELF_STACK
current->start_stack = p = elf_stack - 4;
#endif
bprm->p -= MAX_ARG_PAGES*PAGE_SIZE;
/*
create_elf_tables填写目标文件的参数环境变量等必要信息
在完成装入,启动用户空间的映像运行之前,还需要为目标映像和解释器准备好一些有关的信息,这些信息包括常规的argc、envc等等,还有一些"辅助向量(Auxiliary Vector)"。
这些信息需要复制到用户空间,使它们在CPU进入解释器或目标映像的程序入口时出现在用户空间堆栈上。这里的create_elf_tables()就起着这个作用。
*/
bprm->p = (unsigned long)create_elf_tables((char *)bprm->p, bprm->argc, bprm->envc, 
(interpreter_type == INTERPRETER_ELF ? &elf_ex : NULL), load_addr, 
(interpreter_type == INTERPRETER_AOUT ? 0 : 1));

if(interpreter_type == INTERPRETER_AOUT)
current->mm->arg_start += strlen(passed_fileno) + 1;

//调整内存映射内容
current->mm->start_brk = current->mm->brk = elf_brk;
current->mm->end_code = end_code;
current->mm->start_code = start_code;
current->mm->end_data = end_data;
current->mm->start_stack = bprm->p;
current->suid = current->euid = bprm->e_uid;
current->sgid = current->egid = bprm->e_gid;


current->mm->brk = (elf_bss + 0xfff) & 0xfffff000;
sys_brk((elf_brk + 0xfff) & 0xfffff000);

padzero(elf_bss);

////eip和esp改成新的地址,就使得CPU在返回用户空间时就进入新的程序入口
start_thread(regs, elf_entry, bprm->p);
if (current->flags & PF_PTRACED)
send_sig(SIGTRAP, current, 0);
MOD_DEC_USE_COUNT;
return 0;
}



static inline void start_thread(struct pt_regs * regs, unsigned long eip, unsigned long esp)
{
regs->cs = USER_CS;
regs->ds = regs->es = regs->ss = regs->fs = regs->gs = USER_DS;
regs->eip = eip;
regs->esp = esp;
}


该函数主要完成的功能如下:

  • 检查 ELF 可执行文件的有效性,比如魔数、程序头表中段的数量。
  • 寻找动态链接的 “.interp” 段,设置动态链接器路径。
  • 根据 ELF 可执行文件的程序头表的描述,对ELF文件进行映射,比如代码段,数据段等。
  • 初始化 ELF 进程环境。
  • 将系统调用的返回地址修改为 ELF 可执行文件的入口点,这个入口点取决于程序的链接方式,若是静态链接,则入口地址为 ELF 文件的文件头中 e_entry 所指的地址;对于动态链接的ELF可执行文件,程序入口点是动态链接器。


当 load_elf_binary() 执行完毕,返回至 do_execve() 再返回至 sys_execve() 时,由于上述步骤已经把系统调用的返回地址改成了被装载的ELF可执行程序的入口地址,所以当 sys_execve() 系统调用从内核态返回到用户态时,EIP 寄存器直接跳转到了 LF 程序的入口地址,于是新的程序开始执行,ELF 可执行文件装载完成。

相关推荐

Linux 6.15将更新媒体子系统:高通Iris驱动首次亮相

IT之家3月24日消息,科技媒体phoronix昨日(3月23日)发布博文,报道称Linux6.15内核将于本周启动合并窗口,首批提交的PullRequest请求之一,就...

Ubuntu 25.04发行版登场:Linux 6.14内核,带来多项技术革新

IT之家4月18日消息,科技媒体linuxiac昨日(4月17日)发布博文,报道称代号为PluckyPuffin的Ubuntu25.04发行版正式上线,搭载最新Linu...

WordPress 6.8 版本发布:聚焦性能升级,代号“Cecil”

WordPress6.8版本发布:聚焦性能升级,代号“Cecil”全球最受欢迎的内容管理系统(CMS)WordPress正式推出最新版本6.8,代号“塞西尔”(Cecil),以此致敬传奇钢琴家...

Linus Torvalds接受微软Hyper-V升级 下一代Linux启动会更快

虽然Windows的粉丝和Linux的粉丝经常喜欢进行激烈的键盘大战,但操作系统的制造商们自己也了解彼此的优缺点。毫无疑问,微软也明白这一点,事实上,它甚至鼓励用户尝试Linux,尽管是使用...

Debian 12发布:Linux内核升级6.1(debian更新内核)

IT之家6月11日消息,Debian是最古老的GNU/Linux发行版之一,也是许多其他基于Linux的操作系统的基础,包括Ubuntu、Kali、MX和树莓派OS等。这...

Linux Mint预告新功能:升级Nemo搜索、LMDE 7支持OEM安装

IT之家4月10日消息,LinuxMint发布了最新月度简讯,宣布增强Nemo文件管理器的文件搜索功能、Cinnamon桌面环境开始支持Wayland、LMDE7(LinuxM...

Linux 6.2合并大量网络系统更新:推进Wi-Fi 7和800 Gbps网络

IT之家12月15日消息,Linux6.2合并窗口期内已经确认将会合并大量网络子系统更新。和以往版本相同,Linux6.2内核更新周期在网络功能上有大量的改进,更多的细节可以访问这条...

Linux内核升级实践指南(linux内核怎么升级)

Linux内核升级是一个需要谨慎操作的过程,但掌握正确方法后可以显著提升系统性能、安全性或硬件兼容性。以下是一份详细的实践指南,涵盖主流方法及注意事项:一、准备工作查看当前内核版本bashuname...

Linux又将迎来大版本更新 5.20版可能会被称为Linux 6.0

如果你错过了昨天Linux5.19的发布公告,那么这一消息需要回顾一下:被称为Linux5.20的开发中的内核很可能会被称为Linux6.0。根据LinusTorvalds对现代版本的划分,L...

周六的娱乐就是安装一台OracleLinux虚拟机(一)

每个人有每个人的娱乐方式。老父亲可能喜欢摆弄一些机械工具修修补补。我趁着周六安装一套OracleLinux,寓学于乐。在oracle网站上,下载VirtualBox,现在最新版本是7.0.8。在安装过...

Linux系统下Gaussian 16的安装(linux系统如何安装新软件)

Gaussian是目前使用最多的量子化学计算软件,当前最新的版本为Gaussian16C.01。理论上来说Gaussian并不需要安装,因为高斯一般是不出售源代码的,只有二进制文件,以Gaussi...

「Linux基础」VMWare虚拟机安装CentOS后配置静态ip

在VMWare中安装本地虚拟机CentOS7操作系统,动态IP地址会经常变化,不便于管理与实验。为了便于实验,考虑设置成静态IP地址,目标是本地局域网可以通过NAT网关互相访问,且能连接互联网。由于是...

Win10虚拟机,Hyper-V安装CentOS,一次成功

打开控制面板,选择程序选择启用或关闭Windows功能选中Hyper-V下所有的组件。提示我们需要,重启计算机,保存我们的文件,之后点击“立即重新启动”选择开始菜单,输入Hy,就可以打开虚拟机管理器了...

如何在Windows 10中的Hyper-V虚拟机上安装CentOS Linux

注意双重引导的一种可行且相当不错的替代方法是在虚拟机上安装各种操作系统。Microsoft正式支持CentOS作为Hyper-V的来宾OS,并且在安装时可以很好地集成。自CentOS6.4版以来,用...

安装Linux虚拟机的5个理由,以及不安装的5个原因

虚拟机(VM)允许你在不永久更改计算机的情况下探索Linux。如果你对Linux很好奇,但犹豫是否要尝试,VM提供了一个安全、灵活的解决方案。如果这是你第一次听说虚拟机,虚拟机就像“计算机中的计算机”...