GCC 编译与 Makefile 完全指南

GCC 编译与 Makefile 完全指南
GCC 编译与 Makefile 完全指南本文涵盖 GCC 编译全流程、静态库/动态库、GDB 调试、Makefile 从入门到进阶适合 C/C 开发者系统复习。目录一、GCC 编译基础二、GCC 常用参数三、静态库与动态库四、调试宏 DEBUG五、GDB 调试六、ulimit 与 core 文件七、Makefile 入门八、Makefile 伪目标九、Makefile 变量十、Makefile 模式规则与函数一、GCC 编译基础1.1 文件后缀速查表后缀含义生成方式.cC 源代码手动编写.h头文件函数声明、宏定义手动编写.i预处理后的 C 代码#include、#define已展开gcc -E.s汇编代码gcc -S.o目标文件二进制机器码不可直接运行gcc -c.a静态库多个.o打包ar crsv.so动态库共享库运行时加载gcc -shared1.2 编译四阶段核心以hello.c为例#includestdio.h#defineMAX100#definemax(a,b)((a)(b)?(a):(b))intmain(){printf(MAX%d\n,MAX);printf(max(3,4)%d\n,max(3,4));return0;}阶段 1预处理-Egcc-Ehello.c-ohello.i将#include stdio.h的内容展开粘贴进代码所有宏替换MAX→100max(3,4)→((3)(4)?(3):(4))输出.i文件中不再有#define、#include阶段 2编译-Sgcc-Shello.i-ohello.s语法检查少分号、括号不匹配在此报错将 C 代码翻译成汇编代码阶段 3汇编-cgcc-chello.s-ohello.o将汇编翻译成二进制机器码生成.o目标文件此时还不能运行只是零散的机器码阶段 4链接gcc-ohello hello.o将.o与系统库如printf所在的 libc拼接生成最终可执行程序可用nm test.o查看目标文件的符号表。1.3 三种编译写法# 方法 1分步 4 条命令教学用理解编译全过程gcc-Eproject1.c-oproject1.i gcc-Sproject1.i-oproject1.s gcc-cproject1.s-oproject1.o gcc-oproject1 project1.o# 方法 2简化两步-c 自动走完预处理编译汇编gcc-cproject1.c-oproject1.o gcc-oproject1 project1.o# 方法 3一步到位日常 99% 用这个gcc-oproject1 project1.c二、GCC 常用参数2.1 头文件与宏定义# -I 指定头文件搜索目录gcc main.c-omain-I./inc# -D 编译时临时定义宏不用改源码gcc main.c-omain-DDEBUG gcc main.c-omain-DDEBUG12.2 警告级别由弱到强参数作用使用场景-ansi只报极少警告main 没写 int 等几乎不用-pedantic检测老旧 C 标准语法如 long long老旧设备开发-Wall开启全部警告所有不规范代码全部提示日常必加-Werror警告视为错误有警告直接编译失败企业/严格模式# 日常标准写法一条命令全带上gcc main.c-omain -I./inc-DDEBUG-Wall-Werror2.3 调试与优化gcc test.c-otest-g-O0# -g 生成调试信息-O0 不优化GDB 调试必加gcc test.c-otest-O2# 发布版本开启优化三、静态库与动态库3.1 核心区别对比项静态库 (.a)动态库 (.so)链接时机编译时复制库代码进程序运行时才加载库代码文件大小可执行文件体积大程序小库单独存放文件依赖删除.a不影响运行运行时必须有.so否则报错更新难度库升级 → 所有程序重新编译直接替换.so无需重编空间占用多程序重复复制多份多程序共用一份省空间使用场景程序独立、可随意拷贝大型项目、多程序共用3.2 静态库实操# 1. 编写功能代码 add.c# int add(int a, int b) { return a b; }# 2. 生成目标文件gcc-cadd.c# 3. 打包成静态库命名规范libxxx.aar crsv libadd.a add.o# 4. 编译主程序链接静态库# -L. → 库文件在当前目录# -ladd → 链接 libadd.a省略 lib 前缀和 .a 后缀gcc-omain main.c -L.-ladd# 5. 运行删除 libadd.a 后仍可正常运行./main3.3 动态库实操# 1. 生成位置无关代码-fPIC 必须加gcc-fPIC-Wall-cadd.c# 2. 打包生成动态库gcc-shared-olibadd.so add.o# 3. 编译主程序命令与静态库相同gcc-omain main.c -L.-ladd# 4. 运行前必须解决库搜索路径问题否则报错# 办法 1复制到系统库目录sudocplibadd.so /lib# 办法 2临时设置环境变量重启终端失效exportLD_LIBRARY_PATH./ ./main3.4 配套工具ln-slibadd.so.1.0 libadd.so# 创建软链接版本管理ldd ./main# 查看程序依赖哪些动态库排错神器whichls# 查找命令程序位置四、调试宏 DEBUG4.1 核心思想用#ifdef DEBUG包裹调试代码开发时开启、发布时关闭不用手动删除任何 printf。#includestdio.hintadd(inta,intb){#ifdefDEBUGprintf(进入add函数参数a%d,b%d\n,a,b);#endifreturnab;}intmain(){intresadd(10,20);#ifdefDEBUGprintf(main函数计算结果res%d\n,res);#endifprintf(程序正常运行最终结果%d\n,res);return0;}4.2 两种开启方式# 方式 1编译时 -D 参数推荐不改源码gcc test.c-otest-DDEBUG# 开启调试gcc test.c-otest_release-O2# 发布不加 -DDEBUG// 方式 2代码顶部手动 #define改源码#defineDEBUG// 注释掉这行即关闭4.3 运行效果对比# 调试版输出 进入add函数参数a10,b20 main函数计算结果res30 程序正常运行最终结果30 # 发布版输出 程序正常运行最终结果30五、GDB 调试5.1 准备工作gcc test.c-otest-g-O0# 必须带 -g不加优化gdb ./test# 进入 GDB5.2 程序启动参数set args 100 hello # 设置运行参数等价于 ./test 100 hello show args # 查看已设置的参数 r # 运行程序r run5.3 断点操作重中之重命令作用b 11在第 11 行设普通断点b 11 if loop 5条件断点loop5 时才停tbreak 13临时断点触发一次后自动删除info b查看所有断点编号、行号、状态delete 2删除 2 号断点delete删除所有断点disable 1禁用 1 号断点不删除暂停触发enable 1重新启用 1 号断点condition 1 loop 10修改 1 号断点的条件ignore 1 10跳过 1 号断点前 10 次第 11 次才停5.4 执行控制命令作用c/continue继续运行到下一个断点或程序结束n/next单步执行不进入函数内部s/step单步执行进入函数内部finish跳出当前函数回到调用处5.5 查看变量与堆栈p loop # 打印变量值p print p sum loop * 10 # 打印表达式结果 display loop # 自动打印每次单步自动显示 undisplay 1 # 取消自动显示 bt # 查看函数调用栈段错误定位神器 x loop # 查看变量所在内存5.6 段错误调试完整流程// crash.c#includestdio.hintmain(){int*pNULL;*p100;// 非法访问内存 → 段错误return0;}# 1. 开启 core 文件生成每次新开终端都要执行ulimit-cunlimited# 2. 编译必须带 -ggcc crash.c-ocrash-g-O0# 3. 运行触发崩溃./crash# 输出Segmentation fault (core dumped)# 4. GDB 加载程序 core 快照gdb ./crash core# 5. 一键定位出错行(gdb)bt# 输出会明确指出第 5 行 *p 100 引发段错误六、ulimit 与 core 文件6.1 ulimit 是什么Linux 为防止单个程序耗尽系统资源给每个终端设置资源上限。ulimit用于查看/修改这些限制。ulimit-a# 查看全部资源限制ulimit-cunlimited# 开启 core 文件生成默认 core file size 0即禁止⚠️ulimit只临时生效关闭终端即失效。6.2 core 文件是什么程序崩溃段错误瞬间系统将程序当时的完整状态快照保存为 core 文件包括所有变量的值代码执行到第几行所有函数的调用关系程序占用的内存数据用途程序运行很久才崩溃、难以复现时事后用 core 文件还原崩溃现场定位出错代码行。七、Makefile 入门7.1 为什么需要 Makefile痛点Makefile 的解决方案文件多编译命令又长又容易错把命令写进文件只敲make改 1 个文件却要全部重编增量编译只重编改过的文件7.2 前置知识-c与-ogcc-cmain.c# 只编译不链接 → 产出 main.o半成品gcc-cfunc.c# 只编译不链接 → 产出 func.ogcc-omain main.o func.o# 链接把 .o 拼成可执行文件拆成-c 链接两步是为了让 make 能判断哪个.o过期了实现增量编译。7.3 三元组语法目标: 依赖 Tab命令目标 要做出来的东西依赖 需要的原料命令 具体怎么做⚠️第一大坑命令行行首必须按 Tab 键不能用空格否则报错missing separator。7.4 完整示例main: main.o func.o gcc -o main main.o func.o main.o: main.c gcc -c main.c func.o: func.c gcc -c func.c7.5 执行顺序依赖树main ← 默认目标文件中第一个 target / \ main.o func.o | | main.c func.c ← 叶子源代码无需构建make等价于make main默认做第一个目标自顶向下找依赖自底向上执行make main.o可以只构建某一个目标7.6 增量编译Makefile 的灵魂规则比较目标文件与依赖文件的时间戳。目标比所有依赖都新→ 跳过make: main is up to date.任一依赖比目标新→ 重做该目标场景行为第一次make全部编译啥都没改再make全部跳过打印 “up to date”只改了func.c再make只重编func.o 重链接mainmain.o不动 用touch func.c可模拟修改只更新时间戳验证增量编译。八、Makefile 伪目标8.1 真目标 vs 伪目标真目标背后有同名文件如main.o、main伪目标只是一个动作如clean、rebuild不产生同名文件8.2 不用.PHONY的灾难clean: rm -rf main.o func.o main如果目录里碰巧有个叫clean的文件make clean会打印clean is up to date.而什么都不删8.3 正确写法.PHONY: clean rebuild clean: rm -f main.o func.o main rebuild: clean main.PHONY声明后无视文件存在、无视时间戳每次无条件执行冒号后可一次声明多个伪目标空格隔开所有动作型目标都要声明.PHONY养成防御性习惯8.4 rebuild 执行时间线make rebuild ├─ 先执行 clean → rm 删光所有 .o 和可执行文件 └─ 再构建 main → .o 全被删了无法偷懒 → 全部从头编译make 只补改过的增量make rebuild 强制全量重编。九、Makefile 变量9.1 自定义变量与引用out : main $(out): main.o func.o gcc -o $(out) main.o func.o本质是字符串查找替换$(out)和${out}完全等价建议统一用$()变量可用在目标名、依赖、命令等任何位置9.2vs:必懂递归展开:简单展开展开时机用到时才展开定义时立刻展开定格比喻实时直播 拍立得照片 风险A $(A) x会死循环安全性能每次用到都重新展开慢一次算完快# Case 1用 → 输出 world out hello rout ${out} out world all: echo ${rout} # Case 2用 : → 输出 hello out : hello rout : ${out} out : world all: echo ${rout}铁律定义变量一律用:省心、安全、快。是静默前缀命令前加 不打印命令本身只打印结果。9.3 预定义变量常用三个变量默认值用途CCccC 编译器通常覆盖为CC : gccCFLAGS空编译选项如CFLAGS : -Wall -gRMrm -f删除命令clean 里可用$(RM)这些变量是 make内置隐含规则的调节旋钮——设了CC和CFLAGSmake 默认就知道怎么把.c编成.o。9.4 自动变量规则里的代词变量含义人话$目标文件“本道菜”$第一个依赖“第一种原料”$^所有依赖“全部原料”$?比目标还新的依赖“哪些原料过期了”$vs$^单依赖时相同多依赖时$只取第一个$^取全部。9.5 终极 Makefile变量 自动变量OBJS : main.o func.o CC : gcc main: $(OBJS) $(CC) -o $ $^ main.o: main.c $(CC) -c $^ -o $ func.o: func.c $(CC) -c $^ -o $展开对照规则$$^展开后命令main: main.o func.omainmain.o func.ogcc -o main main.o func.omain.o: main.cmain.omain.cgcc -c main.c -o main.ofunc.o: func.cfunc.ofunc.cgcc -c func.c -o func.o9.6 目录拼接坑DIR : ./ OBJS : main.o func.o # $(DIR)$(OBJS) 展开为 ./main.o func.o ← 只有第一项有 ./原因变量替换是纯文本拼接make 不懂多个文件。正确做法用函数$(addprefix $(DIR), $(OBJS))。十、Makefile 模式规则与函数10.1 两个世界*vs%*shell 通配符%make 模式符生效位置命令行Tab 行规则行冒号行管理者shell (bash)makeclean: rm -rf *.o # ← 命令行归 shell 管用 * 规则的依赖里要按名字配对永远用%绝不用*。用*会全量匹配毁掉增量编译。10.2 模式规则%.o: %.c%.o: %.c $(CC) -c $^ -o $ # 一条顶 N 条%像填空题的下划线两边填同一个词stem构建main.o时%抓住main→ 依赖变成main.c构建func.o时%抓住func→ 依赖变成func.c10.3 变量内%替换SRCS test.c test1.c OBJECTS $(SRCS:%.c%.o) # → test.o test1.o等价于$(patsubst %.c,%.o,$(SRCS))。10.4 内置函数wildcard—— 扫描目录拿文件清单srcfiles : $(wildcard src/*.c)⚠️大坑SRCS *.c不会展开make 在变量赋值里把*当普通字符。必须用wildcard。patsubst—— 模式替换$(patsubst %.c,%.o,$(SRCS)) # 三个参数两个逗号subst—— 纯文本替换死板易误伤少用10.5 循环与$$转义LIST one two three all: for i in $(LIST); do echo $$i; done三个关键点for是 shell 语法不是 make 的$$i而非$imake 先看到$会当成 make 变量展开结果为空。写$$→ make 剥成$→ shell 收到$i正确取值循环必须写在同一行用;或\连接因为 make 每行是独立的 shell 调用\续行时反斜杠必须是行末最后一个字符后面不能有空格。10.6 综合实例SRCS $(wildcard *.c) OBJECTS $(patsubst %.c,%.o,$(SRCS)) TARGETS $(SRCS:%.c%) all: $(TARGETS) for i in $(TARGETS); do gcc -o $${i} $${i}.c; done .PHONY: clean clean: rm -f $(TARGETS)10.7 更地道的写法依赖驱动代替循环SRCS : $(wildcard *.c) TARGETS : $(SRCS:%.c%) CC : gcc all: $(TARGETS) %: %.c $(CC) -o $ $ .PHONY: all clean clean: rm -f $(TARGETS)核心思想循环版每次make都全量重编丢掉了增量编译。依赖驱动版让 make 用时间戳判断只重编改过的文件。在 Makefile 里循环通常是反模式声明依赖、让 make 调度才是正道。全系列知识地图模块核心思维GCC 四阶段预处理 → 编译 → 汇编 → 链接静态库 vs 动态库编译时复制 vs 运行时加载DEBUG 宏条件编译开发/发布一键切换GDB断点 单步 变量查看 bt 定位崩溃Makefile 三元组声明依赖只重做过期的增量编译伪目标.PHONY动作不是文件别比时间戳变量 自动变量起外号消灭重复的字符串模式规则 函数用模式消灭重复的规则一条主线贯穿始终从手写每条命令→变量消重复字符串→模式消重复规则→依赖驱动代替循环。学的不是语法而是声明式 增量的工程思维。