zoukankan      html  css  js  c++  java
  • 从两个程序看Linux下命令行参数及execve内核实现

    一、两个测试程序
    [tsecer@Harry ArgLayout]$  cat ArgLayout.c
    /*
    *简单测试程序,创建命令行参数中指定的进程,但是将execve的第二个参数(也就是子进程的argv数组)修改成随机无意义值
    */
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    int main(int argc, char * argv[],char * envp[])
    {
    pid_t forker = fork();

    if(0 == forker)
    {
    char * myargv[] = {
    "Hello",
    "world",
    NULL,
    };
    execve(argv[1],myargv,envp);
    } else if(-1 == forker)
    {
    fprintf(stderr,"fork failed ");
    } else{
    sleep(10000);
    }
    }
    [tsecer@Harry ArgLayout]$ cat mysleeper.c
    /*
    *测试程序,打印自己的argv,envp数组以及一个根据内核参数布局而计算出来的真实可执行文件名称
    */
    #include <stdlib.h>
    int dumpxv(char * argv[])
    {
    int i=0;
    if (argv)  while(argv[i]) printf("%s ",argv[i++]);
    return i;
    }
    int main(int argc,char * argv[], char * envp[])
    {
        int vc;
        dumpxv(argv);
        if(vc = dumpxv(envp))
        printf("%s ",envp[vc-1]+ strlen(envp[vc-1])+1);
        sleep(1000);
    }
    [tsecer@Harry ArgLayout]$ ./ArgLayout.c.exe ./././././././././././././mysleeper.c.exe 
    Hello
    world 这两个是子进程看到的argv数组,之后是子进程看到的envp数组
    ORBIT_SOCKETDIR=/tmp/orbit-tsecer
    HOSTNAME=Harry
    IMSETTINGS_INTEGRATE_DESKTOP=yes
    ……
    _=./ArgLayout.c.exe
    ./././././././././././././mysleeper.c.exe这里是通过非正统的printf("%s ",envp[vc-1]+ strlen(envp[vc-1])+1);打印的可执行文件的名称
    在另一个窗口中看这两个程序
    tsecer   32299  0.0  0.0   1740   284 pts/3    S+   20:42   0:00 ./ArgLayout.c.e
    tsecer   32300  0.0  0.0   1744   316 pts/3    S+   20:42   0:00 Hello world 通过ps看到进程的显示和路径及名称没有任何关系
    这里需要说明的有:
    1、通过ps看到的子进程的名字是没有意义的,就是execve中第二个参数给出的一个参数列表,子进程对这个内容没有任何分辨内容,完全照单接受。所以在子进程中通过argv[0]看到的内容完全不是自己真实可执行文件的名称,所以如果想从这个argv中找到可执行文件的名称或者路径,并不是天经地义的,只是说由于通常是通过bash执行的命令,而大家都自觉的遵守了这个约定,所以没出问题。
    2、在envp字符串之后,放置着execve的第一个参数,也就是真正的传入的可执行文件的原始信息,这个是靠谱的,因为如果这个是一个鬼扯的地址,那么子进程是无法派生成功的。遗憾的是这个内容对于这种C程序的argc、argv、envp来说是不可见的,也就是这个可靠的内容是不正统的(相对于那个正统的是不可靠的)。
    二、如何获得一个指定pid进程使用的可执行文件
    这一点大家首先应该想到的是gdb的一个功能,就是gdb启动之后通过attach直接来调试一个制定pid的任务,那么这个gdb必须要通过这个pid找到这个进程使用的可执行文件,我们来围观一下万能的gdb是如何实现的。
    gdb-6.5gdblinux-nat.c
    /* Accepts an integer PID; Returns a string representing a file that
       can be opened to get the symbols for the child process.  */
    char *
    child_pid_to_exec_file (int pid)
    {
      char *name1, *name2;

      name1 = xmalloc (MAXPATHLEN);
      name2 = xmalloc (MAXPATHLEN);
      make_cleanup (xfree, name1);
      make_cleanup (xfree, name2);
      memset (name2, 0, MAXPATHLEN);

      sprintf (name1, "/proc/%d/exe", pid);
      if (readlink (name1, name2, MAXPATHLEN) > 0)
        return name2;
      else
        return name1;
    }
    实现是简明扼要,就是通过readlink系统调用来扫描这个任务的/proc/pid/exe,找到这个线程对应的可执行文件。这里做个实现,对于刚才那个错误参数的程序,通过ll看一下这个程序的链接,可以看到它指向的位置还是准确的,虽然它的argv是错误的。
    [tsecer@Harry KernelDebug]$ ll /proc/32512/exe
    lrwxrwxrwx. 1 tsecer tsecer 0 2012-02-29 21:21 /proc/32300/exe -> /home/tsecer/CodeTest/ArgLayout/mysleeper.c.exe
    三、proc/pid/exe是如何知道可执行文件正确路径的
    linux-2.6.21fsproc ask_mmu.c
    int proc_exe_link(struct inode *inode, struct dentry **dentry, struct vfsmount **mnt)
    vma = mm->mmap;
        while (vma) {
            if ((vma->vm_flags & VM_EXECUTABLE) && vma->vm_file)
                break;
            vma = vma->vm_next;
        }

        if (vma) {
            *mnt = mntget(vma->vm_file->f_path.mnt);
            *dentry = dget(vma->vm_file->f_path.dentry);
            result = 0;
        }
    我们cat /proc/pid/maps
    [tsecer@Harry KernelDebug]$ cat /proc/32512/maps
    001e8000-00206000 r-xp 00000000 fd:00 1280       /lib/ld-2.11.2.so
    00206000-00207000 r--p 0001d000 fd:00 1280       /lib/ld-2.11.2.so
    00207000-00208000 rw-p 0001e000 fd:00 1280       /lib/ld-2.11.2.so
    0020a000-0037c000 r-xp 00000000 fd:00 1282       /lib/libc-2.11.2.so
    0037c000-0037d000 ---p 00172000 fd:00 1282       /lib/libc-2.11.2.so
    0037d000-0037f000 r--p 00172000 fd:00 1282       /lib/libc-2.11.2.so
    0037f000-00380000 rw-p 00174000 fd:00 1282       /lib/libc-2.11.2.so
    00380000-00383000 rw-p 00000000 00:00 0 
    005a0000-005a1000 r-xp 00000000 00:00 0          [vdso]
    08048000-08049000 r-xp 00000000 fd:00 459938     /home/tsecer/CodeTest/ArgLayout/mysleeper.c.exe
    08049000-0804a000 rw-p 00000000 fd:00 459938     /home/tsecer/CodeTest/ArgLayout/mysleeper.c.exe
    b776c000-b776d000 rw-p 00000000 00:00 0 
    b7781000-b7783000 rw-p 00000000 00:00 0 
    bf882000-bf897000 rw-p 00000000 00:00 0          [stack]
    可以看到,其中的第一个具有可执行属性的区间对应的文件是/lib/ld-2.11.2.so,但是显式的为什么是正确的呢?
    …………沉默五秒钟……
    其实maps中显示的那个x属性是可执行属性,对应的内核标志位
    #define VM_EXEC        0x00000004
    而这里判断的是
    #define VM_EXECUTABLE    0x00001000
    属性,两个是不同的,这个VM_EXECUTABLE属性是在load_elf_binary中单独对加载的可执行文件的时候设置的:
            elf_flags = MAP_PRIVATE | MAP_DENYWRITE | MAP_EXECUTABLE;
    现在大家觉得很好笑,但是这个问题我还是困惑了很久了的,所以我就调试了一下才找到这里来的。
    四、printf("%s ",envp[vc-1]+ strlen(envp[vc-1])+1);为什么可以还原原始的exeve第一个参数
    linux-2.6.21fsexec.c
    retval = copy_strings_kernel(1, &bprm->filename, bprm);
        if (retval < 0)
            goto out;

        bprm->exec = bprm->p;
        retval = copy_strings(bprm->envc, envp, bprm);
        if (retval < 0)
            goto out;

        retval = copy_strings(bprm->argc, argv, bprm);
    可以看到,在赋值envp数组的内容之前,内核先通过copy_strings_kernel(1, &bprm->filename, bprm)将用户提供的exeve的第一个参数对应的字符串放在了紧邻着envp数组的上面,所以通过envp[vc-1]+ strlen(envp[vc-1])+1就可以知道这个数组的内容。
    那么这个内容到底有什么作用,内核在哪里用到了,用户如何引用?这些问题我想了一段时间(大家断断续续想了几十分钟),然后在网上搜索了一段时间,看书看了一段时间(包括《情景分析》和《ULK》),都没有找到确切的说法(很扫兴,恩?),设置说没有找到有说法的地方,当然最好看一下内核的ChangLog,但是我没这方面的经验,所以我就猜测一下这个的意义:这个保存操作是在do_execve函数中完成的,这个函数是一个可执行文件格式无关的函数,elf格式在用、a.out在用,script在用,misc也在用。所以这里把他压在堆栈的最顶端一个猥琐的位置是为了便于扩展,某些特殊的可执行文件格式(例如,一个我不知道的可执行格式)可能会用到这个字符串,虽然我们通常只认识argc,argv,envp等参数。
    例如考虑一个文件格式文件,一个
    [tsecer@Harry ArgLayout]$ ./demo.sh -c "echo hello" &
    [1] 32739
    [tsecer@Harry ArgLayout]$ ps aux
    USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root         1  0.0  0.0   2044   704 ?        Ss   03:54   0:02 /sbin/init
    ……
    tsecer   32739  0.0  0.1   4924  1064 pts/3    S    21:52   0:00 /bin/sh ./demo.sh -c echo hell
    tsecer   32740  0.0  0.0   3940   476 pts/3    S    21:52   0:00 sleep 1000
    tsecer   32741  0.0  0.0   4692   992 pts/3    R+   21:52   0:00 ps aux
    [tsecer@Harry ArgLayout]$ cat demo.sh 
    #! /bin/sh
    sleep 1000
    可以看到,命令行中输入的命令被替换,第一个参数./demo.sh会作为新派生的/bin/sh的第一个参数。
    linux-2.6.21fsinfmt_script.c
        remove_arg_zero(bprm);
        retval = copy_strings_kernel(1, &bprm->interp, bprm);
    不过这里用的不是do_execve中拷贝到顶端的字符串,而是所以其内容还是没有被使用到。
    五、remove_arg_zero
    这个函数主要是清除argv[0]的字符串内容,然后将argc减一。
    void remove_arg_zero(struct linux_binprm *bprm)
    {
        if (bprm->argc) {
            unsigned long offset;
            char * kaddr;
            struct page *page;

            offset = bprm->p % PAGE_SIZE;
            goto inside;这里是一个无条件跳转。

            while (bprm->p++, *(kaddr+offset++)) {循环结束的条件就是遇到一个零字符*(kaddr+offset++),同时增加bprm->p的值,即递增p指针,这个参数是自底向上增加的,并且argv[0]在最低地址。这里的循环主要是为了解决argv[0]使用的字符串跨越页面的情况
                if (offset != PAGE_SIZE)
                    continue;
                offset = 0;
                kunmap_atomic(kaddr, KM_USER0);
    inside:
                page = bprm->page[bprm->p/PAGE_SIZE];
                kaddr = kmap_atomic(page, KM_USER0);
            }
            kunmap_atomic(kaddr, KM_USER0);
            bprm->argc--;
        }
    }

    六、有啥意义
    这一点在busybox所谓的“多路可执行文件”中是非常有用的,因为所有的可执行文件都是软符号链接,所以在执行的时候调用的execve("/bin/cat","/bin/cat"),这样虽然真正执行的是相同的可执行文件,但是它的参数argv却是原始的链接名,所以通过argv来区分功能,在busybox的busybox可执行文件的入口,是通过下面的方法来确定需要执行什么命令
    int lbb_main(char **argv)--->>>bb_basename
    const char* FAST_FUNC bb_basename(const char *name)
    {
        const char *cp = strrchr(name, '/');即最后一个路径分隔符之后的字符作为功能选择依据
        if (cp)
            return cp + 1;
        return name;
    }

    我在以前编iptable工具的时候,发现它也是一个多路程序:
    [tsecer@Harry ArgLayout]$ ll /sbin/iptabl*
    lrwxrwxrwx. 1 root root    14 2011-03-12 16:59 /sbin/iptables -> iptables-multi
    -rwxr-xr-x. 1 root root 57756 2009-09-17 17:17 /sbin/iptables-multi
    lrwxrwxrwx. 1 root root    14 2011-03-12 16:59 /sbin/iptables-restore -> iptables-multi
    lrwxrwxrwx. 1 root root    14 2011-03-12 16:59 /sbin/iptables-save -> iptables-multi

  • 相关阅读:
    人工智能数学基础笔记(上)
    人工智能简介
    十三,十四 基金收益,税收与基金国际化
    资产配置模型之-BL模型
    十二 基金估值,费用与会计核算
    十一 基金的投资交易与结算
    十 基金业绩评价
    九 投资风险管理
    浙工商oj ___飞龙的飞行方程
    hd1004解题思路
  • 原文地址:https://www.cnblogs.com/tsecer/p/10486196.html
Copyright © 2011-2022 走看看