返回

文章详情

编写一个(有效的)没有 main() 的 C 程序

Hacker News2026年7月25日 16:21

本教程由社区作者创建。社区内容由 iximiuz Labs 团队按最佳努力进行审核。编程教程,Linux 更新于:2026 年 7 月 25 日 编写一个(有效的)没有 main() 的 C 程序 了解 C 编译流程:预处理、编译、汇编和链接。首先从一个普通的 Hello World 开始,检查宏和生成的汇编代码,最后生成一个没有 main() 函数的可执行文件。 为什么选择 C?大多数编程语言的执行从 main() 函数开始。Go 有 func main(),Java 有 public static void main(),C 有 int main()。那么为什么我选择 C 作为本教程的语言?C 是在不编写汇编代码的情况下最接近操作系统的语言。Linux 内核本身就是用 C 编写的,每个程序所依赖的系统调用接口都是基于 C 的约定设计的。当你学习 C 程序是如何编译、链接和执行的时候,你就是在学习 Linux API 的实际工作原理。 典型的 C 程序从一个 main() 函数开始。但是 main() 从哪里来?是编译器?是链接器?还是操作系统?在本教程中,你将了解整个 C 编译流程。你将从一个普通的 Hello World 开始,观察预处理器扩展宏,观察编译器将 C 转换为汇编,观察汇编器和链接器将汇编转化为二进制。然后你将从图中去掉 main(),仍然能够生成一个可运行的可执行文件。 这个练习是在一个普通的 Ubuntu 24.04 机器上进行的,已安装 gcc、as、ld 等工具。 步骤 1:Hello World 创建一个名为 hello.c 的文件,内容如下: #include <stdio.h> int main ( void ) { printf ( "Hello, world! \n " ); return 0 ; } 编译并运行: gcc hello.c -o hello ./hello 你应该能看到熟悉的问候语。现在问问自己:当你输入那条单一的 gcc 命令时,运行了多少个独立的工具?在幕后,gcc 是一个驱动程序,它按顺序运行多个工具: 预处理器 ( cpp ) - 处理 #include、#define 和条件编译。 编译器 ( cc1 ) - 将 C 源代码转为汇编。 汇编器 ( as ) - 将汇编代码转为目标文件。 链接器 ( collect2 / ld ) - 将目标文件和库链接成最终的可执行文件。 教程的剩余部分将访问每个阶段。 C 编译流程的四个阶段。 步骤 2:预处理器 预处理器是一个文本处理器。它不理解 C 类型或控制流;它只扩展指令和宏。-E 标志告诉 gcc 在预处理后停止。在 hello.c 中添加一个宏,使其看起来像这样: #include <stdio.h> #define GREETING "Hello, preprocessed world! \n " int main ( void ) { printf ( "%s" , GREETING); return 0 ; } 运行预处理器并阅读扩展后的输出: gcc -E hello.c -o hello.i 打开 hello.i,尝试找出 GREETING 原先的位置。它发生了什么?为什么文件的大小比原始源文件还大? 步骤 3:从 C 到汇编 编译器将 C 转换为汇编。-S 标志告诉 gcc 在此阶段停止。 gcc -S hello.c -o hello.s 打开 hello.s,查看标记为 main 的函数。具体的指令依赖于架构,但在 x86_64 上你会看到类似函数序言、对 printf 的调用和一个返回。要查看优化如何改变输出,创建一个名为 loop.c 的文件,内容如下: int sum ( int n ) { int s = 0 ; for ( int i = 0 ; i < n; i ++ ) { s += i; } return s; } 生成三个版本的汇编: gcc -O0 -S loop.c -o loop-O0.s gcc -O2 -S loop.c -o loop-O2.s gcc -Os -S loop.c -o loop-Os.s 比较这些文件。使用 -O0 时,编译器是字面上的:它将循环、计数器和加法保持得和书写时完全一致。使用 -O2 时,优化器可能会展开循环、向量化它,甚至用封闭形式的公式 n * (n - 1) / 2 替换它。使用 -Os 时,优化器试图在保持代码小的同时仍然快速运行。优化不是魔法。它是编译器对程序中间表示应用的一组转换。编译器拥有的信息越多,优化就越激进。 步骤 4:从汇编到目标,从目标到二进制 汇编器将 .s 文件转换为目标文件,而链接器将目标文件转换为可执行文件。你可以手动驱动这些步骤。汇编 hello-world 汇编: gcc -c hello.c -o hello.o -c 标志告诉 gcc 在汇编后停止。检查目标文件: nm hello.o objdump -d hello.o 你应该能看到符号 main、printf,可能还会看到对 .rodata 段的引用。printf 是一个未解析的符号,因为目标文件不包含 printf 的实现。链接器将从 C 库中解析它。将目标文件链接成二进制: gcc hello.o -o hello 现在查看动态依赖: ldd ./hello 你将看到 libc.so.6 和动态加载器。C 库并不是链接器静默添加的唯一内容。它还增加了启动代码,通常来自目标文件...

赞助内容

NordVPN Next-gen Antivirus

本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。

请我喝杯咖啡