14.2 ARM

ARM处理器,就像其他的“纯”RISC处理器一样,缺少除法指令,缺少32位常数乘法的单条指令。利用一个技巧,通过加法,减法,移位是可以实现除法的。 这里有一个32位数被10(20,3.3常量除法)除的例子,输出商和余数。

  1. ; takes argument in a1
  2. ; returns quotient in a1, remainder in a2
  3. ; cycles could be saved if only divide or remainder is required
  4. SUB a2, a1, #10 ; keep (x-10) for later
  5. SUB a1, a1, a1, lsr #2
  6. ADD a1, a1, a1, lsr #4
  7. ADD a1, a1, a1, lsr #8
  8. ADD a1, a1, a1, lsr #16
  9. MOV a1, a1, lsr #3
  10. ADD a3, a1, a1, asl #2
  11. SUBS a2, a2, a3, asl #1 ; calc (x-10) - (x/10)*10
  12. ADDPL a1, a1, #1 ; fix-up quotient
  13. ADDMI a2, a2, #10 ; fix-up remainder
  14. MOV pc, lr

14.2.1 Xcode优化模式(LLVM)+ARM模式

  1. __text:00002C58 39 1E 08 E3 E3 18 43 E3 MOV R1, 0x38E38E39
  2. __text:00002C60 10 F1 50 E7 SMMUL R0, R0, R1
  3. __text:00002C64 C0 10 A0 E1 MOV R1, R0,ASR#1
  4. __text:00002C68 A0 0F 81 E0 ADD R0, R1, R0,LSR#31
  5. __text:00002C6C 1E FF 2F E1 BX LR

运行原理

这里的代码和优化模式的MSVC和GCC生成的基本相同。显然,LLVM在产生常量上使用相同的算法。

善于观察的读者可能会问,MOV指令是如何将32位数值写入寄存器中的,因为这在ARM模式下是不可能的。实际上是可能的,但是,就像我们看到的,与标准指令每条有四个字节不同的是,这里的每条指令有8个字节,其实这是两条指令。第一条指令将值0x8E39装入寄存器的低十六位,第二条指令是MOVT,它将0x383E装入寄存器的高16位。IDA知道这些顺序,并且为了精简紧凑,将它精简转换成一条伪代码。

SMMUL (Signed Most Significant Word Multiply)实现两个32位有符号数的乘法,并且将高32位的部分放在r0中,弃掉结果的低32位部分。

  1. MOV R1,R0,ASR#1 指令算数右移一位。
  2. ADD R0,R1,LSR#31 R0=R1+R0>>32

事实上,在ARM模式下,并没有单独的移位指令。相反,像(MOV,ADD,SUB,RSB)3 这样的数据处理指令,第二个操作数需要被移位。ASR表示算数右移,LSR表示逻辑右移。

14.2.2 优化 Xcode(LLVM)+thumb-2 模式

  1. MOV R1, 0x38E38E39
  2. SMMUL.W R0, R0, R1
  3. ASRS R1, R0, #1
  4. ADD.W R0, R1, R0,LSR#31
  5. BX LR

在thumb模式下有些单独的移位指令,这个例子中使用了ASRS(算数右移)

14.2.3 Xcode非优化模式(LLVM) keil模式

非优化模式 LLVM不生成我们之前看到的那样的代码,它插入了一个调用库函数的call __divsi3

关于keil:通常插入一个调用库函数的call __aeabi_idivmod