为什么 AVR-GCC 编译器在乘法后附加一个 "clr r1" 行?
Why AVR-GCC compilers append a "clr r1" line after multiplication?
我正在尝试检查 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 中并且 return 值在 r24 中可用。
直接神箭link:
https://godbolt.org/z/PsPS_N
更新:
我也看到了相关更一般的讨论here。
那将是 the AVR ABI used by GCC 的问题。特别是:
R1
always contains zero. During an insn the content might be destroyed, e.g. by a MUL instruction that uses R0/R1 as implicit
output register. If an insn destroys R1, the insn must restore R1 to
zero afterwards. [...]
这正是您在集会中看到的。 R1 被 MUL
破坏,因此必须随后将其清零。
当实现 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 中,较高的寄存器更强大,因此寄存器分配从较高的寄存器开始——有点保留——因此低寄存器将最后使用。因此使用了寄存器号较小的寄存器。
这个特殊寄存器不是由寄存器分配器管理的,它是"fixed"并且是手动管理的。对于不支持 MUL
指令的早期 AVR,这一切都很简单。然而,随着 MUL
和表兄弟的引入,事情变得更加复杂,因为 MUL
使用寄存器对 R1:R0
作为隐式输出寄存器,因此覆盖 [=23] 中保存的 0
=].
因此您可以实施两种方法:
- 每次使用
CLR __zero_reg__
之前发出 R1
包含 0
.
- 清除 reg 'after' 破坏它的序列。
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
即R1
被清除 两次 即使在第一个 'CLR' 之后 'MUL' 指令再次覆盖它。原则上,avr 后端可以跟踪哪些指令破坏 R1
以及哪些指令(序列)需要 R1=0
,但是目前(v10)尚未实现。
MUL
的引入导致了另一个复杂化:R1
不再是 总是 零,即当中断在 [= 之后立即触发时25=] 那么寄存器一般是 而不是 零。因此,中断服务例程 (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
破坏了程序状态(SREG 在 I/O 地址 0x3f),因此 SREG 也必须围绕该序列保存-恢复,并且为了实现编译器使用 r1
作为作为特殊功能寄存器的临时寄存器不能与 push
/pop
.
一起使用
除此之外,在 MUL
:
之后 no 零寄存器重置的情况
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
之所以在第一个MUL
之后没有CLR
是因为乘法序列内部表示然后发出为one chunk (insn),所以知道不需要对于中间 CLR
。然而,在上面带有 x * x * x
的示例中,内部表示是两个 insn,一个用于任一乘法。
我正在尝试检查 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 中并且 return 值在 r24 中可用。
直接神箭link: https://godbolt.org/z/PsPS_N
更新:
我也看到了相关更一般的讨论here。
那将是 the AVR ABI used by GCC 的问题。特别是:
R1
always contains zero. During an insn the content might be destroyed, e.g. by a MUL instruction that uses R0/R1 as implicit output register. If an insn destroys R1, the insn must restore R1 to zero afterwards. [...]
这正是您在集会中看到的。 R1 被 MUL
破坏,因此必须随后将其清零。
当实现 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 中,较高的寄存器更强大,因此寄存器分配从较高的寄存器开始——有点保留——因此低寄存器将最后使用。因此使用了寄存器号较小的寄存器。
这个特殊寄存器不是由寄存器分配器管理的,它是"fixed"并且是手动管理的。对于不支持 MUL
指令的早期 AVR,这一切都很简单。然而,随着 MUL
和表兄弟的引入,事情变得更加复杂,因为 MUL
使用寄存器对 R1:R0
作为隐式输出寄存器,因此覆盖 [=23] 中保存的 0
=].
因此您可以实施两种方法:
- 每次使用
CLR __zero_reg__
之前发出R1
包含0
. - 清除 reg 'after' 破坏它的序列。
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
即R1
被清除 两次 即使在第一个 'CLR' 之后 'MUL' 指令再次覆盖它。原则上,avr 后端可以跟踪哪些指令破坏 R1
以及哪些指令(序列)需要 R1=0
,但是目前(v10)尚未实现。
MUL
的引入导致了另一个复杂化:R1
不再是 总是 零,即当中断在 [= 之后立即触发时25=] 那么寄存器一般是 而不是 零。因此,中断服务例程 (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
破坏了程序状态(SREG 在 I/O 地址 0x3f),因此 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
之所以在第一个MUL
之后没有CLR
是因为乘法序列内部表示然后发出为one chunk (insn),所以知道不需要对于中间 CLR
。然而,在上面带有 x * x * x
的示例中,内部表示是两个 insn,一个用于任一乘法。