| Предыдущая глава |
↓ Содержание ↓
↑ Свернуть ↑
| Следующая глава |
как работать с константами и с прямыми адресами. То есть чтобы обратиться
например к тому-же консольному терминалу, его адрес (177560) уже должен быть в
одном из регистров процессора. Но как же он туда попадёт? А вот с помощью
такого любопытственного фокуса-покуса:
Вспомним, что регистр с самым большим номером R7 это счетчик команд. Перед
началом выполнения очередной команды он содержит её адрес. Далее команда
считывается, адрес в R7 увеличивается на два и указывает на следующее слово
после команды. И вот теперь эта команда начинает выполняться... Ну так в
процессе её выполнения можно сделать с любым регистром (в том числе и с R7) всё
что нам вздумается! Например MOV R7,R3; (код 010703) копирует в R3 адрес
следующей команды, а MOV (R7),R3; (код 011703) — её саму (уж не знаю зачем).
Соответственно MOV R3,(R7); (код 010317) — наоборот вписывает на место следующей
команды содержимое регистра R3. (Если конешно эта программа не прошита в ПЗУ -
тогда фокус не получится.) Команда MOV R3,-(R7); (код 010347) подменит саму
себя, после чего будет выполнена по-новой, т.к. в R7 после её выполнения
останется её-же адрес...
Но самый полезный фокус заключается в следующем: команда читает следующее
после себя слово и сразу же его пропускает чтобы не выполнять как команду.
Методы адресации 27 и 37, в смысле (R7)+ и @(R7)+. Первый позволяет
использовать это слово как константу, второй — как АБСОЛЮТНЫЙ адрес. (Если
вспомнить еще про "индексные" методы адресации 67 и 77 тоже использующие
дополнительное слово, то из них получается обращение по ОТНОСИТЕЛЬНОМУ адресу,
позволяющему писать "перемещаемые" программы, которым безразлично в какое место
ОЗУ их загрузили.) В ассемблере эти фокусы узаконены в том смысле, что для них
введено удобное отдельное обозначение: непосредственный операнд обозначают
символом # а абсолютный адрес — символом @. Например:
MOV #123456,R3; код 012703 123456 — загрузить в R3 само число 0123456
MOV @123456,R3; 013703 123456 — загрузить туда нечто по этому адресу
MOV 123456,R3; 016703 ?????? — впринципе то же самое, но каким получится
содержимое второго слова команды сильно зависит от того, по какому адресу
окажется сама эта команда. Поэтому так обычно не делают — пишут что-то типа:
MOV AAABBB,R3; где AAABBB — имя переменной — в смысле метка ячейки памяти,
которая если что — будет переезжать по памяти вместе кодом программы.
Еще с невесть каких времён известно, что "в любой программе найдётся хотя бы
одна ошибка". Вот народ (мои шефы, а сам я тогда еще был студентом) и говорит:
как же так, неужели действительно в абсолютно любой? (Ну, разумеется только
пока еще эта программа не отлажена.) Ну а если программа эта -
маленькая-маленькая? Самая маленькая, какая вообще может быть — вообще из одной
команды! Проверим? И проверили (а я при том присутствовал).
Надо сказать, что придумать нетривиальную программу, которая бы состояла
всего из одной команды — задача сама по себе весьма нетривиальная. Но на наше
счастье в (переводном) журнале "в мире науки" попалась любопытственная статья,
содержащая описание компьютерной игры с условным названием "борьба за память".
Смысл этой игры вот в чем: имеется предельно простая абстрактная вычислительная
машинка; два программиста пишут две боевые программы; они загружаются в её
память и выполняются параллельно — выигрывает та программа, которая сможет
испортить другую. Вернее проигрывает та, которая первой выполнит команду СТОП.
В статье приводилось описание этой самой вычислительной машины и примеры
нескольких боевых программ для неё. Архитектура машинки предельно проста:
— никаких внешних устройств и средств ввода/вывода — машинка только для игры
— система счисления — десятичная
— система команд — трёхадресная: слово как раз такой длины, что в нём
помещается код операции (одна десятичная цифра) и три адреса
— размер адреса вроде-бы четыре цифры, т.е объём памяти — 10 килослов
— адресация — исключительно "относительная", т.е. любой адрес указывает
расстояние между адресуемой ячейкой памяти и командой
— команд всего десять штук; конкретные коды команд не помню (при
необходимости можно придумать заново); код команды СТОП, разумеется, ноль.
Трёхадресная система команд не предполагает никаких регистров: первый адрес
указывает где взять первый операнд, второй — где второй; третий — куда положить
результат. Абсолютных адресов нет — программа совершенно одинаково работает в
любом месте оперативной памяти и не имеет никаких средств узнать, где именно она
находится. Память, соответственно "циклическая", т.е. после ячейки с самым
большим номером следует ячейка с нулевым.
В статье было описано несколько боевых программ. Например "карлик" — всего
четыре команды: разбрасывает по памяти нулевые "бомбы", но с таким шагом чтобы
не попасть в самого себя. Программа "блоха", скачущая по памяти, копируя свой
код в другое место. Программа "телескоп" — не агрессивная, способная только
защищаться: она организует справа и слева от себя два "форпоста" и постоянно их
проверяет. Если один из них окажется испорчен, например в результате того, что в
него попала одна из нулевых бомб, разбрасываемых "карликом" — копирует себя за
зону возможного обстрела, а форпосты — восстанавливает. Ну и наконец — программа
"чертёнок", состоящая ровно из одной команды. Эта команда просто копирует самоё
себя в следующую ячейку — ту самую в которой следующая команда, и таким образом
заполняет собою всё память.
Вот Николай Иванович Муравьёв и говорит: а зачем нам абстрактная
вычислительная машина — у нас же конкретная есть (и кивает на Э-100/16) -
давайте напишем "чертёнка" для неё. И написал. Ну так в процессе написания этой
программы действительно была допущена ровно одна ошибка. А в уже отлаженном
виде вся эта программа выглядит так: 014747.
Вышеизложенной информации вполне достаточно чтобы понять как она работает.
Ну-с, думаю с методами адресации всё понятно?
Тогда рассмотрим остальные команды. Как уже было сказано, под них
отведены коды 0 и 7, т.е.:
Х0ХХХХ или если восьмеричные цифры расписать по битам: Х 000 ХХХ ХХХ ХХХ ХХХ
Х7ХХХХ Х 111 ХХХ ХХХ ХХХ ХХХ
Следует сразу сказать, что комбинация 17ХХYY зарезервирована для команд
процессора с плавающей запятой (ППЗ). Все они максимум полутора-адресные, т.е.
полноценный операнд (использующий регистры основного процессора) только один,
помещается в разрядах, обозначенных YY. А расположенный перед ним номер
регистра ППЗ (если есть) занимает всего два бита.
Соответственно под все остальные команды процессора остались комбинации типа:
07ХХХХ и Z0XXYY где один "знаковый" бит, приходящийся на последнюю цифру и
обозначенный здесь Z может, как и в двухадресных командах, использоваться как
признак размера операнда; YY указывать сам операнд, а биты XX использоваться под
код операции. Именно так одноадресные команды и сконструированы. Под них
заняты коды операций 5Х и 6Х:
r0ККaa здесь r — размер операнда, aa — сам операнд A, а КК — код команды:
50 CLR A "очистка" — просто записывает в A число 0: 0 -> A
51 COM A "инверсия" — инвертирует все биты своего операнда: ~A -> A
52 INC A "инкремент" — прибавляет к нему единичку: A+1 -> A
53 DEC A "декремент" — вычитает: A-1 -> A
54 NEG A "негатив" — смена знака на противоположный ~A+1 -> A
55 ADC A прибавляет к операнду признак C (или не прибавляет, если он ноль)
56 SBC A то же самое только вычитает: if(C)A++; и соответственно if(C)A-;
57 TST A берёт операнд и ничего с ним не делает — только устанавливает
признаки результата — биты NZVC в слове состояния процессора
60 ROR A "циклический" сдвиг вправо (в сторону младших разрядов)
61 ROL A -//— влево (в сторону старших)
62 ASR A "арифметический" сдвиг вправо — эквивалентен делению на два
63 ASL A -//— влево — -//— умножению -//-
Вот собственно почти все "содержательные" команды базового набора. (Ну
разве что еще SWAB — обмен байтов в слове с кодом 0003aa, и SXT — расширение
знака с кодом 0067аа.)
Примеры:
CLR R3; код 005003 — просто очищает регистр R3
COMB (R3)+; 105123 — инвертирует байт по адресу в R3 (а R3 = R3+1)
INCB (R7)+; 105227 — наращивает однобайтовый счетчик, находящийся прямо
сразу после команды (но R7 всё равно увеличивается не на 1 а на 2!)
ROR R3; код 006003 — сдвигает все биты регистра R3 на одну позицию в
сторону младших разрядов; в самый старший бит задвигается содержимое
признака C (того, который "перенос"), а самый младший, выдвинутый за пределы
разрядной сетки, помещается в него-же. И следующей такой-же командой может быть
например задвинут в старший разряд следующего слова...
ROL R3; код 006103 — всё то же самое, но в другую сторону
ASR R3; 006203 — здесь сдвигается вправо только 15 бит: старший
"знаковый" разряд остаётся где был и даже копируется в следующий после него.
ASL R3; код 006303 — здесь в признак C переезжает предпоследний разряд, а
"знаковый" тоже остаётся где был; в младший разряд задвигается ноль.
То, что старший разряд слова (да и байта) — "знаковый" упоминалось
неоднократно, но видимо следует пояснить, что числа со знаком в данной
вычислительной машине (да и в подавляющем большинстве других) представляются в
т.н. "дополнительном" коде.
Тут вот какое дело: сами по себе битики, составляющие машинное слово, ничего
не значат. (Спасибо еще что упорядочены.) Смысл им придаёт тот, кто использует.
Но если в троичной системе счисления, где каждый разряд машинного слова "трит"
принимает не два а три значения (такая машина была в мире только одна: Сетунь и
её развитие Сетунь-70 им тов. Бруснецова) и там числа автоматически получаются
со знаком (ибо эти три значения наиболее естественно интерпретировать как +1, 0
и -1, а числа с плавающей запятой там — вообще отдельная песня), то для двоичной
системы счисления подобный фокус проделать невозможно. И чтобы пользоваться не
натуральными (т.е. только положительными, правда включая ноль), а и целыми
числами (то есть со знаком) приходится предпринимать искусственные меры. Разные.
Самая простая и естественная — выделить под знак числа один из разрядов
машинного слова — самый младший, или самый старший или вообще находящийся за
пределами разрядной сетки — не важно. Важно как такое "целое" (т.е. знаковое)
число будет сочетаться с "натуральным" (т.е. беззнаковым) и как мы с ними со
всеми намерены производить операции. Хотя бы самую простую — сложение.
Если просто выделить один разряд — как правило самый старший — чтобы не
мешался под ногами и чтобы положительное знаковое число совпадало с беззнаковым,
то во-первых у нас будет два нуля — положительный и отрицательный, а во-вторых
для сложения понадобятся два устройства — сумматор и вычитатель (потому как
сложение чисел с разными знаками это вычитание). Но так иногда делают. Это
кстати называется "прямой код". Однако есть интересный фокус-покус, позволяющий
обойтись одним устройством и для сложения и для вычитания — его-то все и
используют: хранят отрицательное число в виде т.н. "дополнительного кода".
Предположим, что у нас уже есть какое-то положительное число и мы хотим
сменить ему знак на противоположный. Предположим так-же что это число имеет
бесконечное число разрядов. Вот только само число конечное, и все разряды с
номерами больше некоторого N равны нулю. А разряд N, соответственно единичка.
(Хотя, если число — ноль, то ни одной единички в нём нет.) Ну так вот,
преобразуем его в дополнительный код следующим образом: все биты инвертируем и к
тому что получилось прибавляем единицу. Что получится — не важно, главное, что
перенос "заглохнет" в пределах первых N разрядов, а старшие так и останутся
единичками. Если такое число честно прибавить к положительному — это будет самое
натуральное вычитание! (Проверьте.) Теперь его можно смело обрезать в любом
месте (за пределами N разрядов) и самый старший бит назначать знаковым.
номера разрядов: N 3210 (с 0 считаем или с 1 — не важно)
было ....00000...01ХХХ...ХХХХ
инвертировали ....11111...10YYY...YYYY Y = ~X
прибавили 1 ....11111...1?ZZZ...ZZZZ
А вот если исходное число было 0, то при инверсии все его битики будут
единичками и при прибавлении единицы перенос убежит куда-то в бесконечность. И в
результате опять получится ноль! Т.е. ноль в этой системе — адын штука. И что
суммирование что вычитание выполняет одна и та же схема (дополненная инвертором
и дополнительно подающая из ниоткуда перенос на крайний разряд).
А еще я как всегда забыл рассказать про признаки результата. Хотя тоже
упоминал их неоднократно. Ну так вот:
N — "негатив" — указывает что результат отрицательный (т.е. старший бит = 1)
Z — "зеро" — указывает что весь результат нулевой
V — признак переполнения — указывает что знаковый разряд "неправильный"
C — перенос в следующий разряд, находящийся за пределами разрядной сетки
Эти самые признаки результата автоматически вырабатываются при выполнении
любой содержательной операции (в том числе и при пересылке данных командой MOV),
а команды управления порядком действий их не трогают. Зато те, которые
"ветвления" используют как условие, когда решают, ветвиться им или нет. Правда
"побитовые" команды (BIT, BIC, BIS и XOR), команды пересылки и еще почему-то
INC и DEC признак переноса тоже сохраняют. За сим некоторые операционные
системы используют его как признак ошибки: если при возврате из системного
вызова признак C сброшен — всё хокей, если установлен, значит что-то не
получилось — смотри коды ошибок.
С признаками N и Z всё очевидно; в C попадает бит, выдвинутый из слова
командой сдвига, а вот что происходит при сложении? (Вычитание, это как мы уже
знаем, — разновидность сложения!) На сколько я понимаю, в признак C попадает
перенос из старшего разряда сумматора за пределы разрядной сетки. А как
устанавливается признак V — непонятно. Везде пишут что в результате
"переполнения". А мол это самое переполнение — когда знаковые разряды были
одинаковые, а результат получился с противоположным знаком. (Что, кстати не
плохо бы было проверить.) Однако несомненно (известно из документации) что если
мы сравниваем два числа вычитая одно из другого: А-Б,
то если А==Б -> Z=1 потому что весь результат получится нулевой
и А!=Б -> Z=0
для чисел без знака:
если А>Б -> C=0 потому что переноса за пределы разрядной сетки не будет
| Предыдущая глава |
↓ Содержание ↓
↑ Свернуть ↑
| Следующая глава |