返回介绍

2.3 ARM

发布于 2025-02-22 14:00:41 字数 8467 浏览 0 评论 0 收藏 0

根据作者自身对 ARM 处理器的经验,选择了 2 款在嵌入式开发流行的编译器,Keil Release 6/2013 和苹果的 Xcode 4.6.3 IDE(其中使用了 LLVM-GCC4.2 编译器),这些可以为 ARM 兼容处理器和系统芯片(System on Chip)(SOC)) 来进行编码。比如 ipod/iphone/ipad,windows8 rt,并且包括 raspberry pi。

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

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

#!bash
armcc.exe –arm –c90 –O0 1.c

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

Listing 2.9: Non-optimizing Keil + ARM mode + IDA
#!bash
.text:00000000                  main
.text:00000000 10 40 2D E9              STMFD SP!, {R4,LR}
.text:00000004 1E 0E 8F E2              ADR R0, aHelloWorld ; "hello, world"
.text:00000008 15 19 00 EB              BL __2printf
.text:0000000C 00 00 A0 E3              MOV R0, #0
.text:00000010 10 80 BD E8              LDMFD SP!, {R4,PC}

.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() 函数,这是来说下这条指令时如何工作的:

将 BL 指令(0xC)后面的地址写入 LR 寄存器;
然后把 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 关键字。

2.3.2 未进行代码优化的 Keil 编译: thumb 模式

让我们用下面的指令讲例程用 Keil 的 thumb 模式来编译一下。

#!bash
armcc.exe –thumb –c90 –O0 1.c

我们可以在 IDA 里得到下面这样的代码: Listing 2.10:Non-optimizing Keil + thumb mode + IDA

#!bash
.text:00000000          main
.text:00000000 10 B5                    PUSH {R4,LR}
.text:00000002 C0 A0                    ADR R0, aHelloWorld ; "hello, world"
.text:00000004 06 F0 2E F9              BL __2printf
.text:00000008 00 20                    MOVS R0, #0
.text:0000000A 10 BD                    POP {R4,PC}

.text:00000304 68 65 6C 6C+aHelloWorld  DCB "hello, world",0 ; DATA XREF: main+2

我们首先就能注意到指令都是 2bytes(16bits) 的了,这正是 thumb 模式的特征,BL 指令作为特例是 2 个 16bits 来构成的。只用 16bits 没可能加载 printf() 函数的入口地址到 PC 寄存器。所以前面的 16bits 用来加载函数偏移的高 10bits 位,后面的 16bits 用来加载函数偏移的低 11bits 位,正如我说过的,所有的 thumb 模式下的指令都是 2bytes(16bits)。但是这样的话 thumb 指令就没法使用更大的地址。就像上面那样,最后一个 bits 的地址将会在编码指令的时候省略。总的来讲,BL 在 thumb 模式下可以访问自身地址大于±2M 大的周边的地址。

至于其他指令:PUSH 和 POP,它们跟上面讲到的 STMFD 跟 LDMFD 很类似,但这里不需要指定 SP 寄存器,ADR 指令也跟上面的工作方式相同。MOVS 指令将函数的返回值 0 写到了 R0 里,最后函数返回。

2.3.3 开启代码优化的 Xcode(LLVM)编译: ARM 模式

Xcode 4.6.3 不开启代码优化的情况下,会产生非常多冗余的代码,所以我们学习一个尽量小的版本。

开启-O3 编译选项

#!bash
Listing2.11:Optimizing Xcode(LLVM)+ARM mode
__text:000028C4         _hello_world
__text:000028C4 80 40 2D E9                     STMFD   SP!, {R7,LR}
__text:000028C8 86 06 01 E3                     MOV     R0, #0x1686
__text:000028CC 0D 70 A0 E1                     MOV     R7, SP
__text:000028D0 00 00 40 E3                     MOVT    R0, #0
__text:000028D4 00 00 8F E0                     ADD     R0, PC, R0
__text:000028D8 C3 05 00 EB                     BL      _puts
__text:000028DC 00 00 A0 E3                     MOV     R0, #0
__text:000028E0 80 80 BD E8                     LDMFD   SP!, {R7,PC}

__cstring:00003F62 48 65 6C 6C+aHelloWorld_0    DCB "Hello world!",0

STMFD 和 LDMFD 对我们来说已经非常熟悉了。

MOV 指令就是将 0x1686 写入 R0 寄存器里。这个值也正是字串”Hello world!”的指针偏移。

R7 寄存器里放入了栈地址,我们继续。

MOVT R0, #0 指令时将 R0 的高 16bits 写入 0。这是因为普通情况下 MOV 这条指令在 ARM 模式下,只对低 16bits 进行操作。需要记住的是所有在 ARM 模式下的指令都被限定在 32bits 内。当然这个限制并不影响 2 个寄存器直接的操作。这也是 MOVT 这种写高 16bits 指令存在的意义。其实这样写的代码会感觉有点多余,因为” MOVS R0,#0x1686 ”这条指令也能把高 16 位清 0。或许这就是相对于人脑来说编译器的不足。

