ПРОГРАММИРОВАНИЕ НА ЯЗЫКЕ АССЕМБЛЕРА
ПОД MS-DOS

1998 год
Новиков Максим Глебович.

Глава 3. Внутри компьютера

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

Мозг компьютера состоит из двух частей — мыслящей и запоминающей. Компьютер и работает только благодаря тому, что эти две части постоянно взаимодействуют друг с другом.

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

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

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

Замечу, что современные процессоры способны принимать за 1 раз и две, и четыре, и даже восемь серий сигналов (в последнем случае число контактов, предназначенных для этого, достигает 64), однако количество сигналов в одной серии всегда остается равным восьми.

Однако не будем излишне углубляться в аппаратные тонкости реализации памяти. Для нас больше важна логическая ее структура, которая тем не менее очень сильно напоминает физическую, поскольку является ее отражением и очень тесно с ней завязана. Логически память — это длинная последовательность неких абстрактных ячеек, каждая из которых имеет свой порядковый номер (адрес). Такая ячейка рассчитана на хранение 8-и логических сигналов. Зная адрес такой ячейки, процессор может быстро найти ту серию сигналов, которая ему в данный момент необходима, и отреагировать на нее нужным образом.

Каждый сигнал в серии может иметь всего два логических значения — 0 или 1. Единица соответствует физическому наличию электрического сигнала, ноль — его отсутствию. То есть отсутствие электрического сигнала рассматривается тоже как сигнал, который имеет логическое значение 0. В том, что сигнал имеет только два значения, нет ничего удивительного — технически гораздо проще, дешевле и надежнее отличать наличие электрического сигнала на контакте микросхемы от его полного отсутствия, чем различать, например, 10 его градаций (по величине напряжения или другим характеристикам).

Теперь перейдем к терминологии. Каждый логический сигнал (тот, что может иметь только два значения — 0 или 1) называется битом. Серия из 8-и различных битов называется байтом (легко подсчитать, что байт может содержать до 256 разных сочетаний битов), а последовательность логически связанных между собой байтов (команд для процессора) — программой.

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

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

Такая команда передачи управления необходима для того, чтобы организовать алгоритм наподобие следующего: «сравни эти два числа, и если они равны, сделай то-то (команды для этого „то-то“ лежат по такому-то адресу), а если не равны, то то-то (команды для второго „то-то“ лежат по другому адресу)». Такой алгоритм называется ветвлением: выполнение команд доходит до какой-то точки, после которой в зависимости от результата выполняется та или иная ветвь программы. В этом случае передача управления на другой адрес называется передачей управления по условию (условие в данном случае — это равенство или неравенство сравниваемых чисел).

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

Процессор всегда производит выборку команды с адреса, значение которого он берет из одной из своих внутренних ячеек памяти — регистров процессора. Этот регистр имеет свое название — IP. Регистр никак не связан с обычной памятью. Территориально он расположен внутри процессора, и после выполнения очередной команды его значение увеличивается, что дает возможность считать следующую команду из следующей ячейки памяти.

Регистр IP имеет размер 2 байта (16 бит). Таким образом он может кодировать уже не 256, как 1 байт, а 65536 чисел, следовательно, применяя этот регистр, процессор может адресовать максимум 65536 ячеек. То есть логично предположить, что максимальный размер программы не может превышать 65535 байт. Однако впоследствии я упомяну о возможностях преодоления этого барьера, хотя при программировании на ассемблере нам это вряд ли понадобится. :-)

Продолжим рассмотрение процесса безусловного перехода. Сначала в регистре IP содержится 0. После выполнения каждой команды число автоматически увеличивается на единицу, благодаря чему процессор при запросе следующей команды обращается к следующему по порядку адресу. Значит, для того, чтобы процессор перешел на другой адрес, надо просто соответствующее число занести в этот регистр. Проследим, как это происходит:

Предположим, что процессор выполнял какие-то команды, последовательно выбирая их из памяти, но вот, когда IP содержал число 48 (то есть процессор достиг 49 команды, т.к. нумерация начинается с 0), по этому адресу ему встретился байт, в котором зашифровано число 235. Это команда безусловного перехода. Процессор вошел в режим перехода на другой адрес. Потом он увеличивает IP на 1, и достает следующий байт, содержащий разницу между текущим адресом и адресом, на который необходимо перейти. Предположим, следующий байт равен 20. Он берет, и прибавляет это число к числу, находящемуся в IP. Таким образом IP становится равным 68, и процессор выбирает следующую команду уже из адреса 68. Таким образом и осуществляется безусловный переход.

