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

问题描述 投票:0回答:2

我正在尝试检查 AVR-GCC 编译器如何编译乘法?

输入c代码:

unsigned char square(unsigned char num) {
    return num * num;
}

输出汇编代码:

square(unsigned char):
        mul r24,r24
        mov r24,r0
        clr r1
        ret

我的问题是为什么要添加声明

clr r1
?看起来,假设参数存储在 r24 中并且返回值在 r24 中可用,则可以删除此语句并仍然得到所需的结果。

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

更新:

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

c embedded avr avr-gcc
2个回答
6
投票

这将涉及到 GCC 使用的 AVR ABI 。特别是:

R1

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

这正是您在大会中看到的。 R1 被

MUL
破坏,因此随后必须将其清零。


5
投票

当实现 GCC 的 AVR 后端并设计 avr-gcc ABI 时,事实证明,当存在已知包含

0
的寄存器时,代码生成在某些情况下可以得到改进。作者当时选择了
R1
,即当avr-gcc打印汇编指令时,人们可能会认为
R1=0
就像这个例子:

unsigned add (unsigned x, unsigned char y)
{
    if (x != 64)
        return x + y;
    else
        return x;
}

这将使用

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

__zero_reg__ = 1
add:
    cpi r24,64
    cpc r25,__zero_reg__
    breq .L2
    add r24,r22
    adc r25,__zero_reg__
.L2:
    ret
选择

R1
是因为在 AVR 中,较高的寄存器功能更强大,因此寄存器分配从较高的寄存器开始(有一点保留),因此低位寄存器将最后使用。因此使用了寄存器号较小的寄存器。

这个特殊寄存器不是由寄存器分配器管理的,它是“固定的”并由手工管理。对于不支持

MUL
指令的早期 AVR 来说,这一切都很简单。然而,随着
MUL
和表兄弟的引入,事情变得更加复杂,因为
MUL
使用寄存器对
R1:R0
作为隐式输出寄存器,因此覆盖了
0
中保存的
__zero_reg__

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

    每次使用时
  1. 发出
    CLR __zero_reg__
    prior,因此
    R1
    包含
    0
  2. 在破坏该寄存器的序列“之后”清除该寄存器。

avr后端实现方法2。

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

unsigned char mul (unsigned char x)
{
    return x * x * x;
}

产生

-c -Os -mmcu=atmega8 -save-temps
:

mul:
    mul r24,r24
    mov r25,r0
    clr r1
    mul r25,r24
    mov r24,r0
    clr r1
    ret

即即使在第一个“CLR”之后,“MUL”指令再次覆盖它,

R1
也会被清除两次。原则上,avr 后端可以跟踪哪些指令破坏
R1
以及哪些指令(序列)需要
R1=0
,但是目前(v10)尚未实现。

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

#include <avr/interrupt.h>

char volatile v;

ISR (__vector_1)
{
    v = 0;
}

编译、汇编然后

avr-objdump -d
在目标文件上读取:

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

ISR 的有效负载只是

sts ..., r1
,它将
0
存储到
v
。这需要
R1=0
,因此需要
clr r1
,因此通过push+pop的方式保存-恢复
R1
clr
会破坏程序状态(I/O 地址 0x3f 处的 SREG),因此 SREG 也必须围绕该序列进行保存和恢复,并且为了实现这一点,编译器将
r1
用作临时寄存器作为特殊功能寄存器不能与
push
/
pop
一起使用。

除此之外,在某些情况下,在MUL之后

没有
重置零寄存器:

int square (int a)
{
    return a * a;
}

编译为:

    mul  r24,r24
    movw r18,r0
    mul  r24,r25
    add  r19,r0
    add  r19,r0
    clr  r1
    movw r24,r18
    ret

第一个

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

© www.soinside.com 2019 - 2024. All rights reserved.