Вы находитесь на странице: 1из 5

Введение в программирование

1. Программирование на уровне команд процессора (микропроцессора)


Любое программируемое устройство содержит, по крайней мере, два элемента:
процессор – т.е. устройство, в котором происходит обработка информации, и
оперативную память, в которой размещается информация. При этом микропроцессор
имеет и свои собственные ячейки памяти, которые называются регистрами. Обработка
информации обычно реализована с использованием регистров. Вся информация,
обрабатываемая в процессе выполнения программы, представлена (закодирована) в виде
чисел и её обработка микропроцессором, так или иначе, сводится к выполнению обычных
арифметических операций и операций, обеспечивающих передачу информации между
регистрами и взаимодействие процессора с оперативной памятью, т.е. загрузку и выгрузку
регистров. В качестве примера приведём схему регистров микропроцессора Intel-64,
которые используются в современных персональных компьютерах. Все регистры делятся
на две группы: регистры общего назначения, с которыми непосредственно работает
программа, и регистры состояния и управления, в которых отображается состояние
микропроцессора. К регистрам состояния и управления можно отнести и регистр команд,
содержащий текущую команду, т.е. число-инструкцию, в которой закодировано то, что
необходимо для выполнения текущей операции, но не содержится в других регистрах.
Множество команд, которые может выполнить микропроцессор, называется его системой
команд. Регистры общего назначения, в свою очередь, делятся на три подгруппы:
регистры данных, регистры указателей и индексов и сегментные регистры.
Регистры данных:
RAX RCX RDX RBX
EAX ECX EDX EBX
AX CX DX BX
AH AL CH CL DH DL BH BL

В верхней части таблицы расположены 8-ми байтовые, т.е. 64-х разрядные регистры,
которые доступны только при работе программ в 64-х разрядном режиме. Ниже идут
четырехбайтовые, двухбайтовые и, наконец, однобайтовые регистры. Заметим при этом,
что если в более длинный регистр загружено какое-то число, то какая-то часть этого числа
будет доступна и при обращении к регистрам, расположенным в этой схеме ниже.
Например, если в RAX загружено число, то его младшая половина будет доступна в EAX,
младшая половинка EAX доступна в AX, а однобайтовые регистры AH и AL содержат
старшую и младшую половинки AX. Этот принцип обеспечивает гибкость при обработке
данных. Применение этих регистров в программе иногда может быть произвольным, но
некоторые операции, например, деление, требуют размещения операндов в определенных
регистрах. По принципу наиболее частого использования эти регистры имеют такие
названия: RAX – регистр-аккумулятор, RCX – регистр-счетчик (при выполнении циклов),
RDX – регистр данных, RBX – базовый регистр.
Расположение информации в оперативной памяти определяется её адресом. При этом минимальная адресуемая
ячейка памяти имеет размер 1 байт. Таким образом, адрес это фактически номер первого байта какого-то блока
информации, который может иметь различную длину, в том числе и всего лишь 1 байт. Для работы с адресами
используются регистры указателей и индексов и сегментные указатели. Регистры указателей и индексов:

RSP RBP RSI RDI Rx


