系统相关
首页 > 系统相关> > 从两个程序看Linux下命令行参数及execve内核实现

从两个程序看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\n");
} 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\n",argv[i++]);
return i;
}
int main(int argc,char * argv[], char * envp[])
{
    int vc;
    dumpxv(argv);
    if(vc = dumpxv(envp))
    printf("%s\n",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\n",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.5\gdb\linux-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.21\fs\proc\task_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\n",envp[vc-1]+ strlen(envp[vc-1])+1);为什么可以还原原始的exeve第一个参数
linux-2.6.21\fs\exec.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.21\fs\binfmt_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

标签:可执行文件,00,tsecer,envp,argv,内核,Linux,bprm,execve
来源: https://www.cnblogs.com/tsecer/p/10486196.html