本文主要讨论两部分内容:
-
分支指令,B、BL 等
-
v7中的模式切换,arm切thumb,thumb切arm。理解了模式切换就会明白为什么在做 inline hook 时,有些地址需要加上1,加上 1 的作用是什么。
B
B指令是无条件跳转指令。看描述是一个相对于PC地址的跳转,范围是使用 imm26 来描述。26 位表示的范围是 +/-128M。为啥会是 128M 呢?是因为最后的值还需要乘以4。
举个例子:
在 000000000001EDA4
处有一条 B 指令,执行完该指令之后就会直接跳转到 000000000001EDAC
处,IDA使用一条带箭头的线给出了提示。
之前在学习 so 的时候,写过一个死循环指令:
00 00 00 14
我们可以分析一下其具体的每一位:
00010100 00000000 00000000 00000000 ->
0 00101 00000000000000000000000000
因为是跳转到自己位置,所以 imm26 是 0,很好理解。
对比了一下 v7 与 v8 中的死循环指令,发现一个未曾注意到的区别:
v8: 00 00 00 14
v7: fe ff ff ea
在 v7 中,feffff 表示的是 -2,由于 pc = pc + 8,所以算下来,offset = 8 + (-2)* 4 = 0。
但是在 v8 中,offset 就直接是 imm26 的值,这是否说明在 v8 中,pc就是自身的值?暂时未找到具体文档,先留个疑问。
在 V7 中,存在 arm 与 thumb 指令,那么 B 指令跳转会发生模式切换吗?是不会的,编译器可以保证,除非你修改了指令代码,如果在 B 指令中发生了模式切换,肯定是会出问题的,因为 T 位没有发生变化,执行 B 指令的跳转后,CPU 仍会将跳转后的模式当成跳转前的模式来处理。所以,如果你修改了指令代码,切记保证跳转前后的模式一致。
BL
BL指令与B指令几乎一样,唯一的区别就是它跳转的时候会将下一条指令(PC+4,这里也说明了PC的值就是所见即所得)的地址写入到LR寄存器。这条指令表示调用了一个函数。
看V8中的一个例子:
按下 F7:
可以看到发生了两件事:
-
指令跳转到了目标地址去执行
-
X30 寄存器的值被修改成了BL指令的下一条指令地址,但是这个说法并不准确,因为在 v7 中表现稍微有点不一样。在 arm 模式中,储存的是BL指令的下一条指令地址,在 thumb 模式中,储存的的是下一条指令的地址+1,这个1 是 t 标志位的值。
按下 F7:
看到,LR 寄存器的值变成了 0xB708160F
,这是一个奇数,指令的地址不可能是一个奇数。它的值是 B708160E
的值加上1,这个1表示当从函数跳转回来继续执行的时候,需要以 thumb 模式来执行。
BX
在 v8 中,没有BX指令了,猜测因为BX指令相比B和BL指令的不同之处在于它可以描述模式切换,但是v8中没有模式切换了,所以BX指令就没必要了。
BX后面只能跟寄存器。
.text:000005C8 10 FF 2F E1 BX R0
这里有一个BX指令,它跳转到 R0 指向的地址,一般情况下,R0是直接储存的目标地址,这个时候,表示模式不会切换。当它储存的地址是目标地址+1
的时候,就会发生模式切换。
BLX
这个也是带模式切换的,v8中也没了。
BLX后可以跟寄存器或者立即数。当 BLX 后面跟的是立即数是,一定会发生模式切换。
比如:
BLX sub_B^EBA934
如果当前是arm模式,执行后变成 thumb 模式,如果当前是 thumb 模式,执行后变成 arm 模式。
BLX 与 BL 一样,也会写 LR 寄存器的值,写入的值规则也是一样,下一条指令地址 + t 标志位的值。
BLX 后面跟寄存器的时候,不一定会发生模式切换。如果想带模式切换,需要将寄存器的值+1,这个时候就会进行从 arm 到 thumb 模式的切换。如果本身就处于 thumb 模式,那么寄存器的值就不需要+1,因为寄存器最后一位的值会直接写入到t标志位。当处于 thumb 模式时,寄存器最后一位是0,就会进行从 thumb 到 arm 模式的切换。
总结一下:BX 与 BLX 指令会有模式切换,当指令跟着寄存器的时候,寄存器的最后一位的值会影响模式的切换,最后一位是 0 ,表示需要切换到 arm 模式,最后一位是 1,表示需要切换到 thumb 模式。当前模式与需要切换的模式不一致时,就会发生模式切换。
V7导出函数的arm与thumb模式
有一个问题:就是libc.so中有许多的导出函数,有些函数可能是以arm模式运行,有些函数可能是要以thumb模式运行,那么当我们使用这些函数的时候,程序是如何知道该函数要以何种模式运行的呢?
我们先看看 IDA 中的导出表的 printf 这个函数:
printf 000528A4
可以看到它的地址是一个偶数。
我们再使用 readelf 程序看看这个符号的信息:
6115: 000528a5 84 FUNC GLOBAL DEFAULT 13 printf
发现,这个地址是一个奇数,这说明了,跳转到 printf 函数需要切换成 thumb 模式来执行。
IDA中展示的是函数的真实地址,但是没有带模式,readelf 显示的信息中带上了模式信息。
MOV PC, reg
我们上面讨论 BX 指令的时候,BX会直接跳转到目标地址,相当于是改写了 pc 寄存器的值,所以有些人就觉得mov pc, reg
与 BX 指令就是相等的。但是显然不是,因为 mov 不具有切换模式功能。就算想使用 mov 模拟 BX 来进行模式切换,比如 mov pc, target_addr + 1
,这个指令只会出异常,而不会切换模式。因为奇数地址是非法的。BX指令实际上是将最后一位给了 t 标志位,而不是真的跳转到一个奇数地址。总之,不要使用 mov 指令来修改 PC 寄存器。
LDR PC, [reg]
这个指令就不同与MOV指令了,它是支持模式切换的,与 BX 指令一样,实际上我们看一下IDA的反汇编代码,会发现plt的调用就是使用的 LDR 指令:
.plt:00000540 ; int getchar(void)
.plt:00000540 getchar ; CODE XREF: main:loc_604↓p
.plt:00000540 00 C6 8F E2 ADR R12, 0x548
.plt:00000544 01 CA 8C E2 ADD R12, R12, #0x1000
.plt:00000548 AC FA BC E5 LDR PC, [R12,#(getchar_ptr - 0x1548)]! ; __imp_getchar
这里调用 getchar 函数的时候,就是使用了 LDR 指令,赋值个 PC 的地址有可能是奇数,也有可能是偶数,为奇数表示需要切成 thumb 模式来执行。
再看一个例子:
.text:0000068E D0 B5 PUSH {R4,R6,R7,LR}
.text:00000690 02 AF ADD R7, SP, #8
.text:00000692 0C 46 MOV R4, info
.text:00000694 info = R4 ; unw_proc_info_t_0 *
.text:00000694 co = R0 ; libunwind::AbstractUnwindCursor *
.text:00000694 01 68 LDR R1, [co]
.text:00000696 4A 6A LDR R2, [R1,#0x24]
.text:00000698 21 46 MOV R1, info
.text:0000069A 90 47 BLX R2
.text:0000069A
.text:0000069C 61 68 LDR R1, [info,#4]
.text:0000069E 00 20 MOVS R0, #0
.text:000006A0 00 29 CMP R1, #0
.text:000006A2 04 BF ITT EQ
.text:000006A4 4E F2 6B 60 MOVWEQ R0, #0xE66B
.text:000006A8 CF F6 FF 70 MOVTEQ R0, #0xFFFF
.text:000006AC D0 BD POP {info,R6,R7,PC}
这是一段程序,可以看到,首先将 LR 等寄存器储存起来,我们知道,LR 会由 BX 修改,它的值可能是奇数也可能是偶数。再看最后一条指令,将储存起来的 LR 的值,赋值给了 PC,这就相当于是使用了 LDR 指令了。这个时候,PC 的值就可能是奇数,会发生模式切换。