system()与execv()函数使用详解
2012-12-05 12:12 Rudrj2 阅读(32815) 评论(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的实现可以看到,它是通过产生新进程实现的,从我的分析中可以看到父进程和子进程间没有进程通信,子进程自然改变不了父进程的环境变量。
if(!fork()) { execve("./helloworld",NULL,NULL); exit(0); }
#include <stdio.h> #include <unistd.h> int main() { execve("./helloworld",NULL,NULL); printf("nothing!\n"); return 0; }
#include <stdio.h> #include <unistd.h> int main() { if(!fork()) execve("./helloworld",NULL,NULL); else printf("nothing!\n"); return 0; }
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 }