UNIX环境编程学习笔记(22)——进程管理之system 函数执行命令行字符串

lienhua34
2014-10-15

ISO C 定义了 system 函数,用于在程序中执行一个命令字符串。其声明如下,

#include <stdlib.h>

int system(const char *cmdstring);

system 函数在其实现中调用了 fork、exec 和 waitpid 函数。system 函数调用 fork 函数创建子进程,然后由子进程调用’/bin/sh -c cmdstring’ 来执行命令行参数 cmdstring,此命令执行完后便返回调用的进程。在调用system 函数期间 SIGCHLD 信号会被暂时搁置,SIGINT 和 SIGQUIT 信号则会被忽略。

system 函数的返回值有点复杂,有下面几种情况,

1. 如果 cmdstring 是空指针,则仅当命令处理程序处理程序可用时,返回非 0 值。这一特征可用于确定一个操作系统是否支持 system 函数。UNIX 系统总是支持 system 函数的。

2. 如果 fork 失败或者 waitpid 返回除 EINTR 之外的出错,则 system返回 -1,而且 errno 中设置错误类型值。

3. 如果 exec 失败(表示不能执行 shell),则其返回值相当于 shell 执行了 exit(127) 后的终止状态。

4. 如果所有三个函数(fork、exec 和 waitpid)都执行成功,则 system的返回值是 shell 执行命令参数 cmdstring 后的终止状态。

通过上面对返回值的说明,我们发现当命令行参数 cmdstring 不为空指针时,我们可以通过判断返回值是否为 -1 来判定 system 函数是否成功。而其他情况,我们则可以像文档“获取进程终止状态的 wait 和 waitpid 函数”中说明的处理终止状态一样来处理 system 函数的返回值。下面我们看一下例子,

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>

extern void print_exit(int status);

int
main(void)
{
  int status;

  if ((status = system("date")) < 0) {
    printf("system() error: %s\n", strerror(errno));
    exit(-1);
  }
  print_exit(status);

  if ((status = system("nosuchcommand")) < 0) {
    printf("system() error: %s\n", strerror(errno));
    exit(-1);
  }
  print_exit(status);

  if ((status = system("who; exit 44")) < 0) {
    printf("system() error: %s\n", strerror(errno));
    exit(-1);
  }
  print_exit(status);

  exit(0);
}

void print_exit(int status)
{
  if (WIFEXITED(status)) {
    printf("normal termination, exit status = %d\n",
           WEXITSTATUS(status));
  } else if (WIFSIGNALED(status)) {
    printf("abnormal termination, signal number =%d\n",
           WTERMSIG(status));
  }
}

如上面程序所述,我们可以像处理进程终止状态一样来处理 system 函数的返回值。编译该程序,生成并执行文件 systemdemo,

lienhua34:demo$ gcc -o systemdemo systemdemo.c
lienhua34:demo$ ./systemdemo
2014年 10月 15日 星期三 23:15:28 CST
normal termination, exit status = 0
sh: 1: nosuchcommand: not found
normal termination, exit status = 127
lienhua34 tty7 2014-10-15 22:28
lienhua34 pts/0 2014-10-15 22:37 (:0.0)
lienhua34 pts/3 2014-10-15 23:10 (:0.0)
normal termination, exit status = 44
lienhua34:demo$

system 函数创建进程相对于 fork 与 exec 函数组合的优点是:system函数进行了所需的各种出错处理,以及各种信号处理。但是,system 函数也有其致命弱点:在设置用户 ID 程序中使用 system 函数会存在安全性漏洞。下面,我们来看个例子。

下面程序使用 system 函数执行第一个命令行参数。将该程序编译为可执行文件 tsys。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

int
main(int argc, char *argv[])
{
  int status;
  if (argc < 2) {
    printf("command-line argument required.\n");
    exit(-1);
  }
  if ((status = system(argv[1])) < 0) {
    printf("system error: %s\n", strerror(errno));
    exit(-1);
  }
  exit(0);
}

下面提供一个打印进程实际用户 ID 和有效用户 ID 的程序,将该程序编译为可执行文件 printuids。

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
int
main(void)
{
    printf("real uid=%d, effective uid=%d\n", getuid(), geteuid());
    exit(0);
}

运用这两个可执行文件,我们执行下面的操作,

lienhua34:demo$ ./tsys ./printuids
real uid=1000, effective uid=1000
lienhua34:demo$ su
# chown root tsys
# chmod u+s tsys
# ls -l tsys
-rwsrwxr-x 1 root lienhua34 7358 10月 15 23:37 tsys
# exit
exit
lienhua34:demo$ ./tsys ./printuids
real uid=1000, effective uid=0

在上面的执行过程中,我们将 tsys 文件的所有者设置为超级用户 root,并且设置了设置用户 ID 位。于是,执行 tsys 文件时,进程的有效用户 ID为 0(即 root)。而调用 system 函数执行 printuids 文件的子进程继承了这个有效用户 ID,于是子进程就拥有了很大的权限,可以执行任何可能会造成致命伤害的程序命令。这就是 system 函数存在的安全性漏洞。

如果一个进程以特殊权限(设置用户 ID 或设置组 ID)运行,它又想生成另一个进程执行另一个程序,则应当直接使用 fork 和 exec,而且在fork 之后、exec 之前要改回到普通权限。设置用户 ID 或设置组 ID 程序决不应调用 system 函数。

(done)

时间: 2024-10-25 18:30:03

UNIX环境编程学习笔记(22)——进程管理之system 函数执行命令行字符串的相关文章

