Android JNI 实践基础(四) - native hook的一些背景知识

链接器的主要作用

在阅读本小节之前,请确保已经阅读这篇文章编译和链接,有了一些基础知识之后 看这小节就会轻松很多,尤其是刚从java切换到c的同学来说,务必熟读这篇文章

简单来说,假设我和 jack,rose 一起决定写一本书, 3个人各自写各自的模块, 但是不可避免的 我会用到jack和rose的部分小节,那这个时候咋办呢?

我在我的小节就可以这么写: 此处引用jack部分的第N小节。

最后我们3个人的初稿都给到编辑以后,编辑来确认这个N到底是多少,最终装订成一本书。

这个编辑的工作就类似是链接器的作用了。

符号决议

看下面这个c文件

首先确认一个概念,符号 ,符号就是 变量名,包括全局变量名 以及函数名, 局部变量因为模块私有,所以链接器其实不关心局部变量。

链接器在这一步需要做的工作就是确保所有目标文件引用的外部符号都有定义

编译器在生成目标文件的时候, 有代码区数据区的概念

func_b 这个函数 就在代码区中

而 g_a 就在数据区中。

更进一步,编译器在编译的时候 其实把所有的符号都记录了下来,这个东西就是符号表

符号表就干两件事

  1. 我定义了哪些符号(可以给别的模块使用的)
  2. 我用到了哪些符号(自己需要使用的)

正是因为编译器编译出来的目标文件中 包含了符号表的信息,我们的链接器 才知道最终应该找哪些关键信息了(函数的具体实现 等)

动态链接的两种方式

动态链接其实有两种方式,首先可执行文件中,除了代码区和数据区,还有一个重要的部分 叫动态链接信息区。

程序加载时链接:

os其实自带一个东西叫 加载器,任何一个可执行程序 都是要从disk load 到mem中执行的, 这个过程就叫加载, 这个加载的过程会启动 另外一个程序 动态链接加载器 来完成动态库的链接工作。

运行时加载: 程序在运行时 加载动态库,这种是因为 生成可执行程序时 没有提供依赖的so信息,所以这个任务就给程序员自己指定了,在linux下,我们可以通过dlopen dlsym dlclose 等函数 来完成 运行时加载动态库

动态库的缺点

因为动态库 在内存中就只有一份,所以动态库的代码 在编写时 是有限制的,你无法在动态库的代码中 写死一个绝对地址,为啥? 因为动态库 加载到不同的进程以后 所处的地址空间是不同的,你写死一个绝对地址 执行的时候 必然出错。

所以动态库的引用 其实是用的间接寻址的技术,相对于静态库 会有一丢丢的性能损失。

重定位

如果你看汇编代码的话,你会发现 指令中 根本没有变量的任何信息,取而代之的 全部都是 地址信息

比如 一个fun函数调用 ,对应的可能就是

call 0x4004d6 ,

有人就问了,编译器怎么知道 fun函数的地址是0x4004d6? 其实部分情况下编译器也不知道,所以编译器编译完以后指令大概率就是

call 0x00.

但是呢编译器遇到自己不确定地址的指令或者数据,就会放到 .relo.text 和 .relo.data 字段中,这样

链接器 遇到0x00的时候 就知道去 这2个字段中 找关键信息,从而在生成最终的可执行文件中 去修正这条指令了,修正之后就是call 0x4004d6

这个修正指令的过程,也就是修正符号内存地址的过程就叫重定位

到这里我们就可以总结一下,编译器编译出来的目标文件 总共包含5个区域, 分别是

代码区,数据区,符号表,relo.text 和 relo.data

链接器是先知吗?

如果上面的内容你都理解了,到这里你可能会问,链接器怎么知道 变量或者指令 在程序运行起来以后的内存地址呢? 这也太神奇了吧?还是你说错了?

答案其实是 链接器确实是先知,它确实在程序运行之前就知道 运行时的内存地址了。

具体就是 虚拟内存 在这个过程中发挥了巨大作用

可以看一下这个图

程序运行起来以后的样子,一张老图了(64位操作系统)

这里有人肯定会感到奇怪,为什么代码区都是0x4000000开始?如果有2个进程咋办?

答案是 每个进程都认为自己的代码区从0x4000000开始的

虚拟内存的作用就是让 每个程序都有一种幻觉,我是独占内存的,例如32位我就独占4gb内存。。。

搞清楚这个就知道为啥链接器有先知的功能了,至于虚拟内存 怎么 映射到 物理内存上的,其实是一个叫页表的东西,这里就不展开了。

函数调用

对于我们编写的程序来说,一个函数的调用,可以简单的分成 内部调用 ,和外部调用, 所谓的内部调用就是我调用我自己so内部的函数。

这种是最简单的,为啥?因为我编译的时候只要写一个偏移地址就可以了啊。当这个so被装载到内存里以后,这个函数的地址就是 so的基地址+ 这个偏移地址。

外部调用就太麻烦了,为啥,因为你调用的是别的so中的函数啊,那编译的时候怎么知道别人的函数地址呢?显然这个得留到程序运行时,由我们的动态连接器来负责,到程序运行时我们就可以知道别人的函数地址了。

比如我们写的代码,一个malloc函数,对应的是libc.so 这其实就是一个典型的外部调用

这里就牵涉到一个概念。 所谓的elf 文件, 实际上是存在于两份的,有人觉得奇怪 2份是啥意思?

你编译出来的 so文件,是elf吧, 是在硬盘上的, 这种elf 我们用readelf命令来读。

当这个so被load到内存以后,实际上elf会发生变化,不然怎么知道外部调用 最终要访问到哪个地址?

此时这个elf要读取它 就是用objdump来读了。

所以要有一个认知, 同一份elf文件,有2种状态,分别是编译时和运行时,可以用readelf和objudump来分别读取。

hook的基础知识

有了前面的基础知识 ,就可以快速理解一下 的plt hook了。 对于外部调用的函数来说, 如何获得这个函数的地址呢?

其实就是 先去elf 文件里 找一个plt表的东西,这个plt表 指向一个 got表,got表中就有最终的函数地址了

有人觉得奇怪,为啥要多一步呢,我直接去找got表不就行了吗? 其实是为了性能,多一个plt表,可以延迟绑定 最终的函数地址, 相当于是间接引用了。

但是对于android平台来说,其实不存在延迟绑定这个操作,got表的值 在elf文件生成的时候就确定好地址了。

但是要注意噢,这个地址 也仅仅是偏移地址罢了,最终调用时的地址 一定是 外部so文件的基地址 + 这个偏移地址的

关于hook 强烈推荐阅读如下文章

xhook

android性能优化小册

相关推荐
500了6 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵7 小时前
Android Debug Bridge(ADB)完全指南
android·adb
小雨cc5566ru11 小时前
uniapp+Android面向网络学习的时间管理工具软件 微信小程序
android·微信小程序·uni-app
bianshaopeng12 小时前
android 原生加载pdf
android·pdf
hhzz13 小时前
Linux Shell编程快速入门以及案例(Linux一键批量启动、停止、重启Jar包Shell脚本)
android·linux·jar
火红的小辣椒14 小时前
XSS基础
android·web安全
勿问东西15 小时前
【Android】设备操作
android
五味香16 小时前
C++学习,信号处理
android·c语言·开发语言·c++·学习·算法·信号处理
图王大胜17 小时前
Android Framework AMS(01)AMS启动及相关初始化1-4
android·framework·ams·systemserver
工程师老罗19 小时前
Android Button “No speakable text present” 问题解决
android