从函数调用到进程替换:使用 execl、CMake 与 PowerShell 构建一个多可执行程序

本文从一个具体问题出发:同一个项目中存在两套名称相同、实现不同的 main()use()and(),如何根据输入条件选择其中一套执行?文章将依次说明普通函数调用、编译链接、execl() 进程替换、CMake 多目标构建,以及 PowerShell 调用运算符 & 的原理。

一、问题背景

假设项目中有两套 C 程序。

第一套位于根目录:

复制代码
main.c -> use.c -> and.c

第二套位于 to 目录:

复制代码
to/main.c -> to/use.c -> to/and.c

两套程序中的函数名完全相同,但函数内部可以执行不同逻辑。项目还需要一个入口程序 in.c:读取整数 c,当 c > 3 时执行根目录程序,否则执行 to 目录中的程序。

最终目录如下:

复制代码
move/
├── CMakeLists.txt
├── build.sh
├── in.c
├── main.c
├── use.c
├── and.c
└── to/
    ├── main.c
    ├── use.c
    └── and.c

这个项目可以称为:

  • 多可执行程序 C 项目;

  • CMake 多目标项目;

  • 进程启动器或进程调度器示例。

理解它的关键,是先区分函数、可执行程序和进程。

二、函数、可执行程序与进程并不是一回事

1. 函数

函数是程序内部的一段代码。调用 use() 时,CPU 跳转到 use() 对应的指令,执行结束后再返回调用位置。

根目录程序的调用链为:

复制代码
main() -> use() -> and()

这三个函数始终运行在同一个进程内。函数调用不会启动一个新程序,也不会创建新的独立地址空间。

2. 可执行程序

C 源文件不能直接由操作系统执行。它们需要经过预处理、编译、汇编和链接,最后形成可执行文件:

复制代码
.c 源文件
  -> 预处理
  -> 编译
  -> 汇编
  -> .o 目标文件
  -> 链接
  -> .exe 可执行文件

根目录的三个源文件可以生成一个程序:

复制代码
gcc main.c use.c and.c -o main.exe

to 目录中的三个源文件生成另一个程序:

复制代码
gcc to/main.c to/use.c to/and.c -o to/main.exe

3. 进程

进程是可执行程序的一次运行实例。磁盘上的 main.exe 是文件;运行后的 main.exe 才是进程。

普通函数调用发生在一个进程内部,而 execl() 操作的是整个进程:它使用另一个可执行程序替换当前进程正在运行的程序。

三、为什么两套同名函数不会冲突

根目录和 to 目录中都定义了 main()use()and()。这些名称能够共存,是因为两套源码分别链接成两个可执行程序。

复制代码
main.c + use.c + and.c
          |
          +----> main.exe
​
to/main.c + to/use.c + to/and.c
          |
          +----> to/main.exe

每个可执行程序都有自己的符号表。根目录 main.exe 中的 use()to/main.exe 中的 use() 不属于同一个链接单元,因此不会冲突。

如果把六个源文件一次性链接:

复制代码
gcc main.c use.c and.c to/main.c to/use.c to/and.c -o wrong.exe

链接器会发现多个 mainuseand 定义,并报告重复符号错误。这里需要的不是重命名函数,而是建立两个独立的构建目标。

四、使用 execl() 选择目标程序

1. execl() 的参数结构

execl() 的基本形式为:

复制代码
execl(program_path, argv0, arg1, arg2, ..., NULL);

参数含义如下:

参数 作用
program_path 要执行的可执行文件路径
argv0 目标程序收到的 argv[0],通常填写程序名
arg1arg2 传给目标程序的命令行参数
NULL 表示可变参数列表结束,不能省略

execl() 执行的是编译后的程序,而不是 .c 源文件。因此不能写成:

复制代码
execl("./main.c", "main.c", (char *)NULL); /* 错误思路 */

必须先把源码构建为 main.exe,再执行它。

2. 启动程序的实现

本项目的 in.c 如下:

复制代码
#include <stdio.h>
#include <process.h>

int main(void)
{
    int c;

    printf("input: ");
    fflush(stdout);
    if (scanf("%d", &c) != 1) {
        fputs("invalid input\n", stderr);
        return 1;
    }

    if (c > 3)
        execl("./main.exe", "main.exe", "root-argument", (char *)NULL);
    else
        execl("./to/main.exe", "main.exe", "to-argument-1",
              "to-argument-2", (char *)NULL);

    perror("execl");
    return 1;
}

