【Linux0.11代码分析】07 之 kernel execve() 函数 实现原理

news/2024/10/24 6:37:45/

【Linux0.11代码分析】07 之 kernel execve 函数 实现原理

  • 一、execve()函数实现原理
    • 1.1 execve() 函数调用流程


系列文章如下:

系列文章汇总:《【Linux0.11代码分析】之 系列文章链接汇总(全)》
.
1.《【Linux0.11代码分析】01 之 代码目录分析》
2.《【Linux0.11代码分析】02 之 bootsect.s 启动流程》
3.《【Linux0.11代码分析】03 之 setup.s 启动流程》
4.《【Linux0.11代码分析】04 之 head.s 启动流程》
5.《【Linux0.11代码分析】05 之 kernel 0号进程初始化 init\main.c 代码分析》
6.《【Linux0.11代码分析】06 之 kernel 1号进程 init() 代码分析》
7.《【Linux0.11代码分析】07 之 kernel execve() 函数 实现原理》



在前文《【Linux0.11代码分析】06 之 kernel 1号进程 init() 代码分析》中,我们知道 ,1号进程会创建一个子进程 (2号进程) 来解析/etc/rc 及启动/bin/sh环境,然后通过sh 来执行 /etc/rc 中的内 容,然后子进程 (2号进程) 生命终结; 接着父进程1号进程,再创建另一个子进程(3号进程),打开/dev/tty0 终端,启动/bin/sh,这就是我们登录linuxshell 环境。

代码如下:

    // 5. 使用fork()创建一个子进程,返回值为0 说明当前是子进程,返回值不为0 ,说明当前是父进程// 由子进程来关闭了句柄0(stdin),以只读方式打开/etc/rc 文件,并执行/bin/sh 程序// 父进程则一直wait等待子进程退出if (!(pid = fork ())){close (0);if (open ("/etc/rc", O_RDONLY, 0))_exit (1);		// 如果打开文件失败,则退出(/lib/_exit.c,10)。execve ("/bin/sh", argv_rc, envp_rc);	// 装入/bin/sh 程序并执行。_exit (2);		// 若execve()执行失败则退出(出错码2,“文件或目录不存在”)。}// 6. 父进程运行:wait()是等待子进程停止或终止,其返回值应是子进程的进程号(pid), // &i 是存放返回状态信息的位置, 如果wait()返回值不等于子进程号,则继续等待。if (pid > 0)while (pid != wait (&i))// 7. 在/etc/rc运行完后,再创建子进程3号进程,打开/dev/tty0 终端控制台, 同时复制出1号2号句柄,用作stdin标准输入和stdout标准输出,然后创建/bin/sh环境,这个也就是我们登录`linux` 时的`shell` 环境。while (1){// 创建子进程if ((pid = fork ()) < 0){printf ("Fork failed in init\r\n");continue;}// 当返回的PID == 0 时,子进程运行if (!pid){close (0);		close (1);close (2);setsid ();(void) open ("/dev/tty0", O_RDWR, 0);			// 打开/dev/tty0 终端控制台(void) dup (0);									// 复制出1号句柄,用作stdin标准输入(void) dup (0);									// 复制出2号句柄,用作stdout标准输入_exit (execve ("/bin/sh", argv, envp));			// 创建/bin/sh环境}}

本文,我们来分析分析下 execve 的实现原理:

一、execve()函数实现原理

1.1 execve() 函数调用流程

execve() 函数是通过linux_syscall3(int,execve,const char *,file,char **,argv,char **,envp) 来定义的,

