ARM Как вызвать ветвление?

Просматривал код в отношении цикла.

loopinner ....
          SUBS R2,R2,#1 ; j--
          BGT loopinner ;in this case, loop should continue when j>1

В этом случае я не уверен, как BGT снова переходит к loopinner. Разве мне не нужно указывать, что это больше? Поскольку SUBS вызывает флаги, скажем, если j-- становится значением 1. Как ветвь узнает, какое значение больше?

Это ваш третий простой вопрос о сборке ARM за последние 12 часов. Вы посреди какого-то экзамена?
@ElliotAlderson Не совсем так, но я только начал изучать этот модуль заранее, раньше запланированного графика, и мне интересно узнать больше об ARM.
Инструкции ветвления смотрят на состояние различных флагов, установленных предыдущей инструкцией.
Обратите внимание, что он фактически зациклится, если j>0 (с использованием значения j после уменьшения), а не j>1.
какую часть документации по руке вы не понимаете? это четко задокументировано.
@Мип Ты в порядке? Надеюсь, у тебя все хорошо. С наилучшими пожеланиями!
@jonk Да, все хорошо! Постоянно учусь самостоятельно, спасибо за помощь!

Ответы (3)

Из условий ARM вы можете легко обнаружить, что инструкция проверяет флаги состояния Z, N и V и переходит, когда Z=0 и N=V. Поскольку он проверяет флаг состояния V, а не флаг состояния C, это явно предназначено как подписанный тест. ( Для меня это означает, что это бесполезно для управления беззнаковым циклом - к вашему сведению. )

Я написал это не так давно, с достаточным количеством информации, чтобы понять, что происходит. Но я могу обобщить это здесь.

Давайте использовать более простые 4-битные слова, где всего 16 символов:

Word     Signed     Subtrahend
0000         0         1111
0001         1         1110
0010         2         1101
0011         3         1100
0100         4         1011
0101         5         1010
0110         6         1001
0111         7         1000
1000        -8         0111
1001        -7         0110
1010        -6         0101
1011        -5         0100
1100        -4         0011
1101        -3         0010
1110        -2         0001
1111        -1         0000

Выше, третий столбец — это то, что фактически использует АЛУ при вычитании этого значения. Он просто инвертирует каждый бит перед добавлением. (АЛУ никогда ничего не вычитает. Оно даже не знает, как это сделать.) Итак, инструкция SUB фактически выполняет сложение, используя при сложении вычитаемую форму значения. (Если вы хотите понять семантику битов состояния, очень важно, чтобы вы овладели этой концепцией, поскольку она поможет вам, когда в противном случае вы бы запутались.)

Отпечатайте его себе на лоб -

А ЦП ТОЛЬКО ДОБАВЛЯЕТ. ВЫЧИТАТЬ НЕ МОЖЕТ .

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

А ЦП ТОЛЬКО ДОБАВЛЯЕТ. ВЫЧИТАТЬ НЕ МОЖЕТ .

Все должно быть приведено к семантике сложения. Все.

SUBS R2, R2, #1 в этой 4-битной вселенной, которую я только что создал, также добавили бы 1110 плюс перенос 1. Всего 16 вариантов:

Actual Operation    Operation Result    Operation      Comparison
 R2     SUBS OP        Z N V C ALU       Semantics      Semantics   Z=0 & N=V?
0000 + 1110 + 1        0 1 0 0 1111     0 - 1 = -1       0 > 1 ?    False
0001 + 1110 + 1        1 0 0 1 0000     1 - 1 =  0       1 > 1 ?    False
0010 + 1110 + 1        0 0 0 1 0001     2 - 1 =  1       2 > 1 ?    True
0011 + 1110 + 1        0 0 0 1 0010     3 - 1 =  2       3 > 1 ?    True
0100 + 1110 + 1        0 0 0 1 0011     4 - 1 =  3       4 > 1 ?    True
0101 + 1110 + 1        0 0 0 1 0100     5 - 1 =  4       5 > 1 ?    True
0110 + 1110 + 1        0 0 0 1 0101     6 - 1 =  5       6 > 1 ?    True
0111 + 1110 + 1        0 0 0 1 0110     7 - 1 =  6       7 > 1 ?    True
1000 + 1110 + 1        0 0 1 0 0111    -8 - 1 = -9 E    -8 > 1 ?    False
1001 + 1110 + 1        0 1 0 1 1000    -7 - 1 = -8      -7 > 1 ?    False
1010 + 1110 + 1        0 1 0 1 1001    -6 - 1 = -7      -6 > 1 ?    False
1011 + 1110 + 1        0 1 0 1 1010    -5 - 1 = -6      -5 > 1 ?    False
1100 + 1110 + 1        0 1 0 1 1011    -4 - 1 = -5      -4 > 1 ?    False
1101 + 1110 + 1        0 1 0 1 1100    -3 - 1 = -4      -3 > 1 ?    False
1110 + 1110 + 1        0 1 0 1 1101    -2 - 1 = -3      -2 > 1 ?    False
1111 + 1110 + 1        0 1 0 1 1110    -1 - 1 = -2      -1 > 1 ?    False