程序首先读取 c。输入无法转换为整数时,scanf() 返回值不是 1,程序输出错误并结束。

c > 3 时执行:

复制代码
execl("./main.exe", "main.exe", "root-argument", (char *)NULL);

根目录程序收到:

复制代码
argc = 2
argv[0] = "main.exe"
argv[1] = "root-argument"

c <= 3 时执行:

复制代码
execl("./to/main.exe", "main.exe",
      "to-argument-1", "to-argument-2", (char *)NULL);

to/main.exe 收到:

复制代码
argc = 3
argv[0] = "main.exe"
argv[1] = "to-argument-1"
argv[2] = "to-argument-2"

3. execl() 成功后为什么不返回

execl() 不是普通函数调用。调用成功后,当前进程的程序代码、数据和调用栈会被目标程序替换。

复制代码
in.exe 读取 c
    |
    +-- c > 3  ----> main.exe
    |
    +-- c <= 3 ----> to/main.exe

因此,调用成功时不会继续执行 in.cexecl() 后面的代码。只有调用失败时,程序才会执行:

复制代码
perror("execl");
return 1;

perror() 会根据系统保存的错误码输出原因,例如目标文件不存在或路径错误。

五、两个 main() 如何接收不同参数

两个程序都使用标准入口形式:

复制代码
int main(int argc, char *argv[])

这里不需要发明两种不同的 main() 函数签名。真正不同的是 execl() 传入的参数数量和内容。

根目录 main.c 接收一个业务参数:

复制代码
#include <stdio.h>

void use(void);

int main(int argc, char *argv[])
{
    if (argc != 2)
        return 1;

    puts("move/main.c");
    printf("argument: %s\n", argv[1]);
    use();
    return 0;
}

to/main.c 接收两个业务参数:

复制代码
#include <stdio.h>

void use(void);

int main(int argc, char *argv[])
{
    if (argc != 3)
        return 1;

    puts("move/to/main.c");
    printf("arguments: %s, %s\n", argv[1], argv[2]);
    use();
    return 0;
}

argc 包含 argv[0],所以一个业务参数对应 argc == 2,两个业务参数对应 argc == 3

六、用 CMake 管理三个可执行程序

手动执行三条 GCC 命令可以完成构建,但随着文件数量增加,命令会变得难以维护。CMake 的作用是描述"哪些源文件属于哪个目标",再生成底层构建工具需要的文件。

本项目的 CMakeLists.txt 如下:

复制代码
cmake_minimum_required(VERSION 3.15)
project(move_example C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_executable(move_main main.c use.c and.c)
set_target_properties(move_main PROPERTIES
    OUTPUT_NAME main
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)

add_executable(to_main to/main.c to/use.c to/and.c)
set_target_properties(to_main PROPERTIES
    OUTPUT_NAME main
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/to"
)

add_executable(in in.c)
set_target_properties(in PROPERTIES
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)

1. CMake 目标名与输出文件名

下面两条语句定义了两个不同的 CMake 目标:

复制代码
add_executable(move_main main.c use.c and.c)
add_executable(to_main to/main.c to/use.c to/and.c)

move_mainto_main 是 CMake 内部使用的逻辑目标名,必须不同。通过 OUTPUT_NAME main,它们最终都可以输出为 main.exe

两个文件不会相互覆盖,因为输出目录不同:

复制代码
move_main -> build/main.exe
to_main   -> build/to/main.exe
in        -> build/in.exe

这个输出结构与 in.c 中的路径严格对应:

复制代码
"./main.exe"
"./to/main.exe"

2. CMake 的两个阶段

CMake 通常分为"配置与生成"和"构建"两个阶段。

配置与生成:

复制代码
cmake -S . -B build -G "MinGW Makefiles"
  • -S .:源码目录是当前目录;

  • -B build:生成文件和构建产物放在 build

  • -G "MinGW Makefiles":使用 MinGW Makefiles 生成器。

执行构建:

复制代码
cmake --build build

CMake 本身通常不直接编译 C 源码。它生成 Makefile,再由 mingw32-make 根据 Makefile 调用 GCC 和链接器。

七、用 build.sh 自动完成构建

当前 build.sh 将配置、构建和运行组合起来:

复制代码
#!/usr/bin/env sh
set -eu

root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)

