《编译与链接的知识》word版_第1页
《编译与链接的知识》word版_第2页
《编译与链接的知识》word版_第3页
《编译与链接的知识》word版_第4页
《编译与链接的知识》word版_第5页
已阅读5页,还剩11页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、.编译与链接的知识编译与链接的知识2020-07-05 23:52linux下编译hello.c程序,使用gcc hello.c,然后./a.out就可以运行;在这个简单的命令后面隐藏了许多复杂的过程,这个过程包括了下面的步骤宏定义展开,所有的#define在这个阶段都会被展开预编译命令的处理,包括#if#ifdef一类的命令展开#include的文件,像上面hello world中的stdio.h,把stdio.h中的所有代码合并到hello.c中去掉注释gcc的预编译采用的是预编译器cpp,我们可以通过-E参数来看预编译的结果,如:gcc-E hello.c-o hello.i生成的hel

2、lo.i就是经过了预编译的结果在预编译的过程中不会太多的检查与预编译无关的语法#ifdef之类的还是需要检查,#include文件途径需要检查,但是对于一些诸如;漏掉的语法错误,在这个阶段都是看不出来的。写过makefile的人都知道,我们需要加上-Ipath一系列的参数来标示gcc对头文件的查找途径小提示:1.在一些程序中由于宏的原因导致编译错误,可以通过-E把宏展开再检查错误,这个在编写PHP扩展,python扩展这些大量需要使用宏的地方对于查错误很有帮助。2.假设在头文件中,#include的时候带上途径在这个阶段有时候是可以省不少事情,比方#include public/connect

3、pool/connectpool.h,这样在gcc的-I参数只需要指定一个途径,不会由于不小心导致,文件名正好一样出现冲突的费事事情.带途径的方式要多写一些代码,也是费事的事情,途径由外部指定相对也会灵敏一些.编译这个过程才是进展语法分析和词法分析的地方,他们将我们的C/C+代码翻译成为汇编代码,这也是一个编译器最复杂的地方使用命令gcc-S hello.i-o hello.s可以看到gcc编译出来的汇编代码,现代gcc编译器一般是把预编译和编译合在一起,使用cc1的程序来完成这个过程,编译大文件的时候可以用top命令看一个cc1的进程一直在占用时间,这个时候就是程序在执行编译过程.后面提到的

4、编译过程都是指cc1的处理包括了预编译与编译.汇编如今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生成的标准汇编,在语法检查上存在不少缺陷,假设是我们自己写的汇编代码给as去

5、处理,经常会出现很多莫名奇妙的错误.链接的过程,本质上来说是一个把所有的机器码文件组合成一个可执行的文件上面汇编的结果得到一个.o文件,但是这个.o要生成二执行文件只靠它自己是不行的,它还需要一堆辅助的机器码,帮它处理与系统底层打交道的事情.gcc-o hello hello.o这样就把一个.o文件链接成为了一个二进制可执行文件.这个地方也是本文讨论的重点,在后面会有更详细的说明小提示:有些程序在编译的时候会出现"linker input file unused because linking not done"的提示虽然gcc不认为是错误,这个提示还是会出现的,这里就是把

6、编译和链接使用的参数搞混了,比方g+-c test.cpp-I././ullib/include-L././ullib/lib/-lullib这样的写法就会导致上面的提示,因为在编译的过程中是不需要链接的,它们两个过程其实是独立的静态链接链接的过程这里先介绍一下,链接器所做的工作其实链接做的工作分两块:符号解析和重定位符号解析符号包括了我们的程序中的被定义和引用的函数和变量信息在命令行上使用nm./test test是用户的二进制程序,包括可以把在二进制目的文件中符号表输出00000000005009 b8 A_bss_start 00000000004004 cc tcall_gmon_st

7、art 00000000005009 b8 bcompleted.1 0000000000500788 d_CTOR_END_ 0000000000500780 d_CTOR_LIST_ 00000000005009 a0 D_data_start 00000000005009 a0 Wdata_start 0000000000400630 t_do_global_ctors_aux 00000000004004 f0 t_do_global_dtors_aux 00000000005009 a8 D_dso_handle 0000000000500798 d_DTOR_END_ 000000

8、0000500790 d_DTOR_LIST_ 00000000005007 a8 D_DYNAMIC 00000000005009 b8 A_edata 00000000005009 c0 A_end 0000000000400668 T_fini 0000000000500780 A_fini_array_end 0000000000500780 A_fini_array_start 0000000000400530 tframe_dummy 0000000000400778 r_FRAME_END_ 0000000000500970 D_GLOBAL_OFFSET_TABLE_ w_gm

