Linux | 编译原理、gcc的命令参数、自动化构建工具 make/Makefile

文章目录

  • 编译原理
    • 预处理
    • 编译
    • 汇编
    • 链接
    • gcc的常用命令参数
  • make 和 Makefile 的概念
    • make的运行
    • 通配符
    • 自动化变量
    • 伪目标.PHONE:【命令】


编译原理

在解释 makefile 前,首先解释一下 .c 文件变成 .exe 文件要经过的四个步骤——预处理、编译、汇编和链接(参考来源):
在这里插入图片描述
windows 系统下最后生成的可执行文件为 .exe ,但 Linux 系统下为 .out 。此处的可执行文件仅针对一般 .c/.cpp 代码而言。


预处理

预处理分为四步:

  • 展开所有的宏定义 #define
  • 处理含有 # 部分的代码。如:
    1. 条件编译 “#if”、“#ifdef”、“#elif”、“#else”、“#endif”
    2. 预编译指令 #include ,将被包含的头文件插入到该编译指令的位置。(这个过程是递归进行的,因为被包含的文件可能还包含了其他文件)
  • 删除所有的注释 “//”“/* */”
  • 添加行号和文件名标识,方便后续编译时 编译器产生调试用的行号 以及 在产生编译错误或警告时能够显示行号。
  • 保留所有的 #pragma 编译指令,因为编译器需要使用它们。

编译

编译过程是整个程序构建的核心部分,编译成功,会将源代码由 文本形式转换成机器语言 ,编译过程就是把预处理完的文件进行一系列 词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件(.s)。

  • 词法分析: 使用一种叫做 lex 的程序实现词法扫描,它会按照用户之前描述好的词法规则将输入的字符串分割成一个个记号。产生的记号一般分为:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(运算符、等号等),然后他们放到对应的表中。

  • 语法分析: 语法分析器根据用户给定的语法规则,将词法分析产生的记号序列进行解析,然后将它们构成一棵语法树。对于不同的语言,只是其语法规则不一样。用于语法分析也有一个现成的工具,叫做:yacc。

  • 语义分析: 语法分析完成了对表达式语法层面的分析,但是它不了解这个语句是否真正有意义。有的语句在语法上是合法的,但是却是没有实际的意义,比如说两个指针的做乘法运算,这个时候就需要进行语义分析,但是编译器能分析的语义也只有静态语义。

    1. 静态语义:在编译期就可以确定的语义。 通常包括声明与类型的匹配、类型的转换。比如当一个浮点型的表达式赋值给一个整型的表达式时,其中隐含一个从浮点型到整型的转换,而语义分析就需要完成这个转换,而将一个浮点型的表达式赋值给一个指针,这肯定是不行的,语义分析的时候就会发现两者类型不匹配,编译器就会报错。
    2. 动态语义:只有在运行期才能确定的语义。 比如说两个整数做除法,语法上没问题,类型也匹配,听着好像没毛病,但是,如果除数是0的话,这就有问题了,而这个问题事先是不知道的,只有在运行的时候才能发现他是有问题的,这就是动态语义。
  • 中间代码生成: 初始代码是可以进行优化的,对于一些在编译期间就能确定的值,可以直接直接进行处理,比如说 2+6,在编译期间就可以确定他的值为8了,但是直接在语法上进行优化的话比较困难,这时优化器会先将语法树转成中间代码。中间代码一般与目标机器和运行环境无关。(不包含数据的尺寸、变量地址和寄存器的名字等)。中间代码在不同的编译器中有着不同的形式,比较常见的有三地址码和P-代码。
    中间代码使得编译器可以分为前端和后端。编译器前端负责产生于机器无关的中间代码,编译器后端将中间代码换成机器代码。

  • 目标代码生成与优化: 代码生成器将中间代码转成机器代码,这个过程是依赖于目标机器的,因为不同的机器有着不同的字长、寄存器、数据类型等。
    最后目标代码优化器对目标代码进行优化,比如选择合适的寻址方式、使用唯一来代替乘除法、删除出多余的指令等。


汇编

汇编过程调用 汇编器 as 来完成,将汇编代码转换成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。

使用命令 as hello.s -o hello.o 或者使用 gcc -c hello.s -o hello.o 来执行汇编,对应生成的文件是 .o 文件。


链接

链接的主要内容就是将各个模块之间相互引用的部分正确的衔接起来。它的工作就是把一些指令对其他符号地址的引用加以修正。

