Статья для тех кто не знаком с ассемблерами - но хочет взглянуть "одним глазком". Мы не сделаем вас гуру разработки на ассемблере за 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 - и вуаля.

Спасибо доброму человеку за подсказку в комментах - даже INT 0x80 это по нашим временам архаизьм - есть специальная инструкция syscall которая служит для того же самого (но машинный код у неё конечно другой, в чем можно убедиться если при компиляции запросить создание "листинга" программы).

Если у вас установлен 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 архитектуры) - во-вторых наверное сейчас он вам вряд ли пригодится - в отличие от пяти упомянутых архитектур.

Комментарии (78)


  1. atues
    01.11.2024 05:26

    Неплохо, но Вы ничегошеньки не сказали о настоящих шедеврах: архитектуре и ассемблерах от DEC (ярчайшие звезды PDP-8 и PDP-11). Все, что было после них - убожество в смысле простоты, логичности, изящества. Если что - никого обидеть не хочу


    1. RodionGork Автор
      01.11.2024 05:26

      По той же причине не рассказал и про PIC например :) как видите, я постарался выбрать несколько архитектур которые более-менее в ходу у программистов и самодельщиков и по сей день - так чтобы был практический смысл. Про антикварные, архаичные и страшные системы наверное можно отдельную статью написать именно для любителей поностальгировать.


    1. SIISII
      01.11.2024 05:26

      Ну, у них тоже свои недостатки имелись. И не всё там просто, логично и изящно. Скажем, что в PDP-11 логичного в том, что у XOR один из операндов должен быть в регистре, в то время как у остальных простых команд он может находиться и в памяти? Или почему сложение и вычитание (ADD, SUB) только словные, а остальные операции -- и словные, и байтовые? Или почему сложение и вычитание с переносом (ADC, SBC) имеют только один операнд, т.е. нельзя сложить сразу два операнда и добавить к ним перенос? Или почему нет операции "И", есть только сброс битов, отмеченных единицами, а не нулями во втором операнде (BIC)? А если вспомнить про тамошний FPU, то почему аккумуляторов всего шесть, причём только четыре могут использоваться в любых командах? (Объясняется почти всё, конечно, элементарно: банально не лезли все команды в 16-разрядное слово, их кодирующее, поэтому и пришлось чем-то жертвовать -- но простоты, логичности и изящности это точно не добавляет.)

      Правда, если сравнивать PDP-11 с 8086, то там вообще невозможно понять, какие такие вещества употребляли архитекторы Интела :)


      1. RodionGork Автор
        01.11.2024 05:26

        я думаю можно проще сказать - сравнивая ассемблеры RISC-архитектур (хотя бы те же ARM и AVR) c архитектурами CISC (x86 и 8051) мы сразу замечаем что первые как будто проще и логичнее вторых... но это собственно и следует из первой буквы аббревиатур (риск/циск) - усечённая или сложная система команд.


        1. SIISII
          01.11.2024 05:26

          PDP-11 -- CISC, IBMовская Система 360 -- тоже CISC, но они пологичней будут, скажем, ARMа (и особенно -- если сравнивать с системой команд Thumb, а не с классической ARMовской), особенно PDPшка... Просто и 8086 с его наследниками, и 8051 -- интеловские архитектуры, а значит, изначально отвратительны; их с чем ни сравнивай -- всё ужасом будут выглядеть (особенно если погрузиться в детали, а не глянуть лишь краем глаза)


      1. LAutour
        01.11.2024 05:26

        В PDP было все вполне логично: двух операндые и однооперандные команды разбивались на фиксированные поля (размер данных, регистр источника\приемника, методы адресациии).И ограничения некоторых команд связаны с ограниченностью базового поля кода полноценной двухоперандной команды - для него оставалось всего 3 бита из 16. Для некоторых коменд пришлось жертвовать другими полями. По этой же причине например в MSP430 пришлось ввести отдельную команду PUSH (POP реализован через MOV).

        А по x86 - это да. Я "сломал мозг" когда дошел до их префиксов , читая книжку с низкоуровневым описанием комнад x86 (386).


        1. RodionGork Автор
          01.11.2024 05:26

          возможно дело в том кто с чего начинал :) я понял насколько x86 ассемблер мудрёный только когда позже стал писать под AVR


          1. SIISII
            01.11.2024 05:26

            А как я дико плевался от 8086/8088, до этого поработав с PDP-11, System 360 (ну, с их советскими аналогами, есно), а также с 6502 и 8080 :) Правда, от последнего я тоже плевался -- но 8-разрядному процу ещё как-то прощаешь некоторые вещи. Впрочем, 6502 тоже 8-разрядный, но в тыщу раз приятней (а заодно и быстрей работает, несмотря на в 2-3 раза меньшую тактовую частоту).


    1. DAT540
      01.11.2024 05:26

      В школе на уроках программирования был класс из ДВК и БК0010Ш. Писал на них прямо в машинных кодах, ибо это просто сказка как удобно и легко запоминается все. Бывало только с таблицей команд сверялся по каким нибудь не часто используемым условным переходам. Таскал оттуда БКашку домой на каникулы :) с учителем были отличные отношения. Со второго урока мне были вручены ключи от кабинета с компами и сказано - иди, тебе тут на теории делать нечего.


      1. RodionGork Автор
        01.11.2024 05:26

        вот любопытно что вам казалось удобно и легко - а коллега выше в комментариях вспоминает архитектуру PDP-11 (я так понимаю БК0010 производная от него) как довольно мудрёную :)

        прекрасный показатель того что на вкус и цвет ассемблеры по-разному воспринимаются - но кстати тоже соглашусь что после преодоления первых 10-20 минут шока от непривычности синтаксиса и погружения в архитектуру - дальше уже чувствуешь себя довольно комфортно - не много запоминать требуется :)


        1. atues
          01.11.2024 05:26

          Любая архитектура ограничена во-первых, законами физики, а во-вторых, технологическими возможностями эпохи. Законы физики, что тогда (60 лет назад), что сегодня одни и те же. А вот технологические возможности - несопоставимы (взять хотя бы память: сегодня это микросхемы на гигабайты, а тогда - ферритовые кольца на килобайты). Инженеры DEC создали пусть и не идеальные, но действительно изящные архитектуры. Обидно то, что развитие технологии никак не коррелировало с архитектурой. Технологии развивались, а архитектура - деградировала


          1. LAutour
            01.11.2024 05:26

            Нормально было у DEC с развитием архитектур. Они своевременно делали переходы на VAX и потом Alpha. Просто они долго "забивали" на рынок ПК. А потом было уже поздно.


            1. SIISII
              01.11.2024 05:26

              Альфа оказалась, скажем так, невостребованной. А что до ПК, они пытались в начале 1980-х сделать нечто вроде ПК с системой команд PDP-11 -- что было совершенно правильно, -- но сделали её несовместимой по железу с настоящими PDPшками, чтобы, не дай Бог, злые пользователи не могли воспользоваться штатными ОС (и программами) от минимашин. Если же учесть стоимость всего этого дела, не приходится удивляться, что оно не взлетело -- хотя технически было в 100500 раз совершенней ублюдской IBM PC. Правильным было бы делать именно что полноценную PDPшку, только компактную и под "персональное" использование, но с любыми штатными ОС, прочим ПО и потенциально (с помощью некоего адаптера) с возможностью подключать любую старую периферию. И, естественно, предлагать это за разумные деньги (вплоть до того, что в начале продаж торговать в убыток). Но жадность фраера сгубила (с)


        1. DAT540
          01.11.2024 05:26

          Да, БК и ДВК это классический PDP-11. Может быть это было просто все очень ново для меня тогда и все казалось зеленее и выше чем сейчас :) но маш коды загрузились в голову на ура. На ассемблере как таковом я не писал, компилятора не нашел в классе. Был только Бейсик. До БК немого писал на КР580ВМ80А который наш аналог 8080. Но там был не полноценный комп, а что-то типа самопального Альтаира со светодиодиками и переключателями. Но там маш коды использовал по таблице команд. Такого уровня как с PDP11 не было.


          1. SIISII
            01.11.2024 05:26

            Кстати, в порядке придирок. И БК, и ДВК-1, и ДВК-2 в плане системы команд -- не PDP-11, а LSI-11 (обрезанная PDPшка). Ну а по периферии БК точно не является даже LSIшкой, там только проц совместимый. Насчёт ДВК в этом плане я не в курсе.


            1. DAT540
              01.11.2024 05:26

              Это вполне возможно, читал то наши советские документы на эту технику, там могло быть и не совсем корректно или полно все отражено. Но про LSI-11 даже и не слышал, а PDP-11 с тех лет помню.


            1. AndrewT2
              01.11.2024 05:26

              Асмы были идентичнв. Делал игруху на двк со спрайтами и легко перенес на бк. Многие так и делали, тк дисковод был и редакторы приличные на двк


          1. Javian
            01.11.2024 05:26

            В современное время кросс-компиляторы существуют для старых машин например на КМ1801ВМ2 ? Или демо/софт пишут вручную?


            1. RodionGork Автор
              01.11.2024 05:26

              чаще просто можно старый софт найти и запустить в эмуляторе


            1. DAT540
              01.11.2024 05:26

              Не могу сказать. После школы больше не возвращался к этой платформе.


            1. SIISII
              01.11.2024 05:26

              Ну, если бы мне понадобилось, я бы взял эмулятор полноценной PDPшки, запустил бы на нём полноценную RSX-11M, и в ней бы уже писал. Т.е., по сути, делал бы так же, как 30+ лет назад :)

              В древних версиях GCC была поддержка PDP-11, но её, насколько знаю, давным-давно выпилили, поскольку поддерживать некому.


              1. Javian
                01.11.2024 05:26

                У TI Eclipse для его MSP430 как-то компилирует из C. И Energia IDE для MSP430. Значит что-то современное есть.


                1. SIISII
                  01.11.2024 05:26

                  MSP430 != PDP-11


                  1. Javian
                    01.11.2024 05:26

                    Нашел давно забытую страницу 2004 года, где сделано сравнение:

                    Summary
                    In summary, the MSP-430 instruction set designers sacrificed orthogonality in its addressing modes in order to support a larger number of registers. Several useful tricks were applied in order to save the situation, but those they managed to implement also served to increase the specialized nature of addressing modes in the MSP-430 and those they failed to implement (PC relative calls, for example, which must be emulated) can only leave one wondering what they were thinking about.

                    The MSP-430 reserves an entire bit in the instruction to select between word and byte modes. But the PDP-11 was also able to support byte and word mode accesses, where needed, as well as a full complement of instructions, including integer multiplication and division and a set of floating point opcodes. The PDP-11 also uses a symmetrical form for the addressing modes which extends to both operands in two-operand instructions. (The PDP-11 also balanced the range field for branch offsets with the need for a variety of opcodes - something I won't discuss in detail.)

                    The MSP-430 designers made a pivotal choice (misguided, in my opinion) for 16 registers and in the process found themselves making sacrifices, even while retaining the more important addressing modes and adding some creative adaptations within the limitations they accepted in order to provide some modest but attractive compensations (e.g., the constant generator.)

                    I like the MSP-430 a lot, despite these warts and problems. Frankly, the hardware is impressive and the MCU remains excellent.

                    https://web.archive.org/web/20070208052109/http://users.easystreet.com/jkirwan/new/msp430.html


        1. SIISII
          01.11.2024 05:26

          Просто есть разные стороны "мудрёности". Кодирование команд в PDP-11 -- очень простое и понятное; мой первый комментарий касался уж очень восторженной её оценки, и я просто показал, что там свои проблемы тоже имеются. Но, если брать по совокупности, PDP-11 -- лучшая из 16-разрядных систем команд всего мира, пожалуй.

          И да, я до сих пор часть кодов команд помню :) Правда, в кодах мне писать надобности не было, работая на полноценных СМках.


      1. Alex82901
        01.11.2024 05:26

        В 1989 начал проект на 8035(мл. брат 8051) контроллере. Так получилось, что доступа к ЭВМ(тогда мы еще этим словом пользовались) не было. Поэтому писал весь код в кодах (объем примерно 1.5 кБ).

        Было весело. Особенно когда сдохла батарейка на энерго-независимом электронном диске (набор РУ10 с интерфейсом для СМ1800). Пришлось набирать код заново.


        1. RodionGork Автор
          01.11.2024 05:26

          мне кажется от тех времен осталось ещё немало "учебных макетов" на которых процессор (типа 8085), плата, цифровой индикатор и 16-ричная клавиатура - тоже чтобы в кодах набирать. ну и Клайв Синклер до ZX ведь такую штуку для хоббиистов изобрёл и продавал - за 40 фунтов (наверное как 1000 баксов сейчас) - желающие покупали такое чудо с 256 байтами оперативки.


          но безусловно любопытно услышать "из первых рук" историю человека кто делал реальный проект в таком режиме :)


        1. DAT540
          01.11.2024 05:26

          Да, я тоже как-то делал приборчик на нашем аналоге 35го. Там стояла кроватка для 27Cxx, но там уже я на ассемблере писал и заливал это "Стерхом" программатором. Но было ужасно неудобно стирать каждый раз пачку УФэшек и был создан аналог 27с из РУ10, напаяв на него по питанию большой кондер и пару резисторов на разрешение записи и на включение. Хватало чтобы зашить на Стерхе и воткнуть в прибор и включить.


        1. checkpoint
          01.11.2024 05:26

          Было весело. Особенно когда сдохла батарейка на энерго-независимом электронном диске (набор РУ10 с интерфейсом для СМ1800). Пришлось набирать код заново.

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


    1. Serega_B
      01.11.2024 05:26

      ИМХО - MSP430 имеет ассемблер неотличимый от DEC.


      1. RodionGork Автор
        01.11.2024 05:26

        у меня слишком скромный опыт в ассемблерах от DEC (т.е. опять же для PDP о чем коллеги выше упоминали) - но инторнеты подтверждают ваше наблюдение :) вот прямо вбиваю запрос в гугле в духе "MSP430 assembly instructions resemble PDP" и сразу горстка ссылок где прямо говорится что TI по-видимому нарочно взяли похожую систему команд - и изобретать меньше - и пользователям может оказаться привычнее


      1. SIISII
        01.11.2024 05:26

        Ещё как отличимый. Хотя параллели действительно имеются, и наверняка создатели MSP430 именно PDPшками вдохновлялись.


        1. atues
          01.11.2024 05:26

          О, хорошее слово Вы нашли: "вдохновлялись" :) PDP, действительно, вдохновляла


    1. AndrewT2
      01.11.2024 05:26

      Дай обниму тебя, брат!

      Pdp-11 просто и понятно. После него x86 казался нелогичным.


  1. unreal_undead2
    01.11.2024 05:26

    в "современном исполнении"

    В современном - это всё таки 64 бита и явная инструкция sycall. А приведённый код уже скорее ностальгию вызывает )


    1. RodionGork Автор
      01.11.2024 05:26

      возможно поскольку я сильно погряз в ARM-овских микроконтроллерах для меня 32-бита до сих пор современность :) хотя замечание насчет syscall точно валидное - допишем его в этот параграф


      1. SIISII
        01.11.2024 05:26

        Ну, для микроконтроллеров 64 бита смысла просто не имеет: нет там задач, которые не полезут в 4 Гбайта адресного пространства. Более того, 80% микроконтроллерных задач и на 8-разрядном можно сделать, просто, учитывая стоимость МК, проще уже для всех проектов использовать одну и ту же архитектуру, чем скакать между разными.


        1. RodionGork Автор
          01.11.2024 05:26

          микроконтроллеры разные бывают, я почти уверен что в области DSP что-нибудь найдётся :) вот почти уверен что на сайте TI или Аnalog Devices (Аналоговые Девицы!) - что-то найдётся


          1. SIISII
            01.11.2024 05:26

            Ну, 64-разр микропроцессоры, может, и найдутся, а вот микроконтроллеры... Сомнительно-с


            1. RodionGork Автор
              01.11.2024 05:26

              там ведь размыта дифференциация - вот PIC64 я открываю спецификацию - они обозначаются как MPU а не MCU - но при этом читаю:

              Peripheral Interfaces
              • Up to four TSN Ethernet endpoint ports supporting rates
              from 10M to 10G
              • Two SPI, four UART, four I2C, 64 GPIO, two MDIO, JTAG
              host, timers and watchdogs

              для микропроцессора это довольно необычно :)


              1. SIISII
                01.11.2024 05:26

                Ну, сейчас сплошь SoC (системы на кристалле), хотя граница уже давно стала размываться -- ещё с 1980-х, на самом-то деле. Лично я делю по простому принципу: если можно собрать работающую плату, не используя какую-либо внешнюю память (ни ОЗУ, ни ПЗУ -- в том числе в виде, скажем, SD-карты), то это микроконтроллер, если же внешняя память нужна -- микропроцессор.


                1. RodionGork Автор
                  01.11.2024 05:26

                  мне тоже кажется удобной такая дифференциация, но ранние микроконтроллеры (например 8051) не имели внутренней памяти ведь - и даже более модерновые могут не иметь встроенного генератора частоты например. или скажем младшие AtTiny - у них ни встроенной ОЗУ ни внешней :)

                  ну в общем да, сошлись на том что границы расплылись...


                  1. SIISII
                    01.11.2024 05:26

                    Ну, большинство ранних внутреннюю память имели: скажем, 64 или 128 байт оперативы и 1 или 2 Кбайта УФ ПЗУ. Хотя, вроде, действительно были требующие для работы внешнего ПЗУ, но уж не помню, честно говоря: я имел дело лишь с 8051, очень немного и лишь с вариантом с памятью на борту, всё остальное -- только микро- (или не микро :) ) процессоры. Это сейчас почти исключительно с микроконтроллерами работаю


                    1. Serge78rus
                      01.11.2024 05:26

                      Насколько помню, 8051 без ПЗУ - это 8031, а 8052 (вариант с дополнительной страницей 128 байт ОЗУ) без ПЗУ - соответственно 8032. Так же и более древний 8048 без ПЗУ - это 8035.


                      1. RodionGork Автор
                        01.11.2024 05:26

                        Спасибо за уточнение, я в них явно путаюсь - сразу видно что с оригинальными дела-то не имел :)


                  1. slog2
                    01.11.2024 05:26

                    Совсем без внутренней памяти это 8031. 8051 имели внутреннюю память программ.


                1. LAutour
                  01.11.2024 05:26

                  Требование наличия встроенного (П)ПЗУ из этого простого принципа лучше убрать. В него например микроконтролеры ESP плохо вписываются.


                  1. SIISII
                    01.11.2024 05:26

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


                1. iShrimp
                  01.11.2024 05:26

                  Главное отличие микроконтроллера - отсутствие аппаратного менеджмента памяти (MMU), что затрудняет портирование на них многозадачных ОС с загрузкой пользовательских программ.


                  1. RodionGork Автор
                    01.11.2024 05:26

                    да, сейчас есть такое формальное разделение, например кортекс А и М. но к старым процам (до 286 например) по моему это неприменимо


                  1. SIISII
                    01.11.2024 05:26

                    У микроконтроллеров R-профиля архитектуры ARM MMU тоже отсутствует, у них только MPU (как и у многих M-профиля) -- так что не критерий.


  1. danielsedoff
    01.11.2024 05:26

    Для тех, кто почти ничего не знает об ассемблерах (у меня совсем мало таких знаний) — бомба, живо написанный туториал. Негативные комментарии в основном от тех, кто и так машинный код пишет и машинным маслом запивает.


    1. RodionGork Автор
      01.11.2024 05:26

      спасибо за отклик! действительно любопытно было понять есть ли кому такой "поверхностный" стиль пригодится :) негативных тут вроде не особо - просто пришли коллеги кто явно в ассемблерах погряз может ещё когда я пешком под стол ходил - статейка же именно для "тех кто не в курсе" задумана :)


  1. checkpoint
    01.11.2024 05:26

    А где же наш горячо любимый RISC-V ? Хотя бы RV32I.


    1. RodionGork Автор
      01.11.2024 05:26

      очевидно до них у меня пока руки не дошли - ну что ж, займёмся! тема действительно насущная в последние пару лет они явные конкуренты stm32 - только по-моему uart-загрузчика из коробки нет, возможно поэтому я пока с ними тормозил :)


      1. SIISII
        01.11.2024 05:26

        Подушню малость: RISC-V -- архитектура, а STM32 -- микроконтроллеры. Корректней говорить, что RISC-V -- конкурент для ARM (архитектура против архитектуры).


        1. RodionGork Автор
          01.11.2024 05:26

          ну да, по названиям всё так - но о конкуренции архитектур говорить сложно, архитектуры сами себе пользователям не продаются :) а вот всплывшие в последнее время контроллеры от WCH, подозрительно схожие с STM32 по форм-фактору, но в целом предлагающие бОльшие ресурсы за меньшие деньги - это прямая конкуренция. (т.е. речь о конкретно CH32V203 например в сравнении с ST32F103 или соседними)

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


          1. Javian
            01.11.2024 05:26

            Была статья на Хабре про CH, под которой была ссылка на сообщество разработчиков на этих контроллеров https://t.me/ch32v


          1. SIISII
            01.11.2024 05:26

            Судя по названию, CH32V203 -- скорей, конкурент STM32F2, а не F1. Но от вряд ли конкурент для, скажем, STM32H7 или для какого-нибудь STM32F0. Разные ресурсы = разные задачи, а какая конкретно процессорная архитектура -- уже не столь важно, если говорить именно о моделях МК.


  1. firehacker
    01.11.2024 05:26

    Почему авторы статей базового уровня по ассемблеру x86 так любят реальный режим и DOS?

    По-моему, эта комбинация — одна из самых неприятных и отталкивающих. У многих вообще формируется порочная ассоциативная связь: я как-то общался с одним человеком, который работал преподавателем программирования в колледже — когда я сказал, что, в числе прочего, пишу код на ассемблере, он скривил лицо и сказал что-то вроде «фу, ассемблер это же какой-то допотопный язык времен ДОСа». То, что любой современный исполняемый файл состоит из таких же машинных команд, которые можно записать в виде человеко-читаемого ассемблерного листинга, в его картину мира не укладывалось.

    Виноваты в этом статьи про ассемблер, которые все дружно начинают знакомство читателя с ассемблером приводя в пример 16-битный реальный режим, 16-битные регистры и прерывания DOS/BIOS.


    1. RodionGork Автор
      01.11.2024 05:26

      честно говоря не очень понятно кому это адресовано - если мне - то статья отнюдь не начинается с доса и им не заканчивается. тут акцент выраженно на ассемблер для контроллеров (как по мне - наиболее адекватная сфера для него в наше время)


      1. firehacker
        01.11.2024 05:26

        Не важно, что бОльшая часть посвящена ассемблерам для МК-шных архитектур. Вопрос в том, что как только вы коснулись x86-ассемблера, то сразу всплывают

        mov ah, 9
        int 21h

        Во-первых, это создаёт ощущение того, что вы пересказываете статьи 30-летней давности, для которых такие сниппеты-примеры были актуальными. Хотя как раз вот такое подобное API (между прикладным софтом и ДОС-ом и между гипотетической ОС реального режима и БИОС-ом) морально устарело, а вот сам x86-ассемблер актуален как никогда, покуда жива x86-архитектура, пусть и в виде x86-64.

        Во-вторых, это чисто эстетически не самый приятный и разумный способ организации API. Он исторически сложился, но не стоит начинать знакомство а написанием кода на ассемблере именно с демонстрации такого подхода. Почему бы в качестве примера не привести что-то вроде

        push ...
        push ...
        push ...
        push ...
        call [MessageBoxA]

        Это и актуально, и не так запутанно, и универсально, и пример можно скомпилировать и запустить как под Windows 95, так и под современной виндой. А потом уже в качестве шок-контента показать, как делалось взаимодействие с системными API в стародавние времена.

        В-третьих, даже если закрыть глаза на неактуальность DOS-прерываний в реальном режиме, а представить, что это супер-актуально и что ничего другого нет — опять же, именно такой код в таком виде подаёт дурной пример. И за этим делом стоит, на самом деле, большой философский вопрос. Да, я знаю, что именно так (int 10h) в большинстве своём все и пишут (или писали). Но правильно ли так писать?

        Подумайте вот над каким глубоким вопросом: что самое главное в ассемблере, что первостепеннее всего в ассемблере и почему ассемблер вообще называется ассемблером (а не мнемо-транслятором или машкод-транслятором или чем-то таким)?

        Когда я был юным и глупым, я бы уверенно ответил, что в первую очередь ассемблер это инструмент, который транслирует инструкции для процессора, написанные в человеко-читаемом виде (в виде текста из мнемоник инструкций и операндов) в машинный код в его непосредственном виде. То есть я бы сказал, что написать свой ассемблер — значит написать трансялятор, который для каждой строчки вроде xor eax, eax или nop или int 10h выплюнет в ответ байты 33С0, 90 или CD10. Но прошли годы, мне довелось и свой собственный x86-ассемблер написать, и написать just-for-fun реализацию компилятора Си , и я могу с уверенностью сказать, что трансляция текстового представления инструкций в бинарное представление это вообще ерунда и абсолютно тривиальная задача, а самое главное в ассемблере совсем не это.

        А что же тогда? Тут стоит задаться вопросом: вот есть ассемблер для x86, для MIPS, для ARM, для AVR, а может ли существовать ассемблер вообще ни под какую архитектуру, то есть ассемблер, вообще не знакомый ни с одной из архитектур, не знающий ни одной машинной команды? Будет ли хоть какой-то смысл существования такого инструмента? Тогда можно поставить вопрос иначе: могу ли я взять какой-нибудь x86-ассемблер (например FASM, MASM, NASM) и с его помощью породить на свет машинный код для AVR8? Ведь набор инструкций совсем другой, а даже если какие-то мнемоники и совпадают, опкоды совсем другие и принцип кодирования операндов тоже совсем другой. На самом деле — могу, если просто проведу трансляцию текстового представления машинных инструкций в бинарное и в своём ассемблерном листинге запишу всё в виде директив .db. Нечестная игра? Очень тяжело? На самом деле, не очень-то и тяжело — написано один раз и работает всегда. А есть в ассемблерах (для любых архитектур) кое что ещё, без чего писать код, править и дорабатывать его было бы на порядок или на несколько порядков более тяжело, чем если бы конвертировать инструкции в бинарный вид пришлось бы вручную.

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

        Вот это самое главное (по крайней мере я так считаю) в ассемблерах и самое сложное при написании своего ассемблера для какой бы то ни было архитектуры.

        Почему сложное? Что здесь сложного?

        А вот, например, что. Возьмём для простоты архитектуру x86. С точки зрения человеко-читаемого представления кода всё очень легко:

        jmp <метка1>
        или
        jge <метка2>

        С точки же зрения машинного кода как для безусловного джампа так и для всех условных джампов существует две версии: одна версия инструкции после которой процессор ожидает увидеть относительное смещение в виде 8-битного знакового числа, в другой версии смещение будет закодировано уже в виде 32-битного или 16-битного знакового числа (в зависимости от того, в 32-битном или 16-битном режиме выполняется задача, код которой выполняет процессор + к этому к инструкции может быть примерён префикс 66h переопределяющий размер адреса на противоположный).

        Так вот, если мы пишем свой собственный x86-ассемблер, который идёт по строкам, перебирая одну за другой, и для каждой новой инструкцией делает трансляцию в машинный код, то здесь возникает проблема. Как ассемблеру обработать машинную команду вида jmp some_label? Выбрать для этого короткий форму кодирования с опкодом EB или длинную форму с опкодом E9? Предположим, мы генерируем код для 32-битного режима. Тогда инструкция с опкодом EB будет занимать 2 байта, а инструкция с опкодом E9 — 5 байт, из которых четыре это относительный адрес места назначения прыжка. Можно было бы поступать тупо и везде для всех джампов, как безусловных так и условных, использовать длинный вариант инструкции. Но это бы безмерно разувало машинный код, потому что ветвлений в коде чуть ли половина от всего объёма команд.

        Можно поступать умнее: если jump destination лежит относительно недалеко и смещение относительно адреса следующей инструкции лежит в пределах от –128 до +127, то нужно использовать компактный двухбайтовый вариант инструкции, в противном случае — длинный трёх или пятибайтовый.

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

        Что мы тут можем сделать как авторы гипотетического ассемблера? Мы можем рекурсивно запустить ассемблирование (точнее трансляцию) далее следующих инструкций с указанием вложенноу рекурсивному вызову делать её до тез пор, пока не доберёмся до нужной метки — по возврату мы уже будем знать точное расстояние до интересующей нас метки, сможем выбрать между коротким джампом и длинным джампом, и, сгенерировав его, продолжим трансляцию с того места, до которого добрался только что сделанный рекурсивный вызов. Либо мы можем в условиях неопределённости применять всегда длинный вариант джамп-инструкции, но заносить все такие инструкции в особый список, а по завершению первого прохода, когда взаимное расположение всех меток станет окончательно ясным — провести второй проход и пройтись по всем джамп-инструкциям из списка, и те их них, где можно было бы обойтись коротким вариантом вместо длинного, заменить на короткий вариант.

        Проблема в том, что ни первый ни второй подход не работают. Если метка лежит впереди и джамп ведёт вперёд и мы не знаем, какое до него расстояние и у нас возникает неопределённость относительно длины текущей транслируемой инструкции, мы не можем тупо взять и прямо сейчас попробовать ассемблировать/транслировать последующие инструкции (с целью определить величину смещения), либо отложенным проходом расставить все точки над i, потому внутри этого прыжка могут оказаться другие джамп-инструкции с неопределённой длиной, а у тех, в свою очередь, destination-метка может быть выбрана так, что уже длина тех инструкций тоже будет в состоянии неопределённости и попадёт в зависимость от длины транслируемой в данной момент джамп-инструкции. Иными словами: длины джамп-инструкций зависят от того, насколько далеко нужно прыгать, а если перепрыгивать нужно через другие джамп-инструкции, которые тоже в свою очередь непонятно какой длины в конечном итоге получатся, то зависимость между длинами джамп-инструкций может стать циркулярной, замкнутой. И тогда в первом подходе мы получаем бесконечную рекурсию, а во втором подходе мы получаем бесконечный цикл на втором проходе (компактификации длинных джампов), потому что компактификатор будет заменять длинный джамп на короткий в одном конкретной месте, а маленькое изменение будет приводить к сдвигу всех (или многих) последующих инструкций на несколько байт назад, сдвиг куска машинного кода на несколько байт переведёт к необходимости пересчёта кучи других смещений, стоящих в разных местах кода, и где-то в результате такого пересчёта величина смещения увеличится настолько, что короткий вариант джамп-инструкции перестанет переходить, а это в свою очередь вызовет необходимость поменять короткий джамп на длинный, а это вызовет новый сдвиг точек/меток, новый пересчёт смещений, то есть это запускает цепную реакцию, конца у которой может не быть.

        Конечно, и от этого можно защититься, если для каждого такого места, которое мы собираемся перекраивать в плане замены длины инструкций в пользу большей компактности, ввести флаг или счётчик попыток перекроить инструкцию: если мы сгенерировали сначала длинный джамп (но занесли его в особый список, чтобы потом попробовать применить компактный), потом заменили на компактный (чем вызвали пересчёт всех, а точнее многих прочих меток и смещений), и оказалось, что это вызвало волну пересчёта смещений и вынудило нас где-то в другом месте поменять короткий джамп на длинный, что в свою очередь вынуждает нас опять первоначальный джамп поменять на короткий, то не нужно бесконечно менять короткую вариацию инструкции на длинну и наоборот — уже после второй попытки стоит ставить крест и оставлять длинную вариацию инструкции.

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

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

        Тогда смысл ассемблера, который вообще ни в курсе ни про одну архитектуру и не знает ни одной мнемоники очень простой — с помощью такого ассемблера по прежнему можно создать код машинный код под любых архитектуру, просто забивая опкоды и кодируя операнды ручками, но при этом вычислять и подставлять адреса и относительные смещения за нас будет инструмент. Нам не придётся от одной правки где-то в середине кода пересчитывать десяток адресов/смещений и менять их в коде. С другой стороны, если такой столь странный инструмент умеет работать с метками и директива, мы можем с помощью такого ассемблера сформировать какой-нибудь не исполняемый, но при этом бинарный файл со сложной структурой. Какой-нибудь PDF, или x509-сертификат, или MBR с таблицей разделов или что-то ещё табличное и с древовидной структурой. Нам не придётся вручную считать смещения, адреса и индексы, за нас это сделает ассемблер.

        К слову, наличие таких директив, меток и адресной арифметики с ним даёт нам возможность на выходе ассемблера получить, например, PE-файл или ELF-файл корректного формата даже в том случае, если ассемблер вообще ни сном ни духом ни про PE-формат, ни про ELF-формат и не поддерживает из коробки генерацию исполняемых файлов в таком формате, а может выдавать на выходе только raw-бинарники. Разве что с полем типа CRC/checksum будет проблема — его вычисления распространными директивами типа .org/.align и адресной арифметикой на добьёшься.

        Так вот, к чему я об этом всём? Во-первых, как мне показалось, вы мало внимания уделяете обзору именно этой стороны ассемблера: всем этим директивам, константам и трюкам с адресной арифметикой. А это, между прочим, присуще в той или иной степени всем ассемблерам, и с другой стороны, жизнь без именно этих фишек была бы адом — гораздо большим адом, чем если бы int 10h пришлось бы превращать в .db CDh, 10h вручную, вооружившись табличкой. Хотя у вас пример с применением этой адресной арифметики в коде проскальзывает (len = . - msg), но я вном виде об этом в тексте статьи упоминания нигде нет.

        Ну а потом, это собственно, то с чего я начал третью претензию. Раз вы не собираетесь делать секрет из того, что помимо собственно трансляции машинных команд ассемблеры понимают ещё и директивы, объявления констант, какие-то ассемблеры понимают макросы, почему вы собственно не пользуетесь этими константами?

        Почему mov AH, 9? Почему int 20h? Почему вы не объявляете константы для прерываний, функций и подфункий, не даёте им внятные человеко-читаемые имена? Вам не кажется, что эти 9 и 20h это то, что отталкивает от мысли освоить ассемблер, потому что пугает необходимостью зазубривать все это номера прерываний и функций?


        1. RodionGork Автор
          01.11.2024 05:26

          Почему mov AH, 9? Почему int 20h? 

          потому что статья - краткий обзор а не учебник от Питера Нортона с демонстрацией создания шестнадцатеричного редактора для дисков в объёме 200 страниц.

          цели статьи указаны в самом начале. я не сторонник тюториалов-разжевалок - считаю что важно показать направление, возможности - а дальше кто заинтересовался - сам уж всё найдёт

          однако что мешает Вам исправить ситуацию и написать собственную статью? в частности использовать бОльшую часть данного комментария как отправную точку.


          1. firehacker
            01.11.2024 05:26

            потому что статья - краткий обзор а не учебник от Питера Нортона с демонстрацией создания шестнадцатеричного редактора для дисков в объёме 200 страниц.

            Никто не спорит, но приводя пример ARM-кода вы всё же используете константы GPIO0DIR и GPIO0DATA вместо магических чисел 0x50008000 и 0x50003FFC, в примере с AVR8 вы используете константы PORTD и DDRB вместо магических 0x12 и 0x17 и так далее, но именно в случае x86 почему-то используются магические числа вместо констант с внятными именами. Вот именно к этой непоследовательности и вопрос.

            однако что мешает Вам исправить ситуацию и написать собственную статью? в частности использовать бОльшую часть данного комментария как отправную точку.

            Мешает гнетущение ощущение, что такая статья окажется никому не нужной и не интересной, недостаток времени и почти что неспособность бороться с тенденцией «и тут Остапа понесло» при написании статей (и комментариев тоже, но с комментариев спрос не такой строгий и ответственность не такая сильная).


            1. RodionGork Автор
              01.11.2024 05:26

              что такая статья окажется никому не нужной ... недостаток времени

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

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


              1. firehacker
                01.11.2024 05:26

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

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


        1. vvzvlad
          01.11.2024 05:26

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

          Хотел плюсик в карму поставить, а он там стоит уже(


          1. RodionGork Автор
            01.11.2024 05:26

            вот, значит не мне одному показалось что надо в статью :)

            @firehacker- слышите?


    1. checkpoint
      01.11.2024 05:26

      Наверное потому, что для многих запустить dosbox проще чем as. ;)


    1. SIISII
      01.11.2024 05:26

      А препод, такое говорящий, -- идиот. Хотя бы потому, что ассемблер -- это не "допотопный язык времён ДОСа", а "естественный" язык для любой машины независимо от времени и ОС. Впрочем, кто может работать -- работает, а кто работать не может -- тот учит (с)


      1. RodionGork Автор
        01.11.2024 05:26

        а что, где-то кто-то сказал про допотопный?

        вообще немного забавно когда ассемблер рассматривают как самый нижний уровень на который можно спуститься в разработке. в то время как энтузиасты программируемой логики воплощают собственные процессоры на VHDL


        1. SIISII
          01.11.2024 05:26

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


          1. RodionGork Автор
            01.11.2024 05:26

            ну тут видимо кому что - я электронщик исторически и для меня аппаратура это не полностью "другое". отчасти такой взгляд оправдан - иногда думая "да пачиму разработчики системы команд такой страх сочинили" нужно вспомнить что система команд не рождается из ничего - она воплощена таки в "этих ваших транзисторах" (ну или скорее логических блоках) и порой физические/технические ограничения находят отражение на нашем уже программном (низком) уровне


            1. MaTocoB
              01.11.2024 05:26

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


              1. Javian
                01.11.2024 05:26

                back in the late 70s my software colleagues always said that the PDP-11,
                M6800, M6809, M68000 were all designed by SOFTWARE engineers.

                It was clear to us that the Intel 8008, 8080, 8088, 8086 etc were designed
                by HARDWARE engineers ... just take a look at their architectures,
                instruction sets and opcode mnemonics!

                ёмко и кратко отсюда


      1. MaTocoB
        01.11.2024 05:26

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


  1. MaTocoB
    01.11.2024 05:26

    А как же NASM и FASM в качестве ассемблеров х86?! Не говоря уже о бессмертных MASM и Debug...


    1. RodionGork Автор
      01.11.2024 05:26

      FASM по-моему не только для x86 - не удивлюсь если и сейчас существует. Debug хотел упомянуть т.к. начал естестественно с него и ещё в 2010 году кого-то из коллег им удивлял - но сейчас обнаружил что в досбоксе он не запускается. разбираться не стал.