缓存时间:
2026/10/22 15:57
# 剖析 macOS 上的 gcc 二进制文件
来源: https://zoey-on-github.github.io/blog/macgcc.html
如果你在 Mac 上写过 C(或 C++),你就会知道所谓的 `gcc` 其实就是换了马甲的 clang。
```
$ gcc
clang: error: no input files
```
大多数人(合理地)认为这只是个别名,但事实并非如此!它是一个二进制程序。那么,这个二进制程序*究竟*是什么?我天真地以为它只是调用 `clang`,但结果远比这有趣。
```c
int sub_1000006c4(int arg0, int arg1) {
r19 = arg1;
r20 = arg0;
if (*_mainExecutableToolName.once == -0x1) {
if (*(int8_t *)*_mainExecutableToolName.toolName == 0x63 && *(int8_t *)(*_mainExecutableToolName.toolName + 0x1) == 0x63 && *(int8_t *)(*_mainExecutableToolName.toolName + 0x2) == 0x0) {
r21 = "clang";
} else {
if (strcmp(*_mainExecutableToolName.toolName, "llvm-gcc") == 0x0) {
r21 = "clang";
} else {
r21 = *_mainExecutableToolName.toolName;
if (strcmp(*_mainExecutableToolName.toolName, "c++") == 0x0 || strcmp(r21, "llvm-g++") == 0x0) {
r21 = "clang++";
}
}
xcselect_invoke_xcrun();
}
while (true) {
dispatch_once(_mainExecutableToolName.once, ___block_literal_global);
if (*(int8_t *)*_mainExecutableToolName.toolName == 0x63) {
if (*(int8_t *)(*_mainExecutableToolName.toolName + 0x1) == 0x63) {
if (*(int8_t *)(*_mainExecutableToolName.toolName + 0x2) != 0x0) {
if (strcmp(*_mainExecutableToolName.toolName, "llvm-gcc") == 0x0) {
r21 = "clang";
} else {
if (strcmp(*_mainExecutableToolName.toolName, "c++") != 0x0) {
r21 = *_mainExecutableToolName.toolName;
if (strcmp(*_mainExecutableToolName.toolName, "llvm-g++") == 0x0) {
r21 = "clang++";
}
} else {
r21 = "clang++";
}
}
xcselect_invoke_xcrun();
} else {
xcselect_invoke_xcrun();
}
} else {
if (strcmp(*_mainExecutableToolName.toolName, "llvm-gcc") == 0x0) {
r21 = "clang";
} else {
if (strcmp(*_mainExecutableToolName.toolName, "c++") != 0x0) {
r21 = *_mainExecutableToolName.toolName;
if (strcmp(*_mainExecutableToolName.toolName, "llvm-g++") == 0x0) {
r21 = "clang++";
}
} else {
r21 = "clang++";
}
}
xcselect_invoke_xcrun();
}
} else {
if (strcmp(*_mainExecutableToolName.toolName, "llvm-gcc") == 0x0) {
r21 = "clang";
} else {
if (strcmp(*_mainExecutableToolName.toolName, "c++") != 0x0) {
r21 = *_mainExecutableToolName.toolName;
if (strcmp(*_mainExecutableToolName.toolName, "llvm-g++") == 0x0) {
r21 = "clang++";
}
} else {
r21 = "clang++";
}
}
xcselect_invoke_xcrun();
}
continue;
}
}
}
```
这里有一些有趣的地方(比如专门检查是否以 c++ 或 llvm-gcc 方式运行的特殊逻辑),但我们真正关心的是这一行代码:`xcselect_invoke_xcrun()`。这是来自 `libxcselect.dylib` 的一个函数,但它的行为基本等同于运行 `xcrun`。(有趣的是,`xcrun` 二进制程序本身基本上就是调用了这个函数——https://zoey-on-github.github.io/blog/images/Screenshot_2026-10-21_at_11.38.00_PM.png)
如果你反汇编任何通过 Xcode 安装的开发者工具,你会看到完全相同的代码——https://zoey-on-github.github.io/blog/images/Screenshot_2026-10-21_at_12.57.05_PM.png——这意味着 `xcode-select` 在 macOS 上安装的所有开发者工具实际上都是通过 `xcrun` 来运行的。¹
但这引出了一个重要问题:`xcrun` *到底*是做什么的?在此之前我确实从未听说过它。查看[手册页](https://keith.github.io/xcode-man-pages/xcrun.1.html)后可以看到,它的功能是调用开发者工具目录中的开发者工具。这给了我们一条重要信息:*真正的* gcc 二进制文件的位置。幸运的是,有一个 `--find` 命令可以精确地告诉我们答案。
```
$ xcrun --find gcc
/Library/Developer/CommandLineTools/usr/bin/gcc
```
查看这个目录,我们可以看到:
- `c++`、`llvm-gcc`、`llvm-g++` 并不在这个目录中,这就是为什么它们被硬编码在二进制程序内部
- `cc` 和 `clang++` 只是 `clang` 的别名
但最有趣的是,`g++` 是指向同一目录中一个真正的 gcc 二进制文件的别名。
那么,*那个* gcc 二进制文件又是做什么的呢?用我的反汇编器打开它,我们发现它……调用 `xcrun` 并执行 `clang`(或根据调用方式选择 `clang++`)。
```c
int EntryPoint(int arg0, int arg1) {
r22 = arg1;
r21 = arg0;
asm { sbfiz x8, x21, #0x3, #0x20 };
r0 = malloc(r8 + 0x10);
if (r0 == 0x0) {
err(0x47, r1);
}
r19 = r0;
*r0 = "/usr/bin/xcrun";
r23 = *r22;
r0 = strlen(r23);
if (*(int8_t *)(r23 + (r0 - 0x1 << r64)) == 0x2b && *(int8_t *)(r23 + (r0 - 0x2 << r64)) == 0x2b) {
r8 = "clang++";
} else {
r8 = "clang";
}
r19[0x1] = r8;
if (r21 >= 0x2) {
asm { ubfiz x2, x8, #0x3, #0x20 };
memcpy(r19 + 0x10, r22 + 0x8, r2);
r8 = r21;
} else {
r8 = 0x1;
}
*(r19 + r8 * 0x8 + 0x8) = 0x0;
r0 = execv("/usr/bin/xcrun", r19);
err(0x47, r1);
return r0;
}
```
---
1. 如果你好奇*为什么会*这样,我的猜测是因为 SIP(系统完整性保护)。`/usr/bin` 在 macOS 上是受保护目录,我猜 Apple 不重启就无法更新它,而更新 Xcode 工具不需要重启 Mac。至于为什么不直接把开发者工具目录添加到 PATH 中,我的猜测是他们希望随时可以更改这个目录的自由。[↩︎](https://zoey-on-github.github.io/blog/macgcc.html#fnref1)