SlideShare a Scribd company logo
1 of 77
Download to read offline
Шапорев Т.В., ассистент

Программа учебного курса
“Информатика и применение компьютеров в научных исследованиях”
семестр 2
Москва, 2006

Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C
и основы технологии модульного программирования
Примечание: по техническим причинам практически всё обучение ведётся на ПЭВМ
совместимых с IBM PC, что крайне негативно сказывается на кругозоре студентов. Желательно при всякой возможности объяснять студентам разницу между специфическими
деталями реализации конкретной архитектуры и другими общепринятыми/возможными подходами. В идеале студенты должны понимать не только как нечто реализовано, но и какой
выбор был сделан при проектировании той или иной особенности, и почему именно такой.
1. Причины изучения языка C в курсе информатики. Исторические причины создания и
популярности языка C. Достоинства и недостатки C (по сравнению с Паскалем и/или
другими языками).
2. Перечень конструкций C, соответствующих Паскалю. Возможности C, отсутствующие в
Паскале. Возможности Паскаля, отсутствующие в C (кратко, т.к. всё равно компилятор не
позволит).
3. Концепция макропроцессора. Пример макропроцессора - C-шный препроцессор.
Директивы препроцессора.
4. Общие сведения об архитектуре ЭВМ. Возможные подходы. Понятие микрокода.
5. Наш подопытный кролик - IBM PC и процессор 8086. Представление чисел. Адресация.
Основные регистры и их назначение. Понятие о взаимодействии с периферийными
устройствами и BIOS.
6. Язык ассемблера, как средство описания архитектурно-зависимых программ. Перечень
основных команд процессора 8086.
7. Команды ассемблера, не имеющие непосредственного отображения в машинные команды
и, в частности, команды организации ассемблерных процедур (подпрограмм). Соглашение
о связях.
8. Переменные и регистровая арифметика.
9. Команды сравнения и условные переходы. Организация циклов.
10. Арифметическое устройство (сопроцессор), и компиляция арифметических выражений с
плавающей точкой.
11. Система программных прерываний, как пример системных вызовов, экстракодов и т.п.
Сервис DOS и BIOS.
12. “Продвинутые” возможности: работа со строками (и что ещё?) ввод-вывод в порты.
Возможности ассемблера, которые не изучаются в курсе информатики.
13. Причины возникновения модульной разработки программ. Схема работы типичного
компилятора. Компоновщик и загрузчик. Приёмы модульной организации программ в C.
Приёмы модульной организации программ в ассемблере и C.
14. Сборка программ из нескольких файлов из командной строки. Идея дерева зависимостей,
и его реализация на примере Turbo C. Универсальный механизм сборки больших
проектов — make. Язык make-файлов.

-1-
Содержание семинарских занятий
1. Основные приёмы редактирования программы. Компиляция простейшей программы на C.
Обязательно изучить command line компилятор! (А вот компиляция из интегрированной
среды НЕ обязательна.)
2. Возможности форматированного вывода с помощью процедуры printf(). Обратная
сторона — процедура scanf(),— и почему она менее употребительна.
3. Понятие буферизованного потока ввода-вывода (FILE). Стандартные потоки ввода-вывода.
Процедуры fprintf() и fscanf(). Возможность создать свой поток с помощью функции
fopen(). Процедура fclose(). Другие стандартные функции буферизованного ввода-вывода.
4. Организация текстовых строк в C. Приёмы работы со строками (организация временных
буферов). Функции стандартной библиотеки для работы со строками. Функции sprintf() и
sscanf(), их недостатки и преимущества использования strtol(), strtod() и им подобных.
5. Сборка прогаммы на C из нескольких файлов. Приёмы написания файлов заголовков (.h).
Сборка программы посредством объектного кода. Использование объектного кода для
компоновки программы на разных языках программирования. Пример сборки программы
на C и ассемблере.
6. Генерация компилятором ассемблерного эквивалента C-шной программы. Анализ
полученного кода.
7. Пример программирования “в машинных кодах”.
8. Сравнение ассемблерного результата работы DOS-овского и 32-битового компиляторов
(для одинаковых C-шных фрагментов).

-2-
Введение: место информатики в общеинститутском курсе
и место второго семестра в курсе информатки
Хотелось бы надеяться, что необходимость изучения информатики в настоящее время
ни у кого не вызывает сомнений, однако на практике это не так, и вопрос “зачем физику программирование” не возникнет только у совсем уж беспечного студента (хотя далеко не все
рискуют задать этот вопрос). Наверное, есть резон попытаться на такой вопрос ответить, тем
более, что понимание взаимосвязей между разными частями всего институтского курса и
разными частями курса информатики должно помочь и в понимании материала этого
семестра.
Роль информатики в физическом образовании во многом подобна высшей математике:
дифференциальное и интегральное исчисление нужны не сами по себе, а потому, что громадное количество практических задач описывается дифференциальными уравнениями (в частных производных), и их надо уметь решать. Однако, если даже удалось наконец-то для некоторой задачи выписать соответствующую систему уравнений — это только начало. Если удалось найти решение в виде формул — считайте, что вам сильно повезло, потому что на практике в подавляющем большинстве случаев такие системы не сводятся ни к каким вразумительным формулам. Тем не менее ситуация не безнадёжная: если вспомнить, что формулы как
правило нужны тоже не сами по себе, а для получения численного результата, то вот как раз
этот результат может быть расчитан непосредственно, без промежуточного этапа в виде формул. Методики непосредственного получения результата в численном виде составляют содержание курса вычислительной математики, но прежде чем пытаться что-то понять в вычислительной математике, надо представлять возможности своего рабочего инструмента —
компьютера — не хуже, скажем так, чем уметь брать табличные интегралы. Если все силы
уходят не на содержание задачи, а на войну с “проклятым ящиком” — это, легко понять,
совсем не дело.
Более того, компьютером надо уметь пользоваться эффективно, мысль о том, что современный-то числогрыз переварит всё, что ему ни подсунь — это вредная иллюзия. Были,
есть, и в обозримом будущем будут задачи, которые не под силу ни одному компьютеру, например, прогноз погоды. Прогресс в области вычислительной техники как-то уж слишком
легко сводится на нет небрежностью программирования: быстродействие современной персональной ЭВМ превосходит быстродействие БЭСМ-6 на вычислительных задачах примерно
в 100 раз по порядку величины. Можно ли сказать, что за прошедшие сорок лет точность
прогнозов выросла не на два порядка, а хотя бы вдвое?
Если теперь от общих представлений вернуться к более конкреным нуждам этого
семестра, то в нём, по большому счёту, будут решаться две (с половиной) задачи.
Во-первых, дать представление о том, как вообще устроен и работает компьютер, и в
этом необходимым средством является язык ассемблера.
Необходимость знакомства с языком ассемблера можно более подробно расшифровать
следующим образом. Во-первых, были и будут задачи, для решения которых ни один язык
высокого уровня просто не приспосблен. К таким задачам часто относится работа с периферийными устройствами (если выбирать близкий к физической науке пример, то это может
быть взаимодействие с экспериментальной установкой). Во-вторых, встречаются случаи,
когда качество программы, разработанной на языке высокого уровня, неприемлемо плохое по
сравнению с тем, что могло бы быть написано на ассемблере “вручную”. На практике это как
правило выражается в том, что программа работает слишком долго.
Значение последнего случая всё время снижается, по мере того, как постоянно совершенствуются средства реализации языков высокого уровня. Качество кода, сгенерированного
современным оптимизирующим компилятором, может иной раз превосходить то, что в силах
сочинить нормальный программист. Подвох в том, что это качество из компилятора надо ещё
суметь извлечь (а для этого надо хотя бы отдалённо представлять, во что превращается программа на языке высокого уровня, и вот тут незаменим ассемблер). Бездумное использование
языка высокго уровня как правило осталяет от возможного качества пустое место. Например,
использование в C++ класса “прямоугольник” (CRect) там, где достаточно структуры
“прямоугольник” (RECT), ясно говорит, что программист даже не допускал мысли о том, во
что превратится эта с виду невинная конструкция, а это ведь не худший прмер. На практике
встречаются куски кода, прямо сказать, изумительные в своём идиотизме!
-3-
Вторая задача данного семестра - познакомиться с языком “Си”. Хотя в первом
семестре преподавание на многих потоках ведётся на Паскале, С будет совершенно
необходим в третьем семестре в курсе операционных систем — и это не говоря уж о его
практическом значении!
Наконец, материал данного семестра касается технологии модульного программирования в объёме того минимума, который необходим для работы с C и ассемблером.

Немного о терминологии
В русскоязычной литературе по программированию слово “оператор” используется в
двух довольно различных смыслах: для символа элементарного действия — вроде единственного знака “плюс”, — и для обозначения целого законченного предложения языка программирования. Это тем более обидно, что “в оригинале” такой неоднозначности нет, в английском используются два разных слова: operator и statement соответственно, так что для
утвердившегося словоупотребления не видно никаких оснований, кроме лени и небрежности.
Чтобы избежать неоднозначности в последующем изложении для обозначения элементарного
действия используется слово “операция” (женского рода). Английское statement по-видимому
лучше всего было бы переводить (предложенным Новосибирской школой) словом “предписание”, хотя в современной литературе чаще встречается слово “инструкция” — оба этих
варинта будут дальше использоваться на равных.

Общие сведения о языке C
Язык программирования “Си” был разработан в начале 70-х годов прошлого века
Брайаном Керниганом и Денисом Ричи для операционной системы “Юникс”. С тех пор оба
они — C и UNIX — приобрели немалую популярность, и оба заметно изменились по
сравнению с первоначальным вариантом.
Поскольку преподавание в первом семестре велось на Паскале, постольку C будет
излагаться “с опорой” на Паскаль — между этими языками много общего и те, кто действительно разобрался, как устроен Паскаль, легко поймут C по аналогии. Если у кого-то знания
Паскаля не настолько хороши, что ж, остаётся надеяться, что им удастся разобраться хотя бы с
одним C — хотя в данном курсе и не предусмотрены исчерпывающие объяснения для всех
конструкций C, примеров использования этих конструкций будет немало.
(Некоторое время тому назад были довольно популярны — а может быть и до сих пор остались — споры
о том, какой язык лучше, Паскаль или C. В первую очередь такие споры характеризуют кругозор спорщиков,
характеризуют однозначно и отнюдь не лестно. Желание спорить тут как правило говорит о том, что человек в
глаза не видел даже FORTRAN IV, а уж со стороны Lisp или SNOBOL Паскаль и C смотрятся как
близнецы-братья.)

Хотя между двумя языками много общего, это всё-таки разные языки, и различия в
основном обусловлены разными целями их создания. Паскаль изначально разрабатывался как
учебный язык, поэтому он более строг и академичен; C — это профессиональный жаргон и,
соответственно он более волюнтаристичен.
Мне не известно доподлинно, какие именно цели ставили перед собой создатели C,
однако не вызывает сомнений, что одними из основных целей были легкость создания компилятора и краткость. Без знания этих целей некоторые конструкции C понять невозможно.
Чтобы понять, насколько это хорошо или плохо, необходимо упомянуть о надёжности
языка. Надёжность — если и не главная, то уж точно одна из основных характеристик языка
программирования 1. Вообще-то надёжность языков программирования — весьма обширная
тема, на которую можно прочесть отдельный семестровый курс как минимум. Поскольку
времени на это нет, ограничимся пониманием надёжности как того, насколько язык программирования способствует ясному недвусмысленному изложению на нём намерений программиста и тем самым сокращает количество возможных ошибок. Все популярные языки программирования не очень-то надёжны: дополнительная надёжность (как и следовало ожидать)
даётся только ценой дополнительных усилий программиста, так что высоко надёжные языки
были разработаны, но не стали популярными, используются они (например Ada) в достаточно

1

см. например Янг С. Алгоритмические языки реального времени: конструирование и разработка. Пер.с
англ. — М.: Мир, 1985.
-4-
специальных областях и как правило в военных организациях, поскольку там можно язык
установить в приказном порядке.
В целом Паскаль, как более академичный и строгий язык, обладает большей надёжностью, чем C, тем не менее есть немало аспектов, в которых более надёжным является C.

Общие сведения о работе компилятора
Слово “компилятор” уже использовалось несколько раз, давно пора объяснить, что же
это такое.
Существует два подхода к тому, когда и как выполнять компьютерные программы.
Самым естественным выглядит способ, при котором программа выполняется сразу же,
по мере её чтения компьютером (то есть не просто компьютером, а компьютером при помощи
соответствующих программных средств). Это называется прямой интерпретацией.
Несмотря на кажущуюся естественность, более распространён другой подход: программа сначала как-то предварительно обрабатывается, а только потом уж выполняется.
Смысл этого действа в том, что языки программирования, несмотря на их вроде бы неестественность, всё-так устроены так, чтобы было удобнее человеку, а не компьютеру. Вот этот процесс перевода с исходного языка в более приемлемую для компьютерной обработки форму и
называется компиляцией.
Таким образом компилятор — это некая сущность, “какое-то чего-то”, которое считывает программу на исходном языке и переводит её в эквивалентную программу на целевом
языке. Важной функцией компилятора является вывод сообщений об ошибках в программе
(буде таковые имеются).
Исходная
программа

