GNU Makefile编译C/C++(Linux系统、VSCODE)

本文最后更新于:6 个月前

文章改动

GCC简介

GCC 官方文档网站

GCC(GNU Compiler Collection,GNU编译程序集合)是最重要的开放源码软件。其他所有开放源码软件都在某种层次上依赖于它。甚至其他语言,例如 Python,都是由 C 语言开发的,由 GNU 编译程序编译的。这个软件对于整个自由软件运动而言具有根本性的意义。如果没有它或类似的软件,就不可能有自由软件运动。GCC 为 Linux 的出现提供了可能性。GCC 是由许多组件组成的,但它们也并不总是出现的。有些部分是和语言相关的,所以如果没有安装某种特定语言,系统中就不会出现相关的文件。

常见组成部分

  • c++: gcc 的一个版本,默认语言设置为 C++,而且在链接的时候自动包含标准 C++ 库。这和 g++ 一样
  • configure: GCC 源代码树根目录中的一个脚本。用于设置配置值和创建 GCC 编译程序必需的 make 程序文件
  • g++: gcc 的一个版本,默认语言设置为 C++,而且在链接的时候自动包含标准 C++库。这和 c++ 一样
  • gcc: 该驱动程序等同于执行编译程序和连接程序以产生需要的输出
  • libgcc: 该库包含的例程被作为编译程序的一部分,是因为它们可被链接到实际的可执行程序中。它们是特殊的例程,链接到可执行程序,来执行基本的任务,例如浮点运算。这些库中的例程通常都是平台相关的
  • libstdc++: 运行时库,包括定义为标准语言一部分的所有的 C++类和函数

包含的常见软件

  • ar: 这是一个程序,可通过从文档中增加、删除和析取文件来维护库文件。通常使用该工具是为了创建和管理连接程序使用的目标库文档。该程序是 binutils 包的一部分
  • as: GNU 汇编器。实际上它是一族汇编器,因为它可以被编译或能够在各种不同平台上工作。该程序是 binutjls 包的一部分 autoconf:产生的 shell 脚本自动配置源代码包去编译某个特定版本的 UNIX
  • gdb: GNU 调试器,可用于检查程序运行时的值和行为 GNATS:GNU 的调试跟踪系统(GNU Bug Tracking System)。一个跟踪 GCC和其他 GNU 软件问题的在线系统
  • gprof: 该程序会监督编译程序的执行过程,并报告程序中各个函数的运行时间,可以根据所提供的配置文件来优化程序。该程序是 binutils 包的一部分
  • ld: GNU 连接程序。该程序将目标文件的集合组合成可执行程序。该程序是 binutils 包的一部分
  • libtool: 一个基本库,支持 make 程序的描述文件使用的简化共享库用法的脚本
  • make: 一个工具程序,它会读 makefile 脚本来确定程序中的哪个部分需要编译和连接,然后发布必要的命令。它读出的脚本(叫做 makefile 或 Makefile)定义了文件关系和依赖关系

GCC默认头文件搜索路径

1
echo | gcc -v -x c -E -
  • /usr/lib/gcc/x86_64-linux-gnu/7/include
  • /usr/local/include
  • /usr/lib/gcc/x86_64-linux-gnu/7/include-fixed
  • /usr/include/x86_64-linux-gnu
  • /usr/include

简单编译原理

1 hello, world在计算机的表示

hello 程序的生命周期是从一个源程序(或者说源文件)开始的,程序员通过编辑器创建并保存的文本文件,文件名是 hello.c。源程序实际上就是一个由值 0 和 1组成的位(又称为比特)序列,8 个位被组织成一组,称为字节。每个字节表示程序中的某些文本字符

大部分计算机使用 ASCII 标准来表示文本字符

  • 用一个唯一的单字节大小的整数值息来表示每个字符
  • hello.c 程序是以字节序列的方式储存在文件中的

hello.c 的表示方法说明了一个基本思想∶ 系统中所有的信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串比特表示

2 编译过程

hello 程序的生命周期从一个高级 C 语言程序开始

