一、两个测试程序
[[email?protected] 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 failedn");
} else{
sleep(10000);
}
}
[[email?protected] ArgLayout]$ cat mysleeper.c
/*
*测试程序,打印自己的argv,envp数组以及一个根据内核参数布局而计算出来的真实可执行文件名称
*/
#include <stdlib.h>
int dumpxv(char * argv[])
{
int i=0;
if (argv)? while(argv[i]) printf("%sn",argv[i++]);
return i;
}
int main(int argc,char * envp[])
{
??? int vc;
??? dumpxv(argv);
??? if(vc = dumpxv(envp))
??? printf("%sn",envp[vc-1]+ strlen(envp[vc-1])+1);
??? sleep(1000);
}
[[email?protected] 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("%sn",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,MAXPATHLEN);
? sprintf (name1,?"/proc/%d/exe",pid);
? if (readlink?(name1,name2,MAXPATHLEN) > 0)
??? return name2;
? else
??? return name1;
}
实现是简明扼要,就是通过readlink系统调用来扫描这个任务的/proc/pid/exe,找到这个线程对应的可执行文件。这里做个实现,对于刚才那个错误参数的程序,通过ll看一下这个程序的链接,可以看到它指向的位置还是准确的,虽然它的argv是错误的。
[[email?protected] 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.21fsproctask_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
[[email?protected] 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("%sn",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)将用户提供的exeve的第一个参数对应的字符串放在了紧邻着envp数组的上面,所以通过envp[vc-1]+ strlen(envp[vc-1])+1就可以知道这个数组的内容。
那么这个内容到底有什么作用,内核在哪里用到了,用户如何引用?这些问题我想了一段时间(大家断断续续想了几十分钟),然后在网上搜索了一段时间,看书看了一段时间(包括《情景分析》和《ULK》),都没有找到确切的说法(很扫兴,恩?),设置说没有找到有说法的地方,当然最好看一下内核的ChangLog,但是我没这方面的经验,所以我就猜测一下这个的意义:这个保存操作是在do_execve函数中完成的,这个函数是一个可执行文件格式无关的函数,elf格式在用、a.out在用,script在用,misc也在用。所以这里把他压在堆栈的最顶端一个猥琐的位置是为了便于扩展,某些特殊的可执行文件格式(例如,一个我不知道的可执行格式)可能会用到这个字符串,虽然我们通常只认识argc,argv,envp等参数。
例如考虑一个文件格式文件,一个
[[email?protected] ArgLayout]$ ./demo.sh -c "echo hello" &
[1] 32739
[[email?protected] 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
[[email?protected] ArgLayout]$ cat demo.sh?
#! /bin/sh
sleep 1000
可以看到,命令行中输入的命令被替换,第一个参数./demo.sh会作为新派生的/bin/sh的第一个参数。
linux-2.6.21fsbinfmt_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工具的时候,发现它也是一个多路程序:
[[email?protected] 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-multilrwxrwxrwx. 1 root root??? 14 2011-03-12 16:59 /sbin/iptables-restore -> iptables-multilrwxrwxrwx. 1 root root??? 14 2011-03-12 16:59 /sbin/iptables-save -> iptables-multi