【MacOS】编译简介及动态、静态链接库的创建

一、编译简介

在正式说明如何创建库文件之前,先来重温一下编译的基本流程吧。既然说到编译这个概念,那么首先来思考一下编译是什么?我们为什么需要编译?编译它能为我们带来什么?

想要回答这几个问题,我们的思绪需要先回到20世纪40年代,在那个计算机刚刚诞生的年代,人们将各种算术操作定义为一系列二进制指令(即机器语言)存储在笨重的计算机内部,当专业人员想要操作计算机执行复杂计算时,他们需要手工将二进制指令输入到计算机中,需要执行多个指令时,这些指令合集会被记录到打孔卡中,打孔卡的打孔或者不打孔对应着二进制的0或者1,当打孔卡被输入至计算机中后,计算机便会识别并执行卡中所有的指令进行运算并输出结果。可以说这一张张打孔卡就是最原始的程序了。

记忆辨别用二进制组成的计算机指令显然是十分困难的,而且编程时也非常容易出错。于是在上世纪50年代,汇编语言产生了,它将一系列计算机的指令、寄存器、地址用符号表示例如add %eax, %ebx ,记录这些文本符号显然对人类友好多了,但是机器可只认二进制指令,因此执行程序前,需要汇编器将汇编语言翻译成机器语言,这个过程称之为汇编过程。

虽说汇编语言可以使程序员们不再记忆复杂的二进制指令,但是用它写程序还是麻烦,因为它和机器指令一一对应,更加接近CPU的思维,而不是人的思维。于是人们发明了很多高级语言(Fortran,COBOL等),其中最典型的就是诞生于20世纪70年代初的C语言,目前市面上主流的操作系统都是由它编写的。C语言编译的过程,实际上首先通过 “编译器”将C语言源代码翻译成汇编代码,再通过“汇编器”将汇编代码转化成目标代码(也称之为机器代码)。经过计算机硬件和软件几十年的发展,现在的编译器通常都集成了包括编译和汇编在内的多种功能,目前在Linux系统上最流行的莫过于gcc编译器;Windows系统中通常会选择visual studio集成开发环境,它内嵌了c的编译器;MacOS系统上安装完xcode command line tool后默认的是clang编译器,不过通常会套上gcc的命令外壳。

此时,面对开篇的三个问题答案已经不言而喻了。编译就是为了弥补高级语言和机器语言之间的鸿沟,即人类不想用机器的思维和语言来编写程序,而计算机又只能识别那些用它自己的指令语言编写的程序。编译器就是高级语言和机器语言之间的翻译者,编译过程就是高级语言翻译为机器语言的过程。

二、编译过程

现在我们广义上所说的编译,其实内部会执行很多小步骤,当然不同的高级语言,不同的编译器,内部流程都会有差异。例如用javac编译器编译java程序得出的目标代码其实是字节码,它不能在目标机器上直接执行,而是需要虚拟机jvm进一步解释为机器语言执行,这其实和c的编译是有很大的不同的。在本文中还是以gcc编译c语言程序为例,它主要包含了以下四个具体的步骤。
编译过程

1. 预处理

编译器在这一步骤会扫描源代码,检查其中的宏定义与预处理指令的语句并将其转换,同时删除程序中的注释以及空白字符。例如,#include <stdio.h>是一条预处理指令,编译器扫描到这一指令之后会在此处展开被包含的文件。该过程完成后产生后缀名为.i的与源程序同名文件。不过需要注意的是gcc使用-E选项预处理后是默认输出到控制台的,可以使用重定向符号输出到文件中,例如gcc hello.c -E > hello.i

2. 编译

编译器在这一步骤会对预处理后的文件进行词法与语法分析,若果出现错误它会给出错误提示并终止编译;如果没有出错,则将源代码翻译成可在目标机器上执行的汇编代码。它所产生的的文件为后缀名为.s的与源程序同名文件。gcc的命令示例:gcc hello.i -S

3. 汇编

编译器将上一步产生的汇编代码汇编成目标及其目标机器指令,它所产生的文件为后缀名为.o的源程序同名文件。gcc的命令示例:gcc hello.s -c

4. 链接

