Linux 开发工具进阶:从 `gcc/g++` 编译流程到 `Makefile` 自动化构建,再手写一个进度条
上一篇我们用
yum安装工具、用Vim编辑代码。
但代码写完以后,Linux 并不会直接运行.c/.cpp源文件。
它需要经过编译、汇编、链接,最终变成可执行程序。
这篇文章围绕一条真实开发路线展开:
myproc.c -> gcc/g++ 编译流程 -> .i / .s / .o / 可执行文件 -> make 自动化构建 -> Makefile 管理工程 -> 终端进度条项目我们会用 Linux 命令、表格、图和项目代码,把gcc/g++、make、Makefile、进度条一次串起来。
一、为什么.c文件不能直接运行
先看一个最简单的 C 程序:
#include<stdio.h>intmain(){printf("hello gcc\n");return0;}如果直接运行源文件:
[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: line3: syntax error near unexpected token `('原因很简单:.c是源代码,CPU 不能直接执行。
它必须变成机器能够识别的可执行文件。
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc myproc.c-omyproc[zdt@lavm-ljd6tsvm2x lesson9]$ ./myproc1098...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-Emyproc.c-omyproc.i[zdt@lavm-ljd6tsvm2x lesson9]$ls-lhmyproc.i -rw-rw-r--1zdt zdt 37K Jun1210:20 myproc.i查看开头:
[zdt@lavm-ljd6tsvm2x lesson9]$headmyproc.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-Smyproc.i-omyproc.s[zdt@lavm-ljd6tsvm2x lesson9]$headmyproc.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-cmyproc.s-omyproc.o[zdt@lavm-ljd6tsvm2x lesson9]$filemyproc.o myproc.o: ELF64-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-omyproc[zdt@lavm-ljd6tsvm2x lesson9]$filemyproc myproc: ELF64-bit LSB executable, x86-64运行:
[zdt@lavm-ljd6tsvm2x lesson9]$ ./myproc1098...0这就是一份源代码变成可执行程序的完整路径。
四、链接到底在干什么
我们在代码里写了:
printf("%-2d\r",i);fflush(stdout);sleep(1);这些函数不是我们自己实现的,它们来自系统库。
链接阶段要做的事情,就是把我们自己的目标文件和系统库中的函数实现“拼”到一起。
如果链接不到函数实现,就会出现类似错误:
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc test.o-otest/usr/bin/ld: test.o:infunction`main': test.c:(.text+0x15): undefined reference to`xxx' collect2: error: ld returned1exitstatus这类错误通常不是语法问题,而是链接阶段找不到函数定义。
五、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>usingnamespacestd;intmain(){cout<<"hello g++"<<endl;return0;}用gcc编译 C++ 文件可能链接失败:
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc test.cpp-otest/usr/bin/ld: undefined reference to`std::cout' /usr/bin/ld: undefined reference to`std::ios_base::Init::Init()' collect2: error: ld returned1exitstatus用g++:
[zdt@lavm-ljd6tsvm2x lesson9]$ g++ test.cpp-otest[zdt@lavm-ljd6tsvm2x lesson9]$ ./test hello g++一句话记忆:
写 C 用
gcc,写 C++ 用g++。
六、为什么需要make
如果项目只有一个文件,手敲gcc myproc.c -o myproc问题不大。
但项目一旦变成多个文件:
main.c processbar.c utils.c你每次都手动编译就会很麻烦:
gcc-cmain.c-omain.o gcc-cprocessbar.c-oprocessbar.o gcc-cutils.c-outils.o gcc main.o processbar.o utils.o-oprocessbar而且你还要判断:
- 哪个
.c文件改过? - 哪个
.o文件需要重新生成? - 最终目标是否需要重新链接?
make就是为了解决这类自动化构建问题。
七、Makefile 的基本结构
Makefile 的核心结构是:
目标: 依赖 命令注意:命令前面必须是Tab,不是空格。
最小例子:
myproc: myproc.c gcc myproc.c -o myproc .PHONY: clean clean: rm -f myproc执行:
[zdt@lavm-ljd6tsvm2x lesson9]$makegcc myproc.c-omyproc[zdt@lavm-ljd6tsvm2x lesson9]$makecleanrm-fmyproc八、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]$maketestmyproc.c myproc.o2. 自动变量
| 自动变量 | 含义 | 在当前 Makefile 中 |
|---|---|---|
$@ | 当前目标 | proc.exe或myproc.o |
$^ | 所有依赖 | 所有.o文件 |
$< | 第一个依赖 | 对应的.c文件 |
这段:
$(BIN):$(OBJ) @$(CC) $(LFLAGS) $@ $^展开后大致是:
gcc-oproc.exe myproc.o这段:
%.o:%.c @$(CC) $(FLAGS) $<表示:
任意 .o 文件都可以由同名 .c 文件生成例如:
gcc-cmyproc.c十、.PHONY为什么重要
clean不是一个真实文件,而是一个动作。
.PHONY:clean clean: rm -f $(OBJ) $(BIN)如果没有.PHONY,当目录下真的出现一个叫clean的文件时,make clean可能会误以为目标已经存在,从而不执行清理命令。
.PHONY的意思是:
这个目标是伪目标,不代表真实文件,每次执行都直接运行命令。
十一、make常见执行过程
[zdt@lavm-ljd6tsvm2x lesson9]$makecompling... myproc.c to myproc.o linking... myproc.o to proc.exe再次执行:
[zdt@lavm-ljd6tsvm2x lesson9]$makemake:'proc.exe'is up to date.因为源文件没变,目标文件也没过期,所以make不会重复编译。
修改源文件后:
[zdt@lavm-ljd6tsvm2x lesson9]$vimmyproc.c[zdt@lavm-ljd6tsvm2x lesson9]$makecompling... myproc.c to myproc.o linking... myproc.o to proc.exe这就是自动化构建的价值:该编译的编译,不该编译的不动。
十二、进度条项目:从倒计时开始
你的myproc.c代码是一个很好的进度条前置实验:
#include<stdio.h>#include<unistd.h>intmain(){inti=10;while(i>=0){printf("%-2d\r",i);fflush(stdout);i--;sleep(1);}printf("\n");return0;}运行效果类似:
[zdt@lavm-ljd6tsvm2x lesson9]$ ./myproc1098...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>#defineNUM101#defineSTYLE'='intmain(){charbar[NUM];memset(bar,'\0',sizeof(bar));constchar*label="|/-\\";inti=0;while(i<=100){printf("[%-100s][%3d%%][%c]\r",bar,i,label[i%4]);fflush(stdout);bar[i]=STYLE;i++;usleep(50000);}printf("\n");return0;}编译运行:
[zdt@lavm-ljd6tsvm2x lesson9]$ gcc processbar.c-oprocessbar[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]$makegcc-cprocessbar.c gcc-oprocessbar processbar.o[zdt@lavm-ljd6tsvm2x lesson9]$ ./processbar清理:
[zdt@lavm-ljd6tsvm2x lesson9]$makecleanrm-fprocessbar.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-Emyproc.c-omyproc.i gcc-Smyproc.i-omyproc.s gcc-cmyproc.s-omyproc.o gcc myproc.o-omyprocmakemakeclean真正掌握这些工具后,你会发现 Linux 开发不是“背命令”,而是在理解一条工程化链路:
源代码如何变成程序 程序如何被自动构建 终端输出如何被控制 项目如何被组织和维护