13.2 ARM

13.2.1 无优化 Xcode (LLVM) + ARM模式

清单13.2: 无优化的Xcode(LLVM)+ ARM模式

  1. _strlen
  2. eos = -8
  3. str = -4
  4. SUB SP, SP, #8 ; allocate 8 bytes for local variables
  5. STR R0, [SP,#8+str]
  6. LDR R0, [SP,#8+str]
  7. STR R0, [SP,#8+eos]
  8. loc_2CB8 ; CODE XREF: _strlen+28
  9. LDR R0, [SP,#8+eos]
  10. ADD R1, R0, #1
  11. STR R1, [SP,#8+eos]
  12. LDRSB R0, [R0]
  13. CMP R0, #0
  14. BEQ loc_2CD4
  15. B loc_2CB8
  16. ; ----------------------------------------------------------------
  17. loc_2CD4 ; CODE XREF: _strlen+24
  18. LDR R0, [SP,#8+eos]
  19. LDR R1, [SP,#8+str]
  20. SUB R0, R0, R1 ; R0=eos-str
  21. SUB R0, R0, #1 ; R0=R0-1
  22. ADD SP, SP, #8 ; deallocate 8 bytes for local variables
  23. BX LR

无优化的LLVM生成了太多的代码,但是,这里我们可以看到函数是如何在栈上处理本地变量的。我们的函数里只有两个本地变量,eos和str。

在这个IDA生成的列表里,我把var_8和var_4命名为了eos和str。

所以,第一个指令只是把输入的值放到str和eos里。

循环体从loc_2CB8标签处开始。

循环体的前三个指令(LDR、ADD、STR)将eos的值载入R0,然后值会加一,然后存回栈上本地变量eos。

下一条指令“LDRSB R0, [R0]”(Load Register Signed Byte,读取寄存器有符号字)将从R0地址处读取一个字节,然后把它符号扩展到32位。这有点像是x86里的MOVSX函数(见13.1.1节)。因为char在C标准里面是有符号的,所以编译器也把这个字节当作有符号数。我已经在13.1.1节写了这个,虽然那里是相对x86来说的。 需要注意的是,在ARM里会单独分割使用8位或者16位或者32位的寄存器,就像x86一样。显然,这是因为x86有一个漫长的历史上的兼容性问题,它需要和他的前身:16位8086处理器甚至8位的8080处理器相兼容。但是ARM确是从32位的精简指令集处理器中发展而成的。因此,为了处理单独的字节,程序必须使用32位的寄存器。 所以LDRSB一个接一个的将符号从字符串内载入R0,下一个CMP和BEQ指令将检查是否读入的符号是0,如果不是0,控制流将重新回到循环体,如果是0,那么循环结束。 在函数最后,程序会计算eos和str的差,然后减一,返回值通过R0返回。

注意:这个函数并没有保存寄存器。这是因为由ARM调用时的转换,R0-R3寄存器是“临时寄存器”(scratch register),它们只是为了传递参数用的,它们的值并不会在函数退出后保存,因为这时候函数也不会再使用它们。因此,它们可以被我们用来做任何事情,而这里其他寄存器都没有使用到,这也就是为什么我们的栈上事实上什么都没有的原因。因此,控制流可以通过简单跳转(BX)来返回调用的函数,地址存在LR寄存器中。

13.2.2 优化后的 Xcode (LLVM) + thumb 模式

清单13.3: 优化后的 Xcode(LLVM) + thumb模式

  1. _strlen
  2. MOV R1, R0
  3. loc_2DF6 ; CODE XREF: _strlen+8
  4. LDRB.W R2, [R1],#1
  5. CMP R2, #0
  6. BNE loc_2DF6
  7. MVNS R0, R0
  8. ADD R0, R1
  9. BX LR

在优化后的LLVM中,为eos和str准备的栈上空间可能并不会分配,因为这些变量可以永远正确的存储在寄存器中。在循环体开始之前,str将一直存储在R0中,eos在R1中。

"LDRB.W R2, [R1],#1"指令从R1内存中读取字节到R2里,按符号扩展成32位的值,但是不仅仅这样。 在指令最后的#1被称为“后变址”(Post-indexed address),这代表着在字节读取之后,R1将会加一。这个在读取数组时特别方便。

在x86中这里并没有这样的地址存取方式,但是在其他处理器中却是有的,甚至在PDP-11里也有。这是PDP-11中一个前增、后增、前减、后减的例子。这个很像是C语言(它是在PDP-11上开发的)中“罪恶的”语句形式ptr++、++ptr、ptr–、–ptr。顺带一提,C的这个语法真的很难让人记住。下为具体叙述:

13.2 ARM - 图1

C语言作者之一的Dennis Ritchie提到了这个可能是由于另一个作者Ken Thompson开发的功能,因此这个处理器特性在PDP-7中最早出现了(参考资料[28][29])。因此,C语言编译器将在处理器支持这种指令时使用它。

然后可以指出的是循环体的CMP和BNE,这两个指令将一直处理到字符串中的0出现为止。

MVNS(翻转所有位,也即x86的NOT)指令和ADD指令计算cos-str-1.事实上,这两个指令计算出R0=str+cos。这和源码里的指令效果一样,为什么他要这么做的原因我在13.1.5节已经说过了。

显然,LLVM,就像是GCC一样,会把代码变得更短或者更快。

13.2.3 优化后的 Keil + ARM 模式

清单13.4: 优化后的 Keil + ARM模式

  1. _strlen
  2. MOV R1, R0
  3. loc_2C8 ; CODE XREF: _strlen+14
  4. LDRB R2, [R1],#1
  5. CMP R2, #0
  6. SUBEQ R0, R1, R0
  7. SUBEQ R0, R0, #1
  8. BNE loc_2C8
  9. BX LR

这个和我们之前看到的几乎一样,除了str-cos-1这个表达式并不在函数末尾计算,而是被调到了循环体中间。 可以回忆一下-EQ后缀,这个代表指令仅仅会在CMP执行之前的语句互相相等时才会执行。因此,如果R0的值是0,两个SUBEQ指令都会执行,然后结果会保存在R0寄存器中。