Целевая
→ Компилятор → программа
↓
Сообщения
об ошибках

На первый взгляд может показаться, что интерпретация и компиляция — взаимоисключающие подходы, но на самом деле отношения между ними ближе к симбиозу.
На тему о компиляторах “вообще” можно говорить ну очень долго. Например, я
сознательно использовал в определении слово “сущность”, хотя все известные мне компиляторы являются сущностями более конкретными, каждый компилятор — это программа, хотя
(теоретически) можно сделать и по-другому. Компилятором на самом-то деле является любая
известная мне типографская система, и так далее, и тому подобное. Однако ж в рамках семестрового курса нас будет интересовать довольно узкое подмножество компиляторов - те компиляторы, которые программу на каком-нибудь языке программирования переводят на “машинный язык” (что бы под этим ни понимать); и даже эту ограниченную область мы будем
изучать на примере всего-то двух компиляторов - компиляторов языков C и ассемблера для
IBM PC совместимых персоналок.
Компилятор C работает по следующей схеме.

-5-
Исходная программа
↓
Пользовательские
файлы объявлений (.h) →

Препроцессор

Системные файлы
← объявлений (.h)

↓
Программа со сделанными
подстановками
↓
Компилятор
↓
Ассемблерный код (.asm)
↓
Ассемблер
↓
Объектный код (.obj)
↓
Редактор связей

Системные
← библиотеки (.lib)

↓
Выполняемый файл (.exe)
Любая из клеточек на схеме может сама по себе рассматриваться как отдельный
компилятор.
По такой схеме работал самый первый компилятор языка C (кстати говоря, для
операционной системы UNIX, а вовсе не для DOS). Современные компиляторы как правило
устроены несколько иначе, как правило все эти многочисленные клеточки объединяются
всего в два этапа, тем не менее схемой можно пользоваться довольно смело, поскольку
стандарт языка C требует, чтобы независимо от способа реализации результат работы
компилятора выглядел так, как если бы он этой схеме соответствовал.
Прежде, чем рассматривать особенности конкретных компиляторов, желательно
оговорить ещё несколько понятий.
Под “машинным языком” в разных случаях понимают довольно разные вещи, и, к
сожалению, с распространённой небрежной трактовкой этого термина ничего уже поделать
нельзя — всё это разнообразие необходимо помнить, чтобы понимать уже существующую
литературу. Например, достаточно часто “машинным языком” называют язык ассемблера.
Какой смысл в таком случае может иметь словосочетание “компилятор языка ассемблера в
машинный язык”?
Так вот, когда готовая к выполнению программа находится в памяти ЭВМ (в виде
битиков), то, строго говоря, только в этом случае она и представлена на “машинном языке”.
Такому представлению соответствувет в DOS формат .COM-файлов, но возможности
его настолько ограничены, что вы с ним практически не будете иметь дела.
Когда в этом курсе идёт речь о компиляторе в “машинный язык”, то под “машинным
языком” понимается способ представления информации в выполняемых (.EXE) и объектных
(.OBJ) файлах. По сравнению с “машинным языком” в строгом смысле этого слова, и выполняемые, и объектные файлы содержат некий минимум дополнительной информации, причём
этот “минимум” разный для разных случаев (чем, собственно, и обусловлена необходимость в
двух разных форматах; вообще говоря для этих целей можно использовать один формат, так и
сделано в части современных операционных систем, но мы-то будем тренироваться на
старенькой DOS).
Чтобы объяснить, в чём эта разница, придётся упомянуть ещё пару новых понятий:
редактор связей (по-английски linker) и загрузчик (соответственно loader). Я сознательно гово-6-
рю “упомянуть”, а не “определить” потому, что для точного определения функций этих сущностей знаний пока недостаточно. Тем не менее понимать, о чём идёт речь, уже необходимо.
Так вот, если в программе встречается вызов функции, то в её представлении на “машинном языке” код, отвечающий за этот вызов, должен быть как-то связан с тем кусочком
кода, где эта функция была определена. Эту работу выполняет редактор связей (linker).
Далее, компиляторы как правило генерируют целевую программу так, чтобы она могла
работать в произвольном месте памяти ЭВМ — уж куда её ни определят ответственные за то
программы, чтобы там и работала. Такая вот гибкость даётся не за просто так, обычно это
означает, что такая “отвязанная” программа ни в каком конкретном месте работать не может.
Привязку программы к конкретному месту в памяти (или, забегая вперёд, “размещение по
конкретным адресам”) выполняет загрузчик (loader).
Если теперь вспомнить, с чего начинался весь этот пассаж, то основная разница между
объектными (.OBJ) и выполняемыми (.EXE) файлами заключается в том, что объектные файлы в обязательном порядке содержат информацию для редактора связей, так называемую
таблицу имён, name table. Выполняемые файлы, хотя и могут содержать такую информацию,
предназначены вообще-то для другого. Для выполняемых файлов обязательной является информация загрузчика — таблица перемещений или relocation table (а роль собственно загрузчика в DOS выполняет сама операционная система).
Стоит ещё раз подчеркнуть, что эта схема, хоть и очень характерная, всё-таки специфична для DOS. Эта схема не так уж плоха в качестве примера, но ведь бывает и по-другому. С
одной стороны, как уже было сказано, в ряде современных систем отсутствует деление на
объектные и выполняемые файлы. (Следует отметить, что в таких системах словом “загрузчик” - loader - может быть названа программа, фактически выполняющая функции редактора
связей.) С другой стороны можно встретить системы, в которых загрузчик не упрятан внутрь
операционной системы, а существует как самостоятельная программа.
Наконец, можно ведь реализовать компилятор и так, чтобы результатом его работы
была программа на “машинном языке” в строгом смысле этого слова, уже размещённая в
памяти и готовая к выполенению. При таком подходе можно было бы избежать сложностей,
связанных с объектными и выполняемыми файлами. Тем не менее, в современном компьютерном мире утвердилась более сложная схема, так как она предоставляет больше возможностей
— что это за возможности, вы будете узнавать в течение семестра.
Таким образом вы получили достаточное (на данном этапе) представление о
финальных клеточках в схеме работы компилятора.
Если теперь обратить-таки внимание на начало этой схемы, то обнаруживается первое
существенное отличие языка C от Паскаля (и многих других языков): неотъемлемой частью
языка C является препроцессор.

Препроцессор
На вышеприведённой схеме клеточкой под названием “компилятор” (в отличие от
компилятора в общем смысле этого слова) названа конструкция, которая из программы на
стандартном, якобы машинно-независимом языке, порождает нечто, предназначенное для
вполне конкрентной ЭВМ.
Идея препроцессора заключается в том, чтобы как-то преобразовать исходную программу перед тем, как передать её “собственно компилятору”. В принципе, с формальной
точки зрения, можно всё те же возможности реализовать на одном уровне с основными конструкциями языка, так что препроцессор не является совсем уж необходимым, и в большинстве
языков препроцессор в стандарте языка отсутствует. Тем не менее некоторое время тому назад
препроцессоры были модной темой, так как позволяли сравнительно легко приспособить язык
“под себя”, то есть язык универсальный, а значит под решение большинства задач подходящий лишь постольку-поскольку, приспособить для решения конкретной задачи, добавив в
него с помощью препроцессора конструкции, ориентированные на какую-то конкретную
предметную область. Сейчас модно делать такие вот предметно-ориентированные расширения с помощью полиморфизма в объектно ориентированных языках, но совсем простой препроцессор так и остался в языке С как отголосок былой моды.(Язык ассемблера для PC
включает свой собственный макропроцессор, заметно более мощный, чем C-шный. Хотя
макроассемблер и не нужен для понимания взаимосвязи языков высокого уровня с
-7-
архитектурой ЭВМ, для изучения самой концепции полезно сравнить возможности
макропроцессоров C и ассемблера.)
Внешне директивы препроцессора выглядят как команды какого-то своего собственного языка внутри “нормальной” C-шной программы: все директивы препроцессора начинаются с символа # (диез) в первой позиции строки. (Большинство современных реализаций языка позволяют начинать директивы не только в первой позиции, но лучше наверное так не
делать, вместо этого можно вставлять пробелы после символа #.)
C-шный препроцессор выполняет три функции:
1) слияние файлов;
2) условную компиляцию;
3) макроподстановки.
Во-первых, если исходная программа содержит директиву #include с последующим
именем файла, эта единственная строчка заменяется на весь текст указанного файла. Имя
файла в директиве #include обязательно заключается в кавычки: либо обычные двойные
кавычки, либо угловые скобки - символы “меньше” и “больше”, например так:
#include <stdio.h>
Точная разница между этими двумя способами может разниться от компилятора к
компилятору, но по смыслу двойные кавычки предназначены для нормальных файлов пользователя, а угловые кавычки — для так называемых системных хидеров, то есть файлов, разработанных вместе с компилятором как составная часть реализации языка. Сами включаемые
файлы называются “файлами заголовков” (буквальный перевод исходного header files) и по
традиции имеют расширение .h, хотя это и не обязательно, препроцессору, в отличие от
компилятора, всё равно, какое у файла расширение.
Хотя по сравнению с другими макрогенераторами C-шный препроцессор — это предел
примитивизма, макроподстановки в C — наиболее замысловатая возможность препроцессора
изо всех трёх.
В принципе идея макроподстановок заключается в том, что препроцессор в тексте
программы находит известные ему идентификаторы и заменяет их на некий другой текст.
В простейшем варианте макрос используется для того, чтобы дать константе
мнемоническое имя, например:
#define PI 3.14159265358912
Теперь везде в последующем тексте программы вместо идентификатора PI
автоматически будет подставлено его значение.
Важно понимать, что макропроцессор никак не анализирует, что именно он
подставляет, и текст макроподстановки совсем не обязан быть правильной конструкцией
языка, главное, чтобы правильная конструкция получиась после подстановки.
Макроопределение с параметрами — более сложная конструкция, это аналог функции.
Аналогично функции, при выполнении макроподстановки формальные параметры заменяются фактическими. Например, следующее определение задает вычисление куба числа:
#define cube(x) ((x)*(x)*(x))
Обратите внимание, на множество скобочек и ещё раз вспомните , что препроцессор
меняет текст программы, и не более того. Если бы макрос был определён в виде
#define cube(x) x*x*x
То конструкция cube(x+1) развернулась бы в x+1*x+1*x+1 (то есть 3*x+1), а
совсем не то, что требовалось.
Эмпирическое правило на это счёт заключается в том, что язык C никак не наказывает
за использование “лишних” скобок, поэтому если вы сомневаетесь в результате, то берите в
скобки всё, что можно (хотя и злоупотреблять этим не стоит, так как программа становится
менее читабельной).
О макросах необходимо сделать ещё как минимум два замечания.
Во-первых, понятие области видимости для макросов весьма условно: макрос
действует с того места, где его определили и до конца файла; совершенно неважно, было
определение внутри или снаружи функции, блока и т.п. Если такая глобальная видимость
доставляет неудобства, то каждый отдельный макрос можно отменить директивой #undef.
Во-вторых, существуют так называемые предопределённые макросы, которые компилятор предоставляет что называется “забесплатно”. Предопределённые макросы бывают
-8-
стандартные, то есть обязанные присутствовать в любых компиляторах C, и макросы, специфические для отдельных конкретных компиляторов. Полезными примерами стандартных
макросов являются __FILE__ и __LINE__: первый из них заменяется на имя файла с
текстом программы, а второй — на порядковый номер строки в этом файле. Примерами специфических макросов могут быть __TURBOC__, который содержит закодированный номер
версии компилятора Turbo C (и отсутствует в других), _MSC_VER, который содержит номер
версии Microsoft С и т.п.
Директивы условной компиляции являются препроцессорным аналогом условной
инструкции if, только в силу специфики работы препроцессора, кусок кода внутри директив
условной компиляции, если ему “не повезло”, вообще исключается из целевой программы: он
не попадает на вход “собственно компилятору”, на его хранение и выполнение (на последующих этапах) не тратятся никакие ресурсы и т.п.
В самом первом стандарте Кернигана и Ричи для условной компиляции было всего
пять директив препроцессора: каждый такой “условно компилируемый” кусок начинался
одной из директив #if, #ifdef или #ifndef, обязательно заканчивался #endif, а между ними при
необходимости могло присутствовать #else.
Разница между первыми тремя инструкциями заключается в следующем: условие директивы #ifdef (сокращение от if defined) считается выполненным, если за ней следует имя уже
определённого макроса. Директива #ifndef (if not defined) использует противоположное условие. Наконец, директива #if — наиболее сложная изо всех трёх, после неё должно следовать
константное арифметическое выражение. Условие #if считается выполненным, если значение
этого выражение отлично от нуля. Константное выражение — это выражение, состоящее из
констант (например, целых чисел) и элементарных операций языка; смысл этого требования в
том, чтобы значение выражения могло быть вычислено до начала выполнения программы (и
более того, до того, как программа будет хотя бы приготовлена к выполнению). Хотя
переменные в константное выражение включать нельзя, макросы использовать можно!
Современный стандарт языка включает и другие возможности условной компиляции,
наиболее полезной из них наверное является предикат defined: конструкция defined(ИМЯ)
проверяет, определён ли макрос с заданным именем, но, в отличие от #ifdef предикат можно
включать в константное выражение, так что, например, одной директивой #if можно проверить несколько макросов.
В качестве примера и практической рекомендации стоит упомянуть, что конструкция
#if 0
. . .
#endif
нередко используется для того, чтобы безусловно выключить, что называется
“закомментировать” кусок текста программы.

