目录
- 一、Linux编译器-gcc/g++
- 1.1 gcc/g++ 使用方法
- 1.2 程序的翻译过程
- 1.3 链接 -- 动静态链接特点及区别
- 二、Linux调试器-gdb
- 2.1 背景
- 2.2 使用方法
- 三、Linux项目自动化构建工具-make/Makefile
- 3.1 背景
- 3.2 原理
- 3.3 项目清理
一、Linux编译器-gcc/g++
1.1 gcc/g++ 使用方法
格式: gcc [选项] 要编译的文件 [选项] [目标文件]
,gcc / g++
安装: sudo yum install -y gcc-c++
。安装后的编译器默认的版本是较低的,我们可以使用选项-std=c99
(即使用c99标准),-std=c++11
(即使用c++11的标准)来进行版本提升。使用-o
选项,可以将编译生成的可执行重命名。最后使用./可执行
,来运行程序。如下:
Linux中一个文件要真正的执行:1. 有x
权限;2. 就是一个可执行文件。 很明显文件test.exe
满足上述条件!
1.2 程序的翻译过程
程序的翻译过程,可以说是生成二进制方案的过程,具体可以细分为以下几步:
- 预处理:
预处理功能主要包括宏定义,文件包含,条件编译,去注释等。 预处理指令是以#号开头的代码行。
实例: gcc -E test.c -o test.i
。选项"-E"
,该选项的作用是让gcc
从现在开始进行程序的翻译,预处理完成就停下! 选项"-o"
是指目标文件,".i"
文件为已经过预处理的C原始程序。
- 编译(生成汇编):
在这个阶段中,gcc
首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc
把c语言代码翻译成汇编语言。
实例: gcc -S test.i -o test.s
。用户可以使用"-S"
选项来进行查看,该选项的作用是让gcc
从现在开始进行程序的翻译,编译完成就停下! 可以从test.c
开始编译,也可以从test.i
开始。
- 汇编(生成机器可识别代码):
汇编阶段是把编译阶段生成的".s"
文件转成目标文件(即将汇编语言编译成为,二进制目标文件)
实例: gcc -c test.s -o test.o
。读者在此可使用选项"-c"
,该选项的作用是让gcc
从现在开始进行程序的翻译,汇编完成就停下!。就可看到汇编代码已转化为".o"
的二进制目标代码了。
- 链接(生成可执行文件或库文件):
实例: gcc hello.o –o hello
。在成功编译之后,就进入了链接阶段,最后形成可执行程序。
那么为什么会有这些过程呢?
这就要说到编程语言的历史了。最初的计算机只能处理二进制,二进制是处理器的“母语”。这叫“机器语言”或者“机器码”。在计算机早期阶段,必须用机器码写程序(即二进制编程)(前段时间很火的《三体1》中,早期叶文洁当时就是使用的二进制编程,还用到了打孔带,上面记录的就是二进制信息)。
在1940-1950年代,程序员开发一种新语言(汇编语言),它为每个操作码分配一个简单名字,叫“助记符”,助记符后面紧跟数据,形成完整指令。当然,计算机并不认识助记符,它只认得二进制码,然后就有了汇编的编译器,它可以读懂文字指令,自动转换成二进制指令,这种程序叫“汇编器”
汇编语言直接对应机器码,虽然已经很方便了。但是,汇编器仍然强迫程序员思考,用什么寄存器和内存地址,如果我们突然要用额外一个数,可能要改很多代码。这时候就出现了一些高级语言和与之对应的编译器(如:C,C++,Java…)。
因为计算机只认识二进制。以c语言为例,所以我们要先将c语言代码转化为汇编代码,然后再转为二进制目标文件。 那为什么不直接将c语言转为二进制目标文件呢?因为前人已经写好了汇编的编译器,我们只需站在巨人的肩膀上,再将高级语言转为较简单的汇编代码即可。
那么如何形成第一个汇编写的汇编编译器呢?
先用二进制写一个汇编编译器(此时汇编代码便可通过此编译器形成软件)。汇编编译器本质上就是软件,那么便可先用汇编语言写一个新的编译器,再将这个新的编译器放在二进制写的汇编编译器上执行,最后形成新的汇编编译器(软件)。日后只需维护汇编代码即可! 这个过程也可叫做编译器的自举(bootstrap),c语言的第一个编译器也是如此形成的!
1.3 链接 – 动静态链接特点及区别
我们的C程序中,并没有定义"printf"
的函数实现,且在预编译中包含的"stdio.h"
中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实"printf"
函数的呢?
最后的答案是:系统把这些函数实现都被做到名为 libc.so.6
的库文件中去了,在没有特别指定时,gcc
会到系统默认的搜索路径 /usr/lib
下进行查找,也就是链接到 libc.so.6
库函数中去,这样就能实现函数"printf"
了,而这也就是链接的作用。
函数库一般分为静态库和动态库两种。
- 静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为
".a"
- 动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为
".so"
,如前面所述的libc.so.6
就是动态库。gcc
在编译时默认使用动态库。完成了链接之后,gcc
就可以生成可执行文件,如下所示。gcc test.o –o test
gcc
默认生成的二进制程序,是动态链接的,这点可以通过file
命令验证。
验证:
-static
指令,作用是链接时使用静态链接:
2. ldd 可执行
指令,作用是查看链接的库:
file filename
指令,查看链接属性:
动态库 && 动态链接的优缺点:1. 不能丢失。共享动态库,但是一旦动态库缺失,所有的动态链接这个库的程序,都无法执行! 2. 节省资源。
静态库 && 静态链接的优缺点:1. 一旦形成,与库无关。在编译的时候,把库中的方法,拷贝到我自己的可执行程序中,此时程序将不再关心任何的库; 2. 浪费资源,占用空间大。
二、Linux调试器-gdb
2.1 背景
程序的发布方式有两种,debug
模式和release
模式。Linux gcc/g++
出来的二进制程序,默认是release
模式。 要使用gdb
调试,必须在源代码生成二进制程序的时候,加上 -g
选项,此时编译器形成可执行程序的时候,会给可执行程序添加调试信息。 我们也可以发现可调式的可执行程序比普通的程序占用内存大。
调试信息大致如下:
2.2 使用方法
gdb binFile
退出: ctrl + d
或 quit
调试命令:
- list/l 行号: 显示binFile源代码,接着上次的位置往下列,每次列10行。
- list/l 函数名: 列出某个函数的源代码。
- r或run: 运行程序(对比VS F5)。
- n 或 next: 单条执行,逐过程(对比VS F10)。
- s或step: 进入函数调用,逐语句(对比VS F11)。
- break(b) 行号: 在某一行设置断点。
- break 函数名: 在某个函数开头设置断点。
- info break : 查看断点信息。
- finish: 执行到当前函数返回,然后停下来等待命令。
- print(): 打印表达式的值,通过表达式可以修改变量的值或者调用函数。
- p 变量: 打印变量值。
- set var: 修改变量的值。
- continue(或c): 从当前位置开始连续而非单步执行程序。
- delete breakpoints(b): 删除所有断点。
- delete breakpoints((b) n: 删除序号为n的断点。
- disable breakpoints(b): 禁用断点。
- enable breakpoints(b): 启用断点。
- info(或i) breakpoints(b): 参看当前设置了哪些断点。
- display 变量名: 跟踪查看一个变量,每次停下来都显示它的值。
- undisplay: 取消对先前设置的那些变量的跟踪。
- until X行号: 跳至X行。
- breaktrace(或bt): 查看各级函数调用及参数。
- info(i) locals: 查看当前栈帧局部变量的值。
- quit: 退出gdb。
三、Linux项目自动化构建工具-make/Makefile
3.1 背景
- 会不会写
makefile
,从一个侧面说明了一个人是否具备完成大型工程的能力; - 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,
makefile
定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作; makefile
带来的好处就是——“自动化编译”,一旦写好,只需要一个make
命令,整个工程完全自动编译,极大的提高了软件开发的效率;make
是一个命令工具,是一个解释makefile
中指令的命令工具,一般来说,大多数的IDE
都有这个命令,比如:Delphi
的make
,Visual C++
的nmake
,Linux
下GNU
的make
。可见,makefile
都成为了一种在工程方面的编译方法;make
是一条命令,makefile
是一个文件,两个搭配使用,完成项目自动化构建。
3.2 原理
make
是如何工作的,在默认的方式下,也就是我们只输入make
命令。那么,
make
会在当前目录下找名字叫"Makefile"
或"makefile"
的文件。- 如果找到,它会找文件中的第一个目标文件(
target
),在上面的例子中,他会找到"code.exe"
这个文件,并把这个文件作为最终的目标文件。 - 如果
code.exe
文件不存在,或是code.exe
所依赖的后面的code.o
文件的文件修改时间要比code.exe
这个文件新(可以用touch
测试 - 前面讲过touch
可以修改文件最近一次被修改的时间),那么,他就会执行后面所定义的命令来生成code.exe
这个文件。 - 如果
code.exe
所依赖的code.o
文件不存在,那么make
会在当前文件中找目标为code.o
文件的依赖性,如果找到则再根据那一个规则生成code.o
文件。(这有点像一个堆栈的过程) - 当然,你的C文件和H文件是存在的啦,于是
make
会生成code.o
文件,然后再用code.o
文件声明make
的终极任务,也就是执行文件code.exe
了。 - 这就是整个
make
的依赖性,make
会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。 - 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么
make
就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make
根本不理。 make
只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
makefile / make
会自动根据文件中的依赖关系,进行自动推导,帮助我们执行所有的相关的依赖方法。
makefile
文件中,保存了编译器和链接器的参数选项,并且描述了所有源文件之间的关系。make
程序会读取makefile文
件中的数据,然后根据规则调用编译器,汇编器,链接器产生最后的输出。Makefile
里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。1. 显式规则说明了,如何生成一个或多个目标文件。2.make
有自动推导的功能,所以 隐晦规则可以让我们比较粗糙地简略地书写makefile
,比如源文件与目标文件之间的时间关系判断之类。3. 在makefile
中可以定义变量,当makefile
被执行时,其中的变量都会被扩展到相应的引用位置上。4. 通常使用$(var)
表示引用变量文件指示。包含在一个makefile
中引用另一个makefile
,类似C语言中的include
。5. 注释,makefile
中可以 使用#
在行首表示行注释。- 默认的情况下,
make
命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”
的文件
3.3 项目清理
- 工程是需要被清理的
- 像
clean
这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make
执行。 即命令——“make clean”
,以此来清除所有的目标文件,以便重编译。 - 但是一般我们这种
clean
的目标文件,我们将它设置为伪目标,用.PHONY
修饰,伪目标的特性是,总是被执行的。makefile
中的伪对象表示对象名称并不代表真正的文件名,与实际存在的同名文件没有相互关系,因此伪对象不管同名目标文件是否存在都会执行对应的生成指令。伪对象的作用有两个,1. 使目标对象无论如何都要重新生成。2. 并不生成目标文件,而是为了执行一些指令.
于是我们便可使用vim
编写如上内容。里面$
相当于取值操作,@
最终会被替换目标文件,而^
会被替换为依赖方法。