linux共享库文件
时间:2010-12-19 来源:niuniu2006t
Linux支持共享库已经有悠久的历史了,不再是什么新概念了。大家都知道如何编译、连接以及动态加载(dlopen/dlsym/dlclose) 共享库。但是,可能很多人,甚至包括一些高手,对共享库相关的一些环境变量认识模糊。当然,不知道这些环境变量,也可以用共享库,但是,若知道它们,可能就会用得更好。下面介绍一些常用的环境变量,希望对家有所帮助:
LD_LIBRARY_PATH 这个环境变量是大家最为熟悉的,它告诉loader:在哪些目录中可以找到共享库。可以设置多个搜索目录,这些目录之间用冒号分隔开。在linux下,还提供了另外一种方式来完成同样的功能,你可以把这些目录加到/etc/ld.so.conf中,或则在/etc/ld.so.conf.d里创建一个文件,把目录加到这个文件里。当然,这是系统范围内全局有效的,而环境变量只对当前shell有效。按照惯例,除非你用上述方式指明,loader是不会在当前目录下去找共享库的,正如shell不会在当前目前找可执行文件一样。
LD_PRELOAD 这个环境变量对于程序员来说,也是特别有用的。它告诉loader:在解析函数地址时,优先使用LD_PRELOAD里指定的共享库中的函数。这为调试提供了方便,比如,对于C/C++程序来说,内存错误最难解决了。常见的做法就是重载malloc系列函数,但那样做要求重新编译程序,比较麻烦。使用LD_PRELOAD机制,就不用重新编译了,把包装函数库编译成共享库,并在LD_PRELOAD加入该共享库的名称,这些包装函数就会自动被调用了。在linux下,还提供了另外一种方式来完成同样的功能,你可以把要优先加载的共享库的文件名写在/etc/ld.so.preload里。当然,这是系统范围内全局有效的,而环境变量只对当前shell有效。
LD_ DEBUG 这个环境变量比较好玩,有时使用它,可以帮助你查找出一些共享库的疑难杂症(比如同名函数引起的问题)。同时,利用它,你也可以学到一些共享库加载过程的知识。它的参数如下:
libs display library search paths
reloc display relocation processing
files display progress for input file
symbols display symbol table processing
bindings display information about symbol binding
versions display version dependencies
all all previous options combined
statistics display relocation statistics
unused determined unused DSOs
help display this help message and exit
BIND_NOW 这个环境变量与dlopen中的flag的意义是一致,只是dlopen中的flag适用于显示加载的情况,而BIND_NOW/BIND_NOT适用于隐式加载。
LD_PROFILE/LD_PROFILE_OUTPUT:为指定的共享库产生profile数据,LD_PROFILE指定共享库的名称,LD_PROFILE_OUTPUT指定输出profile文件的位置,是一个目录,且必须存在,默认的目录为/var/tmp/或/var/profile。通过profile数据,你可以得到一些该共享库中函数的使用统计信息。
------------------------------------------------------------------------------------------
调用一些别人编写的动态链接库,但是一般都不提供源文件或.lib文件(有些比较抠门的第三方厂商就如此),就无法进行隐式连接
隐式连接(默认,无须声明)
通过在某些目录搜集共享目标的信息,让系统在程序启动时自动载入和连接共享库
显式链接 让程序自己通过库服务(libdl.so)来载入和连接到共享库 making the program load and link the shared objects thanks to some
用3个C++程序来演示这些服务,因为不严格地,C可以认为是C++的子集,出现的问题都是类似的
共享的库 定义了一个全局的C++类,在载入库时必须进行初始化,在卸载事必须销毁,这套子程序就叫做静态构造和析构器。共享库还提供了在C语言中的入口(在主程序中被调用), 因为C没有mangle(名称修饰)
试验1: 用ld建立共享库
1)在哪里寻找共享库,虽然 LD_LIBRAR_PATH据说是不被推荐使用的 $ export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:`pwd`
2)建立共享库 $ g++ -c base_shared.cc $ ld -shared base_shared.o -o libbase_shared.so
3)建立主程序 $ g++ base.cc -L`pwd` -lbase_shared
/usr/bin/ld: a.out: hidden symbol `__dso_handle’ in /usr/lib/gcc/i486-linux-gnu/4.3.2/crtbegin.o is referenced by DSO /usr/bin/ld: final link failed: Nonrepresentable section on output collect2: ld returned 1 exit status
因为引用了一个隐藏的符号__dso_handle,所以报错了
我们只好改用下面的共享库创建方式
$ g++ -shared base_shared.o -o libbase_shared.so
而不是像上面那样用 ld, 这个很重要
然后用下面的选项 编译 主程序
$ g++ base.cc -L`pwd` -lbase_shared
现在可以正常通过了,运行一下
$ ./a.oout
Constructor of toto Main entry point Shared lib entry point, toto’s var = 18 Destructor of toto
我们可以看到,主程序动态连接到共享库上了,并且 共享库的 静态 构造和解构器 会被自动调用
显示链接共享库
所谓显示,就是在主程序里面必须声明,Linux下是通过dlopen()来主动打开共享库
$ g++ -c base_shared.cc $ ld -shared base_shared.o -o libbase_shared.so $ g++ base1.cc -ldl $ ./a.out
Main entry point Loading shared lib… ./libbase_shared.so: undefined symbol: __dso_handle
运行时报错, 因为无法解析 __dso_handle
我们只好再次选择用gcc生成共享库,并修改编译选项
$ g++ -shared base_shared.o -o libbase_shared.so $ g++ base1.cc -l dl
再试,就好了
实际上,我们之需要编译共享库就好了,不需要重新编译主程序,因为主程序的编译是没有问题的,是运行的时候报错
可以看到,在调用dlopen的时候,共享库被自动载入了构造过程
结论: 无论我们是显式还是隐式调用 共享库,要用g++ -shared来生成,而不是用ld -shared. 因为静态构造器 可能会 用到
If your shared library contains global or static objects with constructors, then make sure to use gcc -shared, not ld, to create the shared library. This will make sure that any processor-specific magic needed to execute the constructors is included.
gcc/g++链接注意事项
-l参数就是用来指定程序要链接的库,-l参数紧接着就是库名 那么库名跟真正的库文件名有什么关系呢?就拿数学库来说,他的库名是m,他的库文件名是libm.so,很容易看出,把库文件名的头lib和尾.so去掉就是库名了 (用IDA反汇编出,也可以看到库名)
-L参数跟着的是库文件所在的目录名。不然,链接时会找不到库文件,除非你放到标准库目录(/lib,/usr/lib等)
PKG_CONFIG_PATH, 默认是 默认是/usr/lib/pkgconf pkg-config base-shared –libs –cflags
生成共享库的另外一种方式
gcc -shared test.c -o libtest.so
-fpic 使输出的对象模块是按照可重定位地址方式生成的。(GOT)
LIBRART_PATH
g++-3.4 char.cpp -L. -lsystem-linux-golden -llanguage-linux-golden
1)函数要声明为extern
extern C {
}
反而不行
2) LD_LIBRARY_PATH要设置正确 export LD_LIBRARY_PATH=.
--------------------------------------------------------------------------------------------
类似Windows系统中的动态链接库,Linux中也有相应的共享库用以支持代码的复用。Windows中为*.dll,而Linux中为*.so,我来详细的告诉你如何在linux下编写动态库,以及如何使用它. 在linux下编写动态链接库的步骤: 1. 编写库的头文件和源文件. 2. 把所有涉及到的源文件用如下方式编译为目标文件: g++/gcc -g -c -fPIC -o library1.o library1.cpp g++/gcc -g -c -fPIC -o library2.o library2.cpp ...... ...... (注释:-fPIC指通过这个选项来生成与位置无关的代码,可以在任何地址被连接和装载,-c指只编译而不连接原程序) 3. 把所有的目标文件链接为动态库: g++/gcc -g -shared -Wl,-soname,lib***.so -o lib***.so.1.0.0 library1.o library2.o .... -lc (注释:-lc选项,表示使用c语言库,一般都要用到) 4. 建立一个库名链接 ln -s lib***.so.1.0.0 lib***.so 现在你就可以引用库了.下面我分别给出简单例子告诉你如何动态和静态使用动态库: 假如你的应用程序源代码叫testlib.cpp 采用\如下方式编译: g++ -g -o testlib testlib.cpp -ldl (注释:-ldl选项,表示生成的对象模块需要使用共享库) ////////这个例子告诉你如何动态的调用.so库 testlib.cpp #include <dlfcn.h> #include <iostream.h> #include ... int main() { void *handle=NULL; //define a pointer which will point to the function in the lib you want to use. YourFuntionType (*pFunc)(YourFunctionPerameterList........); //open the lib you want to use. handle=dlopen("/../../../yourlib.so",RTLD_LAZY); if(handle==NULL) { cout<<"failed loading library!"<<endl; return -1; } dlerror(); //try to load the function in lib pFunc=(YourFuntionType(*)(YourFunctionPerameterList))dlsym(handle,"YourFuntionName"); if(dlerror()!=NULL) { cout<<"Loading function in lib error!"<<endl; return -1; } //now you can use the funtion like this (*pFunc)(YourFuntionPerameterList); return 0; } (注释:dlopen() 第一个参数:指定共享库的名称,将会在下面位置查找指定的共享库。 -环境变量LD_LIBRARY_PATH列出的用分号间隔的所有目录。 -文件/etc/ld.so.cache中找到的库的列表,用ldconfig维护。 -目录usr/lib。 -目录/lib。 -当前目录。(这里就是这种情况) 第二个参数:指定如何打开共享库。 -RTLD_NOW:将共享库中的所有函数加载到内存 -RTLD_LAZY: 会推后共享库中的函数的加载操作,直到调用dlsym()时方加载某函数 dlsym() 调用dlsym时,利用dlopen()返回的共享库的phandle以及函数名称作为参数,返回要加载函数的入口地址。 dlerror() 该函数用于检查调用共享库的相关函数出现的错误。 ) 特别需要注意的几点问题: 1. 当你想用c++写动态库的时候,记住千万别忘了在头文件里面加上如下内容,否则生成的库在动态调用的时候会出问题!!!!!!! #ifdef __cplusplus extern "C" { #endif .... .... #ifdef __cplusplus } #endif 2. 当你的库中包括与omniORB3相关的东西的时候,一定要在makefile中加上 -D__x86__ -D__OSVERSION=4 /////////////这个例子告诉你如何静态调用.so库 首先你得确保你的应用程序能够找到你的.so库,这可以有几种方法来实现. 方法一: 1.你可以把YourLib.so.1.0.0 和YourLib.so放到/usr/lib中,然后执行命令:ldconfig,这样你就可以在你的应用程序中直接调用你库中的函数了,当然你 得把库的头文件包含到你的应用程序中 2.编译你的应用程序 g++/gcc -g -o yourapp yourapp.cpp –lYourLib 方法二: 1.你也可以采用在系统中设置环境变量的办法来实现. 在root目录下: vi .bash_profile 然后添加LD_LIBRARY=/../YourDirIncludingYourLib 然后注消一次,环境变量就生效了,这样你就可以在你的应用程序中直接调用库中的函数了,同样你得有头文件. 2.编译你的应用程序 g++/gcc -g -o yourapp yourapp.cpp –lYourLib 方法三: 你可以直接采用在编译链接的时候告诉系统你的库在什么地方 g++/gcc -g -o yourapp yourapp.cpp -L/YourDirIncludingYourLib –lYourLib ///////////////////////////////// 假如你的库中有个函数:int eat(.....) 那么采用如下方式调用它 yourapp.cpp #include "YourLib.h" int main() { eat(); return 0; } 是不是很easy?对了在静态调用的时候好像不存在上面的"注意1"的问题,不过鉴于保险起见,最好还是按照标准的方式写c++头文件吧,这绝对是个好习惯. ---------------------------------------------------------------------------------------------------- 如果你想添加共享库支持到一个原来不包含共享库支持的 port 或是其它软件, 共享库的版本号应该遵循如下规则。通常来说,由此得出的数字与软件的发行版本无关。
建立共享库的三个原则是:
从1.0开始
如果改动与以前版本相兼容,增加副版本号(注意,ELF系统忽略副版本号)。
如果是个不兼容的改动,增加主版本号。
例如,添加函数和修正错误导致副版本号增加, 而删除函数、函数调用语法改变等,会迫使主版本号改变。
保持这种形式的版本号:主版本号.副版本号 (x.y)。 我们的 a.out 动态链接器不能很好的处理 x.y.z 形式的版本号。在比较共享库版本号以决定跟哪个库文件链接的时候, 任何y以后的版本号(那是指第三个数字) 总是会被忽略。 如果给定的两个共享库的不同在于“细微”版本 (“micro” revision)的话,ld.so将会与较高修订版本的链接。即,如果你要与 libfoo.so.3.3.3链接,链接器只在(ELF文件的)头部记录 3.3, 并且在连接时,与文件名以 libfoo.so.3.(任何数字 >= 3).(现有的最高数字) 开头的任何文件链接。
注意: ld.so 总是会使用 “副”版本号最高的。例如,即使一个程序最初是(被设定)与 libc.so.2.0链接的, ld.so也会优先选择使用libc.so.2.2,而不是 libc.so.2.0。
另外,我们的 ELF 动态链接器完全不处理副版本号。 可我们还是应该指定一个主版本号和副版本号,因为我们的 Makefile 会按系统类型“做正确的事”。
对于不属于某个 port 的库文件,我们的原则是在各个 FreeBSD 正式发行版 (RELEASE)之间只改变一次共享库版本号(译者注:一般只是副版本号)。 并且,在 FreeBSD 正式发行版 (RELEASE) 主版本之间(那是指像从 3.x 到 4.x), 也应该仅改变一次共享库主版本号。 当你需要对系统库做一些改变并要增加版本号时, 请查看 Makefile的提交日志。 这是 committer 的责任:确保自(最近的)正式发行版 (RELEASE) 之后只有第一次这样的改动会让在 Makefile 里的共享库版本号更新, 而随后的(在下一个 RELEASE 之前的)改动不会使共享库版本号更新。
隐式连接(默认,无须声明)
通过在某些目录搜集共享目标的信息,让系统在程序启动时自动载入和连接共享库
显式链接 让程序自己通过库服务(libdl.so)来载入和连接到共享库 making the program load and link the shared objects thanks to some
用3个C++程序来演示这些服务,因为不严格地,C可以认为是C++的子集,出现的问题都是类似的
共享的库 定义了一个全局的C++类,在载入库时必须进行初始化,在卸载事必须销毁,这套子程序就叫做静态构造和析构器。共享库还提供了在C语言中的入口(在主程序中被调用), 因为C没有mangle(名称修饰)
试验1: 用ld建立共享库
1)在哪里寻找共享库,虽然 LD_LIBRAR_PATH据说是不被推荐使用的 $ export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:`pwd`
2)建立共享库 $ g++ -c base_shared.cc $ ld -shared base_shared.o -o libbase_shared.so
3)建立主程序 $ g++ base.cc -L`pwd` -lbase_shared
/usr/bin/ld: a.out: hidden symbol `__dso_handle’ in /usr/lib/gcc/i486-linux-gnu/4.3.2/crtbegin.o is referenced by DSO /usr/bin/ld: final link failed: Nonrepresentable section on output collect2: ld returned 1 exit status
因为引用了一个隐藏的符号__dso_handle,所以报错了
我们只好改用下面的共享库创建方式
$ g++ -shared base_shared.o -o libbase_shared.so
而不是像上面那样用 ld, 这个很重要
然后用下面的选项 编译 主程序
$ g++ base.cc -L`pwd` -lbase_shared
现在可以正常通过了,运行一下
$ ./a.oout
Constructor of toto Main entry point Shared lib entry point, toto’s var = 18 Destructor of toto
我们可以看到,主程序动态连接到共享库上了,并且 共享库的 静态 构造和解构器 会被自动调用
显示链接共享库
所谓显示,就是在主程序里面必须声明,Linux下是通过dlopen()来主动打开共享库
$ g++ -c base_shared.cc $ ld -shared base_shared.o -o libbase_shared.so $ g++ base1.cc -ldl $ ./a.out
Main entry point Loading shared lib… ./libbase_shared.so: undefined symbol: __dso_handle
运行时报错, 因为无法解析 __dso_handle
我们只好再次选择用gcc生成共享库,并修改编译选项
$ g++ -shared base_shared.o -o libbase_shared.so $ g++ base1.cc -l dl
再试,就好了
实际上,我们之需要编译共享库就好了,不需要重新编译主程序,因为主程序的编译是没有问题的,是运行的时候报错
可以看到,在调用dlopen的时候,共享库被自动载入了构造过程
结论: 无论我们是显式还是隐式调用 共享库,要用g++ -shared来生成,而不是用ld -shared. 因为静态构造器 可能会 用到
If your shared library contains global or static objects with constructors, then make sure to use gcc -shared, not ld, to create the shared library. This will make sure that any processor-specific magic needed to execute the constructors is included.
gcc/g++链接注意事项
-l参数就是用来指定程序要链接的库,-l参数紧接着就是库名 那么库名跟真正的库文件名有什么关系呢?就拿数学库来说,他的库名是m,他的库文件名是libm.so,很容易看出,把库文件名的头lib和尾.so去掉就是库名了 (用IDA反汇编出,也可以看到库名)
-L参数跟着的是库文件所在的目录名。不然,链接时会找不到库文件,除非你放到标准库目录(/lib,/usr/lib等)
PKG_CONFIG_PATH, 默认是 默认是/usr/lib/pkgconf pkg-config base-shared –libs –cflags
生成共享库的另外一种方式
gcc -shared test.c -o libtest.so
-fpic 使输出的对象模块是按照可重定位地址方式生成的。(GOT)
LIBRART_PATH
g++-3.4 char.cpp -L. -lsystem-linux-golden -llanguage-linux-golden
1)函数要声明为extern
extern C {
}
反而不行
2) LD_LIBRARY_PATH要设置正确 export LD_LIBRARY_PATH=.
--------------------------------------------------------------------------------------------
类似Windows系统中的动态链接库,Linux中也有相应的共享库用以支持代码的复用。Windows中为*.dll,而Linux中为*.so,我来详细的告诉你如何在linux下编写动态库,以及如何使用它. 在linux下编写动态链接库的步骤: 1. 编写库的头文件和源文件. 2. 把所有涉及到的源文件用如下方式编译为目标文件: g++/gcc -g -c -fPIC -o library1.o library1.cpp g++/gcc -g -c -fPIC -o library2.o library2.cpp ...... ...... (注释:-fPIC指通过这个选项来生成与位置无关的代码,可以在任何地址被连接和装载,-c指只编译而不连接原程序) 3. 把所有的目标文件链接为动态库: g++/gcc -g -shared -Wl,-soname,lib***.so -o lib***.so.1.0.0 library1.o library2.o .... -lc (注释:-lc选项,表示使用c语言库,一般都要用到) 4. 建立一个库名链接 ln -s lib***.so.1.0.0 lib***.so 现在你就可以引用库了.下面我分别给出简单例子告诉你如何动态和静态使用动态库: 假如你的应用程序源代码叫testlib.cpp 采用\如下方式编译: g++ -g -o testlib testlib.cpp -ldl (注释:-ldl选项,表示生成的对象模块需要使用共享库) ////////这个例子告诉你如何动态的调用.so库 testlib.cpp #include <dlfcn.h> #include <iostream.h> #include ... int main() { void *handle=NULL; //define a pointer which will point to the function in the lib you want to use. YourFuntionType (*pFunc)(YourFunctionPerameterList........); //open the lib you want to use. handle=dlopen("/../../../yourlib.so",RTLD_LAZY); if(handle==NULL) { cout<<"failed loading library!"<<endl; return -1; } dlerror(); //try to load the function in lib pFunc=(YourFuntionType(*)(YourFunctionPerameterList))dlsym(handle,"YourFuntionName"); if(dlerror()!=NULL) { cout<<"Loading function in lib error!"<<endl; return -1; } //now you can use the funtion like this (*pFunc)(YourFuntionPerameterList); return 0; } (注释:dlopen() 第一个参数:指定共享库的名称,将会在下面位置查找指定的共享库。 -环境变量LD_LIBRARY_PATH列出的用分号间隔的所有目录。 -文件/etc/ld.so.cache中找到的库的列表,用ldconfig维护。 -目录usr/lib。 -目录/lib。 -当前目录。(这里就是这种情况) 第二个参数:指定如何打开共享库。 -RTLD_NOW:将共享库中的所有函数加载到内存 -RTLD_LAZY: 会推后共享库中的函数的加载操作,直到调用dlsym()时方加载某函数 dlsym() 调用dlsym时,利用dlopen()返回的共享库的phandle以及函数名称作为参数,返回要加载函数的入口地址。 dlerror() 该函数用于检查调用共享库的相关函数出现的错误。 ) 特别需要注意的几点问题: 1. 当你想用c++写动态库的时候,记住千万别忘了在头文件里面加上如下内容,否则生成的库在动态调用的时候会出问题!!!!!!! #ifdef __cplusplus extern "C" { #endif .... .... #ifdef __cplusplus } #endif 2. 当你的库中包括与omniORB3相关的东西的时候,一定要在makefile中加上 -D__x86__ -D__OSVERSION=4 /////////////这个例子告诉你如何静态调用.so库 首先你得确保你的应用程序能够找到你的.so库,这可以有几种方法来实现. 方法一: 1.你可以把YourLib.so.1.0.0 和YourLib.so放到/usr/lib中,然后执行命令:ldconfig,这样你就可以在你的应用程序中直接调用你库中的函数了,当然你 得把库的头文件包含到你的应用程序中 2.编译你的应用程序 g++/gcc -g -o yourapp yourapp.cpp –lYourLib 方法二: 1.你也可以采用在系统中设置环境变量的办法来实现. 在root目录下: vi .bash_profile 然后添加LD_LIBRARY=/../YourDirIncludingYourLib 然后注消一次,环境变量就生效了,这样你就可以在你的应用程序中直接调用库中的函数了,同样你得有头文件. 2.编译你的应用程序 g++/gcc -g -o yourapp yourapp.cpp –lYourLib 方法三: 你可以直接采用在编译链接的时候告诉系统你的库在什么地方 g++/gcc -g -o yourapp yourapp.cpp -L/YourDirIncludingYourLib –lYourLib ///////////////////////////////// 假如你的库中有个函数:int eat(.....) 那么采用如下方式调用它 yourapp.cpp #include "YourLib.h" int main() { eat(); return 0; } 是不是很easy?对了在静态调用的时候好像不存在上面的"注意1"的问题,不过鉴于保险起见,最好还是按照标准的方式写c++头文件吧,这绝对是个好习惯. ---------------------------------------------------------------------------------------------------- 如果你想添加共享库支持到一个原来不包含共享库支持的 port 或是其它软件, 共享库的版本号应该遵循如下规则。通常来说,由此得出的数字与软件的发行版本无关。
建立共享库的三个原则是:
从1.0开始
如果改动与以前版本相兼容,增加副版本号(注意,ELF系统忽略副版本号)。
如果是个不兼容的改动,增加主版本号。
例如,添加函数和修正错误导致副版本号增加, 而删除函数、函数调用语法改变等,会迫使主版本号改变。
保持这种形式的版本号:主版本号.副版本号 (x.y)。 我们的 a.out 动态链接器不能很好的处理 x.y.z 形式的版本号。在比较共享库版本号以决定跟哪个库文件链接的时候, 任何y以后的版本号(那是指第三个数字) 总是会被忽略。 如果给定的两个共享库的不同在于“细微”版本 (“micro” revision)的话,ld.so将会与较高修订版本的链接。即,如果你要与 libfoo.so.3.3.3链接,链接器只在(ELF文件的)头部记录 3.3, 并且在连接时,与文件名以 libfoo.so.3.(任何数字 >= 3).(现有的最高数字) 开头的任何文件链接。
注意: ld.so 总是会使用 “副”版本号最高的。例如,即使一个程序最初是(被设定)与 libc.so.2.0链接的, ld.so也会优先选择使用libc.so.2.2,而不是 libc.so.2.0。
另外,我们的 ELF 动态链接器完全不处理副版本号。 可我们还是应该指定一个主版本号和副版本号,因为我们的 Makefile 会按系统类型“做正确的事”。
对于不属于某个 port 的库文件,我们的原则是在各个 FreeBSD 正式发行版 (RELEASE)之间只改变一次共享库版本号(译者注:一般只是副版本号)。 并且,在 FreeBSD 正式发行版 (RELEASE) 主版本之间(那是指像从 3.x 到 4.x), 也应该仅改变一次共享库主版本号。 当你需要对系统库做一些改变并要增加版本号时, 请查看 Makefile的提交日志。 这是 committer 的责任:确保自(最近的)正式发行版 (RELEASE) 之后只有第一次这样的改动会让在 Makefile 里的共享库版本号更新, 而随后的(在下一个 RELEASE 之前的)改动不会使共享库版本号更新。
相关阅读 更多 +