cmake -S "$root" -B "$root/build" -G "MinGW Makefiles"
cmake --build "$root/build"

cd "$root/build"
./in.exe

set -e 表示某条命令失败后立即停止,避免构建失败后仍然尝试运行旧程序。set -u 表示读取未定义变量时立即报错。

下面这行取得脚本所在目录的绝对路径:

复制代码
root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)

脚本最后切换到 build 目录再运行 in.exe。这一步非常重要,因为 execl() 使用的是相对路径。程序查找 ./main.exe 时,依据的是当前工作目录,而不是 in.exe 文件自身所在目录。

八、为什么不能使用 gcc build.sh

以下命令是错误的:

复制代码
gcc build.sh

GCC 用于处理 C 源文件、汇编文件、目标文件和库。build.sh 是 Shell 脚本,需要由 Shell 解释器逐行执行。

把脚本交给 GCC 后,链接器会尝试把文本内容当作目标文件或链接脚本,因此出现:

复制代码
file format not recognized
syntax error

正确思路是:

复制代码
bash 解释 build.sh
  -> build.sh 调用 CMake
  -> CMake 调用构建工具
  -> 构建工具调用 GCC

九、PowerShell 为什么需要 &

在 PowerShell 中,带引号的路径只是一个字符串:

复制代码
"C:\Program Files\Git\bin\bash.exe"

引号解决了路径中包含空格的问题,但字符串不会自动变成命令。PowerShell 的调用运算符 & 用于把后面的字符串或变量内容作为命令执行:

复制代码
& "C:\Program Files\Git\bin\bash.exe" ./build.sh

PowerShell 会把它解析为:

复制代码
可执行程序:C:\Program Files\Git\bin\bash.exe
参数:      ./build.sh

如果不使用引号:

复制代码
C:\Program Files\Git\bin\bash.exe ./build.sh

PowerShell 会在空格处截断路径,并尝试执行 C:\Program,所以出现"无法将 C:\Program 识别为命令"的错误。

如果只使用引号而不使用 &,PowerShell 得到的仍然只是一个字符串。两者的职责不同:

  • 引号保证含空格的路径被视为一个整体;

  • & 明确要求 PowerShell 执行这个路径指向的程序。

调用运算符也可以执行变量保存的程序路径:

复制代码
$bash = "C:\Program Files\Git\bin\bash.exe"
& $bash ./build.sh

需要注意,PowerShell 开头的 & 表示"调用"。Linux Shell 中放在命令末尾的 & 表示"后台运行"。二者只是符号相同,语义并不相同。

十、Git Bash、WSL 和 PowerShell 的环境差异

Windows 上可能同时存在多个名字相近的 Bash,但它们不是同一个环境。

1. Git Bash

Git Bash 是 Git for Windows 附带的类 Unix 命令环境。应使用公开入口:

复制代码
& "C:\Program Files\Git\bin\bash.exe" ./build.sh

不建议直接调用:

复制代码
& "C:\Program Files\Git\usr\bin\bash.exe" ./build.sh

usr/bin/bash.exe 是内部组件。绕过正常入口可能导致环境没有正确初始化,随后出现:

复制代码
dirname: command not found

2. WSL

下面的路径通常会启动 Windows Subsystem for Linux(WSL):

复制代码
C:\Users\30126\AppData\Local\Microsoft\WindowsApps\bash.exe

WSL 中运行的是 Linux 版 CMake。Linux 版 CMake 不提供 Windows 专用的 MinGW Makefiles 生成器,因此会报告:

复制代码
CMake Error: Could not create named generator MinGW Makefiles

当前 in.c 还使用了 MinGW/Windows 的 <process.h>.exe 文件名,所以当前版本应在 Windows PowerShell 或 Git Bash 环境构建,不应直接放到 WSL 原生构建。

3. PowerShell

PowerShell 可以直接执行 Windows 版 CMake,无须经过 Bash:

复制代码
cmake -S . -B build -G "MinGW Makefiles"
cmake --build build
Set-Location build
.\in.exe

build.sh 的价值是把这些步骤自动化;它不是完成 CMake 构建的唯一入口。

十一、准备构建环境

在 PowerShell 中检查工具:

复制代码
Get-Command cmake
Get-Command gcc
Get-Command mingw32-make
Test-Path "C:\Program Files\Git\bin\bash.exe"

