system()、exec族

system()、exec族

1. 如何在程序中执行 Shell 命令

1.1 背景与目标

  • bash 环境下,可以直接运行命令(如 ls, cd)或程序。
  • 目标:在自己的 C 语言程序中,实现类似 bash 的功能,即执行任意的 shell 命令。

1.2 system 函数接口

  • 功能:执行一条 shell 命令。
  • 头文件stdlib.h
  • 基本用法
    • 类似于在 bash 中操作,通过 printf 打印命令提示符。
    • 获取用户输入数据。
    • 将获取到的字符串传递给 system 函数执行。

1.3 输入获取方式的选择

  • scanf 的问题
    • scanf 遇到空格会停止读取,认为空格是分隔符。
    • 例如输入 cat system.cscanf 可能只读取 cat,导致命令执行不完整。
  • 解决方案 fgets
    • 函数原型:fgets(buffer, size, stdin)
    • 参数说明:
      1. 缓冲区地址。
      2. 大小(如 1024)。
      3. 文件流(标准输入 stdin)。
    • 优势:可以读取包含空格的一整行命令。

1.4 初步实现逻辑

  1. 进入死循环(while(1)),持续等待命令。
  2. 打印提示符(printf)。
  3. 使用 fgets 获取用户输入到 buffer
  4. 调用 system(buffer) 执行命令。
  5. 注意system 执行完毕后通常会刷新缓冲区,无需手动 fflush

1.5 命令执行效果测试

  • ls:正常显示文件列表。
  • cd
    • bashcd 会切换当前目录。
    • system 调用的子 shell 中,cd 只会改变子进程目录,父进程(当前程序)目录不变。
  • cat system.c
    • 若使用 scanf,可能无法正确显示文件内容。
    • 若使用 fgets,可以正确显示文件内容。
  • 进程终止测试
    • 按下 Ctrl+C 终止的是当前执行的命令(如 cat),而不是主程序。
    • 再次按下 Ctrl+C 才会终止主程序。

2. system 函数的底层逻辑与安全风险

2.1 底层执行机制

  • 进程创建system 函数底层会启动一个 /bin/sh 进程。
  • 验证方法
    1. 运行包含 system("sleep 20") 的程序。
    2. 在另一个终端使用 ps -ef | grep sleep 查看进程。
    3. 观察到进程树:主进程 -> sh -c sleep 20 -> sleep 20
  • 结论system 实际上是 fork 了一个子进程,然后在子进程中运行 shell 解释器来执行命令。

