Linux GCC/G++ 编译与 Makefile 构建

文章目录

上一篇已经基本介绍了工具的安装和 Vim 编辑。本篇将从一条编译命令开始,观察它生成了什么,再把这些命令组织成可以重复使用的构建规则。

1. 编译与运行


可以先在自己的学习目录中操作:

bash 复制代码
mkdir -p ~/linux-dev-study/build
cd ~/linux-dev-study/build
vim hello.c

保存以下完整文件 hello.c:

c 复制代码
#include <stdio.h>

#define MESSAGE "hello linux"

int main()
{
    puts(MESSAGE);
    return 0;
}

编译,再运行:

bash 复制代码
gcc hello.c -o hello
./hello

运行示例:

gcc 在这里是编译驱动程序,它会按需要调用各阶段的工具。hello.c 是输入文件,-o hello 指定输出文件名。正常情况下编译成功可以没有任何输出。

./hello 表示运行当前目录中的 hello。

Shell 通常不会默认搜索当前目录,所以直接输入 hello 可能提示找不到命令。

Linux 可执行文件不靠 .exe 后缀识别。起名 hello.exe 也能使用,但不会因此变成 Windows 程序。

GCC、gcc 与 g++

GCC 是编译器工具集合的名称,gcc、g++ 是常见驱动命令。

不能简单说 gcc 只能处理 C、g++ 只能处理 C++:文件后缀和选项会影响语言的选择,两者的默认链接行为也有区别。

对于当前阶段,保持这条实用习惯即可:C 程序使用 gcc,C++ 程序使用 g++,C++ 程序的最终链接也交给 g++,由它处理 C++ 标准库的常规链接。

一个独立的 C++ 小例子,保存为 hello.cpp:

cpp 复制代码
#include <iostream>

int main()
{
    std::cout << "hello cpp" << std::endl;
    return 0;
}
bash 复制代码
g++ hello.cpp -o hello_cpp
./hello_cpp

运行示例:

常用选项的选择

选项 当前需要理解的作用
-o 文件名 指定该次命令的输出文件名,不是专指可执行文件
-g 生成调试信息,便于调试器联系源码、变量与机器代码
-O0 关闭通常的优化,便于我们按源码观察执行
-O2 常见优化级别;本篇实践先不启用

-g 和优化选项相互独立,可以同时使用。没有指定优化选项时 GCC 通常相当于 -O0

2. 编译过程与中间文件


我们常说的"编译程序",有时指的是整条构建链;而狭义的编译阶段,只是其中的一步。

在 hello.c 所在目录执行:

bash 复制代码
gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
gcc -c hello.s -o hello.o
gcc hello.o -o hello
./hello

运行示例:

四条命令分别留下各阶段产物,最后的输出仍为 hello linux。平常是不必手动执行四步的,这里是为了观察。

预处理:处理头文件与宏

-E 表示在预处理后停止。预处理包括头文件包含、宏展开、条件编译等。观察:

bash 复制代码
head -n 15 hello.i
grep -n 'hello linux' hello.i

运行示例:

stdio.h 及其间接包含的内容会被展开,结果通常比原文件长得多。

不加 -o 时,gcc -E hello.c 默认把结果写到标准输出;既可以重定向,也可以用 -o。

编译:产生汇编文本

gcc -S hello.i -o hello.s 把预处理后的代码转换为汇编文本。可以使用 head hello.s 查看头部部分信息。

不同编译器版本、CPU 架构、优化选项可能会生成不同的汇编。

汇编:产生可重定位目标文件

gcc -c hello.s -o hello.o 让汇编代码成为目标文件。.o 通常含有机器代码、数据和链接所需的信息,但还没有完成生成最终程序所需的链接。

如果从源码开始,下面这条命令会自动做前面的必要处理:

bash 复制代码
gcc -c hello.c -o hello.o

所以 -c 不是"只接受汇编文件",而是处理到目标文件为止,不执行最终链接。

链接:把所需部分组织成程序

gcc hello.o -o hello 组织目标文件,并按工具链约定连接运行所需的库等内容,最终生成可执行程序。

3. 多文件编译与链接错误


只有一个源文件时,很难体会到链接的必要性。所以现在我们编写一个求和的函数,把求和功能分开,在新的子目录练习:

