
- •Спекулятивная загрузка
- •Краткое содержание главы
- •Вопросы и задания
- •Глава 6
- •Виртуальная память
- •Страничная организация памяти
- •Реализация страничной организации памяти
- •Вызов страниц по требованию и рабочее множество
- •Политика замещения страниц
- •Размер страниц и фрагментация
- •Сегментация
- •Реализация сегментации
- •Виртуальная память Pentium 4
- •Виртуальная память UltraSparc III
- •Виртуальная память и кэширование
- •Виртуальные команды ввода-вывода
- •Реализация виртуальных команд ввода-вывода
- •Команды управления каталогами
- •Виртуальные команды для параллельной работы
- •Формирование процесса
- •Состояние гонок
- •Синхронизация процесса с использованием семафоров
- •Краткое содержание главы
- •Вопросы и задания
- •Уровень ассемблера
- •Знакомство с ассемблером
- •Макросы
- •Процесс ассемблирования
- •Компоновка и загрузка
- •Глава 8
- •Параллельные компьютерные архитектуры
Знакомство с ассемблером
Трансляторы можно разделить на две группы в зависимости от отношения между входным и выходным языками. Если входной язык является символическим представлением числового машинного языка, то транслятор называется ассемблером, а входной язык — языком ассемблера, или просто ассемблером. Если входной язык является языком высокого уровня (например, Java или С), а выходной язык является либо числовым машинным языком, либо символическим представлением последнего, то транслятор называется компилятором.
Понятие ассемблера
Язык ассемблера — это язык, в котором каждый оператор соответствует ровно одной машинной команде. Иными словами, в программе, написанной на ассемблере, существует взаимно однозначное соответствие между машинными командами и операторами. Если каждая строка в ассемблерной программе содержит ровно один оператор, и каждое машинное слово содержит ровно одну команду, то из ассемблерной программы размером в п строк получится программа на машинном языке из п слов.
Мы программируем на языке ассемблера, а не на машинном языке (в шестнадцатеричной системе счисления), поскольку это гораздо проще. Использовать символические имена и адреса вместо двоичных и восьмеричных намного удобнее. Многие могут запомнить, что обозначениями для сложения (add), вычитания (subtract), умножения (multiply) и деления (divide) служат команды ADD, SUB, MUL и DIV, но мало кто сможет запомнить соответствующие числовые величины, которые использует для этих команд машина. Программисту, пишущему на языке ассемблера, нужно знать только символические названия, поскольку ассемблер транслирует их в машинные команды.
Это утверждение касается и адресов. Программист, пишущий на языке ассемблера, может давать символические имена ячейкам памяти, и уже ассемблер должен позаботиться о том, чтобы получить из них правильные числовые значения. В то же время программисту, пишущему на машинном языке, всегда приходится работать с числовыми значениями адресов. Сейчас уже нет программистов, пишущих программы на машинном языке, хотя несколько десятилетий назад до изобретения ассемблеров программы именно так и писались.
Язык ассемблера имеет несколько особенностей, отличающих его от языков высокого уровня. Во-первых, это взаимно однозначное соответствие между операторами языка ассемблера и машинными командами (об этом мы уже говорили). Во-вторых, программист, пишущий на ассемблере, имеет доступ ко всем объектам и командам целевой машины. У программистов, пишущих на языках высокого уровня, такого доступа нет. Например, если целевая машина содержит бит переполнения, ассемблерная программа может проверить его, a Java-npo- грамма — нет. Ассемблерная программа может выполнить любую команду из набора команд целевой машины, а программа на языке высокого уровня — нет. Короче говоря, все, что можно сделать на машинном языке, можно сделать и на ассемблере, но в то же время программистам, пишущим программы на языках высокого уровня, недоступны многие команды, регистры и другие объекты. Языки для системного программирования (например, С) часто имеют некое промежуточное положение. Они, хотя и обладают синтаксисом, присущим языкам высокого уровня, с точки зрения возможностей доступа ближе к ассемблеру.
Наконец, ассемблерная программа может работать только на компьютерах одного семейства, а программа, написанная на языке высокого уровня, потенциально может работать на разных машинах. Возможность переноса программного обеспечения с одной машины на другую очень важна для многих прикладных программ.
Назначение ассемблера
Работать с языком ассемблера непросто. Написание одной и той же программы на ассемблере занимает гораздо больше времени, чем на языке высокого уровня. Кроме того, очень много времени занимает отладка.
Но зачем же тогда вообще писать программы на ассемблере? Есть две причины: производительность и доступ к аппаратуре. Прежде всего, квалифицированный программист, пишущий на ассемблере, может составить гораздо меньшую по объему и гораздо более быстродействующую программу, чем программа, написанная на языке высокого уровня. Для некоторых программ быстродействие и объем имеют чрезвычайно важное значение. К этой категории относятся многие встроенные прикладные программы (например, в смарт-картах, сотовых телефонах, драйверах устройств) и процедуры BIOS.
Следующая причина в том, что некоторым процедурам требуется полный доступ к аппаратному обеспечению, который обычно невозможно обеспечить средствами языков высокого уровня. В эту категорию попадают обработчики прерываний и исключений операционных систем, а также контроллеры устройств встроенных систем, работающих в режиме реального времени.
Первая причина (достижение высокой производительности) является более важной, поэтому мы рассмотрим ее подробнее. В большинстве программ лишь небольшой процент всего кода сказывается на времени выполнения программы. Обычно 1 % кода отвечает за 50 % времени выполнения, а 10 % кода — за 90 % времени выполнения.
Предположим, что для написания программы на языке высокого уровня требуется 10 человеко-лет, а полученной программе нужно 100 секунд, чтобы выполнить некоторую типичную контрольную программу. (Контрольной называют тестовую программу, которая используется для сравнения компьютеров, компиляторов и т. п.) Написание всей программы на ассемблере может занять 50 человеко-лет. Полученная в результате контрольная программа будет выполняться примерно 33 секунды, поскольку хороший программист может оказаться в три раза умнее компилятора (хотя об этом можно спорить бесконечно). Ситуацию иллюстрирует табл. 7.1.
Таблица 7.1. Сравнение программирования на ассемблере
и языке высокого уровня
Время программирования, человеко-лет |
Время выполнения программы, с |
Язык ассемблера 50 |
33 |
Язык высокого уровня 10 |
100 |
Смешанный подход до подстройки |
|
Критические 10 % 1 |
90 |
Остальные 90 % 9 |
10 |
Всего 10 |
100 |
Смешанный подход после подстройки |
|
Критические 10 % 6 |
30 |
Остальные 90 % 9 |
10 |
Всего 15 |
40 |
Так как только крошечная часть программы отвечает за большую часть времени выполнения этой программы, возможен другой подход. Сначала программа пишется на языке высокого уровня. Затем проводится ряд измерений, чтобы определить, какие части программы по большей части сказываются на времени выполнения. Для таких измерений обычно используется системный тактовый генератор. С его помощью можно узнать, сколько времени выполняется каждая процедура, сколько раз выполняется каждый цикл и т. п.
Предположим, что 10 % программы отвечает за 90 % времени ее выполнения. Это значит, что из 100 секунд работы 90 секунд выполняется десятая часть программы, а 10 секунд — оставшиеся 90 %. Эти 10 % программы можно усовершенствовать, переписав на ассемблере. Этот процесс называется подстройкой (tuning). На подстройку основных процедур потребуется еще 5 человеко-лет, но время выполнения программы сократится с 90 до 30 секунд.
Сравним этот смешанный подход, когда используются и ассемблер, и язык высокого уровня, с подходом, в котором применяется только язык ассемблера (см. табл. 7.1). При втором подходе программа работает примерно на 20 % быстрее (33 секунды против 40), но более чем за тройную цену (50 человеко-лет против 15). Более того, у смешанного подхода есть еще одно преимущество: гораздо проще переписать на ассемблер уже отлаженную процедуру, написанную на языке высокого уровня, чем писать эту процедуру на ассемблере «с нуля». Отметим, что, если бы написание программы занимало ровно 1 год, соотношение между смешанным подходом и подходом, при котором используется только язык ассемблера, составляло бы 4:1 в пользу смешанного подхода.
В то же время программисту, пишущему на языке высокого уровня, не нужно задумываться о перемещении отдельных битов, поэтому он может осмысливать задачу в целом, и иногда ему так удается построить программу, что он добивается реального повышения производительности. Такая ситуация обычно не характерна для программистов, пишущих на ассемблере, — как правило, они возятся с отдельными командами, пытаясь сэкономить несколько циклов.
Как бы то ни было, существует по крайней мере 4 веские причины для изучения ассемблера. Во-первых, желательно уметь писать программы на ассемблере, поскольку успех или неудача большого проекта иногда зависит от того, удастся или нет в несколько раз повысить быстродействие единственной, но важной процедуры.
Во-вторых, обращение к ассемблеру может быть единственно возможным выходом в случае нехватки памяти. Смарт-карты, например, содержат центральный процессор, но лишь у некоторых из них есть хотя бы мегабайт памяти и уж совсем единицы имеют жесткий диск для разбиения на страницы. Однако при таких ограниченных ресурсах они должны выполнять сложные вычисления. Процессоры, встроенные в электроприборы, часто имеют минимальный объем памяти, поскольку они должны быть достаточно дешевыми. Столь же незначительным объемом памяти обычно оснащаются различные электронные устройства, работающие на батарейках, поскольку им нужен компактный, но эффективный код.
В-третьих, компилятор должен либо на выходе производить программу, которая может использоваться ассемблером, либо самостоятельно выполнять ассемблирование. Таким образом, знание языка ассемблера существенно для понимания того, как работает компьютер. И вообще, кто-то ведь должен писать компилятор (и его ассемблер).
Наконец, ассемблер дает прекрасное представление о реальной машине. Для тех кто изучает архитектуру компьютеров, написание ассемблерного кода — единственный способ узнать, что собой представляет машина.
Формат оператора в ассемблере
Хотя структура ассемблерного оператора отражает структуру соответствующей машинной команды, языки ассемблера для разных машин и разных уровней во многом похожи, что позволяет говорить о языке ассемблера в целом. В листингах 7.1-7.3 показаны фрагменты программ на ассемблерах Pentium 4, Motorola 680x0 и (Ultra)SPARC. Все эти программы выполняют вычисление формулы N = I + J. Во всех трех примерах операторы над пустой строкой выполняют вычисление, а операторы под пустой строкой резервируют память для переменных I, J и N. То есть последние операторы не являются символьными представлениями машинных команд.
Для компьютеров семейства Intel существует несколько ассемблеров, которые отличаются друг от друга синтаксисим. В этой книге мы будем использовать язык ассемблера Microsoft MASM. И хотя мы будем говорить о процессоре
Pentium 4, все сказанное применимо и к процессорам 386, 486, Pentium и Pentium Pro. Для процессора SPARC мы будем использовать ассемблер Sun, а все сказанное применимо и к более ранним 32-разрядным версиям. В книге коды операций и регистры всегда обозначаются прописными буквами, причем не только для ассемблера Pentium 4, как это обычно принято, но и для ассемблера Sun, где по соглашению буквы строчные.
Ассемблерные операторы состоят из четырех полей: метки, операции, операндов и комментариев. Метки служат символическими именами для адресов памяти. Они позволяют переходить к командам и данным, позволяя по символическому имени получить доступ к тому месту, где хранятся команды и данные. Если оператор снабжен меткой, то эта метка обычно располагается в начале строки.
В каждом из трех примеров есть метки: FORMULA, I, J и N. Отметим, что в ассемблере SPARC после каждой метки нужно ставить двоеточие, а в ассемблере Motorola — нет. В ассемблере компьютеров Intel двоеточие ставится только после меток команд, но не после меток данных. Данное различие вовсе не является чем-то фундаментальным, просто у разработчиков разных ассемблеров разные вкусы. Архитектура машины никак не влияет на тот или иной выбор. Единственное достоинство двоеточия состоит в том, что метку можно написать на отдельной строке, а код операции — на следующей строке с тем же отступом, что и метка. Без двоеточия компилятору невозможно было бы отличить метку от кода операции при их размещении в отдельных строках.
В некоторых ассемблерах длина метки ограничена значением 6 или 8 символов. В то же время в большинстве языков высокого уровня длина имен произвольна. Длинные и хорошо подобранные имена упрощают чтение и понимание программы.
В каждой машине есть несколько регистров, но названия у них совершенно разные. Регистры Pentium 4 называются ЕАХ, ЕВХ, ЕСХ и т. д., регистры Motorola — DO, DI, D2, регистры SPARC имеют несколько названий. В этой книге для их обозначения мы будем использовать символы Ш и %R2.
В поле кода операции содержится либо символическая аббревиатура этого кода (если оператор является символическим представлением машинной команды), либо команда самого ассемблера. Выбор имени — дело вкуса, и поэтому разные разработчики называют их по-разному. Разработчики ассемблера Intel решили использовать обозначение MOV и для загрузки регистра из памяти и сохранения регистра в память, разработчики ассемблера Motorola выбрали для обеих операций обозначение MOVE, а разработчики ассемблера SPARC решили использовать символы LD для первой операции и ST для второй. Очевидно, что выбор названий в данном случае никак не связан с архитектурой машины.
Напротив, необходимость указывать две машинные команды для доступа к памяти объясняется архитектурой SPARC, поскольку виртуальные адреса могут быть 32-разрядными (как в SPARC версии 8) и 44-разрядными (как в SPARC версии 9), а команды могут содержать максимум 22 бита данных. Следовательно, чтобы передать все биты полного виртуального адреса, всегда требуются две команды. Например:
SETHI %HI(I).%R1
Эта команда обнуляет старшие 32 бита и младшие 10 бит 64-разрядного регистра R1, а затем помещает старшие 22 бита 32-разрядного адреса переменной I в битовые позиции с 10 по 31 регистра R1. Далее:
LD[%R1+%L0(I)].R1
Эта команда складывает R1 и младшие 10 бит адреса I (в результате получается полный адрес I), вызывает данное слово из памяти и помещает его в регистр R1. Указанные команды, прямо скажем, не слишком элегантны, однако разработчики SPARC не гнались за красотой. Перед ними была поставлена задача обеспечить высокую скорость исполнения, и они успешно ее решили.
Процессоры семейства Pentium, 680x0 и SPARC позволяют работать с операндами разной длины (размером с байт, слово и длинное слово). Каким образом ассемблер определяет, какова длина операндов? И опять разработчики разных ассемблеров приняли разные решения. В Pentium 4 регистры разной длины имеют разные названия. Так, для перемещения 32-разрядных значений используется название ЕАХ, для 16-разрядных — АХ, а для 8-разрядных — AL и АН. Разработчики ассемблера Motorola решили прибавлять к каждому коду операции суффикс .L для типа long, .W — для типа word и .В — для типа byte. В SPARC для операндов разной длины используются разные коды операций, например, для загрузки байта, полуслова и слова в 64-разрядный регистр указываются коды операций LDSB, LDSH и LDSW соответственно. Как видите, природа языков совершенно произвольна.
Три ассемблера, которые мы рассматриваем, различаются способом резервирования пространства для данных. Разработчики ассемблера Intel выбрали для этой операции название DD (Define Double — определить двойное слово), поскольку слово процессора 8088 имело длину 8 бит. В Motorola используется аббревиатура DC (Define Constant — определить константу). Разработчики SPARC с самого начала предпочли название .WORD. И снова различия абсолютно случайны.
В поле операндов оператора задаются адреса и регистры, которые являются операндами машинной команды. В поле операндов команды целочисленного сложения указывается, что и к чему нужно прибавить. Поле операндов команд перехода определяет, куда совершается переход. Операндами могут быть регистры, константы, ячейки памяти и т. д.
В поле комментариев поясняются действия программы. Эти пояснения могут пригодиться программистам, которым потом придется использовать и дорабатывать чужую программу, а также самому автору программы, когда он через год вернется к работе над ней. Ассемблерная программа без таких комментариев — нечто совершенно невразумительное (даже для ее автора). Комментарии могут быть полезны только людям и никак не влияют на работу программы.
Директивы
Ассемблерная программа определяет не только машинные команды, которые нужно выполнять процессору, но и команды, которые нужно выполнять самому ассемблеру (например, выделить немного памяти или выдать новую страницу листинга). Команды для ассемблера называются псевдокомандами, или ассемблерными директивами. В листинге 7.1 мы уже встречали типичную псевдокоманду DD. В табл. 7.2 перечислены некоторые другие псевдокоманды (директивы). Они взяты из ассемблера MASM семейства ассемблеров Intel.
Таблица 7.2. Некоторые директивы ассемблера MASM
Директива |
Описание |
SEGMENT |
Начало нового сегмента (текста, данных и т. п.) с определенными атрибутами |
ENDS |
Завершение текущего сегмента |
ALIGN |
Управление выравниванием следующей команды или данных |
EQU |
Определение нового символа, равного данному выражению |
DB |
Выделение памяти для одного или нескольких байтов |
DW |
Выделение памяти для одного или нескольких 16-разрядных полуслов |
DD |
Выделение памяти для одного или нескольких 32-разрядных слов |
Директива |
Описание |
DQ |
Выделение памяти для одного или нескольких 64-разрядных двойных слов |
PROC |
Начало процедуры |
ENDP |
Завершение процедуры |
MACRO |
Начало макроса |
ENDM |
Завершение макроса |
PUBLIC |
Экспорт имени, определенного в данном модуле |
EXTERN |
Импорт имени из другого модуля |
INCLUDE |
Вызов другого файла и включение его в текущий файл |
IF |
Начало условного ассемблирования программы на основе данного выражения |
ELSE |
Начало условного ассемблирования программы, если условие для директивы IF не выполнено |
ENDIF |
Завершение условного ассемблирования программы |
COMMENT |
Определение нового символа начала поля комментариев |
PAGE |
Принудительный разрыв страницы в листинге |
END |
Завершение ассемблерной программы |
Директива SEGMENT начинает новый сегмент, а директива ENDS завершает его. Разрешается начинать текстовый сегмент, затем начинать сегмент данных, затем переходить обратно к текстовому сегменту и т. д.
Директива ALIGN передает следующую строку (обычно данные) по адресу, заданному аргументом директивы. Например, если текущий сегмент уже содержит 61 байт данных, тогда после выполнения директивы ALIGN 4 следующим выделяемым адресом будет адрес 64.
Директива EQU дает символическое название некоторому выражению. Например, после следующей директивы символ BASE можно использовать в программе вместо значения 1000:
BASE EQU 1000
Выражение, которое следует за директивой EQU, может содержать несколько символов, соединенных знаками арифметических и других операций, например:
LIMIT EQU 4 * BASE + 2000
Большинство ассемблеров, в том числе MASM, требуют, чтобы символ был определен в программе до его появления в таком выражении, как это.
Директивы DB, DD, DW и DQ выделяют память для одной или нескольких переменных размером 1, 2, 4 и 8 байт соответственно. Например:
TABLE DB 11, 23, 49
Эта директива выделяет место для 3 байт и присваивает им начальные значения И, 23 и 49 соответственно, кроме того, она определяет символ TABLE, равный тому адресу, по которому хранится значение И.
Директивы PR0C и ENDP определяют начало и конец ассемблерных процедур. Процедуры в ассемблере выполняют ту же роль, что и в языках программирования высокого уровня. Директивы MACRO и ENDM определяют начало и конец макроса. О макросах мы поговорим в следующем разделе.
Директивы PUBLIC и EXTERN определяют видимость (доступность) символов. Программы часто пишут в виде совокупности файлов. Иногда процедуре, находящейся в одном файле, нужно вызвать процедуру или получить доступ к данным, определенным в другом файле. Чтобы такие перекрестные ссылки между файлами стали возможными, символ (имя), который нужно сделать доступным для других файлов, экспортируется с помощью директивы PUBLIC. Чтобы ассемблер не выдавал предупреждений по поводу использования символа, который не определен в данном файле, этот символ может быть объявлен внешним (EXTERN), то есть определенным в другом файле. Символы, которые не определены ни в одной из этих директив, могут использоваться только в пределах одного файла. Поэтому даже если, например, символ F00 встречается в нескольких файлах, это не вызовет никакого конфликта, поскольку указанный символ локален по отношению к каждому файлу.
Директива INCLUDE заставляет ассемблер вызвать другой файл и включить его в текущий. Такие включенные файлы часто содержат определения, макросы и другие элементы, необходимые для разных файлов.
Многие языки ассемблера, в том числе MASM, поддерживают условное ассемблирование программы. Например:
WORDSIZE EQU 16 IF WORDSIZE GT 16 WSIZE: DW32 ELSE
WSIZE: DW 16 ENDIF
Эта программа выделяет в памяти одно 32-разрядное слово и вызывает его адрес WSIZE. Этому слову придается одно из значений: либо 32, либо 16 в зависимости от значения WORDSIZE (в данном случае — 16). Такая конструкция может использоваться в программах для 16-разрядных (как 8088) или 32-разрядных машин (как Pentium 4). Если в начале и в конце машинно-зависимого кода вставить директивы IF и ENDIF, а затем изменить единственное определение, WORDSIZE, программу можно автоматически настроить на один из двух размеров. Применяя такой подход, можно задействовать одну такую исходную программу для нескольких разных машин. В большинстве случаев все машинно-зависимые определения, такие как WORDSIZE, сохраняются в одном файле, причем для разных машин должны быть разные файлы. Путем включения файла с нужными определениями программу можно легко перекомпилировать для разных машин.
Директива COMMENT позволяет пользователю заменить символ начала комментария (точку с запятой) чем-либо другим. Директива PAGE используется для управления листингом программы. Наконец, директивой END помечается конец программы.
В ассемблере MASM есть еще много директив. Другие ассемблеры Pentium 4 содержат другой набор директив, поскольку эти директивы определяются не архитектурой машины, а вкусами разработчиков ассемблера.