18 лет назад 5 октября 2006 в 12:26 1371

Кошмар программиста

Как известно, любой компьютер (даже карманный) работает под управлением операционной системы. А вот интересно, что было бы, если бы вычислительные комплексы ПВО и ПРО работали под управлением операционной системы Windows? Представляю себе: противник осуществляет массированный прорыв глубоко эшелонированной системы ПВО и ПРО, офицеры напряженно всматриваются в экраны радаров, «ведут цели», уже все готово к залпу, и вдруг — р-р-раз — сообщение: «Программа выполнила недопустимую операцию. Перезагрузите компьютер»… К счастью, военные компьютеры защищены от подобных фокусов.
Система управления боем представляет собой вагончик цвета хаки с размещенной в его чреве ЭВМ, в которой, строго говоря, нет ни оперативной памяти, ни жестких дисков, ни периферийных устройств.

Все программы, обеспечивающие управление РЛС, зенитными комплексами, эскадрильями перехватчиков и пр., жестко прошиты в ПЗУ. Для надежности в вагончике две одинаковых ЭВМ — одна находится в рабочем режиме, вторая — в ждущем и в любой момент готова «подстраховать» свою напарницу. Обслуживать эти машины довольно просто. У них нет дисплеев, а информацию о текущем состоянии можно получить по лампочкам на передней панели и по звуку. В случае необходимости оператор (живое воплощение ОС) вмешивается в ход работы, нажимая различные кнопки на панели. В общем, система проста как грабли, но очень, очень надежна. Вывести ее из строя можно только прямым попаданием реактивного снаряда, да и то не всякого, а лишь такого, который способен пробить толстый слой бетона и земляной курган, насыпанный над бетонным куполом. Эти военные ЭВМ дают довольно точное представление об ЭВМ 50-х годов прошлого века.

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

Приблизительно так управлялись самые первые ЭВМ.
Допустимо возражение: как же можно по лампочкам, пускай и точно индицирующим биты регистров процессора, определить хоть что-нибудь, ведь содержимое этих самых регистров меняется каждую секунду по несколько миллиардов раз? Ну, положим, с такой скоростью работают процессоры современных компьютеров, достижения первых ЭВМ были значительно скромнее. Однако и в них индикация лампочками была нужна не в момент работы. Все программы в былые времена всегда выполнялись без участия человека, в так называемом пакетном режиме. Индикаторы же нужны были в режиме отладки при трассировке программы, когда программист последовательно мог просмотреть, что происходит с регистрами после выполнения очередной команды, и только после этого, нажав соответствующую кнопку, вызвать выполнение следующей команды. Если ему что-то не нравилось, он сразу видел, какая команда приводит к неверному результату, и мог исправить ее.

Кстати, на современных компьютерах сохранилась возможность изучения регистров центрального процессора и выполнение программы в режиме покомандной трассировки. Если запустить на современном компьютере программу debug.exe («Пуск» > «Выполнить»), а затем ввести команду «R», то на экране появится информация о состоянии главных регистров центрального процессора. Данные, правда, представлены не в двоичном, а в шестнадцатеричном формате, но особой разницы нет. Имеется и специальная команда для трассировки. Правда, сегодня вряд ли найдется много людей, которые используют эту возможность (ну разве для оперативного взлома, если под рукой нет подходящего дисассемблера). Я думаю, что не ошибусь, если предположу, что сегодня общее количество людей, пользующихся покомандной отладкой и проверкой регистров центрального процессора, не намного превышает число программистов самых первых ЭВМ 50-х годов прошлого века. И если бы не появились языки высокого уровня, количество пользователей до сих пор оставалось бы на том уровне.

Чтобы лучше понять великую радость от появления языков программирования, введем в память компьютера программу в машинных кодах. Не пугайтесь, программу расчета траектории движения космического корабля мы вводить не будем, введем очень простую программу, состоящую всего из трех команд для сложения двух чисел: 1 и 2. Если вы еще не вошли в debug, то сделайте это и введите следующую последовательность:
E 100 B8 01 00 <Enter>
E 103 BB 02 00 <Enter>
E 106 01 D8 <Enter> G 108 <Enter>.

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

Разница лишь в том, что вы использовали мощную программу debug, входящую в состав ОС Windows, и следили за результатом на мониторе, а программист середины прошлого века ничем таким не обладал, он вынужден был контролировать ввод и конечный результат либо по лампочкам на панели ЭВМ, либо по распечаткам. И вводить ему приходилось не шестнадцатеричные, а двоичные числа. К примеру, первая команда из нашей небольшой программы в двоичном представлении выглядит вот так: 101110000000000100000000. Согласитесь, в таком виде этот код еще менее понятен.

Кстати, а что за коды вы только что ввели? E 100, E 103, E 106 и G 108 — это команды debug.exe (E — ввод в память с указанного адреса, G — выполнить команды вплоть до указанного адреса), а между ними расположены реальные коды управления процессором. Первый код — указание процессору поместить в регистр AX число 1; второй — указание поместить в регистр BX число 2. Наконец, третий код требовал сложить содержимое регистра AX с содержимым регистра BX и оставить результат в регистре AX. Рутинность такого подхода очевидна. К тому же, поскольку одно из священных правил программирования гласит, что программ без ошибок не бывает (правда, в ту эпоху оно еще не было сформулировано), в случае обнаружения ошибки несчастным программистам приходилось пристально всматриваться в лампочки или распечатку, состоящую из нулей и единиц.

