通过do_execve源码分析程序的执行(上)(基于linux0.11)

2024-03-27 21:38

本文主要是介绍通过do_execve源码分析程序的执行(上)(基于linux0.11),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

execve函数是操作系统非常重要的一个函数,他使得程序变成进程成为可能。下面我们通过do_execve的实现,了解一下程序变成进程的过程。首先do_execve是一个系统调用。之前分析过系统调用的过程。这里就不详细说了。直接从sys_execve函数开始。

_sys_execve:lea EIP(%esp),%eaxpushl %eaxcall _do_execveaddl $4,%espret

执行_do_execve函数前,先看看这时候的内核栈。
在这里插入图片描述
下面开始分析do_execve的实现。

int do_execve(unsigned long * eip,long tmp,char * filename,char ** argv, char ** envp)
{struct m_inode * inode;struct buffer_head * bh;struct exec ex;unsigned long page[MAX_ARG_PAGES];int i,argc,envc;int e_uid, e_gid;int retval;int sh_bang = 0;unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4;// eip指向系统调用前的eip,eip[1]则指向cs,判断一下这时候的cs是不是用户的csif ((0xffff & eip[1]) != 0x000f)panic("execve called from supervisor mode");for (i=0 ; i<MAX_ARG_PAGES ; i++)	/* clear page-table */page[i]=0;// 通过文件名找到可执行文件if (!(inode=namei(filename)))		/* get executables inode */return -ENOENT;// 计算环境变量和参数个数argc = count(argv);envc = count(envp);restart_interp:if (!S_ISREG(inode->i_mode)) {	/* must be regular file */retval = -EACCES;goto exec_error2;}i = inode->i_mode;// 设置了uid则执行的时候uid是设置的uid,否则是用户的有效ide_uid = (i & S_ISUID) ? inode->i_uid : current->euid;e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;// 相等说明该文件是该用户创建的,则判断user位的权限if (current->euid == inode->i_uid)i >>= 6;// 同上,判断组权限else if (current->egid == inode->i_gid)i >>= 3;/*else 判断 other的权限*/if (!(i & 1) &&!((inode->i_mode & 0111) && suser())) {retval = -ENOEXEC;goto exec_error2;}// 读第一块数据进来if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) {retval = -EACCES;goto exec_error2;}// 前面是执行文件的头,包括一些元数据ex = *((struct exec *) bh->b_data);	/* read exec-header */// 是脚脚本文件,不是编译后的文件,sh_bang控制只会进入一次if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {/** This section does the #! interpretation.* Sorta complicated, but hopefully it will work.  -TYT*/char buf[1023], *cp, *interp, *i_name, *i_arg;unsigned long old_fs;// 把#!之外的字符复制到bufstrncpy(buf, bh->b_data+2, 1022);brelse(bh);iput(inode);buf[1022] = '\0';// 找出buf里第一次出现换行字符的地址,没有则返回NULLif (cp = strchr(buf, '\n')) {// 更新换行字符为\0,表示字符串结束*cp = '\0';// cp指向文件的第一个字符for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);}if (!cp || *cp == '\0') {retval = -ENOEXEC; /* No interpreter name found */goto exec_error1;}// 开始找出解释器名字interp = i_name = cp;i_arg = 0;// interp指向解释器路径的第一个字符,iname指向解释器名称for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {if (*cp == '/')i_name = cp+1;}// 遇到空格或制表符结束的,则修改他的值为\0if (*cp) {*cp++ = '\0';// i_arg指向解释器名称的字符,即参数列表i_arg = cp;}/** OK, we've parsed out the interpreter name and* (optional) argument.*/// sh_bang初始值是0,加一,用作下面代码判断的标记,见下一个sh_bang变量if (sh_bang++ == 0) {p = copy_strings(envc, envp, page, p, 0);p = copy_strings(--argc, argv+1, page, p, 0);}/** Splice in (1) the interpreter's name for argv[0]*           (2) (optional) argument to interpreter*           (3) filename of shell script** This is done in reverse order, because of how the* user environment and arguments are stored.*/// 脚本的名字p = copy_strings(1, &filename, page, p, 1);argc++;// 解释器的参数列表if (i_arg) {p = copy_strings(1, &i_arg, page, p, 2);argc++;}// 解释器名字p = copy_strings(1, &i_name, page, p, 2);argc++;if (!p) {retval = -ENOMEM;goto exec_error1;}/** OK, now restart the process with the interpreter's inode.*/old_fs = get_fs();set_fs(get_ds());if (!(inode=namei(interp))) { /* get executables inode */set_fs(old_fs);retval = -ENOENT;goto exec_error1;}set_fs(old_fs);// 复制完,加载解释器的可执行文件goto restart_interp;}brelse(bh);if (N_MAGIC(ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||ex.a_text+ex.a_data+ex.a_bss>0x3000000 ||inode->i_size < ex.a_text+ex.a_data+ex.a_syms+N_TXTOFF(ex)) {retval = -ENOEXEC;goto exec_error2;}if (N_TXTOFF(ex) != BLOCK_SIZE) {printk("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);retval = -ENOEXEC;goto exec_error2;}// 不是脚本文件if (!sh_bang) {p = copy_strings(envc,envp,page,p,0);p = copy_strings(argc,argv,page,p,0);// 数据太多,超过限制if (!p) {retval = -ENOMEM;goto exec_error2;}}
/* OK, This is the point of no return */// 替换该字段的值if (current->executable)iput(current->executable);current->executable = inode;// 清除信号处理函数for (i=0 ; i<32 ; i++)current->sigaction[i].sa_handler = NULL;// 设置了close_on_exec的则关闭对应的文件for (i=0 ; i<NR_OPEN ; i++)if ((current->close_on_exec>>i)&1)sys_close(i);// 清0current->close_on_exec = 0;// 释放代码段和数据段的页表以及物理页free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));free_page_tables(get_base(current->ldt[2]),get_limit(0x17));if (last_task_used_math == current)last_task_used_math = NULL;current->used_math = 0;// change_ldt返回数据段的最大长度,减去MAX_ARG_PAGES*PAGE_SIZE,得到page的线性地址,加p得到p的线性地址,p是page里的偏移p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;// 复制参数和环境变量到新的地址,栈往大地址增长,p的值变大p = (unsigned long) create_tables((char *)p,argc,envc);// 代码、数据、bss段上面是堆指针current->brk = ex.a_bss +(current->end_data = ex.a_data +(current->end_code = ex.a_text));// p按4kb对齐成为栈指针,栈里面现在是环境变量列表和参数列表current->start_stack = p & 0xfffff000;// 进程的权限,setuid的时候,权限等于可执行文件拥有者的current->euid = e_uid;current->egid = e_gid;i = ex.a_text+ex.a_data;// 如果代码段和数据段的长度不是4kb的倍数(即长度的低12位有值),则把没值的部分填充0while (i&0xfff)put_fs_byte(0,(char *) (i++));// 设置eip的值,返回后从这开始执行eip[0] = ex.a_entry;		/* eip, magic happens :-) */// p成为栈指针即espeip[3] = p;			/* stack pointer */return 0;
exec_error2:iput(inode);
exec_error1:for (i=0 ; i<MAX_ARG_PAGES ; i++)free_page(page[i]);return(retval);
}