链接过程主要包括了地址和空间分配、符号决议和重定向:

  • 符号决议: 有时候也被叫做符号绑定、名称绑定、名称决议、或者地址绑定,其实就是指用符号来去标识一个地址。

              比如说 int a = 6;这样一句代码,用a来标识一个块4个字节大小的空间,空间里边存放的内容就是4.
    
  • 重定位: 重新计算各个目标的地址过程叫做重定位。

链接有两种模式:

  • 静态链接: 程序运行前,将每个模块的源代码文件编译成目标文件(Linux:.o Windows:.obj),然后将 目标文件 和 库 一起链接形成最后的可执行文件。

    库其实就是一组目标文件的包,就是一些最常用的代码变异成目标文件后打包存放。最常见的库就是运行时库,它是支持程序运行的基本函数的集合。

  • 动态链接: 程序运行期间,系统调用动态链接器(ld-linux.so)自动链接的过程。

举例描述:

  • 静态链接: 如果链接到可执行文件中的是 静态连接库 libmyprintf.a ,那么 虚拟内存代码段中的 .rodata 节区 在链接后需要被重定位到一个绝对的虚拟内存地址,以便程序运行时能够正确访问该节区中的字符串信息。
  • 动态链接: 而对于puts,因为它是动态连接库 libc.so 中定义的函数,所以会在程序运行时通过 动态符号链接 找出 puts 函数内存 中的地址,以便程序调用该函数。

gcc的常用命令参数

上面提到的四个步骤可以由 编程语言译器 gcc 来完成,gcc软件 通过 gcc这条命令 来实现各种功能,下面来看一下 gcc命令 的常用选项:

  1. 无选项: 编译链接
gcc test.c // 会默认生成a.out可执行程序
  1. -o :对生成的目标进行重命名,gcc 编译出来的默认文件名是 a.out
gcc test.c -o test  // 会生成名字是test可执行文件而不是默认的a.out
  1. -E :进行预处理,不生成文件, 需要通过 -o 把它重定向到一个输出文件里面。
gcc -E test.c -o test.i //会生成test.i文件
  1. -C :在预处理的时候不删除注释信息,一般和 -E 使用。
  2. -S :进行预处理、编译,生成 .s 文件
gcc -S test.c //会生成test.s文件
  1. -c :进行预处理、编译、和汇编,生成二进制(机器指令).o 文件。
gcc -c test.c //会生成test.o文件
  1. -O :使用编译优化级别1编译程序。级别为0~3(0即无优化),级别越大优化效果越好,但编译时间越长。
gcc -O1 test.c -o test
  1. -g :在编译的时候加入 debug 调试信息,用于 gdb 调试
  2. -pipe :使用管道代替编译中的临时文件。
gcc -pipe -o test test.c
  1. -include file :包含某个代码。相当于在文件中加入 #include
gcc test.c -include /root/file.h
  1. -Idir :当你使用 #include”file” 的时候:

    如果使用 -I 指定了目录,gcc/g++ 会先在指定的目录查找;否则,在当前目录查找指定的头文件。

    如果没有找到,回到默认的头文件目录查找。

  2. -idirafter dir :在 -I 的目录里面查找失败,则到这个目录里面查找。

  3. -llibrary :定制编译的时候使用的库。

gcc -lpthread test.c // 在编译的时候要依赖pthread这个库
  1. -Ldir :指定编译的时候搜索库的路径。如果有自己的库,可以用它来定制搜索目录,否则编译器只在标准库目录里面找。dir 是目录的名字。

  2. -M :生成文件关联信息。包含目标文件所依赖的所有源代码。

gcc -M hello.c
  1. -MM :和 -M 一样,只不过忽略由 #include 所造成的依赖关系。
  2. -MD :和 -M 相同,只不过将输出导入到 .d 文件里面。
  3. -MMD :和 -MM 相同,将输出导入到 .d 文件里面。
  4. -static :链接时使用静态链接,但是要保证系统中有静态库。编译出来的东西,一般都很大。
  5. -share :此选项尽量的使用动态库,所以生成文件比较小,但是必须是系统有动态库。
  6. -shared :生成共享目标文件,通常用在建立共享库。
gcc -shared test.c -o libtest.so // 编译动态库
  1. -w :不生成任何警告信息。
  2. -Wall :生成所有警告信息。

make 和 Makefile 的概念

推荐一个非常全的关于 Makefile 的文章:跟我一起学写 Makefile

