Linux环境下的编译,链接与库的使用.doc
《Linux环境下的编译,链接与库的使用.doc》由会员分享,可在线阅读,更多相关《Linux环境下的编译,链接与库的使用.doc(27页珍藏版)》请在淘文阁 - 分享文档赚钱的网站上搜索。
1、Linux环境下的编译,链接与库的使用From: http:/liuxun.org/为什么使用ullib有时会出现 undefined reference error 的错误?为什么在动态链接库里ul_log会把日志输出到屏幕上?为什么用-static 编译有时候会报warning?我们在使用基础库或者第三方库的时候,经常遇到这样那样的问题,本文结合公司目前的主要环境,说明库的原理,使用的注意事项。从程序到可执行文件从hello world 说起include int main() printf(“hello worldn”); return 0; 上面这一段程序任何一个学过C语言的同学都是闭
2、着眼睛都可以写出来,但是对于将这样一个源代码编译成为一个可执行文件的过程却不一定有所了解。 上面的程序如果要编译,很简单gcc hello.c 然后./a.out就可以运行,但是在这个简单的命令后面隐藏了许多复杂的过程一般来说,可以把这样的过程分成4个: 预编译, 编译, 汇编和链接。预编译:这个过程包括了下面的步骤宏定义展开,所有的#define 在这个阶段都会被展开 预编译命令的处理,包括#if #ifdef 一类的命令 展开#include 的文件,像上面hello world 中的stdio.h , 把stdio.h中的所有代码合并到hello.c中 去掉注释 gcc的预编译 采用的是
3、预编译器cpp, 我们可以通过-E参数来看预编译的结果,如: gcc -E hello.c -o hello.i 生 成的 hello.i 就是经过了预编译的结果 在预编译的过程中不会太多的检查与预编译无关的语法(#ifdef 之类的还是需要检查, #include文件路径需要检查), 但是对于一些诸如 ; 漏掉的语法错误,在这个阶段都是看不出来的。 写过makefile的人都知道, 我们需要加上-Ipath 一系列的参数来标示gcc对头文件的查找路径小提示:1.在一些程序中由于宏的原因导致编译错误,可以通过-E把宏展开再检查错误 , 这个在编写 PHP扩展, python扩展这些大量需要使用
4、宏的地方对于查错误很有帮助。2.如果在头文件中,#include 的时候带上路径在这个阶段有时候是可以省不少事情, 比如 #include , 这样在gcc的-I参数只需要指定一个路径,不会由于不小心导致,文件名正好相同出现冲突的麻烦事情. 不过公司由于早期出现了lib2和lib2-64两个目录, 以及头文件输出在include 目录下, 静态发布等一些历史原因, 有些时候使用带完整路径名的方式不是那么合适( 比如 #include 中间有一个include 显的很别扭). 不过个人认为所有的#include 都应该是尽量采用从cvs 根路径下开始写完整路径名的方式进行预编译的过程,只是受限于
5、公司原有习惯和历史问题而显的不合适, 当然带路径的方式要多写一些代码,也是麻烦的事情, 路径由外部指定相对也会灵活一些.编译:这个过程才是进行语法分析和词法分析的地方, 他们将我们的C/C+代码翻译成为 汇编代码, 这也是一个编译器最复杂的地方使用命令gcc -S hello.i -o hello.s 可 以看到gcc编译出来的汇编代码, 现代gcc编译器一般是把预编译和编译合在一起,使用cc1 的程序来完成这个过程,在我们的开发机上有些时候一些同学编译大文件的时候可以用top命令看一个cc1的进程一直在占用时间,这个时候就是程序在执行编 译过程. 后面提到的编译过程都是指 cc1的处理包括了
6、预编译与编译.汇编: 现在C/C+代码已经成为汇编代码了,直接使用汇编代码的编译器把汇编变成机器码(注意还不是可执行的) .gcc -c hello.c -o hello.o 这里的hello.o就是最后的机器码, 如果作为一个静态库到这里可以所已经完成了,不需要后面的过程.对于静态库, 比如ullib, COM提供的是libullib.a, 这里的.a文件其实是多个.o 通过ar命令打包起来的, 仅仅是为了方便使用,抛开.a 直接使用.o 也是一样的小提示:1. gcc 采用as 进行汇编的处理过程,as 由于接收的是gcc生成的标准汇编, 在语法检查上存在不少缺陷,如果是我们自己写的汇编代
7、码给as去处理,经常会出现很多莫名奇妙的错误. 链接: 链接的过程,本质上来说是一个把所有的机器码文件组合成一个可执行的文件 上面汇编的结果得到一个.o文件, 但是这个.o要生成二执行文件只靠它自己是不行的, 它还需要一堆辅助的机器码,帮它处理与系统底层打交道的事情.gcc -o hello hello.o 这样就把一个.o文件链接成为了一个二进制可执行文件. 我们提供的各种库头文件在编译期使用,到了链接期就需要用-l, -L的方式来指定我们到底需要哪些库。 对于glibc中的strlen之类常用的东西编译器会帮助你去加上可以不需要手动指定。这个地方也是本文讨论的重点, 在后面会有更详细的说明
8、小提示:有些程序在编译的时候会出现 “linker input file unused because linking not done” 的提示(虽然gcc不认为是错误,这个提示还是会出现的), 这里就是把 编译和链接 使用的参数搞混了,比如g+ -c test.cpp -I././ullib/include -L././ullib/lib/ -lullib 这样的写法就会导致上面的提示, 因为在编译的过程中是不需要链接的, 它们两个过程其实是独立的静态链接链接的过程 这里先介绍一下,链接器所做的工作,其实链接做的工作分两块: 符号解析和重定位符号解析符号包括了我们的程序中的被定义和引用的函
9、数和变量信息在命令行上使用 nm ./testtest 是用户的二进制程序,包括可以把在二进制目标文件中符号表输出009b8 A bss_start004cc t call_gmon_start009b8 b completed. d CTOR_END00780 d CTOR_LIST009a0 D data_start009a0 W data_start00630 t do_global_ctors_aux004f0 t do_global_dtors_aux009a8 D dso_handle00798 d DTOR_END00790 d DTOR_LIST007a8 D DYNAMIC0
10、09b8 A edata009c0 A end00668 T fini00780 A fini_array_end00780 A fini_array_start00530 t frame_dummy00778 r FRAME_END00970 D GLOBAL_OFFSET_TABLE w gmon_start U gxx_personality_v0CXXABI_1. T _init00780 A _init_array_end 当然上面由nm输出的符号表可以通过编译命令去除,让人不能直接看到。链接器解析符号引用的方式是将每一个引用的符号与其它的目标文件(.o)的符号表中一个符号的定义联系
11、起来,对于那些和引用定义在相同模块的本地符号(注:static修饰的),编译器在编译期就可以发现问题,但是对于那些全局的符号引用就比较麻烦了下面来看一个最简单程序:includeint foo();int main() foo(); return 0; 我们把文件命名为test.cpp, 采用下面的方式进行编译 g+ -c test.cppg+ -o test test.o 第一步正常结束,并且生成了test.o文件,到第二步的时候报了如下的错误test.o(.text+0x5): In function main: undefined reference tofoo()collect2: l
12、d returned 1 exit status 由于foo 是全局符号, 在编译的时候不会报错,等到链接的时候,发现没有找到对应的符号,就会报出上面的错误。但是如果我们把上面的写法改成下面这样include/注意这里的static static int foo();int main() foo(); return 0; 在运行 g+ -c test.cpp, 马上就报出下面的错误:test.cpp:19: error: int foo() used but never defined 在编译器就发现foo 无法生成目标文件的符号表,可以马上报错,对于一些本地使用的函数使用static一方面可
13、以避免符号污染,另一方面也可以让编译器尽快的发现错误在我们的基础库中提供的都是一系列的.a文件,这些.a文件其实是一批的目标文件(.o)的打包结果这样的目的是可以方便的使用已有代码生成的结果,一般情况下是一个.c/.cpp文件生成一个.o文件,在编译的时候如果带上一堆的.o文件显的很不方便,像:g+ -o main main.cpp a.o b.o c.o 这样大量的使用.o也很容易出错,在linux下使用archive来将这些.o存档和打包所以我们就可以把编译参数写成g+ -o main main.cpp ./libullib.a 我们可以使用./libullib.a 直接使用libulli
14、b.a这个库,不过gcc提供了另外的方式来使用:g+ -o main main.cpp -L./ -lullib -L指定需要查找的库文件的路径, -l 选择需要使用的库名字,不过库的名字需要用lib+name的方式命名,才会被gcc认出来 不过上面的这种方式存在一个问题就是不区分动态库和静态库,这个问题在后面介绍动态库的时候还会提到当存在多个.a ,并且在库之间也存在依赖关系,这个时候情况就比较复杂如果我们要使用lib2-64/dict, dict又依赖ullib, 这个时候需要写成类似下面的形式g+ -o main main.cpp -L./lib2-64/dict/lib -L./lib
15、2-64/ullib/lib -ldict -lullib -lullib 需要写在-ldict的后面,这是由于在默认情况对于符号表的解析和查找工作是由后往前(内部实现是一个类似堆栈的尾递归) 所以当所使用的库本身存在依赖关系的时候,越是基础的库就越是需要放到后面否则如果上面把-ldict -lulib的位置换一下,可能就会出现 undefined reference toxxx 的错误 一般来说对于基础库的依赖关系可以在平台上获取, 若存在一些第三方的依赖,就只有参考相关的帮助说明了当然gcc提供了另外的方式的来解决这个问题g+ -o main main.cpp -L./lib2-64/di
16、ct/lib -L./lib2-64/ullib/lib-Xlinker “-(” -ldict -lullib-Xlinker “-)” 可以看到我们需要的库被-Xlinker “-(“和-Xlinker “-)” 包含起来,gcc在这里处理的时候会循环自动查找依赖关系,不过这样的代价就是延长gcc的编译时间,如果使用的库非常的多时候,对编译的耗时影响还是非常大.-Xlinker有时候也简写成”-Wl, “,它的意思是 它后面的参数是给链接器使用的-Xlinker 和 -Wl 的区别是一个后面跟的参数是用空格,另一个是用”,”我们通过nm命令查看目标文件,可以看到类似下面的结果/lib2-6
17、4/dict/lib/x.html 1 09740 T Z11ds_syn_loadPcS 2 09c62 T Z11ds_syn_seekP16Sdict_search_synPcS1_i 3 07928 T Z11dsur_searchPcS_S 4 &nbs p; U Z11ul_readfilePcS_Pvi 5 &nbs p; U Z11ul_writelogiPKcz 6 000a2 T Z12creat_sign32Pc其中用U标示的符号_Z11ul_readfilePcS_Pvi(其实是ullib中的 ul_readfile) ,表示在dict的目标文件中没有找到ul_read
18、file函数在链接的时候,链接器就会去其他的目标文件中查找_Z11ul_readfilePcS_Pvi的符号小提示:编译的时候采用-Lxxx -lyyy的形式使用库,-L和-l这个参数并没有配对的关系,我们的一些Makefile 为了维护方便把他们写成配对的形式,给一些同学造成了误解 其实我们完全可以写成-Lpath1, -Lpath2, -Lpath3, -llib1 这样的形式在具体链接的时候,gcc是以.o文件为单位, 编译的时候如果写g+ -o main main.cpp libx.o 那么无论main.cpp中是否使用到libx.o,libx.o中的所有符号都会被载入到main函数中
19、但是如果是针对.a,写成g+ -o main main.cpp -L./ -lx, 这个时候gcc在链接的时候只会链接有被用到.o, 如果出现libx.a中的某个.o文件中没有任何一个符号被main用到,那么这个.o就不会被链接到main中gcc编译.c文件的时候和g+ 有一个不一样的地方, 就是在g+ 中对于一个函数必须要先定在再使用,比如上面的例子中需要先定义foo()才能被使用,但对于gcc编译的.c(如果是.cpp会自动换成C+编译) 文件, 可以不需要先定义, 而直接使用. 但这样会出现问题, 如果没有其他地方使用和这个函数同名的函数那么链接的时候会找不到这个函数. 但是如果碰巧在另
20、外的地方存在一个同名函数,那么链接的时候就会被直接连接到这个函数上, 万一使用的时候偏偏传入参数或返回值的类型不对,那么这个时候就可能出现莫名奇妙的错误. 不过我们还是可以用-Wmissing-declarations参数打开这个检查重定位经过上面的符号解析后,所有的符号都可以找到它所对应的实际位置(U表示的链接找到具体的符号位置)as 汇编生成一个目标模块的时候,它不知道数据和代码在最后具体的位置,同时也不知道任何外部定义的符号的具体位置,所以as在生成目标代码的时候,对于位置未知的符号,它会生成一个重定位表目,告诉链接器在将目标文件合并成可执行文件时候如何修改地址成最终的位置g+和gcc
21、采用gcc 和g+ 在编译的时候产生的符号有所不同在C+中由于要支持函数重载,命名空间等特性,g+会把函数参数(可能还有命名空间),把函数命变成一个特殊并且唯一的符号名例如:int foo(int a); 在gcc编译后,在符号表中的名字就是函数名foo, 但是在g+编译后名字可能就变成了_Z3fooi, 我们可以使用c+filt命令把一个符号还原成它原本的样子,比如c+filt _Z3fooi 运行的结果可以得到foo(int)由于在C+和纯C环境中,符号表存在不兼容问题,程序不能直接调用C+编译出来的库,C+程序也不能直接调用C编译出来的库为了解决这个问题C+中引入了extern “C”的
22、方式extern “C” int foo(int a); 这样在用g+编译的时候, c+的编译器会自动把上面的 int foo(int a)当做C的接口进行符号转化这样在纯C里面就可以认出这些符号不过这里存在一个问题,extern “C” 是C+支持的,gcc并不认识,所有在实际中一般采用下面的方式使用c+#ifdef cplusplus extern “C” #endifint foo(int a); #ifdef cplusplus #endif这样这个头文件中的接口即可以给gcc使用也可以给g+使用, 当然在extern “C” 中的接口是不支持重载,默认参数等特性在我们的64位编译环境
23、中如果有gcc的程序,使用上面方式g+编译出来的库,需要加上-lstdc+, 这是因为,对于我们位环境下g+编译出来的库,需要使用到一个gxx_personality_v0的符号,它所在的位置是/usr /lib64/libstdc+.so.6 (C+的标准库iostream都在里面,C+程序都需要的). 但是在我们的32位2.96 g+编译器中是不需要gxx_personality_v0,所有编译可以不加上 -lstdc+小提示:在linux gcc 中,只有在源代码使用.c做后缀,并且使用gcc编译才会被编译成纯C的结果,其他情况像g+编译.c文件,或者gcc 编译.cc, .cpp文件都
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Linux 环境 编译 链接 使用
限制150内