当前构建方式需要:

  • Windows 版 CMake;

  • MinGW GCC;

  • mingw32-make

  • Git Bash,仅在执行 build.sh 时需要。

如果系统没有 CMake,可以使用 Windows Package Manager 安装:

复制代码
winget install --id Kitware.CMake -e

安装完成后重新打开 PowerShell,使新的 PATH 生效,再检查:

复制代码
cmake --version

使用 Git Bash 自动构建:

复制代码
& "C:\Program Files\Git\bin\bash.exe" ./build.sh

或者直接在 PowerShell 中构建:

复制代码
cmake -S . -B build -G "MinGW Makefiles"
cmake --build build
Set-Location build
.\in.exe

十二、运行结果

输入大于 3 的值,例如 5

复制代码
input: 5
move/main.c
argument: root-argument
move/use.c: use()
move/and.c: and()

输入不大于 3 的值,例如 2

复制代码
input: 2
move/to/main.c
arguments: to-argument-1, to-argument-2
move/to/use.c: use()
move/to/and.c: and()

这证明 in.exe 根据条件选择了不同程序,并向两个 main() 传入了不同数量的参数。

十三、常见错误及定位方法

错误现象 根本原因 解决方法
file format not recognized 使用 GCC 处理了 Shell 脚本 使用 Bash 执行 build.sh
无法识别 C:\Program 路径包含空格但未正确调用 使用 & "完整路径" 参数
dirname: command not found 直接启动 Git 内部 Bash,环境未初始化 使用 Git\bin\bash.exe
cmake: command not found CMake 未安装或不在 PATH 安装 CMake 并重启终端
不支持 MinGW Makefiles 在 WSL/Linux 中使用了 Windows 生成器 改用 Windows CMake
execl: No such file or directory 工作目录不正确或目标程序未构建 build 目录运行并检查输出文件
重复定义 mainuseand 两套源码被链接到同一个目标 为两套源码定义不同 CMake 目标

十四、进一步理解几个容易忽略的细节

1. 为什么调用 fflush(stdout)

下面的提示没有换行符:

复制代码
printf("input: ");

输出内容可能暂存在缓冲区中。调用:

复制代码
fflush(stdout);

可以确保提示在程序等待输入之前显示。程序即将执行 exec 类操作时,主动刷新需要保留的输出也是稳妥做法。

2. 为什么声明和定义必须一致

如果 use.c 中声明:

复制代码
void and(void);

那么 and.c 中也应定义为:

复制代码
void and(void)

一处声明为 void、另一处定义为 int 会造成类型不一致。小型示例可以直接声明;正式项目通常把共享声明放入 .h 头文件,避免多个源文件各自维护声明。

3. execl()execlp() 的区别

execl() 使用给定路径:

复制代码
execl("./main.exe", "main.exe", (char *)NULL);

execlp() 可以根据 PATH 环境变量搜索程序。当前项目需要明确区分根目录和 to 目录中的两个 main.exe,因此指定路径更合适。

4. Windows 与 POSIX 的可移植性

在 POSIX 系统中,execl() 通常由 <unistd.h> 声明;当前 MinGW/Windows 示例使用 <process.h>。文件扩展名、头文件和构建生成器都与平台有关。如果要让同一份源码同时支持 Windows 和 Linux,需要通过条件编译处理这些差异,而不能只修改 Shell 命令。

十五、什么时候应该采用这种结构

这种结构适合:

  • 根据配置或输入启动不同工具;

  • 启动器完成参数验证后切换到实际程序;

  • 一个仓库需要构建客户端、服务端和管理程序;

  • 不同功能必须拥有独立的进程和地址空间;

  • 需要分别部署、测试或替换不同程序。

如果两个分支只是执行少量不同计算,直接调用函数通常更合理:

复制代码
if (c > 3)
    root_use();
else
    to_use();

进程替换增加了构建目标、路径、参数传递和错误处理成本。只有确实需要独立程序边界时,才应使用 execl()

十六、结语

这个示例连接了 C 项目中几个经常被分开学习的概念。

main() -> use() -> and() 展示的是同一进程内的函数调用;两套同名函数通过独立链接目标形成两个可执行程序;execl() 根据输入条件使用其中一个程序替换启动器;CMake 负责描述三个目标及其输出位置;build.sh 将构建步骤自动化;PowerShell 的 & 则负责调用路径中包含空格的 Bash 程序。

