thumb汇编在ldr pc时容易忽略的一个细节

最近在修花指令,脚本修复 ldr 的时候,如果是从 PC 开始计算,发现ida的表现和keystone 的表现是不一致的,查了很久没找到原因。直到后面翻arm手册,才发现一个小细节。

一、重现方式

1
2
3
4
5
6
7
8
9
10
__attribute__((target("thumb-mode")))
__attribute__((naked))
int func(){
__asm__("ldr r0,[pc,#4]"); // .text+0
__asm__("ldr r1,[pc,#4]"); // .text+2
__asm__(".inst.w 0"); // .text+4
__asm__(".inst.w 0x11111111"); // .text+8
__asm__(".inst.w 0x22222222"); // .text+12
__asm__(".inst.w 0x33333333"); // .text+16
}
1
➜  /tmp $ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi16-clang -c test.c

二、错误理解

看看这段程序,主要也就把 [PC+4]的内容放到 r0和r1里,因为pc在变,所以访问的内存地址也在变,ok。

在thumb模式下,pc永远等于当前地址+4。

在执行第一句的时候,pc=5(因为是thumb),加上4等于9,访问时候去掉最后一个bit,所以r0是0x11111111。

在执行第二句的时候,pc=7(因为是thumb),加上4等于11,访问时候去掉最后一个bit,所以r1是0x22221111。

但是,实际却不是这样的,请看下面的部分。

2019年09月29日:ADR 同理。

三、正确理解

看看objdump,它认为r0是+8位置,r1也是+8位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
➜  /tmp $ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64/arm-linux-androideabi/bin/objdump -d test.o

test.o: file format elf32-littlearm

Disassembly of section .text:

00000000 <func>:
0:4801 ldrr0, [pc, #4]; (8 <func+0x8>)
2:4901 ldrr1, [pc, #4]; (8 <func+0x8>)
4:0000 movsr0, r0
6:0000 movsr0, r0
8:1111 asrsr1, r2, #4
a:1111 asrsr1, r2, #4
c:2222 movsr2, #34; 0x22
e:2222 movsr2, #34; 0x22
10:3333 addsr3, #51; 0x33
12:3333 addsr3, #51; 0x33

看看ida,它直接告诉我,r0=0x11111111,r1=0x11111111。

那么问题在哪呢?我错误的理解是为什么?于是我翻官方手册,翻到了下面这段话。

A8.8.64 LDR (literal) LDR{<c>}{<q>} <Rt>, <label> Normal form LDR{<c>}{<q>} <Rt>, [PC, #+/-<imm>] Alternative form The label of the literal data item that is to be loaded into <Rt>. The assembler calculates the required value of the offset from the Align(PC, 4) value of the instruction to this label.

注意,这里说访问地址时,将 PC 向4对齐后,再进行访问。而我之前错误的理解是,将最后一个bit去掉,其实是向2对齐了。所以,这里报道存在了偏差,对thumb的 PC 理解是没问题的,是 LDR 对这种情况加了特殊处理。

2019年09月29日:ADR 也会收到受到影响,因为ADR也是和 PC 相关的,

1
2
3
4
5
6
7
if ConditionPassed() then
EncodingSpecificOperations();
result = if add then (Align(PC,4) + imm32) else (Align(PC,4) - imm32);
if d == 15 then
ALUWritePC(result);
else
R[d] = result;

四、工具对它的处理

既然ldr语句自身的位置会影响到即将访问到的位置,那么各个工具是如何正确处理的呢? 这里看 pwntools 和 keystone的表现。

1
2
3
4
5
6
7
8
In [145]: asm('ldr r0,[pc,#4];ldr r0,[pc,#4]', arch='thumb').encode('hex')
Out[145]: '01480148'

In [146]: print disasm('01480148'.replace(' ', '').decode('hex'),arch='thumb')
0: 4801 ldr r0, [pc, #4] ; (8 <.text+0x8>)
2: 4801 ldr r0, [pc, #4] ; (8 <.text+0x8>)
In [148]: [hex(c)[2:] for c in ks.asm("ldr r0, $+4;ldr r0,$+4")[0]]
Out[148]: ['1', '48', '1', '48']

汇编时,它们是按照需求去翻译的,没有问题;反汇编时,会根据实际情况稍微算一下。

五、结论

thumb的ldr在处理pc相对位置时,要先让pc向4对齐,然后再访问相对偏移。

2019年09月29日更新:ADR 指令同样需要让pc向 4 对齐。