为了在系统上运行 hello.c 程序,每条 C 语句都必须被其他程序转化为一系列的低级机器语言指令

然后这些指令按照一种称为可执行目标程序的格式打好包,并以二进制磁盘文件的形式存放起来

GCC 编译器读取源程序文件 hello.c,并把它翻译成一个可执行目标文件 hello。这个翻译过程可分为四个阶段完成,如下图所示

执行这四个阶段的程序(预处理器、编译器、汇编器和链接器)一起构成了编译系统(compilation system)

2.1 预处理阶段

预处理器(cpp)根据以字符#开头的命令,修改原始的 C 程序。比如 hello.c中第 1行的#include < stdio.h>命令告诉预处理器读取系统头文件 stdio.h 的内容,并把它直接插入程序文本中。结果就得到了另一个 C程序,通常是以.i作为文件扩展名。

2.2 编译阶段

编译器(ccl)将文本文件 hello.i翻译成文本文件 hello.s,它包含一 个汇编语言程序。该程序包含函数 main 的定义,如下所示:

1
2
3
4
5
6
7
main:
subq $8, %rsp
mov1 $.LCO,%edi
call puts
mov1 $0,%eax
addq $8,%rsp
ret

每条语句都以一种文本格式描述了一条低级机器语言指令。汇编语言非常有用,它为不同高级语言的不同编译器提供了通用的输出语言

2.3 汇编阶段

汇编器(as)将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序(relocatable object program)的格式,并将结果保存在目标文件 hello.o中。

hello.o 文件是一个二进制文件,它包含的17 个字节是函数 main的指令编码。如果我们在文本编辑器中打开 hello.o文件,将看到一堆乱码。

2.4 链接阶段

注意,hello程序调用了 printf 函数,它是每个 C 编译器都提供的标准 C 库中的一个函数。printf 函数存在于一个名为 printf.o 的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的 hello.o 程序中。

链接器(ld)就负责处理这种合并。结果就得到 hello 文件,它是一个可执行目标文件(或者简称为可执行文件),可以被加载到内存中,由系统执行。

3 hello, world的执行过程

第一步

  • shell 等待我们输入一个命令
  • 当我们在键盘上输入字符串”./hello”(注意这里是编译好的可执行目标文件)后
  • shell 程序将字符逐一读入寄存器
  • 再把它存放到内存中

第二步

  • 当我们在键盘上敲回车键时,shell 程序就知道我们已经结束了命令的输人
  • 然后 shell 执行一系列指令来加载可执行的 hello 文件
  • 这些指令将 hello 目标文件中的代码和数据从磁盘复制到主存
  • 数据包括最终会被输出的字符串”hello,world\n”。

第三步

  • 一旦目标文件 hello 中的代码和数据被加载到主存
  • 处理器就开始执行 hello 程序的 main 程序中的机器语言指令
  • 这些指令将 “hello,world\n” 字符串中的字节从主存复制到寄存器文件
  • 再从寄存器文件中复制到显示设备,最终显示在屏幕上

4 程序在计算机内的存储

  • hello程序的机器指令最初是存放在磁盘上
  • 当程序加载时,它们被复制到主存
  • 当处理器运行程序时,指令又从主存复制到处理器

相似地,数据串 “hello,world\n”开始时在磁盘上,然后被复制到主存,最后从主存上复制到显示设备

C语言的编译

编译 C 语言相关的后缀

.a 静态库文件

.c 源文件

.h 头文件

.i 预处理文件

.o 目标文件

.s 汇编语言文件

.so 动态库 共享库有 运行时库 文件

2 Compiling C

2.1 Preprocessing

1
2
3
# 不会生成 .i 文件
gcc -E main.c
gcc -E main.c -o helloworld.i
  • -E 选项告诉编译器只进行预处理操作
  • -o 选项把预处理的结果输出到指定文件

2.2 Generating Assembly Language

1
2
gcc -S main.c
gcc -S main.c -o xxx.s
  • -S 选项告诉编译器,进行预处理和编译成汇编语言操作

