运行时报错“version `GLIBCXX_3.4.29‘ not found”底层原理分析

1、报错的现象

./main: ./1.2/lib.so: version `VERS_1.2' not found (required by ./main)

(1)在linux中运行一些工具时,可能会遇到工具报错“依赖某个版本的库找不到”;
(2)会报错是因为工具依赖某些动态库,但是当前的系统环境找不到这个动态库或者找不到合适版本的动态库;

2、为什么程序有的报找不到某个版本的动态库,有的报找不到动态库文件?

2.1、找不到动态库

./main: error while loading shared libraries: ./lib.so: cannot open shared object file: No such file or directory

找不到动态库:这个原因就不详细分析了,是很常见的动态库相关知识,简单来说就是程序依赖该动态库但是环境里没有这个动态库;

2.2、找不到某个版本的动态库

./main: ./lib.so: version `VERS_1.2' not found (required by ./main)

2.2.1、报错的原因

第一种:当前系统环境找不到该动态库;
第二种:在当前系统环境找到该动态库,但是动态库的版本和程序依赖的动态库版本不匹配;

2.2.2、动态库的版本是如何指定的,程序又是如何记录依赖的动态库版本?

(1)指定动态库版本:在编译动态库时,添加“符号版本脚本”文件来指定动态库的版本;
(2)程序记录依赖的动态库版本:在链接程序时,程序调用到动态库中的函数时,会将函数的版本号(也就是动态库的“符号版本脚本”指定的版本号)记录下来;

3、符号版本脚本文件

version {
	global:
		xx;
	local:
		*;
};

(1)version是一个字符串,代表符号集名称,当“找不到对应版本号”报错时显示的版本号就是这里指定的;
(2)大括号里记录动态库的符号信息;
(3)global是全局关键字:后面的符号表示是动态库的全局符号,也就是可以对外提供
(4)local是局部关键字:后面的符号表示是动态库的局部符号,只能在动态库内部使用;”*“是表示通配符,只要没在global中指定的,剩下的都是局部符号;
(5)符号版本脚本文件可以定义多个符号集,符号集之间还可以有包含关系;

4、动态库的命名规则

参考博客:《动态库的命名规则》;

5、动态库的兼容性

参考博客:《共享库的兼容性》;

6、用代码模拟共享库libtest.so从1.1版本升级到2.1版本

6.1、模拟的情况

(1)模拟程序依赖某个动态库,在编译程序时依赖低版本的动态库,后续在系统环境中更新了高版本的动态库,但是程序并没有依赖高版本的动态库重新编译过;
(2)高版本的动态库再接口上发生改变,不兼容低版本的动态库;
总结:比如linux中某些工具,拷贝到另一个linux系统中就可能报找不到某个"glibc"版本的错误,原因就是当前系统环境中的glibc版本比编译工具时的glibc版本要低;

6.2、libtest.so的1.1版本

6.2.1源文件libtest.c

#include 

int foo(int a, int b)
{
	printf("foo\n");
	return 0;
}

6.2.2、符号版本脚本文件libtest1_1.ver

VERS_1.1 {
	global:
		foo;
	local:
		*;
};

6.2.3、生成动态库的命令

gcc -shared -fPIC libtest.c -Xlinker --version-script libtest1_1.ver -o libtest.so

6.2.4、动态库的符号信息

0000000000201030 b __bss_start
0000000000201030 b completed.6355
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000000580 t deregister_tm_clones
00000000000005f0 t __do_global_dtors_aux
0000000000200de0 t __do_global_dtors_aux_fini_array_entry
0000000000200df0 d __dso_handle
0000000000200df8 d _DYNAMIC
0000000000201030 d _edata
0000000000201038 b _end
0000000000000688 t _fini
0000000000000665 T foo
0000000000000630 t frame_dummy
0000000000200dd8 t __frame_dummy_init_array_entry
0000000000000718 r __FRAME_END__
0000000000201000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000000518 t _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000200de8 d __JCR_END__
0000000000200de8 d __JCR_LIST__
                 w _Jv_RegisterClasses
                 U puts@@GLIBC_2.2.5
00000000000005b0 t register_tm_clones
0000000000201030 d __TMC_END__
0000000000000000 A VERS_1.1	#这里记录的共享库的版本信息

6.3、libtest.so的2.1版本

6.3.1、源文件libtest.c

#include 

char foo(char a, char b)
{
	printf("foo\n");
	return 0;
}

6.3.2、符号版本脚本文件libtest2_1.ver

VERS_2.1 {
	global:
		foo;
	local:
		*;
};

6.3.3、生成动态库的命令

# -Xlinker  把--version-script传递给连接器
# --version-script 说明要传递符号版本脚本
# libtest2_1.ver 符号版本脚本
gcc -shared -fPIC libtest.c -Xlinker --version-script libtest2_1.ver -o libtest.so

6.3.4、动态库的符号信息

0000000000201030 b __bss_start
0000000000201030 b completed.6355
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000000580 t deregister_tm_clones
00000000000005f0 t __do_global_dtors_aux
0000000000200de0 t __do_global_dtors_aux_fini_array_entry
0000000000200df0 d __dso_handle
0000000000200df8 d _DYNAMIC
0000000000201030 d _edata
0000000000201038 b _end
000000000000068c t _fini
0000000000000665 T foo
0000000000000630 t frame_dummy
0000000000200dd8 t __frame_dummy_init_array_entry
0000000000000718 r __FRAME_END__
0000000000201000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000000518 t _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000200de8 d __JCR_END__
0000000000200de8 d __JCR_LIST__
                 w _Jv_RegisterClasses
                 U puts@@GLIBC_2.2.5
00000000000005b0 t register_tm_clones
0000000000201030 d __TMC_END__
0000000000000000 A VERS_2.1 #这里记录的共享库的版本信息

6.4、libtest.so的1.1版本和2.1版本之间的差异

(1)两个版本都只包含foo函数对外调用,但是foo函数的返回值和传参类型都发生了变化;
(2)两个版本的libtest.so是不兼容的,如果程序编译时依赖的是libtest.so的2.1版本,但是在程序运行时在系统环境中找到的是1.1版本,此时程序运行就应该要报错;

7、调用foo函数

7.1、可执行程序源码

#include 

int main()
{
	foo();
	return 0;
}

7.2、编译指令(指定的是1.1版本的libtest.so)

gcc main.c ./libtest.so -o main

7.3、在当前路径下执行main

root@Storage:~# ./main
foo

7.4、把当前路径下1.1版本的的libtest.so替换成2.1版本

root@Storage:~# ./main
./main: ./lib.so: version `VERS_1.1' not found (required by ./main)	

