上一篇我们用 yum 安装工具、用 Vim 编辑代码。
但代码写完以后,Linux 并不会直接运行 .c/.cpp 源文件。
它需要经过编译、汇编、链接,最终变成可执行程序。
这篇文章围绕一条真实开发路线展开:
myproc.c
-> gcc/g++ 编译流程
-> .i / .s / .o / 可执行文件
-> make 自动化构建
-> Makefile 管理工程
-> 终端进度条项目
我们会用 Linux 命令、表格、图和项目代码,把 gcc/g++、make、Makefile、进度条一次串起来。

一、为什么 .c 文件不能直接运行
先看一个最简单的 C 程序:
#include <stdio.h>
int main()
{
printf("hello gcc\\n");
return 0;
}
如果直接运行源文件:
[zdt@lavm-ljd6tsvm2x lesson9]$ ./myproc.c
bash: ./myproc.c: Permission denied
即使给执行权限:
[zdt@lavm-ljd6tsvm2x lesson9]$ chmod +x myproc.c
[zdt@lavm-ljd6tsvm2x lesson9]$ ./myproc.c
./myproc.c: line 3: syntax error near unexpected token `('
原因很简单:.c 是源代码,CPU 不能直接执行。
它必须变成机器能够识别的可执行文件。
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc myproc.c -o myproc
[zdt@lavm-ljd6tsvm2x lesson9]$ ./myproc
10
9
8
...
0
从源代码到可执行文件,中间其实经历了多个阶段。
二、gcc 编译的四个阶段
gcc myproc.c -o myproc 看起来只是一条命令,但内部可以拆成四步:
预处理 -> 编译 -> 汇编 -> 链接

| 预处理 | -E | .c | .i | 展开头文件、宏替换、去注释 |
| 编译 | -S | .i | .s | 生成汇编代码 |
| 汇编 | -c | .s | .o | 生成可重定位目标文件 |
| 链接 | 无特殊选项 | .o + 库 | 可执行文件 | 合并目标文件和库 |
三、用 Linux 命令亲自拆开编译过程
1. 预处理:.c -> .i
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc -E myproc.c -o myproc.i
[zdt@lavm-ljd6tsvm2x lesson9]$ ls -lh myproc.i
-rw-rw-r– 1 zdt zdt 37K Jun 12 10:20 myproc.i
查看开头:
[zdt@lavm-ljd6tsvm2x lesson9]$ head myproc.i
# 1 "myproc.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "myproc.c"
# 1 "/usr/include/stdio.h" 1 3 4
为什么 .i 文件变大了?
因为 #include <stdio.h>、#include <unistd.h> 这类头文件被展开进来了。
2. 编译:.i -> .s
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc -S myproc.i -o myproc.s
[zdt@lavm-ljd6tsvm2x lesson9]$ head myproc.s
.file "myproc.c"
.section .rodata
.LC0:
.string "%-2d\\r"
.text
.globl main
.type main, @function
main:
.s 是汇编代码,已经非常接近机器执行逻辑了。
3. 汇编:.s -> .o
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc -c myproc.s -o myproc.o
[zdt@lavm-ljd6tsvm2x lesson9]$ file myproc.o
myproc.o: ELF 64-bit LSB relocatable, x86-64
.o 是目标文件,但它还不能直接运行:
[zdt@lavm-ljd6tsvm2x lesson9]$ ./myproc.o
bash: ./myproc.o: Permission denied
即使加权限,它也不是完整可执行程序,因为还没链接。
4. 链接:.o -> 可执行程序
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc myproc.o -o myproc
[zdt@lavm-ljd6tsvm2x lesson9]$ file myproc
myproc: ELF 64-bit LSB executable, x86-64
运行:
[zdt@lavm-ljd6tsvm2x lesson9]$ ./myproc
10
9
8
...
0
这就是一份源代码变成可执行程序的完整路径。
四、链接到底在干什么
我们在代码里写了:
printf("%-2d\\r", i);
fflush(stdout);
sleep(1);
这些函数不是我们自己实现的,它们来自系统库。
链接阶段要做的事情,就是把我们自己的目标文件和系统库中的函数实现“拼”到一起。

如果链接不到函数实现,就会出现类似错误:
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc test.o -o test
/usr/bin/ld: test.o: in function `main':
test.c:(.text+0x15): undefined reference to `xxx'
collect2: error: ld returned 1 exit status
这类错误通常不是语法问题,而是链接阶段找不到函数定义。
五、gcc 和 g++ 的区别
gcc 和 g++ 都属于 GNU 编译工具链,但用途略有不同。
| gcc | C 程序 | 默认按 C 的方式编译链接 |
| g++ | C++ 程序 | 默认按 C++ 的方式编译链接,并自动链接 C++ 标准库 |
| gcc main.cpp | C++ 源文件 | 能识别 C++ 语法,但链接时可能缺 C++ 标准库 |
| g++ main.cpp | C++ 源文件 | 编译 C++ 程序更推荐 |
看一个典型现象:
#include <iostream>
using namespace std;
int main()
{
cout << "hello g++" << endl;
return 0;
}
用 gcc 编译 C++ 文件可能链接失败:
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc test.cpp -o test
/usr/bin/ld: undefined reference to `std::cout'
/usr/bin/ld: undefined reference to `std::ios_base::Init::Init()'
collect2: error: ld returned 1 exit status
用 g++:
[zdt@lavm-ljd6tsvm2x lesson9]$ g++ test.cpp -o test
[zdt@lavm-ljd6tsvm2x lesson9]$ ./test
hello g++
一句话记忆:
写 C 用 gcc,写 C++ 用 g++。
六、为什么需要 make
如果项目只有一个文件,手敲 gcc myproc.c -o myproc 问题不大。
但项目一旦变成多个文件:
main.c
processbar.c
utils.c
你每次都手动编译就会很麻烦:
gcc -c main.c -o main.o
gcc -c processbar.c -o processbar.o
gcc -c utils.c -o utils.o
gcc main.o processbar.o utils.o -o processbar
而且你还要判断:
- 哪个 .c 文件改过?
- 哪个 .o 文件需要重新生成?
- 最终目标是否需要重新链接?
make 就是为了解决这类自动化构建问题。
七、Makefile 的基本结构
Makefile 的核心结构是:
目标: 依赖
命令
注意:命令前面必须是 Tab,不是空格。
最小例子:
myproc: myproc.c
gcc myproc.c -o myproc
.PHONY: clean
clean:
rm -f myproc
执行:
[zdt@lavm-ljd6tsvm2x lesson9]$ make
gcc myproc.c -o myproc
[zdt@lavm-ljd6tsvm2x lesson9]$ make clean
rm -f myproc
八、Makefile 的依赖关系:make 怎么知道该干什么
make 的核心不是“帮你执行命令”,而是“根据依赖关系决定哪些命令需要执行”。

比如:
myproc: myproc.o
gcc myproc.o -o myproc
myproc.o: myproc.c
gcc -c myproc.c -o myproc.o
如果 myproc.c 比 myproc.o 新,说明源代码被改过,make 就会重新生成 myproc.o。
如果 myproc.o 比 myproc 新,说明目标文件更新了,make 就会重新链接生成 myproc。
这就是 make 的时间戳判断机制。
九、结合本地 lesson9 的 Makefile
你的 Makefile 里已经写到了比较实用的版本:
BIN=proc.exe
CC=gcc
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
LFLAGS=-o
FLAGS=-c
RM=rm -f
$(BIN):$(OBJ)
@$(CC) $(LFLAGS) $@ $^
@echo "linking … $^ to $@"
%.o:%.c
@$(CC) $(FLAGS) $<
@echo "compling … $< to $@"
.PHONY:clean
clean:
$(RM) $(OBJ) $(BIN)
.PHONY:test
test:
@echo $(SRC)
@echo $(OBJ)
这里有几个非常值得讲的点。
1. 变量
| BIN=proc.exe | 最终目标文件名 |
| CC=gcc | 使用的编译器 |
| SRC=$(wildcard *.c) | 找到当前目录所有 .c 文件 |
| OBJ=$(SRC:.c=.o) | 把 .c 列表替换成 .o 列表 |
| RM=rm -f | 删除命令 |
验证:
[zdt@lavm-ljd6tsvm2x lesson9]$ make test
myproc.c
myproc.o
2. 自动变量

| $@ | 当前目标 | proc.exe 或 myproc.o |
| $^ | 所有依赖 | 所有 .o 文件 |
| $< | 第一个依赖 | 对应的 .c 文件 |
这段:
$(BIN):$(OBJ)
@$(CC) $(LFLAGS) $@ $^
展开后大致是:
gcc -o proc.exe myproc.o
这段:
%.o:%.c
@$(CC) $(FLAGS) $<
表示:
任意 .o 文件都可以由同名 .c 文件生成
例如:
gcc -c myproc.c
十、.PHONY 为什么重要
clean 不是一个真实文件,而是一个动作。
.PHONY:clean
clean:
rm -f $(OBJ) $(BIN)
如果没有 .PHONY,当目录下真的出现一个叫 clean 的文件时,make clean 可能会误以为目标已经存在,从而不执行清理命令。
.PHONY 的意思是:
这个目标是伪目标,不代表真实文件,每次执行都直接运行命令。
十一、make 常见执行过程
[zdt@lavm-ljd6tsvm2x lesson9]$ make
compling ... myproc.c to myproc.o
linking ... myproc.o to proc.exe
再次执行:
[zdt@lavm-ljd6tsvm2x lesson9]$ make
make: 'proc.exe' is up to date.
因为源文件没变,目标文件也没过期,所以 make 不会重复编译。
修改源文件后:
[zdt@lavm-ljd6tsvm2x lesson9]$ vim myproc.c
[zdt@lavm-ljd6tsvm2x lesson9]$ make
compling ... myproc.c to myproc.o
linking ... myproc.o to proc.exe
这就是自动化构建的价值:该编译的编译,不该编译的不动。
十二、进度条项目:从倒计时开始
你的 myproc.c 代码是一个很好的进度条前置实验:
#include <stdio.h>
#include <unistd.h>
int main()
{
int i = 10;
while(i >= 0)
{
printf("%-2d\\r", i);
fflush(stdout);
i—;
sleep(1);
}
printf("\\n");
return 0;
}
运行效果类似:
[zdt@lavm-ljd6tsvm2x lesson9]$ ./myproc
10
9
8
...
0
如果终端支持 \\r 覆盖显示,你会看到数字在同一行变化。
十三、进度条的三个关键点

| \\r | 回到当前行行首,不换行 |
| fflush(stdout) | 立刻刷新标准输出 |
| sleep(1) | 控制显示节奏 |
| %-2d | 左对齐输出,避免残留字符 |
为什么要 fflush(stdout)?
因为标准输出通常有缓冲区。
如果没有换行 \\n,内容可能不会立刻显示出来。
printf("hello");
sleep(3);
这段代码可能等程序结束才显示。
加上:
printf("hello");
fflush(stdout);
sleep(3);
就会立即显示。
十四、手写一个百分比进度条
可以把倒计时升级成真正的进度条:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#define NUM 101
#define STYLE '='
int main()
{
char bar[NUM];
memset(bar, '\\0', sizeof(bar));
const char *label = "|/-\\\\";
int i = 0;
while(i <= 100)
{
printf("[%-100s][%3d%%][%c]\\r", bar, i, label[i % 4]);
fflush(stdout);
bar[i] = STYLE;
i++;
usleep(50000);
}
printf("\\n");
return 0;
}
编译运行:
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc processbar.c -o processbar
[zdt@lavm-ljd6tsvm2x lesson9]$ ./processbar
[================================================== ][ 50%][/]
这里的核心思想是:
数组 bar 保存进度条内容
每次循环增加一个 '='
\\r 回到行首
fflush 立即刷新
usleep 控制速度
十五、把进度条交给 Makefile 管理
如果项目里有 processbar.c,可以写一个 Makefile:
BIN=processbar
CC=gcc
SRC=$(wildcard *.c)
OBJ=$(SRC:.c=.o)
$(BIN):$(OBJ)
$(CC) -o $@ $^
%.o:%.c
$(CC) -c $<
.PHONY:clean
clean:
rm -f $(OBJ) $(BIN)
执行:
[zdt@lavm-ljd6tsvm2x lesson9]$ make
gcc -c processbar.c
gcc -o processbar processbar.o
[zdt@lavm-ljd6tsvm2x lesson9]$ ./processbar
清理:
[zdt@lavm-ljd6tsvm2x lesson9]$ make clean
rm -f processbar.o processbar
这样进度条项目就不再依赖手敲编译命令。
十六、常见报错排查表
| gcc: command not found | 没安装 gcc | sudo yum install gcc -y |
| g++: command not found | 没安装 g++ | sudo yum install gcc-c++ -y |
| make: command not found | 没安装 make | sudo yum install make -y |
| undefined reference to main | 没有 main 函数或链接对象不对 | 检查入口函数和链接命令 |
| undefined reference to xxx | 函数声明了但没实现,或库没链接 | 补实现或加链接库 |
| missing separator | Makefile 命令前用了空格 | 命令前必须用 Tab |
| No rule to make target | 依赖文件不存在或规则写错 | 检查文件名和依赖关系 |
| Permission denied | 没有执行权限或路径权限不足 | chmod +x 或检查目录权限 |
十七、总结:从工具到工程,再到项目
这篇文章把 Linux 开发工具链串成了一条完整路径:
Vim 写代码
gcc/g++ 编译代码
make 判断依赖
Makefile 描述构建规则
进度条项目验证终端输出机制
核心命令回顾:
gcc -E myproc.c -o myproc.i
gcc -S myproc.i -o myproc.s
gcc -c myproc.s -o myproc.o
gcc myproc.o -o myproc
make
make clean
真正掌握这些工具后,你会发现 Linux 开发不是“背命令”,而是在理解一条工程化链路:
源代码如何变成程序
程序如何被自动构建
终端输出如何被控制
项目如何被组织和维护



