so库_so库文件 - CSDN
精华内容
参与话题
  • Android 调用so库全过程

    万次阅读 多人点赞 2017-12-05 10:41:59
    Android中有时候为了效率以及平台开发的支持,难免会用到NDK开发,那么都会产生一个so文件,通过native方法进行调用,开发和调用步骤很简单,这里就不多说了,本文主要来介绍,我们在使用so的时候总是会出现一些...

     原文地址点击打开链接


    一、前言

    Android中有时候为了效率以及平台开发库的支持,难免会用到NDK开发,那么都会产生一个so文件,通过native方法进行调用,开发和调用步骤很简单,这里就不多说了,本文主要来介绍,我们在使用so的时候总是会出现一些常见的问题,而现在插件化开发也很普遍了,有时候插件中也会包含一些so文件,需要加载,这时候也会出现一些问题。本文就来详细总结一下这些问题出现的原因,以及解决方法,主要还是通过源码来分析。

    二、涉及到的源码类

    因为本文主要通过分析源码来分析so使用的知识点和问题总结,所以涉及到了很多的源码类,这里就现提供一下:

    1、PackageManagerService.java
    +setNativeLibraryPaths:设置应用的native库路径
    +scanPackageDirtyLI:扫描包内容初始化应用信息

    2、ActivityManagerService.java
    +startProcessLocked:发送命令给Zygote进程启动一个虚拟机

    3、NativeLibraryHelper.java

    底层实现类:com_android_internal_content_NativeLibraryHelper.cpp

    +copyNativeBinariesWithOverride:释放apk中的so文件到本地目录

    +findSupportedAbi:遍历apk中的so文件结合abiList值得到应用支持的abi类型索引值

    4、LoadApk类和ApplicationLoaders类

    5、VMRuntime.java

    底层实现类:dalvik_system_VMRuntime.c

    +getInstructionSet:获取虚拟机的指令集类型

    +is64BitAbi:判断VM是否为64位

    6、Runtime.java

    底层实现类:dalvik/vm/native/java_lang_Runtime.cpp,dalvik/vm/Native.cpp

    +nativeLoad:加载so文件

    三、Android中so文件的编译平台

    Android中在进行NDK开发的时候,都知道因为机型杂而多的原因,没有一个大的标准,所以很多厂商都会采用不同型号的cpu,那么在编译so文件的时候,就需要进行交叉编译出多个cpu平台版本,现在主流的cpu架构版本:

    armeabi/armeabi-v7a:这个架构是arm类型的,主要用于Android4.0之后的,cpu值32位的

    x86/x86_64:这个架构是x86类型的,有32位和64位,占用的设备比例比较小

    arm64-v8:这个架构是arm类型,主要用于Android5.0之后,cpu是64位的

    这里可以看到,其中arm类型的是往下兼容策略,比如arm64-v8a肯定兼容armeabi/armeabi-v7a,也就是说armeabi/armeabi-v7a架构的so文件可以用在arm64-v8a的设备中的,而armeabi-v7a也是兼容armeabi的,但是因为cpu型号不同,所以arm体系和x86体系之间是不能相互兼容的。

    四、Android中so加载流程

    在Android中如果想使用so的话,首先得先加载,加载现在主要有两种方法,一种是直接System.loadLibrary方法加载工程中的libs目录下的默认so文件,这里的加载文件名是xxx,而整个so的文件名为:libxxx.so。还有一种是加载指定目录下的so文件,使用System.load方法,这里需要加载的文件名是全路径,比如:xxx/xxx/libxxx.so。

    上面的两种加载方式,在大部分场景中用到的都是第一种方式,而第二种方式用的比较多的就是在插件中加载so文件了。

    不管是第一种方式还是第二种方式,其实到最后都是调用了Runtime.java类的加载方法doLoad:


    这里会先从类加载中获取到nativeLib路径,然后在调用native方法nativeLoad(java_lang_Runtime.cpp):


    这里调用了一个核心的方法dvmLoadNativeCode(dalvik/vm/Native.cpp):


    注意:

    这里有一个检测异常的代码,而这个错误,是我们在使用插件开发加载so的时候可能会遇到的错误,比如现在我们使用DexClassLoader类去加载插件,但是因为我们为了插件能够实时更新,所以每次都会赋值新的DexClassLoader对象,但是第一次加载so文件到内存中了,这时候退出程序,但是没有真正意义上的退出,只是关闭了Activity了,这时候再次启动又会赋值新的加载器对象,那么原先so已经加载到内存中了,但是这时候是新的类加载器那么就报错了,解决办法其实很简单,主要有两种方式:

    第一种方式:在退出程序的时候采用真正意义上的退出,比如调用System.exit(0)方法,这时候进程被杀了,加载到内存的so也就被释放了,那么下次赋值新的类加载就在此加载so到内存了,

    第二种方式:就是全局定义一个static类型的类加载DexClassLoader也是可以的,因为static类型是保存在当前进程中,如果进程没有被杀就一直存在这个对象,下次进入程序的时候判断当前类加载器是否为null,如果不为null就不要赋值了,但是这个方法有一个弊端就是类加载器没有从新赋值,如果插件这时候更新了,但是还是使用之前的加载器,那么新插件将不会进行加载。

    继续往下看:


    这里主要调用了两个核心的系统方法,dlopen和dlsym,这两个方法用途还是很多的,一般是先加载so文件,然后得到指定函数的指针,最后直接调用即可,主要用于调用动态的调用so中的指定函数功能。而且这里注意到了最开始先调用so中的JNI_OnLoad函数,这个函数是so被加载之后调用的第一个方法。

    到这里我们就总结一下Android中加载so的流程:

    1、调用System.loadLibrary和System.load方法进行加载so文件

    2、通过Runtime.java类的nativeLoad方法进行最终调用,这里需要通过类加载器获取到nativeLib路径。

    3、到底层之后,就开始使用dlopen方法加载so文件,然后使用dlsym方法调用JNI_OnLoad方法,最终开始了so的执行。

    五、Android中类加载器关联so路径

    上面分析so加载过程中可以发现有一个地方,就是通过类加载器来获取到so的路径,那么Android中的主要类加载器有两个,一个是PathClassLoader和DexClassLoader,关于这两个类加载不多说了,网上资料很多可以自行查找阅读。而PathClassLoader是我们Android中默认的类加载器,也就是apk文件就是由他来加载的,我们可以通过查看源码得知,Android中加载apk的类加载可以从LoadApk.java类查找到:


    注意:

    这个类很重要的,而这个类加载器也是我们在做插件的时候,需要做一些操作,比如需要把加载插件的DexClassLoader类给添加到这个系统加载器中,就可以解决插件中组件的生命周期问题。

    看看这个类加载器在哪里赋值的:


    去看看ApplicationLoaders.java类:


    看到了,这里就是定义了PathClassLoader类了,所以我们Android中应用的默认加载器是PathClassLoader,再去看看这个类加载器的nativeLib是哪里:


    六、Android中so文件如何释放

    我们在使用System.loadLibrary加载so的时候,传递的是so文件的libxxx.so中的xxx部分,那么系统是如何找到这个so文件然后进行加载的呢?这个就要先从apk文件安装时机说起。

    我们如果还没有分析源码之前,大致能够猜想到的流程是:

    在安装apk的时候,系统解析apk文件,因为so文件肯定是存放在libs下指定平台目录中的,而apk文件本身就是一个压缩文件,所以可以进行解压,然后读取libs目录下的so文件,进行本地释放解压到指定目录,然后在加载的时候就先拼接so文件的全路径,最后在进行加载工作即可。

    通过猜想,下面就通过源码来分析一下流程,系统在安装apk的时候,是调用系统类:PackageManagerService.java类:

    主要的核心方法是scanPackageDirtyLI:


    这个方法主要通过传递的pkg变量,开始构造applicationInfo信息。我们往下面看,找到设置nativeLib信息的代码:


    这里注意有一个判断,是不是多平台架构的应用:


    所以,我们看看info.flags有没有设置这个标志,我们看到上面的pkg变量是通过解析apk文件的类PackageParser.java类中获取到的,所以可以去这个类中找这个标志位的设置。


    这里看到了,如果在AndroidManifest.xml中设置了Application中的multiArch属性值的话就有,但是我们默认都没有设置这个属性值,那么就是false,也就是说一般应用都不是多平台的。所以上面的isMultiArch方法就返回false,代码就走到了这里:


    在这里就有很多知识点了,而这里可以看到,就涉及到了so文件的释放工作了,主要是在NativeLibraryHelper类中,但是这里看到首先获取abiList值:


    通过Build.SUPPORTED_ABIS来获取到的:


    最终是通过获取系统属性:ro.product.cpu.abilist的值来得到的,我们可以使用getprop命令来查看这个属性值:


    这里获取到的值是:arm64-v8a,armeabi-v7a,armeabi,我用的是64位的cpu设备,所以可以看到他有多个cpu架构可选,而且看到这个顺序会想到,这个顺序正好是向下兼容的顺序。

    现在去看看NativeLibraryHelper类的copyNativeBinariesForSupportedAbi方法:


    这个方法中主要干了三件事:

    第一件事是获取应用所支持的arch架构类型

    第二件事是通过架构类型获取so释放的目录

    第三件事是native层中释放apk中的指定架构的so到设备目录中

    第一件事:获取应用所支持的arch架构类型

    NativeLibraryHelper类的findSupportedAbi方法,其实这个方法就是查找系统当前支持的架构型号索引值:


    看看native方法的实现:


    这里看到了,会先读取apk文件,然后遍历apk文件中的so文件,得到全路径然后在和传递进来的abiList进行比较,得到合适的索引值,其实实现逻辑很简单:abiList是:arm64-v8a,armeabi-v7a,armeabi,然后就开始比例apk中有没有这些架构平台的so文件,如果有,就直接返回abiList中的索引值即可,比如说apk中的libs结构如下:


    那么这时候返回来的索引值就是0,代表的是arm64-v8a架构的。如果apk文件中没有arm64-v8a目录的话,那么就返回1,代表的是armeabi-v7a架构的。依次类推。得到应用支持的架构索引之后就可以获取so释放到设备中的目录了。

    第二件事:获取so释放之后的目录

    这里主要通过VMRuntime.java中的getInstructionSet方法:


    这里调用了一个map结构值:


    这里的arch架构和目录对应关系,如果arch是arm64-v8a的话,那么目录就是arm64了。

    第三件事:释放apk中的so文件

    直接调用的是native层方法iterateOverNativeFiles:


    好了到这里就讲完了上面的三件事了,而这三件事做完之后,apk中的so文件就会被释放到本地设备中的指定目录中了,当然这里系统会根据abiList中的值以及apk中包含的arch类型的so来决定释放哪个目录中的so文件,比如这里通过ApplicationInfo类来打印当前应用的nativeLibraryDir值:


    打印的结果:


    看到了,因为是arm64-v8a类型的,所以目录是arm64的,而且可以看到这个应用不是多平台的。

    我们可以看到Android中是如何释放apk中的so文件到本地目录的:

    1、通过遍历apk文件中的so文件的全路径,然后和系统的abiList中的类型值进行比较,如果匹配到了就返回arch类型的索引值

    2、得到了应用所支持的arch类型之后,就开始获取创建本地释放so的目录

    3、然后开始释放so文件

    我们在PackageMangerService类中继续往下看:


    这里还要保存上面获取到应用支持的arch类型值,我们可以使用反射打印这个值:


    打印结果:


    这个值在后面应用创建VM的时候会用到。

    接着开始设置应用的nativeLib路径了:


    看看这个方法的实现:


    这里先判断是不是64位:


    通过arch类型对应的目录来判断的:


    这里如果是64位,目录就是lib,如果是32位就是lib64:


    这样就和我们上面释放so文件的目录保持一致了,所以这里的ApplicationInfo类中的lib路径就是我们上面释放so之后的路径了。

    在之前说到了类加载器中的lib路径,我们可以打印一下库路径的,这里直接使用getClassLoader得到加载器打印即可:

    这里看到Library的目录包含很多路径。

    七、Android中64位系统如何兼容32位的so

    上面分析完了,so文件的释放工作,下面继续来看一下如果一个64位系统的Android设备如何做到能够运行32位的so文件,这个就需要从应用的启动说起了,那么这个类就是ActivityManagerService.java,有一个核心的方法:startProcessLocked,这个方法就是向Zygote进程发送一个消息,为这个应用创建虚拟机开始运行程序了:


    这里在发送消息给Zygote进程,看到这里通过ApplicationInfo中的primaryCpuAbi类型告诉Zygote改创建多少位的虚拟机,我们查看系统启动文件init.rc内容:


    这里会启动一个64位的Zygote进程


    然后启动一个32位的Zygot进程

    所以这里应该就可以想明白了,原来系统启动的时候,如果是64位的系统设备,会启动两个Zygote进程用来兼容32位类型的应用,我们可以使用ps命令查看进程:


    看到了,这里果然启动了两个Zygote进程,一个64位的,一个是32位的。所以兼容功能的大致流程图应该是这样的:


    上层启动应用的时候会把应用的abi类型带过来,然后这里会根据这个类型发送给具体的Zygote进程消息,来创建虚拟机开始运行程序,这样就做到了兼容。

    八、Android插件中如何加载so文件

    有时候我们在开发插件的时候,可能会调用so文件,一般来说有两种方案:

    一种是在加载插件的时候,先把插件中的so文件释放到本地目录,然后在把目录设置到DexClassLoader类加载器的nativeLib中。

    一种在插件初始化的时候,释放插件中的so文件到本地目录,然后使用System.load方法去全路径加载so文件

    这两种方式的区别在于,第一种方式的代码逻辑放在了宿主工程中,同时so文件可以放在插件的任意目录中,然后在解压插件文件找到这个so文件释放即可。第二种方式的代码逻辑是放在了插件中,同时so文件只能放在插件的assets目录中,然后通过把插件文件设置到程序的AssetManager中,最后通过访问assets中的so文件进行释放。

    上面就全部分析完了Android中关于so加载的相关内容:

    1、so编译平台问题

    2、so加载流程分析

    3、so文件释放功能分析

    4、so文件兼容功能分析

    5、插件中so文件调用功能分析

    九、常见问题分析

    第一个问题:Could not find libxxx.so


    这个问题看上去很好理解,就是在调用加载so的方法的时候,到底层使用dlopen方法打开so文件,发现找不到这个so文件,那么这个问题产生的原因主要有两个:

    第一个是我们的确忘了在工程的libs下存放so文件了;

    第二个是我们把so文件放错目录了;

    第一个原因就不多说了,主要来看第二原因:

    有时候我们在开发项目的时候,可能会放多个架构类型的so文件,那么现在假如我的设备是arm64-v8a类型的,我的项目中有三个so文件,比如叫做AAA.so,BBB.so,CCC.so,然后我再arm64-v8a目录中放了AAA.so,BBB.so,而CCC.so忘了放了,但是会放到armeabi-v7a和armeabi目录中,那么这时候就会发生找不到CCC.so的错误,原因很简单:

    上面分析了apk中so文件的释放逻辑,系统会先遍历apk中所有so文件的全路径,然后在结合abiList的值来决定最终释放哪个目录中的so文件,那么现在系统是arm64-v8a了,而apk中的libs下也有arm64-v8a,所以这里就会把apk中的libs\arm64-v8a中的所有so文件释放解压到本地目录中,而不会在去释放armeabi/armeabi-v7a了。因为arm64-v8a中没有CCC.so文件,所以最终释放到本地目录中也是没有这个so文件的,所以加载时找不到文件了。

    解决办法:就是在使用so文件的时候,需要确定在每个架构类型目录中都要有相同的so文件即可。

    第二个问题:32-bit instead of 64-bit


    这个问题的原因主要是因为64位的Zygote进程创建的虚拟机中加载了32位的so文件,这个问题的产生原因主要有两个:

    第一个是我们把不同架构类型的so文件放错目录了,比如armeabi/armeabi-v7a的so文件放到了arm64-v8a中了

    第二个是我们在开发插件的过程中,宿主工程中有arm64-v8a目录,但是插件中加载so却是armeabi/armeabi-v7a类型的

    第一个原因就不多说了,主要是因为so放错目录了,来看一下第二个原因,我们在开发插件的时候有时候需要在插件中去加载so文件,一般都是使用System.load方式去加载全路径的so文件,那么这里就可能存在一个问题,比如宿主工程中,放了所有架构的目录,包括了64位的,因为考虑插件的大小,所以在插件中只放了armeabi-v7a目录的so文件,如果设备是64位的系统,那么这时候插件加载so文件就会报错。原因就在于上面分析的so兼容问题中说到了,因为宿主工程中包含了64位的架构arm64-v8a类型,系统的abiList中也有arm64-v8a类型,所以这时候应用的ApplicationInfo的abi就是arm64-v8a了,那么就会发送消息给Zygote64的进程,创建的也是64位的虚拟机了,而最后插件中加载so的类型是32位的armeabi-v7a,那么就会报错了,因为32位的so文件不能运行在64位的虚拟机中的。

    解决办法:宿主工程和插件工程中的so文件的架构类型保持一致,这个将会带来一个很大的问题,就是插件包会变得很大,因为宿主工程为了兼容多数机型,加入了多个类型的架构so文件,但是插件为了减小包大小,就放了指定类型的so文件,但是最终会存在这种问题,所以这个解决办法就要看项目需要了。

    还有一个类似的问题:64-bit instead of 32-bit:


    原理都是一样的,32位的虚拟机中加载了64位的so文件问题导致的。

    第三个问题:Shared library already opened


    这个问题在上面介绍so加载流程中已经介绍过了,原因主要是因为之前使用DexClassLoader加载so之后,so没有释放还在内存中,而在此启动有弄了一个新的DexClassLoader对象去加载so问题,就出错了。

    我们使用DexClassLoader类去加载插件,但是因为我们为了插件能够实时更新,所以每次都会赋值新的DexClassLoader对象,但是第一次加载so文件到内存中了,这时候退出程序,但是没有真正意义上的退出,只是关闭了Activity了,这时候再次启动又会赋值新的加载器对象,那么原先so已经加载到内存中了,但是这时候是新的类加载器那么就报错了。

    解决办法:

    第一种方式:在退出程序的时候采用真正意义上的退出,比如调用System.exit(0)方法,这时候进程被杀了,加载到内存的so也就被释放了,那么下次赋值新的类加载就在此加载so到内存了。

    第二种方式:就是全局定义一个static类型的类加载DexClassLoader也是可以的,因为static类型是保存在当前进程中,如果进程没有被杀就一直存在这个对象,下次进入程序的时候判断当前类加载器是否为null,如果不为null就不要赋值了,但是这个方法有一个弊端就是类加载器没有从新赋值,如果插件这时候更新了,但是还是使用之前的加载器,那么新插件将不会进行加载。

    十、技术概要

    本文主要介绍了Android中关于so的相关知识,主要包括so编译多架构问题,so加载流程问题,so释放问题,so系统兼容问题以及插件中加载so文件的功能解析,看完本文之后,我们需要了解到的知识点:

    1、在NDK开发时,可以指定多种架构类型编译出多种类型的so文件。

    2、so的加载流程主要是System类中的两个加载方法,最终都会调用Runtime中的nativeLoad的native方法,而这个native方法最终会调用dlopen来打开so文件,然后在调用dlsym方法调用so的JNI_OnLoad方法。

    3、关于apk文件在安装的时候释放so文件到本地目录中,主要是结合当前设备的abiList信息(这个信息主要是通过系统属性:ro.product.cpu.abilist值来获取的)和apk中不同类型架构,来决定最终释放哪个类型目录中的so文件,释放完成之后,还需要设置应用的nativeLib路径,以及应用的abi信息,因为这个abi信息在后面启动虚拟机的时候需要用到。

    4、因为现在有很多设备已经是64位系统了,但是为了兼容32位的so文件,所以这些64位系统就会在系统启动的时候创建两个Zygote进程,一个是64位的,一个是32位的,当一个应用启动的时候,需要创建虚拟机,那么这时候就会把应用的架构类型传递过去,系统会根据这个类型来交给哪个Zygote进程来处理这个应用启动事件。这样就可以做到so调用的兼容问题了。

    5、插件中加载so文件现阶段主要有两种方式,一种是先释放插件中的so文件到本地目录,然后设置DexClassLoader的nativeLib路径;还有一种方式是先释放插件中的so文件,然后调用System.load来加载全局路径的so文件。

    十一、问题总结

    本文还总结了在使用so文件的时候,会遇到的一些问题,主要是三个问题:

    1、so文件找不到问题

    这个问题一般是因为我们忘记放了so文件,或者是so文件没有放置全部,也就是没有在libs目录中所有的架构类型目录中放置。

    2、不同位数的虚拟机运行了不同位数的so文件

    这个问题一般是因为我们在libs目录中把so文件放错目录了,或者是宿主工程和插件工程中的so文件架构类型目录没有保持一致。

    3、类加载器加载so文件再次加载

    这个问题一般是因为插件开发中使用了不同的DexClassLoader去加载多次相同的so文件导致的。

    十二、知识延展

    我们在开发的过程中有时候想知道系统的位数,那么这里网上告知说有好几种方法,其实那些都是忽悠人的,特别是在使用这个api的时候:android.os.Build.CPU_ABI,我就是在项目中被这个方法坑爹了,这个方法其实不是获取系统的位数,而是获取当前应用的架构类型位数,就是我们前面分析的ApplicationInfo中的abi信息,我们可以查看一下源码:


    这里可以看到,这个字段已经被废弃了,因为他不靠谱呀,这个字段在Build类的static块中进行赋值的:


    这里会通过VMRuntime类的is64Bit方法来判断当前虚拟机的位数,来获取这个值


    这里还有两个系统属性:

    ro.product.cpu.abilist32是32位的所有arch架构类型

    ro.product.cpu.abilist64是64位的所有arch架构类型

    而这两个字段值的合集就是前面的ro.product.cpu.abilist属性值。


    而VMRuntime的is64Bit方法是native方法,实现如下:


    看到了,这里得到的是虚拟机的位数,那么就是上面的Zygote进程的位数了。那么问题就来了,假如我的设备是64位的,但是我的项目中没有arm64-v8a类型的so文件,这时候在解析apk进行释放so文件的时候,就会得知架构类型是armeabi/armeabi-v7a了,因为遍历apk文件,没有找到arm64-v8a类型的so文件,这时候应用的abi类型就是armeabi/armeabi-v7a了,这就是32位的了,就会通知32位的Zygote进程创建了一个32位的虚拟机,那么此时我的项目中通过Build.CPU_ABI得到的系统位数就是32了,那么完全不是我们想要的了。

    所以正确的获取系统位数的方法是:

    Android5.0系统之后,可以通过ro.product.cpu.abilist属性字段值来判断,如果这个字段值中包含了64的话,那么就是64位系统了

    Android5.0系统之前,需要通过ro.product.cpu.abi属性字段值来判断,不过5.0系统之前都是32位的,还没有出现64位呢。

    十三、选择适当架构类型减小包大小

    我们上面分析之后可以看到,如果想做到万无一失即,项目不报错,而且so运行效率也是非常高的话,就需要把那几个架构类型的so文件都要在项目中放一遍,那么这个问题就来了,如果so文件较大的话,apk包最终也是很大的,所以这里就需要做一次选择了。

    1、我们在开发一个项目的时候因为,整个项目的so文件结构我们可以控制,所以为了防止apk包增大,我们可以考虑只放几个架构类型的so文件,比如最好的是放armeabi类型的,因为首先现在大部分设备采用cpu型号都是arm的,少数采用x86或者是mips类型的,其次是防止了armeabi类型之后,对于armeabi-v7a和arm64-v8a就可以兼容了,不会存在报错问题。但是因为系统需要兼容所以就会出现so运行效率的问题了,最好的效率就是指定架构类型的so运行在对应架构类型的设备中。因为现在大部分的设备系统版本都是4.0以上了,所以armeabi-v7a架构类型用的比较多了,所以有时候为了效率问题,项目中只放了这个架构类型的so文件,那么像老版本的手机armeabi的话就会报错了,当然这个错误是可以接受的即可。

    2、有时候像x86和mips等少数类型架构的设备,开发程序的时候会单独出一个版本比如叫做xxx应用x86版本

    3、在开发SDK的时候,因为开发之后的SDK包是给其他app接入的,而对于接入的app,我们不能做太多的限制,所以理论上应该把所有架构类型的so都要提供,这样给需要接入的app进行选择即可,比如像百度地图SDK:


    十四、总结

    本文主要是介绍了Android中关于so的相关知识,而这些知识点都是在使用so文件中会经常用到的,同时一些问题也是我们会遇到的,这里只是做了一个总结,同时也给出了插件中加载so文件的方案已经遇到的问题解决思路等内容。


    展开全文
  • so动态和a静态的编译

    千次阅读 2018-06-09 11:32:39
    一般linux环境下的链接库分为a库和so库,分别表示静态链接库和动态链接库,其作用是把C程序编译好做成一种可执行链接文件,c主程序文件调用这些程序的函数接口是可以使用a库或so库,在主程序中只需要include含有库中...

    一般linux环境下的链接库分为a库和so库,分别表示静态链接库和动态链接库,其作用是把C程序编译好做成一种可执行链接文件,c主程序文件调用这些程序的函数接口是可以使用a库或so库,在主程序中只需要include含有库中提供的函数接口声明的头文件即可。

    一、生成a库

    首先,你需要写几个你要调用的函数的源文件,如test1.c、test2.c等。其代码如下:

    再将头文件写好(其实这里的头文件并不需要,若是编译成库的程序之间有调用关系的时候才用得到,只在调用链接库的时候编写头文件即可)。开始编译,将源文件编译成.o文件。命令行指令如下:


    然后开始生成a库,指令如下:


    注意:链接库前缀必须以lib开头

    得到libtest.a库文件后,然后将.a库链接到主程序中,写主程序main.c和头文件main.h 。

    加载a库,生成可执行文件并执行,指令如下:gcc main.c -L. -ltest -o test


    可以看到,a库连接成功,可以调用到里面的函数。

    二、生成so库

    利用上面所写过的test1.c 和test2.c生成.so库的指令如下:


    同样so库的前缀必须为lib,然后将.so库链接到主程序main.c中,这里需要注意一下,因为动态库的特性,编译器会到指定的目录去寻找动态库,目录的地址在/etc/ld.so.conf.d/ 目录里的libc.conf文件里,你可以在里面加一行地址表示你so库的位置,更改完conf文件里的内容,记得输入命令行:ldconfig。

    你还可以将so库复制到默认的目录下。这里是将so库复制到了默认目录下,生成可执行文件并运行,指令如下:

    gcc main.c -L. -ltest -o main


    静态链接库和动态链接库的区别在于,主程序在运行前,静态链接库的链接固定写入在程序中,而动态链接库则是在每次程序运行再加载链接。

    在加载动态链接库的时候,有可能会遇到加载不到的错误,原因在于系统默认加载的动态链接库路径里没有找到你的动态库,有三种解决方法:

    1.在执行gcc main.c -L. -ltest -o main 前,执行 export LD_LIBRARY_PATH=$(pwd)

    2.将你so所在的目录写到/etc/ld.so.conf文件里,然后执行ldconfig。

    3.将你的so放在/etc/ld.so.conf里的路径位置里。

    展开全文
  • C语言调用so动态的两种方式

    万次阅读 2018-07-23 00:28:36
    这种方式生成的程序会在启动时候就加载so动态。 add.h int add(int x, int y); add.c #include "add.h" int add(int x, int y) { return (x + y); } main.c #include <...

    方式1:类似静态库的调用(使用头文件)

    这种方式生成的程序会在启动时候就加载so动态库。

    add.h

    int add(int x, int y);

    add.c

    #include "add.h"
    
    int add(int x, int y) {
        return (x + y);
    }

    main.c

    #include <stdio.h>
    #include "add.h"
    
    int main()
    {
        int sum = add(7, 8);
        printf("7+8 = %d\n", sum);
    
        return 0;
    }

    编译so,生成libadd.so。

    gcc -shared -o libadd.so add.c

    编译main,使用-L./指定add库在当前目录。

    gcc -o main main.c -L./ -ladd

    方式2:使用dlopen/dlsum动态加载动态库(不使用头文件)

    这种方式生成的程序会在代码执行到指定行位置加载so动态库。

    add.c

    int add(int x, int y) {
        return (x + y);
    }

    main.c

    #include <stdio.h>
    #include <dlfcn.h>
    
    int main()
    {
        /*手动加载指定位置的so动态库*/
        void* handle = dlopen("./libadd.so", RTLD_LAZY);
        int (*add)(int a, int b);
    
        /*根据动态链接库操作句柄与符号,返回符号对应的地址*/
        add = dlsym(handle, "add");
    
        int sum = add(7, 8);
        printf("7+8 = %d\n", sum);
    
        dlclose(handle);
        return 0;
    }

    编译so,生成libadd.so

    gcc -shared -o libadd.so add.c

    编译main,不需要指定libadd.so相关信息进行编译,执行时候会在指定目录加载so

    gcc -o main main.c -ldl

    两种调用方式总结

    方式1使用头文件,所以可以直接调用头文件声明的函数。编译的时候指定了动态库位置和名称,程序启动时候系统就会自动加载相应位置的so动态库。
    方式2没有头文件,编译的时候也不需要指定动态库信息。但是需要在程序中使用dlopen函数加载相应位置的so动态库,且要使用dlsym函数根据函数符号去查找此函数的地址。


    BONUS: so动态库中调用so动态库

    add.h

    int add(int x, int y);

    add.c

    #include "add.h"
    
    int add(int x, int y) {
        return (x + y);
    }

    sum.h

    void printsum(int a, int b);

    sum.c

    #include "sum.h"
    #include <stdio.h>
    #include "add.h"
    
    void printsum(int a, int b){
        int sum = add(a, b);
        printf("%d+%d = %d\n", a, b, sum);
    }

    main.c

    #include "sum.h"
    
    int main()
    {
        printsum(1, 3);
        return 0;
    }

    编译libadd.so

    gcc -shared -o libadd.so add.c

    编译libsum.so,需要指定libadd.so信息

    gcc -shared -o libsum.so sum.c -L. -ladd

    编译main,仅需要指定libsum.so

    gcc -o main main.c -L. -lsum

    main运行的时候同时需要libsum.so 和 linadd.so。

    展开全文
  • so库方法的调用过程

    千次阅读 2019-09-14 23:05:42
    但程序运行时,这些地址早已经确定,那程序引用so库中的这些代码地址如何确定呢,这就是这次要整理学习的内容,即so库的在链接和执行时的加载过程。 静动态库 在聊so库之前先聊聊静态库。为了程序更加优雅...

         0. 写在前面

    So库,又动态名库,是Linux下最常见的文件之一,也是Android中最常见的文件之一,是一种ELF文件。这种so库是程序运行时,才会将这些需要的代码拷贝到对应的内存中。但程序运行时,这些地址早已经确定,那程序引用so库中的这些代码地址如何确定呢,这就是这次要整理学习的内容,即so库的在链接和执行时的加载过程。

    1. 静动态库

    在聊so库之前先聊聊静态库。为了程序更加优雅和高效,每一个程序的完成都是采用分而治之的方法,即同一个程序或者项目每个程序员都会完成不同的功能,有的功能是可复用的,而对于一些公共的可复用的功能,会使用库的形式来完成。比如我们在不同模块中多次用到了一个方法ar_public(),我们就可以将其包装到一个公共的文件里面,这样就如果其他的地方有调用就可以把引用这个公共文件从而调用这个方法,这样就有了静态库。简单来说静态库是链接的时候将库中所用到的程序拷贝进来,这样即使在执行阶段吧对应的库删掉都没有关系,因为此时对应方法的真实内容已经被拷贝过来了。但是虽然能做到方法共用,但带来最大的一个问题是如果是同一个项目的不同模块使用的话,使用的每个模块都会把这些拷贝过来,这样会使得应用的内存增大。也恰恰是由于会将静态库的这些方法拷贝进来如果静态库发生改变的话那程序需要重新编译。

    为了解决上面的问题,于是又有了一个动态库的概念。动态库,又称共享库链接的时候它只包含需要的函数引用表,只有在执行的时候那些需要的函数才能被拷贝到内存中,而且在操作系统使用的是虚拟内存,使得一份共享库驻留在内存中被多个程序使用,也同时节约了内存。

       2. 位置无关(PIC

    大家都知道,可执行文件在执行期的时候内存地址已经都确定了,而上面说的只有执行时才会确定那些函数地址拷贝到内存中,那基于这个特点大家第一想道的实现就是像那些段一样预留一个空间,但是这样做的一个最大问题就是会造成空间浪费,我们可以readelf去看下so库中的地址情况,从图一来看和data相关的地址都不是绝对地址(由于程序的起始加载地址都是从data开始,所以data相关的头如果不是绝对地址则可以认为加载的地址不固定)。

                                                                                图一

    在动态共享库中,如果库里面的代码发生改变,重新加载进来之后,我们必须保证它放到修改前的位置  ,否则我们还要为它找一个新的位置。而我们对于这个修改之后希望将动态库编译成可以在任意位置加载无需linker进行修改,这个叫做位置无关代码即PIC,也就是生成so库的-fPIC的那个PIC(这个指令就表示生成位置无关代码)。那PIC的原理是如何实现的呢,我们都知道数据段和代码段的距离在运行时是可以确定的,其中就是利用这一点来做地址定位的。如图二其实就是静态库中的一个变量,他的地址就是%rip+rel

                                                                                      图二

     3.静态分析:

    这个代码无关的特性具体是怎么实现的呢,我们先从静态的角度来分析下这些是怎么执行的。自己写一个引用一个最常见的printf函数(如图三),编译之后通过最常用的objdump –d 反编译,先看下print_banner()对应的反编译代码(如图四)

        

    图三

                                                                图四

    从main函数开始,跳转到print_baner,而print_baner里面最主要的方法是callq400400这个pc值,我们再看下601018(rip+200C12)内存的内容。

                                                                         图五

     使用GDB的看下对应的值是多少,发现这个值是0x400406<printf@plt+6>,即执行后面pushq $0x0,然后再jmpq到<printf@plt-0x10>,即pushq到,然后再jmp到,然后再退出,这样整个printf的方法就执行完了。从静态代码来看只是几次jmp和push就完成了这个在so库中调用printf的操作,的确是这样,不过是这些jmp到的方法有自己的规范和名称,这就是GOT和PLT。

                                                              图六

    1. GOTPLT

    首先我们说过这些是一个规范的有名称的,那么每一个可执行文件只要有这种so库的调用就一定会为他分配特定的存储空间。我们使用readelf看下(图7)

                                  图7

     

    关于GOT(),也叫全局偏移表,由于这个表和静态变量或者静态函数的相对地址是固定的,所以这个表的作用一个很大的作用是用来寻址。在上图中要注意的是.got的权限,是具有写权限的,也就是说这个在后面是会修改里面的值的,这个大家可以在对应的/proc下面去看下,这个地址是在data区的,关于这个是如何的写我们后面再看。

    在反汇编代码中有一个pushq $0x0的操作,这个实际上是将printf对应的GOT数组中的条目方法入栈,且printf的条目偏移地址为0x0,对应GOT条目是一个共享库符号值保留的,而这里的0x0实际上是push第四个GOT条目,即GOT[3],下面是出自计算机系统圣经的CSAPP中GOT表的截图(图中的printf就是和本文so库中的printf条目一样)。

                                                                                      图8

     第一个条目是指.dynamic段;第二个条目是指存放link_map结构的地址,动态链接器利用该地址来对符号进行解析,第三个条目是存放了指向动态链接器_dl_runtime_resolve()函数的地址,该函数用来解析共享库函数的实际符号地址,第四个条目就是printf的PLT[1]地址,也就是<printf@plt>的地址。

    下面说下PLT,在图五的反汇编中可以看到有很多的带plt的方法,这些都是plt表中对应的条目。在图五中可以看到首先进到的是<printf@plt>地址,这些汇编很简单,前面也说过这里的pushq 0x0是将GOT[3]入队,执行完<printf@plt>之后,执行的jmpq到<printf@plt-0x10>中,这里也很指令简单,只不过操作数比较复杂,先说下pushq 0x601008,这里地址就是前面说的GOT[1],即这个程序的link_map,下一条jmpq 0x601010,则是GOT[2],即_dl_runtime_resolve()函数的地址。后续控制权就交给动态链接器了,解析出printf的地址。

    对printf的解析完成之后,后面所有的对PLT条目中printf的调用都会直接跳转到printf中,而不是重新再进行这些跳转。通过watch 第一次jmp的值就可以看到,执行完成之后值以及由0x400406变化到0xFFFFFFFFF7A62800。

                              图9

    东一句西一句啰嗦了这么多,其实总结起来就是对so库的里面方法的调用:

    1. 调用函数先跑到被调用的so库中方法的PLT(printf@plt)方法里面;
    2. PLT代码做一次到GOT中地址的间接跳转;
    3. GOT条目存放了指向PLT的地址,该地址存放在push指令中;
    4. push $0x0指令将printf() GOT条目的偏移量压栈;
    5. 最后的printf() PLT指令是指向PLT-0代码的jmp指令;
    6. PLT-0的第一条指令将GOT[1]的地址压栈,GOT[1]中存放了指向printf()的link_map结构的偏移地址;
    7. PLT-0的第二条指令会跳转到GOT[2]存放的地址,该地址指向动态链接器的_dl_runtime_resolve函数,_dl_runtime_resolve函数会通过把printf()函数的符号值加到.got.plt节对应的GOT条目中,来处理重定位。
    8. 下一次再做跳转的时候PLT条目会直接跳转到函数本身

     

    在这里补充几点:1.为何引用so的方法需要先通过PLT再到GOT,这里这样做是因为这个data段的权限能读能写,而text段只有写的权限;2.这写的是so库方法的加载过程,而如果是仅是变量的话是由/lib/ld-linux.so.2填充的;3.关于_dl_runtime_resolve方法也可以去网上找下源码和实现,还有一些关于重定位相关的内容,等下次再总结分析吧,这个发生在so库之前,还有就是有的时候可以利用GOT的写权限做一些劫持的工作。

     

     

     

     

     

     

     

     

     

    https://www.cnblogs.com/cdcode/p/5551649.html

    https://blog.csdn.net/ylcangel/article/details/18145155

    https://www.jianshu.com/p/eca50b89a423

    https://docs.oracle.com/cd/E24847_01/html/E22196/chapter6-14428.html

    https://www.cnblogs.com/fellow1988/p/6158240.html

    https://blog.csdn.net/linyt/article/details/51635768

    https://blog.csdn.net/conansonic/article/details/54634142

    https://www.cnblogs.com/xingyun/archive/2011/12/10/2283149.html

    https://www.freebuf.com/articles/system/135685.html

    https://bbs.pediy.com/thread-221821.htm

    https://www.cnblogs.com/pannengzhi/p/2018-04-09-about-got-plt.html

    https://www.cnblogs.com/LittleHann/p/4244863.html

     

     

    展开全文
  • LLDB全称Low Level Debugger ,并不是低水平的调试器,而是轻量级的高性能调试器 每个操作系统都会为运行在该系统下的应用程序提供应用程序二进制接口(Application Binary Interface,ABI) 1. 什么是CPU架构及ABI ...
  • 在linux环境下的链接库分为静态链接库(.a库)和动态链接库(.so库),其作用是把C程序编译好做成一种可执行链接文件,主程序文件调用这些程序的函数接口是可以使用a库或so库,在主程序中只需要include含有库中提供...
  • so库

    千次阅读 2019-07-31 09:51:09
    简介 xxx 分类 armeabi armeabi-v7a arm64-v8a x86 x86_64 mips mips64
  • 动态加载so库

    千次阅读 2019-06-21 15:15:24
    今天在集成虹软的人脸demo的时候,发现so库太大也就导致apk体积大。于是用动态加载,理论上是从服务器下载,然后放到指定位置进行加载,这里先在本地进行拷贝。一共两个文件:libarcsoft_face.so和libarcsoft_face_...
  • nm查看共享库so文件中导出函数列表

    万次阅读 2012-11-20 11:04:34
    linux中查看so文件中的导出函数表; > nm -D mylib.so 列出所有导出的函数,包括mylib.so静态链接的中的那些导出函数。
  • Android apk中so库文件未压缩

    万次阅读 2020-03-05 17:59:12
    查看apk包,发现apk 包中so库文件未被压缩. 但是一个类似项目,相同版本却没有问题 升级前 升级后 升级后Raw File Size 正好是未压缩的大小 可能原因 不同版本AGP gradle build task 实现不一样, 再某情况下回不进行...
  • Android NDK开发中,除了可以通过自己编写C/C++代码来构建动态连接库进行调用之外,还可以通过直接调用现成的so库开进行NDK开发。接下来,我将介绍在Android Studio中如何调用第三方动态连接库。 首先需要考虑的是...
  • 原贴:http://soho-schubert.blogspot.com/2007/08/linux.htmlLinux编程使用 昨天写了一小段测试MySQL的C代码,编译顺利,但运行时报告找不到,系统是FC4,MySQL是直接下载的Binary:# gcc test_mysql.c -o test_...
  • 我们很多c程序在windows下是以dll形式展现的,在linux则是以so 形式展现的。 windows一般不会因为编译dll文件的编译器版本不同而出先dll文件不能执行。 但是linux下,不同版本内核的linux下编译的c程序,在其他...
  • AndroidStudio中调用So库方法

    千次阅读 2019-03-06 17:40:13
    Android QQ交流群:813273942 想要调用.so文件,必须先引入.so……是不是有点废话了。 据说没有图片的阅读很麻烦(完全正确啊)…… 首先 ...(1)So库有对应的jar包,把jar包放进libs里就可以...
  • Linux动态(.so)符号表

    万次阅读 2017-02-24 10:42:50
    动态(.so)符号表 最近编译libbinder.so发现system/lib/libbinder.so只有358K,但单独编译生成的obj/SHARED_LIBRARIES/libbinder_intermediates/LINKED/libbinder.so有5M多,原因是前者做了strip命令操作。 1...
  • android64位机子兼容32位.so库文件

    万次阅读 热门讨论 2015-11-21 15:05:49
    猴子在调用高德地图的时候,遇到一个操作系统兼容问题,异常堆栈说是找不到so库文件,猴子就不懂了,明明有他说的那个文件。  后来和同行交流,他们的jniLibs中有一个arm64-v8a的文件夹用来存放64位操作系统的so库...
  • Android使用SO库时要注意的一些问题

    万次阅读 2016-06-05 23:38:42
    或许这些问题对于经常和SO库开发打交道的同学来说已经是老生长谈,但是既然要讨论一整个动态加载系列,我想还是有必要说说使用SO库时的一些问题。 在项目里使用SO库非常简单,在 加载SD卡中的SO
  • 一般linux环境下的链接库分为a库和so库,分别表示静态链接库和动态链接库,其作用是把C程序编译好做成一种可执行链接文件,c主程序文件调用这些程序的函数接口是可以使用a库或so库,在主程序中只需要include含有库中...
  • so库的反编译,反汇编

    万次阅读 2013-12-11 14:11:41
    Linux APP,SO的反汇编工具, ida Pro,可以反汇编app和SO库,有函数名,但是不能反编译到code这一级别。 下载最强的反编译工具 ida Pro 6.4 Plus.rar  还有这个反汇编工具,没用过,转自:...
  • 在/etc/ld.so.conf.d/下创建xxx.conf,在文本中加入.so所在路径,如:/usr/xxx等等,然后使用相应ldconfig命令使之生效。 将.so所在路径添加为PATH环境变量。 在编译命令中使用-Wl,-rpath=./参数,并将相应.so拷贝...
1 2 3 4 5 ... 20
收藏数 329,819
精华内容 131,927
关键字:

so库