1. 程式人生 > >從兩個程序看Linux下命令行參數及execve內核實現

從兩個程序看Linux下命令行參數及execve內核實現

sprintf 賦值 else turn urn chan 裏的 inf proc

一、兩個測試程序
[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

從兩個程序看Linux下命令行參數及execve內核實現