linux内核接口——Linux二进制兼容性问题


Linux上二进制有一个显著的特点就是可移植性不强。我们在不同的发行版之间,不同的内核版本之间,程序往往是不能通用的。

硬件平台

众所周知的,硬件平台不一样,指令集不一样,二进制就几乎没有可移植的能力。

ABI

在架构上,必须要区分x86x64两种架构,一般的x64的机器都能运行x86的程序,但是如果你把程序编译为x86,你就得面对大量的x64服务器的性能瓶颈。这个架构上的区别在任何的平台上都一样的,并不是linux的特有问题。

ABI的不同则是linux内核和glibc的升级导致的规范变化导致的。不同的ABI程序和库在不同的环境下很高概率是不能运行的,除非是低版本最原始的ABI在现代系统上跑一般都可以向下兼容。而这种不兼容主要发生在C++身上,因为C++近几年的特性改变速度相对较快,管理困难。X86上常见的elf ABI有:

OS/ABI:                            UNIX - Linux

OS/ABI:                            UNIX - System V

OS/ABI:                            UNIX - GNU

其中GNULinux两种是相同的,只是使用不同版本的readelf会现实不同的结果。而system v则是最古老的,也是兼容性最好的。有的老一些的系统上只识别system vABI。但是system v ABI for x86_64却是比linux还要先进的ABI。因为这个ABI把大部分的参数转由寄存器传递,而不是由栈传递,对栈的使用减少就增加了以往的缓存溢出的难度。还有x32上的return to libc等攻击的手法也得变通,难度提高。


大小端

一个elf支持被编译成各种大小端,我们不能用编译成大端的elf在小端的内核上执行。但是考虑到intelCPU全部是小端,所以我们在intel平台上编译部署不需要过多的考虑这个问题

内核版本

Gcc在编译的时候可以使用--enable-kernel指定最低支持的内核版本,这个选项会在elf头部添加.note.ABI-tag,如果你用readelf读取头部,会发现:

 

这里面就是规定的最低支持的内核版本,运行时会直接检查这个与当前内核版本之间的区别,不满足就会FATAL: kernel too old。这种不兼容你也可以通过file命令发现,这个命令会输出二进制的最小兼容的版本。有的库也携带着这个限制,有的则没有,你的进程所依赖的任何一个库满足了这个限制都会导致执行不成功。

 

Gcc倾向于动态编译和动态加载,golang就是吐槽这一点的生动代表。但是动态编译携带库确实能减小大工程结果文件大小,而且这是linux系统的传统价值观。所以在很多地方还是有必要性的。

而各个系统所有的库的版本不一样,很多库的调用名的symbol都会在后面追加版本号,如果版本号不匹配,库则不能通用,例如我们经常见到libc.so.6: version `GLIBC_2.14' not found这类的打印。strings /lib64/libc.so.6 |grep GLIBC_  通过这个命令可以排查,大部分的库问题根源都在libc,但是不是绝对的。libgcc_s.so.1GCC的组件,编译时候运行时候都需要,一个版本GCC编译的程序常常不能在装有另一个版本GCC的平台上运行,就是这个原因,所以从高到低版本的迁移需要带着它。libc.so.6是最底层的库,操作系统和其中所有应用程序几乎都依赖,是应用程序能够跟操作系统通信的基础。原本UNIX中的libcGNU开发的第三方版本glibc,像这里的名字虽然是libc,但事实上就是glibc,功能没有太大差别。libm.so.6则是对libc里面的数学部分优化后的版本。

另外一个需要注意的地方是连接的时候不需要指定后面的版本号,因为系统一般会建立到这个版本号的软链接,现代的gcc即使在连接的时候不指定版本号,并且不存在软连接,他也能正确的找到完整名字的库文件,实际的链接的是带版本号的库,所以即使你在编译的时候使用了不带库版本号的动态库,使用时还是需要携带带版本号的库文件。

编译器

Gcc5.1版本的编译器会在编译时做大量激进的优化,但是有的优化是只对于最新的CPU特性有效,老一些的CPU在硬件层面就不支持这些优化,所以如此编译的程序就有兼容性问题。方法是用更老的编译器或者是用5.2之后解决了这个问题的更新的编译器。

 

 

你可能感兴趣的:(linux)