Скачиваний:
100
Добавлен:
01.05.2014
Размер:
1.56 Mб
Скачать

Основы компиляции

Давайте вспомним существо наших изысканий. Первое - мы захотели, чтобы спецификации статического типа, предъявляемые "при сборке сервера" отличались от спецификаций типа, предъявляемых "при сборке клиента". Мы выделили список методов доступа к объекту, влияющий и на клиент и на сервер, и выделили прочие части конструкции сервера, влияющие только на сервер. Второе - пользуясь списком методов, который мы упаковали в абстрактный класс, мы получили вполне работающий доступ клиента к серверу. Третье - мы не смогли решить проблему первоначального создания этого объекта клиентом - ни прямо в своем коде, ни косвенно - посредством изобретения специальной функции в составе сервера и хитрой системы именования. Мы решили её только при помощи "виртуальных абстрактных классов".

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

Схема устройства двоичного исполняемого модуля

Можно видеть, что модуль имеет секционированную структуру - в нём есть заголовок, таблицы и секции (сегменты). Заголовок - служебная информация, описывающая загрузчику операционной системы с чем он имеет дело, как данный модуль должен загружаться в память и настраиваться для того, чтобы он мог быть выполнен. Таблицы - аналогичная информация, описывающая требования к окружению - какие ещё модули должны загружаться, чтобы данный мог выполняться, либо, наоборот, что данный модуль может предоставить в распоряжение других модулей. А вот секции - секции и есть собственно "части тела" нашей программы. Необходимость секционирования связана с тем, что двоичные данные имеют разную семантику - часть двоичных чисел действительно описывает "данные", а часть - команды процессора. Поскольку процессор "просто выполняет команды", то он не может отличить является ли двоичное число кодом команды программы или же - обрабатываемыми программой данными. Поэтому все данные собираются в один сегмент, а все команды - в другой. Сказанное - только очень примитивная иллюстрация, существует еще немало причин, по которым и данные и код должны быть структурированы по секциям и далее. Поэтому в составе исполняемого модуля обычно содержится несколько секций данных разного сорта и несколько секций кода, но для нас сейчас важно одно - данные и код на самом деле "разложены по разным карманам", они на стороне процессора не "прослаиваются" и вместе никогда не располагаются.

А вот на стороне программиста, в языке высокого уровня на котором он пишет программу, ситуация обратна - в его тексте данные и код группируются вместе, "прослаиваются" и следуют логике мысли программиста, а не семантике двоичных данных. Ввиду чего у программиста, не знающего истинного положения вещей, могут возникать некоторые иллюзии, изживаемые обычно долгими часами бесплодной отладки… Но на рисунке показано, что объект класса Fooбудет разложен на "данные - отдельно, методы - отдельно", каким бы образом в исходном тексте что ни определялось.

Перевод программы из языка программиста на язык процессора выполняет специальная программа - транслятор. Транслятор принимает исходные предложения, записанные программистом, анализирует их и заменяет конструкции программиста равносильными конструкциями процессора. При этом транслятор ведет свои внутренние таблицы, в которых запоминает пары - "идентификатор - адрес двоичной конструкции". Всякий раз, когда в исходном тексте программист ссылается на идентификатор, транслятор на стороне процессора подставляет адрес из своей таблицы (называемый в таком случае ссылкой), соответствующий этому идентификатору.

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

Схема построения исполняемого модуля

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

Итак, на вход транслятору попало описание нашего класса Foo. Транслятор проанализирует текст и где-то там себе отметит, что классFooсостоит из двух элементов данных и трёх элементов процедур. Если они синтаксически правильны, то транслятор заполнит идентификаторами свои таблицы и расставит адреса данных класса относительно начала класса. Никакого кода здесь не порождается - вы знаете, что описание класса предназначено только транслятору.

Транслятор двигается дальше и видит в тексте модуля определения методов класса. Здесь они особо не показаны - только из соображений экономии площади рисунка. Располагаются они в месте, обозначенном жирным пунктиром - там же, где и другие конструкции программы. Ничего особенного в них нет, это - просто функции, связанные с классом. Методы - порождают код, поэтому транслятор в таблице смещений кода подставит в пару "идентификатор метода - адрес точки входа в него" адрес и откомпилирует текст, разместив команды в секции кода. Т.к. методы пока ниоткуда не вызываются, то это - тоже пока всё.

Дальше транслятор встречает определение объекта класса Foo Cls. Это предложение требует создать объект, т.е. отвести память под него и вызвать конструктор. Трансляция в этом и состоит - в сегменте данных транслятор отводит память и в таблице смещений данных помечает адреса, по которым он фактически разместил объект (вызов конструктора нас сейчас не интересует, а потому - пропускается).

