【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
,这就是我们登录linux
的shell
环境。
代码如下:
// 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)
宏定义展开,
伪代码形式如下:
- 将原有
eax, ebx, ecx, edx
寄存器值全部入栈 - 参数分别保存在
ebx、ecx、edx
中,系统调用中断号保存在eax
中 - 通过
int $0x80
触发80
中断后,会根据eax
寄存器的系统调用号来判断要调用的中断函数,__NR_execve
为中断号11
- 函数返回值保存在
eax
寄存器中 - 通过判断返回值区分函数调用是否成功,如果失败的话将错误返回码保存在
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>