В команде, как мы видим, учавствуют 2 байта. Один — это собственно команда перехода, а второй — данные для вычисления адреса. Фактически второй байт содержит смещение в памяти относительно текущего адреса до того места, куда надо осуществить переход.

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

Кроме команд перехода есть множество других команд, позволяющих наполнить программу смыслом. Они кодируются другими числами.

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

Глава 4. Двоичная система счисления

В предыдущей главе мы узнали, что одним байтом может кодироваться 256 чисел. Действительно, если мы начнем перечислять все вариации, начиная с 00000000, и кончая 11111111, мы насчитаем их как раз 256. Но чтобы каждой вариации присвоить свое число, необходимо изобрести какую-то систему, чтобы не запутаться. Для этого идеально подходит использование так называемой двоичной системы счисления. Что же это такое?

Обычно мы считаем: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11… Видите — после цифры 9 для отображения числа мы стали пользоваться уже двумя числами. Сколько же получилось одинарных чисел? Их 10, включая ноль.

Так вот: двоичная система — это когда таких цифр — две. Оп-па! В числах не могут применяться цифры 2–9. Попробуем посчитать в двоичной системе, используя только нули и единицы. 0, 1, далее цифры кончаются, и следующее по порядку число, состоящее только из нулей и единиц — двойное. Это 10. Десяток завершен.

После 10 следует 11, как и в десятичной системе, а затем мы вынуждены применить уже тройное число — 100. Считаем дальше: 101, 110, 111. Дальше возможно число только с четырьмя цифрами: 1000, 1001, 1010, 1011, 1100, 1101, 1110, 1111. Какое следующее число?

Замечу, что если в десятичной системе счисления смещение цифры в числе на одно место влево увеличивает ее вес в 10 раз, то в двоичной — в 2 раза. Например, 700 в десять раз больше 070, но двоичное 100 всего в два раза больше двоичного 010 (можешь проверить).

Мы видим, что для счета можно вполне обходиться двумя цифрами — единицей и нулем. Правда с ростом числа резко увеличивается его длина, но зато мы можем применить их для записи в байт, где каждый бит как раз чисто физически может принимать только два значения.

Чтобы при записи на бумаге не путать десятичные и двоичные числа, в конце двоичного числа ставят латинскую «b», например 11111110b = 254. Чтобы быстро переводить числа из двоичной в десятичную и наоборот, можно воспользоваться такими алгоритмами:

Перевод двоичного в десятичное на примере числа 10001101b:

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

1 0 0 0 1 1 0 1
128 64 32 16 8 4 2 1

Теперь сложим те десятичные цифры, которые соответствуют единичкам двоичного числа:

128+8+4+1=141

Результат и будет десятичным эквивалентом двоичного числа 10001101b.

Перевод десятичного в двоичное на примере числа 141:

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

141 / 2 = 70 остаток 1
70 / 2 = 35 остаток 0
35 / 2 = 17 остаток 1
17 / 2 = 8 остаток 1
8 / 2 = 4 остаток 0
4 / 2 = 2 остаток 0
2 / 2 = 1 остаток 0
1 / 2 = 0 остаток 1

Считываем остатки снизу вверх: 10001101b. Правда, магия? ;-)

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

На этом закончим второе занятие, на котором, я надеюсь, ты усвоил основу вычислительной техники — двоичную систему счисления, и понял причину и природу ее существования. Рекомендую еще пару раз прочитать эту статью, поскольку материал довольно сложный, а излагал я его в максимально сжатой форме. Если все равно некоторые места окажутся непонятными, не отчаивайся :-) Отложи текст, и попробуй освоить его завтра. За ночь информация уляжется, и тот же самый текст на следующий день станет простым и понятным. Это не магия, это свойство человеческого мозга. Химические процессы в мозгу не ускоришь, пускай они протекут так, как это им положено.

Некоторые вещи смогут стать понятными только потом, в процессе практических занятий. Так что не стоит торопиться. Всему свое время.

[Вернуться в начало]
Главы 1–2
Главы 5–7 [Оставить отзыв в гостевой]
Hosted by uCoz