2.2 安全风险(命令注入)

  • 风险来源system 支持 shell 语法,包括命令连接符(如 ;, |, &)。
  • 示例代码
    char cmd[1024];
    // 假设用户输入文件名
    fgets(cmd, sizeof(cmd), stdin);
    // 拼接命令
    char final_cmd[1024];
    sprintf(final_cmd, "cat %s", cmd); 
    system(final_cmd);
    
  • 攻击场景
    • 正常输入:system.c -> 执行 cat system.c
    • 恶意输入:system.c; rm -rf /*
    • 实际执行:cat system.c; rm -rf /*
    • 后果:不仅显示了文件,还执行了删除命令,造成严重破坏。
  • 结论system 函数存在命令注入风险,不适合用于执行不可信输入。若需执行特定程序,应使用 exec 函数族。

3. exec 函数族详解

3.1 exec 函数族概述

  • 目的:替换当前进程映像为一个新的程序。
  • 特点
    • 直接执行指定程序,不经过 shell 解析,安全性更高。
    • 不支持 shell 语法(如通配符、管道),只能执行单一程序。
    • 一旦执行成功,当前进程代码被替换,后续代码不再执行(除非出错)。
  • 头文件unistd.h

3.2 函数命名规则与参数差异

exec 族函数通过后缀字母区分参数传递方式和特性:

  1. l (list)
    • 参数逐个列出,以 NULL 结尾。
    • 示例:execl(path, arg0, arg1, ..., NULL)
  2. v (vector)
    • 参数通过字符串数组传递。
    • 示例:execv(path, argv[])
  3. p (path)
    • 自动在 PATH 环境变量中查找可执行文件。
    • 参数只需提供文件名,无需完整路径。
    • 示例:execlp("ls", "ls", "-l", NULL)
  4. e (environment)
    • 允许显式传递环境变量数组。
    • 示例:execle(path, arg0, ..., NULL, envp[])

1737012296247-8c3a759a-d1ec-4733-bac8-21b2a1287b8b.png

3.3 参数细节

  • 参数列表
    • 第一个参数通常是程序路径(p 后缀除外)。
    • 第二个参数通常是程序名(约定俗成,对应 argv[0])。
    • 后续为实际参数。
    • 最后必须以 NULL(空指针)结尾。
  • 环境变量 envp
    • 格式:字符串数组,每个元素为 KEY=VALUE
    • 例如:"USER=linux", "SHELL=/bin/bash", NULL
    • 若使用带 e 的函数,新进程将使用指定的环境变量,不再继承父进程环境变量。

3.4 返回值与执行流

  • 成功:不返回,当前进程被新程序完全替换(进程 ID 不变,代码段、数据段等变为新程序)。
  • 失败:返回 -1,并设置 errno
  • 代码逻辑
    exec(...);
    perror("exec failed"); // 只有执行失败才会运行到这行
    

3.5 环境变量继承

  • 默认行为exec 族函数(不带 e)默认继承父进程的环境变量。
  • 进程关系
    • 父进程(如 bash)创建子进程。
    • 子进程继承父进程的环境变量(如 PATH, USER, SHELL)。
    • 使用 exec 替换后,新进程依然保留这些继承的环境变量。
  • 查看环境变量
    • 命令:printenvenv
    • 代码中可通过 extern char **environ; 访问。

3.6 示例代码对比

  • execl

    execl("/bin/pwd", "pwd", NULL);
    

    需指定完整路径 /bin/pwd

  • execlp

    execlp("ls", "ls", "-l", NULL);
    

    无需指定路径,自动查找 PATH

  • execv

    char *args[] = {"ls", "-l", NULL};
    execv("/bin/ls", args);
    

    使用数组传递参数。


4. 综合案例:自定义实现 system 函数

4.1 实现思路

要模仿标准库的 system 函数,需要组合使用多进程编程的三个核心接口:

  1. fork:创建子进程。
  2. exec:在子进程中替换映像,执行 shell 命令。
  3. wait/waitpid:父进程等待子进程结束,并获取退出状态。

4.2 逻辑步骤

  1. 定义函数int my_system(const char *cmd)
  2. 创建子进程
    • 调用 fork()
    • 若返回 -1,创建失败,打印错误并返回。
    • 若返回 0,进入子进程逻辑。
    • 若返回 >0,进入父进程逻辑。
  3. 子进程逻辑
    • 调用 execl("/bin/sh", "sh", "-c", cmd, NULL)
    • 解释:启动 sh,使用 -c 参数执行传入的字符串命令。
    • execl 返回,说明执行失败,调用 perror 并退出子进程。
  4. 父进程逻辑
    • 调用 waitpid() 等待子进程结束。
    • 获取子进程的退出状态。
    • 处理状态码(正常退出或信号终止)。
    • 将状态返回给调用者。

4.3 代码实现细节

  • 头文件依赖
    • stdlib.h (system, exit)
    • stdio.h (printf, fgets)
    • unistd.h (fork, execl)
    • sys/types.h, sys/wait.h (waitpid, status macros)
  • 状态处理宏
    • WIFEXITED(status):判断是否正常退出。
    • WEXITSTATUS(status):获取退出码。
    • WIFSIGNALED(status):判断是否被信号终止。
    • WTERMSIG(status):获取终止信号号。

4.4 验证测试

  • 编译代码:gcc my_system.c -o my_system
  • 运行测试:
    • 输入 ls:应显示文件列表。
    • 输入 pwd:应显示当前路径。
    • 输入复杂命令:验证 sh -c 是否正确解析。
  • 结果:自定义的 my_system 功能与标准 system 基本一致。

4.5 注意事项

  • 头文件包含:某些类型定义(如 pid_t)可能隐含在其他头文件中,但建议显式包含 sys/types.h 以确保兼容性。
  • 错误处理forkexec 失败时需谨慎处理,避免进程状态异常。
  • 返回值waitpid 的返回值和状态码需正确传递,以便上层调用者判断命令执行结果。

5. 总结

  • system 函数:方便但存在安全风险(命令注入),底层通过 sh -c 执行。
  • exec 函数族:更安全,直接执行程序,支持多种参数传递方式(l, v, p, e)。
  • 进程替换exec 成功则不返回,当前进程映像被替换。
  • 环境变量:默认继承父进程,可通过 exec...e 系列函数自定义。
  • 组合应用:通过 fork + exec + wait 可以实现灵活的进程控制和命令执行逻辑,是理解 Linux 多进程编程的核心案例。
进程状态与管理 2026-03-06

评论区