9、on_start_ U_gxx_personality_v0CXXABI_1.3 0000000000400448 T_init 0000000000500780 A_init_array_end当然上面由nm输出的符号表可以通过编译命令去除,让人不能直接看到。链接器解析符号引用的方式是将每一个引用的符号与其它的目的文件.o的符号表中一个符号的定义联络起来,对于那些和引用定义在一样模块的本地符号注:static修饰的,编译器在编译期就可以发现问题,但是对于那些全局的符号引用就比较费事了.下面来看一个最简单程序:#include stdio.h int foo;int mainfoo;retur

10、n 0;我们把文件命名为test.cpp,采用下面的方式进展编译g+-c test.cpp g+-o test test.o第一步正常完毕,并且生成了test.o文件,到第二步的时候报了如下的错误test.o.text+0x5:In functionmain':undefined reference tofoo'collect2:ld returned 1exit status由于foo是全局符号,在编译的时候不会报错,等到链接的时候,发现没有找到对应的符号,就会报出上面的错误。但是假设我们把上面的写法改成下面这样#include stdio.h/注意这里的static sta

11、tic int foo;int mainfoo;return 0;在运行g+-c test.cpp,马上就报出下面的错误:test.cpp:19:error:'int foo'used but never defined在编译器就发现foo无法生成目的文件的符号表,可以马上报错,对于一些本地使用的函数使用static一方面可以防止符号污染,另一方面也可以让编译器尽快的发现错误.在根底库中提供的都是一系列的.a文件,这些.a文件其实是一批的目的文件.o的打包结果.这样的目的是可以方便的使用已有代码生成的结果,一般情况下是一个.c/.cpp文件生成一个.o文件,在编译的时候假设带上

12、一堆的.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直接使用libullib.a这个库,不过gcc提供了另外的方式来使用:g+-o main main.cpp-L./-lullib-L指定需要查找的库文件的途径,-l选择需要使用的库名字,不过库的名字需要用lib+name的方式命名,才会被gcc认出来.不过上面的这种方式存在一个问题就是不区分动态

13、库和静态库,这个问题在后面介绍动态库的时候还会提到.当存在多个.a,并且在库之间也存在依赖关系,这个时候情况就比较复杂.假设要使用lib2-64/dict,dict又依赖ullib,这个时候需要写成类似下面的形式g+-o main main.cpp-L./lib2-64/dict/lib-L./lib2-64/ullib/lib-ldict-lullib-lullib需要写在-ldict的后面,这是由于在默认情况对于符号表的解析和查找工作是由后往前内部实现是一个类似堆栈的尾递归.所以当所使用的库本身存在依赖关系的时候,越是根底的库就越是需要放到后面.否那么假设上面把-ldict-lulib的位

14、置换一下,可能就会出现undefined reference to xxx的错误.当然gcc提供了另外的方式的来解决这个问题g+-o main main.cpp-L./lib2-64/dict/lib-L./l ib2-64/ullib/lib-Xlinker"-"-ldict-lullib-Xlinker"-"可以看到我们需要的库被-Xlinker"-"和-Xlinker"-"包含起来,gcc在这里处理的时候会循环自动查找依赖关系,不过这样的代价就是延长gcc的编译时间,假设使用的库非常的多时候,对编译的耗时影响

15、还是非常大.-Xlinker有时候也简写成"-Wl,",它的意思是它后面的参数是给链接器使用的.-Xlinker和-Wl的区别是一个后面跟的参数是用空格,另一个是用","我们通过nm命令查看目的文件,可以看到类似下面的结果10000000000009740 T_Z11ds_syn_loadPcS_ 20000000000009 c62 T_Z11ds_syn_seekP16Sdict_search_synPcS1_i 30000000000007928 T_Z11dsur_searchPcS_S_ 4&nbs p;U _Z11ul_readfil

16、ePcS_Pvi 5&nbs p;U _Z11ul_writelogiPKcz 600000000000000 a2 T_Z12creat_sign32Pc其中用U标示的符号_Z11ul_readfilePcS_Pvi其实是ullib中的ul_readfile,表示在dict的目的文件中没有找到ul_readfile函数.在链接的时候,链接器就会去其他的目的文件中查找_Z11ul_readfilePcS_Pvi的符号小提示:编译的时候采用-Lxxx-lyyy的形式使用库,-L和-l这个参数并没有配对的关系,我们的一些Makefile为了维护方便把他们写成配对的形式,造成了误解.其实完全

