Linux内核-exec系统调用

Linux内核-exec系统调用

这里我们以0.1内核为例,查看execve的系统调用实现

copy_strings

该函数的作用是拷贝execve传递进入的agrv参数表到内核空间中

几个段寄存器

在正式开始分析代码之前,我们需要先有几个概念:

  • cs:代码段
  • ds:数据段
  • es:额外段
  • ss:栈
  • fs:用户数据段

而ds和fs有两种情况:

dsfs
用户态工作用户程序数据段用户程序数据段
内核态工作内核程序数据段用户程序数据段

函数声明

/*
 * '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)

其中argc,argv都是传递进入的参数,而page是在当前进程空间找到的一片内存页,一共32页对应128KBp指向page的最上方

参数表拷贝png

这里还有一个需要注意的就是from_kmem参数,其实从注释也能大致知道是什么:

 * from_kmem     argv *        argv **
 *    0          user space    user space
 *    1          kernel space  user space
 *    2          kernel space  kernel space

根据值的不同argv *agrv **指向内核或者用户空间

获取ds,fs指针

    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();

首先是获取得到dsfs指针

    if (from_kmem==2)
        //设置fs为ds
        set_fs(new_fs);

这里如果from_kmem为2就说明argv *agrv **指向内核空间,直接让fs的值指向ds

循环拷贝参数表

    while (argc-- > 0) {
        if (from_kmem == 1)
            //设置fs为ds
            set_fs(new_fs);
        //tmp指向最后一个参数结尾地址
        if (!(tmp = (char *)get_fs_long(((unsigned long *)argv)+argc)))
            panic("argc is wrong");
        if (from_kmem == 1)
            //设置fs为fs
            set_fs(old_fs);
        len=0;        /* remember zero-padding */
        do {
            len++;
        } while (get_fs_byte(tmp++)); //计算每一条参数的长度并且存在len里面
        if (p-len < 0) {    /* this shouldn't happen - 128kB */
            set_fs(old_fs);
            return 0;
        }
        while (len) {
            --p; --tmp; --len;
            if (--offset < 0) {
                offset = p % PAGE_SIZE;
                if (from_kmem==2)
                    set_fs(old_fs);
                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);
        }
    }

1.首先让tmp指向最后一个参数结尾地址

//tmp指向最后一个参数结尾地址
if (!(tmp = (char *)get_fs_long(((unsigned long *)argv)+argc)))

2.然后计算每一条参数的长度并且存在len里面

        len=0;        /* remember zero-padding */
        do {
            len++;
        } while (get_fs_byte(tmp++)); //计算每一条参数的长度并且存在len里面

3.通过一个while循环将该参数每一个字节拷贝到page数组中

        while (len) {
            --p; --tmp; --len;
            if (--offset < 0) {
                offset = p % PAGE_SIZE;
                if (from_kmem==2)
                    set_fs(old_fs);
                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);
        }

注意这里拷贝的顺序,参数表最后一个拷贝到page数组第一个,以此类推,可以建上图

返回内存中的page表

    return p;

需要注意此时p指针已经指向了page最底部,也就是argv[0]所在的底部
原因是上面的--p;

do_execve

do_execve()函数时系统中断调用(int 0x80)调用的C处理函数,是exec()函数族的内核实现函数 。当一个程序使用fork函数创建了一个子进程后,通常会在该子进程中调用exec()簇函数之一以加载执行另一个新程序,此时子进程的代码,数据段,将完全被新程序替换掉,并在子进程中开始执行新程序

/*
 * 'do_execve()' executes a new program.
 * 
 * 1 检查文件类型和权限等
 * 2 读取文件的第一块数据到缓冲区
 * 3 脚本文件与可执行文件的判断
 * 4 校验可执行文件是否能执行
 * 5 进程管理结构的调整
 * 6 释放进程占有的页面
 * 7 调整线性地址空间、参数列表、堆栈地址等
 * 8 设置 eip 和 esp,完成摇身一变
 *
 */
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[1]值

    //查看权限
    if ((0xffff & eip[1]) != 0x000f)
        panic("execve called from supervisor mode");
    for (i=0 ; i<MAX_ARG_PAGES ; i++)    /* clear page-table */
        page[i]=0;