UNIX环境编程学习笔记(20)——进程管理之exec 函数族

lienhua342014-10-07 在文档“进程控制三部曲”中,我们提到 fork 函数创建子进程之后,通常都会调用 exec 函数来执行一个新程序.调用 exec 函数之后,该进程就将执行的程序替换为新的程序,而新的程序则从 main 函数开始执行. UNIX 提供了 6 种不同的 exec 函数供我们使用.它们的原型如下所示, #include <unistd.h>int execl(const char *pathname, const char *arg0, ... /* (cha

UNIX环境编程学习笔记(21)——进程管理之获取进程终止状态的 wait 和 waitpid 函数

lienhua342014-10-12 当一个进程正常或者异常终止时,内核就向其父进程发送 SIGCHLD信号.父进程可以选择忽略该信号,或者提供一个该信号发生时即被调用的函数(信号处理程序).对于这种信号的系统默认动作是忽略它. 在文档“进程控制三部曲”中,我们讲的第三部曲是使用 wait 函数来获取终止子进程的终止状态.那么,有几个问题我们这里需要详细的学习一下. 1. 父进程一定能够获取到子进程的终止状态吗?如果子进程在父进程调用 wait 函数前就终止了,怎么办? 2. 如果父进程没有获

UNIX环境编程学习笔记(19)——进程管理之fork 函数的深入学习

lienhua342014-10-07 在“进程控制三部曲”中,我们学习到了 fork 是三部曲的第一部,用于创建一个新进程.但是关于 fork 的更深入的一些的东西我们还没有涉及到,例如,fork 创建的新进程与调用进程之间的关系.父子进程的数据共享问题等.fork 是否可以无限制的调用?如果不行的话,最大限制是多少?另外,我们还将学习一个 fork 的变体 vfork. 1 fork 创建的新进程与调用进程之间的关系 UNIX 操作系统中的所有进程之间的关系呈现一个树形结构.除了进程 ID

UNIX环境编程学习笔记(18)——进程管理之进程控制三部曲

lienhua342014-10-05 1 进程控制三部曲概述 UNIX 系统提供了 fork.exec.exit 和 wait 等基本的进程控制原语.通过这些进程控制原语,我们即可完成对进程创建.执行和终止等基本操作.进程的控制可以划分为三部曲, • 第一部:fork 创建新进程. • 第二部:exec 执行新程序. • 第三部:exit 和 wait 处理终止和等待终止. 2 第一部:fork 创建新进程 在一个现有的进程中,我们可以通过调用 fork 函数来创建一个新进程, #includ

UNIX环境编程学习笔记(17)——进程管理之进程的几个基本概念

lienhua342014-10-05 1 main 函数是如何被调用的? 在编译 C 程序时,C 编译器调用链接器在生成的目标可执行程序文件中,设置一个特殊的启动例程为程序的起始地址.当内核执行 C 程序时,在调用 main 前先调用这个特殊的启动例程,该启动例程从内核取得命令行参数和环境变量值. 2 共享库 共享库使得可执行文件中不再需要包含共用的库例程,而只需在所有进程都可引用的存储区中维护这种库例程的一个副本.程序第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链

UNIX环境编程学习笔记(23)——信号处理初步学习

lienhua342014-10-29 1 信号的概念 维基百科中关于信号的描述是这样的: 在计算机科学中,信号(英语:Signals)是 Unix.类 Unix 以及其他 POSIX 兼容的操作系统中进程间通讯的一种有限制的方式.它是一种异步的通知机制,用来提醒进程一个事件已经发生.当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断.如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数. 关于这段描述,我们可以从中学习到下面几点关于信号

UNIX环境编程学习笔记(6)——文件I/O之判断文件类型

lienhua342014-09-01 1 文件类型 我们平时最常接触的文件类型有普通文件(regular file)和目录(di-rectory file),但是 UNIX 系统提供了多种文件类型: (1) 普通文件(regular file) 这种文件包含了某种形式的数据,这些数据无论是文件还是二进制对于 UNIX 内核而言都是一样的.对普通文件内容的解释有处理该文件的应用程序进行. (2) 目录文件(directory file) 目录文件包含了其他文件的名字以及指向与这些文件有关信息的指

UNIX环境编程学习笔记(8)——文件I/O之校验当前登录用户对文件的访问权限

lienhua342014-09-03 通过前面一篇随笔(文件访问权限与进程访问控制),我们知道内核校验文件的访问权限使用的是进程的有效用户 ID 和有效组 ID.但有时我们需要知道当前登录用户对某个文件访问权限.虽然说进程的有效用户 ID 和有效组 ID 通常分别等于当前登录用户 ID 和用户所在组 ID.例如,一个进程可能因设置用户 ID 以另一个用户权限运行,它仍可能想验证当前实际登录的用户是否能否访问一个给定的文件. access 函数提供了按照实际用户 ID 和实际组 ID 进行访问权

UNIX环境编程学习笔记(16)——进程管理之进程环境变量

lienhua342014-10-03 1 环境表和环境指针 在每个进程启动时,都会接到一张环境表.环境表是一个字符指针数组,其中每个指针包含一个以 null 结束的 C 字符串的地址.全局变量environ 则包含了该指针数组的地址, extern char **environ; 例如,图 1 显示了包含有 5 个环境字符串的环境表, 图 1: 含有 5 个环境字符串的环境表 2 环境变量 环境字符串的形式通常为,name=value. ISO C 定义了一个函数 getenv,用于获取环境变