每个平台对应的汇编语言的形式是不同的,例如有很多型号的开发板,有很多型号的 CPU

2.3 Source File to Object File

1
2
3
4
gcc -c main.c
gcc -c main.c -o xxx.o
# 编译多个 .c 文件
gcc -c main.c add.c minus.c

2.4 Single Source to Executable

  • 注意:后面三个命令执行后并没有按编译过程出现 .i .s 或 .o 文件,并不意味着没有经历这些过程
1
2
gcc main.c
gcc main.c -o xxx

执行程序

1
./可执行文件

2.5 Multiple Sources to Executable

1
2
gcc main.c add.c minus.c -o exec
./exec

3 Creating a Static Library

  • 编译成 .o 的文件

    1
    2
    gcc -c [.c] -o [自定义文件名] 
    gcc -c [.c] [.c] ...
  • 编静态库

    1
    ar -r [lib自定义库名.a] [.o] [.o] ...
  • 链接成可执行文件

    1
    2
    gcc [.c] [.a] -o [自定义输出文件名]
    gcc [.c] -o [自定义输出文件名] -l[库名] -L[库所在路径]

4 Creating a Shared Library

编译二进制.o文件

1
gcc -c -fpic [.c/.cpp][.c/.cpp]... 

编库

1
gcc -shared [.o][.o]... -o [lib自定义库名.so]
  • 链接库到可执行文件
1
gcc [.c/.cpp] -o [自定义可执行文件名]  -l[库名] -L[库路径] -Wl,-rpath=[库路径]

C++的编译

编译c++的相关后缀

.a 静态库

.c .c++ .cc .cp .cpp .cxx

.h 头文件

.hpp 头文件

.ii 与处理文件

.o 目标文件

.so 动态库

.s 汇编语言文件

2 Compiling C++

2.1 Preprocessing

1
2
g++ -E helloworld.c
g++ -E helloworld.c -o helloworld.i
  • -E 选项告诉编译器只进行预处理操作
  • -o 选项把预处理的结果输出到指定文件

2.2 Generating Assembly Language

1
2
g++ -S helloworld.c
g++ -S helloworld.c -o helloworld.s
  • -S 选项告诉编译器,进行预处理和编译成汇编语言操作

每个平台对应的汇编语言的形式是不同的,例如有很多型号的开发板,有很多型号的 CPU 

2.3 Source File to Object File

1
2
3
4
g++ -c helloworld.c
g++ -c helloworld.c -o harumph.o
# 编译多个 .c 文件
g++ -c helloworld.c helloworld1.c helloworld2.c

2.4 Single Source to Executable

  • 注意:后面三个命令执行后并没有按编译过程出现 .i .s 或 .o 文件,并不意味着没有经历这些过程
1
2
g++ helloworld.c
g++ helloworld.c -o howdy

执行程序

1
./可执行文件

2.5 Multiple Source to Executable

1
$ g++ hellomain.c sayhello.c -o hello

3 Creating a Static Library

  • 编译成 .o 的文件

    1
    2
    g++ -c [.c] -o [自定义文件名] 
    g++ -c [.c] [.c] ...
  • 编静态库

    1
    ar -r [lib自定义库名.a] [.o] [.o] ...
  • 链接成可执行文件

    1
    2
    g++ [.c] [.a] -o [自定义输出文件名]
    g++ [.c] -o [自定义输出文件名] -l[库名] -L[库所在路径]

4 Creating a Shared Library

  • 编译二进制.o文件

    1
    g++ -c -fpic [.c/.cpp][.c/.cpp]... 
  • 编库

    1
    g++ -shared [.o][.o]... -o [lib自定义库名.so]
  • 连接动态库到可执行文件

    1
    g++ [.c/.cpp] -o [自定义可执行文件名]  -l[库名] -L[库路径] -Wl,-rpath=[库路径]

makefile

Intro

1 基本格式

