It’s very much possible to memorize machine code equivalent of assembly instructions. Actually, when writing code in assembly language, one often happens to see hex code through machine code monitors, disassemblers, assembly listings, etc. As a result, over time some instructions can be memorized in their hex form without any extra effort.
- In the picture an 6502 ROM monitor is seen, where hex code and assembly mnemonics are shown side by side.
Second skill you’re going to need is to transform hex code into binary which is quite easy with a trick I’ll explain in a bit.
Think of the following instructions:
OPCODE HEX
LDA #imm 0xA9 imm
STA adr 0x85 adr
STA (adr),Y 0x91 adr
LDY #imm 0xA0 imm
With above opcodes memorized only, we can write the following machine code using only pen and paper:
0xA9 0x00
0x85 0x01
0xA9 0x02
0x85 0x02
0xA0 0x00
0xA9 0x01
0x91 0x01
Actually the above is the following assembly code in mnemonic form:
LDA #00
STA $01
LDA #02
STA $02
LDY #00
LDA #01
STA ($01), Y
- The above code puts a white pixel at the top-left corner of screen in 6502asm.com assembler/emulator, go ahead and try it out!
Now the trick for converting hexadecimals into binary and vice versa is to work it out only for nibbles (4-bit values).
First, remember how to convert binary into decimal. Every time you see 1, multiply that by its binary power. E.g. 101
would be 4 + 0 + 1 = 5
. It can be visualized like this:
1 1 1 1 --> binary points
| | | |
v v v v
8 + 4 + 2 + 1
| | | +---> 2^0 * 1 Ex: 13 is 8 + 4 + 0 + 1
| | +-------> 2^1 * 1 1 1 0 1 -> 1101 (0xD)
| +-----------> 2^2 * 1 Ex: 7 is 0 + 4 + 2 + 1
+---------------> 2^3 * 1 0 1 1 1 -> 0111 (0x7)
With this in mind, and well practiced, the following should be possible:
LDA #00 -> 0xA9 0x00 -> 1010 1001 0000 0000
STA $01 -> 0x85 0x01 -> 1000 0101 0000 0001
LDA #02 -> 0xA9 0x02 -> 1010 1001 0000 0010
STA $02 -> 0x85 0x02 -> 1000 0101 0000 0010
LDY #00 -> 0xA0 0x00 -> 1010 0000 0000 0000
LDA #01 -> 0xA9 0x01 -> 1010 1001 0000 0001
STA ($01),Y -> 0x91 0x01 -> 1001 0001 0000 0001
With some retro computing spirit, motivation and fun, we could actually have written the entire code in binary without writing down the intermediate steps.
On a related note, Paul Allen coded a boot loader for Altair 8800 with pen and paper on an airplane, and possibly had to translate it to binary also with pen and paper: https://www.youtube.com/watch?v=2wEyqJnhec8
It’s very much possible to memorize machine code equivalent of assembly instructions. Actually, when writing code in assembly language, one often happens to see hex code through machine code monitors, disassemblers, assembly listings, etc. As a result, over time some instructions can be memorized in their hex form without any extra effort.
- In the picture an 6502 ROM monitor is seen, where hex code and assembly mnemonics are shown side by side.
Second skill you’re going to need is to transform hex code into binary which is quite easy with a trick I’ll explain in a bit.
Think of the following instructions:
OPCODE HEX
LDA #imm 0xA9 imm
STA adr 0x85 adr
STA (adr),Y 0x91 adr
LDY #imm 0xA0 imm
With above opcodes memorized only, we can write the following machine code using only pen and paper:
0xA9 0x00
0x85 0x01
0xA9 0x02
0x85 0x02
0xA0 0x00
0xA9 0x01
0x91 0x01
Actually the above is the following assembly code in mnemonic form:
LDA #00
STA $01
LDA #02
STA $02
LDY #00
LDA #01
STA ($01), Y
- The above code puts a white pixel at the top-left corner of screen in 6502asm.com assembler/emulator, go ahead and try it out!
Now the trick for converting hexadecimals into binary and vice versa is to work it out only for nibbles (4-bit values).
First, remember how to convert binary into decimal. Every time you see 1, multiply that by its binary power. E.g. 101
would be 4 + 0 + 1 = 5
. It can be visualized like this:
1 1 1 1 --> binary points
| | | |
v v v v
8 + 4 + 2 + 1
| | | +---> 2^0 * 1 Ex: 13 is 8 + 4 + 0 + 1
| | +-------> 2^1 * 1 1 1 0 1 -> 1101 (0xD)
| +-----------> 2^2 * 1 Ex: 7 is 0 + 4 + 2 + 1
+---------------> 2^3 * 1 0 1 1 1 -> 0111 (0x7)
With this in mind, and well practiced, the following should be possible:
LDA #00 -> 0xA9 0x00 -> 1010 1001 0000 0000
STA $01 -> 0x85 0x01 -> 1000 0101 0000 0001
LDA #02 -> 0xA9 0x02 -> 1010 1001 0000 0010
STA $02 -> 0x85 0x02 -> 1000 0101 0000 0010
LDY #00 -> 0xA0 0x00 -> 1010 0000 0000 0000
LDA #01 -> 0xA9 0x01 -> 1010 1001 0000 0001
STA ($01),Y -> 0x91 0x01 -> 1001 0001 0000 0001
With some retro computing spirit, motivation and fun, we could actually have written the entire code in binary without writing down the intermediate steps.
On a related note, Paul Allen coded a boot loader for Altair 8800 with pen and paper on an airplane, and possibly had to translate it to binary also with pen and paper: https://www.youtube.com/watch?v=2wEyqJnhec8
Первоначалом информационных технологий является бит, второначалом — кубит, ну а дальше — тёмный лес. Цель дальнейшего состоит в том, чтобы дать первичную развёртку ИТ, определив граничным условием бит как минимальную единицу информации.
Открываем папку «/Логика/ИТ», создаём в ней директорию «двоичные технологии», прописываем «проблему останова» вместо когерентного критерия логической истинности, предельным решением которой можно считать возможность полного тестирования программы на предмет корректности её реализации, и идём дальше.
1. Разрядность
Итак, кругом одни биты, и с ними надо что-то делать. Что с ними нужно сделать в первую очередь? Пожалуй, упаковать в минимальную единицу памяти. От памяти в ИТ никуда не денешься, понятно что она должна быть как-то структурирована, и что самый простой способ это сделать — представить её в виде линейной последовательности ячеек одинаковой разрядности. Если память определяет пространственный (статический) аспект информационной системы, то временному (динамическому) следует сопоставить процесс, который независимо от целевых задач можно было бы представить в виде совокупности действий по обновлению памяти. Таким образом имеем память и управляющие ею устройства, задающие динамику информационного процесса, с прилегающей информационной структурой, определяющей способ взаимодействия устройств с памятью и между собой. Достаточно богатая архитектура предполагает иерархическую структуру управления, и при всём потенциальном многообразии её функциональных устройств на первых шагах развёртки двоичных технологий возникает потребность в определении такого управляющего устройства, которое было бы способно обеспечить функционирование всей системы посредством формального языка — одним словом необходим процессор. Вмеру богатые информационные системы могут управляться одним процессором, хотя такой вариант решения едва ли можно считать типичным, поскольку «разделение труда» в информационных процессах всегда приветствуется. Ну и с повышением сложности задач это число может быть увеличено от одного до, скажем, мильёна процессоров (распределённые вычисления), образующих собственную иерархию управления. Максимум — «почтальёна», ведь если сложность моделируемого процесса превышает возможности этого «почтальёна» (скажем — за секунду потребуется перелопатить всё дерево вариантов в шахматах), то для решения задач такого уровня уже понадобятся квантовые технологии с использованием кубитов, позволяющих осуществить понижение геометрической прогрессии в вычислениях до арифметической. Таким образом можно определить горизонты целесообразности применения двоичных технологий.
Существует два принципиальных способа интерпретирования хранящихся в памяти данных: собственно данные и код, исполняемый арифметико-логическим устройством (АЛУ), задающим множество элементарных способов манипулирования данными, составляющих формализованный язык низкого уровня, выступающий в роли посредника между «железом» и абстрактной моделью информационного процесса. Все действия по управлению системой можно разделить на внутренние, осуществляющие взаимодействие регистров процессора между собой и с адресуемой им непосредственно памятью, и внешние, реализующие «обмен веществ» между функциональным ядром и «окружающей средой» посредством прямых и обратных связей. Таким образом, одной из наиболее приоритетных задач при проектировании информационных систем является создание машинного языка (асемблера), определяющего множество атомарных действий, обеспечивающих хранение, обмен и преобразование данных. Понятно что устройство, которое всем этим занимается, должно быть логическим и в немалой степени арифметическим — отсюда аббревиатура АЛУ.
На данном этапе имеем неопределимое множество способов сочетания машинного языка с принципами коммуникации между устройствами. Первый шаг формализации состоит в определении правил, справедливых для любой функциональной системы, сочетающей централизованное управление информационными потоками посредством машинного языка с линейной и моноширной организацией памяти. Исторически сложилось так, что понятие «разрядность» потеряло свою актуальность в связи с бурным развитием ИТ по закону Мура, из-за которого (закона, а не Мура) приходилось делать всё наспех а потом затыкать дыры и плодить кучу балластной информации, необходимой для решения проблем совместимости. Во избежание нестыковок по крайней мере на базовом уровне организации информационных систем целесообразно постулировать разрядность фундаментальной константой, предполагая при этом, что упущенные возможности, доступные при вольном с ней обращении, будут несущественными в сравнении с тем преимуществом, которое даёт возможность моноразрядного способа квантования данных.
2. Страница памяти
Итак, следующим элементом в списке дефиниций после бита (память, процессор и устройства считаем само собой разумеющимися) полагаем разрядность. Отметив галочкой опцию «моноразрядность», пройдёмся по другим глобальным настройкам и посмотрим, какие значения там стоят по умолчанию.
По умолчанию имеем линейную организацию памяти и как следствие наиболее тривиальный способ нумерации и идентификации хранящихся в ней данных, при этом содержимое ячеек в зависимости от ситуации может быть интерпретировано либо как данные, либо как код исполняемой команды. Задаём ключевые параметры, определяющие архитектуру информационной системы, которую с учётом принятых на данный момент соглашений можно наделить предикатами «двоичная» и «моноразрядная»:
R — разрядность (ширина единицы памяти в битах)
A — количество адресуемых единиц памяти (объём адресного пространства)
C — количество машинных инструкций (объём командного пространства)
Задаём ключевое условие, способствующее целостности и завершённости архитектуры, в основу которой положен принцип моноразрядности:
A = C = 2 ^ R
Соблюдение данного условия означает, что любая ячейка памяти способна вместить адрес любой другой ячейки, а также содержит исчерпывающую информацию о выполняемом действии. Уменьшение адресного пространства чревато ссылками на несуществующую память, а увеличение — избыточной информацией, необходимой для обеспечения возможности адресовать всю память. То же самое с командным пространством (в дальнейшем буду обозначать его аббревиатурой КП).
Существуют ли другие условия, удовлетворяющие перечисленным выше критериям и очевидные без углубления в детали разработки низкоуровневой архитектуры? Да, по крайней мере одно такое условие существует:
R = 2 ^ N
Соблюдение данного условия означает, что для задания номера любого бита в ячейке требуется целое число бит (опять же, избыток или недостаток здесь нежелателен из соображений удобства написания и надёжности работы машинного кода).
Очевидно, что N=4 — это минимальное приемлемое значение, поскольку 256 байт памяти при N=3 годится разве что для программирования ёлочных гирлянд. При N=5 получаем объём памяти, соответствующий последним достижениям в сфере ИТ. Но если считать главным назначением памяти, адресуемой процессором, хранение машинного кода (движка), а большую часть данных, которыми манипулирует этот движок, вынести за пределы адресуемого пространства (и как следствие — предполагать их носителями внешние устройства), то при таком объёме памяти (16 Гб) машинный код будет составлять лишь незначительную её часть, а всё остальное пространство будут занимать данные. В мегабите же машинного кода при N=4, написанного на эффективном асемблере, при условии вынесения данных за пределы адресного пространства можно будет разместить, пожалуй, ядро полноценной операционной системы. Так что в принципе моношестнадцатиразрядная архитектура способна обеспечить необходимый минимум ресурсов, а именно: 65536 слов адресуемой памяти и столько же машинных команд, составляющих полный список элементарных действий, выполняемых процессором.
Ограничение довольно существенное и создаёт ряд неудобств, вызванных прежде всего небольшим объёмом адресуемой памяти, требующим вынесения обрабатываемых данных за пределы адресного пространства. Удобно ведь, когда они «лежат под рукой» — то есть доступны процессору непосредственно, а не расположены «где-то там», во внешней памяти, откуда их необходимо пересылать во внутреннюю, что-то с ними делать, после чего отсылать обратно. Это момент является, пожалуй, самым узким местом моношестнадцатиразрядной архитектуры и чреват усложнением языка высокого уровня, поскольку написанные на нём программы не смогут абстрагироваться от нюансов, связанных с организацией обмена данными с внешними устройствами. Правильная расстановка акцентов позволяет решить все эти проблемы, ну и вообще при написании программы программисту было бы не лишним представлять себе динамику информационного процесса в полном объёме, а не на уровне отвлечённых абстракций. Также можно отметить, что идеология программирования в таком случае сместит акценты в сторону создания целостных и завершённых решений, помещающихся в одну страницу памяти размером в мегабит — а это немалый объём для обеспечения достаточно сложной функциональности. Принципиальные способы разрешения проблем, возникающих при урезании объёма адресуемой памяти до 64-х килослов, будут рассмотрены позже, здесь же я бы хотел акцентировать внимание на преимуществах от принятия N=4 в качестве фундаментальной константы.
Прежде всего это касается командного пространства (КП): 16-ти бит вполне хватает, чтобы реализовать АЛУ в его классическом виде. Так, увеличение разрядности современных процессоров до 32, а впоследствии — до 64, практически не сказалось на их системе машинных команд и по сути асемблер остался 16-разрядным. В принципе, ничего не мешает придумать машинный язык, система команд которого будет эффективно использовать 32 и более бит, но как показывает практика, острой необходимости в этом не возникает, поэтому здесь целесообразно принять 16-разрядный объём КП в качестве значения по умолчанию. К тому же N=4 обеспечивает дополнительное удобство, а именно:
R % N = 0
Соблюдение данного условия означает, что в одной ячейке памяти можно разместить целое число значений в диапазоне [ 0… R ) (например, для N=5 это условие не соблюдается, поскольку в данном случае R, равное 32-м битам, не делится на 5 без остатка). Поскольку разрядность является фундаментальным параметром, такое свойство позволит обеспечить в дальнейшем дополнительные преимущества, хотя в сравнении с предыдущими аргументами этот — третьего порядка значимости.
3. Внешнее ОЗУ
Итак, принятие числа 16 в качестве фундаментальной константы определяет базовую совместимость пространственного и временного аспектов информационного процесса, а именно: страницу памяти объёмом 64 килослова и перечень атомарных действий того же объёма и формата. Отсюда понятие разрядности, обеспечивающей этот общий объём и формат — мегабит «каши» из нулей и единиц, первично структурированный квантами по 16 бит, составляющих слово данных. Как следствие — одна ячейка памяти способна вместить адрес любой другой ячейки или содержит исчерпывающую информацию о выполняемом на данный момент действии. Предполагается, что независимо от деталей реализации системной архитектуры, разрядность обеспечивает совместимость её функциональных составляющих на первом уровне абстрагирования от физ.процессов, способствуя тем самым удобству формализации этой архитектуры. При этом упущение потенциальных возможностей от принятия такого ограничения предполагается несущественным — то есть далее я исхожу из предположения о том, что если рационально распорядиться этими начальными ресурсами, то можно будет создать на их базе эффективную информационную модель, скажем, любой сложности в пределах «почтальёна». Мегабит данных является ведь не верхним ограничителем объёма информационных ресурсов, а очередной (конкретно — третьей после бита и слова) ступенью формализации. Усложнение моделируемых процессов естественным образом ведёт к иерархической структуре управления, и полагая разрядность актуальной и для следующих ступеней их организации, получаем величину объёма внешнего ОЗУ, удобную своей совместимостью с шестнадцатиразрядной архитектурой: 65536 * 65536 страниц памяти = 8 ГБ. Перевожу на общепринятые байты, и как можно убедиться эта цифра находится в хорошем соответствии с текущими требованиями к ресурсоёмкости технологий. Принципиальное отличие состоит в том, что общая память не может быть адресована процессором напрямую или же придётся использовать для этого два слова памяти. Последнее само по себе не проблема, однако в связи с тем что составляющие систему устройства и процессоры должны между собой эту память как-то поделить, возникает множество нюансов по её распределению и обеспечению к ней доступа. Полагая изначально произвольным способ, которым это можно сделать, попытаюсь всё же выделить ключевые моменты, которые с необходимостью должна учитывать формализованная модель информационного процесса, построенная на философии моноразрядности.
4. Интегрированная система
До этого момента адресуемая память предполагалась ориентированной на хранение машинного кода, хотя это и не исключало возможности хранение в ней данных. Но так не получится обработать напрямую страницу памяти целиком, ведь хоть какую-то её часть должна занимать сама программа обработки. Поскольку предполагается принятие страницы в качестве стандартной единицы фрагментации данных, возникает необходимость эту проблему как-то решить. Проще всего это сделать путём включения в зону видимости процессора дополнительной страницы, которая могла бы послужить буфером, осуществляющим обмен данных с внешней памятью, не затрагивая страницу машинного кода (при этом два бита статуса придётся отвести под определение источника и приёмника соответственно, чтобы данные можно было гонять как в пределах одной из двух страниц памяти, так и между ними). Ввиду небольшого объёма страницы технологически не составит труда обеспечить пересылку данных одним кадром, не дробя этот мегабит на кванты по одному слову — что позволит свести на «нет» временные издержки на фрагментацию больших объёмов внешних данных, предназначенных для обработки процессором. Можно также обойтись без того чтобы гонять данные туда-сюда, используя технологию подстановки, позволяющую выбрать любую из 65536 страниц внешнего ОЗУ и адресовать её процессором напрямую. Если то же самое сделать со страницей кода, то можно будет выполнять хранящиеся во внешней памяти программы, снимая таким образом ограничение на их размер в 64 килослова и расширяя его до 4-х гигаслов.
Опираясь на критерий допустимости подстановки страницы кода/данных из внешнего ОЗУ в адресуемую процессором область, целесообразно разделить процессоры на ведущие и ведомые. С одной стороны это разделение условно, поскольку иерархия управления может быть структурирована как угодно; с другой стороны есть смысл выделить один или несколько процессоров, составляющих верхний уровень управления системой и способных её конфигурировать — таких как ядро операционной системы, диспетчер внешней памяти, аналог BIOS и прочих, ориентированных на приватное хранение исполняемого кода и в которых все эти подстановки данных (а особенно неизвестного кода из внешнего ОЗУ) из соображений надёжности использовать было бы нежелательно. Собственно, на то и рассчитано число 16, чтобы естественным образом обеспечить первичную разметку информационных ресурсов согласно требованиям к быстродействию и степени автономности процессов, этими ресурсами оперирующих. Тут всё достаточно тривиально: исходя из представления о том, что в основу манипулирования данными положен двоичный код, берём двойку и записываем её фундаментальные последовательности:
— n ^ 2 | 2 ^ n
— | 2 ^ 0 = 1
2 * 2 = 4 | 2 ^ 1 = 2
4 * 4 = 16 | 2 ^ 2 = 4
16 * 16 = 256 | 2 ^ 4 = 16
256 * 256 = 65536 | 2 ^ 16 = 65536
— Как видно, что число 16 — величина не только минимально допустимая, но ещё и «самая круглая» среди близлежащих — то есть не вдаваясь в детали реализации информационной модели можно констатировать удобство расфасовки информации порциями именно такой ёмкости (вышеупомянутое условие «R % N = 0», соблюдающееся для следующей по списку величины разрядности, равной аж 256). Из приведённых ранее соображений можно также убедиться в том, что первичная развёртка принципа моношестнадцатиразрядности на иерархию управления хорошо согласуется с практической применимостью, ведь информационные процессы в зависимости от степени автономности рассматриваемого элемента системы дифференцируются согласно критерию быстродействия, обратно пропорционального информационной ёмкости процесса — из чего следует понятие «кэша», расписанного по уровням организации системы: «кэш первого уровня», «кэш второго уровня», и вся эта иерархия обычно укладывается в несколько этажей. При R=16 имеем следующие показатели:
кэш 0-го уровня (слово шириной в 16 бит): R-егистры процессора, предназначенные для адресации внутренней памяти (быстрее всего они будут обмениваться данными между собой в виду отсутствия необходимости выходить за рамки физической локализации АЛУ)
кэш 1-го уровня (блок памяти объёмом в 64 килослова = 1 мегабит): страницы внутренней памяти, предназначенные для непосредственной адресации процессором (одна или две — в большем их числе при наличии возможности полного обновления страницы за несколько процессорных тактов необходимости не возникает). Закреплю за этим типом памяти аббревиатуру АЗУ — то бишь адресуемая.
кэш 2-го уровня (внешнее ОЗУ ёмкостью в 64 килостраницы = 64 гигабит): память, используемая совместно устройствами, составляющими интегрированную систему — автономную единицу, завершённую модель информационного процесса, способную взаимодействовать с другими системами на общих основаниях (с точки зрения системы сетевая карта, обеспечивающая возможность межсистемного взаимодействия — лишь одно из устройств, функционирование которого может быть описано теми же выразительными средствами, что предназначены и для отражения процессов обмена данными внутри автономно функционирующей системы).
кэш 3-го уровня (ПЗУ ёмкостью до 64 килоОЗУ = 4 петабита): на жёсткий диск такого размера поместятся, например, все снятые в мире фильмы, так что информационную ёмкость этого уровня можно считать неограниченной. Также можно считать неактуальным очередное умножение на 65536, задающее следующий константный показатель информационной ёмкости. Собственно, и от этого нет особого толку, поскольку оперирование столь большими объёмами данных размывает представление о том, для чего они могут быть предназначены и как они могут быть структурированы. Кроме того, эта память слишком медленна, чтобы можно было считать её «кэшем» — так что в отличии от предыдущих уровней её нет смысла жёстко лимитировать. 4 гигаслова ОЗУ предполагаются достаточными для того, чтобы не возникало потребности слишком часто гонять данные в ПЗУ и обратно. Если же вычисления окажутся более ресурсоёмкими, то естественно было бы решить эту проблему путём задействования достаточного числа процессоров и эффективного распределения между ними функциональной нагрузки.
Так на основе разрядности рекурсивным методом образуются 3 уровня абстракции, задающие структурную глубину вложенности процессов, поименованных перечислимым множеством языковых конструкций, определяющих формат и граничные условия применимости сопоставленных им действий:
0 => 1: система машинных команд, исчерпывающе определяющая первичный способ квантования действий
1 => 2: язык программирования высокого уровня (предполагается естественной его объектно-ориентированная направленность, и как следствие — специализированность внешнего ОЗУ на хранении объектов)
2 => 3: язык системных запросов (макросов) — например командная или адресная строка, позволяющая выполнять сложные последовательности действий одной командой с множеством предусмотренных для неё параметров. По сути этот уровень можно считать сферой формализации ключевых функций операционной системы.
3 => 0: изначально предполагается условным в виду условной неисчерпаемости ресурсов, входящих в его состав. Исходя из представления о том, что одним точечным действием «по клику мыши» можно задействовать информационный процесс любой информационной ёмкости и сложности, считаем что сюда можно отнести любые поименованные действия, не вошедшие в три предыдущих категории языковых абстракций. Наделить этот уровень конкретикой позволяет его замыкание на уровень «железа» — о чём дальнейшие соображения.
По смыслу формальный язык следующего уровня (если считать его не нулевым, а четвёртым) предполагает выход за рамки автономной системы, и как следствие должен быть предназначен для именования действий, организующих межсистемное взаимодействие. Но как уже было сказано, межсистемный обмен данными образует собственную иерархическую структуру — начиная с низкоуровневых протоколов оцифровки аналоговых сигналов, через простой и универсальный протокол TCP-IP, на котором держится работа всемирной паутины, ну и так далее — по мере расширения горизонтов применимости универсализация программных реализаций становится всё более затруднительной. Здесь просто следует отметить, что формализация сетевых сообщений имеет свою специфику и как следствие собственную терминологическую структуру — начиная с простых алгоритмов по оцифровке непрерывных сигналов с последующим поэтапным приведением этой «каши из битов» к виду, «съедобному» для «потребляющих» её программ, которые сами выстраиваются в иерархию по мере усложнения принципов организации обмена данными от низкого уровня к высокому. То есть вся эта информация — она должна идти параллельно с развитием языков формализации, при этом не оказывая на них существенного влияния и являясь по сути лишь одной из неопределённого множества категорий задач, посредством этих языков решаемых. А поскольку все эти вопросы должны решаться на уровне операционной системы, то и способы их формального отображения следует как-то распределить на первых трёх уровнях системной организации. Например, сам TCP-IP пишется на asm-e, программа преобразования посылок данных в объекты — на C++, поддержка адресной строки оформляется в виде макроса ОС, ну а макрос — понятие достаточно растяжимое в плане функциональности и ёмкости действий, оперирующих объектами, уже сконструированными в ОЗУ объектно-ориентированными средствами. Он может содержать вложенные макросы, выполняющие каждый по отдельности достаточно сложные действия — так что при дальнейшем углублении вложенности не возникает повода для введения принципиально новой информации.
Очередной уровень абстрагирования от конкретики (то есть физических процессов, локализованных в «железе») должен ещё на порядок повысить выразительную мощность понятийной базы — то есть этот переход должен быть качественным, а не количественно увеличивающим ресурсоёмкость сопоставленных ему абстракций. Перечисленным условиям удовлетворяет возможность автоматизации конструирования архитектуры произвольной сложности на физическом уровне на основании информационной модели, исчерпывающе описанной формальным языком. То есть рисуем в редакторе схему коммуникаций, определяем разъёмы ввода-вывода для подключения внешних устройств, выбираем прошивки для одного или нескольких процессоров, составляющих функциональное ядро системы, жмём большую кнопку «создать», после чего устройство, с которым работает эта программа (на данный момент гипотетическое), не хуже чем принтер электронные документы печатает электронные платы, упакованные в коробочку, внутри которой реализована требуемая архитектура. Подключаем внешние устройства, жмём «пуск», и всё работает — естественно при условии корректности написания и пригодности для совместной работы движков, «зашитых» в программную память управляющих процессоров. Классическая реализация предполагает вариант с одним процессором, который в числе прочих системных утилит крутит программу, способную загружать остальные движки ядра. Те в свою очередь могут использовать оставшиеся в наличии процессоры, если таковые предусмотрены в системе, согласно целевым задачам — будь то работа с конкретными устройствами, реализация многозадачности или организация распределённых вычислений. В принципе, допустим вариант с одним процессором, на котором держится вся система, хотя такой способ чреват серьёзной нагрузкой на быстродействие.
Сейчас эти подробности не столь важны, здесь я бы хотел акцентировать внимание на целесообразности выделения среди языковых абстракций замыкающего уровня такого термина как прошивка — то есть программа (статический код), составленная на машинном языке, не превышающая размером одной страницы памяти и предназначенная для выполнения одним процессором, а также реализующая некое целостное решение по управлению информационным процессом.
5. Арифметический и геометрический аспекты информационного процесса
Здесь я подведу промежуточные итоги.
Итак, разрядность R=16, зафиксированная в качестве фундаментального параметра информационной системы, совмещает пространственный (АЗУ) и временной (КП) аспекты информационного процесса, обеспечивая тем самым базовую совместимость функциональных устройств, и как следствие — упрощая построение формализованной модели различных процессов. Располагая такими начальными ресурсами, рекурсивным методом последовательно определяются три уровня системной организации (выделены в списках жирным), предполагающие отображение на иерархию языковых абстракций соответствующей выразительной мощности. С целью адаптации инженерной терминологии { двоичный разряд => ячейка => АЗУ => ОЗУ => ПЗУ } к лингвистической предлагаю для ассоциирования следующую последовательность: { буква => слово => страница => том => библиотека }
Последний уровень абстракции, замыкающий иерархию управления на нулевой (то есть опять же на физ.процессы, только с обратной стороны), выносится за рамки списка, поскольку рассмотрение его элементов имеет смысл лишь при наличии готовой архитектуры, на которой они могли бы выстраиваться — в то время как на уровне проектирования эту архитектуру лишь предстоит материализовать. С учётом приведённых выше соображений, этот способ состоит в определении схемы коммуникаций и выборе прошивок управляющих процессоров. Отсюда два принципиальных объекта формализации, первый из которых представлен информационными каналами для обмена данными и завязанными в них коммуникационными узлами, а второй — машинным кодом, квантованным элементарными действиями по преобразованию данных. Первый аспект можно назвать «геометрическим», поскольку его формальное отображение удобно представлять графически — в виде схем со стрелочками, указывающими направление циркулирования информационных потоков в системе, а также прямоугольничками и прочими условными обозначениями её функциональных составляющих. Второй, соответственно — «арифметическим», поскольку система команд процессора ориентирована на реализацию базовых математических вычислений. Также можно выделить критерий, на основании которого определяется принципиальная разница в их функциональном предназначении: если «геометрические» абстракции определяют способы организации пересылки данных между устройствами, то «математические» — способы их преобразования. При этом оба аспекта должны быть отображены в системе команд, то есть C = G U A, где C — командное пространство общим объёмом 65536 машинных инструкций, G — множество инструкций, инициирующих внешние механизмы обмена данными, A — команды внутренней адресации через регистры. Поскольку инструкции, составляющие множество G, абстрагируются от преобразования данных и по сути сводятся к команде MOV source —> destination, их формальное определение сводится к идентификации источника и приёмника. Но если для множества A все способы их идентификации заведомо в него включены, то команды из множества G, задействующие процессы по «обмену веществ с окружающей средой», не могут заведомо «знать», откуда и куда им придётся перекидывать данные, поскольку схема коммуникаций не отображена в составе АЛУ. Чтобы такая возможность было доступной, эту схему придётся полагать фиксированной на уровне проектирования системы — что лишит процесс проектирования гибкости, а дальнейшее рассмотрение этого вопроса смысла. Если же функциональность команд из группы G полагать заведомо неопределимой, и с учётом того что суть их действия всегда сводится к перегонке данных от некого источника к некому приёмнику, то нагрузкой на командное пространство для множества G можно пренебречь и свести его к одному элементу — единственной команде без параметров (назову её банально CMD). При этом информацию о том, что именно и куда именно следует пересылать, можно будет черпать из регистров, ссылающихся на произвольные участки АЗУ и ОЗУ — так что здесь нет принципиальных ограничений на объём и способ структурирования целевых данных, которыми манипулирует CMD. В предложенном случае информация об этом способе перекладывается на аппаратную реализацию той части процессора, которая отвечает за внешние коммуникации и как следствие — является составной частью их схемы. Теперь КП почти целиком принадлежит множеству A, а задачи формального определения системы машинных инструкций и принципов организации управления данными ничего теперь не мешает решать независимо друг от друга.
Подытожу итоги: зная схему коммуникаций и чем прошита командная память ведущих процессоров, а также предполагая корректность работы каждой прошивки по отдельности и надёжность совместного их функционирования, можно дать корректное и исчерпывающее формальное описание работы системы в целом.
6. АЛУ
Геометрический аспект оставляю пока в стороне, предполагая что и для него можно определить необходимый и достаточный минимум выразительных средств, оптимально согласующийся с представлениями о низком уровне программирования. Предмет же моих насущных интересов состоит в нахождении наиболее эффективного способа распределения КП — то есть задача заключается в том, чтобы определить 16-разрядную систему машинных инструкций, наилучшим образом коррелирующую с принципом необходимости и достаточности («есть всё что нужно и нет ничего лишнего»). На первом шаге эту корреляцию можно проследить чётко и однозначно, определив 5 необходимых и достаточных сущностей, обеспечивающих полноценное функционирование программы:
1. PC (Program Counter — программный счётчик) — отражает принцип пошагового выполнения программы в направлении увеличения адреса исполняемой команды. Вот стандартная последовательность действий с программным счётчиком: отправляем АЛУ на «съедение» команду, код которой содержится в ячейке АЗУ, на которую указывает PC; после чего инкрементируем PC (теперь он указывает на следующее после кода выполняемой в данный момент инструкции слово адресуемой памяти).
2. SP (Stack Pointer — указатель на вершину стека) — отражает принцип построения программы, предполагающий её оформление в виде иерархической структуры подпрограмм. Как следствие, должен запоминаться адрес возврата в то место, откуда подпрограмма была вызвана, а поскольку эти вызовы могут быть вложенными, необходимо отвести какой-то участок памяти под хранение всех этих адресов. Отсюда — организация стека по принципу LIFO (акроним Last In, First Out, «последним пришёл — первым ушёл») и два принципиальных способа его использования: «заталкивание» текущего адреса в стек ( PC —> (-SP) ) и «выталкивание» его оттуда обратно в программный счётчик ( (SP+) —> PC ). Возможны и другие способы использования стека, но эти два действия являются необходимыми для обеспечения возможности структурирования программы классическими средствами. Примечание: здесь и в дальнейшем знаки преддекремента (-) и постинткремента (+) будут ставиться непосредственно перед или после того, к чему они применяются — дальше по тексту будут приводится различные типы адресации, среди которых встречаются случаи неоднозначного их интерпретирования при использовании мнемоники асемблера процессоров PDP-11, который при первом рассмотрении будет полезен в качестве базового прототипа.
3. STATUS — содержит флаги, необходимые для условных переходов. Условие — оно либо соблюдается, либо не соблюдается, и для того чтобы это определить, нужны флаги состояния. Обязательных флага, отражающих результаты простейших математических действий, всего три: ноль/не ноль, положительный/отрицательный, есть перенос/нет переноса (последний нужен чтобы не терялись биты, вылезающие за пределы ячейки памяти). Для совместимости с 16-разрядной архитектурой регистр статуса конечно же удобно расширить до 16-ти флагов, которым будет несложно в дальнейшем найти применение, однако на первом этапе формализации я выделяю элементы, соответственно, лишь первой степени необходимости. Сюда ещё нужно добавить упомянутые выше два флага, путём переключения которых можно адресовать помимо памяти кода дополнительное АЗУ — страницу данных. Остальные 11, распределение функций которых требует больше фантазии чем это допустимо на первом шаге формализации, выносятся пока за рамки рассмотрения.
4. R-егистры общего назначения — играют роль локальных переменных, обеспечивающих насущные потребности выполняемой в данный момент подпрограммы. Могут также передавать значения между подпрограммами, взаимодействовать со стеком, адресовать память, одним словом, в отличии от первых трёх — неспециализированы. Над их количеством долго задумываться не приходится: 16 штук оптимально не только по идеологическим соображениям, но и потому что имея некоторый опыт программирования на асме могу сказать, что 8 мало (как правило эти регистры в программе «нарасхват», поэтому при таком их количестве много кода уходит на временную пересылку их значений в стек и обратно), в то время как 32 регистра для локальных функций — это уже излишество (хранить значения переменных можно ведь и в АЗУ). Далее для краткости регистры общего назначения будут обозначаться РОН-ами, а специального — соответственно РСН-ами.
5. Interrupt — прерывания от внешних устройств. Поскольку определяемая система команд абстрагируется от внешних информационных потоков, о прерывании здесь достаточно знать, что оно может произойти в любой момент, необходима подпрограмма для его обработки (соответственно — указание адреса, с которого она начинается), а также возможность возврата в то место, на котором основная программа была прервана. Чтобы не плодить сущности сверх необходимости и не забивать память избыточными данными (например — таблицей, в которой хранятся адреса обработки для каждого из прерываний), принимаем нулевой адрес за начало обработки любого прерывания, а в стек, помимо адреса возврата и текущего значения регистра статуса, помещаем информацию о самом прерывании. Если этой информации недостаточно для определения необходимых действий по обработке прерывания, дополнительная информация может быть загружена в адресуемую память посредством использования команд(-ы) из множества G. Увеличение нагрузки на быстродействие в сравнении со способом, использующим таблицу адресов, несущественно — просто в обработчик прерываний, расположенный по нулевому адресу, придётся добавить некоторое количество проверок и условных переходов, организующих ветвление по номеру прерывания, помещённого в момент его возникновения на вершину стека. Сами проверки можно расположить в порядке приоритетности проверяемых номеров — так чтобы прерывания с наибольшей частотой отвлечения процессора и требующие обработки в первую очередь проверялись соответственно первыми. Эти незначительные издержки на быстродействие с избытком компенсируются свойством монолитности страницы АЗУ, содержащей исполняемый код — что хорошо сказывается на цельности программной реализации. К тому же, все внутренние информационные потоки будут сосредоточены в трёх специализированных регистрах и R регистрах общего назначения — чего необходимо и достаточно для обеспечения базовой функциональности низкоуровневых средств программирования. Так архитектуру процессора легче всего удержать в голове, а память может быть распределена каким угодно образом — лишь бы с нулевого адреса шёл исполняемый код, а не данные, ну и ещё программа должна корректно обрабатывать прерывания в случае если таковые предусмотрены. На момент запуска процессора все регистры обнуляются, PC инкрементируется после отправления на АЛУ очередной команды, а SP декрементируется перед засылкой очередного слова данных в стек — так что при исходных нулевых значениях между стеком и машинным кодом программы никаких накладок возникать не должно.
На этом первый шаг формализации можно считать завершённым, и следующим я попытаюсь дать представление о том, сколько «весит» мегабит КП. Если вам знакома архитектура процессоров PDP-11, дальнейшие расчёты будет проще визуализировать. В древние времена у программистов они были в ходу, а потом их вытеснила враждебная идеология Intel-овских движков, и моему стремлению раскопать эту тему с первоначал во многом способствовала ностальгия по тем временам, когда количество «дырок» в организации компьютерной архитектуры было ещё терпимым. С этих позиций Intel-овский асемблер можно считать образцово-показательным примером того, как делать не нужно, поскольку ключевые несоответствия с предлагаемым здесь подходом прослеживаются при первом ознакомлении с его мнемоникой. Например, в двухоперандной команде должен указываться сначала источник, а потом приёмник — то есть в естественной последовательности, отражающей причинно-следственные связи между физическими процессами, происходящими в «железе». Также машинные инструкции процессора Intel не используют в явном виде PC — то есть налицо явные признаки CISC-ориентированной идеологии, стремящейся «скрыть за ширмой» аппаратную реализацию машинных инструкций. Короче говоря, RISC-и рулят, а СISC-и гоухом.
В следующем параграфе показатель субъективизма в моих рассуждениях несколько возрастёт, и они станут более ориентированными на энтузиастов в области низкоуровневого программирования.
6. Система машинных инструкций
Итак, в нашем распоряжении 3 специальных и 16 общих регистров, к которым применимо некоторое множество способов адресации памяти. В исходном состоянии память принимается чистой и «незапятнанной» специализированной информацией, требующей введения каких-то иных сущностей помимо перечисленных на первом шаге итерации. Процессор стартует с нулевого адреса страницы кода, последовательно выполняет команды, совершает условные и безусловные переходы, вызывает подпрограммы, и наконец — отвлекается на прерывания (последние допустимо вообще никак не отображаться на множество А, поскольку алгоритм возврата из прерывания можно позаимствовать от обычного способа завершения подпрограммы командой RET, сводящийся к восстановлению PC и STATUS-а из стека, чтобы программа смогла корректно продолжить свою работу).
Чтобы проще было «взвесить» навскидку КП, обратим внимание на то обстоятельство, что большую часть его объёма составляют машинные инструкции с двумя операндами (трёхоперанндные можно заведомо исключить, так как они явно не вписываются в страницу КП, да и не очень-то согласуются с представлением о низком уровне). Три бита из 16-ти «откусываются» сразу на указание номера команды, чтобы вместить классическую семёрку { MOV, CMP, ADD, SUB, AND, OR, XOR }, потеря любой которых была бы чревата значительным упущением возможностей, в то время как в добавлении к этому списку новых команд уже нет насущной потребности, поскольку через них можно сделать всё остальное. Умножение и деление реализуются алгоритмически и в отличии от перечисленных команд аппаратный способ их реализации на мультиплексорах является элементарным. Данное требование целесообразно зачислить в список принятых по умолчанию решений (скажем — галочкой на опции «trivial instructions only» ): все команды, составляющие КП, должны быть не сложнее операций сложения/вычитания. На уровне проектирования информационных систем усложнять процессор нет смысла, поскольку с учётом возможности собрать его, в идеале, по атомам, все проблемы по распределению вычислительной нагрузки можно решить путём регулирования взаимного расположения процессоров в пространстве, где скорость света будет единственным ограничителем скорости их взаимодействия. Как следствие, достижение требуемого быстродействия будет предпочтительнее осуществить путём увеличения числа процессоров и организации их эффективного взаимодействия программными средствами, при том что каждый процессор по отдельности в силу простоты своего устройства способен будет обеспечить достаточно высокие показатели быстродействия. Так что идеология RISC предлагаемому подходу будет явно дружественной — тем более что места в командном пространстве и так впритык. И наконец, главное удобство от принятия соглашения о тривиальности составляющих АЛУ машинных инструкций состоит в выравнивании действий по сложности на фундаментальном уровне проектирования информационных систем — в таком случае термин «язык низкого уровня» искомому решению будет в полной мере соответствовать.
Дальнейшая оценка информационной ёмкости КП будет опираться на представление о том, что 7/8 его объёма занимают инструкции с двумя операндами, один из которых представлен в коде команды полем адресации источника, а второй — полем адресации приёмника. Поле адресации должно содержать исчерпывающую информацию о регистре, через который производится адресация, а также о самом способе адресации. Регистров всего 19, то есть на указание номера регистра требуется по усреднённой оценке log2(19) бит. Три бита ушло на указание номера команды, остальные 13 дают два поля адресации по 6.5 бит на каждое, так что навскидку имеем 2 ^ [ 6.5 — log2(19) ] ~ 4.8 видов адресации на операнд. АЛУ процессоров PDP-11 использует 3 бита на указание номера адресации и столько же на номер регистра — то есть включает 8 регистров и 8 способов адресации через них, при том что PC и SP входят в число РОН-ов. Последнее свойство полезно тем, что позволяет сделать систему команд ортогональной, и как следствие — даёт возможность отдельно запоминать команды, и отдельно — методы доступа к операндам (чем собственно и привлекали в своё время эти процессоры программистов). В данном же случае такой подход неприемлем по двум взаимодополняющим причинам:
1. Для специализированных регистров (главным образом для PC) многие способы адресации неприменимы, поскольку ведут либо к гарантированному зависанию ( (-PC) ), либо к сбою программы вследствие выполнения данных вместо кода ( (PC) ). Как следствие, КП содержит инструкции, коих немалое число, формально допустимые, но на практике неприменимые. Прямой адресации РСН-ов вообще следует избегать — с точки зрения адресации в этом состоит их принципиальное отличие от РОН-ов, для которых прямая адресация считается естественной, в то время как использование РСН-ов с целью временного хранения значений по понятным причинам неприемлемо. То есть для PC и SP началом отсчёта является косвенная адресация, а не прямая, ведь прежде всего нас интересует не само значение этих регистров, а содержимое ячейки, на которую оно ссылается. Сказанное не означает полного исключения из АЛУ возможности прямой адресации РСН-ов, и если программисту потребуется, скажем, восстановить стек напрямую или перепрыгнуть дальше, чем это позволяют локальные переходы, то для подобных целей должны быть предусмотрены соответствующие инструкции. Разница состоит в том, что эти инструкции будут проходить в системе команд не на общих основаниях, а в виде небольшого числа специализированных («поштучных») команд, дабы не нагружать КП и исключить практически малоприменимые способы адресации. Возьмём, к примеру, применение к PC и SP побитовых операций AND, OR и XOR — гипотетически такая возможность допустима, но потребуется немало фантазии, чтобы придумать ситуацию, когда такая необходимость возникнет (а если и возникнет, то из этого будет следовать лишь то, что придётся сделать то же самое двумя командами вместо одной). Таким образом, использование в программе РСН-ов имеет свою специфику, в соответствии с которой должно определяться множество допустимых действий над каждым из них. Есть и другие принципиальные отличия от набора команд PDP-11, из-за которых дальнейшее его сопоставление с определяемой здесь реализацией АЛУ будет менее показательным.
2. Вторая причина, как уже было сказано, состоит в том, что в КП слишком мало места, чтобы можно было позволить себе так им разбрасываться, как это было описано в предыдущем пункте — в связи с чем ортогональный набор команд придётся отменить. Увеличение числа РОН-ов с 6-ти (PDP-11) до 16-ти привело к урезанию числа способов адресации с 8 до 4.8 в среднем на операнд, вследствие чего адресация стала «узким местом» 16-разрядного АЛУ, использующего в общей сложности 19 регистров. Например, для РОН-ов в командное пространство помещается лишь 4 стандартных типа адресации: R, (R ), (R+), (-R). В принципе, в большем их числе особой потребности и не возникает, поскольку двойная косвенная адресация (взятие адреса по адресу) по вышеупомянутым причинам актуальна лишь для PC и SP. А про остальные, включённые в состав 8-ми PDP-шных адресаций, можно сказать, что они бы не помешали, но если их исключить, то это не будет большой потерей ввиду нетипичности фигурирования РОН-ов в других типах адресации. Вообще говоря, для PC применимы лишь два способа адресации: (PC+) и &(PC+) (первый называется «косвенная автоинкрементная», второй — «двойная косвенная автоинкрементная»). Постинкремент необходим для того, чтобы перепрыгнуть через слово данных после его взятия АЛУ на обработку — во избежание выполнения случайной команды, предназначенной для выполнения следующей. Как следствие, первый способ адресации по смыслу означает взятие константы — значения, которое находится в момент исполнения команды в ячейке, расположенное в АЗУ следом за кодом этой команды (назовём эту ячейку «текущей»), а второй — обращение к переменной (на этот раз в текущей ячейке содержится адрес той ячейки, где хранится значение переменной — отсюда двойная косвенность). Использование первого вида адресации имеет смысл лишь применительно к источнику, поскольку данные обычно пересылаются из константы, а не наоборот — в константу. В принципе, ничего не мешает записать какие-то данные по текущему адресу, но смысл это имеет лишь в том случае, если предполагается дальнейшее их использование — для чего необходимо знать адрес текущей ячейки. Получается что константа в данном случае превращается в переменную, которую можно было бы хранить и в любой другой ячейке — то есть на приёмнике данный способ адресации будет избыточным, и при скурпулёзном отборе адресаций согласно критерию полезности его следует исключить. Таким образом, если PC помимо этих двух способов (формально их даже не два, а полтора) нет смысла адресовать как-то по-другому, то для РОН-ов большее количество адресаций просто не помещается в адресное пространство, и если оставаться в рамках классической реализации АЛУ, то можно считать эти способы необходимыми и достаточными.
Зато на SP и STATUS в командном пространстве остаётся достаточно места, чтобы обеспечить более широкие возможности их адресации. Стек обычно используется в программе достаточно часто, чтобы уделить ему внимание и сопоставить более солидное множество адресаций. В текущей реализации это множество содержит 16 видов адресации, половину из которого составляют адресации со смещением { N(SP), N+(SP), -N(SP), &N(SP), &N+(SP), &-N(SP), &N(SP)+, &N-(SP) }, где N — байт смещения, под который отведена половина регистра статуса — то есть величина смещения от вершины стека не может превышать 255. Зато в отличии от системы команд PDP-11 здесь N является переменной, а не константой — что позволяет применять к нему автоинкремент/автодекремент и доставать значения из разных мест стека, оставляя нетронутой его вершину. Как правило нас интересуют данные, расположенные не выше вершины стека (собственно, занесённые в стек, а не вышележащие, которые обычно считаются уже отработанными), следовательно включать в диапазон величины смещения отрицательные значения нецелесообразно. Остальная половина адресаций выглядит следующим образом: { (SP), (+SP-), (-SP+), &(SP), &(SP+), &(SP-), &(SP)+, &-(SP) }. Во избежание путаницы в интерпретации второго и третьего элементов в списке отмечу, что это одно из немногих исключений асимметрии адресаций для источника и приёмника (здесь для источника предполагается пост-инкремент/декремент, в то время как для приёмника — пред-. Последние две адресации, как и в предыдущей восьмёрке, предполагают использование целевой ячейки стека в качестве счётчика, инициализированного начальным адресом массива памяти. Элементы списка подбирались по принципу наименьшей надуманности и наибольшей полезности, и на данный момент мне эта подборка, количество элементов которой подогнано под «круглое» число 16, видится мне оптимальной.
Регистр статуса оказался наиболее продуктивным в плане разнообразия способов адресации. Задаваясь вопросом о том, как наиболее эффективно распорядиться остальными 11-ю битами, я пришёл к выводу, что лучше всего разбить STATUS на два байта, один из которых будет содержать флаги специального назначения, а второй — общего (по аналогии с базовой классификацией регистров). Каждый из байтов делится в свою очередь на два полубайта. Первый специализированный полубайт содержит 4 арифметических флага, 3 из которых описаны выше, а второй полубайт — 4 «геометрических» флага, 2 из которых отведены под переключатели адресуемых страниц памяти. Арифметические флаги можно «добить» флагом чётности, проверяющим младший бит результата (для симметрии со знаковым флагом, проверяющим старший разряд), а предназначение двух оставшихся «геометрических» флагов зарезервированы под функции внешних коммуникаций, которые пока не определены. Флаги специального назначения (ФСН-ы) выношу за рамки дальнейшего рассмотрения — с точки зрения адресации здесь прежде всего интересны ФОН-ы, точнее — байт общего назначения, один из примеров использования которого приведён выше (там где про адресацию SP со смещением). Обозначив полубайты переменными Bx и By, представлю начальный список адресаций регистра статуса: { B, Bx, By } — то есть к байту в целом и к полубайтам по отдельности применим прямой способ адресации, можно хранить там значения и преобразовывать их на общих основаниях. Но главная цель дальнейшей фрагментации регистра статуса состояла в том, чтобы хранить в полубайтах номер регистра, и как следствие — обеспечить возможность индексации РОН-ов: { Rx, Ry, (Rx), (Ry) }. Пример: MOV (Rx),(-SP) — переслать в стек значение переменной, адрес которой помещён в регистр, номер которого указан в Bx. Теперь в РОН-ах можно хранить короткие массивы, ссылаться на них по значению, ну и вообще — образуется дополнительный этаж в иерархии доступа к памяти: 4-разрядный мини-регистр указывает на 2^4=16-разрядный РОН (слово), а тот в свою очередь может ссылаться на ячейку 2^16=65536-словного АЗУ (страница). Весьма полезный и перспективный наворот, расширяющий возможности структурирования процессов обработки данных, а также нетребовательный к месту в КП. Ещё одна возможность, которой я тоже думаю не стоит пренебрегать, состоит в привязке полубайтов общего назначения к арифметическим флагам (фигурирующими далее под индексами z, n, c, p): { Rz, (Rz), Rn, (Rn), Rc, (Rc), Rp, (Rp), Bz, Bn, Bc, Bp }. Пример: MOV (SP+),(Rn) — взять с вершины стека значение и переслать его в ячейку, адрес которой указан в регистре, номер которого в зависимости от состояния флага n находится либо в Bx, либо в By (остальные действия в списке можно определить по аналогии). Предложенный способ использования полубайтов статуса существенно повышает выразительную мощность составляющих командное пространство абстракций, при том что аппаратная реализация сопоставленных им действий не выходит за рамки тривиальности, полагаемой необходимым условием соответствия идеологии RISC. Здесь, к слову, пригодилось вышеупомянутое преимущество числа 16, как «самого круглого» (для которого соблюдается условие R % N = 0): номер 16-разрядного регистра можно разместить в другом 16-разрядном регистре (в данном случае — в регистре статуса) аккурат 4 раза.
На данном этапе определения АЛУ командное пространство поделено на 8 равных частей, из которых 7 закреплены за двухоперандными командами, а 8-я отведена под остальные. Следующей по ёмкости идёт группа инструкций, осуществляющих локальные переходы и включающих в свой код величину смещения относительно места их выполнения. Отводить под смещение меньше байта нет смысла, а больше не получится по всё той же банальной причине нехватки места. От оставшихся 13 бит «откусываем» байт смещения, и остаётся 5 бит для указания номера команды — то есть их не может быть больше 32-х. И ещё как минимум необходимо выделить место под однооперандные команды. Много там не «откусишь» (обойтись менее чем 24-мя командами из этой группы сложновато), но поскольку однооперандные команды «весят» в 2 раза меньше в силу того что им требуется 7, а не 8 бит на представление операнда, то из 8-ми команд ветвления можно сделать 16 однооперандных — чего в условиях жёсткой экономии командных ресурсов вполне хватает. Данные показатели близки к PDP-шным, ну и опыт выкладывания мозаики согласно заданным в условии этой задачки критериям показывает, что такие пропорции оптимальны и устойчивы, а именно: 7/8 командного пространства занимают двухоперандные команды, а оставшуюся 8-ю часть делят между собой однооперандные команды и локальные переходы в соотношении объёмов 1: 3. Полная система машинных инструкций АЛУ не исчерпывается этими тремя группами команд, но они являются наиболее ходовыми в программе, а с точки зрения нагрузки на командное пространство — определяющими. Для остальных же (безоперандных, флаговых и прочих, возможно не вписывающихся в общий формат команд) предполагается выделение места из остатков, нетронутых таблицами адресаций трёх базовых множеств машинных инструкций. Таким образом, на втором шаге итерации получаем 7 + 16 + 24 + { примерно столько же дополнительных } команд, составляющих полную реализацию АЛУ. Однозначный их состав определён лишь в первой группе в виде «классической семёрки»; состав второй группы предполагает включение в неё с десятка стандартных команд, встречающихся практически в любых реализациях АЛУ, и ещё нескольких полезных на уровне обеспечения базовых средств низкоуровневого программирования (скажем — перестановка байтов в слове); третья группа содержит команды ветвления и организации циклов в диапазоне байта смещения; остальные обеспечивают дополнительные удобства и гибкость написания машинного кода. При этом одна команда может содержать в себе от 1 (безоперандные) до 8192 (двухоперандные) машинных инструкций.
Ещё не помешало бы снабдить процессор дополнительными регистрами — такими как TMС (счётчик системного таймера), TMR (регистр предустановки системного таймера, задающий период отсчёта), PTI (порт ввода), PTO (порт вывода) и CFG (регистр системной конфигурации, содержащий предделители для таймера, регулирующие его скорость отсчёта; флаг разрешения прерывания от таймера для обеспечения возможности тактирования процессов; также могут оказаться полезными флаги, обеспечивающие синхронизацию работы таймера с состоянием портов ввода и вывода). В таком случае придётся добавить в множество G некоторое количество инструкций, осуществляющих обмен данными между основными и только что перечисленными дополнительными регистрами.
На этом, пожалуй, всё — если не выходить далеко за рамки насущно необходимых для обеспечении базовой функциональности процессора элементов. Как можно убедиться из приведённых расчётов, для обеспечения приемлемой реализации низкоуровневых (RISC-ориентированных) средств программирования необходимо и достаточно 16-разрядного КП, и в стремлении соблюсти первую часть условия задачи («есть всё необходимое») на вторую («нет ничего лишнего») в КП почти не остаётся места. Так что по сути вопрос сводился к распределению по группам команд и адресаций с предварительным их выделением и последующим согласованием с вместимостью страницы данных.
7. Итоги
Приведённые рассуждения ориентированы на поиск наиболее эффективных выразительных средств для описания формализованной модели информационного процесса — так чтобы её легко можно было легко удержать в голове хотя бы на уровне состыковки физических процессов с логическими абстракциями. Принципиальный способ это сделать состоит в фиксации двух ключевых моментов:
1. Принимаем разрядность фундаментальной константой, призванной совместить пространственный и временной аспекты информационного процесса, через которую рекурсивно определяем трёхуровневую архитектуру, пространственный аспект которой задаёт ёмкость единицы памяти на каждом из уровней { слово (ячейка) => страница (АЗУ) => том (ОЗУ) }, а временной — сложность процессов, инициируемых распределёнными по этим уровням управляющими действиями { машинная инструкция (команда) => управляющая прошивка (программа) => интегрированная система (собственно, компьютер) }
2. Исходим из представления о принципиальной возможности исчерпывающего описания этой архитектуры выразительными средствами двух непересекающихся абстрактных языков, первый из которых (система обозначений информационных каналов и узлов, или «геометрический язык») способствует исчерпывающему определению способа взаимодействия между её функциональными составляющими, а второй (система машинных инструкций, или «арифметический язык») предоставляет формальную базу для разработки алгоритма функционирования системы на низком уровне.
Формально оба языка должны пересекаться в одном КП — то есть команды управления внешними коммуникациями должны быть включены в число обрабатываемых АЛУ. Но реально последние могут быть представлены в системе команд единственной инструкцией, результат выполнения которой зависит от содержимого регистров, в которые предварительно заносится информация об источнике и приёмнике, объёме пересылаемой информации, а также сопутствующие данные, определяющие характер информационного обмена. Много места эта информация не потребует: 2 регистра способны вместить номер любого из 4-х миллиардов компьютеров, ещё 2 — номер любого из 4-х миллиардов файлов на этом компьютере, следующие 2 — указать номер любой ячейки в файле размером до 4-х гигаслов. То есть, для однозначной идентификации источника и приёмника (скажем, в пределах всемирной паутины и с точностью до адреса конкретного слова данных, хранящегося в памяти конкретного компьютера) понадобится 6*2 = 12 регистров. Ещё 2 потребуется на то чтобы указать объём пересылаемой информации, после чего останется 2 на размещение уточняющих данных — так что в 16 регистров вполне можно уложиться. Если же информации для исчерпывающего описания выполняемых действий потребуется больше, то ничего не мешает использовать часть регистров в качестве указателей на блоки памяти, размещённые в АЗУ.
Таким образом, все инструкции КП, кроме одной и возможно ещё незначительного числа, целиком абстрагируются от внешних коммуникаций, так что их «зона видимости» исчерпывается 16+3 регистрами и двумя страницами АЗУ — благодаря чему становится возможной разработка «арифметического» и «геометрического» языков независимо друг от друга. При первом рассмотрении (по умолчанию) на опциях «моноканальность» и «монокомандность» можно поставить галочки, подразумевая при этом, что весь исполняемый код пишется на одной версии асемблера, а все принципы внешних коммуникаций, проецируемые на содержимое РОН-ов перед вызовом CMD, прописаны одним списком соглашений. На уровне проектирования системы снятие первой галочки просто означало бы возможность выбора АЛУ для каждого процессора из списка альтернативных версий — лишь бы в состав всех этих АЛУ была включена инструкция CMD и 16 РОН-ов, способных вместить информацию, необходимую для исчерпывающего описания всех видов коммуникаций, поддерживаемых аппаратной («геометрической») составляющей моделируемого процесса. То есть вопрос об унификация АЛУ, как способа описания программной («арифметической») составляющей, менее принципиален, поскольку в отличии от процессоров и прилегающих к ним низкоуровневым средствам формализации этого процесса, схема коммуникаций из соображений архитектурной целостности должна быть представлена в системе лишь в единственном числе. На уровне проектирования завершённых архитектурных решений этот момент отражает естественную последовательность действий при отключении опций «моноканальность» и «монокомандность»: создавая новый проект выбираем способ описания схемы коммуникаций (задающий формат файла, содержащего исчерпывающее описание информационной модели), а по мере добавления в эту схему новых процессоров определяем для каждого из них систему команд из списка доступных реализаций асемблера.
Разработка «геометрических» средств формализации двоичных технологий имеет свою специфику, отличную от «арифметических». Если множество машинных инструкций должно уложиться в конкретный объём КП (и по сути высказанные в предыдущем параграфе соображения были призваны убедиться в том, что 16-ти бит, хоть и впритык, но хватает для размещения там полноценного RISC-ориентированного асм-а на 16-ти РОН-ах), то в отношении «геометрического» аспекта не существует объективных ограничений на количество составляющих его языковых единиц. Тем не менее, это количество тоже должно стремиться к необходимому минимуму, и принцип «есть всё нужно, нет ничего лишнего» здесь не менее актуален, поскольку формальная база, на которой предполагается выстраивание следующих этажей формализации информационного процесса, должна удовлетворять требованиям компактности и легкоусвояемости. Полагаю, что в этой области тоже можно путём нехитрого анализа найти классическое решение, которое при минимуме информационных затрат и ограничений возможностей построения формализованной модели информационного процесса обеспечило бы надёжность функционирования и удобство проектирования создаваемой на базе этой модели архитектуры.
Автоматизация физического конструирования информационных систем является предположительно следующим шагом на пути развития информационных технологий, а сложность системы, обеспечивающей такие возможности, примерно на порядок превышает текущие достижения. В силу этих причин становится значительной помехой избыточная информация, предназначенная для решения проблем совместимости на всех уровнях организации системной архитектуры — как это водится при стихийном развитии языков и уровней формализации информационных процессов. Здесь я пытался расставить лишь ключевые акценты, варьируя в некоторых пределах степенью жёсткости принимаемых ограничений — так чтобы убирая в обратном порядке проставленные в «опциях по умолчанию» галочки (собственно, это «двоичность», «моноразрядность», «моноканальность», «монокомандность» и «RISC-ориентированная архитектура»), можно было бы вернуться в корневую директорию «/Логика».
- LG G6: большой тест — обзор
- 11 секретов профессиональных фотографов из National Geogaphic
- МЭСМ – наша точка отсчёта
- Вольный обзор языков программирования
- Java — великий и могучий
- Репортажная зеркальная цифровая фотокамера Nikon D500. Новый флагман для искушенных.
- Учимся пилотировать квадрокоптер. Часть 2
Секреты машинного кода
от: 02-10-2016 22:38 | раздел: Soft / Программирование
В одной из прошлых статей мы поговорили о языках программирования высокого уровня. Сегодня мы опустимся гораздо ниже — в самые недра программных систем и попробуем разобраться в том, на каком «языке» говорит с пользователями и программистами сама ЭВМ.
Двоичный код
Мы часто вкладываем в это понятие самые различные смыслы. «Двоичный код», «машинный код», «программный код». Между тем, значение этих терминов различно. А вот использование в машинной математике двоичного кода очевидно и вполне логично.
Цифровая электроника работает с двумя типами электрических сигналов, или, если точнее, с двумя «состояниями» — высоким уровнем сигнала в его источнике либо передатчике и низким его уровнем. Высокий уровень сигнала — это «ноль», низкий — «единица». Это наиболее надежная схема передачи сигналов, которая дает стабильный результат. Какие бы помехи не возникали на пути сигнала, мы всегда поймем, где «единица», а где — «ноль».
На такую схему великолепно ложится двоичная система счисления. Справедливости ради стоит отметить, что впервые такая система была описана в древнекитайской «книге Перемен». В дальнейшем двоичная система счисления только обретала теоретическую основу. Математик Лейбниц, например, доказал, что основой системы счисления может быть любое число, кроме нуля. Были, а, возможно и будут и другие теоретические работы в этом направлении. Сейчас используются, как минимум, три системы счисления — двоичная, восьмеричная, шестнадцатиричная. Но нас интересует именно двоичная.
Как работает «двоичный код»
С помощью этого кода можно легко перевести десятичное число в понятное компьютеру двоичное. Возьмем число (например, 11001). Пронумеруем его с конца, причем, нумерацию начнем с нуля. Порядковый номер каждой цифры станет степенью, в которую мы возведем основание системы счисления (2), затем перемножим результат с цифрой. Нужно провести эту операцию с каждой цифрой двоичного числа, затем суммировать результат:
1*(2^4)+1*(2^3)+10*(2^2)+0*(2^1)+1*(2^0)
16+8+0+0+2 = 25
Теперь попробуем перевести число 25 обратно, в двоичный код. Используем для этого простейшую арифметическую операцию — получение остатка от деления.
25/2 = 1
12/2 = 0
6/2 = 0
3/2 = 1
1/2 = 1
В результатах, снизу вверх, читаем наше число.
Таким образом считать довольно легко, причем, даже на «пальцах». У нас ведь на руке пять пальцев? Если каждому присвоить единицу или ноль (ноль – палец согнут, единица – выпрямлен) и прикинуть значение основания системы, возведенной в нужную степень, то можно считать от нуля и до 1023. Правда, в случае с числом 25 жест руки будет немного неприличным.
Для любителей посчитать есть сегодня любые инструменты — начиная от калькулятора и заканчивая таблицами разрядности.
С дробными числами расчет ведется по иному. Всем цифрам до «запятой», степень присваивается в обратном порядке, от нуля, как и обычно, а вот числа после «запятой» сопровождаются отрицательной степенью, причем, она уменьшается от запятой. Вот как это выглядит:
Число:
1101,101
1*(2^3)+1*(2^2)+0*(2^1)+1*(2^0)+1*(2^-1)+0*(2^-2)+1*(2^-3)
8+4+0+1 + 0,5+0+0,125 = 13,625
В остальном, операция выполняется также.
Как считает компьютер?
Компьютер не может, допустим, приписать «минус» к двоичному числу и считать его отрицательным. Этот минус нужно где-то поместить. Простейший способ — снабдить число дополнительным знаком (в восьмиразрядной ячейке, в этом случае мы сможем поместить семиразрядное двоичное число, где дополнительный разряд будет означать знак — единица отрицательный, ноль — положительный). Это называется «прямым кодом».
0111010
0111010
Но отрицательные числа лучше представлять по-другому — для того, чтобы компьютер мог с ними работать. «Единица» в крайнем разряде по-прежнему будет выполнять роль «маркера» отрицательного числа. Но остальные разряды требуется инвертировать (то есть, разряды с нулями заменить единицей и наоборот). На месте остается только маркер.
1000101
Теперь к числу прибавляем единицу:
1 1000110
Для чего же вся эта малопонятная морока? – спросит читатель. Для того, чтобы ЭВМ смогла выполнять операции с числами.
0 0111010+0 0111010 = 1110100 (58+58 = 116)
Знаковые разряды в этом случае просто не учитываются.
Но если число отрицательное — все меняется. Переводим его в дополнительный или обратный код (инвертируем все разряды кроме знакового, затем прибавляем единицу) и складываем снова, затем из результата вычитаем единицу и инвертируем его:
1 1000110+1 1000110 = 1 0001011 = 1 1110100 (-116)
Вычитание работает по тому же принципу, умножение — путем последовательного сложения (я очень сильно упрощаю но принцип именно таков). Что касается операции деления, то она реализуется путем неоднократного сложения делимого с дополнительным кодом делителя до получения необходимого результата.
Подробно эти действия описывать не буду. Расскажу лишь про арифметический сдвиг. Это ещё одна «хитрость», которая позволяет упростить вычисления. Если сдвинуть двоичное число вправо или влево, то оно меняется, согласно определенным правилам.
1111101 = 125
Сдвиг вправо (слева добавляем единицу, последний разряд убираем):
1111110 = 126
Ещё один сдвиг вправо:
1111111 = 127
С дополнительным кодом — ещё интереснее. Сдвиг числа влево дает умножение на 2, вправо – деление на 2. Это очень удобно для умножения и деления целых чисел на числа, равные степени 2 (2, 4, 8, 16, 32, 64). Существует множество вариантов сдвига — арифметический (мы его выполнили), циклический, логический и т.д. Каждый выполняет свою задачу.
Добавлю, что всё, о чем я говорил — это «азы» компьютерных вычислений. Когда-то они выполнялись компьютером именно так. Сегодня они ведутся уже на ином уровне. Персональный компьютер выполняет расчеты при помощи встроенного в ядро основного ЦПУ математического сопроцессора, где каждая операция выполняется одной командой, что значительно ускоряет работу. Математический сопроцессор оптимизирован именно для вычислений, эти функции заложены в него на аппаратном уровне. Но описание работы сопроцессора — это тема для отдельной статьи или даже целой книги.
Кодируем… двоичный код
Сейчас мы подошли к самому главному моменту: мы уже знаем, что компьютер выполняет операции с числами в двоичном коде. Причем, это единственный пока вариант для ЭВМ и не такой уж сложный, если вдуматься, для нашего с вами понимания. Остается выяснить, как же объяснить компьютеру, что он должен делать с числом (числами), чтобы получить результат.
Для этого, очевидно, нужны команды, которые будут точно поняты и интерпретированы центральным процессором. А поскольку ничего, кроме двоичных чисел ЭВМ не понимает, то команды должны состоять из определенного набора цифр — машинной инструкции. Она представляет собой запись, состоящую из нескольких элементов — начального и конечного маркеров и самого её «тела».
Машинные инструкции разрабатываются для каждого семейства процессоров и некоторых отдельных их разновидностей. Поддержка инструкций заложена в самом процессоре.
Конечно, программу из таких инструкций написать очень и очень трудно, так как ручное управление процессором — задача нетривиальная и трудоемкая, для этого существуют специальные языки нижнего уровня, которые работают напрямую с процессором — ассемблеры. Специальная программа компилятор, превращает команды ассемблеров в машинные инструкции, которые располагаются в специальных бинарных (двоичных) файлах.
Коды, коды, коды
В принципе, кодирование информации происходит в компьютере неоднократно. Это циклический процесс, реализованный и на программном и на аппаратном уровнях. Начинается он со скан-кода (в IBM-совместимых компьютерах), с помощью которого драйвер клавиатуры распознает нажатия и отпускания клавиш.
Далее в электронных документах, с которыми мы работаем, символы кодируются при помощи кодовых таблиц (кодировок) — (КОИ — 8, СР1251, СР866, Мас, ISO, Unicode), в которых на один символ отводится от одного и более байт. Коды у одинаковых символов в разных кодировках различны. Допустим, двоичное число 11000010 в кодировке KOI-8 будет означать строчную букву «б», а в кодировке CP1251 — прописную «В».
С помощью стандартных кодировок записываются и команды абсолютно всех языков программирования. А затем программа проходит через транслятор, который снова кодирует её, только уже в понятные процессору машинные инструкции либо в ассемблер. В принципе, транслятор решает массу задач но это — тема для другого разговора. Наша цель — составить более-менее стройную картину преобразования информации внутри ЭВМ.
Ассемблеры
Собственно, мы уже несколько раз упоминали это «семейство» низкоуровневых языков программирования. Сейчас их используют реже, чем это было раньше, так как появилось немало более удобных, высокоуровневых языков. Тем не менее, ассемблеры всё же применяются — когда нужен небольшой но высокоэффективный фрагмент программного кода. Ассемблерные вставки поддерживаются и в ряде высокоуровневых языков программирования, с целью ускорения работы кода.
Ассемблеры — аппаратно-ориентированные языки, они привязаны к архитектуре процессора, поэтому, их синтаксис для разных процессоров будет различен. Но их объединяет один принцип. Команды процессора, которые представляют собой просто числа, в ассемблерах представлены как «мнемонические» символы. Чтобы не расшифровывать это понятие, я просто продемонстрирую несколько небольших программ.
Вот как будет выглядеть программа на ассемблере 8086 (x86-совместимая), которая выводит на консоль Hello World:
use16
org 100h
mov dx,hello
mov ah,9
int 21h
mov ax,4C00h
int 21h
hello db 'Hello, world!$'
Понять, что здесь написано не так уж и трудно. Use16 – генерация 16-битного кода, регистр DX (mov dx) содержит адрес строки (строка заканчивается значком $ и объявляется директивой db), регистр AH (mov ah) содержит 9 — номер функции DOS, int 21h — обращение к функциям DOS. 4C00h — завершение программы, где 0 — успешное её завершение.
Может даже показаться, что программирование на ассемблере — легкая задача. Но это отнюдь не так. Если вы начнете делать что-то более или менее сложное, писанины будет куда больше. Вот, взгляните на вывод латинских букв по алфавиту в цикле:
use16
org 100h
mov ah,02h
mov dl,'A'
mov cx,26
tt:
int 21h
inc dl
loop tt
mov ah,09h
mov dx,press
int 21h
mov ah,08h
int 21h
mov ax,4C00h
int 21h
press:
db 13,10,'Press any key...$'
Кроме уже знакомых нам команд здесь есть и другие. 02h — вывод символа, dl — первый символ, Inc dl – следующий символ, 08h — ввод без отображения (чтобы не закрылась программа), 13,10 — возврат каретки, перевод строки, сх — счетчик повторений. Loop — метка, которая «выбрасывает» цикл к началу.Собственно, далеко не каждый, даже опытный программист, сможет работать с ассемблерами. Для этого нужно очень хорошо знать архитектуру процессора. Для промышленного программирования гораздо удобнее пользоваться языками высокого уровня, о которых мы говорили в прошлый раз.
Уровни общения
Команды ассемблера — это, практически, прямые инструкции для процессора, которые выполняются и трактуются однозначно. Машинный код программ, написанных на современных языках программирования высокого уровня, генерируется и оптимизируется автоматически, порой проходя несколько уровней кодирования. Это сделано для того, чтобы ускорить и упростить работу программиста. Но «упрощение», в данном случае — весьма относительное понятие.
Современные языки программирования представляют собой сложнейшие системы, которые позволяют относительно быстро создавать и тестировать программные комплексы любого масштаба и сложности.
Эдуард ТРОШИН
Компьютерная газета
Заходите на наш сайт ITквариат в Беларуси за новой порцией интересных новостей!
А также подписывайтесь и читайте новости от ITквариат раньше остальных в нашем Telegram-канале !
Поделитесь этой новостью с друзьями!
Заметили ошибку? Выделите ее мышкой и нажмите Ctrl+Enter!
И еще об интересном…
А что вы об этом думаете? Напишите нам!
Все операции в ЭВМ
выполняются над электрическими сигналами,
закодированными двоичной цифрой 0 или
1. Поэтому перед занесением в память
данные и команды должны быть вручную
или аппаратно преобразованы в двоичную
форму. Однако при написании программы
команды могут быть записаны в любой
другой форме (например, мнемонической,
как в табл. 1). В общем случае программа
может кодироваться четырьмя способами:
в двоичной, восьмеричной, шестнадцатеричной
и символической или мнемонической
форме.
Двоичная форма
команды. Эта
форма является единственной, которую
понимает машина. Каждой ЭВМ присуща
система команд в двоичном коде, которая
понятна ей. Регистр команд, куда помещается
команда из памяти, связан электрическими
цепями с устройством управления ЭВМ,
где производится декодирование команды
и инициируются сигналы по реализации
команды. В табл. 2 приведено содержимое
части памяти ЭВМ, где хранятся программа
и данные для суммирования двух чисел
из примера параграфа 2.1. Левый столбец
— это адреса ячеек в двоичной форме.
Правый столбец представляет содержимое
ячеек в двоичной форме. Адрес имеет 11
разрядов (память ЭВМ состоит из 2048 = 211
ячеек), а каждая из ячеек — по 16 разрядов.
Для удобства чтения двоичных чисел и
их перевода в шестнадцатеричную систему
они разбиты на тетрады.
Восьмеричное и
шестнадцатеричное кодирование.
Из предыдущего
примера ясно, что записывать программу
в двоичном коде очень неудобно и
утомительно, если учесть, что средняя
по сложности программа состоит из
нескольких сотен или тысяч нулей и
единиц. Для сокращения записи целесообразно
использовать систему счисления с
основанием 23
= 8 или 24
= 16. Выбор для кодирования команд той
или иной системы счисления в основном
связан с форматом команд ЭВМ. В нашем
случае удобнее шестнадцатеричная
система, так как большинство полей в
командах базовой ЭВМ (см. рис. 3) равно
или кратно 4. В табл. 3 приведена та же
программа, что и в табл. 1 и 2, только
закодированная в шестнадцатеричной
системе. Для программиста такое
кодирование более удобно, чем двоичное.
Таблица 2 Таблица
3
Пример двоичного
кодирования Пример
шестнадцатеричного кодирования
содержимого
памяти содержимого
памяти
Мнемоническое
(символическое) кодирование.
Шестнадцатеричное
(или восьмеричное) кодирование имеет
очевидное преимущество перед двоичным.
Однако для длинных программ и оно
неудобно. Программист должен выучить
наизусть шестнадцатеричные коды всех
команд, использующихся в машине (в
некоторых ЭВМ их более сотни). Чтобы
упростить процесс написания, отладки
и чтения программы, предложен мнемонический
или символический код: каждая команда
представляется простым двух-, трех- или
четырехбуквенным мнемоническим символом.
Мнемонические символы значительно
легче связать с машинными операциями,
так как их можно выбирать таким образом,
чтобы они напоминали название команды.
Большинство мнемонических кодов — это
сокращения английских названий команд:
SUB
от SUBtract
(вычесть), BR
от Branch
(перейти), BPL
от Branch
if
Plus
(перейти по положительному числу) и т.
п. Намного легче запомнить, что
инвертирование аккумулятора (CoMplement
Accumulator)
кодируется СМА, чем запомнить двоичный
код 1111010000000000 или даже его шестнадцатеричный
эквивалент F400.
Пример символического кодирования
программы был приведен в параграфе 1
(см. табл. 1).
Хотя символическое
кодирование очень удобно для программиста,
оно не может быть понято машиной.
Единственным языком, понятным машине,
является язык двоичных кодов. Следовательно,
необходимо транслировать символическую
программу в ее двоичный эквивалент. Это
можно сделать вручную, используя таблицы
соответствия (вида табл. 4). На практике
трансляция осуществляется специальной
машинной программой.
Классификация
команд. ЭВМ
способна понимать и выполнять определенный
набор команд. При составлении программы
программист ограничен этими командами.
Количество и тип команд изменяются в
зависимости от возможностей и назначения
ЭВМ.
В зависимости от
того, к каким блокам машины обращается
команда или на какие блоки она ссылается,
команды можно разделить на три группы:
обращения к памяти (адресные команды);
обращения к регистрам (регистровые или
безадресные); ввода-вывода.
Команды обращения
к памяти предписывают машине производить
действия с содержимым ячейки памяти,
адрес которой указан в адресной части
команды. Например, команда ADD
20 из табл.
2.1 является командой обращения к памяти.
Она предписывает машине обращение по
адресу 20 и использование содержимого
этой ячейки в качестве первого операнда.
Второй операнд находится в аккумуляторе.
Эти два операнда суммируются.
Безадресные команды
выполняют различные действия без ссылок
на ячейку памяти. Например, команда CLA
из табл. 1
предписывает машине очистить аккумулятор.
Эта команда имеет дело с операндом,
расположенным в конкретном месте — в
аккумуляторе. Другой пример безадресной
команды — команда HLT
из табл.
1.
Команды ввода-вывода
осуществляют обмен данными между ЭВМ
и внешними устройствами. В них задаются
адрес (название) устройства ввода-вывода
и код той операции, которую должно
выполнить это устройство (приказ на
ввод-вывод).
В табл. 4 дан перечень
команд базовой ЭВМ. Подробно действия,
выполняемые машиной по этим командам,
рассмотрены в следующих параграфах.
Поясним одно из описаний: (СК) + 1 => СК
в командах ISZ
и TSF.
После
выполнения этой операции счетчик команд
будет указывать не на следующую команду
программы, а на команду, расположенную
за ней. Это произойдет потому, что после
выборки команды ISZ
или TSF
содержимое
счетчика команд уже было увеличеноавтоматически
на единицу.
Существует и другой
способ разделения команд на группы. Он
основан на учете функций, выполняемых
командой. Можно выделить восемь типов
команд: пересылок или обмена, арифметические,
логические, сдвигов, переходов, обращения
к подпрограмме, управления и ввода-вывода.
Форматы команд
и способы адресации.
Ранее
рассматривались различные форматы
(структуры) команд. Разработчики базовой
ЭВМ выбрали три формата 16-битовых
(однословных) команд с 4-битовым кодом
операций (рис. 4).
Таблица 4.Система
команд базовой ЭВМ
С помощью 4-битового
числа можно закодировать не более чем
24=16
различныхопераций. Разработчики отвели
два кода (1110 и 1111) на команды ввода-вывода
и безадресные команды, а так как в этих
командах либо используется меньшая по
длине адресная часть (8-битовый адрес
устройства ввода-вывода на рис. 4, в),
либо эта часть
вообще отсутствует (рис. 4, б),
то появилась
возможность иметь до 24=
16 команд ввода-вывода (4-битовый приказ
на ввод-вывод) и до 212
= 4096 безадресных команд (12-битовое
расширение кода операции).
В
командах обращения к памяти на адрес
отведено 11 бит, что позволяет осуществить
прямое
адресование
всех 2048 (2м)
ячеек памяти базовой ЭВМ. Однако
встречаются приложения, когда в команде
целесообразнее размещать не сам адрес
операнда (результата или перехода), а
его указатель, т. е. адрес ячейки памяти,
в которой сохраняется адрес операнда
(результата или перехода). Такое косвенное
адресование упрощает
построение циклических программ,
организацию работы с подпрограммами,
а также создает условия для расширения
адресуемого пространства (косвенное
адресование 16-битовых ячеек базовой
ЭВМ позволяет ей иметь память объемом
до 216
= 65536 слов).
Для указания вида
адресации в командах используется бит
с номером 11 (рис. 4, а),
в который при
прямой адресации следует записывать
0, а при косвенной— 1. В мнемонических
изображениях команд для указания
косвенной адресации операнд помещается
в скобки. Так, на рис. 5 команда ADD
25 или 4025 указывает, что из ячейки 25 должно
быть взято число (53), которое нужно
сложить с содержимым аккумулятора.
Команда же ADD
(25) или 4825
указывает, что из ячейки 25 должен быть
взят адрес ячейки (53), в которой хранится
число (47), которое и нужно сложить с
содержимым аккумулятора. («А это веселая
птица-синица, которая ловко ворует
пшеницу, которая в темном чулане хранится
в доме, который построил Джек.» Похоже?).
Соседние файлы в папке Тема 2 Лекция 2
- #
- #
Введение в машинный код
Содержание
- Вы читали «Хроники Амбера» Роджера Желязны? Там есть такой эпизод:
Главный герой находится в заточении. В абсолютной тьме. У него были выколоты глаза, но за год они регенерировали, и зрение постепенно к нему возвращается…
И однажды каким-то чудом в одной камере с ним оказывается загадочный Дворкин — создатель Лабиринта. Именно «чудом» — он просто появился неизвестно откуда. Он тоже находится «в заключении», но, в отличие от Корвина (главного героя), может спокойно ходить через каменные стены.
Удивленный Корвин спрашивает его:
— Как ты оказался в моей камере? Ведь здесь нет дверей.
Дворкин отвечает:
— Двери есть везде. Просто нужно знать, как в них войти.
Будем считать это эпиграфом…
1.1. Система счисления
#1. Наверняка среди ваших знакомых есть «крутые» программисты, или люди, таковыми себя считающие ;). Попробуйте как-нибудь проверить их «на вшивость». Предложите им в уме перевести число 12 из шестнадцатеричной в двоичную систему счисления. Если над подобным вопросом «крутой программист» будет думать дольше 10 секунд — значит он вовсе не так крут, как говорит…
#2. Система счисления (сие не подвластное человеческой логике определение взято из математической энциклопедии) — это совокупность приемов представления обозначения натуральных чисел. Этих «совокупностей приемов представления» существует очень много, но самая совершенная из всех — та, которая подчиняется позиционному принципу. А согласно этому принципу один и тот же цифровой знак имеет различные значения в зависимости от того места, где он расположен. Такая система счисления основывается на том, что некоторое число n единиц (radix) объединяются в единицу второго разряда, n единиц второго разряда объединяются в единицу третьего разряда и т. д.
#3. «Разрядам» нас учили еще в начальных классах школы. Например, у числа 35672 цифра «2» имеет первый разряд, «7» — второй, «6» — третий, «5» — четвертый и «3» — пятый. А «различные значения» цифрового знака «в зависимости от того места, где он расположен» и «объединение в единицу старшего разряда» на тех же уроках арифметики «объяснялось» следующим образом:
35672 = 30000 + 5000 + 600 + 70 + 2
35672 = 3*10000 + 5*1000 + 6*100 + 7*10 + 2*1
35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100 (1)
#4. Очень наглядно это отображают обыкновенные счеты. Набранное на них число 35672 будет выглядеть… см. рисунок слева в общем…
Чтобы набрать число 35672 мы должны передвинуть влево две «костяшки» на первом «прутике», 7 на втором, 6 на третьем, 5 на четвертом и 3 на пятом. (У нас ведь 1 «костяшка» на втором — это то же самое, что и 10 «костяшек» на первом, а одна на третьем равна десяти на втором, и так далее…) Пронумеруем наши «прутики» снизу вверх — да так, чтобы номером первого был «0»… И снова посмотрим на наши выражения:
35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100
Это (если сверху вниз считать) сколько на каждом «прутике» «костяшек» влево отодвинуто.
35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100
Это номер прутика (самый нижний — 0), на котором отодвинуто определенное число костяшек.
35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100
Это на каждом прутике — по 10 костяшек нанизано, не все влево отодвинуты, но всего-то их — 10!
Кстати, красненькое 10 в последнем выражении соответствует основанию (radix) системы счисления (number system).
#5. Пальцев на руках у человека 10, поэтому и считать мы привыкли в системе счисления с основанием 10, то есть в десятичной. Если вы хорошо представляете себе счеты и немного поупражнялись в разложении чисел аналогично выражению 1, то перейти на систему счисления с основанием, отличным от привычной, особого труда для вас не составит. Нужно всего лишь представить себе счеты, на каждый прут которых нанизано не привычные 10 костяшек, а… скажем, 9 или 8, или 16, или 32, или 2 и… попробовать мысленно считать на них.
#6. Для обозначения десятичных чисел мы используем цифры от 0 до 9, для обозначения чисел в системах счисления с основанием менее 10 мы используем те же цифры:
radix 9 - 0, 1, 2, 3, 4, 5, 6, 7, 8;
radix 8 - 0, 1, 2, 3, 4, 5, 6, 7;
radix 2 - 0, 1 и т. д.
Если же основание системы счисления больше десяти, то есть больше, чем десять привычных нам чисел, то начинают использоваться буквы английского алфавита. Например, для обозначения чисел в системе счисления с основанием 11 «как цифра» будет использоваться буква А:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A
В системе счисления с основанием 16 — буквы от A до F:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
И так далее…
Правда, при определенном основании (при каком?) буквы аглицкого алфавита закончатся…
Но нам это, пока что, глубоко фиолетово, так как работать мы будем только с тремя radix-ами: 10 (ну естественно), 16 и 2. Правда, если кто на ДВК поизучать это дело собирается, тому еще и radix 8 понадобится.
#7. Числа в любой системе счисления строятся аналогично десятичной. Только на «счетах» не с 10, а с другим количеством костяшек.
Например, когда мы пишем десятичное число 123, то имеем в виду следующее:
1 раз 100 (10 раз по 10)
+ 2 раза 10
+ 3 раза 1
Если же мы используем символы 123 для представления, например, шестнадцатеричного числа, то подразумеваем следующее:
1 раз 256 (16 раз по 16)
+ 2 раза 16
+ 3 раза 1
Короче — полный беспредел. Говорим одно, а подразумеваем другое. И последнее не для красного словца сказано. А потому, что так оно и есть…
Истина где-то рядом…
#8. Трудность у вас может возникнуть при использовании символов A, B, C и т. д. Чтобы решить эту проблему раз и навсегда, необходимо назубок вызубрить ма-а-аленькую табличку «соответствия» между употребляемыми в «компьютерном деле» систем счисления:
radix 10
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
radix 16
0
1
2
3
4
5
6
7
8
9
A
B
C
D
E
F
radix 2
0
1
10
11
100
101
110
111
1000
1001
1010
1011
1100
1101
1110
1111
Следуя этой таблице, число 5BC в шестнадцатеричном формате «строится» так:
5 раз 256 (16 раз по 16)
+ 11 раз 16 (10 - потому что по таблице B как бы равно 11)
+ 12 раз 1
А теперь, если пораскинуть мозгами, с легкостью переведем 5BC из шестнадцатеричной в десятичную систему счисления:
5*256 + 11*16 + 12 = 1468
Вот и объединили цифры с буквами. Пространство со временем поучимся объединять немного позже — если не испугаетесь сложностей низкоуровневого программирования.
В общем-то решать вам. В Delphi тоже много чего объединять можно.
#9. Двоичная система по-компьютерному обзывается «bin», «родная» десятичная — «dec», а шестнадцатеричная — «hex». Это так компьютерщики обозвали те системы счисления, с которыми имеют дело… А обозвали потому, что у них ведь полный бардак в голове, оказывается!
Например, 10 — что это за число? Да это вообще не число! Палка и барабан — и только… А вот 10d или же 10_10 — уже понятно, что это — число, соответствующее количеству пальцев на обеих руках. И именно на обеих, а не на двух. Почему не на двух? — А потому что на двух в какой системе? Ежели в двоичной, так это на десяти! То бишь 100, если в десятичной…
Вот и придумали программисты после числа буковку писать — b, d или h. А самые ленивые еще и директиву специальную придумали: напишут в самом начале программы какой-нибудь .radix 16
и будут автоматически все числа, которые без этих букв, за шестнадцатеричные приниматься.
#10. Еще немного про перевод между «радиксами». (Вообще-то это плевое дело, конечно, если представляешь себе, что такое «совокупность приемов представления обозначения натуральных чисел»).
Например, преобразование числа 42936 из десятичного в шестнадцатеричный формат проводится следующим образом (в скобках — остаток):
42936/16 = 2683(8) 8 - младшая цифра
2683/16 = 167(11) B (11d=Bh по таблице)
167/16 = 10(7) 7
10/16 = 0(10) A - старшая цифра
--------------------------------------
42936d=A7B8h
А вот и обратный процесс — перевод из HEX в DEC числа A7B8h:
10*16=160 160+7=167 (10 - потому что Ah=10d)
167*16=2672 2672+11=2683
2683*16=42928 42928+8=42936
--------------------------------------------
A7B8h=42936d
Преобразования чисел в системы счисления с другим основанием проводятся аналогично… Счеты! Обыкновенные счеты, только с «плавающим» числом «костяшек» на каждом «прутике»…
#11. Если честно, то конкретный «рисунок» цифр — единица там палкой обозначается, двойка — лебедем — это все лишь историческая случайность. Мы запросто можем считать в «троичной» системе счисления с цифрами %, *, _ (где запятая — это знак препинания, а вовсе не число):
%, *, _, *%, **, *_, _%, _*, __, *%%, *%*, *%_, **%...
Или использовать родные цифры в десятичной системе счисления, но по другому «вектору упорядоченных цифр» — 1324890576:
1, 3, 2, 4, 8, 9, 0, 5, 7, 6, 31, 33,34, 34,38, 39, 30, 35, 37...
Правда, этим немножко затрудняется понимание происходящего? А ведь тоже десятичная система! И рисунок цифр как бы знакомый :-)))
Или вообще считать в 256-ричной системе счисления, используя в качестве «рисунка цифр» таблицу ASCII-символов! (По сравнению с вами, извращенцами, любой Биллгейтс будет девственником казаться!!).
#12. Теперь самая интересная часть Марлезонского балета.
Компьютер, как известно, считает только в двоичной системе счисления. Человеку привычна десятичная. Так нахрена еще и шестнадцатеричную какую-то знать нужно?
Все очень просто. В умных книжках пишут, что «шестнадцатеричная нотация является удобной формой представления двоичных чисел». Что это значит?
Переведите число A23F из шестнадцатеричной «нотации» в двоичную. (Один из возможных алгоритм приведен в п.10.). В результате длительных манипуляций у вас должно получиться 1010001000111111.
А теперь еще раз посмотрите на таблицу в п. 8. (которую вы как бы уже и выучили) и попробуйте то же самое сделать в уме :
Ah=1010b
2h=0010b
3h=0011b
Fh=1111b
A23Fh = 1010 0010 0011 1111b
Каждой шестнадцатеричной цифре соответствует тетрада (4 штуки) ноликов и единичек. Все, что потом нужно сделать — «состыковать» эти тетрады. Круто? Вас еще не то ждет!
#13. Кстати (наверняка вы это уже знаете):
00000123 = 123, но!!
123 <> 12300000
… но это так… кстати…
#14. И, напоследок, еще несколько слов про HEX и BIN :). Зайдите в Norton Commander, наведите указатель на какой-нить файл и нажмите там F3. А когда он откроется — на F4. Там какие-то нездоровые циферки попарно сгруппированы. Это и есть «нолики и единички» (которыми в компьютере все-все-все описывается), но в шестнадцатеричном формате…
Следует основательно разобраться с системой счисления. Минимум, что должен вынести из этой главы юзвер, вступивший на скользкий путь низкоуровневого программирования — это научиться переводить числа между DEC, HEX и BIN… хе-хе… В УМЕ!
1.2. Регистры
#1. Наверняка вы имеете представление о том, что такое переменная. Наиболее продвинутые даже знают, что переменная имеет тип. Кажется вполне естественным, что любой высокоуровневый язык программирования позволяет создавать любое количество переменных того или иного типа…
Так вот, господа — при программировании на ассемблере вас ждет большая неожиданность. Потому что для всех ваших навороченных вычислений разрешается использовать только несколько переменных с фиксированными «именами собственными» и имеющих фиксированную «длину». Эти «предопределенные переменные» называются регистрами, и каждая из них имеет свою специализацию.
О специализации нам пока что говорить рано, описание наподобие «регистр-указатель базы кадра стека» вам вряд ли о чем-то скажет. Поэтому для начала познакомимся только с так называемыми регистрами общего назначения (РОН), и то не со всеми, а только с четырьмя основными, которые являются своего рода «рабочими лошадками» микропроцессора.
Вот их «имена собственные» — AX, CX, DX, BX (именно в такой последовательности они «упорядочены» в Intel’овских микропроцессорах).
А сейчас мы поближе посмотрим на эти «рабочие лошадки» микропроцессора.
1. Запустите программу DEBUG.EXE1.
2. Когда появится приглашение в виде «минусика», введите букву «R» (можно и «r» — регистр символов значения не имеет) и нажмите на «Enter».
Не правда ли, весьма похоже на то, что показывают в художественных фильмах про хакеров?
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=18B2 ES=18B2 SS=18B2 CS=18B2 IP=0100 NV UP EI PL NZ NA PO NC
18B2:0100 6A DB 6A
— Ну и что это такое? — скептически спросите вы.
— А черт его знает! Будем разбираться!
#2. То, что у вас должно появиться — это список доступных регистров и текущее значение каждого из них. Как видите, значения регистров AX, BX, CX, DX равны 0. Не правда ли, создается впечатление, что они просто-напросто ждут того, чтобы в них внесли какое-либо значение?
Природа не терпит пустоты. Писателей приводит в ужас чистый лист бумаги…
Весьма скоро и вы при виде «пустых» регистров будете испытывать непреодолимое наркотическое желание чем-нибудь их заполнить…
Однако прежде чем мы сделаем это в первый раз, давайте уточним тип этих «переменных».
А он очень простой, этот тип — шестнадцатеричное число в диапазоне 0…FFFF. Или, если в BIN, то — от 0 до 1111 1111 1111 1111.
Маловато будет? А вот создатели первых «IBM-совместимых» компьютеров посчитали, что и этого много2! 16-битная переменная еще и на две части дробится — для совместимости с языками ассемблера для предыдущих моделей процессора Intel, работавших только с 8-битными регистрами; да и просто ради удобства…
В общем, в умных книжках рисуют вот такую вот «нездоровую» схемку3:
А означает она следующее.
Физически существует один регистр — AX, а вот логически он делится на два — на старшую (AH) и младшую (AL) части (от английского — high и low).
Очевидно, что присвоить AX значение, например, 72F9h, мы можем следующими способами:
1. AX = 72F9h (одной командой);
2. AH = 72h; AL = F9h (двумя командами).
Точно так же присвоить значение 78h регистру AH можно двумя способами:
1. AH = 78h;
2. AX = 7800h.
То же самое, но для регистра AL:
1. AL = 78h;
2. AX = 0078h .
Тех, кого смущают числа с буквами, мы со зловредной ухмылкой отсылаем к 1.1. Система счисления :-]
#3. Если рассматривать регистр «целиком», то каждый из них имеет «длину» 16 бит, которые принято нумеровать справа налево4 . Так, для числа 2F4Dh, внесенного, например, в регистр AX, мы можем нарисовать такую вот «навороченную» табличку:
AX
2F4D
AH
AL
2F
4D
Значение бита
0
0
1
0
1
1
1
1
0
1
0
0
1
1
0
1
Номер бита
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
0
Тетрады
Старшая AH
Младшая AH
Старшая AL
Младшая AL
Внимательно смотрим на таблицу: одной шестнадцатеричной цифре соответствует тетрада двоичных цифр (4 шт., они же — 4 бита). «Емкость» регистров AH и AL — две тетрады, т. е. 8 бит. Точно такую «длину» имеют: коды символов, скан-коды клавиш, номера функций прерываний и куча всего прочего, чего вы пока еще не знаете.
Емкость AX (состоящего из двух половинок) — 4 тетрады, т. е. 16 бит; они же (эти 16 бит) иначе еще называются «словом»…
#4. «Принудительно» присвоить регистру значение можно при помощи той же команды «R», только с параметром «имя собственное регистра».
Например, команда
- R AX [Enter]
выбросит вам на монитор
:
Введите после двоеточия, например, число 123 и снова нажмите на Enter:
:123 [Enter]
На дисплее опять появится приглашение «-«, на которое мы отвечаем командой «R» без параметров и таким образом вновь просматриваем значения наших регистров:
AX=0123 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=18B2 ES=18B2 SS=18B2 CS=18B2 IP=0100 NV UP EI PL NZ NA PO NC
18B2:0100 6A DB 6A
Смотрим внимательно — AX=0123, что и требовалось доказать…
Примечания
1). В W9X она находится в папке WINDOWSCOMMAND. В Y2K и XP — WINDOWSSYSTEM32. В обоих случаях достаточно набрать в командной строке «debug», чтобы она запустилась.
2). Не забудьте, что при тогдашней технологической базе и это было большим прорывом. А экстенсивное расширение, например, разрядности, во-первых, нужно правильно предвидеть (вспомните, сколько в том же MS-DOS закладок на будущее, которые никуда не пошли за ненадобностью), а во-вторых, правильно оценить (в буквальном смысле). Неужели вы думаете, что, например, производители памяти не могут легким мановением руки увеличить ширину шины, соединяющую память с процессором? Могут, но во-первых — это резко повысит стоимость памяти, а во-вторых — не гарантирует повышения производительности.
3). Впоследствии мы немного усложним эту схемку — регистры современных процессоров 32-разрядные и называются немного иначе
4). «Первый справа» бит мы будем называть «нулевым». Однако нам попадались руководства, в которых это же бит обозван как «первый». Можно долго обсуждать тонкости русского языка (которые, к сожалению, не всегда понимает переводчик), однако это выходит за рамки данной книги. Просто имейте это ввиду, что можете с этим столкнуться, и будьте бдительнее, читая документацию.
1.3. Память
#1. Первым видом памяти, с которым мы войдем (придется!) в тесный физический контакт, будет оперативная, она же — RAM (от английского — Random Access Memory). Оперативная память — это своего рода «рабочая площадка», по которой суетится этакий шустрый многорукий дядька-процессор — чего-то там собирает, от кучи к куче бегает, всех ругает…
Оперативная память — это ряд пронумерованых ячеек размером в байт. Мы можем получить доступ к первому байту памяти, ко второму, к третьему и т.д.
Короче — пришло время испробовать еще одну команду из скромного арсенала DEBUG’a! Запустите debug и введите команду D (от английского — DUMP).
«Картинка», которую вы увидели, называется «дамп памяти» (что в переводе с английского означает «свалка») и она насыщена не только важной информацией, но и специальной низкоуровневой энергетикой. Да чего уж там греха таить — каждый ассемблерщик знает, что рассматривание дампа памяти поднимает настроение, жизненный тонус и другие, не менее важные вещи
18B2:0100 6A 00 68 4B 01 66 83 7E-E0 00 74 05 B8 4C 01 EB j.hK.f.~..t..L..
18B2:0110 03 B8 4A 01 2B D2 52 50-57 FF 36 C4 34 00 A1 18 ..J.+.RPW.6.4...
18B2:0120 F7 7F 83 C4 12 56 9A 16-44 F7 7F FF 76 FE 9A 59 .....V..D...v..Y
18B2:0130 04 8F 17 B8 FE FF 1F 5E-5F C9 CA 06 00 90 C8 54 .......^_......T
18B2:0140 04 00 57 56 8B 76 04 33-C0 89 46 D6 B9 0B 00 8D ..WV.v.3..F.....
18B2:0150 7E D8 16 07 F3 AB 89 46-BC B9 0C 00 8D 7E BE F3 ~......F.....~..
18B2:0160 AB 9A 21 9C 8F 17 89 46-FA A1 08 01 8E 46 06 26 ..!....F.....F.&
18B2:0170 39 44 02 0F 84 55 01 C7-46 BC 1A 00 C4 5E 0C 26 9D...U..F....^.&
Слева — это адрес памяти. В центре — 16 столбцов из спаренных цифр…
А здесь и повториться лишний раз не грех. Каждая пара шестнадцатеричных цифр — это байт. Смотрите внимательно на дамп! Байт по адресу 100 имеет значение 6A, байт по адресу 101 — 00, байт по адресу 102 — 68… Эти «сладкие парочки» — и есть неделимая «единица адресации» оперативной памяти.
Тех, кого смущает наличие буковок в адресе, в очередной раз отсылаем ознакомиться с шестнадцатеричной системой счисления, так как все числа, отображаемые программой debug — именно шестнадцатеричные.
И, наконец, столбец справа — это символы, соответствующие шестнадцатеричным кодам центрального столбца (например, коду 6A соответствует символ J). Большинству кодов не соответствует никакой из «печатных символов» — таким в колонке справа соответствуют точки.
#2. А теперь потренируем наши пальчики дампировать память — пройдемся по некоторым «историческим местам» нашей оперативной памяти… Для этого мы будем вводить команду D с параметром.
Например, команда (параметр L8 означает «вывести 8 байтов»):
- D FFFF:5 L8 [Enter]
покажет вам системную дату в правом столбце дампа.
Короче, искателям приключений выдаем «простыню» самых интересных адресов (большинство слов в описании вам пока должны быть непонятны, но вы не пугайтесь — понимание придет!).
- 0:417 — два байта разрядов состояния клавиатуры. Они активно используются ROM-BIOS для управления интерпретаций действий клавиатуры. Изменение этих байтов изменяет значение нажатых клавиш (например, верхний или нижний регистр).
- 0:41A — cлово по этому адресу указывает на начало буфера BIOS для ввода с клавиатуры, расположенного начиная с адреса 41E. В этом буфере хранятся и ждут обработки результаты нажатия на клавиши. Конец буфера — слово по адресу 41C.
- 0:43E — байт указывает, необходима ли проверка дискеты перед подводом головки на дорожку. Разряды 0…3 соответствуют дисководам 0…3. Если разряд установлен в 0, то необходима проверка дискеты. Как правило, вы можете обнаружить, что разряд установлен в 0, если при предыдущем обращении к дисководу имели место какие-либо проблемы. Например, разряд проверки будет равен 0, если вы попытаетесь запросить каталог на дисководе, на котором нет дискеты, и затем на запрос, появившийся на экране дисплея: «Not ready reading drive B: Abort, Retry, Ignore?» вы ответите: «A».
- 0:44C (2 байта) — длина регенерации экрана. Это число байтов, используемых для страницы экрана. Зависит от режима.
- 0:44E (2 байта) — смещение для адреса начала текущей страницы в памяти дисплея. Этот адрес указывает, какая страница в данный момент используется (маленькая, но неприятная подробность — это смещение внутри текущего сегмента видеопамяти, без учета самого сегмента. Например, для нулевой страницы смещение всегда будет равно нулю.)
- 0:460 (2 байта) — размер курсора, представленный в виде диапазона строк развертки. Первый байт задает конечную, а второй — начальную строку развертки.
- 0:449 — значение этого байта определяет текущий видеорежим. Для расшифровки требуется целая таблица. Например, 3 — 80-колонный текст, 16 цветов; 13h (19) — 256-цветный графический режим 320×200 и т. д.
Ну и хватит для первого раза. Кому мало — ищите дополнительную документацию :-p «
1.4. Программа
#1. Любая программа выполняется последовательно (мы ведь пока обсуждаем «простой» IBM PC, а не какой-нибудь крутой векторный параллельный суперкомпьютер). То есть пока не выполнилась текущая «строка» (инструкция) программы, следующая не выполнится. Совсем другой вопрос, какая «строка» будет выполнена после «текущей» (здесь мы имеем дело со всевозможными логическими «ветвлениями», «циклами» и т. д.), или же строчку из какой программы процессор выполнит следующей, а какая — будет ждать своей очереди (так называемая «многозадачность», которую пока трогать не будем — в большинстве случаев мы можем прекрасно прожить и без нее, поскольку все заботы об этом все равно берут на себя операционные системы).
Итак, у нас есть оперативная память, в которую загружается программа перед ее выполнением (сразу же по нажатию на Enter из Norton Commander). Операционная система, которая, собственно, и загружает программу, сообщает процессору, что надо начать обрабатывать команды, которые в памяти начинаются с такого-то адреса. И здесь первый подводный камень, вернее скала, которую трудно не заметить.
Начало программы в памяти процессор различает легко — ему указывает на это командный интерпретатор, а вот конец программы программист должен указывать сам!
Каким образом? А очень легко! Компьютер «распознает» как выход из программы специальную последовательность байтов. Например, для исполнимых файлов типа com (именно с этим типом файлов мы будем работать на начальном этапе) достаточно последовательности CD и 20.
Пробуем-проверяем? Ну конечно же! Только для этого вам понадобится какой-нибудь шестнадцатеричный редактор, например, HexWorkshop.
Все очень просто — создаем новый файл, единственным содержимым которого является последовательность CD 20, и сохраняем его как, например, myprg_1.com. Если вы позаботились о том, чтобы после CD 20 не было никаких прочих символов, то исполнимая программа будет «весить» только 2 байта.
Запускать это ваше первое творение лучше из Norton или Volcov Commander (все же это пока что DOS’овская программулька).
Что же она делает, эта 2-байтовая малышка? А ничего, просто этот файл обладает двумя важными свойствами:
- это — программа;
- эта программа — программа с корректным выходом.
Последнее и является единственным, что она пока что может делать (корректно выгружаться из памяти)…
Еще к вопросу о выгружаемости — если после CD 20 вы напишите еще что-нибудь (чепуху), она все равно будет проигнорирована. Дело до ее выполнения просто-напросто не дойдет. Другое дело — если вы напишите чепуху до…
#2. Честно говоря, опасно при низкоуровневом программировании чепуху писать. Можно невзначай и винт отформатировать :))). Поэтому лабуду писать не будем, вернее — будем, но не лабуду…
Итак, продолжим наше извращение. Познакомимся еще с некоторыми «машинными командами» (в нашем случае — последовательностями шестнадцатеричных циферек).
- B82301 — внести значение 0123h в AX;
- 052500 — прибавить значение 0025h к AX;
- 8BD8 — переслать содержимое AX в BX;
- 03D8 — прибавить содержимое AX к BX;
- 8BCB — переслать содержимое BX в CX;
- 31C0 — очистка AX;
- CD20 — конец программы. Передача управления операционной системе.
Вот и давайте создадим еще одну программу типа com со следующим «шестнадцатеричным содержимым»:
B8-23-01-05-25-00-8B-D8-03-D8-8B-CB-31-C0-CD-20
Если вы все ввели правильно, то прога у вас без проблем запустится, а операционная система не будет ругаться… Правда, визуально (в смысле на мониторе) вы ее работу так и не заметите, но поверьте на слово — она работает! В этом вы еще убедитесь, когда посмотрите на ее работу изнутри — не различающими цветов глазами компьютера… Только сначала еще немного теории…
#3. Теперь поговорим о втором подводном камне :). Один из принципов фон Неймана звучит приблизительно так: машине безразлично целевое назначение данных… Одна и та же цепочка битов может быть и машинными командами, и данными (например, символами, выраженными в виде кодов — есть такая «таблица символов ASCII», наверняка вы ее знаете).
Что из этого следует? А то, что компьютеру нужно указывать, что подразумевается под той или иной «простыней» из битов — данные или код.
На высоком уровне это делает операционная система. Например, она не пытается загрузить в память для выполнения файлы с расширениями, отличными от COM, EXE и BAT (последний вообще не из этой оперы, но принцип сохраняется).
Хотя…, вы всегда можете поэкспериментировать! Смените, например, у какого-нибудь текстового файла тип с TXT на COM и попробуйте его запустить на выполнение (хотя мы это делать настоятельно не рекомендуем!). В большинстве случаев ваш компьютер зависнет! Потому что:
- Он пытается интерпретировать данные как код. Соответственно, в процессор «попадает» всякая ерунда.
- Вряд ли он натолкнется на последовательность CD 20 вашем тексте :). Даже в том случае, если этот код выполнится «успешно» — ваша программа не возвратит управление операционной системе, а пойдет выполняться хлам, содержащийся в оперативной памяти. Как-то — остатки ранее выполненных программ, куски чьих-то данных, интерпретированные как код… и прочая многочисленная ерунда…
Почти такой же эффект, но с потенциально большей разрушительной силой может получиться, если управление получит ИСПОРЧЕННЫЙ код, который вроде бы «в основном» правильный, но часть его вместо инициализации переменных и прочих подготовительных действий в лучшем случае ничего не делает, а в худшем портит другой код и данные…
Как вам тяжело «въехать» в смысл повествования, состоящего из кусков различных книг, так и компьютеру тяжело понять подобную «мешанину». С той лишь разницей, что любую «неинтересную книгу» вы можете использовать в качестве туалетной бумаги, а вот «компутер» подобного права выбора лишен — он должен в это «въезжать», его процессор начинает перегреваться, а мозги кипят и вытекают через низкоуровневые порты ввода-вывода (командами IN и OUT соответственно).
#4. Еще немного идеологии. О программе, которая выполняется в памяти…
Сколько бы ни было «мозгов» в вашей навороченной тачке, любая программа выполняется в 640 килобайтах «нижней» (или основной) памяти. Если отнять от этой цифры «резидентную часть» операционной системы, многочисленные драйвера и т.д., то оставшееся и есть объем памяти, в котором выполняется ваша программа. А остальные мегабайты — это место для кэширования диска, хранения промежуточных данных и т.п.
Страшно? Медитируйте!
#5. Как уже говорилось в #3, одна и та же последовательность битов в памяти может быть:
- кодом (т.е. что компьютеру нужно делать) — последовательностью инструкций;
- данными (т.е. с чем компьютеру нужно выполнять ту или иную работу). Именно данные являются исходной «задачей» и конечным результатом работы процессора.
- стеком — это область памяти, позволяющая писать реентерабельный/рекурсивный код и служащая для хранения адресов возврата и локальных данных и передачи параметров.
Соответственно, и программа состоит из трех частей (сегментов): сегмента данных (data), сегмента кода (code) и сегмента стека (stack)…
Оставим пока что «гнилой базар» про смысл словосочетаний «реентерабельный/рекурсивный код» и «адрес возврата». Чтобы не затруднять себе понимание происходящего, мы попытаемся абстрагироваться от всех этих ужасающих вещей и для начала заняться только кодом.
#6. Помните, как в конце фильма «Matrix» Нео в конце концов увидел ее — черно-зеленую «матрицу»? Сейчас с вами произойдет нечто подобное!
Посмотрите на машинные коды, и «что они делают» в #2. Немножко дополним эту «простыню». Например, командой «внести значение» 1234 последовательно в каждый из «регистров общего пользования»:
B83412 - AX=1234
BB3412 - BX=1234
B93412 - CX=1234
BA3412 - DX=1234
Наиболее наблюдательные должны для себя отметить, что первый байт — это команда «переместить в регистр», а второй и третий — само число, только байты почему-то «наоборот».
Однако никто не пишет программы в шестнадцатеричных редакторах! Никто! Это большая глупость! Единственное, зачем мы вам про это рассказываем — это чтобы вы поняли, что могут означать загадочные пары шестнадцатеричных цифр в дампе…
Нет необходимости заучивать, что B8 — это «переместить в регистр AX», BB — «переместить в регистр BX» и так далее… Когда-нибудь это может пригодиться тому, кто будет писать компилятор, умеющий генерировать исполняемый код, упаковщики исполняемых файлов, самомодифицирующийся код или, на худой конец, конструкторы полиморфных самошифрующихся вирусов. Но это мы оставим на будущее…
Все намного проще!
В этом вы можете убедиться, загрузив вашу программу myprg_1.com в debug (например, командной строкой
debug myprg_1.com
и введя команду «u».
А вот дальше начинается самое интересное :)))
#7. Вот что вы должны увидеть:
11B7:0100 B82301 MOV AX,0123 ; Внести значение 0123h в AX
11B7:0103 052500 ADD AX,0025 ; Прибавить значение 0025h к AX
11B7:0106 8BD8 MOV BX,AX ; Переслать содержимое AX в BX
11B7:0108 03D8 ADD BX,AX ; Прибавить содержимое AX к BX
11B7:010A 8BCB MOV CX,BX ; Переслать содержимое BX в CX
11B7:010C 31C0 XOR AX,AX ; Очистка AX
11B7:010E CD20 INT 20 ; Конец программы
Возвратившись к #2, перенесем сюда «описание» машинных команд.
Эти mov, add, xor, int — так называемые «мнемонические команды» (более или менее понятные человеку), на основе которых формируется (это debug делает) «машинный код». Не правда ли, так намного легче?
Соответственно, вместо шестнадцатеричных кодов мы легко могли вводить эти команды при помощи команды «A» (однако этим мы займемся позже).
#8. А теперь мы выполним нашу программу пошагово — произведем так называемую «трассировку» при помощи команды «T».
Итак, вводим «T» и жмем на Enter!
Вот что мы видим:
AX=0123 BX=0000 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0103 NV UP EI PL NZ NA PO NC
11B7:0103 052500 ADD AX,0025
Смотрим на значение AX и вспоминаем предыдущую инструкцию — «внести значение 0123h в AX». Внесли? И правда! А в самом низу — код и мнемоника команды, которая будет выполняться следующей…
Вводим команду «T» снова:.
AX=0148 BX=0000 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0106 NV UP EI PL NZ NA PE NC
11B7:0106 8BD8 MOV BX,AX
AX=0148 — «прибавить значение 0025h к AX». Сделали? Сделали!!
Вводим команду «T» снова:.
AX=0148 BX=0148 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0108 NV UP EI PL NZ NA PE NC
11B7:0108 03D8 ADD BX,AX
AX=0148=BX — «переслать содержимое AX в BX». Сделали? Сделали!!
Вводим команду «T» снова:
AX=0148 BX=0290 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010A NV UP EI PL NZ AC PE NC
11B7:010A 8BCB MOV CX,BX
«Прибавить содержимое AX к BX». Оно? А то!
Вводим команду «T» снова:
AX=0148 BX=0290 CX=0290 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010C NV UP EI PL NZ AC PE NC
11B7:010C 31C0 XOR AX,AX
«Переслать содержимое BX в CX». Сделано!
Вводим команду «T» снова:
AX=0000 BX=0290 CX=0290 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010E NV UP EI PL ZR NA PE NC
11B7:010E CD20 INT 20
«Очистка AX»? И точно: AX=0000!
Вводим команду «T» снова… И ГРОМКО РУГАЕМСЯ!!
Потому что, по идее, сейчас наша программа должна была завершиться — у нас же там код выхода прописан, а она куда лезет? NOPы какие-то (если продолжать команду «T» вводить), CALL 1085 (да вы продолжайте «трассировку», продолжайте!)
Для тех, кому лень продолжать жать на букву «T», введите для разнообразия команду «G» (от английского GO). На монитор должна вывалиться надпись «Нормальное завершение работы программы».
— Уф, — должны сказать вы — Работает!
А то!
#9. Только непонятно вот, почему вдруг между int 20 (CD 20) и надписью «Нормальное завершение работы программы» куча всяких «левых» непонятных команд (в том случае, если вы и дальше производили тарассировку, а не воспользовались «халявной» командой «G»)?
А потому, дорогие наши, что вы имели счастье нарваться на прерывание (interrupt)!
Понимаете ли, завершить программу — дело непростое :). Нужно восстановить первоначальное значение регистров, восстановить переменные среды и кучу всего другого! Знаете, как это сложно?
Однако эта процедура насколько сложная, настолько и типичная для исполняемых программ. А по сему разработчики операционной системы решили избавить программистов от необходимости делать это вручную, и включили эту стандартную процедуру в ядро операционной системы. И сказали: «да будешь ты (процедура обработки прерывания) вызываться как int 20, и будешь ты обеспечивать корректную передачу управления из выполняемой программы — назад в ядро». И стало так…
Ну, посудите сами, должна же операционная система ну хоть что-нибудь делать!!
1.5. Прерывания
#1. Прерывание — это СИГНАЛ процессору, что одно из устройств в компьютере НУЖДАЕТСЯ в обслуживании со стороны программного обеспечения. В развитие этой же идеи, программам позволили самим посылать запросы на обслуживание через механизм прерываний. Получив этот сигнал, процессор временно переключается на выполнение другой программы («обработчика прерывания») с последующим ВОЗОБНОВЛЕНИЕМ выполнения ПРЕРВАННОЙ программы.
Когда же и «кем» генерируются эти «сигналы» (в смысле «прерывания»)?
- Многочисленными «схемами» компьютера, его устройствами. Например, соответствующее прерывание генерируется при нажатии клавиши на клавиатуре.
- Также прерывания генерируются как «побочный продукт» при некоторых «необычных» ситуациях (например, при делении на «букву О»), из которых компьютеру хочешь, не хочешь, но приходится как-то выкручиваться…
- Наконец, прерывания могут преднамеренно генерироваться программой, для того чтобы произвести то или иное «низкоуровневое» действие.
Когда процессор получает сигнал прерывания, он останавливает работу приложения и активизирует «программу обработки прерывания», соответствующую «номеру прерывания» (т.е. разных сигналов прерываний больше одного — точнее, их 256). После того как обработчик свое отработает, снова продолжает выполняться основная программа.
Для тех, кто не понял. Представьте себе, что вы сидите за компом и выполняете какую-либо работу. И вдруг ловите себя на мысли, что вам СРОЧНО НУЖНО сходить в туалет (терпеть вы больше уже не можете). Вот это СРОЧНО НУЖНО и есть сигнал-прерывание, по которому вы начинаете выполнять определенную СТАНДАРТНУЮ последовательность инструкций (программу обработки прерывания), как-то: встать, пойти туда-то, включить свет … вернуться, сесть за комп и ПРОДОЛЖИТЬ РАБОТУ с того же самого места, на котором вы остановились перед выполнением программы «поход в туалет». В данном случае наш мозг выполняет роль процессора, наши внутренние органы сигнализируют мозгу о потребности в обслуживании, а само обслуживание проводится «программой-навыком», заложенным в процессе нашего развития и (хм!) воспитания.
#2. Программы обработки прерывания располагаются в оперативной памяти (ну а где же еще им располагаться?!) и, следовательно, имеют свой АДРЕС. Однако генератору прерывания этот адрес знать не обязательно :). Есть такая замечательная штука (спросите у тех, кто пишет вирусы) — таблица векторов прерываний. Это таблица соответствия номеров и адресов памяти, по которым находятся программы их обработки.
Почему «спросите у вирмейкеров?». А потому, что поменять адрес «программы обработки прерывания» на другой — проще пареной репы (мы этим еще займемся), в результате чего при запуске классической программы «HELLO, WORLD» может получиться еще более классический format c:…
Программы обработки прерывания автоматически сохраняют значения регистра флагов, регистра кодового сегмента CS и указателя инструкции IP, чтобы по завершении «обработки прерывания», к нашей безумной радости, снова возвратиться к выполняемой программе (просто программе)… Остальные регистры, содержимое которых меняется в обработчике, должен сохранять сам обработчик — и если он этого делать не будет, то нарушится выполнение основной программы. Ведь она даже не знает, что ее «ненадолго» прервали!
Однако на самом деле все намного сложнее :)). Но ведь это только «первое погружение» в прерывания, верно? А посему — пока что без особых «наворотов»…
#3. Одно прерывание мы с вами уже знаем. Это 20-е прерывание, обеспечившее «выход» из нашей COM-программы. Сегодня мы пойдем немножко дальше — помимо «выхода» попробуем поработать еще с одним прерыванием.
Итак, я достаю свой толстый талмуд с описанием прерываний и выбираю, каким бы это прерыванием вас занять на ближайшие 1/2 часа ;)…
Ну, например, вот одно симпатичное, под названием «прокрутить вверх активную страницу».
Внимательно читаем описание (и наши комментарии):
INT 10h, AH=06h (_1) — прокручивает вверх произвольное окно на дисплее на указанное количество строк.
ВХОДНЫЕ ПАРАМЕТРЫ: (_2)
- AH=06h; (_3)
- AL — число строк прокрутки (0…25) (AL=0 означает гашение всего окна); (_4)
- BH — атрибут, использованный в пустых строках (00h…FFh); (_5)
- CH — строка прокрутки — верхний левый угол; (_6)
- CL — столбец прокрутки — верхний левый угол;
- DH — строка прокрутки — нижний правый угол;
- DL — столбец прокрутки — нижний правый угол;
Далее представим входные параметры в виде таблички: (_7)
AH
06h
AL
Число строк
BH
Атрибут
BL
Не имеет значения
CH
Строка (верх)
CL
Столбец (верх)
DH
Строка (низ)
DL
Столбец (низ)
Плюс подробнейшее толкование, что подразумевается под словом «атрибут» (регистр BH):
| 0000 черный |
| 0001 синий |
| 0010 зеленый |
| 0011 голубой | Цвет фона
| 0100 красный |(разряды 654)
| 0101 пурпурный |
Цвет символа | 0110 коричневый |
(разряды 3210) | 0111 белый |
|
| 1000 серый |
| 1001 светло-синий |
| 1010 светло-зеленый | Повышенная
| 1011 светло-голубой | яркость
| 1100 светло-красный | (разряд 3)
| 1101 светло-пурпурный |
| 1110 желтый |
| 1111 яркий белый |
Мерцание
(разряд 7)
ВЫХОДНЫЕ ПАРАМЕТРЫ: отсутствуют (т.е. ни один регистр не меняется). (_8)
Входные строки гасятся в нижней части окна. (_9)
Нормальное значение байта атрибута — 07h. (_10)
Совсем недавно, если бы вам показали подобное «описание», вы бы ничего в нем не поняли и ужаснулись. Теперь же, после прочтения предыдущих глав курса, в эти «таблицы» вы, более или менее, но «въехать» должны! Тем более, что сейчас я сделаю комментарии для… хм… «отстающих» учеников (внимательно смотрим на циферки в скобках):
_1. Черным по белому, в толстом талмуде, описывающем функции прерываний, написано: «Драйвер видео вызывается по команде INT 10h и выполняет все функции, относящиеся к управлению дисплеем».
И далее. «…ДЕЙСТВИЕ: после входа управление передается одной из 18 программ в соответствии с кодом функции в регистре AH. При использовании запрещенного кода функции, управление возвращается вызывающей программе.
НАЗНАЧЕНИЕ: прикладная программа может использовать INT 10h для прямого выполнения функций видео…»
Вот что из этого следует:
- выполнив команду INT 10h, мы ВЫПОЛНЯЕМ одну из «функций видео»;
- так как функций видео — много, необходимо УКАЗАТЬ, КАКУЮ именно ФУНКЦИЮ из МНОЖЕСТВА мы хотим ВЫПОЛНИТЬ.
Дело в том, что «прерывание номер десять» — это не только «прокрутка окна», но и, например, «установка режима видео», «установка типа курсора», «установка палитры» и многое другое. Нас же интересует именно первое, поэтому из списка возможных значений (он приведен ниже) мы выбираем именно AH=06h.
Нижеследующая табличка называется «Функции, реализуемые драйвером видео»:
AH=00h Установить режим видео
AH=01h Установить тип курсора
AH=02h Установить позицию курсора
AH=03h Прочитать позицию курсора
AH=04h Прочитать позицию светового пера
AH=05h Выбрать активную страницу видеопамяти
AH=06h Прокрутить вверх активную страницу
AH=07h Прокрутить вниз активную страницу
AH=08h Прочитать атрибут/символ
AH=09h Записать символ/атрибут
AH=0Ah Записать только символ
AH=0Bh Установить палитру
AH=0Ch Записать точку
AH=0Dh Прочитать точку
AH=0Eh Записать TTY
AH=0Fh Прочитать текущее состояние видео
AH=13h Записать строку
Соответственно, если перед выполнением INT 10 в регистре AH будет значение 06h, то выполнится именно «прокрутить вверх активную страницу», а не что-то другое из «простыни» функций десятого прерывания…
Теперь читаем описание дальше (смотрим на циферки в скобках):
_2. Входные параметры? Что тут может быть непонятного? Даже запуск ракеты с атомной боеголовкой требует прежде всего указать координаты цели… Чего уж тут говорить об обыкновенной функции?
_3. То, о чем мы уже говорили — номер функции из «простыни».
_4. Т.е. на СКОЛЬКО строчек прокручивать. Вспомните так называемый «скроллинг» в любой прикладной программе. На кнопки Up, Down подвешен скроллинг на одну строчку (не путать с координатами курсора), а вот на PgUp и PgDown — штук на 18 строк (AL=01h и AL=12h соответственно). А вот AL=0, вместо того чтобы вообще не скроллировать (по идее), поступает наоборот — «скроллирует» все, что может.
_5. Скажем так — какого цвета будет окно и символы в нем после скроллирования.
_6. Как известно из школьного курса геометрии, прямоугольник можно построить по двум точкам. Это утверждение справедливо и для окна, в котором мы желаем проскроллировать наш текст.
_7. Резюме того, что было написано выше.
_8. К примеру, попала ли наша ракета в цель или нет ;).
_9. Если бы мы использовали функцию 07h, то было бы глубокомысленно написано, что «строки гасятся в верхней части окна».
_10. Это то самое, которое в DOS по умолчанию. Т.е. белыми буквами на черном фоне. Правда, это 07h лучше все же рассматривать как 00000111b но это уже совсем другая проблема…
#4. А теперь мы напишем программу. Ручками, без использования компилятора. Запускаем наш любимый debug.exe, вводим команду «а» и судорожно стучим по клавиатуре:
-a
119A:0100 xor al,al ;гашение всего окна
119A:0102 mov bh,70 ;белое окно
119A:0104 mov ch,10 ;четыре координаты прямоугольника
119A:0106 mov cl,10
119A:0108 mov dh,20
119A:010A mov dl,20
119A:010C mov ah,06 ;такая-то функция прерывания
119A:010E int 10 ;Go!!
119A:0110 int 20 ;выход...
119A:0112
-r cx ;в CX - сколько байтов программы
;писать 112h-100h=12h
CX 0000
:12
-n @int10.com
-w
Запись: 00012 байтов
Сначала запускаем из-под Norton Commander. Затем запускаем из-под debug. Трассируем. Открываем в HEX-редакторе. Смотрим на «бессмыслицу» шестнадцатеричных циферек. Медитируем, медитируем и еще раз медитируем…
1.6. Немножко программируем и немножко отлаживаем
#1. Тем, кто не в курсе — НАСТОЯТЕЛЬНО рекомендую проштудировать предыдущие части курса, иначе «въехать» будет сложно. Тем же, кто внимательно читал предыдущие главы, нижеследующие упражнения для ума и пальцев покажутся детским лепетом…
Давайте немножко видоизменим программу, которую мы писали в прошлый раз. Сделаем так, чтобы наше «окошко скроллинга» располагалось более или менее посередине экрана.
:0100 XOR AL,AL ;ПРИМЕЧАНИЕ: с целью экономии пространства и
:0102 MOV BH,10 ;времени мы немножко сократили наш DEBUG-й
:0104 MOV CH,05 ;листинг, т.е. убрали адрес сегмента и
:0106 MOV CL,10 ;машинные коды, соответствующие мнемоническим
:0108 MOV DH,10 ;командам :))
:010A MOV DL,3E ;А так, конечно, в оригинале первая строка вот
:010C MOV AH,06 ;как должна выглядеть:
:010E INT 10 ;11B7:0100 30C0 XOR AL,AL
:0110 INT 20
Теперь наша задача — написать программу, которая последовательно выводит пять таких окошек, причем каждое последующее окно «вложено» в предыдущее, а значение атрибута в шестнадцатеричной нотации на 10 больше предыдущего.
Если мы будем программировать ЭТО линейно (именно так для начала), то очевидно, что все, что мы должны сделать — это заданное количество раз (5) заставить машину выполнить вышеуказанные операции, изменяя перед «запуском прерывания» значения регистров AL, BH, CX, DX (полное описание 6-й функции 10-го прерывания ищите в прошлых главах).
#2. Вот к каким умозаключениям вы должны были придти, пораскинув мозгами. За атрибут (то бишь цвет) у нас отвечает регистр BH. Он был равен 10h, а нужно на 10h больше… это, значит, 20h будет…
Ладно… CX (он же CH и CL, как известно) — это ТОЖЕ ШЕСТНАДЦАТЕРИЧНЫЕ координаты левого верхнего угла нашего окошка. Чтобы «окно в окне» получилось, все это нужно на строчку больше сделать и на колонку больше тоже, и все считать в HEX’e. Получается, что в регистр СH нужно вместо значения 05h внести 06h, а в регистр CL вместо 10h — 11h.
А еще можно одним махом в CX записать число 0510h той же командой mov.
Ладно… DX (DH и DL соответственно) — это координаты правого нижнего угла прямоугольника. DH=10h-1h=Fh и DL=3Eh-1h=3Dh.
Ну, а AL=0 и AH=6 — это уже и ежу понятно из описания данной функции (mov AH,6) данного прерывания (INT 10h).
Все, что осталось — это набить в debug’е после команды «a» эти мнемоники энное количество раз. (кажется 5). Набиваем!!
:0100 XOR AL,AL ;первый раз
:0102 MOV BH,10
:0104 MOV CH,05
:0106 MOV CL,10
:0108 MOV DH,10
:010A MOV DL,3E
:010C MOV AH,06
:010E INT 10
:0110 INT 20
:0112 XOR AL,AL ;второй раз
:0114 MOV BH,10
:0116 MOV CH,06
:0118 MOV CL,11
:011A MOV DH,0F
:011C MOV DL,3D
:011E MOV AH,06
:0120 INT 10
:0122 INT 20 ;третий раз
:0124 XOR AL,AL
: и т.д
Правда, красивые циферки-буковки? Набиваем-набиваем! Если сейчас к вам подойдут недZенствующие приятели/коллеги и посмотрят, что вы тут колупаете, то ни черта не поймут и покрутят пальцем у виска. Привыкайте к этому. Только не говорите им, что пытаетесь сейчас получить Матрицу, ПОТОМУ ЧТО это неправда. А неправда это потому, что сейчас Матрица в очередной раз обманула вас!
#3. ВСЕ ПОТОМУ, ЧТО МОЗГАМИ ДУМАТЬ НАДО, А НЕ ТОЛЬКО СЛЕПО СЛЕДОВАТЬ РУКОВОДСТВУ!
У вас только первое окно прорисуется, сразу же после чего программа натолкнется на INT 20h и благополучно завершится! А следовательно, и все, что после первого CD 20 написано будет — останется проигнорированным! Исправляйте! (Т.е. уберите все INT 20 КРОМЕ ПОСЛЕДНЕГО).
Второй момент. ВОЗВРАЩАЕТ ЛИ это прерывание ЧТО-НИБУДЬ В РЕГИСТР AX? Смотрите описание. Ничего? Ну так какого черта тогда по новой вводить XOR AL,AL и MOV AH,06 и переприсваивать AH значение 6h, если и без того AH = 6h? Один раз ввести — более чем достаточно!
Скажите, какая мелочь- байтом больше, байтом меньше! А я скажу вот что — на то он и assembler, чтобы «байтом меньше».
Исправляйте!
#4. — Исправляйте? — возмутитесь вы — Да это же по-новому все вводить нужно!
— По-новому? — возмутимся мы в свою очередь! — Зачем по-новому? Вы что, с ума сошли?
1. Что вам мешает после команды «a» указать адрес, который вы желаете переассемблировать? И благополучно заменить старую команду на новую!
— А что делать, если не переассемблировать нужно, а вообще удалить?
2. Существует куча способов, что вы в самом-то деле! Например, в HEX Workshop с блоками шестнадцатеричных цифр запросто можно работать. Да и в других программах это можно делать — например, в HIEW или даже в Volcov Commander.
Кстати, если процессор встретит команду NOP, то он просто побездельничает некоторое очень короткое время.
ПРОБУЙТЕ!! В конце концов, ваша прога должна принять такой вот вид:
:0100 XOR AL,AL ;окошко первое
:0102 MOV BH,10
:0104 MOV CH,05
:0106 MOV CL,10
:0108 MOV DH,10
:010A MOV DL,3E
:010C MOV AH,06
:010E INT 10
:0110 MOV BH,20 ;окошко второе
:0112 MOV CH,06
:0114 MOV CL,11
:0116 MOV DH,0F
:0118 MOV DL,3D
:011A INT 10
:011C MOV BH,30 ;окошко третее
:011E MOV CH,07
:0120 MOV CL,12
:0122 MOV DH,0E
:0124 MOV DL,3C
:0126 INT 10
:0128 MOV BH,40 ;окошко четвертое
:012A MOV CH,08
:012C MOV CL,13
:012E MOV DH,0D
:0130 MOV DL,3B
:0132 INT 10
:0134 MOV BH,50 ;окошко пятое
:0136 MOV CH,09
:0138 MOV CL,14
:013A MOV DH,0C
:013C MOV DL,3A
:013E INT 10
:0140 INT 20 ;конец проги...
О, да! Получившаяся у вас программа написана долго и бездарно! Имейте это в виду :)). Мы же торжественно обещаем, что в последующих главах обязательно ее усовершенствуем. Да, вот еще что — особенно извращенные могут попытаться заменить INT 20 на JMP 100. Получится, конечно, не ахти, но все же — «анимация»
#5. А теперь мы попробуем ОТЛАДОЧНЫЙ прием! Все кракеры его знают и пользуются им для взлома софта. Имейте в виду, пока вы будете использовать его для своих исполняемых программ — вы программер, исправляющий ошибки, а как только попытаетесь использовать это для отвязки чужой программы от какого-нибудь серийного номера — ваша деятельность станет считаться неэтичной или незаконной. Так что думайте сами, что лучше — флаг в руки или барабан вместе с петлей на шею.
Итак, вводим приблизительно такую командную строку — debug имя_проги.com или же подгружаем прогу в отладчик командой «l» (от слова load) и трассируем, как вы уже неоднократно это делали.
Цель — «на лету» (без изменения кода) заставить первое окошко «рисоваться» не синим (BH=10h), а красным (BH=40h) цветом.
Мы просто приведем вам последовательность действий, а вывод «зачем это нужно» и прочие возможные выводы вы уже сами делать будете. Ок?
-t
AX=0000 BX=0000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0102 NV UP EI PL ZR NA PE NC
11B7:0102 B710 MOV BH,10
-
Состояние: обнулился регистр AX (первую команду MOV AL,AL мы не видим). Процессор готовится выполнить команду MOV BH,10. Дадим ему это сделать!
-t
AX=0000 BX=1000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0104 NV UP EI PL ZR NA PE NC
11B7:0104 B505 MOV CH,05
-
Состояние — в BX уже внесен код синего цвета, который нам по условию необходимо заменить на красный (т. е. заменить значение регистра BX с 1000h на 4000h).
Вот теперь-то мы и делаем это «на лету»:
-r bx
BX 1000
:4000
-
А действительно ли сделали? Проверим!
-r
AX=0000 BX=4000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0104 NV UP EI PL ZR NA PE NC
11B7:0104 B505 MOV CH,05
-
Состояние? BH теперь равно 40h! Мы «вклинились» между строчками:
:0102 MOV BH,10
:0104 MOV CH,05
И изменили текущую цепь событий, заставив программу делать ТО, ЧТО НАМ НУЖНО! Поздравляю!
А дальше — вводим команду «g» и даем нашей тупорылой программе исполниться. 1:0 не в пользу Матрицы!
Медитируйте!
1.7. Стек
#1. В средней школе, где когда-то учился автор, учителем физкультуры был настоящий зверь. Помимо того, что он заставлял нас школьников бегать-прыгать-подтягиваться, у него еще любимое наказание было — проходило оно в так называемом «зале тяжелой атлетики».
Что такое тяжелая атлетика вы наверняка знаете. Видели по телевизору, когда выходит на помост этакий здоровяк, и рвет собственное здоровье, поднимая штангу.
Штанга — это такая «палка», по бокам которой навешиваются так называемые «блины» — круглые плоские с дыркой посередине диски невероятной тяжести.
Хранятся же эти диски на штырях, которые представляют собой те же «палки», но вторкнутые вертикально в пол. На них и хранились диски — один на другой положенные.
«Наказание» заключалось вот в чем — тот «педагог» заставлял нерадивого ученика комплектовать штангу! Это элементарно сделать, если диски просто валяются на полу. Но когда они аккуратно сложены на штырь — это намного сложнее :(.
Садист! Чтобы достать со штыря диск заданной тяжести (который обычно находился внизу) необходимо было снять со штыря все «вышележащие» диски, достать самый нижний, а остальные снова надеть на штырь. А потом точно так же достать диск другой «тяжести» со второго штыря. А он как бы случайно тоже в самом низу. И так далее — до полной победы идиотизма над здравым рассудком. Не правда ли, изощренная пытка?
К чему это лирическое бредисловие? А к тому, что стек — это тоже своего рода штырь с блинами. И уж поверьте, упражняться вы с ним будете намного чаще, чем мы делали это на уроках физкультуры. С той лишь несущественной разницей, что у нас на следующий день болела спина, а у вас на следующий день будут болеть мозги.
Так вот, о стеке: «штырь» для блинов находится в оперативной памяти (где же еще?). А роль блинов выполняют хорошо знакомые нам всем регистры, вернее — их «значения».
Правила работы с ним те же — вы можете снять только верхний «блин». Чтобы получить самый нижний «блин» — вам нужно прежде снять все те, которые НАД ним.
Очевидно, что из десяти «блинов», которые вы надели на «штырь», первым будет сниматься последний из надетых (верхний), а последним — первый, то есть самый нижний.
Все очень просто: «первый пришел — последним уйдешь» и наоборот «пришел последним — уйдешь первым».
Это вам не очередь времен социализма… Это очередь «загрузки-разгрузки» стека!
#2. Для работы со стеком вам пока что необходимо знать только две команды: push и pop. Так как в качестве «блинов» у нас регистры, то, соответственно, необходимо после этих команд указывать и «имена собственные» помещаемых в стек значений регистров.
Соответственно:
push AX ;ПОМЕЩАЕТ В СТЕК значение регистра AX
pop AX ;ИЗВЛЕКАЕТ ИЗ СТЕКА значение регистра AX
Ну а как делать то же самое с остальными регистрами вы, наверняка, уже и сами догадались.
Очень важно помнить, каким «нездоровым» образом в стеке реализована ОЧЕРЕДЬ -поместить/извлечь. Помните, мы вас предупреждали, что нам нельзя верить на слово? Не верьте! А посему — обязательно убедитесь в истинности/ложности нашего голословного утверждения при помощи следующей программульки:
:0100 MOV AX,0001 ;AX = 1
:0103 PUSH AX ;В стек записана 1
:0104 MOV AX,0002
:0107 PUSH AX ;В стек записана 2
:0108 MOV AX,0003
:010B PUSH AX ;В стек записана 3
:010C MOV AX,0004
:010F PUSH AX ;В стек записана 4
:0110 MOV AX,0005
:0113 PUSH AX ;В стек записана 5
:0114 POP AX
:0115 POP AX
:0116 POP AX
:0117 POP AX
:0118 POP AX
:0119 INT 20
С очередностью заполнения стека, наверное, все понятно :). Я много про абстрактные «блины» загружал. А вот с адреса 114 начинается извлечение из стека. В какой последовательности это делается, вы можете увидеть сами, произведя трассировку этой небольшой проги.
-r
AX=0000 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=14DC ES=14DC SS=14DC CS=14DC IP=0100 NV UP EI PL NZ NA PO NC
14DC:0100 B80100 MOV AX,0001
-
Анализируем. Прога еще не начала работать, готовится выполниться команда по адресу 100. Делаем ШАГ!
-t
AX=0001 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=14DC ES=14DC SS=14DC CS=14DC IP=0103 NV UP EI PL NZ NA PO NC
14DC:0103 50 PUSH AX
-
Анализируем. AX=0001 — значит, команда выполнилась правильно :). Следующая команда, по идее, должна поместить 1 в стек.
-t
AX=0001 BX=0000 CX=001B DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000
DS=14DC ES=14DC SS=14DC CS=14DC IP=0104 NV UP EI PL NZ NA PO NC
14DC:0104 B80200 MOV AX,0002
-
И что? Команда выполнилась, но где мы можем увидеть, что в стек действительно «ушла» единица? Увы, но здесь это не отображается :). Проверим потом. Ведь логично предположить, что если эти значения действительно сохранились в стеке, то мы их потом без проблем оттуда извлечем, т.е. если найдем «там» наши 1, 2, 3, 4, 5 — значит все Ок.
А поэтому — дадим программе работать дальше до адреса 114 (не включительно), не вдаваясь в подробный анализ. Что тут анализировать? Если значение регистра AX последовательно меняется от 1 до 5 — значит, команда mov работает. А стек (команда push) проверим потом, как и договорились.
Проехали до адреса 114.
-g 114
AX=0005 BX=0000 CX=001B DX=0000 SP=FFF4 BP=0000 SI=0000 DI=0000
DS=14DC ES=14DC SS=14DC CS=14DC IP=0114 NV UP EI PL NZ NA PO NC
14DC:0114 58 POP AX
-
А вот теперь снова анализируем :). При следующем шаге выполнится команда, извлекающая некогда «запомненное» значение AX из стека.
Обратите внимание, регистр IP указывает на адрес (114) выполняемой команды. Мы с вами это уже проходили, не так ли?
Поехали дальше!!
-t
AX=0005 BX=0000 CX=001B DX=0000 SP=FFF6 BP=0000 SI=0000 DI=0000
DS=14DC ES=14DC SS=14DC CS=14DC IP=0115 NV UP EI PL NZ NA PO NC
14DC:0115 58 POP AX
-
Выполнился первый POP. Готовится выполниться второй. AX=5. Т.е., по сравнению с предыдущим шагом, вроде ничего не изменилось… Но на самом деле это не так. AX=5 — эта пятерка «загрузилась» из стека :)). В этом вы легко убедитесь, сделав следующий шаг трассировки.
-t
AX=0004 BX=0000 CX=001B DX=0000 SP=FFF8 BP=0000 SI=0000 DI=0000
DS=14DC ES=14DC SS=14DC CS=14DC IP=0116 NV UP EI PL NZ NA PO NC
14DC:0116 58 POP AX
-
Ууупс… AX=4 :). А команда, вроде, та же — POP AX
-t
AX=0003 BX=0000 CX=001B DX=0000 SP=FFFA BP=0000 SI=0000 DI=0000
DS=14DC ES=14DC SS=14DC CS=14DC IP=0117 NV UP EI PL NZ NA PO NC
14DC:0117 58 POP AX
-
AX=3
-t
AX=0002 BX=0000 CX=001B DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000
DS=14DC ES=14DC SS=14DC CS=14DC IP=0118 NV UP EI PL NZ NA PO NC
14DC:0118 58 POP AX
-
AX=2
-t
AX=0001 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=14DC ES=14DC SS=14DC CS=14DC IP=0119 NV UP EI PL NZ NA PO NC
14DC:0119 CD20 INT 20
-
AX=1 То есть нашлись-таки наши 1, 2, 3, 4, 5 :). Восстановились из стека. Теперь поверили? А то!
Еще раз обращаю ваше внимание на то, что последовательность записи (четыре PUSH’а) была — 1, 2, 3, 4, 5, а вот последовательность извлечения (четыре POP’а) — 5, 4, 3, 2, 1. Т.е. «последний пришел — первый ушел». Зарубите это себе на носу! (Как сделал это на своем перебитом носе наш школьный учитель физкультуры).
Медитируйте над этой темой до полного просветления! Иначе потом придется туго!
1.8. Цикл
#1. Наша программа для работы со стеком линейна. А линейное программирование — это плохо. Хотя и не всегда
Итак, давайте еще раз посмотрим на нашу программу для работы со стеком. С 100-го до 113-го адреса у нас имеется пять почти идентичных блоков. Изменяется только значение AX, но на одно и то же число — на единицу в большую сторону. То есть AX = предыдущее значение + 1. Это очевидно.
Еще более очевидно, что простая команда POP AX (с 114 по 119) повторяется у нас тоже 5 раз.
Мне почему-то сразу вспомнился анекдот о том, как два мента едут в машине, и один спрашивает у другого: «Глянь, работает ли у нас мигалка на крыше». Тот высунул в голову в форточку и говорит: «Работает-не работает-работает-не работает-работает-не работает…»
Так вот, не будем уподобляться этим нехорошим людям и сделаем нашу прогу более нормальной.
Добьемся мы этого с помощью так называемого «цикла». Цикл — это… Не буду давать общепринятые определения; кто хочет — поищите в книжках, благо, их навалом.
Скажу только: «сесть-встать, сесть-встать, сесть-встать» — это не цикл, а вот «сесть-встать и так три раза» — уже можно считать циклом.
Реализуется же он (цикл), например, при помощи регистра CX и команды LOOP следующим образом.
Число циклов заносится в регистр CX. После этого следует «простыня» из команд, которые вы хотите «зациклить», т. е. выполнить энное количество раз. Заканчиваться все это должно LOOP’ом с указанием адреса «строки», с которой необходимо начать цикл (обычно это «строка», следующая сразу же после mov СХ.
Давайте мы сначала «набьем» нелинейный вариант нашей проги, а потом разберемся, что там к чему. Набиваем:
:0100 ХOR AX,AX ; AX=0
:0102 MOV CX,0005 ; нижеследующий до команды LOOP кусок повторить CX раз
:0105 ADD AX,0001 ; AX=AX+1 (у нас же значение AX на 1 увеличивается...)
:0108 PUSH AX ; помещаем в стек
:0109 LOOP 0105 ; конец цикла; инициируем повторение; CX уменьшается на 1
:010B MOV CX,0005 ; второй цикл повторить тоже 5 раз
:010E POP AX ; достаем из стека
:010F LOOP 010E ; конец цикла; повторить! ; CX=CX-1
:0111 INT 20 ; выход из нашей "правильной" проги...
Наверное, вы уже поняли, что цикл повторяется до тех пор, пока CX не станет равен 0. Несмотря на то что CX — он как бы регистр общего назначения, для «зацикливания» используется именно он :). С остальными такой фокус не проходит. Это и есть так называемая «специализация регистров», о которой мы уже вскользь упоминали.
Протрассируйте эту программу! Искренне надеюсь, что вы поняли, чем это я вас тут загружал.
Медитируйте!
#2. А теперь вопрос на засыпку ;). Сколько раз выполнится следующий цикл:
:0102 MOV CX,0000
:0105 ADD AX,0001
:0108 LOOP 0105
Очевидный ответ — 0 раз. В CX же у нас занесен 0. Так вот — ответ неправильный.
Менее очевидный ответ — 1 раз! Ведь перед LOOP’ом сложение один раз все-таки выполнится. Так вот, этот ответ тоже неправильный.
Самые подозрительные могут сразу же посмотреть на этот цикл под отладчиком, и с удивлением обнаружат, что LOOP сначала уменьшает значение CX (0-1=FFFF), а потом уже проверяет, не равен ли он нулю. И с гордостью за задний ум своей головы воскликнут: FFFFh раз!
Так вот: этот ответ близок к истине, но тоже неправильный
Правильный ответ — цикл выполнится 10000h (65536d) раз.
Но только вы и мне не верьте! Истинно только то утверждение, которое вы сами проверили на практике. Медитируйте!
1.9. Немножко оптимизации
Как мы уже говорили, линейное программирование — это плохо, но не всегда. Сравните размеры ваших линейной и нелинейной программ. Не знаю, как у вас, но у нас линейная «весит» 27, а нелинейная — 19 байтов. Как по-вашему, какая быстрее работать будет?
— Ну, естественно, нелинейная, потому что она меньше! — скажете вы и будете неправы.
Попытайтесь оттрассировать «зацикленную». Не правда ли, она трассируется намного дольше своего линейного аналога?
Угу, всё поняли? Сам знаю, что ни черта.
Объясняю: в «зацикленной» программе «компутеру» приходится выполнять БОЛЬШЕ команд, нежели в «незацикленной».
Аргументирую это голословное утверждение следующей таблицей (построенной на основе трассировки):
Что делает линейная
Что делает нелинейная
AX=1
AX=0
Помещаем в стек 1
CX=5
AX=2
AX=AX+1=1
Помещаем в стек 2
Помещаем в стек 1
AX=3
Конец цикла — переход
Помещаем в стек 3
AX=AX+1=2
AX=4
Помещаем в стек 2
Помещаем в стек 4
Конец цикла — переход
AX=5
AX=AX+1=3
Помещаем в стек 5
Помещаем в стек 3
Достаем из стека 5
Конец цикла — переход
Достаем из стека 4
AX=AX+1=4
Достаем из стека 3
Помещаем в стек 4
Достаем из стека 2
AX=AX+1=5
Достаем из стека 1
Помещаем в стек 5
Выход
CX=5
Достаем из стека 5
Конец цикла — переход
Достаем из стека 4
Конец цикла — переход
Достаем из стека 3
Конец цикла — переход
Достаем из стека 1
Выход
Ну и как по-вашему, какую из двух простыней процессор быстрее обработает? Сказать вам по секрету? А вот ничего я вам не скажу! Сами думайте! :]
Как сейчас помню, был в моем Турбо-Си в преференсах к компилятору такой радиобуттон: «оптимайзить» по размеру или по скорости выполнения. Угадайте, на чем основан принцип этой оптимизации?
Только не вздумайте писать линейные проги! Пишите «нелинейные»! Нелинейную в линейную «переоптимайзить» — как два пальца намочить! А вот наоборот — :((
Резюме — бесплатный сыр бывает только в мышеловке, и за все надо платить. Компактность и скорость — обычно параметры конфликтующие, поэтому в каждом конкретном случае нужно выбирать, что предпочтительнее. Исследования, проведенные в свое время еще Кнутом, показали, что 80% времени затрачивается на выполнение 20% программы. Соответственно, рекомендуется тратить время на оптимизацию скорости именно тех 20% программы, а остальные можно оптимизировать по размеру (в частности, за счет циклов). Так и получается баланс между компактностью и скоростью программы ;).
1.10. Разборка с процедурами
#1. В разделе 1.7. #4 мы сделали глупую линейную программульку, выводящую окошки. Обещали, что в следующей главе сделаем ее менее «тупой», да отвлеклись почему-то на циклы и стек. То есть я-то знаю, ПОЧЕМУ, но вот вам об этом — не скажу! Догадайтесь сами. Итак, поехали…
Шаг первый. Внимательно посмотрев на «линейную» прогу из 1.7. #4 и прочитав «условие задачи» из главы 1.7. #1, вы обязаны возмутиться — зачем мы использовали команду MOV, если и ежу понятно, что отличия следующего окошка от предыдущего можно выразить более лаконично: BH=BH+10, CH=CH+1, CL=CL+1, DH=DH-1, DL=DL-1? И не нужно напрягать мозги, подсчитывая новое значение регистра вручную.
Если вы так подумали, то оказались совершенно правы! Программу из #4 запросто можно было представить в таком вот виде:
:0100 XOR AL,AL ;окошко первое
:0102 MOV BH,10
:0104 MOV CH,05
:0106 MOV CL,10
:0108 MOV DH,10
:010A MOV DL,3E
:010C MOV AH,06
:010E INT 10
:0110 ADD BH,10 ;окошко второе
:0113 ADD CH,01
:0116 ADD CL,01
:0119 SUB DH,01
:011C SUB DL,01
:011F INT 10
:0121 ADD BH,10 ;окошко третье
:0124 ADD CH,01
:0127 ADD CL,01
:012A SUB DH,01
:012D SUB DL,01
:0130 INT 10
:0132 ADD BH,10 ;окошко четвертое
:0135 ADD CH,01
:0138 ADD CL,01
:013B SUB DH,01
:013E SUB DL,01
:0141 INT 10
:0143 ADD BH,10 ;окошко пятое
:0146 ADD CH,01
:0149 ADD CL,01
:014C SUB DH,01
:014F SUB DL,01
:0152 INT 10
:0154 INT 20 ;конец программы
И несмотря на то, что размер ее оказался несколько большим, она тоже будет работать правильно :).
Но тут любой более или менее наблюдательный программер возмутится повторно — да что это за программа такая? В ней целых четыре раза повторяется один и тот же кусок:
:0143 ADD BH,10
:0146 ADD CH,01
:0149 ADD CL,01
:014C SUB DH,01
:014F SUB DL,01
:0152 INT 10
И знаете что? Этот наблюдательный программер будет прав! А если он еще и нехорошо выразится по поводу такого «неправильного» стиля программирования — будет прав… или почти прав…
Есть такой процесс — оптимизация, одной из особенностей которой является уменьшение параметризации (параметризация — вставка процедур в место вызова или фиксация значения отдельных параметров) и развертка циклов. То, что обычно такими вещами должен заниматься компилятор, суть меняет не сильно — тем более, что ассемблер оптимизацией сам не занимается :). Но до ассемблера мы с вами еще не добрались, поэтому говорить об этом пока еще рано.
Внимательно всмотритесь в полный текст программы и в этот выделенный кусок. И помедитируйте над ним до полного просветления текущей «обстановки»…
#2. Итак, у нас есть ПОВТОРЯЮЩАЯСЯ ЧАСТЬ программы. А еще у нас есть пальцы, которым, как правило, лень набивать длинные «простыни» программного кода. Это одна из многочисленных причин, по которым и придумали такого «зверя» как ПОДПРОГРАММУ (она же — ПРОЦЕДУРА, она же — ФУНКЦИЯ). Остальные причины мы рассмотрим попозже, а вот на счет «лени» поговорим прямо сейчас:
Если мы возьмем наш «часто повторяющийся» кусок программы и допишем в конец команду RET, то получится у нас именно ПРОЦЕДУРА — во всей своей красе:
:011E ADD BH,10 ;"точка входа"; она же - начало "тела".
:0121 ADD CH,01
:0124 ADD CL,01
:0127 SUB DH,01
:012A SUB DL,01
:012D INT 10 ;конец "тела"
:012F RET
Красота ее вот в чем заключается — процедуру можно «вызвать» командой CALL :)))
Все более чем просто. Когда в программе встречается CALL с указанием АДРЕСА-НАЧАЛА-ПРОЦЕДУРЫ (в нашем случае это 011E), то компьютер «идет» по этому адресу и выполняет все команды, расположенные между «точкой входа» (включительно) и командой RET, то есть так называемое «тело» процедуры.
RET — это тоже команда, но к «телу» (адреса 11E… 12D) она не относится. Она является «ОРГАНИЗАТОРОМ» этого «тела». Процессор, встретив команду RET, возвращает управление обратно после последнего CALL (т.е. «перепрыгивает» на строчку ниже «вызвавшего» данную процедуру CALL’а)…
Короче, CALL XXXX означает — «выполнить процедуру, начинающуюся по адресу XXXX». А RET означает — «конец процедуры» и, соответственно, переход на строчку ниже вызвавшего его CALL’а.
Если же говорить более формально и строго, то процедура — это средство обобщения, когда некоторая общая последовательность действий получает «имя», и потом при необходимости ПОВТОРНОГО ИСПОЛЬЗОВАНИЯ данного кода к нему просто идет обращение по «имени» (напомним, что в отличие от языков высокого уровня и даже ассемблера, в машинном коде от имен остаются одни только адреса, а язык, принимаемый DEBUG, является промежуточным между машинным кодом и ассемблером). Более того, следующим логическим шагом после обобщения является параметризация, когда некоторые части общего кода зависят от передаваемой извне информации (параметров), с чем очень хорошо знакомы программисты на языках высокого уровня. Но о параметризации и ее применении в ассемблере мы поговорим в другой раз.
Не ругайтесь. Мы знаем, что вы ни черта не поняли. А по сему набьем в debug’е эту прогу и посмотрим, что она делает.
Те, кто читал внимательно, могут отметить, что инструкция CALL по своему действию очень похожа на инструкцию генерации прерывания INT, с той лишь разницей, что аргументом CALL является адрес процедуры, а не индекс в таблице «векторов прерываний», где и хранится адрес обработчика прерывания (той же процедуры). А для особо продвинутых отметим, что в ранних моделях процессоров от Intel при подаче запроса на обработку от внешнего устройства контроллер прерываний, помимо собственно сигнала прерывания, посылал в процессор инструкцию CALL.
#3. Кстати, вы уже поняли, почему мы называем debug «до боли любимой программой»? Нет? Неужели вы еще не полюбили это произведение программерского гения всеми фибрами своей души? Еще нет? М-да… мы в вас разочаровались.
А по сему: — НАБИВАЕМ! — злобно кричим, брызгая слюной на эргономичный коврик:
:0100 XOR AL,AL ;первое окошко рисуем, как и раньше...
:0102 MOV BH,10
:0104 MOV CH,05
:0106 MOV CL,10
:0108 MOV DH,10
:010A MOV DL,3E
:010C MOV AH,06
:010E INT 10
:0110 CALL 011E ;четыре раза вызываем подпрограмму,
:0113 CALL 011E ;начинающуюся по адресу 011E
:0116 CALL 011E
:0119 CALL 011E
:011C INT 20 ;выход из программы...
:011E ADD BH,10 ;начало процедуры
:0121 ADD CH,01
:0124 ADD CL,01
:0127 SUB DH,01
:012A SUB DL,01
:012D INT 10
:012F RET ;конец процедуры
Не правда ли, красиво получилось?
Первое, что вас может смутить — это то, что команда выхода (INT 20) расположена не там, где вы привыкли, то есть не в конце программы.
Ну что я вам могу на это ответить? Концы — они-то разные бывают! Последняя строчка в листинге вовсе не означает, что последней будет выполняться именно она. И это не должно вас смущать! А если все же смущает — смотрим, как работает эта прога из-под отладчика.
Итак, до адреса 0110 вам все должно быть понятно, мы это рассматривали. Трассируем дальше…
— Что значит «трассируем»? — попросите вы напомнить.
Мысленно мы ругаем вас нехорошими словами (ну сколько раз повторять-то можно!), а вслух скажем: Команда «T» и Enter. Команда «T» и Enter. Команда «T» и Enter…
Команда CALL 011E по адресу 0110 говорит процессору: «Дальше мы не пойдем, пока не выполним простыню, начинающуюся по адресу 011E». И далее, естественно, следует переход на этот адрес.
Входим в тело процедуры, начиная с 011E, и выполняем команды до 012D включительно…
А теперь внимательно смотрим, на какой адрес нас «перекинет» команда RET.
На 113-й? И это правильно! По 113-му адресу у нас какая команда? Да вот опять CALL 011E!
Опять процедура с адреса 011E, опять RET[URN] на строку ниже, то есть на 116…
И так далее, до того момента, пока следующей строчкой не окажется INT 20 — собственно, на этом и программе конец.
Ну оно и ежу понятно, что, несмотря на то что INT 20 — не в конце программы, последним выполнится именно он.
Короче, куда бы вас не посылали всяческие «столбы с указателями», конец вашего пути только один… А плутать вокруг да около этого конца вы можете сколько вам заблагорассудится…
Кстати, именно это и является одной из многочисленных тайн программинга.
Кто после этого скажет, что программисты — недZенствующие люди?
#4. Те, кто внимательно ознакомились с циклами, они и на этом не остановятся! Посмотрев на адреса 110…119, они вообще возьмут и возомнят себя воистину крутыми парнями! Знаете, что они напишут? А вот что (предвидим!):
:0100 XOR AL,AL
:0102 MOV BH,10
:0104 MOV CH,05
:0106 MOV CL,10
:0108 MOV DH,10
:010A MOV DL,3E
:010C MOV AH,06
:010E INT 10
:0110 MOV CX,0004
:0113 CALL 011A
:0116 LOOP 0113
:0118 INT 20
:011A ADD BH,10
:011D ADD CH,01
:0120 ADD CL,01
:0123 SUB DH,01
:0126 SUB DL,01
:0129 INT 10
:012B RET
То бишь еще и CALL в цикл при помощи MOV CX,4 и LOOP’а «закрутят». И что? А попробуйте!
Что, не «пашет»? А что надо делать, если «не пашет, а должно бы»? Правильно! Смотреть из-под отладчика!
Смотрим? Если посмотрите, то сразу же и «загвоздку» увидите — CX, использованный в качестве «счетчика» циклов, «перебивает» тот же CX, но используемый как «координаты верхнего левого угла окна». И что с этим делать прикажете?
Вот вы и столкнулись с одной из самых больших проблем. В процессорах фирмы Intel есть только 4 регистра общего назначения (и то в большинстве случаев — специализированных). Помните, мы вам говорили об этом?
А теперь попробуйте выкрутиться из этой нехорошей ситуации с использованием стека :). Кстати, весьма «мозгопрочищающая» задачка :).
1.11. Переходы
#1. «Переходы» бывают разные. Если вы пришли в гости, а вас просто послали к черту — такой переход называется «безусловный». А нежели вам сказали: «Если без пива — то иди к черту, а если с пивом — тогда проходи», — то это уже «условный» переход.
Соответствено, для успешного перехода необходимо указать: ПРИ КАКОМ УСЛОВИИ выполнить переход, КУДА ПЕРЕЙТИ, ну и, наконец, сам пинок под зад нужно СДЕЛАТЬ, чтобы переход «гостя» в заданном направлении все-таки «состоялся».
Безусловный переход у нас «делает» мнемоническая команда JMP, после которой следует указать адрес, на который «компьютер» должен пойти «на» ;). В данном случае УСЛОВИЕМ у нас будет «при любых обстоятельствах»: хоть пустой, хоть с пивом, хоть с … все равно. Когда рисовали окошки, вы уже использовали эту команду для создания «спецдефекта». Если кто еще не понял, что делает эта команда — к нему (см. 1.7 #4) и отсылаю. Сделайте «спецдефект» и посмотрите на него под отладчиком. Когда до вас дойдет, почему мы там не предусмотрели выхода (INT 20h) — можете переходить к п.2 текущей главы.
#2. Условный переход у нас организуется в два шага. На первом шаге мы вычисляем условие («принес ли пиво?»), на втором шаге «посылаем» или не «посылаем» — в зависимости от результатов вычислений. Можно привести такую аналогию — на первом шаге два груза кладутся на весы, сравнивающие их массу. Соответственно, возможны только три их положения: наклон влево (груз в левой чашке тяжелее), наклон вправо (груз в правой чашке тяжелее) и равновесие. На втором шаге мы предпринимаем действия в зависимости от положения весов.
Например, на первом шаге можно использовать как «аптекарские весы» инструкцию CMP, которой обязательно нужно указать, ЧТО и С ЧЕМ она будет сравнивать.
Пишем, например,
CMP AX,BX
В зависимости от значений регистров у нас возможны следующие состояния: «наклон влево» (AX &tt; BX), «наклон вправо» (AX > BX) и «равновесие» (AX = BX). Таким образом ВЫЧИСЛЕНИЕ УСЛОВИЯ у нас уже организовано :). Только условие не бинарное, а есть еще и «серединный вариант» (и даже несколько других!). Это нормально. Это для того сделано, чтобы мы могли выражения типа «больше-или-равно», «меньше-или-равно» да и просто «равно» в своих программах использовать…
Итак, УСЛОВИЕ есть. Теперь решаем, что нам делать при том или ином условии. Вот далеко не полный список возможных «прыг-скоков»:
- JE — переход если равно;
- JNE — переход если не равно;
- JA — переход, если больше;
- JAE — переход, если больше или равно;
- JB — переход, если меньше;
- JBE — переход, если меньше или равно…
- и т.д.
Естественно, что после мнемоники («прыгнуть, если») должен стоять АДРЕС, куда нужно «прыгнуть», если условие соблюдено. Если же условие не соблюдено, то прыжок не происходит, и выполняется нижеследующая строка программы.
Задание на медитирование — зрительно представьте себе «весы правосудия». И побросайте на их чашки разную шестнадцатеричную дрянь в различных «пропорциях» и «комбинациях». Просветлиться вы должны следующим образом — в какую бы сторону эти ваши «весы» ни склонялись, вы все равно заставите систему работать так, как ЭТО вам угодно! «Весы» — они только констатируют факт. А вот «приговор» выносят судьи. Хе… и пусть после этого только кто-нибудь скажет, что программерам чужда политика — дело, как известно, весьма грязное.
А о чем это мы? Ах да, переходы…
#3. Продолжим программировать, что ли? Напишем что-нибудь красивое и неизменно тупое? С использованием условных и безусловных переходов?
Поехали! Слабаем мы сейчас что-то наподобие графического редактора :)). Не верите?
У-у-у… Сложная задачка! Если въедете, что да как — значит, молодцы! Значит, разобрались-таки с дzебагом! Значит, подключились-таки к программерскому эгрегору и более или менее привели в порядок свои мозги :)… А это сложная штука, мы вам скажем — мозги в порядок приводить! Особенно когда есть куча инструментов, которые «порядок в коде» сами как бы наводят :).
Думаете, вы по нашим текстам программировать учитесь? По-настоящему программировать мы еще не начали! Все, чем мы пока занимаемся — это приводим в порядок свои мозги и тренируемся на кнопки клавиатуры нажимать :)). А вот ско-о-оро НАЧНЕМ… тогда «прощай, здоровье» будет настоящее!
Итак, сначала рассмотрим прерывания, которые в нашем «графическом редакторе» будут использоваться. Их три штуки, и все — BIOS’овские:
mov AH,00 ;функция 0 прерывания 10h устанавливает "режим видео"
mov AL,04 ;"на входе" (в регистре AL) - номер режима .
int 10h ;если AL=4, то устанавливается цветной 320х200 графический режим
mov CX,64h ; CX и DX - координаты точки
mov DX,64h
mov AH,0Ch; функция 0Ch (в регистре AH!) прерывания 10h рисует точку.
mov AL,1Bh ;в AL - "код цвета" этой точки.
int 10h
Опять-таки — подробности о координатах и «кодах» цвета ищите сами!! Благо, знаете, где искать.
mov AH,00 ;функция 0 прерывания 16h
int 16h ;"читать код нажатой клавиши"
Эта функция считывает код сканирования и код символа (клавиши на клавиатуре и соответствующий ей ASCII-код) из буфера клавиатуры (есть такой). Если в буфере ничего нет — она ждет, пока там что-нибуть появится. То есть ЖДЕТ, чтобы вы нажали на какую-нибудь клавишу, код которой будет занесен в регистр AX. Причем в AL — «символ», а вот в AH — так называемый «код сканирования»…
Кодами вы пока голову не забивайте. Достаточно знать, что после нажатия клавиши Up в AH «попадет» значение 48h, Down — 50h, Left — 4Bh, Right — 4Dh.
Как работает последний кусок кода, обязательно проверьте под отладчиком, это полезно :).
#4. И лезем, лезем в наш горячо любимый DZEBUG, дабы набить там драгоценные строчки машинного мнемонического никому-кроме-вас-непонятного кода!
-a
:0100 MOV AH,00 ;устанавливаем графический режим
:0102 MOV AL,04
:0104 INT 10
:0106 MOV CX,0064 ;координаты Первой Точки
:0109 MOV DX,0064
:010C MOV AH,0C ;рисуем точку!
:010E MOV AL,1B
:0110 INT 10
:0112 MOV AH,00 ;ждем нажатия на клавишу
:0114 INT 16
:0116 CMP AH,4B ;а не нажат ли у нас Left?
:0119 JE 012A ;если да - то "прыг"!
;если нет - то следующая строчка
:011B CMP AH,4D ;а не нажат ли у нас Right?
:011E JE 012D
:0120 CMP AH,48 ;а не нажат ли у нас Up?
:0123 JE 0130
:0125 CMP AH,50 ;а не нажат ли у нас Down?
:0128 JE 0133
:012A DEC CX ;задаем новые координаты, в зависимости
:012B JMP 010C ;от нажатой клавиши - и скок в начало!
:012D INC CX
:012E JMP 010C
:0130 DEC DX
:0131 JMP 010C
:0133 INC DX
:0134 JMP 010C
Тут один из автору вставили шпильку:
«Не хватает проверки и выхода (со сбросом видеорежима!) по Esc — такие действия должны быть обязательным атрибутом, а не домашним заданием.»
Совершенно верная шпилька, товарищи! Но все равно — пусть это будет домашним заданием.
Если вы все ввели правильно — должно заработать! Полюбуйтесь плодами своей медитации… Красиво?
#5. А сейчас мы это все дело прокомментируем:
- адреса 100…200 — устанавливаем графический режим, указываем функцию (100), указываем номер режима (102) и вызываем прерывание (104);
- адреса 106…109 — инициализируем координаты первой точки. Координаты последующих точек будут определяться «динамически» — в зависимости от нажатой клавиши;
- адреса 10С…110 — рисуем точку. Первый раз — в координатах, инициализированных командами по адресам 106 и 109. Все последующие разы — по координатам, «инкрементированным» или «декрементированным» (во словеса!) по адресам: 12A, 12D, 130 и 133;
- адреса 112…114 — ждем нажатия на клавишу;
- адреса 116…128 — «щемим» нужные нам клавиши. «Взвешиваем». На каждую из курсорных клавиш по адресам 12A…134 приготовлены «обработчики». Если найдена «нужная клавиша», то делаем прыг на «обработчик» этой клавиши;
- aдреса 12A…134 — в этом блоке определяется, что делать с координатами следующей точки. После чего — прыжок на «рисуем точку» :).
Правда, здорово получилось?
1.12. Данные
#1. Работать с кодом мы с вами научились. Сейчас поучимся заставить наш код обрабатывать данные…
Итак, запускаем DZEBUG и вводим следующую команду:
-e cs:115
Которая означает: «набиваем память всяким дерьмом начиная со смещения 115».
В ответ он вам выплюнет:
17B3:0115 00.
Что означает: байт по смещению 115 равно 00. И точка. Но это не простая точка — это приглашение ввести НОВОЕ ЗНАЧЕНИЕ этого байта. Когда вы его ввели, нужно нажать на пробел.
Если вы вознамеритесь последовательно ввести 1,2,3,4,5, то это будет выглядеть приблизительно так:
17B3:0115 00.1 75.2 AD.3
17B3:0118 66.4 FF.5 [Enter]
А теперь делаем дамп памяти и смотрим, что за дрянь у нас получилась…
А ведь получилoсь же!!
#2. Мы запросто умеем «присваивать» регистру любое значение (mov AL,1C какой-нить), запросто можем «копировать» содержимое одного регистра в другой (mov AL,BL например)… А сейчас мы с вами научимся при помощи той же команды MOV еще и с данными из памяти работать.
Все проще пареной репы… Если мы напишем
MOV AL,[115]
то в результате выполнения этой команды в регистр AL «внесутся» две шестнадцатеричные циферки (байт), которые по адресу 115 находятся. То есть в нашем случае AL станет равным 1.
А теперь посмотрите, что делает «обратная» команда:
mov AL,55
mov [115],AL
В первой строчке мы присвоили AL значение 55, а второй строчкой «скопировали» значения регистра в байт по адресу 115. Правда, проще некуда?
Обязательно посмотрите на этот процесс под отладчиком!
#3. А еще вот какой изврат с этим можно делать:
mov BX,115
mov AL,[BX]
Сие присваивает регистру AL значение байта по адресу 115 :). Ну… через посредника «BX» присваивает! Который у нас «переменная», как известно :).
mov AL,1C
mov BX,115
mov [BX],AL
А этот кусок кода у нас «записал» 1C в сегмент данных по адресу 115 :). Ну, и извращения наподобие:
mov AL,[BX+1]
и
mov [BX+1],AL
Тоже весьма и весьма полезны в программерском деле :).
Короче: все, что в квадратных скобках, — это адрес в памяти, с которым вы собираетесь «работать». Другой вопрос, что этот адрес может быть «составным»…
#4. Низкоуровневый Paint мы с вами уже писали. Сегодня напишем низкоуровневый дZенский EXCEL.
Задание простое… Есть у нас табличка типа:
1 8 ?
2 9 ?
3 1 ?
4 2 ?
5 2 ?
в которой данные в формате HEX. И все, что нам нужно с ними сделать — это просуммировать каждую «строчку», а сумму занести в третий «столбец»… В EXCEL’е это делается элементарно… А на машинном уровне, в общем-то, не намного сложней!!
Для начала мы наберем «исходные данные» и зарезервируем место (например, забьем нулями) под третий столбец, в который собираемся помещать результат…
Набиваем блок данных, начиная с адреса, например, 115:
-e ds:115
17EA:0115 01.1 08.8 02.0
17EA:0118 09.2 02.9 00.0 03.3 03.1 00.0 04.4 04.2
17EA:0120 00.0 05.5 05.2 00.0
Вот так это у меня в DZEBUG’е выглядело :). Только я еще дамп посмотрел, правильно ли я ввел:
17EA:0110 03 E2 F3 CD 20 01 08 00-02 09 00 03 01 00 04 02 .... ...........
17EA:0120 00 05 02 00 6A 87 04 FF-76 FE 57 57 9A 5C 6C 87 ....j...v.WW.l.
Вроде правильно :)). Ну а программу я вот какую придумал:
17EA:0100 BB1501 MOV BX,0115
17EA:0103 B90500 MOV CX,0005
17EA:0106 8A07 MOV AL,[BX]
17EA:0108 024701 ADD AL,[BX+01]
17EA:010B 884702 MOV [BX+02],AL
17EA:010E 83C303 ADD BX,3
17EA:0111 E2F3 LOOP 0106
17EA:0113 CD20 INT 20
В BX я занес адрес начала блока данных (он же — верхний левый угол нашей таблицы). В CX внес 5, чтобы столько раз цикл выполнился (LOOP по адресу 111). А тело цикла вообще простое:
Команда по адресу 106 забирает в AL цифирь из первого столбца.
108 — суммирует «цифирь из первого столбца с цифирью из второго столбца» (сумма, само собой, в AL’е остается).
10B — записывает сумму в третий столбец :).
Ну и ADD BX,3 для перехода на следующую строчку :).
И все на этом…
Сделайте трассировку (внутрь INT 20 залезать не надо) и посмотрите на дамп нашего блока данных
Я и говорю: ПРОЩЕ ПАРЕНОЙ РЕПЫ!!
#5. Видите? В качестве переменных «в компьютере» можно использовать не только регистры, но и «куски» памяти! А уж там вы можете клепать свои переменные в почти неограниченном количестве! Единственное, что нужно иметь ввиду: с переменными-регистрами компьютер работает намного быстрее, чем с переменными-в-памяти :).
Кстати, если вы хотите сохранить плод своих сегодняшних трудов на веник, то имейте ввиду, что вы и сегмент данных тоже должны сохранить! То есть: вам нужно сохранить весь «диапазон» от адреса 100 до 123 включительно :).
Ну и, само собой, при попытке дизассемблирования с адреса 115 у вас абракадабра пойдет… мы об этом уже говорили и упоминали один из принципов фон Неймана.
Диагноз
Полагаю, вы уже поняли, что значит «выучить язык ассемблера» и теперь с удовольствием кинете грязью в того, кто скажет вам, что это сложно
Что значит «выучить язык», и что значит «программировать»? А проводите сами границы между этими понятиями! Только имейте в виду, кто скажет «выучить — значит все команды запомнить — тот дурак :((.
Слова и понятия извращать можно по-всякому. Переопределите собственный тип и носите свежеобмоченные пеленки в полной уверенности, что это носки… В моем понимании «знать ассемблер» и «изучить на ассемблере» — синонимы (хотя лингвисты могут придраться, но мне это пофиг). Согласно границам, которые провел для себя (гы… ну и для вас немножко) автор курса, ассемблер вы уже знаете, а вот программировать на нем пока еще не умеете…
Да о чем это я, в общем-то? (Утомлен кофием, поэтому речь несвязна)? Просто хотел сообщить вам, что первая часть курса закончилась. Вооружившись справочником команд и прерываний, вы уже можете программировать под дос. Если вы внимательно штудировали предыдущие главы, то идеология этого дела (под дос) вам уже должна быть понятна как 2х2=100b.
Введение в машинный код
Содержание
- Вы читали «Хроники Амбера» Роджера Желязны? Там есть такой эпизод:
Главный герой находится в заточении. В абсолютной тьме. У него были выколоты глаза, но за год они регенерировали, и зрение постепенно к нему возвращается…
И однажды каким-то чудом в одной камере с ним оказывается загадочный Дворкин — создатель Лабиринта. Именно «чудом» — он просто появился неизвестно откуда. Он тоже находится «в заключении», но, в отличие от Корвина (главного героя), может спокойно ходить через каменные стены.
Удивленный Корвин спрашивает его:
— Как ты оказался в моей камере? Ведь здесь нет дверей.
Дворкин отвечает:
— Двери есть везде. Просто нужно знать, как в них войти.
Будем считать это эпиграфом…
1.1. Система счисления
#1. Наверняка среди ваших знакомых есть «крутые» программисты, или люди, таковыми себя считающие ;). Попробуйте как-нибудь проверить их «на вшивость». Предложите им в уме перевести число 12 из шестнадцатеричной в двоичную систему счисления. Если над подобным вопросом «крутой программист» будет думать дольше 10 секунд — значит он вовсе не так крут, как говорит…
#2. Система счисления (сие не подвластное человеческой логике определение взято из математической энциклопедии) — это совокупность приемов представления обозначения натуральных чисел. Этих «совокупностей приемов представления» существует очень много, но самая совершенная из всех — та, которая подчиняется позиционному принципу. А согласно этому принципу один и тот же цифровой знак имеет различные значения в зависимости от того места, где он расположен. Такая система счисления основывается на том, что некоторое число n единиц (radix) объединяются в единицу второго разряда, n единиц второго разряда объединяются в единицу третьего разряда и т. д.
#3. «Разрядам» нас учили еще в начальных классах школы. Например, у числа 35672 цифра «2» имеет первый разряд, «7» — второй, «6» — третий, «5» — четвертый и «3» — пятый. А «различные значения» цифрового знака «в зависимости от того места, где он расположен» и «объединение в единицу старшего разряда» на тех же уроках арифметики «объяснялось» следующим образом:
35672 = 30000 + 5000 + 600 + 70 + 2 35672 = 3*10000 + 5*1000 + 6*100 + 7*10 + 2*1 35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100 (1)
#4. Очень наглядно это отображают обыкновенные счеты. Набранное на них число 35672 будет выглядеть… см. рисунок слева в общем…
Чтобы набрать число 35672 мы должны передвинуть влево две «костяшки» на первом «прутике», 7 на втором, 6 на третьем, 5 на четвертом и 3 на пятом. (У нас ведь 1 «костяшка» на втором — это то же самое, что и 10 «костяшек» на первом, а одна на третьем равна десяти на втором, и так далее…) Пронумеруем наши «прутики» снизу вверх — да так, чтобы номером первого был «0»… И снова посмотрим на наши выражения:
35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100
Это (если сверху вниз считать) сколько на каждом «прутике» «костяшек» влево отодвинуто.
35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100
Это номер прутика (самый нижний — 0), на котором отодвинуто определенное число костяшек.
35672 = 3*104 + 5*103 + 6*102 + 7*101 + 2*100
Это на каждом прутике — по 10 костяшек нанизано, не все влево отодвинуты, но всего-то их — 10!
Кстати, красненькое 10 в последнем выражении соответствует основанию (radix) системы счисления (number system).
#5. Пальцев на руках у человека 10, поэтому и считать мы привыкли в системе счисления с основанием 10, то есть в десятичной. Если вы хорошо представляете себе счеты и немного поупражнялись в разложении чисел аналогично выражению 1, то перейти на систему счисления с основанием, отличным от привычной, особого труда для вас не составит. Нужно всего лишь представить себе счеты, на каждый прут которых нанизано не привычные 10 костяшек, а… скажем, 9 или 8, или 16, или 32, или 2 и… попробовать мысленно считать на них.
#6. Для обозначения десятичных чисел мы используем цифры от 0 до 9, для обозначения чисел в системах счисления с основанием менее 10 мы используем те же цифры:
radix 9 - 0, 1, 2, 3, 4, 5, 6, 7, 8; radix 8 - 0, 1, 2, 3, 4, 5, 6, 7; radix 2 - 0, 1 и т. д.
Если же основание системы счисления больше десяти, то есть больше, чем десять привычных нам чисел, то начинают использоваться буквы английского алфавита. Например, для обозначения чисел в системе счисления с основанием 11 «как цифра» будет использоваться буква А:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A
В системе счисления с основанием 16 — буквы от A до F:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
И так далее…
Правда, при определенном основании (при каком?) буквы аглицкого алфавита закончатся…
Но нам это, пока что, глубоко фиолетово, так как работать мы будем только с тремя radix-ами: 10 (ну естественно), 16 и 2. Правда, если кто на ДВК поизучать это дело собирается, тому еще и radix 8 понадобится.
#7. Числа в любой системе счисления строятся аналогично десятичной. Только на «счетах» не с 10, а с другим количеством костяшек.
Например, когда мы пишем десятичное число 123, то имеем в виду следующее:
1 раз 100 (10 раз по 10) + 2 раза 10 + 3 раза 1
Если же мы используем символы 123 для представления, например, шестнадцатеричного числа, то подразумеваем следующее:
1 раз 256 (16 раз по 16) + 2 раза 16 + 3 раза 1
Короче — полный беспредел. Говорим одно, а подразумеваем другое. И последнее не для красного словца сказано. А потому, что так оно и есть…
Истина где-то рядом…
#8. Трудность у вас может возникнуть при использовании символов A, B, C и т. д. Чтобы решить эту проблему раз и навсегда, необходимо назубок вызубрить ма-а-аленькую табличку «соответствия» между употребляемыми в «компьютерном деле» систем счисления:
radix 10 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
radix 16 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | A | B | C | D | E | F |
radix 2 | 0 | 1 | 10 | 11 | 100 | 101 | 110 | 111 | 1000 | 1001 | 1010 | 1011 | 1100 | 1101 | 1110 | 1111 |
Следуя этой таблице, число 5BC в шестнадцатеричном формате «строится» так:
5 раз 256 (16 раз по 16) + 11 раз 16 (10 - потому что по таблице B как бы равно 11) + 12 раз 1
А теперь, если пораскинуть мозгами, с легкостью переведем 5BC из шестнадцатеричной в десятичную систему счисления:
5*256 + 11*16 + 12 = 1468
Вот и объединили цифры с буквами. Пространство со временем поучимся объединять немного позже — если не испугаетесь сложностей низкоуровневого программирования.
В общем-то решать вам. В Delphi тоже много чего объединять можно.
#9. Двоичная система по-компьютерному обзывается «bin», «родная» десятичная — «dec», а шестнадцатеричная — «hex». Это так компьютерщики обозвали те системы счисления, с которыми имеют дело… А обозвали потому, что у них ведь полный бардак в голове, оказывается!
Например, 10 — что это за число? Да это вообще не число! Палка и барабан — и только… А вот 10d или же 10_10 — уже понятно, что это — число, соответствующее количеству пальцев на обеих руках. И именно на обеих, а не на двух. Почему не на двух? — А потому что на двух в какой системе? Ежели в двоичной, так это на десяти! То бишь 100, если в десятичной…
Вот и придумали программисты после числа буковку писать — b, d или h. А самые ленивые еще и директиву специальную придумали: напишут в самом начале программы какой-нибудь .radix 16
и будут автоматически все числа, которые без этих букв, за шестнадцатеричные приниматься.
#10. Еще немного про перевод между «радиксами». (Вообще-то это плевое дело, конечно, если представляешь себе, что такое «совокупность приемов представления обозначения натуральных чисел»).
Например, преобразование числа 42936 из десятичного в шестнадцатеричный формат проводится следующим образом (в скобках — остаток):
42936/16 = 2683(8) 8 - младшая цифра 2683/16 = 167(11) B (11d=Bh по таблице) 167/16 = 10(7) 7 10/16 = 0(10) A - старшая цифра -------------------------------------- 42936d=A7B8h
А вот и обратный процесс — перевод из HEX в DEC числа A7B8h:
10*16=160 160+7=167 (10 - потому что Ah=10d) 167*16=2672 2672+11=2683 2683*16=42928 42928+8=42936 -------------------------------------------- A7B8h=42936d
Преобразования чисел в системы счисления с другим основанием проводятся аналогично… Счеты! Обыкновенные счеты, только с «плавающим» числом «костяшек» на каждом «прутике»…
#11. Если честно, то конкретный «рисунок» цифр — единица там палкой обозначается, двойка — лебедем — это все лишь историческая случайность. Мы запросто можем считать в «троичной» системе счисления с цифрами %, *, _ (где запятая — это знак препинания, а вовсе не число):
%, *, _, *%, **, *_, _%, _*, __, *%%, *%*, *%_, **%...
Или использовать родные цифры в десятичной системе счисления, но по другому «вектору упорядоченных цифр» — 1324890576:
1, 3, 2, 4, 8, 9, 0, 5, 7, 6, 31, 33,34, 34,38, 39, 30, 35, 37...
Правда, этим немножко затрудняется понимание происходящего? А ведь тоже десятичная система! И рисунок цифр как бы знакомый :-)))
Или вообще считать в 256-ричной системе счисления, используя в качестве «рисунка цифр» таблицу ASCII-символов! (По сравнению с вами, извращенцами, любой Биллгейтс будет девственником казаться!!).
#12. Теперь самая интересная часть Марлезонского балета.
Компьютер, как известно, считает только в двоичной системе счисления. Человеку привычна десятичная. Так нахрена еще и шестнадцатеричную какую-то знать нужно?
Все очень просто. В умных книжках пишут, что «шестнадцатеричная нотация является удобной формой представления двоичных чисел». Что это значит?
Переведите число A23F из шестнадцатеричной «нотации» в двоичную. (Один из возможных алгоритм приведен в п.10.). В результате длительных манипуляций у вас должно получиться 1010001000111111.
А теперь еще раз посмотрите на таблицу в п. 8. (которую вы как бы уже и выучили) и попробуйте то же самое сделать в уме :
Ah=1010b 2h=0010b 3h=0011b Fh=1111b A23Fh = 1010 0010 0011 1111b
Каждой шестнадцатеричной цифре соответствует тетрада (4 штуки) ноликов и единичек. Все, что потом нужно сделать — «состыковать» эти тетрады. Круто? Вас еще не то ждет!
#13. Кстати (наверняка вы это уже знаете):
00000123 = 123, но!! 123 <> 12300000
… но это так… кстати…
#14. И, напоследок, еще несколько слов про HEX и BIN :). Зайдите в Norton Commander, наведите указатель на какой-нить файл и нажмите там F3. А когда он откроется — на F4. Там какие-то нездоровые циферки попарно сгруппированы. Это и есть «нолики и единички» (которыми в компьютере все-все-все описывается), но в шестнадцатеричном формате…
Следует основательно разобраться с системой счисления. Минимум, что должен вынести из этой главы юзвер, вступивший на скользкий путь низкоуровневого программирования — это научиться переводить числа между DEC, HEX и BIN… хе-хе… В УМЕ!
1.2. Регистры
#1. Наверняка вы имеете представление о том, что такое переменная. Наиболее продвинутые даже знают, что переменная имеет тип. Кажется вполне естественным, что любой высокоуровневый язык программирования позволяет создавать любое количество переменных того или иного типа…
Так вот, господа — при программировании на ассемблере вас ждет большая неожиданность. Потому что для всех ваших навороченных вычислений разрешается использовать только несколько переменных с фиксированными «именами собственными» и имеющих фиксированную «длину». Эти «предопределенные переменные» называются регистрами, и каждая из них имеет свою специализацию.
О специализации нам пока что говорить рано, описание наподобие «регистр-указатель базы кадра стека» вам вряд ли о чем-то скажет. Поэтому для начала познакомимся только с так называемыми регистрами общего назначения (РОН), и то не со всеми, а только с четырьмя основными, которые являются своего рода «рабочими лошадками» микропроцессора.
Вот их «имена собственные» — AX, CX, DX, BX (именно в такой последовательности они «упорядочены» в Intel’овских микропроцессорах).
А сейчас мы поближе посмотрим на эти «рабочие лошадки» микропроцессора.
1. Запустите программу DEBUG.EXE1.
2. Когда появится приглашение в виде «минусика», введите букву «R» (можно и «r» — регистр символов значения не имеет) и нажмите на «Enter».
Не правда ли, весьма похоже на то, что показывают в художественных фильмах про хакеров?
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=18B2 ES=18B2 SS=18B2 CS=18B2 IP=0100 NV UP EI PL NZ NA PO NC 18B2:0100 6A DB 6A
— Ну и что это такое? — скептически спросите вы.
— А черт его знает! Будем разбираться!
#2. То, что у вас должно появиться — это список доступных регистров и текущее значение каждого из них. Как видите, значения регистров AX, BX, CX, DX равны 0. Не правда ли, создается впечатление, что они просто-напросто ждут того, чтобы в них внесли какое-либо значение?
Природа не терпит пустоты. Писателей приводит в ужас чистый лист бумаги…
Весьма скоро и вы при виде «пустых» регистров будете испытывать непреодолимое наркотическое желание чем-нибудь их заполнить…
Однако прежде чем мы сделаем это в первый раз, давайте уточним тип этих «переменных».
А он очень простой, этот тип — шестнадцатеричное число в диапазоне 0…FFFF. Или, если в BIN, то — от 0 до 1111 1111 1111 1111.
Маловато будет? А вот создатели первых «IBM-совместимых» компьютеров посчитали, что и этого много2! 16-битная переменная еще и на две части дробится — для совместимости с языками ассемблера для предыдущих моделей процессора Intel, работавших только с 8-битными регистрами; да и просто ради удобства…
В общем, в умных книжках рисуют вот такую вот «нездоровую» схемку3:
А означает она следующее.
Физически существует один регистр — AX, а вот логически он делится на два — на старшую (AH) и младшую (AL) части (от английского — high и low).
Очевидно, что присвоить AX значение, например, 72F9h, мы можем следующими способами:
1. AX = 72F9h (одной командой); 2. AH = 72h; AL = F9h (двумя командами).
Точно так же присвоить значение 78h регистру AH можно двумя способами:
1. AH = 78h; 2. AX = 7800h.
То же самое, но для регистра AL:
1. AL = 78h; 2. AX = 0078h .
Тех, кого смущают числа с буквами, мы со зловредной ухмылкой отсылаем к 1.1. Система счисления :-]
#3. Если рассматривать регистр «целиком», то каждый из них имеет «длину» 16 бит, которые принято нумеровать справа налево4 . Так, для числа 2F4Dh, внесенного, например, в регистр AX, мы можем нарисовать такую вот «навороченную» табличку:
AX | 2F4D | |||||||||||||||
AH | AL | 2F | 4D | |||||||||||||
Значение бита | 0 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 |
Номер бита | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
Тетрады | Старшая AH | Младшая AH | Старшая AL | Младшая AL |
Внимательно смотрим на таблицу: одной шестнадцатеричной цифре соответствует тетрада двоичных цифр (4 шт., они же — 4 бита). «Емкость» регистров AH и AL — две тетрады, т. е. 8 бит. Точно такую «длину» имеют: коды символов, скан-коды клавиш, номера функций прерываний и куча всего прочего, чего вы пока еще не знаете.
Емкость AX (состоящего из двух половинок) — 4 тетрады, т. е. 16 бит; они же (эти 16 бит) иначе еще называются «словом»…
#4. «Принудительно» присвоить регистру значение можно при помощи той же команды «R», только с параметром «имя собственное регистра».
Например, команда
- R AX [Enter]
выбросит вам на монитор
:
Введите после двоеточия, например, число 123 и снова нажмите на Enter:
:123 [Enter]
На дисплее опять появится приглашение «-«, на которое мы отвечаем командой «R» без параметров и таким образом вновь просматриваем значения наших регистров:
AX=0123 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000 DS=18B2 ES=18B2 SS=18B2 CS=18B2 IP=0100 NV UP EI PL NZ NA PO NC 18B2:0100 6A DB 6A
Смотрим внимательно — AX=0123, что и требовалось доказать…
Примечания
1). В W9X она находится в папке WINDOWSCOMMAND. В Y2K и XP — WINDOWSSYSTEM32. В обоих случаях достаточно набрать в командной строке «debug», чтобы она запустилась.
2). Не забудьте, что при тогдашней технологической базе и это было большим прорывом. А экстенсивное расширение, например, разрядности, во-первых, нужно правильно предвидеть (вспомните, сколько в том же MS-DOS закладок на будущее, которые никуда не пошли за ненадобностью), а во-вторых, правильно оценить (в буквальном смысле). Неужели вы думаете, что, например, производители памяти не могут легким мановением руки увеличить ширину шины, соединяющую память с процессором? Могут, но во-первых — это резко повысит стоимость памяти, а во-вторых — не гарантирует повышения производительности.
3). Впоследствии мы немного усложним эту схемку — регистры современных процессоров 32-разрядные и называются немного иначе
4). «Первый справа» бит мы будем называть «нулевым». Однако нам попадались руководства, в которых это же бит обозван как «первый». Можно долго обсуждать тонкости русского языка (которые, к сожалению, не всегда понимает переводчик), однако это выходит за рамки данной книги. Просто имейте это ввиду, что можете с этим столкнуться, и будьте бдительнее, читая документацию.
1.3. Память
#1. Первым видом памяти, с которым мы войдем (придется!) в тесный физический контакт, будет оперативная, она же — RAM (от английского — Random Access Memory). Оперативная память — это своего рода «рабочая площадка», по которой суетится этакий шустрый многорукий дядька-процессор — чего-то там собирает, от кучи к куче бегает, всех ругает…
Оперативная память — это ряд пронумерованых ячеек размером в байт. Мы можем получить доступ к первому байту памяти, ко второму, к третьему и т.д.
Короче — пришло время испробовать еще одну команду из скромного арсенала DEBUG’a! Запустите debug и введите команду D (от английского — DUMP).
«Картинка», которую вы увидели, называется «дамп памяти» (что в переводе с английского означает «свалка») и она насыщена не только важной информацией, но и специальной низкоуровневой энергетикой. Да чего уж там греха таить — каждый ассемблерщик знает, что рассматривание дампа памяти поднимает настроение, жизненный тонус и другие, не менее важные вещи
18B2:0100 6A 00 68 4B 01 66 83 7E-E0 00 74 05 B8 4C 01 EB j.hK.f.~..t..L.. 18B2:0110 03 B8 4A 01 2B D2 52 50-57 FF 36 C4 34 00 A1 18 ..J.+.RPW.6.4... 18B2:0120 F7 7F 83 C4 12 56 9A 16-44 F7 7F FF 76 FE 9A 59 .....V..D...v..Y 18B2:0130 04 8F 17 B8 FE FF 1F 5E-5F C9 CA 06 00 90 C8 54 .......^_......T 18B2:0140 04 00 57 56 8B 76 04 33-C0 89 46 D6 B9 0B 00 8D ..WV.v.3..F..... 18B2:0150 7E D8 16 07 F3 AB 89 46-BC B9 0C 00 8D 7E BE F3 ~......F.....~.. 18B2:0160 AB 9A 21 9C 8F 17 89 46-FA A1 08 01 8E 46 06 26 ..!....F.....F.& 18B2:0170 39 44 02 0F 84 55 01 C7-46 BC 1A 00 C4 5E 0C 26 9D...U..F....^.&
Слева — это адрес памяти. В центре — 16 столбцов из спаренных цифр…
А здесь и повториться лишний раз не грех. Каждая пара шестнадцатеричных цифр — это байт. Смотрите внимательно на дамп! Байт по адресу 100 имеет значение 6A, байт по адресу 101 — 00, байт по адресу 102 — 68… Эти «сладкие парочки» — и есть неделимая «единица адресации» оперативной памяти.
Тех, кого смущает наличие буковок в адресе, в очередной раз отсылаем ознакомиться с шестнадцатеричной системой счисления, так как все числа, отображаемые программой debug — именно шестнадцатеричные.
И, наконец, столбец справа — это символы, соответствующие шестнадцатеричным кодам центрального столбца (например, коду 6A соответствует символ J). Большинству кодов не соответствует никакой из «печатных символов» — таким в колонке справа соответствуют точки.
#2. А теперь потренируем наши пальчики дампировать память — пройдемся по некоторым «историческим местам» нашей оперативной памяти… Для этого мы будем вводить команду D с параметром.
Например, команда (параметр L8 означает «вывести 8 байтов»):
- D FFFF:5 L8 [Enter]
покажет вам системную дату в правом столбце дампа.
Короче, искателям приключений выдаем «простыню» самых интересных адресов (большинство слов в описании вам пока должны быть непонятны, но вы не пугайтесь — понимание придет!).
- 0:417 — два байта разрядов состояния клавиатуры. Они активно используются ROM-BIOS для управления интерпретаций действий клавиатуры. Изменение этих байтов изменяет значение нажатых клавиш (например, верхний или нижний регистр).
- 0:41A — cлово по этому адресу указывает на начало буфера BIOS для ввода с клавиатуры, расположенного начиная с адреса 41E. В этом буфере хранятся и ждут обработки результаты нажатия на клавиши. Конец буфера — слово по адресу 41C.
- 0:43E — байт указывает, необходима ли проверка дискеты перед подводом головки на дорожку. Разряды 0…3 соответствуют дисководам 0…3. Если разряд установлен в 0, то необходима проверка дискеты. Как правило, вы можете обнаружить, что разряд установлен в 0, если при предыдущем обращении к дисководу имели место какие-либо проблемы. Например, разряд проверки будет равен 0, если вы попытаетесь запросить каталог на дисководе, на котором нет дискеты, и затем на запрос, появившийся на экране дисплея: «Not ready reading drive B: Abort, Retry, Ignore?» вы ответите: «A».
- 0:44C (2 байта) — длина регенерации экрана. Это число байтов, используемых для страницы экрана. Зависит от режима.
- 0:44E (2 байта) — смещение для адреса начала текущей страницы в памяти дисплея. Этот адрес указывает, какая страница в данный момент используется (маленькая, но неприятная подробность — это смещение внутри текущего сегмента видеопамяти, без учета самого сегмента. Например, для нулевой страницы смещение всегда будет равно нулю.)
- 0:460 (2 байта) — размер курсора, представленный в виде диапазона строк развертки. Первый байт задает конечную, а второй — начальную строку развертки.
- 0:449 — значение этого байта определяет текущий видеорежим. Для расшифровки требуется целая таблица. Например, 3 — 80-колонный текст, 16 цветов; 13h (19) — 256-цветный графический режим 320×200 и т. д.
Ну и хватит для первого раза. Кому мало — ищите дополнительную документацию :-p «
1.4. Программа
#1. Любая программа выполняется последовательно (мы ведь пока обсуждаем «простой» IBM PC, а не какой-нибудь крутой векторный параллельный суперкомпьютер). То есть пока не выполнилась текущая «строка» (инструкция) программы, следующая не выполнится. Совсем другой вопрос, какая «строка» будет выполнена после «текущей» (здесь мы имеем дело со всевозможными логическими «ветвлениями», «циклами» и т. д.), или же строчку из какой программы процессор выполнит следующей, а какая — будет ждать своей очереди (так называемая «многозадачность», которую пока трогать не будем — в большинстве случаев мы можем прекрасно прожить и без нее, поскольку все заботы об этом все равно берут на себя операционные системы).
Итак, у нас есть оперативная память, в которую загружается программа перед ее выполнением (сразу же по нажатию на Enter из Norton Commander). Операционная система, которая, собственно, и загружает программу, сообщает процессору, что надо начать обрабатывать команды, которые в памяти начинаются с такого-то адреса. И здесь первый подводный камень, вернее скала, которую трудно не заметить.
Начало программы в памяти процессор различает легко — ему указывает на это командный интерпретатор, а вот конец программы программист должен указывать сам!
Каким образом? А очень легко! Компьютер «распознает» как выход из программы специальную последовательность байтов. Например, для исполнимых файлов типа com (именно с этим типом файлов мы будем работать на начальном этапе) достаточно последовательности CD и 20.
Пробуем-проверяем? Ну конечно же! Только для этого вам понадобится какой-нибудь шестнадцатеричный редактор, например, HexWorkshop.
Все очень просто — создаем новый файл, единственным содержимым которого является последовательность CD 20, и сохраняем его как, например, myprg_1.com. Если вы позаботились о том, чтобы после CD 20 не было никаких прочих символов, то исполнимая программа будет «весить» только 2 байта.
Запускать это ваше первое творение лучше из Norton или Volcov Commander (все же это пока что DOS’овская программулька).
Что же она делает, эта 2-байтовая малышка? А ничего, просто этот файл обладает двумя важными свойствами:
- это — программа;
- эта программа — программа с корректным выходом.
Последнее и является единственным, что она пока что может делать (корректно выгружаться из памяти)…
Еще к вопросу о выгружаемости — если после CD 20 вы напишите еще что-нибудь (чепуху), она все равно будет проигнорирована. Дело до ее выполнения просто-напросто не дойдет. Другое дело — если вы напишите чепуху до…
#2. Честно говоря, опасно при низкоуровневом программировании чепуху писать. Можно невзначай и винт отформатировать :))). Поэтому лабуду писать не будем, вернее — будем, но не лабуду…
Итак, продолжим наше извращение. Познакомимся еще с некоторыми «машинными командами» (в нашем случае — последовательностями шестнадцатеричных циферек).
- B82301 — внести значение 0123h в AX;
- 052500 — прибавить значение 0025h к AX;
- 8BD8 — переслать содержимое AX в BX;
- 03D8 — прибавить содержимое AX к BX;
- 8BCB — переслать содержимое BX в CX;
- 31C0 — очистка AX;
- CD20 — конец программы. Передача управления операционной системе.
Вот и давайте создадим еще одну программу типа com со следующим «шестнадцатеричным содержимым»:
B8-23-01-05-25-00-8B-D8-03-D8-8B-CB-31-C0-CD-20
Если вы все ввели правильно, то прога у вас без проблем запустится, а операционная система не будет ругаться… Правда, визуально (в смысле на мониторе) вы ее работу так и не заметите, но поверьте на слово — она работает! В этом вы еще убедитесь, когда посмотрите на ее работу изнутри — не различающими цветов глазами компьютера… Только сначала еще немного теории…
#3. Теперь поговорим о втором подводном камне :). Один из принципов фон Неймана звучит приблизительно так: машине безразлично целевое назначение данных… Одна и та же цепочка битов может быть и машинными командами, и данными (например, символами, выраженными в виде кодов — есть такая «таблица символов ASCII», наверняка вы ее знаете).
Что из этого следует? А то, что компьютеру нужно указывать, что подразумевается под той или иной «простыней» из битов — данные или код.
На высоком уровне это делает операционная система. Например, она не пытается загрузить в память для выполнения файлы с расширениями, отличными от COM, EXE и BAT (последний вообще не из этой оперы, но принцип сохраняется).
Хотя…, вы всегда можете поэкспериментировать! Смените, например, у какого-нибудь текстового файла тип с TXT на COM и попробуйте его запустить на выполнение (хотя мы это делать настоятельно не рекомендуем!). В большинстве случаев ваш компьютер зависнет! Потому что:
- Он пытается интерпретировать данные как код. Соответственно, в процессор «попадает» всякая ерунда.
- Вряд ли он натолкнется на последовательность CD 20 вашем тексте :). Даже в том случае, если этот код выполнится «успешно» — ваша программа не возвратит управление операционной системе, а пойдет выполняться хлам, содержащийся в оперативной памяти. Как-то — остатки ранее выполненных программ, куски чьих-то данных, интерпретированные как код… и прочая многочисленная ерунда…
Почти такой же эффект, но с потенциально большей разрушительной силой может получиться, если управление получит ИСПОРЧЕННЫЙ код, который вроде бы «в основном» правильный, но часть его вместо инициализации переменных и прочих подготовительных действий в лучшем случае ничего не делает, а в худшем портит другой код и данные…
Как вам тяжело «въехать» в смысл повествования, состоящего из кусков различных книг, так и компьютеру тяжело понять подобную «мешанину». С той лишь разницей, что любую «неинтересную книгу» вы можете использовать в качестве туалетной бумаги, а вот «компутер» подобного права выбора лишен — он должен в это «въезжать», его процессор начинает перегреваться, а мозги кипят и вытекают через низкоуровневые порты ввода-вывода (командами IN и OUT соответственно).
#4. Еще немного идеологии. О программе, которая выполняется в памяти…
Сколько бы ни было «мозгов» в вашей навороченной тачке, любая программа выполняется в 640 килобайтах «нижней» (или основной) памяти. Если отнять от этой цифры «резидентную часть» операционной системы, многочисленные драйвера и т.д., то оставшееся и есть объем памяти, в котором выполняется ваша программа. А остальные мегабайты — это место для кэширования диска, хранения промежуточных данных и т.п.
Страшно? Медитируйте!
#5. Как уже говорилось в #3, одна и та же последовательность битов в памяти может быть:
- кодом (т.е. что компьютеру нужно делать) — последовательностью инструкций;
- данными (т.е. с чем компьютеру нужно выполнять ту или иную работу). Именно данные являются исходной «задачей» и конечным результатом работы процессора.
- стеком — это область памяти, позволяющая писать реентерабельный/рекурсивный код и служащая для хранения адресов возврата и локальных данных и передачи параметров.
Соответственно, и программа состоит из трех частей (сегментов): сегмента данных (data), сегмента кода (code) и сегмента стека (stack)…
Оставим пока что «гнилой базар» про смысл словосочетаний «реентерабельный/рекурсивный код» и «адрес возврата». Чтобы не затруднять себе понимание происходящего, мы попытаемся абстрагироваться от всех этих ужасающих вещей и для начала заняться только кодом.
#6. Помните, как в конце фильма «Matrix» Нео в конце концов увидел ее — черно-зеленую «матрицу»? Сейчас с вами произойдет нечто подобное!
Посмотрите на машинные коды, и «что они делают» в #2. Немножко дополним эту «простыню». Например, командой «внести значение» 1234 последовательно в каждый из «регистров общего пользования»:
B83412 - AX=1234 BB3412 - BX=1234 B93412 - CX=1234 BA3412 - DX=1234
Наиболее наблюдательные должны для себя отметить, что первый байт — это команда «переместить в регистр», а второй и третий — само число, только байты почему-то «наоборот».
Однако никто не пишет программы в шестнадцатеричных редакторах! Никто! Это большая глупость! Единственное, зачем мы вам про это рассказываем — это чтобы вы поняли, что могут означать загадочные пары шестнадцатеричных цифр в дампе…
Нет необходимости заучивать, что B8 — это «переместить в регистр AX», BB — «переместить в регистр BX» и так далее… Когда-нибудь это может пригодиться тому, кто будет писать компилятор, умеющий генерировать исполняемый код, упаковщики исполняемых файлов, самомодифицирующийся код или, на худой конец, конструкторы полиморфных самошифрующихся вирусов. Но это мы оставим на будущее…
Все намного проще!
В этом вы можете убедиться, загрузив вашу программу myprg_1.com в debug (например, командной строкой
debug myprg_1.com
и введя команду «u».
А вот дальше начинается самое интересное :)))
#7. Вот что вы должны увидеть:
11B7:0100 B82301 MOV AX,0123 ; Внести значение 0123h в AX 11B7:0103 052500 ADD AX,0025 ; Прибавить значение 0025h к AX 11B7:0106 8BD8 MOV BX,AX ; Переслать содержимое AX в BX 11B7:0108 03D8 ADD BX,AX ; Прибавить содержимое AX к BX 11B7:010A 8BCB MOV CX,BX ; Переслать содержимое BX в CX 11B7:010C 31C0 XOR AX,AX ; Очистка AX 11B7:010E CD20 INT 20 ; Конец программы
Возвратившись к #2, перенесем сюда «описание» машинных команд.
Эти mov, add, xor, int — так называемые «мнемонические команды» (более или менее понятные человеку), на основе которых формируется (это debug делает) «машинный код». Не правда ли, так намного легче?
Соответственно, вместо шестнадцатеричных кодов мы легко могли вводить эти команды при помощи команды «A» (однако этим мы займемся позже).
#8. А теперь мы выполним нашу программу пошагово — произведем так называемую «трассировку» при помощи команды «T».
Итак, вводим «T» и жмем на Enter!
Вот что мы видим:
AX=0123 BX=0000 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0103 NV UP EI PL NZ NA PO NC 11B7:0103 052500 ADD AX,0025
Смотрим на значение AX и вспоминаем предыдущую инструкцию — «внести значение 0123h в AX». Внесли? И правда! А в самом низу — код и мнемоника команды, которая будет выполняться следующей…
Вводим команду «T» снова:.
AX=0148 BX=0000 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0106 NV UP EI PL NZ NA PE NC 11B7:0106 8BD8 MOV BX,AX
AX=0148 — «прибавить значение 0025h к AX». Сделали? Сделали!!
Вводим команду «T» снова:.
AX=0148 BX=0148 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0108 NV UP EI PL NZ NA PE NC 11B7:0108 03D8 ADD BX,AX
AX=0148=BX — «переслать содержимое AX в BX». Сделали? Сделали!!
Вводим команду «T» снова:
AX=0148 BX=0290 CX=0010 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010A NV UP EI PL NZ AC PE NC 11B7:010A 8BCB MOV CX,BX
«Прибавить содержимое AX к BX». Оно? А то!
Вводим команду «T» снова:
AX=0148 BX=0290 CX=0290 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010C NV UP EI PL NZ AC PE NC 11B7:010C 31C0 XOR AX,AX
«Переслать содержимое BX в CX». Сделано!
Вводим команду «T» снова:
AX=0000 BX=0290 CX=0290 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=010E NV UP EI PL ZR NA PE NC 11B7:010E CD20 INT 20
«Очистка AX»? И точно: AX=0000!
Вводим команду «T» снова… И ГРОМКО РУГАЕМСЯ!!
Потому что, по идее, сейчас наша программа должна была завершиться — у нас же там код выхода прописан, а она куда лезет? NOPы какие-то (если продолжать команду «T» вводить), CALL 1085 (да вы продолжайте «трассировку», продолжайте!)
Для тех, кому лень продолжать жать на букву «T», введите для разнообразия команду «G» (от английского GO). На монитор должна вывалиться надпись «Нормальное завершение работы программы».
— Уф, — должны сказать вы — Работает!
А то!
#9. Только непонятно вот, почему вдруг между int 20 (CD 20) и надписью «Нормальное завершение работы программы» куча всяких «левых» непонятных команд (в том случае, если вы и дальше производили тарассировку, а не воспользовались «халявной» командой «G»)?
А потому, дорогие наши, что вы имели счастье нарваться на прерывание (interrupt)!
Понимаете ли, завершить программу — дело непростое :). Нужно восстановить первоначальное значение регистров, восстановить переменные среды и кучу всего другого! Знаете, как это сложно?
Однако эта процедура насколько сложная, настолько и типичная для исполняемых программ. А по сему разработчики операционной системы решили избавить программистов от необходимости делать это вручную, и включили эту стандартную процедуру в ядро операционной системы. И сказали: «да будешь ты (процедура обработки прерывания) вызываться как int 20, и будешь ты обеспечивать корректную передачу управления из выполняемой программы — назад в ядро». И стало так…
Ну, посудите сами, должна же операционная система ну хоть что-нибудь делать!!
1.5. Прерывания
#1. Прерывание — это СИГНАЛ процессору, что одно из устройств в компьютере НУЖДАЕТСЯ в обслуживании со стороны программного обеспечения. В развитие этой же идеи, программам позволили самим посылать запросы на обслуживание через механизм прерываний. Получив этот сигнал, процессор временно переключается на выполнение другой программы («обработчика прерывания») с последующим ВОЗОБНОВЛЕНИЕМ выполнения ПРЕРВАННОЙ программы.
Когда же и «кем» генерируются эти «сигналы» (в смысле «прерывания»)?
- Многочисленными «схемами» компьютера, его устройствами. Например, соответствующее прерывание генерируется при нажатии клавиши на клавиатуре.
- Также прерывания генерируются как «побочный продукт» при некоторых «необычных» ситуациях (например, при делении на «букву О»), из которых компьютеру хочешь, не хочешь, но приходится как-то выкручиваться…
- Наконец, прерывания могут преднамеренно генерироваться программой, для того чтобы произвести то или иное «низкоуровневое» действие.
Когда процессор получает сигнал прерывания, он останавливает работу приложения и активизирует «программу обработки прерывания», соответствующую «номеру прерывания» (т.е. разных сигналов прерываний больше одного — точнее, их 256). После того как обработчик свое отработает, снова продолжает выполняться основная программа.
Для тех, кто не понял. Представьте себе, что вы сидите за компом и выполняете какую-либо работу. И вдруг ловите себя на мысли, что вам СРОЧНО НУЖНО сходить в туалет (терпеть вы больше уже не можете). Вот это СРОЧНО НУЖНО и есть сигнал-прерывание, по которому вы начинаете выполнять определенную СТАНДАРТНУЮ последовательность инструкций (программу обработки прерывания), как-то: встать, пойти туда-то, включить свет … вернуться, сесть за комп и ПРОДОЛЖИТЬ РАБОТУ с того же самого места, на котором вы остановились перед выполнением программы «поход в туалет». В данном случае наш мозг выполняет роль процессора, наши внутренние органы сигнализируют мозгу о потребности в обслуживании, а само обслуживание проводится «программой-навыком», заложенным в процессе нашего развития и (хм!) воспитания.
#2. Программы обработки прерывания располагаются в оперативной памяти (ну а где же еще им располагаться?!) и, следовательно, имеют свой АДРЕС. Однако генератору прерывания этот адрес знать не обязательно :). Есть такая замечательная штука (спросите у тех, кто пишет вирусы) — таблица векторов прерываний. Это таблица соответствия номеров и адресов памяти, по которым находятся программы их обработки.
Почему «спросите у вирмейкеров?». А потому, что поменять адрес «программы обработки прерывания» на другой — проще пареной репы (мы этим еще займемся), в результате чего при запуске классической программы «HELLO, WORLD» может получиться еще более классический format c:…
Программы обработки прерывания автоматически сохраняют значения регистра флагов, регистра кодового сегмента CS и указателя инструкции IP, чтобы по завершении «обработки прерывания», к нашей безумной радости, снова возвратиться к выполняемой программе (просто программе)… Остальные регистры, содержимое которых меняется в обработчике, должен сохранять сам обработчик — и если он этого делать не будет, то нарушится выполнение основной программы. Ведь она даже не знает, что ее «ненадолго» прервали!
Однако на самом деле все намного сложнее :)). Но ведь это только «первое погружение» в прерывания, верно? А посему — пока что без особых «наворотов»…
#3. Одно прерывание мы с вами уже знаем. Это 20-е прерывание, обеспечившее «выход» из нашей COM-программы. Сегодня мы пойдем немножко дальше — помимо «выхода» попробуем поработать еще с одним прерыванием.
Итак, я достаю свой толстый талмуд с описанием прерываний и выбираю, каким бы это прерыванием вас занять на ближайшие 1/2 часа ;)…
Ну, например, вот одно симпатичное, под названием «прокрутить вверх активную страницу».
Внимательно читаем описание (и наши комментарии):
INT 10h, AH=06h (_1) — прокручивает вверх произвольное окно на дисплее на указанное количество строк.
ВХОДНЫЕ ПАРАМЕТРЫ: (_2)
- AH=06h; (_3)
- AL — число строк прокрутки (0…25) (AL=0 означает гашение всего окна); (_4)
- BH — атрибут, использованный в пустых строках (00h…FFh); (_5)
- CH — строка прокрутки — верхний левый угол; (_6)
- CL — столбец прокрутки — верхний левый угол;
- DH — строка прокрутки — нижний правый угол;
- DL — столбец прокрутки — нижний правый угол;
Далее представим входные параметры в виде таблички: (_7)
AH | 06h | AL | Число строк |
BH | Атрибут | BL | Не имеет значения |
CH | Строка (верх) | CL | Столбец (верх) |
DH | Строка (низ) | DL | Столбец (низ) |
Плюс подробнейшее толкование, что подразумевается под словом «атрибут» (регистр BH):
| 0000 черный | | 0001 синий | | 0010 зеленый | | 0011 голубой | Цвет фона | 0100 красный |(разряды 654) | 0101 пурпурный | Цвет символа | 0110 коричневый | (разряды 3210) | 0111 белый | | | 1000 серый | | 1001 светло-синий | | 1010 светло-зеленый | Повышенная | 1011 светло-голубой | яркость | 1100 светло-красный | (разряд 3) | 1101 светло-пурпурный | | 1110 желтый | | 1111 яркий белый | Мерцание (разряд 7)
ВЫХОДНЫЕ ПАРАМЕТРЫ: отсутствуют (т.е. ни один регистр не меняется). (_8)
Входные строки гасятся в нижней части окна. (_9)
Нормальное значение байта атрибута — 07h. (_10)
Совсем недавно, если бы вам показали подобное «описание», вы бы ничего в нем не поняли и ужаснулись. Теперь же, после прочтения предыдущих глав курса, в эти «таблицы» вы, более или менее, но «въехать» должны! Тем более, что сейчас я сделаю комментарии для… хм… «отстающих» учеников (внимательно смотрим на циферки в скобках):
_1. Черным по белому, в толстом талмуде, описывающем функции прерываний, написано: «Драйвер видео вызывается по команде INT 10h и выполняет все функции, относящиеся к управлению дисплеем».
И далее. «…ДЕЙСТВИЕ: после входа управление передается одной из 18 программ в соответствии с кодом функции в регистре AH. При использовании запрещенного кода функции, управление возвращается вызывающей программе.
НАЗНАЧЕНИЕ: прикладная программа может использовать INT 10h для прямого выполнения функций видео…»
Вот что из этого следует:
- выполнив команду INT 10h, мы ВЫПОЛНЯЕМ одну из «функций видео»;
- так как функций видео — много, необходимо УКАЗАТЬ, КАКУЮ именно ФУНКЦИЮ из МНОЖЕСТВА мы хотим ВЫПОЛНИТЬ.
Дело в том, что «прерывание номер десять» — это не только «прокрутка окна», но и, например, «установка режима видео», «установка типа курсора», «установка палитры» и многое другое. Нас же интересует именно первое, поэтому из списка возможных значений (он приведен ниже) мы выбираем именно AH=06h.
Нижеследующая табличка называется «Функции, реализуемые драйвером видео»:
AH=00h Установить режим видео AH=01h Установить тип курсора AH=02h Установить позицию курсора AH=03h Прочитать позицию курсора AH=04h Прочитать позицию светового пера AH=05h Выбрать активную страницу видеопамяти AH=06h Прокрутить вверх активную страницу AH=07h Прокрутить вниз активную страницу AH=08h Прочитать атрибут/символ AH=09h Записать символ/атрибут AH=0Ah Записать только символ AH=0Bh Установить палитру AH=0Ch Записать точку AH=0Dh Прочитать точку AH=0Eh Записать TTY AH=0Fh Прочитать текущее состояние видео AH=13h Записать строку
Соответственно, если перед выполнением INT 10 в регистре AH будет значение 06h, то выполнится именно «прокрутить вверх активную страницу», а не что-то другое из «простыни» функций десятого прерывания…
Теперь читаем описание дальше (смотрим на циферки в скобках):
_2. Входные параметры? Что тут может быть непонятного? Даже запуск ракеты с атомной боеголовкой требует прежде всего указать координаты цели… Чего уж тут говорить об обыкновенной функции?
_3. То, о чем мы уже говорили — номер функции из «простыни».
_4. Т.е. на СКОЛЬКО строчек прокручивать. Вспомните так называемый «скроллинг» в любой прикладной программе. На кнопки Up, Down подвешен скроллинг на одну строчку (не путать с координатами курсора), а вот на PgUp и PgDown — штук на 18 строк (AL=01h и AL=12h соответственно). А вот AL=0, вместо того чтобы вообще не скроллировать (по идее), поступает наоборот — «скроллирует» все, что может.
_5. Скажем так — какого цвета будет окно и символы в нем после скроллирования.
_6. Как известно из школьного курса геометрии, прямоугольник можно построить по двум точкам. Это утверждение справедливо и для окна, в котором мы желаем проскроллировать наш текст.
_7. Резюме того, что было написано выше.
_8. К примеру, попала ли наша ракета в цель или нет ;).
_9. Если бы мы использовали функцию 07h, то было бы глубокомысленно написано, что «строки гасятся в верхней части окна».
_10. Это то самое, которое в DOS по умолчанию. Т.е. белыми буквами на черном фоне. Правда, это 07h лучше все же рассматривать как 00000111b но это уже совсем другая проблема…
#4. А теперь мы напишем программу. Ручками, без использования компилятора. Запускаем наш любимый debug.exe, вводим команду «а» и судорожно стучим по клавиатуре:
-a 119A:0100 xor al,al ;гашение всего окна 119A:0102 mov bh,70 ;белое окно 119A:0104 mov ch,10 ;четыре координаты прямоугольника 119A:0106 mov cl,10 119A:0108 mov dh,20 119A:010A mov dl,20 119A:010C mov ah,06 ;такая-то функция прерывания 119A:010E int 10 ;Go!! 119A:0110 int 20 ;выход... 119A:0112 -r cx ;в CX - сколько байтов программы ;писать 112h-100h=12h CX 0000 :12 -n @int10.com -w Запись: 00012 байтов
Сначала запускаем из-под Norton Commander. Затем запускаем из-под debug. Трассируем. Открываем в HEX-редакторе. Смотрим на «бессмыслицу» шестнадцатеричных циферек. Медитируем, медитируем и еще раз медитируем…
1.6. Немножко программируем и немножко отлаживаем
#1. Тем, кто не в курсе — НАСТОЯТЕЛЬНО рекомендую проштудировать предыдущие части курса, иначе «въехать» будет сложно. Тем же, кто внимательно читал предыдущие главы, нижеследующие упражнения для ума и пальцев покажутся детским лепетом…
Давайте немножко видоизменим программу, которую мы писали в прошлый раз. Сделаем так, чтобы наше «окошко скроллинга» располагалось более или менее посередине экрана.
:0100 XOR AL,AL ;ПРИМЕЧАНИЕ: с целью экономии пространства и :0102 MOV BH,10 ;времени мы немножко сократили наш DEBUG-й :0104 MOV CH,05 ;листинг, т.е. убрали адрес сегмента и :0106 MOV CL,10 ;машинные коды, соответствующие мнемоническим :0108 MOV DH,10 ;командам :)) :010A MOV DL,3E ;А так, конечно, в оригинале первая строка вот :010C MOV AH,06 ;как должна выглядеть: :010E INT 10 ;11B7:0100 30C0 XOR AL,AL :0110 INT 20
Теперь наша задача — написать программу, которая последовательно выводит пять таких окошек, причем каждое последующее окно «вложено» в предыдущее, а значение атрибута в шестнадцатеричной нотации на 10 больше предыдущего.
Если мы будем программировать ЭТО линейно (именно так для начала), то очевидно, что все, что мы должны сделать — это заданное количество раз (5) заставить машину выполнить вышеуказанные операции, изменяя перед «запуском прерывания» значения регистров AL, BH, CX, DX (полное описание 6-й функции 10-го прерывания ищите в прошлых главах).
#2. Вот к каким умозаключениям вы должны были придти, пораскинув мозгами. За атрибут (то бишь цвет) у нас отвечает регистр BH. Он был равен 10h, а нужно на 10h больше… это, значит, 20h будет…
Ладно… CX (он же CH и CL, как известно) — это ТОЖЕ ШЕСТНАДЦАТЕРИЧНЫЕ координаты левого верхнего угла нашего окошка. Чтобы «окно в окне» получилось, все это нужно на строчку больше сделать и на колонку больше тоже, и все считать в HEX’e. Получается, что в регистр СH нужно вместо значения 05h внести 06h, а в регистр CL вместо 10h — 11h.
А еще можно одним махом в CX записать число 0510h той же командой mov.
Ладно… DX (DH и DL соответственно) — это координаты правого нижнего угла прямоугольника. DH=10h-1h=Fh и DL=3Eh-1h=3Dh.
Ну, а AL=0 и AH=6 — это уже и ежу понятно из описания данной функции (mov AH,6) данного прерывания (INT 10h).
Все, что осталось — это набить в debug’е после команды «a» эти мнемоники энное количество раз. (кажется 5). Набиваем!!
:0100 XOR AL,AL ;первый раз :0102 MOV BH,10 :0104 MOV CH,05 :0106 MOV CL,10 :0108 MOV DH,10 :010A MOV DL,3E :010C MOV AH,06 :010E INT 10 :0110 INT 20 :0112 XOR AL,AL ;второй раз :0114 MOV BH,10 :0116 MOV CH,06 :0118 MOV CL,11 :011A MOV DH,0F :011C MOV DL,3D :011E MOV AH,06 :0120 INT 10 :0122 INT 20 ;третий раз :0124 XOR AL,AL : и т.д
Правда, красивые циферки-буковки? Набиваем-набиваем! Если сейчас к вам подойдут недZенствующие приятели/коллеги и посмотрят, что вы тут колупаете, то ни черта не поймут и покрутят пальцем у виска. Привыкайте к этому. Только не говорите им, что пытаетесь сейчас получить Матрицу, ПОТОМУ ЧТО это неправда. А неправда это потому, что сейчас Матрица в очередной раз обманула вас!
#3. ВСЕ ПОТОМУ, ЧТО МОЗГАМИ ДУМАТЬ НАДО, А НЕ ТОЛЬКО СЛЕПО СЛЕДОВАТЬ РУКОВОДСТВУ!
У вас только первое окно прорисуется, сразу же после чего программа натолкнется на INT 20h и благополучно завершится! А следовательно, и все, что после первого CD 20 написано будет — останется проигнорированным! Исправляйте! (Т.е. уберите все INT 20 КРОМЕ ПОСЛЕДНЕГО).
Второй момент. ВОЗВРАЩАЕТ ЛИ это прерывание ЧТО-НИБУДЬ В РЕГИСТР AX? Смотрите описание. Ничего? Ну так какого черта тогда по новой вводить XOR AL,AL и MOV AH,06 и переприсваивать AH значение 6h, если и без того AH = 6h? Один раз ввести — более чем достаточно!
Скажите, какая мелочь- байтом больше, байтом меньше! А я скажу вот что — на то он и assembler, чтобы «байтом меньше».
Исправляйте!
#4. — Исправляйте? — возмутитесь вы — Да это же по-новому все вводить нужно!
— По-новому? — возмутимся мы в свою очередь! — Зачем по-новому? Вы что, с ума сошли?
1. Что вам мешает после команды «a» указать адрес, который вы желаете переассемблировать? И благополучно заменить старую команду на новую!
— А что делать, если не переассемблировать нужно, а вообще удалить?
2. Существует куча способов, что вы в самом-то деле! Например, в HEX Workshop с блоками шестнадцатеричных цифр запросто можно работать. Да и в других программах это можно делать — например, в HIEW или даже в Volcov Commander.
Кстати, если процессор встретит команду NOP, то он просто побездельничает некоторое очень короткое время.
ПРОБУЙТЕ!! В конце концов, ваша прога должна принять такой вот вид:
:0100 XOR AL,AL ;окошко первое :0102 MOV BH,10 :0104 MOV CH,05 :0106 MOV CL,10 :0108 MOV DH,10 :010A MOV DL,3E :010C MOV AH,06 :010E INT 10 :0110 MOV BH,20 ;окошко второе :0112 MOV CH,06 :0114 MOV CL,11 :0116 MOV DH,0F :0118 MOV DL,3D :011A INT 10 :011C MOV BH,30 ;окошко третее :011E MOV CH,07 :0120 MOV CL,12 :0122 MOV DH,0E :0124 MOV DL,3C :0126 INT 10 :0128 MOV BH,40 ;окошко четвертое :012A MOV CH,08 :012C MOV CL,13 :012E MOV DH,0D :0130 MOV DL,3B :0132 INT 10 :0134 MOV BH,50 ;окошко пятое :0136 MOV CH,09 :0138 MOV CL,14 :013A MOV DH,0C :013C MOV DL,3A :013E INT 10 :0140 INT 20 ;конец проги...
О, да! Получившаяся у вас программа написана долго и бездарно! Имейте это в виду :)). Мы же торжественно обещаем, что в последующих главах обязательно ее усовершенствуем. Да, вот еще что — особенно извращенные могут попытаться заменить INT 20 на JMP 100. Получится, конечно, не ахти, но все же — «анимация»
#5. А теперь мы попробуем ОТЛАДОЧНЫЙ прием! Все кракеры его знают и пользуются им для взлома софта. Имейте в виду, пока вы будете использовать его для своих исполняемых программ — вы программер, исправляющий ошибки, а как только попытаетесь использовать это для отвязки чужой программы от какого-нибудь серийного номера — ваша деятельность станет считаться неэтичной или незаконной. Так что думайте сами, что лучше — флаг в руки или барабан вместе с петлей на шею.
Итак, вводим приблизительно такую командную строку — debug имя_проги.com или же подгружаем прогу в отладчик командой «l» (от слова load) и трассируем, как вы уже неоднократно это делали.
Цель — «на лету» (без изменения кода) заставить первое окошко «рисоваться» не синим (BH=10h), а красным (BH=40h) цветом.
Мы просто приведем вам последовательность действий, а вывод «зачем это нужно» и прочие возможные выводы вы уже сами делать будете. Ок?
-t AX=0000 BX=0000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0102 NV UP EI PL ZR NA PE NC 11B7:0102 B710 MOV BH,10 -
Состояние: обнулился регистр AX (первую команду MOV AL,AL мы не видим). Процессор готовится выполнить команду MOV BH,10. Дадим ему это сделать!
-t AX=0000 BX=1000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0104 NV UP EI PL ZR NA PE NC 11B7:0104 B505 MOV CH,05 -
Состояние — в BX уже внесен код синего цвета, который нам по условию необходимо заменить на красный (т. е. заменить значение регистра BX с 1000h на 4000h).
Вот теперь-то мы и делаем это «на лету»:
-r bx BX 1000 :4000 -
А действительно ли сделали? Проверим!
-r AX=0000 BX=4000 CX=0043 DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=11B7 ES=11B7 SS=11B7 CS=11B7 IP=0104 NV UP EI PL ZR NA PE NC 11B7:0104 B505 MOV CH,05 -
Состояние? BH теперь равно 40h! Мы «вклинились» между строчками:
:0102 MOV BH,10 :0104 MOV CH,05
И изменили текущую цепь событий, заставив программу делать ТО, ЧТО НАМ НУЖНО! Поздравляю!
А дальше — вводим команду «g» и даем нашей тупорылой программе исполниться. 1:0 не в пользу Матрицы!
Медитируйте!
1.7. Стек
#1. В средней школе, где когда-то учился автор, учителем физкультуры был настоящий зверь. Помимо того, что он заставлял нас школьников бегать-прыгать-подтягиваться, у него еще любимое наказание было — проходило оно в так называемом «зале тяжелой атлетики».
Что такое тяжелая атлетика вы наверняка знаете. Видели по телевизору, когда выходит на помост этакий здоровяк, и рвет собственное здоровье, поднимая штангу.
Штанга — это такая «палка», по бокам которой навешиваются так называемые «блины» — круглые плоские с дыркой посередине диски невероятной тяжести.
Хранятся же эти диски на штырях, которые представляют собой те же «палки», но вторкнутые вертикально в пол. На них и хранились диски — один на другой положенные.
«Наказание» заключалось вот в чем — тот «педагог» заставлял нерадивого ученика комплектовать штангу! Это элементарно сделать, если диски просто валяются на полу. Но когда они аккуратно сложены на штырь — это намного сложнее :(.
Садист! Чтобы достать со штыря диск заданной тяжести (который обычно находился внизу) необходимо было снять со штыря все «вышележащие» диски, достать самый нижний, а остальные снова надеть на штырь. А потом точно так же достать диск другой «тяжести» со второго штыря. А он как бы случайно тоже в самом низу. И так далее — до полной победы идиотизма над здравым рассудком. Не правда ли, изощренная пытка?
К чему это лирическое бредисловие? А к тому, что стек — это тоже своего рода штырь с блинами. И уж поверьте, упражняться вы с ним будете намного чаще, чем мы делали это на уроках физкультуры. С той лишь несущественной разницей, что у нас на следующий день болела спина, а у вас на следующий день будут болеть мозги.
Так вот, о стеке: «штырь» для блинов находится в оперативной памяти (где же еще?). А роль блинов выполняют хорошо знакомые нам всем регистры, вернее — их «значения».
Правила работы с ним те же — вы можете снять только верхний «блин». Чтобы получить самый нижний «блин» — вам нужно прежде снять все те, которые НАД ним.
Очевидно, что из десяти «блинов», которые вы надели на «штырь», первым будет сниматься последний из надетых (верхний), а последним — первый, то есть самый нижний.
Все очень просто: «первый пришел — последним уйдешь» и наоборот «пришел последним — уйдешь первым».
Это вам не очередь времен социализма… Это очередь «загрузки-разгрузки» стека!
#2. Для работы со стеком вам пока что необходимо знать только две команды: push и pop. Так как в качестве «блинов» у нас регистры, то, соответственно, необходимо после этих команд указывать и «имена собственные» помещаемых в стек значений регистров.
Соответственно:
push AX ;ПОМЕЩАЕТ В СТЕК значение регистра AX pop AX ;ИЗВЛЕКАЕТ ИЗ СТЕКА значение регистра AX
Ну а как делать то же самое с остальными регистрами вы, наверняка, уже и сами догадались.
Очень важно помнить, каким «нездоровым» образом в стеке реализована ОЧЕРЕДЬ -поместить/извлечь. Помните, мы вас предупреждали, что нам нельзя верить на слово? Не верьте! А посему — обязательно убедитесь в истинности/ложности нашего голословного утверждения при помощи следующей программульки:
:0100 MOV AX,0001 ;AX = 1 :0103 PUSH AX ;В стек записана 1 :0104 MOV AX,0002 :0107 PUSH AX ;В стек записана 2 :0108 MOV AX,0003 :010B PUSH AX ;В стек записана 3 :010C MOV AX,0004 :010F PUSH AX ;В стек записана 4 :0110 MOV AX,0005 :0113 PUSH AX ;В стек записана 5 :0114 POP AX :0115 POP AX :0116 POP AX :0117 POP AX :0118 POP AX :0119 INT 20
С очередностью заполнения стека, наверное, все понятно :). Я много про абстрактные «блины» загружал. А вот с адреса 114 начинается извлечение из стека. В какой последовательности это делается, вы можете увидеть сами, произведя трассировку этой небольшой проги.
-r AX=0000 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0100 NV UP EI PL NZ NA PO NC 14DC:0100 B80100 MOV AX,0001 -
Анализируем. Прога еще не начала работать, готовится выполниться команда по адресу 100. Делаем ШАГ!
-t AX=0001 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0103 NV UP EI PL NZ NA PO NC 14DC:0103 50 PUSH AX -
Анализируем. AX=0001 — значит, команда выполнилась правильно :). Следующая команда, по идее, должна поместить 1 в стек.
-t AX=0001 BX=0000 CX=001B DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0104 NV UP EI PL NZ NA PO NC 14DC:0104 B80200 MOV AX,0002 -
И что? Команда выполнилась, но где мы можем увидеть, что в стек действительно «ушла» единица? Увы, но здесь это не отображается :). Проверим потом. Ведь логично предположить, что если эти значения действительно сохранились в стеке, то мы их потом без проблем оттуда извлечем, т.е. если найдем «там» наши 1, 2, 3, 4, 5 — значит все Ок.
А поэтому — дадим программе работать дальше до адреса 114 (не включительно), не вдаваясь в подробный анализ. Что тут анализировать? Если значение регистра AX последовательно меняется от 1 до 5 — значит, команда mov работает. А стек (команда push) проверим потом, как и договорились.
Проехали до адреса 114.
-g 114 AX=0005 BX=0000 CX=001B DX=0000 SP=FFF4 BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0114 NV UP EI PL NZ NA PO NC 14DC:0114 58 POP AX -
А вот теперь снова анализируем :). При следующем шаге выполнится команда, извлекающая некогда «запомненное» значение AX из стека.
Обратите внимание, регистр IP указывает на адрес (114) выполняемой команды. Мы с вами это уже проходили, не так ли?
Поехали дальше!!
-t AX=0005 BX=0000 CX=001B DX=0000 SP=FFF6 BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0115 NV UP EI PL NZ NA PO NC 14DC:0115 58 POP AX -
Выполнился первый POP. Готовится выполниться второй. AX=5. Т.е., по сравнению с предыдущим шагом, вроде ничего не изменилось… Но на самом деле это не так. AX=5 — эта пятерка «загрузилась» из стека :)). В этом вы легко убедитесь, сделав следующий шаг трассировки.
-t AX=0004 BX=0000 CX=001B DX=0000 SP=FFF8 BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0116 NV UP EI PL NZ NA PO NC 14DC:0116 58 POP AX -
Ууупс… AX=4 :). А команда, вроде, та же — POP AX
-t AX=0003 BX=0000 CX=001B DX=0000 SP=FFFA BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0117 NV UP EI PL NZ NA PO NC 14DC:0117 58 POP AX -
AX=3
-t AX=0002 BX=0000 CX=001B DX=0000 SP=FFFC BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0118 NV UP EI PL NZ NA PO NC 14DC:0118 58 POP AX -
AX=2
-t AX=0001 BX=0000 CX=001B DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000 DS=14DC ES=14DC SS=14DC CS=14DC IP=0119 NV UP EI PL NZ NA PO NC 14DC:0119 CD20 INT 20 -
AX=1 То есть нашлись-таки наши 1, 2, 3, 4, 5 :). Восстановились из стека. Теперь поверили? А то!
Еще раз обращаю ваше внимание на то, что последовательность записи (четыре PUSH’а) была — 1, 2, 3, 4, 5, а вот последовательность извлечения (четыре POP’а) — 5, 4, 3, 2, 1. Т.е. «последний пришел — первый ушел». Зарубите это себе на носу! (Как сделал это на своем перебитом носе наш школьный учитель физкультуры).
Медитируйте над этой темой до полного просветления! Иначе потом придется туго!
1.8. Цикл
#1. Наша программа для работы со стеком линейна. А линейное программирование — это плохо. Хотя и не всегда
Итак, давайте еще раз посмотрим на нашу программу для работы со стеком. С 100-го до 113-го адреса у нас имеется пять почти идентичных блоков. Изменяется только значение AX, но на одно и то же число — на единицу в большую сторону. То есть AX = предыдущее значение + 1. Это очевидно.
Еще более очевидно, что простая команда POP AX (с 114 по 119) повторяется у нас тоже 5 раз.
Мне почему-то сразу вспомнился анекдот о том, как два мента едут в машине, и один спрашивает у другого: «Глянь, работает ли у нас мигалка на крыше». Тот высунул в голову в форточку и говорит: «Работает-не работает-работает-не работает-работает-не работает…»
Так вот, не будем уподобляться этим нехорошим людям и сделаем нашу прогу более нормальной.
Добьемся мы этого с помощью так называемого «цикла». Цикл — это… Не буду давать общепринятые определения; кто хочет — поищите в книжках, благо, их навалом.
Скажу только: «сесть-встать, сесть-встать, сесть-встать» — это не цикл, а вот «сесть-встать и так три раза» — уже можно считать циклом.
Реализуется же он (цикл), например, при помощи регистра CX и команды LOOP следующим образом.
Число циклов заносится в регистр CX. После этого следует «простыня» из команд, которые вы хотите «зациклить», т. е. выполнить энное количество раз. Заканчиваться все это должно LOOP’ом с указанием адреса «строки», с которой необходимо начать цикл (обычно это «строка», следующая сразу же после mov СХ.
Давайте мы сначала «набьем» нелинейный вариант нашей проги, а потом разберемся, что там к чему. Набиваем:
:0100 ХOR AX,AX ; AX=0 :0102 MOV CX,0005 ; нижеследующий до команды LOOP кусок повторить CX раз :0105 ADD AX,0001 ; AX=AX+1 (у нас же значение AX на 1 увеличивается...) :0108 PUSH AX ; помещаем в стек :0109 LOOP 0105 ; конец цикла; инициируем повторение; CX уменьшается на 1 :010B MOV CX,0005 ; второй цикл повторить тоже 5 раз :010E POP AX ; достаем из стека :010F LOOP 010E ; конец цикла; повторить! ; CX=CX-1 :0111 INT 20 ; выход из нашей "правильной" проги...
Наверное, вы уже поняли, что цикл повторяется до тех пор, пока CX не станет равен 0. Несмотря на то что CX — он как бы регистр общего назначения, для «зацикливания» используется именно он :). С остальными такой фокус не проходит. Это и есть так называемая «специализация регистров», о которой мы уже вскользь упоминали.
Протрассируйте эту программу! Искренне надеюсь, что вы поняли, чем это я вас тут загружал.
Медитируйте!
#2. А теперь вопрос на засыпку ;). Сколько раз выполнится следующий цикл:
:0102 MOV CX,0000 :0105 ADD AX,0001 :0108 LOOP 0105
Очевидный ответ — 0 раз. В CX же у нас занесен 0. Так вот — ответ неправильный.
Менее очевидный ответ — 1 раз! Ведь перед LOOP’ом сложение один раз все-таки выполнится. Так вот, этот ответ тоже неправильный.
Самые подозрительные могут сразу же посмотреть на этот цикл под отладчиком, и с удивлением обнаружат, что LOOP сначала уменьшает значение CX (0-1=FFFF), а потом уже проверяет, не равен ли он нулю. И с гордостью за задний ум своей головы воскликнут: FFFFh раз!
Так вот: этот ответ близок к истине, но тоже неправильный
Правильный ответ — цикл выполнится 10000h (65536d) раз.
Но только вы и мне не верьте! Истинно только то утверждение, которое вы сами проверили на практике. Медитируйте!
1.9. Немножко оптимизации
Как мы уже говорили, линейное программирование — это плохо, но не всегда. Сравните размеры ваших линейной и нелинейной программ. Не знаю, как у вас, но у нас линейная «весит» 27, а нелинейная — 19 байтов. Как по-вашему, какая быстрее работать будет?
— Ну, естественно, нелинейная, потому что она меньше! — скажете вы и будете неправы.
Попытайтесь оттрассировать «зацикленную». Не правда ли, она трассируется намного дольше своего линейного аналога?
Угу, всё поняли? Сам знаю, что ни черта.
Объясняю: в «зацикленной» программе «компутеру» приходится выполнять БОЛЬШЕ команд, нежели в «незацикленной».
Аргументирую это голословное утверждение следующей таблицей (построенной на основе трассировки):
Что делает линейная | Что делает нелинейная |
AX=1 | AX=0 |
Помещаем в стек 1 | CX=5 |
AX=2 | AX=AX+1=1 |
Помещаем в стек 2 | Помещаем в стек 1 |
AX=3 | Конец цикла — переход |
Помещаем в стек 3 | AX=AX+1=2 |
AX=4 | Помещаем в стек 2 |
Помещаем в стек 4 | Конец цикла — переход |
AX=5 | AX=AX+1=3 |
Помещаем в стек 5 | Помещаем в стек 3 |
Достаем из стека 5 | Конец цикла — переход |
Достаем из стека 4 | AX=AX+1=4 |
Достаем из стека 3 | Помещаем в стек 4 |
Достаем из стека 2 | AX=AX+1=5 |
Достаем из стека 1 | Помещаем в стек 5 |
Выход | CX=5 |
Достаем из стека 5 | |
Конец цикла — переход | |
Достаем из стека 4 | |
Конец цикла — переход | |
Достаем из стека 3 | |
Конец цикла — переход | |
Достаем из стека 1 | |
Выход |
Ну и как по-вашему, какую из двух простыней процессор быстрее обработает? Сказать вам по секрету? А вот ничего я вам не скажу! Сами думайте! :]
Как сейчас помню, был в моем Турбо-Си в преференсах к компилятору такой радиобуттон: «оптимайзить» по размеру или по скорости выполнения. Угадайте, на чем основан принцип этой оптимизации?
Только не вздумайте писать линейные проги! Пишите «нелинейные»! Нелинейную в линейную «переоптимайзить» — как два пальца намочить! А вот наоборот — :((
Резюме — бесплатный сыр бывает только в мышеловке, и за все надо платить. Компактность и скорость — обычно параметры конфликтующие, поэтому в каждом конкретном случае нужно выбирать, что предпочтительнее. Исследования, проведенные в свое время еще Кнутом, показали, что 80% времени затрачивается на выполнение 20% программы. Соответственно, рекомендуется тратить время на оптимизацию скорости именно тех 20% программы, а остальные можно оптимизировать по размеру (в частности, за счет циклов). Так и получается баланс между компактностью и скоростью программы ;).
1.10. Разборка с процедурами
#1. В разделе 1.7. #4 мы сделали глупую линейную программульку, выводящую окошки. Обещали, что в следующей главе сделаем ее менее «тупой», да отвлеклись почему-то на циклы и стек. То есть я-то знаю, ПОЧЕМУ, но вот вам об этом — не скажу! Догадайтесь сами. Итак, поехали…
Шаг первый. Внимательно посмотрев на «линейную» прогу из 1.7. #4 и прочитав «условие задачи» из главы 1.7. #1, вы обязаны возмутиться — зачем мы использовали команду MOV, если и ежу понятно, что отличия следующего окошка от предыдущего можно выразить более лаконично: BH=BH+10, CH=CH+1, CL=CL+1, DH=DH-1, DL=DL-1? И не нужно напрягать мозги, подсчитывая новое значение регистра вручную.
Если вы так подумали, то оказались совершенно правы! Программу из #4 запросто можно было представить в таком вот виде:
:0100 XOR AL,AL ;окошко первое :0102 MOV BH,10 :0104 MOV CH,05 :0106 MOV CL,10 :0108 MOV DH,10 :010A MOV DL,3E :010C MOV AH,06 :010E INT 10 :0110 ADD BH,10 ;окошко второе :0113 ADD CH,01 :0116 ADD CL,01 :0119 SUB DH,01 :011C SUB DL,01 :011F INT 10 :0121 ADD BH,10 ;окошко третье :0124 ADD CH,01 :0127 ADD CL,01 :012A SUB DH,01 :012D SUB DL,01 :0130 INT 10 :0132 ADD BH,10 ;окошко четвертое :0135 ADD CH,01 :0138 ADD CL,01 :013B SUB DH,01 :013E SUB DL,01 :0141 INT 10 :0143 ADD BH,10 ;окошко пятое :0146 ADD CH,01 :0149 ADD CL,01 :014C SUB DH,01 :014F SUB DL,01 :0152 INT 10 :0154 INT 20 ;конец программы
И несмотря на то, что размер ее оказался несколько большим, она тоже будет работать правильно :).
Но тут любой более или менее наблюдательный программер возмутится повторно — да что это за программа такая? В ней целых четыре раза повторяется один и тот же кусок:
:0143 ADD BH,10 :0146 ADD CH,01 :0149 ADD CL,01 :014C SUB DH,01 :014F SUB DL,01 :0152 INT 10
И знаете что? Этот наблюдательный программер будет прав! А если он еще и нехорошо выразится по поводу такого «неправильного» стиля программирования — будет прав… или почти прав…
Есть такой процесс — оптимизация, одной из особенностей которой является уменьшение параметризации (параметризация — вставка процедур в место вызова или фиксация значения отдельных параметров) и развертка циклов. То, что обычно такими вещами должен заниматься компилятор, суть меняет не сильно — тем более, что ассемблер оптимизацией сам не занимается :). Но до ассемблера мы с вами еще не добрались, поэтому говорить об этом пока еще рано.
Внимательно всмотритесь в полный текст программы и в этот выделенный кусок. И помедитируйте над ним до полного просветления текущей «обстановки»…
#2. Итак, у нас есть ПОВТОРЯЮЩАЯСЯ ЧАСТЬ программы. А еще у нас есть пальцы, которым, как правило, лень набивать длинные «простыни» программного кода. Это одна из многочисленных причин, по которым и придумали такого «зверя» как ПОДПРОГРАММУ (она же — ПРОЦЕДУРА, она же — ФУНКЦИЯ). Остальные причины мы рассмотрим попозже, а вот на счет «лени» поговорим прямо сейчас:
Если мы возьмем наш «часто повторяющийся» кусок программы и допишем в конец команду RET, то получится у нас именно ПРОЦЕДУРА — во всей своей красе:
:011E ADD BH,10 ;"точка входа"; она же - начало "тела". :0121 ADD CH,01 :0124 ADD CL,01 :0127 SUB DH,01 :012A SUB DL,01 :012D INT 10 ;конец "тела" :012F RET
Красота ее вот в чем заключается — процедуру можно «вызвать» командой CALL :)))
Все более чем просто. Когда в программе встречается CALL с указанием АДРЕСА-НАЧАЛА-ПРОЦЕДУРЫ (в нашем случае это 011E), то компьютер «идет» по этому адресу и выполняет все команды, расположенные между «точкой входа» (включительно) и командой RET, то есть так называемое «тело» процедуры.
RET — это тоже команда, но к «телу» (адреса 11E… 12D) она не относится. Она является «ОРГАНИЗАТОРОМ» этого «тела». Процессор, встретив команду RET, возвращает управление обратно после последнего CALL (т.е. «перепрыгивает» на строчку ниже «вызвавшего» данную процедуру CALL’а)…
Короче, CALL XXXX означает — «выполнить процедуру, начинающуюся по адресу XXXX». А RET означает — «конец процедуры» и, соответственно, переход на строчку ниже вызвавшего его CALL’а.
Если же говорить более формально и строго, то процедура — это средство обобщения, когда некоторая общая последовательность действий получает «имя», и потом при необходимости ПОВТОРНОГО ИСПОЛЬЗОВАНИЯ данного кода к нему просто идет обращение по «имени» (напомним, что в отличие от языков высокого уровня и даже ассемблера, в машинном коде от имен остаются одни только адреса, а язык, принимаемый DEBUG, является промежуточным между машинным кодом и ассемблером). Более того, следующим логическим шагом после обобщения является параметризация, когда некоторые части общего кода зависят от передаваемой извне информации (параметров), с чем очень хорошо знакомы программисты на языках высокого уровня. Но о параметризации и ее применении в ассемблере мы поговорим в другой раз.
Не ругайтесь. Мы знаем, что вы ни черта не поняли. А по сему набьем в debug’е эту прогу и посмотрим, что она делает.
Те, кто читал внимательно, могут отметить, что инструкция CALL по своему действию очень похожа на инструкцию генерации прерывания INT, с той лишь разницей, что аргументом CALL является адрес процедуры, а не индекс в таблице «векторов прерываний», где и хранится адрес обработчика прерывания (той же процедуры). А для особо продвинутых отметим, что в ранних моделях процессоров от Intel при подаче запроса на обработку от внешнего устройства контроллер прерываний, помимо собственно сигнала прерывания, посылал в процессор инструкцию CALL.
#3. Кстати, вы уже поняли, почему мы называем debug «до боли любимой программой»? Нет? Неужели вы еще не полюбили это произведение программерского гения всеми фибрами своей души? Еще нет? М-да… мы в вас разочаровались.
А по сему: — НАБИВАЕМ! — злобно кричим, брызгая слюной на эргономичный коврик:
:0100 XOR AL,AL ;первое окошко рисуем, как и раньше... :0102 MOV BH,10 :0104 MOV CH,05 :0106 MOV CL,10 :0108 MOV DH,10 :010A MOV DL,3E :010C MOV AH,06 :010E INT 10 :0110 CALL 011E ;четыре раза вызываем подпрограмму, :0113 CALL 011E ;начинающуюся по адресу 011E :0116 CALL 011E :0119 CALL 011E :011C INT 20 ;выход из программы... :011E ADD BH,10 ;начало процедуры :0121 ADD CH,01 :0124 ADD CL,01 :0127 SUB DH,01 :012A SUB DL,01 :012D INT 10 :012F RET ;конец процедуры
Не правда ли, красиво получилось?
Первое, что вас может смутить — это то, что команда выхода (INT 20) расположена не там, где вы привыкли, то есть не в конце программы.
Ну что я вам могу на это ответить? Концы — они-то разные бывают! Последняя строчка в листинге вовсе не означает, что последней будет выполняться именно она. И это не должно вас смущать! А если все же смущает — смотрим, как работает эта прога из-под отладчика.
Итак, до адреса 0110 вам все должно быть понятно, мы это рассматривали. Трассируем дальше…
— Что значит «трассируем»? — попросите вы напомнить.
Мысленно мы ругаем вас нехорошими словами (ну сколько раз повторять-то можно!), а вслух скажем: Команда «T» и Enter. Команда «T» и Enter. Команда «T» и Enter…
Команда CALL 011E по адресу 0110 говорит процессору: «Дальше мы не пойдем, пока не выполним простыню, начинающуюся по адресу 011E». И далее, естественно, следует переход на этот адрес.
Входим в тело процедуры, начиная с 011E, и выполняем команды до 012D включительно…
А теперь внимательно смотрим, на какой адрес нас «перекинет» команда RET.
На 113-й? И это правильно! По 113-му адресу у нас какая команда? Да вот опять CALL 011E!
Опять процедура с адреса 011E, опять RET[URN] на строку ниже, то есть на 116…
И так далее, до того момента, пока следующей строчкой не окажется INT 20 — собственно, на этом и программе конец.
Ну оно и ежу понятно, что, несмотря на то что INT 20 — не в конце программы, последним выполнится именно он.
Короче, куда бы вас не посылали всяческие «столбы с указателями», конец вашего пути только один… А плутать вокруг да около этого конца вы можете сколько вам заблагорассудится…
Кстати, именно это и является одной из многочисленных тайн программинга.
Кто после этого скажет, что программисты — недZенствующие люди?
#4. Те, кто внимательно ознакомились с циклами, они и на этом не остановятся! Посмотрев на адреса 110…119, они вообще возьмут и возомнят себя воистину крутыми парнями! Знаете, что они напишут? А вот что (предвидим!):
:0100 XOR AL,AL :0102 MOV BH,10 :0104 MOV CH,05 :0106 MOV CL,10 :0108 MOV DH,10 :010A MOV DL,3E :010C MOV AH,06 :010E INT 10 :0110 MOV CX,0004 :0113 CALL 011A :0116 LOOP 0113 :0118 INT 20 :011A ADD BH,10 :011D ADD CH,01 :0120 ADD CL,01 :0123 SUB DH,01 :0126 SUB DL,01 :0129 INT 10 :012B RET
То бишь еще и CALL в цикл при помощи MOV CX,4 и LOOP’а «закрутят». И что? А попробуйте!
Что, не «пашет»? А что надо делать, если «не пашет, а должно бы»? Правильно! Смотреть из-под отладчика!
Смотрим? Если посмотрите, то сразу же и «загвоздку» увидите — CX, использованный в качестве «счетчика» циклов, «перебивает» тот же CX, но используемый как «координаты верхнего левого угла окна». И что с этим делать прикажете?
Вот вы и столкнулись с одной из самых больших проблем. В процессорах фирмы Intel есть только 4 регистра общего назначения (и то в большинстве случаев — специализированных). Помните, мы вам говорили об этом?
А теперь попробуйте выкрутиться из этой нехорошей ситуации с использованием стека :). Кстати, весьма «мозгопрочищающая» задачка :).
1.11. Переходы
#1. «Переходы» бывают разные. Если вы пришли в гости, а вас просто послали к черту — такой переход называется «безусловный». А нежели вам сказали: «Если без пива — то иди к черту, а если с пивом — тогда проходи», — то это уже «условный» переход.
Соответствено, для успешного перехода необходимо указать: ПРИ КАКОМ УСЛОВИИ выполнить переход, КУДА ПЕРЕЙТИ, ну и, наконец, сам пинок под зад нужно СДЕЛАТЬ, чтобы переход «гостя» в заданном направлении все-таки «состоялся».
Безусловный переход у нас «делает» мнемоническая команда JMP, после которой следует указать адрес, на который «компьютер» должен пойти «на» ;). В данном случае УСЛОВИЕМ у нас будет «при любых обстоятельствах»: хоть пустой, хоть с пивом, хоть с … все равно. Когда рисовали окошки, вы уже использовали эту команду для создания «спецдефекта». Если кто еще не понял, что делает эта команда — к нему (см. 1.7 #4) и отсылаю. Сделайте «спецдефект» и посмотрите на него под отладчиком. Когда до вас дойдет, почему мы там не предусмотрели выхода (INT 20h) — можете переходить к п.2 текущей главы.
#2. Условный переход у нас организуется в два шага. На первом шаге мы вычисляем условие («принес ли пиво?»), на втором шаге «посылаем» или не «посылаем» — в зависимости от результатов вычислений. Можно привести такую аналогию — на первом шаге два груза кладутся на весы, сравнивающие их массу. Соответственно, возможны только три их положения: наклон влево (груз в левой чашке тяжелее), наклон вправо (груз в правой чашке тяжелее) и равновесие. На втором шаге мы предпринимаем действия в зависимости от положения весов.
Например, на первом шаге можно использовать как «аптекарские весы» инструкцию CMP, которой обязательно нужно указать, ЧТО и С ЧЕМ она будет сравнивать.
Пишем, например,
CMP AX,BX
В зависимости от значений регистров у нас возможны следующие состояния: «наклон влево» (AX &tt; BX), «наклон вправо» (AX > BX) и «равновесие» (AX = BX). Таким образом ВЫЧИСЛЕНИЕ УСЛОВИЯ у нас уже организовано :). Только условие не бинарное, а есть еще и «серединный вариант» (и даже несколько других!). Это нормально. Это для того сделано, чтобы мы могли выражения типа «больше-или-равно», «меньше-или-равно» да и просто «равно» в своих программах использовать…
Итак, УСЛОВИЕ есть. Теперь решаем, что нам делать при том или ином условии. Вот далеко не полный список возможных «прыг-скоков»:
- JE — переход если равно;
- JNE — переход если не равно;
- JA — переход, если больше;
- JAE — переход, если больше или равно;
- JB — переход, если меньше;
- JBE — переход, если меньше или равно…
- и т.д.
Естественно, что после мнемоники («прыгнуть, если») должен стоять АДРЕС, куда нужно «прыгнуть», если условие соблюдено. Если же условие не соблюдено, то прыжок не происходит, и выполняется нижеследующая строка программы.
Задание на медитирование — зрительно представьте себе «весы правосудия». И побросайте на их чашки разную шестнадцатеричную дрянь в различных «пропорциях» и «комбинациях». Просветлиться вы должны следующим образом — в какую бы сторону эти ваши «весы» ни склонялись, вы все равно заставите систему работать так, как ЭТО вам угодно! «Весы» — они только констатируют факт. А вот «приговор» выносят судьи. Хе… и пусть после этого только кто-нибудь скажет, что программерам чужда политика — дело, как известно, весьма грязное.
А о чем это мы? Ах да, переходы…
#3. Продолжим программировать, что ли? Напишем что-нибудь красивое и неизменно тупое? С использованием условных и безусловных переходов?
Поехали! Слабаем мы сейчас что-то наподобие графического редактора :)). Не верите?
У-у-у… Сложная задачка! Если въедете, что да как — значит, молодцы! Значит, разобрались-таки с дzебагом! Значит, подключились-таки к программерскому эгрегору и более или менее привели в порядок свои мозги :)… А это сложная штука, мы вам скажем — мозги в порядок приводить! Особенно когда есть куча инструментов, которые «порядок в коде» сами как бы наводят :).
Думаете, вы по нашим текстам программировать учитесь? По-настоящему программировать мы еще не начали! Все, чем мы пока занимаемся — это приводим в порядок свои мозги и тренируемся на кнопки клавиатуры нажимать :)). А вот ско-о-оро НАЧНЕМ… тогда «прощай, здоровье» будет настоящее!
Итак, сначала рассмотрим прерывания, которые в нашем «графическом редакторе» будут использоваться. Их три штуки, и все — BIOS’овские:
mov AH,00 ;функция 0 прерывания 10h устанавливает "режим видео" mov AL,04 ;"на входе" (в регистре AL) - номер режима . int 10h ;если AL=4, то устанавливается цветной 320х200 графический режим mov CX,64h ; CX и DX - координаты точки mov DX,64h mov AH,0Ch; функция 0Ch (в регистре AH!) прерывания 10h рисует точку. mov AL,1Bh ;в AL - "код цвета" этой точки. int 10h
Опять-таки — подробности о координатах и «кодах» цвета ищите сами!! Благо, знаете, где искать.
mov AH,00 ;функция 0 прерывания 16h int 16h ;"читать код нажатой клавиши"
Эта функция считывает код сканирования и код символа (клавиши на клавиатуре и соответствующий ей ASCII-код) из буфера клавиатуры (есть такой). Если в буфере ничего нет — она ждет, пока там что-нибуть появится. То есть ЖДЕТ, чтобы вы нажали на какую-нибудь клавишу, код которой будет занесен в регистр AX. Причем в AL — «символ», а вот в AH — так называемый «код сканирования»…
Кодами вы пока голову не забивайте. Достаточно знать, что после нажатия клавиши Up в AH «попадет» значение 48h, Down — 50h, Left — 4Bh, Right — 4Dh.
Как работает последний кусок кода, обязательно проверьте под отладчиком, это полезно :).
#4. И лезем, лезем в наш горячо любимый DZEBUG, дабы набить там драгоценные строчки машинного мнемонического никому-кроме-вас-непонятного кода!
-a :0100 MOV AH,00 ;устанавливаем графический режим :0102 MOV AL,04 :0104 INT 10 :0106 MOV CX,0064 ;координаты Первой Точки :0109 MOV DX,0064 :010C MOV AH,0C ;рисуем точку! :010E MOV AL,1B :0110 INT 10 :0112 MOV AH,00 ;ждем нажатия на клавишу :0114 INT 16 :0116 CMP AH,4B ;а не нажат ли у нас Left? :0119 JE 012A ;если да - то "прыг"! ;если нет - то следующая строчка :011B CMP AH,4D ;а не нажат ли у нас Right? :011E JE 012D :0120 CMP AH,48 ;а не нажат ли у нас Up? :0123 JE 0130 :0125 CMP AH,50 ;а не нажат ли у нас Down? :0128 JE 0133 :012A DEC CX ;задаем новые координаты, в зависимости :012B JMP 010C ;от нажатой клавиши - и скок в начало! :012D INC CX :012E JMP 010C :0130 DEC DX :0131 JMP 010C :0133 INC DX :0134 JMP 010C
Тут один из автору вставили шпильку:
«Не хватает проверки и выхода (со сбросом видеорежима!) по Esc — такие действия должны быть обязательным атрибутом, а не домашним заданием.»
Совершенно верная шпилька, товарищи! Но все равно — пусть это будет домашним заданием.
Если вы все ввели правильно — должно заработать! Полюбуйтесь плодами своей медитации… Красиво?
#5. А сейчас мы это все дело прокомментируем:
- адреса 100…200 — устанавливаем графический режим, указываем функцию (100), указываем номер режима (102) и вызываем прерывание (104);
- адреса 106…109 — инициализируем координаты первой точки. Координаты последующих точек будут определяться «динамически» — в зависимости от нажатой клавиши;
- адреса 10С…110 — рисуем точку. Первый раз — в координатах, инициализированных командами по адресам 106 и 109. Все последующие разы — по координатам, «инкрементированным» или «декрементированным» (во словеса!) по адресам: 12A, 12D, 130 и 133;
- адреса 112…114 — ждем нажатия на клавишу;
- адреса 116…128 — «щемим» нужные нам клавиши. «Взвешиваем». На каждую из курсорных клавиш по адресам 12A…134 приготовлены «обработчики». Если найдена «нужная клавиша», то делаем прыг на «обработчик» этой клавиши;
- aдреса 12A…134 — в этом блоке определяется, что делать с координатами следующей точки. После чего — прыжок на «рисуем точку» :).
Правда, здорово получилось?
1.12. Данные
#1. Работать с кодом мы с вами научились. Сейчас поучимся заставить наш код обрабатывать данные…
Итак, запускаем DZEBUG и вводим следующую команду:
-e cs:115
Которая означает: «набиваем память всяким дерьмом начиная со смещения 115».
В ответ он вам выплюнет:
17B3:0115 00.
Что означает: байт по смещению 115 равно 00. И точка. Но это не простая точка — это приглашение ввести НОВОЕ ЗНАЧЕНИЕ этого байта. Когда вы его ввели, нужно нажать на пробел.
Если вы вознамеритесь последовательно ввести 1,2,3,4,5, то это будет выглядеть приблизительно так:
17B3:0115 00.1 75.2 AD.3 17B3:0118 66.4 FF.5 [Enter]
А теперь делаем дамп памяти и смотрим, что за дрянь у нас получилась…
А ведь получилoсь же!!
#2. Мы запросто умеем «присваивать» регистру любое значение (mov AL,1C какой-нить), запросто можем «копировать» содержимое одного регистра в другой (mov AL,BL например)… А сейчас мы с вами научимся при помощи той же команды MOV еще и с данными из памяти работать.
Все проще пареной репы… Если мы напишем
MOV AL,[115]
то в результате выполнения этой команды в регистр AL «внесутся» две шестнадцатеричные циферки (байт), которые по адресу 115 находятся. То есть в нашем случае AL станет равным 1.
А теперь посмотрите, что делает «обратная» команда:
mov AL,55 mov [115],AL
В первой строчке мы присвоили AL значение 55, а второй строчкой «скопировали» значения регистра в байт по адресу 115. Правда, проще некуда?
Обязательно посмотрите на этот процесс под отладчиком!
#3. А еще вот какой изврат с этим можно делать:
mov BX,115 mov AL,[BX]
Сие присваивает регистру AL значение байта по адресу 115 :). Ну… через посредника «BX» присваивает! Который у нас «переменная», как известно :).
mov AL,1C mov BX,115 mov [BX],AL
А этот кусок кода у нас «записал» 1C в сегмент данных по адресу 115 :). Ну, и извращения наподобие:
mov AL,[BX+1]
и
mov [BX+1],AL
Тоже весьма и весьма полезны в программерском деле :).
Короче: все, что в квадратных скобках, — это адрес в памяти, с которым вы собираетесь «работать». Другой вопрос, что этот адрес может быть «составным»…
#4. Низкоуровневый Paint мы с вами уже писали. Сегодня напишем низкоуровневый дZенский EXCEL.
Задание простое… Есть у нас табличка типа:
1 8 ? 2 9 ? 3 1 ? 4 2 ? 5 2 ?
в которой данные в формате HEX. И все, что нам нужно с ними сделать — это просуммировать каждую «строчку», а сумму занести в третий «столбец»… В EXCEL’е это делается элементарно… А на машинном уровне, в общем-то, не намного сложней!!
Для начала мы наберем «исходные данные» и зарезервируем место (например, забьем нулями) под третий столбец, в который собираемся помещать результат…
Набиваем блок данных, начиная с адреса, например, 115:
-e ds:115 17EA:0115 01.1 08.8 02.0 17EA:0118 09.2 02.9 00.0 03.3 03.1 00.0 04.4 04.2 17EA:0120 00.0 05.5 05.2 00.0
Вот так это у меня в DZEBUG’е выглядело :). Только я еще дамп посмотрел, правильно ли я ввел:
17EA:0110 03 E2 F3 CD 20 01 08 00-02 09 00 03 01 00 04 02 .... ........... 17EA:0120 00 05 02 00 6A 87 04 FF-76 FE 57 57 9A 5C 6C 87 ....j...v.WW.l.
Вроде правильно :)). Ну а программу я вот какую придумал:
17EA:0100 BB1501 MOV BX,0115 17EA:0103 B90500 MOV CX,0005 17EA:0106 8A07 MOV AL,[BX] 17EA:0108 024701 ADD AL,[BX+01] 17EA:010B 884702 MOV [BX+02],AL 17EA:010E 83C303 ADD BX,3 17EA:0111 E2F3 LOOP 0106 17EA:0113 CD20 INT 20
В BX я занес адрес начала блока данных (он же — верхний левый угол нашей таблицы). В CX внес 5, чтобы столько раз цикл выполнился (LOOP по адресу 111). А тело цикла вообще простое:
Команда по адресу 106 забирает в AL цифирь из первого столбца.
108 — суммирует «цифирь из первого столбца с цифирью из второго столбца» (сумма, само собой, в AL’е остается).
10B — записывает сумму в третий столбец :).
Ну и ADD BX,3 для перехода на следующую строчку :).
И все на этом…
Сделайте трассировку (внутрь INT 20 залезать не надо) и посмотрите на дамп нашего блока данных
Я и говорю: ПРОЩЕ ПАРЕНОЙ РЕПЫ!!
#5. Видите? В качестве переменных «в компьютере» можно использовать не только регистры, но и «куски» памяти! А уж там вы можете клепать свои переменные в почти неограниченном количестве! Единственное, что нужно иметь ввиду: с переменными-регистрами компьютер работает намного быстрее, чем с переменными-в-памяти :).
Кстати, если вы хотите сохранить плод своих сегодняшних трудов на веник, то имейте ввиду, что вы и сегмент данных тоже должны сохранить! То есть: вам нужно сохранить весь «диапазон» от адреса 100 до 123 включительно :).
Ну и, само собой, при попытке дизассемблирования с адреса 115 у вас абракадабра пойдет… мы об этом уже говорили и упоминали один из принципов фон Неймана.
Диагноз
Полагаю, вы уже поняли, что значит «выучить язык ассемблера» и теперь с удовольствием кинете грязью в того, кто скажет вам, что это сложно
Что значит «выучить язык», и что значит «программировать»? А проводите сами границы между этими понятиями! Только имейте в виду, кто скажет «выучить — значит все команды запомнить — тот дурак :((.
Слова и понятия извращать можно по-всякому. Переопределите собственный тип и носите свежеобмоченные пеленки в полной уверенности, что это носки… В моем понимании «знать ассемблер» и «изучить на ассемблере» — синонимы (хотя лингвисты могут придраться, но мне это пофиг). Согласно границам, которые провел для себя (гы… ну и для вас немножко) автор курса, ассемблер вы уже знаете, а вот программировать на нем пока еще не умеете…
Да о чем это я, в общем-то? (Утомлен кофием, поэтому речь несвязна)? Просто хотел сообщить вам, что первая часть курса закончилась. Вооружившись справочником команд и прерываний, вы уже можете программировать под дос. Если вы внимательно штудировали предыдущие главы, то идеология этого дела (под дос) вам уже должна быть понятна как 2х2=100b.