я пытался сделать программу, которая мерцает RPI3 b+ с Armv7 Assembly и заметил, что она не работает, используя этот код для функции задержки
delay:
b loop
loop:
add r10, r10, #1
cmp r10, r4
bne loop
beq return
return:
mov r10, #0
bx lr
r10 — это регистр, используемый для счетчика, а r4 содержит, что r10 должен достичь, чтобы остановить и вернуться к основному коду. Посмотрев учебник, я обнаружил, что они выполняют операцию xor для регистра счетчика, я добавил исправление, и теперь код выглядит так.
delay:
eor r10, r10, r10
b loop
loop:
add r10, r10, #1
cmp r10, r4
bne loop
beq return
return:
mov r10, #0
bx lr
Я скомпилировал, загрузил его в rpi3, и теперь он работает, но зачем мне было добавлять эту строку, я знаю, что такое xor, но если два входа равны, он вернет точно такое же значение. В чем смысл этой операции?
Обычной практикой является использование xor для обнуления регистра на x86 из-за различных особенностей и истории, которые привели к тому, что это было более эффективно, чем очевидная инструкция mov. Ничто из этого не относится к ARM, поэтому вы можете и должны использовать mov r10, #0, который более понятен и, по крайней мере, столь же эффективен, возможно, даже больше, потому что нет зависимости от ложного чтения от r10.
Чтобы добавить к просмотру кода, две условные ветки подряд не нужны и поэтому являются плохой практикой; условный переход к следующей инструкции также не имеет смысла.





