您的位置:首页 > 其它

nice命令兼容性分析实例

2016-03-19 17:34 218 查看

背景

产品实验室出现一例日志转储问题,经定位发现当前版本没有提供nice命令,而cron拉起定时任务时,却调用了nice命令,对定时任务做优先级调整。

毫无疑问后续版本需要提供nice命令,但是能否在之前的版本升级到后续版本时,将该nice命令copy一份到升级前的版本里,先做日志转储,再升级呢?

Linux 兼容性

当前版本的nice命令,能否在低版本上正常运行,这属于二进制兼容性的范畴。

Linux领域有个二进制规范标准,称为LSB(Linux Standards Base),这个组织专门制订Linux二进制接口的标准(当前最新发布标准为LSB 5.0),任何影响二进制接口变化的因素,她都需要考虑。

当然LSB支持的二进制兼容性是向后兼容(backwards compatibility),这是什么意思呢?简单地说就是:

在低版本Linux编译的二进制,可以直接运行在高版本上

在高版本Linux编译的二进制,不能保证一定可以运行在低版本上

事实上,我们遇到的兼容性问题都是第2类,而非第1类。在这种场景上(特别是开源软件),从技术上是无法做保证100%兼容的。

为什么会有LSB,大家想过吗?只要API兼容,大不了编译一把出一个新的二进制,不就可以解决这个二进制兼容性问题了么?

那是因为Linux发行版非常多(几百种不在话下,著名者如Redhat,SUSE和Ubuntu),站在软件供应商的角度试想一下,比如Linux防火墙(Checkpoint)软件供应商,他们就需要在每个发行版上发布一个二进制软件,而且同一发行版不同版本(比如Ubuntu 12.4和14.4)都得发布不同的二进制软件,对谁来说都听吃不消。

二进制兼容,在业界其实算不上什么新鲜的事情,各种场景下根本无法重编和重出版本(因为老版本压根就没有这个东西,重出版本解决不了升级场景,因为他影响不了老版本)

LSB就是来解决这个事情的,只需发布一个二进制程序,就能通吃所有的发布版。

nice命令依赖分析

前面提及,LSB或者一般软件,只能保证向后兼容,无法保证向前兼容。现在遇到的问题是有限版本的向前兼容问题,只需要正向分析一下以前所有版本是否OK就可以了。

当前所有历史版本,kernel和glibc的版本号是没有发生变化的,他们遵守的LSB版本也是相同的,从这个角度来说,应该是兼容的。除非nice依赖glibc/kernel二进制中的新增接口,造成新软件老法在老版本上运行。

nice命令库依赖分析

通过ldd命令可一目了然知道nice命令依赖那些运行库:

$ ldd /usr/bin/nice
linux-vdso.so.1 => (0x00007ff5761000)
libc.so.6 => /lib64/libc.so.6 (0x00007fd4c2268000)
/lib64/ld-linux-x86-64.so.2(0x00007fd4c25c6000)


咋一看nice依赖3个运行库,分别是 linux-vdso, libc以及ld-linux。

linux-vdso.so.1是Linux kernel提供的虚拟库,不是nice二进制明确依赖的。

libc.so.6正是 glibc运行库,nice依赖于它

ld-linux-x86-64.so.2是86-64 Linux的动态链接器

综上,只需之前所有版本都提供libc.so.6和ld-linux-x86-64.so.2这两个库,并且SONAME保持一致即可。

经分析,满足。

nice命令函数依赖分析

通过readelf可查看nice命令所调用的外部定义函数和变量:

