Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1814267
  • 博文数量: 438
  • 博客积分: 9799
  • 博客等级: 中将
  • 技术积分: 6092
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-25 17:25
文章分类

全部博文(438)

文章存档

2019年(1)

2013年(8)

2012年(429)

分类: 系统运维

2012-03-29 13:41:40

在一个程序里执行一个命令字符串是很方便的。例如,假定我们想把一个时间日期戳放入一个特定的文件。我们可以使用在6.10节描述的函数来完成:调用 time来得到当前日历时间,然后调用localtime把它转换成分解时间,接着调用strftime来格式化这个结果,并把结果写入到这个文件里。然而,这样做会容易得多:
system("date > file");


ISO C定义了system函数,但是它的操作有很强的系统依赖性。POSIX.1包含了system接口,基于ISO C定义展开来描述在一个POSIX环境里的它的行为。



  1. #include <stdlib.h>

  2. int system(const char *cmdstring);

  3. 返回值如下。


如果cmdstring是一个空指针,system仅当一个命令处理器可用时返回非0值。这个特性决定了system函数是否被指定的操作系统支持。在UNIX系统下,system一直可用。


因为system是通过调用fork、exec和waitpid来实现的,所以有三种返回值的类型:


1、如果fork失败或waitpid返回一个错误而不是EINTR,system返回-1,并设置errno为指定错误。


2、如果exec失败,暗示shell不能被执行,返回值就好像shell执行了exit(127)。


3、否则,所有三个函数--fork、exec和waitpid--成功,从system的返回值是外壳的终止状态,以waitpid指定的格式。一些 system的早期实现返回一个错误(EINTR),如果waitpid被一个捕获到的信号中断。因为没有一个程序可以使用的清理策略来从这种错误类型返 回,POSIX随后加上需求,system在这种情况不返回一个错误。(我们在10.5节讨论中断的系统调用)。


下面的代码展示了一个system函数的实现。它不处理的特性就是信号。我们将在10.8节用信号处理更新这个函数。



  1. #include <sys/wait.h>
  2. #include <errno.h>
  3. #include <unistd.h>

  4. int
  5. system(const char *cmdstring) /* version without signal handling */
  6. {
  7.     pid_t pid;
  8.     int status;

  9.     if (cmdstring == NULL)
  10.         return(1); /* always a command processor with UNIX */

  11.     if ((pid = fork()) < 0)
  12.         status = -1; /* probaly out of processes */
  13.     else if (pid == 0) { /* child */
  14.         execl("/bin/sh", "sh", "-c", cmdstring, (char*)0);
  15.         _exit(27); /* execl error */
  16.     } else { /* parent */
  17.         while (waitpid(pid, &status, 0) < 0) {
  18.             if (errno != EINTR) {
  19.                 status = -1; /* error other than EINTR from waipid() */
  20.                 break;
  21.             }
  22.         }
  23.     }
  24. }



外壳的-c选项告诉它接受下一个参数--在这种情况下是cmdstring--作为它的命令输入而不是从标准输入或从一个给定的文件读取。外壳解析这个 null终止的C字符串并把它分解为这个命令的单独的命令行参数。传给外壳的真实的命令字符串可以包含任何合法的外壳命令。例如,使用<和> 的输入和输出重定向可以被使用。

如果我们没有使用外壳来执行这个命令,但是自己尝试执行这个命令,那会更难。首先,我们想要调用execlp而不是execl,来使用PATH变量,就和 shell一样。我们也必须为execlp的调用把这个null终止的字符串分解为分隔的命令行参数。最终,我们不能使用任何外壳元字符。


注意我们调用_exit而不是exit。我们这样是避免任何通过fork从父进程拷贝到子进程的标准I/O缓冲被冲洗到子进程。


