编译过程
C语言的编译过程一般认为分为4个步骤:预处理、编译、汇编和链接。
预处理:主要是进行文本替换,把include<>
中的头文件插入到当前.c
程序文本中。预处理之后,得到的文件名习惯上以.i
结尾。
编译:将预处理之后的.i
文件翻译成汇编语言,编译之后的汇编文件名通常以.s
结尾。
汇编:将.s
的汇编文件翻译成机器语言,称为以.o
结尾的目标对象,此时已经是01形式的二进制文件了,不再是文本文件。
链接:.o
的目标对象并不是可以执行的,还需要把所需要的其他目标对象链接在一起。比如你编译了一个hello.o
的目标对象,但是其中用到了printf
函数,那么就需要将该hello.o
的目标对象,和另一个printf.o
的目标对象链接起来,这样才能实现一个真正可执行的程序。
假设现在有一个add.c
的源文件,里面包含一个myadd()
的函数,如下:
int myadd(int a, int b){
int c = a + b;
return c;
}
首先我们将该add.c
文件直接编译成目标对象文件add.o
,以备后用。
gcc -c add.c
当然,还要有一个该文件的文件头add.h
int myadd(int a, int b);
如果我们在下面的源代码main.c
中调用add.c
文件中的myadd()
函数
#include
#include "add.h"
int main(){
int x = 12;
int y = 10;
int z = myadd(x,y);
printf("x + y = %d\n",z);
return 0;
}
下面我们来逐步进行编译,首先是预处理:
gcc -E main.c -o main.i
查看main.i
文件可看到多了很多行代码,其中很多一部分是因为我们include
中的内容,而我们自己写的add.h
也通过文本替换进行了预处理:
...
# 2 "main.c" 2
# 1 "add.h" 1
int myadd(int a, int b);
# 3 "main.c" 2
int main()
{
int x = 12;
int y = 10;
int z = myadd(x,y);
printf("x + y = %d\n",z);
return 0;
}
然后是编译,将预处理之后的main.i
翻译成main.s
的汇编语言:
gcc -S main.i
得到的汇编语言如下(省略部分内容):
.file "main.c"
...
movl -4(%rbp), %eax
movl %edx, %esi
movl %eax, %edi
call myadd
...
.ident "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-39)"
.section .note.GNU-stack,"",@progbits
然后是编译,将汇编语言翻译成机器码,生成main.o
的目标对象。
gcc -c main.s
此时的目标对象并不是一个可执行的对象,我们可以使用nm
命令来查看二进制文件中的函数。
nm main.o
# 0000000000000000 T main
# U myadd
# U printf
这儿的myadd
和printf
都是U
,即表示未定义。所以这也是为什么要进行链接的原因。
我们尝试执行下面的代码,直接从源文件,编译成最后的可执行文件。
gcc main.c -o main
# /tmp/ccl95whV.o: In function `main':
# main.c:(.text+0x21): undefined reference to `myadd'
# collect2: error: ld returned 1 exit status
会出现错误提示,因为我们前面提到了,myadd
是一个未定义的对象。但是为什么没有提示printf
错误呢?printf
也是一个未定义的对象啊!
其实,gcc编译器在编译的时候会在系统默认路径下寻找相应的目标对象,而printf
是C语言标准库中的函数,所以不用我们指定目标对象,gcc便能够找到相应的函数。但是,myadd
,是我们在当前路径下自定义的一个函数,gcc不会在当前路径下寻找对应的目标对象,所以我们上述的编译过程就出现了无法找到myadd
的错误。
为了解决上述问题,我们在编译的时候需要指定myadd
函数所在的目标对象,即add.o
。如果你想看一下链接到的C语言标准库,可以使用-Wl,--trace
参数:
gcc main.c add.o -o main -Wl,--trace
# /usr/bin/ld: mode elf_x86_64
# /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crt1.o
# /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crti.o
# /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtbegin.o
# /tmp/ccbCG20V.o
# /home/shared/YiguanWang/C_learn/add.o
# -lgcc_s (/usr/lib/gcc/x86_64-redhat-linux/4.8.5/libgcc_s.so)
# /lib64/libc.so.6
# (/usr/lib64/libc_nonshared.a)elf-init.oS
# /lib64/ld-linux-x86-64.so.2
# /lib64/ld-linux-x86-64.so.2
# -lgcc_s (/usr/lib/gcc/x86_64-redhat-linux/4.8.5/libgcc_s.so)
# /usr/lib/gcc/x86_64-redhat-linux/4.8.5/crtend.o
# /usr/lib/gcc/x86_64-redhat-linux/4.8.5/../../../../lib64/crtn.o
这儿, printf
链接到了/lib64/libc.so.6
动态库中。而myadd
链接到了/home/shared/haha/C_learn/add.o
目标对象中。
这时,再执行编译后的程序,就完全没有问题了。
./main
# x + y = 22
静态库和动态库
你可能还会留意到一个问题,为什么在上面链接过程中,一个是.o
的目标对象,而另一个是.so.6
的文件?这就涉及到C语言函数库库的两种形式。
函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为”.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库。
静态库
制作静态库比较简单,使用ar
命令直接生成就可以了。比如,我们把上面add.o
的目标对象转换成一个静态库。
ar -rv libadd.a add.o
# ar: creating libadd.a
# a - add.o
习惯上,静态库以.a
为文件名结尾。
然后我们将此静态库链接到之前的main.c
源代码中:
gcc main.c libadd.a -o main_static
./main_static
# x + y = 22
此时生成的可执行。如果将libadd.a
的静态库删除,那么main_static
仍然可以执行。
动态库
动态库在链接的时候并没有将代码拷贝到最终生成的可执行程序中,而是在执行的时候才去加载。动态库的使用很广泛,但是制作一个动态库要比静态库麻烦一些。
不同的操作系统中,我们习惯上使用不同的后缀名,在Linux上,动态库通常是.so
结尾,在OSX上,通常是.dylib
结尾,而在Windows上,通常是.dll
结尾。
gcc -shared -fpic add.c -o libadd.so
其中,-shared
即表示创建一个动态库,-fpic
(position-independent code)将绝对地址转换成相对地址,这样不同的程序可以在不同的虚拟地址中加载该动态库。
我们将上述动态库libadd.so
链接到我们的main.c
的程序中,
gcc main.c libadd.so -o main_dyn
或者可以通过制定动态库名和路径来链接,如下:
gcc main.c -L /home/shared/YiguanWang/C_learn -l add -o main_dyn
这儿-L
用来指定动态库所在的路径,-l
用来指定库名称,注意这个库名称是libadd.so
去掉前缀lib
和后缀.so
之后的名称。上述两种方法编译后的可执行程序是完全一样的。
此时我们运行上述在动态库上建立的可执行程序,如下:
./main_dyn
# ./main_dyn: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory
此时居然提示错误,说找不到libadd.so
的动态库!
这就是动态库和静态库的区别,静态库编译完成之后,完全不依赖于静态库,即便删除了静态库,程序仍然可以正确执行,但是动态库不一样,编译好的程序的运行需要依赖于外部动态库的存在。程序在运行时会按照一定的顺序寻找所需要的动态库,比如RPATH
,LD_LIBRARY_PATH
, /etc/ld.so.conf
, /lib/
, /usr/lib/
等,如果在这些路径中没有找到所需动态库,那么就出出现上述错误提示。
因为我们自定义的libadd.so
动态库并不在上述路径中,所以我们必须给链接器指定其所在的路径。一种方法是,我们可以把libadd.so
所在的路径添加到链接路径LD_LIBRARY_PATH
中,然后再运行程序,就不会出现错误了。如下
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/home/shared/YiguanWang/C_learn
./main_dyn
# x + y = 22
另一种方法是在我们链接的时候,使用RPATH
,即“runtime library path”,该路径会保存在最后生成的可执行程序中,在程序运行中,会按照该路径寻找相应的动态库。如下,使用-Wl,-rpath
来指定该路径。
gcc main.c -Wl,-rpath=/home/shared/YiguanWang/C_learn/ -L /home/shared/YiguanWang/C_learn -l add -o main_dyn1
我们比较一下加入了RPATH
的可执行程序main_dyn1
和没有加入该路径的main_dyn
两个程序的链接情况,如下:
ldd main_dyn
# linux-vdso.so.1 => (0x00007ffef55eb000)
# libadd.so => not found
# libc.so.6 => /lib64/libc.so.6 (0x00007fb0c780b000)
# /lib64/ld-linux-x86-64.so.2 (0x00007fb0c7bd9000)
ldd main_dyn1
# linux-vdso.so.1 => (0x00007fff9d708000)
# libadd.so => /home/shared/YiguanWang/C_learn/libadd.so (0x00007fcc1b8c9000)
# libc.so.6 => /lib64/libc.so.6 (0x00007fcc1b4fb000)
# /lib64/ld-linux-x86-64.so.2 (0x00007fcc1bacb000)
可以看到加入RPATH
的main_dyn1
程序已经可以成功连接到libadd.so
动态库上了,而另一个则仍然提示没有找到对应的动态库。
和静态库相比,整个程序的运行完全依赖于动态库,所以如果你把动态库删除,上述程序仍然会提示错误。
综上,动态库相比于静态库,主要的难点就是如何在程序执行过程中顺利找到外部所依赖的动态库。而动态库的优点是能都很大程度上减少程序的大小。比如我们可以比较一下上述静态库链接的可执行程序main_static
和动态库链接的可执行程序main_dyn
的大小。
ls -l main_*
# -rwxrwxr-x 1 hhywan59 hhywan59 8400 Jun 23 12:52 main_dyn
# -rwxrwxr-x 1 hhywan59 hhywan59 8400 Jun 23 13:29 main_dyn1
# -rwxrwxr-x 1 hhywan59 hhywan59 8416 Jun 23 11:04 main_static
静态库链接的程序比动态库链接的程序大了16B。当然,此处它们相差不是太大,主要是我们的库文件很简单,如果是一个很大很长的库文件,那么静态库链接的程序可能会比动态库的大很多。
此外,动态库还能够更有效的利用内存。如果对库文件进行修改,我们也不需要重新编译程序。
后记:很多生信软件在安装的过程中,会用到各种C语言库,即便软件本身不是C语言写的,在安装过程中涉及到很多底层依赖库。而一旦出现错误,提示信息往往令人摸不着头脑,本文比较基础地简述了编译和链接过程,对理解软件安装过程中出现错误信息会有所帮助。
【谢谢阅读,欢迎转发分享】
参考资料:https://blog.csdn.net/keneyr/article/details/87277585?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-2 https://stackoverflow.com/questions/8482152/whats-the-difference-between-rpath-and-l https://www.cprogramming.com/tutorial/shared-libraries-linux-gcc.html http://nickdesaulniers.github.io/blog/2016/11/20/static-and-dynamic-libraries/ https://www.quora.com/What-are-the-pros-and-cons-of-static-and-dynamic-linking-in-c++