Существует еще одна интерпретация слова, о которой я пока не упоминал слово может интерпретироваться как команда. Слова памяти содержат не только данные, на основании которых действует компьютер, но и программу, действующую на эти данные. Существует ограниченное количество операций, которые могут быть выполнены центральным процессором — ЦП — и часть некоего слова (обычно несколько первых битов) интерпретируются как название типа команды, которая должна быть выполнена. Что же означают остальные биты в слове-команде? Чаще всего, они говорят, на какие другие слова памяти надо воздействовать. Иными словами, остальные биты являются указателем на какое-либо другое слово (или слова) памяти. Каждое слово в памяти имеет свое расположение, как дом на улице; это расположение называется адресом. Память может иметь одну «улицу» или много «улиц» — они называются страницами. Таким образом, адрес любого слова — это номер страницы (если память подразделена на страницы) и его расположение на этой странице. Итак, «указатель» — это часть команды, содержащая числовой адрес какого-либо слова (или слов) в памяти. На указатель нет никаких ограничений, так что команда может даже указывать сама на себя — в этом случае, когда она действует, она изменяет саму себя.
Откуда компьютер знает, в какой момент надо выполнять ту или иную команду? Об этом заботится ЦП. В нем есть специальный указатель, который указывает (то есть хранит соответствующий адрес) на следующее слово-команду. ЦП извлекает это слово из памяти и копирует его на специальное слово в самом ЦП. (Слова в ЦП обыкновенно называют не словами, а регистрами.) После этого ЦП выполняет эту команду. Команда может вызывать любую из большого количества возможных операций; типичные операции включают:
ДОБАВИТЬ слово, указанное в команде, к регистру. (В этом случае данное слово интерпретируется как число.)
НАПЕЧАТАТЬ слово, указанное в команде, в виде букв. (В этом случае данное слово интерпретируется не как число, а как строчка букв.)
ПЕРЕЙТИ к слову, указанному в команде. (В этом случае ЦП интерпретирует данное слово, как следующую команду.)
Если первоначальная команда не содержит явного указания поступить иначе, ЦП просто обращается к следующему слову и интерпретирует его, как команду. Иными словами, ЦП предполагает, что он должен двигаться вдоль по «улице» последовательно, как почтальон, интерпретируя слово за словом как команды. Однако это последовательное движение может быть прервано некоторыми командами, такими как, например, ПЕРЕХОД.
Язык машины и язык ассемблера
Вы только что прочитали очень краткий обзор машинного языка. В этом языке типы существующих операций составляют конечный репертуар, который не может быть расширен. Таким образом любая программа, какой бы большой и сложной она не была, должна состоять из этих типов команд. Рассматривать программу, написанную на машинном языке, это все равно что рассматривать молекулу ДНК атом за атомом. Если вы вернетесь к рис. 41, где изображена последовательность нуклеотидов молекулы ДНК (и имейте в виду, что в каждом нуклеотиде около двух дюжин атомов) и представите себе, что вам надо записать, атом за атомом, ДНК крохотного вируса (уж не говоря о человеке!), то вы получите представление о том, что такое создание сложной программы на машинном языке и каково пытаться понять, что происходит в этой программе, если у вас есть доступ только к ее описанию на машинном языке.
Надо сказать, что первоначально программирование делалось на еще более низком уровне, чем машинный язык: соединялись определенные провода, так что нужные операции как бы «телеграфировались» машине. Этот процесс настолько примитивен по современным понятиям, что теперь его трудно себе вообразить. И все же люди, впервые это сделавшие, безусловно испытали такую же радость, какую когда-либо чувствовали создатели современных компьютеров…
Перейдем теперь на более высокую ступень иерархии уровней описания программ — уровень языка ассемблера. Между машинным языком и языком ассемблера дистанция не так уж велика; скорее, это маленький шажок. Главное здесь то, что между командами на языке машины и командами на языке ассемблера существует взаимно однозначное соответствие. Язык ассемблера представляет отдельные команды машинного языка в виде «блоков», так что, желая например, записать команду сложения, вместо последовательности битов «010111000» вы пишете просто ДОБАВИТЬ, и вместо того, чтобы давать адрес в двоичном коде, вы можете указать на слово в памяти, назвав его по имени. Следовательно, программа на языке ассемблера — это что-то вроде программы на машинном языке, сделанной более удобной для людского чтения. Машинную версию программы можно сравнить с деривацией ТТЧ, записанной в туманной нотации Гёделевых номеров, в то время как версия на языке ассемблера сравнима с изоморфной деривацией ТТЧ, записанной в более легкой для понимания первоначальной нотации самой ТТЧ. Или, возвращаясь к образу ДНК: разница, существующая между машинным языком и языком ассемблера подобна разнице между определением нуклеотидов при помощи их кропотливого, атом за атомом, описания и определением нуклеотидов по именам (как, например, «A», «G», «С» или «Т»). Подобная операция «превращения в блоки» представляет собой огромную экономию труда, хотя концептуально почти ничего при этом не меняется.
Программы, переводящие программы
Возможно, что самое важное в языке ассемблера — не его отличие от машинного языка, которое не столь уж велико, но сама идея того, что программы вообще могут быть написаны на различных уровнях. Ведь компьютерная аппаратура построена так, чтобы «понимать» программы на машинном языке — последовательности битов — а не буквы и не числа в десятичной записи! Что происходит, когда в эту аппаратуру вводится программа на языке ассемблера? Это напоминает попытку заставить клетку узнать бумажку с записанном буквами нуклеотидом, вместо самого нуклеотида со всеми его химическими компонентами. Что делать клетке с этой бумажкой? Что делать компьютеру с программой на языке ассемблера?
Здесь мы подошли к главному: возможно написать на машинном языке программу-переводчик. Эта программа, под названием ассемблер, берет имена, десятичные числа и другие сокращения, которые программист может легко запомнить, и превращает их в монотонные, но необходимые последовательности битов. После того, как программа на языке ассемблера собрана (то есть переведена), она — точнее, ее эквивалент на машинном языке — выполняется компьютером. Однако здесь это лишь вопрос терминологии; программа какого уровня выполняется машиной? Вы не ошибетесь, сказав, что выполняется программа на машинном языке поскольку в выполнении любой программы всегда задействована аппаратура — но вполне разумно также предположить, что выполняется программа на языке ассемблера. Например, вполне можно сказать: «В данный момент ЦП выполняет команду „ПЕРЕХОД“», вместо того, чтобы говорить «В данный момент ЦП выполняет команду „111010000“». Пианист, играющий ноты G-E B-E-B-G. в то же время играет арпеджио в ми миноре. Нет причин отказываться от описания вещей с точки зрения высших уровней. Таким образом, можно считать, что программа на языке ассемблера выполняется одновременно с программой на машинном языке то, что происходит в ЦП, можно описать двумя способами.
Языки высших уровней, компиляторы и интерпретаторы
На следующем уровне иерархии крайне важная идея о том, что сами компьютеры можно заставить переводить программы с высших на низшие уровни, развивается еще далее. В начале 1950-х годов, когда программа ассемблера уже использовалась в течение нескольких лет, было подмечено, что существуют несколько характерных структур, появляющихся в программе за программой. По-видимому, так же как и в шахматах, это были некие характерные структуры, естественно возникающие тогда, когда люди пытаются найти алгоритмы — точные описания процессов, которые они хотят осуществить. Иными словами, кажется, что в алгоритмах есть некие компоненты высшего уровня, при помощи которых они могут быть описаны с большей легкостью и эстетизмом, нежели на весьма ограниченном машинном языке или языке ассемблера. Обычно такой компонент высшего уровня в алгоритме представляет собой не одну-две машинных команды, но целый конгломерат; при этом эти команды не обязательно соседствуют в памяти. Подобный компонент может быть представлен на языке высшего уровня как некое единство, или блок.
Оказывается, что кроме стандартных блоков (только что открытых компонентов, из которых могут быть построены все алгоритмы), почти все программы содержат еще большие блоки — так сказать, суперблоки. Эти суперблоки меняются от программы к программе, в зависимости от типа задания на высшем уровне, которое данная программа должна выполнить. Мы уже говорили о суперблоках в главе V, употребляя общепринятые названия, «подпрограммы» и «процедуры». Ясно, что если бы удалось определить новые единицы высшего уровня в терминах уже известных единиц и затем вызывать их по имени, это было бы важнейшим дополнением к любому языку программирования. Таким образом разделение на блоки оказалось бы включено в сам язык. Вместо определенного репертуара команд, из которых пришлось бы кропотливо собирать каждую программу, программист смог бы создавать свои собственные модули. Каждый из них имел бы собственное имя и мог бы использоваться в любом месте программы, как если бы он был неотъемлемой характеристикой языка. Разумеется, невозможно избежать того, что внизу, на уровне машинного языка, все будет состоять из прежних команд на этом языке; но они будут неявны и не будут видны программисту, работающему на высшем уровне. Новые языки, основанные на этих идеях, были названы языки-компиляторы. Один из самых первых и элегантных получил имя «АЛГОЛ» (от английского Algorithmic Language — алгоритмический язык). В отличие от языка ассемблера, здесь нет взаимно-однозначного соответствия между высказываниями на АЛГОЛе и командами на машинном языке. Однако некий тип соответствия между АЛГОЛом и машинным языком все же существует, хотя оно гораздо более запутано, чем соответствие между языком ассемблера и машинным языком. Грубо говоря, перевод программы на АЛГОЛе в ее машинный эквивалент сравним с переводом словесного выражения алгебраической проблемы на язык формул. (На самом деле, переход от словесного выражения задачи к её выражению в формулах гораздо более сложен, но он дает определенное представление о типе «распутывания», необходимом при переводе с языка высшего уровня на язык низшего уровня ) В середине 1950-х годов были созданы удачные программы под названием компиляторы, функцией которых был перевод с языка-компилятора на машинный язык.