Linux下的C语言开发
GCC、glibc和GNU C的关系
GCC全称GNU Compiler Collection,是GNU项目的一部分,主要是一套编译器工具集,支持多种编程语言,包括C、C++、Objective-C、Fortran、Ada、Go和D等。GCC最初作为GNU操作系统的官方编译器,用于编译GNU/Linux系统和应用程序。它是自由软件,遵循GNU GPL(GNU General Public License)开源协议发布。
GCC的主要作用是将源代码编译成机器语言,生成可执行文件或库文件。它也提供了一些优化选项,可以在编译过程中优化代码,提高程序运行的效率。
glibc,全称GNU C Library,是GNU项目的一部分,是C语言标准库的一个实现版本,为C语言提供了标准的API,包括输入输出处理、字符串操作、内存管理等。glibc是Linux系统上最常用的C标准库实现之一,它实现了C标准规定的所有标准库函数以及POSIX(可移植操作系统接口)的扩展。
glibc对于Linux系统和基于Linux的应用程序至关重要,因为它提供了与操作系统交互的基本接口和运行时环境。应用程序通过调用glibc提供的函数来执行文件操作、内存管理、进程控制等操作。
GNU C通常指的是GNU项目的C语言编程标准,特别是在GCC中实现的C语言的扩展和特性。GNU C包括ANSI C(现在通常指C89或C90)的所有特性,以及对C99、C11等更现代C标准的支持和一些GNU特有的扩展。
三者的关系
GCC使用glibc作为其C语言程序的标准库。当GCC编译C语言程序时,程序中使用的标准库函数(如printf或malloc)是通过glibc提供的。
GNU C是GCC中实现的C语言的一个版本,包含了对C语言标准的支持以及GNU特有的扩展。这些扩展可以在使用GCC编译程序时通过特定的编译选项启用。
总的来说,GCC是编译器,负责将源代码转换为可执行的机器代码;glibc是运行时库,提供程序运行所需的标准函数和操作系统服务的接口;而GNU C则定义了GCC支持的C语言的标准和扩展。
这三者共同构成了GNU/Linux系统下开发和运行C语言程序的基础。
C语言编译过程
预处理命令
在C语言编译过程中,预处理是其中的第一个阶段,它的主要目的是处理源代码文件中的预处理指令,将它们转换成编译器可以识别的形式。预处理主要包含宏替换、文件包含、条件编译、注释移除等几种任务。预处理的输出通常是经过预处理后的源代码文件,它会被保存成一个临时文件,并作为编译器的输入。预处理器处理后的文件通常会比原始源文件大,因为它会展开宏和包含其他文件的内容。
用下面的命令对两个源文件进行预处理:
1
2
linux_app/1_C/02_compile$ gcc -E hello.c -o hello.i
linux_app/1_C/02_compile$ gcc -E main.c -o main.i
- -E(大写):Expand(展开)的缩写,指定gcc执行预处理。
- .i:intermediate(中间的)的缩写,预处理后的源文件通常以.i作为后缀。
- 注意:-o不能省略
得到的hello.i和main.i就是预处理之后的文件。
.i文件内容解读
与处理后的.i文件包含了经过C预处理器处理的源代码及行控制指令等内容。
此处对行控制指令做简要介绍。
.i文件中以#开头的是预处理器插入的行控制指令,用于标示从下一行起的代码来源,格式大致为
1
\# 行号 "文件名" 标志
行号和文件名表示从下一行开始的源代码来源于哪个文件的哪一行。
标志可以是数字1,2,3,4,每个数字的含义如下:
- 表示接下来的内容开始于一个新的文件。
- 表示控制权从被包含的文件返回。这用于当预处理器完成一个包含文件的读取,回到包含它的文件继续处理时。
- 指示接下来的内容来自系统头文件。
- 表明接下来的内容应被视为被extern “C”包围,这主要用于C++中,以指示C链接约定。extern C是C++中的关键字组合,我们不必关注。
行号为0通常是预处理器的一种特殊标记用法,并不指向源代码中的实际行号。它可能用于初始化或特殊标记,比如标识文件的开始,而不直接对应于源代码中的行。我们只需要知道.i文件是将源码中宏定义处理之后的源文件,其它内容了解即可。
编译
编译阶段,编译器会将经过预处理的源代码文件转换成汇编代码。在这个阶段,编译器会将源代码翻译成机器能够理解的中间代码,包括词法分析、语法分析、语义分析和优化等过程。编译器会检查代码的语法和语义,生成对应的汇编代码。编译阶段是整个编译过程中最复杂和耗时的阶段之一,它对源代码进行了深入的分析和转换,确保了程序的正确性和性能。
1
2
3
4
5
6
执行下面的命令对刚生成的预处理文件进行编译生成汇编代码:
linux_app/1_C/02_compile$ gcc -S hello.i -o hello.s
linux_app/1_C/02_compile$ gcc -S main.i -o main.s
也可以对c文件直接进行编译生成汇编代码
linux_app/1_C/02_compile$ gcc -S hello.c -o hello.s
linux_app/1_C/02_compile$ gcc -S main.c -o main.s
- -S(大写):Source(源代码)的缩写, 用来生成汇编源代码。
- .s:Assembly Source(汇编源码)的缩写,通常编译后的汇编文件以.s作为后缀。
汇编
汇编阶段是C语言编译过程中的重要阶段,它将汇编代码转换成目标机器的机器语言代码,也就是目标代码。这个阶段由汇编器(Assembler)完成,其主要任务是将汇编指令翻译成目标机器的二进制形式。主要包含以下几个任务:符号解析、指令翻译、地址关联、重定位、代码优化。最终,汇编器会将翻译和处理后的目标代码输出到目标文件中,用于后续的链接和生成可执行程序或共享库文件。
1
2
3
4
5
执行下面的指令对刚刚生成的汇编文件进行汇编生成机器码:
linux_app/1_C/02_compile$ gcc -c main.s -o main.o
linux_app/1_C/02_compile$ gcc -c hello.s -o hello.o
也可以直接对C文件进行汇编生成机器码,汇编时-o可以省略,默认就是同名.o文件
linux_app/1_C/02_compile$ gcc -c main.c hello.c
- -c(小写):Compile (汇编生成机器代码)的缩写,该参数可以指定gcc将汇编代码翻译为机器码,但不做链接。此外,该参数也可以用于将.c文件直接处理为机器码,同样不做链接。
- .o:Object的缩写,通常汇编得到的机器码文件以.o为后缀。
这次生成的文件已经是二进制文件了,我们不能用文本编辑器直接查看该文件。可以用下面的指令查看main.o文件中所有包含数据的节(section):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
linux_app/1_C/02_compile$ 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......
文件大致可以分为五个部分:
- 文件格式
最上面的行标明了文件的格式为:elf64-x86-64-64,为x86-64架构设计的64位ELF文件格式。ELF英文全称为Executable and Linkable Format,即可执行链接格式,了解即可。
- .text节
从Contents of section .text开始到下一个Contents之间的属于.text节。这部分包含了程序的机器代码或指令,是程序实际执行的代码。
①列之间是以空格分隔的,左侧第一列为四位16进制数,用于表示当前行的地址偏移量,上述文件中,.text节第一列第一行为0x0000,表示这一行的地址偏移量是从0开始的,第一列第二行为0x0010,表示这一行的偏移量是从十进制的16开始的。
②从第二至第五列共4列均为16进制数表示的机器码,一行写满,刚好占用16个字节,因此,第一行的地址从0开始,第二行从16开始。可以看到,main函数源码处理之后得到的机器码共占用了25个字节的空间。
③第六列即最后一列是机器码的ASCII码表示,和②中的16进制表示相对应。对于ASCII码无法表示的字符,全部用.表示,对于.text节,这部分是无意义的,因为机器码的意义和作用与ASCII码表示无关。
- .comment节
从Contents of section .comment至下一个Contents之间的属于.comment节。这部分包含编译器版本的信息,用于记录编译这个文件的环境。
这一节及后续两节的列布局与.text相同,即第一列为地址偏移量,最后一列为机器码的ASCII码表示,其余列为机器码的16进制表示,不再赘述。
本节记录的信息可以从ASCII码表示部分读取:编译器版本为GCC:(Ubuntu 11.4.0-1ubuntu1~22.04)11.4.0。
-
.note.gnu.property节
从Contents of section .note.gnu.property到下一个Contents之间的部分属于.note.gnu.property节。通常包含了一些GNU特定的属性。这部分内容与.text节相同,无法通过ASCII码解读。了解即可。 -
.eh_frame节
从Contents of section .eh_frame到下一个Contents之间的部分属于.eh_frame节。包含了用于异常处理的元数据,如每个函数的堆栈信息,可用于异常处理和调试。同样无法通过ASCII码解读。
可以执行下面的指令对main.o内容进行反汇编:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
linux_app/1_C/02_compile$ 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列为汇编指令对应的机器码,从第七列开始的均为汇编指令。
链接
链接阶段,由链接器完成。链接器将各个目标文件以及可能用到的库文件进行链接,生成最终的可执行程序。在这个阶段,链接器会解析目标文件中的符号引用,并将它们与符号定义进行匹配,以解决符号的地址关联问题。链接器还会处理全局变量的定义和声明,解决重定位问题,最终生成可执行文件或共享库文件。
我们在say_hello()函数中调用了printf()函数,这个函数是在stdio.h中声明的,后者来源于glibc库,printf()的实现在glibc的二进制组件中,通常是在共享库(如libc.so)或静态库(如libc.a)文件中。因此,我们除了要链接main.o、hello.o,还需要和glibc库的文件链接。通常,C语言的链接共有三种方式:静态链接、动态链接和混合链接。三者的区别就在于链接器在链接过程中对程序中库函数调用的解析。
- 静态链接 将所有目标文件和所需的库在编译时一并打包进最终的可执行文件。库的代码被复制到最终的可执行文件中,使得可执行文件变得自包含,不需要在运行时查找或加载外部库。
1
linux_app/1_C/02_compile$ gcc -static main.o hello.o -o main
-static:该参数指示编译器进行静态链接,而不是默认的动态链接。使用这个参数,GCC会尝试将所有用到的库函数直接链接到最终生成的可执行文件中,包括C标准库(libc)、数学库(libm)和其他任何通过代码引用的外部库。
- 动态链接
库在运行时被加载,可执行文件包含了需要加载的库的路径和符号信息。动态链接的可执行文件比静态链接的小,因为它们共享系统级的库代码。与静态链接不同,库代码不包含在可执行文件中。
1
linux_app/1_C/02_compile$ gcc main.o hello.o -o main2
没有添加-static关键字,gcc默认执行动态链接,即glibc库文件没有包含到可执行文件中。
我们也可以将自己编写的部分代码处理为动态库。
执行下面的指令将hello.o编译为动态链接库libhello.so。
1
linux_app/1_C/02_compile$ gcc -fPIC -shared hello.o -o libhello.so
-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的动态共享库文件。
编译完成后查看刚刚编译的动态链接库:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
linux_app/1_C/02_compile$ 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
-rwxrwxr-x 1 atguigu atguigu 15576 3月 27 20:23 libhello.so*
-rw-rw-r-- 1 atguigu atguigu 67 3月 27 15:17 main.c
-rw-rw-r-- 1 atguigu atguigu 225 3月 27 15:18 main.i
-rw-rw-r-- 1 atguigu atguigu 1360 3月 27 15:18 main.o
-rw-rw-r-- 1 atguigu atguigu 589 3月 27 15:18 main.s
drwxrwxr-x 2 atguigu atguigu 4096 3月 27 14:24 .vscode/
使用动态链接库编译新的可执行文件:
1
linux_app/1_C/02_compile$ 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。
1
2
3
这时如果我们直接执行main_d文件,会收到以下报错:
linux_app/1_C/02_compile$ ./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不在其中,所以我们需要在执行的时候指明额外的动态链接库文件夹.
1
2
linux_app/1_C/02_compile$ LD_LIBRARY_PATH=/home/zxf/桌面/linux_app/1_C/02_compile ./main_d
Hello world!
glibc的动态库和静态库分别位于/usr/lib/x86_64-linux-gnu/目录下的libc.so和libc.a文件中。
Make基础
Make是一个自动化构建工具,用于从源代码到构建程序、运行和删除文件等操作。它需要我们提供一个名为Makefile的文本文件。它通常包含了一系列规则,这些规则描述了如何根据源代码文件生成可执行文件或者其他目标文件。Makefile的核心概念是规则和依赖关系,规则定义了如何生成一个或多个目标文件,而依赖关系则指定了生成目标文件所需要的源文件或其他依赖文件。下面我们通过一步一步编写Makefile来学习Makefile规则。
build-essential: 常用的软件包集合,它包含了编译和构建软件所需的基本开发工具和库文件,如:GCC、Make、。。。
1
linux_app/1_C/03_makefile$ sudo apt install -y build-essential
(1)编写Makefile
1
linux_app/1_C/03_makefile$ touch 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
# Makefile内容通常由以下部分组成
# <目标>: <前置依赖>
# <需要执行的命令>
# 放在第一个的是默认目标
# main目标依赖main.o和hello.o文件
# 编译的命令: gcc hello.o main.o -o main
main: hello.o main.o
gcc main.o hello.o -o main
# main.o目标依赖main.c hello.h
# 编译命令:gcc -c main.c
main.o: main.c hello.h
gcc -c main.c -o main.o
# hello.o目标依赖hello.c hello.h
# 编译命令:gcc -c hello.c
hello.o: hello.c
gcc -c hello.c -o hello.o
# clean目标不依赖任何文件
# 作用:清理编译的临时文件
clean:
rm main main.o hello.o
(2)文件内容解读
规则是Makefile的构建单元,Make工具通过解析这些规则来执行构建过程。
① 规则的基本结构
我们用空行将Makefile的不同规则划分开来。规则有两行构成,第一行为目标和前置依赖,二者通过冒号区分开来,目标在前,前置依赖在后。
1
2
# <目标>: <前置依赖>
# <需要执行的命令>
② 目标:本条规则需要生成的目标文件名。
③ 前置依赖:生成目标文件需要的依赖文件列表。
④ 命令:一系列将被Shell执行的命令,用于从前置依赖构建目标。
注意:Makefile中每个规则的命令必须以一个制表符(tab)开始,而不能是空格。否则会提示“缺失分隔符”。
⑤ 上文提到,gcc的-c参数不仅可以将汇编代码转换为机器码,还可以直接将C语言源文件转换为机器码,gcc -c main.c就是第二种用法,这里省略了-o main.o。默认情况下,在指定-c参数时,gcc会将与源文件名去掉扩展名再加上后缀.o作为目标文件的名称。
执行make命令:
1
2
linux_app/1_C/03_makefile$ make
make: "main"已是最新。
提示我们“main”已是最新,这是因为上面的操作已经生成了最终的可执行文件“main”,要看到make的作用,需要先将之前编译好的文件删除
1
linux_app/1_C/03_makefile$ rm main.o hello.o main
1
2
3
4
5
重新执行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目标:
1
2
3
同理,执行make clean可以执行clean目标:
linux_app/1_C/03_makefile$ make clean
rm -f main main.o hello.o
这个目标我们定义了如何清理编译的残留文件和结果。执行这个目标后,我们的编译结果和临时文件就都被清理了。
这就是Makefile,可以批处理进行一键编译,大大提高了编译效率。
引入变量
Makefile中为了方便,可以引入临时变量:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 定义变量
# objects := hello.o main.o
objects := hello.o \
main.o
# 在目标中引入变量
main: $(objects)
gcc $(objects) -o main
main.o: main.c hello.h
gcc -c main.c
hello.o: hello.c
gcc -c hello.c
# clean目标中也可以引入变量
clean:
rm main $(objects)
- objects为变量名
- :=的组合相当于C语言中的=,表示赋值,:=后面为变量的值
- +=的组合为以空格拼接右边的值
- \为续行符,表示命令或定义延续到下一行。此处的作用是将hello.o和main.o合并为一行,此处的定义等价于objects := hello.o main.o。
- $(变量名)表示获取变量的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
依赖文件的作用
要注意的是,不指定依赖文件也可以正确执行,但是当没有显式声明的依赖文件发生更改时Make无法追踪。
① 删除main.o目标中的hello.h依赖, 第一次执行目标生成目标文件
main.o: main.c
gcc -c main.c
linux_app/1_C/03_makefile$ make main.o
gcc -c main.c -o main.o
② 更改hello.h,在其中任意位置加入空行
③ 重新执行
linux_app/1_C/03_makefile$ make main.o
make: “main.o”已是最新。
此时Make工具没有检测到hello.h的更新。
④ 将Makefile恢复为以下内容。
main.o: main.c hello.h
gcc -c main.c
⑤ 执行make
linux_app/1_C/03_makefile$ make main.o
make: “main.o”已是最新。
⑥ 更改hello.h,删除任意空行
⑦ 重新执行
linux_app/1_C/03_makefile$ make main.o
gcc -o main hello.o main.o
⑧ 总结:只有在Makefile中显式声明依赖的头文件才会被追踪,当它们发生更改时,重新执行make命令,会再次执行相应规则的命令。
引入伪目标
(1)伪目标
伪目标并不代表实际的文件名,它们更多的是行为或动作的标识符。伪目标并不生成具体文件。
(2).PHONY目标
① .PHONY是Makefile中一个特殊的目标,用于声明其它目标是伪目标。
② 语法:.PHONY: 伪目标名称
③ 细心的同学可能发现,目标为clean的规则没有前置依赖,这是因为它是用来执行清理操作的,并不是要生成名为clean的文件,因此不需要前置依赖。我们可以将clean声明为伪目标。
④ 修改Makefile,如下。
1
2
3
4
5
6
# 声明伪目标
.PHONY: clean
# clean目标中也可以引入变量
clean:
rm main $(objects)
1
2
linux_app/1_C/03_makefile$ make clean
rm main hello.o main.o
(3)为什么需要声明伪目标
我们看到,将clean声明为伪目标后执行make clean的结果与之前并无二致。那么声明伪目标的意义何在?
执行以下操作。
① 在helloworld目录下创建名为clean的文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
linux_app/1_C/03_makefile$ touch clean
linux_app/1_C/03_makefile$ ll
总计 96
drwxrwxr-x 3 atguigu atguigu 4096 3月 28 15:24 ./
drwxr-x--- 23 atguigu atguigu 4096 3月 28 15:00 ../
-rw-rw-r-- 1 atguigu atguigu 0 3月 28 15:24 clean
-rw-rw-r-- 1 atguigu atguigu 92 3月 28 14:45 hello.c
-rw-rw-r-- 1 atguigu atguigu 69 3月 28 14:45 hello.h
-rw-rw-r-- 1 atguigu atguigu 18009 3月 27 17:27 hello.i
-rw-rw-r-- 1 atguigu atguigu 680 3月 27 17:27 hello.s
-rw-rw-r-- 1 atguigu atguigu 1642 3月 27 20:41 libhello.a
-rwxrwxr-x 1 atguigu atguigu 16024 3月 28 14:30 main123*
-rw-rw-r-- 1 atguigu atguigu 67 3月 28 14:53 main.c
-rwxrwxr-x 1 atguigu atguigu 15952 3月 27 20:40 main_d*
-rw-rw-r-- 1 atguigu atguigu 225 3月 27 15:18 main.i
-rw-rw-r-- 1 atguigu atguigu 589 3月 27 15:18 main.s
-rw-rw-r-- 1 atguigu atguigu 279 3月 28 14:52 Makefile
drwxrwxr-x 2 atguigu atguigu 4096 3月 27 14:24 .vscode/
linux_app/1_C/03_makefile$
② 删除Makefile中的.PHONY: clean,保存退出 ③ 重新执行make
1
2
3
4
linux_app/1_C/03_makefile$ make
gcc -c hello.c
gcc -c main.c
gcc -o main hello.o main.o
④ 重新执行make clean
1
2
linux_app/1_C/03_makefile$ make clean
make: “clean”已是最新。
我们发现,执行make clean并没有像我们预想的那样删除文件,而是告诉我们“clean”已是最新。这是因为,make将clean作为普通目标处理,它先检查clean的依赖(不存在),然后发现clean文件已存在且没有依赖更新(因为不存在,自然不需要更新),因此不会执行规则下的命令,并在控制台输出以上内容。显然,这不是我们期望的行为。
⑤ 在Makefile中添加.PHONY: clean将clean声明为伪目标,并保存退出
⑥ 重新执行make clean
1
2
linux_app/1_C/03_makefile$ make clean
rm main hello.o main.o
可以看到,虽然目录下有名为clean的文件,但make仍执行了clean所在规则的命令。这是我们期望的行为。
⑦ 总结:将某些不生成目标文件的行为或动作(如清理、安装)声明为伪目标可以确保无条件执行规则下的命令。即便执行make命令时当前目录下存在与目标同名的文件,依然可以得到我们期望的效果。
忽略错误
当一个目标下包含多条命令时,如果前面的命令执行失败了,后面的命令就不会执行。
如果需要后面的命令依然能执行,就需要忽略错误。
忽略错误的编码只需要在命令的左侧加上“-”。
1
2
3
4
clean2:
rm hello.o
rm main.o
rm main
如果hello.o不存在,执行clean2目标时,由于第一条命令执行失败,后面的2条命令就不会执行。
添加忽略错误配置,再执行clean2目标,可以看到后面的2条命令都执行了。
1
2
3
4
clean2:
-rm hello.o
rm main.o
rm main
目标名和命令中输出文件名的关系
(1)修改Makefile
1
2
3
4
5
6
# 定义变量objects
objects := hello.o main.o
# 在目标中引入变量
main: $(objects)
gcc $(objects) -o main123
将命令中最终输出的文件名由main更改为main123。
保存退出。
(2)执行make
上一步已经删除了目录下的main.o,hello.o和main。
1
2
3
4
linux_app/1_C/03_makefile$ make
gcc -c hello.c
gcc -c main.c
gcc -o main123 hello.o main.o
(3)可以看到make的日志中,最终生成的文件为main123,查看当前目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
linux_app/1_C/03_makefile$ ll
总计 104
drwxrwxr-x 3 atguigu atguigu 4096 3月 28 15:49 ./
drwxr-x--- 23 atguigu atguigu 4096 3月 28 15:49 ../
-rw-rw-r-- 1 atguigu atguigu 0 3月 28 15:24 clean
-rw-rw-r-- 1 atguigu atguigu 92 3月 28 14:45 hello.c
-rw-rw-r-- 1 atguigu atguigu 69 3月 28 14:45 hello.h
-rw-rw-r-- 1 atguigu atguigu 18009 3月 27 17:27 hello.i
-rw-rw-r-- 1 atguigu atguigu 1496 3月 28 15:49 hello.o
-rw-rw-r-- 1 atguigu atguigu 680 3月 27 17:27 hello.s
-rw-rw-r-- 1 atguigu atguigu 1642 3月 27 20:41 libhello.a
-rwxrwxr-x 1 atguigu atguigu 16024 3月 28 15:49 main123*
-rw-rw-r-- 1 atguigu atguigu 67 3月 28 14:53 main.c
-rwxrwxr-x 1 atguigu atguigu 15952 3月 27 20:40 main_d*
-rw-rw-r-- 1 atguigu atguigu 225 3月 27 15:18 main.i
-rw-rw-r-- 1 atguigu atguigu 1360 3月 28 15:49 main.o
-rw-rw-r-- 1 atguigu atguigu 589 3月 27 15:18 main.s
-rw-rw-r-- 1 atguigu atguigu 316 3月 28 15:49 Makefile
drwxrwxr-x 2 atguigu atguigu 4096 3月 27 14:24 .vscode/
(4)分析
当前目录下不存在名为main的文件,只有名为mian123的文件,可以得出结论:规则中的命令决定了生成目标文件的名称。目标名并不影响目标文件名。
(5)再次执行make
1
2
linux_app/1_C/03_makefile$ make
gcc -o main123 hello.o main.o
我们发现,make没有提示目标文件已是最新,而是重新执行了gcc -o main123 hello.o main.o。这是因为,make会按照目标名称在当前目录下追踪目标文件,如果不存在与目标同名的文件,会再次执行规则下的命令。
(6)总结
make输出的文件名取决于规则下的命令,而目标名称决定make追踪的目标文件名。如果二者不一致,make就会认为目标文件不存在而不断执行命令。我们应确保命令生成的目标文件名和目标名一致。
文件I/O
我们在linux_app下新建目录2_IO,本章的所有例程全部放到该目录下。
标准I/O库函数
打开/关闭文件
fopen
新建01_fopen.c,写入以下内容。
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
27
28
29
30
31
#include <stdio.h>
int main(int argc, char const *argv[])
{
/**
* 作用:打开文件
* 参数1: 文件路径 字符串
* 参数2:打开文件的模式 字符串
* r 读模式 文件不存在失败
* w 覆盖写模式 文件不存在自动创建
* a 追加写模式 文件不存在自动创建
* r+ 可读 + 可写,从头开始写一个字符替换一个
* w+ 覆盖写 + 可读
* a+ 追加写 + 可读
* 返回值
* 成功:FILE * 指向结构体FILE的指针
* 失败:NULL
* FILE *fopen (const char *__restrict __filename, const char *__restrict __modes)
*/
FILE *file = fopen("io.txt", "a+");
if (file == NULL)
{
printf("打开文件失败\n");
}
else
{
printf("打开文件成功 (%p)\n", file);
}
return 0;
}
(2)新建Makefile,写入以下内容
1
2
3
4
5
6
CC:=gcc
fopen: 01_fopen.c
-$(CC) $^ -o $@
-./$@
-rm $@
说明:
① 有时编译器不只是gcc,我们将编译器定义为变量CC,当切换编译器时只需要更改该变量的定义,而无须更改整个Makefile。
② $^相当于当前target所有依赖文件列表,此处为fopen_test.c
③ $@相当于当前target目标文件的名称,此处为fopen_test。
④ ./$@的作用是执行目标文件
⑤ rm $@的作用是在执行完毕后删除目标文件,如果没有这个操作,当源文件fopen_test.c未更改时就无法重复执行,会提示: make:“fopen_test”已是最新。此处删除目标文件,使得我们在不更改源文件的情况下可以多次执行。
⑥ 所有命令前都添加了“-”符号以忽略错误,确保即便上面的命令执行失败,仍然会向下执行。这样做是为了在发生错误时,确保删除目标文件,使得再次执行相同target时不会提示:make:“fopen_test”已是最新,可以重新执行target下的命令。
fclose
(1)创建02_fclose.c文件,写入以下内容。
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
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
int main(int argc, char const *argv[])
{
// 打开文件
FILE *file = fopen("io.txt", "a+");
if (file == NULL)
{
printf("打开文件失败\n");
}
else
{
printf("打开文件成功 (%p)\n", file);
}
/**
* 作用:关闭文件流
* 参数:被关闭的文件流 前面打开的文件流
* 返回值:成功:为0,失败:为-1(EOF)通常失败会造成系统崩溃
*
* int fclose (FILE *__stream);
*/
// 关闭文件
// fclose(file);
int result = fclose(file);
if (result != 0)
{
printf("关闭文件失败 \n");
}
else
{
printf("关闭文件成功 \n");
}
return 0;
}
Makefile中补充以下内容
1
2
3
4
fclose: 02_fclose.c
-$(CC) $^ -o $@
-./$@
-rm $@