编译与链接的过程可以分解为4个步骤:分别是预处理(Prepressing )、编译(Compilation )、汇编(Assembly )和链接(Linking ),一个helloworld的编译过程如下:
预处理
相当于执行g++ -E helloworld.cpp -o helloworld.i
,其中:-E
的编译选项,意味着只执行到预编译,直接输出预编译结果
预处理过程主要处理那些源代码文件,主要处理以#
开始的预编译指令。比如#include
、#define
等,主要处理规则如下所述:
还有
#undef
,则将取消对某个宏的定义,使以后该串的出现不再被替换
#if
、#ifdef
、#elif
、#else
、#endif
#include
预编译指令,将被包含的文件插入到该预编译指令的位置//
和/**/
中的内容#2 "test.c" 2
,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号#pragma
编译器指令,因为编译器需要使用它们经过预编译后的 .i
文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i
文件中。所以当无法判断宏定义是否正确或头文件包含是否正确时,可以查看预处理后的文件来确定问题
编译
编译过程就是把预处理完的文件进行一系列的词法分析、语法分析 语义分析以及优化后产生相应的汇编代码文件,这个过程往往是整个程序构建的核心部分,也是最复杂的部分之一。编译过程相当于如下命令:
g++ -s helloworld.i -o helloworld.s
其中,-S
的编译选项,表示只执行到源代码到汇编代码的转换,输出汇编代码
在这个过程中,编译器做的就是将高级语言翻译成机器可以执行的指令和数据。编的过程一般分为6步:扫描(词法分析)、语法分析、语义分析、源代码优化、 代码生成和目标代码优化,整个过程如下:
运用一种类似于有限状态机的算法将源代码的字符序列分割成一系列的记号
语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树。整个分析过程采用了上下文无关文法的分析手段。简单地讲,由语法分析器生成的语法树就是以表达式为节点的树
可以看到,整个语句被看作一个赋值表达式:赋值表达式的左边是一个数组表达式;它的右边是一个乘法表达式;数组表达式又由两个符号表达式组成,等等。符号和数字是最小的表达式,它们不是由其他的表达式来组成的,所以通常为整个语法树的叶节点。在语法分析的同时,很多运算符的优先级和含义也被确定下来
语义分析是由语义分析器完成。语法分析仅仅是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义。编译器所能分析的语义是静态语义,所谓静态语义是指在编译期间可以确定的语义,与之对应的动态语义就是只有在运行期间才能确定的语义。静态语义通常包括声明和类型的匹配及类型的转换等,动态语义一般指在运行期间出现的语义相关的问题。经过语义分析阶段后,整个语法树的表达式都被标识了类型,如果有些类型需要做隐式转化,语义分析程序会在语法树中插入相应的转换节点。语义分析后的语法树:
现代的编译器有着很多层次的优化,往往在源代码级别会有一个优化过程。源代码优化器会在源代码级别进行优化,直接在语法树上进行这类优化比较困难,所以源代码优化器往往将整个语法树转换成中间代码,它是语法树的顺序表示,并接近目标代码。中间代码一般跟目标机器和运行时环境是无关的,比如不包含数据的尺寸、变量的地址和寄存器的名字等。
链接
把每个源代码模块独立地编译,然后按照要将它们“组装”起来,这个组装模块的过程就是链接。链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确的衔接。从原理上讲,它的工作就是把一些指令对其他符号地址的引用加以修正。链接过程主要包括了地址和空间分配、符号决议和重定位等这些步骤。
静态链接过程如图所示,每个模块的源代码文件经过编译器编译成目标文件,目标文件和库一起链接形成最终可执行文件。
每个目标文件除了拥有自己的数据和二进制代码外,还提供了3个表:未解决符号表、导出符号表、地址重定向表,具体如下所述:
编译器将 extern
声明的变量置入未解决符号表,而不置入导出符号表,这属于外部链接
编译器将 static
声明的全局变量不置入未解决符号表,也不置入导出符号表,因此其他单元无法使用,这属于内部链接
链接分为静态链接和动态链接,对函数库的链接是放在编译时期完成的是静态链接。有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。程序在运行时,与函数库再无瓜葛,因为所有需要的函数已复制到相关位置,这些函数库被称为静态库,通常文件名
为 libxxx.a
的形式。无论是静态库文件还是动态库文件,都是由 .o
文件创建的
把对一些库函数的链接载入推迟到程序运行时期(runtime),这就是动态链接库(dynamic link library)技术。
静态链接库、动态链接库各自的特点:
当某个程序在运行中要调用某个动态链接库函数的时候,如果内存里已有此库函数的拷贝了,则让其共享那一个拷贝;只有没有时才链接载入。如果系统中多个程序都要调用某个静态链接库函数时,则每个程序都要将这个库函数拷贝到自己的代码段中
只要动态库提供给该程序的接口没变,只要重新用新生成的动态库替换原来就可以了,而使用静态库就需要重新进行编译
程序员在编写程序的时候,可以明确的指明什么时候或者什么情况下,链接载入哪个动态链接库函数
makefile文件
一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,如何更高效率地编译整个工程,需要用到 makefile/make 命令工具。makefile 中会定义一系列的规则,指定哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。
makefile 带来的好处就是“自动化编译”,一旦写好,只需要一个 make 命令,整个工程完全自动编译,极大地提高了软件开发的效率。make 命令是一个命令工具,是一个解释makefile 中指令的命令工具。
makefile 就像 shell 脚本 样,其中也可以执行操作系统的命令。
makefile 主要含有一系列的规则:
A: B
(tab)<command>
(tab)<command>
每个命令行前都必须有 tab
符号。
假设helloworld 依赖 filel.o file2.o 两个目标文件:
helloworld : filel.o file2.o
编译出 helloworld 可执行文件,-o
后面加你指定的目标文件名:
g++ filel.o file2.o -o helloworld
file2.o 依赖 file2.cpp 文件:
file2.o : file2.cpp
-c
表示 C++ 只把给它的文件编译成目标文件
g++ -c file2.cpp -o file2.o
编译出 filel.o 文件:
filel.o:filel.cpp filel.h
g++ -c filel.cpp -o filel.o
clean
删除文件
clean:
rm -rf *.o helloworld
makefile中的变量设定,要设定一个变量,只要在一行的前端写下这个变量的名字,后面跟个=
,后面跟要设定的这个变量的值即可,以后要引用这个变量,只写一个$
符号,后面是在括号里的变量名即可
XX = g++
$(XX) -c helloworld.cpp -o helloworld.o
在 makefile 中使用函数:
在 makefile 规则中,通配符会被自动展开,但在变量的定义和函数引用时,通配符将失效。这种情况下如果需要通配符有效,就需要使用函数
wildcard
,它的用法是: $(wildcard PATTERN ...) 在 makefile 中,它被展开为已经存在的、使用空格分开的、匹配此模式的所有文件列表。如果不存在任何符合此模式的文件,函数会忽略模式并返回空。patsubst
函数,用于匹配替换,有3个参数。第一个是一个要匹配的式样,第二个表示用什么来替换它,第三个是一个需要被处理的由空格分隔的列表,比如: $(patsubst %.c %.o $(dir) ) 该代码指示用 patsubst 把$(dir)中的变量符合后缀是.c
的全部替换成.0
makefile的内部变量:
$@
扩展成当前规则的目的文件名$<
扩展成依靠列表中的第一个依靠文件$^
扩展成整个依靠的列表(除掉了里面所有重复的文件名)目标文件
ELF 是一种用于二进制文件、可执行文件、目标代码、共享库和核心转储的标准文件格式。ELF 标准的目的是为软件开发人员提供二进制接口定义,这些接口可以延伸到多种操作环境中,从而减少重新编码、编译程序的需要
目标文件有3种类型,如下所述:
这是由汇编器汇编生成的
.o
文件,链接器拿一个或一些可重定位的目标文件作为输入,经链接处理后,生成一个可执行的目标文件或者一个可被共享的对象文件(.so
文件)。可以使用 ar 工具将众多的.o
文件归档(archive)成.a
静态库文件
这些就是所谓的动态库文件,也即
.so
文件。动态库在发挥作用的过程中,必须经过两个步骤 1. 链接器拿它和其他可重定位的文件(.o
文件)以及其他.so
文件作为输入,经链接处理后,生成另外的可共享的目标文件(.so
文件)或者可执行的目标文件;2. 在运行时,动态链接器拿它和一个可执行的目标文件以及另外一些可共享的目标文件 (.so
) 来 起处理,在 Linux 系统里面创建一个进程映像
有两种视图可以来说明 ELF 的组成格式,即链接视图和执行视图,这是因为 ELF 格式需要使用在两种场合,1. 组成不同的可重定位文件,以参与可执行文件或者可被共享的对象文件的链接。2. 组成可执行文件或者可被共享的对象文件,以在运行时内存中进程映像的构建。构建对象文件组成如表:
ELF 文件头被固定地放在不同类对象文件的最前面 因此,我们可以用 file 命令来看文件是属于哪种 ELF 文件,如下:
结果展示了, add.o、sub.o 都是可重定位文件, libmymath.so 是可被共享文件, main是可执行文件
减少目标文件大小的工具一一 strip
它能清除执行文件中不必要的标示符及调试信息,可减小文件大小而不影响正常使用。不过文件一旦进行 strip 操作后就不能恢复原样了,所以 strip 可以认为是一个“减肥”工具而不是压缩工具。而且,被 strip 后的文件不包含调试信息。strip 命令能从 ELF 文件中有选择地除去行号信息、重定位信息、调试段、 typchk 段、注释段、文件头以及所有或部分符号表。一旦使用该命令,则很难调试文件的符号;因此,通常只在已经调试和测试过的生成模块上使用 strip 命令,来减少对象文件所需的存储量开销。
调试的方法一般有两种:
strace
应用程序是不能直接访问 Linux 内核的,它既不能访问内核所占内存空间,也不能调用内核函数。不过,应用程序可以跳转到 system_call 的内核位置,内核会检查系统调用号,这个号码会告诉内核进程正在请求哪种服务。然后,它查看系统调用表,找到所调用的内核函数入口地址,调用该函数,然后返回到进程。所有操作系统在其内核都有一些内建的函数,这些函数可以用来完成一些系统级别的功能,一般称 Linux 系统上的这些函数为 系统调用 (system call)。这些函数代表了用户空间到内核空间的一种转换。例如,在用户空间调用 open 函数,在内核空间则会调用 sys_open。
系统调用的错误码 :系统调用并不直接返回错误码,而是将错误码放入一个名为 errno的全局变量中。如果一个系统调用失败,你可以读出 errno 的值来确定问题的所在。
strace 是一个通过跟踪系统调用来让开发者知道一个程序在后台所做事情的工具。
-c
参数来统计系统调用-T
参数将每个系统调用的时间打印出来starce ./可执行文件
gdb
gdb是gcc 的调试工具,主要用于 C和C++ 这两种语言编写的程序。它的功能很强大,主要体现在以下4点:
要调试 C和C++ 的程序,首先在编译时,必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++) 的 -g
数可以做到这一点,如下代码:
gcc -g hello.c -o hello
g++ -g hello.cpp -o hello
如果没有-g
,你将看不见程序的函数名、变量名,所代替的全是运行时的内存地址。
启动 gdb 的方法:
program是可执行文件
用 gdb 同时调试一个运行程序和 core 文件, core 是程序非法执行后 core dump 产生的文件
如果程序是一个服务程序,那么可以指定这个服务程序运行时的进程 ID, gdb会自动进行 attach 操作,并调试这个程序。并且 program 应该在 PATH 环境变量中搜索得到
综上,一个简单的使用gdb来进行调试的demo为:
gcc -g hello.c -o hello
gdb hello
进入gdb调试模式后:
输入"1",表示从第一行开始列出源码
按下Enter键,表示重复上一次命令
输入"b num",表示在bum行设置断点
输入"r"表示运行程序,run 命令简写
输入"n",表示单条语句执行, next 命令简写
输入p i
p arr[i]
,分别打印变量i和变量arr[i]的值
输入"bt",查看函数堆拢
输入"finish"退出函数
可以使用 gdb 分析 coredump 文件
产生 coredump 文件的一些原因:
Linux 中的 ps
(process status )命令列出的是当前在运行的进程的快照,就是执行 ps 命令的那个时刻的那些进程,如果想要动态地显示进程信息,就可以使用 top
命令
Linux 上进程有5种状态,如下所述:
ps 具标识进程的5种状态码
l) D 不可中断 uninterruptible sleep (usually IO)
2) R 运行 runnable (on run queue)
3) S 中断 sleeping
4) T 停止 traced or stopped
5) Z 僵死 a defunct (”zombie”) process
命令格式是: ps [参数]。命令功能是用来显示当前进程的状态。eg : ps -all
ps命令的一些操作
ps -u username
ps -ef
ps -l
ps aux
Linux 程序内存空间布局 一个典型的 Linux 的运行中的C程序的内存空间布局
一个典型的 Linux 下的 C 程序内存空间由如下几部分组成:
malloc/free
等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张)或释放的内存从堆中被剔除(堆被缩减)堆和栈的区别
int b;
系统自动在栈中为b
开辟空间栈:在函数调用时,第一个进栈的是主函数中后的下一条指令( 函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的,当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行 堆: 一般是在堆的头部用一个字节存放堆的大小,堆中的具体内容由程序员安排
常见的内存动态管理错误包括以下几种:
申请和释放所使用的函数需匹配,如new申请的空间应使用delete释放,而malloc申请的空间应使用free释放
申请和释放的内存空间大小应该一致