1
2
targets : prerequisties
[tab键]command
  • target:目标文件,可以是 OjectFile,也可以是执行文件,还可以是一个标签(Label),对于标签这种特性,在后续的“伪目标”章节中会有叙述。
  • prerequisite:要生成那个 target 所需要的文件或是目标。
  • command:是 make 需要执行的命令,

2 Makefile 规则

  • make 会在当前目录下找到一个名字叫 Makefilemakefile 的文件
  • 如果找到,它会找文件中第一个目标文件(target),并把这个文件作为最终的目标文件
  • 如果 target 文件不存在,或是 target 文件依赖的 .o 文件(prerequities)的文件修改时间要比 target 这个文件新,就会执行后面所定义的命令 command 来生成 target 这个文件
  • 如果 target 依赖的 .o 文件(prerequisties)也存在,make 会在当前文件中找到 target 为 .o 文件的依赖性,如果找到,再根据那个规则生成 .o 文件

3 伪目标

“伪目标” 不是一个文件,只是一个标签。我们要显示地指明这个 “目标” 才能让其生效

“伪目标” 的取名不能和文件名重名,否则不会执行命令

为了避免和文件重名的这种情况,我们可以使用一个特殊的标记 .PHONY 来显示地指明一个目标是“伪目标”,向 make 说明,不管是否有这个文件,这个目标就是 “伪目标”

1
.PHONY : clean

只要有这个声明,不管是否有“clean”文件,要运行 “clean” 这个目标,只有”make clean” 这个命令

Variable

变量在声明时需要给予初值,而在使用时,需要给在变量名前加上 $ 符号,并用小括号 () 把变量给包括起来。

1 变量的定义

1
2
cpp := src/main.cpp 
obj := objs/main.o

2 变量的引用

  • 可以用 (){}
1
2
3
4
5
6
7
cpp := src/main.cpp 
obj := objs/main.o

$(obj) : ${cpp}
@g++ -c $(cpp) -o $(obj)

compile : $(obj)

3 预定义变量

  • $@: 目标(target)的完整名称
  • $<: 第一个依赖文件(prerequisties)的名称
  • $^: 所有的依赖文件(prerequisties),以空格分开,不包含重复的依赖文件
1
2
3
4
5
6
7
8
9
cpp := src/main.cpp 
obj := objs/main.o

$(obj) : ${cpp}
@g++ -c $< -o $@
@echo $^

compile : $(obj)
.PHONY : compile

Operator&Symbols

1 =

  • 简单的赋值运算符
  • 用于将右边的值分配给左边的变量
  • 如果在后面的语句中重新定义了该变量,则将使用新的值

示例

1
2
3
4
5
6
7
8
HOST_ARCH   = aarch64
TARGET_ARCH = $(HOST_ARCH)

# 更改了变量 a
HOST_ARCH = amd64

debug:
@echo $(TARGET_ARCH)

2 :=

  • 立即赋值运算符
  • 用于在定义变量时立即求值
  • 该值在定义后不再更改
  • 即使在后面的语句中重新定义了该变量

示例

1
2
3
4
5
6
7
8
HOST_ARCH   := aarch64
TARGET_ARCH := $(HOST_ARCH)

# 更改了变量 a
HOST_ARCH := amd64

debug:
@echo $(TARGET_ARCH)

3 ?=

  • 默认赋值运算符
  • 如果该变量已经定义,则不进行任何操作
  • 如果该变量尚未定义,则求值并分配
1
2
3
4
5
HOST_ARCH  = aarch64
HOST_ARCH ?= amd64

debug:
@echo $(HOST_ARCH)

4 累加 +=

1
2
3
CXXFLAGS := -m64 -fPIC -g -O0 -std=c++11 -w -fopenmp

CXXFLAGS += $(include_paths)

5 \

  • 续行符

示例

1
2
3
LDLIBS := cudart opencv_core \
gomp nvinfer protobuf cudnn pthread \
cublas nvcaffe_parser nvinfer_plugin

6 * 与 %

  • *: 通配符表示匹配任意字符串,可以用在目录名或文件名中
  • %: 通配符表示匹配任意字符串,并将匹配到的字符串作为变量使用

functions