# init\main.c// 读取并执行/etc/rc文件时所使用的命令行参数和环境参数
static char *argv_rc[] ={"/bin/sh", NULL};		// 调用执行程序时参数的字符串数组。
static char *envp_rc[] ={"HOME=/", NULL};		// 调用执行程序时的环境字符串数组。
static char *argv[] ={"-/bin/sh", NULL};		// 同上。
static char *envp[] ={"HOME=/usr/root", NULL};execve("/bin/sh",argv_rc,envp_rc);
_exit(execve("/bin/sh",argv,envp));
----------------------------------------------------------------------------# lib\execve.c
_syscall3(int,execve,const char *,file,char **,argv,char **,envp)#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \							
{ \																	
long __res; \																	
__asm__ volatile ("int $0x80" \													: "=a" (__res) \															: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \		 
if (__res>=0) \return (type) __res; \
errno=-__res; \
return -1; \
}

我们把 _syscall3(int,execve,const char *,file,char **,argv,char **,envp) 宏定义展开,
伪代码形式如下:

  1. 将原有 eax, ebx, ecx, edx 寄存器值全部入栈
  2. 参数分别保存在 ebx、ecx、edx中,系统调用中断号保存在 eax
  3. 通过 int $0x80 触发 80 中断后,会根据 eax 寄存器的系统调用号来判断要调用的中断函数,__NR_execve为中断号11
  4. 函数返回值保存在 eax寄存器中
  5. 通过判断返回值区分函数调用是否成功,如果失败的话将错误返回码保存在 errno
int execve(const char * file, char ** argv, char ** envp)
{	// pushl edxpushl ecxpushl ebxpushl eax// 2. 参数赋值 eax = __NR_execve   	//	eax传递系统调用号	==========>	// include\unistd.h+	#define __NR_execve 11		// `__NR_execve`为中断号`11`ebx = (long) file;	 	// 第一个参数ecx = (long) argv;		// 第二个参数edx = (long) envp;		// 第二个参数// 3. 触发int $0x80 中断,运行汇编sys_execve程序int $0x80// 4. 接收返回值 __res = eax  		// 5. 弹出原寄存器的值 popl eaxpopl ebxpopl ecxpopl edx// 6. 判断是否执行成功if(__res > 0) return int __res;errno = -__res;return -1;
}

系统触发 int $0x80 中断后,其对应的中断函数配置为

# kernel\sched.c
// 调度程序的初始化子程序。
void sched_init (void){set_system_gate (0x80, &system_call);
}对应的汇编代码如下: -----------------------------------
# kernel\system_call.s
.align 2
system_call:cmpl $nr_system_calls-1,%eax 	# 调用号如果超出范围的话就在eax 中置-1 并退出。ja bad_sys_callpush %ds 							# 保存原段寄存器值push %espush %fs// 一个系统调用最多可带有3个参数,也可以不带参数,  ebx 中可存放第1个参数,ecx中存放第2个参数,edx中存放第3个参数pushl %edx		 					# ebx,ecx,edx 中放着系统调用相应的C 语言函数的调用参数。pushl %ecx 							# push %ebx,%ecx,%edx as parameterspushl %ebx 							# to the system callmovl $0x10,%edx 					# set up ds,es to kernel spacemov %dx,%ds 						# ds,es 指向内核数据段(全局描述符表中数据段描述符)。mov %dx,%esmovl $0x17,%edx 					# fs points to local data spacemov %dx,%fs 						# fs 指向局部数据段(局部描述符表中数据段描述符)。call _sys_call_table(,%eax,4)		# 调用地址 = _sys_call_table + %eax * 4  , 我们此处eax=11, 调用sys_execve===============-> // 系统调用指针表  kernel\sched.c+	// 系统调用函数指针表。用于系统调用中断处理程序(int 0x80),作为跳转表。+	fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,+	  sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,+	  sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,+	  sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,+	  sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,+	  sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,+	  sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,+	  sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,+	  sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,+	  sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,=	  sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,+	  sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,+	  sys_setreuid, sys_setregid+	};<-===============pushl %eax 							// 把系统调用返回值入栈movl _current,%eax 					// 取当前任务(进程)数据结构地址 eaxcmpl $0,state(%eax) # state			// 查看当前任务的运行状态。如果不在就绪状态(state 不等于0)就去执行调度程序jne reschedule================>	// kernel\system_call.sreschedule:pushl $ret_from_sys_call # 将ret_from_sys_call 的地址入栈(101 行)。jmp _schedule<================cmpl $0,counter(%eax) # counter		// 如果该任务在就绪状态但counter值等于0,则也去执行调度程序。je rescheduleret_from_sys_call:						// 返回// 首先判别当前任务是否是初始任务task0,如果是则不必对其进行信号量方面的处理,直接返回	movl current,%eax					// task[0] cannot have signalscmpl task,%eaxje 3f                   			// 向前(forward)跳转到标号3处退出中断处理// 通过对原调用程序代码选择符的检查来判断调用程序是否是用户任务。如果不是则直接退出中断。// 这是因为任务在内核态执行时不可抢占。否则对任务进行信号量的识别处理。// 这里比较选择符是否为用户代码段的选择符0x000f(RPL=3,局部表,第一个段(代码段))来判断是否为用户任务。// 如果不是则说明是某个中断服务程序跳转到上面的,于是跳转退出中断程序// 如果原堆栈段选择符不为0x17(即原堆栈不在用户段中),也说明本次系统调用的调用者不是用户任务,则也退出cmpw $0x0f,CS(%esp)				# was old code segment supervisor ?jne 3fcmpw $0x17,OLDSS(%esp)			# was stack segment = 0x17 ?jne 3f// 处理当前任务中的信号。首先取当前任务结构中的信号位图(32位,每位代表1种信号),// 然后用任务结构中的信号阻塞(屏蔽)码,阻塞不允许的信号位,取得数值最小的信号值,// 再把原信号位图中该信号对应的位复位(置0),最后将该信号值作为参数之一调用do_signal().movl signal(%eax),%ebx          # 取信号位图→ebx,1位代表1种信号,共32个信号movl blocked(%eax),%ecx         # 取阻塞(屏蔽)信号位图→ecxnotl %ecx                       # 每位取反andl %ebx,%ecx                  # 获得许可信号位图bsfl %ecx,%ecx                  # 从低位(0)开始扫描位图,看是否有1的位,若有,则ecx保留该位的偏移值je 3f                           # 如果没有信号则向前跳转退出btrl %ecx,%ebx                  # 复位该信号(ebx含有原signal位图)movl %ebx,signal(%eax)          # 重新保存signal位图信息→current->signal.incl %ecx                       # 将信号调整为从1开始的数(1-32)pushl %ecx                      # 信号值入栈作为调用do_signal的参数之一call do_signal                  # 调用C函数信号处理程序(kernel/signal.c)popl %eax                       # 弹出入栈的信号值
3:	popl %eax                       # eax中含有上面入栈系统调用的返回值popl %ebxpopl %ecxpopl %edxpop %fspop %espop %dsiret

可以看出,在汇编中,通过 call _sys_call_table(,%eax,4) 来调用 sys_execve
我们来看看 sys_execve是如何实现的:

# kernel\system_call.s
### 这是sys_execve系统调用。取中断调用程序的代码指针作为参数调用C函数do_execve().
.align 2
sys_execve:lea EIP(%esp),%eax  # eax指向堆栈中保存用户程序的eip指针处(EIP+%esp)pushl %eaxcall do_execveaddl $4,%esp        # 丢弃调用时压入栈的EIP值ret

# fs\exec.c
int do_execve(unsigned long * eip,long tmp,char * filename, char ** argv, char ** envp)




<https://cjdhy.blog.csdn.net/article/details/127785709>


http://www.ppmy.cn/news/63745.html

相关文章

react组件发布到npm

创建库 新建文件夹 test-react-library 进入项目&#xff0c;初始化&#xff1a; yarn init -y此处 -y 就是yes&#xff0c;相当于执行了一路回车 安装依赖包&#xff0c;其中rollup用3以上版本会报错&#xff0c;还没研究处理方法。 yarn add rollup^2.0.0 rollup-plugin…

处理日期和时间的 chrono 库

C11 中提供了日期和时间相关的库 chrono&#xff0c;通过 chrono 库可以很方便地处理日期和时间&#xff0c;为程序的开发提供了便利。chrono 库主要包含三种类型的类&#xff1a;时间间隔duration、时钟clocks、时间点time point。 1. Ratio 时间精度(节拍) std::chrono::ra…

2023零售店铺管理系统最新排名,这5款性价比高!

很多零售店铺的老板&#xff0c;每天都在被开单收银、记账对账、商品销售、销售数据等各种琐事困扰&#xff0c;使用传统的人工管理模式&#xff0c;耗费了大量的时间和成本&#xff0c;也没有达到理想的效果。 其实&#xff0c;零售店铺管理也可以很简单省事&#xff0c;借助零…

Word三线表创建

三线表是论文写作中经常使用到的表格格式 自定义三线表 “插入”-->“表格”&#xff0c;随便插入一个表格&#xff0c;然后将光标移动到表格内 “表设计”-->“其他”-->“新建表格样式” 修改模板名称为“三线表”&#xff0c;方便下次直接套用 首先设置标题行【…

卡尔曼滤波器-公式推导 | 原理分析 | 将卡尔曼滤波器在MatLab中简单实现

目录 1.状态转移2.协方差矩阵3.噪声协方差矩阵的传递4.观测矩阵5.状态更新6.噪声协方差矩阵的更新7.在MatLab中实现卡尔曼滤波器1.状态转移 卡尔曼滤波器又称为最佳线性滤波器。优点有实现简单、纯时域滤波器、不需要进行频域变换等。 假设有一辆汽车在路上行驶,用位置和速度…

C++14:AVL树

由于二叉搜索树在某些特定的情况下会退化成单叉树,为了解决这个问题&#xff0c;保证二叉搜索树能在绝大多数情况下保持高速搜索&#xff0c;G.M. Adelson-Velsky和E.M. Landis这两位俄国数学家提出了AVL树的概念&#xff0c;也就是高度平衡的搜索二叉树。 AVL树平衡大体逻辑&…

.netCHARTING 10.5 dotnetcharting Crack

.net图表 10.5 为柱形图和条形图添加拐角半径控件。 5月 05&#xff0c; 2023 - 16&#xff1a;18新版本 特征 直角或直线组织连接线 - 默认情况下&#xff0c;通过以直角绘制组织连接线来增强组织连接线的显示方式。您可以使用直线选项更改此默认值&#xff0c;并直接在点…

经典神经网络(1)LeNet及其在Fashion-MNIST数据集上的应用

经典神经网络(1)LeNet 1、卷积神经网络LeNet 之前对于Fashion-MNIST服装分类数据集&#xff0c;为了能够应⽤softmax回归和多层感知机&#xff0c;我们⾸先将每个大小为28 28的图像展平为⼀个784维的固定⻓度的⼀维向量&#xff0c;然后⽤全连接层对其进⾏处理&#xff0c;此…