Подобные ошибки, скорее всего, терзают код, написанный на других языках ассемблера.

В моем университете курс компьютерной архитектуры начального уровня учит студентов программировать на ассемблере MIPS. Хотя процессоры MIPS больше не популярны, классификация RISC (компьютер с сокращенным набором инструкций) набора инструкций MIPS делает его привлекательным для образовательных целей. В соответствии с философией RISC сборка MIPS включает относительно небольшой набор простых команд. Кроме того, навыки программиста MIPS могут быть успешно распространены на более практически релевантные RISC-архитектуры, такие как ARM (которая широко используется в мобильных и встроенных системах). Здесь я опишу две ошибки, с которыми часто спотыкаются начинающие программисты MIPS; вполне вероятно, что подобные ошибки могут возникать и в программах, написанных в других наборах инструкций RISC.

Ошибка №1. При настройке операций ветвления не думайте, что метки служат преградой для выполнения блока инструкций.

Если вы хотите настроить условный оператор (например, «Если») в MIPS, вам нужно будет использовать одну из инструкций ветвления, наиболее фундаментальной из которых является beq(«ветвь на равных ») и bne(“ветвь не равна”). Вот краткий пример:

beq $t0, $t1, myLabel
sub $t2,$t3,$t4
j endLabel #this is the instruction that people often forget!
myLabel: 
add $t2,$t3,$t4
endLabel:
or $t5,$t6,$7

Этот блок кода говорит: сравните целочисленное содержимое регистров $t0 и $t1 и, если они равны, выполните «переход» к первой инструкции (здесь — инструкции add), которая следует за myLabelметка (метки обозначаются текстом, за которым следует «:»). Более конкретно: адрес, хранящийся в $pc, регистре счетчика программ, обновляется до адреса первой инструкции, следующей за myLabel. Если бы использовалось bne, а не beq, то переход возник бы, когда содержимое $t0 и $t1 не равно.

Обратите внимание, что в случае перехода две инструкции, следующие за строкой beq, не выполняются. В стандартном синтаксисе if…then это аналогично переходу к блоку else после невыполнения оператора if(t0 != t1). После выполнения инструкции add регистр $pc указывает на инструкцию или. Наличие метки endLabel не препятствует этому: метки не препятствуют выполнению кода.

Как следует из комментария к примеру кода, классическая ошибка связана с невозможностью включить инструкцию j («переход») перед меткой myLabel. Как правило, блоки инструкций, предшествующие и следующие за меткой myLabel, должны быть взаимоисключающими: как и в логике if…then, один блок выполняется только в том случае, если условие выполняется. , а другой блок выполняется, если условие ложно. Однако для обеспечения этого результата необходимо вставить инструкцию j, которая реализует безусловный переход к endLabel. Если бы инструкция j отсутствовала, то за выполнением инструкции sub последовало бы выполнение инструкции add, потому что myLabel не делит автоматически окружающие строки инструкций на взаимоисключающие блоки, как это сделал бы оператор else.

Ошибка № 2. При загрузке данных размером в байт или половину слова из памяти не забывайте, что стандартные инструкции загрузки расширяют бит знака, чтобы заполнить оставшуюся часть 32-битного слова; для предотвращения этого требуется неподписанная загрузка.

В MIPS принято копировать данные из одного из 32-битных регистров по указанному адресу в основной памяти и наоборот. В зависимости от выбранной вами инструкции, вы можете хранить все содержимое регистра или его часть по адресу, указанному вами с помощью схемы относительной адресации. Пример того, как можно сохранить все 32 бита (= 1 «слово»), показан ниже (обратите внимание, что первые четыре строки служат только для установки значений регистров, а все шестнадцатеричные значения в конечном итоге сохраняются как двоичные):

lui $t1, 0x1001 #sets top 16 bits of $t1 to 0x1001
ori $t1, $t1, 0x0000 #sets lower 16 bits of $t1 to 0x0000
lui $t2, 0xffff #sets top 16 bits of $t2 to 0xffff (16 1's)
ori $t2, 0xffff #sets lower 16 bits of $t2 to 0xffff
sw $t2, 4($t1) #stores 0xffffffff (32 1's) at the byte address equal to $t1 + 4 (0x10010000 + 4 = 0x10010004)

