Почему компиляторы AVR-G CC добавляют строку "clr r1" после умножения? - PullRequest
4 голосов
/ 28 января 2020

Я пытаюсь проверить, как компилятор AVR-G CC компилируется для умножения?

Ввод 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

ОБНОВЛЕНИЕ:

Я также вижу связанное более общее обсуждение здесь .

Ответы [ 2 ]

6 голосов
/ 28 января 2020

Это будет вопрос AVR ABI, используемый G CC. В частности:

R1

всегда содержит ноль. Во время insn содержимое может быть уничтожено, например, с помощью инструкции MUL, которая использует R0 / R1 в качестве неявного выходного регистра. Если insn уничтожает R1, insn должен восстановить R1 до нуля после этого. [...]

И это именно то, что вы видите в сборке. R1 засоряется MUL, поэтому он должен быть очищен до нуля.

3 голосов
/ 29 января 2020

Когда был реализован сервер AVR G CC и был разработан avr-g cc ABI , оказалось, что генерация кода может быть улучшена в некоторых ситуациях, когда есть регистр, который как известно, содержит 0. Тогда автор выбрал R1, т. Е. Когда avr-g cc печатает инструкции по сборке, можно предположить, что 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 старшие регистры более мощные и, следовательно, распределение регистров начинается - с частичкой соли - с старшие регистры, следовательно, младшие регистры будут использоваться последними. Таким образом, был использован регистр с небольшим номером регистра.

Этот специальный регистр не управляется распределителем регистров, он «фиксирован» и управляется вручную. Это было все просто с ранними AVR, которые не поддерживали инструкции MUL. Однако с введением MUL и двоюродных братьев все стало сложнее, поскольку MUL использует регистровую пару R1:R0 в качестве неявного выходного регистра и, следовательно, переопределяет 0, хранящийся в __zero_reg__.

Таким образом Вы можете реализовать два подхода:

  1. Излучать CLR __zero_reg__ до для каждого использования, поэтому 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

т.е. R1 очищается дважды хотя сразу после 1-го «CLR» инструкция «MUL» снова переопределяет его. В принципе, серверная часть avr может отследить, какие команды clobber 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, следовательно, save-restore R1 с помощью push + pop. clr закрывает состояние программы (SREG по адресу ввода-вывода 0x3f), поэтому SREG также должен быть сохранен-восстановлен вокруг этой последовательности, и для достижения sh, что компилятор использует r1 в качестве рабочего регистра поскольку регистры специальных функций нельзя использовать с push / pop.

Кроме того, существуют ситуации, когда no сбрасывает нулевой регистр после 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 после 1-го MUL заключается в том, что последовательность умножения внутренне представлена ​​и затем выдается как один кусок (insn) , следовательно, есть знания, что нет необходимости в промежуточном CLR. Однако в приведенном выше примере с x * x * x внутренним представлением является два insns, по одному для любого умножения.

...