ARM Thumb BL 指令循环到自身

ARM Thumb BL instruction loops to itself

我正在尝试 assemble 使用 Keystone 并使用 Unicorn 引擎执行此代码:

start:
    add r0, r0, #1
    add r1, r1, #2
    bl start
    b start

在我看来,bl指令应该将下一条指令的地址保存到lr寄存器中,然后跳转到start。所以这将是一个无限循环,将 1 添加到 r0 并将 2 添加到 r1.

显然,我错了,因为 bl start 分支 到它自己 而不是!

我正在使用 Python Keystone、Capstone 和 Unicorn 的包装器来处理程序集。这是我的代码:

import keystone as ks
import capstone as cs
import unicorn as uc

print(f'Keystone {ks.__version__}\nCapstone {cs.__version__}\nUnicorn {uc.__version__}\n')


code = '''
start:
    add r0, r0, #1
    add r1, r1, #2
    bl start
    b start
'''

assembler = ks.Ks(ks.KS_ARCH_ARM, ks.KS_MODE_THUMB)
disassembler = cs.Cs(cs.CS_ARCH_ARM, cs.CS_MODE_THUMB)
emulator = uc.Uc(uc.UC_ARCH_ARM, uc.UC_MODE_THUMB)

machine_code, _ = assembler.asm(code)
machine_code = bytes(machine_code)
print(machine_code.hex())

initial_address = 0
for addr, size, mnem, op_str in disassembler.disasm_lite(machine_code, initial_address):
    instruction = machine_code[addr:addr + size]
    print(f'{addr:04x}|\t{instruction.hex():<8}\t{mnem:<5}\t{op_str}')

emulator.mem_map(initial_address, 1024)  # allocate 1024 bytes of memory
emulator.mem_write(initial_address, machine_code)  # write the machine code
emulator.hook_add(uc.UC_HOOK_CODE, lambda uc, addr, size, _: print(f'Address: {addr}'))
emulator.emu_start(initial_address | 1, initial_address + len(machine_code), timeout=500)

这是它的输出:

Keystone 0.9.1
Capstone 5.0.0
Unicorn 1.0.2

00f1010001f10201fff7fefff8e7
0000|   00f10100    add.w   r0, r0, #1
0004|   01f10201    add.w   r1, r1, #2
0008|   fff7feff    bl      #8         ; why not `bl #0`?
000c|   f8e7        b       #0
Address: 0
Address: 4
Address: 8  # OK, we arrived at BL start
Address: 8  # we're at the same instruction again?
Address: 8  # and again?
Address: 8
< ... >
Address: 8
Address: 8
Traceback (most recent call last):
  File "run_ARM_bug.py", line 32, in <module>
    emulator.emu_start(initial_address | 1, initial_address + len(machine_code), timeout=500)
  File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/unicorn-1.0.2rc3-py3.7.egg/unicorn/unicorn.py", line 317, in emu_start
unicorn.unicorn.UcError: Emulation timed out (UC_ERR_TIMEOUT)

异常不是问题(超时是我自己设置的)。问题是 bl start 总是跳转到自己而不是 start.

如果我跳 向前 ,但是,一切都会按预期工作,所以这有效 - bl 跳转到正确的地址:

start:
    ; stuff
    bl next
    ; hello

next:
    add r0, r0, #1
    bkpt

编辑

我继续 assemble使用 Clang 编写此代码:

; test.s

.text
.syntax unified
.globl  start       
.p2align    1
.code   16       
.thumb_func
start:
    add r0, r0, #1
    add r1, r1, #2
    bl start
    b start

使用了以下命令:

$ clang -c test.s -target armv7-unknown-linux -o test.bin -mthumb
clang-11: warning: unknown platform, assuming -mfloat-abi=soft

然后disassembled test.bin with objdump:

$ objdump -d test.bin

test.bin:       file format elf32-littlearm


Disassembly of section .text:

00000000 <start>:
       0: 00 f1 01 00                   add.w   r0, r0, #1
       4: 01 f1 02 01                   add.w   r1, r1, #2
       8: ff f7 fe ff                   bl      #-4
       c: ff f7 fe bf                   b.w     #-4 <start+0x10>
$ 

所以bl的参数实际上是一个偏移量。这是消极的,因为我们正在倒退。但是,as the documentation says:

For B, BL, CBNZ, and CBZ instructions, the value of the PC is the address of the current instruction plus 4 bytes.

因此 bl #-4 将跳转到 (the address of bl) + 4 bytes - 4 bytes,或者换句话说,再次跳转到它自己!

所以,出于某种原因我不能bl向后?这里发生了什么以及如何解决它?

所有工具"chain" link用户都必须处理函数调用或其他对外部资源的调用,您会看到类似 bl 的指令被编码为指向自身的分支或指向零的分支或类似的不完整说明(当然是针对外部标签)。这里的切线是某些版本的 clang 似乎有时会为本地地址编码,有时不会(在 assembler 级别)。但是当 linked 时 offset/address 被修补了(就像在这种情况下)。

对象级别的通用 clang(所有目标,默认 x86 主机)3.7 给出了正确的指令。 3.8 没有。那似乎是这种变化发生的时间。 Clang 10 generic 不是针对一个目标的手工构建的 clang 10.0.0,它确实在 assemble 时间给出了正确的答案。

所有这些都是切线,因为那是在汇编时而不是最终输出。当 linked 你得到正确的答案(到目前为止,OP 可能有其他情况没有)。

.thumb
.syntax unified
.thumb_func
start:
    add r0, r0, #1
    add r1, r1, #2
    bl start
    b start

clang-3.8 -c so.s -target armv7-unknown-linux -o so.o
clang: warning: unknown platform, assuming -mfloat-abi=soft
arm-none-eabi-objdump -D so.o

so.o:     file format elf32-littlearm


Disassembly of section .text:

00000000 <start>:
   0:   f100 0001   add.w   r0, r0, #1
   4:   f101 0102   add.w   r1, r1, #2
   8:   f7ff fffe   bl  0 <start>
   c:   e7f8        b.n 0 <start>

bl 这是self的一个分支,不完整。

但是拿那个东西 link 它

arm-none-eabi-ld -Ttext=0 so.o -o so.elf
arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 0000000000000000
arm-none-eabi-objdump -d so.elf

so.elf:     file format elf32-littlearm


Disassembly of section .text:

00000000 <start>:
   0:   f100 0001   add.w   r0, r0, #1
   4:   f101 0102   add.w   r1, r1, #2
   8:   f7ff fffa   bl  0 <start>
   c:   e7f8        b.n 0 <start>

你得到了正确的答案。

很抱歉在我偏离正题之前的误导性回答。

现在,如果 linking 无法在所有情况下为您解决问题,请发表评论。

问题的另一部分是工具无法帮助您:

0008|   fff7feff    bl      #8         ; why not `bl #0`?

8: ff f7 fe ff                   bl      #-4

这是同一条指令,以前是一对拇指指令 0xF7FF、0xFFFE 但对于 armv7-ar,它被认为是一条指令,不可分割 0xF7FFFFFE.

感谢再次查找以解决这个问题,我发现了这一点,因为我要么知道但忘记了,要么不知道。

Before ARMv6T2, J1 and J2 in encodings T1 and T2 were both 1, resulting in a smaller branch range. The instructions could be executed as two separate 16-bit instructions

我已经证明了在 armv7 架构之前这两条指令是相互独立的,并且表明它们不是一条指令。

无论如何:

与 gnu 中的指令相同

   8:   f7ff fffe   bl  0 <start>

gnu one 好一点但是还是有问题,编码不是 bl 0 <start> 但是那个输出表明了最终的愿望,最后在 [=51 时被重新编码为正确的=]编辑。

所以这些工具也可能是理解正在发生的事情的问题的一部分,因为它没有以正确的可解码格式表示机器代码。