参数eip[1]是调用本次系统调用的原用户程序代码段寄存器CS值,其中的段选择符必须是当前任务的代码段选择符(0x000f)。若不是那么CS只能是内核代码段的选择符(0x0008),这是不允许的,因为内核代码是常住内存不能被替换掉。

这里panic打印的意思就是是内核态调用的这个系统调用

获取得到文件inode

    //打开指定文件的inode
    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;
    e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
    e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
    //文件所有者与当前进程uid相同
    if (current->euid == inode->i_uid)
        i >>= 6;
    //文件所有者所在组和当前进程组相同
    else if (current->egid == inode->i_gid)
        i >>= 3;
    //如果当前进程不是文件所有者或者所有者所在组进程,那么就查看外部组的读写执行权限和是否是超级用户
    if (!(i & 1) &&
        !((inode->i_mode & 0111) && suser())) {
        retval = -ENOEXEC;
        goto exec_error2;
    }

在这里有一个restart_interp标签,用于goto的,后续会用到

获取文件头内容

    //读取当前执行程序或shell脚本的文件头结构
    if (!(bh = bread(inode->i_dev,inode->i_zone[0]))) {      // 根据 inode 读取文件第一块数据(1024KB)
        retval = -EACCES;
        goto exec_error2;
    }
    ex = *((struct exec *) bh->b_data);    /* read exec-header */

这里获取得到inode的i_zone[0]也就对应文件头内容,然后使用一个struct exec指针指向它,其结构定义如下(这时的是.out格式):

// 执行文件结构。 
 // ============================= 
 // 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 // 数据重定位信息长度,字节数。 
 // -----------------------------
struct exec {
  unsigned long a_magic;    /* Use macros N_MAGIC, etc for access */
  unsigned a_text;        /* length of text, in bytes */
  unsigned a_data;        /* length of data, in bytes */
  unsigned a_bss;        /* length of uninitialized data area for file, in bytes */
  unsigned a_syms;        /* length of symbol table data in file, in bytes */
  unsigned a_entry;        /* start address */
  unsigned a_trsize;        /* length of relocation info for text, in bytes */
  unsigned a_drsize;        /* length of relocation info for data, in bytes */
};

判断是否是shell脚本

    if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang)) {
        /*
         * This section does the #! interpretation.
         * Sorta complicated, but hopefully it will work.  -TYT
         */

一个shell脚本的第一行是以#!开头的,这里先判断是否是一个shell脚本

获取解析文件名和执行文件名

        char buf[1023], *cp, *interp, *i_name, *i_arg;
        unsigned long old_fs;

        strncpy(buf, bh->b_data+2, 1022);
        brelse(bh);
        iput(inode);
        buf[1022] = '\0';
        if (cp = strchr(buf, '\n')) {
            *cp = '\0';
            for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);
        }
        if (!cp || *cp == '\0') {
            retval = -ENOEXEC; /* No interpreter name found */
            goto exec_error1;
        }
        //解析出shell脚本的解析程序名 ./run
        interp = i_name = cp;
        i_arg = 0;
        for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++) {
             if (*cp == '/')
                 //runw文件名
                i_name = cp+1;
        }

一般一个shell脚本里面都会有真正要去执行的文件名,这里就是去找到执行程序文件名i_name

参数和环境变量放入page

        /*
         * OK, we've parsed out the interpreter name and
         * (optional) argument.
         */
        if (sh_bang++ == 0) {
            p = copy_strings(envc, envp, page, p, 0);
            p = copy_strings(--argc, argv+1, page, p, 0);
        }

现在最上层放入环境变量,然后紧接着放入命令参数,这里--argc因为传入的agrv中最后一个是NULL ,要减去