bash 复制代码
mkdir -p ~/linux-dev-study/build/sum
cd ~/linux-dev-study/build/sum

创建三个完整文件。sum.h 声明接口:

c 复制代码
#ifndef SUM_H
#define SUM_H

int sum_range(int first, int last);

#endif

sum.c 提供实现:

c 复制代码
#include "sum.h"

int sum_range(int first, int last)
{
    int total = 0;
    for (int i = first; i <= last; ++i) 
    {
        total += i;
    }
    return total;
}

main.c 调用接口:

c 复制代码
#include <stdio.h>
#include "sum.h"

int main()
{
    printf("sum(1..100) = %d\n", sum_range(1, 100));
    return 0;
}

先分别生成目标文件,再进行链接:

bash 复制代码
gcc -std=c11 -c main.c -o main.o
gcc -std=c11 -c sum.c -o sum.o
gcc main.o sum.o -o sum_demo
./sum_demo

运行示例:

也可以一步完成:

bash 复制代码
gcc -std=c11 main.c sum.c -o sum_demo

-std=c11 选项指定 C11,兼顾一些默认语言标准较旧的 GCC 环境。

根据错误位置判断原因

若 main.c 漏写一个分号,编译器在翻译源码时就会报语法错误。若 main.c 本身语法正确,但链接时漏掉 sum.o:

bash 复制代码
gcc main.o -o missing_demo

通常会看到关于 sum_range 的 undefined reference:声明存在,所以对函数的调用可以被编译,但是最终找不到定义,链接无法完成。

库的作用:了解即可

多个目标文件可以直接一起链接,也可以把可复用实现组织为库。Linux 下常见静态库后缀为 .a,共享库常见为 .so 及其版本形式。

静态库链接通常把需要的目标模块纳入最终程序,不是无条件把整个库全部复制进去。动态链接通常保留对共享库的依赖,在装载、运行过程中由相应机制完成连接。

在常见 Linux GCC 工具链环境中,默认生成的程序通常动态链接 C 运行库。

bash 复制代码
file sum_demo
ldd ./sum_demo

运行示例:

输出中的路径、加载器名称和地址因系统而异。

4. Makefile 的规则与增量构建


当我们编写的代码文件多起来之后,每次都手敲一串编译命令就容易遗漏。如果把命令写进普通脚本可以实现批量执行,但如果只改了一个 .c 文件,仍然每次全量编译就有些浪费。

make 的价值是:根据目标与依赖关系,判断哪些结果需要更新,再执行相应命令。 make 是工具,Makefile 是它读取的规则文件,make 自己并不承担 C 语言编译。

先写最小规则

在刚才的 sum 目录中创建 Makefile:

makefile 复制代码
sum_demo: main.c sum.c sum.h
	gcc -std=c11 main.c sum.c -o sum_demo

.PHONY: clean
clean:
	rm -f sum_demo

配方行 gcc ... 与 rm ... 开头是Tab ,不是若干空格。复制后若出现 missing separator,首先检查这一点。

规则的一般形式为:

makefile 复制代码
目标: 前置依赖
	生成或更新目标的命令

在示例中,sum_demo 依赖三个源码文件,只要其中一个比它新,就需要重新生成它。把

bash 复制代码
make
./sum_demo
make

运行示例:

第一次需要时执行编译命令;第二次若文件没有变化,通常提示目标已是最新,不再次执行配方。

把规则分解到目标文件

上面的写法能够工作,但每次有变化仍一起编译两个源文件。所以现在将刚才的 Makefile 整体替换为:

makefile 复制代码
CC = gcc
CFLAGS = -std=c11 -g -O0

sum_demo: main.o sum.o
	$(CC) main.o sum.o -o sum_demo

main.o: main.c sum.h
	$(CC) $(CFLAGS) -c main.c -o main.o

sum.o: sum.c sum.h
	$(CC) $(CFLAGS) -c sum.c -o sum.o

.PHONY: clean
clean:
	rm -f main.o sum.o sum_demo

CC 与 CFLAGS 是变量,$(变量名) 取出内容。链接命令接收 .o 文件,不再重复编译源文件。

使用以下命令观察完整过程:

bash 复制代码
make clean
make
make
touch sum.c
make
touch sum.h
make