17、可以写成-Lpath1,-Lpath2,-Lpath3,-llib1这样的形式.在详细链接的时候,gcc是以.o文件为单位,编译的时候假设写g+-o main main.cpp libx.o那么无论main.cpp中是否使用到libx.o,libx.o中的所有符号都会被载入到mian函数中.但是假设是针对.a,写成g+-o main main.cpp-L./-lx,这个时候gcc在链接的时候只会链接有被用到.o,假设出现libx.a中的某个.o文件中没有任何一个符号被main用到,那么这个.o就不会被链接到main中重定位经过上面的符号解析后,所有的符号都可以找到它所对应的实际位置U表示的链接

18、找到详细的符号位置.as汇编生成一个目的模块的时候,它不知道数据和代码在最后详细的位置,同时也不知道任何外部定义的符号的详细位置,所以as在生成目的代码的时候,对于位置未知的符号,它会生成一个重定位表目,告诉链接器在将目的文件合并成可执行文件时候如何修改地址成最终的位置g+和gcc采用gcc和g+在编译的时候产生的符号有所不同.在C+中由于要支持函数重载,命名空间等特性,g+会把函数+参数可能还有命名空间,把函数命变成一个特殊并且唯一的符号名.例如:int fooint a;在gcc编译后,在符号表中的名字就是函数名foo,但是在g+编译后名字可能就变成了_Z3fooi,我们可以使用c+fil

19、t命令把一个符号复原成它本来的样子,比方c+filt _Z3fooi运行的结果可以得到fooint由于在C+和纯C环境中,符号表存在不兼容问题,C程序不能直接调用C+编译出来的库,C+程序也不能直接调用C编译出来的库.为理解决这个问题C+中引入了extern"C"的方式.extern"C"int fooint a;这样在用g+编译的时候,c+的编译器会自动把上面的int fooint a当做C的接口进展符号转化.这样在纯C里面就可以认出这些符号.不过这里存在一个问题,extern"C"是C+支持的,gcc并不认识,所有在实际中一般采用

20、下面的方式使用+#ifdef _cplusplus extern"C"#endif int fooint a;#ifdef _cplusplus#endif这样这个头文件中的接口即可以给gcc使用也可以给g+使用,当然在extern"C"中的接口是不支持重载,默认参数等特性在我们的64位编译环境中假设有gcc的程序使用上面方式g+编译出来的库,需要加上-lstdc+,这是因为,对于我们64位环境下g+编译出来的库,需要使用到一个_gxx_personality_v0的符号,它所在的位置是/usr/lib64/libstdc+.so.6C+的标准库iost

21、ream都在里面,C+程序都需要的.但是在32位2.96 g+编译器中是不需要_gxx_personality_v0,所有编译可以不加上-lstdc+小提示:在linux gcc中,只有在源代码使用.c做后缀,并且使用gcc编译才会被编译成纯C的结果,其他情况像g+编译.c文件,或者gcc编译.cc,.cpp文件都会被当作C+程序编译成C+的目的文件,gcc和g+唯一的不同在于gcc不会主动链接-lstdc+在extern"C"中假设存在默认参数的接口,在g+编译的时候不会出现问题,但是gcc使用的时候会报错.因为对于函数重载,接口的符号表还是和不用默认参数的时候是一样的.

22、符号表冲突编译程序的时候时常会遇到类似于multiple definition offoo'的错误.这些错误的产生都是由于所使用的.o文件中存在了一样的符号造成的.比方:libx.cpp int fooreturn 30;liby.cpp int fooreturn 20;将libx.cpp,liby.cpp编译成libx.o和liby.o两个文件g+-o main main.cpp libx.o liby.o这个时候就会报出multiple definition offoo'的错误一些参数可以把这个警报关掉但是假设把libx.o和liby.o分别打包成libx.a和liby.

23、a用下面的方式编译g+-o main main.cpp-L./-lx-ly这个时候编译不会报错,它会选择第一个出现的库,上面的例子中会选择libx中的foo可以通过g+-o main main.cpp-L./-lx-ly-Wl,-trace-symbol=_Z3foov的命令查看符号详细是链接到哪个库中,g+-o main main.cpp-L./-lx-ly-Wl,-cref可以把所有的符号链接都输出无论是否最后被使用小提示:对于一些定义在头文件中的全局常量,gcc和g+有不同的行为,g+中const也同时是static的,但gcc不是例如:foo.h中存在一个const int INTVA

24、LUE=2000;的全局常量有两个库a和b,他们在生成的时候有使用到了INTVALUE,假设有一个程序main同时使用到了a库和b库,在链接的时候gcc编译的结果就会报错,但假设a和b都是g+编译的话结果却一切正常.这个原因主要是在g+中会把INTVALUE这种const常量当做static的,这样就是一个部分变量,不会导致冲突,但是假设是gcc编译的话,这个地方INTVALUE会被认为是一个对外的全局常量是非static的,这个时候就会造成链接错误动态链接对于静态库的使用,有下面两个问题当我们需要对某一个库进展更新的时候,我们必须把一个可执行文件再完好的进展一些重新编译在程序运行的时候代码是