Второй код из нашей программы в двоичном виде выглядит вот так: 101110110000001000000000. Бьюсь об заклад, что вы не сразу найдете, какими битами отличается первый код от второго. Конечно, если бы вы постоянно имели дело с программами в двоичных кодах, то сказали бы, что крайняя левая четверка нулей и единиц (1011) — это код операции занесения непосредственного значения в один из регистров общего назначения, следующая (1000 для первой команды и 1011 — для второй) указывает, в какой именно регистр нужно занести значение, а следующие нули и единицы — это то самое непосредственное значение, которое в указанный регистр заносится. Наверное, сейчас, после объяснений, эти двузначные числа уже не кажутся такими непонятными. Однако, чтобы жизнь программиста полувековой давности не казалась вам такой уж безоблачной, замечу, что двухбайтовое значение, заносимое в регистры, выглядит все же непривычно.

Если мы заносим в двухбайтовый регистр BX число 2, которое занимает один байт, то по нашим прикидкам в коде оно должно выглядеть вот так: 00000000 00000010 (00 02). Но нас самом деле оно выглядит так: 00000010 00000000 (02 00). Вот и попробуйте найти в этой каше нулей и единиц ошибку, если она имеется, и исправить ее. И учтите, что в первых машинах после исправления ошибки приходилось всю программу вводить полностью заново. Понятное дело, сон первых программистов был очень и очень неспокойным, им снились ночные кошмары, в которых бесконечные вереницы нулей и единиц окружали их со всех сторон, и они тщетно пытались понять, какой ноль нужно заменить единицей.

В связи с этим прискорбным обстоятельством первые программы содержали не более нескольких сотен команд. Правда, выполнялись они очень быстро и были очень компактными — результат, который ныне уже невозможно повторить, но писать и исправлять их было очень сложно. Поэтому не удивительно, что появление первого языка программирования вселило в программистов оптимизм. Однако пока речь идет не о фортране, а об ассемблере (от англ. assemble — собирать, монтировать). Чтобы ощутить радость первых программистов, введите в debug.exe (вы ведь еще не вышли из этой программы?) команду U 100 106 и нажмите Enter. Вы получите нечто вроде этого (вернее, именно это вы и увидите на экране, за исключением числа 1516 в левой колонке — у вас, скорее всего, будет другое значение):

1516:0100 B80100 MOV AX,0001
1516:0103 BB0200 MOV BX,0002
1516:0106 01D8 ADD AX,BX

Да это же только что введенные вами три кода, представленные в виде ассемблерных команд. Что приятно, тут сразу видно, что первые две строчки — это самая часто используемая команда MOV (от англ. move — движение), которая заносит в регистры и память различные значения. А третья строка — это команда сложения ADD (от англ. add — прибавить). Причем сразу видно, какие регистры используются и какие значения в них заносятся. Итак, после появления ассемблера программисты впали в эйфорию, и стало им казаться, что теперь не жизнь, а малина — живи да радуйся.

Однако так продолжалось недолго, ибо оказалось, что программы на ассемблере более понятны в тех объемах, в которых они существовали в виде машинных кодов, но при увеличении количества команд (операторов), прозрачность программ теряется. В самом деле, листинг из ста команд ассемблера был понятнее, чем листинг из ста длиннющих двоичных чисел, но листинг из тысячи команд ассемблера был столь же туманным. А усиление мощи ЭВМ требовало и новых программ, решающих все более сложные задачи. Требовалось что-то более радикальное и понятное, чем ассемблер. И это новое решение было найдено при создании ЭВМ IBM 704.

Проблема переносимости кода

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

Так, если в нашей программе кодом операции занесения числа в регистр был 1011, то для других типов процессоров (не Intel-совместимых) этот код может быть другим. Не решает проблему и перенос программы в виде ассемблерного листинга, ибо программы на ассемблере сильно зависят от архитектуры машины. Например, команда MOV используется на машинах с любой архитектурой, но вот команда MOV AX, 1 может быть использована только для архитектуры IBM PC-совместимых машин, поскольку в других машинах регистра AX может и не быть (вернее, мнемонически регистр-аккумулятор может обозначаться иначе).

Набор команд процессоров

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

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

Так, увеличивающаяся популярность сетевых и мультимедиа-приложений в свое время способствовала созданию технологии Intel под названием MultiMedia eXtentions, благодаря которой появился новый процессор Pentium MMX, имеющий MMX-команды. После этого все программы, которые были написаны для старых Intel-процессоров, могли выполняться на Pentium MMX и последующих, но программы, использующие MMX-команды, могли быть выполнены только на процессорах, поддерживающих этот набор. UP

Дмитрий Румянцев

Никто не прокомментировал материал. Есть мысли?