Makefile 的常用函数

函数调用,很像变量的使用,也是以 “$” 来标识的,其语法如下:

1
$(fn, arguments) or ${fn, arguments}
  • fn: 函数名
  • ​ arguments: 函数参数,参数间以逗号 , 分隔,而函数名和参数之间以“空格”分隔&emsp;

1 shell

1
$(shell <command> <arguments>)
  • 名称:shell 命令函数 —— shell
  • 功能:调用 shell 命令 command
  • 返回:函数返回 shell 命令 command 的执行结果

示例

1
2
3
4
# shell 指令,src 文件夹下找到 .cpp 文件
cpp_srcs := $(shell find src -name "*.cpp") #找到所有.cpp文件
# shell 指令, 获取计算机架构
HOST_ARCH := $(shell uname -m)

2 subst

1
$(subst <from>,<to>,<text>)
  • 名称:字符串替换函数——subst
  • 功能:把字串 <text> 中的 <from> 字符串替换成 <to>
  • 返回:函数返回被替换过后的字符串

    示例:

    1
    2
    3
    4

    cpp_srcs := $(shell find src -name "*.cpp")
    cpp_objs := $(subst src/,objs/,$(cpp_objs))

&emsp;

3 patsubst

1
$(patsubst <pattern>,<replacement>,<text>)
  • 名称:模式字符串替换函数 —— patsubst
  • 功能:通配符 %,表示任意长度的字串,从 text 中取出 patttern, 替换成 replacement
  • 返回:函数返回被替换过后的字符串

    示例

    1
    2
    cpp_srcs := $(shell find src -name "*.cpp") #shell指令,src文件夹下找到.cpp文件
    cpp_objs := $(patsubst %.cpp,%.o,$(cpp_srcs)) #cpp_srcs变量下cpp文件替换成 .o文件

4 foreach

1
$(foreach <var>,<list>,<text>)
  • 名称:循环函数——foreach。
  • 功能:把字串<list>中的元素逐一取出来,执行<text>包含的表达式
  • 返回:<text>所返回的每个字符串所组成的整个字符串(以空格分隔)

示例:

1
2
3
4
library_paths := /datav/shared/100_du/03.08/lean/protobuf-3.11.4/lib \
/usr/local/cuda-10.1/lib64 \

library_paths := $(foreach item,$(library_paths),-L$(item))

同等效果

1
I_flag := $(include_paths:%=-I%)

5 dir

1
$(dir <names...>)
  • 名称:取目录函数——dir。
  • 功能:从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前
    的部分。如果没有反斜杠,那么返回“./”。
  • 返回:返回文件名序列的目录部分。
  • 示例:
    1
    $(dir src/foo.c hacks)    # 返回值是“src/ ./”。

6 notdir

1
$(notdir <names...>)

示例

1
libs   := $(notdir $(shell find /usr/lib -name lib*))&emsp;

7 filter

1
2
3
4
$(filter <names...>)
libs := $(notdir $(shell find /usr/lib -name lib*))
a_libs := $(filter %.a,$(libs))
so_libs := $(filter %.so,$(libs));

8 basename

1
2
3
4
$(basename <names...>)
libs := $(notdir $(shell find /usr/lib -name lib*))
a_libs := $(subst lib,,$(basename $(filter %.a,$(libs))))
so_libs := $(subst lib,,$(basename $(filter %.so,$(libs))))

实战

1 编译过程

1.1 预处理

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cpp_srcs := $(shell find src -name *.cpp)
pp_files := $(patsubst src/%.cpp,src/%.i,$(cpp_srcs))

src/%.i : src/%.cpp
@g++ -E $^ -o $@

preprocess : $(pp_files)