ESP EBP ESI EDI RxD
SP BP SI DI RxW
SPL BPL SIL DIL RxB
Здесь RBP – указатель базы, RSI – индекс источника, RDI – индекс приемника, Rx –
группа регистров, при этом x – число от 8 до 15, RSP – указатель стека. Очевидно, что
разнообразие этих регистров обеспечивает разнообразие способов указания адреса.
Особо отметим наличие регистра, который называется указатель стека. Дело в
том, что доступные программе области оперативной памяти подразделяются на стек
(stack) и кучу (heap). Стек – это область памяти, выделяемая программе в момент ее
старта, она предназначена для индивидуального использования. Иногда бывает так, что
программа использует всю память стека, но пытается получить из него еще какой-то блок.
В таких случаях происходит аварийное завершение программы по ошибке «Переполнение
стека» (Stack overflow).
Куча – это область памяти, которая предназначена для совместного использования
многими программами. Программа может обратиться к операционной системе,
управляющей совместно используемой памятью, и с ее помощью взять какой-то блок
памяти из кучи. Как только этот блок памяти станет ей не нужным, программа должна
вернуть его обратно. Если при запросе на выделение памяти из кучи окажется, что в
данный момент нет незанятой оперативной памяти, то опять в дело вмешивается
операционная система. Она выгружает часть оперативной памяти на диск и, если
выгруженная часть потребуется какой-либо программе, загружает его обратно. Этот
процесс загрузки-выгрузки называется свопинг. Таким образом, используя кучу,
программы могут выполняться и при недостатке оперативной памяти. Очевидно, что
свопинг существенно (иногда – катастрофически) замедляет работу компьютера. Но все
программы, тем не менее, выполняются! Почему же всё-таки используются не только куча
но и стек? Дело в том, что получение памяти из стека происходит гораздо быстрее,
поскольку оно не требует обращения к операционной системе.
Все перечисленные регистры предназначены только для работы с целыми числами.
Для работы с вещественными числами, представленными как числа с плавающей запятой,
имеется еще восемь 64-х разрядных регистров, обозначаемых как ST0 — ST7, а также
регистры расширений, обеспечивающие работу операций ускоренной обработки видео- и
аудио- данных.
Очевидно, что при программировании на уровне команд микропроцессора
программист обязан не только знать архитектуру микропроцессора и его систему команд,
но и вручную записывать числовой код каждой команды. Такой уровень
программирования когда-то был заметно распространен. Я даже имел дело с целой
операционной системой, запрограммированной на уровне команд, при этом пришлось
писать для нее программы на таком же уровне. Но архитектура процессора и система
команд были гораздо проще, чем у нынешних ПК. Сейчас уровень команд иногда
используется для программирования микропроцессоров (микроконтроллеров),
встроенных в различные средства автоматики. Во всех остальных случаях для разработки
программ используют языки программирования, в огромной степени облегчающие этот
процесс.

2. Языки программирования и их классификация.


Языки программирования классифицируются по их уровню, т.е. по степени
приближенности к уровню команд микропроцессора, по способу выполнения
разработанной программы и по лингвистическим особенностям.

2.1. Языки программирования низкого уровня


Языки низкого уровня обычно принято называть ассемблерами. Как и в случае
программирования на уровне кодов микропроцессора, описанию подлежит каждая
команда программы. Однако вместо числовых кодов машинные команды в этих языках
обозначаются именами, т.е. мнемокодами. Именами можно обозначать и блоки памяти.
Все это очень существенно снижает трудоёмкость программирования, но в подавляющем
большинстве случаев для разработки программы на языке ассемблера требуется больше
времени, чем при использовании языка высокого уровня. Вместе с тем, ассемблерная
программа, написанная квалифицированным программистом, обычно выполняется
быстрее, поскольку не содержит лишних команд, которые появляются при переводе с
языка высокого уровня. Поэтому ассемблеры обычно используются для разработки
программ, требующих особого быстродействия, а также программ, управляющих какими-
либо нестандартными устройствами. Каждый ассемблер жестко привязан к архитектуре и
системе команд конкретного микропроцессора, поэтому какого-то единого языка
ассемблера не существует.
Очевидно, что программа, написанная на языке ассемблера, должна быть
переведена на язык машинных кодов. Такие программы-переводчики принято называть
компиляторами. Заметим также, что программу – переводчик с языка ассемблера иногда
тоже называют ассемблером. Иногда требуется понять, как работает уже готовый
машинный код, в таких случаях используются программы – дизассемблеры,
превращающие машинный код в программу на языке ассемблера. Следующий шаг –
переход к языку высокого уровня, очевидно, будет весьма сложным, такие программы мне
не известны.
Для примера рассмотрим фрагмент программы, в которой имеются тексты
инструкций, написанных на языке высокого уровня (С++), а также результат их
компиляции (машинный код) и результат дизассемблирования машинного кода.