在我们日常写代码中,一个工程的源文件不计其数, 按照类型、功能、模块等分别放在若干个目录中,这时候我们就可以利用 Makefile 来指定哪些文件先编译,哪些后编译,以及更复杂的操作。

make 是一个命令工具,它解释 Makefile 中的指令。我们只需要在 Makefile 里指定所有的操作,再用 make 这个操作,即可让整个工程自动编译。


makefile 的格式如下:

target : prerequisites
	command
  • target: 目标文件 ,可以是多个文件,以空格分开,可以使用通配符。可以是 Object File执行文件 。甚至还可以是一个 标签(Label),如:clean
  • prerequisites: target依赖对象 。如果其中的 某个文件 要比 目标文件 要新,那么,目标文件 就被认为是 过时的 ,需要重新生成。
  • command: 命令行 ,如果其不与 target:prerequisites 在一行,那么,必须以 [Tab键] 开头,如果在一行,那么可以用分号做为分隔。

一般来说,make会以UNIX的标准Shell,也就是/bin/sh来执行命令。


写一个 makefile 文件为例:
Linux | 编译原理、gcc的命令参数、自动化构建工具 make/Makefile_第1张图片

目标程序:
Linux | 编译原理、gcc的命令参数、自动化构建工具 make/Makefile_第2张图片

执行 make 指令:
Linux | 编译原理、gcc的命令参数、自动化构建工具 make/Makefile_第3张图片
这样就生成了 .i,.s.o.out 文件。那么 make 是怎么运行的呢?


make的运行

  1. 在当前目录下依次找三个文件—— GNUmakefilemakefileMakefile 。其按顺序找这三个文件,一旦找到,就开始读取这个文件并执行。

也可以给 make 命令 指定一个 特殊名字Makefile 。这需要使用 make-f 或是 --file 参数( --makefile 参数也行)。例如,我们有个 Makefile 的名字是 hchen.mk ,则可以这样执行 make 命令:

make –f hchen.mk

如果在 make 的命令行中,不只一次地使用了 -f 参数,那么,所有指定的 Makefile 将会被连在一起传递给 make 执行。

  1. 接下来,它会找文件中的第一个 target (上面例子中的 test ),并把这个目标文件作为最终生成的文件。
  2. 如果 test 文件尚未生成;或是虽然 test 已经生成,但后面的依赖对象 test.o 文件的最后修改时间要比 test 这个文件新(可以用命令 touch 测试),那么,make 就会重新生成 test 这个文件。
  3. 如果 test 所依赖的 test.o 文件不存在,那么 make 会在当前文件中找目标文件为 test.o 的规则,如果找到则再根据那一个规则生成test.o文件。
  4. 如果没有目标文件为 test.o 的规则,则提前退出;否则,生成 test 文件并退出。

这就是整个 make 的运行过程,make 会一层又一层地去找文件的依赖关系,直到:

  • 最终编译出第一个目标文件(默认目标)并返回退出码;
  • 或者因为缺少必要规则而直接返回退出码。

make命令执行后有三个退出码:

  • 0 :表示成功执行。
  • 1 :如果 make 运行时出现任何错误,返回 1
  • 2 :如果你使用了 make-q 参数,导致一些目标不需要更新,那么返回 2

而对于所定义的命令的错误,或是编译不成功,make根本不理。


通配符

可以通过通配符来简化命令行:

  • ~ :Unix下, ~/test 表示当前用户的 $HOME 目录下的 test 目录。而 ~hchen/test 则表示用户 hchen 的宿主目录下的 test 目录。而在 Windows 或是 MS-DOS下用户没有宿主目录 ,那么波浪号所指的目录则根据环境变量 HOME 而定。(make支持UNIX下的通配符用法)
  • * :表示任意长度的字符串,*.c 表示所有后缀为c的文件。而当文件名中有通配符,如: ~ ,那么可以用转义字符 \ ,如 \~ 来表示真实的 ~ 字符。
  • ? :表示任意一个字符串。

自动化变量

