代码改变世界

system()与execv()函数使用详解

2012-12-05 12:12  Rudrj2  阅读(32346)  评论(0编辑  收藏  举报

在网上搜了很久都没有一个很好的解释,都只说了一方面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 }