iY = 10; Переменной iY присваивается значение 10

машинный код инструкции команда ассемблера


c7 85 c0 00 00 00 0a 00 00 00 movl $0xa,0xc0(%rbp)
movl – мнемокод команды загрузки числа. Расшифровка команды:
Берется число 0xa (т.е. 10 в шестнадцатеричной системе) и загружается в ячейку
памяти, выделенной для переменой iY. Адрес этой переменной определяется как сумма
адреса начала блока памяти, выделенного для переменных, который содержится в
регистре RBP и смещения С0, соответствующего переменной iY.

iD = 5 + iY + 2 * iY++;
8b 85 c0 00 00 00 mov 0xc0(%rbp),%eax
Команда mov загружает определенное в предыдущей инструкции значение переменной iY
в регистр EAX.

8d 48 05 lea 0x5(%rax),%ecx
Таинственная команда lea. Судя по ее описанию, она используется при работе с
адресами. Однако, как мы затем убедимся, здесь она всего лишь загружает в регистр ECX
число 5.
8b 85 c0 00 00 00 mov 0xc0(%rbp),%eax
Казалось бы, повторение пройденного, т.е. в регистр в регистр EAX опять записывается
значение переменной iY, которое там уже имеется!
8d 50 01 lea 0x1(%rax),%edx
Опять таинственная команда lea. Как мы затем убедимся, здесь она умудряется записать
в регистр EDX то, что имеется в регистре ECX (т.е. iY), но увеличенное на 1!
89 95 c0 00 00 00 mov %edx,0xc0(%rbp)
С командой mov никаких фокусов нет. Она делает то, что в ней написано. Т.е. содержимое
регистра EDX, содержащее увеличенное на 1 значение iY она записывает в уже знакомую
ячейку памяти, отведенную для iY. Т.е. делает iY++, что и записано в исходном тексте.
Это можно проверить и при пошаговом выполнении команд – в этом месте iY получает
новое значение.
01 c0 add %eax,%eax
Здесь всё просто. Вместо дорогостоящей операции умножения содержимое регистра EAX,
в котором имеется прежнее значение iY, складывается само с собой.
01 c8 add %ecx,%eax
Здесь тоже просто. К результату сложения – умножения на 2 добавляется число 5, которое
ранее было записано в регистр ECX командой lea.
89 85 bc 00 00 00 mov %eax,0xbc(%rbp)
И, наконец, еще проще – в ячейку памяти, отведенную для переменной iD записывается
окончательный результат.
На этом примере мы видим, что полностью понять результат дизассемблирования
бывает не очень просто…
Иногда к языкам низкого уровня относят и такие языки, которые могут
непосредственно работать с адресами, например, языки С и С++. Однако такие языки
будет правильней назвать языками высокого уровня, имеющими некоторые возможности
языков низкого уровня. Когда-то в стандарте языка С++ была возможность делать
ассемблерные вставки, смешивая коды С++ и команды ассемблера. Но сейчас это не
используется.

2.2. Языки программирования высокого уровня.