25、会被载入机器的内存中,假设采用静态库就会出现一个库需要被copy到多个内存程序中,这个一方面占用了一定的内存,另一方面对于CPU的cache不够友好链接的控制,从前面的介绍中可以看到静态库的连接行为我们不好控制,做不到在运行期交换使用的库编译后的程序就是二进制代码,有些代码它们涉及到不同的机器和环境,假设在A机器上编译了一个程序X,把它直接放到B机器上去运行,由于A和B环境存在差异,直接运行X程序可能存在问题,这个时候假设把和机器相关的这部分做成动态库C,并且保证接口一致,编译X程序的时候只调用C的对外接口.对于一般的用户态的X程序而言,就可以简单的从A环境放到B环境中.但假设是静态编译,就可

26、能做不到这点,需要在B机器上重新编译一次.动态链接库在linux被称为共享库shared library,下文提到的共享库和动态链接库都是指代shared library,它主要是为理解决上面列出静态库的缺点而提出的.。共享库的使用共享库的使用主要有两种方式,一种方式和.a的静态库类似由编译器来控制,其本质和二进制程序一样都是由系统中的载入器ld-linux.so载入,另一种是写在代码中,由我们自己的代码来控制.还是以前面的例子为例:g+-shared-fPIC-o libx.so libx.cpp编译的时候和静态库类似,只是加上了-shared和-fPIC,将输出命名改为.so然后和可执行文

27、件链接.a一样,都是g+-o main main.cpp-L./-lx这样main就是调用libx.so,在运行的时候可能会出现找不到libx.so的错误,这个原因是由于动态的库查找途径的问题,动态库默认的查找途径是由/etc/ld.so.conf文件来指定,在运行可执行文件的时候,按照顺会去这些目录下查找需要的共享库。我们可以通过环境变量LD_LIBRARY_PATH来指定共享库的查找途径注:LD_LIBRARY_PATH的优先级比ld.so.conf要高.命令上运行ldd./main我们可以看到这个二进制程序在运行的时候需要使用的动态库,例如:libx.so=/home/bnh/tmp/t

28、est/libx.so0x003cb000libstdc+.so.6=/usr/lib/libstdc+.so.60x 00702000libm.so.6=/lib/tls/libm.so.60x00bde000libgcc_s.so.1=/lib/libgcc_s.so.10x00c3e000libc.so.6=/lib/tls/libc.so.60x00aab000这里列出了mian所需要的动态库,假设有看类似libx.so=no found的错误,就意味着途径不对,需要设置LD_LIBRARY_PATH来指定途径手动载入共享库除了采用类型于静态库的方式来使用动态库,我们还可以通过由代码来

29、控制动态库的使用。这种方式允许应用程序在运行时加载和链接共享库,主要有下面的四个接口载入动态链接库void*dlopenconst char*filename,int flag;获取动态库中的符号void*dlsymvoid*handle,char*symbol;关闭动态链接库void dlclosevoid*handle;输出错误信息const char*dlerrorvoid;看下面的例子:typedef int foo_t;foo_t*foo=foo_t*dlsymhandle,"foo";通过上面的方式我们可以载入符号"foo"所对应的地址,然后

30、通过强迫类型转换给一个函数指针,当然这里函数指针的类型需要和符号的原型类型保持一致,这些一般是由共享库所对应的头文件提供.这里要注意一个问题,在dlsym中载入的符号表示是和我们使用nm库文件所看到符号表要保持一致,这里就有一个前面提到的gcc和g+符号表的不同,一个int foo,假设是g+编译,并且没有extern"C"导出接口,那么用dlsym载入的时候需要用dlsymhandle,"_Z3foov"方式才可以载入函数int foo,所以建议所以的共享库对外接口都采用extern"C"的方式导出纯C接口对外使用,这样在使用上也会

31、比较方便dlopen的flag标志可以选择RTLD_GLOBAL,RTLD_NOW,RTLD_LAZY.RTLD_NOW,RTLD_LAZY只是表示载入的符号是一开场就被载入还等到使用的时候被载入,对于多数应用而言没有什么特别的影响.这两个标志都可以通过|和RTLD_GLOBAL一起连用这里主要是说明RTLD_GLOBAL的功能,考虑这样的一个情况:我们有一个main.cpp,调用了两个动态libA,和libB,假设A中有一个对外接口叫做testA,在main.cpp可以通过dlsym获取到testA的指针,进展使用.但是对于libB中的接口,它是看到不libA的接口,使用testA是不能调用到libA中的testA的,但是假设在dlopen翻开libA.so的时候,设置了RTLD_

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论