Особенности языка
Если попытаться сравнить между собой программы на Паскале и C, то на первый
взгляд может показаться, что между этими двумя языками сплошные различия: вместо begin end фигурные скобки { }, вместо операции присваивания единственный знак равенства и так
далее. Более-менее полный перечень приведён в приложении для изучения самостоятельно
или на семинарах. Тем не менее легко понять, что, несмотря на разницу в обозначениях,
обозначают-то они примерно одно и то же. Более серьёзных различий не так уж и много.
Во-первых, в C, в отличие от Паскаля, различаются большие и малые буквы. Разница,
вроде бы, совсем не принципиальная, но на практике вызывает на удивление много ошибок.
Во-вторых, структура программы упрощена по сравнению с Паскалем: вложенные
функции запрещены, а понятие “раздела описаний” размыто: внутри функций все описания
должны содержаться в начале блока до первого выполняемого оператора, но порядок описаний не регламентируется, а снаружи функций, то есть при описании глобальных объектов, нет
даже этих ограничений. Основное правило — объект языка должен быть описан раньше его
первого использования, и в общем-то это всё.
(Может быть здесь следует подчеркнуть разницу между тем, как программа читается
компилятором и как она выполняется: если порядок обращения к разным функциям на этапе
выполнения программы определяется логикой действий этой программы, и в общем случае
-9-
этот порядок непредсказуем, то порядок чтения программы компилятором всегда один и тот
же: “слева направо и сверху вниз”, если смотреть в текстовом редакторе.)
Если однако же заглянуть внутрь описаний, то синтаксис описания типов заметно
другой. Если сравнивать с Паскалем, в котором в инструкции описания переменных слева
присутствует полная спецификация типа, одинаковая для всех последующих переменных, то в
C сначала указывается только базовый тип, который затем может быть изменён модификатором рядом с именем переменной. Такая двойственная система придумана для сокращения
записи, например, инструкция «int a, *b;» “зараз” описывает переменные двух разных
типов: целочисленного и указателя на целое. За всё приходится платить, и такая краткая
запись менее наглядна, чем в Паскале, особенно невразумительно подобный способ записи
выглядит в инструкциях описания типов (typedef).
Наконец, язык C в явном виде использует разбиение программы на файлы.
Если вспомнить Виртовский стандарт Паскаля, то какое бы то ни было деление программы на части там просто проигнорировано: программа считается единым монолитным
объектом, вся целиком подаётся на обработку компилятору и т.п. Такой подход мало приемлем, так сказать, “в реальной жизни”.
Во-первых, реальные программные проекты как правило разрабатываются не одним
человеком, а командой. Если такая команда попытается одновременно менять текст программы — каждый человек на свой лад, — то результатом будет полная неразбериха.
Во-вторых, объём “настоящих” программных проектов измеряется как правило
сотнями тысяч и миллионами строк. Попытайтесь представить, насколько неудобно и
медленно редактировать и компилировать такой объём информации за раз. Даже поиск
нужной строки в таком количестве информации превращается в проблему.
Словом, давным-давно принято хоть сколько-нибудь большую программу делить на
разные файлы и, например, Turbo Pascal предлагает для этого не менее двух разных способов.
Увы, оба они нестандартные, и нет никаких гарантий, что эти способы заработают в любой
другой реализации Паскаля.
Разработчики C заранее предусмотрели возможность разбиения программы на части.
(Хотя, справедливости ради, нужно отметить, что возможности эти до предела примитивны,
интересующиеся могут для сранения познакомиться с Виртовской же Модулой или модным пока ещё - языком Java.). Программа на C компонуется из файлов, будь то файлы с исходным
кодом или скомпилированные объектные файлы. Каждый файл представляет собой самостоятельный модуль и, например, объекты, объявленные в файле как глобальные, можно сделать
невидимыми из других файлов с помощью ключевого слова static.

Начальные сведения об устройстве компьютера.
Слово “компьютер” вообще говоря обозначает весьма широкий класс устройств. Ради
примера можно упомянуть так называемые аналоговые компьютеры в которых, образно говоря, вместо вычисления значения функции “синус” используется генератор синусоидального
сигнала. Такого рода компьютеры развиваются и постепенно приобретают всё большую популярность, вот только относительная их доля стремительно снижается на фоне просто чудовищного прогресса компьютеров, основанных на цифровом принципе.
Информатику принято считать достижением двадцатого века, причём второй его половины. Это не совсем так. Всякая уважающая себя наука пытается искать своё начало в трудах Аристотеля. Забираться так далеко вглубь времён конечно можно, но если говорить
всерьёз, то деление современных компьютеров на функциональные блоки вполне соответствует идеям английского ученого XIX века Чарлза Бэбиджа.
Практически любой современный компьютер включает следующие фукциональные
узлы:
⇒ устройство, обеспечивающее организацию выполнения программы и согласованное
взаимодействие узлов машины в ходе этого процесса — устройство управления, УУ;
⇒ устройство, обеспечивающее собственно обработку информации —
арифметико-логическое устройство, АЛУ;
⇒ устройство для хранения исходных данных, промежуточных величин и результатов
расчётов, а также самой программы обработки информации — запоминающее устройство
или просто память;
- 10 -
⇒ устройства, преобразующие информацию в форму, доступную компьютеру — устройства
ввода;
⇒ устройства, преобразующие результаты обработки в форму, предназначенную для
человека (или устройства какого-то типа, отличного от данного компьютера) —
устройства вывода.

память
→
(ОЗУ, ПЗУ) ←

устройства
ввода
↑↓
процессор
(УУ, АЛУ)
↑↓
устройства
вывода

→
←

внешняя
память

Следует подчернкнуть, что приведённое деление — именно фукциональное, в противовес общепринятому ныне конструктивному. Если, грубо говоря, необходимо назвать набор
ингредиентов, которые надо купить для сборки компьютера, то совершенно естественно не
делать различий между УУ и АЛУ, так как оба они (вместе со многими другими интересными
вещами) находятся внутри центрального процессора, “памятью” при этом оказываются микросхемы ОЗУ, но не регистры процессора и не жесткие диски, внешняя память вместе с устройствами ввода-вывода чохом попадает в категорию “периферийные устройства” и т.д. В
общем-то такой подход действительно лучше подходит для многих практических задач, но
только не для данного курса.
Систематическое изложение принципов постороения ЭВМ было сделано ближе к нашим дням - в середине XX века - группой авторов, включавшей Джона фон Неймана. Из-за высокого авторитета Неймана теперь архитектура ЭВМ в целом носит название “фон-неймановской”. (Такое положение дел преувеличивает личные заслуги Неймана, но бороться за
историческую справедливость теперь вряд ли уместно.)
Фон-неймановская архитектура опирается на следующие принципы:
⇒ Двоичное представление чисел и других данных (для сравнения можно напомнить, что
первые компьютеры имели десятичное представление данных, а в 80х годах всерьёз обсуждались преимущества троичного представления данных — если бы существовала достаточно эффективная инженерная реализация устройства с тремя состояниями, то пользовались бы мы троичными компьютерами).
⇒ Программа хранится в той же памяти, что и обрабатываемые данные (в виде набора нулей
и единиц), то есть принцип “хранимой программы” (для сравнения, первые ЭВМ
программировались ручной перекоммутацией электрических цепей).
⇒ Память (для команд и данных) поделена на ячейки, доступ к ячейке осуществляется по её
порядковому номеру или адресу — так называемый принцип адресности.
⇒ На каждом шаге из памяти выбирается и выполняется команда, адрес которой хранится в
специальном устройстве. Наличие такого устройства — программного счетчика — ещё
один принцип фон-неймановской архитектуры.
Изложенные принципы выбраны так, чтобы при минимальном объёме максимамльно
облегчить понимание работы компьютеров. Следует быть готовым к тому, что реальный
компьютер будет похож на изложенную идеализированную схему лишь постольку-поскольку.
Например, практически во всех современных компьютерах нет одного арифметического
устройства, вместо этого используются несколько разных специализированных устройств,
вплоть до того, что в старых моделях IBM PC целочисленная арифметика выполнялась центральным процессором, а устройство “действительной” арифметики даже конструктивно (а не
только функционально) было отдельным блоком, отдельной микросхемой. Существуют примеры и более фундаментальных различий (но менее наглядные).

- 11 -
Иерархия памяти.
То, что оперативная память является отдельным устройством, влечёт много важных
последствий, сейчас остановимся на таком из них: устройства в составе центрального процессора не работают непосредственно с оперативной памятью. Например АЛУ для выполнения какой-либо операции сначала считывает операнды внутрь себя, а результат операции тоже
получается внутри АЛУ и в оперативную память его ещё (может быть) придётся переписать.
То есть внутри процессора тоже существуют некие специализированные устройства хранения
информации — они называются регистрами.
Регистры бывают двух типов: регистры устройств используются этими самыми устройствами по мере надобности, но система команд машины совершенно необязатльно даёт
программисту возможность обращаться к таким регистрам непосредственно. Кроме того существуют так называемые регистры процессора, которые видны в системе команд (и, соответственно, языке ассемблера). Регистры процессора как правило играют роль сверхоперативной
памяти.
Таким образом, устройства памяти образуют иерархию, упорядоченную по мере
убывания быстродействия и увеличения объёма: регистры - оперативная память - внешняя
память. Важной частью современных компьютеров являются также устройства постоянной
памяти и кэш-память, но из-за нехватки времени мы их сейчас рассматривать не будем.
Справедливости ради следует упомянуть, что регистры далеко не всегда рассматриваются как разновидность памяти. По-видимому это связано с тем, с какой именно ЭВМ автор
учебника познакомился первой; действительно, если взять IBM PC, в которой регистров немного или, пуще того, БЭСМ-6, в которой регистров данных было этак примерно полтора, то,
казалось бы, при чём тут память? Если же взять какую-нибудь из современных ЭВМ, в
которой регистры считаются десятками, то картина выглядит несколько иначе. Я не вижу
смысла спорить, какой из подходов более правильный, лучше вместо этого заранее договориться о терминах.

Микрокод.
Если проанализировать материал предыдущего раздела, то можно заметить ещё одну
существенную особенность: устройство машины, каким оно предстаёт программисту в виде
системы команд, может существенно отличаться от реального положения дел. Действительно,
многие с виду элементарные команды на самом деле реализованы как небольшие программы,
хранящиеся непосредственно в центральном процессоре, а не в оперативной памяти (помните,
как первые ЭВМ программировались физической перекоммутацией? — идея та же). Набор
таких программ называется микрокодом.
Это понятие можно проиллюстрировать следующим примером: команда сдвига в языках программирования высокого уровня (как правило) отображается в команду сдвига соответствующей ЭВМ. В процессоре Intel 8088 не было устройства сдвига на заданное значение:
там было устройство сдвига на единичку, а сдвиг на другие значения был реализован в виде
микропрограммного цикла, сдвигавшего операнд по единичке за раз до нужного результата.

Цикл работы центрального процессора.
При выполнении очередной команды практически любой современный процессор (а
точнее — его устройство управления) проделывает примерно одинаковый набор действий:
1. считывание очередной команды из памяти по счётчику адреса;
2. дешифрация команды (например, не нужны ли команде данные из памяти);
3. формирование нового значения счётчика адреса (адреса следующей команды);
4. выборка операндов из памяти (если нужно);
5. выполнение команды (например в арифметическом устройстве);
6. запись результата в память (если требуется).
Подавляющее большинство современных процессоров укладывается в эту простенькую схему, всё разнообразие их поведения обеспечивается пятым пунктом. Полезно обратить
внимание на следующие особенности.
Во-первых невразумительное положение пункта “вычисление адреса следующей команды” в середине списка. Связано это с тем, что длина команды в общем случае (и в IBM PC
- 12 -
в частности) может быть разной, и вычисляется она как раз в процессе дешифрации. Существует много процессоров, в которых все команды намеренно сделаны одинаковой длины, в
этом случае вычисление адреса следующей команды может быть выполнено сразу же, то есть
вторым пунктом. Ключевым моментом здесь является то, что данный шаг не может выполняться в конце цикла, так как есть команды, назначение которых именно подменять автоматически вычисленный адрес следующей команды на нечто другое.
Во-вторых, цикл обработки команд — бесконечный, его завершение не предусмотрено.
Хотя современные процессоры могут включать команду “конец работы” она же “останов
процессора” — это скорее дань традиции, чем необходимость; современный компьютер, пока
включен, всегда чем-нибудь занят: при завершении прикладной программы он всё свое время
уделяет выполнению операцинной системы, а операционная система, если нет других дел, в
бесконечном цикле ждёт команд пользователя.
В третьих, в данной схеме напрочь проигнорирован такой принципиально важный
момент, как обработка преываний. Что это такое — вы узнаете в третьем семестре в курсе
операционных систем.
Наконец, эта схема позволяет проиллюстрировать ещё одно важное понятие. Устройство управления имеет достаточно сложную внутреннюю структуру, и как правило каждый шаг
схемы выполняется отдельной подсистемой внутри УУ, то есть в какой-то мере самостоятельным устройством. Так что для увеличения скорости работы процессора можно выполнение последовательных команд частично совместить по времени: пока очередная команда
выполняется арифметическим устройством, для следующей за ней выбираются из памяти
операнды, в это же время третья команда дешифруется и т.д. Этот приём, общепринятый для
повышения производительности современных процессоров, называется конвейеризацией.

