Android Linker学习笔记
2016-01-23 09:57
211 查看
原文地址: http://drops.wooyun.org/tips/12122
Linker是Android系统动态库so的加载器/链接器,要想轻松地理解Android linker的运行机制,我们需要先熟悉ELF的文件结构,再了解ELF文件的装入/启动,最后学习Linker的加载和启动原理。
鉴于ELF文件结构网上有很多资料,这里就不做累述了。
我们知道如果一个APP需要使用某一共享库so的话,它会在JAVA层声明代码:
此代码完成library的加载工作。翻看system.loadLibrary的源代码,可以发现:
System.loadLibrary也是一个native方法,它的调用的过程是:
打开函数dvmLoadNativeCode,可以找到以下代码:
从上面的代码可以看出Android系统加载共享库的关键代码为dlopen函数。这个dlopen函数的代码在bionic/linker/dlfcn.c中:
此函数主要通过调用
继续查看
显然,重点在
继续往下深入:
先不去关心那些错误处理信息,我们假设各个函数的返回值均在预期范围内,这个函数的执行流程为:
使用find_loaded_library函数在已经加载的动态链接库链表里面查找该动态库。如果找到了,就返回该动态库的soinfo,否则执行第②步;
此时,说明指定的动态链接库还没有被加载,就使用load_library函数来加载该动态库。
使用open_library函数打开指定so文件;
创建ElfReader类对象,并通过该对象的load方法,读取Elf文件头,然后通过分析Elf文件来加载各个segments;
使用soinfo_alloc函数分配一个soinfo结构体,并为这个结构体中的各个成员赋值。
下面对
Linker使用ElfRead类的load函数完成so文件的分析工作。该类的源代码在
显然此函数依次调用ReadElfHeader、ReadProgramHeader等函数。
首先,我们需要知道Android系统加载segments的机制:
一个ELF文件的程序头表包含一个或多个
当前,我们忽略
可以加载的segments能在虚拟地址范围
各个segments的虚拟地址范围不可重叠;
如果一个segment的
segment的虚拟地址范围的起、始地址不是必须在某一页的边界。两个不同的segments的起、始地址可以在同一页,在这种情况,该页继承后一segment的映射标记(mapping flags)
每一个segment实际加载的地址并非p
下面是两个loadable segments的信息:
相当于这两个segments的虚拟地址范围分别为:
如果加载器决定将第一个segment加载到0xa0000000的话(通过后面的分析会知道,这个加载地址是在加载程序头部表的时候由系统确定的),那么它们的实际虚拟地址范围就是:
换句话说,所有的segments的实际加载开始地址与其vaddr的偏差值是固定的(0xa0030000 – 0x30000 = 0xa0040000 – 0x40000)。
但是,在实际情况下,segments的地址并不是在每一页的边界出开始的。考虑到我们只能在页面边界进行内存映射,因此,这就意味着加载地址的偏差bias应当按照如下方法进行计算:
所以第一个segment的
这里
注意:ELF要求如下条件,以满足mmap正常工作:
每一个loadable segments的
1.1.1 ReadProgramHeader
理清了Android加载segments的机制,我们就来看linker中的实际代码,先看ReadProgramHeader:
首先读取elf文件的程序头部表项数目
然后分别获取程序头部表在页边界对齐后的起始地址
再以只读模式建立一个私有映射,该映射将elf文件中偏移值为
(注:红色代码为倒数第三句)
首先
然后
最后再将其转换成
1.1.2 ReserveAddressSpace
再来看ReserveAddressSpace:
这里有一个关键函数
通俗点讲,此函数就是返回ELF文件中包含的可加载segments总共需要占用的空间大小,并设置其最小虚拟地址的值(是页对齐的)。值得注意的是,原函数有4个参数,但是在ReserveAddressSpace中调用该函数时却只传递了3个参数,忽略了
现在回到ReserveAddressSpace函数。求得
1.1.3 LoadSegments
现在就开始加载ELF文件中的可加载segments了:
此部分功能很简单:就是将ELF中的可加载segments依次映射到内存中,并进行一些辅助扫尾工作。
1.1.4 FindPhdr
返回程序头部表在内存中地址。这与
要理解这段代码,我们需要知道段类型PT_PHDR所表示的意义:指定程序头表在文件及程序内存映像中的位置和大小。此段类型不能在一个文件中多次出现。此外,仅当程序头表是程序内存映像的一部分时,才可以出现此段。此类型(如果存在)必须位于任何可装入段的各项的前面。有关详细信息,请参见程序的解释程序。
至此so文件的读取、加载工作就分析完毕了。我们可以发现,Android对so的加载操作只是以段为单位,跟section完全没有关系。另外,通过查看VerifyElfHeader的代码,我们还可以发现,Android系统仅仅对ELF文件头的
在1.1我们详细分析了Android so的加载机制,现在就开始分析so的链接机制。在分析linker的关于链接的源代码之前,我们需要学习ELF文件关于动态链接方面的知识。
1.2.1 动态节区
如果一个目标文件参与动态链接,它的程序头部表将包含类型为
该
首先,我们需要从程序头部表中获取dynamic节区信息:
此函数很简单:
成功获取了dynamic节区信息,我们就可以根据该节区中的
完成dynamic数组的遍历后,就说明我们已经获取了其中的有用信息了,那么现在就需要根据这些信息进行处理:
上面的
如果获取的si不为空,就说明so的加载和链接操作正确完成,那么就可以执行so的初始化构造函数了:
由于我们只分析so库,所以只需要关心
这里需要对
首先是
这里共三个函数指针,每个指针指向一个函数地址。值得注意的是,上图中每个函数指针的值都加了1,这是因为地址的最后1位置1表明需要使得处理器由ARM转为Thumb状态来处理Thumb指令。将目标地址处的代码解释为Thumb代码来执行。
然后再来看CallFunction的具体实现:
至此,整个Android so的linker机制就分析完毕了!
0x00 知识预备
Linker是Android系统动态库so的加载器/链接器,要想轻松地理解Android linker的运行机制,我们需要先熟悉ELF的文件结构,再了解ELF文件的装入/启动,最后学习Linker的加载和启动原理。鉴于ELF文件结构网上有很多资料,这里就不做累述了。
0x01 so的加载和启动
我们知道如果一个APP需要使用某一共享库so的话,它会在JAVA层声明代码:System.loadLibrary也是一个native方法,它的调用的过程是:
do_dlopen函数来返回一个动态链接库的句柄,该句柄为一个soinfo结构体。Soinfo结构体的具体定义在
bionic/linker/linker.h中。
继续查看
do_dlopen函数,代码在linker.cpp中:
find_library函数。此函数代码如下:
使用find_loaded_library函数在已经加载的动态链接库链表里面查找该动态库。如果找到了,就返回该动态库的soinfo,否则执行第②步;
此时,说明指定的动态链接库还没有被加载,就使用load_library函数来加载该动态库。
load_library函数是整个so加载过程的重中之重!它创建了动态链接库的句柄,代码如下:
load_library函数的执行过程可以概括如下:
使用open_library函数打开指定so文件;
创建ElfReader类对象,并通过该对象的load方法,读取Elf文件头,然后通过分析Elf文件来加载各个segments;
使用soinfo_alloc函数分配一个soinfo结构体,并为这个结构体中的各个成员赋值。
下面对
步骤二加以详细介绍。
1.1 SO文件的读取与加载工作
Linker使用ElfRead类的load函数完成so文件的分析工作。该类的源代码在linker_phdr.cpp中。Load函数代码如下:
首先,我们需要知道Android系统加载segments的机制:
一个ELF文件的程序头表包含一个或多个
PT_LOAD segments,这些segments标志ELF文件中需要被映射到进程空间的区域。每一个可以加载的segment都含有如下重要属性:
p_offset: 段在文件的偏移地址
p_filesz:段的大小
p_memsz:段在内存中占据的大小(通常大于p_filesz)。
p_vaddr: 段的虚拟地址
p_flags:段的标记(可读,可写,可执行)
当前,我们忽略
p_paddr和
p_align成员。
可以加载的segments能在虚拟地址范围
[p_vaddr…p_vaddr+p_memsz)以列表的形式展现。其中有如下几个规则:
各个segments的虚拟地址范围不可重叠;
如果一个segment的
p_filesz小于
p_memsz,那么两者之间的额外数据将被初始化为0;
segment的虚拟地址范围的起、始地址不是必须在某一页的边界。两个不同的segments的起、始地址可以在同一页,在这种情况,该页继承后一segment的映射标记(mapping flags)
每一个segment实际加载的地址并非p
_vaddr。而是由加载器决定将第一个segment加载到内存中的哪个位置,然后剩下的segments就以第一个segment为参照物,进行加载。比如:
下面是两个loadable segments的信息:
但是,在实际情况下,segments的地址并不是在每一页的边界出开始的。考虑到我们只能在页面边界进行内存映射,因此,这就意味着加载地址的偏差bias应当按照如下方法进行计算:
load_bias= 0xa0030000 – 0x30000&0xfffff000 = 0xa00000000。
这里
phdr0_load_address必须以某一页的边界为起始地址,所以该segments的真正内容的开始地址为:
p_vaddr都必须加上
load_bias,其和就是该segments在内存中的实际开始地址。
1.1.1 ReadProgramHeader
理清了Android加载segments的机制,我们就来看linker中的实际代码,先看ReadProgramHeader:
phdr_num;
然后分别获取程序头部表在页边界对齐后的起始地址
page_min、结束地址
page_max和偏移地址
page_offset。并根据
page_max与
page_start计算出程序头部表占据的页面大小
phdr_size;
再以只读模式建立一个私有映射,该映射将elf文件中偏移值为
page_min,大小为
phdr_size的区域映射到内存中。将映射后的内存地址赋给
phdr_mmap_,简单一句话:将程序头部表映射到内存中,并将内存地址赋值;
reinterpret_cast<new_type>(expression),这是c++中的强制类型转换符,类似于
(new_type*)(expression)。这里我们对上面红色部分代码加以解释:
(注:红色代码为倒数第三句)
首先
reinterpret_cast<char*>(mmap_result):经
void*型指针
mmap_result强制转换成
char*型;
然后
reinterpret_cast<char*>(mmap_result) + page_offset:
char*型指针+
page_offset,表示指向程序头部表真正开始的地方;
最后再将其转换成
ElfW(Phdr)*型指针,显然
phdr_table_指向程序头部表开始地址。
1.1.2 ReserveAddressSpace
再来看ReserveAddressSpace:
phdr_table_get_load_siz:
out_max_vaddr。在我个人看来是因为已知了
out_min_vaddr及两者的差值
load_size,所以可以通过
out_min_vaddr + load_size来求得
out_max_vaddr。
现在回到ReserveAddressSpace函数。求得
load_size之后,就需要为这些segments分配足够的内存空间。这里需要注意的是mmap的第一个参数并非为Null,而是addr。这就表示将映射区间的开始地址放在进程的addr地址处(一般不会成功,而是由系统自动分配,所以可以看作是Null),mmap返回实际映射后的内存开始地址start。显然
load_bias_ = start – addr就是实际映射内存地址同linker期望的映射地址的误差值。后面的操作中,linker就可以通过
p_vaddr + load_bias_来获取某一segments在内存中的开始地址了。
1.1.3 LoadSegments
现在就开始加载ELF文件中的可加载segments了:
1.1.4 FindPhdr
返回程序头部表在内存中地址。这与
phdr_table_是不同的,后者是一个临时的、在so被重定位之前会为释放的变量:
至此so文件的读取、加载工作就分析完毕了。我们可以发现,Android对so的加载操作只是以段为单位,跟section完全没有关系。另外,通过查看VerifyElfHeader的代码,我们还可以发现,Android系统仅仅对ELF文件头的
e_ident、
e_type、
e_version、
e_machine进行验证(当然,
e_phnum也是不能错的),所以,这就解释了为什么有些加壳so文件头的section相关字段可以任意修改,系统也不会报错了。
1.2 so的链接机制
在1.1我们详细分析了Android so的加载机制,现在就开始分析so的链接机制。在分析linker的关于链接的源代码之前,我们需要学习ELF文件关于动态链接方面的知识。1.2.1 动态节区
如果一个目标文件参与动态链接,它的程序头部表将包含类型为
PT_DYNAMIC的元素。此“段”包含
.dynamic节区(这个节区是一个数组)。该节区采用一个特殊符号
_DYNAMIC来标记,其中包含如下结构的数组:
Elf32_Dyn数组就是soinfo结构体中的dynamic成员,我们在第2节介绍的
load_library函数中发现,
si->dynamic被赋值为null,这就说明,在加载阶段是不需要此值的,只有在链接阶段才需要。Android的动态库的链接工作还是由linker完成,主要代码就是在linker.cpp的
soinfo_link_image(
find_library_internal方法中调用)中,此函数的代码相当多,我们来分块分析:
首先,我们需要从程序头部表中获取dynamic节区信息:
Elf32_Dyn数组来进行so链接操作了。我们需要从dynamic节区中抽取有用的信息,linker采用遍历dynamic数组的方式,根据每个元素的flags()进行相应的处理:
0x02 开始执行so文件
上面的find_library_internal函数中的
soinfo_link_image函数执行完后就返回到上层函数
find_library中,然后进一步返回到
do_dlopen函数:
CallArray("DT_INIT_ARRAY", init_array, init_array_count, false)函数即可:
init_array节区的结构和作用加以说明。
首先是
init_array节区的数据结构。该节中包含指针,这些指针指向了一些初始化代码。这些初始化代码一般是在main函数之前执行的。在C++程序中,这些代码用来运行静态构造函数。另外一个用途就是有时候用来初始化C库中的一些IO系统。使用IDA查看具有
init_array节区的so库文件就可以找到如下数据:
这里共三个函数指针,每个指针指向一个函数地址。值得注意的是,上图中每个函数指针的值都加了1,这是因为地址的最后1位置1表明需要使得处理器由ARM转为Thumb状态来处理Thumb指令。将目标地址处的代码解释为Thumb代码来执行。
然后再来看CallFunction的具体实现:
相关文章推荐
- Android Activtity Security
- Android应用安全开发之源码安全
- Android之TextView的样式类Span的使用具体解释
- Android应用方法隐藏及反调试技术浅析
- Android应用公布的准备——渠道注冊与认证
- android高级控件之Volley
- Android之SurfaceView学习(一)
- android sim 卡短信读写
- android 启动页启动慢或白屏的解决方法
- Android应用资源文件格式解析与保护对抗研究
- Android Hook(2) Java2java
- Android Hook(1) Dexposed原理
- Android ImageView图片自适应
- Android 创建内容提供器(ContentResolver)
- android auto-Providing Messaging for Auto(UnreadConversation)
- Android Studio——SAX解析XML
- Android 编码规范及代码风格
- Android 命名规范 (提高代码可以读性)
- Android LayoutInflater深度解析
- android auto-Providing Audio Playback for Auto