shell 中的 自动化变量(又名:特殊变量) ,make 也是支持的,经常用到下面前三个自动化变量

  • $@目标对象 。在模式规则中,如果有多个目标,那么, $@ 就是匹配于目标中模式定义的集合。
  • $^ :所有 依赖对象 ,以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份。
  • $< :所有 依赖对象第一个 。如果依赖目标是以 模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
  • $? :所有比 目标对象 依赖对象 的集合。以空格分隔。
  • $+ : 这个变量很像 $^ ,也是所有 依赖对象 的集合。只是它不去重。
  • $% :仅当 目标对象 是函数库文件中、表示规则中的目标成员名。例如,如果一个目标是 foo.a(bar.o) ,那么, $% 就是 bar.o$@ 就是 foo.a 。如果目标不是函数库文件(Unix下是 .a ,Windows下是 .lib ),那么,其值为空。
  • $* :这个变量表示目标模式中 % 及其之前的部分。(如果 目标对象dir/a.foo.b ,并且 目标对象模式a.%.b ,那么, $* 的值就是 dir/a.foo 。)
    • 这个变量对于构造有关联的文件名是比较有用的。(如果 目标对象 中没有 模式 的定义,那么 $* 也就不能被推导出,但是,如果 目标文件 的后缀是 make 所识别的,那么 $* 就是除了后缀的那一部分。)

例如:如果 目标对象foo.c ,因为 .cmake 所能识别的后缀名,所以, $* 的值就是 foo 。这个特性是 GNU make 的,很有可能不兼容于其它版本的 make ,所以,尽量避免使用 $* ,除非是在 隐含规则 或是 静态模式 中。如果 目标对象 中的后缀是 make 所不能识别的,那么 $* 就是空值。

我们可以利用 自动化变量 简化 makefile 文件:
Linux | 编译原理、gcc的命令参数、自动化构建工具 make/Makefile_第4张图片
执行 make 命令:
Linux | 编译原理、gcc的命令参数、自动化构建工具 make/Makefile_第5张图片

我们还能进一步再简化,可以利用通配符来表示,在多个 目标对象依赖对象命令行 都相似时,利用通配符 % 来减少工作量,这样就可以不用一个个写出每个文件的生成规则了。


伪目标.PHONE:【命令】

.PHONE: [命令]  // 声明伪目标,无论目标是否最新,每次都重新生成。

举个 伪目标 的例子:

clean:
    rm *.o temp

既然我们生成了许多编译文件,那么我们也应该提供一个清除它们的 目标 以备完整地重编译。 (以“make clean”来使用该目标)

之所以将 clean 称为 伪目标 , 是因为我们并不生成 clean 这个文件。伪目标 并不是一个 文件 ,只是一个 标签 ,由于 伪目标 不是 文件 ,所以 make 无法生成它的 依赖对象 ,无法决定它是否要执行 命令行 。我们只有显式地指明这个 目标 才能让其生效。当然,伪目标 的取名不能和 文件名 重名,不然其就失去了 伪目标 的意义了。

因此我们需要用 .PHONY 声明 伪目标 ,从而区分 伪目标目标文件

.PHONY : clean

而只要有 .PHONY:clean 这个声明,不管是否有 clean 文件,只要执行 make clean 命令,就会运行 clean 。因此,我们要在声明后面跟上 clean 的具体内容:

.PHONY : clean
clean :
    rm *.o temp

通常需要生成的程序不会设置伪对象,因为每个项目的构建需要很长的时间,所以尽可能判断不需要生成就不用重新生成。

伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。

一个示例就是,如果你的 Makefile 需要一口气生成若干个可执行文件,但你只想简单地敲一个 make 完事,并且,所有的目标文件都写在一个 Makefile 中,那么你可以这样做:

all : prog1 prog2 prog3
.PHONY : all

prog1 : prog1.o utils.o
    cc -o prog1 prog1.o utils.o

prog2 : prog2.o
    cc -o prog2 prog2.o

prog3 : prog3.o sort.o utils.o
    cc -o prog3 prog3.o sort.o utils.o

Makefile 中的第一个目标会被作为其默认目标。 我们声明了一个 all 的伪目标,其依赖于其它三个目标。由于 默认目标总是被执行的 ,而上面的 Makefile 文件中的第一个目标(默认目标) all 又是一个伪目标。因此 all 是一定会被执行的,但又因为伪目标只是一个标签不会生成文件,所以不会有 all 文件产生。于是,其它三个目标的规则总是会被执行。也就达到了我们一口气生成多个目标的目的。 .PHONY : all 声明 all 这个目标为 伪目标 。(注:这里的显式 .PHONY : all 不写的话一般情况也可以正确的执行,这样 make 可通过隐式规则推导出, all 是一个伪目标,执行 make 不会生成 all 文件,而是执行后面的多个目标。建议:显式写出是一个好习惯。)

你可能感兴趣的:(操作系统,makefile,编译原理,gcc/gdb编译调试,Linux,kali,linux)