Тактовая частота.
Рассмотренная схема демонстрирует ещё одну важную идею: выполнение любой команды подразделяется на какое-то количество элементарных шагов — тактов (содержание
каждого шага может очень сильно различаться в зависимости от устройства процессора и самой команды). Для того, чтобы разные устройства работали согласованно, сигнал к началу
каждого такта подается специальным устройством, единым на весь компьютер — генератором
тактовых импульсов (этакий большой полковой барабан, под который все “делают левой”).
Чем больше тактовая частота, тем, при прочих равных, быстрее работает компьютер.
Оговорка “при прочих равных” весьма существенна.
Во-первых, тактовую частоту нельзя увеличивать произвольно — с какого-то момента
устройства компьютера просто перестанут успевать выполнять команды.
Во-вторых, нельзя забывать о разнице в устройстве процессоров. Даже в случае одного
семейства Intel 86 количество тактов на выполнение одной и той же команды может
различаться в разы для разных моделей процессора. В случае процессоров разных типов
сравнение тактовых частот может просто не иметь смысла.
Наконец, производительность компьютера в целом зависит не только от центрального
процессора, а частенько — даже и не в первую очередь от него.

Типизация системы команд.
Общим местом в современных учебниках стало то, что система команд процессора
может относиться к одному из двувх типов: CISC или RISC. Строго говоря, в такой вот
категоричной форме этот тезис просто неверен; наверное любой реально существующий
процессор включает элементы и того, и другого. Тем не менее эти аббревиатуры обозначают
реально существующие противоборствующие тенденции в архитектуре компьютеров.
Обозначение CISC расшифровывается как Complex Instruction Set Computer и говорит о
том, что в процессоре предусмотрено как можно больше разнообразных команд на все случаи
жизни.
Буквы RISC означают, что процессор умеет выполнять только самый минимум команд,
без которых совсем нельзя обойтись, зато уж эти-то команды реализованы максимально
эффективно.
Смысл этого противостояния в том, что сложность процессора всегда чем-то ограничена, в первую очередь технологией производства (и не стоит забывать о цене изделия!), так
- 13 -
что, грубо говоря, одно и то же количество вентилей в кристалле можно употребить либо на
разнообразный набор операций, либо на лучшую коммутацию между элементами, либо на
что-нибудь ещё, но на всё сразу и по-максимуму никак не получится.
Наиболее характерной чертой CISC-процессоров (кроме набора команд) является
наличие выделенных регистров: какие-то операции могут выполняться только с одним или
несколькими регистрами, но не с какими-то другими. (Например, в любимом 86 семействе,
команды умножения и деления работают только с сумматором, но не со счётчиком или
несколькими другими регистрами, зато команда сдвига может иметь вторым аргументом
только регистр счётчика и так далее).
В противоположность этому, в RISC-процессорах регистры как правило равноправны.
Исторически, поскольку разработчики первых процессоров были ограничены в средствах и вынуждены были программировать только необходимый минимум, постольку, как
только технология это позволила, процессоры стали развиваться в направлении CISC. На сегодняшний день преобладает по-видимому тенденция RISC.
Если вернуться к нашему любимому примеру, то процессор Pentium имеет внутри себя
RISC ядро, а унаследованная от предков система команд типа CISC выполняется с помощью
микрокода.

Структура команды и её “адресность”.
Представление команды процессора в памяти компьютера (в двоичном виде) обычно
подразделяется на код операции (что именно надо сделать) и адресную часть (то есть откуда
брать данные и куда поместить результат). В зависимости от кода операции адресная часть
может отсутствовать. Если она всё же присутствует, то может быть организована по-разному.
Допустим, для примера, что нам надо закодировать машинный аналог элементарного
оператора:
A = B + C
В такой операции участвуют адреса трёх разных переменных и представляется
естественным, что все три адреса должны присутствовать в команде. Системы команд
организованные в соответствии с данным принципом, принято называть трёхадресными.
Теперь немного арифметики: общепринятая длина адреса в современных компьютерах
32 бита, то есть трёхадресная команда заняла бы более 96 бит — чёртову дюжину байт. Такие
длинные команды практически нигде не встречаются. Каким образом?
На практике более употребительными являются системы команд двухадресного типа в
которых результат кладётся на место одного из аргументов. При таком подходе приведённый
пример пришлось бы транслировать в две машинные команды следующего типа:
A = B; A = A + C
или, используя более уместную в таком случае C-шную нотацию:
A = B; A += C
В результате у нас имеются две команды с четырьмя адресными частями: то есть
количество битиков в машинном представлении возросло на один адрес и один код операции.
В чём смысл подобной “экономии”?
Подвох в том, что “в реальной жизни” обычно используются выражения более
сложные, чем B+C. Добавим в пример единственное слагаемое:
A = B + C + D
В случае трёхадресной системы команд это выражение всё равно пришлось бы
развернуть в несколько машинных команд, примерно так:
A = B + C; A = A + D
Для двухадресной системы команд получилась бы последовательность
A = B; A += C; A += D
Количество адресных частей сравнялось. Если ещё хоть чем-нибудь усложнить
пример, то двухадресная система команд даст более короткий машинный код.
Можно развить эту идею дальше: если, например, принять соглашение, что все арифметические операции помещают результат в специальный регистр процессора, откуда его
можно потом переписать в память отдельной командой, то можно сформировать одноадресную систему команд.
- 14 -
Обозначим греческой ∑ регистр сумматора (не требующий адресной части), тогда
приведённые примеры развернутся соответственно в
∑ = B; ∑ += C; A = ∑ и
∑ = B; ∑ += C; ∑ += D; A = ∑
То есть ни одного лишнего адреса, зато гораздо больше кодов операций. Поскольку
как правило код операции короче адреса, такой подход даёт выигрыш в объёме кода и
как правило проигрыш в быстродействии.
Реальное положение дел, как обычно, не вполне соответствует теоретическим построениям. В системе команд семейства Intel 86 большинство команд устроено по двухадресному
принципу, при этом однако ж во-первых не все команды, во-вторых из этих двух “адресов”
хотя бы один должен относиться к регистру, а адрес ячейки памяти может быть только один.
Если ещё вспомнить, что регистры процессора не всегда считают памятью… Для обозначения
такого положения дел иногда изобретаются термины вроде “полутора-адресной” системы
команд — остроумно, но не информативно.

Семейство Intel 86 и IBM PC.
Вооружившись знанием теории, можно наконец приступать к изучению реальной
аппаратуры. Нашими лабораторными мышками будут процессоры семейства Intel 86.
Выбор для изучения именно этого типа процессоров имеет массу недостатков, но к
сожалению, это единственный реально доступный выбор. Если у вас есть возможность
изучить процессор другого типа — сделайте это обязательно! Впрочем, в любой ситуации
можно найти преимущества — безалаберное устройство Intel 86 затрудняет его изучение, зато
наглядно даёт понять разницу между терией и практикой.
Кроме того, изучение будет сконцентрировано на возможностях, существующих с самых старых версий процессора (и потому присутствующих во всём семействе). Такой выбор
также небезупречен: в современные версии процессора добавлено множество возможностей
для повышения производительности и организации многозадачной работы; было бы весьма
поучительно, например, проследить, какие именно команды были добавлены в i386 для обработки критических участков, вот только знания, необходимые для этого, излагаются семестром позже в курсе операцинных систем. Что же касается материала текущего семестра, то в
задачу генерации кода типичной прикладной программы эти новые возможности не вносят
принципиальных отличий.
Для понимания особенностей устройства процессоров Intel 86 и основанных на них
компьютеров IBM PC необходимо помнить, что эти особенности — результат очень непростого компромисса между несколькими взаимно противоречивыми тенденциями: желанием получить высокую производительность при низкой стоимости изделия, при этом ещё обеспечив
совместимость со старыми версиями. Система команд самого первого процессора в серии
составлялась так, чтобы названия команд совпадали с ещё более старым процессором 8080,
впоследствии это требование ещё ужесточилось, общая часть команд у старых и новых
процессоров семейства совпадает не только по названиям, но даже в двоичном представлении
(в том самом “машинном языке”)! Так что упрёк в “безалаберности” фирма Intel не заслужила
— на самом деле система команд продумана весьма тщательно, вот только удобство
программирования при этом было отнюдь не основным требованием.
Самый, пожалуй, яркий пример такого компромиссного решения — это организация
оперативной памяти.
С понятием “ссылки” или “указателя” вы должны быть знакомы по первому семестру;
в общем случае (в языках вроде Lisp или Java) за ним может скрываться весьма непростая сущность, но постольку, поскольку мы имеем дело с ассемблером IBM PC или языком C, эта
сущность вырождается в очень наглядную конструкцию: оперативная память компьютера
рассматривается как последовательность байт, а указатель по сути является порядковым
номером байта в памяти. Если речь идёт об языке ассемблера, то указатель принято называть
“адресом”.
В случае IBM PC эта идея порядкового номера извращена почти что до полной неузнаваемости. Суть конфликта в следующем: процессор 8086 приспособлен для работы с так называемыми “словами” из 16 бит, то есть всего возможно 216 = 65536 различных слов, так что
использование в качестве адреса одного слова ограничило бы объём памяти 64-мя килобай- 15 -
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования
Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования

More Related Content

Viewers also liked

HSC Algebra Lec 01 Matrix
HSC Algebra Lec 01 MatrixHSC Algebra Lec 01 Matrix
HSC Algebra Lec 01 Matrixlatifulkabir
 
HSC Algebra Lec 06 Determinant
HSC Algebra Lec 06 DeterminantHSC Algebra Lec 06 Determinant
HSC Algebra Lec 06 Determinantlatifulkabir
 
HSC Algebra Lec 04 Determinant
HSC Algebra Lec 04 DeterminantHSC Algebra Lec 04 Determinant
HSC Algebra Lec 04 Determinantlatifulkabir
 
Introduction to E-Commerce with Shopping Cart System
Introduction to E-Commerce with Shopping Cart SystemIntroduction to E-Commerce with Shopping Cart System
Introduction to E-Commerce with Shopping Cart SystemRavi Shankar Ojha
 
Makalah Sistem Pemilu di Indonesia
Makalah Sistem Pemilu di IndonesiaMakalah Sistem Pemilu di Indonesia
Makalah Sistem Pemilu di IndonesiaRiyanto Kasnuri
 
Makalah : Pancasila Sebagai Sistem Hukum
Makalah : Pancasila Sebagai Sistem HukumMakalah : Pancasila Sebagai Sistem Hukum
Makalah : Pancasila Sebagai Sistem HukumRiyanto Kasnuri
 

Viewers also liked (9)

Pintar
PintarPintar
Pintar
 
HSC Algebra Lec 01 Matrix
HSC Algebra Lec 01 MatrixHSC Algebra Lec 01 Matrix
HSC Algebra Lec 01 Matrix
 
HSC Algebra Lec 06 Determinant
HSC Algebra Lec 06 DeterminantHSC Algebra Lec 06 Determinant
HSC Algebra Lec 06 Determinant
 
T3 huesos-musculos
T3 huesos-musculosT3 huesos-musculos
T3 huesos-musculos
 
HSC Algebra Lec 04 Determinant
HSC Algebra Lec 04 DeterminantHSC Algebra Lec 04 Determinant
HSC Algebra Lec 04 Determinant
 
Auto Loan
Auto LoanAuto Loan
Auto Loan
 
Introduction to E-Commerce with Shopping Cart System
Introduction to E-Commerce with Shopping Cart SystemIntroduction to E-Commerce with Shopping Cart System
Introduction to E-Commerce with Shopping Cart System
 
Makalah Sistem Pemilu di Indonesia
Makalah Sistem Pemilu di IndonesiaMakalah Sistem Pemilu di Indonesia
Makalah Sistem Pemilu di Indonesia
 
Makalah : Pancasila Sebagai Sistem Hukum
Makalah : Pancasila Sebagai Sistem HukumMakalah : Pancasila Sebagai Sistem Hukum
Makalah : Pancasila Sebagai Sistem Hukum
 

Similar to Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования

Дополнительная общеразвивающая программа «Основы программирования В C/C++»
Дополнительная общеразвивающая программа «Основы программирования В C/C++»Дополнительная общеразвивающая программа «Основы программирования В C/C++»
Дополнительная общеразвивающая программа «Основы программирования В C/C++»rnmc7
 
Web20 from zero
Web20 from zeroWeb20 from zero
Web20 from zeroqweasdrty
 