В разделе «Результат операции» у меня есть столбец для ALU . Поле ALU — это то , что возвращается в R2 после завершения инструкции SUBS. ( Флаг состояния V генерируется XOR переноса следующего за старшим значащим бита во время операции и самого бита переноса.) Также обратите внимание, что есть единственный случай, отмеченный E, когда произошло переполнение со знаком . .

Теперь вы можете легко понять, почему инструкция BGT применяет эти конкретные биты состояния именно так, как она это делает. По общему признанию, это использует 4-битные слова. Но точно такая же идея применима к гораздо более широким размерам слов без каких-либо изменений.

Оглядываясь назад на таблицу, вы можете видеть, что условие истинно тогда и только тогда, когда перед вычитанием R2 было равно 2 или больше , а не 1 или 0 или меньше.

Ваш вопрос:

Разве мне не нужно указывать, что это больше? Поскольку SUBS вызывает флаги, скажем, если j-- становится значением 1. Как ветвь узнает, какое значение больше?

Начнем со следующей таблицы из Справочного руководства по архитектуре ARMv6-M , стр. A6-99:

введите описание изображения здесь

Условие GT описывается как « Подписано больше, чем ». Причина, по которой в документации не указана константа, заключается в том, что этот тест выполняется после некоторой предыдущей инструкции. Эта предыдущая инструкция определяет контекст. Но без этого контекста все, что можно сказать, это общий знак > .

Итак, если предыдущей инструкцией была CMP:

введите описание изображения здесь

Тогда контекстом будет сравнение двух значений со знаком, и инструкция BGT будет означать «ветвь, когда знаковый операнд 1 больше, чем знаковый операнд 2».

Но в вашем случае с «SUBS R2, R2, # 1» контекст меняется, и инструкция BGT будет означать «ветвь, в то время как подписанный R2 все еще остается больше 0».

Сама инструкция условного перехода фактически не знает , какой была предыдущая инструкция. Он также даже не знает, какие регистры задействованы. Эти знания остаются у человека (или компилятора), который генерирует поток инструкций. Таким образом, инструкция ветвления на самом деле не имеет фиксированного постоянного значения и не имеет регистра для сравнения. Это полностью зависит от того, что более ранние инструкции делали с битами состояния. Он просто проверяет полученный статус, а затем делает то, что делает. Вам решать, знать контекст и правильно его использовать.

(Кстати, комментарий к исходному коду может вводить в заблуждение или быть неверным.)

Примечание

Эллиот возражает (см. обсуждение ниже) без доказательств. Он пишет: «Я мог бы также утверждать, что ЦП может только вычитать». Он может привести этот аргумент, но он чисто академический. На самом деле дело в том, что процессоры не вычитают. Они добавляют.

Так что, хотя это отчасти и мой ответ, предоставляющий четкие, недвусмысленные доказательства в поддержку, чтобы даже Эллиот мог понять ситуацию на местах, сегодня это также отличный переход. Поэтому я очень рад возможности, которую Эллиот предоставил мне для расширения дискуссии.

Мой первый процессор был собран из 7400 деталей, которые я построил и успешно завершил в 1974 году. К моему удивлению, появились газетные репортеры и написали об этом статью. Это мой первый опыт. С тех пор я профессионально работал в Intel, занимаясь тестированием чипсета для чипсета BX, и, поскольку это имело отношение к преподаванию этого предмета, я вел классы компьютерной архитектуры в качестве адъюнкт-профессора в Портлендском государственном университете в 1990-х годах, в классах было примерно 65-75 студентов. Это крупнейший 4-летний университет в штате Орегон.