完整调用链可以概括为:

复制代码
PowerShell 的 &
  -> 启动 Git Bash
  -> Git Bash 解释 build.sh
  -> build.sh 调用 CMake
  -> CMake 调用 MinGW 构建工具
  -> 构建工具调用 GCC
  -> 生成 in.exe 和两个 main.exe
  -> in.exe 使用 execl() 选择目标程序
  -> 目标 main() 调用 use()
  -> use() 调用 and()

理解这条链路后,命令解释、构建生成、编译链接、进程替换和函数调用之间的边界就会清晰:每一层只负责自己的任务,上一层通过明确的接口驱动下一层。

附录:完整项目源码

以下代码与本文对应,可以按照开头的目录结构分别保存。

1. in.c

复制代码
#include <stdio.h>
#include <process.h>

int main(void)
{
    int c;

    printf("input: ");
    fflush(stdout);
    if (scanf("%d", &c) != 1) {
        fputs("invalid input\n", stderr);
        return 1;
    }

    if (c > 3)
        execl("./main.exe", "main.exe", "root-argument", (char *)NULL);
    else
        execl("./to/main.exe", "main.exe", "to-argument-1",
              "to-argument-2", (char *)NULL);

    perror("execl");
    return 1;
}

2. 根目录 main.c

复制代码
#include <stdio.h>

void use(void);

int main(int argc, char *argv[])
{
    if (argc != 2)
        return 1;

    puts("move/main.c");
    printf("argument: %s\n", argv[1]);
    use();
    return 0;
}

3. 根目录 use.c

复制代码
#include <stdio.h>

void and(void);

void use(void)
{
    puts("move/use.c: use()");
    and();
}

4. 根目录 and.c

复制代码
#include <stdio.h>

void and(void)
{
    puts("move/and.c: and()");
}

5. to/main.c

复制代码
#include <stdio.h>

void use(void);

int main(int argc, char *argv[])
{
    if (argc != 3)
        return 1;

    puts("move/to/main.c");
    printf("arguments: %s, %s\n", argv[1], argv[2]);
    use();
    return 0;
}

6. to/use.c

复制代码
#include <stdio.h>

void and(void);

void use(void)
{
    puts("move/to/use.c: use()");
    and();
}

7. to/and.c

复制代码
#include <stdio.h>

void and(void)
{
    puts("move/to/and.c: and()");
}

8. CMakeLists.txt

复制代码
cmake_minimum_required(VERSION 3.15)
project(move_example C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_executable(move_main main.c use.c and.c)
set_target_properties(move_main PROPERTIES
    OUTPUT_NAME main
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)

add_executable(to_main to/main.c to/use.c to/and.c)
set_target_properties(to_main PROPERTIES
    OUTPUT_NAME main
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/to"
)

add_executable(in in.c)
set_target_properties(in PROPERTIES
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)

9. build.sh

复制代码
#!/usr/bin/env sh
set -eu
​
root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
​
cmake -S "$root" -B "$root/build" -G "MinGW Makefiles"
cmake --build "$root/build"
​
cd "$root/build"
./in.exe
相关推荐
奶油话梅糖1 小时前
锐捷常用命令速查表
java·网络·windows
Dlrb12111 小时前
信息查询Web服务器-->电子商城信息查询项目
linux·服务器·数据库·html·嵌入式·epoll·数据查询
纳兰青华1 小时前
在 PVE 上通过 LXC 部署 Ubuntu 24.04:为 OpenClaw 打造轻量高效的 AI 环境
linux·ai编程
love530love2 小时前
CodexPro + MCP + Cloudflare 配置详解:HTTP2、QUIC 与连接排障
ide·人工智能·windows·ai agent
kyle~2 小时前
Schema---数据结构的形式化规则描述
数据结构·windows·microsoft
Auv开心2 小时前
Ubuntu 20.04 默认视频播放器(Totem)无法播放 MP4 文件,通常是因为缺少多媒体编解码器。以下是几种解决方法:
linux·ubuntu·音视频
天空'之城2 小时前
Linux 系统编程 15:UDP 编程
linux·系统编程·udp协议
utf8mb4安全女神2 小时前
如果复制windows 内容到 Linux,带入不可见特殊符,怎么解决
linux·运维·服务器
艾德金的溪2 小时前
【windows安装使用openclaw】
运维·windows·ai