$ readelf -s /usr/bin/nice | grep UND | grep -v NOTYPE
1:0000000000000000     0 FUNC    GLOBAL DEFAULT  UND memset@GLIBC_2.2.5 (2)
2:0000000000000000     0 FUNC    GLOBAL DEFAULT UND mbrtowc@GLIBC_2.2.5 (2)
3:0000000000000000     0 FUNC    GLOBAL DEFAULT UND abort@GLIBC_2.2.5 (2)
4:0000000000000000     0 FUNC    GLOBAL DEFAULT UND __fprintf_chk@GLIBC_2.3.4 (3)
6:0000000000000000     0 FUNC    GLOBAL DEFAULT UND textdomain@GLIBC_2.2.5 (2)
7:0000000000000000     0 FUNC    GLOBAL DEFAULT UND execvp@GLIBC_2.2.5 (2)
8:0000000000000000     0 FUNC    GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
9:0000000000000000     0 FUNC    GLOBAL DEFAULT UND __assert_fail@GLIBC_2.2.5 (2)
10:0000000000000000    0 FUNC    GLOBAL DEFAULT UND __printf_chk@GLIBC_2.3.4 (3)
11:0000000000000000    0 FUNC    GLOBAL DEFAULT UND bindtextdomain@GLIBC_2.2.5 (2)
12:0000000000000000    0 FUNC    GLOBAL DEFAULT UND malloc@GLIBC_2.2.5 (2)
13:0000000000000000    0 FUNC    GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
14:0000000000000000    0 FUNC    GLOBAL DEFAULT UND getpriority@GLIBC_2.2.5 (2)
15:0000000000000000    0 FUNC    GLOBAL DEFAULT UND _exit@GLIBC_2.2.5 (2)
16:0000000000000000    0 FUNC    GLOBAL DEFAULT UND __cxa_atexit@GLIBC_2.2.5 (2)
17:0000000000000000    0 FUNC    GLOBAL DEFAULT UND free@GLIBC_2.2.5 (2)
18:0000000000000000    0 FUNC    GLOBAL DEFAULT UND strlen@GLIBC_2.2.5 (2)
19:0000000000000000    0 FUNC    GLOBAL DEFAULT UND opterr@GLIBC_2.2.5 (2)
20:0000000000000000    0 FUNC    GLOBAL DEFAULT UND __ctype_get_mb_cur_max@GLIBC_2.2.5 (2)
21:0000000000000000    0 FUNC    GLOBAL DEFAULT UND __vfprintf_chk@GLIBC_2.3.4 (3)
22:0000000000000000    0 FUNC    GLOBAL DEFAULT UND __ctpe_b_loc@GLIBC_2.3.4 (3)
23:0000000000000000    0 FUNC    GLOBAL DEFAULT UND fputs_unlocked@GLIBC_2.2.5 (2)
24:0000000000000000    0 FUNC    GLOBAL DEFAULT UND setpriority@GLIBC_2.2.5 (2)
25:0000000000000000    0 FUNC    GLOBAL DEFAULT UND __fpending@GLIBC_2.2.5 (2)
26:0000000000000000    0 FUNC    GLOBAL DEFAULT UND strtol@GLIBC_2.2.5 (2)
27:0000000000000000    0 FUNC    GLOBAL DEFAULT UND memcpy@GLIBC_2.2.5 (2)
28:0000000000000000    0 FUNC    GLOBAL DEFAULT UND strchr@GLIBC_2.2.5 (2)
29:0000000000000000    0 FUNC    GLOBAL DEFAULT UND getopt_long@GLIBC_2.2.5 (2)
30:0000000000000000    0 FUNC    GLOBAL DEFAULT UND mbsinit@GLIBC_2.2.5 (2)
31:0000000000000000    0 FUNC    GLOBAL DEFAULT UND __errno_location@GLIBC_2.2.5 (2)
33:0000000000000000    0 FUNC    GLOBAL DEFAULT UND calloc@GLIBC_2.2.5 (2)
34:0000000000000000    0 FUNC    GLOBAL DEFAULT UND fclose@GLIBC_2.2.5 (2)
35:0000000000000000    0 FUNC    GLOBAL DEFAULT UND realloc@GLIBC_2.2.5 (2)
36:0000000000000000    0 FUNC    GLOBAL DEFAULT UND setlocale@GLIBC_2.2.5 (2)
37:0000000000000000    0 FUNC    GLOBAL DEFAULT UND error@GLIBC_2.2.5 (2)
38:0000000000000000    0 FUNC    GLOBAL DEFAULT UND iswprint@GLIBC_2.2.5 (2)


从上面可以看到,nice命令调用glibc的函数并不多,都是一些很常用的函数,并且这些函数都是POSIX标准函数。

如果你是位眼尖的读者,会注意到上述每个glibc函数符号后面都带一个版本号信息(比如error@GLIBC_2.2.5)。没错,可能你已经猜到了,glibc正是采用符号版本机制实现向后兼容的。

举个例子吧(完全是举例,软件版本编号不真实,仅仅是打个比方而已)

LSB 3.0标准定义了posix_spawn函数,而2.2.5版本的glibc源代码实现该规范中定义的posix_spawn函数,并且将posix)spawn函数打上版本@GLIBC_2.2.5信息。