TL:DR: XOR то же самое, то же самое похоже на sub same,same, производя ноль.
Этот учебник не очень хорош, как и обнуление XOR на ARM или любой RISC ISA. Используйте его только в x86 asm (и 8080), а не в asm для других ISA, и не в языках высокого уровня.
но если два входа равны, он вернет точно такое же значение.
Нет, это будет обычное неисключающее ИЛИ. XOR дает вам биты, которые были другими. Когда оба входа одинаковы, результат равен 0.
XOR-обнуление хорошо только на x86. (См. Как лучше всего обнулить регистр в сборке x86: xor, mov или and? подробнее почему). Ни одна из этих причин не применима к ARM: mov reg, #0 имеет тот же размер в машинном коде, что и eor reg,reg,reg, поэтому не было исторической причины поддерживать EOR как «обнуляющую идиому», которая особенно характерна для современных процессоров.
(Это верно даже в коде Thumb, хотя в этом случае вы хотите movs reg, #0 для меньшей кодировки, по крайней мере, с r0-r7. r8-r14 требуется 4-байтовая кодировка Thumb2 независимо от установки флагов или нет.)
На самом деле ЦП ARM даже архитектурно не позволяет оптимизировать eor dst, same,same, чтобы сломать ложную зависимость, потому что правила упорядочения зависимостей памяти требуют EOR и других операций для переноса зависимости. (например, для использования результата нагрузки std::memory_order_consume.) Не то чтобы они стали тратить на это транзисторы и питание, поскольку машинному коду ARM нет причин использовать это в первую очередь, когда mov reg, #0 работает отлично.
Так что eor r10, r10, r10 явно хуже, чем mov r10, #0.
Никогда не используйте его, если вам не нужен 0, который зависит от старого значения R10. Если вы не знаете, что это значит, вы этого не хотите; это было бы полезно только в многопоточном коде для результата загрузки, такого как флаг data_ready, или в экспериментах с микробенчмарками для тестирования неупорядоченного планирования или задержки по сравнению с пропускной способностью путем создания постоянного значения с зависимостью данных от некоторого результата.
На x86 он сохранил байт размера машинного кода по сравнению с mov ax, 0 и 3 байта в 32-битном режиме, поэтому реальный код использовал его везде. Более поздние ЦП эволюционировали, чтобы сделать его по-прежнему эффективным даже при неупорядоченном выполнении, где в противном случае чтение старого значения регистра в качестве ввода было бы проблемой. (В отличие от mov reg, 0, который, как мы ожидаем, не будет иметь ложной зависимости даже без какой-либо специальной поддержки. mov всегда нарушает зависимость; специальный регистр xor same,same на x86 просто делает его равным в этом смысле. xor-zeroing лучше в другие способы на x86.)
Этот «учебник» явно был написан другим новичком в качестве учебного упражнения (что характерно для случайных руководств, которые вы найдете в Интернете; написать хороший учебник — большая работа).
Это не пример хорошего эффективного кода, учитывая эту ошибку (отсутствует обнуление счетчика циклов) и две бесполезные b next_instruction инструкции. Выполнение в любом случае переходит к следующей инструкции, даже если вы этого не сделаете b или beq return.
Большинство условных ветвей должны быть просто сравнением и одной ветвью, а другой путь выполнения должен быть проходным. Это что-то вроде анти-шаблона для начинающего кода — помещать другую ветку с противоположным условием одну за другой. Или сделать нижнюю часть цикла while(1) { if (cond)break } вместо просто do{}while(cond); - в вашем цикле по крайней мере бесполезная ветвь находится вне цикла. Но это цикл задержки, который в любом случае существует только для того, чтобы тратить время, поэтому на самом деле он просто тратит впустую размер кода и изменяет коэффициент задержки циклов на счет.
Если вам нужно, чтобы выполнение выполнялось куда-то еще в обоих случаях (т.е. обе возможные цели находятся после другого кода, который должен попасть в него), то вторая ветвь должна быть безусловной b. И вы никогда не должны писать ветку, которая переходит к следующей инструкции в исходном порядке, потому что выполнение все равно пойдет туда, даже если ветки не было.
Спасибо чувак. Изучение ассемблера действительно сбивает с толку после использования только языков программирования более высокого уровня.
@ jack07Code, начните с того, что вы уже знаете из других языков, и посмотрите, как то же самое делается в ассемблере: циклы for, while; если-то/если-то-иначе; массивы и индексация; указатели; вызовы функций. Каждый из них имеет относительно прямой и простой перевод на ассемблере.
@ jack07Code: В продолжение комментария Эрика см. Как удалить «шум» из вывода сборки GCC/clang? о том, как писать простые функции на C, которые компилируются именно в тот ассемблер, который вы хотите видеть.
Я думаю, что eor vs mov — отвлекающий маневр. Вы очищаете r10 в начале процедуры и в конце процедуры. Согласно ABI, r10 — это регистр сохранения вызываемого абонента. Вы не можете знать, что r10 по возвращении будет равно нулю. Просто переместите mov.
Вот подпрограмма, которую можно вызвать из 'C'.
# Put count in `r0` and count down.
delay:
; you can add 'nop' instructions here to increase loop time.
subs r0, r0, #1 ; subtract and set conditon codes
bne delay ; branch if not zero
bx lr ; return to caller.
mov Rx, #0 и eor Rx, Rx, Rx функционально эквивалентны, так как в «Rx» впоследствии равно нулю. Сроки, коды состояния и другие вещи могут отличаться. Но вряд ли поэтому у вас не работает задержка.
Его можно назвать от 'C' как delay(20);. Если у вас вся кодовая база на ассемблере, то вполне вероятно, что какой-то регистр где-то затерт и вам нужно показать полный пример (или дать ссылку на туториал).
Есть лучшие примеры задержки, которые делают время постоянным (ветвь или отсутствие ветвления), но этого примера достаточно для обучения.
Код Oldtimers, вероятно, лучше читать, чем любой учебник, который вы просматриваете. github.com/dwelch67 DaveSpace тоже хороший ресурс. Эта книга дешевая и очень хорошая. Все разные дейвы.
Тогда вы неправильно поняли
xor. Если входные данные одинаковы, он дает ноль. Это обычная практика для обнуления регистра. Поскольку вы хотите, чтобыr10считалось с нуля, вы должны обнулить его. Вам не нужно использоватьxorдля этого, конечно, вы можете с таким же успехомmov r10, #0. Такжеb loopбесполезен (в любом случае это следующая инструкция). Непонятно, почемуr10обнуляется в конце перед возвратом, но это, вероятно, плохая идея.