Я чувствую двусмысленность (выражая двойственное отношение к тому, как могут быть выполнены вычисления) о том, как процессоры генерируют свои биты состояния и как они вычисляют, только приводит студентов к ненужной неопределенности, путанице и трудностям, на исправление которых могут уйти часы, недели, месяцы, а иногда даже годы. Точно так же, как преподавание теоретико-групповой абстрактной алгебры до того, как познакомить с основами, могло бы сбить с толку большинство студентов-первокурсников, так же и преподавание академических абстракций о том, как компьютеры могут что-то делать. Больше студентов пострадает, чем поможет.

Простая истина заключается в том, что декодирование инструкции выдает ADD, даже когда текст инструкции (в конце концов, это просто текст — это не то, что на самом деле происходит) говорит SUB. Декодирование по-прежнему выдает ADD. Он просто изменяет некоторые детали операнда по пути.

Точно так же, как и в случае с процессором ARM, приведенная выше теория — это все, что вам нужно, чтобы понять, как все устроено на самом деле.

Пожалуйста, не путайте себя! Компьютеры доп. Они не вычитают. Они просто немного возятся, чтобы это выглядело так, как будто они вычитают.

Хорошо это или плохо, важно понимать, что на самом деле делает компьютер, чтобы понимать определенные биты состояния; что они делают и почему они это делают. Другого пути нет. Вышеупомянутая теоретическая модель - это то, как все работает в современных процессорах, и как правильно работать и понимать биты состояния. Есть веская причина, почему все так , как есть.

Я надеюсь, что эти детали, приведенные выше, и те, что я напишу ниже, будут полезны. Любая ошибка, связанная с сообщением здесь, лежит на мне, и я с радостью постараюсь исправить, исправить и улучшить этот документ, где смогу.

Чтобы продолжить, я буду использовать Справочное руководство по архитектуре ARMv6-M в качестве справочного материала.

Начнем со страницы A6-187 (зарегистрировать регистр):

введите описание изображения здесь

Здесь вы можете видеть, что они четко документируют это поведение:

AddWithCarry(R[n], NOT(shifted), '1')

Это сложение с инвертированным операндом 2 (вычитаемое) и переносом, установленным на «1». Так же, как я писал выше, бывает. (Просто так это делается.)

В случае расширений из нескольких слов перейдите на страницу A6-173 и найдите SBCS:

введите описание изображения здесь

Здесь обратите внимание, что они снова используют дополнение:

AddWithCarry(R[n], NOT(shifted), APSR.C)

Вместо жестко запрограммированной «1» переноса, как для инструкции SUBS, теперь используется последнее сохраненное значение переноса. В этом случае обычно ожидается, что это будет выполнение предыдущей инструкции SUBS (или SBCS).

Для операций с несколькими словами каждый начинает с SUBS (или ADDS), а затем продолжает процесс с последующими SBCS (или ADCS), которые используют выполнение более ранних инструкций для поддержки операции с несколькими словами.

В сложении из нескольких слов этот перенос можно рассматривать как перенос , которым он и является. «1» означает, что перенос произошел и его необходимо устранить. «0» означает, что переноса не произошло.

В случае вычитания из нескольких слов этот перенос лучше рассматривать как перевернутое заимствование из . «1» означает, что не было необходимости заимствовать слова более высокого порядка. «0» указывает на необходимость заимствования. Поскольку инструкция SUBS всегда устанавливает это значение равным «1», это означает, что заимствования нет (результат вычитания требует «приращения», чтобы компенсировать инвертированный операнд 2). Но для инструкции SBCS, если APSR.C является « 0", то никакого "приращения" не происходит, и это то же самое, что и заимствование (поскольку требуется приращение, если заимствования нет).

Инструкция ADCS, найденная на странице A6-106, но не показанная здесь, также использует выполнение предыдущих инструкций. Он не инвертирует значение переноса или иным образом не делает что-то странное или необычное только потому, что это инструкция ADCS. Она делает то же самое, что и инструкция SBCS, за исключением одной незначительной детали — инструкция SBCS инвертирует операнд 2, а ADCS — нет. Вот и все.

Это один из действительно крутых аспектов работы этих деталей. Требуется совсем немного дополнительной логики, чтобы превратить сложение в вычитание и/или сложение из нескольких слов в вычитание из нескольких слов.

И, наконец, чтобы завершить историю, см. стр. A2-35:

введите описание изображения здесь

В соответствии с моим описанием того, как все работает на самом деле, выше.

Очень приятно видеть, как все это работает. Стоит некоторое время поиграть с различными знаковыми и беззнаковыми значениями и вручную установить и использовать флаги состояния. Это действительно углубляет эти идеи. И они очень хорошие!

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

