2.3.1 未进行代码优化的Keil编译:ARM模式

让我们在Keil里编译我们的例子

armcc.exe –arm –c90 –O0 1.c

armcc编译器可以生成intel语法的汇编程序列表,但是里面有高级的ARM处理器相关的宏,对我们来讲更希望看到的是IDA反汇编之后的结果。

Listing 2.9: Non-optimizing Keil + ARM mode + IDA

  1. #!bash
  2. .text:00000000 main
  3. .text:00000000 10 40 2D E9 STMFD SP!, {R4,LR}
  4. .text:00000004 1E 0E 8F E2 ADR R0, aHelloWorld ; "hello, world"
  5. .text:00000008 15 19 00 EB BL __2printf
  6. .text:0000000C 00 00 A0 E3 MOV R0, #0
  7. .text:00000010 10 80 BD E8 LDMFD SP!, {R4,PC}
  8. .text:000001EC 68 65 6C 6C +aHelloWorld DCB "hello, world",0 ; DATA XREF: main+4

针对ARM处理器,我们需要预备一点知识,要知道ARM处理器至少有2种模式:ARM模式和thumb模式,在ARM模式下,所有的指令都被激活并且都是32位的。在thumb模式下所有的指令都是16位的。Thumb模式比较需要注意,因为程序可能需要更为紧凑,或者当微处理器用的是16位内存地址时会执行的更快。但也存在缺陷,在thumb模式下可用的指令没ARM下多,只有8个寄存器可以访问,有时候ARM模式下一条指令就能解决的问题,thumb模式下需要多个指令来完成。

从ARMv7开始引入了thumb-2指令集。这是一个加强的thumb模式。拥有了更多的指令,通常会有误解,感觉thumb-2是ARM和thumb的混合。Thumb-2加强了处理器的特性,并且媲美ARM模式。程序可能会混合使用2种模式。其中大量的ipod/iphone/ipad程序会使用thumb-2是因为Xcode将其作为了默认模式。

在例子中,我们可以发现所有指令都是4bytes的,因为我们编译的时候选择了ARM模式,而不是thumb模式。

最开始的指令是STMFD SP!, {R4, LR},这条指令类似x86平台的PUSH指令,会写2个寄存器(R4和LR)的变量到栈里。不过在armcc编译器里输出的汇编列表里会写成PUSH {R4, LR},但这并不准确,因为PUSH命令只在thumb模式下有,所以我建议大家注意用IDA来做反汇编工具。

这指令开始会减少SP的值,已加大栈空间,并且将R4和LR写入分配好的栈里。

这条指令(类似于PUSH的STMFD)允许一次压入好几个值,非常实用。有一点跟x86上的PUSH不同的地方也很赞,就是这条指令不像x86的PUSH只能对sp操作,而是可以指定操作任意的寄存器。

ADR R0, aHelloWorld这条指令将PC寄存器的值与"hello, world"字串的地址偏移相加放入R0,为什么说要PC参与这个操作那?这是因为代码是PIC(position-independet code)的,这段代码可以独立在内存运行,而不需要更改内存地址。ADR这条指令中,指令中字串地址和字串被放置的位置是不同的。但变化是相对的,这要看系统是如何安排字串放置的位置了。这也就说明了,为何每次获取内存中字串的绝对地址,都要把这个指令里的地址加上PC寄存器里的值了。

BL __2print这条指令用于调用printf()函数,这是来说下这条指令时如何工作的:

  1. BL指令(0xC)后面的地址写入LR寄存器;
  2. 然后把printf()函数的入口地址写入PC寄存器,进入printf()函数。

当printf()函数完成之后,函数会通过LR寄存器保存的地址,来进行返回操作。

函数返回地址的存放位置也正是“纯”RISC处理器(例如:ARM)和CISC处理器(例如x86)的区别。

另外,一个32位地址或者偏移不能被编码到BL指令里,因为BL指令只有24bits来存放地址,所有的ARM模式下的指令都是4bytes(32bits),所以一条指令里不能放满4bytes的地址,这也就意味着最后2bits总会被设置成0,总的来说也就是有26bits的偏移(包括了最后2个bit一直被设为0)会被编码进去。这也够去访问大约±32M的了。

下面我们来看MOV R0, #0这条语句,这条语句就是把0写到了R0寄存器里,这是因为C函数返回了0,返回值当然是放在R0里的。

最后一条指令是LDMFD SP!, R4,PC,这条指令的作用跟开始的那条STMFD正好相反,这条指令将栈上的值保存到R4和PC寄存器里,并且增加SP栈寄存器的值。这非常类似x86平台里的POP指令。最前面那条STMFD指令成对保存了R4,和LR寄存器,LDMFD的时候将当时这两个值保存到了R4和PC里完成了函数的返回。

我前面也说过,函数的返回地址会保存到LD寄存器里。在函数的最开始会把他保存到栈里,这是因为main()函数里还需要调用printf()函数,这个时候就会影响LD寄存器。在函数的最后就会将LD拿出栈放入PC寄存器里,完成函数的返回操作。最后C/C++程序的main()函数会返回到类似系统加载器上或者CRT里面。

汇编代码里的DCB关键字用来定义ASCII字串数组,就像x86汇编里的DB关键字。