system()與execv()函數使用詳解


在網上搜了很久都沒有一個很好的解釋,都只說了一方面system調用子進程后繼續執行父進程,execv是調用一個新的進程,所以打算自己讀讀這兩個執行文件源碼,自己再找找其他不同:

 相關函數: fork,execl,execle,execlp,execv,execvp
 
 表頭文件: #include<unistd.h>

 定義函數: int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
 
 函數說明: execve()用來執行參數filename字符串所代表的文件路徑,第二個參數系利用數組指針來傳遞給執行文件,最后一個參數則為傳遞給執行文件的新環境變量數組。
 
 返回值:   如果執行成功則函數不會返回,執行失敗則直接返回-1,失敗原因存於errno 中。
 
 錯誤代碼:
 
 EACCES
 1. 欲執行的文件不具有用戶可執行的權限。
 2. 欲執行的文件所屬的文件系統是以noexec 方式掛上。
 3.欲執行的文件或script翻譯器非一般文件。
 
 EPERM
 1.進程處於被追蹤模式,執行者並不具有root權限,欲執行的文件具有SUID 或SGID 位。
 2.欲執行的文件所屬的文件系統是以nosuid方式掛上,欲執行的文件具有SUID 或SGID 位元,但執行者並不具有root權限。
 
 E2BIG 參數數組過大
 ENOEXEC 無法判斷欲執行文件的執行文件格式,有可能是格式錯誤或無法在此平台執行。
 EFAULT 參數filename所指的字符串地址超出可存取空間范圍。
 ENAMETOOLONG 參數filename所指的字符串太長。
 ENOENT 參數filename字符串所指定的文件不存在。
 ENOMEM 核心內存不足
 ENOTDIR 參數filename字符串所包含的目錄路徑並非有效目錄
 EACCES 參數filename字符串所包含的目錄路徑無法存取,權限不足
 ELOOP 過多的符號連接
 ETXTBUSY 欲執行的文件已被其他進程打開而且正把數據寫入該文件中
 EIO I/O 存取錯誤
 ENFILE 已達到系統所允許的打開文件總數。
 EMFILE 已達到系統所允許單一進程所能打開的文件總數。
 EINVAL 欲執行文件的ELF執行格式不只一個PT_INTERP節區
 EISDIR ELF翻譯器為一目錄
 ELIBBAD ELF翻譯器有問題。
 
 范例:
#include<unistd.h> main() {   char * argv[ ]={“ls”,”-al”,”/etc/passwd”,(char *)0};   char * envp[ ]={“PATH=/bin”,0}   execve(“/bin/ls”,argv,envp); } 執行 -rw-r--r-- 1 root root 705 Sep 3 13 :52 /etc/passwd


http://www.tutorialspoint.com/unix_system_calls/execve.htm

http://blog.tianya.cn/blogger/post_read.asp?BlogID=1285060&PostID=12814565

http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=blob;f=fs/exec.c#l1376

下面是system執行文件代碼:

相關函數: fork,execve,waitpid,popen

