来源:尚硅谷嵌入式Linux应用层开发,linux网络编程,linux进程线程,linux文件io_哔哩哔哩_bilibili
目录
1.8.2 为main.c和hello.c编写基本Makefile
1.8.8 安装vscode-makefile-term插件
第 1 章 Linux下的C语言开发
1.1 GCC、glibc和GNU C的关系
1.1.1 GCC
GCC
全称GNU Compiler Collection,是GNU项目的一部分,主要是一套编译器工具集,支持多种编程语言,包括C、C++、Objective-C、Fortran、Ada、Go和D等。GCC最初作为GNU操作系统的官方编译器,用于编译GNU/Linux系统和应用程序。它是自由软件,遵循GNU General Public License(GNU GPL)发布。GCC的主要作用是将源代码编译成机器语言,生成可执行文件或库文件。它也提供了一些优化选项,可以在编译过程中优化代码,提高程序运行的效率。
1.1.2 glibc
glibc
,全称GNU C Library,是GNU项目的一部分,是C语言标准库的一个实现版本,为C语言提供了标准的API,包括输入输出处理、字符串操作、内存管理等。glibc是Linux系统上最常用的C标准库实现之一,它实现了C标准规定的所有标准库函数以及POSIX(可移植操作系统接口)的扩展。
glibc对于Linux系统和基于Linux的应用程序至关重要,因为它提供了与操作系统交互的基本接口和运行时环境。应用程序通过调用glibc提供的函数来执行文件操作、内存管理、进程控制等操作。
1.1.3 GNU C
GNU C
通常指的是GNU项目的C语言编程标准,特别是在GCC中实现的C语言的扩展和特性。GNU C包括ANSI C(现在通常指C89或C90)的所有特性,以及对C99、C11等更现代C标准的支持和一些GNU特有的扩展。
1.1.4 三者之间的关系
GCC使用glibc作为其C语言程序的标准库。当GCC编译C语言程序时,程序中使用的标准库函数(如printf或malloc)是通过glibc提供的。
GNU C是GCC中实现的C语言的一个版本,包含了对C语言标准的支持以及GNU特有的扩展。这些扩展可以在使用GCC编译程序时通过特定的编译选项启用。
总的来说,GCC是编译器,负责将源代码转换为可执行代码;glibc是运行时库,提供程序运行所需的标准函数和操作系统服务的接口;而GNU C则定义了GCC支持的C语言的标准和扩展。
这 三者共同构成了GNU/Linux系统下开发和运行C语言程序的基础。
1.2 POSIX
POSIX,全称为“可移植操作系统接口”(Portable Operating System Interface),是一组标准,用来确保各种不同的操作系统能够提供相同的应用编程接口(API)。这套标准由 IEEE(电气和电子工程师协会)制定,标识符为IEEE 1003。
POSIX标准的主要目的是促进应用软件与多种类型的操作系统之间的兼容性。通过遵循POSIX标准,开发人员可以编写能够在各种不同系统上运行的程序,而无需对程序进行大量修改。这包括Unix、Linux、MacOS以及其他类Unix系统。
主要内容包括:
(1)系统调用和库:定义了操作系统应提供的核心服务,如文件系统操作、进程管理和线程控制。
(2)Shell和工具:规定了标准命令行接口和一系列基本工具,如awk、echo等。
(3)程序接口:包括语言、函数库等接口规范,使程序能够在任何遵循POSIX的操作系统上运行。
POSIX标准的遵循,有助于提高软件的可移植性和灵活性,是现代操作系统开发的重要基石。
1.3 安装IDE编程环境
众所周知,没有真正的大佬会在记事本里面写代码,一定会使用一款趁手的开发工具。所以首先我们需要安装一款合适的代码开发工具。C语言开发比较经典的IDE有VC、VS(VC扩展版)、Clion(付费,和IDEA一个公司)等。这里我们选择VS。
#使用命令安装vs
sudo snap install code --classic
1.4 安装gcc
GCC是C语言的编译器,VS只是代替了记事本,编译器需要根据自己的操作系统额外安装。执行命令:
sudo apt install gcc
根据提示输入y即可。
测试使用GCC
atguigu@ubuntu:~$ gcc
gcc: fatal error: no input files
compilation terminated.
1.5 设置使用VS
1.5.1 启动VSCode
终端输入code命令即可。
code
注意:XShell不支持图形化功能,因此,上述命令的执行及下文的所有图形化操作都要在VMWare中完成。
1.5.2 安装扩展
可以选择自己提前搜索扩展程序,添加对应的扩展。也可以等待系统提示的时候,选择自动安装(推荐)。
1)在家目录创建一个文件夹,helloworld
mkdir helloworld
2)用VSCode打开
在弹出的窗口中勾选如下,点击“是,我信任此作者”即可。之后启动不再弹窗。
3)新建main.c文件
输入文件名。
之后会有弹窗如下,点击右上角的“创建文件”即可。
4)根据提示安装插件
5)查看扩展列表,此时应有6个扩展
1.6 彻底卸载VS
要在Ubuntu系统中彻底卸载Visual Studio Code(简称VSCode),请按照以下步骤操作:
使用终端(Terminal)执行卸载命令:
sudo snap remove code # 对于通过Snap安装的VSCode
删除配置文件和缓存数据:
rm -rf ~/.config/Code # 删除VSCode的用户配置文件
rm -rf ~/.vscode # 删除VSCode的工作区设置和个人扩展
请注意,上述命令会删除所有VSCode相关的用户配置和扩展,如果你想要保留这些设置,请自行备份。
确认卸载后,Visual Studio Code应该已经从你的Ubuntu系统中完全移除。不过,这不会影响你在系统其他位置手动创建的VSCode快捷方式或启动器,如有需要,你可以手动删除这些残留项。
1.7 C语言编译过程
1.7.1 预处理
1)预处理命令
在C语言编译过程中,预处理是其中的第一个阶段,它的主要目的是处理源代码文件中的预处理指令,将它们转换成编译器可以识别的形式。预处理主要包含宏替换、文件包含、条件编译、注释移除等几种任务。预处理的输出通常是经过预处理后的源代码文件,它会被保存成一个临时文件,并作为编译器的输入。预处理器处理后的文件通常会比原始源文件大,因为它会展开宏和包含其他文件的内容。
用下面的命令对两个源文件进行预处理:
gcc -E hello.c -o hello.i
gcc -E main.c -o main.i
-E:Expand(展开)的缩写,该参数指定gcc执行预处理操作。
.i:intermediate(中间的)的缩写,预处理后的源文件通常以.i作为后缀。
得到的hello.i和main.i就是预处理之后的文件。我们可以查看它们的内容,例如main.i:
# 0 "main.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "main.c"
# 1 "hello.h" 1
void say_hello();
# 2 "main.c" 2
int main()
{
say_hello();
return 0;
}
可以看到hello.h的内容已经被展开了。
2).i文件内容解读
与处理后的.i文件包含了经过C预处理器处理的源代码及行控制指令等内容。
源代码不必多数,此处对行控制指令做简要介绍。
.i文件中以#开头的是预处理器插入的行控制指令,用于标示从下一行起的代码来源,格式大致为
行号 "文件名" 标志
1.7.2 编译
编译阶段,编译器会将经过预处理的源代码文件转换成汇编代码。在这个阶段,编译器会将源代码翻译成机器能够理解的中间代码,包括词法分析、语法分析、语义分析和优化等过程。编译器会检查代码的语法和语义,生成对应的汇编代码。编译阶段是整个编译过程中最复杂和耗时的阶段之一,它对源代码进行了深入的分析和转换,确保了程序的正确性和性能。
执行下面的命令对刚刚生成的预处理文件进行编译:
gcc -S hello.i -o hello.s
gcc -S main.i -o main.s
-S:Source(源代码)的缩写,该参数指定gcc将预处理后的源码编译为汇编语言。
.s:Assembly Source(汇编源码)的缩写,通常编译后的汇编文件以.s作为后缀。
我们同样可以观察编译器生成的汇编文件,例如main.s:
# 文件中记录了汇编代码,此外还有编译器版本表示等元数据
.file "main.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $0, %eax
call say_hello@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.....
1.7.3 汇编
1)汇编命令
汇编阶段是C语言编译过程中的重要阶段,它将编译器生成的中间代码或汇编代码转换成目标机器的机器语言代码,也就是目标代码。这个阶段由汇编器(Assembler)完成,其主要任务是将汇编指令翻译成目标机器的二进制形式。主要包含以下几个任务:符号解析、指令翻译、地址关联、重定位、代码优化。最终,汇编器会将翻译和处理后的目标代码输出到目标文件中,用于后续的链接和生成可执行程序或共享库文件。
执行下面的指令对刚刚生成的汇编文件进行汇编:
gcc -c main.s -o main.o
gcc -c hello.s -o hello.o
-c:可以被理解为Compile or Assemble(编译或汇编),该参数可以指定gcc将汇编代码翻译为机器码,但不做链接。此外,该参数也可以用于将.c文件直接处理为机器码,同样不做链接。
-o:Object的缩写,通常汇编得到的机器码文件以.o为后缀。
2).o文件内容解读
这次生成的文件已经是二进制文件了,我们不能用文本编辑器直接查看该文件。可以用下面的指令查看main.o文件实质的内容:
用下面的指令查看main.o文件实质的内容:
objdump -s main.o
main.o: 文件格式 elf64-x86-64-64
Contents of section .text:
0000 f30f1efa 554889e5 b8000000 00e80000 ....UH..........
0010 0000b800 0000005d c3 .......].
Contents of section .comment:
0000 00474343 3a202855 62756e74 75203131 .GCC: (Ubuntu 11
0010 2e342e30 2d317562 756e7475 317e3232 .4.0-1ubuntu1~22
0020 2e303429 2031312e 342e3000 .04) 11.4.0.
Contents of section .note.gnu.property:
0000 04000000 10000000 05000000 474e5500 ............GNU.
0010 020000c0 04000000 03000000 00000000 ................
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 19000000 00450e10 8602430d .........E....C.
0030 06500c07 08000000 .P......
3)反汇编
可以执行下面的指令对main.o内容进行反汇编:
objdump -d main.o
main.o: 文件格式 elf64-x86-64-64
Disassembly of section .text:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: b8 00 00 00 00 mov $0x0,%eax
d: e8 00 00 00 00 call 12 <main+0x12>
12: b8 00 00 00 00 mov $0x0,%eax
17: 5d pop %rbp
18: c3 ret
反汇编内容保留了objdump -s main.o,主要是将.text节的内容反汇编为汇编代码。从000000..(若干0) <main>:下一行起的内容均为汇编代码。同样地,汇编代码可以按照空格分隔,第一列为地址偏移量,第二列至第六列共5列为汇编指令对应的机器码,从第七列开始的均为汇编指令。
1.7.4 链接
链接阶段,由链接器完成。链接器将各个目标文件以及可能用到的库文件进行链接,生成最终的可执行程序。在这个阶段,链接器会解析目标文件中的符号引用,并将它们与符号定义进行匹配,以解决符号的地址关联问题。链接器还会处理全局变量的定义和声明,解决重定位问题,最终生成可执行文件或共享库文件。
1)链接方式
我们在say_hello()函数中调用了printf()函数,这个函数是在stdio.h中声明的,后者来源于glibc库,printf()的实现在glibc的二进制组件中,通常是在共享库(如libc.so)或静态库(如libc.a)文件中。因此,我们除了要链接main.o、hello.o,还需要和glibc库的文件链接。通常,C语言的链接共有三种方式:静态链接、动态链接和混合链接。三者的区别就在于链接器在链接过程中对程序中库函数调用的解析。
(1)静态链接
将所有目标文件和所需的库在编译时一并打包进最终的可执行文件。库的代码被复制到最终的可执行文件中,使得可执行文件变得自包含,不需要在运行时查找或加载外部库。
gcc -static main.o hello.o -o main
-static:该参数指示编译器进行静态链接,而不是默认的动态链接。使用这个参数,GCC会尝试将所有用到的库函数直接链接到最终生成的可执行文件中,包括C标准库(libc)、数学库(libm)和其他任何通过代码引用的外部库。
(2)动态链接
库在运行时被加载,可执行文件包含了需要加载的库的路径和符号信息。动态链接的可执行文件比静态链接的小,因为它们共享系统级的库代码。与静态链接不同,库代码不包含在可执行文件中。
① 方式一
gcc main.o hello.o -o main
没有添加-static关键字,gcc默认执行动态链接,即glibc库文件没有包含到可执行文件中。
② 方式二
我们也可以将自己编写的部分代码处理为动态库。
执行下面的指令将hello.o编译为动态链接库libhello.so。
gcc -fPIC -shared -o libhello.so hello.o
-fPIC:这个选项告诉编译器为“位置无关代码(Position Independent Code)”生成输出。在创建共享库时使用这个选项是非常重要的,因为它允许共享库被加载到内存中的任何位置,而不影响其执行。这是因为位置无关代码使用相对地址而非绝对地址进行数据访问和函数调用,使得库在被不同程序加载时能够灵活地映射到不同的地址空间。
-shared:这个选项指示GCC生成一个共享库而不是一个可执行文件。共享库可以被多个程序同时使用,节省了内存和磁盘空间。
-o libhello.so:这部分指定了输出文件的名称。-o选项后面跟着的是输出文件的名字,这里命名为libhello.so。按照惯例,Linux下的共享库名称以lib开头,扩展名为.so(表示共享对象)。
hello.o:这是命令的输入文件,即之前编译生成的目标文件。在这个例子中,GCC会将hello.o中的代码和数据打包进最终的共享库libhello.so中。
上述命令的作用是:使用GCC,采用位置无关代码的方式,从hello.o目标文件创建一个名为libhello.so的动态共享库文件。
编译完成后查看刚刚编译的动态链接库:
ll
总计 80
drwxrwxr-x 3 atguigu atguigu 4096 3月 27 20:38 ./
drwxr-x--- 23 atguigu atguigu 4096 3月 27 19:12 ../
-rw-rw-r-- 1 atguigu atguigu 91 3月 27 15:17 hello.c
-rw-rw-r-- 1 atguigu atguigu 68 3月 27 15:17 hello.h
-rw-rw-r-- 1 atguigu atguigu 18009 3月 27 17:27 hello.i
-rw-rw-r-- 1 atguigu atguigu 1496 3月 27 17:27 hello.o
-rw-rw-r-- 1 atguigu atguigu 680 3月 27 17:27 hello.s
.....
使用动态链接库编译新的可执行文件:
gcc main.o -L ./ -lhello -o main_d
-L ./:指定了库文件搜索路径。-L选项告诉链接器在哪些目录下查找库文件,./表示当前目录。这意味着在链接过程中,链接器将会在当前目录下搜索指定的库文件。
-lhello:指定了要链接的库。-l选项后面跟库的名称,这里是hello。根据约定,链接器会搜索名为libhello.so(动态库)或libhello.a(静态库)的文件来链接。链接器会根据-L选项指定的路径列表查找这个库。
当前目录下只有libhello.so而没有libhello.a,因此,这条命令的最终效果是动态链接当前目录下的libhello.so库以及默认的glibc库,生成可执行文件main_d。
编译后查看结果:
ll
总计 96
drwxrwxr-x 3 atguigu atguigu 4096 3月 27 20:40 ./
drwxr-x--- 23 atguigu atguigu 4096 3月 27 19:12 ../
-rw-rw-r-- 1 atguigu atguigu 91 3月 27 15:17 hello.c
-rw-rw-r-- 1 atguigu atguigu 68 3月 27 15:17 hello.h
-rw-rw-r-- 1 atguigu atguigu 18009 3月 27 17:27 hello.i
-rw-rw-r-- 1 atguigu atguigu 1496 3月 27 17:27 hello.o
-rw-rw-r-- 1 atguigu atguigu 680 3月 27 17:27 hello.s
-rwxrwxr-x 1 atguigu atguigu 15576 3月 27 20:23
......
这时如果我们直接执行main_d文件,会收到以下报错:
atguigu@ubuntu:~/helloworld$ ./main_d
./main_d: error while loading shared libraries: libhello.so:
cannot open shared object file: No such file or directory
这句报错的意思时main_d在执行过程中,没有找到动态链接库文件libhello.so文件,链接失败无法执行。Linux的默认动态链接库文件夹是/lib 和/usr/lib,而我们的libhello.so不在其中,所以我们需要在执行的时候指明额外的动态链接库文件夹。
LD_LIBRARY_PATH=/home/atguigu/helloworld ./main_d
# Hello World!
(3)混合链接
某些库静态链接,而其他库动态链接。这种方式结合了静态链接和动态链接的优点。
执行下面的指令可以将hello.o编译为静态链接库 libhello.a
ar crv libhello.a hello.o
ar:归档命令,用于处理静态库文件。
crv:ar命令的选项,由三个字符组成,每个字符代表一个选项:
c:创建归档文件。如果指定的归档文件不存在,ar会创建它。
r:替换归档文件中现有的文件或者向归档文件中添加新文件。如果hello.o已经在libhello.a中,它会被新版本替换;如果不存在,则会被添加。
v:详细模式(verbose mode),在处理文件时显示详细信息。使用这个选项,ar会列出它正在执行的操作,包括哪些文件被添加或替换。
libhello.a:要创建或更新的静态库文件的名称。按照惯例,Linux下的静态库文件名以lib开头,并以.a作为文件扩展名。
hello.o:输入文件,即要添加到静态库libhello.a中的目标文件。此处只有一个目标文件hello.o,但ar命令支持同时指定多个文件。
删除动态链接库,如果相同目录下同时存在hello的静态库和动态库文件,链接时会默认选择动态链接。
rm libhello.so
利用静态库文件生成可执行的main文件:
gcc main.o -L ./ -lhello -o main
-L ./:表示额外的库文件位置为当前目录;
-lhello:表示链接libhello.a文件。注意这里要去掉开头的lib前缀和结尾的.a后缀。
编译完成后的main文件同样可以执行,并且不依赖于静态库libhello.a。
需要注意的是,虽然我们静态链接了libhello.a库,但是main文件在执行时依然需要动态链接glibc的库。因此,这种方式实质上并非静态链接,而是混合链接。
2)gblic 的动态库和静态库
glibc 的动态库和静态库分别位于/usr/lib/x86_64_64-linux-gnu/目录下的libc.so和libc.a文件中。
1.8 Makefile基础
Makefile是一种用于管理和自动化软件编译过程的文本文件。它通常包含了一系列规则,这些规则描述了如何根据源代码文件生成可执行文件或者其他目标文件。Makefile的核心概念是规则和依赖关系,规则定义了如何生成一个或多个目标文件,而依赖关系则指定了生成目标文件所需要的源文件或其他依赖文件。下面我们通过一步一步编写Makefile来学习Makefile规则。
1.8.1 安装build-essential工具包
sudo apt install -y build-essential
1.8.2 为main.c和hello.c编写基本Makefile
(1)编写Makefile
vim Makefile
文件内容如下:
# Makefile内容通常由以下部分组成
# <目标>: <前置依赖>
# <需要执行的命令>
# 放在第一个的是默认目标
# 目标为编译出main文件,依赖main.o和hello.o文件
# 编译的命令为 gcc -o main hello.o main.o
main: hello.o main.o
gcc -o main hello.o main.o
# main.o目标依赖main.c hello.h
# 编译命令为gcc -c main.c
main.o: main.c hello.h
gcc -c main.c
# hello.hello.c hello.h
# 编译命令为gcc -c hello.c
hello.o: hello.c hello.h
gcc -c hello.c
# clean目标可以清理编译的临时文件
clean:
rm main main.o hello.o
保存退出。
(2)文件内容解读
规则是Makefile的构建单元,Make工具通过解析这些规则来执行构建过程。
① 规则的基本结构
我们用空行将Makefile的不同规则划分开来。规则有两行构成,第一行为目标和前置依赖,二者通过冒号区分开来,目标在前,前置依赖在后。
# <目标>: <前置依赖>
# <需要执行的命令>
② 目标:本条规则需要生成的目标文件名。
③ 前置依赖:生成目标文件需要的依赖文件列表。
④ 命令:一系列将被Shell执行的命令,用于从前置依赖构建目标。
需要注意的是,Makefile中每个规则的命令必须以一个制表符(tab)开始,而不能是空格。否则会提示“缺失分隔符”。
⑤ 上文提到,gcc的-c参数不仅可以将汇编代码转换为机器码,还可以直接将C语言源文件转换为机器码,gcc -c main.c就是第二种用法,这里省略了-o main.o。默认情况下,在指定-c参数时,gcc会将与源文件名去掉扩展名再加上后缀.o作为目标文件的名称。
(3)测试
① 执行make命令:
make
# make: "main"已是最新。
提示我们“main”已是最新,这是因为上面的操作已经生成了最终的可执行文件“main”,要看到make的作用,需要先将之前编译好的文件删除
rm main.o hello.o main
② 重新执行make
atguigu@ubuntu:~/helloworld$ make
gcc -c hello.c
gcc -c main.c
gcc -o main hello.o main.o
可以看到make首先将hello.c转换为hello.o,然后将main.c转换为main.o,最后生成main可执行文件。
③ 同理,执行make clean可以执行clean目标:
atguigu@ubuntu:~/helloworld$ make clean
rm -f main main.o hello.o
这个目标我们定义了如何清理编译的残留文件和结果。执行这个目标后,我们的编译结果和临时文件就都被清理了。
这就是Makefile,可以批处理进行一键编译,大大提高了编译效率。
1.8.3 引入变量
Makefile中为了方便,可以引入临时变量:
# 定义变量objects
objects := hello.o\
main.o
# 在目标中引入变量
main: $(objects)
gcc -o main $(objects)
main.o: main.c hello.h
gcc -c main.c
hello.o: hello.c hello.h
gcc -c hello.c
# clean目标中也可以引入变量
clean:
rm main $(objects)
objects 为变量名
:= 的组合相当于C语言中的=,表示赋值
:= 后面为变量的值
\ 为续行符,表示命令或定义延续到下一行。此处的作用是将hello.o和main.o合并为一行,此处的定义等价于objects := hello.o main.o。
$(变量名) 表示获取变量的值
1.8.4 引入make自动推导:
(1)更改Makefile
make可以根据目标自动加入所需的依赖文件和命令。例如main.o目标,会默认将main.c作为依赖加入,同时也可以自动推导出编译main.o的命令,于是我们的Makefile就可以改成以下内容:
objects := hello.o\
main.o
main: $(objects)
gcc -o main $(objects)
# 利用make的自动推导
clean:
rm main $(objects)
(2)依赖文件的作用
要注意的是,虽然这种方式精简Makefile的内容,但是当没有显式声明的依赖文件发生更改时Make无法追踪。
① 执行make命令,确保控制台输出如下
make
# make: “main”已是最新。
② 更改hello.h,在其中任意位置加入空行
③ 重新执行
make
# make: “main”已是最新。
此时Make工具没有检测到hello.h的更新。
④ 将Makefile恢复为以下内容。
# Makefile内容通常由以下3部分组成
# <目标名称>:<前置依赖>
# \t<需要执行的命令>
# 定义变量 objects
objects := hello.o \
main.o
# 放在第一个的是默认目标
# 目标是编译出main文件 依赖hello.o和main.o文件
# 编译的命令是gcc hello.o main.o -o main
main: $(objects)
gcc $(objects) -o main
#目标是main.o 依赖main.c和hello.h
#编译的命令是gcc -c main.c
# main.o: main.c hello.h
# gcc -c main.c
main.o: hello.h
#目标是hello.o 依赖hello.c和hello.h
#编译的命令是gcc -c hello.c
# hello.o: hello.c hello.h
# gcc -c hello.c
hello.o: hello.h
clean:
rm main $(objects)
⑤ 执行make
make
# make: “main”已是最新。
⑥ 更改hello.h,删除任意空行
⑦ 重新执行
make
# gcc -c main.c
# gcc -o main hello.o main.o
⑧ 总结:只有在Makefile中显式声明依赖的头文件才会被追踪,当它们发生更改时,重新执行make命令,会再次执行相应规则的命令。
1.8.5 引入伪目标
(1)伪目标
伪目标并不代表实际的文件名,它们更多的是行为或动作的标识符。伪目标并不生成具体文件。
(2).PHONY目标
① .PHONY是Makefile中一个特殊的目标,用于声明其它目标是伪目标。
② 语法:.PHONY:<伪目标名称>
③ 细心的同学可能发现,目标为clean的规则没有前置依赖,这是因为它是用来执行清理操作的,并不是要生成名为clean的文件,因此不需要前置依赖。我们可以将clean声明为伪目标。
④ 修改Makefile,如下。
# 定义变量objects
objects := hello.o\
main.o
# 在目标中引入变量
main: $(objects)
gcc -o main $(objects)
main.o: hello.h
hello.o: hello.h
# 声明伪目标
.PHONY: clean
# clean目标中也可以引入变量
clean:
rm main $(objects)
保存退出。
⑤ 执行
make clean
# rm main hello.o main.o
rm main hello.o main.o
(3)为什么需要声明伪目标
我们看到,将clean声明为伪目标后执行make clean的结果与之前并无二致。那么声明伪目标的意义何在?
执行以下操作。
① 在helloworld目录下创建名为clean的文件
② 删除Makefile中的.PHONY: clean,保存退出
③ 重新执行make
atguigu@ubuntu:~/helloworld$ make
gcc -c hello.c
gcc -c main.c
gcc -o main hello.o main.o
④ 重新执行make clean
atguigu@ubuntu:~/helloworld$ make clean
make: “clean”已是最新。
我们发现,执行make clean并没有像我们预想的那样删除文件,而是告诉我们“clean”已是最新。这是因为,make将clean作为普通目标处理,它先检查clean的依赖(不存在),然后发现clean文件已存在且没有依赖更新(因为不存在,自然不需要更新),因此不会执行规则下的命令,并在控制台输出以上内容。显然,这不是我们期望的行为。
⑤ 在Makefile中添加.PHONY: clean将clean声明为伪目标,并保存退出
⑥ 重新执行make clean
atguigu@ubuntu:~/helloworld$ make clean
rm main hello.o main.o
可以看到,虽然目录下有名为clean的文件,但make仍执行了clean所在规则的命令。这是我们期望的行为。
⑦ 总结:将某些不生成目标文件的行为或动作(如清理、安装)声明为伪目标可以确保无条件执行规则下的命令。即便执行make命令时当前目录下存在与目标同名的文件,依然可以得到我们期望的效果。
1.8.6 忽略错误
如果我们在没有编译时执行make clean目标,会收到以下报错:
atguigu@ubuntu:~/helloworld$ make clean
rm main hello.o main.o
rm: 无法删除 'main': 没有那个文件或目录
rm: 无法删除 'hello.o': 没有那个文件或目录
rm: 无法删除 'main.o': 没有那个文件或目录
make: *** [Makefile:20:clean] 错误 1
make命令最终以返回码1推出了。我们可以将Makefile改为下面的形式:
# 定义变量objects
objects := hello.o\
main.o
# 在目标中引入变量
main: $(objects)
gcc -o main $(objects)
main.o: hello.h
hello.o: hello.h
# 声明伪目标
.PHONY: clean
# clean目标中也可以引入变量
clean:
-rm main $(objects)
rm前面的-告诉make,如果该命令执行失败,不要停止执行剩余的过程,即忽略错误。
再次执行make clean目标,提示信息会变为:
atguigu@ubuntu:~/helloworld$ make clean
rm main hello.o main.o
rm: 无法删除 'main': 没有那个文件或目录
rm: 无法删除 'hello.o': 没有那个文件或目录
rm: 无法删除 'main.o': 没有那个文件或目录
make: [Makefile:20:clean] 错误 1 (已忽略)
错误已被忽略。
1.8.7 目标名和命令中输出文件名的关系
(1)修改Makefile
# 定义变量objects
objects := hello.o\
main.o
# 在目标中引入变量
main: $(objects)
gcc -o main123 $(objects)
main.o: hello.h
hello.o: hello.h
# 声明伪目标
.PHONY: clean
# clean目标中也可以引入变量
clean:
-rm main $(objects)
将命令中最终输出的文件名由main更改为main123。
保存退出。
(2)执行make
上一步已经删除了目录下的main.o,hello.o和main。
atguigu@ubuntu:~/helloworld$ make
gcc -c hello.c
gcc -c main.c
gcc -o main123 hello.o main.o
(3)分析
当前目录下不存在名为main的文件,只有名为mian123的文件,可以得出结论:规则中的命令决定了生成目标文件的名称。目标名并不影响目标文件名。
(4)再次执行make
atguigu@ubuntu:~/helloworld$ make
gcc -o main123 hello.o main.o
我们发现,make没有提示目标文件已是最新,而是重新执行了gcc -o main123 hello.o main.o。这是因为,make会按照目标名称在当前目录下追踪目标文件,如果不存在与目标同名的文件,会再次执行规则下的命令。
(5)总结
make输出的文件名取决于规则下的命令,而目标名称决定make追踪的目标文件名。如果二者不一致,make就会认为目标文件不存在而不断执行命令。我们应确保命令生成的目标文件名和目标名一致。
1.8.8 安装vscode-makefile-term插件
在插件市场搜索并安装vscode-makefile-term插件,完成后,在VScode中打开Makefile文件,可以看到每个target上方都出现了执行按钮。
点击这些按钮vscode-makefile-term插件就会帮助我们执行相应的target。