我们可以用下面的代码来测试这个版本的system。(pr_exit函数是8.5节定义的。)



  1. #include <sys/wait.h>

  2. void pr_exit(int status);
  3. int system(const char *cmdstring);

  4. int
  5. main(void)
  6. {
  7.     int status;

  8.     if ((status = system("date")) < 0) {
  9.         printf("system() error\n");
  10.     }
  11.     pr_exit(status);

  12.     if ((status = system("nosuchcommand")) < 0) {
  13.         printf("system() error\n");
  14.     }
  15.     pr_exit(status);

  16.     if ((status = system("who; exit 44")) < 0) {
  17.         printf("system() error\n");
  18.     }
  19.     pr_exit(status);

  20.     exit(0);
  21. }


程序运行结果:
$ ./a.out
2012年 03月 02日 星期五 23:51:25 CST
normal termination, exit status = 0
sh: nosuchcommand: not found
normal termination, exit status = 127
tommy    pts/0        2012-03-02 21:00 (:0)
normal termination, exit status = 44

使用system而不直接使用fork和exec的优点是,system处理所有的错误和(在我们10.18节的这个函数的版本里)所有需要的信号处理。


早期系统,包括SVR3.2和4.3BSD,没有waitpid函数可用。相反,父进程等待子进程,使用一个如下的语句:
while ((lastpid = wait(&status)) != pid && lastpid != -1)
  ;


如果调用system的进程在调用system之前产生了它自己的子进程,则会发生一个问题。因为上面的while语句持续循环,直到system产生的 子进程终止,如果进程的任何子进程在被pid标识的进程前终止,那么进程ID和其它进程的终止状态会被while语句舍弃掉。事实上,无法等待一个指定的 子进程是POSIX.1 Rationale引入waitpid函数的原因之一。我们将在15.3节看到发生popen和pclose函数上的同样的问题,如果系统没有提供一个 waitpid函数。


设置用户ID程序


如果我们从一个设置用户ID程序调用system会发生什么呢?这样做是一个安全漏洞,而且不该这样。下面的代码展示一个简单的程序,只为它的命令行参数调用system:



  1. #include <stdlib.h>

  2. void pr_exit(const char *status);

  3. int
  4. main(int argc, char *argv[])
  5. {
  6.     int status;
  7.     if (argc < 2) {
  8.         printf("command-line argument required\n");
  9.         exit(1);
  10.     }
  11.     if ((status = system(argv[1])) < 0) {
  12.         printf("system() error\n");
  13.         exit(1);
  14.     }
  15.     pr_exit(status);

  16.     exit(0);
  17. }


我们把这个程序编译成可执行文件tsys。

下面代码展示了另一个简单的程序,打印它的真实和有效用户ID:



  1. #include <stdio.h>

  2. int
  3. main(void)
  4. {
  5.     printf("real uid = %d, effective uid = %d\n", getuid(), geteuid());
  6.     exit(0);
  7. }



我们把这个程序编译成可执行文件printuids。运行这两个程序的结果为:
$ ./tsys ./printuids
real uid = 1000, effective uid = 1000
normal termination, exit status = 0
$ su
密码:
# chown root tsys
# chmod u+s tsys
# ls -l tsys
-rwsrwxr-x 1 root tommy 7334 2012-03-03 20:23 tsys
# exit
exit
$ ./tsys ./printuids
real uid = 1000, effective uid = 0
normal termination, exit status = 0

我们给tsys程序的超级用户权限通过system的fork和exec被得到了。

当/bin/sh是bash版本2时,前一个例子不会工作,因为bash将会把有效用户ID设置为真实用户ID,当它们不匹配时。


如果它用特殊权限运行时--设置用户ID或设置组ID--并想产生另一个进程,那么一个进程应该直接直接使用fork和exec,可以确保在调用exec前和fork后变回普通权限。system函数不应该从一个设置用户ID或设置组ID程序使用。


劝告的原因是system调用shell来解析命令字符串,shell使用它的IFS变量作为输入域的分隔。shell的早期版本当被调用时没有把这个变 量重置为一个普通的字符集。这允许一个恶意用户在system被调用前设置IFS,导致system来执行一个不同的程序。

阅读(1180) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~