表頭文件: #i nclude
<stdlib.h>
定義函數:
int system(const char * string);
函數說明: system()會調用fork()產生子進程,由子進程來調用
/bin/sh-c string來執行參數string字符串所代表的命令,此命>令執行完后隨即返回原調用的進程。在調用system()期間SIGCHLD 信號會被暫時擱置,SIGINT和SIGQUIT 信號則會被忽略。
返回值:
=-1:出現錯誤 =0:調用成功但是沒有出現子進程 >0:成功退出的子進程的id 如果system()在調用/bin/sh時失敗則返回127,其他失敗原因返回-1。若參數string為空指針(NULL),則返回非零值>。 如果system()調用成功則最后會返回執行shell命令后的返回值,但是此返回值也有可能為 system()調用/bin/sh失敗所返回的127,因此最好能再檢查errno 來確認執行成功。
附加說明: 在編寫具有SUID
/SGID權限的程序時請勿使用system(),system()會繼承環境變量,通過環境變量可能會造成系統安全的問題。
范例: #include
<stdlib.h> main() {   system(“ls -al /etc/passwd /etc/shadow”); }
執行結果:
-rw-r--r-- 1 root root 705 Sep 3 13 :52 /etc/passwd -r--------- 1 root root 572 Sep 2 15 :34 /etc/shado 例2: char tmp[]; sprintf(tmp,"/bin/mount -t vfat %s /mnt/usb",dev); system(tmp); 其中dev是/dev/sda1。
system源碼 #include #include #include #include
int system(const char * cmdstring) { pid_t pid; int status; if(cmdstring == NULL){ return (1); } if((pid = fork())<0){ status = -1; } else if(pid == 0){ execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); -exit(127); //子進程正常執行則不會執行此語句 } else{ while(waitpid(pid, &status, 0) < 0){ if(errno != EINTER){ status = -1; break; } } } return status; }

先分析一下原理,然后再看上面的代碼大家估計就能看懂了:   

當system接受的命令為NULL時直接返回,否則fork出一個子進程,因為fork在兩個進程:父進程和子進程中都返回,這里要檢查返回的pid,fork在子進程中返回0,在父進程中返回子進程的pid,父進程使用waitpid等待子進程結束,子進程則是調用execl來啟動一個程序代替自己,execl("/bin/sh", "sh", "-c", cmdstring,(char*)0)是調用shell,這個shell的路徑是/bin/sh,后面的字符串都是參數,然后子進程就變成了一個shell進程,這個shell的參數是cmdstring,就是system接受的參數。在windows中的shell是command,想必大家很熟悉shell接受命令之后做的事了。
   
如果上面的你沒有看懂,那我再解釋下fork的原理:當一個進程A調用fork時,系統內核創建一個新的進程B,並將A的內存映像復制到B的進程空間中,因為A和B是一樣的,那么他們怎么知道自己是父進程還是子進程呢,看fork的返回值就知道,上面也說了fork在子進程中返回0,在父進程中返回子進程的pid。

execl是編譯器的函數(在一定程度上隱藏具體系統實現),在linux中它會接着產生一個linux系統的調用execve, 原型見下:
    int execve(const char * file,const char **argv,const char **envp);
   
看到這里你就會明白為什么system()會接受父進程的環境變量,但是用system改變環境變量后,system一返回主函數還是沒變,原因從system的實現可以看到,它是通過產生新進程實現的,從我的分析中可以看到父進程和子進程間沒有進程通信,子進程自然改變不了父進程的環境變量。

system調用最終依然是依靠execve()實現調用的,那么分析下execve:
 
典型的用法,就是在shell中,執行一個文件,比如說,一個編譯好的文件,叫做helloworld,那么,在shell下執行./helloworld的時候,shell就去fork()一個子進程,然后在子進程里面execve("./helloworld",NULL,NULL);
這樣,就執行了這個文件!
具體點:
在shell中:
 
    if(!fork())
    {
        execve("./helloworld",NULL,NULL);
        exit(0);
    }
那么,這個execve函數,都做了什么,追一下內核看看!
首先,利用參數(文件名),調用函數namei(filename),能取得這個文件名對應的i節點!
然后把當前進程(子進程)的i節點置成上面取得的那個i節點。
釋放所有資源,釋放內存頁表並且修改LDT。
凶狠的把中斷壓入的EIP的值都給改了,改成了從上面那個i節點讀出的可執行文件的頭部那個文件執行的頭字段。
更猛的是,把棧也給改了!
好了,這下子跟父進程一點關系都沒有了!
中斷返回后,程序從i節點指向的那個可執行程序開始執行!
這里,我們注意到個問題!
execve之后,原來的那些代碼,都沒了,也就是說,上面的那個exit(0),根本執行不到那!因為在execve調用中,代碼就換成了那個i節點的了,以前的那些,都釋放了!牛!
於是,
 
    #include <stdio.h>
    #include <unistd.h>
    int main()
    {
        execve("./helloworld",NULL,NULL);
        printf("nothing!\n");
        return 0;
    }        

 

這個程序輸出什么?(假設helloworld程序輸出hello world!)
那么,這個程序輸出的,就是hello world!
你會想,為什么沒有下面的nothing!啊?
因為,execve調用中,這些代碼都沒有了,代碼被替換成helloworld的了,而且,只執行helloworld就完了!
那么,你會說,那怎么執行完helloworld后,繼續做事啊?
這樣!
 
  #include <stdio.h>
  #include <unistd.h>
  int main()
  {
    if(!fork())
      execve("./helloworld",NULL,NULL);
    else
      printf("nothing!\n");
    return 0;
  }
 
執行一下,絕大多數會輸出hello world!后,輸出nothing!
這個execve夠變態!
 
下面是找的一個比較好的解釋,有時間再慢慢研究:
View Code
  1 //execve()系統中斷調用函數。加載並執行子進程(其它程序)。
  2 // 該函數系統中斷調用(int 0x80)功能號__NR_execve 調用的函數。
  3 // 參數:eip - 指向堆棧中調用系統中斷的程序代碼指針eip 處,參見kernel/system_call.s 程序
  4 // 開始部分的說明;tmp - 系統中斷調用本函數時的返回地址,無用;
  5 // filename - 被執行程序文件名;argv - 命令行參數指針數組;envp - 環境變量指針數組。
  6 // 返回:如果調用成功,則不返回;否則設置出錯號,並返回-1。
  7 int
  8 do_execve (unsigned long *eip, long tmp, char *filename,
  9        char **argv, char **envp)
 10 {
 11   struct m_inode *inode;    // 內存中I 節點指針結構變量。
 12   struct buffer_head *bh;    // 高速緩存塊頭指針。
 13   struct exec ex;        // 執行文件頭部數據結構變量。
 14   unsigned long page[MAX_ARG_PAGES];    // 參數和環境字符串空間的頁面指針數組。
 15   int i, argc, envc;
 16   int e_uid, e_gid;        // 有效用戶id 和有效組id。
 17   int retval;            // 返回值。
 18   int sh_bang = 0;        // 控制是否需要執行腳本處理代碼。
 19 // 參數和環境字符串空間中的偏移指針,初始化為指向該空間的最后一個長字處。
 20   unsigned long p = PAGE_SIZE * MAX_ARG_PAGES - 4;
 21 
 22 // eip[1]中是原代碼段寄存器cs,其中的選擇符不可以是內核段選擇符,也即內核不能調用本函數。
 23   if ((0xffff & eip[1]) != 0x000f)
 24     panic ("execve called from supervisor mode");
 25 // 初始化參數和環境串空間的頁面指針數組(表)。
 26   for (i = 0; i < MAX_ARG_PAGES; i++)    /* clear page-table */
 27     page[i] = 0;
 28 // 取可執行文件的對應i 節點號。
 29   if (!(inode = namei (filename)))    /* get executables inode */
 30     return -ENOENT;
 31 // 計算參數個數和環境變量個數。
 32   argc = count (argv);
 33   envc = count (envp);
 34 
 35 // 執行文件必須是常規文件。若不是常規文件則置出錯返回碼,跳轉到exec_error2(第347 行)。
 36 restart_interp:
 37   if (!S_ISREG (inode->i_mode))
 38     {                /* must be regular file */
 39       retval = -EACCES;
 40       goto exec_error2;
 41     }
 42 // 檢查被執行文件的執行權限。根據其屬性(對應i 節點的uid 和gid),看本進程是否有權執行它。
 43   i = inode->i_mode;
 44   e_uid = (i & S_ISUID) ? inode->i_uid : current->euid;
 45   e_gid = (i & S_ISGID) ? inode->i_gid : current->egid;
 46   if (current->euid == inode->i_uid)
 47     i >>= 6;
 48   else if (current->egid == inode->i_gid)
 49     i >>= 3;
 50   if (!(i & 1) && !((inode->i_mode & 0111) && suser ()))
 51     {
 52       retval = -ENOEXEC;
 53       goto exec_error2;
 54     }
 55 // 讀取執行文件的第一塊數據到高速緩沖區,若出錯則置出錯碼,跳轉到exec_error2 處去處理。
 56   if (!(bh = bread (inode->i_dev, inode->i_zone[0])))
 57     {
 58       retval = -EACCES;
 59       goto exec_error2;
 60     }
 61 // 下面對執行文件的頭結構數據進行處理,首先讓ex 指向執行頭部分的數據結構。
 62   ex = *((struct exec *) bh->b_data);    /* read exec-header *//* 讀取執行頭部分 */
 63 // 如果執行文件開始的兩個字節為'#!',並且sh_bang 標志沒有置位,則處理腳本文件的執行。
 64   if ((bh->b_data[0] == '#') && (bh->b_data[1] == '!') && (!sh_bang))
 65     {
 66 /*
 67 * This section does the #! interpretation.
 68 * Sorta complicated, but hopefully it will work. -TYT
 69 */
 70 /*
 71 * 這部分處理對'#!'的解釋,有些復雜,但希望能工作。-TYT
 72 */
 73 
 74       char buf[1023], *cp, *interp, *i_name, *i_arg;
 75       unsigned long old_fs;
 76 
 77 // 復制執行程序頭一行字符'#!'后面的字符串到buf 中,其中含有腳本處理程序名。
 78       strncpy (buf, bh->b_data + 2, 1022);
 79 // 釋放高速緩沖塊和該執行文件i 節點。
 80       brelse (bh);
 81       iput (inode);
 82 // 取第一行內容,並刪除開始的空格、制表符。
 83       buf[1022] = '\0';
 84       if (cp = strchr (buf, '\n'))
 85     {
 86       *cp = '\0';
 87       for (cp = buf; (*cp == ' ') || (*cp == '\t'); cp++);
 88     }
 89 // 若該行沒有其它內容,則出錯。置出錯碼,跳轉到exec_error1 處。
 90       if (!cp || *cp == '\0')
 91     {
 92       retval = -ENOEXEC;    /* No interpreter name found */
 93       goto exec_error1;
 94     }
 95 // 否則就得到了開頭是腳本解釋執行程序名稱的一行內容。
 96       interp = i_name = cp;
 97 // 下面分析該行。首先取第一個字符串,其應該是腳本解釋程序名,iname 指向該名稱。
 98       i_arg = 0;
 99       for (; *cp && (*cp != ' ') && (*cp != '\t'); cp++)
100     {
101       if (*cp == '/')
102         i_name = cp + 1;
103     }
104 // 若文件名后還有字符,則應該是參數串,令i_arg 指向該串。
105       if (*cp)
106     {
107       *cp++ = '\0';
108       i_arg = cp;
109     }
110 /*
111 * OK, we've parsed out the interpreter name and
112 * (optional) argument.
113 */
114 /*
115 * OK,我們已經解析出解釋程序的文件名以及(可選的)參數。
116 */
117 // 若sh_bang 標志沒有設置,則設置它,並復制指定個數的環境變量串和參數串到參數和環境空間中。
118       if (sh_bang++ == 0)
119     {
120       p = copy_strings (envc, envp, page, p, 0);
121       p = copy_strings (--argc, argv + 1, page, p, 0);
122     }
123 /*
124 * Splice in (1) the interpreter's name for argv[0]
125 * (2) (optional) argument to interpreter
126 * (3) filename of shell script
127 *
128 * This is done in reverse order, because of how the
129 * user environment and arguments are stored.
130 */
131 /*
132 * 拼接 (1) argv[0]中放解釋程序的名稱
133 * (2) (可選的)解釋程序的參數
134 * (3) 腳本程序的名稱
135 *
136 * 這是以逆序進行處理的,是由於用戶環境和參數的存放方式造成的。
137 */
138 // 復制腳本程序文件名到參數和環境空間中。
139       p = copy_strings (1, &filename, page, p, 1);
140 // 復制解釋程序的參數到參數和環境空間中。
141       argc++;
142       if (i_arg)
143     {
144       p = copy_strings (1, &i_arg, page, p, 2);
145       argc++;
146     }
147 // 復制解釋程序文件名到參數和環境空間中。若出錯,則置出錯碼,跳轉到exec_error1。
148       p = copy_strings (1, &i_name, page, p, 2);
149       argc++;
150       if (!p)
151     {
152       retval = -ENOMEM;
153       goto exec_error1;
154     }
155 /*
156 * OK, now restart the process with the interpreter's inode.
157 */
158 /*
159 * OK,現在使用解釋程序的i 節點重啟進程。
160 */
161 // 保留原fs 段寄存器(原指向用戶數據段),現置其指向內核數據段。
162       old_fs = get_fs ();
163       set_fs (get_ds ());
164 // 取解釋程序的i 節點,並跳轉到restart_interp 處重新處理。
165       if (!(inode = namei (interp)))
166     {            /* get executables inode */
167       set_fs (old_fs);
168       retval = -ENOENT;
169       goto exec_error1;
170     }
171       set_fs (old_fs);
172       goto restart_interp;
173     }
174 // 釋放該緩沖區。
175   brelse (bh);
176 // 下面對執行頭信息進行處理。
177 // 對於下列情況,將不執行程序:如果執行文件不是需求頁可執行文件(ZMAGIC)、或者代碼重定位部分
178 // 長度a_trsize 不等於0、或者數據重定位信息長度不等於0、或者代碼段+數據段+堆段長度超過50MB、
179 // 或者i 節點表明的該執行文件長度小於代碼段+數據段+符號表長度+執行頭部分長度的總和。
180   if (N_MAGIC (ex) != ZMAGIC || ex.a_trsize || ex.a_drsize ||
181       ex.a_text + ex.a_data + ex.a_bss > 0x3000000 ||
182       inode->i_size < ex.a_text + ex.a_data + ex.a_syms + N_TXTOFF (ex))
183     {
184       retval = -ENOEXEC;
185       goto exec_error2;
186     }
187 // 如果執行文件執行頭部分長度不等於一個內存塊大小(1024 字節),也不能執行。轉exec_error2。
188   if (N_TXTOFF (ex) != BLOCK_SIZE)
189     {
190       printk ("%s: N_TXTOFF != BLOCK_SIZE. See a.out.h.", filename);
191       retval = -ENOEXEC;
192       goto exec_error2;
193     }
194 // 如果sh_bang 標志沒有設置,則復制指定個數的環境變量字符串和參數到參數和環境空間中。
195 // 若sh_bang 標志已經設置,則表明是將運行腳本程序,此時環境變量頁面已經復制,無須再復制。
196   if (!sh_bang)
197     {
198       p = copy_strings (envc, envp, page, p, 0);
199       p = copy_strings (argc, argv, page, p, 0);
200 // 如果p=0,則表示環境變量與參數空間頁面已經被占滿,容納不下了。轉至出錯處理處。
201       if (!p)
202     {
203       retval = -ENOMEM;
204       goto exec_error2;
205     }
206     }
207 /* OK, This is the point of no return */
208 /* OK,下面開始就沒有返回的地方了 */
209 // 如果原程序也是一個執行程序,則釋放其i 節點,並讓進程executable 字段指向新程序i 節點。
210   if (current->executable)
211     iput (current->executable);
212   current->executable = inode;
213 // 清復位所有信號處理句柄。但對於SIG_IGN 句柄不能復位,因此在322 與323 行之間需添加一條
214 // if 語句:if (current->sa[I].sa_handler != SIG_IGN)。這是源代碼中的一個bug。
215   for (i = 0; i < 32; i++)
216     current->sigaction[i].sa_handler = NULL;
217 // 根據執行時關閉(close_on_exec)文件句柄位圖標志,關閉指定的打開文件,並復位該標志。
218   for (i = 0; i < NR_OPEN; i++)
219     if ((current->close_on_exec >> i) & 1)
220       sys_close (i);
221   current->close_on_exec = 0;
222 // 根據指定的基地址和限長,釋放原來程序代碼段和數據段所對應的內存頁表指定的內存塊及頁表本身。
223   free_page_tables (get_base (current->ldt[1]), get_limit (0x0f));
224   free_page_tables (get_base (current->ldt[2]), get_limit (0x17));
225 // 如果“上次任務使用了協處理器”指向的是當前進程,則將其置空,並復位使用了協處理器的標志。
226   if (last_task_used_math == current)
227     last_task_used_math = NULL;
228   current->used_math = 0;
229 // 根據a_text 修改局部表中描述符基址和段限長,並將參數和環境空間頁面放置在數據段末端。
230 // 執行下面語句之后,p 此時是以數據段起始處為原點的偏移值,仍指向參數和環境空間數據開始處,
231 // 也即轉換成為堆棧的指針。
232   p += change_ldt (ex.a_text, page) - MAX_ARG_PAGES * PAGE_SIZE;
233 // create_tables()在新用戶堆棧中創建環境和參數變量指針表,並返回該堆棧指針。
234   p = (unsigned long) create_tables ((char *) p, argc, envc);
235 // 修改當前進程各字段為新執行程序的信息。令進程代碼段尾值字段end_code = a_text;令進程數據
236 // 段尾字段end_data = a_data + a_text;令進程堆結尾字段brk = a_text + a_data + a_bss。
237   current->brk = ex.a_bss +
238     (current->end_data = ex.a_data + (current->end_code = ex.a_text));
239 // 設置進程堆棧開始字段為堆棧指針所在的頁面,並重新設置進程的用戶id 和組id。
240   current->start_stack = p & 0xfffff000;
241   current->euid = e_uid;
242   current->egid = e_gid;
243 // 初始化一頁bss 段數據,全為零。
244   i = ex.a_text + ex.a_data;
245   while (i & 0xfff)
246     put_fs_byte (0, (char *) (i++));
247 // 將原調用系統中斷的程序在堆棧上的代碼指針替換為指向新執行程序的入口點,並將堆棧指針替換
248 // 為新執行程序的堆棧指針。返回指令將彈出這些堆棧數據並使得CPU 去執行新的執行程序,因此不會
249 // 返回到原調用系統中斷的程序中去了。
250   eip[0] = ex.a_entry;        /* eip, magic happens :-) *//* eip,魔法起作用了 */
251   eip[3] = p;            /* stack pointer *//* esp,堆棧指針 */
252   return 0;
253 exec_error2:
254   iput (inode);
255 exec_error1:
256   for (i = 0; i < MAX_ARG_PAGES; i++)
257     free_page (page[i]);
258   return (retval);
259 }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM