从两个程序看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