过了几年之后,LSB规范发布到了5.0版本,该标准更改了posix_spawn函数的语义(比如是默认行为改变了),那么LSB也必须保证自身是向后兼容的,那么它规定支持LSB 5.0规范的glibc软件,必然要提供两个版本的posix_spawn函数,该函数的版本号分别是@GLIBC_2.2.5和 @GLIBC_2.14,而规定该版本的posix_spawn@GLIBC_2.2.5函数必须与2.2.5版本glibc中的posix_spawn@GLIBC_2.2.5功能和语义完全一致。

LSB标准规定glibc两个不同源代码的glibc版本生成相同的符号版本信息,来保证二进制的兼容性。

各版本的运行环境分析

还是回到最初的问题,新版本的nice命令能否在之前的所有旧版本上运行呢?

根据前面分析,只需要在运行环境上分析 glibc运行库的SONAME是否保持不变,再看nice依赖的那些函数和变量(算上版本号)在当目标glibc上能否找到。

如果上述问题的答案都是Yes,那么结果就是兼容的。

$ readelf -d /lib64/libc.so.6
Dynamic section at offset 0x157b60 contains 26 entries:
Tag                Type              Name/Value
0x0000000000000001 (NEED)            Shared library: [ld-linux-x86-64.so.2]
0x000000000000000e (SONAME)          Library soname: [libc.so.6]   # 这个项是该库的SONAME


libc的SONAME是完全一样的。

然后通过readelf -s /lib64/libc.so.6命令输出的符号列表,一个个符号进行对照,发现也是能对得上的。

经过分析,是可以兼容的。那么人工分析是否有遗漏呢?我们把nice命令拷贝到最早的版本上运行,是可以的。这一点更佐证了之前的分析,更有底气说是兼容的了。

把Ubuntu的nice命令拷贝到设备上运行

即然nice命令调用的函数比较常见,所以Ubuntu 12.4上的nice拷贝到设备上运行,应该也是没有问题的。

于是拷贝过来并运行,发现有一个错误提示信息:

$ ./nice
./nice: /lib64/libc.so.6: version 'GLIBC_2.14' not found (reqiured by ./nice)


噢,报错耶,出乎意料之外。看出错报告,发现Ubuntu版本nice命令依赖了一个XXX@GLIBC_2.14符号。于是乎使用readelf命令查找到底是哪个符号出了问题:

readelf -s ./nice | grep GLIBC_2.14
29:000000000000000    0 FUNC    GLOBAL DEFAULT UND memcpy@GLIBC_2.14


天呐,原来是memcpy函数。奇怪了,memcpy函数自打C89标准以来,不是雷打不动的吗?怎么出了一个新版本函数呢?

好吧,分析一个设备上libc的memcpy函数版本信息:

# readelf -s /lib64/libc.so.6 | grep -w memcpy
1087:0000000000081a30    1125 FUNC    GLOBAL DEFAULT    13 memcpy@GLIBC_2.2.5
4480:0000000000081a30    1125 FUNC    GLOBAL DEFAULT    13 memcpy


原来设备上的glibc只支持到memcpy@GLIBC_2.2.5版本的memcpy函数,而Ubuntu上的memcpy函数最新版本是memcpy@GLIBC_2.14。memcpy不是一个相对简单的函数吗?POSIX对它的语义重新做了定义,从而有两个版本的memcpy。

带着疑问,将glibc源码翻了个朝天,把glibc-2.18版本源代码所有memcpy的定义都看了一遍,还真是别有洞天:

compat_symbol(libc, memmove, memcpy, GLIBC_2_2_5);


原来memcpy@GLIBC_2.2.5函数语义跟memmove完全是一样的,跟我们理解中的memcpy并不一样。

然后找到了memcpy@GLIBC_2.14版本的定义如下:

versioned_symbol(libc, __new_memcpy, memcpy, GLIBC_2_14)


深入分析__new_memcpy函数内部实现,发现它利用了X86的SSE单指令多数据功能做拷贝优化,提升速度。当然这不是最重要的,最重要的是memcpy@GLIBC_2.14与我们心中所理解的那个memcpy在语义上终于一致了。

从这里就可以推导出,LSB标准定义的memcpy二进制接口语义发生了变化,从而定义两个版本的memcpy函数。Ubuntu上的nice无法在设备上运行这个例子更进一步说明了:

Linux通过LSB标准,实现二进制向后兼容,依然无法实现前向兼容
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: