Основы ассемблера Motorola 68000
Автор гида: vladikcomperОсновано на гиде «SCHG How-to:Work with Motorola 68000 assembly»
Обновление от 2023:
Этот гид был значительно обновлен 17.11.2023. Обновление исправило ранее допущенные ошибки и неточности формулировок, особенно в первой части. Настоящий гид все еще не претендует на полноту и пытается подать огромное количество информации в рамках одной статьи. Возможно, в будущем автор напишет полноценный гид с несколькими главами.
Этот гид для тех, кто слабо знает ассемблер или не знает его совсем. Из него вы узнаете что такое ассемблер, изучите самые базовые его команды.
Ассемблер (сокращенно: АСМ) — язык программирования низкого уровня, а также программа, которая переводит мнемоники этого языка в машинный код. Ассемблер исключительно близок к машинному коду, с той лишь разницей, что вместо нулей и единиц используются более удобные для восприятия людьми мнемоники (команды). Например, ассемблерная команда BRA (branch - прыжок) транслируется в байт со значением 60 (шестнадцатеричное) в машинном коде. Код ассемблера представляется в текстовом формате и редактируется в текстовом редакторе. Машинный код имеет двоичный (бинарный) формат и для его редактирования можно использовать разве что HEX-редактор.
По сравнению с языками высокого уровня, в ассемблере очень мало команд, следовательно его очень легко выучить, однако на нем довольно трудно программировать, чтобы реализовать какую-нибудь простую вещь, у вас может утйи много времени и страниц кода. Список команд, которые Вы изучите в этом гиде, даст вам мощный толчок, так что Вы вполне сможете изучить остальные команды самостоятельно, лишь узнав их назначение из справочника.
Команда MOVE
move.w #$1234, $FF0000
«Move» дословно значит «перемещать». Эта команда записывает значение в заданный адрес памяти. В данном случае в адрес памяти $FF0000 будет записано число «$1234». Весь код, выполняемый процессором так или иначе связан с записью чисел в разные места памяти.
На что за «.w» на после команды и адреса памяти? Это размер данных, в данном случае word. Процессор Motorola 68000 поддерживает следующие размеры команд:
- .b — byte (байт) — занимает 8 бит / 1 байт, значения от $0 до $FF;
- .w — word (слово) — занимает 16 бит / 2 байта, значения от $0 до $FFFF;
- .l — long (длинное слово) — занимает 32 бита / 4 байта, значения от $0 до $FFFFFFFF;
Внимательный читатель может сразу спросить: а какой размер у адреса памяти, в который мы записали число из примера выше? Здесь кроется подвох. Если вы еще раз посмотрите таблицу размеров, то увидите, что инструкция MOVE с размером «.w» (или word) запишет целых 2 байта. Однако, размер одной «ячейки памяти» в компьютерах - 1 байт. Так что же произойдет?
Наше значение $1234 состоит из 2 байт: $12 и $34. Первый байт будет записан по адресу $FF0000, второй — по следующему адресу $FF0001. Именно в таком порядке читаются и записываются 16-битные числа в Motorola 68000 (M68K). Такой порядок байт называется Big-endian. И напротив, есть little-endian процессоры (например, Z80), где порядок байт строго обратный (т.е. в первый адрес запишется $34, а во второй — $12).
Теперь изменим размер команды с word на long:
move.l #$00001234, $FF0000
Что изменилось? Теперь мы записываем не 2 байта, а целых 4! Давайте разобъем значение на байты и проследим, как оно затронет адреса памяти с новым размером:
- Мы записываем байты: $00, $00, $12, $34 начиная с адреса памяти $FF0000
- В адрес $FF0000 будет записан байт $00
- В адрес $FF0001 будет записан байт $00
- В адрес $FF0002 будет записан байт $12
- В адрес $FF0003 будет записан байт $34
Команды ADD и SUB
Это базовые алгебраические операции, ADD реализует сложение, SUB — вычитание.
move.w #$1010, ($FF0000) add.w #2, ($FF0000) ; Сложение: $1010 + $0002 = $1012 sub.w #4, ($FF0000) ; Вычитание: $1012 - $0004 = $100E
Обратите внимание на знак доллара ($) перед значением «1010» в первой строке. Он обозначает, что число записано в шестнадцатеричной системе счисления. Эту же роль он выполняет и перед адресом памяти $FF0000. В исходном коде Сеговских игр при работе с числами используется в основном именно эта система. Этот знак обычно опускают перед числами от 0 до 9, так как они в обоих системах обозначают одни и те же числа. Однако никто вам не мешает опустить знак доллара перед числом, чтобы записать его в десятичной системе.
А символ «;» начинает комментарий. Весь текст на строке после этого знака игнорируется компилятором. Комментарии, однако, очень полезны, они помогают ориентироваться в коде и узнавать, что делает тот или иной его участок (если комментарии есть).
Давайте попробуем поиграть с размерами команд:
move.w #$1010, ($FF0000) add.b #2, ($FF0000) sub.b #4, ($FF0000)
Что теперь будет записано в $FF0000 и $FF0001, и почему?
Ответ: После add.b #2 в $FF0000 будет $12, в $FF0001 так и останется $10 (16-битное значение: $1210). После sub.b #4 в $FF0000 будет $0E, в $FF0001 так и останется $10 (16-битное значение: $0E10). Если у вас возникает сложность в понимании этого, перечитайте про размеры данных и как записываются числа в память на примере команды MOVE выше.
Регистры данных и адресов
Регистры данных (от d0 до d7) — подобно адресам памяти, содержат в себе числовые значение. Они вмещают до 32 бит или 4 байт данных (в зависимости от размера инструкции). Регистры используются чаще всего для промежуточных вычислений, многим напоминая переменные в языках высокого уровня. Некоторые команды асемблера работают исключительно с регистрами, или имеют ограничения на адресацию памяти. В отличие от внешней памяти доступ к регистрам — моментальный, соответственно команды над регистрами работают быстрее.
Приведу простой пример их использования. Допустим, нам нужно взять число из одного адреса памяти, увеличить его и записать в другой:
move.b ($FFFFFEB8).w,d0 ; записываем число из указанного адреса в регистр add.b #2,d0 ; увеличиваем это число на 2 move.b d0,($FFFFFEE6).w ; записываем это число в другой адрес памяти
Регистры адресов (от a0 до a6) — ссылаются на адреса памяти, многим напоминая указатели в языках высокого уровня. Скажем, в коде внутриигровых объектов они ссылаются на адреса памяти, в которых и хранятся все данные об объекте. Их так же можно использовать вместо адресов, при записи в них данных, эти данные отправлются в адрес памяти, на который они указывают.
Внимательный читатель может заметить, что регистры данных идут до d7 включительно, а регистры адресов заканчиваются на a6. Куда делся a7? Он здесь, но трогать его не следует: он зарезервирован под стек.
Команды сравнения и перехода
Именно эти команды оживляют ассемблер, они выполняют роль конструкции «if» из языков высокого уровня.
Небольшой пример:
Subroutine1: cmp.w #$1010,($FFFFFEB8).w ; сравнить значение в адресе $FFFFFEB8 с $1010 beq Subroutine2 ; если значения равны, переходим rts ; завершить суброутину Subroutine2: move.w ($FFFFFEB8).w,d0 add.w d0,($FFFFFEB8).w ; увеличиваем число в 2 раза rts
В этом коде присутствует немало новых вещей. Давайте все разберем.
Команда «cmp» сравнивает заданное значение (в данном случае это — число $1010) с тем, что записано по адресу $FFFFFEB8. Потом в работу включается команда «beq», которая переходит к суброутине «Subroutine2», если «cmp» сказал, что значения равны. Если переход состоялся, то все что следует за «beq», выполняться не будет.
«beq» — команда перехода. На них стоит остановиться подробнее, так как именно они определяют условие перехода и являются важными, часто-используемыми командами.
Команд перехода очень много, вот самые нужные из них:
BEQ — Branch if EQual — Перейти, если равно
BNE — Branch if Not Equal — Перейти, если не равно
BGT — Branch if Greater Than — Перейти, если больше
BGE — Branch if Greater or Equal — Перейти, если больше или равно
BLT — Branch if Less Than — Перейти, если меньше
BLE — Branch if Less or Equal — Перейти, если меньше или равно
Используя команду BNE можно составить код, эквивалентный примеру выше:
Subroutine1: cmp.w #$1010,($FFFFFEB8).w ; сравнить значение в адресе $FFFFFEB8 с $1010 bne Subroutine2 ; если значения не равны, переходим move.w ($FFFFFEB8).w,d0 add.w d0,($FFFFFEB8).w ; увеличиваем число в 2 раза Subroutine2: rts
Результат работы кода будет таким же, но вглядитесь — он получился меньше, потому что пропала одна команда «rts». Вы, должно быть сейчас думаете: «Что за фигня?». Не бойтесь, до меня все тоже не сразу дошло.
Так, давайте разберемся. Если одного «rts» нет, то что завершает Subroutine1? «Rts» из Subroutine2! Многие люди, знающие языки высокого уровня вначале принимают Subroutine1 и Subroutine2 за функции, у которых есть начало и конец.
Subroutine1 и Subroutine2 — это лейбелы. Лейбелы могут содержать из латинские символы и цифры. Пробелов и цифры в начале названия не допустимы. Они не несут на код никакой смысловой нагрузки, это лишь «закладки» для навигации по коду. Так что когда выполнится команда «add.w d0,($FFFFFEB8).w», процессор перейдет к команде «rts».
Команды безусловного перехода
Команды безусловного перехода выполняют переход вне зависимости от ситуации, поэтому перед ними не нужна команда «cmp». Их немного: BRA/JMP, BSR/JSR. Мнемоники BRA/BSR образованы от слова «Branch», а JMP/JSR — от «Jump».
И BRA, и JMP делают одно и то же: прыгают в заданное место кода.
Аналогично, BSR и JSR тоже идентичны: переходят в заданное место, но когда в той ветке кода встречается команда «rts», они возвращаются обратно и продолжают выполнять код после себя.
У читателя может возникнуть вопрос: почему BRA и JMP разные команды, если они делают фактически одно и то же? Аналогично с BSR и JSR.
Команды «Branch» (BRA, BSR и другие) имеют ограничение на «дальность» прыжка. Они чаще используются для «локальных» переходов — в место программы, не слишком далекое от самой инструкции перехода.
Команды «Jump» (JMP и JSR) поддерживают разные режимы адресации и, в общем-то, имеют власть прыгнуть в абсолютно любое место кода, неважно насколько далекое. Разумеется, у «далеких» прыжков есть цена — инструкция занимает больше байт и выполняется чуть медленнее.
Некоторые нюансы
Поздравьте себя, у вас есть базовые знания ассемблера! Отдохните, почитайте исходник Сониковских игр, потренируйтесь и переварите всё выше-прочитанное. Тогда вы поймете многие вещи и дальнейшее изучение ассемблера будет легким и приятным.
Пока можете расслабиться и принять к сведению некоторые нюансы:
Существуют разновидности некоторых знакомых вам комманд. Скажем, у MOVE есть разновидность MOVEQ, у команд CMP, ADD и SUB разновидностей еще больше: ADDI, ADDQ, SUBI, SUBQ, CMPI— только основные из них. Когда увидите такие команды, не пугайтесь — эти команды выполняют тоже самое. Скажем, приставки -Q и -I означают «Quick» и «Immediate» соответственно. MOVEQ, ADDQ и SUBQ работают быстрее, но имеют ограничения на размер чисел.
Так же помните, что многие адреса памяти заняты и жизненно важны для правильной работы игры. Так что если вам нужен новый адрес, куда вы хотите записывать свои данные, потрать время на поиск свободного. Занятые адреса и их назначение указано в справочниках. Вот, скажем таблица адресов для первого Соника: RAM-адреса.
Все, гид окончен, можете свободно вздохнуть.