- 第一章 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
13.2 ARM
13.2.1 无优化 Xcode (LLVM) + ARM 模式
清单 13.2: 无优化的 Xcode(LLVM)+ ARM 模式
#!bash
_strlen
eos = -8
str = -4
SUB SP, SP, #8 ; allocate 8 bytes for local variables
STR R0, [SP,#8+str]
LDR R0, [SP,#8+str]
STR R0, [SP,#8+eos]
loc_2CB8 ; CODE XREF: _strlen+28
LDR R0, [SP,#8+eos]
ADD R1, R0, #1
STR R1, [SP,#8+eos]
LDRSB R0, [R0]
CMP R0, #0
BEQ loc_2CD4
B loc_2CB8
; ----------------------------------------------------------------
loc_2CD4 ; CODE XREF: _strlen+24
LDR R0, [SP,#8+eos]
LDR R1, [SP,#8+str]
SUB R0, R0, R1 ; R0=eos-str
SUB R0, R0, #1 ; R0=R0-1
ADD SP, SP, #8 ; deallocate 8 bytes for local variables
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 模式
#!bash
_strlen
MOV R1, R0
loc_2DF6 ; CODE XREF: _strlen+8
LDRB.W R2, [R1],#1
CMP R2, #0
BNE loc_2DF6
MVNS R0, R0
ADD R0, R1
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 的这个语法真的很难让人记住。下为具体叙述:
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 模式
#!bash
_strlen
MOV R1, R0
loc_2C8 ; CODE XREF: _strlen+14
LDRB R2, [R1],#1
CMP R2, #0
SUBEQ R0, R1, R0
SUBEQ R0, R0, #1
BNE loc_2C8
BX LR
这个和我们之前看到的几乎一样,除了 str-cos-1 这个表达式并不在函数末尾计算,而是被调到了循环体中间。 可以回忆一下-EQ 后缀,这个代表指令仅仅会在 CMP 执行之前的语句互相相等时才会执行。因此,如果 R0 的值是 0,两个 SUBEQ 指令都会执行,然后结果会保存在 R0 寄存器中。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

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