- 第一章 CPU 简介
- 第二章 Hello,world!
- 第三章 函数开始和结束
- 第四章 栈
- Chapter 5 printf() 与参数处理
- Chapter 6 scanf()
- CHAPER7 访问传递参数
- Chapter 8 一个或者多个字的返回值
- Chapter 9 指针
- Chapter 10 条件跳转
- 第 11 章 选择结构 switch()/case/default
- 第 12 章 循环结构
- 第 13 章 strlen()
- Chapter 14 Division by 9
- chapter 15 用 FPU 工作
- Chapter 16 数组
- Chapter 17 位域
- 第 18 章 结构体
- 19 章 联合体
- 第二十章 函数指针
- 第 21 章 在 32 位环境中的 64 位值
- 第二十二章 SIMD
- 23 章 64 位化
- 24 章 使用 x64 下的 SIMD 来处理浮点数
- 25 章 温度转换
- 26 章 C99 的限制
- 27 章 内联函数
- 第 28 章 得到不正确反汇编结果
- 第 29 章 花指令
- 第 30 章 16 位 Windows
- 第 31 章 类
- 三十二 ostream
- 34.2.2 MSVC
- 34.2.3 C++ 11 std::forward_list
- 34.3 std::vector
- 34.4 std::map and std::set
2.3 ARM
根据作者自身对 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 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论