运行示例:

  • 依次观察到:清理后生成两个 .o 并链接;无改动时不重建;
  • 更新 sum.c 的时间后重建 sum.o 并链接;
  • 更新 sum.h 的时间后,两个 .o 都重建并链接。
  • touch 在这里用于模拟依赖"变新",即使文件内容没变,也会影响判断。

默认的文件目标更新主要比较修改时间 mtime:目标不存在,或前置依赖的 mtime 更新,通常就需要重建。

系统时钟异常、时间戳未发生有效变化,也可能影响判断。

这套关系还解释了执行顺序:虽然最终目标写在前面,但要得到它,得先确保 main.o、sum.o 可用。通常从所需目标沿依赖向下检查,再执行必要的生成命令。

.PHONY 与 clean

clean 表达的是清理动作,不是生成一个名叫 clean 的文件。

makefile 复制代码
.PHONY: clean

这条声明告诉 make:clean 是伪目标,不以同名文件是否存在、是否最新来判断它。在本篇示例中,每一次的请求 make clean 都会执行清理配方。

不要为了"保证编译"把最终可执行文件随便声明成伪目标,否则会失去它本来的增量判断。本例中 all、clean 等动作或聚合入口适合声明,真实 .o、可执行文件保留为文件目标。

更精简的写法:在读懂规则后使用

同一个小项目可以改写为下面的完整 Makefile:

makefile 复制代码
CC = gcc
CFLAGS = -std=c11  -g -O0
OBJ = main.o sum.o

sum_demo: $(OBJ)
	$(CC) $^ -o $@

%.o: %.c sum.h
	$(CC) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:
	rm -f $(OBJ) sum_demo
写法 本例含义
$@ 当前规则的目标
$^ 当前规则的前置依赖列表,去掉重复项
$< 第一个前置依赖;模式规则里对应匹配的 .c
%.o: %.c sum.h 为同名 .c 与 .o 建立一类规则,并显式依赖 sum.h
  • 这里的 % 匹配共同的文件名主体,不是 Shell 的 *.c 展开。
  • 在命令的开头加上 @ 可隐藏命令回显,初学时可以保留回显来方便观察。

若文件数量增加,可以用 SRC = $(wildcard *.c) 获取当前目录匹配的源码,再用 OBJ = $(SRC:.c=.o) 得到目标文件列表。

5. 小结


  • GCC/G++ 编译分为四阶段:预处理、编译、汇编、链接;可分步生成 .i、.s、.o 目标文件,也可一步完成编译链接。

  • 多文件项目:.h头文件存放函数声明,.c源文件存放实现;编译阶段只检查声明,链接阶段需要找到函数实现,缺失实现会报undefined reference。

  • 常用编译选项:-std指定 C 语言标准,-g添加调试信息,-O0关闭优化,-c只生成.o目标文件不链接。

  • Make 通过文件修改时间 mtime 判断是否重建,只重新编译过期的文件,减少重复编译耗时。

  • touch命令修改文件时间戳,常用于测试 Makefile 的依赖关系。

相关推荐
螺丝钉tina2 小时前
[Playwright][08] Upload 模块自动化:如何设计真实业务测试用例?
运维·自动化·测试用例
迪菲赫尔曼2 小时前
【基础篇 12】把环境与源码接起来:检查 GPU,再安装服务器上的项目
运维·服务器·开发语言·人工智能·python·yolo·目标检测
cq林志炫2 小时前
两个Linux服务器之间同步文件
linux·运维·服务器
yyuuuzz2 小时前
云服务器安全组规则优先级的机制记录
运维·服务器·网络·安全·阿里云·aws
姚瑞南3 小时前
【PRD】招投标商机雷达爬虫系统
人工智能·经验分享·笔记·爬虫·产品运营·产品经理
KaiwuDB3 小时前
KaiwuDB 运维实战06:数据库备份与恢复:从理论到实战
运维·数据库·时序数据库·kaiwudb
无人生还别怕3 小时前
如何搭建k8s服务(containerd部署)
linux·云原生·容器·kubernetes·openanolisos
智购科技自动售货机厂家3 小时前
让AI经营自动售货机:从Project Vend看工具权限与业务规则~YH
大数据·服务器·单片机·嵌入式硬件·深度学习
jnrjian3 小时前
delete file shell 让 AI 写脚本
服务器·开发语言