7.5、为什么可执行程序main会去找"VERS_1.1"版本的libtest.so

7.5.1、可执行程序main的符号表

0000000000601034 B __bss_start
0000000000601034 b completed.6355
0000000000601030 D __data_start
0000000000601030 W data_start
0000000000400540 t deregister_tm_clones
00000000004005b0 t __do_global_dtors_aux
0000000000600e08 t __do_global_dtors_aux_fini_array_entry
00000000004006a8 R __dso_handle
0000000000600e18 d _DYNAMIC
0000000000601034 D _edata
0000000000601038 B _end
0000000000400694 T _fini
                 U foo@@VERS_1.1	# foo函数后面指定了共享库的版本
00000000004005d0 t frame_dummy
0000000000600e00 t __frame_dummy_init_array_entry
00000000004007d8 r __FRAME_END__
0000000000601000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
00000000004004a8 T _init
0000000000600e08 t __init_array_end
0000000000600e00 t __init_array_start
00000000004006a0 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000600e10 d __JCR_END__
0000000000600e10 d __JCR_LIST__
                 w _Jv_RegisterClasses
0000000000400690 T __libc_csu_fini
0000000000400620 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
00000000004005fd T main
0000000000400570 t register_tm_clones
0000000000400510 T _start
0000000000601038 D __TMC_END__

(1) 可以看到main函数中调用了foo函数,但是在符号表中foo函数是用“foo@@VERS_1.1”表示,记录了foo函数的版本;VERS_1.1就是在“符号版本脚本”中指定的符号集名称;
(2)如果在编译libtest.so时不指定“符号版本脚本”,使用默认的“符号版本脚本”编译出libtest.so,然后再编译main,可以看到main的符号表中,foo函数的符号是"foo",不会带版本的后缀;

7.5.2、运行时报错

main在运行时会去找“VERS_1.1”版本的foo函数,如果当前环境存在的是2.1版本的libtest.so,则只有“VERS_2.1”版本而没有“VERS_1.1”版本的foo函数,所以会报错;

8、实际工作/学习中动态库版本的维护

8.1、开源库/开源代码

(1)比较成熟的开源库都会有上面介绍的通过“符号版本脚本”来指定动态库的版本;
(2)所以我们在移植开源工具时,可能在自己的系统环境中运行时会报错,显示某个版本的动态库找不到,需要把动态库更新到对应的版本;

8.2、工作中维护的库

(1)在工作中实际维护的库在编译时,不会指定“符号版本脚本”,都是使用默认的规则,所以可执行程序的符号表里函数的符号也不会包含对应的版本号;
(2)但是在库的源码编译脚本里,会把库的svn版本和编译时间编译到源码里,并在初始化阶段打印出来,方便调试程序时指定当前运行库的版本;

使用“符号版本脚本”的优劣处

优势:从可执行程序的符号表就能知道依赖的某个函数所在动态库的版本号,并且在程序运行初期就报错,很容易定位问题;
劣势
a、使用“符号版本脚本”需要程序员比较了解底层原理,对程序员要求较高,库的维护成本增加;
b、库的维护人员需要时刻关注库的对外接口变化,要知道接口的哪些改变会导致库不兼容之前的版本,必须要提升库的版本号,修改“符号版本脚本”;

9、和C++语言对比

(1)“符号版本脚本”是C语言对没有符号可见范围的控制机制的一种补充;
(2)C++的函数重载机制和“符号版本脚本”的效果基本是一致的。C++源码编译出来的函数符号是带参数类型的,比如上面的foo函数,char型的foo函数在符号表里是foocc,int型的foo函数在符号表里是fooii;

你可能感兴趣的:(#,《程序员的自我修养》,编译原理,C语言)