linux下编译原理分析
linux下编译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生成的 hello.i 就是经过了预编译的结果(中间文件)。在预编译的过程中不会太多的检查与预编译无关的语法(#ifdef 之类的还是须要检查, #include文件路径须要检查), 可是对于一些诸如 ; 漏掉的语法错误,在这个阶段都是看不出来的。写过makefile的人都知道, 我们须要加上-Ipath 一系列的參数来标示gcc对头文件的查找路径小提示:
1.在一些程序中因为宏的原因导致编译错误,能够通过-E把宏展开再检查错误 , 这个在编写 PHP扩展, python扩展这些大量须要使用宏的地方对于查错误非常有帮助。
2.假设在头文件里,#include 的时候带上路径在这个阶段有时候是能够省不少事情, 比方 #include <public/connectpool/connectpool.h>, 这样在gcc的-I參数仅仅须要指定一个路径,不会因为不小心导致,文件名称正好同样出现冲突的麻烦事情. 带路径的方式要多写一些代码,也是麻烦的事情, 路径由外部指定相对也会灵活一些.
======================================================================================
编译:
这个过程才是进行语法分析和词法分析的地方, 他们将我们的C/C++代码翻译成为 汇编代码, 这也是一个编译器最复杂的地方
使用命令
gcc -S hello.i -o hello.s能够看到gcc编译出来的汇编代码, 现代gcc编译器通常是把预编译和编译合在一起,使用cc1 的程序来完毕这个过程,编译大文件的时候能够用top命令看一个cc1的进程一直在占用时间,这个时候就是程序在运行编译过程. 后面提到的编译过程都是指 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去处理,常常会出现非常多莫名奇异的错误.
======================================================================================
链接:
链接的过程,本质上来说是一个把全部的机器码文件组合成一个可运行的文件上面汇编的结果得到一个.o文件, 可是这个.o要生成二运行文件仅仅靠它自己是不行的, 它还须要一堆辅助的机器码,帮它处理与系统底层打交道的事情.
gcc -o hello hello.o这样就把一个.o文件链接成为了一个二进制可运行文件.
这个地方也是本文讨论的重点, 在后面会有更具体的说明
小提示:
有些程序在编译的时候会出现 "linker input file unused because linking not done" 的提示(尽管gcc不觉得是错误,这个提示还是会出现的), 这里就是把 编译和链接 使用的參数搞混了,比方
g++ -c test.cpp -I../../ullib/include -L../../ullib/lib/ -lullib这种写法就会导致上面的提示, 由于在编译的过程中是不须要链接的, 它们两个过程事实上是独立的
静态链接
======================================================================================
链接的过程这里先介绍一下,链接器所做的工作
事实上链接做的工作分两块: 符号解析和重定位
符号解析
符号包含了我们的程序中的被定义和引用的函数和变量信息
在命令行上使用 nm ./test
test 是用户的二进制程序,包含
能够把在二进制目标文件里符号表输出
00000000005009b8 A __bss_start 00000000004004cc t call_gmon_start 00000000005009b8 b completed.1 0000000000500788 d __CTOR_END__ 0000000000500780 d __CTOR_LIST__ 00000000005009a0 D __data_start 00000000005009a0 W data_start 0000000000400630 t __do_global_ctors_aux 00000000004004f0 t __do_global_dtors_aux 00000000005009a8 D __dso_handle 0000000000500798 d __DTOR_END__ 0000000000500790 d __DTOR_LIST__ 00000000005007a8 D _DYNAMIC 00000000005009b8 A _edata 00000000005009c0 A _end 0000000000400668 T _fini 0000000000500780 A __fini_array_end 0000000000500780 A __fini_array_start 0000000000400530 t frame_dummy 0000000000400778 r __FRAME_END__ 0000000000500970 D _GLOBAL_OFFSET_TABLE_ w __gmon_start__ U __gxx_personality_v0@@CXXABI_1.3 0000000000400448 T _init 0000000000500780 A __init_array_end ...当然上面由nm输出的符号表能够通过编译命令去除,让人不能直接看到。
链接器解析符号引用的方式是将每个引用的符号与其他的目标文件(.o)的符号表中一个符号的定义联系起来, 对于那些和引用定义在同样模块的本地符号(注:static修饰的),编译器在编译期就能够发现问题,可是对于那些全局的符号引用就比較麻烦了.
======================================================================================
以下来看一个最简单程序:
g++ -c test.cpp g++ -o test test.o
第一步正常结束,而且生成了test.o文件,到第二步的时候报了例如以下的错误
test.o(.text+0x5): In function `main': : undefined reference to `foo()' collect2: ld returned 1 exit status
因为foo 是全局符号, 在编译的时候不会报错,等到链接的时候,发现没有找到相应的符号,就会报出上面的错误。可是假设我们把上面的写法改成以下这样
在执行 g++ -c test.cpp, 立即就报出以下的错误:
test.cpp:19: error: 'int foo()' used but never defined
在编译器就发现foo 无法生成目标文件的符号表,能够立即报错,对于一些本地使用的函数使用static一方面能够避免符号污染,还有一方面也能够让编译器尽快的发现错误.
在基础库中提供的都是一系列的.a文件,这些.a文件事实上是一批的目标文件(.o)的打包结果.这种目的是能够方便的使用已有代码生成的结果,普通情况下是一个.c/.cpp文件生成一个.o文件,在编译的时候假设带上一堆的.o文件显的非常不方便,像:
g++ -o main main.cpp a.o b.o c.o
这样大量的使用.o也非常easy出错,在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认出来.只是上面的这样的方式存在一个问题就是不区分动态库和静态库, 这个问题在后面介绍动态库的时候还会提到.
当存在多个.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的位置换一下,可能就会出现 undefined reference to xxx 的错误.
当然gcc提供了另外的方式的来解决问题
g++ -o main main.cpp -L../lib2-64/dict/lib -L../lib2-64/ullib/lib -Xlinker "-(" -ldict -lullib -Xlinker "-)"
能够看到我们须要的库被 -Xlinker "-(" 和 -Xlinker "-)" 包括起来,gcc在这里处理的时候会循环自己主动查找依赖关系,只是这种代价就是延长gcc的编译时间,假设使用的库很的多时候,对编译的耗时影响还是很大.
-Xlinker有时候也简写成"-Wl, ",它的意思是 它后面的參数是给链接器使用的.-Xlinker 和 -Wl 的差别是一个后面跟的參数是用空格,还有一个是用","
我们通过nm 命令查看目标文件,能够看到类似以下的结果
1 0000000000009740 T _Z11ds_syn_loadPcS_
2 0000000000009c62 T _Z11ds_syn_seekP16Sdict_search_synPcS1_i
3 0000000000007928 T _Z11dsur_searchPcS_S_
4 &nbs p; U _Z11ul_readfilePcS_Pvi
5 &nbs p; U _Z11ul_writelogiPKcz
6 00000000000000a2 T _Z12creat_sign32Pc
当中用 U 标示的符号_Z11ul_readfilePcS_Pvi (事实上是ullib中的 ul_readfile) ,表示在dict的目标文件里没有找到ul_readfile函数.
在链接的时候,链接器就会去其它的目标文件里查找_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中的全部符号都会被加载到mian函数中.可是假设是针对.a,写成g++ -o main main.cpp -L./ -lx, 这个时候gcc在链接的时候仅仅会链接有被用到.o, 假设出现libx.a中的某个.o文件里没有不论什么一个符号被main用到,那么这个.o就不会被链接到main中
======================================================================================
重定位
经过上面的符号解析后,全部的符号都能够找到它所相应的实际位置(U表示的链接找到详细的符号位置).
as 汇编生成一个目标模块的时候,它不知道数据和代码在最后详细的位置,同一时候也不知道不论什么外部定义的符号的详细位置,所以as在生成目标代码的时候,对于位置未知的符号,它会生成一个重定位表目,告诉链接器在将目标文件合并成可运行文件时候怎样改动地址成终于的位置
======================================================================================
g++和gcc
採用gcc 和g++ 在编译的时候产生的符号有所不同.
在C++中因为要支持函数重载,命名空间等特性,g++会把函数+參数(可能还有命名空间),把函数命变成一个特殊而且唯一的符号名.比如:
在gcc编译后,在符号表中的名字就是函数名foo, 可是在g++编译后名字可能就变成了_Z3fooi, 我们能够使用 c++filt命令把一个符号还原成它原本的样子,比方
c++filt _Z3fooi
执行的结果能够得到 foo(int)
因为在C++和纯C环境中,符号表存在不兼容问题,C程序不能直接调用C++编译出来的库,C++程序也不能直接调用C编译出来的库.为了解决问题C++中引入了 extern "C" 的方式.
extern "C" int foo(int a);
这样在用g++编译的时候, c++的编译器会自己主动把上面的 int foo(int a)当做C的接口进行符号转化.这样在纯C里面就能够认出这些符号.
只是这里存在一个问题,extern "C" 是C++支持的,gcc并不认识,全部在实际中一般採用以下的方式使用++
这样这个头文件里的接口即能够给gcc使用也能够给g++使用, 当然在extern "C" { } 中的接口是不支持重载,默认參数等特性
在我们的64位编译环境中假设有gcc的程序使用上面方式g++编译出来的库,须要加上-lstdc++, 这是由于,对于我们64位环境下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文件都会被当作C++程序编译成C++的目标文件, gcc和g++唯一的不同在于gcc不会主动链接-lstdc++
-
在 extern "C" { }中假设存在默认參数的接口,在g++编译的时候不会出现故障,可是gcc使用的时候会报错.由于对于函数重载,接口的符号表还是和不用默认參数的时候是一样的.
======================================================================================
符号表冲突
编译程序的时候时常会遇到类似于
multiple definition of `foo()'
的错误.
这些错误的产生都是因为所使用的.o文件里存在了同样的符号造成的.
比方:
libx.cpp
liby.cpp
将libx.cpp, liby.cpp编译成 libx.o和liby.o两个文件
g++ -o main main.cpp libx.o liby.o这个时候就会报出 multiple definition of `foo()' 的错误(一些參数能够把这个警报关掉)可是假设把libx.o和liby.o分别打包成libx.a和liby.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 中存在一个
的全局常量
有两个库 a和b, 他们在生成的时候有使用到了 INTVALUE,假设有一个程序main同一时候使用到了 a库和b库,在链接的时候gcc编译的结果就会报错,但假设a和b都是g++编译的话结果却一切正常.
这个原因主要是在g++中会把INTVALUE 这样的const常量当做static的,这样就是一个局部变量,不会导致冲突,可是假设是gcc编译的话,这个地方INTVALUE会被觉得是一个对外的全局常量是非static的,这个时候就会造成链接错误
======================================================================================
动态链接
对于静态库的使用,有以下两个问题
- 当我们须要对某一个库进行更新的时候,我们必须把一个可运行文件再完整的进行一些又一次编译
- 在程序执行的时候代码是会被加载机器的内存中,假设採用静态库就会出现一个库须要被copy到多个内存程序中,这个一方面占用了一定的内存,还有一方面对于CPU的cache不够友好
- 链接的控制,从前面的介绍中能够看到静态库的连接行为我们不好控制,做不到在执行期替换使用的库
- 编译后的程序就是二进制代码,有些代码它们涉及到不同的机器和环境,假设在A 机器上编译了一个程序X, 把它直接放到B机器上去执行,因为A和B环境存在差异,直接执行X程序可能存在问题,这个时候假设把和机器相关的这部分做成动态库C,而且保证接口一致,编译X程序的时候仅仅调用C的对外接口.对于一般的用户态的X程序而言,就能够简单的从A环境放到B环境中.但假设是静态编译,就可能做不到这点,须要在B机器上又一次编译一次.
动态链接库在linux被称为共享库(shared library,下文提到的共享库和动态链接库都是指代shared library),它主要是为了解决上面列出静态库的缺点而提出的.。
======================================================================================
共享库的使用
共享库的使用主要有两种方式,一种方式和.a的静态库类似由编译器来控制,事实上质和二进制程序一样都是由系统中的加载器(ld-linux.so)加载,还有一种是写在代码中,由我们自己的代码来控制.
还是曾经面的样例为例:
g++ -shared -fPIC -o libx.so libx.cpp编译的时候和静态库类似,仅仅是加上了 -shared 和 -fPIC, 将输出命名改为.so然后和可运行文件链接.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/test/libx.so (0x003cb000) libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x00702000) libm.so.6 => /lib/tls/libm.so.6 (0x00bde000) libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x00c3e000) libc.so.6 => /lib/tls/libc.so.6 (0x00aab000)
这里列出了mian所须要的动态库, 假设有看类似 libx.so=>no found的错误,就意味着路径不正确,须要设置LD_LIBRARY_PATH来指定路径
======================================================================================
手动加载共享库
除了採用类型于静态库的方式来使用动态库,我们还能够通过由代码来控制动态库的使用。
这样的方式同意应用程序在执行时载入和链接共享库,主要有以下的四个接口
加载动态链接库
看以下的样例:
通过上面的方式我们能够加载符号"foo"所相应的地址,然后通过强制类型转换给一个函数指针,当然这里函数指针的类型须要和符号的原型类型保持一致,这些通常是由共享库所相应的头文件提供.
这里要注意一个问题,在dlsym中加载的符号表示是和我们使用nm 库文件所看到符号表要保持一致,这里就有一个前面提到的 gcc和g++符号表的不同,一个 int foo(), 假设是g++编译,而且没有extern "C"导出接口,那么用dlsym加载的时候须要用 dlsym(handle, "_Z3foov") 方式才干够加载函数 int foo(), 所以建议所以的共享库对外接口都採用 extern "C"的方式导出 纯C接口对外使用,这样在使用上也会比較方便
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_GLOBAL这个选项,就能够把libA.so中的接口升级为全局可见, 这样在libB中就能够直接调用libA中的testA,假设在多个共享库都有同样的符号,而且有RTLD_GLOBAL选项,那么会优先选择第一个。
另外这里注意到一个问题, RTLD_GLOBAL使的动态库之间的对外接口是可见的,可是动态库是不能调用主程序中的全局符号,为了解决问题, gcc引入了一个參数-rdynamic,在编译加载共享库的可运行程序的时候最后在链接的时候加上-rdynamic,会把可运行文件里全部的符号变成全局可见,对于这个可运行程序而言,它加载的动态库在运行中能够直接调用主程序中的全局符号,并且假设共享库(自己或者另外的共享库 RTLD_GLOBAL) 加中有同名的符号,会选择可运行文件里使用的符号,这在一些情况下可能会带来一些莫名其妙的运行错误。
小提示:
- /usr/sbin/lsof -p pid 能够查看到由pid在执行期所加载的全部共享库
- 共享库不管是通过dlopen方式加载还是加载器加载,实质都是通过 mmap的方式把共享库映射到内存空间中去。mmap的參数MAP_DENYWRITE能够在改动已经被加载某个进程文件的时候阻止对于内存数据的改动,因为如今内核中已经禁用这个參数,直接导致的结果就是假设对mmap的文件进行改动,这个时候的改动会被直接反映到已经被mmap映射的空间上。因为内核的不支持,使得共享库不能在执行期进行热切换,共享库在更新的时候须要由加载的程序通过一些外部的方式来推断,主动使用dlclose,而且dlopen 又一次加载共享库,假设是加载器加载那么须要重新启动程序。另外这里的热切换指的是直接copy覆盖原有的共享库,假设是採用mv或者软连接的方式那么还是安全的,共享库被mv后不会影响原来的已经加载它的程序。
-
g++ 加上 -rdynamic 參数实质上相当于ld链接的时候加上-E或者--export-dynamic參数,效果与g++ -Wl,-E或者g++ -Wl,--export-dynamic的效果是一样的。
======================================================================================
静态库和动态库的混合编译
静态库与动态库的混合使用,常常会出现一些奇怪的错误,使用的时候须要有所关注
对于普通情况下,仅仅要静态库与共享库之间没有依赖关系,没有使用全局变量(包含static变量),不会出现太多的问题,以下以出现的问题作样例来说明使用的注意事项。