Непосредственно на машинном языке в наше время не программирует
практически никто. Первый уровень, позволяющий абстрагироваться от схемы
кодирования команд, — это уже упоминавшийся язык
ассемблера. В языке ассемблера каждой команде машинного языка соответствует
мнемоническое обозначение. Все приведенные ранее примеры написаны именно
на языке ассемблера, да и в тексте использовались не бинарные коды команд,
а их мнемоники.
Встречаются ассемблеры, которые предоставляют мнемонические обозначения
для часто используемых групп команд. Большинство таких языков позволяет
пользователю вводить свои собственные мнемонические обозначения — так
называемые макроопределения или макросы
(macros), в том числе и параметризованные (пример 2.7).
Отличие макроопределений от процедур языков высокого уровня в том, что
процедура компилируется один раз, и затем ссылки на нее реализуются в
виде команд вызова. Макроопределение же реализуется путем подстановки
тела макроопределения (с заменой параметров) на место ссылки на него и
компиляцией полученного текста. Компиляция ассемблерного текста, таким
Пример 2.7. Пример использования макроопределений
; Фрагмент драйвера LCD для микроконтроллера PIC
; (с) 1996, Дмитрий Иртегов.
; Таблица знакогенератора: 5 байт/символ.
; W содержит код символа. Пока символов может быть
; только 50, иначе возникнет переполнение.
; Scanline содержит номер байта (не строки!)
; Сначала определим макрос, а то устанем таблицу сочинять. ; Необходимо
упаковать 7 скан-строк по 5 бит в 5 байт.
CharDef macro scanl, scan2, scan3, scan4, scanS, зсапб, scan7 ; Следующий
символ
RetLW (scan? & Oxlc) » 2
RetLW ((scan5 E, 0x10) » 4) + ((зсапб S Oxlf) « 1) + ((scan7 & 0x3)
«
6)
RetLW ((scan4 & Oxle) » 1) + ((scanb & Oxf) « 4)
RetLW ((scan2 & 0x18) » 3) + ((зсапЗ & Oxlf) « 2) + ( (scan4 &
Oxl) « 7)
RetLW (scanl & Oxlf) + ( (scan2 & 0x7) « 5)
endm
FetchOneScanline IFNDEF NoDisplay
ClrF PCLATH
AddWF PCL, 1
NOP ; else
RetLW 0 endif
; А вот идет собственно таблица:
Nolist
; О
CharDef В'OHIO',В110001',В'10001',В'10001',В'10001',В'10001',В'01110'
; 1
CharDef В'00100',В'01100',В'00100',В'00100',В100100',В'00100',В'01110'
; 2
CharDef В'OHIO',В'10001',В'00001',В'00010',В100100',В'01000',В'11111'
; 3
CharDef В'01110',В'10001',В'00001',В'00110',В100001',В110001',В'OHIO'
; 4
CharDef В'00010',В'00110',В'01010',В'10010',В'11111',В'00010', В'00010'
; 5
CharDef В'11111',В'10000',В'11110',В'00001',В'00001',В110001',В1OHIO'
; б
CharDef В'OHIO1,840001' ,В' 10000',В' НПО' ,В' 10001' ,В'10001' ,В'OHIO1
; 7
CharDef В'11111',В'00001',В'00010',В'00100',В'01000',В'01000',В'01000'
; 8
CharDef В'OHIO', В'10001' ,В'10001' ,В'OHIO' , В'10001', В'10001' ,В'OHIO'
; 9
CharDef В'OHIO', В'10001', В'10001' ,В'01111' ,В'00001' ,В'10001' ,В'OHIO'
4 Зэк Х(,
Constant CharacterA = Oxa
CharDef В ' 00100 ', В ' 01010 ', В ' 10001', В110001' ,841111' , В' 10001'
,В' 10001' Ifndef NO_ALPHABET ; В Constant CharacterW = Oxb
CharDef В'11110',В'10001',B'10001',В'11110' ,В'10001' ,В'10001' ,В'11110'
else ; Р — для аона
CharDef В' 11110 ',840001', В '10001', В '10001', В' 11110 ',840000',
В '10000' endif ; С
CharDef В'01110',В'10001',В'10000',ВЧОООО',В'10000',В110001',В'01110'
; о
CharDef В' 11110 ',В' 10001 ',В' 10001 ',В' 10001 ',840001', В' 10001
',841110' ; Е
CharDef B'lllll' ,ВЧ0001', В'10000', В'11110 ' ,В'10000', В'10001' ,В'11111'
; F
CharDef В'11111',В'10001',В'10000',В'11110',В'10ЮОО',В'10000',В'10000'
; пробел Constant SPACE_CHARACTER = 0x10
CharDef В'00000',В'ООООО',В'ООООО',В'00000',В'00000',В100000',В'00000'
Constant DASH_CHARACTER = Oxll
CharDef В'00000',В'00000',В'00000',В'11111',В'00000',В'ООООО',В'ООООО'
List
Макропроцессор, кроме раскрытия макросов, обычно предоставляет
также директивы условной компиляции — в зависимости
от условий, те или иные участки кода могут передаваться компилятору или
нет. Условия, конечно же, должны быть известны уже на этапе компиляции.
Например, в зависимости от типа целевого процессора одна и та же конструкция
может реализоваться как в одну команду, так и эмулирующей программой.
В зависимости от используемой операционной системы могут применяться разные
системные вызовы (это чаще случается при программировании на языках высокого
уровня), или в зависимости от значений параметров макроопределения, макрос
может порождать совсем разный код.
Макросредства есть не только в ассемблерах, но и во многих языках высокого
уровня (ЯВУ). Наиболее известен препроцессор языка С. В действительности,
многие средства, предоставляемые языками, претендующими на большую, чем
у С, "высокоуровневость" (что бы под этим ни подразумевалось),
также реализуются по принципу макрообработки, т. е. при помощи текстовых
подстановок и компиляции результата: шаблоны (template) C++, параметризованные
типы Ada и т. д.
Умелое использование макропроцессора облегчает чтение кода и увеличивает
возможности его повторного использования в различных ситуациях. Злоупотребление
же макросредствами (как, впрочем, и многими другими мощными и выразительными
языковыми конструкциями) или просто бестолковое их применение может приводить
к совершенно непонятному коду и трудно диагностируемым ошибкам, поэтому
многие теоретики программирования выступали за полный отказ от использования
макропроцессоров.
Современные методы оптимизации в языках высокого уровня — проверка константных
условий, разворачивание циклов, inlme-функции — часто стирают различия
между макрообработкой и собственно компиляцией.
Кроме избавления программиста от необходимости запоминать коды команд,
ассемблер выполняет еще одну, пожалуй, даже более важную функцию: он позволяет
снабжать символическими именами (метками)
или (символами) команды или ячейки памяти, предназначенные для данных.
Значение этой возможности для практического программирования трудно переоценить.
Рассмотрим простой пример из жизни: мы написали программу, которая содержит
команду перехода (бывают и программы, которые ни одной команды перехода
не содержат, но это вырожденный случай). Затем, в процессе тестирования
этой программы или уточнения спецификаций мы поняли, что между командой
перехода и точкой, в которую переход совершается, необходимо вставить
еще два десятка команд. Для вставки необходимо пересчитать адрес перехода.
На практике, вставка даже одной только инструкции часто затрагивает и
приводит к необходимости пересчитывать адреса множества команд перехода,
поэтому возможность автоматизировать этот процесс крайне важна.
Важное применение меток — организация ссылок между модулями в программах,
собираемых из нескольких раздельно компилируемых файлов. Изменение объема
кода или данных в любом из модулей приводит к необходимости пересчета
адресов во всех остальных модулях. В современных программах, собираемых
из сотен отдельных файлов и содержащих тысячи индивидуально адресуемых
объектов, выполнять такой пересчет вручную невозможно. Способы автоматического
решения этой задачи обсуждаются в разд. Сборка
программ.
Фаза сопоставления символов с реальными адресами присутствует и при компиляции
языков высокого уровня — компилятор генерирует символы не только для переменных,
процедур и меток, которые могут быть использованы в операторе
goto, но и для реализации "структурных" условных операторов
и циклов. Нередко в описании компилятора эту фазу так и называют — ассемблирование.
Многие компиляторы как старые, так и современные, например, популярный
компилятор GNU С, даже не выполняют фазу ассемблирования самостоятельно,
а вместо этого генерируют текст на языке ассемблера и вызывают внешний
ассемблер. Средства межпроцессного взаимодействия современных ОС позволяют
передавать этот промежуточный текст, не создавая промежуточного файла,
поэтому для конечного пользователя эта деталь реализации часто оказывается
незаметной.
Компиляторы, имеющие встроенный ассемблер, такие, как Microsoft C/C++
или Watcom, часто могут генерировать ассемблерное представление порождаемого
кода. Это бывает полезно при отладке или написании подпрограмм на ассемблере,
которые должны взаимодействовать с откомпилированным кодом.