А процессор только добавляет. Он не может вычесть.

Не имеет смысла догматически настаивать на том, что ЦП может только добавлять. Точно так же я мог бы утверждать, что ЦП может только вычитать, а «складывать» он отрицает второй операнд и вычитает. ALU — это всего лишь комбинационная логика, оптимизированная по времени, площади и мощности. Если я реализую сумматор/вычитатель в ПЛИС, я получаю один мультиплексор, который выполняет как сложение, так и вычитание, и ни одна операция не является более фундаментальной, чем другая.
@ElliotAlderson Раньше они могли делать что-то по-разному, и в те дни понимание статуса было сложнее, чем сегодня. Вы должны были прочитать руководство и посмотреть, что сделал какой-то дизайнер в тот раз. Но в настоящее время, единственное, что они делают, это добавляют. Что хорошо , а не плохо. Причина, по которой я делаю такой акцент, заключается в том, что как только эта идея проникнет глубоко в мозг, все всегда обретает смысл , и вы можете точно разобрать что угодно. Меньше запоминания, больше понимания. Так лучше.
@ElliotAlderson Между прочим, все, что я написал, совершенно точно.
Ну тогда согласимся не согласиться. По моему личному опыту, предположение, что у меня есть абсолютное знание, обычно имеет неприятные последствия.
@ElliotAlderson Принято. Спасибо. И вы были бы очень правы "в прошлом". Я не могу сосчитать количество странных решений, сделанных случайными дизайнерами процессоров. Я постоянно выискивал детали из руководств, потому что казалось, что нет двух людей, которые сделали бы одинаковый выбор. Там нет разногласий. Но я работал в Intel в конце 1990-х над проектированием и тестированием чипсета P II, и в то время я был наслышан о том, что теперь все находятся на одной волне в этом конкретном выборе конструкции ЦП. Это правда. Если вы когда-нибудь найдете современное исключение ALU из того, что я написал, предоставьте его, и я удалю свой ответ здесь.
@ElliotAlderson Кроме того, и что более важно, с ARM это точно так.
Что ж, тогда я извиняюсь, я не знал, что вы разработали ALU во всех реализациях кремния ARM, а также программные ядра, предусмотренные для реализации в FPGA. Тем не менее, я думаю, что мой пример реализации ALU на FPGA все еще актуален, и я понял, что эти обсуждения на самом деле не плодотворны. Мир.
@ElliotAlderson Ты знаешь, что я прав. В этой теме лучше быть недвусмысленным. И если под «быть плодотворным» вы подразумеваете, что вынуждать меня изменить правильную точку зрения, против которой нет контрдоказательств, как пустую трату времени, это достаточно верно. Но опять же, если вы предоставите мне хоть один современный пример в качестве доказательства, я признаю свою несостоятельность и тоже удалю пост. Вы получите все, что хотите. Просто найдите доказательства. Я очень открыт для того, чтобы ошибаться. Я просто знаю, что это не так. И тебе не нравится моя уверенность. Ну что ж.
В контексте Arm (согласно вопросу) @jonk является архитектурно правильным. Различные ArmARM определяют SUBSкак AddWithCarry(R[n], NOT(imm32), '1')(я могу расширить это, если это будет полезно), что охватывает этот ответ.
@ElliotAlderson Пожалуйста, посмотрите обновление. awjlogan подсказал мне, как найти нужные документы. (С уважением.) Это все есть.
@jonk - именно так, извините, слишком медленно с моей стороны :)
«Я мог бы с тем же успехом утверждать, что ЦП может только вычитать» — возможно, но вы будете предоставлены сами себе. Вычитание в ALU было реализовано как «отменить второй операнд, а затем добавить», по крайней мере, с 6502, возможно, до 8008, возможно, даже с 4004 или ранее. Задействованная основная схема называется «сумматор». Любая сравнимая схема «вычитателя» более сложна, чем сумматор, и по сути представляет собой сумматор с инвертором (который также является сумматором) перед ним (поскольку отрицание дополнения до двух - это «инвертировать биты» и добавить один ) . Извини, чувак, по сути это все дополнение.
@ElliotAlderson Я написал: «Если вы предоставите мне хоть один современный пример в качестве доказательства, я признаю свою неудачу ...», потому что принятие веских доказательств и признание ошибок важно для меня и определяет, кем я хочу быть для других. Ваш ответ: «Ну, тогда я извиняюсь, я не знал, что вы разработали АЛУ во всех реализациях кремния ARM…» Я бы не смог написать. Это не в моих костях. Если бы ситуация изменилась, я бы громко и ясно извинился, признал ошибку и пообещал исправиться. Вы позволяете этому стать поведением, которое определяет вас? Надеюсь нет.
Кто-то серьезно задается вопросом, используется ли логика сложения для вычитания, я подумал, что на данный момент каждый, кто действительно работает над этим или потратил несколько минут, чтобы посмотреть, знает это. На самом деле не имеет значения, решил ли технический писатель для некоторых задокументированных указать это или нет, имеет значение то, что непосредственно сделал компилятор языка, библиотека ячеек или автор кода.
@old_timer Да. Эллиот поставил меня в невыгодное положение, а затем защитился, приведя глупые аргументы, не имеющие под собой реальной основы. Он просто издевался надо мной. Но у нас с Эллиотом фундаментально противоречивые взгляды на преподавание (из предыдущих дискуссий). Мы оба были учителями — он говорит, что до сих пор им является. Точно так же он хочет, чтобы я следовал его личным правилам преподавания здесь. Долгая дискуссия, в которой нам пришлось не согласиться и оставить это там. Я подозреваю, что это было больше обо мне и его продолжающихся попытках запугать меня. И больше ничего, если честно. Теперь он оставил себя незащищенным.
@jonk ааа, да, у меня есть другие пользователи, с которыми у меня похожие проблемы. Да совсем разоблачен.