Вкратце: мы настроили регистр $t1 для хранения значения 0x10010000, а $t2 — значение 0xffffffff. Мы используем инструкцию sw («сохранить слово»), чтобы указать процессору MIPS скопировать все значение 0xffffffff в $t2 в место, указанное суммой содержимого $t1 и непосредственного значения 4, в результате чего по адресу назначения 0x10010004. Поскольку MIPS использует адресацию байтов, это относится к байту # 0x10010004 в основной памяти. «Первый» байт в 0xffffffff (где значение «первого» зависит от порядка следования байтов) попадает конкретно в байт 0x10010004, а остальные 3 байта хранятся в 0x10010005–0x10010007.

Давайте добавим в этот блок кода две альтернативные инструкции сохранения:

lui $t1, 0x1001 #sets top 16 bits of $t1 to 0x1001
ori $t1, $t1, 0x0000 #sets lower 16 bits of $t1 to 0x0000
lui $t2, 0xffff #sets top 16 bits of $t2 to 0xffff (16 1's)
ori $t2, 0xffff #sets lower 16 bits of $t2 to 0xffff
sw $t2, 4($t1) #stores 0xffffffff (32 1's) at the byte address equal to $t1 + 4 (0x10010000 + 4 = 0x10010004)
sh $t2, 8($t1) #stores 0xffff starting at byte address 0x10010008
sb $t2, 12($t1)#stores 0xff at byte address 0x1001000c

Первая новая инструкция использует sh («сохранить полуслово») для сохранения младших 16 битов в $t2 (0xffff), начиная с адреса байта 0x10010008. Следовательно, байты 0x10010008–0x10010009 будут содержать значение 0xffff; никакие новые значения не записываются в 0x1001000a–0x1001000b. Вторая новая инструкция использует sb («сохранить байт») для сохранения младших 8 битов в $t2 (0xff) по адресу байта 0x1001000c. Никакие новые значения не записываются ни в один из следующих байтов.

Выше были рассмотрены инструкции магазина MIPS. Как указано в заголовке раздела, ошибки часто возникают, когда значения загружаются обратно из памяти в регистры. Если мы хотим загрузить целое 32-битное слово, то этот процесс прост; мы можем использовать инструкцию lw («загрузить слово»):

lw $t3, 4($t1) #copy 32 bits, starting at 0x10010004, into $t3

После выполнения вышеуказанной инструкции в регистре $t3 появится значение 0xffffffff. Ситуация усложняется, когда мы рассматриваем загрузку полуслов и байтов. Мы рассмотрим пример с полусловами. MIPS предлагает два варианта загрузки полуслов: lh («загрузить полуслово») и lhu («загрузить полуслово без знака»). Разница в работе этих двух инструкций показана ниже:

lh $t4, 8($t1) #will load 0xffffffff into register $t4 
lhu $t5, 8($t1) #will load 0x0000ffff into register $t5

Первоначально мы сохраняли в памяти только полуслово 0xffff по адресу 8($t1). Так почему же lh загружает 0xffffffff в $t4? Это происходит потому, что lh выполняет sign-extension: он идентифицирует бит знака полуслова, расположенного по адресу 0x10010008 (напомним, что в дополнении до двух бит знака является старшим битом), и копирует этот бит (для 0xffff, «1», указывающий отрицательный знак) в старшие 16 бит регистра назначения. Такое поведение желательно, если мы намеревались интерпретировать полуслово 0xffff как десятичное значение со знаком -1; в этом случае расширение знака гарантирует сохранение отрицательного значения, когда оно занимает регистр $t4.

Если мы предпочитаем, чтобы 0xffff интерпретировался как целое число без знака — в данном случае 65535 — мы должны использовать lhu. Эта инструкция выполняет нуль-расширение: она заполняет старшие 16 бит регистра назначения нулями, независимо от состояния знакового бита нашего сохраненного полуслова. Аналогичная логика применяется к различию между инструкцией «загрузить байт» (lb) и инструкцией «загрузить байт без знака» (lbu).

Большинство ошибок, которые я вижу, связаны с использованием операций со знаком (lh/lb), когда уместны операции без знака (lhu/lbu). Например, программист может попытаться упаковать массив целых чисел без знака, все размером в байт (то есть ‹ 2⁸), в последовательность адресов байтов в памяти. Это вполне разумно, но если любое из этих целых чисел превышает 127, старший бит в сохраненном байте будет равен 1, и инструкция lb загрузит отрицательные значения в регистры назначения.