编译器将一个文件中引用的符号用另一个文件中该符号的定义链接起来,换句话说就是将分散的目标代码聚合到一个文件中来。例如,一个程序中使用了标准库函数printf,链接的工作就是将标准库中的printf指令链接到该应用程序中,然后生成可执行程序。如果进一步细分的话,链接还可分为静态链接和动态链接。所谓的静态链接指的是应用程序依赖的代码指令在编译期间便被聚合到可执行文件中;而动态链接指的是应用程序依赖的代码指令延迟到运行时才被装载调用。gcc命令示例:gcc hello.o -o hello

三、静态链接库

从上文来看使用静态链接的好处是执行速度会比动态链接库略微快一些,而且不需要考虑使用者的机器上是否安装了应用程序需要的依赖库以及对应的版本问题,因为所有的依赖都被打包进可执行文件中了。相应的,它的缺点就是发布的程序会更加占用磁盘和内存空间。

以下面这个demo为例,src目录下包含着主程序link_test.c以及它的依赖hello.c;head目录下包含着依赖函数的声明头文件hello.h。制作静态链接库的方式为在src目录下输入命令gcc hello.c -c -o ../lib/hello.o && cd ../lib && ar -r libhello_static.a hello.o即可在lib目录下生成libhello_static.a文件,一般来说静态库在Unix平台上命名应以lib开头,后缀名为.a;而在Windows上一般是一个后缀为.lib的文件。其中的ar命令类似于tar一样的打包命令,将多个目标文件归档到一起。

生成了静态库后,我们在编译主程序时可以指定链接该库文件gcc link_test.c -o ../bin/static_link_test -L../lib -lhello_static,这样在bin目录下就会生成一个名为static_link_test的可执行文件。其中参数-L指定搜索路径,-l指定库名称,在本例中它会去上级的lib目录下查找以lib开头名为hello_static的库文件。

$ tree .
.
├── bin
│   ├── dynamic_link_test   # 使用动态链接方式生成的可执行文件
│   ├── link_test           # 同时编译、链接依赖源文件和主源文件生成的可执行文件
│   └── static_link_test    # 使用静态链接方式生成的可执行文件
├── head
│   └── hello.h				# 该头文件包含源文件依赖函数的声明
├── lib
│   ├── hello.o				# 编译依赖源文件得出的目标文件
│   ├── libhello.so			# 动态链接库
│   └── libhello_static.a	# 静态链接库
└── src
    ├── hello.c				# 依赖的源文件,实现依赖函数的定义
    └── link_test.c			# 主源文件,包含main函数

4 directories, 9 files

四、动态链接库

根据动态链接的特性来看虽然会造成程序运行效率的降低,但是它带来的好处却是非常显著的,例如可以节省磁盘和内存消耗,依赖库的解耦和独立管理等。从下面ls的输出来看,动态链接产生的可执行文件确实会比静态链接的要小。据估算,动态链接与静态链接相比,性能损失大约在5%以下,但这点性能损失用来换取程序在空间上的节省和程序构建和升级时的灵活性(只要库函数接口不变,源程序就不必重新编译),是相当值得的。因此gcc采用的默认链接方式就是动态链接。

$ ls -l bin
total 72
-rwxr-xr-x  1 hch  staff  8432  9 20 20:21 dynamic_link_test
-rwxr-xr-x  1 hch  staff  8464  9 20 20:22 link_test
-rwxr-xr-x  1 hch  staff  8464  9 20 11:04 static_link_test

还是以上述demo为例,在src目录下输入gcc hello.c -shared -fPIC -o ../lib/libhello.so即可在lib目录下生成libhello.so文件,和静态库一样在Unix上它们都是以lib开头,不同的是动态库的后缀则是.so(shared object);在Windows上则是一个后缀为.dll的文件。

构建完动态库后,在src目录下输入命令gcc link_test.c -o ../bin/dynamic_link_test -L../lib -lhello可生成一个名为dynamic_link_test的可执行文件,它采用的是动态链接,因此若将动态库删除或者重命名都会导致程序运行报错。另外由于编译时采用的相对路径,所以在执行程序时所在的目录并非编译时目录也会报错,因为它会去上级的lib目录去搜索动态库,此时可以将-L指定为一个绝对路径重新编译;或者将动态库移动至/usr/lib这类默认搜索路径,然后去除-L参数重新编译。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值