欢迎光临
我们一直在努力

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>

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. 自动变量

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

自动变量含义在当前 Makefile 中
$@ 当前目标 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 开发不是“背命令”,而是在理解一条工程化链路:

源代码如何变成程序
程序如何被自动构建
终端输出如何被控制
项目如何被组织和维护

赞(0)
未经允许不得转载:171主机测评 » Linux 开发工具进阶:从 `gcc/g++` 编译流程到 `Makefile` 自动化构建,再手写一个进度条
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址