为什么 AVR-GCC 编译器在乘法后附加“clr r1”行?

use*_*039 4 c embedded avr avr-gcc

我想检查 AVR-GCC 编译器如何编译乘法?

输入 c 代码:

unsigned char square(unsigned char num) {
    return num * num;
}
Run Code Online (Sandbox Code Playgroud)

输出汇编代码:

square(unsigned char):
        mul r24,r24
        mov r24,r0
        clr r1
        ret
Run Code Online (Sandbox Code Playgroud)

我的问题是为什么要添加该语句clr r1?表面上,假设参数存储在 r24 中并且返回值在 r24 中可用,则可以删除此语句并仍然获得所需的结果。

直接 Godbolt 链接:https ://godbolt.org/z/PsPS_N

更新:

我还在这里看到了相关的更一般性讨论。

Joh*_*ger 6

这将是GCC 使用的 AVR ABI的问题。特别是:

R1

始终包含零。在 insn 期间,内容可能会被破坏,例如通过使用 R0/R1 作为隐式输出寄存器的 MUL 指令。如果 insn 破坏了 R1,则 insn 必须在之后将 R1 恢复为零。[...]

这正是您在装配中看到的。R1 被 破坏MUL,因此必须随后将其清零。


ema*_*uts 5

当实现 GCC 的 AVR 后端并设计avr-gcc ABI时,事实证明,当已知寄存器包含0. 作者R1当时选择的,即avr-gcc在打印汇编指令时,可能会假设R1=0像这个例子一样:

\n
unsigned add (unsigned x, unsigned char y)\n{\n    if (x != 64)\n        return x + y;\n    else\n        return x;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这将编译-c -Os -save-temps为下面的代码。它使用R1又名。__zero_reg__因此它可以打印更短的指令序列:

\n
__zero_reg__ = 1\nadd:\n    cpi r24,64\n    cpc r25,__zero_reg__\n    breq .L2\n    add r24,r22\n    adc r25,__zero_reg__\n.L2:\n    ret\n
Run Code Online (Sandbox Code Playgroud)\n

R1选择它是因为在 AVR 中,较高的寄存器功能更强大,因此寄存器分配从 \xe2\x80\x93 开始,并在较高的寄存器中添加一点 \xe2\x80\x93 ,因此最后使用低位寄存器。因此,使用了寄存器号较小的寄存器。

\n

这个特殊寄存器不由寄存器分配器管理,它是“固定的”并由手工管理。对于不支持MUL指令的早期 AVR 来说,这一切都很简单。然而,随着 和 表兄弟的引入MUL,事情变得更加复杂,因为MUL使用寄存器R1:R0对作为隐式输出寄存器,因此覆盖了0中保存的内容__zero_reg__

\n

因此,您可以实施两种方法:

\n
    \n
  1. 每次使用CLR __zero_reg__ 发出,因此R1包含0.
  2. \n
  3. 在破坏它的序列“之后”清除该注册表。
  4. \n
\n

avr后端实现了方法2。

\n

因为在当前的 avr 后端(至少到 v10)中,该寄存器是手动管理的,所以没有信息是否确实需要清除该寄存器或可能被省略:

\n
unsigned char mul (unsigned char x)\n{\n    return x * x * x;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

产生-c -Os -mmcu=atmega8 -save-temps

\n
mul:\n    mul r24,r24\n    mov r25,r0\n    clr r1\n    mul r25,r24\n    mov r24,r0\n    clr r1\n    ret\n\n
Run Code Online (Sandbox Code Playgroud)\n

即被R1清除两次,即使在第一个“CLR”之后,“MUL”指令再次覆盖它。原则上,avr 后端可以跟踪哪些指令破坏R1以及哪些指令(序列)需要R1=0,但是目前(v10)尚未实现。

\n

的引入MUL导致了另一个复杂化:R1不再总是为零,即当在 a 之后立即触发中断时,MUL寄存器通常为零。因此,中断服务例程 (ISR) 在可能使用以下内容时必须保存+恢复它R1

\n
#include <avr/interrupt.h>\n\nchar volatile v;\n\nISR (__vector_1)\n{\n    v = 0;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

编译、汇编然后avr-objdump -d在目标文件上读取:

\n
00000000 <__vector_1>:\n   0:   1f 92           push    r1\n   2:   1f b6           in      r1, 0x3f\n   4:   1f 92           push    r1\n   6:   11 24           eor     r1, r1\n   8:   10 92 00 00     sts     0x0000, r1\n   c:   1f 90           pop     r1\n   e:   1f be           out     0x3f, r1\n  10:   1f 90           pop     r1\n  12:   18 95           reti\n
Run Code Online (Sandbox Code Playgroud)\n

ISR 的有效负载只是sts ..., r1存储0v. 这需要R1=0,因此需要,因此需要通过推送+弹出来clr r1保存-恢复。R1破坏clr程序状态(I/O 地址 0x3f 处的 SREG),因此 SREG 也必须围绕该序列进行保存和恢复,并且为了实现编译器将其用作r1暂存寄存器,因为特殊功能寄存器不能与push/一起使用pop

\n

除此之外,在某些情况下,在以下情况下不会重置零寄存器MUL

\n
int square (int a)\n{\n    return a * a;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

编译为:

\n
    mul  r24,r24\n    movw r18,r0\n    mul  r24,r25\n    add  r19,r0\n    add  r19,r0\n    clr  r1\n    movw r24,r18\n    ret\n
Run Code Online (Sandbox Code Playgroud)\n

CLR第一个之后没有的原因MUL是因为乘法序列在内部表示,然后作为一个块 (insn)发出,因此知道不需要中间CLR。然而,在上面的示例中x * x * x,内部表示是两个 insn,一个用于任一乘法。

\n