shell文件名和执行程序文件名放入page

        /*
         * 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;
        }

这里可以看到copy_strings最后一位from_kmem参数,在放入filename是1,而放入i_name是2,因为一个是用户空间的一个是内和空间

获取可执行文件的inode

        /*
         * 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;

最后我们就取得执行文件的inode,并且跳转到restart_interp执行可执行程序

这里需要注意的是获取inode我们调用了namei函数,但是参数是从用户空间得到的,也就是fs段获得的,因此在调用函数前需要先临时让fs指向内核数据空间,以让内核空间得到执行文件名,并在调用结束之后恢复fs

文件信息判定

此时我们已经来到了执行用户程序阶段,首先会去根据文件头信息进行一些文件判定

    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;
    }
    //文件头不为1K则出错
    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;
        }
    }

这里的if (!sh_bang)是根据前面shell脚本分支中的sh_bang进行判断,如果前面执行了shell分支,这里就不用拷贝了,前面拷贝过了

执行新的进程前的切换和更新工作

//执行线程,进程时要做的切换和更新
/* OK, This is the point of no return */
    //更新当前进程的执行程序的inode节点
    if (current->executable)
        iput(current->executable);
    current->executable = inode;
    //清空当前进程的信号处理函数
    for (i=0 ; i<32 ; i++)
        current->sigaction[i].sa_handler = NULL;
    //关闭原进程指定执行新进城后需要关闭的文件并清空其位图
    for (i=0 ; i<NR_OPEN ; i++)
        if ((current->close_on_exec>>i)&1)
            sys_close(i);
    current->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;
    //设置整个程序的内存映射,并且更新进程的各段信息
    p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;
    p = (unsigned long) create_tables((char *)p,argc,envc);
    current->brk = ex.a_bss +
        (current->end_data = ex.a_data +
        (current->end_code = ex.a_text));
    current->start_stack = p & 0xfffff000;
    current->euid = e_uid;
    current->egid = e_gid;
    i = ex.a_text+ex.a_data;
    //清空bss段
    while (i&0xfff)
        put_fs_byte(0,(char *) (i++));
    eip[0] = ex.a_entry;        /* eip, magic happens :-) */
    eip[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);

释放原来进程内存页表

    //释放原进程的代码段和数据段
    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;

根据当前进程(还未执行新进程)指定的基地址和限长,释放原来的程序的代码段和数据段所对应的内存页表

放入新程序的代码段和数据段

    //设置整个程序的内存映射,并且更新进程的各段信息
    p += change_ldt(ex.a_text,page)-MAX_ARG_PAGES*PAGE_SIZE;
    p = (unsigned long) create_tables((char *)p,argc,envc);
    current->brk = ex.a_bss +
        (current->end_data = ex.a_data +
        (current->end_code = ex.a_text));
    current->start_stack = p & 0xfffff000;
    current->euid = e_uid;
    current->egid = e_gid;
    i = ex.a_text+ex.a_data;

这里首先根据新执行文件头结构中的代码长度字段a_text的值修改局部描述表中描述符基地址和段限长,并以128KB的参数和环境空间页面放置在数据段末尾

然后调用create_tables在栈空间创建环境和参数变量指针表 ,供程序main函数使用,并返回栈指针p

接着在修改进程task_struct的各个字段值为新执行文件的信息:

  • 代码尾字段end_code等于执行文件的代码段长度a_text
  • 数据尾字段end_data等于执行文件的代码段长度加数据段长度a_data
  • 进程堆结尾字段brk = a_text + a_data + a_bss

然后设置进程的有效用户id和组id

最终内存布局

由于数据段限长为 64MB,进程的线性地址空间被划分为以下区域(从低地址到高地址):

地址范围区域状态
0x00000000 起始代码段(Text)
长度 = ex.a_text(对齐至页边界)
页表项为空。未分配物理页,也未读入磁盘数据。执行时触发缺页中断,由内核根据 inode 从磁盘按需读入。
代码段结束数据段(Data)
长度 = ex.a_data
页表项为空。按需加载。
数据段结束BSS 段
长度 = ex.a_bss
页表项为空(但请注意,do_execve 末尾有一段 while (i&0xfff) put_fs_byte(0, ...) 循环,它会在 BSS 段首部对应的线性地址上执行写零操作。由于页表为空,这会立即触发缺页中断,并在缺页处理中分配一个清零的物理页,因此 BSS 段的第一页(或未对齐部分)在返回用户态前已被实际分配并清零,其余部分仍是按需分配零页)。
BSS 结束 ~ 64MB – 128KB空闲/未映射区域未分配页表项。
64MB – 128KB ~ 64MB
(即 0x03FE0000 ~ 0x04000000)
参数(argv)和环境变量(envp)页面
大小固定为 MAX_ARG_PAGES * 4KB = 128KB
已映射到物理内存change_ldt 中的 for 循环从数据段末端(data_base + 64MB)反向递减,调用 put_page 将内核中存放参数字符串的物理页面挂载到这些线性地址上。这是当前唯一实际存在于物理内存中的程序数据。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