加载可执行文件的时候,分为两种,第一个是编译好的二进制文件,第二种是脚本文件。脚本文件的话,会加载对应的解释器。这里只解释编译好的二进制文件。跟着上面的代码。我们按照分步解析重点的地方。
1 判断文件是否可执行和当前进程是否有权限。通过的话,加载可执行文件的第一块数据进来,只需要加载头文件就行。不需要加载具体的代码。等真正执行的时候会发送缺页中断,那时候再加载就行。

2 申请物理内存,保存环境变量和参数。具体实现在copy_string函数。

/** 'copy_string()' copies argument/envelope strings from user* memory to free pages in kernel mem. These are in a format ready* to be put directly into the top of new user memory.** Modified by TYT, 11/24/91 to add the from_kmem argument, which specifies* whether the string and the string array are from user or kernel segments:* * from_kmem     argv *        argv ***    0          user space    user space*    1          kernel space  user space*    2          kernel space  kernel space* * We do this by playing games with the fs segment register.  Since it* it is expensive to load a segment register, we try to avoid calling* set_fs() unless we absolutely have to.*/
static unsigned long copy_strings(int argc,char ** argv,unsigned long *page,unsigned long p, int from_kmem)
{char *tmp, *pag;int len, offset = 0;unsigned long old_fs, new_fs;if (!p)return 0;	/* bullet-proofing */new_fs = get_ds();old_fs = get_fs();if (from_kmem==2)set_fs(new_fs);// 每个循环复制一个字符串while (argc-- > 0) {if (from_kmem == 1)set_fs(new_fs);// tmp指向最后一行的首地址,但是转成一级指针看起来似乎有问题if (!(tmp = (char *)get_fs_long(((unsigned long *)argv)+argc)))panic("argc is wrong");if (from_kmem == 1)set_fs(old_fs);len=0;		/* remember zero-padding */// 先len++表示最后的\0,然后每次循环加一,遇到\0就退出循环,不需要加一了do {len++;} while (get_fs_byte(tmp++));// 没有空间了,p从最大空间开始减if (p-len < 0) {	/* this shouldn't happen - 128kB */set_fs(old_fs);return 0;}while (len) {// 复制全部数据过程中,每复制一个字节p减一,tmp减一代表从字符串的后面往前面复制。--p; --tmp; --len;// offset是页内偏移,p是整个page数组内的偏移,p初始化的时候是4096-4,后面都是4095if (--offset < 0) {offset = p % PAGE_SIZE;if (from_kmem==2)set_fs(old_fs);/*从后往前复制,page的当前最后一个元素是否分配了对应的内存,没有分配的话,给分配一页,如果数据少,可能只需要分配一页就够了*/if (!(pag = (char *) page[p/PAGE_SIZE]) &&!(pag = (char *) page[p/PAGE_SIZE] =(unsigned long *) get_free_page())) return 0;if (from_kmem==2)set_fs(new_fs);}// 从后往前复制*(pag + offset) = get_fs_byte(tmp);}}if (from_kmem==2)set_fs(old_fs);return p;
}

