深入分析Android native exception框架---native编译

来源:互联网 发布:网店数据分析毕业论文 编辑:程序博客网 时间:2024/06/05 11:41
概要
    从前面的章节我们了解到,Android native程序都是用arm-linux-androideabi-gcc编译的。
    了解gcc如何将*.c/cpp编译成*.o再将其链接为可执行程序或/lib库,有助于我们将native从编译/加载/执行到崩溃一条路贯通起来。
    Android的Makefile只需要将source file填入LOCAL_SRC_FILES,然后include $(BUILD_SHARED_LIBRARY)或$(BUILD_EXECUTABLE)就可以将*.c/cpp/s编译为动态库或可执行程序。其中编译系统做了很多工作,我们不会介绍其中的原因,想要了解的话应该看build相关的文档。
    本章节会讲解gcc部分参数的原理及链接的过程。
1. 编译为obj
    在build/core/definitions.mk有定义transform-c-or-s-to-o-no-deps和transform-cpp-to-o,分别将每个*.c/s和*.cpp编译成*.o,里面传了很多参数给gcc,其中-fpic -fPIE和-fstack-protector是下面会讲解的。
(1). -fpic -fPIE
    PIC是Position-Independent Code的缩写,经常被用在共享库中,这样就能将相同的库代码为每个程序映射到一个位置,不用担心覆盖掉其他程序或共享库。
    PIE是Position-Independent-Executable的缩写,只能应用在可执行程序中。PIE和PIC很像,但做了一些调整(不用PLT,使用PC相关的重定位)。-fPIE给编译用,-pie给链接(ld)用。
    例如,一个程序没有使用PIC被链接到0地址,那么系统将其加载到0地址,程序可以正常运行:
    如果系统将其加载到0x8000,程序会异常:
    如果开启PIC,这会是这样:
    使用了什么技术可以达到这样呢?
    a. 对于访问自己的函数等可以使用以PC+偏移量的方式达到效果:

    b. 而访问外部共享库等需要GOT/PLT支持才行。
        PLT(Procedure Linkage Table): 由一个个桩函数组成的跳转表
        GOT(Global Offset Table): 由一个个偏移量组成的偏移表,决定PLT跳转的位置(由linker修改)
    要调用外部的函数会先跳转到PLT,PLT再从GOT获取实际的位置,再跳转过去。PLT/GOT类似一座桥:
    实例分析
    某个程序的函数xxx()调用外部函数__futex_wait(),xxx()在.text段里会直接跳转到__futex_wait的PLT里,
PLT会读取对应GOT的地址,直接跳转过去:
    GOT表的地址会在linker加载库时被修改,因为所有的库都是由linker加载的,也只有linker知道__futex_wait()被加载到哪里去了。
    这里linker会有两种做法:
        第1:在加载库时就把GOT全部修改好,后面程序调用就无需修改,加快了程序的运行,但启动较慢。这个就是所谓立即绑定,Android目前使用这种方法。
        第2:先不改GOT,此时GOT默认会指向第1个PLT,这个PLT会比较特殊,linker会修改这个PLT对应的GOT,使它指向linker某个函数,那这样只有GOT没有被修改下会自动跳转到linker,由linker再去单独修改这个GOT,达到按需修改,相比第1种,可以快速启动,但可能影响运行。这种也称为迟绑定技术(Lazy binding)。
    大家看到这里肯定会想,方法有了,那linker将GOT修改为什么值呢?其实在动态库里有.rel.dyn.rel.plt段,里面包含了哪里要修改,修改的方法和对应的符号,linker就可以轻松完成这件艰巨的任务:
(2). -fstack-protector
    顾名思义就是保护堆栈,每一个函数在运行时都有自己的栈帧,如果代码没有写好,很可能将自己甚至是其他的栈帧踩坏,那如何防护呢?简单的方法就是在栈帧头部也就是在局部变量开始之前多存储一个__stack_chk_guard值,用于在函数返回前取出来和_stack_chk_guard做对比,失败则调用__stack_chk_fail函数,这个就是该参数完成的行为。
    以下是示意图:
    打开该功能后,编译会自动插入所需代码:
    当然该参数不是所有的函数都保护的,一般踩坏栈帧多半是for/while循环迭代数组,所以可以如下配置:
        -fstack-protector:只保护有定义局部数组且数组个数大于8的函数。
        --param=ssp-buffer-size=xx:在-fstack-protector基础上增加该参数可以修改数组个数。
        -fstack-protector-all:保护所有函数。
    发生stack corruption时会跑到__stack_chk_fail(),印出相关信息:

2. 静态链接
    build/core/combo/TARGET_linux-arm.mk里有定义transform-o-to-static-executable-inner,将*.o链接成静态可执行程序,静态可执行程序是一个完整的程序,不需要额外的共享库即可执行,比如/init,/sbin/adbd等。
    链接器用的是arm-linux-androideabi-g++,主要的参数和介绍如下(这里以test.c编译为test为例子):
        -Bstatic ##表示静态链接##
        out/target/product/$proj/obj/lib/crtbegin_static.o
        out/target/product/$proj/obj/EXECUTABLES/test_intermediates/test.o
        prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-$version/lib/gcc/arm-linux-androideabi/$version-google/armv7-a/libgcc.a
        out/target/product/$proj/obj/lib/crtend_android.o
    大家注意到没有,除了test.o居然还链接了crtbegin_static.o/libgcc.a和crtend_android.o,这是怎么回事??
    libgcc.a后面会详细讲,而crtbegin_static.o/crtend_android.o对应代码是(从bionic/libc/Andoird.mk看出)bionic/libc/arch-arm/bionic目录下的crtbegin.c和crtend.s,其中定义了.preinit_array,.init_array和.fini_array段以及_start函数。
    再查看gcc默认的链接脚本(prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-$version/arm-linux-androideabi/lib/ldscripts/armelf_linux_eabi.x):
    里面定义了入口函数是_start()(注意不是main()!!!),以及加载位置是0x8000。其中crtbegin.c至关重要,main()函数在__libc_init()里调用到,而之前已经做了很多事,这也就是crt(c-runtime,c运行环境)的工作。
3. 动态链接
    build/core/combo/TARGET_linux-arm.mk里有定义transform-o-to-executable-inner和transform-o-to-shared-lib-inner,分别将*.o链接为动态可执行程序和共享库。动态可执行程序需要linker才能进一步运行的。链接器也是用arm-linux-androideabi-g++,下面分析讲解:
(1). 动态可执行程序
    主要的参数和介绍如下:
        -Bdynamic ##表示动态链接##
        -fPIE ##链接为位置无关##
        -pie
        -Wl,-z,now ##表示立即绑定##
        -Wl,-dynamic-linker,/system/bin/linker ##指定解释器##
        -lc -lstdc++ -lm ##所需的lib库,需要显式指定,否则编译报错##
        out/target/product/$proj/obj/lib/crtbegin_dynamic.o
        out/target/product/$proj/EXECUTABLES/test_intermediates/test.o
        prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-$version/lib/gcc/arm-linux-androideabi/$version-google/armv7-a/libgcc.a
        out/target/product/$proj/obj/lib/crtend_android.o
    相比静态链接,多了PIE,立即绑定,解释器。crtbegin_dynamic.o和crtbegin_static.o一样。另外的差别是加载地址可以有系统决定,不像静态链接程序固定到0x8000。
(2). 共享库
    主要的参数和介绍如下:
        -Wl,-shared,-Bsymbolic ##编译为共享库##
        -Wl,-z,now ##表示立即绑定##
        -lc -lstdc++ -lm ##该lib所需的lib库,需要显式指定,否则编译报错##
        out/target/product/$proj/obj/lib/crtbegin_so.o
        out/target/product/$proj/EXECUTABLES/test_intermediates/test.o
        prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-$version/lib/gcc/arm-linux-androideabi/$version-google/armv7-a/libgcc.a
        out/target/product/$proj/obj/lib/crtend_so.o
    相比动态可执行程序,没有太大变化,不过crtbegin_so.o和crtend_so.o少了_start()等相关代码,共享库不需要入口函数。
4. 移除调试信息
    链接后的程序一般都比较大,因为包含了调试信息,而这些信息不会在运行时用到,所以会将其删除后在放入手机里。这里用到了arm-linux-androideabi-strip --strip-all功能:
5. libgcc.a
    GCC在一些平台上提供了一个低级运行时库,libgcc.a或者libgcc_s.so.1. 一旦需要执行某些过于复杂而无法通过内嵌代码实现的操作,GCC便会自动生成对这些库函数的调用。
    大多数libgcc中的函数用来处理目标处理器不能直接执行的算术运算(包括整数乘除,所有浮点运算), 还包括异常处理, 少数杂项操作。
    详细请参考: 
http://gcc.gnu.org/onlinedocs/gccint/Libgcc.html#Libgcc
6. crt*.o
    这些都是c运行时库,用于执行进入main之前的初始化和退出main之后的扫尾工作:
        __PREINIT_ARRAY__, __INIT_ARRAY__:初始化时调用的函数数组
        __FINI_ARRAY__: 结束时调用的函数数组
        __CTOR_LIST__: 初始化时调用的构造函数数组
    这些数组都是以-1开始,以0结束:
    对应的代码(crtbegin.c)如下:
    
结语
    上面只是讲解编译涉及到的部分知识,实际上用到的技术更多,需要大家自己深入了解和实践。
0 0
原创粉丝点击