Вся остальная часть этой лекции будет посвящена языкам высокого уровня. При
программировании на таких языках программисту не требуется знать ни архитектуру, ни
систему команд микропроцессора, и как мы видели в рассмотренном примере, каждая
инструкция обычно приводит к образованию множества машинных команд. Рассмотрим
классификации таких языков. Прежде всего, они делятся на компилируемые языки и
интерпретируемые языки.
Программы, написанные на компилируемых языках, проходят предварительную
обработку программами – компиляторами и, так же как и ассемблерные программы,
превращаются в программы, представленные в машинных кодах. Для выполнения таких,
обработанных компилятором программ, больше не требуется каких-либо дополнительных
программ, достаточно наличия операционной системы. В операционных системах
Windows такие программы размещаются в файлах с расширениями .exe и .dll. При этом
программам, расположенным в файлах .exe (т.е. исполняемым файлам) часто требуются
наличие файлов .dll (динамически загружаемых библиотек). Библиотеки DLL обычно
создаются в тех случаях, когда один и тот же код требуется различным программам. Это
не только сокращает общий объем файлов, но часто облегчает и модернизацию программ.
Если какие-то изменения вносятся в DLL, (например, исправляется ранее не замеченная
ошибка), то эти изменения становятся доступными и всем программам, использующим
эту DLL.
Для выполнения программ, написанных на интерпретируемых языках,
недостаточно операционной системы, они могут выполняться лишь под управлением
специальных программ – интерпретаторов.
Заметим при этом, что скорость выполнения программы, скомпилированной
в машинный код, превосходит скорость интерпретируемой программы, как правило, в
десятки и сотни раз. Но, зато, в случае использования компилятора, при внесении
изменений в исходный код программы, прежде чем эти изменения можно будет увидеть в
работе программы, необходимо выполнить компиляцию исходного текста.
Компилируемые языки обычно позволяют получить более быструю и, возможно, более
компактную программу, и поэтому применяются для создания часто используемых
программ.
Деление языков на эти классы часто бывает условным, поскольку для некоторых из
них разработаны как интерпретаторы, так и компиляторы. Но, тем не менее, некоторые из
них всё же, гораздо чаще (если не всегда) используются с компиляторами, а некоторые – с
интерпретаторами.
В связи с этим к компилируемым языкам относят, например Pascal, Delphi, C, C++. К
интерпретируемым – Python, JavaScript, PHP, Visual Basic For Applications (VBA),
VBScript. Программы, написанные для интерпретируемых языков программирования,
часто называют скриптами. При этом для Python разработаны специальные
интерпретаторы. Некоторые из них позволяют выполнять программы в обычной среде, а
некоторые ориентированы на web-программирование. JavaScript, PHP и VBScript
предназначены для web – программирования, при этом JavaScript и VBScript могут
использоваться как для создания функций, работающих в среде web-браузеров, так и для
функций, выполняемых на web-сервере. Скрипты языка PHP выполняются только под
управлением web – серверов. Программы на языке VBA выполняются под управлением
приложений Microsoft Office (Word, Excel), их принято называть макросами.
Отдельно рассмотрим такие языки как C# и Java. Программы, написанные на этих
языках, тоже компилируются, но в результате компиляции получаются не команды
микропроцессора, а специальный код, который может выполняться только специальными
программами. Такой код иногда называют псевдокодом, а компиляцию –
псевдокомпиляцией. В случае Java программы, выполняющие псевдокод, называются Java
– машинами, а псевдокод, полученный из C#, выполянется под управлением CLR –
Common Language Runtime. Java – машины разработаны практически для всех
операционных систем, поэтому одна и та же программа, написанная на языке Java и
обработанная псевдокмпилятором, может выполняться в различных операционных
системах. Когда-то то же самое предполагалось и для CLR, но в настоящее время эта
среда исполнения имеется только в операционных системах Windows.

По лингвистическим особенностям языки высокого уровня, прежде всего, делятся


на функциональные языки и императивные языки. В настоящее время наиболее
распространены императивные языки.
Принципы, построения императивных языков.
1. В исходном коде программы записываются инструкции (команды).
2. Инструкции должны выполняться последовательно.
3. Данные, получаемые при выполнении предыдущих инструкций, могут читаться из
памяти последующими инструкциями.
4. Данные, полученные при выполнении инструкции, могут записываться в память.
При императивном подходе к составлению кода широко используется присваивание.
Наличие операторов присваивания увеличивает сложность и делает императивные
программы подверженными специфическим ошибкам, не встречающимся при
функциональном подходе.
Основные черты императивных языков:
использование именованных переменных;
использование оператора присваивания;
использование составных выражений;
использование подпрограмм.

Лекцию можно продолжить описанием особенностей функциональных языков


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