Обучающая сборка - часть 4.1

Напишем код языка ассемблера!

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

Ассемблер - это язык программирования очень низкого уровня - все, что вы пишете, «близко к металлу». Это означает, что язык очень зависит от оборудования, на котором он установлен. В этой статье мы узнаем, как написать вариант на ассемблере. В частности, мы рассмотрим язык ассемблера 6502. 6502 - классический процессор, который можно найти во многих старых технологиях. Не позволяйте возрасту беспокоить вас, основные принципы сборки останутся неизменными, независимо от того, смотрим ли мы на 6502 или Intel i7.

Чтобы узнать, как написать этот код, мы рассмотрим два небольших примера. Примеры были отобраны, чтобы дать нам краткое представление об основных функциях языка. Мы также рассмотрим, как сделать наш код более читабельным, используя функции, которые может предоставить нам Ассемблер.

Технически это часть 4 из серии «обучающих сборок», которую я пишу. Однако по возможности я стараюсь обеспечить независимость каждой части. Если все, что вас интересует, это азы сборки, надеюсь, вам это подойдет.

Давайте начнем с очень краткого обзора оборудования 6502.

Небольшое резюме

Язык ассемблера может быть определен набором инструкций. Инструкция - это команда, которую процессор сможет интерпретировать (например, добавить или переместить что-то в памяти). Доступные нам инструкции глубоко связаны с самим оборудованием. Использование процессора 6502 дает нам доступ примерно к 50 инструкциям. Помимо этих 50 инструкций, если мы хотим, чтобы процессор что-то делал, нам нужно будет создать это самостоятельно.

Более подробное описание аппаратного обеспечения 6502 доступно здесь, но мы можем остановиться на некоторых вещах, которые могут быть важны для нас сегодня:

Накопитель: это часть оборудования 6502, которая может помочь нам в арифметических операциях. Если бы мы хотели сложить два числа, мы могли бы отправить их в аккумулятор, где они «накапливались» вместе.

Регистр переноса: Представьте, если бы мы сделали сумму 26 + 15.

 (1) <- our carry
  26
+̲ ̲1̲5̲
  41

Что ж, мы добавили 5 и 6, что дает 1, нести-а-1. Затем мы могли бы сделать 20 + 10 + -10-мы перенесли, чтобы получить 40. Объединяя их, мы получаем ответ 41.

Что ж, когда мы складываем двоичные числа, происходит нечто подобное - мы все еще несем вещи. Однако в 6502 мы можем отслеживать только то, что умещается в 8-битном числе. Если мы сделаем некоторые математические вычисления, которые приведут к тому, что у нас будет что-то, что не умещается в 8-битном формате, нам нужно это отслеживать. Например, 1000 0001 + 1000 0000 дает 1 0000 0001. Однако мы не можем уместить крайнюю левую единицу, которую мы должны были нести, в 8-битные, но мы также не можем забыть об этом, если хотим, чтобы наша математика работала.

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

Добавление

Самый простой пример - 1 + 2. Вот код:

LDA     #01      
ADC     #02      
STA     $0402

Слева от каждой строки находится инструкция, а справа - данные или ячейка памяти, необходимая для работы инструкции. В этом стиле пишется язык ассемблера. Давайте рассмотрим пример по очереди.

Строка 1: LDA указывает 6502 загрузить часть данных, которая следует в накопитель (A). Вот это #01. Мы можем префикс наших данных с помощью символа, чтобы сообщить ассемблеру, какие данные мы ему передаем. В этом случае мы используем хэштег, который означает, что наши данные являются «буквальными» - мы буквально передаем им число. После этой строки содержимое A равно 1.

Строка 2: ADC означает добавление следующего значения ко всему, что в настоящее время находится в аккумуляторе. Буква «C» в инструкции говорит нам, что она будет отслеживать любые переносы, которые произошли в предыдущих расчетах. Здесь мы предполагаем, что C = 0. Следовательно, все, что мы добавляем в аккумулятор, is#02. Теперь значение в A равно 3.

Строка 3: инструкция STA отправляет содержимое аккумулятора в место в памяти. В нашем примере это отправит значение 3 по адресу памяти $0402. Обратите внимание, мы не использовали # - это потому, что он не буквальный, это адрес. Кроме того, мы использовали $, который сообщает ассемблеру, что это значение находится в двоичном формате.

После этих трех инструкций мы успешно сделали 1 + 2 - молодец! Если бы мы хотели увидеть ответ, нам нужно было бы посмотреть $0402.