Практическое создание крупного масштабируемого web 2.0 c нуля (Дмитрий Бородин)
Практическое создание крупного масштабируемого web 2.0 c нуля (Дмитрий Бородин)Практическое создание крупного масштабируемого web 2.0 c нуля (Дмитрий Бородин)
Практическое создание крупного масштабируемого web 2.0 c нуля (Дмитрий Бородин)Ontico
 
лекция1
лекция1лекция1
лекция1shagore
 
Deep c slides_oct2011_rus
Deep c slides_oct2011_rusDeep c slides_oct2011_rus
Deep c slides_oct2011_rusGarrikus
 
Mortal Sins and Guilty Pleasures of Automation Engineers
Mortal Sins and Guilty Pleasures of Automation EngineersMortal Sins and Guilty Pleasures of Automation Engineers
Mortal Sins and Guilty Pleasures of Automation EngineersÞorgeir Ingvarsson
 
Характеристики языка С++
Характеристики языка С++Характеристики языка С++
Характеристики языка С++DEVTYPE
 
Yuri Trukhin - Software developement best practices
Yuri Trukhin - Software developement best practicesYuri Trukhin - Software developement best practices
Yuri Trukhin - Software developement best practicesbeloslab
 
Статья «Формирование универсальных требований к пользовательским программам п...
Статья «Формирование универсальных требований к пользовательским программам п...Статья «Формирование универсальных требований к пользовательским программам п...
Статья «Формирование универсальных требований к пользовательским программам п...ph.d. Dmitry Stepanov
 
C++ осень 2013 лекция 1
C++ осень 2013 лекция 1C++ осень 2013 лекция 1
C++ осень 2013 лекция 1Technopark
 
портрет профессии программист
портрет профессии программистпортрет профессии программист
портрет профессии программистОльга Михеева
 

Similar to Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования (20)

Дополнительная общеразвивающая программа «Основы программирования В C/C++»
Дополнительная общеразвивающая программа «Основы программирования В C/C++»Дополнительная общеразвивающая программа «Основы программирования В C/C++»
Дополнительная общеразвивающая программа «Основы программирования В C/C++»
 
Web20 from zero
Web20 from zeroWeb20 from zero
Web20 from zero
 
Практическое создание крупного масштабируемого web 2.0 c нуля (Дмитрий Бородин)
Практическое создание крупного масштабируемого web 2.0 c нуля (Дмитрий Бородин)Практическое создание крупного масштабируемого web 2.0 c нуля (Дмитрий Бородин)
Практическое создание крупного масштабируемого web 2.0 c нуля (Дмитрий Бородин)
 
лекция1
лекция1лекция1
лекция1
 
Deep c slides_oct2011_rus
Deep c slides_oct2011_rusDeep c slides_oct2011_rus
Deep c slides_oct2011_rus
 
лек11 7
лек11 7лек11 7
лек11 7
 
лек11 7
лек11 7лек11 7
лек11 7
 
прак 15.docx
прак 15.docxпрак 15.docx
прак 15.docx
 
пр 15.docx
пр 15.docxпр 15.docx
пр 15.docx
 
OO Design with C++: 0. Intro
OO Design with C++: 0. IntroOO Design with C++: 0. Intro
OO Design with C++: 0. Intro
 
UML: Kinds of Diagram
UML:  Kinds of DiagramUML:  Kinds of Diagram
UML: Kinds of Diagram
 
6
66
6
 
Mortal Sins and Guilty Pleasures of Automation Engineers
Mortal Sins and Guilty Pleasures of Automation EngineersMortal Sins and Guilty Pleasures of Automation Engineers
Mortal Sins and Guilty Pleasures of Automation Engineers
 
Характеристики языка С++
Характеристики языка С++Характеристики языка С++
Характеристики языка С++
 
Это сложно
Это сложноЭто сложно
Это сложно
 
Yuri Trukhin - Software developement best practices
Yuri Trukhin - Software developement best practicesYuri Trukhin - Software developement best practices
Yuri Trukhin - Software developement best practices
 
Статья «Формирование универсальных требований к пользовательским программам п...
Статья «Формирование универсальных требований к пользовательским программам п...Статья «Формирование универсальных требований к пользовательским программам п...
Статья «Формирование универсальных требований к пользовательским программам п...
 
программист
программистпрограммист
программист
 
C++ осень 2013 лекция 1
C++ осень 2013 лекция 1C++ осень 2013 лекция 1
C++ осень 2013 лекция 1
 
