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,完成摇身一变

核心逻辑就是加载文件调整内存开始执行三个步骤

一部分一部分看吧

  1. 第一部分

    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类型

  2. 第二部分

    .....
    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

  3. 第三部分

    ......
    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 格式了,但大体的思想是一致的。

  1. 第四部分

    if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {
    	.......
    }
    brelse(bh);
    .....
    

    我们写一个 Linux 脚本文件的时候,通常可以看到前面有这么一坨东西。

    #!/bin/sh
    #!/usr/bin/python
    

    就是通过判断第一个和第二个元素是否是# 和 !来判断是否是脚本程序的

    我们这里不是脚本程序,所以就不进入这个if了,暂时不需要看了

    看下一个brelse(bh) fs文件 -> buffer.c

    void 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结构的数据的那个缓存块释放掉,好存储下一个

  2. 第五部分

    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二头部,其实就是把这两个字符串存入了进去

  1. 第六部分

    #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

  2. 第七部分

    ......
    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了

    1. 第八部分

      .....
      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程序正在开始了;

posted @ 2022-08-23 10:39  水三丫  阅读(173)  评论(0编辑  收藏  举报