Средства, которые мы применяем, оказывают глубокое (и тонкое) влияние на наши способы мышления и, следовательно, на нашу способность мыслить. Эдсгер Дейкстра
Введение
Нельзя сказать, что ассемблер относится к числу незаслуженно забытых языков программирования, тем не менее, негативное мнение о его возможностях и целесообразности применения получило весьма широкое распространение. Если такой точки зрения придерживаются начинающие программисты или 'продвинутые пользователи', то это можно отнести к недостатку практических навыков и ограниченности профессиональных знаний. Но отрицательные высказывания, которые можно встретить в некоторых книгах и статьях, лично у меня вызывают недоумение. Более того, я считаю, что подобная неконструктивная критика любого языка просто вредна. Это обстоятельство и послужило поводом для написания данных заметок.
Сравнивать язык команд с алгоритмическими языками имеет смысл только в отношении сложности и трудоемкости процесса программирования. Безусловно, при таком сравнении ассемблер уступает алгоритмическим языкам, но это не означает, что его не следует знать и уметь применять в случае необходимости.
Как известно, универсального языка программирования пока не придумали и потребность в нем совсем не очевидна. Поэтому, для расширения сферы деятельности и повышения профессионального уровня, программист должен владеть несколькими языками (быть полиглотом) и желательно, чтобы одним из них был ассемблер. Ниже я попытаюсь обосновать эту точку зрения.
Немного истории
Ассемблер (assembly language, assembler) является машинно-ориентированным языком программирования. В переводе с английского слово assembly означает собрание, сборка, монтаж и т. п. Выбор такого названия объясняется тем, что первые трансляторы с Алгола, Фортрана, Кобола и PL/I использовали ассемблер в качестве промежуточного языка при сборке программы из отдельных модулей.
В свое время создание ассемблера было первым и очень важным шагом на пути автоматизации процесса программирования. Он избавлял программистов от необходимости записывать тексты программ в виде последовательности восьмеричных или шестнадцатеричных кодов. Появилась возможность использовать вместо чисел имена и специальные символы для обозначения машинных инструкций, их операндов и адресов. Кроме того, трансляторы, а в дальнейшем компиляторы, позволяли применять специальные директивы для описания переменных, оформления макроопределений и сегментирования программ. Все это существенно упрощало рутинную работу программиста.
Ассемблер недолго оставался в группе лидеров, его потеснили алгоритмические языки. Они требовали от программиста меньшей специальной подготовки, а их машинная независимость позволяла организовать широкий обмен алгоритмами и программами и их публикацию в печати. Ассемблер же постепенно становился языком профессионалов.
С появлением персональных компьютеров (ПК) маятник качнулся в обратную сторону и, на некоторое время, ассемблер вновь оказался в числе лидеров. Это произошло потому, что ограниченные возможности первых моделей ПК не позволяли применять компиляторы с языков высокого уровня. Но технический прогресс постепенно все расставил по своим местам, и программисты вновь стали отдавать предпочтение алгоритмическим языкам.
Разные языки ассемблера появляются, развиваются и отмирают вместе с семействами процессоров или микропроцессоров, для программирования которых они предназначены. Поэтому версии соответствующих компиляторов обновляются при появлении новых моделей процессоров или микропроцессоров. А отечественные и зарубежные издательства регулярно выпускают новые книги по программированию на языке ассемблера.
Специфические особенности языка
Ассемблер является языком программирования низкого уровня, составленная на нем программа представляет собой последовательность команд конкретной ЭВМ, чаще конкретного семейства ЭВМ, записанных в некой условной мнемонической форме. Именно машинная ориентация определяет достоинства и недостатки этого языка.
Неоспоримым достоинством ассемблера является возможность составления программ, рационально использующих все особенности системы команд конкретной ЭВМ. Он предоставляет неограниченные возможности для различного рода трюков (в хорошем смысле этого слова), тут все зависит от профессиональных навыков программиста и его изобретательности.
Другим положительным свойством является универсальность языка, - он позволяет составить программу для любой задачи, которая имеет решение и может быть решена на машинах данного семейства. Это утверждение основано на том очевидном факте, что любая программа, составленная на языке высокого уровня, при компиляции преобразуется в последовательность машинных команд.
Очевидным недостатком является низкий уровень абстрагирования от особенностей конкретной ЭВМ, необходимость знать и учитывать эти особенности. В то время как при работе с языками высокого уровня программист может полностью сосредоточить свое внимание на особенностях реализуемого алгоритма.
Другим недостатком является большое, а иногда и очень большое, количество команд, выполняемых конкретной машиной. Например, у микропроцессора Pentium 4 их около 500! Это вынуждает пользоваться при работе специальными справочниками, - не держать же в голове все разнообразие имен команд и особенностей их выполнения.
Ассемблер и макроассемблер
С точки зрения программиста язык ассемблера является подмножеством языка макроассемблера. Последний обязательно компилирует полный набор инструкций конкретного семейства микропроцессоров и, кроме того, исполняет большой набор операторов и директив (далее макросредств) различного назначения. У программистов, работающих на компьютерах IBM PC, наибольшей популярностью пользуются макроассемблеры MASM (компании Microsoft) и TASM (компании Borland), имеющие примерно одинаковые возможности и во многом (но не полностью) совместимые. В данном разделе мы будем говорить об особенностях MASM.
Прежде всего, следует отметить, что в чистом виде язык ассемблера применяется только при оформлении вставок в тексты программ, составляемых на языках высокого уровня. Без использования макросредств невозможно составить даже простую подпрограмму, не говоря о завершенных программах, а вот без использования команд ассемблера это возможно.
При определенных условиях исходный текст программы, выполняющей достаточно сложные действия, может содержать только макросредства. В нем не будет ни одной команды ассемблера! О какой сложности и трудоемкости программирования можно говорить в подобных случаях?
В руководствах по MASM, в том числе и в HELP, обычно описываются 8 типов операторов и 12 типов директив, начиная с версии MASM 6.10, их количество не изменялось. В рамках данной статьи не имеет смысла приводить полное описание макросредств, поэтому мы ограничимся их общей характеристикой.
Директивы макроассемблера предназначены для выполнения трех основных категорий действий:
управление распределением оперативной памяти выполняют директивы, предназначенные для описания простых переменных, структур данных, записей и сегментирования программ;
управление работой с внешними и внутренними подпрограммами, фрагментами программ и макроопределениями выполняют директивы их явного или предварительного описания и вызова;
управление процессом компиляции программы выполняют директивы условной компиляции, среди них встречаются такие знакомые каждому программисту имена, как IF, ELSE, FOR, WHILE, REPEAT, GOTO.
Независимо от конкретного назначения все директивы и операторы исполняются на стадии компиляции, при этом сами они не преобразуются в команды. В результате их исполнения в объектный модуль включаются или не включаются группы команд, находящиеся в исходном тексте программы или во внешних библиотеках. Кроме того, в объектный модуль включаются данные, необходимые компоновщику (LINK) для сборки и построения задачи.
Таким образом, при обсуждении достоинств и недостатков программирования на ассемблере не следует забывать, что на практике мы, как правило, работаем с языком макроассемблера, а это далеко не одно и тоже.
Техника программирования
Ахиллесовой пятой ассемблера является трудоемкость программирования на этом языке. Что может сделать программист для упрощения собственного труда?
Единственным возможным решением является накопление и использование собственного и чужого опыта, оформленного в виде библиотек или отдельных модулей, желательно с критической оценкой этого опыта. Макроассемблер позволяет применять при разработке задачи готовые исходные и объектные модули. Первые включаются в исходный текст программы с помощью директивы Include, а вторые подключаются к списку объектных модулей на стадии построения задачи компоновщиком. Есть разные источники объектных и исходных модулей, одни из них более доступны, а другие менее. Выбор зависит от ваших возможностей.
В состав полной версии системы программирования на макроассемблере (дистрибутивного пакета) входит набор макроопределений и подпрограмм различного назначения и примеры их использования. Внимательно просмотрите все подкаталоги вашей версии макроассемблера, наверняка в них найдутся полезные и интересные решения, которые можно применять в вашей работе.
Не забывайте о том, что при составлении программ можно использовать модули библиотек, входящих в состав систем программирования на языках высокого уровня, например, на Си. Правда, для этого надо иметь описание этих библиотек, но без него невозможно их использование и в рамках той системы программирования, для которой они созданы.
Если у вас есть доступ к сети Internet, то на разных ее сайтах можно найти множество готовых решений буквально на все случаи жизни. Только обращайте внимание на даты разработки программ и подпрограмм. Многие из них морально устарели и при использовании новых инструкций микропроцессоров могут быть существенно упрощены.
В процессе работы с ассемблером у вас постепенно будет накапливаться набор собственных решений, которые пригодятся в новых разработках. Для удобства их последующего использования старайтесь при написании программ составлять как можно больше подпрограмм, оформленных в виде отдельных модулей, лучше исходных, а не объектных - первые проще модифицировать в случае необходимости. Применение подпрограмм несколько увеличивает размеры задачи и замедляет процесс ее выполнения. Но взамен вы получаете возможность отлаживать большую программу по частям и использовать отлаженные подпрограммы в других своих разработках.
Не забывайте о существовании такого эффективного средства, как макросы - макроопределения и макровызовы. Они позволяют сокращать не только исходный текст программы, но и количество возможных ошибок при его наборе. Кроме того, макровызовы удобно применять при запросе системных функций, которые выполняют много полезных действий таких, как работа с окнами, с файловой системой, ввод и вывод данных и пр.
Таким образом, при наличии опыта и практических навыков, затраты труда при программирования на ассемблере можно свести к такому уровню, при котором окажется возможной разработка достаточно больших проектов. Безусловно, прежде чем приступать к их реализации надо взвесить все доводы за и против и учесть свои реальные возможности.
Когда ассемблеру нет альтернативы
Традиционно ассемблер применяется для написания подпрограмм и вставок в программы, составленные на алгоритмических языках. За последние несколько лет целесообразность такого его применения не только увеличилась, но и приобрела новый смысл. Это объясняется тем, что задачи, получаемые при программировании на алгоритмических языках, крайне неэффективно используют ресурсы современных микропроцессоров. Разумеется, в этом виноваты не языки, а компиляторы, которые не поддерживают работу с групповыми операциями и не учитывают особенности конвейерной обработки. Все старания инженеров и проектировщиков, направленные на повышение производительности микропроцессоров оказываются бесполезными, как и ваши финансовые затраты на приобретение нового оборудования.
Обвинять в этом разработчиков компиляторов не имеет смысла, - проблема не столь тривиальна, и для ее решения нужно не только улучшать технику компиляции, но и разрабатывать принципиально новые языковые средства. Мы привыкли к тому, что арифметические и логические операции выполняются над одной парой операндов, иногда над одним операндом (смена знака или отрицание). А групповые операции современных микропроцессоров (не только семейства Pentium) могут оперировать сразу с несколькими парами операндов, количество и разрядность которых не фиксированы.
В этой ситуации альтернативы ассемблеру просто не существует. Недаром в технической документации к компилятору 'Intel C++ Compiler' рекомендуются три варианта работы с новыми командами:
прямые вставки ассемблерного текста в тело программы, написанной на Си;
составление процедур (подпрограмм) на языке ассемблера;
использование Intrinsic (встроенные функции).
Последние являются альтернативной формой записи групповых операций и, по существу, не упрощают (скорее усложняют) задачу программиста.
Кроме введения групповых операций, для повышения производительности современных микропроцессоров применяется конвейерная обработка инструкций. В целях расширения ее возможностей непрерывно усложняется микроархитектура, что неизбежно увеличивает стоимость изделий.
Использовать преимущества конвейерной обработки не так просто, как это может показаться. При составлении программы, неважно на каком языке, мы исходим из предположения о том, что машина сначала выполняет одну команду, и только потом приступает к выполнению другой. На самом деле современные процессоры уже давно так не работают, - они пытаются полностью или частично совместить выполнение нескольких команд. Очевидно, что обычные задачи не приспособлены для этого и при их выполнении производительность процессора не соответствует его реальным возможностям.
В связи с этим у старой проблемы оптимизации появились новые аспекты: надо выявлять и изменять те места задачи, в которых производительность процессора занижена, желательно исключить из программы или свести к минимуму условные ветвления (например, по результату сравнения операндов) и т. д. Решать подобные проблемы в процессе компиляции программы, составленной на алгоритмическом языке пока невозможно, и мы снова обращаемся к ассемблеру.
Компания Intel разработала и продает специальный анализатор производительности микропроцессора в процессе выполнения конкретной задачи (VTuneTM Performance Analyzer 6.0). Он позволяет выявить в ней слабые места (критические участки кода задачи), и затем исправить их с помощью ассемблерных вставок.
Подробнее о новых возможностях микропроцессоров вы можете прочитать в моей статье 'Pentium глазами программиста'[1]. В Internet она опубликована на странице http://www.macro.aaanet.ru/apnd_4.html
Резюме
В начале статьи я уже писал о том, что в процессе работы программисту время от времени приходится осваивать новые языки. При этом он может преследовать разные цели, от простого удовлетворения любопытства и расширения кругозора до сугубо утилитарных в виде решения задач, для которых создан новый язык.
Если вы собираетесь расширять свой кругозор, то вспомните высказывание Э. Дейкстры о влиянии языка на способ мышления. Осваивая только алгоритмические языки, вы привыкаете думать при программировании формальными категориями, основанными на синтаксисе и семантике. Я не считаю что это плохо, скорее наоборот хорошо, но несколько однобоко. В конце концов, мы имеем дело не с виртуальной, а с реальной техникой и желательно представлять, как она работает. В этом случае вам будет проще понять что можно, а чего и почему нельзя сделать с помощью средств того или иного алгоритмического языка.
Чтобы оставаться на острие технического прогресса и уметь создавать задачи, рационально использующие возможности самых современных компьютеров, надо в совершенстве владеть искусством программирования ЭВМ. Овладеть этим искусством без знания языка ассемблера, на мой взгляд, невозможно.