Адресация и флаги

В Части 3 мы обсудили многочисленные регистры, которые могут быть установлены в 6502. Они имеют множество применений, в том числе для отслеживания аспектов арифметики. Мы также упоминали выше, что ADC также добавит все, что было перенесено из последнего вычисления (т. Е. Все, что было в регистре переноса). Нам нужно быть осторожными, чтобы то, что происходит, на самом деле происходило именно так, как мы хотим. Мы не хотим добавлять в Carry, когда мы этого не хотим, и мы не хотим пропустить это, если мы действительно этого хотим. Мы можем вручную сбросить флаги, чтобы исключить непредвиденные значения в нашей арифметике - лучше перестраховаться, чем сожалеть. Это то, что делают CLC (очистить флаг переноса) и CLD (очистить десятичный флаг).

В предыдущем примере мы напрямую ссылались на значения 1 и 2. Там все было нормально, так как у нас есть только небольшое количество значений, которые нам нужно использовать в наших расчетах. Однако, если бы мы занимались чем-то более сложным, это было бы утомительно. В Python вы можете вызвать переменную «x» или «y» и использовать их во всем коде. Обычно вы сохраняете свои значения где-нибудь в памяти, а затем в коде указываете на это место в памяти. Это избавит нас от явной передачи значений. Например, значение 1 может храниться в ячейке $0400, а значение 2 - в ячейке $0401. Теперь мы можем просто обратиться к ним и получить тот же результат, что и раньше.

Ниже приведен код с этими двумя изменениями.

CLC            
CLD            

LDA     $0400   
ADC     $0401  
STA     $0402

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

  • Вы можете хранить значения в «переменной». Итак, вы можете сказать ADR1 = $0400, а затем всякий раз, когда вы вызываете ADR1, ассемблер поймет, что вы имели в виду адрес, содержащийся в нем.
  • Вы можете оставлять комментарии с помощью;.
  • Вы можете давать названия блокам кода. Это позволяет нам многократно обращаться к нему в нашей программе. Скоро мы увидим пример, где это может быть полезно.

Приведенный ниже код включает приведенные выше предложения:

    ADR1 = $0400
    ADR2 = $0401
    ADR3 = $0402

    CLC             ; clear carry bit
    CLD             ; clear decimal bit

ADD LDA     ADR1    ; load the contents of ADR1 into accumulator
    ADC     ADR2    ; add the contents of ADR2 to accumulator
    STA     ADR3    ; save result to ADR3

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

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

Чит для маленького дивизиона

Перед попыткой умножения (в следующий раз!) Мы быстро рассмотрим деление на 2. Это легко сделать, посмотрев на операции, которые мы можем выполнять с двоичными числами. Давайте посмотрим, что произойдет, если мы сдвинем двоичное число вправо. Число 16 (0001 0000) становится 8 (0000 1000), а 56 (0011 1000) становится 28 (0001 1100). Сдвиг вправо эквивалентен делению на два! По сравнению с «правильным» делением и умножением это значительно проще. Думаю, это тоже круто!

Такое переключение стало возможным благодаря многочисленным инструкциям, которые есть в 6502. Набор команд содержит 2 смены и 2 поворота. На следующем рисунке показан пример каждого из них:

ROL вращает число влево, при этом самый левый бит попадает в регистр переноса, а старое содержимое C перемещается в крайний правый бит.

LSR сдвигает число вправо. Здесь, правда, самый правый бит попадает в перенос, а самый левый заменяется на 0. Это то, что мы можем использовать для деления на два.

У этих двух инструкций есть эквиваленты, которые также действуют в противоположном направлении. А пока, однако, нам понадобятся ROL и LSR для умножения.

Вот краткий пример того, как может выглядеть разделение:

    ADR1 = $0400

    CLC             ; clear carry bit
    CLD             ; clear decimal bit

DIV LSR     ADR1    ; shift all the bits in ADR1 to the right

Заключение

Мы видели два полезных примера кода: сложение и упрощенная версия деления. Эти примеры открывают для нас ряд инструкций, доступных на 6502.

Раньше у этой статьи было около 20 минут для чтения, но я решил разделить ее на две части для удобства чтения. Итак, в следующий раз мы рассмотрим более сложный пример: умножение.

Это четвертая часть моей серии «обучающих сборок».

Эта статья адаптирована из моего личного блога. Большая часть контента, о котором я говорю, будет поступать из двух основных источников: Программирование на языке ассемблера 6502 Ланса Левенталя и Программирование 6502 Родни Закса.