clean :
@rm -f src/*.i

debug :
@echo $(pp_files)

.PHONY : debug preprocess clean

&emsp;

1.2 编译成汇编语言

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cpp_srcs := $(shell find src -name *.cpp)
as_files := $(patsubst src/%.cpp,src/%.s,$(cpp_srcs))

src/%.s : src/%.cpp
@g++ -S $^ -o $@

assemble : $(as_files)

clean :
@rm -f src/*.s

debug :
@echo $(as_files)

.PHONY : debug assemble clean

&emsp;

1.3 编译成目标文件

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cpp_srcs := $(shell find src -name *.cpp)
cpp_objs := $(patsubst src/%.cpp,objs/%.o,$(cpp_srcs))

objs/%.o : src/%.cpp
@mkdir -p $(dir $@)
@g++ -c $^ -o $@

objects : $(cpp_objs)

clean :
@rm -rf objs src/*.o

debug :
@echo $(as_files)

.PHONY : debug objects clean

&emsp;

1.4 链接可执行文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cpp_srcs := $(shell find src -name *.cpp)
cpp_objs := $(patsubst src/%.cpp,objs/%.o,$(cpp_srcs))


objs/%.o : src/%.cpp
@mkdir -p $(dir $@)
@g++ -c $^ -o $@

workspace/exec : $(cpp_objs)
@mkdir workspace/exec
@g++ $^ -o $@

run : workspace
@./$<

clean :
@rm -rf objs workspace/exec

debug :
@echo $(as_files)

.PHONY : debug run clean

&emsp;

2 编译选项

编译选项

  • -m64: 指定编译为 64 位应用程序
  • -std=: 指定编译标准,例如:-std=c++11、-std=c++14
  • -g: 包含调试信息
  • -w: 不显示警告
  • -O: 优化等级,通常使用:-O3
  • -I: 加在头文件路径前
  • fPIC: (Position-Independent Code), 产生的没有绝对地址,全部使用相对地址,代码可以被加载到内存的任意位置,且可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的

链接选项

  • -l: 加在库名前面
  • -L: 加在库路径前面
  • -Wl,<选项>: 将逗号分隔的 <选项> 传递给链接器
  • -rpath=: “运行” 的时候,去找的目录。运行的时候,要找 .so 文件,会从这个选项里指定的地方去找

&emsp;

3 Implicit Rules

  • CC: Program for compiling C programs; default cc
  • CXX: Program for compiling C++ programs; default g++
  • CFLAGS: Extra flags to give to the C compiler
  • CXXFLAGS: Extra flags to give to the C++ compiler
  • CPPFLAGS: Extra flags to give to the C preprocessor
  • LDFLAGS: Extra flags to give to compilers when they are supposed to invoke the linker

&emsp;

4 编译带头文件的程序

add.hpp

1
2
3
4
5
#ifndef ADD_HPP
#define ADD_HPP
int add(int a, int b);

#endif // ADD_HPP

add.cpp

1
2
3
4
int add(int a, int b)
{
return a+b;
}

minus.hpp

1
2
3
4
5
#ifndef MINUS_HPP
#define MINUS_HPP
int minus(int a, int b);

#endif // MINUS_HPP

minus.cpp

1
2
3
4
int minus(int a, int b)
{
return a-b;
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include "add.hpp"
#include "minus.hpp"

int main()
{
int a=10; int b=5;
int res = add(a, b);
printf("a + b = %d\n", res);
res = minus(a, b);
printf("a - b = %d\n", res);

return 0;
}

Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cpp_srcs := $(shell find src -name *.cpp)
cpp_objs := $(patsubst src/%.cpp,objs/%.o,$(cpp_srcs))

# 你的头文件所在文件夹路径(建议绝对路径)
include_paths :=
I_flag := $(include_paths:%=-I%)


objs/%.o : src/%.cpp
@mkdir -p $(dir $@)
@g++ -c $^ -o $@ $(I_flag)

workspace/exec : $(cpp_objs)
@mkdir -p $(dir $@)
@g++ $^ -o $@

run : workspace/exec
@./$<

debug :
@echo $(I_flag)

clean :
@rm -rf objs

.PHONY : debug run

GNU Makefile编译C/C++(Linux系统、VSCODE)
http://example.com/2023/04/17/GNU-Makefile编译C-C-Linux系统、VSCODE/
作者
fan fan
发布于
2023年4月17日
许可协议