ADD R0,PC,R0 “指令把 R0 寄存器的值与 PC 寄存器的值进行相加并且保存到 R0 寄存器里面,用来计算”Hello world!”这个字串的绝对地址。上面已经介绍过了,这是因为代码是 PIC(Position-independent code) 的,所以这里需要这么做。

BL 指令用来调用 printf() 的替代函数 puts() 函数。

GCC 将 printf()函数替换成了 puts()。因为 printf() 函数只有一个参数的时候跟 puts() 函数是类似的。

printf() 函数的字串参数里存在特殊控制符(例如 ”%s”,”\n” ,需要注意的是,程序里字串里没有“\n”,因为在 puts() 函数里这是不需要的)的时候,两个函数的功效就会不同。

为什么编译器会替换 printf() 到 puts() 那?因为 puts() 函数更快。

puts() 函数效率更快是因为它只是做了字串的标准输出(stdout) 并不用比较%符号。

下面,我们可以看到非常熟悉的”MOV R0, #0”指令,用来将 R0 寄存器设为 0。

2.3.4 开启代码优化的 Xcode(LLVM) 编译 thumb-2 模式

在默认情况下,Xcode4.6.3 会生成如下的 thumb-2 代码

Listing 2.12:Optimizing Xcode(LLVM)+thumb-2 mode

#!bash
__text:00002B6C         _hello_world
__text:00002B6C 80 B5           PUSH    {R7,LR}
__text:00002B6E 41 F2 D8 30     MOVW    R0, #0x13D8
__text:00002B72 6F 46           MOV     R7, SP
__text:00002B74 C0 F2 00 00     MOVT.W  R0, #0
__text:00002B78 78 44           ADD     R0, PC
__text:00002B7A 01 F0 38 EA     BLX     _puts
__text:00002B7E 00 20           MOVS    R0, #0
__text:00002B80 80 BD           POP     {R7,PC}
...
__cstring:00003E70 48 65 6C 6C 6F 20+aHelloWorld DCB "Hello world!",0xA,0

BL 和 BLX 指令在 thumb 模式下情况需要我们回忆下刚才讲过的,它是由一对 16-bit 的指令来构成的。在 thumb-2 模式下这条指令跟 thumb 一样被编码成了 32-bit 指令。非常容易观察到的是,thumb-2 的指令的机器码也是从 0xFx 或者 0xEx 的。对于 thumb 和 thumb-2 模式来说,在 IDA 的结果里机器码的位置和这里是交替交换的。对于 ARM 模式来说 4 个 byte 也是反向的,这是因为他们用了不同的字节序。所以我们可以知道,MOVW,MOVT.W 和 BLX 这几个指令的开始都是 0xFx。

在 thumb-2 指令里有一条是”MOVW R0, #0x13D8”,它的作用是写数据到 R0 的低 16 位里面。

MOVT.W R0, #0 ”的作用类似与前面讲到的 MOVT 指令,但它可以工作在 thumb-2 模式下。

还有些跟上面不同的地方,比如 BLX 指令替代了上面用到的 BL 指令,这条指令不仅将控制 puts() 函数返回的地址放入了 LR 寄存器里,并且讲代码从 thumb 模式转换到了 ARM 模式(或者 ARM 转换到 thumb(根据现有情况判断))。这条指令跳转到下面这样的位置(下面的代码是 ARM 编码模式)。

#!bash
__symbolstub1:00003FEC _puts             ; CODE XREF: _hello_world+E
__symbolstub1:00003FEC 44 F0 9F E5       LDR PC, =__imp__puts

可能会有细心的读者要问了:为什么不直接跳转到 puts() 函数里面去?

因为那样做会浪费内存空间。

很多程序都会使用额外的动态库(dynamic libraries)(Windows 里面的 DLL,还有*NIX 里面的.so,MAC OS X 里面的.dylib),通常使用的库函数会被放入动态库中,当然也包括标准 C 函数 puts()。

在可执行的二进制文件里(Windows 的 PE 里的.exe 文件,ELF 和 Mach-O 文件) 都会有输入表段。它是一个用来引入额外模块里模块名称和符号(函数或者全局变量)的列表。

系统加载器(OS loader)会加载所有需要的模块,当在主模块里枚举输入符号的时候,会把每个符号正确的地址与相应的符号确立起来。

在我们的这个例子里, __imp__puts 就是一个系统加载器加载额外模块的 32 位的地址值。LDR 指令只需要把这个值加载到 PC 里面去,就可以控制程序流程到 puts() 函数里去。

所以只需要在系统加载器里的时候,一次性的就能将每个符号所对应的地址确定下来,这是个提高效率的好方式。

外加,我们前面也指出过,我们没办法只用一条指令并且不做内存操作的情况下就将一个 32bit 的值保存到寄存器里,ARM 并不是唯一的模式的情况下,程序里去跳入动态库中的某个函数里,最好的办法就是这样做一些类似与上面这样单一指令的函数(称做 thunk function),然后从 thumb 模式里也能去调用。

在上面的例子(ARM 编译的那个例子)中 BL 指令也是跳转到了同一个 thunk function 里。尽管没有进行模式的转变(所以指令里不存在那个”X”)。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文