该函数执行后的结构如下:
在这里插入图片描述
3 执行下面代码释放原来进程的页目录和页表项信息,解除到物理地址的映射。这些信息是通过fork继承下来的。

// 释放代码段和数据段的页表以及物理页free_page_tables(get_base(current->ldt[1]),get_limit(0x0f));free_page_tables(get_base(current->ldt[2]),get_limit(0x17));

4 调用change_ldt函数设置代码段、数据段的基地址和限长。数据段限长是64MB,代码段限长是根据执行文件头信息得到的。然后把page数组保存的物理地址信息映射到数据段的线性地址。

static unsigned long change_ldt(unsigned long text_size,unsigned long * page)
{unsigned long code_limit,data_limit,code_base,data_base;int i;// 不够一页则占一页code_limit = text_size+PAGE_SIZE -1;// 4kb对齐code_limit &= 0xFFFFF000;// 64MBdata_limit = 0x4000000;// 代码段和数据段的基地址是一样的,见fork.c的copy_memcode_base = get_base(current->ldt[1]);data_base = code_base;// 基地址和fork的时候是一样的,limit变了set_base(current->ldt[1],code_base);// 代码段的长度就是limitset_limit(current->ldt[1],code_limit);set_base(current->ldt[2],data_base);set_limit(current->ldt[2],data_limit);
/* make sure fs points to the NEW data segment */// 17是选择子,即0x10001,ldt的第三项__asm__("pushl $0x17\n\tpop %%fs"::);// 指向数据段最后一页的末尾data_base += data_limit;for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--) {// 减去一页,指向数据段最后一页的首地址data_base -= PAGE_SIZE;/*如果page[i]已经指向了物理地址,则建立线性地址和物理地址的映射,比如环境变量和参数把page的最后一个元素映射到数据段的最后一页,倒数第二个元素映射到数据段倒数第二页,以此类推*/if (page[i])put_page(page[i],data_base);}return data_limit;
}

