Выполнение простых условных тестов с помощью сборки - PullRequest
0 голосов
/ 08 июня 2018

Я пытаюсь закодировать простой оператор if-else в сборке, однако ret возвращается к подпрограмме start вместо предполагаемой подпрограммы test.

Как это исправить?Спасибо.

start:
    ldi r16, 0
    call test
    rjmp start

test:
    cpi r16, 0
    breq doFirst;

    cpi r16, 1
    breq doFirst;

    cpi r16, 2
    breq doSecond;

    jmp test;

doFirst:
    inc r16;
    ret;

doSecond:
    inc r17
    ret;

Ответы [ 2 ]

0 голосов
/ 08 июня 2018

ret в основном pop в ПК.

Вы перешли на doFirst вместо того, чтобы вызывать его, поэтому вы должны вернуться с rjmp к метке внутри test вместо ret.

Или, если doFirst была отдельной функцией, вы сделали оптимизированный хвостовой вызов, чтобы она вернулась к вашему вызывающему.(Например, как вы (или компилятор) реализовали бы int foo() { return bar(); })


Вам не нужно делать doFirst отдельной функцией.Вы можете вставить его один раз, потому что он не нужен для возврата в 2 разных места в вашем цикле test.

test:
    cpi    r16, 0
    breq   doFirst0;
    cpi    r16, 1
    breq   doFirst1;
 donefirst:

    cpi    r16, 2
    brne   noSecond   ;  conditionally SKIP the inlined doSecond body
     inc   r17        ; inlined body of doSecond
  noSecond:
    rjmp   test       ; You don't need a slow 4-byte JMP, just a short relative jump

doFirst0:
    inc   r16
doFirst1:          ; fall through instead of jumping back and finding that r16 == 1 now
    inc   r16
    rjmp  donefirst

Примечание: эта оптимизация изменит поведение для r16 == 0, еслиу нас были обе команды breq, прыгающие в одно и то же место .Мы только увеличиваем один раз, а затем выполняем оставшуюся часть цикла, вместо того, чтобы увеличивать, затем проверять и находить r16 == 1 и снова увеличивать.

На самом деле мы могли бы также сделать if(r16 <=0) r16 = 2; с ldi r16, 2

Если он был слишком большим для дублирования, и вы действительно хотели rcall / ret для вспомогательной функции, вы можете условно перепрыгнуть через rcall инструкцию.


Использование 2 пар CPI / BREQ - очень неэффективный способ проверить r16 <= 1 (без знака). AVR имеет BRLO (BRanch, если LOwer (без знака)) .

Поскольку инструкции BR более быстрые, если не выполняются, мы оставим этот код вне строки вместо его вставки (например,Я сделал для doSecond) и перепрыгнул через него с BRSH (То же или выше).

test:
    cpi    r16, 1
    brlo   doFirst    ; 
 donefirst:           ; r16 will become 2, so we can't put this label later and skip the next check

    cpi    r16, 2
    brne   noSecond   ;  conditionally SKIP the inlined doSecond body
    inc    r17
  noSecond:           ; this label might as well be at test: directly
    rjmp   test

doFirst:         ; rare case, only runs the first 2 iters, put it out of line
    ldi   r16, 2      ; 1 or 2 inc instructions make r16 = 2
    rjmp  donefirst

Весь этот цикл не имеет большого смысла, если обработчик прерываний не может изменить или использовать r16 или r17,хоть.Если это не так, вы действительно хотите просто очистить первую итерацию, а затем попасть в бесконечный цикл, который ничего не делает.

Когда мы входим в ваш цикл (если ваши ret s пошли туда, где и должны)с r16 <= 2 мы получим r16 = 2 и r17, увеличенные один раз на этой первой итерации.

test:
    cpi    r16, 2
    brhi   above2
    ldi    r16, 2          ; result of 1 or 2 inc instructions
    ; flags may differ from your version.

  infloop_r16_eq_2:        ; loop for the r16==2 case
    inc    r17
    rjmp   infloop_r16_eq_2

  above2:

  infloop:                 ; loop for the r16!=2 case
    rjmp   infloop

Но если регистры являются глобальными переменными, которые могут быть изменены асинхронноМы не можем просто проверить r16 один раз, а затем продолжить вечно.

Мне было любопытно, что будет делать gcc, поэтому я использовал register volatile unsigned char a asm("r16"); и "r17".Удивительно, но это частично работает, хотя и предупреждает, что оптимизация может исключить чтение и / или запись в регистр переменных.Похоже, это произошло для inc r17 с AVR gcc4.6.4 -O3, но не для -O1.gcc7.3 для x86-64 фактически сохраняет его при -O3. Посмотрите в проводнике компилятора Godbolt и посмотрите также Как удалить "шум" из вывода сборки GCC / clang? .

// x86-64 and AVR both have r14 and r15, but not r16/r17
register volatile unsigned char a asm("r14");
register volatile unsigned char b asm("r15");

void test() {
    while(1) {
        if (a <= 1) 
            a++;        // or  a = 2;
        if (a == 2)
            b++;
    }
}

AVR gcc -O1 вывод:

test:
.L7:
        cpi r16,lo8(2)
        brsh .L2
        subi r16,lo8(-(1))   ; inc r16
.L2:
        cpi r16,lo8(2)
        brne .L7             ; back to the top
         ; else fall through
        subi r17,lo8(-(1))   ; inc r17
        rjmp .L7             ; then back to the top

Это выглядит довольно разумно.

lo8(2) только 2, я не знаю, почему gcc генерирует сборку таким образом.Возможно, для адресов символов или чего-то подобного, например lo8(test), полезно получить младший байт адреса метки.

Это своего рода оптимизация дублирования хвоста цикла, но один из хвостов пуст.Таким образом, вместо того, чтобы пропустить inc r17, мы просто прыгаем прямо к вершине цикла.

brne .L7 может перейти к инструкции brsh, поскольку флаги по-прежнему установлены в cpi r16, 2.Gcc не делает этого, потому что мы сказали, что регистры изменчивы, поэтому он не оптимизирует второе чтение регистра.

0 голосов
/ 08 июня 2018

Инструкция breq не хранит адрес возврата в стеке, поэтому ret не вернется к этой точке в программе.Вам нужно будет использовать инструкции call, icall или rcall AVR.

На самом деле, лучшим решением будет использование brne, чтобы просто пропустить условный код, если условиеложный.doFirst не обязательно должна быть подпрограммой, если она вызывается только один раз.

Вы можете попробовать скомпилировать некоторые операторы if с помощью avr-gcc и посмотреть на сборку, чтобы увидеть, как это делает компилятор.

...