当前位置: 首页 > news >正文

Linux 开发工具进阶:从 `gcc/g++` 编译流程到 `Makefile` 自动化构建,再手写一个进度条

上一篇我们用yum安装工具、用Vim编辑代码。
但代码写完以后,Linux 并不会直接运行.c/.cpp源文件。
它需要经过编译、汇编、链接,最终变成可执行程序。

这篇文章围绕一条真实开发路线展开:

myproc.c -> gcc/g++ 编译流程 -> .i / .s / .o / 可执行文件 -> make 自动化构建 -> Makefile 管理工程 -> 终端进度条项目

我们会用 Linux 命令、表格、图和项目代码,把gcc/g++makeMakefile、进度条一次串起来。


一、为什么.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

这类错误通常不是语法问题,而是链接阶段找不到函数定义。


五、gccg++的区别

gccg++都属于 GNU 编译工具链,但用途略有不同。

命令常用对象特点
gccC 程序默认按 C 的方式编译链接
g++C++ 程序默认按 C++ 的方式编译链接,并自动链接 C++ 标准库
gcc main.cppC++ 源文件能识别 C++ 语法,但链接时可能缺 C++ 标准库
g++ main.cppC++ 源文件编译 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.cmyproc.o新,说明源代码被改过,make就会重新生成myproc.o
如果myproc.omyproc新,说明目标文件更新了,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.o

2. 自动变量

自动变量含义在当前 Makefile 中
$@当前目标proc.exemyproc.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没安装 gccsudo yum install gcc -y
g++: command not found没安装 g++sudo yum install gcc-c++ -y
make: command not found没安装 makesudo yum install make -y
undefined reference to main没有main函数或链接对象不对检查入口函数和链接命令
undefined reference to xxx函数声明了但没实现,或库没链接补实现或加链接库
missing separatorMakefile 命令前用了空格命令前必须用 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 开发不是“背命令”,而是在理解一条工程化链路:

源代码如何变成程序 程序如何被自动构建 终端输出如何被控制 项目如何被组织和维护
http://www.cnnetsun.cn/news/2908466.html

相关文章:

  • NHSE:动物森友会存档编辑器的终极指南与使用教程
  • requests-oauthlib:给 Requests 配上 OAuth 认证
  • OBS源独立录制插件:终极视频制作工作流自动化解决方案
  • 30米分辨率DEM数据实战:如何精准划定小流域边界并提取水系网络
  • NXP KE1xZ微控制器SIM与TRGMUX模块实战:从寄存器配置到硬件协同设计
  • 新手ESP8266常见问题
  • 别再死记硬背D-H参数了!用Python+NumPy手把手推导机器人连杆变换矩阵
  • Scrapy + Splash 渲染爬取微博:从动态页面到数据挖掘的完整实战
  • 智能调度与反爬突破:基于Crawlera代理中间件的天猫海量数据爬取实战
  • 3分钟解锁网易云音乐:ncmdump让NCM加密文件变身通用MP3
  • 多线程经典问
  • 【Android】瞬净ins版-无水印解析-无水印视频保存
  • 【Android】myReader电子书阅读器-一键扫描阅读小说
  • 3个常见误区:为什么你的网络压力测试总是失败?
  • 评测全网10款主流降AI率平台:只选真正管用的那一款!
  • MC68SZ328 DragonBall Super VZ:经典嵌入式SoC的架构解析与实战设计
  • Synology HDD db:群晖NAS硬盘兼容性终极解决方案
  • OmicVerse实战指南:高效多组学分析的5大核心优势
  • 从文字到视觉:5分钟掌握Flowchart Fun的智能流程图创作技巧
  • Python进阶:从执行模型与对象机制理解真实Bug根源
  • 成功的大数据治理项目须坚持“六个导向”和“三个相结合”
  • 新手必看:用eNSP模拟真实网络,手把手教你搞定BGP跨AS通信(含路由黑洞排查)
  • 从Arduino到树莓派:手把手教你玩转UART、IIC、SPI通信(附Python/C++代码示例)
  • 冥想第一千九百零九天
  • MC9S08QE128内存管理与寄存器映射实战:从原理到高效嵌入式开发
  • 符合消防专项要求玻璃防火门多场景合规落地应用研究摘要
  • MC68341定时器与QSPI模块深度解析:从寄存器原理到实战调试
  • 腾讯AI,有自己的坐标
  • 如何打造终极iOS漫画阅读体验:E-Hentai Viewer完全指南 [特殊字符]
  • yolov26改进 | 损失函数改进篇 | 最新ShapeIoU、InnerShapeIoU损失助力细节涨点(含三十余种损失函数改进方法)