返回介绍

13.2 ARM

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

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 的这个语法真的很难让人记住。下为具体叙述:

enter image description here

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 技术交流群。

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

发布评论

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