执行完该函数后的内存布局如下。
在这里插入图片描述
5 调用create_tables函数。分配执行环境变量和参数的数组,copy_string只是保存了环境和参数的内容。

static unsigned long * create_tables(char * p,int argc,int envc)
{unsigned long *argv,*envp;unsigned long * sp;// 四个字节对齐sp = (unsigned long *) (0xfffffffc & (unsigned long) p);sp -= envc+1;envp = sp;sp -= argc+1;argv = sp;put_fs_long((unsigned long)envp,--sp);put_fs_long((unsigned long)argv,--sp);put_fs_long((unsigned long)argc,--sp);// 复制参数到新的地址中while (argc-->0) {// p指向参数列表的第一个元素的地址,把这个地址存到新地址argv中put_fs_long((unsigned long) p,argv++);// 非空说明是参数的内容,为空,说明p++是下一个元素的地址while (get_fs_byte(p++)) /* nothing */ ;}// 复制NULL给最后一个元素put_fs_long(0,argv);// 同上while (envc-->0) {put_fs_long((unsigned long) p,envp++);while (get_fs_byte(p++)) /* nothing */ ;}put_fs_long(0,envp);return sp;
}

执行完的内存布局:
在这里插入图片描述
6 设置栈、堆的位置。堆位置在代码段、数据段、bss段上面。栈在p的位置(4kb对齐)。最后设置eip为执行文件头里指定的值。esp为当前的栈位置。

// 代码、数据、bss段上面是堆指针current->brk = ex.a_bss +(current->end_data = ex.a_data +(current->end_code = ex.a_text));// p按4kb对齐成为栈指针,栈里面现在是环境变量列表和参数列表current->start_stack = p & 0xfffff000;// 进程的权限,setuid的时候,权限等于可执行文件拥有者的current->euid = e_uid;current->egid = e_gid;i = ex.a_text+ex.a_data;// 如果代码段和数据段的长度不是4kb的倍数(即长度的低12位有值),则把没值的部分填充0while (i&0xfff)put_fs_byte(0,(char *) (i++));// 设置eip的值,返回后从这开始执行eip[0] = ex.a_entry;		/* eip, magic happens :-) */// p成为栈指针即espeip[3] = p;			/* stack pointer */

至此加载可执行文件的分析就完毕了。下篇分析执行第一条指令后会发生什么。

这篇关于通过do_execve源码分析程序的执行(上)(基于linux0.11)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/853470

相关文章

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

maven 编译构建可以执行的jar包

💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」👈,「stormsha的知识库」👈持续学习,不断总结,共同进步,为了踏实,做好当下事儿~ 专栏导航 Python系列: Python面试题合集,剑指大厂Git系列: Git操作技巧GO

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

kubelet组件的启动流程源码分析

概述 摘要: 本文将总结kubelet的作用以及原理,在有一定基础认识的前提下,通过阅读kubelet源码,对kubelet组件的启动流程进行分析。 正文 kubelet的作用 这里对kubelet的作用做一个简单总结。 节点管理 节点的注册 节点状态更新 容器管理(pod生命周期管理) 监听apiserver的容器事件 容器的创建、删除(CRI) 容器的网络的创建与删除

jenkins 插件执行shell命令时,提示“Command not found”处理方法

首先提示找不到“Command not found,可能我们第一反应是查看目标机器是否已支持该命令,不过如果相信能找到这里来的朋友估计遇到的跟我一样,其实目标机器是没有问题的通过一些远程工具执行shell命令是可以执行。奇怪的就是通过jenkinsSSH插件无法执行,经一番折腾各种搜索发现是jenkins没有加载/etc/profile导致。 【解决办法】: 需要在jenkins调用shell脚

red5-server源码

red5-server源码:https://github.com/Red5/red5-server