关于elf动态库的几篇文章

来源:互联网 发布:栅格数据重采样 编辑:程序博客网 时间:2024/04/30 11:14

Ian Lance Taylor正在写连载文章介绍linkers:

http://www.cs.virginia.edu/~wh5a/blog/i386%20Linux%E4%B8%8B%20ELF%20%E5%8A%A8%E6%80%81%E9%93%BE%E6%8E%A5%E5%88%86%E6%9E%90%20%EF%BC%88%E4%B8%80%EF%BC%89.html

Intel平台下Linux中ELF文件动态链接的加载、解析及实例分析(一): 加载:

http://www.ibm.com/developerworks/cn/linux/l-elf/part1/index.html

ELF的GOT和PLT以及PIC

http://blog.csdn.net/kendyhj9999/article/details/7008637

动态符号链接细节

http://www.360doc.com/content/12/0627/23/7775902_220867578.shtml

linux动态库剖析

http://www.360doc.com/content/10/1231/10/1378815_82852933.shtml

linux下文件的类型是不依靠于其后缀名的,但一般来讲: 

  .o,是目标文件,相当于windows中的.obj文件 

  .so 为共享库,是shared object,用于动态连接的,和dll差未几 

  .a为静态库,是好多个.o合在一起,用于静态连接 

  .la为libtool自动天生的一些共享库,vi编辑查看,主要记录了一些配置信息。可以用如下命令查看*.la文件的格式 $file *.la 

  *.la: ASCII English text 

  所以可以用vi来查看其内容。 

  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 

  创建.a库文件和.o库文件: 

  [yufei@localhost perl_c2]$ pwd 

  /home/yufei/perl_c2 

  [yufei@localhost perl_c2]$ cat mylib.c 

  #include 

  #include 

  void hello(){ 

  printf("success call from perl to c library\n"); 

  } 

  [yufei@localhost perl_c2]$ cat mylib.h 

  extern void hello(); 

  [yufei@localhost perl_c2]$ gcc -c mylib.c 

  [yufei@localhost perl_c2]$ dir 

  mylib.c mylib.h mylib.o 

  [yufei@localhost perl_c2]$ ar -r mylib.a mylib.o 

  ar: 正在创建 mylib.a 

  [yufei@localhost perl_c2]$ dir 

  mylib.a mylib.c mylib.h mylib.o 

  @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 

   

  动态链接库*.so的编译与使用- - 

  动态库*.so在linux下用c和c 编程时经常会碰到,最近在网站找了几篇文章先容动态库的编译和链接,总算搞懂了这个之前一直不太了解得东东,这里做个笔记,也为其它正为动态库链接库而苦恼的兄弟们提供一点帮助。 

  1、动态库的编译 

  下面通过一个例子来先容如何天生一个动态库。这里有一个头文件:so_test.h,三个.c文件:test_a.c、test_b.c、test_c.c,我们将这几个文件编译成一个动态库:libtest.so。 

  so_test.h: 

  #include 

  #include 

  void test_a(); 

  void test_b(); 

  void test_c(); 

  test_a.c: 

  #include "so_test.h" 

  void test_a() 

  { 

  printf("this is in test_a...\n"); 

  } 

  test_b.c: 

  #include "so_test.h" 

  void test_b() 

  { 

  printf("this is in test_b...\n"); 

  } 

  test_c.c: 

  #include "so_test.h" 

  void test_c() 

  { 

  printf("this is in test_c...\n"); 

  } 

  将这几个文件编译成一个动态库:libtest.so 

  $ gcc test_a.c test_b.c test_c.c -fPIC -shared -o libtest.so 

  2、动态库的链接 

  在1、中,我们已经成功天生了一个自己的动态链接库libtest.so,下?**颐峭ü桓龀绦蚶吹饔谜飧隹饫锏暮3绦虻脑次募簍est.c。 

  test.c: 

  #include "so_test.h" 

  int main() 

  { 

  test_a(); 

  test_b(); 

  test_c(); 

  return 0; 

  } 

  l 将test.c与动态库libtest.so链接天生执行文件test: 

  $ gcc test.c -L. -ltest -o test 

  l 测试是否动态连接,假如列出libtest.so,那么应该是连接正常了 

  $ ldd test 

  l 执行test,可以看到它是如何调用动态库中的函数的。 

  3、编译参数解析 

  最主要的是GCC命令行的一个选项: 

  -shared 该选项指定天生动态连接库(让连接器天生T类型的导出符号表,有时候也天生弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件 

  l -fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载进时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。 

  l -L.:表示要连接的库在当前目录中 

  l -ltest:编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.so来确定库的名称 

  l LD_LIBRARY_PATH:这个环境变量指示动态连接器可以装载动态库的路径。 

  l 当然假如有root权限的话,可以修改/etc/ld.so.conf文件,然后调用 /sbin/ldconfig来达到同样的目的,不过假如没有root权限,那么只能采用输出LD_LIBRARY_PATH的方法了。 

  4、留意 

  调用动态库的时候有几个题目会经常碰到,有时,明明已经将库的头文件所在目录 通过“-I” include进来了,库所在文件通过 

  “-L”参数引导,并指定了“-l”的库名,但通过ldd命令察看时,就是死活找不到你指定链接的so文件,这时你要作的就是通过修改 

  LD_LIBRARY_PATH或者/etc/ld.so.conf文件来指定动态库的目录。通常这样做就可以解决库无法链接的题目了。 

  makefile里面怎么正确的编译和连接天生.so库文件,然后又是在其他程序的makefile里面如何编译和连接才能调用这个库文件的函数???? 

  答: 

  你需要告诉动态链接器、加载器ld.so在哪里才能找到这个共享库,可以设置环境变量把库的路径添加到库目录/lib和/usr/lib,LD_LIBRARY_PATH=$(pwd),这种方法采用命令行方法不太方便,一种替换方法 

  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^注释^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

  LD_LIBRARY_PATH可以在/etc/profile还是 ~/.profile还是 ./bash_profile里设置,或者.bashrc里, 

  改完后运行source /etc/profile或 . /etc/profile 

  更好的办法是添进/etc/ld.so.conf, 然后执行 /sbin/ldconfig 

  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^注释^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 

  是把库路径添加到/etc/ld.so.conf,然后以root身份运行ldconfig 

  也可以在连接的时候指定文件路径和名称 -I -L. 

  GCC=gcc 

  CFLAGS=-Wall -ggdb -fPIC 

  #CFLAGS= 

  all: libfunc test 

  libfunc:func.o func1.o 

  $(GCC) -shared -Wl,-soname,libfunc.so.1 -o libfunc.so.1.1 $ 

  编译目标文件时使用gcc的-fPIC选项,产生与位置无关的代码并能被加载到任何地址: 

  gcc fPIC g c liberr.c o liberr.o 

  使用gcc的-shared和-soname选项; 

  使用gcc的-Wl选项把参数传递给连接器ld; 

  使用gcc的-l选项显示的连接C库,以保证可以得到所需的启动(startup)代码,从而避免程序在使用不同的,可能不兼容版本的C库的系统上不能启动执行。 

  gcc g shared Wl,-soname,liberr.so o liberr.so.1.0.0 liberr.o lc 

  建立相应的符号连接: 

  ln s liberr.so.1.0.0 liberr.so.1; 

  ln s liberr.so.1.0.0 liberr.so; 

  在MAKEFILE中: 

  $@ 

  表示规则中的目标文件集。在模式规则中,假如有多个目标,那么,"$@"就是匹配于目标中模式定义的集合。

  $% 

  仅当目标是函数库文件中,表示规则中的目标成员名。例如,假如一个目标是"foo.a(bar.o)",那么,"$%"就是"bar.o","$@"就是 

  "foo.a"。假如目标不是函数库文件(Unix下是[.a],Windows下是[.lib]),那么,其值为空。 

  $ 

  *********************************************注释************************************************** ********************* 

  test: test.o libfunc 

  $(GCC) -o test test.o -L. -lfunc 

  %.o:%.c 

  $(GCC) -c $(CFLAGS) -o $@ $ref:http://niefei.blog.ccidnet.com/blog/ccid/do_s howone/tid_.html 

  1. 先容 

  使用GNU的工具我们如何在Linux下创建自己的程序函数库?一个“程序 函数库”简单的说就是一个文件包含了一些编译好的代码和数据,这些编 

  译好的代码和数据可以在事后供其他的程序使用。程序函数库可以使整个程序更加模块化,更轻易重新编译,而且更方便升级。程序函数库可分为3种类型:静态函 

  数库(static libraries)、共享函数库(shared libraries)和动态加载函数库(dynamically loaded 

  libraries)。 

  静态函数库是在程序执行前就加进到目标程序中往了;而共享函数库则是在程序启动的时候加载到程序中,它可以被 

  不同的程序共享;动态加载函数库则可以在程序运行的任何时候动态的加载。实际上,动态函数库并非另外一种库函数格式,区别是动态加载函数库是如何被程序员 

  使用的。后?**颐墙倮得鳌?

  本文档主要参考Program Library HOWTO,作者是luster( 

   

  ),任何非贸易目的的再次发行本文档都是答应的,但是请保存作者信息和本版权声明。本文档首先在 

  www.linuxaid.com.cn 

  发布。 

  2. 静态函数库 

  静态函数库实际上就是简单的一个普通的目标文件的集合,一般来说习惯用“.a”作为文件的后缀。可以用ar这个程序来产生静态函数库文件。Ar 

  是archiver的缩写。静态函数库现在已经不在像以前用得那么多了,主要是共享函数库与之相比较有很多的上风的原因。慢慢地,大家都喜欢使用共享函数 

  库了。不过,在一些场所静态函数库仍然在使用,一来是保持一些与以前某些程序的兼容,二来它描述起来也比较简单。 

  静态库函数答应程序 

  员把程序link起来而不用重新编译代码,节省了重新编译代码的时间。不过,在今天这么快速的计算机眼前,一般的程序的重新编译也花费不了多少时间,所以 

  这个上风已经不是像它以前那么明显了。静态函数库对开发者来说还是很有用的,例如你想把自己提供的函数给别人使用,但是又想对函数的源代码进行保密,你就 

  可以给别人提供一个静态函数库文件。理论上说,使用ELF格式的静态库函数天生的代码可以比使用共享函数库(或者动态函数 

  库)的程序运行速度上快一些,大概1-5%。 

  创建一个静态函数库文件,或者往一个已经存在地静态函数库文件添加新的目标代码,可以用下面的命令: 

  ar rcs my_library.a file1.o file2.o 

  这个例子中是把目标代码file1.o和file2.o加进到my_library.a这个函数库文件中,假如my_library.a不存在

  则创建一个新的文件。在用ar命令创建静态库函数的时候,还有其他一些可以选择的参数,可以参加ar的使用帮助。这里不再赘述。 

  一旦 

  你创建了一个静态函数库,你可以使用它了。你可以把它作为你编译和连接过程中的一部分用来天生你的可执行代码。假如你用gcc来编译产生可 

  执行代码的话,你可以用“-l”参数来指定这个库函数。你也可以用ld来做,使用它的“-l”和“-L”参数选项。具体用法,可以参考info:gcc。 

  3. 共享函数库 

  共享函数库中的函数是在当一个可执行程序在启动的时候被加载。假如一个共享函数库正常安装,所有的程序在重新运行的时候都可以自动加载最新的函数库中的函数。对于Linux系统还有更多的可以实现的功能: 

  o 升级了函数库但是仍然答应程序使用老版本的函数库。 o 当执行某个特定程序的时候可以覆盖某个特定的库或者库中指定的函数。 o 可以在库函数被使用的过程中修改这些函数库。 

  3.1. 一些约定 

  假如你要编写的共享函数库支持所有有用的特性,你在编写的过程中必须遵循一系列约定。你必须理解库的不同的名字间的区别,例如它的 

  “soname”和“real 

  name”之间的区别和它们是如何相互作用的。你同样还要知道你应该把这些库函数放在你文件系统的什么位置等等。下?**颐蔷咛蹇纯凑庑┨饽俊?

  3.1.1. 共享库的命名 

  每个共享函数库都有个特殊的名字,称作“soname”。Soname名字命名必须以“lib”作为前缀,然后是函数库的名字,然后是“.so”,最后是版本号信息。不过有个特例,就是非常底层的C库函数都不是以lib开头这样命名的。 

  每个共享函数库都有一个真正的名字(“real name”),它是包含真正库函数代码的文件。真名有一个主版本号,和一个发行版本号。最后一个发行版本号是可选的,可以没有。主版本号和发行版本号使你可以知道你到底是安装了什么版本的库函数。 

  另外,还有一个名字是编译器编译的时候需要的函数库的名字,这个名字就是简单的soname名字,而不包含任何版本号信息。 

  治理共享函数库的关键是区分好这些名字。当可执行程序需要在自己的程序中列出这些他们需要的共享库函数的时候,它只要用soname就可以了; 

  反过来,当你要创建一个新的共享函数库的时候,你要指定一个特定的文件名,其中包含很细节的版本信息。当你安装一个新版本的函数库的时候,你只要先将这些 

  函数库文件拷贝到一些特定的目录中,运行ldconfig这个实用就可以。Ldconfig检查已经存在的库文件,然后创建soname的符号链接到真正 

  的函数库,同时设置/etc/ld.so.cache这个缓冲文件。这个我们稍后再讨论。 

  Ldconfig并不设置链接的名字,通常 

  的做法是在安装过程中完成这个链接名字的建立,一般来说这个符号链接就简单的指向最新的soname 

  或者最新版本的函数库文件。最好把这个符号链接指向soname,由于通常当你升级你的库函数的后,你就可以自动使用新版本的函数库勒。 

  我们来举例看看: 

  /usr/lib/libreadline.so.3 

  是一个完全的完整的soname,ldconfig可以设置一个符号链接到其他某个真正的函数库文件,例如是 

  /usr/lib/libreadline.so.3.0。同时还必须有一个链接名字,例如/usr/lib/libreadline.so 

  就是一个符号链接指向/usr/lib/libreadline.so.3。 

  3.1.2. 文件系统中函数库文件的位置 

  共享函数库文件必须放在一些特定的目录里,这样通过系统的环境变量设置,应用程序才能正确的使用这些函数库。大部分的源码开发的程序都遵循 

  GNU的一些标准,我们可以看info帮助文件获得相信的说明,info信息的位置是:info: 

  standards#Directory_Variables。GNU标准建议所有的函数库文件都放在/usr/local/lib目录下,而且建议命令 

  可执行程序都放在/usr/local/bin目录下。这都是一些习惯题目,可以改变的。 

  文件系统层次化标准FHS(Filesystem Hierarchy Standard)( 

  http://www.pathname.com/fhs 

  )规定了在一个发行包中大部分的函数库文件应该安装到/usr/lib目录 下,但是假如某些库是在系统启动的时候要加载的,则放到/lib目录下,而那些不是系统本身一部分的库则放到/usr/local/lib下面。 

  上面两个路径的不同并没有本质的冲突。GNU提出的标准主要对于开发者开起源码的,而FHS的建议则是针对发行版本的路径的。具体的位置信息可以看/etc/ld.so.conf里面的配置信息。 

  3.2. 这些函数库如何使用 

  在基于GNU glibc的系统里,包括所有的linux系统,启动一个ELF格式的二进制可执行文件会自动启动和运行一个program 

  loader。对于Linux系统,这个loader的名字是/lib/ld-linux.so.X(X是版本号)。这个loader启动后,反过来就会 

  load所有的其他本程序要使用的共享函数库。 

  到底在哪些目录里查找共享函数库呢?这些定义缺省的是放在 

  /etc/ld.so.conf文件里面,我们可以修改这个文件,加进我们自己的一些 

  特殊的路径要求。大多数RedHat系列的发行包的/etc/ld.so.conf文件里面不包括/usr/local/lib这个目录,假如没有这个目 

  录的话,我们可以修改/etc/ld.so.conf,自己手动加上这个条目。 

  假如你想覆盖某个库中的一些函数,用自己的函数替换它们,同时保存该库中其他的函数的话,你可以在/etc/ld.so.preload中加进你想要替换的库(.o结尾的文件),这些preloading的库函数将有优先加载的权利。

  当程序启动的时候搜索所有的目录显然会效率很低,于是Linux系统实际上用的是一个高速缓冲的做法。Ldconfig缺省情况下读出 

  /etc/ld.so.conf相关信息,然后设置适当地符号链接,然后写一个cache到/etc/ld.so.cache这个文件中,而这个 

  /etc/ld.so.cache则可以被其他程序有效的使用了。这样的做法可以大大进步访问函数库的速度。这就要求每次新增加一个动态加载的函数库的时 

  候,就要运行ldconfig来更新这个cache,假如要删除某个函数库,或者某个函数库的路径修改了,都要重新运行ldconfig来更新这个 

  cache。通常的一些包治理器在安装一个新的函数库的时候就要运行ldconfig。 

  另外,FreeBSD使用cache的文件不一样。FreeBSD的ELF cache是/var/run/ld-elf.so.hints,而a.out的cache责是/var/run/ld.so.hints。它们同样是通过ldconfig来更新。 

  3.3. 环境变量 

  各种各样的环境变量控制着一些关键的过程。例如你可以临时为你特定的程序的一次执行指定一个不同的函数库。Linux系统中,通常变量 

  LD_LIBRARY_PATH就是可以用来指定函数库查找路径的,而且这个路径通常是在查找标准的路径之前查找。这个是很有用的,特别是在调试一个新的 

  函数库的时候,或者在特殊的场合使用一个肥标准的函数库的时候。环境变量LD_PRELOAD列出了所有共享函数库中需要优先加载的库文件,功能和 

  /etc/ld.so.preload类似。这些都是有/lib/ld-linux.so这个loader来实现的。值得一提的是, 

  LD_LIBRARY_PATH可以在大部分的UNIX-linke系统下正常起作用,但是并非所有的系统下都可以使用,例如HP-UX系统下,就是用 

  SHLIB_PATH这个变量,而在AIX下则使用LIBPATH这个变量。 

  LD_LIBRARY_PATH在开发和调试过程中经常大量使用,但是不应该被一个普通用户在安装过程中被安装程序修改,大家可以往参考 

  http://www.visi.com/~barr/ldpath.html 

  ,这里有一个文档专门先容为什么不使用LD_LIBRARY_PATH这个 变量。 

  事实上还有更多的环境变量影响着程序的调进过程,它们的名字通常就是以LD_或者RTLD_打头。大部分这些环境变量的使用的文档都是不全,通常搞得人头昏眼花的,假如要真正弄清楚它们的用法,最好往读loader的源码(也就是gcc的一部分)。 

  答应用户控制动态链接函数库将涉及到setuid/setgid这个函数假如特殊的功能需要的话。因此,GNU 

  loader通常限制或者忽略用户对这些变量使用setuid和setgid。假如loader通过判定程序的相关环境变量判定程序的是否使用了 

  setuid或者setgid,假如uid和euid不同,或者gid和egid部一样,那么loader就假定程序已经使用了setuid或者 

  setgid,然后就大大的限制器控制这个老链接的权限。假如阅读GNU 

  glibc的库函数源码,就可以清楚地看到这一点,特别的我们可以看elf/rtld.c和sysdeps/generic/dl-sysdep.c这两 

  个文件。这就意味着假如你使得uid和gid与euid和egid分别相等,然后调用一个程序,那么这些变量就可以完全起效。 

  3.4. 创建一个共享函数库 

  现在我们开始学习如何创建一个共享函数库。实在创建一个共享函数库非常轻易。首先创建object文件,这个文件将加进通过gcc fPIC 

  参数命令加进到共享函数库里面。PIC的意思是“位置无关代码”(Position Independent Code)。下面是一个标准的格式: 

  gcc -shared -Wl,-soname,your_soname -o library_name file_list library_list 

  下面再给一个例子,它创建两个object文件(a.o和b.o),然后创建一个包含a.o和b.o的共享函数库。例子中”-g”和“-Wall”参数不是必须的。 

  gcc -fPIC -g -c -Wall a.cgcc -fPIC -g -c -Wall b.cgcc -shared -Wl, 

  -soname,liblusterstuff.so.1 -o liblusterstuff.so.1.0.1 a.o b.o -lc 

  下面是一些需要留意的地方: 

  · 

  不用使用-fomit-frame-pointer这个编译参数除非你不得不这样。固然使用了这个参数获得的函数库仍然可以使用,但是这使得调试程序几乎 

  没有用,无法跟踪调试。· 使用-fPIC来产生代码,而不是-fpic。· 某些情况下,使用gcc 

  来天生object文件,需要使用“-Wl,-export-dynamic”这个选项参数。通常,动态函数库的符号表里面包含了这些动态的对象的符号。 

  这个选项在创建ELF格式的文件时候,会将所有的符号加进到动态符号表中。可以参考ld的帮助获得更具体的说明。 

  3.5. 安装和使用共享函数库 

  一旦你了一个共享函数库,你还需要安装它。实在简单的方法就是拷贝你的库文件到指定的标准的目录(例如/usr/lib),然后运行ldconfig。 

  假如你没有权限往做这件事情,例如你不能修改/usr/lib目录,那么你就只好通过修改你的环境变量来实现这些函数库的使用了。首先,你需要 

  创建这些共享函数库;然后,设置一些必须得符号链接,特别是从soname到真正的函数库文件的符号链接,简单的方法就是运行ldconfig: 

  ldconfig -n directory_with_shared_libraries 

  然后你就可以设置你的LD_LIBRARY_PATH这个环境变量,它是一个以逗号分隔的路径的集合,这个可以用来指明共享函数库的搜索路径。例如,使用bash,就可以这样来启动一个程序my_program: 

  LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH my_program 

  假如你需要的是重载部分函数,则你就需要创建一个包含需要重载的函数的object文件,然后设置LD_PRELOAD环境变量。通常你可以很 

  方便的升级你的函数库,假如某个API改变了,创建库的程序会改变soname。然而,假如一个函数升级了某个函数库而保持了原来的soname,你可以 

  强行将老版本的函数库拷贝到某个位置,然后重新命名这个文件(例如使用原来的名字,然后后面加.orig后缀),然后创建一个小的“wrapper”脚本 

  来设置这个库函数和相关的东西。例如下面的例子: 

  #!/bin/sh export LD_LIBRARY_PATH=/usr/local/my_lib:$LD_LIBRARY_PATH exec 

  /usr/bin/my_program.orig $* 

  我们可以通过运行ldd来看某个程序使用的共享函数库。例如你可以看ls这个实用工具使用的函数库: 

  ldd /bin/ls 

  libtermcap.so.2 => /lib/libtermcap.so.2 (0x4001c000) 

  libc.so.6 => /lib/libc.so.6 (0x) 

  /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x) 

  通常我么可以看到一个soname的列表,包括路径。在所有的情况下,你都至少可以看到两个库: 

  · /lib/ld-linux.so.N(N是1或者更大,一般至少2)。 

  这是这个用力加载其他所有的共享库的库。 

  · libc.so.N(N应该大于或者即是6)。这是C语言函数库。 

  值得一提的是,不要在对你不信任的程序运行ldd命令。在ldd的manual里面写得很清楚,ldd是通过设置某些特殊的环境变量(例如,对 

  于ELF对象,设置LD_TRACE_LOADED_OBJECTS),然后运行这个程序。这样就有可能使得某地程序可能使得ldd来执行某些意想不到的 

  代码,而产生不安全的隐患。 

  3.6. 不兼容的函数库 

  假如一个新版的函数库要和老版本的二进制的库不兼容,则soname需要改变。对于C语言,一共有4个基本的理由使得它们在二进制代码上很难兼容: 

  o. 一个函数的行文改变了,这样它就可能与最开始的定义不相符合。 

  o. 输出的数据项改变了。 

  o. 某些输出的函数删除了。 

  o. 某些输出函数的接口改变了。 

  假如你能避免这些地方,你就可以保持你的函数库在二进制代码上的兼容,或者说,你可以使得你的程序的应用二进制接口(ABI:Application Binary Inte***ce)上兼容。



原创粉丝点击