Ещё дальше - предложение Cls.SetA(12). То самое предложение, ради рассмотрения трансляции которого всё это и написано! Трансляция состоит в вызове методаSetAв применении к объектуCls. Перед кодогенерацией транслятор, конечно, убедится, что вызвать метод не запрещено правилами видимости и разделения доступа, что метод объектаFooприменяется действительно к экземпляру объектаFooи т.д. - все эти ограничения являются синтаксическими, они не влияют на генерацию кода. А вот дальше начинается интересное - транслятор построит вызовCALL<адрес метода из секции кода> и передаст ему в качестве параметра <адрес объекта из секции данных>. Точнее, это будет пара примерно таких команд:

PUSH <адрес объекта из секции данных>

CALL <адрес метода из секции кода>

На рисунке место, где компилятор будет их размещать обозначено плюсиком красного цвета. Обратите внимание - то, что объект из секции данных семантически соотносится с методом из секции кода нигде не отмечено - это знает только транслятор из собственных своих таблиц. Это он сам просто непосредственно подставляет в код адреса. Элементы же данных "сами" ничего не знают, где располагаются соответствующие им методы, а методы - не знают, где располагаются "их" элементы данных. Когда транслятор закончит свою работу и завершится, таблицы эти будут забыты, и никто не узнает, каким там был объект Foo, но останется правильно сочинённая программа. Для проектного программирования нам больше ничего и не требовалось - всё работает так, как было описано на языке высокого уровня.

Здесь должно также быть понятно, чем должен бы являться и чисто абстрактный невиртуальный класс(если бы таковой в языке C++ существовал) - он вообще ничего не порождал бы в коде, но он должен был бы заполнить таблицы транслятора, когда транслятор начинал разбор методов классов от него произведённых. И если транслятор углядел бы при этом несоответствие абстрактного класса производному от него - должна была возникнуть синтаксическая ошибка. И только! Однако, функциональность такого класса разработчики языка сочли избыточной - зачем, если всё то же, плюс еще кое-что полезное делает и виртуальный класс? И в языке в качестве абстрактного типа остался только виртуальный класс, что нисколько не вредит "потребительским качествам языка C++", но несколько затрудняет объяснение этой тонкой разницы для программистов не знающих Ассемблера.

Из описанной же схемы ясно следует, что при компонентной конструкции программ клиент, вызывающий <адрес объекта из секции данных> никак не сможет узнать <адреса объекта из секции кода> - соответствующих таблиц-то связывающих одно и другое уже и нет. Поэтому решение проблемы "как получить адрес метода моего класса" должно быть перенесено с компилятора на сам объект, в его составе должна появиться особая таблица - таблица, в которой находятся адреса методов, которые может вызывать клиент. Это - специальный указатель на функцию, генерируемый компилятором в составе статического типа, как только компилятор видит словоvirtualв описании метода.

Более точно ситуация обстоит следующим образом - компилятор генерирует так называемую "таблицу виртуальных функций" (ее аббревиатура называется Vtbl), таблицу указателей по числу виртуальных методов, описанных в данном статическом типе. А в составе статического типа появляется указатель на этуVtbl. Соответствующая микроархитектура показана на рисунке:

Двоичная микроархитектура объекта имеющего виртуальные методы

Вы видите, что в составе одного только объекта данных, входящего в состав статического типа теперь имеется и информация о методах этого объекта - адреса точек входа в них. А сам объект данных состоит из структуры данных и служебной таблицы Vtbl. Указатель наVtblв составе объекта данных генерируется компилятором всегда на одном и том же месте, поэтому просто знания того, что у класса должна бытьVtblдостаточно, чтобы компилятор правильно на неё сослался. Сама жеVtblесть не что иное, как двоичная структура времени выполнения, соответствующаявиртуальному абстрактному классу. Словом, для того, чтобы знать и данные объекта и его методы нам теперь вполне достаточно знать только указатель на сам объект, на рисунке это адрес "объектаClsстатического типаFoo". А для того, чтобы знать только методы объектаClsнам достаточно извлечь из него ссылку наVtbl. И сделать это можно и на клиентской стороне ничего не зная о данных, составляющих сам объект сервера.

Нужно так же сказать - на рисункеизображена совершенно точная двоичная структура "объекта экспонирующего интерфейс" на которой и построенCOM. Единственная возможная здесь неточность - в какой именно памяти располагаются и сам объект данных иVtblкласса. Здесь нарисовано, что они располагаются в статической памяти модуля, хотя это бывает (и значительно чаще!) и не так.

Отметим - предложенного решения вполне достаточно: компилятор на клиентской стороне всё построит правильно. Соединённая архитектура клиент-серверного взаимодействия с использованием такого механизма на нижнем уровне показана на рисунке:

Клиент-серверное взаимодействие посредством Vtbl

На этом рисунке показано, как имея указатель ptrна объект классаFooкомпилятор в теле клиента будет вызывать методSetA. Весь вызов сводится, по сути, только к двум командам процессора - извлечению изptrссылки наVtblи косвенному переходу по адресу вVtbl. Там, где изображено жирное отточие компилятор вставит команды, соответствующие передаче аргументов вызываемому методу - они ничем не отличаются от команд, которые генерируются и при вызове "обычного" метода.

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