В вашем примере кода SUBинструкция имеет Sсуффикс, это означает, что подинструкция будет устанавливать флаги условий, которые будут BGTоцениваться. Чтобы ветка была принята, Zфлаг должен быть равен 0, а Nфлаг должен быть равенV

Я знаю, что подинструкция установит флаги условия, где N = не отрицательное, Z = не нулевое, C/V = беззнаковое/знаковое переполнение. Но как ветвь в этом случае узнает, когда перейти во внутренний цикл, когда, например, j=2, после декремента это 1, следовательно, N=0, Z=0, C/V=0, как BGT (больше ?) снова возвращается в цикл?
Почему он будет равен нулю, если R2 по-прежнему имеет значение 1? Что касается BGT, с чем он по сравнению? Больше чем? Извините за эти вопросы, новичок в ARM.
Команды меняют флаги. Проверка флагов позволяет проводить условное тестирование. Используйте C, чтобы объяснить ассемблер. Если j > 0, переход.
@Meep Если бы он был равен, Z был бы равен 1, если бы он был меньше, то я не совсем уверен, но, вероятно, C был бы равен 1. Поскольку оба они равны нулю, оно должно быть больше.
Это правильно. Он проверяет, больше ли он нуля, в этом случае оба флага C, V будут иметь одинаковое значение, а Z будет равно 0.

В документации по руке четко указано, что GT является знаковым больше чем, он будет разветвляться, когда Z==0,N==V.

Когда r2 = 2. Помните из начальной школы, что x - y = x + (-y), и с первого дня (или вскоре после этого) в компьютерной инженерии/науке/любом дополнительном отрицании до двух инвертируется и добавляется единица, поэтому x - y = x + (~y) + 1. Это экономит логику и позволяет выполнять вычитание.

      1  add one
   0010 
 + 1110  invert
==========

четырех бит более чем достаточно, чтобы увидеть, что происходит, результат такой же, как и 32 бита.

   11101
    0010 
  + 1110
 ==========
    0001

Таким образом, N = 0 и Z = 0 из результата. Перенос и перенос мсбита одинаковы, поэтому V = 0 (исключающее ИЛИ переноса и переноса мсбита также может быть выполнено путем проверки старших битов операндов и результата).

Нам нужны Z == 0 и N == V, чтобы выполнить ветвление, и они есть, поэтому ветвление происходит.

Вы обнаружите, что это относится к положительным числам, так как это знаковое больше, чем, если вы хотите, чтобы беззнаковое было больше, чем тогда, используйте bcs/bhs, логика работает так же, как она просто оптимизируется для использования только выполнения (это также можно увидеть, если вы смотрите на сгенерированный table jonk или создаете его сами)

Когда г2 = 1

   11111
    0001
 +  1110
 ==========
    0000

Z = 1, N = 0, V = 0

N == V, но Z != 0, поэтому ветвления не происходит.

короткая версия ответа Джонка, показывающая, как мы получаем N, V, Z. проголосуйте за джонки, если/когда/вместо этого вы проголосуете за этот.