linux-0.11分析:进程初始化函数init(),第三部分,fork创建第二个进程,第十四篇随笔
第三部分,fork创建第二个进程
[引用github这个博主 多多支持][ https://github.com/sunym1993/flash-linux0.11-talk ]
先看看init
中的这段代码
if(!(pid=fork())) { close(0); if (open("/etc/rc",O_RDONLY,0)) _exit(1); execve("/bin/sh",argv_rc,envp_rc); _exit(2); }
看见了pid=fork()
又在创建进程了开启int 0x80
中断,去执行sys_fork
了
这个进程创建结束了
调用了一个close(0)
函数
lib文件 -> close.c
_syscall1(int,close,int,fd)
fs文件 -> open.c
int sys_close(unsigned int fd) { struct file * filp; if (fd >= NR_OPEN) return -EINVAL; current->close_on_exec &= ~(1<<fd); if (!(filp = current->filp[fd])) return -EINVAL; current->filp[fd] = NULL; if (filp->f_count == 0) panic("Close: file count is 0"); if (--filp->f_count) return (0); iput(filp->f_inode); return (0); }
close(0) 就是关闭 0 号文件描述符,也就是进程 1 复制过来的打开了 tty0 并作为标准输入的文件描述符,那么此时 0 号文件描述符就空出来了。
然后就是打开了一个/etc/rc
文件
open("/etc/rc",O_RDONLY,0)
将0号文件描述符指向了这个/etc/rc
然后再执行了一个重要的函数execve("/bin/sh",argv_rc,envp_rc);
execve("/bin/sh",argv_rc,envp_rc);
include文件 -> execve.c
_syscall3(int,execve,const char *,file,char **,argv,char **,envp)
kernel文件 -> system_call.s
EIP = 0x1C _sys_execve: lea EIP(%esp),%eax pushl %eax call _do_execve addl $4,%esp ret
这里调用了 do_execve
fs文件 -> exec.c
#define MAX_ARG_PAGES 32 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; ...... }
这个文件相对比较长,就用省略号代替了
[引用github这个博主 多多支持][ https://github.com/sunym1993/flash-linux0.11-talk ]
eip 调用方触发系统调用时由 CPU 压入栈空间中的 eip 的指针 。
tmp 是一个无用的占位参数。
filename 是 "/bin/sh"
argv 是 { "/bin/sh", NULL }
envp 是
执行步骤
1 检查文件类型和权限等
2 读取文件的第一块数据到缓冲区
3 脚本文件与可执行文件的判断
4 校验可执行文件是否能执行
5 进程管理结构的调整
6 释放进程占有的页面
7 调整线性地址空间、参数列表、堆栈地址等
8 设置 eip 和 esp,完成摇身一变
核心逻辑就是加载文件、调整内存、开始执行三个步骤
一部分一部分看吧
-
第一部分
static int count(char ** argv) { int i=0; char ** tmp; if (tmp = argv) while (get_fs_long((unsigned long *) (tmp++))) i++; return i; } ...... 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); 先初始化了
page[32]
,全部赋值为0然后读取了
filename
这个文件,inode=namei(filename)
,filename是 "/bin/sh"然后把这两个参数:argv 是 { "/bin/sh", NULL },envp 是 { "HOME=/", NULL },转化为了int类型
-
第二部分
..... e_uid = (i & S_ISUID) ? inode->i_uid : current->euid; e_gid = (i & S_ISGID) ? inode->i_gid : current->egid; if (current->euid == inode->i_uid) i >>= 6; else if (current->egid == inode->i_gid) i >>= 3; ..... if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) { //struct buffer_head * bh; retval = -EACCES; goto exec_error2; } 前面都在设置一些进程1和进程2的关联,把进程2的文件的i_uid和进程1的euid相关联
看看重要的bh = bread(inode->i_dev,inode->i_zone[0]读取filename的第一块文件,也就是1024B=1K
-
第三部分
...... ex = *((struct exec *) bh->b_data); ...... 解析这1KB的数据为exec的结构体
看看这个exec的结构体
include文件 -> a.out.h
struct exec { unsigned long a_magic; /* 使用宏N_MAGIC等进行访问 */ unsigned a_text; /* 文本长度,以字节为单位 */ unsigned a_data; /* 数据长度,以字节为单位 */ unsigned a_bss; /* 文件未初始化数据区的长度,以字节为单位 */ unsigned a_syms; /* 文件中符号表数据的长度,以字节为单位 */ unsigned a_entry; /* 起始地址 */ unsigned a_trsize; /* 文本的重新定位信息长度,以字节为单位 */ unsigned a_drsize; /* 数据的重新定位信息长度,以字节为单位 */ };
现在的 Linux 已经弃用了这种古老的格式,改用 ELF 格式了,但大体的思想是一致的。
-
第四部分
if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) { ....... } brelse(bh); ..... 我们写一个 Linux 脚本文件的时候,通常可以看到前面有这么一坨东西。
#!/bin/sh #!/usr/bin/python 就是通过判断第一个和第二个元素是否是# 和 !来判断是否是脚本程序的
我们这里不是脚本程序,所以就不进入这个if了,暂时不需要看了
看下一个
brelse(bh)
fs文件 -> buffer.cvoid brelse(struct buffer_head * buf) { if (!buf) return; wait_on_buffer(buf); if (!(buf->b_count--)) panic("Trying to free free buffer"); wake_up(&buffer_wait); } 就是把已经解析为exec结构的数据的那个缓存块释放掉,好存储下一个
-
第五部分
unsigned long p=PAGE_SIZE*MAX_ARG_PAGES-4; // 128k -4 ...... ...... //省略了一部分错误判断 if (!sh_bang) { //sh_bang = 0 p = copy_strings(envc,envp,page,p,0); p = copy_strings(argc,argv,page,p,0); if (!p) { retval = -ENOMEM; goto exec_error2; } } 主要是执行了
copy_strings
这个函数,这里的argv 是 { "/bin/sh", NULL },envp 是 { "HOME=/", NULL },把这两个参数的字符串存放到p指向的这个位置,那么p指向哪里
还记得线性内存地址吗,每个进程占64MB,现在p就指向进程2二头部,其实就是把这两个字符串存入了进去
-
第六部分
#define MAX_ARG_PAGES 32 #define PAGE_SIZE 4096 ..... p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE; ..... 前面做了一些当前线程的修改参数的操作
根据 ex.a_text 修改局部描述符中的代码段限长 code_limit
-
第七部分
...... p = (unsigned long) create_tables((char *)p,argc,envc); ...... 这里就真正构造参数表了
先看看这个
create_tables
函数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) { put_fs_long((unsigned long) p,argv++); while (get_fs_byte(p++)) /* nothing */ ; } 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; } 这里就是往哪个128K的参数表中放入了一些值,获取新的指针位置进行改变
执行完把sp返回吗,那么p=sp了
-
第八部分
..... eip[0] = ex.a_entry; /* eip, magic happens :-) */ eip[3] = p; /* stack pointer */ return 0; ..... 前面也是更新了当前线程的一些数据
接下来的
eip[0],eip[3]
就是把代码指针eip,栈的指针esp指向了其他地方,到底指向了那,为什么要改变这个东西了这个函数是通过系统中断int 0x80的系统调用函数,中断的时候会把一些寄存器信息的信息入栈
这里的
ex.a_entry
就是exec结构体的起始位置,也就是从磁盘刚刚读取的信息数据;所以这里经过赋值后,把代码段的指令寄存器eip变成了读取数据( /bin/sh)的位置,把栈顶位置指向了p
执行完返回时就进入了 /bin/sh 程序,也就是 shell 程序开始执行 ;但是这里我们是开启分段和分页机制的,这个时候进程2并没有设置页表映射,使用这时候会触发一个页表寻找错误的中断信息
do_no_page
缺页中断,会去执行相应的代码,然后把逻辑地址->线性地址->物理地址的映射完成,这里面比较复杂,暂时不搞,只需要知道完成这个缺页中断后就可以寻找到物理地址了,然后就是把磁盘的sh程序加载进来,跳转到shell程序正在开始了;
-
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)