портрет профессии программист
портрет профессии программистпортрет профессии программист
портрет профессии программист
 

Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования

  • 1. Шапорев Т.В., ассистент Программа учебного курса “Информатика и применение компьютеров в научных исследованиях” семестр 2 Москва, 2006 Отображение языков высокого уровня на архитектуру ЭВМ на примере языка C и основы технологии модульного программирования Примечание: по техническим причинам практически всё обучение ведётся на ПЭВМ совместимых с IBM PC, что крайне негативно сказывается на кругозоре студентов. Желательно при всякой возможности объяснять студентам разницу между специфическими деталями реализации конкретной архитектуры и другими общепринятыми/возможными подходами. В идеале студенты должны понимать не только как нечто реализовано, но и какой выбор был сделан при проектировании той или иной особенности, и почему именно такой. 1. Причины изучения языка C в курсе информатики. Исторические причины создания и популярности языка C. Достоинства и недостатки C (по сравнению с Паскалем и/или другими языками). 2. Перечень конструкций C, соответствующих Паскалю. Возможности C, отсутствующие в Паскале. Возможности Паскаля, отсутствующие в C (кратко, т.к. всё равно компилятор не позволит). 3. Концепция макропроцессора. Пример макропроцессора - C-шный препроцессор. Директивы препроцессора. 4. Общие сведения об архитектуре ЭВМ. Возможные подходы. Понятие микрокода. 5. Наш подопытный кролик - IBM PC и процессор 8086. Представление чисел. Адресация. Основные регистры и их назначение. Понятие о взаимодействии с периферийными устройствами и BIOS. 6. Язык ассемблера, как средство описания архитектурно-зависимых программ. Перечень основных команд процессора 8086. 7. Команды ассемблера, не имеющие непосредственного отображения в машинные команды и, в частности, команды организации ассемблерных процедур (подпрограмм). Соглашение о связях. 8. Переменные и регистровая арифметика. 9. Команды сравнения и условные переходы. Организация циклов. 10. Арифметическое устройство (сопроцессор), и компиляция арифметических выражений с плавающей точкой. 11. Система программных прерываний, как пример системных вызовов, экстракодов и т.п. Сервис DOS и BIOS. 12. “Продвинутые” возможности: работа со строками (и что ещё?) ввод-вывод в порты. Возможности ассемблера, которые не изучаются в курсе информатики. 13. Причины возникновения модульной разработки программ. Схема работы типичного компилятора. Компоновщик и загрузчик. Приёмы модульной организации программ в C. Приёмы модульной организации программ в ассемблере и C. 14. Сборка программ из нескольких файлов из командной строки. Идея дерева зависимостей, и его реализация на примере Turbo C. Универсальный механизм сборки больших проектов — make. Язык make-файлов. -1-
  • 2. Содержание семинарских занятий 1. Основные приёмы редактирования программы. Компиляция простейшей программы на C. Обязательно изучить command line компилятор! (А вот компиляция из интегрированной среды НЕ обязательна.) 2. Возможности форматированного вывода с помощью процедуры printf(). Обратная сторона — процедура scanf(),— и почему она менее употребительна. 3. Понятие буферизованного потока ввода-вывода (FILE). Стандартные потоки ввода-вывода. Процедуры fprintf() и fscanf(). Возможность создать свой поток с помощью функции fopen(). Процедура fclose(). Другие стандартные функции буферизованного ввода-вывода. 4. Организация текстовых строк в C. Приёмы работы со строками (организация временных буферов). Функции стандартной библиотеки для работы со строками. Функции sprintf() и sscanf(), их недостатки и преимущества использования strtol(), strtod() и им подобных. 5. Сборка прогаммы на C из нескольких файлов. Приёмы написания файлов заголовков (.h). Сборка программы посредством объектного кода. Использование объектного кода для компоновки программы на разных языках программирования. Пример сборки программы на C и ассемблере. 6. Генерация компилятором ассемблерного эквивалента C-шной программы. Анализ полученного кода. 7. Пример программирования “в машинных кодах”. 8. Сравнение ассемблерного результата работы DOS-овского и 32-битового компиляторов (для одинаковых C-шных фрагментов). -2-
  • 3. Введение: место информатики в общеинститутском курсе и место второго семестра в курсе информатки Хотелось бы надеяться, что необходимость изучения информатики в настоящее время ни у кого не вызывает сомнений, однако на практике это не так, и вопрос “зачем физику программирование” не возникнет только у совсем уж беспечного студента (хотя далеко не все рискуют задать этот вопрос). Наверное, есть резон попытаться на такой вопрос ответить, тем более, что понимание взаимосвязей между разными частями всего институтского курса и разными частями курса информатики должно помочь и в понимании материала этого семестра. Роль информатики в физическом образовании во многом подобна высшей математике: дифференциальное и интегральное исчисление нужны не сами по себе, а потому, что громадное количество практических задач описывается дифференциальными уравнениями (в частных производных), и их надо уметь решать. Однако, если даже удалось наконец-то для некоторой задачи выписать соответствующую систему уравнений — это только начало. Если удалось найти решение в виде формул — считайте, что вам сильно повезло, потому что на практике в подавляющем большинстве случаев такие системы не сводятся ни к каким вразумительным формулам. Тем не менее ситуация не безнадёжная: если вспомнить, что формулы как правило нужны тоже не сами по себе, а для получения численного результата, то вот как раз этот результат может быть расчитан непосредственно, без промежуточного этапа в виде формул. Методики непосредственного получения результата в численном виде составляют содержание курса вычислительной математики, но прежде чем пытаться что-то понять в вычислительной математике, надо представлять возможности своего рабочего инструмента — компьютера — не хуже, скажем так, чем уметь брать табличные интегралы. Если все силы уходят не на содержание задачи, а на войну с “проклятым ящиком” — это, легко понять, совсем не дело. Более того, компьютером надо уметь пользоваться эффективно, мысль о том, что современный-то числогрыз переварит всё, что ему ни подсунь — это вредная иллюзия. Были, есть, и в обозримом будущем будут задачи, которые не под силу ни одному компьютеру, например, прогноз погоды. Прогресс в области вычислительной техники как-то уж слишком легко сводится на нет небрежностью программирования: быстродействие современной персональной ЭВМ превосходит быстродействие БЭСМ-6 на вычислительных задачах примерно в 100 раз по порядку величины. Можно ли сказать, что за прошедшие сорок лет точность прогнозов выросла не на два порядка, а хотя бы вдвое? Если теперь от общих представлений вернуться к более конкреным нуждам этого семестра, то в нём, по большому счёту, будут решаться две (с половиной) задачи. Во-первых, дать представление о том, как вообще устроен и работает компьютер, и в этом необходимым средством является язык ассемблера. Необходимость знакомства с языком ассемблера можно более подробно расшифровать следующим образом. Во-первых, были и будут задачи, для решения которых ни один язык высокого уровня просто не приспосблен. К таким задачам часто относится работа с периферийными устройствами (если выбирать близкий к физической науке пример, то это может быть взаимодействие с экспериментальной установкой). Во-вторых, встречаются случаи, когда качество программы, разработанной на языке высокого уровня, неприемлемо плохое по сравнению с тем, что могло бы быть написано на ассемблере “вручную”. На практике это как правило выражается в том, что программа работает слишком долго. Значение последнего случая всё время снижается, по мере того, как постоянно совершенствуются средства реализации языков высокого уровня. Качество кода, сгенерированного современным оптимизирующим компилятором, может иной раз превосходить то, что в силах сочинить нормальный программист. Подвох в том, что это качество из компилятора надо ещё суметь извлечь (а для этого надо хотя бы отдалённо представлять, во что превращается программа на языке высокого уровня, и вот тут незаменим ассемблер). Бездумное использование языка высокго уровня как правило осталяет от возможного качества пустое место. Например, использование в C++ класса “прямоугольник” (CRect) там, где достаточно структуры “прямоугольник” (RECT), ясно говорит, что программист даже не допускал мысли о том, во что превратится эта с виду невинная конструкция, а это ведь не худший прмер. На практике встречаются куски кода, прямо сказать, изумительные в своём идиотизме! -3-
  • 4. Вторая задача данного семестра - познакомиться с языком “Си”. Хотя в первом семестре преподавание на многих потоках ведётся на Паскале, С будет совершенно необходим в третьем семестре в курсе операционных систем — и это не говоря уж о его практическом значении! Наконец, материал данного семестра касается технологии модульного программирования в объёме того минимума, который необходим для работы с C и ассемблером. Немного о терминологии В русскоязычной литературе по программированию слово “оператор” используется в двух довольно различных смыслах: для символа элементарного действия — вроде единственного знака “плюс”, — и для обозначения целого законченного предложения языка программирования. Это тем более обидно, что “в оригинале” такой неоднозначности нет, в английском используются два разных слова: operator и statement соответственно, так что для утвердившегося словоупотребления не видно никаких оснований, кроме лени и небрежности. Чтобы избежать неоднозначности в последующем изложении для обозначения элементарного действия используется слово “операция” (женского рода). Английское statement по-видимому лучше всего было бы переводить (предложенным Новосибирской школой) словом “предписание”, хотя в современной литературе чаще встречается слово “инструкция” — оба этих варинта будут дальше использоваться на равных. Общие сведения о языке C Язык программирования “Си” был разработан в начале 70-х годов прошлого века Брайаном Керниганом и Денисом Ричи для операционной системы “Юникс”. С тех пор оба они — C и UNIX — приобрели немалую популярность, и оба заметно изменились по сравнению с первоначальным вариантом. Поскольку преподавание в первом семестре велось на Паскале, постольку C будет излагаться “с опорой” на Паскаль — между этими языками много общего и те, кто действительно разобрался, как устроен Паскаль, легко поймут C по аналогии. Если у кого-то знания Паскаля не настолько хороши, что ж, остаётся надеяться, что им удастся разобраться хотя бы с одним C — хотя в данном курсе и не предусмотрены исчерпывающие объяснения для всех конструкций C, примеров использования этих конструкций будет немало. (Некоторое время тому назад были довольно популярны — а может быть и до сих пор остались — споры о том, какой язык лучше, Паскаль или C. В первую очередь такие споры характеризуют кругозор спорщиков, характеризуют однозначно и отнюдь не лестно. Желание спорить тут как правило говорит о том, что человек в глаза не видел даже FORTRAN IV, а уж со стороны Lisp или SNOBOL Паскаль и C смотрятся как близнецы-братья.) Хотя между двумя языками много общего, это всё-таки разные языки, и различия в основном обусловлены разными целями их создания. Паскаль изначально разрабатывался как учебный язык, поэтому он более строг и академичен; C — это профессиональный жаргон и, соответственно он более волюнтаристичен. Мне не известно доподлинно, какие именно цели ставили перед собой создатели C, однако не вызывает сомнений, что одними из основных целей были легкость создания компилятора и краткость. Без знания этих целей некоторые конструкции C понять невозможно. Чтобы понять, насколько это хорошо или плохо, необходимо упомянуть о надёжности языка. Надёжность — если и не главная, то уж точно одна из основных характеристик языка программирования 1. Вообще-то надёжность языков программирования — весьма обширная тема, на которую можно прочесть отдельный семестровый курс как минимум. Поскольку времени на это нет, ограничимся пониманием надёжности как того, насколько язык программирования способствует ясному недвусмысленному изложению на нём намерений программиста и тем самым сокращает количество возможных ошибок. Все популярные языки программирования не очень-то надёжны: дополнительная надёжность (как и следовало ожидать) даётся только ценой дополнительных усилий программиста, так что высоко надёжные языки были разработаны, но не стали популярными, используются они (например Ada) в достаточно 1 см. например Янг С. Алгоритмические языки реального времени: конструирование и разработка. Пер.с англ. — М.: Мир, 1985. -4-
  • 5. специальных областях и как правило в военных организациях, поскольку там можно язык установить в приказном порядке. В целом Паскаль, как более академичный и строгий язык, обладает большей надёжностью, чем C, тем не менее есть немало аспектов, в которых более надёжным является C. Общие сведения о работе компилятора Слово “компилятор” уже использовалось несколько раз, давно пора объяснить, что же это такое. Существует два подхода к тому, когда и как выполнять компьютерные программы. Самым естественным выглядит способ, при котором программа выполняется сразу же, по мере её чтения компьютером (то есть не просто компьютером, а компьютером при помощи соответствующих программных средств). Это называется прямой интерпретацией. Несмотря на кажущуюся естественность, более распространён другой подход: программа сначала как-то предварительно обрабатывается, а только потом уж выполняется. Смысл этого действа в том, что языки программирования, несмотря на их вроде бы неестественность, всё-так устроены так, чтобы было удобнее человеку, а не компьютеру. Вот этот процесс перевода с исходного языка в более приемлемую для компьютерной обработки форму и называется компиляцией. Таким образом компилятор — это некая сущность, “какое-то чего-то”, которое считывает программу на исходном языке и переводит её в эквивалентную программу на целевом языке. Важной функцией компилятора является вывод сообщений об ошибках в программе (буде таковые имеются). Исходная программа Целевая → Компилятор → программа ↓ Сообщения об ошибках На первый взгляд может показаться, что интерпретация и компиляция — взаимоисключающие подходы, но на самом деле отношения между ними ближе к симбиозу. На тему о компиляторах “вообще” можно говорить ну очень долго. Например, я сознательно использовал в определении слово “сущность”, хотя все известные мне компиляторы являются сущностями более конкретными, каждый компилятор — это программа, хотя (теоретически) можно сделать и по-другому. Компилятором на самом-то деле является любая известная мне типографская система, и так далее, и тому подобное. Однако ж в рамках семестрового курса нас будет интересовать довольно узкое подмножество компиляторов - те компиляторы, которые программу на каком-нибудь языке программирования переводят на “машинный язык” (что бы под этим ни понимать); и даже эту ограниченную область мы будем изучать на примере всего-то двух компиляторов - компиляторов языков C и ассемблера для IBM PC совместимых персоналок. Компилятор C работает по следующей схеме. -5-
  • 6. Исходная программа ↓ Пользовательские файлы объявлений (.h) → Препроцессор Системные файлы ← объявлений (.h) ↓ Программа со сделанными подстановками ↓ Компилятор ↓ Ассемблерный код (.asm) ↓ Ассемблер ↓ Объектный код (.obj) ↓ Редактор связей Системные ← библиотеки (.lib) ↓ Выполняемый файл (.exe) Любая из клеточек на схеме может сама по себе рассматриваться как отдельный компилятор. По такой схеме работал самый первый компилятор языка C (кстати говоря, для операционной системы UNIX, а вовсе не для DOS). Современные компиляторы как правило устроены несколько иначе, как правило все эти многочисленные клеточки объединяются всего в два этапа, тем не менее схемой можно пользоваться довольно смело, поскольку стандарт языка C требует, чтобы независимо от способа реализации результат работы компилятора выглядел так, как если бы он этой схеме соответствовал. Прежде, чем рассматривать особенности конкретных компиляторов, желательно оговорить ещё несколько понятий. Под “машинным языком” в разных случаях понимают довольно разные вещи, и, к сожалению, с распространённой небрежной трактовкой этого термина ничего уже поделать нельзя — всё это разнообразие необходимо помнить, чтобы понимать уже существующую литературу. Например, достаточно часто “машинным языком” называют язык ассемблера. Какой смысл в таком случае может иметь словосочетание “компилятор языка ассемблера в машинный язык”? Так вот, когда готовая к выполнению программа находится в памяти ЭВМ (в виде битиков), то, строго говоря, только в этом случае она и представлена на “машинном языке”. Такому представлению соответствувет в DOS формат .COM-файлов, но возможности его настолько ограничены, что вы с ним практически не будете иметь дела. Когда в этом курсе идёт речь о компиляторе в “машинный язык”, то под “машинным языком” понимается способ представления информации в выполняемых (.EXE) и объектных (.OBJ) файлах. По сравнению с “машинным языком” в строгом смысле этого слова, и выполняемые, и объектные файлы содержат некий минимум дополнительной информации, причём этот “минимум” разный для разных случаев (чем, собственно, и обусловлена необходимость в двух разных форматах; вообще говоря для этих целей можно использовать один формат, так и сделано в части современных операционных систем, но мы-то будем тренироваться на старенькой DOS). Чтобы объяснить, в чём эта разница, придётся упомянуть ещё пару новых понятий: редактор связей (по-английски linker) и загрузчик (соответственно loader). Я сознательно гово-6-
  • 7. рю “упомянуть”, а не “определить” потому, что для точного определения функций этих сущностей знаний пока недостаточно. Тем не менее понимать, о чём идёт речь, уже необходимо. Так вот, если в программе встречается вызов функции, то в её представлении на “машинном языке” код, отвечающий за этот вызов, должен быть как-то связан с тем кусочком кода, где эта функция была определена. Эту работу выполняет редактор связей (linker). Далее, компиляторы как правило генерируют целевую программу так, чтобы она могла работать в произвольном месте памяти ЭВМ — уж куда её ни определят ответственные за то программы, чтобы там и работала. Такая вот гибкость даётся не за просто так, обычно это означает, что такая “отвязанная” программа ни в каком конкретном месте работать не может. Привязку программы к конкретному месту в памяти (или, забегая вперёд, “размещение по конкретным адресам”) выполняет загрузчик (loader). Если теперь вспомнить, с чего начинался весь этот пассаж, то основная разница между объектными (.OBJ) и выполняемыми (.EXE) файлами заключается в том, что объектные файлы в обязательном порядке содержат информацию для редактора связей, так называемую таблицу имён, name table. Выполняемые файлы, хотя и могут содержать такую информацию, предназначены вообще-то для другого. Для выполняемых файлов обязательной является информация загрузчика — таблица перемещений или relocation table (а роль собственно загрузчика в DOS выполняет сама операционная система). Стоит ещё раз подчеркнуть, что эта схема, хоть и очень характерная, всё-таки специфична для DOS. Эта схема не так уж плоха в качестве примера, но ведь бывает и по-другому. С одной стороны, как уже было сказано, в ряде современных систем отсутствует деление на объектные и выполняемые файлы. (Следует отметить, что в таких системах словом “загрузчик” - loader - может быть названа программа, фактически выполняющая функции редактора связей.) С другой стороны можно встретить системы, в которых загрузчик не упрятан внутрь операционной системы, а существует как самостоятельная программа. Наконец, можно ведь реализовать компилятор и так, чтобы результатом его работы была программа на “машинном языке” в строгом смысле этого слова, уже размещённая в памяти и готовая к выполенению. При таком подходе можно было бы избежать сложностей, связанных с объектными и выполняемыми файлами. Тем не менее, в современном компьютерном мире утвердилась более сложная схема, так как она предоставляет больше возможностей — что это за возможности, вы будете узнавать в течение семестра. Таким образом вы получили достаточное (на данном этапе) представление о финальных клеточках в схеме работы компилятора. Если теперь обратить-таки внимание на начало этой схемы, то обнаруживается первое существенное отличие языка C от Паскаля (и многих других языков): неотъемлемой частью языка C является препроцессор. Препроцессор На вышеприведённой схеме клеточкой под названием “компилятор” (в отличие от компилятора в общем смысле этого слова) названа конструкция, которая из программы на стандартном, якобы машинно-независимом языке, порождает нечто, предназначенное для вполне конкрентной ЭВМ. Идея препроцессора заключается в том, чтобы как-то преобразовать исходную программу перед тем, как передать её “собственно компилятору”. В принципе, с формальной точки зрения, можно всё те же возможности реализовать на одном уровне с основными конструкциями языка, так что препроцессор не является совсем уж необходимым, и в большинстве языков препроцессор в стандарте языка отсутствует. Тем не менее некоторое время тому назад препроцессоры были модной темой, так как позволяли сравнительно легко приспособить язык “под себя”, то есть язык универсальный, а значит под решение большинства задач подходящий лишь постольку-поскольку, приспособить для решения конкретной задачи, добавив в него с помощью препроцессора конструкции, ориентированные на какую-то конкретную предметную область. Сейчас модно делать такие вот предметно-ориентированные расширения с помощью полиморфизма в объектно ориентированных языках, но совсем простой препроцессор так и остался в языке С как отголосок былой моды.(Язык ассемблера для PC включает свой собственный макропроцессор, заметно более мощный, чем C-шный. Хотя макроассемблер и не нужен для понимания взаимосвязи языков высокого уровня с -7-
  • 8. архитектурой ЭВМ, для изучения самой концепции полезно сравнить возможности макропроцессоров C и ассемблера.) Внешне директивы препроцессора выглядят как команды какого-то своего собственного языка внутри “нормальной” C-шной программы: все директивы препроцессора начинаются с символа # (диез) в первой позиции строки. (Большинство современных реализаций языка позволяют начинать директивы не только в первой позиции, но лучше наверное так не делать, вместо этого можно вставлять пробелы после символа #.) C-шный препроцессор выполняет три функции: 1) слияние файлов; 2) условную компиляцию; 3) макроподстановки. Во-первых, если исходная программа содержит директиву #include с последующим именем файла, эта единственная строчка заменяется на весь текст указанного файла. Имя файла в директиве #include обязательно заключается в кавычки: либо обычные двойные кавычки, либо угловые скобки - символы “меньше” и “больше”, например так: #include <stdio.h> Точная разница между этими двумя способами может разниться от компилятора к компилятору, но по смыслу двойные кавычки предназначены для нормальных файлов пользователя, а угловые кавычки — для так называемых системных хидеров, то есть файлов, разработанных вместе с компилятором как составная часть реализации языка. Сами включаемые файлы называются “файлами заголовков” (буквальный перевод исходного header files) и по традиции имеют расширение .h, хотя это и не обязательно, препроцессору, в отличие от компилятора, всё равно, какое у файла расширение. Хотя по сравнению с другими макрогенераторами C-шный препроцессор — это предел примитивизма, макроподстановки в C — наиболее замысловатая возможность препроцессора изо всех трёх. В принципе идея макроподстановок заключается в том, что препроцессор в тексте программы находит известные ему идентификаторы и заменяет их на некий другой текст. В простейшем варианте макрос используется для того, чтобы дать константе мнемоническое имя, например: #define PI 3.14159265358912 Теперь везде в последующем тексте программы вместо идентификатора PI автоматически будет подставлено его значение. Важно понимать, что макропроцессор никак не анализирует, что именно он подставляет, и текст макроподстановки совсем не обязан быть правильной конструкцией языка, главное, чтобы правильная конструкция получиась после подстановки. Макроопределение с параметрами — более сложная конструкция, это аналог функции. Аналогично функции, при выполнении макроподстановки формальные параметры заменяются фактическими. Например, следующее определение задает вычисление куба числа: #define cube(x) ((x)*(x)*(x)) Обратите внимание, на множество скобочек и ещё раз вспомните , что препроцессор меняет текст программы, и не более того. Если бы макрос был определён в виде #define cube(x) x*x*x То конструкция cube(x+1) развернулась бы в x+1*x+1*x+1 (то есть 3*x+1), а совсем не то, что требовалось. Эмпирическое правило на это счёт заключается в том, что язык C никак не наказывает за использование “лишних” скобок, поэтому если вы сомневаетесь в результате, то берите в скобки всё, что можно (хотя и злоупотреблять этим не стоит, так как программа становится менее читабельной). О макросах необходимо сделать ещё как минимум два замечания. Во-первых, понятие области видимости для макросов весьма условно: макрос действует с того места, где его определили и до конца файла; совершенно неважно, было определение внутри или снаружи функции, блока и т.п. Если такая глобальная видимость доставляет неудобства, то каждый отдельный макрос можно отменить директивой #undef. Во-вторых, существуют так называемые предопределённые макросы, которые компилятор предоставляет что называется “забесплатно”. Предопределённые макросы бывают -8-
  • 9. стандартные, то есть обязанные присутствовать в любых компиляторах C, и макросы, специфические для отдельных конкретных компиляторов. Полезными примерами стандартных макросов являются __FILE__ и __LINE__: первый из них заменяется на имя файла с текстом программы, а второй — на порядковый номер строки в этом файле. Примерами специфических макросов могут быть __TURBOC__, который содержит закодированный номер версии компилятора Turbo C (и отсутствует в других), _MSC_VER, который содержит номер версии Microsoft С и т.п. Директивы условной компиляции являются препроцессорным аналогом условной инструкции if, только в силу специфики работы препроцессора, кусок кода внутри директив условной компиляции, если ему “не повезло”, вообще исключается из целевой программы: он не попадает на вход “собственно компилятору”, на его хранение и выполнение (на последующих этапах) не тратятся никакие ресурсы и т.п. В самом первом стандарте Кернигана и Ричи для условной компиляции было всего пять директив препроцессора: каждый такой “условно компилируемый” кусок начинался одной из директив #if, #ifdef или #ifndef, обязательно заканчивался #endif, а между ними при необходимости могло присутствовать #else. Разница между первыми тремя инструкциями заключается в следующем: условие директивы #ifdef (сокращение от if defined) считается выполненным, если за ней следует имя уже определённого макроса. Директива #ifndef (if not defined) использует противоположное условие. Наконец, директива #if — наиболее сложная изо всех трёх, после неё должно следовать константное арифметическое выражение. Условие #if считается выполненным, если значение этого выражение отлично от нуля. Константное выражение — это выражение, состоящее из констант (например, целых чисел) и элементарных операций языка; смысл этого требования в том, чтобы значение выражения могло быть вычислено до начала выполнения программы (и более того, до того, как программа будет хотя бы приготовлена к выполнению). Хотя переменные в константное выражение включать нельзя, макросы использовать можно! Современный стандарт языка включает и другие возможности условной компиляции, наиболее полезной из них наверное является предикат defined: конструкция defined(ИМЯ) проверяет, определён ли макрос с заданным именем, но, в отличие от #ifdef предикат можно включать в константное выражение, так что, например, одной директивой #if можно проверить несколько макросов. В качестве примера и практической рекомендации стоит упомянуть, что конструкция #if 0 . . . #endif нередко используется для того, чтобы безусловно выключить, что называется “закомментировать” кусок текста программы. Особенности языка Если попытаться сравнить между собой программы на Паскале и C, то на первый взгляд может показаться, что между этими двумя языками сплошные различия: вместо begin end фигурные скобки { }, вместо операции присваивания единственный знак равенства и так далее. Более-менее полный перечень приведён в приложении для изучения самостоятельно или на семинарах. Тем не менее легко понять, что, несмотря на разницу в обозначениях, обозначают-то они примерно одно и то же. Более серьёзных различий не так уж и много. Во-первых, в C, в отличие от Паскаля, различаются большие и малые буквы. Разница, вроде бы, совсем не принципиальная, но на практике вызывает на удивление много ошибок. Во-вторых, структура программы упрощена по сравнению с Паскалем: вложенные функции запрещены, а понятие “раздела описаний” размыто: внутри функций все описания должны содержаться в начале блока до первого выполняемого оператора, но порядок описаний не регламентируется, а снаружи функций, то есть при описании глобальных объектов, нет даже этих ограничений. Основное правило — объект языка должен быть описан раньше его первого использования, и в общем-то это всё. (Может быть здесь следует подчеркнуть разницу между тем, как программа читается компилятором и как она выполняется: если порядок обращения к разным функциям на этапе выполнения программы определяется логикой действий этой программы, и в общем случае -9-
  • 10. этот порядок непредсказуем, то порядок чтения программы компилятором всегда один и тот же: “слева направо и сверху вниз”, если смотреть в текстовом редакторе.) Если однако же заглянуть внутрь описаний, то синтаксис описания типов заметно другой. Если сравнивать с Паскалем, в котором в инструкции описания переменных слева присутствует полная спецификация типа, одинаковая для всех последующих переменных, то в C сначала указывается только базовый тип, который затем может быть изменён модификатором рядом с именем переменной. Такая двойственная система придумана для сокращения записи, например, инструкция «int a, *b;» “зараз” описывает переменные двух разных типов: целочисленного и указателя на целое. За всё приходится платить, и такая краткая запись менее наглядна, чем в Паскале, особенно невразумительно подобный способ записи выглядит в инструкциях описания типов (typedef). Наконец, язык C в явном виде использует разбиение программы на файлы. Если вспомнить Виртовский стандарт Паскаля, то какое бы то ни было деление программы на части там просто проигнорировано: программа считается единым монолитным объектом, вся целиком подаётся на обработку компилятору и т.п. Такой подход мало приемлем, так сказать, “в реальной жизни”. Во-первых, реальные программные проекты как правило разрабатываются не одним человеком, а командой. Если такая команда попытается одновременно менять текст программы — каждый человек на свой лад, — то результатом будет полная неразбериха. Во-вторых, объём “настоящих” программных проектов измеряется как правило сотнями тысяч и миллионами строк. Попытайтесь представить, насколько неудобно и медленно редактировать и компилировать такой объём информации за раз. Даже поиск нужной строки в таком количестве информации превращается в проблему. Словом, давным-давно принято хоть сколько-нибудь большую программу делить на разные файлы и, например, Turbo Pascal предлагает для этого не менее двух разных способов. Увы, оба они нестандартные, и нет никаких гарантий, что эти способы заработают в любой другой реализации Паскаля. Разработчики C заранее предусмотрели возможность разбиения программы на части. (Хотя, справедливости ради, нужно отметить, что возможности эти до предела примитивны, интересующиеся могут для сранения познакомиться с Виртовской же Модулой или модным пока ещё - языком Java.). Программа на C компонуется из файлов, будь то файлы с исходным кодом или скомпилированные объектные файлы. Каждый файл представляет собой самостоятельный модуль и, например, объекты, объявленные в файле как глобальные, можно сделать невидимыми из других файлов с помощью ключевого слова static. Начальные сведения об устройстве компьютера. Слово “компьютер” вообще говоря обозначает весьма широкий класс устройств. Ради примера можно упомянуть так называемые аналоговые компьютеры в которых, образно говоря, вместо вычисления значения функции “синус” используется генератор синусоидального сигнала. Такого рода компьютеры развиваются и постепенно приобретают всё большую популярность, вот только относительная их доля стремительно снижается на фоне просто чудовищного прогресса компьютеров, основанных на цифровом принципе. Информатику принято считать достижением двадцатого века, причём второй его половины. Это не совсем так. Всякая уважающая себя наука пытается искать своё начало в трудах Аристотеля. Забираться так далеко вглубь времён конечно можно, но если говорить всерьёз, то деление современных компьютеров на функциональные блоки вполне соответствует идеям английского ученого XIX века Чарлза Бэбиджа. Практически любой современный компьютер включает следующие фукциональные узлы: ⇒ устройство, обеспечивающее организацию выполнения программы и согласованное взаимодействие узлов машины в ходе этого процесса — устройство управления, УУ; ⇒ устройство, обеспечивающее собственно обработку информации — арифметико-логическое устройство, АЛУ; ⇒ устройство для хранения исходных данных, промежуточных величин и результатов расчётов, а также самой программы обработки информации — запоминающее устройство или просто память; - 10 -
  • 11. ⇒ устройства, преобразующие информацию в форму, доступную компьютеру — устройства ввода; ⇒ устройства, преобразующие результаты обработки в форму, предназначенную для человека (или устройства какого-то типа, отличного от данного компьютера) — устройства вывода. память → (ОЗУ, ПЗУ) ← устройства ввода ↑↓ процессор (УУ, АЛУ) ↑↓ устройства вывода → ← внешняя память Следует подчернкнуть, что приведённое деление — именно фукциональное, в противовес общепринятому ныне конструктивному. Если, грубо говоря, необходимо назвать набор ингредиентов, которые надо купить для сборки компьютера, то совершенно естественно не делать различий между УУ и АЛУ, так как оба они (вместе со многими другими интересными вещами) находятся внутри центрального процессора, “памятью” при этом оказываются микросхемы ОЗУ, но не регистры процессора и не жесткие диски, внешняя память вместе с устройствами ввода-вывода чохом попадает в категорию “периферийные устройства” и т.д. В общем-то такой подход действительно лучше подходит для многих практических задач, но только не для данного курса. Систематическое изложение принципов постороения ЭВМ было сделано ближе к нашим дням - в середине XX века - группой авторов, включавшей Джона фон Неймана. Из-за высокого авторитета Неймана теперь архитектура ЭВМ в целом носит название “фон-неймановской”. (Такое положение дел преувеличивает личные заслуги Неймана, но бороться за историческую справедливость теперь вряд ли уместно.) Фон-неймановская архитектура опирается на следующие принципы: ⇒ Двоичное представление чисел и других данных (для сравнения можно напомнить, что первые компьютеры имели десятичное представление данных, а в 80х годах всерьёз обсуждались преимущества троичного представления данных — если бы существовала достаточно эффективная инженерная реализация устройства с тремя состояниями, то пользовались бы мы троичными компьютерами). ⇒ Программа хранится в той же памяти, что и обрабатываемые данные (в виде набора нулей и единиц), то есть принцип “хранимой программы” (для сравнения, первые ЭВМ программировались ручной перекоммутацией электрических цепей). ⇒ Память (для команд и данных) поделена на ячейки, доступ к ячейке осуществляется по её порядковому номеру или адресу — так называемый принцип адресности. ⇒ На каждом шаге из памяти выбирается и выполняется команда, адрес которой хранится в специальном устройстве. Наличие такого устройства — программного счетчика — ещё один принцип фон-неймановской архитектуры. Изложенные принципы выбраны так, чтобы при минимальном объёме максимамльно облегчить понимание работы компьютеров. Следует быть готовым к тому, что реальный компьютер будет похож на изложенную идеализированную схему лишь постольку-поскольку. Например, практически во всех современных компьютерах нет одного арифметического устройства, вместо этого используются несколько разных специализированных устройств, вплоть до того, что в старых моделях IBM PC целочисленная арифметика выполнялась центральным процессором, а устройство “действительной” арифметики даже конструктивно (а не только функционально) было отдельным блоком, отдельной микросхемой. Существуют примеры и более фундаментальных различий (но менее наглядные). - 11 -
  • 12. Иерархия памяти. То, что оперативная память является отдельным устройством, влечёт много важных последствий, сейчас остановимся на таком из них: устройства в составе центрального процессора не работают непосредственно с оперативной памятью. Например АЛУ для выполнения какой-либо операции сначала считывает операнды внутрь себя, а результат операции тоже получается внутри АЛУ и в оперативную память его ещё (может быть) придётся переписать. То есть внутри процессора тоже существуют некие специализированные устройства хранения информации — они называются регистрами. Регистры бывают двух типов: регистры устройств используются этими самыми устройствами по мере надобности, но система команд машины совершенно необязатльно даёт программисту возможность обращаться к таким регистрам непосредственно. Кроме того существуют так называемые регистры процессора, которые видны в системе команд (и, соответственно, языке ассемблера). Регистры процессора как правило играют роль сверхоперативной памяти. Таким образом, устройства памяти образуют иерархию, упорядоченную по мере убывания быстродействия и увеличения объёма: регистры - оперативная память - внешняя память. Важной частью современных компьютеров являются также устройства постоянной памяти и кэш-память, но из-за нехватки времени мы их сейчас рассматривать не будем. Справедливости ради следует упомянуть, что регистры далеко не всегда рассматриваются как разновидность памяти. По-видимому это связано с тем, с какой именно ЭВМ автор учебника познакомился первой; действительно, если взять IBM PC, в которой регистров немного или, пуще того, БЭСМ-6, в которой регистров данных было этак примерно полтора, то, казалось бы, при чём тут память? Если же взять какую-нибудь из современных ЭВМ, в которой регистры считаются десятками, то картина выглядит несколько иначе. Я не вижу смысла спорить, какой из подходов более правильный, лучше вместо этого заранее договориться о терминах. Микрокод. Если проанализировать материал предыдущего раздела, то можно заметить ещё одну существенную особенность: устройство машины, каким оно предстаёт программисту в виде системы команд, может существенно отличаться от реального положения дел. Действительно, многие с виду элементарные команды на самом деле реализованы как небольшие программы, хранящиеся непосредственно в центральном процессоре, а не в оперативной памяти (помните, как первые ЭВМ программировались физической перекоммутацией? — идея та же). Набор таких программ называется микрокодом. Это понятие можно проиллюстрировать следующим примером: команда сдвига в языках программирования высокого уровня (как правило) отображается в команду сдвига соответствующей ЭВМ. В процессоре Intel 8088 не было устройства сдвига на заданное значение: там было устройство сдвига на единичку, а сдвиг на другие значения был реализован в виде микропрограммного цикла, сдвигавшего операнд по единичке за раз до нужного результата. Цикл работы центрального процессора. При выполнении очередной команды практически любой современный процессор (а точнее — его устройство управления) проделывает примерно одинаковый набор действий: 1. считывание очередной команды из памяти по счётчику адреса; 2. дешифрация команды (например, не нужны ли команде данные из памяти); 3. формирование нового значения счётчика адреса (адреса следующей команды); 4. выборка операндов из памяти (если нужно); 5. выполнение команды (например в арифметическом устройстве); 6. запись результата в память (если требуется). Подавляющее большинство современных процессоров укладывается в эту простенькую схему, всё разнообразие их поведения обеспечивается пятым пунктом. Полезно обратить внимание на следующие особенности. Во-первых невразумительное положение пункта “вычисление адреса следующей команды” в середине списка. Связано это с тем, что длина команды в общем случае (и в IBM PC - 12 -
  • 13. в частности) может быть разной, и вычисляется она как раз в процессе дешифрации. Существует много процессоров, в которых все команды намеренно сделаны одинаковой длины, в этом случае вычисление адреса следующей команды может быть выполнено сразу же, то есть вторым пунктом. Ключевым моментом здесь является то, что данный шаг не может выполняться в конце цикла, так как есть команды, назначение которых именно подменять автоматически вычисленный адрес следующей команды на нечто другое. Во-вторых, цикл обработки команд — бесконечный, его завершение не предусмотрено. Хотя современные процессоры могут включать команду “конец работы” она же “останов процессора” — это скорее дань традиции, чем необходимость; современный компьютер, пока включен, всегда чем-нибудь занят: при завершении прикладной программы он всё свое время уделяет выполнению операцинной системы, а операционная система, если нет других дел, в бесконечном цикле ждёт команд пользователя. В третьих, в данной схеме напрочь проигнорирован такой принципиально важный момент, как обработка преываний. Что это такое — вы узнаете в третьем семестре в курсе операционных систем. Наконец, эта схема позволяет проиллюстрировать ещё одно важное понятие. Устройство управления имеет достаточно сложную внутреннюю структуру, и как правило каждый шаг схемы выполняется отдельной подсистемой внутри УУ, то есть в какой-то мере самостоятельным устройством. Так что для увеличения скорости работы процессора можно выполнение последовательных команд частично совместить по времени: пока очередная команда выполняется арифметическим устройством, для следующей за ней выбираются из памяти операнды, в это же время третья команда дешифруется и т.д. Этот приём, общепринятый для повышения производительности современных процессоров, называется конвейеризацией. Тактовая частота. Рассмотренная схема демонстрирует ещё одну важную идею: выполнение любой команды подразделяется на какое-то количество элементарных шагов — тактов (содержание каждого шага может очень сильно различаться в зависимости от устройства процессора и самой команды). Для того, чтобы разные устройства работали согласованно, сигнал к началу каждого такта подается специальным устройством, единым на весь компьютер — генератором тактовых импульсов (этакий большой полковой барабан, под который все “делают левой”). Чем больше тактовая частота, тем, при прочих равных, быстрее работает компьютер. Оговорка “при прочих равных” весьма существенна. Во-первых, тактовую частоту нельзя увеличивать произвольно — с какого-то момента устройства компьютера просто перестанут успевать выполнять команды. Во-вторых, нельзя забывать о разнице в устройстве процессоров. Даже в случае одного семейства Intel 86 количество тактов на выполнение одной и той же команды может различаться в разы для разных моделей процессора. В случае процессоров разных типов сравнение тактовых частот может просто не иметь смысла. Наконец, производительность компьютера в целом зависит не только от центрального процессора, а частенько — даже и не в первую очередь от него. Типизация системы команд. Общим местом в современных учебниках стало то, что система команд процессора может относиться к одному из двувх типов: CISC или RISC. Строго говоря, в такой вот категоричной форме этот тезис просто неверен; наверное любой реально существующий процессор включает элементы и того, и другого. Тем не менее эти аббревиатуры обозначают реально существующие противоборствующие тенденции в архитектуре компьютеров. Обозначение CISC расшифровывается как Complex Instruction Set Computer и говорит о том, что в процессоре предусмотрено как можно больше разнообразных команд на все случаи жизни. Буквы RISC означают, что процессор умеет выполнять только самый минимум команд, без которых совсем нельзя обойтись, зато уж эти-то команды реализованы максимально эффективно. Смысл этого противостояния в том, что сложность процессора всегда чем-то ограничена, в первую очередь технологией производства (и не стоит забывать о цене изделия!), так - 13 -
  • 14. что, грубо говоря, одно и то же количество вентилей в кристалле можно употребить либо на разнообразный набор операций, либо на лучшую коммутацию между элементами, либо на что-нибудь ещё, но на всё сразу и по-максимуму никак не получится. Наиболее характерной чертой CISC-процессоров (кроме набора команд) является наличие выделенных регистров: какие-то операции могут выполняться только с одним или несколькими регистрами, но не с какими-то другими. (Например, в любимом 86 семействе, команды умножения и деления работают только с сумматором, но не со счётчиком или несколькими другими регистрами, зато команда сдвига может иметь вторым аргументом только регистр счётчика и так далее). В противоположность этому, в RISC-процессорах регистры как правило равноправны. Исторически, поскольку разработчики первых процессоров были ограничены в средствах и вынуждены были программировать только необходимый минимум, постольку, как только технология это позволила, процессоры стали развиваться в направлении CISC. На сегодняшний день преобладает по-видимому тенденция RISC. Если вернуться к нашему любимому примеру, то процессор Pentium имеет внутри себя RISC ядро, а унаследованная от предков система команд типа CISC выполняется с помощью микрокода. Структура команды и её “адресность”. Представление команды процессора в памяти компьютера (в двоичном виде) обычно подразделяется на код операции (что именно надо сделать) и адресную часть (то есть откуда брать данные и куда поместить результат). В зависимости от кода операции адресная часть может отсутствовать. Если она всё же присутствует, то может быть организована по-разному. Допустим, для примера, что нам надо закодировать машинный аналог элементарного оператора: A = B + C В такой операции участвуют адреса трёх разных переменных и представляется естественным, что все три адреса должны присутствовать в команде. Системы команд организованные в соответствии с данным принципом, принято называть трёхадресными. Теперь немного арифметики: общепринятая длина адреса в современных компьютерах 32 бита, то есть трёхадресная команда заняла бы более 96 бит — чёртову дюжину байт. Такие длинные команды практически нигде не встречаются. Каким образом? На практике более употребительными являются системы команд двухадресного типа в которых результат кладётся на место одного из аргументов. При таком подходе приведённый пример пришлось бы транслировать в две машинные команды следующего типа: A = B; A = A + C или, используя более уместную в таком случае C-шную нотацию: A = B; A += C В результате у нас имеются две команды с четырьмя адресными частями: то есть количество битиков в машинном представлении возросло на один адрес и один код операции. В чём смысл подобной “экономии”? Подвох в том, что “в реальной жизни” обычно используются выражения более сложные, чем B+C. Добавим в пример единственное слагаемое: A = B + C + D В случае трёхадресной системы команд это выражение всё равно пришлось бы развернуть в несколько машинных команд, примерно так: A = B + C; A = A + D Для двухадресной системы команд получилась бы последовательность A = B; A += C; A += D Количество адресных частей сравнялось. Если ещё хоть чем-нибудь усложнить пример, то двухадресная система команд даст более короткий машинный код. Можно развить эту идею дальше: если, например, принять соглашение, что все арифметические операции помещают результат в специальный регистр процессора, откуда его можно потом переписать в память отдельной командой, то можно сформировать одноадресную систему команд. - 14 -
  • 15. Обозначим греческой ∑ регистр сумматора (не требующий адресной части), тогда приведённые примеры развернутся соответственно в ∑ = B; ∑ += C; A = ∑ и ∑ = B; ∑ += C; ∑ += D; A = ∑ То есть ни одного лишнего адреса, зато гораздо больше кодов операций. Поскольку как правило код операции короче адреса, такой подход даёт выигрыш в объёме кода и как правило проигрыш в быстродействии. Реальное положение дел, как обычно, не вполне соответствует теоретическим построениям. В системе команд семейства Intel 86 большинство команд устроено по двухадресному принципу, при этом однако ж во-первых не все команды, во-вторых из этих двух “адресов” хотя бы один должен относиться к регистру, а адрес ячейки памяти может быть только один. Если ещё вспомнить, что регистры процессора не всегда считают памятью… Для обозначения такого положения дел иногда изобретаются термины вроде “полутора-адресной” системы команд — остроумно, но не информативно. Семейство Intel 86 и IBM PC. Вооружившись знанием теории, можно наконец приступать к изучению реальной аппаратуры. Нашими лабораторными мышками будут процессоры семейства Intel 86. Выбор для изучения именно этого типа процессоров имеет массу недостатков, но к сожалению, это единственный реально доступный выбор. Если у вас есть возможность изучить процессор другого типа — сделайте это обязательно! Впрочем, в любой ситуации можно найти преимущества — безалаберное устройство Intel 86 затрудняет его изучение, зато наглядно даёт понять разницу между терией и практикой. Кроме того, изучение будет сконцентрировано на возможностях, существующих с самых старых версий процессора (и потому присутствующих во всём семействе). Такой выбор также небезупречен: в современные версии процессора добавлено множество возможностей для повышения производительности и организации многозадачной работы; было бы весьма поучительно, например, проследить, какие именно команды были добавлены в i386 для обработки критических участков, вот только знания, необходимые для этого, излагаются семестром позже в курсе операцинных систем. Что же касается материала текущего семестра, то в задачу генерации кода типичной прикладной программы эти новые возможности не вносят принципиальных отличий. Для понимания особенностей устройства процессоров Intel 86 и основанных на них компьютеров IBM PC необходимо помнить, что эти особенности — результат очень непростого компромисса между несколькими взаимно противоречивыми тенденциями: желанием получить высокую производительность при низкой стоимости изделия, при этом ещё обеспечив совместимость со старыми версиями. Система команд самого первого процессора в серии составлялась так, чтобы названия команд совпадали с ещё более старым процессором 8080, впоследствии это требование ещё ужесточилось, общая часть команд у старых и новых процессоров семейства совпадает не только по названиям, но даже в двоичном представлении (в том самом “машинном языке”)! Так что упрёк в “безалаберности” фирма Intel не заслужила — на самом деле система команд продумана весьма тщательно, вот только удобство программирования при этом было отнюдь не основным требованием. Самый, пожалуй, яркий пример такого компромиссного решения — это организация оперативной памяти. С понятием “ссылки” или “указателя” вы должны быть знакомы по первому семестру; в общем случае (в языках вроде Lisp или Java) за ним может скрываться весьма непростая сущность, но постольку, поскольку мы имеем дело с ассемблером IBM PC или языком C, эта сущность вырождается в очень наглядную конструкцию: оперативная память компьютера рассматривается как последовательность байт, а указатель по сути является порядковым номером байта в памяти. Если речь идёт об языке ассемблера, то указатель принято называть “адресом”. В случае IBM PC эта идея порядкового номера извращена почти что до полной неузнаваемости. Суть конфликта в следующем: процессор 8086 приспособлен для работы с так называемыми “словами” из 16 бит, то есть всего возможно 216 = 65536 различных слов, так что использование в качестве адреса одного слова ограничило бы объём памяти 64-мя килобай- 15 -