Статья для тех кто не знаком с ассемблерами - но хочет взглянуть "одним глазком". Мы не сделаем вас гуру разработки на ассемблере за 15 минут - но покажем ассемблеры для нескольких популярных архитектур микроконтроллеров (ARM32, AVR, MSP430, 8051) - и для настольных наших компьютеров (x86 под Linux и DOS) - чтобы увидеть их различия и сходства - и не бояться погрузиться глубже, если что-то из этого может быть вам полезно.
Наша цель не призвать всех писать на ассемблере (ассемблерах!) - это не так уж сложно, но для большинства задач не очень практично. Цель именно познакомить! Чтобы было уже не страшно изредка заглянуть в потроха какой-то отладки - или сделать какую-то оптимизацию с ассемблерной вставкой - а может вы соберетесь написать компилятор или что-то в этом духе.
Бонусом - для любопытных - ассемблер для Intel-4004 - 4-разрядного процессора которому уже больше 50 лет. К нему будет также небольшой "интерактивчик".
Общие замечания
Ассемблер - язык позволяющий записывать команды процессора. Только не в виде шестнадцатеричных кодов (можно было бы и так!) а в виде человекочитаемых "мнемоник".
Процессоры бывают разные (разных архитектур и типов) - и команды у них разные. Поэтому ассемблеры отличаются как минимум этими самыми мнемониками команд. Кроме того разные авторы ассемблеров придумали немного различающиеся форматы записи этих самых команд.
Означает ли это что программы на ассемблере совершенно "неперносимы" в отличие от Си? Необязательно. Большинство ассемблеров имеют средства (дефайны, макросы и т.п.), которые позволяют писать какие-то части в унифицированном виде. Впрочем это редко нужно.
О процессорах и системе
Знакомство с любым ассемблером обычно начинается с разглядывания архитектуры процессора - какими средствами и возможностями он обладает. Первое о чем заходит речь - регистры. Ячейки собственной крохотной памяти проца. Их обычно немного - где-то от 1 до 32. Не считая регистры специальные - например PC (program counter) - счетчик содержащий адрес выполняемой инструкции (и увеличивающийся по мере выполнения.
Часть команд процессора - это разные манипуляции над регистрами. Например арифметические операции. Но кроме них нужны и другие - например для переходов по программе (в случае условий, циклов) - хотя если подумать, "переход" - это просто принудительная запись нужного значения в регистр PC - с тем чтобы дальше выполнение шло не по порядку а с требуемого места.
Команды для общения с памятью (или лучше сказать с "адресным пространством") - очень важная штука. Мы просто говорим "по адресу 0x1020BEDA запиши число 0x1F" - и процессор идёт и записывает - но что находится по этому адресу? Это самое "адресное пространство" физически представлено проводниками шины адреса и данных - и к нему можно подключить как саму оперативную память системы - так и какие-нибудь дополнительные устройства.
Если мы записываем число по заданному адресу и там находится именно память - ну что ж, впоследствии мы сможем его оттуда прочитать. А если по этому адресу (к выбранным проводникам) присоединён порт ввода-вывода "ног" микроконтроллера - или звуковой синтезатор? Вполне возможно на "ногах" изменятся напряжения а из динамика послышится какой-нибудь звук. Таким именно образом процессор общается с системой!
Из этого следует ещё один осложняющий фактор - даже для одинаковых процессоров (ядер) система подключенных устройств (периферия) может быть разной. Это может добавлять нюансов к переносимости - и материала для изучения при разработке.
Однако давайте уже к делу, то есть к ассемблерам! Сперва рассмотрим несколько микроконтроллерных архитектур - некоторые из них (вроде AtTiny15) обладают настолько крохотными ресурсами (память на 512 команд и полный 0 оперативки) что ассемблер там очень удобен.
ARM32 - мир микроконтроллеров
Мы начнём со "средней весовой категории" - любители электроники и самоделок знают что хорошую конкуренцию базовым "ардуинам" составляют 32-разрядные контроллеры архитектуры ARM. Кто такой "микроконтроллер" вообще? Скажем так - разновидность процессора с полезными встроенными устройствами, так чтобы им удобно было пользоваться в разных электронных девайсах. Встроенные интерфейсы, таймеры, немного оперативки и т.п. Вот один из первых проектов на LPC1110 (по-моему) из их семейства, который я сделал - робот, управляемый звуками флейты:
Ну и вот ARM32 сейчас вероятно представляют наибольшую долю всех процессоров в мире - т.к. они идут во всевозможные устройства начиная от "умных ёршиков для унитаза" - заканчивая телефонами и планшетами. Их простейшие модели стоят порядка доллара за штуку - иногда дешевле более примитивных 8-битных контроллеров - поэтому встраивать их куда угодно - милое дело.
ARM32 как следует из названия работает в основном с 32-разрядными данными. У него 32-битовые регистры - из них "общего назначения" - первые 12-16 штук. С помощью 32-разрядного числа можно адресовать аж 4 гигабайта памяти - понятно что у большинства систем столько нет (простейшие контроллеры имеют несколько килобайт реальной оперативной памяти) - так что всё "свободное" пространство годится для адресации периферийных устройств. Посмотрим на пример программы, мигающей светодиодом (на контроллере (LPC1114 от NXP)
.syntax unified
.equ GPIO0DATA, 0x50003FFC
.equ GPIO0DIR, 0x50008000
.text
Reset_Handler:
ldr r6, =GPIO0DIR
ldr r0, =0x0C
str r0, [r6]
ldr r0, =0x04
ldr r6, =GPIO0DATA
ldr r1, =0x0C
blink:
str r0, [r6]
eors r0, r1
ldr r2, =0x300000
loop:
subs r2, 1
bne loop
b blink
Разберемся в этой белиберде! :) Для начала отметим что строчки с точкой в начале - это не команды для процессора а директивы для самого ассемблера. Например .equ
- это вроде сишного #define
- способ чтобы обозвать константу каким-то именем. Директива .text
говорит что дальше пойдёт собственно секция с кодом. А .syntax ...
в самом верху позволяет выбрать один из нескольких популярных синтаксисов записи команд. Разницу мы увидим позже.
А где же команды? Вот первые три: ldr r6, =GPIO0DIR
- команда "ldr" это сокращение от "load register" - то есть загрузить в регистр R6 такую-то константу. В данном синтаксисе константы предваряются знаком равенства почему-то. Итак в результате этой команды в r6 запишется 0x50008000
(это адрес устройства связанного с "ногами" контроллера, конкретно с тем включены ноги на вход или на выход - каждый бит отвечает за отдельную ногу - вся эта информация конечно относится уже не к ассемблеру а к устройству LPC1114 и находится из 400-страничной инструкции к нему).
Следующую ldr r0, =0xC0
мы расшифруем уже легко - в регистр R0 записывается некое число, в котором установлены в 1 два бита (биты 14й и 15й считая с нуля). Третьей командой str r0, [r6]
- сокращение от "store register" - мы записываем число из R0 по адресу, находящемуся в R6. В данном синтаксисе вот такой формат со скобочками поясняет что мы пишем не в сам R6 а именно в ячейку адресного пространства, на которую R6 указывает. Очевидно, сделано "по аналогии" с Си.
В результате этих манипуляций по адресу GPIODIR оказалось записано число которое "включает" некоторые ноги контроллера "на выход". Теперь если по другому адресу (тому что в GPIODATA) писать единички и нули в соответствующих битах - на этих ногах будут появляться напряжения близкие то к плюсу то к минусу питания (3.3В и 0В иначе говоря).
Следующие три инструкции подготавливают значения в регистрах R0, R1, R6 - пропустим их и посмотрим что происходит в главном цикле. Главный цикл у нас начинается с метки blink:
- метки это тоже не команды процессора а именно способ отметить нужное место в программе. Фактически теперь имени "blink" соответствует адрес следующей за ней команды. Мы сможем использовать это значение в инструкции перехода в конце, чтобы "зациклить" выполнение.
В начале цикла мы опять видим запись из R0 в ячейку по адресу из R6 - только теперь это адрес GPIODATA=0x50003FFC - здесь управляется именно напряжение на выбранных "ногах". А число в R0 будет меняться на каждой итерации главного цикла благодаря следующей команде eors r0, r1
- таким непривычным названием обозначили привычный со школы XOR (exclusive or) - регистр R0 "ксорится" с R1 (и результат записываетс обратно в R0). Поскольку в R1 значение не меняется, то в R0 те биты, которые установлены в R1, будут переключаться из 0 в 1 и обратно при каждом выполнении этого "ксора". А поскольку выше мы их отправляем в ячейку управляющую напряжением на "ногах" - то и напряжения на ногах этих будут переключаться.
В самом конце программы мы видим b blink
- в данном ассемблере "b" означает "branch" - переход по заданному адресу. Благодаря этому программа зацикливается.
Осталось рассмотреть кусочек создающий задержку - маленький внутренний цикл. Перед меткой loop:
мы заносим в R2 довольно большое число - и потом уменьшаем его инструкцией subs
(вычитание) на единичку. Команда bne
означает "branch if not equal" - переход если не равно. Не равно что? И чему? :) В процессорах обычно есть специальный регистр битовых "флажков", которые выставляются или сбрасываются в зависимости от результата операции. Многие арифметические операции проставляют флажок Z (нуля) если в результате команды получен 0. Вот операция bne
и проверяет, установлен ли флажок нуля или нет. Она называется проверкой на "равенство" из-за того что часто используется с командой сравнения а не вычитания. Сравнение идентично вычитанию, только результат никуда не записывается (а лишь выставляются флажки) - получается если два числа были равны (например если в r2 перед вычитанием была единица) то после выполнения флажок устанавливается - и команда bne не выполнит переход а пропустит выполнение дальше.
Таким образом этот маленький цикл создаёт задержку что-то порядка секунды (если контроллер работает на частоте в несколько мегагерц). Достаточно для того чтобы разглядеть мигание подключенного к ноге светодиода невооруженным взглядом.
Пожалуй хватит про ARM32 - упомянем только что здесь использовался кросс-компилятор GNU - а полный текст и дополнительные подробности можно посмотреть в гитхабе - в скрипте для сборки видно что использовался пакет arm-linux-gnueabi
. Прошивать же эти контроллеры можно через UART (как и STM32 - у них готовый загрузчик внутри).
AVR и Avrasm
Это архитектура 8-битных процессоров фирмы Atmel (давно уже купленной конкурентами) - известная тем что они широко использованы в Arduino. Здесь 32 регистра, только они маленькие, 8-битные. А область адресного пространства связанная с периферийными устройствами строго выделена (адреса где-то с 32 по 95) - поэтому для общения с периферией используют особые команды IN
и OUT
(читать оттуда или писать туда). Хотя можно и обычными командами для общения с памятью воспользоваться. Даже сами регистры мэпятся в адресное пространство, в младшие 16 адресов.
Популярным компилятором является AvrAsm
- или его сторонняя версия AvrA
- он имеет немного отличающийся синтаксис. Примеров "мигания светодиодом" вы найдёте сколько угодно, поэтому не будем сейчас отвлекать внимание детальным разбором - просто сравним какой-нибудь фрагмент с виденным ранее:
#define DDRB 0x17
#define PORTD 0x12
setup:
ldi r16, (0xF << 0)
out DDRB, r16
ldi r16, (1 << 2) | (1 << 3)
out PORTD, r16
rjmp again
Видим то о чем говорилось - другой синтаксис. #define
вместо .equ
(хотя скорее всего и то и другое поддерживается). Команда загрузки числа в регистр называется ldi
(load immediate - загрузка непосредственного значения) - а для записи в периферийную ячейку с адресом DDRB используется команда OUT. Команда перехода здесь называется RJMP (relative jump) хотя условные переходы в ARM обычно тоже называются с буквы B
от слова branch.
Очевидным "неудобством" в сравнении с ARM - например при реализации "мигания" светодиодом - вы обнаружите что сделать цикл задержки на миллион операций с помощью одного только регистра не получится (ведь регистры не вмещают значения больше чем 255) - поэтому надо либо вложенные циклы делать, либо в одном цикле работать с числом содержащимся в нескольких регистрах (там есть операции с переносом и займом из следующего регистра). Другое важное отличие от ARM - память программ отдельная от общего адресного пространства. Это немного усложняет жизнь при использовании констант из флэша и при разработке компиляторов (вроде ардуиновского).
Отметим запись операторов в виде выражений (1 << 2) | (1 << 3)
- это конечно не означает что процессор готов считать такие сложные конструкции записанные в "сишном" стиле. Это лишь константы которые вычисляются на этапе компиляции - использовать здесь вместо чисел значения регистров, конечно, не удастся. Можно было написать 0xC - но м.б. это чуть менее понятно - а так ясно, выставляются 2-й и 3-й биты.
Как пример проекта на AtTiny2313 использующий этот ассемблер можно глянуть вот это незамысловатое радио. Про компиляцию с помощью AvrAsm/Avra уже было сказано - а для прошивки нужен типичный атмеловский программатор - в качестве него можно использовать Arduino с прошивкой ISP из стандартных примеров (хотя когда-то я пользовался 5 проводками на LPT-порту).
MSP430 - 16-разрядные контроллеры
Эти контроллеры от Texas Instruments я использовал в основном потому что у них есть встроенный загрузчик так что не нужно отдельно покупать программатор, а можно прошивать скомпилированный код прямо по UART - это же относится к LPC и STM32 из ARM32 семейства. По цене они не выгоднее ARM-ов и по возможностям скромнее - но зато у них есть версии в корпусах DIP и SOIC. Разводить и травить платы под крохотные ARM-овские чипы многие из нас конечно умеют, но для каких-то простых поделок иногда хочется корпус попроще.
Ну и конечно 16-разрядный процессор немножко особняком стоит между AVR и ARM - а ассемблер который я к нему нашёл - немало напоминает ассемблеры для x86 архитектуры, о чем речь будет дальше. А ещё больше напоминает команды для DEC-овских машин и их производных (PDP-11 и советский БК-0010).
Посмотрим на фрагмент кода очередной "мигалки":
.org 0xf800
start:
mov.w #WDTPW|WDTHOLD, &WDTCTL
mov.b #2, &P1DIR
repeat:
mov.w #2, r8
mov.b r8, &P1OUT
mov.w #60000, r9
wait1:
dec r9
jnz wait1
Директива .org
задаёт адрес с которого размещаются дальнейшие команды - у данных процессоров как ни странно память программ начинается не с 0. Константы предваряются символом #
а если нужно записать в ячейку на которую указывает регистр - то используется амперсанд &
. Все эти WDTPW и прочие константы объявлены в отдельном файле, но в остальном принцип тот же.
Всевозможные команды для записи в регистры и память называются просто mov
с суффиксом обозначающим размер операнда (слово или байт). Команда условного перехода jnz
- аналог bne
- в этот раз расшифровывается как "jump if not zero".
Это синтаксис ассемблера naken_asm
- на тот момент первый который удачно подвернулся для этих контроллеров. Как видите - смысл мнемоник для разных процессоров обычно похож, но названия каждый автор компилятора склонен придумывать свои. Тут ещё характерная особенность что целевой оператор (куда записывается результат) - второй а не первый.
Примеры проектов на MSP430 найдутся в моём гитхабе (поиском по префкису "msp430") - в частности вот сам блинкер.
Семейство 8051
Эти контроллеры старше многих из нас :) Интел выпустил их кажется в начале 80х - современные версии на этой архитектуре гораздо более продвинутые (наверное топовые контроллеры выпускает SiLabs) - тем не менее ядро остаётся тем же. Немного странным, немного архаичным. Контроллеры 8-битные, адресные пространства для оперативки (если есть) и периферии разделены. У них много особенностей в которые наверное лучше пока вдаваться не будем но посмотрим на небольшой фрагмент чтобы отметить некие сходства.
CHANNEL EQU 5Fh ; 0 is FM, 1 - MW/LW, 2 and further - SW
COMMAND_AREA EQU 60h
RESPONSE_AREA EQU 70h
ORG 0
START:
MOV SCON, #01000000b ; UART mode 1 (8bit, variable baud, receive disabled)
MOV PCON, #80h
MOV TMOD, #00100000b ; T1 mode 2 (autoreload)
MOV TH1, #0FFh ; T1 autoreload value, output frq = 24mhz/24/(256-TH1)/16 (X2=0, SCON1=1)
MOV TCON, #01000000b ; T1 on
MOV IE, #90h ; enable interrupts, enable uart interrupt
MOV SBUF, #55h
SJMP MAIN
Это код опять для радиоприёмника, на другом чипе (проект здесь) - а процессор от той же фирмы Atmel которая делала AVR. У этих At89 нет загрузчика по UART так что мне пришлось написать загрузчик на Ардуино (тоже где-то в гитхабе лежит). В остальном они неплохие, даже чем-то забавные. Интересная фича - у ног нет регистра "направления" - читать можно когда на ногу подана единица (похоже на AVR-ский режим PULL_UP).
Так вот - мы видим знакомые нам директивы определения констант (хотя и без точек) и смещения адреса программы (в данном случае ORG 0) - также типичные метки с двоеточием.
Константы со знаком диеза мы тоже уже видели. А вот необычная особенность - команды MOV позволяют отправлять эти самые константы прямо в ячейки управляющие периферией! Все эти SCON, PCON, TMOD - это предопределенные адреса ячеек периферийных устройств. Естественно это сокращает программу - в AVR и MSP430 как мы видели константу сначала надо записать в регистр, а уже регистр отправить в адресное пространство. Команду SJMP мы легко расшифруем как "short jump" - переходы во многих процессорах есть разных видов - короткий переход требует меньше бит для записи кода самой команды.
Один из популярных компиляторов, который использовал и я asem-51
.
Наконец x86 - тогда (TASM, DOS)
Ассемблер для наших обычных компьютеров, в основном x86 архитектуры - это был второй язык с которым я стал экспериментировать после Turbo Pascal - поскольку в книжке по Паскалю упоминались где-то магические "ассемблерные вставки" - код которых тогда казался абсолютно непонятен.
Возможность написать программу в формате .COM
под ДОС, состоящую буквально из нескольких десятков байт - это выглядело очень любопытно. Сейчас если вы захотите поиграться с ассемблером тех времен - используйте DOSBOX и TASM (он входил в набор Borland Pascal / C++ по-моему) например.
Регистров у x86 процессора тоже восемь, они изначально 16-битные (как развитие 8-битного предшественника 8008) с именами AX, BX, CX, DX, BP, SP и пр (то есть не привычные из контроллеров Rnn). Причём каждый из первых четырех делился на два 8-битных, например AH и AL - старшая и младшая половина. Приведу целиком программу, печатающую строчку:
assume cs:code,ds:code
code segment public
org 100h
main proc near
lea dx, message
mov ah, 9
int 21h
int 20h
main endp
message db 'Hi, Peoplez!', 13, 10, '$'
code ends
end main
Здесь опять директивы компилятора без точек, что сперва сбивает с толку. В частности "code segment public" относится к организации памяти в виде сегментов по 64кб и "assume ..." сверху подсказывает компилятору что сегментные регистры будут загружены одним и тем же адресом сегмента, в котором живёт и программа и данные. Сегментные регистры использовались для того чтобы указать в каком сегменте памяти (которой было доступно в "реальном режиме" почти мегабайт) сейчас загружены программа и данные. Этакая двухступенчатая адресация.
Для компиляции в com-файл смещение начала программы всегда было 256 байт, что задано директивой ORG. Наконец дальше идёт программа - здесь есть метка "процедуры" - можно было и обычную с двоеточием использовать.
Команда LEA (load effective address) загружает в регистр адрес памяти по которому расположена нужная нам строка. Вы видите её дальше, с меткой "message" - она состоит из текста, байт 13 и 10 (возврат каретки и перевод строки) - и символа доллара (о нем чуть дальше).
Команда MOV AH, 9 - тут уже вам пояснять не надо, записывает 9 в регистр AH. Дальше происходит интересное - что это за INT 21h?
Мы многое пропустили в описании процессоров и архитектур - одна из важных фич - это "прерывания". В самом начале памяти записываются адреса разных процедур которые можно выполнить по наступлению (обычно внезапному) того или иного события - пришёл сигнал на какую-то ногу контроллера - или сработал таймер. Программа пользователя прерывается, выполняется обслуживающая подпрограмма прерывания - и всё возвращается к пользовательскому коду в том же месте где прервались.
В x86 архитектуре есть и специальная команда чтобы вызвать прерывание "вручную" - это она и есть (от слова interrupt) - и дальше указывается номер прерывания. Прерывание 21h содержало множество небольших процедур относящихся к операционной системе DOS. Номер нужной функции выбирался при вызове числом в регистре AH - в частности 9-я функция это печать строки. А адрес строки должен быть в регистре DX. В общем кроме инструкций процессора настольным мануалом был большой справочник по функциям ДОСа и БИОСа (эти вызывались через INT 10h).
Строка для данной функции должна заканчиваться символом доллара - вот и всё.
Следом вызывается INT 20h - это тоже прерывание ДОС, но с всего одной функцией - оно выходит из программы обратно в ОС. Для микроконтроллеров мы такой функции не видели - им "выходить" некуда (вообще это не единственный способ выйти).
директива DB
после метки "message" это не команда процессора, но на сгенерированный код она влияет - благодаря ей в выполняемый файл с данного места записываются указанные далее байты данных (DB - data bytes). Ведь строка должна присутствовать в коде чтобы её напечатать.
x86 - теперь (GAS, Linux)
Недавно мне пришлось искать бажок в компиляторе (точнее библиотеке) архаичного языка BCPL (о нем недавно писал) - и обнаружилось что часть его, конечно, написана на ассемблере. Естественно он показался немало знаком, хотя уже для 32-битной системы. Давайте посмотрим на ту же программу в "современном исполнении".
.section .data
msg: .ascii "Hi, Peoplez!\n"
len = . - msg
.section .text
.global _start
_start:
movl $4, %eax
movl $1, %ebx
movl $msg, %ecx
movl $len, %edx
int $0x80
movl $1, %eax
movl $0, %ebx
int $0x80
Как видим, 32-битные регистры получили названия EAX, EBX и так далее. Синтаксис в данном случае - дефолтный для компилятора GNU, хотя как вы помните из примера с ARM, его можно переключать. В данном синтаксисе перед регистрами ставятся знаки процента, перед константами доллары. В программе две отдельных секции - data - где лежат наши данные (строка для печати) и text - где собственно код.
Сами команды выглядят уже знакомо! Мы замечаем что вместо функций ДОСа мы теперь вызываем функции Линукса - но это делается тоже через "ручное" прерывание, хотя и с номером 0x80. Номер функции в EAX - в частности 4 означает вывод данных. Число 1 в EBX - это номер канала куда выводить (помните stdout / stderr и файловые "хэндлы" в Си? вот это про то - 1 соответствует stdout). В ECX записан адрес строки а в EDX их длина - причем длина выше вычисляется с помощью директивы вычитающей адрес метки "msg" из текущего адреса (точка).
Вторая функция - теперь с кодом 1 в EAX - это выход из программы. Легко догадаться что второй её параметр (0 в EBX) - это код возврата. Делаем ещё один INT 0x80
- и вуаля.
Если у вас установлен GCC то скорее всего попробовать этот код вы можете "не отходя от кассы". Сохраните код в файл test.s
и выполните команды:
as test.s
ld a.out -o test
./test
Первая из них компилирует в объектный файл (a.out) - а вторая линкует его в готовый бинарник который мы запускаем как ./test
Intel-4004 - в качестве бонуса
Это первый коммерчески продававшийся микропроцессор, но каких-то популярных компьютеров на его базе не было создано. Он использовался в настольных бизнес-калькуляторах и автоматах для продажи газировки - то есть выполнял скорее роль микроконтроллера хотя не имел для этого микроконтроллерных фишек. Поэтому сложно говорить о какой-то стандартной системе или периферии для него (что напаяте - то и будет).
Практический смысл изучения ассемблера для него - нулевой - зато он любопытен с точки зрения упражнений. Поэтому для него сделан (когда-то мной) небольшой эмулятор на Python - а также несколько задач на моём сайте (вместе с небольшой формочкой чтобы выполнять программы). Тут можно найти и алгоритмы Брезенхема для графики и микро-вариант игры "Жизнь".
Хотя Intel-4004 является предком 8008, а через него 8080, 8086, 80386 и там уж наших современных компьютеров - с регистрами у него ситуация несколько отличается от виденного ранее: Регистров также 16 (все 4-битные) - от R0 до R15 - но есть ещё выделенный регистр-аккумулятор Acc - и многие операции (особенно арифметические, логические) могут использовать только его. Эта особенность впрочем была и в 8051 упомянутом выше.
Каких-то обширных программ мы рассматривать не будем (при желании - попробуйте решать задачи используя прилагаемую к ним инструкцию) - но посмотрим пример нескольких команд:
ldm 5 ; загрузить 5 в Acc
xch r2 ; обменять значениями R2 и Acc
Из-за использования аккумулятора (принудительного) получается что многие команды имеют лишь 1 аргумент или не имеют вовсе. Есть и более длинные и сложные команды:
fim r4 $57 ; загружает 8-битное число в пару регистров R5:R4
add r10 ; суммирует Acc + R10 + Carry (флаг переноса)
Получается что перед выполнением арифметики всегда надо очищать или устанавливать Carry в зависимости от нужной операции.
Здесь же вы на практике сможете познакомиться со стеком вызовов и подпрограммами - мы сознательно пропустили эту часть при рассмотрении прочих архитектур. Это исключительно важная и активно используемая фича - но всё же она не является абсолютно необходимой в маленьких (или тестовых) программах - так что поначалу можно не забивать голову.
Заключение
Как упоминалось выше - цель данной статьи не в том чтобы научить вас писать на ассемблере или сделать его поклонниками - а больше в том чтобы показать "из чего он состоит" - и какие сходства и разнообразия мы обычно встречаем сталкиваясь с разными архитектурами и версиями компиляторов.
Тем не менее если вы попробуете что-то программировать таким образом - думаю вы согласитесь что это по меньшей мере любопытно - и определенным образом "упражняет мозг" :)
Некоторым недостатком отметим что мы не коснулись AMD64 и ARM64 архитектур - но с другой стороны у вас и так уже наверное немного пестрит в глазах от этих мнемоник - а как можно догадаться, там будет определенное сходство с x86 и ARM32. В то же время популярный когда-то ассемблер для Z80 (на котором так много написано под ZX Spectrum) я включать не стал - во-первых это производная от 8080 (одного из предков x86 архитектуры) - во-вторых наверное сейчас он вам вряд ли пригодится - в отличие от пяти упомянутых архитектур.
Комментарии (20)
unreal_undead2
01.11.2024 05:26в "современном исполнении"
В современном - это всё таки 64 бита и явная инструкция sycall. А приведённый код уже скорее ностальгию вызывает )
RodionGork Автор
01.11.2024 05:26возможно поскольку я сильно погряз в ARM-овских микроконтроллерах для меня 32-бита до сих пор современность :) хотя замечание насчет syscall точно валидное - допишем его в этот параграф
atues
Неплохо, но Вы ничегошеньки не сказали о настоящих шедеврах: архитектуре и ассемблерах от DEC (ярчайшие звезды PDP-8 и PDP-11). Все, что было после них - убожество в смысле простоты, логичности, изящества. Если что - никого обидеть не хочу
RodionGork Автор
По той же причине не рассказал и про PIC например :) как видите, я постарался выбрать несколько архитектур которые более-менее в ходу у программистов и самодельщиков и по сей день - так чтобы был практический смысл. Про антикварные, архаичные и страшные системы наверное можно отдельную статью написать именно для любителей поностальгировать.
SIISII
Ну, у них тоже свои недостатки имелись. И не всё там просто, логично и изящно. Скажем, что в PDP-11 логичного в том, что у XOR один из операндов должен быть в регистре, в то время как у остальных простых команд он может находиться и в памяти? Или почему сложение и вычитание (ADD, SUB) только словные, а остальные операции -- и словные, и байтовые? Или почему сложение и вычитание с переносом (ADC, SBC) имеют только один операнд, т.е. нельзя сложить сразу два операнда и добавить к ним перенос? Или почему нет операции "И", есть только сброс битов, отмеченных единицами, а не нулями во втором операнде (BIC)? А если вспомнить про тамошний FPU, то почему аккумуляторов всего шесть, причём только четыре могут использоваться в любых командах? (Объясняется почти всё, конечно, элементарно: банально не лезли все команды в 16-разрядное слово, их кодирующее, поэтому и пришлось чем-то жертвовать -- но простоты, логичности и изящности это точно не добавляет.)
Правда, если сравнивать PDP-11 с 8086, то там вообще невозможно понять, какие такие вещества употребляли архитекторы Интела :)
RodionGork Автор
я думаю можно проще сказать - сравнивая ассемблеры RISC-архитектур (хотя бы те же ARM и AVR) c архитектурами CISC (x86 и 8051) мы сразу замечаем что первые как будто проще и логичнее вторых... но это собственно и следует из первой буквы аббревиатур (риск/циск) - усечённая или сложная система команд.
SIISII
PDP-11 -- CISC, IBMовская Система 360 -- тоже CISC, но они пологичней будут, скажем, ARMа (и особенно -- если сравнивать с системой команд Thumb, а не с классической ARMовской), особенно PDPшка... Просто и 8086 с его наследниками, и 8051 -- интеловские архитектуры, а значит, изначально отвратительны; их с чем ни сравнивай -- всё ужасом будут выглядеть (особенно если погрузиться в детали, а не глянуть лишь краем глаза)
LAutour
В PDP было все вполне логично: двух операндые и однооперандные команды разбивались на фиксированные поля (размер данных, регистр источника\приемника, методы адресациии).И ограничения некоторых команд связаны с ограниченностью базового поля кода полноценной двухоперандной команды - для него оставалось всего 3 бита из 16. Для некоторых коменд пришлось жертвовать другими полями. По этой же причине например в MSP430 пришлось ввести отдельную команду PUSH (POP реализован через MOV).
А по x86 - это да. Я "сломал мозг" когда дошел до их префиксов , читая книжку с низкоуровневым описанием комнад x86 (386).
RodionGork Автор
возможно дело в том кто с чего начинал :) я понял насколько x86 ассемблер мудрёный только когда позже стал писать под AVR
DAT540
В школе на уроках программирования был класс из ДВК и БК0010Ш. Писал на них прямо в машинных кодах, ибо это просто сказка как удобно и легко запоминается все. Бывало только с таблицей команд сверялся по каким нибудь не часто используемым условным переходам. Таскал оттуда БКашку домой на каникулы :) с учителем были отличные отношения. Со второго урока мне были вручены ключи от кабинета с компами и сказано - иди, тебе тут на теории делать нечего.
RodionGork Автор
вот любопытно что вам казалось удобно и легко - а коллега выше в комментариях вспоминает архитектуру PDP-11 (я так понимаю БК0010 производная от него) как довольно мудрёную :)
прекрасный показатель того что на вкус и цвет ассемблеры по-разному воспринимаются - но кстати тоже соглашусь что после преодоления первых 10-20 минут шока от непривычности синтаксиса и погружения в архитектуру - дальше уже чувствуешь себя довольно комфортно - не много запоминать требуется :)
atues
Любая архитектура ограничена во-первых, законами физики, а во-вторых, технологическими возможностями эпохи. Законы физики, что тогда (60 лет назад), что сегодня одни и те же. А вот технологические возможности - несопоставимы (взять хотя бы память: сегодня это микросхемы на гигабайты, а тогда - ферритовые кольца на килобайты). Инженеры DEC создали пусть и не идеальные, но действительно изящные архитектуры. Обидно то, что развитие технологии никак не коррелировало с архитектурой. Технологии развивались, а архитектура - деградировала
DAT540
Да, БК и ДВК это классический PDP-11. Может быть это было просто все очень ново для меня тогда и все казалось зеленее и выше чем сейчас :) но маш коды загрузились в голову на ура. На ассемблере как таковом я не писал, компилятора не нашел в классе. Был только Бейсик. До БК немого писал на КР580ВМ80А который наш аналог 8080. Но там был не полноценный комп, а что-то типа самопального Альтаира со светодиодиками и переключателями. Но там маш коды использовал по таблице команд. Такого уровня как с PDP11 не было.
SIISII
Просто есть разные стороны "мудрёности". Кодирование команд в PDP-11 -- очень простое и понятное; мой первый комментарий касался уж очень восторженной её оценки, и я просто показал, что там свои проблемы тоже имеются. Но, если брать по совокупности, PDP-11 -- лучшая из 16-разрядных систем команд всего мира, пожалуй.
И да, я до сих пор часть кодов команд помню :) Правда, в кодах мне писать надобности не было, работая на полноценных СМках.
Alex82901
В 1989 начал проект на 8035(мл. брат 8051) контроллере. Так получилось, что доступа к ЭВМ(тогда мы еще этим словом пользовались) не было. Поэтому писал весь код в кодах (объем примерно 1.5 кБ).
Было весело. Особенно когда сдохла батарейка на энерго-независимом электронном диске (набор РУ10 с интерфейсом для СМ1800). Пришлось набирать код заново.
RodionGork Автор
мне кажется от тех времен осталось ещё немало "учебных макетов" на которых процессор (типа 8085), плата, цифровой индикатор и 16-ричная клавиатура - тоже чтобы в кодах набирать. ну и Клайв Синклер до ZX ведь такую штуку для хоббиистов изобрёл и продавал - за 40 фунтов (наверное как 1000 баксов сейчас) - желающие покупали такое чудо с 256 байтами оперативки.
но безусловно любопытно услышать "из первых рук" историю человека кто делал реальный проект в таком режиме :)
Serega_B
ИМХО - MSP430 имеет ассемблер неотличимый от DEC.
RodionGork Автор
у меня слишком скромный опыт в ассемблерах от DEC (т.е. опять же для PDP о чем коллеги выше упоминали) - но инторнеты подтверждают ваше наблюдение :) вот прямо вбиваю запрос в гугле в духе "MSP430 assembly instructions resemble PDP" и сразу горстка ссылок где прямо говорится что TI по-видимому нарочно взяли похожую систему команд - и изобретать меньше - и пользователям может оказаться привычнее
SIISII
Ещё как отличимый. Хотя параллели действительно имеются, и наверняка создатели MSP430 именно PDPшками вдохновлялись.
atues
О, хорошее слово Вы нашли: "вдохновлялись" :) PDP, действительно, вдохновляла