• Название:

    2006 328 с Макаров CIL и системное программиро..

  • Размер: 0.85 Мб
  • Формат: PDF
  • или

    Редакционная коллегия:
    В.В. Борисенко
    В.С. Люцарев
    И.В. Машечкин
    А.А. Михалев
    Е.В. Панкратьев
    А.М. Чеповский
    В.Г. Чирский
    А.В. Шкред

    Главный редактор серии:
    А.В. Михалев

    МОСКОВСКИМ ГОСУДАРСТВЕННЫМ УНИВЕРСИТЕТОМ
    имени М.В. Ломоносова
    и
    Интернет-Университетом
    Информационных Технологий
    при поддержке корпорации
    Microsoft

    Серия издается совместно

    ОСНОВЫ ИНФОРМАТИКИ И МАТЕМАТИКИ

    Серия учебных пособий по информатике и ее математическим основам открыта в 2005 году с целью современного изложения широкого спектра направлений информатики на базе соответствующих разделов математических курсов, а также примыкающих вопросов, связанных с информационными технологиями.
    Особое внимание предполагается уделять возможности использования материалов публикуемых пособий в преподавании
    информатики и ее математических основ для непрофильных специальностей. Редакционная коллегия также надеется представить вниманию читателей широкую гамму практикумов по информатике и ее математическим основам, реализующих основные алгоритмы и идеи теоретической информатики.
    Выпуск серии начат при поддержке корпорации Microsoft
    в рамках междисциплинарного научного проекта МГУ имени
    М.В. Ломоносова.

    Информация о серии

    Интернет-университет информационных технологий
    Москва • 2006

    Допущено учебно-методическим объединением вузов
    по университетскому политехническому образованию
    в качестве учебного пособия для студентов высших
    учебных заведений, обучающихся по направлению
    «Информатика и вычислительная техника»

    Common Intermediate
    Language и
    системное
    программирование в
    Microsoft .NET

    А.В. Макаров,
    С.Ю. Скоробогатов,
    А.М. Чеповский

    А.В. Макаров, С.Ю. Скоробогатов, А.М. Чеповский
    Common Intermediate Language и системное программирование в
    Microsoft.NET : учеб. пособие для студентов вузов, обучающихся по
    направлению «Информатика и вычисл. техника» / А. В. Макаров,
    С. Ю. Скоробогатов, А. М. Чеповский. – М. : Интернет-Ун-т Информ.
    Технологий, 2006. – 328 с. : ил. – ISBN 5-9556-0055-8.

    © Текст: А.В. Макаров, С.Ю. Скоробогатов, А.М. Чеповский, 2006
    © Оформление: Интернет-университет информационных технологий, 2006

    ISBN 5-9556-0055-8

    Допущено учебно-методическим объединением вузов по университетскому
    политехническому образованию в качестве учебного пособия для
    студентов высших учебных заведений, обучающихся по направлению
    «Информатика и вычислительная техника»

    В книге описаны основы архитектуры платформы .NET и промежуточный язык
    этой платформы – Common Intermediate Language (CIL). Подробно рассмотрен
    прием программирования, называемый динамической генерацией кода. Дано
    введение в многозадачность и описаны подходы к разработке параллельных приложений на базе платформы .NET.
    Адресовано всем изучающим вопросы создания метаинструментария и разработки компиляторов для различных операционных систем.
    Для студентов и преподавателей университетов, а также для специалистов, повышающих свою квалификацию.

    М15

    УДК 004.72(075.8)
    ББК 32.973.202-018
    М15

    Основой данной книги явился учебный курс, задачей которого было
    изучение достижений компьютерных наук в области системного программного обеспечения на примере революционных для практического
    программирования технологий, реализованных в платформе .NET.
    Курс читается на дополнительном образовании механико-математического факультета МГУ им. М.В. Ломоносова и для студентов одной из
    программистских специальностей МГТУ им. Н.Э. Баумана.
    Наш учебник посвящен системному программированию в .NET. Это
    означает, что в нем мы в основном будем затрагивать вопросы, существенные для разработчиков системного программного обеспечения. Поэтому
    из нашего учебника вы ничего не узнаете о технологиях ASP .NET и ADO
    .NET и не научитесь использовать очень удобную библиотеку
    Windows.Forms для создания графического пользовательского интерфейса.
    Кроме всего прочего, языки программирования C# и Visual Basic .NET тоже останутся за кадром нашего изложения. Однако многие примеры в
    учебнике будут написаны на C#, так как мы исходим из предположения,
    что вы уже знакомы с этим языком или способны достаточно легко понять
    примеры на объектно-ориентированном языке программирования.
    Вместо этого книга поможет изучить архитектуру платформы .NET и
    промежуточный язык этой платформы – Common Intermediate Language
    (сокращенно CIL). Реализация концепции промежуточного языка является наиболее интересным достижением современной компьютерной технологии. Именно эта технология и сам промежуточный язык рассматривается в нашей книге.
    Кроме того, мы подробно рассматриваем прием программирования,
    называемый динамической генерацией кода. Этот прием широко использовался еще 10-15 лет назад, но потом в силу некоторых причин стал менее
    популярен. Его смысл заключается в том, что код программы порождается
    прямо во время ее выполнения! Технология .NET, похоже, способна дать
    новый импульс этому направлению программирования, так как в .NET
    включены специальные средства для поддержки динамической генерации
    кода.
    В заключительных двух главах книги обсуждается параллельное программирование, которое становится все более популярным в программистском сообществе из-за бурного развития «материальной части» для высокопроизводительных вычислений. Рассматриваются механизмы многоза-

    ПРЕДИСЛОВИЕ

    дачности и создание приложений с параллельным выполнением операций,
    предоставляемых ядром операционной системы Windows. Обсуждается реализация параллельного выполнения кода в .NET, использование библиотечных средств платформы .NET для создания параллельных приложений.
    Изложение материала основывается на документированной спецификации [1 - 5]. В краткий список русскоязычной литературы [6 – 11] внесены книги, которые можно использовать для углубления знаний по рассмотренным темам.
    Материалы книги могут использоваться в соответствии с требованиями «Совокупности знаний по информатики» рекомендаций Компьютерного общества Института инженеров по электротехнике и электронике
    (IEEE-CS) и Ассоциации по вычислительной технике (ACM) «Computing
    Curricula 2001 Computer Science» (CC2001) [перевод: Рекомендации по преподаванию информатики в университетах/ Пер. с англ.: СПб.: Издательство СПбГУ, 2002. – 372 с.] в таких областях знаний как Операционные системы (OS), Языки программирования (PL) и Программная инженерия
    (SE). Перечислим разделы «Совокупности знаний по информатики» документа CC2001, которым соответствует содержание книги:
    OS2. Основы операционных систем;
    OS3. Параллелизм;
    OS4. Планирование и диспетчеризация;
    OS5. Управление памятью;
    PL2. Виртуальные машины;
    PL4. Переменные и типы данных;
    PL5. Механизмы абстракции;
    PL6. Объектно-ориентированное программирование;
    PL8. Системы трансляции;
    PL9. Системы типов;
    PL11. Разработка языков программирования;
    SE2. Использование программных интерфейсов приложений (API);
    SE3. Программные средства и окружения.
    Книга печатается в серии, открытой публикацией отечественных рекомендаций по преподаванию информатики:
    Преподавание информатики и математических основ информатики для
    непрофильных специальностей классических университетов: [учеб. пособие]/В. В. Борисенко [и др.]; [ред. А.В. Михалев]. – М.: Интернет-Ун-т Информ. Технологий, 2005. – 144 с.: ил., табл. – (Основы информатики и математики).
    Приведем разделы «Совокупность знаний по математике и информатике» вышеупомянутой разработки, которым соответствует содержание
    книги:

    vi

    P3. Операционные системы;
    P4. Низкоуровневое программирование;
    P7. Объектно-ориентированное программирование;
    IT1. Языки программирования.
    Книга и сопутствующие ей учебные курсы появились при поддержке
    корпорации Microsoft.
    Авторы надеются, что данная книга поможет в освоении последних
    достижений практического программирования студентам самых различных специальностей, преподавателям программирования и людям, самостоятельно изучающим современные компьютерные технологии.

    vii

    ОБ АВТОРАХ

    Чеповский Андрей Михайлович – доцент, к.т.н. Преподает в МГТУ
    им. Н.Э. Баумана и на дополнительном образовании механико-математического факультета МГУ им. М.В. Ломоносова, консультант МГУП. На протяжении
    многих лет читал различные курсы по программированию: алгоритмические
    языки, функциональное программирование, теоретическое программирование, теория формальных языков, информационные системы и базы данных,
    распределенные системы обработки информации, параллельное программирование.

    Скоробогатов Сергей Юрьевич – ассистент МГТУ им. Н.Э. Баумана.
    Ведет занятия по курсам алгоритмических языков, функциональному программированию, разработке программного обеспечения.

    Макаров Андрей Владимирович – старший преподаватель МГТУ
    им. Н.Э. Баумана. В течении многих лет читает курсы по архитектуре компьютеров, операционным системам, системному программированию, программированию под ОС Windows.

    viii

    Глава 2. Структура программных компонентов . . . . . . . . . . . . . . . . . . . . . 32
    2.1. Формат исполняемых файлов . . . . . . . . . . . . . . . . . . . . . . . . . . 32
    2.1.1. Управление памятью в Windows . . . . . . . . . . . . . . . . . . . 34
    2.1.2. Обзор структуры PE-файла . . . . . . . . . . . . . . . . . . . . . . 36
    2.1.3. Заголовки . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
    2.1.4. Особые секции PE-файла . . . . . . . . . . . . . . . . . . . . . . . . 49
    2.1.5. Заголовок CLI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
    2.1.6. Пример генерации PE-файла . . . . . . . . . . . . . . . . . . . . . 53
    2.2. Формат метаданных . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
    2.2.1. Расположение метаданных и кода внутри сборки . . . . 65
    2.2.2. Структура метаданных . . . . . . . . . . . . . . . . . . . . . . . . . . 67
    2.2.3. Таблицы метаданных . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
    2.3. Взаимодействие программных компонентов . . . . . . . . . . . . . . 72
    2.3.1. Обзор компонентных технологий . . . . . . . . . . . . . . . . . 73
    2.3.2. Взаимодействие компонентов в среде .NET . . . . . . . . 76
    2.3.3. Общая спецификация языков . . . . . . . . . . . . . . . . . . . . 82

    Глава 1. Введение в архитектуру Microsoft .NET Framework . . . . . . . . . . . 1
    1.1. Знакомство с .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
    1.1.1. Главные темы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
    1.1.2. Предшественники платформы .NET . . . . . . . . . . . . . . . 3
    1.1.3. Обзор архитектуры .NET . . . . . . . . . . . . . . . . . . . . . . . . . 5
    1.2. Общая система типов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
    1.2.1. Ядро системы типов .NET . . . . . . . . . . . . . . . . . . . . . . . 11
    1.2.2. Дополнительные элементы системы типов .NET . . . . 17
    1.3. Виртуальная система выполнения . . . . . . . . . . . . . . . . . . . . . . 21
    1.3.1. Состояние виртуальной машины . . . . . . . . . . . . . . . . . 21
    1.3.2. Состояние метода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
    1.4. Автоматическое управление памятью . . . . . . . . . . . . . . . . . . . . 28
    1.4.1. Выделение памяти в управляемой куче . . . . . . . . . . . . 28
    1.4.2. Алгоритм сборки мусора . . . . . . . . . . . . . . . . . . . . . . . . . 29
    1.4.3. Основные приемы повышения эффективности сборки
    мусора . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

    Оглавление

    ix

    Глава 4. Анализ кода на CIL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
    4.1. Граф потока управления . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
    4.1.1. Основные элементы графа потока управления . . . . . 133
    4.1.2. Блоки обработки исключений в графе потока
    управления . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
    4.1.3. Дерево блоков в графе потока управления . . . . . . . . . 138
    4.2. Преобразование линейной последовательности
    инструкций в граф потока управления . . . . . . . . . . . . . . . . . . 140
    4.2.1. Создание массива узлов . . . . . . . . . . . . . . . . . . . . . . . . 141
    4.2.2. Создание дерева блоков . . . . . . . . . . . . . . . . . . . . . . . . 142
    4.2.3. Присвоение родительских блоков узлам графа . . . . . 145
    4.2.4. Формирование дуг . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 145

    Глава 3. Common Intermediate Language . . . . . . . . . . . . . . . . . . . . . . . . . . 83
    3.1. Поток инструкций языка CIL . . . . . . . . . . . . . . . . . . . . . . . . . . 83
    3.1.1. Формат потока инструкций . . . . . . . . . . . . . . . . . . . . . . 83
    3.2. Язык CIL: инструкции общего назначения . . . . . . . . . . . 88
    3.2.1. Инструкции для загрузки и сохранения значений . . . 88
    3.2.2. Арифметические инструкции . . . . . . . . . . . . . . . . . . . . 91
    3.2.3. Инструкции для организации передачи управления . 100
    3.3. Язык CIL: инструкции для поддержки объектной модели . . 105
    3.3.1. Инструкции для работы с объектами . . . . . . . . . . . . . 105
    3.3.2. Инструкции для работы с массивами . . . . . . . . . . . . . 108
    3.3.3. Инструкции для работы с типами-значениями . . . . . 111
    3.3.4. Инструкции для работы с типизированными
    ссылками . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
    3.4. Язык CIL: обработка исключений . . . . . . . . . . . . . . . . . . . . . 116
    3.4.1. Предложения обработки исключений в заголовках
    методов . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
    3.4.2. Инструкции CIL для обработки исключений . . . . . . 119
    3.4.3. Правила размещения областей . . . . . . . . . . . . . . . . . . 121
    3.4.4. Ограничения на передачу управления . . . . . . . . . . . . 122
    3.4.5. Семантика обработки исключений . . . . . . . . . . . . . . . 123
    3.5. Синтаксис ILASM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
    3.5.1. Основные элементы лексики . . . . . . . . . . . . . . . . . . . . 124
    3.5.2. Синтаксис . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
    3.5.3. Пример программы . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

    x

    Глава 6. Основы многозадачности . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
    6.1. Многозадачность в Windows . . . . . . . . . . . . . . . . . . . . . . . . . . 183
    6.1.1. Основные понятия . . . . . . . . . . . . . . . . . . . . . . . . . . . . 183
    6.1.2. Реализация в Windows . . . . . . . . . . . . . . . . . . . . . . . . . . 194
    6.2. Общие подходы к реализации приложений с параллельным
    выполнением операций . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201
    6.2.1. Асинхронный ввод-вывод . . . . . . . . . . . . . . . . . . . . . . . 202
    6.2.2. Асинхронные вызовы процедур . . . . . . . . . . . . . . . . . . 207
    6.2.3. Процессы, потоки и объекты ядра . . . . . . . . . . . . . . . 207
    6.2.4. Основы использования потоков и волокон . . . . . . . . 212

    Глава 5. Динамическая генерация кода . . . . . . . . . . . . . . . . . . . . . . . . . . 163
    5.1. Введение в динамическую генерацию кода . . . . . . . . . . . . . . 163
    5.1.1. Обобщенный алгоритм интегрирования . . . . . . . . . . . 164
    5.1.2. Представление выражений . . . . . . . . . . . . . . . . . . . . . . 165
    5.1.3. Трансляция выражений в C# . . . . . . . . . . . . . . . . . . . . 166
    5.1.4. Трансляция выражений в CIL . . . . . . . . . . . . . . . . . . . 168
    5.1.5. Сравнение эффективности трех способов
    вычисления выражений . . . . . . . . . . . . . . . . . . . . . . . . 169
    5.2. Генерация линейных участков кода для стековой машины . 170
    5.2.1. Генерация кода для выражений . . . . . . . . . . . . . . . . . . 170
    5.2.2. Оптимизация линейных участков кода . . . . . . . . . . . . 173
    5.3. Генерация развилок . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
    5.3.1. Генерация кода для логических выражений . . . . . . . . 175
    5.3.2. Генерация кода для управляющих конструкций . . . . 178
    5.3.3. Оптимизация кода, содержащего развилки . . . . . . . . 179

    4.3. Верификация CIL-кода . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
    4.3.1. Классификация применяемых на практике
    алгоритмов верификации . . . . . . . . . . . . . . . . . . . . . . . 147
    4.3.2. Особенности верификатора кода, используемого
    в .NET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
    4.3.3. Алгоритм верификации . . . . . . . . . . . . . . . . . . . . . . . . 149
    4.4. Библиотеки для создания метаинструментов . . . . . . . . . . . . 152
    4.4.1. Metadata Unmanaged API . . . . . . . . . . . . . . . . . . . . . . . 153
    4.4.2. Reflection API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
    4.4.3. Сравнение возможностей библиотек . . . . . . . . . . . . . 162

    xi

    Приложение B. Исходный код программы Integral . . . . . . . . . . . . . . . . 302
    B.1. Expr.cs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 302
    B.2. Integral.cs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 308

    Приложение Б. Исходный код программы CilCodec . . . . . . . . . . . . . . . 291

    Приложение A. Исходный код программы pegen . . . . . . . . . . . . . . . . . . 274
    A.1. macros.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
    A.2. pe.h . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
    A.3. pe.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
    A.4. main.c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288

    Литература . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273

    Глава 7. Разработка параллельных приложений для ОС Windows . . . . . . 218
    7.1. Применение потоков и волокон . . . . . . . . . . . . . . . . . . . . . . . 218
    7.1.1. Пулы потоков, порт завершения ввода-вывода . . . . . 218
    7.1.2. Память, локальная для потоков и волокон . . . . . . . . . 225
    7.1.3. Привязка к процессору и системы с неоднородным
    доступом к памяти . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
    7.2. Взаимодействие процессов и потоков . . . . . . . . . . . . . . . . . . 231
    7.2.1. Синхронизация потоков . . . . . . . . . . . . . . . . . . . . . . . . 231
    7.2.2. Процессы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
    7.3. Параллельные операции в .NET . . . . . . . . . . . . . . . . . . . . . . . 250
    7.3.1. Потоки и пул потоков . . . . . . . . . . . . . . . . . . . . . . . . . . 251
    7.3.2. Асинхронный ввод-вывод . . . . . . . . . . . . . . . . . . . . . . 255
    7.3.3. Асинхронные процедуры . . . . . . . . . . . . . . . . . . . . . . . 257
    7.3.4. Синхронизация и изоляция потоков . . . . . . . . . . . . . . 260
    7.3.5. Таймеры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 271

    xii

    1

    1.1.1.1. Разработка метаинструментов
    Создание любого программного продукта подразумевает знакомство
    программиста с предметной областью. То есть разработчик бухгалтерской
    программы должен в какой-то степени разбираться в бухучете, а создатель
    Интернет-магазина – в принципах ведения торговли. Нетрудно догадаться, что создание новых инструментов для разработки программ требует от
    программиста знакомства с тем, с чем он и так хорошо знаком – с разработкой программ! Наверное, поэтому это занятие столь увлекательно.

    В этой книге, рассматривая возможности системного программирования в .NET, мы будем преследовать две главные цели: разработка метаинструментов и конструирование компиляторов.

    1.1.1. Главные темы

    Без всякого преувеличения можно сказать, что платформа .NET стоит в одном ряду с самыми значительными достижениями корпорации
    Microsoft. Более того, с точки зрения программиста, работающего в области создания компиляторов и других средств разработки программ, .NET
    является технологией неизмеримо более привлекательной, чем все продукты, ранее созданные в Microsoft.
    Разработка платформы .NET началась в 1998 году. Изначально ей дали рабочее название Project 42, которое затем было изменено на COM
    Object Runtime (сокращенно, COR). Видимо, аббревиатура COR использовалась достаточно длительное время, так как ее до сих пор можно найти в
    названиях dll-файлов и именах библиотечных функций. Потом платформа
    сменила еще несколько названий: Lightning, COM+ 2.0, Next Generation
    Web Services и, в конце концов, стала называться .NET Framework.
    Спецификация основной части платформы .NET стандартизована
    ассоциацией ECMA (European Computer Manufactures Association). Это означает, что корпорация Microsoft приветствует независимые реализации
    платформы.

    1.1. Знакомство с .NET

    Глава 1.
    Введение в архитектуру
    Microsoft .NET Framework

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    Оптимизатор

    Back-end

    Резюмируя вышесказанное, можно сказать, что в большинстве учебников и учебных курсов, посвященных разработке компиляторов, основное
    внимание уделяется алгоритмам лексического и синтаксического анализа,
    то есть они учат в основном программированию «front-end'ов». В нашем
    учебнике мы концентрируем внимание на архитектуре и языке целевой
    платформы (.NET), а также изучаем работу с графами потоков управления.
    Это означает, что мы ориентируемся на программирование «back-end'ов».

    Рис. 1.1. Структура современного компилятора

    Front-end

    Промежуточное
    представление
    (возможно, граф потока
    управления)

    1.1.1.2. Конструирование компиляторов
    В структуре практически любого современного компилятора можно
    выделить, по крайней мере, две части: «front-end» и «back-end». «Frontend» осуществляет лексический и синтаксический анализ программы и
    переводит программу в некоторое промежуточное представление. А «backend» на основе этого промежуточного представления генерирует код для
    целевой аппаратной платформы. Между этими двумя частями может находиться оптимизатор, анализирующий и преобразующий промежуточное
    представление программы (см. рис. 1.1).
    В нашем учебнике мы будем рассматривать представление кода в виде графа потока управления, узлы которого соответствуют инструкциям
    языка, а ребра обозначают передачу управления между ними. Такое представление можно использовать в качестве промежуточного представления
    кода в компиляторе.

    Мы будем называть метаинструментами программы, для которых
    другие программы выступают в роли данных. Метаинструменты используются для разработки, тестирования, анализа и преобразования программ.
    Это могут быть компиляторы, средства быстрой разработки приложений
    (RAD), оптимизаторы, отладчики, верификаторы, профайлеры и т.п. Знания, полученные из этого учебника, вы сможете применять для создания
    метаинструментов, которые работают на платформе .NET.

    2

    3

    1.1.2.2. Технология ANDF
    Технология ANDF (Architectural Neutral Distribution Format) была
    разработана в первой половине 1990-х годов в OSF (Open Software
    Foundation) для увеличения переносимости программного обеспечения.
    Смысл технологии заключается в разделении процесса компиляции программ на две разнесенные во времени и пространстве фазы:

    1.1.2.1. UCSD p-System
    Операционная система UCSD p-System была разработана в 1978 году
    в Калифорнийском университете для учебных целей. Главное ее достоинство заключалось в том, что она могла работать как на компьютерах PDP11, стоявших в вычислительном центре университета, так и на домашних
    микрокомпьютерах студентов.
    Независимость операционной системы от аппаратной платформы
    достигалась путем введения понятия виртуальной p-машины (p-Machine),
    обладавшей собственным набором инструкций, который назывался p-кодом (p-code). Сама операционная система и все работавшие в ней программы были закодированы на p-коде, поэтому для того чтобы запустить
    их на новой аппаратной платформе, требовалось всего лишь реализовать
    для этой платформы интерпретатор p-кода.
    Виртуальная p-машина была похожа на обычный компьютер и обладала процессором и памятью. Программы хранились в памяти машины
    вместе с данными, а все вычисления выполнялись через расположенный в
    памяти стек. Виртуальный процессор содержал пять регистров, один из которых использовался для хранения адреса текущей выполняемой инструкции (program counter – PC), а остальные обеспечивали работу со стеком.
    Платформа .NET использует похожую схему обеспечения независимости программ от аппаратной платформы. Все программы, работающие
    на платформе .NET, закодированы на языке CIL (Common Intermediate
    Language), который представляет собой набор инструкций некой абстрактной стековой машины. Основное отличие UCSD p-System от .NET заключается в принципах выполнения программ. Программы, закодированные в p-коде, непосредственно выполнялись интерпретатором, тогда как
    программы на CIL перед выполнением транслируются в код для конкретного процессора специальным компилятором.

    Многие идеи, которые легли в основу платформы .NET, были разработаны задолго до ее появления. В этом разделе мы совершим краткий
    экскурс в историю и рассмотрим несколько программных систем, которые по праву можно считать предшественниками платформы .NET.

    1.1.2. Предшественники платформы .NET

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    ST10

    RS6000

    ANDF

    Ada 95

    ST9

    SPARC
    Philips-XA

    PowerPC

    Java

    1.1.2.3. Платформа Java
    Платформа Java по архитектуре и своим возможностям наиболее
    близка к платформе .NET. Она была разработана в середине 1990-х годов в
    Sun Microsystems для бытовых приборов, подключаемых к компьютерным

    Рис. 1.2. Схема использования технологии ANDF

    Инсталляторы для каждой целевой платформы

    MIPS

    80x86

    C/C++

    Генераторы ANDF для различных языков программирования

    1. перевод программы в формат ANDF;
    2. трансляция программы, представленной в формате ANDF, в исполняемый файл при установке программы на компьютер пользователя.
    Формат ANDF не зависит ни от языков программирования, ни от
    особенностей аппаратных платформ и операционных систем. Программы,
    распространяемые в формате ANDF, могут быть установлены на любой
    платформе, для которой имеется транслятор из ANDF в исполняемый код.
    Схема использования технологии ANDF показана на рис. 1.2. Для каждого языка программирования реализован компилятор, который генерирует файл в формате ANDF. Такой компилятор называется генератором
    ANDF (ANDF producer). Для каждой аппаратной платформы реализован
    инсталлятор ANDF (ANDF installer), который переводит программу из
    формата ANDF в формат исполняемых файлов.
    Технология ANDF имеет много общего с принципами распространения программного обеспечения, используемыми на платформе .NET.
    Программы для .NET также распространяются в независимом от аппаратной платформы виде. Более того, программа, устанавливаемая на компьютер пользователя, может быть тут же переведена в код для процессора,
    используемого в этом компьютере.

    4

    5

    1.1.3.1. Спецификация CLI
    Разработчику системного программного обеспечения важно понимать, что .NET – всего лишь одна из возможных реализаций так называемой общей инфраструктуры языков (Common Language Infrastructure, сокращенно CLI), спецификация которой разработана корпорацией
    Microsoft.
    Можно, руководствуясь этой спецификацией, разработать собственную реализацию CLI (рис. 1.3). В настоящее время ведутся по крайней мере два посвященных этому проекта. Это платформа Mono, создаваемая
    компанией Ximian, и разрабатываемый в рамках GNU проект Portable
    .NET. Кроме того, Microsoft распространяет в исходных текстах еще одну
    свою реализацию CLI, работающую как в Windows, так и под управлением

    Платформа .NET состоит из двух основных компонентов. Это
    Common Language Runtime и .NET Framework Class Library.
    Common Language Runtime (сокращенно CLR) можно назвать «двигателем» платформы .NET. Его задача – обеспечить выполнение приложений .NET, которые, как правило, закодированы на языке CIL, рассчитаны
    на автоматическое управление памятью и вообще требуют гораздо больше
    заботы, чем обычные приложения Windows. Поэтому CLR занимается управлением памятью, компиляцией и выполнением кода, работой с потоками управления, обеспечением безопасности и т.п.
    .NET Framework Class Library – это набор классов на все случаи жизни. Далее мы рассмотрим эту библиотеку подробнее, а сейчас остановимся на двух ключевых моментах, которые с ней связаны. Во-первых, на
    платформе .NET реализованы компиляторы для различных языков программирования, и большинство этих языков позволяют легко использовать одну и ту же библиотеку классов. То есть .NET Framework Class Library
    – это единая библиотека для всех языков платформы .NET. Во-вторых, использование этой библиотеки позволяет существенно сократить размер
    приложений, что способствует их распространению через Internet.

    1.1.3. Обзор архитектуры .NET

    сетям. Затем произошло стремительное развитие Internet-технологий, которое способствовало широкому распространению Java. В настоящее время Java является основным конкурентом платформы .NET.
    Краеугольным камнем платформы Java является виртуальная машина, которая отвечает за независимость Java-программ от операционных
    систем и аппаратных платформ. Набор инструкций этой виртуальной машины (так называемый Java byte-code) может выполняться как на специализированных Java-процессорах, так и путем компиляции в исполняемый
    код конкретной аппаратной платформы.

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    L

    Linux
    F

    FreeBSD

    Рис. 1.3. Существующие реализации CLI и поддерживаемые ими
    операционные системы

    Windows

    F

    Итак, чтобы понять, как работает .NET, необходимо изучить спецификацию CLI. Этим мы и займемся в ближайшее время, а пока перечислим ее составные части:
    • Общая система типов (Common Type System, сокращенно CTS)
    – охватывает большую часть типов, встречающихся в распространенных языках программирования.
    • Виртуальная система исполнения (Virtual Execution System, сокращенно VES) – отвечает за загрузку и выполнение программ,
    написанных для CLI.
    • Система метаданных (Metadata System) – предназначена для
    описания типов, хранится в независимом от конкретного языка
    программирования виде, используется для передачи типовой
    информации между различными метаинструментами, а также
    между этими инструментами и VES.
    • Общий промежуточный язык (Common Intermediate Language,
    сокращенно CIL) – независимый от платформы объектно-ориентированный байт-код, выступающий в роли целевого языка
    для любого поддерживающего CLI компилятора.
    • Общая спецификация языков (Common Language Specification,
    сокращенно CLS) – соглашение между разработчиками языков
    программирования и разработчиками библиотек классов, в котором определено подмножество CTS и набор правил. Если разработчики языка реализуют хотя бы определенное в этом согла-

    W

    W

    L

    W

    F

    L

    Portable.NET

    Common
    Language
    Infrastructure

    Mono

    SSCLI (Rotor)

    W

    .NET
    Framework

    FreeBSD. Эта реализация называется Shared Source CLI (иногда можно услышать другое название – Rotor).

    6

    7

    1.1.3.3. Сборка мусора
    Одни из самых неприятных ошибок, которые портят жизнь программисту, это, безусловно, ошибки, связанные с управлением памятью. В таких языках, как C и C++, в которых управление памятью целиком возло-

    1.1.3.2. JIT-компиляция
    Программы для платформы .NET распространяются в виде так называемых сборок (assemblies). Каждая сборка представляет собой совокупность метаданных, описывающих типы, и CIL-кода.
    Ключевой особенностью выполнения программ в среде .NET является JIT-компиляция. Аббревиатура JIT расшифровывается как Just-InTime, и термин JIT-компиляция можно перевести как компиляция программ «на лету». JIT-компиляция заключается в том, что CIL-код, находящийся в запускаемой сборке, тут же компилируется в машинный код, на
    который затем передается управление.
    Такая схема выполнения программ в среднем является более эффективной, чем интерпретация инструкций CIL, так как потеря времени на
    предварительную компиляцию CIL-кода с лихвой компенсируется высокой скоростью работы откомпилированного кода.
    В .NET реализованы два JIT-компилятора: один компилирует сборку
    непосредственно перед ее выполнением, а другой позволяет откомпилировать ее заранее и поместить в так называемый кэш откомпилированных
    сборок. JIT-компилятор первого типа вызывается автоматически при запуске программы, а JIT-компилятор второго типа реализован в виде служебной программы ngen, которая входит в состав .NET Framework SDK.
    Программу ngen нельзя воспринимать как простой компилятор, позволяющий превратить сборку .NET в обычное приложение Windows. Дело
    в том, что откомпилированная сборка не может быть непосредственно запущена пользователем – загрузчик выдает сообщение об ошибке, гласящее, что запускаемая программа не является правильным приложением
    Windows. Откомпилированная сборка запускается системой только при
    вызове исходной сборки!

    шении подмножество CTS и при этом действуют в соответствии
    с указанными правилами, то пользователь языка получает возможность использовать любую соответствующую спецификации CLS библиотеку. То же самое верно и для разработчиков библиотек: если их библиотеки используют только определяемое в
    соглашении подмножество CTS и при этом написаны в соответствии с указанными правилами, то эти библиотеки можно использовать из любого соответствующего спецификации CLS
    языка.

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    жено на программиста, львиная доля времени, затрачиваемого на отладку
    программы, приходится на борьбу с подобными ошибками.
    Давайте перечислим типичные ошибки при управлении памятью
    (некоторые из них особенно усугубляются в том случае, если в программе
    существуют несколько указателей на один и тот же блок памяти):
    1. Преждевременное освобождение памяти (premature free).
    Эта беда случается, если мы пытаемся использовать объект, память для которого была уже освобождена. Указатели на такие
    объекты называются висящими (dangling pointers), а обращение
    по этим указателям дает непредсказуемый результат.
    2. Двойное освобождение (double free).
    Иногда бывает важно не перестараться и не освободить ненужный объект дважды.
    3. Утечки памяти (memory leaks).
    Когда мы постоянно выделяем новые блоки памяти, но забываем освобождать блоки, ставшие ненужными, память в конце
    концов заканчивается.
    4. Фрагментация адресного пространства (external fragmentation).
    При интенсивном выделении и освобождении памяти может
    возникнуть ситуация, когда непрерывный блок памяти определенного размера не может быть выделен, хотя суммарный объем
    свободной памяти вполне достаточен. Это происходит, если используемые блоки памяти чередуются со свободными блоками и
    размер любого из свободных блоков меньше, чем нам нужно.
    Проблема особенно критична в серверных приложениях, работающих в течение длительного времени.
    В программах, работающих в среде .NET, все вышеперечисленные
    ошибки никогда не возникают, потому что эти программы используют реализованное в CLR автоматическое управление памятью, а именно –
    сборщик мусора. Если не вдаваться в излишние на данном этапе изучения
    .NET подробности, можно сказать, что работа сборщика мусора заключается в освобождении памяти, занятой ненужными объектами. При этом
    сборщик мусора также умеет «двигать» объекты в памяти, тем самым устраняя фрагментацию адресного пространства.
    Все эти чудеса, которые творит сборщик мусора, возможны исключительно благодаря тому, что во время выполнения программы известны
    типы всех используемых в ней объектов. Другими словами, данные, с которыми работает программа, находятся под полным контролем среды выполнения и называются, соответственно, управляемыми данными (managed
    data).

    8

    9

    Абстрагировавшись от конкретных особенностей .NET, можно сказать, что основная цель системы типов заключается в предотвращении определенного класса ошибок в программе до ее выпонения.

    1.2. Общая система типов

    1.1.3.4. Верификация кода
    При разработке платформы .NET было уделено много внимания
    обеспечению безопасности выполняемого программного кода. С точки
    зрения обеспечения безопасности можно привести следующую классификацию CIL-кода:
    • Недопустимый код (illegal code).
    Это код, который не может быть обработан JIT-компилятором,
    то есть не может быть транслирован в машинный код.
    • Допустимый код (legal code).
    Это код, который может быть представлен в виде машинного
    кода. При этом он может содержать вредоносные фрагменты
    (например, вирусы) или ошибки, способные нарушить работу
    не только программы, но и среды выполнения и даже операционной системы.
    • Безопасный код (safe code).
    Безопасный код не содержит вредоносных фрагментов (в том
    числе ошибок) и не может повредить ни системе выполнения,
    ни операционной системе, ни другим выполняемым программам.
    • Верифицируемый код (verifiable code).
    Верифицируемый код – это код, безопасность которого может
    быть строго доказана алгоритмом верификации, встроенным в
    CLR.
    Весь код, который поступает в JIT-компилятор, автоматически подвергается верификации. Верификатор платформы .NET реализует достаточно простой линейный алгоритм проверки правильной работы программного кода с типами данных и для каждого метода, входящего в сборку .NET, способен дать ответ на вопрос, проходит код этого метода верификацию или нет. В зависимости от настроек безопасности .NET система
    выполнения может разрешить или не разрешить выполнять на машине
    код, отбракованный верификатором.
    Следует понимать, что верифицируемый код всегда является безопасным, а обратное в общем случае неверно. То есть, можно себе представить такой CIL-код, который определенно является безопасным, но в силу тех или иных причин не может пройти верификацию.

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    В качестве примеров перехватываемых ошибок можно привести деление на ноль и обращение к памяти по нулевому указателю. А неперехватываемые ошибки возникают, например, при передаче управления на неправильный адрес или при выходе за границы массива (если отсутствует
    динамическая проверка размера массива).
    Фрагмент программы, в котором не могут возникнуть неперехватываемые ошибки, называется безопасным (safe). Языки программирования,
    которые обеспечивают безопасность написанных на них программ, также
    называются безопасными (safe languages). Безопасность – одно из важнейших свойств языка. Она уменьшает время отладки благодаря отсутствию в
    программах неперехватываемых ошибок. Кроме того, она гарантирует целостность данных, что позволяет использовать автоматическое управление памятью (в частности, сборку мусора).
    Для любого языка программирования можно определить класс ошибок, называемых запрещенными (forbidden errors). В этот класс следует
    включить все неперехватываемые ошибки, а также некоторое подмножество перехватываемых ошибок. Говорят, что фрагмент программы имеет
    хорошее поведение (well behaved), если в нем не могут возникать запрещенные ошибки. Языки программирования, которые гарантируют хоро-

    Рис. 1.4. Классификация ошибок в программах

    Разрешенные
    перехватываемые
    ошибки

    Перехватываемые
    ошибки

    Запрещенные
    перехватываемые
    ошибки

    Неперехватываемые
    ошибки

    Ошибки

    Давайте кратко приведем классификацию ошибок и разберемся, с
    какими ошибками помогает справиться система типов. При этом мы не
    будем рассматривать ошибки в алгоритмах, а ограничимся ошибками,
    возникающими в результате неправильного кодирования алгоритмов.
    Все ошибки можно разделить на две категории: перехватываемые и
    неперехватываемые (рис. 1.4). При возникновении перехватываемой
    ошибки (trapped error) выполнение программы немедленно прекращается,
    а неперехватываемая ошибка (untrapped error) остается незамеченной и может проявиться через некоторое время в абсолютно неожиданном месте.

    10

    11

    Общая система типов достаточно обширна, так как при ее проектировании учитывалась необходимость поддержки различных языков программирования и, кроме того, уделялось большое внимание эффективности выполнения программ в среде .NET. Поэтому, для ясности изложения

    1.2.1. Ядро системы типов .NET

    шее поведение всех написанных на них программ, называются языками со
    строгой проверкой (strongly checked).
    Таким образом, для программы, написанной на языке со строгой
    проверкой, справедливы следующие утверждения:
    • неперехватываемые ошибки не могут возникнуть;
    • запрещенные перехватываемые ошибки также невозможны;
    • другие перехватываемые ошибки могут возникать, и борьба с
    ними остается в компетенции программиста.
    Существуют два пути для диагностики запрещенных ошибок: статическая проверка программы до ее выполнения (static checking) и динамическая проверка во время выполнения (dynamic checking). Статическая
    проверка характерна для языков, имеющих систему типов, а динамическая проверка – для так называемых бестиповых (typeless) языков, в которых либо вообще нет системы типов, либо существует только один универсальный тип данных. Динамическая проверка требует дополнительных
    ресурсов, поэтому статическая проверка является предпочтительной, так
    как чем больше ошибок диагностируется статически, тем выше эффективность программы.
    Система типов в языке программирования разрабатывается для того
    чтобы можно было осуществлять статическую проверку программы. Она
    представляет собой набор правил, определяющих условия, при которых
    конструкции языка не вызывают запрещенных ошибок.
    Так как платформа .NET спроектирована с учетом поддержки разных
    языков программирования, то ее общая система типов (Common Type
    System – CTS) является объединением систем типов основных распространенных в настоящее время языков. Из этого следует, что все языки платформы .NET (объектно-ориентированные, процедурные, функциональные) совместно используют единую систему типов, и это обеспечивает взаимодействие программных компонентов, написанных на разных языках.
    Наличие в .NET общей системы типов позволяет осуществлять статическую проверку программы не только на уровне компилятора, но и на
    уровне системы выполнения. Другими словами, система может проводить
    верификацию двоичных исполняемых файлов непосредственно перед их
    запуском. Это гарантирует безопасность кода, выполняемого в среде .NET
    и тем самым обеспечивает возможность автоматического управления памятью (сборку мусора).

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    Интерфейсы

    Массивы

    Классы

    Встроенные
    ссылочные типы

    Самоописывающие
    типы

    Ссылочные типы

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

    Рис. 1.5. Ядро общей системы типов

    Типы с плавающей
    запятой

    Целые типы

    Типы-значения

    Типы

    материала мы сначала рассмотрим основное подмножество общей системы типов. Будем называть это подмножество ядром системы типов.
    Схема ядра общей системы типов .NET приведена на рис. 1.5. На схеме все типы делятся на две категории: это типы-значения (value types) и
    ссылочные типы (reference types). Для того чтобы понять причину такого
    разделения, приведем следующую аналогию. В некоторых языках программирования используются два способа передачи параметров при вызове функции: передача параметра по значению и передача параметров по
    ссылке. Передача параметра по значению (by value) подразумевает копирование значения параметра, а при передаче параметра по ссылке (by reference) копирования не происходит (вместо этого вызываемая функция
    получает адрес параметра). Обобщив эту аналогию, мы получим основное
    отличие типов-значений от ссылочных типов, а именно: использование
    типов-значений всегда связано с копированием их значений, а работа со
    ссылочными типами всегда осуществляется через адреса их значений.

    12

    13

    native unsigned int

    bool
    char
    int8
    int16
    int32
    int64
    unsigned int8
    unsigned int16
    unsigned int32
    unsigned int64
    native int

    Тип

    System.UIntPtr

    Имя в .NET
    Framework
    Class Library
    System.Boolean
    System.Char
    System.SByte
    System.Int16
    System.Int32
    System.Int64
    System.Byte
    System.Uint16
    System.Uint32
    System.UInt64
    System.IntPtr

    Таблица 1.1. Целые типы

    булевский (8 бит)
    символ Unicode (16 бит)
    целое со знаком (8 бит)
    целое со знаком (16 бит)
    целое со знаком (32 бит)
    целое со знаком (64 бит)
    целое без знака (8 бит)
    целое без знака (16 бит)
    целое без знака (32 бит)
    целое без знака (64 бит)
    целое со знаком
    (разрядность процессора)
    целое без знака
    (разрядность процессора)

    Описание

    1.2.1.1. Встроенные типы-значения
    Встроенные типы-значения делятся на две группы: целые типы и типы с плавающей запятой. Они перечислены в таблицах 1.1 и 1.2, соответственно. В первом столбце каждой из таблиц приведены имена типов, используемые в текстовом представлении CIL (в программах, компилируемых ассемблером ILASM). Во втором столбце перечислены имена, используемые для тех же самых типов в библиотеке классов .NET. В третьем
    столбце находится краткое описание, в котором указывается знаковость и
    разрядность типа.

    Ссылочные типы описывают так называемые объектные ссылки
    (object references), которые представляют собой адреса объектов.
    Значения любого типа хранятся в ячейках (location). В качестве ячеек могут выступать локальные и глобальные переменные, параметры методов, поля объектов и элементы массивов. Для каждой ячейки известен
    тип значений, которые она может содержать.
    Особо важным является то обстоятельство, что ячейки не могут содержать объекты. Все объекты размещаются в специальной области памяти, называемой кучей (heap). Таким образом, в ячейках могут храниться
    только значения типов-значений или объектные ссылки.

    Введение в архитектуру Microsoft .NET Framework

    вещественное (32 бит)
    вещественное (64 бит)

    1.2.1.2. Самоописывающие ссылочные типы
    В некоторых объектно-ориентированных языках программирования
    (например, в C++) объекты могут храниться как в куче (в динамической
    памяти), так и в переменных: глобальных (в статической памяти) и локальных (на стеке). Поэтому системы типов в таких языках содержат отдельные типы для самого объекта и для объектной ссылки (указателя на
    объект).
    В среде .NET объекты и объектные ссылки хранятся раздельно, а
    именно: объекты хранятся в куче, а ссылки – в ячейках. Поэтому общая система типов спроектирована таким образом, что один и тот же ссылочный
    тип может являться как типом объекта, так и типом объектной ссылки.
    Каждый объект в куче содержит информацию о своем типе. Поэтому
    ссылочные типы, представляющие объекты, называются самоописывающими (self-describing).
    Два самоописывающих типа являются встроенными – это
    System.Object (или просто object в текстовом представлении CIL) и
    System.String (или string). Тип System.Object является общим базовым
    классом, от которого непосредственно или транзитивно наследует любой
    другой класс. Тип System.String используется для представления строковых данных в формате Unicode.
    Основу самоописывающих типов составляют классы. Классы могут
    агрегировать значения других типов, а также наследоваться друг от друга
    (в .NET поддерживается только одиночное наследование). Классы могут
    содержать следующие элементы:
    • Поля (fields).
    Поля являются ячейками, в которых хранятся значения других
    типов.

    Для встроенных типов-значений определены правила преобразования значений одного типа в другой тип. Такие преобразования бывают сужающие (narrowing) и расширяющие (widening). При сужающих преобразованиях значение с большей разрядностью переводится в значение с
    меньшей разрядностью, что может приводить к потере значащих битов.
    Расширяющие преобразования никогда не приводят к такой потере.

    Имя в .NET
    Framework
    Class Library
    System.Single
    System.Double

    Описание

    CIL и системное программирование в Microsoft .NET

    Таблица 1.2. Типы с плавающей запятой

    float32
    float64

    Тип

    14

    15

    Массивы бывают как одномерными (в этом случае они обрабатываются особенно эффективно благодаря использованию специальных инструкций CIL), так и многомерными. Кроме того, система поддерживает
    массивы, нижняя граница которых отлична от нуля.

    Рис. 1.6. Особенности представления массивов

    Массив типа-значения

    Массив ссылочного типа

    • Методы (methods).
    Методы представляют собой функции классов. Они бывают статическими (static method) и объектными (instance method). Вызываемый объектный метод всегда получает ссылку на объект,
    для которого он вызывается. Объектные методы делятся на виртуальные и невиртуальные.
    • Свойства (properties).
    Свойство представляет собой пару методов, один из которых
    возвращает некоторое значение, а другой устанавливает это значение.
    • События (events).
    События используются для асинхронного внесения изменений
    в объект.
    Типы-массивы также относятся к самоописывающим типам, то есть
    каждый массив представляет собой объект в куче, доступ к которому осуществляется через объектную ссылку. Хотя, строго говоря, типы-массивы
    не являются классами, считается, что все они наследуют от библиотечного класса System.Array. Типы-массивы интересны тем, что, в отличие от
    классов, определяемых программистом самостоятельно, они формируются системой автоматически. То есть если мы имеем некоторый тип X, то
    тип массива, состоящего из элементов типа X, нам уже объявлять не нужно – об этом позаботится система выполнения.

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    1.2.1.5. Идентичность и равенство значений
    Для объектных ссылок и значений типов-значений вводятся отношения идентичности (identity) и равенства (equality). Эти отношения являют-

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

    1.2.1.3. Типы-интерфейсы
    Интерфейсы служат для компенсации отсутствия в .NET множественного наследования. Они могут рассматриваться как чисто абстрактные
    классы, содержащие только перечисленные ниже элементы:
    • Абстрактные методы.
    • Статические методы.
    • Статические поля.
    • Абстрактные свойства.
    • Абстрактные события.
    Хотя любой класс может наследоваться только от одного базового
    класса, он может реализовывать произвольное количество интерфейсов.
    То есть, интерфейс определяет контракт, которому должен удовлетворять
    любой класс, реализующий этот интерфейс.
    Нужно отметить, что интерфейс может содержать реализации статических методов, но все остальные методы, включая методы свойств и событий, должны оставаться абстрактными.

    Особого внимания заслуживают особенности представления массивов объектов и массивов типов-значений. Дело в том, что так как объекты
    не могут храниться в ячейках, мы вынуждены вместо массивов объектов
    использовать массивы объектных ссылок (см. рис. 1.6), в то время как значения типов-значений хранятся прямо в элементах массива.

    16

    17

    A = B= C

    A идентична B
    A не идентична C
    B не идентична C

    Рис. 1.7. Пример, объясняющий отношения идентичности и
    равенства объектных ссылок

    “Hello”

    “Hello”

    Из соображений эффективности выполнения программ разработчики платформы .NET добавили в общую систему типов дополнительные
    элементы, а именно: пользовательские типы-значения (структуры и пере-

    1.2.2. Дополнительные элементы системы типов .NET

    Отношение идентичности для объектных ссылок вводится следующим образом: две объектных ссылки идентичны тогда и только тогда, когда они содержат адреса одного и того же объекта. На рис. 1.7 изображены
    три объектных ссылки A, B и C, а также два равных объекта-строки. Так
    как ссылки A и B содержат адрес одного и того же объекта, то они идентичны между собой, но при этом они не идентичны ссылке C, содержащей
    адрес другого объекта.
    Отношение равенства для объектных ссылок формулируется так: две
    объектных ссылки равны тогда и только тогда, когда они содержат адреса
    равных объектов. Все три объектные ссылки, изображенные на рис. 1.7,
    равны между собой.
    Отношение идентичности для типов-значений определяется следующим образом: два значения идентичны тогда и только тогда, когда они
    принадлежат одному и тому же типу-значению, и представляющие их последовательности битов равны.
    Отношение равенства для примитивных типов-значений совпадает с
    отношением идентичности.
    Отношения идентичности и равенства играют большую роль при
    программировании в среде .NET. Любой объект имеет виртуальный метод
    Equals, унаследованный от System.Object и выполняющий сравнение объектов на равенство. В зависимости от реализации этого метода, отношение равенства может существенно меняться. При переопределении метода Equals нужно иметь в виду, что два идентичных объекта обязательно
    должны быть равны.

    C

    B

    A

    ся отношениями эквивалентности, то есть они рефлексивны, симметричны и транзитивны.

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    Классы

    Массивы

    Упакованные
    типы-значения

    Типы с плавающей запятой

    Структуры

    Перечисления

    Неправляемые
    указатели

    Управляемые
    указатели

    Указатели

    Интерфейсы

    1.2.2.1. Структуры и перечисления
    Как показал опыт платформы Java, которая была разработана задолго до платформы .NET, одной из основных причин ухудшения производительности Java-программ является медленная работа сборщика мусора,
    вызванная большим количеством мелких объектов в куче. Это явление
    можно наблюдать в двух случаях:
    1. Интенсивное создание временных объектов с очень малым временем жизни. Зачастую такие объекты создаются и используются в теле одного метода.
    2. Использование гигантских массивов объектов, при котором
    возникает ситуация, изображенная на рис. 1.6, а именно: в массиве хранятся ссылки на огромное количество небольших объектов.
    Разработчиками .NET был подмечен тот факт, что использование типов-значений вместо объектов позволяет избежать описанных выше проблем, потому что:

    Рис. 1.8. Общая система типов

    Встроенные
    ссылочные типы

    Самоописывающие типы

    Ссылочные
    типы

    Целые типы

    Встроенные
    типы-значения

    Типы-значения

    Типы

    числения) и указатели. Схема общей системы типов, на которой отражены
    эти дополнительные элементы, приведена на рис. 1.8.

    18

    19

    1.2.2.2. Указатели
    Использование указателей может значительно увеличить производительность, и в некоторых языках программирования указатели применяются исключительно часто (например, в C). Однако считается, что применение указателей чревато появлением в программах большого количества
    трудноуловимых неперехватываемых ошибок. Поэтому, например, система типов уже упоминавшейся платформы Java обходится без указателей.
    Тем не менее, разработчикам .NET удалось добавить указатели в общую систему типов. При этом появилось две категории указателей: управляемые указатели (managed pointers) и неуправляемые указатели (unmanaged pointers).
    Для того чтобы программы оставались безопасными, на использование управляемых указателей наложен целый ряд ограничений:
    1. Управляемые указатели могут содержать только адреса ячеек, то
    есть они могут указывать исключительно только на глобальные
    и локальные переменные, параметры методов, поля объектов и
    ячейки массивов. Для полноты картины следует заметить, что

    1. временные значения хранятся не в куче, а непосредственно в
    локальных переменных метода;
    2. в массивах типов-значений содержатся не ссылки на значения,
    а непосредственно сами значения.
    Поэтому в общую систему типов были добавлены так называемые
    пользовательские типы-значения. Эти типы могут быть объявлены программистом, но, как и встроенные типы-значения, размещаются не в куче, а в ячейках.
    Пользовательские типы-значения делятся на структуры и перечисления.
    Структуры являются аналогом классов. Они, как и классы, могут содержать поля, методы, свойства и события. Все структуры неявно наследуют от библиотечного класса System.ValueType, и, более того, встроенные
    типы-значения также наследуют от этого класса. Тут сразу следует заметить, что система типов не предусматривает никакого наследования структур, кроме данного неявного. Другими словами, структуры не могут наследоваться друг от друга и, тем более, не могут наследоваться от классов
    (кроме System.ValueType).
    Перечисления представляют собой структуры с одним целочисленным полем Value. Кроме того, перечисления содержат набор констант, определяющих возможные значения поля Value. При этом для каждой константы в перечислении хранится ее имя. Перечисления неявно наследуют
    от библиотечного класса System.Enum, который, в свою очередь, является
    наследником все того же класса System.ValueType.

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    1.2.2.3. Упакованные типы-значения
    Наличие в общей системе типов структур, которые во многом напоминают классы, но в действительности классами не являются, в некоторых случаях вызывает некоторые неудобства. Например, в библиотеке
    классов .NET существуют достаточно удобные контейнерные классы
    (наиболее часто используется класс ArrayList, представляющий массив с
    динамически меняющимся размером). Эти классы могут хранить ссылки
    на любые объекты, но не могут работать с типами-значениями.
    Для решения этой проблемы в общей системе типов предусмотрены
    так называемые упакованные типы-значения. Эти типы являются ссылочными и самоописывающими. Объекты этих типов предназначены для хранения значений типов-значений.
    Упакованные типы-значения не могут быть объявлены программистом. Система автоматически определяет такой тип для любого типа-значения.
    Получение объекта упакованного типа-значения осуществляется путем упаковки (boxing). Упаковка заключается в том, что в куче создается
    пустой объект нужного размера, а затем значение копируется внутрь этого
    объекта.
    С помощью упаковки мы можем превратить значение любого типазначения (встроенного примитивного типа, структуры, перечисления) в
    объект и в дальнейшем работать с этим значением как с настоящим объектом (в том числе, мы можем положить его в ArrayList).
    Если же нам требуется произвести обратное действие, мы можем осуществить распаковку (unboxing). Распаковка заключается в том, что мы
    получаем управляемый указатель на содержимое объекта упакованного
    типа-значения.

    управляемые указатели могут содержать адрес, непосредственно
    следующий за последним элементом массива.
    2. За каждым указателем закреплен тип ячейки, на которую он может указывать. Другими словами, void-указатели запрещены.
    3. Указатели могут храниться только в локальных переменных и
    параметрах методов.
    4. Запрещены указатели на указатели.
    На использование неуправляемых указателей никаких ограничений
    не накладывается, то есть они могут содержать абсолютно любой адрес.
    Программа, в которой используются неуправляемые указатели, автоматически считается небезопасной и не может пройти верификацию.

    20

    21

    Изучение работы виртуальной машины CLI заключается в том, чтобы понять, что представляет собой состояние виртуальной машины и как
    это состояние меняется во времени. В этом разделе мы не будем затрагивать вопрос изменения состояния, так как оно связано с выполнением инструкций языка CIL, разговор о котором мы отложим до третьей главы нашего учебника.
    На рис. 1.9 показана схема состояния виртуальной машины, из которой видно, что виртуальная машина может выполнять сразу несколько
    нитей (threads). Как уже говорилось ранее, виртуальная машина является
    всего лишь моделью поведения конкретных реализаций CLI, поэтому мы
    будем предполагать, что все нити выполняются параллельно. На самом деле, нити могут работать как параллельно, так и в режиме вытесняющей
    многозадачности, могут отображаться на процессы или на нити операци-

    1.3.1. Состояние виртуальной машины

    Виртуальная система выполнения (Virtual Execution System – VES)
    представляет собой абстрактную виртуальную машину, способную выполнять управляемый код. Можно сказать, что виртуальная система выполнения существует только «на бумаге», потому что ни одна из реализаций CLI
    не содержит интерпретатора CIL-кода (вместо этого используется JITкомпилятор, транслирующий инструкции CIL в команды процессора).
    Другими словами, виртуальная система выполнения не зря называется
    виртуальной (то есть мнимой), ее предназначение – служить образцом,
    которому должна соответствовать любая реализация CLI. Какую бы технологию ни использовала эта реализация для выполнения программ, эта
    технология должна работать так же, как работала бы виртуальная система
    выполнения.
    Если сравнить CLI с ее ближайшим конкурентом – платформой Java,
    можно прийти к выводу, что VES является значительно более абстрактной
    моделью, чем виртуальная машина Java (Java Virtual Machine – JVM). Причина такого отличия кроется в том, что изначально Java была ориентирована на реализацию в бытовых приборах. При этом, естественно, подразумевалось, что байт-код Java будет непосредственно выполняться специальными процессорами, и поэтому JVM является фактически спецификацией такого процессора. Аппаратная реализация VES никогда даже не
    предполагалась, и это позволило избежать при составлении ее спецификации ненужных деталей, дав тем самым каждой реализации CLI большую
    свободу выбора наиболее оптимальной стратегии выполнения CIL-кода.

    1.3. Виртуальная система выполнения

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    Состояние
    метода

    Состояние
    метода

    Состояние
    метода

    Состояние
    метода

    Нить

    Куча

    Состояние кучи определяется состояниями содержащихся в ней объектов. Спецификация VES содержит упоминание о том, что допустимо существование сразу нескольких куч (например, реализация CLI может ран-

    Рис. 1.9. Состояние виртуальной машины

    Состояние
    метода

    Состояние
    метода

    Нить

    Состояние
    метода

    Нить

    Среда выполнения

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

    22

    23

    Описатель безопасности

    Стек вычислений

    Рис. 1.10. Состояние метода

    Область локальных данных

    Параметры

    Состояние возврата

    Описатель метода

    Локальные переменные

    Неизменяемые данные

    Изменяемые данные
    Указатель инструкции

    Состояние метода

    На рис. 1.10 изображена схема состояния метода. Элементы состояния метода можно условно разделить на две группы: изменяемые данные
    и неизменяемые данные. Изменяемые данные доступны из тела метода
    для чтения и записи, в то время как неизменяемые данные либо доступны
    только для чтения либо вообще предназначены для внутреннего использования в системе выполнения.

    1.3.2. Состояние метода

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

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    Давайте перечислим элементы состояния метода, входящие в группу
    изменяемых данных:
    • Указатель инструкции (Instruction Pointer).
    Содержит адрес следующей инструкции в теле метода, которая
    будет выполнена системой выполнения. (Когда мы говорим, что
    указатель инструкции относится к изменяемым данным, мы
    имеем в виду, что его значение изменяется при переходе от инструкции к инструкции.)
    • Стек вычислений (Evaluation Stack).
    Виртуальная система выполнения работает по принципу стекового процессора. Это означает, что операнды инструкций, а также возвращаемые инструкциями значения хранятся в специальной области памяти, а именно на стеке вычислений.
    Каждое состояние метода имеет собственный стек вычислений,
    содержимое которого сохраняется при вызове методов (то есть,
    если наш метод вызывает другой метод, то по завершении работы вызванного метода содержимое стека никуда не денется).
    • Локальные переменные (Local Variable Array).
    Для хранения локальных переменных в состоянии метода предусмотрена отдельная область памяти, состоящая из так называемых слотов (slots). Каждой локальной переменной соответствует свой слот. Значения локальных переменных сохраняются при
    вызове методов аналогично содержимому стека вычислений.
    • Параметры (Argument Array).
    Фактические параметры, переданные методу, записываются в
    специальную область памяти, которая организована так же, как
    и область локальных переменных.
    • Область локальных данных (Local Memory Pool).
    В языке CIL предусмотрена инструкция localloc, которая позволяет динамически размещать объекты в области памяти, локальной для метода. Объекты в этой области живут до тех пор
    пока метод не завершится.
    Обратите внимание, что стек вычислений, локальные переменные и
    параметры, а также локальные данные метода представляют собой логически отдельные области памяти. Каждая конкретная реализация CLI самостоятельно решает вопрос, где размещать эти области.
    В группу неизменяемых данных входят следующие элементы состояния метода:
    • Описатель метода (methodInfo handle).
    Содержит сигнатуру метода, в которую входят количество и типы формальных параметров, а также тип возвращаемого значения. Кроме этого, описатель метода включает в себя информа-

    24

    25

    1.3.2.1. Стек вычислений
    Итак, несмотря на то, что большинство современных процессоров
    для организации вычислений используют регистры, в виртуальной системе выполнения вместо регистров применяется стек вычислений. Это связано, скорее всего, с тем, что стековые вычисления достаточно легко можно отобразить на регистры процессора, так как модель, использующая
    стек, более абстрактна, чем регистровая модель.
    Стек вычислений в VES состоит из слотов. При этом глубина стека
    (максимальное количество слотов) всегда ограничена и задается статически в заголовке метода. Решение ограничить глубину стека было принято
    разработчиками спецификации CLI для того, чтобы облегчить создание
    JIT-компиляторов.
    На входе метода стек вычислений всегда пуст. Затем он используется
    для передачи операндов инструкциям CIL, для передачи фактических параметров вызываемым методам, а также для получения результатов выполнения инструкций и вызываемых методов. Если метод возвращает какое-то
    значение, то оно кладется на стек вычислений перед завершением метода.
    Важной особенностью организации стека вычислений является то
    обстоятельство, что его слоты не адресуются, то есть мы не можем получить указатель на какой-либо слот и записать в него значение.
    Каждый слот стека вычислений может содержать ровно одно значение одного из следующих типов:
    • int64 – 8-байтовое целое со знаком;
    • int32 – 4-байтовое целое со знаком;
    • native int – знаковое целое, разрядность которого зависит от
    аппаратной платформы (может быть 4 или 8 байт);
    • F – число с плавающей точкой, разрядность которого зависит от
    аппаратной платформы (не может быть меньше 8 байт);

    цию о количестве и типах локальных переменных и об обработчиках исключений.
    Описатель метода доступен из кода метода, но в основном он
    используется системой выполнения при сборке мусора и обработке исключений.
    • Описатель безопасности (Security Descriptor).
    Используется системой безопасности CLI и недоступен из кода
    метода.
    • Состояние возврата (Return State Handle).
    Служит для организации списка состояний методов внутри системы выполнения и недоступно из кода метода. Фактически
    представляет собой указатель на состояние метода, из тела которого был вызван текущий метод.

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    • & – управляемый указатель;
    • O – объектная ссылка;
    • Пользовательский тип-значение.
    Таким образом, слоты стека вычислений могут иметь различный размер в зависимости от типов записанных в них значений.
    Также мы можем видеть, что допустимые типы значений для стека вычислений не совпадают с общей системой типов CTS. Например, в CTS существуют целые типы разрядности 1 и 2 байта, которые не могут содержаться на стеке вычислений. И наоборот, тип F стека вычислений не имеет аналога в CTS. Кроме того, для стека вычислений все управляемые указатели и
    объектные ссылки отображаются в два типа: & и O соответственно.
    Давайте обсудим, как в VES осуществляется работа с типами данных,
    не поддерживаемыми напрямую стеком вычислений.
    Во-первых, короткие целые типы (bool, char, int8, int16, unsigned
    int8, unsigned int16) при загрузке на стек вычислений расширяются до
    int32. При этом знаковые короткие целые типы (int8, int16) расширяются с сохранением знака, а беззнаковые расширяются путем добавления нулевых битов. При сохранении значения со стека вычислений в переменной, параметре, поле объекта или элементе массива происходит обратное
    сужающее преобразование.
    Во-вторых, беззнаковый тип unsigned int32 при загрузке на стек вычислений становится знаковым int32, и аналогично, беззнаковый
    unsigned int64 становится знаковым int64. При этом, естественно, никаких преобразований не происходит – просто последовательность бит, которая раньше считалась беззнаковым целым, копируется на стек вычислений. Вообще говоря, утверждение, что целые типы int32, int64 и native
    int на стеке вычислений имеют знак, достаточно спорно. Правильнее было бы сказать, что они могут представлять как знаковые, так и беззнаковые целые числа в зависимости от того, какие инструкции CIL используются для их обработки.
    В-третьих, типы float32 и float64 при копировании на стек вычислений преобразуются к типу F. Разрядность этого типа определяется конкретной реализацией CLI, которая, однако, должна гарантировать, что
    точность типа F не ниже, чем точность типа float64.
    В-четвертых, типы-перечисления при копировании на стек вычислений автоматически превращаются в целые типы. Вообще, VES устроена
    таким образом, что типы-перечисления и целые типы являются совместимыми по присваиванию. Этим они отличаются от обычных типов-значений, которые при копировании на стек сохраняют свой тип и не совместимы с целыми типами.
    И, наконец, для VES не имеет значения, какой точный тип имеют управляемые указатели и объектные ссылки. Любой управляемый указатель

    26

    27

    1.3.2.3. Область локальных данных
    Область локальных данных является составной частью состояния метода и используется для размещения объектов, тип и/или размер которых
    неизвестен на этапе компиляции, но которые по тем или иным причинам
    нежелательно размещать в куче.
    Память из области локальных данных может быть явно выделена с
    помощью инструкции localloc. Так как в языке CIL не существует инструкции, освобождающей память в области локальных данных, то эту область невозможно использовать для реализации менеджера памяти общего назначения.

    1.3.2.2. Локальные переменные и параметры
    Для хранения локальных переменных и параметров метода используются два массива, которые, как и стек вычислений, состоят из слотов. При
    этом каждой переменной и каждому параметру соответствует ровно один
    слот.
    Для доступа к локальным переменным и параметрам используются
    их индексы в массивах переменных и параметров. При этом нумерация
    осуществляется с нуля.
    Если один и тот же слот стека вычислений в разные моменты времени может содержать данные разных типов, то слоты, используемые для
    хранения переменных и параметров, строго типизированы, но зато поддерживают все типы, определенные в спецификации CTS. Типы локальных переменных и параметров задаются в заголовке метода и доступны во
    время выполнения программы через описатель метода. Специальный
    флаг, также находящийся в заголовке метода, показывает, нужно ли обнулять локальные переменные при входе в метод.
    Слоты, из которых состоят массивы переменных и параметров, адресуемы. Это означает, что в языке CIL существуют специальные инструкции, позволяющие получить адрес локальной переменной или параметра
    метода в виде управляемого указателя.
    Компилятор, генерирующий CIL-код, не должен делать никаких
    предположений о том, как переменные и параметры размещены в памяти.
    Дело в том, что реализации CLI могут любым образом переупорядочивать
    переменные и параметры, могут произвольно выравнивать их в памяти и
    даже использовать для их хранения регистры процессора.

    считается имеющим тип &, а любая объектная ссылка представляется типом O. Это означает, что согласно спецификации CLI система выполнения
    не обязана отслеживать правильность типов управляемых указателей и
    объектных ссылок. Действительно, контроль за правильностью типов находится в компетенции верификатора.

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    Под управляемую кучу резервируется непрерывная область адресного пространства процесса. Система выполнения поддерживает специальный указатель (назовем его HeapPtr), содержащий адрес, по которому будет выделена память для следующего объекта. Когда куча не содержит ни
    одного объекта, HeapPtr указывает на начало кучи. Выделение памяти для
    объекта заключается в увеличении HeapPtr на количество байт, занимаемое этим объектом в куче.
    Для некоторых объектов определены методы Finalize, выполняющие некие действия при удалении объекта из кучи. Эти методы являются
    аналогами деструкторов языка C++ и используются главным образом для
    освобождения системных ресурсов, связанных с объектами. В целях повышения эффективности сборщика мусора при выделении памяти для объекта, имеющего метод Finalize, адрес этого объекта заносится в список завершения (finalization list).
    Если сравнить механизм выделения памяти в управляемой куче .NET
    с работой функции malloc языка C, можно прийти к выводу, что функция
    malloc работает гораздо менее эффективно. Причина в том, что исполняющая среда языка C организует кучу в виде связного списка блоков памя-

    1.4.1. Выделение памяти в управляемой куче

    Одной из основных особенностей платформы .NET, делающих ее
    привлекательной для разработки приложений, является механизм автоматического управления памятью, известный как сборка мусора (garbage collection).
    Спецификация CLI утверждает, что память для объектов, используемых в программе, выделяется в управляемой куче (managed heap), которая
    периодически очищается от ненужных объектов сборщиком мусора.
    Принцип работы сборщика мусора в спецификации не определен, поэтому разработчики реализаций CLI могут использовать любые алгоритмы,
    корректно выполняющие очистку управляемой кучи.
    В .NET реализован так называемый сборщик мусора с поколениями
    (generational garbage collector), работающий на основе построения графа
    достижимости объектов.

    1.4. Автоматическое управление памятью

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

    28

    29

    Перед тем как приступить к описанию алгоритма сборки мусора в
    .NET, необходимо сделать важное замечание, касающееся уровня абстракции, на котором мы будем рассматривать этот вопрос. Дело в том, что нам
    придется отрешиться от понятий, с которыми оперирует спецификация
    CLI, потому что сборщик мусора относится не к спецификации, а к конкретной реализации. Другими словами, мы не можем обсуждать сборщик
    мусора в терминах виртуальной системы выполнения. Вместо этого нам
    придется перейти на уровень конкретной системы выполнения, имеющей
    следующие особенности:
    • она исполняет не CIL-код, а порожденный JIT-компилятором
    код процессора семейства Intel x86;
    • для каждого потока выполнения существует стек, в котором расположены фреймы вызванных методов. Каждый фрейм содержит адрес возврата, адрес фрейма предыдущего метода в стеке, а
    также локальные переменные и параметры метода;
    • стеки вычислений в явном виде отсутствуют. Вместо них используются регистры процессора и стек потока;
    • объектные ссылки представляют собой обычные указатели на
    объекты в управляемой куче.
    Ключевую роль в работе сборщика мусора играет понятие корень
    (root). Корнем считается указатель на объект кучи, расположенный вне
    кучи. Таким образом, корнями являются глобальные переменные, статические поля классов, локальные переменные и параметры методов, а также регистры процессора, содержащие указатели на объекты кучи.
    Работа сборщика мусора основана на предположении, что объекты,
    непосредственно или транзитивно достижимые из корней, нужно сохранить в куче, так как они могут использоваться в программе. Все остальные
    объекты можно удалить.
    Запуск сборщика мусора осуществляется в тот момент, когда совокупный размер объектов в куче достигает некоторой границы. При этом
    все потоки, запущенные приложением, приостанавливаются до завершения сборки мусора.
    Для каждой точки в коде программы сборщик мусора может эффективно определить набор корней благодаря специальной таблице корней.
    Эта таблица строится JIT-компилятором, и в ней каждому адресу в коде ка-

    1.4.2. Алгоритм сборки мусора

    ти. При этом размеры блоков в общем случае различны. Функции malloc
    приходится выполнять поиск свободного блока нужного размера, разбивать этот блок и затем вносить необходимые изменения в список блоков.
    Ясно, что выполнение этих действий требует значительно больше времени, чем простое увеличение указателя HeapPtr.

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    Проведение сборки мусора только для части объектов кучи позволяет существенно сократить время работы сборщика. Поэтому все объекты
    делятся на три категории, называемые поколениями. В поколении 0 сборка

    1.4.3. Основные приемы повышения
    эффективности сборки мусора

    ждого метода сопоставляется набор тех регистров процессора, локальных
    переменных и параметров этого метода, которые являются корнями. При
    этом локальные переменные и параметры задаются своими смещениями
    относительно фрейма. Таким образом, для определения полного набора
    корней сборщику мусора достаточно выполнить следующие операции:
    1. взять адреса всех глобальных переменных и статических полей
    классов, имеющих ссылочные типы;
    2. определить точки выполнения для каждого потока программы и
    добавить корни, которые в таблице корней соответствуют адресам этих точек;
    3. просканировать стек каждого потока, обращаясь к таблице корней в точках вызова методов и добавляя полученные из таблицы
    корни.
    После вычисления полного набора корней сборщик мусора строит
    граф достижимости объектов кучи. Входами в граф служат объекты, на которые указывают корни. Поля этих объектов, имеющие ссылочные типы,
    сканируются, и в граф добавляются объекты, на которые эти поля указывают. Затем сканируются поля этих добавленных объектов. Процесс построения графа продолжается до тех пор, пока в него не войдут все объекты, достижимые из корней. При этом информацию о полях объектов
    сборщик мусора берет из метаданных, и ни один объект не рассматривается дважды.
    Среди объектов, не попавших в граф достижимости, сборщик мусора
    ищет такие объекты, адреса которых записаны в список завершения. Эти
    адреса добавляются в очередь завершения, а сами объекты считаются достижимыми и не подлежащими удалению. Методы Finalize объектов, попавших в очередь завершения, выполняются затем в отдельном потоке.
    Нетрудно сообразить, что эти объекты могут быть удалены только при следующей сборке мусора, и, более того, только при условии, что их методы
    Finalize успели выполниться и не привели к тому, чтобы объекты стали
    достижимыми из корней.
    После того как определены все достижимые объекты, сборщик мусора выполняет дефрагментацию кучи. Дефрагментация заключается в
    сдвиге достижимых объектов к началу кучи на место удаляемых недостижимых объектов. При этом сборщику мусора приходится корректировать
    поля объектов и корни.

    30

    31

    мусора проводится чаще всего. Объекты, пережившие сборку мусора в поколении 0, переводятся в поколение 1, в котором сборка мусора осуществляется реже. Объекты, не удаленные после сборки мусора в поколении 1,
    переводятся в поколение 2. Сборка мусора в поколении 2 выполняется совсем редко.
    Эффективность организации сборки мусора с поколениями обосновывается тем, что молодые объекты имеют меньшее время жизни. Это утверждение получено эмпирическим путем и справедливо для подавляющего большинства реальных приложений.
    Еще одним способом увеличения производительности сборщика мусора является выделение отдельной кучи для больших объектов. Большими считаются объекты, размер которых превышает 85000 байт. Куча больших объектов никогда не дефрагментируется, и все объекты в ней считаются принадлежащими поколению 2.

    Введение в архитектуру Microsoft .NET Framework

    CIL и системное программирование в Microsoft .NET

    Исполняемый файл (executable file) – это файл, который может быть
    загружен в память загрузчиком операционной системы и затем исполнен.
    В операционной системе Windows исполняемые файлы, как правило, имеют расширения «.exe» и «.dll». Расширение «.exe» имеют программы, которые могут быть непосредственно запущены пользователем. Расширение
    «.dll» имеют так называемые динамически связываемые библиотеки
    (dynamic link libraries). Эти библиотеки экспортируют функции, используемые другими программами.
    Для того чтобы загрузчик операционной системы мог правильно загрузить исполняемый файл в память, содержимое этого файла должно соответствовать принятому в данной операционной системе формату исполняемых файлов. В разных операционных системах в разное время существовало и до сих пор существует множество различных форматов. В этой
    главе мы рассмотрим формат Portable Executable (PE). Формат PE – это основной формат для хранения исполняемых файлов в операционной системе Windows. Сборки .NET тоже хранятся в этом формате.
    Кроме того, формат PE может использоваться для представления
    объектных файлов. Объектные файлы служат для организации раздельной
    компиляции программы. Смысл раздельной компиляции заключается в
    том, что части программы (модули) компилируются независимо в объектные файлы, которые затем связываются компоновщиком в один исполняемый файл.
    А теперь – немного истории. Формат PE был создан разработчиками
    Windows NT. До этого в операционной системе Windows использовались
    форматы New Executable (NE) и Linear Executable (LE) для представления
    исполняемых файлов, а для хранения объектных файлов использовался
    Object Module Format (OMF). Формат NE предназначался для 16-разрядных приложений Windows, а формат LE, изначально разработанный для
    OS/2, был уже 32-разрядным. Возникает вопрос: почему разработчики
    Windows NT решили отказаться от существующих форматов? Ответ становится очевидным, если обратить внимание на то, что большая часть команды, работавшей над созданием Windows NT, ранее работала в Digital
    Equipment Corporation. Они занимались в DEC разработкой инструмента-

    2.1. Формат исполняемых файлов

    Глава 2.
    Структура программных компонентов

    32

    33

    рия для операционной системы VAX/VMS, и у них уже были навыки и готовый код для работы с исполняемыми файлами, представленными в формате Common Object File Format (COFF). Соответственно, формат COFF в
    слегка модифицированном виде был перенесен в Windows NT и получил
    название PE.
    В «.NET Framework Glossary» сказано, что PE – это реализация
    Microsoft формата COFF. В то же время в [5] утверждается, что PE – это
    формат исполняемых файлов, а COFF – это формат объектных файлов.
    Вообще, мы можем наблюдать путаницу в документации Microsoft относительно названия формата. В некоторых местах они называют его COFF,
    а в некоторых – PE. Правда, можно заметить, что в новых текстах название COFF используется все меньше и меньше. Более того, формат PE постоянно эволюционирует. Например, несколько лет назад в Microsoft отказались от хранения отладочной информации внутри исполняемого файла, и поэтому теперь многие поля в структурах формата COFF просто не
    используются. Кроме того, формат COFF – 32-разрядный, а последняя
    редакция формата PE (она называется PE32+) может использоваться на
    64-разрядных аппаратных платформах. Поэтому, видимо, дело идет к тому,
    что название COFF вообще перестанут использовать.
    Интересно отметить, что исполняемые файлы в устаревших форматах NE и LE до сих пор поддерживаются Windows. Исполняемые файлы в
    формате NE можно запускать под управлением NTVDM (NT Virtual DOS
    Machine), а формат LE используется для виртуальных драйверов устройств
    (VxD).
    Почему в названии формата PE присутствует слово «portable» («переносимый»)? Дело в том, что Windows NT была реализована не только
    для платформы Intel x86, но и для платформ MIPS R4000, DEC Alpha и
    PowerPC. И во всех реализациях для хранения исполняемых файлов использовался формат PE. При этом речь не шла о достижении двоичной
    совместимости между этими платформами, то есть exe-файл, предназначенный для выполнения на платформе Intel x86, нельзя было запустить на
    PowerPC. Важно понимать, что переносимость формата еще не означает
    переносимость исполняемых файлов, записанных в этом формате. Формат PE переносим в том смысле, что он слабо зависит от типа процессора и поэтому подходит для разных платформ (в том числе и для платформы .NET).
    Далее в этой главе мы не будем затрагивать 64-разрядный вариант
    формата PE, потому что в настоящее время сборки .NET хранятся в прежнем 32-разрядном формате. Однако отметим, что 64-разрядный PE очень
    слабо отличается от 32-разрядного. Основное отличие касается разрядности полей структур PE-файла.

    Структура программных компонентов

    0

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

    Рис. 2.1. Виртуальное адресное пространство процесса

    Доступно процессу

    2.1.1.1. Виртуальное адресное пространство процесса
    Каждый процесс в Windows запускается в своем виртуальном адресном пространстве размером в 4 Гб. При этом первые 2 Гб адресного пространства могут непосредственно использоваться процессом, а остальные
    2 Гб резервируются операционной системой для своих нужд (рис. 2.1).
    4 ГБ
    Зарезервировано
    операционной
    системой
    2 ГБ

    Прежде чем перейти к рассмотрению формата PE, необходимо поговорить об особенностях управления памятью в Windows, так как без знания этих особенностей невозможно понять некоторые существенные детали формата.
    Управление памятью в Windows NT/2k/XP/2k3 осуществляет менеджер виртуальной памяти (virtual-memory manager). Он использует страничную схему управления памятью, при которой вся физическая память
    делится на одинаковые отрезки размером в 4096 байт, называемые физическими страницами. Если физических страниц не хватает для работы системы, редко используемые страницы могут вытесняться на жесткий диск,
    в один или несколько файлов подкачки (pagefiles). Вытесненные страницы затем могут быть загружены обратно в память, если возникнет необходимость. Таким образом, программы могут использовать значительно
    большее количество памяти, чем реально присутствует в системе.

    2.1.1. Управление памятью в Windows

    CIL и системное программирование в Microsoft .NET

    35

    Виртуальный
    адрес
    vx

    0

    Физический
    адрес
    px
    Физическая
    страница
    pnum

    Физическая
    память

    0

    Рассмотрим на примере, как осуществляется перевод некоторого
    виртуального адреса vx в физический адрес px (рис 2.2). Сначала вычисляется номер vnum виртуальной страницы, соответствующий виртуальному
    адресу vx, а также смещение delta виртуального адреса относительно начала этой виртуальной страницы:
    vnum := vx div 4096;
    delta := vx mod 4096;
    Далее возможны три варианта развития событий:
    1. Виртуальная страница vnum недоступна. В этом случае перевод
    виртуального адреса vx в физический адрес невозможен, и процесс завершается с сообщением «Access Violation»;
    2. Виртуальная страница находится в файле страничной подкачки,

    Рис. 2.2. Перевод виртуального адреса в физический адрес

    Виртуальная
    страница
    vnum

    Виртуальное адресное
    пространство

    Виртуальные страницы могут отображаться операционной системой
    в страницы физической памяти, могут храниться в файле подкачки, а также могут быть вообще недоступны процессу. Обращение к недоступной
    виртуальной странице вызывает аварийное завершение процесса с сообщением «Access violation».
    Адресное пространство процесса называется виртуальным, потому
    что процесс для работы с памятью использует не реальные адреса физической памяти, а так называемые виртуальные адреса. При обращении по
    некоторому виртуальному адресу происходит перевод этого виртуального
    адреса в физический адрес. Перевод виртуальных адресов в физические
    адреса реализован на аппаратном уровне в процессоре и поэтому осуществляется достаточно быстро.

    Структура программных компонентов

    delta

    34

    delta

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

    Исполняемые файлы в формате PE, кроме всего прочего, обладают
    одной приятной особенностью – PE-файл, загруженный в оперативную
    память для исполнения, почти ничем не отличается от своего представления на диске. PE-файл сравнивается со сборным домом: стоит привезти
    его на место, свинтить отдельные детали, подключить электричество и водопровод, и все – можно жить.

    2.1.2. Обзор структуры PE-файла

    2.1.1.2. Отображаемые в память файлы
    Отображаемые в память файлы (memory-mapped files) – это мощная
    возможность операционной системы. Она позволяет приложениям осуществлять доступ к файлам на диске тем же самым способом, каким осуществляется доступ к динамической памяти, то есть через указатели.
    Смысл отображения файла в память заключается в том, что содержимое
    файла (или часть содержимого) отображается в некоторый диапазон виртуального адресного пространства процесса, после чего обращение по какому-либо адресу из этого диапазона означает обращение к файлу на диске. Естественно, не каждое обращение к отображенному в память файлу
    вызывает операцию чтения/записи. Менеджер виртуальной памяти кэширует обращения к диску и тем самым обеспечивает высокую эффективность работы с отображенными файлами.

    ми:

    и ее надо сначала загрузить в память. Тогда пусть pnum будет номером физической страницы, в которую мы загружаем нашу
    виртуальную страницу;
    3. Виртуальная страница уже находится в памяти, и ей соответствует некоторая физическая страница. В этом случае pnum – номер этой физической страницы.
    После чего адрес px вычисляется следующим образом:
    px := pnum*4096 + delta;
    Такая организация памяти процесса обладает следующими свойства-

    CIL и системное программирование в Microsoft .NET

    Заголовок MS-DOS

    Таблица секций
    Доп. заголовок PE-файла
    Заголовок PE-файла

    Секция 1

    Секция 2

    ...

    Секция N

    Образ в памяти

    37

    Благодаря этой особенности загрузчик операционной системы должен просто отобразить отдельные части PE-файла в адресное пространство процесса, подправить абсолютные адреса в исполняемом коде в соответствии с таблицей релокаций, создать таблицу адресов импорта и затем
    передать управление на точку входа (в случае exe-файла).
    На рис. 2.3 изображена схема PE-файла. Слева показана структура
    файла на диске, а справа – его образ в памяти. Мы видим, что PE-файл начинается с заголовков, за которыми располагаются несколько секций.
    В секциях размещаются код и данные исполняемого файла, а также служебная информация, необходимая загрузчику (например, секция «.reloc»
    на схеме содержит таблицу релокаций). Секции в оперативной памяти
    должны быть выровнены по границам страниц, поэтому загрузчик отображает каждую секцию, начиная с новой страницы адресного пространства
    процесса. Это приводит к тому, что в памяти секции, как правило, располагаются менее компактно, чем в файле (и это отражено на схеме).
    Так как расположение элементов PE-файла в памяти и на диске отличаются, для их локализации приходится вводить два понятия: относитель-

    Рис. 2.3. PE-файл на диске и в оперативной памяти

    Заголовок MS-DOS

    Таблица секций
    Доп. заголовок PE-файла
    Заголовок PE-файла

    Секция 1

    Секция 2

    ...

    Секция N

    Секция .reloc

    Неотображаемые в память
    данные

    PE-файл

    Структура программных компонентов

    Смещение в файле

    36

    RVA

    CIL и системное программирование в Microsoft .NET

    2.1.2.1. Секции
    Секция в PE-файле представляет либо код, либо некоторые данные
    (глобальные переменные, таблицы импорта и экспорта, ресурсы, таблица
    релокаций). Каждая секция имеет набор атрибутов, задающий ее свойства. Атрибуты секции определяют, доступна ли секция для чтения и записи, содержит ли она исполняемый код, должна ли она оставаться в памяти после загрузки исполняемого файла, могут ли различные процессы использовать один экземпляр этой секции и т.д.
    Исполняемый файл всегда содержит, по крайней мере, одну секцию,
    в которой помещен исполняемый код. Кроме этого, как правило, в исполняемом файле содержится секция с данными, а динамические библиотеки
    обязательно включают отдельную секцию с таблицей релокаций.
    Каждая секция имеет имя. Оно не используется загрузчиком и предназначено главным образом для удобства человека. Разные компиляторы
    и компоновщики дают секциям различные имена. Например, компоновщик от Microsoft размещает код в секции «.text», константы – в секции
    «.rdata», таблицы импорта и экспорта – в секциях «.idata» и «.edata», таблицу релокаций – в секции «.reloc», ресурсы – в секции «.rsrc». В то же
    время компоновщик фирмы Borland использует имена «CODE» для секций, содержащих код, и «DATA» для секций с данными.
    Выравнивание секций в исполняемом файле на диске и в образе файла в памяти чаще всего отличается. В памяти они, как правило, выровнены по границам страниц. В принципе, возможно сгенерировать PE-файл
    с одинаковым выравниванием секций как на диске, так и в памяти. Смещения элементов в таком файле будут совпадать с их RVA, что существен-

    ный виртуальный адрес элемента в памяти (Relative Virtual Address – RVA)
    и смещение элемента в файле (file offset).
    RVA некоторого элемента PE-файла – это разность виртуального адреса данного элемента и базового адреса, по которому PE-файл загружен
    в память. Например, если файл загружен по адресу 0x400000, и некоторый
    элемент в нем располагается по адресу 0x402000, то RVA этого элемента
    равен (0x402000 – 0x400000) = 0x2000.
    Смещение элемента в файле представляет собой количество байт, которое надо отсчитать от начала файла, чтобы попасть на начало элемента.
    Смещения используются гораздо реже, чем RVA, потому что основное их
    назначение состоит в обеспечении соответствия положения секций PEфайла в файле на диске и в памяти.
    Загрузчик формирует образ PE-файла в памяти таким образом, что
    соблюдается следующее правило: пусть ox и oy – смещения каких-то элементов в файле, а rx и ry – RVA этих элементов, тогда если ox < oy, то
    rx < ry.

    38

    39

    2.1.2.3. Импорт функций
    В PE-файле существует специальная секция «.idata», описывающая
    функции, который этот файл импортирует из динамических библиотек.
    Описание импортируемых функций в секции «.idata» приводит к тому, что
    библиотеки загружаются загрузчиком операционной системы еще до запуска программы. В принципе, необязательно описывать каждую импортируемую функцию в этой секции, так как динамические библиотеки
    можно загружать с помощью функции LoadLibrary из Win32 API прямо во
    время выполнения программы.
    В процессе загрузки программы осуществляется связывание (binding)
    функций, импортируемых из динамических библиотек. Связывание подразумевает загрузку соответствующих динамических библиотек и составление таблицы адресов импорта (Import Address Table – IAT). Адрес каж-

    2.1.2.2. Выбор базового адреса образа PE-файла в памяти
    Давайте обсудим, каким образом загрузчик определяет базовый адрес, по которому нужно загрузить PE-файл. Для exe-файлов это тривиальная задача: в заголовках файла присутствует поле ImageBase, содержащее
    значение базового адреса. Так как для выполнения exe-файла создается
    отдельный процесс со своим виртуальным адресным пространством, то
    обычно не возникает никаких проблем с тем чтобы отобразить файл в это
    адресное пространство по заданному адресу. Как правило, все exe-файлы
    содержат в поле ImageBase значение 0x400000.
    А вот выбор базового адреса для dll-файла куда сложнее. Дело в том,
    что динамическая библиотека, как правило, загружается в адресное пространство уже существующего процесса, и хотя dll-файл тоже содержит
    некоторое значение в поле ImageBase, очень часто может так получиться,
    что этот адрес уже занят чем-то другим (например, по нему уже загружена
    другая динамическая библиотека). Что же делать загрузчику, если он не
    может загрузить dll-файл по заданному адресу? Ему ничего не остается,
    как загрузить этот файл по какому-то другому адресу. Но тут возникает новая проблема – в файле могут содержаться инструкции с абсолютными адресами (это, в основном, инструкции абсолютных переходов, инструкции
    вызова подпрограмм, а также инструкции для работы с глобальными данными). При загрузке динамической библиотеки по другому адресу все адреса, содержащиеся в этих инструкциях, становятся неправильными, и загрузчик вынужден их поправить. Для того, чтобы загрузчик мог это сделать, в файле содержится таблица релокаций, в которой прописаны RVA
    всех абсолютных адресов.

    но упрощает создание генератора кода. Недостатком такого подхода является увеличение размеров PE-файлов.

    Структура программных компонентов

    CIL и системное программирование в Microsoft .NET

    2.1.3.1. Заголовок MS-DOS
    Каждый PE-файл начинается с небольшой (128 байт) программы, записанной в формате исполняемых файлов MS-DOS. Эта программа выводит на экран сообщение «This program cannot be run in DOS mode». В настоящее время наличие такого «заголовка» вряд ли имеет смысл, но во
    время повсеместного использования операционной системы MS-DOS
    люди зачастую случайно пытались запускать PE-файлы из ДОСовской командной строки, и эта маленькая программа в начале файла давала им возможность осознать свою ошибку, выбросить MS-DOS на помойку и установить наконец-то Windows NT!
    Рассмотрим шестнадцатеричный дамп заголовка MS-DOS, представленный на рис. 2.4.

    2.1.3. Заголовки

    2.1.2.4. Экспорт функций
    Экспорт функций из сборки .NET осуществляется достаточно редко.
    Дело в том, что наличие метаданных в сборках позволяет нам экспортировать любые элементы сборки, такие как классы, методы, поля, свойства и
    т.д. Таким образом, обычный механизм экспорта функций становится
    ненужным.
    Необходимость в экспорте функций возникает только тогда, когда
    сборка .NET должна использоваться обычной программой Windows, код
    которой не управляется средой выполнения .NET.
    Информация об экспортируемых функциях хранится внутри PE-файла в специальной секции «.edata». При этом каждой функции присваивается уникальный номер, и с этим номером связывается RVA тела функции, и,
    возможно, имя функции. Не всякая экспортируемая функция имеет имя,
    так как имена служат, главным образом, для удобства программистов.

    дой импортируемой функции заносится в эту таблицу и в дальнейшем используется для вызова данной функции.
    Секция «.idata» в сборках .NET в некотором смысле носит вспомогательный характер, так как импортируемые сборкой динамические библиотеки описываются в метаданных. Задача этой секции – обеспечить запуск среды выполнения .NET, поэтому в ней описывается только одна импортируемая из mscoree.dll функция (_CorExeMain для exe-файлов и
    _CorDllMain – для dll-файлов). При запуске сборки .NET управление сразу
    же передается этой функции, которая запускает Common Language
    Runtime, осуществляющий JIT-компиляцию программы и контролирующий в дальнейшем ее выполнение.

    40

    41

    Заголовок начинается с сигнатуры «MZ». Она представляет собой
    инициалы одного из разработчиков операционной системы MS-DOS 2.0
    Марка Збиковски и знаменита тем, что ни одна инструкция процессоров
    семейства Intel x86 с нее не начинается. В свое время эта ее особенность
    давала загрузчику исполняемых файлов MS-DOS возможность отличать
    exe-файлы, которые появились только во второй версии MS-DOS, от comфайлов.
    Исполняемые com-файлы пришли в MS-DOS из операционной системы CP/M. Их формат был настолько примитивным, что вряд ли заслуживает того, чтобы вообще называться форматом исполняемых файлов.
    Загрузчик должен был попросту загрузить com-файл в память, и после
    нехитрых манипуляций, не вдаваясь в подробности внутренней структуры
    файла, передать управление на его начало.
    В принципе, PE-файл не обязан начинаться именно с такого заголовка. Вы можете поместить в его начало любой exe-файл, работающий в MSDOS. При этом 32-разрядное слово, расположенное по смещению 0x3c в
    этом exe-файле, должно содержать его размер. Для стандартного заголовка это значение равно 0x00000080 (подчеркнуто в дампе).
    Сразу после заголовка MS-DOS следует сигнатура PE-файла, состоящая из четырех байт: 0x50, 0x45, 0x00 и 0x00 (в строковом представлении
    она выглядит как «PE\0\0»). Поэтому при просмотре дампа PE-файла
    очень просто понять, где заканчивается заголовок MS-DOS – достаточно
    поискать глазами две буквы «PE».

    Рис. 2.4. Шестнадцатитеричный дамп заголовка MS-DOS

    Структура программных компонентов

    CIL и системное программирование в Microsoft .NET

    2.1.3.2. Заголовок PE-файла
    Заголовок PE-файла непосредственно следует за сигнатурой PE-файла. В современной документации он называется «PE File Header», но в более старых текстах можно встретить название «COFF Header».
    Заголовок PE-файла состоит из следующих полей:
    unsigned short Machine;
    Это поле содержит идентификатор процессора, для которого
    предназначен исполняемый файл. Для сборок .NET всегда используется значение 0x14c.
    unsigned short NumberOfSections;
    Задает количество секций в PE-файле. Массив заголовков секций следует сразу после всех заголовков, и это поле, таким образом, определяет размер этого массива.
    long TimeDateStamp;
    Время создания файла. Отсчитывается в секундах от начала 1
    января 1970 года по Гринвичу. Самый простой способ получения
    времени в этом формате – вызов функции time() из стандартной библиотеки языка C.
    long PointerToSymbolTable;
    long NumberOfSymbols;
    Эти два поля использовались раньше для организации хранения
    отладочной информации внутри COFF-файла. В настоящий
    момент они не используются и всегда содержат нули.
    unsigned short OptionalHeaderSize;
    Задает размер дополнительного заголовка PE-файла, который
    следует непосредственно за заголовком PE-файла. Сборки
    .NET, как правило, содержат значение 0xE0 в этом поле. Вообще, наличие этого поля позволяет расширять формат путем добавления новых полей в дополнительный заголовок PE-файла.
    unsigned short Characteristics;
    Представляет собой комбинацию флагов, задающую характеристики исполняемого файла. Для сборок .NET требуется установить следующий набор флагов:
    0x0002 – файл является исполняемым;
    0x0004 – файл не содержит информации о номерах строк исход-

    При разработке программного обеспечения, выполняющего чтение
    PE-файлов, важно не забыть осуществить проверку сигнатуры. Дело в
    том, что исполняемые файлы в устаревших форматах также начинаются с
    похожего заголовка MS-DOS, после которого располагаются другие сигнатуры: «NE» для 16-разрядных приложений Windows, «LE» для виртуальных драйверов устройств, и даже «LX» для исполняемых файлов OS/2.

    42

    43

    2.1.3.3. Дополнительный заголовок PE-файла
    Дополнительный заголовок PE-файла следует сразу за основным заголовком. В современной документации он называется «PE Optional
    Header». Строго говоря, «optional» означает «необязательный», а не «дополнительный». Дело в том, что в объектных файлах этот заголовок действительно необязателен, но так как в исполняемых файлах он всегда присутствует, мы будем называть его «дополнительным».
    Поля дополнительного заголовка можно разделить на три группы:
    • Стандартные поля.
    Группа стандартных полей пришла в PE из формата COFF. Они
    содержат основную информацию, необходимую для загрузки и
    исполнения PE-файла.
    • Поля, специфичные для Windows NT.
    Эти поля специально предназначены для загрузчика Windows
    NT. В формате COFF они изначально не присутствовали.
    • Директории данных.
    Местонахождение некоторых важных структур данных в образе
    загруженного в память PE-файла задается в так называемых директориях данных (Data Directories). Каждая директория содержит RVA и размер соответствующей структуры. Всего в дополнительном заголовке хранятся 16 директорий данных.
    В состав дополнительного заголовка PE-файла входят следующие
    стандартные поля:
    unsigned short Magic;
    Константа, задающая тип PE-файла:
    0x010B – 32-разрядный файл;
    0x020B – 64-разрядный файл.
    Для сборок .NET должно быть установлено значение 0x010B.
    char LMajor;
    Старшее число версии компоновщика. Для сборок .NET – 6.

    ной программы;
    0x0008 – файл не содержит информации о символах исходной
    программы;
    0x0100 – файл предназначен для исполнения на 32-разрядной
    машине.
    Если сборка представляет собой динамическую библиотеку, то
    дополнительно нужно установить флаг 0x2000.
    Таким образом, значение поля Characteristics для exe-файлов
    – 0x010E, а для dll-файлов – 0x210E.
    Если исполняемый файл не содержит таблицы релокаций, то
    дополнительно нужно установить флаг 0x0001.

    Структура программных компонентов

    44

    char LMinor;
    Младшее число версии компоновщика. Для сборок .NET – 0.
    long CodeSize;
    Суммарный размер всех кодовых секций, всегда выровнен по
    значению SectionAlignment (см. далее).
    long InitializedDataSize;
    Суммарный размер всех секций, содержащих инициализированные данные. Выровнен по значению SectionAlignment (см.
    далее). Для сборок .NET характерно то, что в состав секций, содержащих инициализированные данные, включают секции с
    метаданными и CIL-кодом.
    long UninitializedDataSize;
    Суммарный размер всех секций, содержащих неинициализированные данные. В сборках .NET, как правило, это поле содержит
    значение 0 (нет таких секций).
    long EntryPointRVA;
    RVA точки входа в программу. Для dll-файлов (обычных, не сборок .NET) может быть равен 0, а может указывать на код, вызываемый в процессе инициализации, завершения работы, а также
    во время создания или уничтожения потоков управления.
    Передаче управления на точку входа всегда предшествует корректировка абсолютных адресов (в соответствии с таблицей релокаций), а также формирование таблицы адресов импорта.
    Для сборок .NET (как exe, так и dll) значение этого поля всегда
    указывает на 6 байт, расположенных в кодовой секции PE-файла. Эти 6 байт начинаются с двух байтов 0xFF 0x25, за которыми
    следует некий абсолютный адрес x. Тем самым кодируется следующая инструкция:
    jmp dword ptr ds:[x]
    Для exe-файлов адрес x представляет собой сумму значения поля ImageBase (как правило, это 0x400000) и RVA ячейки в таблице адресов импорта, которая соответствует функции
    _CorExeMain, импортируемой из динамической библиотеки
    mscoree.dll.
    Для dll-файлов адрес x представляет собой сумму значения поля
    ImageBase (как правило, это либо 0x400000, либо 0x10000000, либо 0x11000000) и RVA ячейки в таблице адресов импорта, которая соответствуюет функции _CorDllMain, импортируемой из динамической библиотеки mscoree.dll.
    Интересно, что описание этого поля в [2] явно не соответствует
    действительности: «RVA of entry point, needs to point to bytes 0xFF
    0x25 followed by the RVA+0x4000000 in a section marked

    CIL и системное программирование в Microsoft .NET

    45

    execute/read for EXEs or 0 for DLLs». Налицо две ошибки: лишний ноль в адресе (который подчеркнут), а также информация о
    том, что для dll-файлов поле должно быть равно 0.
    long BaseOfCode;
    RVA первой кодовой секции в PE-файле.
    Описание этого поля в [2] абсолютно неправильное: «RVA of the
    code section, always 0x00400000 for exes and 0x10000000 for DLL.»
    Авторы явно путают относительные адреса с абсолютными, а
    также базовый адрес образа PE-файла в памяти с адресом кодовой секции.
    long BaseOfData;
    RVA первой секции, содержащей данные. Видимо, не используется загрузчиком, потому что различные версии компоновщика
    по-разному устанавливают это поле. В 64-разрядной версии
    формата PE от этого поля вообще отказались.
    В сборках .NET, не содержащих секций с данными, принято записывать в это поле RVA секции, которая могла бы идти непосредственно после последней секции в PE-файле.
    Следующие поля специфичны для Windows NT:
    long ImageBase;
    Предпочтительный базовый адрес, по которому PE-файл загружается в память (то есть, если файл загружается по этому адресу,
    то применение таблицы релокаций не нужно). Для exe-файлов,
    как правило, равен 0x400000, а для dll-файлов – 0x10000000.
    Нужно отметить, что в выборе базового адреса для dll-файлов
    наблюдается большой плюрализм мнений. Например, dll-файлы, сгенерированные компилятором C#, содержат в поле
    ImageBase значение 0x11000000. А dll-файлы, сгенерированные
    ассемблером ILASM, содержат в этом поле значение 0x400000
    (как и exe-файлы).
    long SectionAlignment;
    Задает выравнивание секций в памяти. Для сборок .NET всегда
    равно 0x2000.
    long FileAlignment;
    Задает выравнивание секций в PE-файле. Для сборок .NET разрешены значения 0x200 и 0x1000.
    unsigned short OSMajor;
    Старшее число версии Windows, для которой предназначена
    сборка. Это поле игнорируется загрузчиком, и в случае сборок
    .NET должно содержать значение 4.
    unsigned short OSMinor;
    Младшее число версии Windows, для которой предназначена

    Структура программных компонентов

    46

    сборка. Это поле игнорируется загрузчиком, и в случае сборок
    .NET должно содержать значение 0.
    unsigned short UserMajor;
    Старшее число версии данного PE-файла. Для сборок .NET всегда 0.
    unsigned short UserMinor;
    Младшее число версии данного PE-файла. Для сборок .NET
    всегда 0.
    unsigned short SubsysMajor;
    Старшее число версии подсистемы Windows, которая требуется
    для запуска программы. В свое время применялось для того,
    чтобы отличать программы, использующие новый по тем временам интерфейс Windows 95 и Windows NT 4.0. В настоящее время не используется. Для сборок .NET всегда равно 4.
    unsigned short SubsysMinor;
    Младшее число версии подсистемы Windows. Для сборок .NET
    всегда равно 0.
    long Reserved;
    Это поле зарезервировано и всегда содержит 0.
    long ImageSize;
    Размер образа PE-файла в памяти. Это поле равно RVA секции,
    которая могла бы идти непосредственно после последней секции в PE-файле. Естественно, что оно выровнено по значению
    SectionAlignment.
    long HeaderSize;
    Суммарный размер всех заголовков, включая заголовок MSDOS, заголовок PE-файла, дополнительный заголовок PE-файла и массив заголовков секций. Суммарный размер кратен значению из поля FileAlignment.
    long FileChecksum;
    Контрольная сумма PE-файла. Для сборок .NET – всегда 0.
    unsigned short SubSystem;
    Идентифицирует подсистему для запуска PE-файла. Для сборок
    .NET допустимы следующие значения:
    0x2 – необходим графический пользовательский интерфейс
    Windows;
    0x3 – запускается в консольном режиме;
    0x9 – необходим графический пользовательский интерфейс
    Windows CE.
    В [2] приводится неверная информация об этом поле:
    «Subsystem required to run this image. Shall be either IMAGE_SUBSYSTEM_WINDOWS_CE_GUI (0x3) or IMAGE_SUBSYS-

    CIL и системное программирование в Microsoft .NET

    47

    TEM_WINDOWS_GUI (0x2).»
    unsigned short DLLFlags;
    В [2] сказано, что это поле всегда равно 0. На практике оно иногда содержит значение 0x400 («No safe exception handler»), когда
    именно – установить пока не удалось. Самое интересное, что в
    [5] флаг 0x400 вообще не описан.
    long StackReserveSize;
    Количество виртуальной памяти, резервируемое под стек. Как
    правило, содержит 0x100000.
    long StackCommitSize;
    Начальный размер стека. Как правило, равен 0x1000.
    long HeapReserveSize;
    Количество виртуальной памяти, резервируемое под кучу. Как
    правило, содержит 0x100000.
    long HeapCommitSize;
    Начальный размер кучи. Как правило, равен 0x1000.
    long LoaderFlags;
    Не используется и всегда содержит 0.
    long NumberOfDataDirectories;
    Количество директорий данных в дополнительном заголовке.
    Для сборок .NET обязательно равно 16.
    В конце дополнительного заголовка размещается массив из 16 директорий данных. Каждая директория данных состоит из двух полей:
    long RVA;
    RVA некоторой структуры. Если данная структура отсутствует в
    PE-файле, это поле равно 0.
    long size;
    Размер структуры. Для отсутствующей структуры размер равен
    0.
    Для сборок .NET важны 4 из 16 директорий данных (остальные 12 директорий, как правило, могут быть обнулены):
    • Директория импорта (номер 2, находится по смещению 8 относительно начала массива директорий). Указывает на данные о
    функциях, импортруемых из динамическх библиотек (другими
    словами, указывает на секцию «.idata»).
    • Директория релокаций (номер 6, смещение 40). Указывает на
    таблицу релокаций.
    • Директория таблицы адресов импорта (номер 13, смещение 96).
    В некотором смысле дублирует директорию импорта, указывая
    на таблицу адресов импорта.
    • Директория заголовка CLI (номер 15, смещение 112). Указывает
    на заголовок, описывающий метаданные сборки .NET.

    Структура программных компонентов

    CIL и системное программирование в Microsoft .NET

    2.1.3.4. Заголовки секций
    Непосредственно после дополнительного заголовка следует массив
    заголовков секций. Количество секций и, соответственно, размер этого
    массива задается полем NumberOfSections заголовка PE-файла. Секции в
    массиве отсортированы по их начальным адресам (по RVA).
    Заголовок каждой секции состоит из следующих полей:
    char Name[8];
    Имя секции представляет собой ASCIIZ-строку, содержащую
    не более 8 символов. Если имя содержит ровно 8 символов, то
    оно не оканчивается на 0.
    long VirtualSize;
    Размер секции, когда она загружена в память. Значение этого
    поля не нужно выравнивать.
    Если размер секции в памяти превышает размер той же секции
    в PE-файле (см. далее SizeOfRawData), то разница заполняется
    нулями.
    long VirtualAddress;
    RVA секции, когда она загружена в память.
    long SizeOfRawData;
    Размер секции в PE-файле, выровненный по значению
    FileAlignment из дополнительного заголовка PE-файла.
    Если секция содержит только неинициализированные данные,
    значение этого поля должно быть равно 0.
    long PointerToRawData;
    Смещение секции относительно начала PE-файла. Значение
    этого поля всегда выровнено по значению FileAlignment из дополнительного заголовка PE-файла.
    long PointerToRelocations;
    Смещение таблицы релокаций для данной секции. Используется только в объектных файлах – в исполняемых файлах равно 0.
    long PointerToLinenumbers;
    Смещение информации о номерах строк. В сборках .NET всегда
    равно 0.
    short NumberOfRelocations;
    Количество релокаций для этой секции. В исполняемых файлах
    всегда равно 0.
    short NumberOfLinenumbers;
    Количество номеров строк. В сборках .NET всегда равно 0.
    long Characteristics;
    Комбинация флагов, задающая свойства секции:
    0x00000020 – секция содержит исполняемый код;
    0x00000040 – секция содержит инициализированные данные;

    48

    49

    2.1.4.1. Секция импорта
    В секции импорта перечисляются все dll-файлы, используемые программой, а также все функции и глобальные переменные, импортируемые
    из этих файлов. Для краткости будем называть такие функции и глобальные переменные символами.
    Директория импорта в дополнительном заголовке PE-файла должна
    указывать на данные, расположенные в секции импорта.
    Секция импорта содержит названия dll-файлов и имена импортируемых символов, представленные в виде ASCIIZ-строк. При этом используется непрямая схема хранения этих строк, потому что вся основная информация в секции импорта организована в виде таблиц, а строковые данные, в силу их произвольного размера, в таблицах хранить неудобно. Поэтому названия dll-файлов и имена символов компактно хранятся где-то
    внутри PE-файла (чаще всего – в каком-нибудь свободном месте секции
    импорта), и вместо них в таблицах записываются их RVA.
    Схема секции импорта приведена на рис. 2.5. Ключевым элементом
    этой секции является таблица импорта (Import Directory Table), представляющая собой массив так называемых входов в таблицу импорта (Import
    Directory Entry). При этом самый последний вход в таблицу импорта заполнен нулями и сигнализирует о конце массива.
    Каждому dll-файлу, используемому программой, соответствует ровно
    один вход в таблицу импорта. Этот вход содержит указатели (в форме
    RVA) на два идентичных массива, которые называются таблицей адресов

    Секции PE-файла, как правило, содержат исполняемый код и данные, которые не имеют специального смысла для загрузчика. Но из всякого правила бывают исключения, поэтому далее мы рассмотрим структуру
    секции импорта «.idata», а также особенности хранения таблицы релокаций в секции «.reloc». В состав PE-файла могут входить и другие особые
    секции, но мы не будем их обсуждать, так как они не встречаются в сборках .NET.

    2.1.4. Особые секции PE-файла

    0x00000080 – секция содержит неинициализированные данные;
    0x02000000 – секция может быть удалена из исполняемого файла (этот флаг установлен для секции «.reloc», содержащей таблицу релокаций);
    0x20000000 – код секции может быть исполнен;
    0x40000000 – секция доступна для чтения;
    0x80000000 – секция доступна для записи.
    Для секций, содержащих метаданные и CIL-код, необходимо
    использовать значение флагов 0x60000020.

    Структура программных компонентов

    CIL и системное программирование в Microsoft .NET

    NULL

    NULL

    Hint/Name
    Table

    У тех, кто внимательно прочитал предыдущий абзац, обязательно
    должен возникнуть вопрос: а зачем нужно хранить в секции импорта два
    идентичных массива ILT и IAT?
    Дело в том, что раньше никакого ILT не было и секция импорта содержала только массив IAT. При загрузке программы происходило так называемое связывание, при котором информация из IAT использовалась
    для определения адресов импортируемых символов. Эти адреса записывались загрузчиком прямо в IAT (естественно, в образе PE-файла в памяти,
    а не на диске) поверх той информации, которая там содержалась.
    Необходимость в дополнительном массиве ILT возникла после изобретения предварительного связывания, при котором таблица адресов
    импорта заранее заполняется адресами импортируемых символов. Предварительное связывание осуществляется утилитой BIND, которая вычисляет эти адреса и записывает их прямо в PE-файл на диске. Это позволяет несколько ускорить загрузку программы, но при этом возникают новые проблемы. А что если предварительно связанный dll-файл вдруг изменится? Ведь тогда все адреса могут поменяться? Увы, это так. Правда,
    загрузчик способен определить этот факт и вычислить новые адреса, и
    для этого ему как раз и нужна копия таблицы адресов импорта, которая
    находится в ILT.

    Рис. 2.5. Схема секции импорта

    Import Directory Entry 3

    Import Lookup
    Table

    NULL

    Import Directory Entry 2

    Import Directory Entry 1

    Import Directory Table

    Import Address
    Table

    импорта (Import Address Table, далее IAT) и таблицей имен импорта
    (Import Lookup Table, далее ILT). Элементы этих массивов описывают
    символы, импортируемые из данного dll-файла. При этом каждый массив
    заканчивается нулевым элементом.
    Директория таблицы адресов импорта в дополнительном заголовке
    PE-файла должна указывать на таблицу адресов импорта (IAT).

    50

    51

    2.1.4.2. Секция релокаций
    В секции релокаций («.reloc») содержится таблица исправлений
    (Fix-up Table), в которой перечислены все абсолютные адреса в PE-файле,
    которые надо исправить, если файл загружается по адресу, отличному от
    указанного в поле ImageBase.

    Мы не будем рассматривать детали организации секции импорта, относящиеся к механизму предварительного связывания.
    Вход в таблицу импорта представляет собой структуру, состоящую из
    нескольких полей:
    long ImportLookupTableRVA;
    RVA таблицы имен импорта (ILT). Ранее, до изобретения предварительного связывания, это поле называлось Characteristics.
    long TimeDateStamp;
    Это поле изначально равно нулю (в PE-файле на диске), но после загрузки dll-файла в него (уже в памяти) записывается время
    загрузки. (В предварительно связанном PE-файле в поле
    TimeDateStamp должно быть записано значение -1.)
    long ForwarderChain;
    Должно быть равно -1.
    long NameRVA;
    RVA ASCIIZ-строки, содержащей имя dll-файла.
    long ImportAddressTableRVA;
    RVA таблицы адресов импорта (IAT).
    Теперь рассмотрим, как организованы наши идентичные массивы
    ILT и IAT. Их элементами являются 32-разрядные целые числа. Если старший бит (31-й) такого 32-разрядного числа установлен в 1, то оставшиеся
    31 бит обозначают порядковый номер импортируемого символа. Если же
    старший бит равен 0, то это 32-разрядное число обозначает RVA структуры Hint/Name, в которой хранится имя импортируемого символа.
    Структура Hint/Name состоит из трех полей:
    short Hint;
    Это поле является подсказкой для загрузчика. Оно содержит
    предполагаемый номер импортируемого символа. Загрузчик
    сначала ищет этот символ по указанному номеру. В случае
    неудачи он выполняет бинарный поиск символа по имени.
    char Name[x];
    Имя импортируемого символа в виде ASCIIZ-строки.
    char Pad;
    Это поле служит для выравнивания структуры по четной границе, то есть оно присутствует только тогда, когда структура имеет
    нечетный размер. Всегда равно нулю.

    Структура программных компонентов

    CIL и системное программирование в Microsoft .NET

    Директория заголовка CLI в дополнительном заголовке PE-файла
    должна указывать на заголовок CLI, который служит главным образом для
    локализации метаданных в PE-файле.
    Заголовок CLI состоит из следующих полей:
    long Cb;
    Размер заголовка в байтах.
    short MajorRuntimeVersion;
    short MinorRuntimeVersion;
    Эти два поля содержат информацию о версии CLR, для которой

    2.1.5. Заголовок CLI

    Директория релокаций в дополнительном заголовке PE-файла должна указывать на таблицу исправлений.
    Таблица исправлений разбита на блоки. Каждый блок описывает исправления, которые нужно внести в определенную страницу (4K байт) загруженного в память PE-файла. Каждый блок должен начинаться на 32битовой границе.
    В начале каждого блока располагается заголовок, состоящий из следующих полей:
    long PageRVA;
    Это поле содержит RVA страницы PE-файла, исправления в которой описываются данным блоком.
    long BlockSize;
    Суммарный размер блока в байтах, включая заголовок.
    После заголовка следует массив 16-разрядных слов, каждое из которых описывает одно исправление. При этом старшие четыре бита каждого
    из этих слов задают тип исправления, а остальные 12 бит обозначают смещение относительно начала страницы, соответствующей данному блоку.
    В сборках .NET в качестве типа исправления используется значение
    3. Этот тип означает, что по заданному смещению относительно начала
    описываемой блоком страницы PE-файла находится 32-разрядное значение, которое необходимо исправить.
    Рассмотрим, как загрузчик выполняет исправление образа PE-файла.
    Пусть ActualAddress – это адрес, по которому загружен PE-файл. И пусть
    delta – это смещение исправляемого 32-разрядного значения относительно начала страницы. Тогда адрес исправляемого значения вычисляется
    следующим образом:
    FixupAddress = ActualAddress + PageRVA + delta;
    Внесение исправления в 32-разрядное значение, которое находится
    по адресу FixupAddress, выполняется так (преобразования типов для простоты не указаны):
    *FixupAddress += ActualAddress – ImageBase;

    52

    53

    В приложении A приведен исходный код программы pegen, демонстрирующей генерацию PE-файла. Эта программа создает сборку hello.exe,
    работа которой заключается в дублировании строки, введенной пользователем с клавиатуры. Несмотря на то, что генерируемая сборка столь примитивна, программа pegen может служить основой для разработки реального генератора исполняемых файлов .NET.
    Программа pegen написана на языке C и состоит из двух частей:
    1. модуль генерации PE-файла, оформленный как отдельная библиотека;
    2. главный модуль, использующий модуль генерации для создания
    простейшей сборки .NET.

    2.1.6. Пример генерации PE-файла

    предназначена данная сборка. В настоящее время эти поля
    должны содержать значения 2 и 0 соответственно.
    struct { long RVA, Size; } Metadata;
    В этом поле указываются RVA и размер в байтах метаданных в
    образе PE-файла.
    long Flags;
    Это поле описывает свойства сборки. Для обычных сборок .NET
    равно 1.
    long EntryPointToken;
    Токен метаданных, указывающий на точку входа в сборку.
    struct { long RVA, Size; } Resources;
    В этом поле указываются RVA и размер в байтах ресурсов сборки.
    struct { long RVA, Size; } StrongNameSignature;
    В этом поле указываются RVA и размер данных, используемых
    загрузчиком CLI для контроля версий связываемых динамических библиотек.
    long CodeManagerTable[2];
    Это поле не используется и всегда заполнено нулями.
    struct { long RVA, Size; } VTableFixups;
    В этом поле указываются RVA и размер данных, используемых
    загрузчиком для исправления таблиц виртуальных методов. Так
    как эти таблицы, вообще говоря, порождаются только некоторыми «экзотическими» компиляторами (предположительно, Visual
    C++ With Managed Extensions), мы их рассматривать не будем.
    long ExportAddressTableJumps[2];
    Это поле не используется и всегда заполнено нулями.
    long ManagedNativeHeader[2];
    Это поле не используется и всегда заполнено нулями.

    Структура программных компонентов

    CIL и системное программирование в Microsoft .NET

    В модуле генерации определена функция make_file, которая принимает блок входных параметров и дескриптор выходного файла:
    void make_file (FILE* file, PINPUT_PARAMETERS inP)
    {
    make_headers
    (file, inP); // 1 этап
    make_text_section
    (file, inP); // 2 этап
    make_cli_section
    (file, inP); // 3 этап
    make_reloc_section
    (file, inP); // 4 этап
    };
    Как видно из приведенного листинга, эта функция вызывает еще четыре функции, поскольку процесс генерации PE файла разбит на четыре
    этапа.
    Блок входных параметров описывается структурой INPUT_PARAMETERS:
    unsigned long Type;
    Тип исполняемого файла: exe или dll. Поле может принимать
    значения:
    EXE_TYPE – выходной файл-exe;
    DLL_TYPE – выходной файл-dll.
    unsigned char*
    metadata;
    Это поле содержит указатель на область памяти, где находятся
    метаданные в бинарном виде.
    unsigned char*
    cilcode;
    Указатель на область памяти, где лежит CIL-код методов в бинарном виде.
    unsigned long SizeOfMetadata;
    Размер метаданных.
    unsigned long SizeOfCilCode;
    Размер CIL-кода методов.
    unsigned long ImageBase;
    Базовый адрес загрузки.
    unsigned long FileAlignment;
    Выравнивание секций в файле.
    unsigned long EntryPointToken;
    Точка входа в сборку (токен метаданных, соответствующий
    некоторому статическому методу).
    unsigned short
    Subsystem;
    Тип подсистемы Console или Windows GUI. Поле может принимать значения:
    IMAGE_SUBSYSTEM_WINDOWS_GUI – подсистема Windows GUI;
    IMAGE_SUBSYSTEM_WINDOWS_CUI – подсистема Windows CUI.
    Этих входных данных достаточно для генерации сборки .NET.
    Подробно рассмотрим каждый этап выполнения программы.

    54

    55

    struct _IMAGE_OPTIONAL_HEADER {
    //Дополнительный заголовок PE
    unsigned short Magic;
    unsigned char
    LMajor;
    unsigned char
    LMinor;
    unsigned long
    CodeSize;
    unsigned long
    SizeOfInitializedData;
    unsigned long
    SizeOfUninitializedData;
    unsigned long
    EntryPointRVA;
    unsigned long
    BaseOfCode;
    unsigned long
    BaseOfData;
    unsigned long
    ImageBase;
    unsigned long
    SectionAlignment;
    unsigned long
    FileAlignment;
    unsigned short OSMajor;
    unsigned short OSMinor;
    unsigned short UserMajor;
    unsigned short UserMinor;

    struct _IMAGE_FILE_HEADER { // заголовок PE
    unsigned short Machine;
    unsigned short NumberOfSections;
    unsigned long
    TimeDateStamp;
    unsigned long
    PointerToSymbolTable;
    unsigned long
    NumberOfSymbols;
    unsigned short OptionalHeaderSize;
    unsigned short Characteristics;
    }PeHdr;

    2.1.6.1. Этап 1. Заполнение заголовка PE-файла
    Первый этап включает заполнение структуры HEADERS. Всю работу на
    этом этапе выполняет функция make_headers, принимающая блок входных
    параметров и файловый дескриптор. Прототип функции:
    void make_headers (FILE* file, PINPUT_PARAMETERS inP);
    Структура HEADERS включает в себя заголовок MS-DOS, сигнатуру PE,
    заголовок PE, дополнительный заголовок PE, директории данных и заголовки
    секций.
    Формат
    структур
    IMAGE_DATA_DIRECTORY
    и
    IMAGE_SECTION_HEADER, которые входят в структуру HEADERS, можно найти
    дальше:
    struct HEADERS {
    char ms_dos_header[128]; // заголовок MS-DOS
    unsigned long signature; // сигнатура PE

    Структура программных компонентов

    56

    short
    short
    long
    long
    long
    long
    short
    short
    long
    long
    long
    long
    long
    long

    SubsysMajor;
    SubsysMinor;
    Reserved;
    ImageSize;
    HeaderSize;
    FileCheckSum;
    Subsystem;
    DllFlags;
    StackReserveSize;
    StackCommitSize;
    HeapReserveSize;
    HeapCommitSize;
    LoaderFlags;
    NumberOfDataDirectories;

    // Поле не используется в сборках. Заполняется нулями
    struct IMAGE_DATA_DIRECTORY STUB5;

    // Директория заголовка CLI
    struct IMAGE_DATA_DIRECTORY CLI_DIRECTORY;

    // Поле не используется в сборках. Заполняется нулями
    struct IMAGE_DATA_DIRECTORY STUB4;

    // Директория таблицы адресов импорта
    struct IMAGE_DATA_DIRECTORY IAT_DIRECTORY;

    // Поле не используется в сборках. Заполняется нулями
    struct IMAGE_DATA_DIRECTORY STUB3[6];

    // Директория релокации
    struct IMAGE_DATA_DIRECTORY BASE_RELOC_DIRECTORY;

    // Поле не используется в сборках. Заполняется нулями
    struct IMAGE_DATA_DIRECTORY STUB2[3];

    // Директория импорта
    struct IMAGE_DATA_DIRECTORY IMPORT_DIRECTORY;

    // Поле не используется в сборках. Заполняется нулями
    struct IMAGE_DATA_DIRECTORY STUB1;

    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    }OptHdr;

    CIL и системное программирование в Microsoft .NET

    57

    struct IMAGE_SECTION_HEADER { // Заголовок секции
    unsigned char Name[8];
    unsigned long VirtualSize;
    unsigned long VirtualAddress;
    unsigned long SizeOfRawData;
    unsigned long PointerToRawData;
    unsigned long PointerToRelocations;
    unsigned long PointerToLinenumbers;
    unsigned short NumberOfRelocations;
    unsigned short NumberOfLinenumbers;
    unsigned long Characteristics;
    };
    В свою очередь функция make_headers вызывает функцию
    make_headers_const, которая заполняет поля-константы, одинаковые во
    всех сборках.
    Для нашего учебного примера выберем расположение секции в файле, указанное на рис. 2.6.
    Как можно заметить, сгенерированная сборка .NET состоит из 3 секций:
    1. Секция «.text» (содержит тела методов и метаданные);
    2. Секция «.cli» (содержит точку входа, заголовок CLI, таблицу импорта);
    3. Секция «.reloc» (секция релокаций).
    Следовательно, после дополнительного заголовка в структуре HEADERS
    будут находиться 3 заголовка секций.
    Для сборок .NET необходимы 4 директории данных:
    1. Директория импорта;
    2. Директория релокации;
    3. Директория заголовка CLI;
    4. Директория таблицы адресов импорта.

    struct IMAGE_DATA_DIRECTORY { // Директория данных
    unsigned long
    RVA;
    unsigned long
    Size;
    };

    // Заголовок .text секции
    struct IMAGE_SECTION_HEADER TEXT_SECTION;
    // Заголовок .cli секции
    struct IMAGE_SECTION_HEADER CLI_SECTION;
    // Заголовок .reloc секции
    struct IMAGE_SECTION_HEADER RELOC_SECTION;
    };

    Структура программных компонентов

    RVA_OF_TEXT

    RVA

    Рис. 2.6. Схематичное расположение секций и заголовков

    Заголовок MS-DOS

    Заголовок PE

    Дополнительный заголовок PE

    Таблица секций

    Метаданные

    Тела методов

    Секция .text

    Точка входа (jmp _CorExeMain)

    Заголовок CLI

    Таблицы для импорта mscoree.dll

    Секция .cli

    Секция релокации

    CIL и системное программирование в Microsoft .NET

    RVA_OF_CLI

    На основе блока входных параметров вычисляется расположение
    секций в памяти. Вычисления осуществляются внутри набора макросов
    (см. таблицу 2.1).

    58

    RVA_OF_RELOC

    59

    Код функции align:
    #include
    unsigned long align(unsigned long x, unsigned long alignment)
    {
    div_t t = div(x,alignment);
    return t.rem == 0 ? x : (t.quot+1)*alignment;
    };

    Формат и назначение структуры CLI_SECTION_IMAGE описан в 2.1.6.3.

    Макрос:
    RVA_OF_RELOC(params)
    Описание:
    RVA секции «.reloc». Принимает в качестве аргумента блок входных параметров (INPUT_PARAMETERS)
    Подстановка:
    RVA_OF_CLI(params) + SIZEOF_CLI_M
    В свою очередь макрос SIZEOF_CLI_M определен как:
    align(sizeof(struct CLI_SECTION_IMAGE), SECTION_ALIGNMENT)

    Макрос:
    RVA_OF_CLI(params)
    Описание:
    RVA секции «.cli». Принимает в качестве аргумента блок входных параметров (INPUT_PARAMETERS)
    Подстановка:
    RVA_OF_TEXT +
    align(params->SizeOfMetadata, SECTION_ALIGNMENT)

    Макрос:
    RVA_OF_TEXT
    Описание:
    RVA секции «.text»
    Подстановка:
    align(sizeof(struct HEADERS), SECTION_ALIGNMENT)
    Код функции align приведен в конце таблицы.
    align – округляет первый аргумент в большую сторону до числа, кратного значению SECTION_ALIGNMENT.
    SECTION_ALIGNMENT – фиксированное выравнивание секций 0x2000

    Таблица 2.1. Описание макросов

    Структура программных компонентов

    CIL и системное программирование в Microsoft .NET

    2.1.6.3. Этап 3. Генерация секции «.cli»
    Всю работу на этом этапе выполняет функция make_cli_section. Прототип функции:
    void make_cli_section (FILE* file, PINPUT_PARAMETERS inP);
    В секции «.cli» содержится структура CLI_SECTION_IMAGE, в которой
    находится точка входа в приложение, заголовок CLI, таблица импорта и
    таблица адресов импорта:
    struct CLI_SECTION_IMAGE {
    struct _JMP_STUB {
    // Точка входа
    unsigned short JmpInstruction;
    unsigned long JmpAddress;
    }JMP_STUB;
    struct _CLI_HEADER { // Заголовок CLI
    unsigned long
    cb;
    unsigned short
    MajorRuntimeVersion;

    2.1.6.2. Этап 2. Генерация секции «.text»
    Функция, выполняющая работу на этом этапе – make_text_section.
    Прототип функции:
    void make_text_section (FILE* file, PINPUT_PARAMETERS inP);
    В секции «.text» находятся метаданные и тела методов. Сначала в памяти выделяется массив, кратный выравниванию в файле. Размер массива задается макросом SIZEOF_TEXT(params), который определен следующим
    образом:
    #define SIZEOF_TEXT(params)
    \
    align(params->SizeOfMetadata+params->SizeOfCilCode,
    \
    params->FileAlignment)
    Макрос принимает в качестве аргумента блок входных параметров.
    В выделенную память записываются метаданные из массива
    metadata и тела методов из массива cilcode, адреса которых передаются в
    функцию через поля inP->metadata и inP->cilcode блока входных параметров. Затем этот массив записывается в выходной файл сразу после заголовка HEADERS. Если размер метаданных и CIL-кода не кратен
    inP->FileAlignment, то разница дописывается нулями.

    В заключение структура HEADERS пишется в начало выходного файла,
    причем записывается количество байт, равное значению макроса
    SIZE_OF_HEADERS(params), который объявлен следующим образом:
    #define SIZEOF_HEADERS(params) \
    align(sizeof(struct HEADERS), params->FileAlignment)
    Обычно размер структуры HEADERS не кратен
    inP->FileAlignment, следовательно разница дописывается нулями.

    60

    61

    Поле JmpAddress заполняется значением выражения:
    RVA_OF_CLI(inP) + OFFSETOF(struct CLI_SECTION_IMAGE,IMPORT_TABLE.Hint)
    + inP->ImageBase;
    Заметим, что
    #define OFFSETOF(s,m) (size_t)&(((s *)0)->m).
    Таким образом, к абсолютному адресу секции «.cli» прибавляется
    смещение поля Hint в структуре CLI_SECTION_IMAGE.
    Сразу за точкой входа находится заголовок CLI, который служит для
    определения положения метаданных в PE-файле. В заголовке находится
    информация об RVA и размере метаданных, а также информация о версии
    CLR, для которой предназначена сборка и токен метаданных, указывающий на точку входа в сборку. У DLL токен точки входа равен 0, т.к. DLL
    не может сама выполнять какие-либо действия.

    };

    struct _IMPORT_TABLE {
    // Import Address Table
    unsigned long
    HintNameTableRVA2;
    unsigned long
    zero2;
    // Вход в таблицу импорта
    unsigned long
    ImportLookupTableRVA;
    unsigned long
    TimeDateStamp;
    unsigned long
    ForwarderChain;
    unsigned long
    NameRVA;
    unsigned long
    ImportAddressTableRVA;
    unsigned char
    zero[20];
    // Import Lookup Table
    unsigned long
    HintNameTableRVA1;
    unsigned long
    zero1;
    // Hint/Name Table
    unsigned short
    Hint;
    char
    Name[12];
    // Dll name (“mscoree.dll”)
    char
    DllName[12];
    }IMPORT_TABLE;

    unsigned short
    MinorRuntimeVersion;
    struct IMAGE_DATA_DIRECTORY MetaData;
    unsigned long
    Flags;
    unsigned long
    EntryPointToken;
    struct IMAGE_DATA_DIRECTORY NotUsed[6];
    }CLI_HEADER;

    Структура программных компонентов

    CIL и системное программирование в Microsoft .NET

    2.1.6.5. Метаданные и методы
    Если описать метаданные и методы сгенерированной сборки на CIL
    с использованием синтаксиса ILASM, то получится следующая IL-программа:

    2.1.6.4. Этап 4. Генерация секции «.reloc»
    Функция, ответственная за этот этап – make_reloc_section. Прототип
    данной функции:
    void make_reloc_section (FILE* file, PINPUT_PARAMETERS inP);
    Заключительная секция релокации содержит исправления для единственного абсолютного адреса в сборке, который находится в точке входа
    jmp dword ptr ds:[x] в секции «.cli». Адрес x надо исправить, если сборка
    грузится по адресу, отличному от базового. Сгенерированная секция
    «.reloc» содержит единственную структуру RELOC_SECTION, в которой есть
    все необходимые поля для исправления.
    Поле PageRVA содержит адрес страницы, в которой надо произвести
    исправление. Заполняется значением макроса RVA_OF_CLI. Поле BlockSize
    заполняется значением макроса SIZEOF_RELOC_NOTALIGNED, который определен так:
    #define SIZEOF_RELOC_NOTALIGNED sizeof(struct RELOC_SECTION).
    В сборках .NET в качестве типа исправления используется значение
    3. Смещение адреса x на странице равно 2, т.к. расположение секций в памяти выровнено по страницам:
    struct RELOC_SECTION
    {
    unsigned long PageRVA;
    // адрес страницы
    unsigned long BlockSize;
    // размер блока
    unsigned short TypeOffset;
    // тип исправления и
    // смещение на странице
    unsigned short Padding;
    // завершающие нули
    };
    Структура записывается в конец файла после секции «.cli». Чтобы
    размер файла был кратен inP->FileAlignment, в него дописывается определенное количество нулей.

    В конце работы функции структура CLI_SECTION_IMAGE пишется в выходной файл, сразу после секции «.text». Записывается количество байт,
    равное значению макроса SIZEOF_CLI, который имеет следующий вид:
    #define SIZEOF_CLI(params)
    \
    align(sizeof(struct CLI_SECTION_IMAGE), params->FileAlignment)
    Если
    структура
    CLI_SECTION_IMAGE
    не
    кратна
    inP->FileAlignment, то разница дописывается нулями.

    62

    63

    Метаданные, используемые при генерации сборки, находятся в массиве metadata, который в программе описан следующим образом (полное
    описание не приводится из-за его большого размера, полностью листинг
    массива metadata приводится в исходных текстах учебного примера):
    unsigned char metadata[] = {
    0x42, 0x53, 0x4A, 0x42, 0x01, 0x00, 0x01, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00,
    . . . . . . . . . . . . . . . . . . . . . . . .
    0x00, 0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00
    };
    В такой же форме в программе находится CIL-код методов:
    unsigned char cilcode[] = {
    0x56, 0x72, 0x01, 0x00, 0x00, 0x70, 0x28, 0x02,

    .assembly extern mscorlib
    {
    .ver 1:0:5000:0
    }
    .assembly arith
    {
    .hash algorithm 0x00008004
    .ver 1:0:1:1
    }
    .module arith.exe
    // MVID: {86612D1B-0333-4F08-A88A-857326D72DDF}
    .imagebase 0x11000000
    .subsystem 0x00000003
    .file alignment 4096
    .corflags 0x00000001
    // Image base: 0x02ef0000
    .method public static void calc() cil managed
    {
    .entrypoint
    // Code size
    21 (0x15)
    .maxstack 8
    IL_0000: ldstr “Hello”
    IL_0005: call void [mscorlib]System.Console::WriteLine(string)
    IL_000a: call string [mscorlib]System.Console::ReadLine()
    IL_000f: call void [mscorlib]System.Console::WriteLine(string)
    IL_0014: ret
    }

    Структура программных компонентов

    };

    0x00, 0x00, 0x0A, 0x28, 0x01, 0x00, 0x00, 0x0A,
    0x28, 0x02, 0x00, 0x00, 0x0A, 0x2A

    CIL и системное программирование в Microsoft .NET

    Метаданные служат для описания типов, находящихся в сборке
    .NET, и хранятся в исполняемых файлах. Для хранения метаданных используется достаточно сложный двоичный формат, изложение которого
    заняло бы слишком много времени, поэтому в этом разделе мы ограничимся лишь частичным и в большой степени поверхностным описанием
    формата метаданных.
    Если поставить себе цель в двух словах охарактеризовать формат метаданных в сборках .NET, то можно сказать следующее: он был бы значительно проще, если бы его разработчики не уделяли чрезмерного внимания вопросу компактности хранения метаданных. Дело в том, что спецификация
    формата определяет по нескольку способов кодирования одной и той же
    информации, описывает способы сжатия отдельных элементов метаданных
    (например, сигнатур методов) и, тем самым, оказывается загроможденной
    множеством деталей, затрудняющим разработку метаинструментов.
    Для того чтобы провести обзор формата метаданных в этом разделе,
    мы используем следующий прием: рассмотрим только те детали формата,
    которые используются в незатейливом примере, выводящем на экран надпись «Hello, World!», а затем ожидающим ввода данных с клавиатуры. Может показаться, что пример слишком прост, однако, как мы увидим в даль-

    2.2. Формат метаданных

    2.1.6.6. Пример работы программы
    Итак, попробуем запустить нашу программу, набрав в консоли
    pegen.exe (так будет называться наша программа):
    C:\>Pegen.exe
    Если все прошло успешно, то на экране мы увидим сообщение об успешной генерации сборки hello.exe:
    File: hello.exe generated
    Запустим сгенерированную сборку:
    C:\>hello.exe
    Программа распечатает на экране строку «Hello» и попросит ввести
    произвольный текст. Введем, например, строку:
    Hello Programm
    В результате программа распечатает на экране строку, введенную ранее, и закончит свою работу:
    Hello Programm

    64

    65

    В предыдущем разделе данной главы мы рассмотрели формат исполняемых файлов .NET и выяснили, что он дает разработчику достаточно
    большую свободу для размещения отдельных элементов внутри исполняемого файла. В частности, исполняемые файлы могут содержать несколько секций, и расположение этих секций, а также данных внутри них является более или менее произвольным.
    Давайте выберем для нашего учебного примера схему размещения
    данных внутри исполняемого файла, изображенную на рисунке 2.7. Мы
    будем использовать две секции: секция «.text» будет содержать всю основную информацию, а в секции «.reloc» будет размещена таблица релокаций.

    2.2.1. Расположение метаданных и кода внутри сборки

    // Завершаем выполнение
    ret
    }

    // Выводим введенную строку на экран
    call void [mscorlib]System.Console::WriteLine(string)

    // Ожидаем, пока пользователь введет строку
    call string [mscorlib]System.Console::ReadLine()

    // Выводим строку на экран
    call void [mscorlib]System.Console::WriteLine(string)

    нейшем, генерация метаданных даже для такого несложного примера требует больших усилий. Сборку .NET, соответствующую нашему примеру,
    можно получить, если откомпилировать следующую программу, записанную в синтаксисе ассемблера ILASM:
    .assembly HelloWorld
    {
    .hash algorithm 0x00008004
    .ver 1:0:1:1
    }
    .module HelloWorld.exe
    // hello() – единственный метод в нашей сборке
    .method public static void hello() cil managed
    {
    .entrypoint
    .maxstack 8
    // Загружаем строку “Hello, World!” на стек
    ldstr “Hello, World!”

    Структура программных компонентов

    Рис. 2.7. Размещение данных внутри исполняемого файла для
    учебного примера

    Заголовок MS-DOS

    Заголовок PE-файла

    Дополнительный заголовок PE-файла

    Таблица секций

    Тело метода hello

    Метаданные

    Таблицы для импорта из mscoree.dll
    Точка входа (jmp_CorExeMain)
    Заголовок CLI

    Секция .text

    Секция .reloc

    CIL и системное программирование в Microsoft .NET

    На схеме видно, что в начале секции «.text» располагается тело метода
    hello, за которым следуют метаданные. Напомним, что расположение метаданных задается в заголовке CLI, который мы рассматривали в предыдущем разделе данной главы. Тем самым, мы вольны выбрать для метаданных
    практически любое место внутри секции, и их расположение, изображенное на схеме, является лишь одним из многих возможных вариантов.
    Заметим, что метаданные и CIL-код практически не зависят от остальных элементов формата исполняемого файла. Они занимают часть
    сборки .NET, при этом заголовок CLI указывает на метаданные, а внутри
    метаданных хранятся RVA тел методов. Тело метода hello в нашем примере расположено в самом начале секции исключительно для того, чтобы
    облегчить вычисление RVA этого метода (RVA метода совпадает с RVA
    секции).

    66

    67

    Offset;
    Size;
    Name [x];

    каждого потока */
    StreamHeader

    struct MetadataRoot
    {
    long Signature;
    /* 0x424A5342 */
    ...
    unsigned short
    Streams;
    }

    /* Для
    struct
    {
    long
    long
    char
    }

    Потоки метаданных (metadata streams) предназначены для хранения
    определенных видов информации. Заголовок каждого потока метаданных
    представляет собой структуру, состоящую из трех полей:
    long Offset;
    Смещение потока метаданных относительно начала метаданных
    в файле (то есть относительно начала корня метаданных).
    long Size;
    Размер потока метаданных в байтах (должен быть кратен четырем).

    Рис. 2.8. Структура метаданных

    Корень метаданных

    Заголовки потоков метаданных

    Blob heap (#Blob)

    String heap (#Strings)

    User String heap (#US)

    GUID heap (#GUID)

    Таблицы метаданных

    На рисунке 2.8 представлена схема структуры метаданных. Метаданные начинаются с заголовка, называемого корнем метаданных (metadata
    root). Корень метаданных начинается с 32-разрядной сигнатуры
    0x424A5342. Если каждый байт сигнатуры рассматривать в виде ASCII-кода, то получится строка «BSJB», составленная из начальных букв имен четырех основных разработчиков .NET Framework: Брайана Харри (Brian
    Harry), Сьюзан Радке-Спроулл (Susan Radke-Sproull), Джейсона Зандера
    (Jason Zander) и Билла Эванса (Bill Evans). Далее в корне метаданных следует информация, относящаяся к версии .NET, а заканчивается корень метаданных 16-разрядным целым числом, содержащим количество так называемых потоков метаданных, заголовки которых располагаются непосредственно после корня метаданных.

    2.2.2. Структура метаданных

    Структура программных компонентов

    Потоки метаданных

    CIL и системное программирование в Microsoft .NET

    Имя потока
    “#GUID”

    ”#~”

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

    В спецификации CLI определены несколько десятков видов таблиц
    метаданных. Мы ограничимся рассмотрением только тех из них, которые
    используются в нашем учебном примере.
    На рис. 2.9 представлена структура потока таблиц метаданных для
    учебного примера. Поток таблиц начинается с заголовка таблиц, непосредственно после которого следуют сами таблицы.
    Заголовок таблиц метаданных содержит большое количество полей,
    из которых нас интересуют в первую очередь три поля:
    char HeapSizes;
    Различные биты этого поля задают размеры индексов, используемых для адресации куч метаданных. Бит 0 соответствует куче
    строк, бит 1 – куче GUID'ов, бит 3 – куче двоичных данных.

    2.2.3. Таблицы метаданных

    Таблицы метаданных

    Куча двоичных данных “#Blob”

    Куча пользовательских ”#US”
    строк
    Куча строк
    “#Strings”

    Поток
    Куча GUID'ов

    Таблица 2.2. Потоки метаданных

    char Name[x];
    ASCIIZ-строка, содержащая имя потока метаданных. Это поле
    имеет переменную длину.
    В спецификации CLI определено пять видов потоков метаданных.
    Четыре потока метаданных представляют собой так называемые кучи, то
    есть хранилища однородных объектов, таких как строки и GUID'ы, и один
    поток метаданных имеет реляционную структуру и содержит таблицы метаданных. В таблице 2.2 приведено описание каждого из пяти потоков.

    68

    8

    0

    16

    00000000

    24

    00000000

    32

    00000011

    00000000
    48

    40

    69

    2.2.3.1. Таблица сборок (Assembly – 0x20)
    В этой таблице содержится только одна запись, описывающая нашу
    сборку. В этой записи для нас интерес представляют следующие поля:

    Если некоторый бит установлен, это означает, что соответствующая куча адресуется 32-разрядными индексами. В противном
    случае куча адресуется 16-разрядными индексами.
    char Valid[8];
    Размер этого поля – 64 бита. При этом каждый бит соответствует
    одной таблице метаданных. Если некоторый бит установлен, значит, соответствующая ему таблица присутствует в метаданных.
    unsigned long Rows[7];
    Массив 32-разрядных целых чисел, содержащих количество записей в каждой из присутствующих таблиц метаданных. В нашем учебном примере этот массив имеет размер 7, так как мы
    используем семь таблиц.
    На рис. 2.10 представлено распределение элементов метаданных, используемых в учебном примере, по таблицам метаданных. При этом каждая таблица имеет порядковый номер, который соответствует номеру описывающего ее бита в массиве Valid.
    Давайте подробнее рассмотрим каждую из семи используемых в примере таблиц метаданных.

    56

    00000000

    struct MetadataTables
    {
    ...
    long HeapSizes;
    ...
    char Valid[8];
    ...
    unsigned long
    Rows[7];
    }
    00000000

    Рис. 2.9. Структура потока таблиц метаданных

    0000100

    01000111

    Заголовок таблиц

    Module Table – 0x00

    TypeRef Table 0x01

    TypeDef Table – 0x02

    Method Table – 0x06

    ModuleRef Table – 0x0A

    Assembly Table – 0x20

    AssemblyRef Table – 0x23

    Структура программных компонентов


    System.Console
    HelloWorld.exe

    TypeDef Table – 0x02

    TypeRef Table – 0x01

    Module Table – 0x00

    short MajorVersion;
    short MinorVersion;
    short BuildNumber;
    short RevisionNumber;
    Эти четыре поля хранят информацию о версии сборки.
    short Name;
    Это поле содержит индекс в куче строк, по которому хранится
    имя сборки («HelloWorld»).

    Рис. 2.10. Распределение элементов метаданных учебного примера
    по таблицам

    hello

    WriteLine

    Method Table – 0x06

    ReadLine

    HelloWorld

    Assembly Table – 0x20

    MemberRef Table – 0x0A

    mscorlib

    AssemblyRef Table – 0x23

    CIL и системное программирование в Microsoft .NET

    2.2.3.3. Таблица определенных в сборке типов (TypeDef – 0x02)
    В этой таблице каждая запись соответствует одному типу, объявленному в сборке. В нашем учебном примере нет классов, но для того чтобы

    2.2.3.2. Таблица модулей (Module – 0x00)
    Таблица модулей содержит только одну запись, описывающую модуль. В этой записи существенными являются два поля:
    short Name;
    Это поле содержит индекс в куче строк, по которому хранится
    имя модуля («HelloWorld.exe»).
    short Mvid;
    Это поле содержит индекс в куче GUID'ов, по которому хранится глобальный уникальный идентификатор модуля.

    70

    71

    2.2.3.5. Таблица импортируемых сборок (AssemblyRef – 0x23)
    Все импортируемые сборки должны быть перечислены в таблице импортируемых сборок. Каждая запись этой таблицы содержит следующие
    поля:
    short MajorVersion;
    short MinorVersion;
    short BuildNumber;
    short RevisionNumber;
    Эти четыре поля хранят информацию о версии импортируемой
    сборки.
    short Name;

    2.2.3.4. Таблица методов (Method – 0x06)
    Таблица методов описывает методы, объявленные в сборке. Каждая
    запись этой таблицы содержит информацию об одном методе, представленную в следующих полях:
    long RVA;
    RVA тела метода в исполняемом файле.
    short Flags;
    Набор флагов, задающих область видимости метода и другие его
    атрибуты.
    short Name;
    Это поле содержит индекс в куче строк, по которому хранится
    имя метода.
    short Signature;
    Индекс в куче двоичных данных, по которому расположена сигнатура метода.
    short ParamList;
    В этом поле хранится индекс в таблице описателей параметров
    метода.

    объявить глобальную функцию, нам требуется специальный абстрактный
    тип . Дело в том, что все глобальные функции и поля считаются
    принадлежащими этому типу. Запись, описывающая этот тип, содержит
    следующие интересующие нас поля:
    short Name;
    Это поле содержит индекс в куче строк, по которому хранится
    имя типа («»).
    short FieldList;
    short MethodList;
    Эти два поля содержат индексы в таблицах полей и методов, начиная с которых расположены описатели полей и методов типа.

    Структура программных компонентов

    Это поле содержит индекс в куче строк, по которому хранится
    имя импортируемой сборки (в нашем случае это «mscorlib»).

    CIL и системное программирование в Microsoft .NET

    В настоящее время большой популярностью пользуется компонентный подход к разработке программного обеспечения. Этот подход характеризуется тем, что создаваемый программный продукт состоит из взаимодействующих компонентов. При этом различные компоненты могут
    независимо разрабатываться разными группами программистов, и при создании каждого компонента может применяться наиболее подходящий
    язык программирования. В качестве примера можно привести Microsoft
    Visual Studio .NET и Microsoft Office.

    2.3. Взаимодействие программных компонентов

    2.2.3.7. Таблица членов импортируемых типов (MemberRef – 0x0A)
    В таблице членов импортируемых типов перечислены все методы,
    поля и свойства этих типов, которые используются в программе. Каждая
    запись этой таблицы содержит следующие поля:
    short Class;
    Специальным образом закодированная информация об импортируемом типе.
    short Name;
    Это поле содержит индекс в куче строк, по которому хранится
    имя члена импортируемого типа.
    short Signature;
    Индекс в куче двоичных данных, по которому расположена сигнатура члена импортируемого типа.

    2.2.3.6. Таблица импортируемых типов (TypeRef – 0x01)
    Каждая запись в этой таблице соответствует одному из импортируемых типов и содержит следующие поля:
    short ResolutionScope;
    Специальным образом закодированная информация о том, какой сборке или какому модулю принадлежит данный тип.
    short Name;
    Это поле содержит индекс в куче строк, по которому хранится
    имя импортируемого типа (в нашем случае это «Console»).
    short Namespace;
    Это поле содержит индекс в куче строк, по которому хранится
    имя пространства имен; данному пространству принадлежит
    импортируемый тип (в нашем случае это «System»).

    72

    73

    2.3.1.1. Библиотеки подпрограмм
    Наиболее древний способ заключается в использовании библиотек
    подпрограмм. Такие библиотеки создаются одной группой разработчиков,

    Существует множество способов и технологий организации компонентного программирования. Давайте проведем беглый обзор достижений
    в этой области.

    2.3.1. Обзор компонентных технологий

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

    Структура программных компонентов

    CIL и системное программирование в Microsoft .NET

    2.3.1.2. Открытые исходные тексты
    Еще один интересный путь создания компонентных программ придумали в мире открытых исходников, в котором компоненты распространяются в виде исходных текстов. Тем самым вроде бы решается часть проблем.
    По крайней мере, можно попытаться переносить эти компоненты с платформы на платформу (перекомпилируя исходники и исправляя возникающие ошибки). Кроме того, исходные тексты в отличие от двоичных файлов
    содержат информацию о типах, что позволяет использовать объектно-ориентированные возможности. Но и здесь нас подстерегают неприятности:
    • Если компоненты написаны на разных языках, то соединить их
    вместе не так-то просто. Отсюда проистекает тяга фанатов открытых исходников к превращению своих программ в набор маленьких утилит, написанных на C и соединенных посредством
    корявых и трудночитаемых скриптов.
    • Открытые компоненты почти всегда имеют так называемые «заразные» лицензии (например, GNU Public License – GPL). Та-

    а затем могут быть использованы другими программистами в других проектах. Исходный код библиотек может быть закрыт, то есть они могут распространяться в откомпилированном виде, защищая тем самым интересы
    разработчиков. Однако организация компонентного программирования
    на базе библиотек подпрограмм обладает следующими недостатками:
    • Двоичный код библиотеки жестко привязан к аппаратной платформе, так как содержит инструкции конкретного процессора. Это
    уменьшает переносимость программы, использующей библиотеку.
    • Как правило, библиотека рассчитана на использование конкретного компоновщика и может поддерживать ограниченное
    количество языков программирования, компиляторы которых
    генерируют объектные файлы в нужном формате и используют
    нужные соглашения о вызове подпрограмм.
    • Библиотеки подпрограмм плохо подходят для объектно-ориентированных языков, так как не содержат никакой информации
    о типах. Например, если вы разработаете библиотеку классов на
    C++, вам придется распространять вместе с ней заголовочные
    файлы, содержащие объявления классов.
    Динамические библиотеки некоторым образом облегчают компонентное программирование. Они поддерживаются операционной системой, и поэтому разработчики компиляторов вынуждены следовать требованиям, налагаемым операционной системой. То есть динамические библиотеки не зависят от причуд компиляторов и компоновщиков и могут использоваться для большего диапазона языков программирования. Однако
    в остальном они не лучше, чем обычные библиотеки подпрограмм.

    74

    75

    2.3.1.3. Технологии COM и CORBA
    Особого внимания заслуживают технологии COM (Component Object
    Model) и CORBA (Common Object Request Broker Architecture). Технология
    COM, разработанная корпорацией Microsoft, поддерживает все три вида
    взаимодействия компонентов, а именно: взаимодействие внутри адресного пространства процесса, межпроцессное взаимодействие и взаимодействие по сети. Технология CORBA ориентирована исключительно на взаимодействие компонентов по сети.
    Первое, с чем сталкивается программист при использовании COM или
    CORBA, это необходимость описывать метаданные на языке IDL (Interface
    Definition Language). В COM и CORBA используются несколько разные варианты языка IDL, но для нас сейчас это несущественно. Подобное неудобство объясняется тем, что эти технологии не подразумевают специальной
    поддержки в компиляторах языков программирования. Например, мы можем написать COM-объект на языке C++, но ничего не знающий о COM
    компилятор C++, естественно, не сгенерирует никаких метаданных! Вот
    поэтому метаданные нужно отдельно описывать на языке IDL.
    Технологии COM и CORBA определяют двоичный стандарт для передачи данных между компонентами. Так как компоненты могут быть написаны на разных языках и, соответственно, внутреннее представление данных в них может существенно различаться, то компонент, осуществляющий передачу, вынужден выполнять преобразование передаваемых данных в стандартное представление (маршалинг), а компоненту, принимающему данные, приходится выполнять преобразование данных из стандартного представления к своему внутреннему представлению (демаршалинг). При межпроцессном взаимодействии и взаимодействии по сети затраты на преобразование данных не играют существенной роли, но при
    взаимодействии внутри адресного пространства одного процесса они могут сильно сказаться на производительности программы.
    На рис. 2.11 изображена схема взаимодействия двух объектов при использовании технологии COM или CORBA. Объекты Client и Server находятся в разных компонентах, поэтому объект Client не может непосредственно передать сообщение объекту Server. Вместо этого он обращается к
    специальному объекту-заглушке ClientStub, который осуществляет упаковку сообщения и затем передает его информационной магистрали
    (в терминах CORBA информационная магистраль называется ORB –
    Object Request Broker). Информационная магистраль передает сообщение

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

    Структура программных компонентов

    Client
    Stub

    магистраль

    Информационная
    Server
    Stub

    Server

    Компонент2

    Рис. 2.11. Взаимодействие двух объектов через COM или CORBA

    Client

    Компонент1

    CIL и системное программирование в Microsoft .NET

    Организация компонентного программирования на платформе .NET
    необычайно проста и удобна для программиста. Давайте обсудим, какие
    особенности .NET обеспечивают это удобство.
    Во-первых, компилятор любого языка программирования, реализованного на платформе .NET, генерирует сборки, содержащие CIL-код и
    метаданные. При этом формат этих сборок определяется спецификацией
    CLI и не зависит от языка программирования.
    Во-вторых, любая программа, работающая в среде .NET, использует
    общую систему типов CTS. Поэтому представление данных в памяти определяется CTS и не зависит от языка программирования, на котором написана программа.

    2.3.2. Взаимодействие компонентов в среде .NET

    серверному объекту-заглушке ServerStub, который распаковывает сообщение и вызывает соответствующий метод объекта Server. Результат, возвращаемый методом объекта Server, совершает обратный путь к объекту
    Client аналогичным образом. Данный пример показывает, что использование технологий COM и CORBA связано с большими трудностями. Ситуация, пожалуй, облегчается лишь тем, что объекты-заглушки могут быть
    сгенерированы автоматически на основе описания объекта Server на языке IDL (для этого существуют специальные компиляторы).
    Хотя технологии COM и CORBA продолжают активно применяться,
    есть все основания утверждать, что в самом ближайшем будущем они будут вытеснены более эффективными и удобными технологиями (например, .NET).

    76

    77

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

    Компоненты на платформе .NET представляют собой сборки .NET.
    Сборка .NET может статически импортировать любую другую сборку и
    свободно использовать типы, экспортируемые из этой сборки. Для этого
    информация об импортируемой сборке заносится в таблицу метаданных
    AssemblyRef, информация о каждом импортируемом типе – в таблицу
    TypeRef, а информация о каждом импортируемом методе и поле – в таблицу MemberRef. Кроме того, сборка может импортироваться динамически через рефлексию (мы рассмотрим эту возможность в пункте 4.4.2).

    Server

    Компонент2

    Рис. 2.12. Взаимодействие двух объектов в среде .NET

    Client

    Компонент1

    Common Language Runtime

    В-третьих, выполнение любой программы управляется средой выполнения CLR. Это означает, что среда выполнения контролирует JITкомпиляцию CIL-кода программы, выполняет управление памятью и в
    каждый момент времени имеет всю информацию о состоянии программы.
    Эти три особенности платформы .NET позволяют среде выполнения
    автоматически обеспечивать взаимодействие компонентов вне зависимости от того, на каком языке они написаны.
    На рис. 2.12 изображена схема взаимодействия двух объектов на
    платформе .NET. Объекты Client и Server находятся в разных компонентах, работающих в адресном пространстве одного процесса (здесь мы
    не рассматриваем межпроцессное взаимодействие, а также взаимодействие по сети). Передача сообщения от объекта Client объекту Server сводится к простому вызову соответствующего метода объекта Server, то есть
    в среде .NET не нужны никакие объекты-заглушки.

    Структура программных компонентов

    CIL и системное программирование в Microsoft .NET

    0x00000004

    0x00000005

    0x00000006

    0x00000007

    NestedFamily

    NestedAssembly

    NestedFamAndAssem

    NestedFamOrAssem

    Описание
    Тип не экспортируется из сборки
    Тип экспортируется из сборки
    Вложенный тип доступен везде
    Вложенный тип доступен только
    внутри того типа, в который он
    вложен
    Вложенный тип доступен наследникам того типа, в который
    он вложен
    Вложенный тип доступен везде
    внутри сборки
    Вложенный тип доступен наследникам того типа, в который
    он вложен, но только внутри
    сборки
    Вложенный тип доступен везде
    внутри сборки, и, кроме того,
    наследникам того типа, в который он вложен

    2.3.2.2. Пример межъязыкового взаимодействия
    Рассмотрим учебный пример, который представляет собой компонентную систему, написанную сразу на четырех языках. Диаграмма классов примера дана на рисунке 2.13.
    Абстрактный класс SortedArray реализован на Visual Basic .NET.
    В этом классе определено поле Arr, представляющее собой массив целых
    чисел. Конструктор класса SortedArray копирует в это поле массив, передаваемый ему в качестве параметра, а затем вызывает абстрактный метод

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

    Значение
    0x00000000
    0x00000001
    0x00000002
    0x00000003

    Флаг
    NotPublic
    Public
    NestedPublic
    NestedPrivate

    Таблица 2.3. Флаги видимости для типов

    Видимость типа (т.е. экспортируется он из сборки или нет) определяется флагом видимости и хранится в поле Flags соответствующей этому
    типу записи в таблице метаданных TypeDef. В таблице 2.3 приведен набор
    флагов видимости для типов.

    78

    0x00000003
    0x00000004
    0x00000005
    0x00000006

    Assembly
    Family
    FamOrAssem
    Public

    79

    InsertSortedArray
    +.ctor()
    #Sort()

    Public MustInherit Class SortedArray
    Protected Arr() As Integer
    Protected MustOverride Sub Sort()
    Public Sub New(ByVal A() As Integer)
    Dim i As Integer
    Arr = New Integer(A.Length – 1) {}

    Sort() для сортировки этого массива. Для доступа к отсортированному
    массиву используются свойства Array и Count:

    Main
    +main()

    Описание
    Доступ контролируется компилятором
    Доступен только внутри типа
    Доступен наследникам типа,
    объявленным внутри сборки
    Доступен только внутри сборки
    Доступен наследникам типа
    Доступен внутри сборки, а также
    наследникам типа
    Доступен везде

    Рис. 2.13. Диаграмма классов учебного примера

    BubleSortedArray
    +.ctor()
    #Sort()

    SortedArray
    #Arr
    +.ctor()
    #Sort
    +get_Array():Integer
    +get_Count():Integer

    0x00000001
    0x00000002

    Значение
    0x00000000

    Private
    FamAndAssem

    Флаг
    CompilerControlled

    Таблица 2.4. Флаги доступа для членов типа

    Структура программных компонентов

    End Class

    For i = 0 To A.Length – 1
    Arr(i) = A(i)
    Next
    Sort()
    End Sub
    Default Public ReadOnly Property
    Array (ByVal Index As Integer) As Integer
    Get
    Return Arr(Index)
    End Get
    End Property
    Public ReadOnly Property Count() As Integer
    Get
    Return Arr.Length
    End Get
    End Property

    CIL и системное программирование в Microsoft .NET

    public __gc class BubbleSortedArray: public SortedArray
    {
    protected:
    void Sort()
    {
    for (int i = Arr->Length, flag = 1; i > 1 && flag; i--)
    {
    flag = 0;
    for (int j = 0; j < i-1; j++)
    if (Arr[j] < Arr[j+1])
    {
    int tmp = Arr[j];
    Arr[j] = Arr[j+1];
    Arr[j+1] = tmp;
    flag = 1;
    }
    }
    }

    Класс BubbleSortedArray написан на Visual C++ with Managed
    Extensions. Он переопределяет абстрактный метод Sort(), реализуя в нем
    пузырьковую сортировку:
    using namespace VBLib;

    80

    81

    public:
    BubbleSortedArray(int A __gc []): SortedArray(A) { }
    };
    Класс InsertSortedArray написан на Visual C#. Он переопределяет абстрактный метод Sort(), реализуя в нем сортировку вставками.
    using VBLib;
    public class InsertSortedArray: SortedArray
    {
    protected override void Sort()
    {
    for (int i = 0; i < Arr.Length-1; i++)
    {
    int max = i;
    for (int j = i+1; j < Arr.Length; j++)
    if (Arr[j] > Arr[max])
    max = j;
    int tmp = Arr[i];
    Arr[i] = Arr[max];
    Arr[max] = tmp;
    }
    }
    public InsertSortedArray(int[] A): base(A) { }
    }
    И, наконец, все вышеперечисленные классы используются в программе, написанной на Visual J#.
    package JsApp;
    import VBLib.SortedArray;
    public class Main
    {
    public static void main(String[] args)
    {
    int A[] = new int[] { 5, 1, 6, 0, -4, 3 };
    SortedArray SA1 = new BubbleSortedArray(A),
    SA2 = new InsertSortedArray(A);
    for (int i = 0; i < SA1.get_Count(); i++)
    System.out.print(“”+SA1.get_Array(i)+” “);
    System.out.println();
    for (int i = 0; i < SA2.get_Count(); i++)
    System.out.print(“”+SA2.get_Array(i)+” “);
    System.out.println();
    }
    }

    Структура программных компонентов

    CIL и системное программирование в Microsoft .NET

    Различные языки программирования, которые уже реализованы или
    могут быть реализованы на платформе .NET, используют общую систему
    типов (CTS) в качестве модели данных. Общая система типов поддерживает все типы, с которыми работают распространенные в настоящее время
    языки программирования, но не каждый язык поддерживает все типы, реализованные в общей системе типов. Например, Visual Basic допускает типизированные ссылки в качестве параметров методов, и эти типизированные ссылки реализованы в общей системе типов, но C# их не понимает и,
    соответственно, не может использовать методы с такими параметрами.
    Для того, чтобы любую библиотеку классов можно было использовать из любого языка платформы .NET, разработчики .NET придумали общую спецификацию языков (Common Language Specification – CLS).
    В этой спецификации оговариваются правила, которым должны следовать
    разработчики языков и библиотек. То есть она описывает некоторое подмножество общей системы типов, и если некий язык реализует хотя бы это
    подмножество, а библиотека использует только входящие в это подмножество типы, то такая библиотека может быть использована из этого языка.
    В терминологии CLS библиотеки, соответствующие спецификации
    CLS, называются средами (frameworks), но мы будем называть их CLS-библиотеками. Компиляторы, генерирующие код, из которого можно получить доступ к CLS-библиотекам, называются потребителями (consumers).
    Компиляторы, которые являются потребителями, но, к тому же, способны
    создавать новые CLS-библиотеки, называются расширителями (extenders).
    Приведем основные правила общей спецификации языков:
    1. Спецификация распространяется только на доступные извне
    части экспортируемых из библиотеки типов. То, что остается
    внутри библиотеки, может использовать любые типы, не оглядываясь на спецификацию.
    2. Упакованные типы-значения, неуправляемые указатели и типизированные ссылки не должны использоваться. Дело в том, что
    отнюдь не во всех языках, реализованных на платформе .NET,
    есть соответствующие понятия, и, более того, добавление этих
    понятий во все языки представляется нецелесообразным.
    3. Регистр букв в идентификаторах не имеет значения. Это правило объясняется тем, что в некоторых языках программирования
    (например, в Паскале) регистр букв не различается.
    4. Методы не должны иметь переменного количества параметров.
    5. Глобальные поля и методы не поддерживаются спецификацией.
    6. Объекты исключений должны наследовать от System.Exception.

    2.3.3. Общая спецификация языков

    82

    83

    Тело метода в сборке .NET закодировано в виде потока инструкций языка CIL. Поток инструкций представляет собой массив байт, в котором размещены последовательности байт, кодирующие каждую инструкцию. При этом
    инструкции размещаются последовательно друг за другом без промежутков.
    Если сравнить поток инструкций CIL с потоком инструкций обычного процессора, можно заметить одно очень существенное отличие. Дело в том, что обычный процессор занимается непосредственным выполнением инструкций, то есть в каждый конкретный момент времени его
    интересует только та инструкция, которую он в этот момент выполняет.
    Это означает, что поток инструкций для обычного процессора может содержать последовательности байт, не являющиеся правильными кодами
    инструкций, при условии, что на эти последовательности никогда не будет
    передано управление. Такие «неправильные» последовательности зачастую представляют собой некоторые данные (или места, зарезервированные для данных), используемые программой. Так как поток инструкций
    CIL предназначен для JIT-компиляции, он не может содержать «непра-

    3.1.1. Формат потока инструкций

    Язык CIL (Common Intermediate Language) является независимым от
    аппаратной платформы объектно-ориентированным ассемблером, используемым на платформе .NET для представления исполняемого кода. Для выполнения сборки .NET содержащийся в ней CIL-код переводится JIT-компилятором, входящим в состав CLR, в код конкретного процессора.
    Инструкции CIL можно разделить на четыре основные группы.
    В первую группу входят инструкции общего назначения, которые служат
    для организации вычислений. Вторая группа содержит инструкции для
    работы с объектной моделью. Третья служит для генерации и обработки
    исключений. В четвертую мы относим неверифицируемые инструкции,
    которые генерируются, главным образом, компилятором языка C для
    представления небезопасных конструкций языка.
    Разработчики набора инструкций CIL уделили большое внимание
    компактности CIL-кода. Для этого в набор инструкций было введено
    большое количество сокращенных вариантов инструкций, представляющих собой частные случаи других инструкций и кодирующихся меньшим
    количеством байт.

    3.1. Поток инструкций языка CIL

    Глава 3.
    Common Intermediate Language

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    Размер в байтах Описание
    0
    У некоторых инструкций встроенные
    операнды отсутствуют
    int8
    1
    Знаковое 8-битовое целое число
    int32
    4
    Знаковое 32-битовое целое число
    int64
    8
    Знаковое 64-битовое целое число
    unsigned int8 1
    Беззнаковое 8-битовое целое число
    unsigned int16 2
    Беззнаковое 16-битовое целое число
    float32
    4
    32-битовое число с плавающей запятой
    float64
    8
    64-битовое число с плавающей запятой
    token
    4
    Токен метаданных
    switch
    переменный
    Массив адресов переходов для инструкции switch

    Операнд
    none

    Таблица 3.1. Варианты встроенных операндов инструкций CIL

    3.1.1.1. Формат инструкции
    Последовательность байт, кодирующая инструкцию CIL, начинается
    с кода инструкции. Часто используемые инструкции имеют однобайтовые
    коды. Инструкции, которые используются реже, имеют двухбайтовые коды (при этом первый байт всегда равен 0xFE).
    В разделе, посвященном виртуальной системе выполнения VES, говорилось о том, что операнды инструкций CIL размещаются на стеке вычислений. Тем не менее, многие инструкции имеют дополнительные встроенные
    операнды (inline operands), которые находятся прямо в потоке инструкций.
    Например, инструкция ldloc, загружающая на стек вычислений значение
    локальной переменной, имеет встроенный операнд, задающий номер переменной. А инструкция call, вызывающая метод, имеет встроенный операнд,
    задающий токен метаданных, по которому можно найти описание вызываемого метода. Встроенные операнды размещаются в потоке инструкций сразу после кода инструкции. В таблице 3.1 перечислены все варианты встроенных операндов. Для кодирования встроенных операндов, занимающих более
    одного байта и не являющихся токенами метаданных, используется порядок
    байт, при котором младший байт идет первым («little-endian»).

    вильных» последовательностей байт. То есть даже если поток инструкций
    CIL содержит «мертвые» участки, которые никогда не получат управление, эти участки должны представлять собой правильную последовательность инструкций CIL.
    Разные инструкции CIL кодируются последовательностями байт различной длины. Размер каждой инструкции, а также порядок и смысл составляющих ее байт определяется описанием инструкции, которое можно найти в [3].

    84

    85

    Особого внимания заслуживает встроенный операнд для инструкции
    switch. Эта инструкция осуществляет множественный условный переход в
    зависимости от некоторого целого значения, которое берется из стека вычислений. Ее встроенный операнд представляет собой массив адресов переходов. Он кодируется следующим образом: сначала идет 32-разрядное
    целое число без знака, обозначающее количество адресов переходов (размер массива), затем следуют сами адреса. При этом каждый адрес кодируется в виде 32-разрядного целого числа со знаком.
    Рассмотрим примеры кодирования инструкций CIL:
    1. Инструкция ldarg.0 загружает на стек вычислений значение
    первого аргумента метода. Она является сокращенной версией
    инструкции ldarg, не содержит встроенных операндов и имеет
    код 0x02:
    /* 02 */ ldarg.0
    2. Инструкция arglist загружает на стек вычислений специальный описатель массива переменных параметров метода. Она
    не содержит встроенных операндов и имеет двухбайтовый код
    0xFE 0x00:
    /* FE 00 */ arglist
    3. Инструкция ldc.i4.s 16 загружает на стек вычислений целочисленную константу 16. Она является сокращенной версией инструкции ldc.i4, имеет код 0x1F и содержит встроенный операнд
    типа int8:
    /* 1F | 10 */ ldc.i4.s 16
    4. Инструкция ldc.r4 1.0 загружает на стек вычисления число 1.0
    (константу с плавающей запятой). Она имеет код 0x22 и содержит встроенный операнд типа float32:
    /* 22 | 0000803F */ ldc.r4 1.0
    5. Инструкция isinst System.String служит для динамической
    проверки типа объекта на стеке вычислений. Она имеет код 0x75
    и содержит встроенный операнд типа token, в котором хранится
    токен метаданных, указывающий на тип:
    /* 75 | (02)00000F */ isinst System.String
    В скобки помещен первый байт токена метаданных, обозначающий номер таблицы метаданных. Обратите внимание, что значение токена метаданных для типа System.String в различных
    сборках может отличаться.
    6. Инструкция call System.String::Compare вызывает метод. Ее
    встроенный операнд содержит токен метаданных, указывающий на описание вызываемого метода:
    /* 28 | (06)0000CD */ call System.String::Compare

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    3.1.1.3. Ограничения на последовательности инструкций
    В спецификации языка CIL в описании каждой инструкции указаны
    условия, при которых допустимо ее использование. Кроме того, на формирование последовательности инструкций наложен ряд ограничений, позволяющих упростить создание JIT-компилятора. Наличие этих ограничений означает, что в программах допускаются не любые сочетания инструкций, а только те сочетания, которые удовлетворяют некоторым условиям.
    Упрощение JIT-компилятора благодаря введению ограничений на
    последовательности инструкций достигается, главным образом, за счет
    того, что эти ограничения позволяют использовать в JIT-компиляторе однопроходный алгоритм перевода CIL-кода в код процессора.

    3.1.1.2. Адреса переходов
    В состав набора инструкций CIL входят инструкции для организации
    условных и безусловных переходов. Встроенные операнды этих инструкций содержат адреса переходов. При этом допустимы только такие адреса,
    которые указывают на первые байты инструкций в теле данного метода.
    Мы будем называть абсолютным адресом инструкции смещение первого байта инструкции относительно начала потока инструкций. Инструкцию, на которую передается управление в результате выполнения инструкции перехода, назовем целью перехода.
    В качестве адресов перехода используются не абсолютные адреса целей перехода, а так называемые относительные адреса. Относительный адрес является разностью абсолютного адреса цели перехода и абсолютного
    адреса инструкции, непосредственно следующей за инструкцией перехода. Для того чтобы лучше понять принцип вычисления адресов перехода,
    обратимся к следующему примеру:
    ...
    target_addr: add
    ; цель перехода
    ...
    br rel_addr ; инструкция перехода
    next_addr: ...
    Здесь используется инструкция безусловного перехода br. При этом в
    качестве цели перехода выступает инструкция add, расположенная по абсолютному адресу target_addr. Если инструкция, следующая за инструкцией br, имеет абсолютный адрес next_addr, то адрес перехода rel_addr
    вычисляется следующим образом:
    reladdr := target_addr – next_addr
    Адреса переходов кодируются во встроенных операндах инструкций
    перехода в виде 8-битных или 32-битных целых чисел со знаком. При этом
    8-битные адреса используются в сокращенных вариантах инструкций перехода.

    86

    87

    Давайте сформулируем эти ограничения:
    1. Ограничение на структуру стека вычислений.
    Структура стека вычислений определяется количеством и типами значений, лежащих на стеке.
    Для любой инструкции, входящей в поток инструкций, структура стека должна быть постоянной вне зависимости от того, из
    какого места программы на нее передается управление.
    2. Ограничение на размер стека вычислений.
    В заголовке метода должна быть указана максимальная глубина
    стека вычислений. Другими словами, максимальное количество
    значений, которое может размещаться на стеке вычислений в
    процессе выполнения метода, должно быть заранее известно
    еще до JIT-компиляции этого метода.
    Это ограничение, на первый взгляд, может показаться несколько странным, если принять во внимание, что благодаря ограничению 1 JIT-компилятор может легко вычислить максимальную
    глубину стека в процессе компиляции. Цель введения подобного ограничения состоит в том, чтобы JIT-компилятор, приступая к компиляции метода, мог сразу выделить нужное количество памяти под свои внутренние структуры данных.
    3. Ограничение на обратные переходы.
    Если при последовательном переборе инструкций, формирующих тело метода, JIT-компилятор встречает инструкцию, расположенную сразу за инструкцией безусловного перехода, и если на эту инструкцию еще не было перехода, то JIT-компилятор
    не может вычислить структуру стека вычислений для этой инструкции. В этом случае он предполагает, что стек для этой инструкции должен быть пуст.
    Чтобы лучше понять данную ситуацию, рассмотрим пример:
    ...
    br L2
    L1: ldc.0 ; здесь стек считается пустым
    ...
    L2: br L1 ; обратный переход на L1
    ...
    Когда JIT-компилятор доходит до инструкции ldc.0, расположенной непосредственно после инструкции безусловного перехода br
    L2, он не может определить для нее структуру стека вычислений,
    так как еще не дошел до того места программы, откуда на нее передается управление. В принципе, просканировав дальше программу, это место можно обнаружить (это инструкция br L1), но
    тогда алгоритм JIT-компилятора должен быть многопроходным.

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    3.2.1.2. Работа с переменными и параметрами методов
    Локальные переменные и параметры методов имеют номера от 0 до
    65534. Существуют три варианта инструкций для работы с переменными и
    параметрами:
    • сокращенные инструкции, которые работают с переменными и
    параметрами, имеющими номера от 0 до 3;
    • сокращенные инструкции, допускающие номера переменных и
    параметров от 0 до 255;
    • обычные инструкции, работающие с любыми переменными и
    параметрами.
    В таблице 3.3 перечислены инструкции, выполняющие загрузку значений переменных и параметров на стек вычислений. Все они имеют следующую диаграмму стека:
    ... -> ... , value
    Кроме инструкций, загружающих значения переменных и параметров, существуют инструкции, загружающие на вершину стека вычислений
    адреса переменных и параметров (см. таблицу 3.4). Загружаемые адреса
    имеют тип управляемых указателей. Диаграмма стека для этих инструкций
    выглядит следующим образом:
    ... -> ... , address

    3.2.1.1. Загрузка констант
    Эта группа инструкций (см. таблицу 3.2) служит для загрузки константных значений на стек вычислений. При этом значения кодируются в
    самих инструкциях в виде их кодов или встроенных операндов.
    Диаграмма стека для всех инструкций этой группы выглядит следующим образом:
    ... -> ... , constant

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

    3.2.1. Инструкции для загрузки и сохранения значений

    В этом разделе мы рассмотрим ту часть инструкций языка CIL, которая служит для организации вычислений, а именно:
    • инструкции для загрузки и сохранения значений;
    • арифметические инструкции;
    • инструкции для организации передачи управления.

    3.2. Язык CIL: инструкции общего назначения

    88

    float64

    Инструкция Встроенный
    операнд
    0x02 – 0x05 ldarg.0 –

    ldarg.3
    0x06 – 0x09 ldloc.0 —

    ldloc.3
    0x0E
    ldarg.s
    unsigned
    int8
    0x11
    ldloc.s
    unsigned
    int8
    0xFE 0x09 ldarg
    unsigned
    int16
    0xFE 0x0C ldloc
    unsigned
    int16

    Код

    ldc.r8

    0x23

    int32
    int64
    float32

    Загрузка константы null
    Загрузка целого числа -1 (int32)
    Загрузка целых чисел
    от 0 до 8 (int32)
    Загрузка целых чисел от -128 до
    127 (int32)
    Загрузка целых чисел (int32)
    Загрузка целых чисел (int64)
    Загрузка чисел с плавающей запятой (F)
    Загрузка чисел с плавающей запятой (F)

    Описание

    89

    Загрузка параметров с номерами
    от 0 до 3
    Загрузка локальных переменных
    с номерами от 0 до 3
    Загрузка параметров с номерами
    от 0 до 255
    Загрузка локальных переменных
    с номерами от 0 до 255
    Загрузка параметров с номерами
    от 0 до 65534
    Загрузка локальных переменных
    с номерами от 0 до 65534

    Описание

    Таблица 3.3. Инструкции для загрузки параметров и локальных
    переменных

    ldc.i4
    ldc.i8
    ldc.r4

    0x20
    0x21
    0x22

    Инструкция Встроенный
    операнд
    0x14
    ldnull

    0x15
    ldc.m1

    0x16 – 0x1E ldc.0 –

    ldc.8
    0x1F
    ldc.s
    int8

    Код

    Таблица 3.2. Инструкции для загрузки констант

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    ldloca

    0xFE 0x0D

    unsigned
    int16
    unsigned
    int16

    Загрузка адресов параметров с
    номерами от 0 до 255
    Загрузка адресов локальных
    переменных с номерами
    от 0 до 255
    Загрузка адресов параметров
    с номерами от 0 до 65534
    Загрузка адресов локальных
    переменных с номерами
    от 0 до 65534

    Описание

    Инструкция Встроенный Описание
    операнд
    0x0A – 0x0D stloc.0 –

    Сохранение значений
    stloc.3
    в локальных переменных
    с номерами от 0 до 3
    0x10
    starg.s
    unsigned
    Сохранение значений
    int8
    в параметрах с номерами
    от 0 до 255
    0x13
    stloc.s
    unsigned
    Сохранение значений
    int8
    в локальных переменных
    с номерами от 0 до 255
    0xFE 0x0B starg
    unsigned
    Сохранение значений
    int16
    в параметрах с номерами
    от 0 до 65534
    0xFE 0x0E stloc
    unsigned
    Сохранение значений
    int16
    в локальных переменных
    с номерами от 0 до 65534

    Код

    Таблица 3.5. Инструкции для сохранения значений в параметрах и
    локальных переменных

    Инструкции, представленные в таблице 3.5, выполняют сохранение
    значения на вершине стека в переменную или параметр. Они имеют следующую диаграмму стека:
    ... , value -> ...

    ldarga

    0xFE 0x0A

    0x12

    Инструкция Встроенный
    операнд
    ldarga.s
    unsigned
    int8
    ldloca.s
    unsigned
    int8

    Таблица 3.4. Инструкции для загрузки адресов параметров и локальных переменных

    0x0F

    Код

    90

    91

    Арифметические инструкции можно разделить на четыре категории:
    • бинарные операции;
    • унарные операции;

    3.2.2. Арифметические инструкции

    3.2.1.4. Специальные инструкции для работы со стеком
    В отличие от «железных» стековых процессоров, CLI не содержит
    развитой системы инструкций для чисто стековых манипуляций. В таблице 3.8 представлены две имеющиеся в наличии инструкции.

    3.2.1.3. Косвенная загрузка и сохранение значений
    При косвенной загрузке и сохранении значений работа с памятью
    осуществляется через адреса (управляемые и неуправляемые указатели).
    Особенностью инструкций данной группы является наличие разных
    инструкций для работы со значениями разных типов. Причина в том, что
    при загрузке или сохранении значения бывает необходимо выполнить его
    преобразование к другому типу, а так как JIT-компилятор в процессе компиляции не собирает информацию о типах управляемых указателей, ему
    надо явно указывать тип загружаемых и сохраняемых значений. Необходимость выполнения преобразований объясняется тем, что не все примитивные типы могут находиться на стеке вычислений (поэтому, например,
    значение типа int8 при загрузке на стек расширяется до int32).
    В таблице 3.6 перечислены инструкции для косвенной загрузки значений. Обратите внимание, что инструкции ldind.i8 и ldind.u8 являются
    псевдонимами (имеют один и тот же код). Дело в том, что загрузка любых
    64-разрядных целых значений на стек не вызывает их преобразования,
    ибо хотя на стеке не предусмотрено наличие беззнаковых 64-разрядных
    значений, их загрузка все равно сводится к простому побитовому копированию. Вышесказанное справедливо также и для 32-разрядных целых
    значений, но для их загрузки зачем-то зарезервировано сразу две инструкции.
    Диаграмма стека для инструкций косвенной загрузки выглядит следующим образом:
    ... , address -> ... , value
    Инструкций для косвенного сохранения значений (см. таблицу 3.7)
    меньше, чем инструкций для косвенной загрузки (можно заметить, что
    инструкции для сохранения значений беззнаковых целых типов отсутствуют). Причина в том, что сохранение беззнаковых целых ничем не отличается от сохранения знаковых целых.
    Диаграмма стека для инструкций косвенного сохранения выглядит
    следующим образом:
    ... , address , value -> ...

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    • инструкция ckfinite, проверяющая конечность значений с плавающей точкой;
    • инструкции преобразования значений.

    3.2.2.1. Бинарные арифметические операции
    Бинарные арифметические операции потребляют со стека вычислений два операнда. Соответственно, диаграмма стека для таких операций
    выглядит следующим образом:
    ... , value1 , value2 -> ... , result
    Действие бинарных операций можно записать как
    result := value1 op value2,

    0x50

    0x4F

    0x4E

    0x4D

    0x4C

    0x4B

    0x4A

    0x49

    0x48

    0x47

    Инструкция Встроенный Описание
    операнд
    ldind.i1

    Косвенная загрузка значения
    int8
    ldind.u1

    Косвенная загрузка значения
    unsigned int8
    ldind.i2

    Косвенная загрузка значения
    int16
    ldind.u2

    Косвенная загрузка значения
    unsigned int16
    ldind.i4

    Косвенная загрузка значения
    int32
    ldind.u4

    Косвенная загрузка значения
    unsigned int32
    ldind.i8

    Косвенная загрузка значения
    (ldind.u8)
    int64 и unsigned int64
    ldind.i

    Косвенная загрузка значения
    native int
    ldind.r4

    Косвенная загрузка значения
    float32
    ldind.r8

    Косвенная загрузка значения
    float64
    ldind.ref

    Косвенная загрузка объектной
    ссылки

    Таблица.3.6. Инструкции для косвенной загрузки значений

    0x46

    Код

    92

    Инструкция Встроенный Описание
    операнд
    dup

    Копирование значения на вершине стека:... , value -> ... ,
    value, value
    pop

    Удаление значения с вершины
    стека:... , value -> ...
    то есть например, если op соответствует операции вычитания, то из value1
    вычитается value2.
    Некоторые бинарные операции могут использоваться для операндов
    различных типов. Другими словами, в коде инструкции не содержится информации о типах ее операндов, так как эти типы определяются на этапе
    JIT-компиляции. Поэтому, например, одну и ту же инструкцию add можно
    использовать для сложения как двух целых чисел, так и двух чисел с плава-

    0x26

    0x25

    Код

    93

    Инструкция Встроенный Описание
    операнд
    stind.ref

    Косвенное сохранение объектной ссылки
    stind.i1

    Косвенное сохранение значения
    int8
    stind.i2

    Косвенное сохранение значения
    int16
    stind.i4

    Косвенное сохранение значения
    int32
    stind.i8

    Косвенное сохранение значения
    int64
    stind.r4

    Косвенное сохранение значения
    float32
    stind.r8

    Косвенное сохранение значения
    float64
    stind.i

    Косвенное сохранение значения
    native int

    Таблица 3.8. Специальные инструкции для работы со стеком.

    0xDF

    0x57

    0x56

    0x55

    0x54

    0x53

    0x52

    0x51

    Код

    Таблица 3.7. Инструкции для косвенного сохранения значений

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    and
    or
    xor

    0x5F
    0x60
    0x61








    Сложение
    Вычитание
    Умножение
    Деление
    Деление беззнаковых целых чисел
    Остаток от деления
    Остаток от деления беззнаковых
    целых чисел
    Побитовое И
    Побитовое ИЛИ
    Побитовое ИСКЛЮЧАЮЩЕЕ
    ИЛИ

    Описание

    Инструкции, представленные в таблице 3.10, используются только
    для целочисленных операндов. Они отличаются от базовых бинарных операций тем, что осуществляют контроль переполнения (при переполнении
    генерируется исключение OverflowException).
    Операции сдвига (см. таблицу 3.11) выполняют сдвиг значения первого операнда (value1) в нужную сторону на количество бит, указанное во
    втором операнде (value2).
    Операции, приведенные в таблице 3.12, выполняют сравнение значений своих операндов. Результатом сравнения являются числа 0 или 1 (типа int32). Число 0 обозначает ложь, а число 1 – истину.
    Семантика операций сравнения для чисел с плавающей запятой существенно отличается от их семантики для целых чисел. Дело в том, что

    rem
    rem.un

    Инструкция Встроенный
    операнд
    add

    sub

    mul

    div

    div.un


    0x5D
    0x5E

    0x58
    0x59
    0x5A
    0x5B
    0x5C

    Код

    Таблица 3.9. Базовые бинарные арифметические операции

    ющей запятой. При этом применение бинарной операции не допускается,
    если тип одного ее операнда – целый, а другого – с плавающей запятой.
    Тип результата бинарной операции зависит от типов операндов. Если
    операнды целые, то и результат будет целый. Если операнды представляют собой числа с плавающей запятой, то результатом будет являться число с плавающей запятой.
    В таблице 3.9 представлены базовые инструкции, выполняющие бинарные операции.

    94

    Инструкция Встроенный Описание
    операнд
    shl

    Сдвиг целых чисел влево
    shr

    Сдвиг целых чисел со знаком
    вправо
    shr.un

    Сдвиг целых чисел без знака
    вправо
    числа с плавающей запятой могут дополнительно принимать значения
    +inf (положительная бесконечность), -inf (отрицательная бесконечность)
    и NaN (Not a Number – не число). Поэтому описание каждой инструкции
    содержит две части: для целых чисел и для чисел с плавающей запятой.
    При этом в описании используются следующие обозначения:
    • I и J – целые числа со знаком, причем I < J;
    • K и L – целые числа без знака, причем K < L;
    • A и B – конечные числа с плавающей запятой (то есть они не равны NaN, +inf и -inf), причем A < B;
    • C – любое число с плавающей запятой (может принимать значения NaN, +inf и -inf).

    0x64

    0x62
    0x63

    Код

    Инструкция Встроенный Описание
    операнд
    add.ovf

    Сложение целых чисел со знаком с контролем переполнения
    add.ovf.un

    Сложение целых чисел без знака
    с контролем переполнения
    mul.ovf

    Умножение целых чисел со знаком с контролем переполнения
    mul.ovf.un

    Умножение целых чисел без знака с контролем переполнения
    sub.ovf

    Вычитание целых чисел со знаком с контролем переполнения
    sub.ovf.un

    Вычитание целых чисел без знака с контролем переполнения

    Таблица 3.11. Операции сдвига

    0xDB

    0xDA

    0xD9

    0xD8

    0xD7

    0xD6

    Код

    95

    Таблица 3.10. Бинарные арифметические операции с контролем
    переполнения

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    0xFE 0x03

    0xFE 0x04

    0xFE 0x02

    Инструкция Встроенный Описание
    операнд
    ceq

    Сравнение на равенство.
    Для целых чисел:
    I ceq I => 1,
    иначе => 0
    Для чисел с плавающей запятой:
    +inf ceq +inf => 1,
    -inf ceq -inf => 1,
    A ceq A => 1,
    иначе => 0
    cgt

    Сравнение на «больше».
    Для целых чисел:
    J cgt I => 1,
    иначе => 0
    Для чисел с плавающей запятой:
    A cgt -inf => 1,
    +inf cgt A => 1,
    +inf cgt -inf => 1,
    B cgt A => 1,
    иначе => 0
    clt

    Сравнение на «меньше».
    Для целых чисел:
    I clt J => 1,
    иначе => 0
    Для чисел с плавающей запятой:
    A clt +inf => 1,
    -inf clt A => 1,
    -inf clt +inf => 1,
    A clt B => 1,
    иначе => 0
    cgt.un

    Сравнение на «больше» беззнаковых целых чисел или неупорядоченных чисел с плавающей

    Таблица 3.12. Операция сравнения

    0xFE 0x01

    од

    96

    0x65
    0x66

    Код

    clt.un



    97

    запятой. (Два числа с плавающей запятой называются неупорядоченными, если хотя бы одно из них равно NaN.)
    Для целых чисел:
    L cgt.un K => 1,
    иначе => 0
    Для чисел с плавающей запятой:
    NaN cgt.un C => 1,
    C cgt.un NaN => 1,
    A cgt.un -inf => 1,
    +inf cgt.un A => 1,
    +inf cgt.un -inf => 1,
    B cgt.un A => 1,
    иначе => 0
    Сравнение на «меньше» беззнаковых целых чисел или неупорядоченных чисел с плавающей
    запятой.
    Для целых чисел:
    K clt.un L => 1,
    иначе => 0
    Для чисел с плавающей запятой:
    NaN clt.un C => 1,
    C clt.un NaN => 1,
    A clt.un +inf => 1,
    -inf clt.un A => 1,
    -inf clt.un +inf => 1,
    A clt.un B => 1,
    иначе => 0

    Инструкция Встроенный Описание
    операнд
    neg

    Изменение знака числа
    not

    Побитовое НЕ (для целых чисел)

    Таблица 3.13. Унарные арифметические операции

    0xFE 0x05

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    Инструкция Встроенный Описание
    операнд
    ckfinite

    Проверка того, что число с плавающей запятой является конечным

    3.2.2.4. Преобразование значений
    Инструкции преобразования значений потребляют один операнд со
    стека вычислений и преобразуют его к нужному типу. Диаграмма стека для
    этих инструкций выглядит следующим образом:
    ... , value -> ... , result
    Базовые инструкции преобразования представлены в таблице 3.15.
    Они обладают следующими особенностями:
    • Преобразование чисел с плавающей запятой к целому типу обрезает дробную часть числа. Если при этом возникает переполнение,
    то возвращаемый результат неопределен (зависит от реализации).

    0xC3

    Код

    Таблица 3.14. Инструкция ckfinite

    3.2.2.3. Инструкция ckfinite
    Инструкция ckfinite (см. таблицу 3.14) генерирует исключение
    ArithmeticException, если число с плавающей запятой, находящееся на
    вершине стека вычислений, равно NaN, +inf или -inf. Если исключение
    не генерируется, то стек вычислений не меняется, поэтому диаграмма стека выглядит следующим образом:
    ... , value -> ... , value

    3.2.2.2. Унарные арифметические операции
    В таблице 3.13 приведены две инструкции, выполняющие унарные
    арифметические операции. Диаграмма стека для унарных операций выглядит следующим образом:
    ... , value -> ... , result
    Инструкция neg применима как для целых чисел, так и для чисел с
    плавающей запятой и обладает двумя особенностями:
    • Результатом применения этой инструкции к наименьшему отрицательному целому числу (такое число не имеет «парного»
    положительного числа) является само это наименьшее отрицательное число. Для того чтобы иметь возможность перехватить
    эту ситуацию, необходимо вместо инструкции neg использовать
    sub.ovf.
    • Результатом применения этой инструкции к NaN является NaN.

    98

    99






    Встроенный
    операнд









    Преобразовать к int8
    Преобразовать к int16
    Преобразовать к int32
    Преобразовать к int64
    Преобразовать к float32
    Преобразовать к float64
    Преобразовать к unsigned int32
    Преобразовать к unsigned int64
    Преобразовать беззнаковое целое число в число с плавающей
    запятой
    Преобразовать к unsigned int16
    Преобразовать к unsigned int8
    Преобразовать к native int
    Преобразовать к unsigned native int

    Описание

    • Преобразование значения с плавающей запятой к типу float32
    может вызывать потерю точности. Кроме того, если это значение слишком велико для float32, то результатом преобразования является +inf или -inf.
    • Инструкция conv.r.un интерпретирует целое значение, лежащее
    на вершине стека, как не имеющее знака и преобразует его к вещественному типу (либо float32, либо float64 в зависимости от
    значения).
    • Если переполнение возникает при преобразовании значения
    одного целого типа к другому целому типу, то обрезаются старшие биты значения.
    В таблице 3.16 приведены инструкции для преобразования значений,
    имеющих знак, к целым типам с контролем переполнения. В случае возникновения переполнения эти инструкции генерируют исключение
    OverflowException.
    Инструкции, представленные в таблице 3.17, используются для преобразования беззнаковых значений к нужному типу и генерируют исключение OverflowException в случае переполнения.

    conv.u2
    conv.u1
    conv.i
    conv.u

    conv.i1
    conv.i2
    conv.i4
    conv.i8
    conv.r4
    conv.r8
    conv.u4
    conv.u8
    conv.r.un

    0x67
    0x68
    0x69
    0x6A
    0x6B
    0x6C
    0x6D
    0x6E
    0x76

    0xD1
    0xD2
    0xD3
    0xE0

    Инструкция

    Код

    Таблица 3.15. Преобразование значений без контроля переполнения.

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    conv.ovf.i1
    conv.ovf.u1
    conv.ovf.i2
    conv.ovf.u2
    conv.ovf.i4
    conv.ovf.u4
    conv.ovf.i8
    conv.ovf.u8
    conv.ovf.i
    conv.ovf.u

    0xB3
    0xB4
    0xB5
    0xB6
    0xB7
    0xB8
    0xB9
    0xBA
    0xD4
    0xD5

    Встроенный
    операнд











    Преобразование к int8
    Преобразование к unsigned int8
    Преобразование к int16
    Преобразование к unsigned int16
    Преобразование к int32
    Преобразование к unsigned int32
    Преобразование к int64
    Преобразование к unsigned int64
    Преобразование к native int
    Преобразование к unsigned
    native int

    Описание

    conv.ovf.i1.un
    conv.ovf.i2.un
    conv.ovf.i4.un
    conv.ovf.i8.un
    conv.ovf.u1.un
    conv.ovf.u2.un
    conv.ovf.u4.un
    conv.ovf.u8.un
    conv.ovf.i.un
    conv.ovf.u.un

    0x82
    0x83
    0x84
    0x85
    0x86
    0x87
    0x88
    0x89
    0x8A
    0x8B

    Встроенный
    операнд











    Преобразование к int8
    Преобразование к int16
    Преобразование к int32
    Преобразование к int64
    Преобразование к unsigned int8
    Преобразование к unsigned int16
    Преобразование к unsigned int32
    Преобразование к unsigned int64
    Преобразование к native int
    Преобразование к unsigned
    native int

    Описание

    Инструкции для организации передачи управления можно разделить
    на пять категорий:
    • инструкции безусловного перехода;

    3.2.3. Инструкции для организации передачи управления

    Инструкция

    Код

    Таблица 3.17. Преобразование беззнаковых значений с контролем переполнения

    Инструкция

    Таблица 3.16. Преобразование значений со знаком с контролем
    переполнения

    Код

    100

    инструкции условного перехода;
    инструкция множественного выбора switch;
    инструкция вызова метода call;
    инструкция возврата из метода ret.

    101

    Инструкция Встроенный Описание
    операнд
    br.s
    int8
    Короткий безусловный переход
    br
    int32
    Длинный безусловный переход

    0x3A

    0x39

    0x2D

    0x2C

    Код

    Инструкция Встроенный Описание
    операнд
    brfalse.s
    int8
    Короткий условный переход, если значение равно 0 или null
    brtrue.s
    int8
    Короткий условный переход, если значение не равно 0 или null
    brfalse
    int32
    Длинный условный переход, если значение равно 0 или null
    brtrue
    int32
    Длинный условный переход, если значение не равно 0 или null

    Таблица 3.19. Базовые инструкции условного перехода

    3.2.3.2. Условный переход
    Базовые инструкции условного перехода, приведенные в таблице
    3.19, потребляют со стека вычислений один операнд и, в зависимости от
    его значения, осуществляют или не осуществляют переход по указанному
    во встроенном операнде относительному адресу. Диаграмма стека для этих
    инструкций выглядит следующим образом:
    ... , value -> ...
    Как и в случае инструкций безусловного перехода, существуют короткий и длинный варианты инструкций условного перехода, которые отличаются только разрядностью встроенного операнда (int8 и int32).

    0x2B
    0x38

    Код

    Таблица 3.18. Инструкции безусловного перехода

    3.2.3.1. Безусловный переход
    Существуют две инструкции безусловного перехода (см. таблицу
    3.18), которые различаются только разрядностью встроенного операнда
    (int8 и int32). При этом встроенный операнд этих инструкций обозначает относительное смещение цели перехода.






    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    0x44

    0x42
    0x43

    0x3F
    0x40
    0x41

    0x3D
    0x3E

    0x3B
    0x3C

    Код

    Инструкция Встроенный Описание
    операнд
    beq
    int32
    ceq; brtrue
    bge
    int32
    (int): clt; brfalse
    (F) : clt.un; brfalse
    bgt
    int32
    cgt; brtrue
    ble
    int32
    (int): cgt; brfalse
    (F):
    cgt.un; brfalse
    blt
    int32
    clt; brtrue
    bne.un
    int32
    ceq; brfalse
    bge.un
    int32
    (int): clt.un; brfalse
    (F):
    clt; brfalse
    bgt.un
    int32
    cgt.un; brtrue
    ble.un
    int32
    (int): cgt.un; brfalse
    (F):
    cgt; brfalse
    blt.un
    int32
    clt.un; brtrue

    Таблица 3.20. Дополнительные длинные инструкции условного перехода

    Инструкции brfalse присвоены два псевдонима: brnull и brzero,
    имеющих одинаковый с ней код. Аналогично, brfalse.s имеет псевдонимы brnull.s и brzero.s.
    Кроме базовых, существуют дополнительные инструкции условного
    перехода, потребляющие сразу два операнда. Каждая из дополнительных
    инструкций условного перехода является сокращенной записью последовательности из двух инструкций, первая из которых является бинарной операцией сравнения, а вторая – базовой инструкцией условного перехода.
    Таким образом, дополнительные инструкции условного перехода
    имеют следующую диаграмму стека:
    ... , value1 , value2 -> ...
    Длинные варианты дополнительных инструкций условного перехода
    перечислены в таблице 3.20, а короткие – в таблице 3.21. В описании каждой инструкции приводится эквивалентная ей комбинация бинарной
    операции сравнения и базовой инструкции условного перехода. В связи с
    тем, что семантика операций сравнения для целых чисел существенно отличается от их семантики для чисел с плавающей запятой (см. пункт
    3.2.2.1), для инструкций bge, ble, bge.un и ble.un (и для коротких вариантов этих инструкций) приводится по две эквивалентные им комбинации.
    Комбинация, помеченная меткой (int), используется в случае целых операндов, а комбинация, помеченная меткой (F), используется в том случае,
    если операнды представляют собой числа с плавающей запятой.

    102

    Инструкция Встроенный Описание
    операнд
    beq.s
    int8
    ceq; brtrue.s
    bge.s
    int8
    (int): clt; brfalse.s
    (F):
    clt.un; brfalse.s
    bgt.s
    int8
    cgt; brtrue.s
    ble.s
    int8
    (int): cgt; brfalse.s
    (F):
    cgt.un; brfalse.s
    blt.s
    int8
    clt; brtrue.s
    bne.un.s
    int8
    ceq; brfalse.s
    bge.un.s
    int8
    (int): clt.un; brfalse.s
    (F):
    clt; brfalse.s
    bgt.un.s
    int8
    cgt.un; brtrue.s
    ble.un.s
    int8
    (int): cgt.un; brfalse.s
    (F):
    cgt; brfalse.s
    blt.un.s
    int8
    clt.un; brtrue.s

    0x42

    Код

    Инструкция Встроенный
    операнд
    switch
    unsigned
    int32,
    int32 ...
    int32

    Таблица 3.22. Инструкция switch

    Осуществляет переход по
    таблице переходов
    в соответствии со значением
    на вершине стека

    Описание

    3.2.3.3. Инструкция switch
    Инструкция множественного выбора switch представлена в таблице
    3.22. Встроенный операнд этой инструкции имеет сложный формат: первое 32-разрядное слово содержит размер N таблицы переходов, после чего
    следует N 32-рязрядных относительных смещений целей перехода.
    Инструкция switch берет со стека вычислений один операнд. Обозначим этот операнд через I. Значение I интерпретируется как целое число без знака и сравнивается с N. Если I < N, то управление передается на
    I-тую цель в таблице переходов (нумерация целей осуществляется с 0). Ес-

    0x37

    0x35
    0x36

    0x32
    0x33
    0x34

    0x30
    0x31

    0x2E
    0x2F

    Код

    103

    Таблица 3.21. Дополнительные короткие инструкции безусловного
    перехода

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    Инструкция Встроенный Описание
    операнд
    call
    token
    Выполняет вызов метода

    0x2A

    Код

    Инструкция Встроенный Описание
    операнд
    ret

    Осуществляет возврат из метода

    Таблица 3.24. Инструкция ret

    3.2.3.5. Инструкция ret
    Инструкция ret (см. таблицу 3.24) осуществляет возврат из метода.
    Если метод возвращает значение, то оно должно быть загружено на вершину стека вычислений.

    Информация о методе, на которую указывает токен метаданных, позволяет JIT-компилятору определить, является ли вызываемый метод статическим, экземплярным, виртуальным или глобальной функцией. Особенностью инструкции call (по сравнению с инструкцией callvirt, которую
    мы будем рассматривать далее в этой главе) является то, что адрес вызываемого метода вычисляется статически, то есть еще во время JIT-компиляции.
    Параметры вызываемого метода должны быть расположены в стеке
    слева направо, то есть сначала на стек должен быть загружен первый параметр, затем второй и т.д. При вызове экземплярного метода в качестве первого параметра должна выступать объектная ссылка (параметр this).
    Если вызываемый метод возвращает значение, то оно загружается на
    стек вызывающего метода.

    0x28

    Код

    Таблица 3.23. Инструкция call

    3.2.3.4. Инструкция call
    Инструкция call (см. таблицу 3.23) выполняет вызов метода, который указан во встроенном операнде инструкции. Встроенный операнд
    представляет собой токен метаданных, указывающий на описывающую
    вызываемый метод запись в таблицах метаданных. Непосредственно перед инструкцией call может стоять префикс .tail, который говорит о том,
    что состояние текущего метода должно быть освобождено перед передачей управления вызываемому методу (хвостовой вызов).

    ли I >= N, то управление передается на инструкцию, непосредственно
    следующую за инструкцией switch.
    Для инструкции switch можно записать следующую диаграмму стека:
    ... , value -> ...

    104

    105

    Инструкция Встроенный Описание
    операнд
    newobj
    token
    Создает новый объект и вызывает для него конструктор
    Диаграмма стека для инструкции newobj:
    ... , arg1, ... , argN -> ... , obj
    Инструкция newobj потребляет со стека вычислений параметры конструктора и оставляет на стеке ссылку на созданный объект. Параметры
    вызываемого конструктора должны быть расположены в стеке слева направо, то есть сначала на стек должен быть загружен первый аргумент, затем второй и т.д.

    0x73

    Код

    Таблица 3.25. Инструкция newobj

    3.3.1.1. Создание объектов
    Инструкция newobj (см. таблицу 3.25) выполняет выделение памяти
    для объекта в куче и затем вызывает для этого объекта конструктор. Операции выделения памяти и вызова конструктора объединены в одной инструкции не случайно, так как это гарантирует отсутствие в куче неинициализированных объектов.
    Токен метаданных, находящийся во встроенном операнде инструкции, указывает на описатель конструктора в таблицах метаданных. Тип создаваемого объекта определяется через информацию о конструкторе (это
    тот класс, внутри которого объявлен конструктор).

    Инструкции для работы с объектами – это базовые инструкции для
    поддержки объектно-ориентированной парадигмы.

    3.3.1. Инструкции для работы с объектами

    Язык CIL, в отличие от большинства других ассемблерных языков, содержит богатый набор инструкций, предназначенных для поддержки объектной модели. Этот набор можно разделить на четыре основные категории:
    • инструкции для работы с объектами;
    • инструкции для работы с массивами;
    • инструкции для работы с типами-значениями;
    • инструкции для работы с типизированными ссылками.

    3.3. Язык CIL:
    инструкции для поддержки объектной модели

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    Инструкция Встроенный Описание
    операнд
    castclass
    token
    Проверяет, соответствует ли тип
    объекта на вершине стека вычислений указанному типу.
    В случае несоответствия генерирует исключение
    InvalidCastException.
    isinst
    token
    Проверяет, соответствует ли тип
    объекта на вершине стека вычислений указанному типу. Если
    соответствует, то оставляет объект на стеке, в противном случае
    заменяет объект на null

    Диаграмма стека для инструкций проверки типа объекта:
    ... , obj -> ... , obj
    Через инструкции проверки типа объекта реализуются операции
    приведения типов в языках высокого уровня.

    0x75

    0x74

    Код

    Таблица 3.26. Инструкция проверки типа объекта

    3.3.1.2. Проверка типа объекта
    В таблице 3.26 приведены две инструкции, осуществляющие проверку типа объекта, ссылка на который лежит на вершине стека вычислений.
    Токен метаданных, находящийся во встроенном операнде инструкции,
    указывает на описатель типа в таблицах метаданных.

    Хотя первым (неявным) параметром для любого конструктора является ссылка на инициализируемый объект (параметр this), перед вызовом
    инструкции newobj этот параметр не должен загружаться на стек вычислений. Дело в том, что ссылка на объект формируется в процессе выполнения инструкции (после выделения памяти в куче и до вызова конструктора) и затем автоматически передается конструктору. Происходит как бы
    «подкладывание» ссылки this под другие параметры конструктора на стеке вычислений.
    Особый случай применения инструкции newobj связан с созданием
    экземпляров типов-значений на стеке вычислений. Если указанный во
    встроенном операнде конструктор принадлежит типу-значению, то новый
    экземпляр этого типа создается не в куче, а прямо на стеке вычислений.

    106

    107

    Инструкция Встроенный Описание
    операнд
    ldfld
    token
    Загружает значение поля объекта. Диаграмма стека:
    ... , obj -> ... , value
    ldflda
    token
    Загружает адрес поля объекта.
    Диаграмма стека:
    ... , obj -> ... , addr
    stfld
    token
    Сохраняет значение в поле объекта. Диаграмма стека:
    ... , obj, value -> ...
    ldsfld
    token
    Загружает значение статического
    поля объекта. Диаграмма стека:
    ... -> ... , value
    ldsflda
    token
    Загружает адрес статического
    поля объекта. Диаграмма стека:
    ... -> ... , addr
    stsfld
    token
    Сохраняет значение в статическом поле объекта. Диаграмма
    стека:
    ... , val -> ...
    3.3.1.4. Вызов виртуальных методов
    Инструкция callvirt (см. таблицу 3.28) отличается от инструкции
    call главным образом тем, что адрес вызываемого метода определяется во
    время выполнения программы путем анализа типа объекта, для которого
    вызывается метод. Тем самым реализуется идея позднего связывания,
    необходимая для поддержки полиморфизма.
    Диаграмма стека для инструкции callvirt:
    ... , obj, arg1, ... , argN -> ... , retVal

    0x80

    0x7F

    0x7E

    0x7D

    0x7C

    0x7B

    Код

    Таблица 3.27. Инструкция для работы с полями объектов

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

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    Инструкция Встроенный Описание
    операнд
    ldstr
    token
    Создает на вершине стека объект-строку

    В принципе, массивы являются такими же объектами, как и экземпляры других классов. Но для более эффективной реализации и для сокращения размеров CIL-кода в наборе инструкций CLI предусмотрена спе-

    3.3.2. Инструкции для работы с массивами

    Диаграмма стека для инструкции ldstr:
    ... -> ... , obj

    0x72

    Код

    Таблица 3.29. Инструкция ldstr

    3.3.1.5. Загрузка строковых констант
    Для загрузки на стек вычислений строковых констант предусмотрена
    отдельная инструкция ldstr, приведенная в таблице 3.29. Токен, находящийся во встроенном операнде инструкции, указывает на образ строки в
    куче пользовательских строк, находящейся в составе метаданных. Инструкция создает объект класса System.String, копирует в него образ строки и оставляет ссылку на созданный объект на вершине стека вычислений.

    Токен метаданных, находящийся во встроенном операнде инструкции, указывает на информацию о вызываемом методе в таблицах метаданных (имя метода, класс и сигнатура). Определение метода, который нужно вызвать, происходит следующим образом. Система выполнения анализирует класс объекта, для которого вызывается метод (на диаграмме стека
    этот объект обозначен как obj), выполняя поиск принадлежащего этому
    классу экземплярного метода, имеющего нужные имя и сигнатуру. Если
    такой метод отсутствует, аналогичный поиск производится по порядку на
    всей цепочке суперклассов, от которых наследует класс объекта. Если в
    результате метод не будет найден, то генерируется исключение
    MissingMethodException, но эта ситуация невозможна в верифицированном коде.

    Инструкция Встроенный Описание
    операнд
    callvirt
    token
    Вызов метода с использованием
    позднего связывания

    Таблица 3.28. Инструкция callvirt

    0x6F

    Код

    108

    109

    Инструкция Встроенный Описание
    операнд
    newarr
    token
    Создает новый массив с элементами указанного типа

    Инструкция Встроенный Описание
    операнд
    ldlen

    Загружает на стек длину
    массива

    3.3.2.3. Работа с элементами массивов
    Инструкции ldelem (см. таблицу 3.32) предназначены для загрузки
    значения элемента одномерного массива на стек вычислений.

    Диаграмма стека для инструкции ldlen:
    ... , array -> ... , length
    Инструкция потребляет со стека объектную ссылку на массив и оставляет на стеке его размер в виде числа типа native unsigned int.

    0x8E

    Код

    Таблица 3.31. Инструкция ldlen

    3.3.2.2. Загрузка длины массива
    Инструкция ldlen (см. таблицу 3.31) загружает размер одномерного
    массива на стек вычислений.

    Диаграмма стека для инструкции newarr:
    ... , num -> ... , array
    Инструкция newarr потребляет со стека вычислений размер массива
    (на диаграмме обозначен как num) и оставляет объектную ссылку на созданный массив в куче.
    Элементы созданного массива автоматически обнуляются.

    0x8D

    Код

    Таблица 3.30. Инструкция newarr

    3.3.2.1. Создание массивов
    Инструкция newarr (см. таблицу 3.30) выделяет память под одномерный массив, индексируемый с нуля. Тип элементов массива указывается
    через токен метаданных во встроенном операнде инструкции (в качестве
    типа элементов может выступать тип-значение).

    циальная инструкция для работы с одномерными массивами, индексируемыми с нуля.

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    Инструкция

    ldelem.i1
    ldelem.u1

    ldelem.i2
    ldelem.u2

    ldelem.i4
    ldelem.u4

    ldelem.i8
    (ldelem.u8)
    ldelem.i

    ldelem.r4
    ldelem.r8
    ldelem.ref

    Код

    0x90
    0x91

    0x92
    0x93

    0x94
    0x95

    0x96

    0x97

    0x98
    0x99
    0x9A

    Встроенный Описание
    операнд

    Загрузить элемент типа int8

    Загрузить элемент типа unsigned
    int8

    Загрузить элемент типа int16

    Загрузить элемент типа unsigned
    int16

    Загрузить элемент типа int32

    Загрузить элемент типа unsigned
    int32

    Загрузить элемент типа int64
    или unsigned int64

    Загрузить элемент типа native int
    или unsigned native int

    Загрузить элемент типа float32

    Загрузить элемент типа float64

    Загрузить элемент – объектную
    ссылку

    Таблица 3.32. Инструкции для загрузки элементов массивов

    Так как в процессе JIT-компиляции не отслеживаются точные типы
    объектных ссылок на стеке вычислений, то JIT-компилятору требуются
    указания о том, элементы какого типа содержатся в массиве. Поэтому для
    разных типов существуют разные варианты инструкции ldelem.
    Следует иметь в виду, что значения целых типов, разрядность которых меньше 32 бит, при загрузке на стек удлиняются. Значения со знаком
    и значения без знака удлиняются по-разному, поэтому наличие отдельных
    инструкций для знаковых и беззнаковых целых типов вполне объяснимы.
    При этом остается неясным, для чего потребовалось определять две инструкции ldelem.i4 и ldelem.u4 для 32-разрядных целых типов, потому что
    они, очевидно, имеют одинаковый эффект.
    Диаграмма стека для инструкций ldelem выглядит следующим образом:
    ... , array, index -> ... , value
    Инструкция потребляет со стека объектную ссылку на массив и индекс элемента (типа native int), значение которого надо загрузить.

    110

    111

    Инструкция Встроенный Описание
    операнд
    ldelema
    token
    Загружает адрес элемента массива с указанным индексом

    3.3.3.2. Загрузка размера значения
    Инструкция sizeof (см. таблицу 3.36) загружает на стек вычислений
    размер в байтах типа-значения (размер представляет собой значение типа
    unsigned int32). Во встроенном операнде этой инструкции содержится токен метаданных, указывающий на информацию о типе-значении.

    3.3.3.1. Инициализация значения
    Инструкция initobj (см. таблицу 3.35) предназначена для инициализации значения типа-значения. Во встроенном операнде этой инструкции
    содержится токен метаданных, указывающий на информацию о типе-значении. В отличие от инструкции newobj, инструкция initobj не вызывает
    конструктор.
    Инструкция initobj потребляет со стека вычислений адрес значения:
    ... , addr -> ...

    Специальный набор инструкций предусмотрен для поддержки операций с типами-значениями.

    3.3.3. Инструкции для работы с типами-значениями

    Инструкции stelem (см. таблицу 3.34) предназначены для сохранения
    значения со стека вычислений в элементе одномерного массива. Аналогично инструкциям ldelem, существуют различные варианты stelem для
    разных типов элементов массива. При этом варианты stelem для беззнаковых целых типов отсутствуют за ненадобностью.
    Диаграмма стека для инструкций stelem:
    ... , array, index, value -> ...

    0x8F

    Код

    Таблица.3.33. Инструкция ldelema

    Инструкция ldelema (см. таблицу 3.33) загружает на стек вычислений
    адрес элемента одномерного массива (управляемый указатель). Тип элементов массива указывается через токен метаданных во встроенном операнде инструкции (в качестве типа элементов может выступать тип-значение). Диаграмма стека для нее выглядит следующим образом:
    ... , array, index -> ... , addr

    Common Intermediate Language

    stelem.i1

    stelem.i2

    stelem.i4

    stelem.i8

    stelem.r4

    stelem.r8

    stelem.ref

    stelem.i

    0x9C

    0x9D

    0x9E

    0x9F

    0xA0

    0xA1

    0xA2

    0x9B

    Встроенный Описание
    операнд

    Сохраняет значение типа int8 в
    элементе массива с указанным
    индексом

    Сохраняет значение типа int16 в
    элементе массива с указанным
    индексом

    Сохраняет значение типа int32 в
    элементе массива с указанным
    индексом

    Сохраняет значение типа int64 в
    элементе массива с указанным
    индексом

    Сохраняет значение типа float32
    в элементе массива с указанным
    индексом

    Сохраняет значение типа float64
    в элементе массива с указанным
    индексом

    Сохраняет значение объектной
    ссылки в элементе массива с
    указанным индексом

    Сохраняет значение типа native
    int в элементе массива с указанным индексом

    0xFE 0x15

    Инструкция Встроенный Описание
    операнд
    initobj
    token
    Заполняет все поля значения
    нулями

    Таблица 3.35. Инструкция initobj

    Инструкция

    Код

    CIL и системное программирование в Microsoft .NET

    Таблица 3.34. Инструкции для сохранения значений в элементы
    массивов

    Код

    112

    113

    Инструкция Встроенный Описание
    операнд
    sizeof
    token
    Загружает на стек размер значения указанного типа

    0x81

    0x71

    0x70

    Код

    Инструкция Встроенный Описание
    операнд
    cpobj
    token
    Копирует значение. Диаграмма стека:
    ... , destAddr, srcAddr -> ...
    (Здесь destAddr – адрес приемника, а
    srcAddr – адрес источника.)
    ldobj
    token
    Загружает значение на стек вычислений. Диаграмма стека:
    ... , addr -> ... , valObj
    (Здесь addr – адрес загружаемого
    значения.)
    stobj
    token
    Сохраняет значение со стека вычислений в память. Диаграмма стека:
    ... , addr, valObj -> ... (Здесь addr
    – адрес, по которому будет сохранено значение.)

    Таблица 3.37. Инструкции для копирования значений

    3.3.3.3. Копирование значений
    Инструкции, приведенные в таблице 3.37, выполняют копирование
    значений типов-значений. Во встроенных операндах этих инструкций содержится токен метаданных, указывающий на информацию о типе-значении в таблицах метаданных.
    Инструкция ldobj используется главным образом при вызове методов для загрузки параметров (если вызываемый метод имеет параметры
    типов-значений). Инструкции cpobj и stobj применяются сравнительно
    редко, хотя и имеют однобайтовые коды.

    Диаграмма стека для инструкций sizeof:
    ...-> ... , size

    0xFE 0x1C

    Код

    Таблица 3.36.Инструкция sizeof

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    Инструкция Встроенный Описание
    операнд
    unbox
    token
    Выполняет распаковку значения. Диаграмма стека:
    ... , obj -> ... , ptr
    box
    token
    Упаковывает значение. Диаграмма стека:
    ... , value -> ... , obj

    0xD0

    Код

    Инструкция Встроенный Описание
    операнд
    ldtoken
    token
    Загружает описатель токена метаданных (структуру
    RuntimeTypeHandle,
    RuntimeMethodHandle,
    RuntimeFieldHandle)

    Таблица 3.39. Инструкция ldtoken

    3.3.3.5. Загрузка описателя токена метаданных
    Инструкция ldtoken (см. таблицу 3.39) применяется для работы с библиотекой рефлексии. Фактически она переводит токены метаданных в
    специальные структуры данных рефлексии. Так как переводимый токен
    жестко зашит в инструкцию (он находится во встроенном операнде), то
    можно говорить о том, что инструкция ldtoken представляет собой инструкцию загрузки константы.

    Выполнение инструкции box заключается в создании в куче «объекта-обертки» для значения, после чего осуществляется побитовое копирование значения внутрь «обертки».
    При распаковке значения с помощью инструкции unbox никакого копирования не происходит. Вместо этого на стек вычислений загружается
    адрес значения находящегося внутри «обертки».

    0x8C

    0x79

    Код

    Таблица 3.38. Инструкции для упаковки и распаковки значений

    3.3.3.4. Упаковка и распаковка значений
    Инструкции, приведенные в таблице 3.38, выполняют упаковку и
    распаковку значений типов-значений. Во встроенных операндах этих инструкций содержится токен метаданных, указывающий на информацию о
    типе-значении в таблицах метаданных.

    114

    115

    Инструкция Встроенный Описание
    операнд
    mkrefany
    token
    Создает типизированную ссылку
    на вершине стека вычислений

    Инструкция
    refanytype

    Код
    0xFE 0x1D

    Встроенный Описание
    операнд

    Загружает токен, хранящийся в
    типизированной ссылке

    Таблица 3.41. Инструкция refanytype

    3.3.4.2. Загрузка типа типизированной ссылки
    Инструкция refanytype (см. таблицу 3.41) загружает токен метаданных, хранящийся в типизированной ссылке, на вершину стека вычислений.

    Диаграмма стека для инструкций mkrefany:
    ... , ptr -> ... , typedRef

    0xC6

    Код

    Таблица 3.40. Инструкция mkrefany

    3.3.4.1. Создание типизированной ссылки
    Инструкция mkrefany (см. таблицу 3.40) предназначена для создания
    типизированных ссылок. Она упаковывает вместе управляемый указатель
    на некоторое значение и токен метаданных, описывающий тип этого значения. При этом токен содержится во встроенном операнде инструкции.

    Типизированные ссылки в системе типов .NET реализованы исключительно для поддержки некоторых особенностей синтаксиса и семантики языка Visual Basic .NET. Они представляют собой гибрид управляемого
    указателя и типа-значения.
    Для работы с типизированными ссылками предусмотрены три инструкции CIL, которые мы рассмотрим в этом разделе.

    3.3.4. Инструкции для работы с типизированными ссылками

    Диаграмма стека для инструкций ldtoken:
    ... -> ... , runtimeHandle
    Эта инструкция отнесена к группе инструкций для работы с типамизначениями, потому что описатели токенов представляют собой значения
    типов-значений.

    Common Intermediate Language

    Диаграмма стека для инструкций refanytype:
    ... , typedRef -> ... , type

    CIL и системное программирование в Microsoft .NET

    refanyval

    0xC2

    Встроенный Описание
    операнд
    token
    Загружает адрес, хранящийся в
    типизированной ссылке

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

    3.4. Язык CIL: обработка исключений

    Диаграмма стека для инструкций refanyval:
    ... , typedRef -> ... , ptr

    Инструкция

    Код

    Таблица 3.42. Инструкция refanyval

    3.3.4.3. Загрузка значения типизированной ссылки
    Инструкция refanyval (см. таблицу 3.42) загружает управляемый указатель, хранящийся в типизированной ссылке, на вершину стека вычислений.

    116

    117

    Для дальнейшего изложения нам понадобится ввести понятия области в коде метода и координат области.
    Будем называть областью непрерывную последовательность инструкций в коде метода. При этом область будет определяться своими координатами, а именно парой чисел (offset, length), где offset – это смещение
    первой инструкции области относительно начала тела метода, а length –
    длина области. Как смещение, так и длину будем измерять в байтах.
    Заголовок каждого метода содержит специальный массив, элементы
    которого называются предложениями обработки исключений (exception
    handling clause).
    Каждое предложение обработки исключений представляет собой
    структуру, состоящую из нескольких полей. В этих полях записаны координаты двух или трех областей, а именно: в любом предложении присутствуют координаты защищенной области (protected block) и области обработчика (exception handler), а в некоторых предложениях дополнительно
    описана область фильтра (filter block).
    Если говорить в терминах языка C#, то защищенная область – это
    try-блок, а область обработчика – это либо catch-блок, либо finally-блок.
    Аналог для области фильтра в языке C# отсутствует, но зато он есть в Visual
    Basic .NET и в Visual C++ with Managed Extensions. Область фильтра содержит код, принимающий решение о том, может ли данное исключение
    быть обработано обработчиком. Естественно, такое представление о назначении областей в предложении обработки исключений несколько примитивно и понадобится нам лишь на начальном этапе.
    Давайте рассмотрим два возможных формата, в которых кодируется
    массив предложений обработки исключений. В каждом из двух форматов
    содержатся одни и те же поля, и различаются они только размерами полей.
    Первый формат называется коротким форматом (см. таблицу 3.43) и используется тогда, когда смещения областей не превышают 65535 байт, а
    длины областей не превышают 255 байт. Во втором, длинном формате
    (см. таблицу 3.44) допускаются любые смещения и длины. Строго говоря,

    3.4.1. Предложения обработки исключений в заголовках методов

    Основная трудность для понимания деталей реализации обработки
    исключений в CLI заключается в том, что обработка исключений частично
    закодирована в телах методов (в виде специальных инструкций), а частично – в заголовках методов. Скорее всего, такая смешанная схема была выбрана разработчиками CLI для обеспечения компактности сборок. Поэтому мы в данном разделе сначала рассмотрим ту часть информации об обработке исключений, которая расположена в заголовках методов, затем перейдем к инструкциям CIL и, в конце концов, свяжем все воедино, приведя семантику обработки исключений виртуальной системой выполнения.

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    Размер
    2
    2
    1
    2
    1
    4
    4

    Поле
    Flags
    TryOffset
    TryLength
    HandlerOffset
    HandlerLength
    ClassToken
    FilterOffset

    Описание
    Флаги
    Координаты защищенной
    области
    Координаты области
    обработчика
    Токен метаданных
    Смещение области фильтра

    Размер
    4
    4
    4
    4
    4
    4
    4

    Поле
    Flags
    TryOffset
    TryLength
    HandlerOffset
    HandlerLength
    ClassToken
    FilterOffset

    Описание
    Флаги
    Координаты защищенной
    области
    Координаты области
    обработчика
    Токен метаданных
    Смещение области фильтра

    Итак, координаты защищенной области задаются парой (TryOffset,
    TryLength), а координаты области обработчика – парой (HandlerOffset,
    HandlerLength). Для области фильтра указывается только ее смещение, потому что подразумевается, что она непосредственно предшествует области
    обработчика (длину области фильтра можно вычислить: она равна
    HandlerOffset – FilterOffset).
    Обратите внимание, что смещения полей ClassToken и FilterOffset
    совпадают. Это означает, что фактически они представляют собой одно
    поле. Просто иногда оно интерпретируется как токен метаданных, а иногда – как смещение области фильтра.
    Поле Flags, возможные значения которого перечислены в таблице
    3.45, задает тип обработчика.
    Всего возможны четыре типа обработчиков исключений, отличаю-

    Смещение
    0
    4
    8
    12
    16
    20
    20

    Таблица 3.44. Поля предложения обработки исключений в случае
    длинного формата

    Смещение
    0
    2
    4
    5
    7
    8
    8

    Таблица 3.43. Поля предложения обработки исключений в случае
    короткого формата

    не любые, а укладывающиеся в 32 бита. Но на практике этого более чем
    достаточно.

    118

    119

    Описание
    Обработчик исключений с фильтрацией по типу
    Обработчик исключений с пользовательской фильтрацией
    Обработчик finally
    Обработчик fault

    0x7A

    Код

    Инструкция Встроенный Описание
    операнд
    throw

    Генерирует исключение

    Таблица 3.46. Инструкция throw

    3.4.2.1. Инструкции для генерации исключений
    Инструкция throw (см. таблицу 3.46) генерирует исключение, включая тем самым механизм обработки исключений.

    В CIL предусмотрено несколько инструкций, отвечающих за порождение исключений и передачу управления из обработчиков исключений.

    3.4.2. Инструкции CIL для обработки исключений

    Первые два типа обработчиков мы будем относить к категории обработчиков с фильтрацией, а последние два – к категории обработчиков без
    фильтрации.

    Значение
    0
    1
    2
    4

    Таблица 3.45. Допустимые значения поля Flags предложения обработки исключений

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

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    Инструкция Встроенный Описание
    операнд
    rethrow

    Генерирует то же самое исключение, что было поймано обработчиком

    0xDD
    0xDE

    Код

    Инструкция Встроенный Описание
    операнд
    leave
    int32
    Выход из области
    leave.s
    int8
    Выход из области (короткий переход)

    Таблица 3.48. Инструкция leave

    3.4.2.2. Инструкции передачи управления между блоками
    Инструкции, представленные в таблице 3.48, являются аналогами
    инструкций безусловного перехода и используются для выхода из защищенных областей и областей обработчиков с фильтрацией. Необходимость существования этих инструкций обусловлена тем фактом, что обычные инструкции безусловного перехода не могут пересекать границы этих
    областей.

    Диаграмма стека для инструкции rethrow:
    ... -> ...

    0xFE 0x1A

    Код

    Таблица 3.47. Инструкция rethrow

    Диаграмма стека для инструкции throw:
    ... , obj -> ...
    Объектная ссылка, которую инструкция throw потребляет со стека
    вычислений, должна указывать на объект в куче, описывающий исключение. Вообще говоря, в качестве такого объекта может выступать объект
    любого типа, в том числе упакованный тип-значение, но спецификация
    CLS требует, чтобы базовым классом для типа объекта-исключения являлся класс System.Exception.
    Если при выполнении инструкции throw на стеке вычислений
    лежит
    нулевая
    ссылка,
    то
    генерируется
    исключение
    System.NullReferenceException.
    Инструкция rethrow (см. таблицу 3.47) разрешена только внутри обработчика исключений с фильтрацией по типу и предназначена для генерации
    того же самого исключения, которое было поймано обработчиком.

    120

    121

    endfinally
    (endfault)

    0xDC

    Встроенный Описание
    операнд

    Выход из обработчиков finally и
    fault

    Инструкция Встроенный Описание
    операнд
    endfilter

    Завершение области фильтра

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

    3.4.3. Правила размещения областей

    Диаграмма стека для инструкции endfilter:
    ... , value -> ...

    0xFE 0x11

    Код

    Таблица 3.50. Инструкция endfilter

    Диаграмма стека для инструкции endfinally:
    ... -> ...
    Инструкция endfilter (см. таблицу 3.50) завершает область фильтра.
    Ее основная задача состоит в том, чтобы вернуть целое число (0 или 1).
    Значение 0 означает, что данное исключение не может быть обработано и
    нужно поискать другой обработчик. Значение 1 говорит о том, что нужно
    передать управление на обработчик.

    Инструкция

    Код

    Таблица 3.49. Инструкция endfinally

    Диаграмма стека для инструкции leave:
    ... ->
    Как видно из диаграммы, побочным эффектом при выполнении инструкции leave является очистка стека вычислений.
    Инструкция endfinally (см. таблицу 3.49) используется для выхода из
    областей обработчиков без фильтрации. У нее есть псевдоним – endfault.

    Common Intermediate Language

    a. если защищенная область предложения A находится внутри защищенной области предложения B, то области фильтра и обработчика предложения A также должны располагаться внутри защищенной области предложения B;
    b. если защищенная область предложения A не пересекается
    с защищенной областью предложения B, то области фильтров и обработчиков этих предложений тоже не должны пересекаться;
    c. если защищенная область предложения A совпадает с защищенной областью предложения B, то области фильтров
    и обработчиков этих предложений не должны пересекаться.

    CIL и системное программирование в Microsoft .NET

    Передача управления внутрь защищенных областей, из них и между
    ними и их обработчиками регламентирована следующими правилами:
    1. Передача управления на обработчики осуществляется только
    через механизм обработки исключений.
    2. Существует только два способа передать управление извне на
    защищенную область:
    a. передача управления на первую инструкцию защищенной
    области;
    b. использование инструкции leave из области обработчика с
    фильтрацией, связанной с данной защищенной областью
    (область обработки связана с защищенной областью, если
    их координаты указаны в одном и том же предложении обработки исключений).
    3. Перед входом в защищенную область стек вычислений должен
    быть пустым.
    4. Для выхода из защищенной области, из области фильтра или из
    области обработчика существуют только следующие возможности:
    a. порождение исключения инструкцией throw;
    b. использование инструкции leave из защищенной области
    или области с фильтрацией;
    c. использование инструкции endfilter из области фильтра;
    d. использование инструкции endfinally из области без
    фильтрации;
    e. использование инструкции rethrow из области с фильтрацией.

    3.4.4. Ограничения на передачу управления

    122

    123

    В составе .NET Framework SDK поставляется ассемблер ILASM, который позволяет компилировать текстовые файлы, содержащие CIL-код и

    3.5. Синтаксис ILASM

    Давайте рассмотрим последовательность действий, осуществляемую
    системой выполнения для обработки сгенерированного исключения.
    Пусть в некотором методе инструкция, расположенная по некоторому адресу, породила исключение. Система выполнения обрабатывает это
    исключение в два этапа. Задача первого этапа – поиск подходящего для
    этого исключения обработчика с фильтрацией. Задача второго этапа – выполнение нужных обработчиков без фильтрации и передача управления
    найденному во время первого этапа обработчику с фильтрацией.
    Выполнение первого этапа начинается с просмотра массива предложений обработки исключений, принадлежащего методу, где произошло исключение. В этом массиве осуществляется поиск такого предложения, что:
    1. оно описывает обработчик с фильтрацией;
    2. адрес инструкции, породившей исключение, попадает в диапазон адресов защищенной области этого предложения;
    3. исключение удовлетворяет фильтру обработчика.
    Таким образом, на первом этапе finally и fault-блоки пропускаются.
    Кроме того, происходит последовательный вызов фильтров для блоков с
    пользовательской фильтрацией.
    Если в методе, внутри которого было сгенерировано исключение,
    не оказалось подходящего предложения обработки исключений, то система выполнения переходит на следующий метод в стеке вызовов (то есть,
    на метод, из которого данный метод был вызван). В следующем методе система выполнения продолжает искать нужное предложение, причем в качестве адреса инструкции, породившей исключение, используется адрес
    инструкции, вызывающей предыдущий метод.
    Первый этап может завершиться либо нахождением подходящего
    предложения, либо обнаружением того факта, что подходящее предложение не существует на всей последовательности методов в стеке вызовов.
    В первом случае система переходит к следующему этапу, а во втором – выполнение программы аварийно завершается.
    На втором этапе система выполнения повторно просматривает массивы предложений, вызывая все обработчики без фильтрации. Она останавливается, когда доходит до предложения, найденного на первом этапе,
    после чего вызывает обработчик, описываемый этим предложением.

    3.4.5. Семантика обработки исключений

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    125

    IL-программа представляет собой последовательность объявлений.
    В этом разделе мы рассмотрим синтаксис объявлений следующих элементов IL-программы:
    • сборка;
    • модуль;
    • тип;
    • поле;
    • метод.

    Программы в IL-формате состоят из следующих лексических элементов:
    • идентификаторы;
    • метки;
    • константы;
    • зарезервированные слова;
    • специальные знаки;
    • комментарии.
    Идентификаторы и метки чаще всего представляют последовательности символов, начинающиеся с латинской буквы (или с символов «_»,
    «$», «@» и «?»), за которой следуют латинские буквы, цифры или символы
    «_», «$», «@» и «?». Кроме того, для идентификаторов и меток существует
    особая форма записи в апострофах: она позволяет включать в идентификаторы любые символы Unicode. Например:
    Label_1 $Name 'Идентификатор'
    Несколько идентификаторов могут быть объединены в один идентификатор с помощью точек. Например:
    System.Console.WriteLine
    Целочисленные константы записываются либо в десятичной системе
    счисления, либо в шестнадцатеричной (тогда перед ними ставится префикс «0x»). Например:
    128 -10 0xFF10B000
    В вещественных константах точка используется для разделения целой
    и дробной части, а символы «e» и «E» служат для указания экспоненциальной части. Кроме того, поддерживается особая форма записи float32 (целая_константа) и float64 (целая_константа), позволяющая представить
    целое число в виде числа с плавающей точкой. Например:
    5.5 -1.05e10 float32(128) float64(50)
    Строковые константы записываются в двойных кавычках и могут
    содержать Escape-последовательности «\t», «\n» и «\xxx», где восьмеричное число xxx задает код символа от 0 до 255. Для переноса строковой
    константы на другую строку программы используется символ «\». Кроме
    того, для строковых констант поддерживается операция конкатенации
    «+». Например:
    “Alpha Beta Gamma” “Hello, World\n” “Concat”+”enation”

    3.5.2.2. Типы
    Объявление типа осуществляется с помощью директивы «.class» и состоит из четырех частей:
    1. последовательность атрибутов типа;
    2. имя типа;
    3. базовый тип;
    4. список реализуемых интерфейсов.

    3.5.2.1. Сборки и модули
    Каждый IL-файл для ассемблера ILASM представляет собой отдельный модуль сборки. Мы не будем касаться вопросов компиляции сборки,
    состоящей из нескольких модулей, поэтому приведем образец заголовка
    IL-файла для одномодульной сборки:
    .assembly MyProgram { }
    .module MyProgram.exe
    .assembly extern mscorlib { }
    В заголовке используются три директивы: директива «.assembly» позволяет задать имя нашей сборки. Директива «.module» определяет имя модуля и совпадает с именем исполняемого файла, в который будет записана откомпилированная сборка. Директива «.assembly extern» указывает,
    что мы будем импортировать сборку mscorlib, в которой находится основная часть библиотеки классов .NET. В фигурных скобках после имени
    сборки могут перечисляться свойства сборки, но в простейшем случае их
    можно оставить пустыми.

    3.5.2. Синтаксис

    Комментарии в IL-программах записываются так же, как в языке C#:
    • Если в строке программы встречается «//», то остаток строки
    считается комментарием.
    • Текст, начинающийся с «/*», оканчивающийся на «*/» и не содержащий «*/», считается комментарием.

    Common Intermediate Language

    3.5.1. Основные элементы лексики

    метаданные. В этом разделе мы проведем краткий обзор формата, в котором записываются эти файлы, и рассмотрим несколько примеров программ. Будем называть IL-форматом формат файлов, поддерживаемый ассемблером ILASM, а программы, записанные в IL-формате, – IL-программами.

    124

    CIL и системное программирование в Microsoft .NET

    Описание
    Тип является абстрактным классом
    Тип является интерфейсом
    Тип не экспортируется из сборки
    Тип экспортируется из сборки
    Тип не может являться базовым классом для другого
    типа (от него нельзя наследовать)
    Экземпляры типа могут быть сериализованы

    После атрибутов следует идентификатор, задающий имя объявляемого типа.
    Если объявляемый тип наследует от какого-нибудь другого типа
    (базового класса), отличного от System.Object, то необходимо указать имя
    базового класса после ключевого слова «extends». При этом, если в качестве базового класса выбран System.ValueType, то объявляемый тип будет типом-значением.
    Если объявляемый тип реализует методы каких-либо интерфейсов,
    то должен быть приведен список этих интерфейсов после ключевого слова «implements».
    Рассмотрим несколько примеров:
    1. Объявление экспортируемого абстрактного класса, реализующего интерфейс IEnumerable.
    .class public abstract MyAbstractClass
    extends [mscorlib]System.Object
    implements [mscorlib]System.Collections.IEnumerable
    { }
    2. Объявление неэкспортируемого интерфейса.
    .class private interface MyInterface { }
    3. Объявление экспортируемого типа-значения.
    .class public sealed MyValueType
    extends [mscorlib]System.ValueType
    { }
    Обратите внимание, что перед именами библиотечных классов и интерфейсов в квадратных скобках указывается имя сборки, в которой они
    содержатся.

    serializable

    Атрибут
    abstract
    interface
    private
    public
    sealed

    Таблица 3.51. Атрибуты типов

    Последовательность атрибутов следует непосредственно после ключевого слова «.class». В таблице 3.51 приведен набор наиболее часто используемых атрибутов.

    126

    127

    Описание
    Поле видимо внутри сборки
    Поле видимо для наследников типа
    Поле видимо для всех
    Поле видимо только внутри типа
    Поле является статическим

    3.5.2.4. Методы
    Методы объявляются внутри объявлений типов. Объявление метода
    осуществляется с помощью директивы «.method» и состоит из пяти частей:
    1. последовательность атрибутов метода;
    2. тип возвращаемого значения;
    3. имя метода;
    4. список параметров метода;
    5. тело метода.
    Последовательность атрибутов следует непосредственно после ключевого слова «.method». В таблице 3.53 приведен набор наиболее часто используемых атрибутов.
    После атрибутов следует тип возвращаемого значения и идентификатор, задающий имя метода. Если метод не возвращает значения, в качест-

    После атрибутов следует тип поля и идентификатор, задающий имя
    поля.
    Рассмотрим несколько примеров:
    1. Объявление поля x типа массив.
    .field private int32[] x
    2. Объявление поля table типа Hashtable.
    .field public class
    [mscorlib]System.Collections.Hashtable table

    Атрибут
    assembly
    family
    public
    private
    static

    Таблица 3.52. Атрибуты полей

    3.5.2.3. Поля
    Поля объявляются внутри объявлений типов. Объявление поля осуществляется с помощью директивы «.field» и состоит из трех частей:
    1. последовательность атрибутов поля;
    2. тип;
    3. имя поля.
    Последовательность атрибутов следует непосредственно после ключевого слова «.field». В таблице 3.52 приведен набор наиболее часто используемых атрибутов.

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    Давайте рассмотрим пример программы, написанной прямо на CIL с
    использованием синтаксиса ILASM. Мы не станем приводить весь текст
    программы сразу, а будем рассматривать ее постепенно, по частям.
    Естественно, наша программа будет начинаться с заголовка, объявляющего имена сборки и модуля и импортирующего стандартную библиотеку:

    3.5.3. Пример программы

    ве типа возвращаемого значения указывается void. Конструкторы всегда
    имеют имя «.ctor», а статические конструкторы – «.cctor».
    Список параметров метода следует за именем метода и заключается в
    круглые скобки. Для каждого параметра указывается его тип и имя.
    Прежде чем перейти к рассмотрению синтаксиса объявлений тел методов, приведем несколько примеров заголовков методов:
    1. Объявление конструктора с двумя параметрами.
    .method public void .ctor
    (int32 x, class [mscorlib]System.String s)
    2. Виртуальный метод с управляемым указателем в качестве параметра.
    .method private virtual int32 myMethod(int32& pX)
    3. Статический метод, возвращающий массив:
    .method public static int32[] MyStaticMethod()
    Тело метода заключается в фигурные скобки и содержит инструкции
    языка CIL. Каждая инструкция записывается на новой строке программы.
    Если нужно, то инструкции может предшествовать метка, отделяемая от
    инструкции двоеточием. Например:
    Hello: ldstr “Hello, World!”
    call void [mscorlib]System.Console.WriteLine(string)
    Кроме инструкций CIL тело метода может содержать директивы тела
    метода. Они перечислены в таблице 3.54.

    Описание
    Метод видим внутри сборки
    Метод видим для наследников типа
    Метод видим для всех
    Метод видим только внутри типа
    Метод является абстрактным
    Метод является виртуальным
    Метод не может переопределяться в наследниках
    Метод является статическим

    Таблица 3.53. Атрибуты методов

    Атрибут
    assembly
    family
    public
    private
    abstract
    virtual
    final
    static

    128

    129

    Описание
    Показывает, что данный метод является точкой
    входа в сборку (метод должен быть статическим, возвращать int32 или ничего не возвращать, иметь в качестве параметров массив
    строк или вообще не иметь параметров)
    Определяет набор локальных переменных метода. Локальные переменные объявляются аналогично параметрам метода
    Задает глубину стека вычислений

    Далее объявим тип-значение Point, реализующий понятие точки на
    плоскости. Он будет содержать два поля x и y типа float64, а также конструктор и статический метод, вычисляющий расстояние между двумя точками:
    .class public sealed Point extends [mscorlib]System.ValueType
    {
    .field public float64 x
    .field public float64 y
    .method public void .ctor (float64 x, float64 y)
    {
    .maxstack
    3
    ldarg.0
    dup
    ldarg.1
    stfld
    float64 Point::x
    ldarg.2
    stfld
    float64 Point::y
    ret
    }
    .method public static float64 Distance
    (valuetype Point a, valuetype Point b)
    {
    .maxstack
    3
    ldarga
    a
    ldfld
    float64 Point::x

    .assembly Sample1 { }
    .module sample1.exe
    .assembly extern mscorlib { }

    .maxstack число

    .locals (объявления)

    Директива
    .entrypoint

    Таблица 3.54. Директивы тела метода

    Common Intermediate Language

    ldarga
    ldfld
    sub
    dup
    mul
    ldarga
    ldfld
    ldarga
    ldfld
    sub
    dup
    mul
    add
    call
    ret

    float64 [mscorlib]System.Math::Sqrt(float64)

    a
    float64 Point::y
    b
    float64 Point::y

    b
    float64 Point::x

    CIL и системное программирование в Microsoft .NET

    }
    }
    А теперь объявим вспомогательный класс SampleClass, который будет
    содержать точку входа в нашу сборку. Метод Demo (точка входа) будет вычислять расстояние между точками (0.0,0.0) и (1.0,1.0) и выводить результат на экран:
    .class public SampleClass
    {
    .method public static void Demo()
    {
    .entrypoint
    .maxstack 3
    ldc.r8
    0.0
    ldc.r8
    0.0
    newobj
    void Point::.ctor(float64,float64)
    ldc.r8
    1.0
    ldc.r8
    1.0
    newobj
    void Point::.ctor(float64,float64)
    call
    float64 Point::Distance(valuetype Point, valuetype Point)
    call
    void [mscorlib]System.Console::WriteLine (float64)
    ret
    }
    }
    Откомпилируем нашу программу, которая записана в текстовом файле sample1.il. Подразумевается, что мы работаем в Windows и у нас переменная окружения path настроена таким образом, что программы ILASM и
    PEVERIFY можно вызывать без указания путей. Наберем в консоли команду:

    130

    131

    1,4142135623731

    В результате на экран выводится расстояние между точками
    (0.0,0.0) и (1.0,1.0):

    sample1.exe

    Таким образом, сборка успешно прошла верификацию и мы можем
    рискнуть ее запустить:

    All Classes and Methods in sample1.exe Verified

    Microsoft (R) .NET Framework PE Verifier Version 1.1.4322.573
    Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

    В ответ верификатор выведет на экран:

    peverify sample1.exe

    Итак, наша программа успешно откомпилировалась и на диске появилась сборка sample1.exe. Попробуем провести ее верификацию:

    Assembled method Point::.ctor
    Assembled method Point::Distance
    Assembled method SampleClass::Demo
    Creating PE file
    Emitting members:
    Global
    Class 1 Fields: 2;
    Methods: 2;
    Class 2 Methods: 1;
    Resolving member refs: 9 -> 9 defs, 0 refs
    Writing PE file
    Operation completed successfully

    Microsoft (R) .NET Framework IL Assembler. Version 1.1.4322.573
    Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
    Assembling 'sample1.il', no listing file, to EXE --> 'sample1.EXE'
    Source file is ANSI

    Мы получим следующее сообщение от компилятора ILASM:

    ilasm sample1.il

    Common Intermediate Language

    CIL и системное программирование в Microsoft .NET

    Код метода в сборке .NET представляет собой линейную последовательность CIL-инструкций и массив описателей блоков обработки исключений. Так как представленный в теле метода алгоритм в общем случае
    нелинейный, то есть содержит ветвления и циклы, то кодирование его в
    виде линейной последовательности требует определения семантики передачи управления от одной инструкции CIL к другой. Можно выделить четыре механизма передачи управления между инструкциями:
    1. Явная передача управления с помощью инструкции перехода.
    При этом в параметре инструкции перехода указано относительное смещение инструкции, на которую будет передано управление.
    2. Неявная (или естественная) передача управления на следующую
    инструкцию в последовательности.
    3. Передача управления на обработчик исключения при выходе (нормальном или аварийном) из защищенного блока. При такой передаче управления просматривается массив описателей блоков
    обработки исключений до нахождения первого подходящего
    блока, из описателя этого блока берется адрес обработчика и
    осуществляется переход на инструкцию по этому адресу.
    4. Передача управления между методами.
    В качестве примера рассмотрим фрагмент программы на языке CIL:
    .method private static int32 find(int32[] X, int32 k) {
    .locals init (int32 i, int32 result)
    .try {
    ldc.i4.0
    [2]
    stloc.0
    [2]
    br.s
    loop_cond
    [1]
    loop_body: ldloc.0
    [2]
    ldc.i4.1
    [2]
    add
    [2]
    stloc.0
    [2]
    loop_cond: ldarg.0
    [2]
    ldloc.0
    [2]
    ldelem.i4
    [2]
    ldarg.1
    [2]

    4.1. Граф потока управления

    Глава 4. Анализ кода на CIL

    132

    133

    Граф потока управления – это ориентированный граф, узлы которого
    соответствуют инструкциям CIL, а дуги изображают передачу управления
    между инструкциями.
    В качестве примера рассмотрим фрагмент программы на CIL:
    .method private static void print(int32[] X) {
    .locals init (int32 i)

    4.1.1. Основные элементы графа потока управления

    bne.un.s loop_body
    [1]
    ldloc.0
    [2]
    stloc.1
    [2]
    leave.s exit
    [3]
    }
    catch System.IndexOutOfRangeException {
    pop
    [2]
    ldc.i4.m1
    [2]
    stloc.1
    [2]
    leave.s exit
    [3]
    }
    exit: ldloc.1
    [2]
    ret
    [4]
    }
    Метод find выполняет поиск элемента k в массиве X. Если элемент
    найден, то возвращается его индекс. В противном случае возвращается -1.
    В листинге программы справа от каждой инструкции в квадратных скобках
    приведен тип передачи управления от нее на следующую инструкцию.
    Схема представления кода в виде линейной последовательности инструкций типична для ассемблерных языков и является наиболее компактной. Действительно, бoльшая часть кода метода состоит из линейных
    последовательностей инструкций с неявной передачей управления, а так
    как неявная передача определяется порядком следования инструкций и
    не требует дополнительного кодирования, то код занимает меньше места.
    Некоторые метаинструменты, выполняющие только анализ CIL-кода, могут непосредственно работать с линейной последовательностью инструкций. Это JIT-компиляторы, интерпретаторы, верификаторы и отладчики. Но для метаинструментов, которые выполняют преобразование CILкода, такое представление неудобно, так как при попытке вставить новую
    инструкцию в последовательность или удалить инструкцию из последовательности необходимо корректировать адреса во всех инструкциях перехода и во всех описателях блоков обработки исключений. Этих проблем можно избежать, если вместо линейной последовательности инструкций использовать представление CIL-кода в виде графа потока управления.

    Анализ кода на CIL

    }

    loop_cond:

    loop_body:

    ldc.i4.0
    stloc.0
    br.s
    loop_cond
    ldarg.0
    ldloc.0
    ldelem.i4
    call
    void System.Console::WriteLine(int32)
    ldloc.0
    ldc.i4.1
    add
    stloc.0
    ldloc.0
    ldarg.0
    ldlen
    conv.i4
    blt.s
    loop_body
    ret

    CIL и системное программирование в Microsoft .NET

    Как известно, блоки обработки исключений в CIL реализованы в виде массива описателей, который хранится отдельно от CIL-кода. Для аде-

    4.1.2. Блоки обработки исключений в графе потока управления

    Граф потока управления для приведенного в примере метода изображен на рис. 4.1. Узлы графа обозначены прямоугольниками, в которых записаны инструкции CIL. Точка входа в метод обозначена специальным узлом с меткой «Метод print». Передачи управления между инструкциями
    показаны стрелками.
    Любопытно, что инструкция безусловного перехода br.s на графе отсутствует. Действительно, она не несет никакого смысла и нужна только
    для кодирования тела метода в виде линейной последовательности инструкций.
    Количество дуг, исходящих из узла графа, зависит от записанной в
    нем инструкции. Это видно на примере инструкции blt.s, из которой исходит сразу две дуги. Дуга, помеченная числом 1, обозначает передачу управления в случае истинности проверяемого инструкцией blt.s условия.
    В случае ложности условия передача управления осуществляется по дуге,
    помеченной числом 0.
    Вообще, имеет смысл нумеровать дуги, исходящие из узла графа. При
    этом номер дуги должен задавать ее семантику. Всего можно выделить четыре варианта нумерации дуг графа. Эти варианты представлены в таблице 4.1.

    134

    0

    1

    stloc.0

    add

    ldc.i4.1

    135

    кватного представления блоков обработки исключений нам придется добавить в граф потока управления специальные узлы, обозначающие входы
    в блоки, а также специальные дуги, отражающие взаимосвязи между ними. Кроме того, мы обобщим понятие блока, введя так называемый блок
    тела метода.
    Блоки, представленные в графе потока управления, можно разделить
    на четыре основные категории:
    • Блок тела метода – главный блок графа, в который непосредственно или транзитивно входят все остальные узлы графа. Этот
    блок содержится в графе в единственном экземпляре и задает
    точку входа в граф (в примере он был помечен строкой «Метод
    print»).
    • Защищенный блок – соответствует try-блоку в программе. При
    выходе из защищенного блока управление может передаваться
    на один или несколько блоков обработки исключений.

    Рис. 4.1. Граф потока управления для метода print

    ret

    blt.s

    ldloc.0

    call ...

    ldlen

    conv.i4

    ldelem.i4

    ldarg.0

    stloc.0

    ldarg.0

    ldloc.0

    ldc.14.0

    Метод print

    Анализ кода на CIL

    Из узлов, которые соответствуют
    некоторым инструкциям, связанным с
    выходом из блока (throw, rethrow,
    endfinally, endfilter, ret),
    вообще не исходит дуг

    0

    • Блок обработки исключений – прикреплен к защищенному блоку и может получить управление при выходе из этого защищенного блока. В графе существуют три типа блоков обработки исключений: блок с фильтрацией по типу (catch-блок), блок с
    пользовательской фильтрацией и finally/fault-блок.
    • Блок фильтрации – прикреплен к блоку обработки исключений
    с пользовательской фильтрацией и осуществляет принятие решения о передачи управления на обработчик.
    Независимо от категории, к которой принадлежит блок, он имеет
    ровно один вход – входной узел, а выход из него может осуществляться
    только через один или несколько выходных узлов.

    Дуга с номером 0 обозначает передачу
    управления на следующую инструкцию

    2

    Нумерация
    для инструкции
    условного
    перехода
    Нумерация
    для последовательных
    инструкций
    Нумерация
    для
    «тупиковых»
    инструкций

    Дуга с номером 0 обозначает передачу
    управления, которая происходит при
    «неудаче» (когда ни один из случаев,
    перечисленных в инструкции switch,
    не получил управления).
    Если в инструкции switch записано N переходов, то передачи управления для этих
    переходов обозначают дугами с номерами
    от 1 до N
    Дуги с номерами 0 и 1 обозначают
    соответственно false-ветку и true-ветку
    условного перехода

    Семантика

    1

    Количество
    дуг
    Любое

    Таблица 4.1. Варианты нумерации дуг графа

    CIL и системное программирование в Microsoft .NET

    Вариант
    нумерации
    Нумерация
    для инструкции
    switch

    136

    Exception handling
    block

    ...

    Exception handling
    block

    ...

    ...

    ...

    Exit

    Exit

    Exit

    137

    Схема обработки исключений отражена в структуре графа с помощью введения дополнительных связей между защищенным блоком и блоками обработки исключений. Так, каждый защищенный блок имеет ссылки на соответствующие блоки обработки исключений. При этом ссылки
    пронумерованы в том порядке, в каком происходит выполнение обработчиков исключений. Другими словами, обработка исключений требует введения в граф особых дуг, которые отражают не передачу управления между инструкциями, а последовательность обработчиков исключений, присоединенных к защищенному блоку (см. рис. 4.2).
    Блок обработки исключений с пользовательской фильтрацией имеет
    более сложную структуру, чем другие блоки обработки исключений. Он соединен дополнительной дугой с блоком фильтрации. Подразумевается, что
    при активации блока с пользовательской фильтрацией управление сначала
    передается на этот блок фильтрации, который принимает решение о том,
    следует или нет передавать управление собственно на обработчик.
    Проиллюстрируем следующим примером особенности графа потока
    управления с блоками обработки исключений:
    .method private static int32 checkedAdd(int32 x, int32 y) {
    .locals init (int32 result)
    .try {
    ldarg.0
    ldarg.1
    add.ovf
    stloc.0
    leave.s exit
    }

    Рис. 4.2. Защищенный блок и прикрепленные к нему блоки обработки исключений

    N

    O

    Protected block

    Анализ кода на CIL

    CIL и системное программирование в Microsoft .NET

    ldc.i4.0

    stloc.0

    add.ovf

    leave.s

    stloc.0

    leave.s

    Когда выполнение некоторой инструкции в линейной последовательности инструкций порождает исключение, всегда можно легко определить защищенный блок, которому эта инструкция принадлежит. Для
    этого требуется всего лишь просканировать массив описателей блоков и

    4.1.3. Дерево блоков в графе потока управления

    Рис. 4.3. Граф потока управления для метода checkedAdd

    ret

    ldloc.0

    pop

    Catch-блок

    ldarg.0

    Защищенный
    блок

    ldarg.1

    Граф потока управления для метода checkedAdd показан на рис. 4.3.

    catch System.OverflowException {
    pop
    ldc.i4.0
    stloc.0
    leave.s exit
    }
    exit: ldloc.0
    ret
    }

    Метод
    checkedAdd

    138

    139

    Инструкции

    Защищенный блок

    Инструкции

    Инструкции

    Блоки обработки
    исключений

    Инструкции

    Инструкции

    Блоки обработки
    исключений

    Защищенный блок

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

    Рис. 4.4. Дерево блоков в структуре графа потока управления

    Инструкции

    Блоки обработки
    исключений

    Защищенный блок

    Блок тела метода

    найти первый блок, покрывающий диапазон адресов, в который попадает
    адрес нужной инструкции. Время выполнения этой операции зависит
    только от длины массива описателей, а так как он чаще всего бывает достаточно коротким, время поиска оказывается невелико.

    Анализ кода на CIL

    CIL и системное программирование в Microsoft .NET

    Разработчик любого метаинструмента, использующего граф потока
    управления в процессе анализа CIL-кода, сталкивается с проблемой преобразования линейной последовательности инструкций в граф потока управления. В данной главе мы рассмотрим алгоритм такого преобразования.
    Наш алгоритм будет работать на уровне метода, то есть в качестве
    входных данных для него будут выступать тело метода и массив предложений обработки исключений для этого метода. На выходе алгоритма будет
    построенный граф потока управления. Таким образом, для того чтобы построить набор графов потока управления для всей сборки, необходимо запустить этот алгоритм для каждого метода, входящего в сборку.
    Итак, пусть дан массив инструкций P размера N и массив предложений обработки исключений EH размера M. Требуется построить граф потока управления и возвратить ссылку на блок тела метода построенного
    графа.
    Мы будем предполагать, что в массивах P и EH адреса инструкций
    предварительно заменены на их номера (это касается встроенных операндов инструкций перехода, а также границ областей в предложениях обработки исключений). Кроме того, все массивы, которые мы будем рассматривать при описании алгоритма, включая массивы P и EH, будут индексироваться, начиная с нуля.

    4.2. Преобразование линейной
    последовательности инструкций
    в граф потока управления

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

    140

    141

    На первом этапе работы алгоритма мы создаем узел графа для каждой
    инструкции и формируем из созданных узлов массив.
    На входе первого этапа мы имеем массив P. Для каждой инструкции,
    входящей в массив P, мы создаем соответствующий ей узел графа. В этот
    узел записываются все данные об инструкции, кроме информации о передаче управления и принадлежности блокам. Другими словами, созданные
    узлы не связываются друг с другом дугами и не имеют ссылки на родительский блок.
    Узлы записываются в массив Nodes, состоящий из N элементов. При
    этом в массиве Nodes сохраняется порядок инструкций, то есть если некоторая инструкция располагается в массиве P по индексу i, то соответствующий ей узел будет размещен в i-том элементе массива Nodes.
    На C#-подобном псевдоязыке первый этап работы алгоритма можно
    записать следующим образом:
    Nodes = новый массив узлов размера N;
    for (int i = 0; i < N; i++)
    {

    4.2.1. Создание массива узлов

    Напомним также, что каждое предложение в массиве EH имеет следующий набор полей:
    • Flags.
    Задает тип обработчика исключений: обработчик с фильтрацией
    по типу, обработчик с пользовательской фильтрацией, обработчик finally или обработчик fault.
    • TryOffset.
    Номер инструкции в массиве P, с которой начинается защищенная область.
    • TryLength.
    Количество инструкций, входящих в защищенную область.
    • HandlerOffset.
    Номер инструкции в массиве P, с которой начинается область
    обработчика.
    • HandlerLength.
    Количество инструкций, входящих в область обработчика.
    • ClassToken.
    Токен метаданных, обозначающий тип исключения (используется в случае обработчика с фильтрацией по типу).
    • FilterOffset.
    Номер инструкции в массиве P, с которой начинается область
    фильтра (используется в случае обработчика с пользовательской
    фильтрацией).

    Анализ кода на CIL

    CIL и системное программирование в Microsoft .NET

    На втором этапе работы алгоритма мы строим дерево блоков на основе информации, находящейся в массиве предложений обработки исключений.
    На входе второго этапа мы имеем массив EH. На выходе получаем дерево блоков и вспомогательный массив B, связывающий блоки с информацией о диапазонах входящих в них инструкций (другими словами, с информацией об областях кода). Каждый элемент массива B будет состоять
    из трех полей:
    • Поле block.
    Это поле содержит ссылку на блок.
    • Поле offset.
    Содержит целое число, обозначающее индекс первой инструкции блока в массиве P.
    • Поле length.
    Содержит количество инструкций, входящих в блок (длина
    блока).
    Следует отметить, что размер массива B заранее неизвестен и зависит
    от информации в массиве EH. Нетрудно догадаться, что минимальное количество блоков в массиве B равно M+2 (если мы имеем один защищенный
    блок, M блоков обработки исключений и один блок тела метода). Аналогично, максимальное количество блоков вычисляется по формуле 2*M+1
    (M защищенных блоков, M блоков обработки исключений и один блок тела
    метода). Таким образом, необходимо либо сделать массив B динамическим, либо выделить для него 2*M+1 записей. В любом случае, пусть в дальнейшем BN обозначает текущее количество блоков, информация о которых
    хранится в массиве B.
    Сначала создадим блок тела метода (назовем его MBB). Он будет являться корнем дерева, которое мы строим. К нему в дальнейшем будут
    «прицеплены» все остальные узлы графа, и именно его наш алгоритм будет возвращать в результате своей работы. Для блока MBB в массив B добавляется запись, поле start которой содержит значение 0, а поле length –
    значение N.

    4.2.2. Создание дерева блоков

    Nodes[i] = новый узел, содержащий информацию об инструкции P[i];
    }
    Следует особо отметить, что для инструкций безусловного перехода
    также создаются отдельные узлы. Инструкцию nop мы будем считать инструкцией безусловного перехода по относительному адресу 0. Эти узлы для
    инструкций безусловного перехода являются временными и на последнем
    этапе алгоритма удаляются из графа.

    142

    143

    B = новый массив размера 2*M+1 для хранения информации о блоках;
    B[0].block = MBB; B[0].start = 0; B[0].length = N;
    BN = 1;
    for (int i = M-1; i >= 0; i--)
    {

    Напомним, что предложения обработки исключений расположены в
    массиве EH в определенном порядке, гарантирующем, что более вложенный блок находится ближе к началу массива, чем объемлющий его блок.
    Так как нам требуется строить дерево блоков в направлении от корня к листам, то есть от менее вложенных блоков к более вложенным, то мы будем
    просматривать массив EH от конца до начала.
    Итак, пусть переменная i пробегает значения от M-1 до 0 включительно. Тогда для каждого i-го предложения мы будем выполнять следующее:
    1. Поиск в массиве B блока, диапазон инструкций которого содержит защищенную область i-го предложения.
    Мы перебираем элементы массива B в обратном порядке, начиная с последнего элемента, поэтому найденный блок будет самым вложенным из блоков, диапазон инструкций которых содержит защищенную область i-го предложения.
    2. Создание нового защищенного блока и добавление его в дерево.
    Возможен случай, когда диапазон инструкций блока, найденного в пункте 1, совпадает с защищенной областью i-го предложения. Если это так, то создание нового защищенного блока
    не требуется. В противном случае мы создаем новый блок и добавляем информацию о нем в массив B. При этом родителем для
    созданного блока становится блок, найденный в пункте 1.
    3. Создание блока обработки исключений.
    На основе информации i-го предложения мы создаем новый
    блок обработки исключений, добавляем его в массив B и связываем его с защищенным блоком. При этом родителем созданного блока будет являться родительский блок защищенного блока.
    4. Создание блока фильтрации.
    Если мы имеем дело с блоком обработки исключений с пользовательской фильтрацией, нам придется создать новый блок
    фильтрации. При этом родителем для блока фильтрации является блок обработки исключений.
    На нашем псевдоязыке второй этап алгоритма можно записать следующим образом:
    MBB = новый блок тела метода, в который записана информация о
    методе:имя метода, сигнатура и данные о типах локальных
    переменных;

    Анализ кода на CIL

    144

    }

    if (EH[i].Flags обозначает блок с пользовательской фильтрацией)
    {
    /* Создание блока фильтрации */
    B[BN].block = новый блок фильтрации;
    Сделать блок B[BN-1].block родителем блока B[BN].block;
    B[BN].offset = EH[i].FilterOffset;
    B[BN].length = EH[i].HandlerOffset – EH[i].FilterOffset;
    BN++;
    }

    /* Создание блока обработки исключений */
    B[BN].block = новый блок обработки исключений,
    тип которого определяется значением EH[i].Flags;
    Сделать родителем блока B[BN].block тот же блок,
    который является родителем блока B[j].block;
    Добавить блок B[BN].block в конец списка
    обработчиков, ассоциированных с блоком B[j].block;
    B[BN].offset = EH[i].HandlerOffset;
    B[BN].length = EH[i].HandlerLength;
    BN++;

    if (tryOffset != B[j].offset || tryLength != B[j].length)
    {
    /* Создание нового защищенного блока и
    добавление его в дерево */
    B[BN].block = новый защищенный блок;
    Сделать блок B[j].block родителем блока B[BN].block;
    B[BN].offset = tryOffset;
    B[BN].length = tryLength;
    j = BN; BN++;
    }

    /* Поиск в массиве B блока, диапазон инструкций которого содержит
    защищенную областью i-го предложения */
    int tryOffset = EH[i].TryOffset, tryLength = EH[i].TryLength;
    for (int j = BN-1; j >= 0; j--)
    if (tryOffset >= B[j].offset &&
    tryOffset+tryLength <= B[j].offset+B[j].length)
    break;

    CIL и системное программирование в Microsoft .NET

    145

    На четвертом этапе в граф потока управления добавляются дуги, обозначающие передачу управления между инструкциями.
    Определенную трудность представляет необходимость включения в
    граф потока управления узлов-блоков. Предварительным шагом для этого
    служит создание специального массива blockNodes размера N и запись в
    этот массив ссылок на блоки. При этом запись осуществляется следующим образом: мы перебираем все структуры в массиве B и для каждой
    i-ой структуры записываем в элемент массива blockNodes с индексом
    B[i].offset ссылку B[i].block. В результате, для того, чтобы определить,
    какой блок начинается с инструкции с номером k, достаточно посмотреть
    k-тый элемент массива blockNodes.
    Формирование массива blockNodes удобно совместить с проведением
    дуг от узлов-блоков к узлам, соответствующим первым инструкциям этих

    4.2.4. Формирование дуг

    На третьем этапе узлы, соответствующие инструкциям, получают родителей. Тем самым завершается построение дерева блоков.
    Можно заметить, что все блоки графа, включая блок тела метода,
    хранятся в массиве B. При этом порядок их следования в массиве таков,
    что вложенные блоки размещаются перед объемлющими их блоками. Это
    следует из того факта, что на втором этапе алгоритма блоки добавлялись в
    массив в порядке, обратном их следованию в массиве предложений обработки исключений (EH).
    Присвоение родительских блоков узлам графа заключается в том, что
    мы перебираем все элементы массива B по порядку, начиная с первого элемента (в нем содержится информация о блоке тела метода). При этом для
    каждого i-го элемента массива B мы выполняем следующую операцию:
    каждый узел графа, имеющий в массиве Nodes индекс от B[i].offset до
    B[i].offset+B[i].length-1, получает в качестве родителя блок B[i].block.
    for (int i = 0; i < BN; i++)
    {
    for (int j = B[i].offset; j < B[i].offset+B[i].length; j++)
    Сделать блок B[i].block родителем узла Nodes[j];
    }
    Таким образом, на первой итерации цикла родителем всех узлов становится блок тела метода (MBB), а на последующих итерациях некоторые
    диапазоны инструкций меняют родителей, и это происходит до тех пор,
    пока не будут рассмотрены все элементы массива B. В конце концов родителями всех узлов становятся именно те блоки, в которые эти узлы непосредственно входят.

    4.2.3. Присвоение родительских блоков узлам графа

    Анализ кода на CIL

    CIL и системное программирование в Microsoft .NET

    for (int i = 0; i < N; i++)
    {
    int[] Flow = новый массив, в который добавляются
    номера инструкций, на которые может быть передано
    управление от инструкции, соответствующей узлу
    Nodes[i];
    for (int j = 0; j < Flow.Length; j++)
    {
    int n = Flow[j];
    if (blockNodes[n] != null && blockNodes[n] не является
    непосредственно или транзитивно родителем узла Nodes[i])
    Провести дугу от Nodes[i] к blockNodes[n];
    else
    Провести дугу от Nodes[i] к Nodes[n];
    }
    }

    блоков. Для этого нужно каждый блок, расположенный в массиве
    blockNodes по индексу i, соединить дугой с узлом Nodes[i]:
    blockNodes = новый массив ссылок на узлы размера N,
    изначально заполненный нулевыми ссылками;
    for (int i = 0; i < BN; i++)
    {
    Провести дугу от B[i].block к Nodes[B[i].offset];
    blockNodes[B[i].offset] = B[i].block;
    }
    Добавление остальных дуг осуществляется путем перебора всех узлов, находящихся в массиве Nodes. Давайте рассмотрим, как это происходит для некоторого узла Nodes[i].
    Прежде всего мы определяем номера инструкций, на которые может
    быть передано управление от инструкции, соответствующей узлу Nodes[i].
    Естественно, для этого должна учитываться семантика инструкции. Полученные номера инструкций записываются в массив Flow.
    Затем для каждого номера n, входящего в массив Flow, мы проводим
    дугу от узла Nodes[i] к соответствующему номеру n узлу графа:
    • это узел blockNodes[n] в том случае, если blockNodes[n] содержит
    ссылку на блок (то есть если инструкция под номером n является первой инструкцией некоторого блока) и Nodes[i] непосредственно или транзитивно принадлежит этому блоку;
    • это узел Nodes[n] в противном случае.
    На псевдоязыке это выглядит следующим образом:

    146

    147

    Алгоритмы верификации являются метавычислительными алгоритмами (верификаторы кода стоят в одном ряду с такими программами, как
    частичные вычислители и суперкомпиляторы). Поэтому совершенно
    неудивительно, что алгоритмы верификации в общем случае имеют проблемы, свойственные любым метавычислительным алгоритмам, а именно:
    экспоненциальная сложность и потенциальная нетерминируемость.
    Понятно, что алгоритмы, работающие неизвестно сколько времени
    (а возможно, вообще никогда не завершающиеся) и потребляющие неизвестно какое количество ресурсов, представляют скорее теоретический,
    нежели практический интерес. Поэтому их разработчикам приходится каким-то образом ограничивать задачу, чтобы получить применимые на практике результаты. Для алгоритмов верификации кода известно два подхода, позволяющие снизить их сложность и добиться терминируемости:
    1. Концентрация усилий на выявлении только таких ошибок в
    программах, поиск которых не требует экспоненциальной
    сложности и выполняется за конечное время.
    Для этого подхода характерно то, что существуют программы, в
    которых верификатор не находит ни одной ошибки и которые,
    тем не менее, разрушают память.
    2. Работа лишь с некоторым подмножеством программ, для которых факт неразрушения ими памяти можно доказать за конечное время с использованием алгоритма приемлемой сложности.
    Этот тип верификаторов отличается тем, что существуют не разрушающие память программы, которые не могут быть доказаны
    алгоритмом.
    Если задача некоторого метавычислительного алгоритма была как-то
    ограничена, то сразу же возникает вопрос о том, какие входные данные
    (программы) он сможет обработать. Для суперкомпиляторов и частичных

    4.3.1. Классификация применяемых на практике алгоритмов
    верификации

    Говорят, что код программ не разрушает память, если эти программы,
    будучи запущенными в одном адресном пространстве, корректно изолированы друг от друга, то есть ошибки в одной программе не могут привести к изменению структур данных другой программы.
    Верификация кода – это процесс автоматического доказательства того, что этот код не разрушает память.
    В этой главе мы рассмотрим общие вопросы верификации кода и алгоритм верификации, приведенный в спецификации CLI и реализованный в Microsoft .NET Framework.

    4.3. Верификация CIL-кода

    Анализ кода на CIL

    CIL и системное программирование в Microsoft .NET

    Верификатор кода .NET относится ко второму типу. Он может доказать, что сборки, генерируемые компиляторами C#, J# и Visual Basic .NET,
    не разрушают память. При этом он терминируем и имеет линейную сложность.
    Здесь может возникнуть следующий вопрос: зачем нужен верификатор, если компиляторы и так генерируют не разрушающий память код?
    Чтобы ответить на этот вопрос, нужно вспомнить, что:
    1. существуют компиляторы (например, Visual C++ with Managed
    Extensions), которые в общем случае могут порождать код, разрушающий память;
    2. в языке C# предусмотрены unsafe-блоки и unsafe-методы, внутри
    которых может находиться код, способный разрушить память;
    3. любой CIL-код, в том числе и разрушающий память, можно
    непосредственно компилировать с помощью ILASM;

    4.3.2. Особенности верификатора кода, используемого в .NET

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

    148

    149

    4.3.3.1. Совместимость типов
    Пусть S и T – типы. Тогда S[] и T[] – соответствующие им массивные
    типы, а S& и T& – типы соответствующих управляемых указателей.
    Тот факт, что S совместим по присваиванию с T, мы будем записывать
    как S := T.
    Операция := рефлексивна и транзитивна.
    Правила совместимости типов:
    1. S := T, если S – базовый класс для T или интерфейс, реализуемый T, и при этом T не является типом-значением.
    2. S := T, если S и T – интерфейсы, и реализация T требует реализации S.
    3. S := null, если S – объектный тип или интерфейс.
    4. S[] := T[], если S := T и размерности массивов совпадают.
    5. S& := T&, если S := T.
    Если ни одно из этих правил не выполняется, то типы S и T несовместимы.

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

    4.3.3. Алгоритм верификации

    4. всеми этими возможностями может воспользоваться злоумышленник для написания вредоносного кода.
    Другими словами, верификатор .NET предназначен не для поиска
    ошибок в программах, а для обеспечения безопасности системы.
    Мы будем различать два достаточно близких понятия, относящихся к
    верификации. Это верифицированный код и верифицируемый код.
    Верифицируемый код – это код, для которого верификатор может
    доказать, что он не разрушает память. Другими словами, для верифицируемого кода можно заранее, еще до запуска верификатора, сказать, что он
    успешно пройдет верификацию (такой код может генерироваться компилятором C#).
    Верифицированный код – это код, для которого верификатор доказал,
    что он не разрушает память. То есть чтобы из верифицируемого кода получить верифицированный код, нужно обязательно запустить верификатор.

    Анализ кода на CIL

    CIL и системное программирование в Microsoft .NET

    4.3.3.2. Конфигурации стека
    Будем называть конфигурацией стека данные о количестве слотов на
    стеке и типы значений, лежащих в этих слотах. При этом конфигурацию,
    содержащую 0 слотов, будем называть пустой конфигурацией.
    Рассмотрим две операции над конфигурациями, которые используются в алгоритме верификации:
    1. проверка совместимости двух конфигураций;
    2. слияние двух конфигураций.
    Операция проверки совместимости имеет два операнда: конфигурации C и K. Она возвращает булевское значение, показывающее, совместима ли конфигурация C с конфигурацией K.
    Приведем алгоритм вычисления операции проверки совместимости:
    1. Если количество слотов в конфигурациях C и K различно, то возвращаем false. В противном случае пусть N – количество слотов
    в конфигурации C (естественно, и в K тоже).
    2. Если N = 0, то возвращаем true.
    3. Пусть i пробегает значения от 1 до N. Тогда для каждого такого i
    выполняем следующее:
    a. пусть S – тип i-го слота конфигурации C, а T – тип i-го слота конфигурации K;
    b. если не T := S, то возвращаем false.
    4. Возвращаем true.
    Операция слияния двух конфигураций также имеет два операнда:
    конфигурации C и K. Она может либо закончиться неуспехом, либо возвращает конфигурацию R, вычисляемую по следующему алгоритму:
    1. Если количество слотов в конфигурациях C и K различно, то алгоритм завершается неуспехом. В противном случае пусть N –
    количество слотов в конфигурации C (естественно, и в K тоже).
    2. Если N = 0, то возвращаем пустую конфигурацию.
    3. Пусть i пробегает значения от 1 до N. Тогда для каждого такого i
    выполняем следующее:
    a. пусть S – тип i-го слота конфигурации C, а T – тип i-го слота конфигурации K.
    b. вычисляем тип U i-го слота результирующей конфигурации:
    i. если S := T, то U = S.
    ii. в противном случае, если T := S, то U = T.
    iii.в противном случае, если T и S – объектные типы и у
    них существует ближайший базовый тип V, то U = V.
    iv. в противном случае, алгоритм завершается неуспехом.
    4. Возвращаем результирующую конфигурацию.

    150

    151

    4.3.3.3. Описание алгоритма
    В процессе работы алгоритма для каждой инструкции CIL вычисляется конфигурация стека. При этом фактические значения, лежащие на
    стеке, в локальных переменных и параметрах метода, не учитываются.
    Алгоритм верификации работает со следующими данными:
    • Неизменяемые данные:
    o Метаданные сборки, в том числе количество и типы локальных переменных и параметров метода.
    o Массив P, содержащий инструкции CIL в том порядке, в
    каком они записаны в теле метода. Пусть N – количество
    инструкций.
    • Изменяемые данные:
    o Массив M размера N, в котором хранятся вычисляемые в
    процессе верификации конфигурации стека.
    Возможны два результата работы алгоритма:
    • Успешная верификация.
    Метод не содержит неверифицируемых инструкций. Все возможные пути передачи управления в теле метода рассмотрены.
    Для каждой инструкции вычислена конфигурация стека.
    • Неуспешная верификация.
    Метод либо содержит неверифицируемые инструкции, либо в
    процессе вычисления конфигурации стека для некоторой инструкции было выявлено противоречие.
    В начале работы алгоритма в массиве M не записано ни одной конфигурации.
    Алгоритм последовательно просматривает массив P, то есть на каждом шаге работает с одной инструкцией P[i], где i пробегает значения от
    1 до N (будем считать, что массивы P и M нумеруются, начиная с единицы).
    На каждом шаге алгоритма выполняются следующие действия:
    1. Итак, P[i] – текущая рассматриваемая инструкция. Тогда смотрим, что собой представляет M[i]. Если M[i] еще не содержит
    конфигурацию стека, то записываем в M[i] пустую конфигурацию.
    2. Проверяем, верифицируема ли инструкция P[i], учитывая конфигурацию стека M[i] и информацию, содержащуюся в метаданных. Если инструкция в принципе не верифицируема или
    не может быть выполнена при заданной конфигурации стека, то
    алгоритм завершается неуспехом.
    3. Осуществляем абстрактное выполнение инструкции P[i]. Другими словами, вычисляем конфигурацию стека S, которая получилась бы после реального выполнения инструкции.
    4. Определяем, на какие инструкции может быть передано выпол-

    Анализ кода на CIL

    CIL и системное программирование в Microsoft .NET

    Во второй главе мы рассмотрели написанную на языке C программу,
    осуществлявшую генерацию сборки .NET. Эта программа не использовала никаких библиотек для работы с метаданными и CIL-кодом, то есть задача генерации сборки была решена на самом низком уровне – на уровне
    двоичных форматов. Хотя такой подход во многих случаях бывает оправдан, на практике гораздо удобнее воспользоваться специальными библиотеками, предназначенными для разработчиков метаинструментов.
    Напомним, что метаинструменты для платформы .NET – это программы, рассматривающие сборки .NET в качестве объектов анализа, генерации или преобразования. При этом анализ существующей сборки выполняется верификаторами, загрузчиками, отладчиками и т.д. Генерация
    новой сборки осуществляется компиляторами и RAD-средствами. Преобразование сборки выполняется оптимизаторами и средствами, затрудняющими декомпиляцию и взлом сборки.
    Сборка .NET представляет собой совокупность метаданных, CIL-кода и, возможно, ресурсов. Поэтому библиотеки, необходимые для создания метаинструментов, должны обеспечивать чтение и генерацию этой
    информации. В составе .NET Framework SDK поставляются две библиотеки, частично решающие эти задачи. Это Metadata Unmanaged API и библиотека рефлексии (Reflection API). Кроме того, существуют созданные
    сторонними разработчиками библиотеки AbsIL SDK и Reflection Extension
    API. В данном разделе проведем обзор этих библиотек.

    4.4. Библиотеки для создания
    метаинструментов

    нение после инструкции P[i]. Для каждой такой инструкции
    P[j] выполняем следующее:
    a. если j < i, то проверяем, совместима ли конфигурация S с
    конфигурацией M[j]. Если оказывается, что несовместима,
    то алгоритм завершается неуспехом;
    b. если j >= i и M[j] еще не содержит конфигурации стека, то
    M[j] = S;
    c. если j >= i и M[j] уже содержит конфигурацию стека, то
    выполняем попытку слияния конфигураций S и M[j]. Если
    попытка увенчалась успехом, то записываем результат слияния в M[j]. В противном случае алгоритм завершается
    неуспехом.
    Если все инструкции были просмотрены и ни на одном шаге алгоритм не завершился неуспехом, то метод успешно прошел верификацию.

    152

    153

    IMetadataImport
    IMetadataEmit

    Интерфейс
    IMetadataDispenserEx

    Описание
    Позволяет загружать образ исполняемого
    либо объектного файла в память или создавать новый образ
    Предназначен для чтения метаданных
    Предназначен для генерации метаданных

    Таблица 4.2. Интерфейсы Metadata Unmanaged API

    Metadata Unmanaged API осуществляет импорт и генерацию метаданных. Это API, как явствует из его названия, работает не под управлением
    .NET Runtime. Оно предназначено, главным образом, для использования
    в компиляторах и загрузчиках, которым требуется высокая скорость доступа к метаданным и работа с метаданными на низком уровне.
    При использовании Metadata Unmanaged API метаинструмент работает с образом сборки в памяти (в документации такой образ называется
    scope). Метаинструмент может создать образ новой сборки или загрузить
    существующую сборку из файла, а кроме того, может сохранить образ
    сборки в файл. Навигация через иерархию метаданных осуществляется на
    достаточно низком уровне с использованием токенов метаданных (напомним, что токен некоторого элемента метаданных – это 32-разрядное число, старший байт которого обозначает таблицу, в которой хранятся элементы метаданных соответствующего типа, а остальные три байта являются индексом элемента в этой таблице). Metadata Unmanaged API предоставляет прямой доступ к таблицам метаданных, позволяет сливать несколько сборок в одну, но не содержит явных средств для работы с CIL-кодом.
    Кроме того, метаинструмент, использующий это API, должен быть написан на C++.
    Для того чтобы использовать Metadata Unmanaged API из программы, написанной на Visual C++, необходимо включить в программу следующие строки:
    #include
    #pragma comment(lib, “format.lib”)
    Первая строка подключает заголовочный файл, в котором описываются нужные интерфейсы, а также вспомогательные структуры данных и
    функции. Вторая строка дает указание компоновщику использовать библиотеку format.lib.
    Взаимодействие с Metadata Unmanaged API осуществляется через набор COM-интерфейсов. Они перечислены в таблице 4.2.

    4.4.1. Metadata Unmanaged API

    Анализ кода на CIL

    CIL и системное программирование в Microsoft .NET

    Работа с Metadata Unmanaged API начинается с инициализации системы COM и получения указателя на интерфейс IMetadataDispenserEx:
    CoInitialize(NULL);
    IMetaDataDispenser *dispenser;
    HRESULT h = CoCreateInstance(
    CLSID_CorMetaDataDispenser,
    NULL,
    CLSCTX_INPROC_SERVER,
    IID_IMetaDataDispenserEx,
    (void **)&dispenser
    );
    if (h)
    printf(“Error”);
    Затем можно с помощью метода OpenScope этого интерфейса загрузить существующую сборку в память или с помощью метода DefineScope
    создать новую сборку. Оба метода возвращают указатель на интерфейс, через который можно в дальнейшем осуществлять чтение или генерацию
    метаданных. В следующем фрагменте программы происходит вызов метода OpenScope для чтения метаданных из сборки test.exe:
    IMetaDataImport *mdimp;
    HRESULT h = dispenser->OpenScope(
    L”test.exe”,
    0,
    IID_IMetaDataImport,
    (IUnknown**)&mdimp
    );
    if (h)
    printf(“Error”);
    При завершении работы с Metadata Unmanaged API необходимо освободить указатели на полученные интерфейсы:
    mdimp->Release();
    dispenser->Release();
    Чтение метаданных из сборки осуществляется через методы интерфейса IMetadataImport, которые условно можно разделить на три основные группы:
    1. Методы EnumXXX возвращают массивы токенов, описывающих
    определенную категорию элементов метаданных. Ответственность за выделение достаточного количества памяти для хранения возвращаемого массива токенов лежит на программисте,
    использующем библиотеку, а так как количество токенов заранее неизвестно, то приходится использовать прием, проиллюстрированный следующим примером. В нем мы получаем массив

    154

    155

    токенов, который соответствует типам, объявленным в сборке
    (другими словами, получаем содержимое таблицы TypeDef):
    mdTypeDef tmp;
    mdTypeDef *tokens;
    HCORENUM Enum = 0;
    unsigned long tokenCount;
    mdimp->EnumTypeDefs(&Enum,&tmp,1,&tokenCount);
    mdimp->CountEnum(Enum,&tokenCount);
    if (tokenCount > 0)
    {
    tokens = new mdTypeDef [tokenCount];
    tokens[0] = tmp;
    if (tokenCount > 1)
    mdimp->EnumTypeDefs(
    &Enum,tokens+1,tokenCount-1,
    &tokenCount
    );
    mdimp->CloseEnum(Enum);
    }
    Первый вызов метода EnumTypeDefs создает дескриптор массива
    токенов (он записывается в переменную Enum), а также возвращает первый элемент этого массива (он сохраняется в переменной tmp).
    Затем метод CountEnum записывает в переменную tokenCount размер массива токенов, после чего выделяется нужное количество
    памяти (для массива tokens) и второй раз вызывается метод
    EnumTypeDefs. Обратите внимание, что первому элементу массива tokens мы присваиваем значение переменной tmp.
    2. Методы FindXXX предназначены для поиска элементов метаданных, удовлетворяющих некоторым критериям. Например, поиск
    типа по его имени («MyType1») проводится следующим образом:
    mdTypeDef token;
    mdimp->FindTypeByName(L”MyType1”,NULL,&token);
    3. Методы GetXXX используются для получения свойств элементов
    метаданных. Например, получение содержимого строки, токен
    которой хранится в переменной strToken, выглядит так:
    unsigned short s[1024];
    unsigned long len;
    mdimp->GetUserString(strToken,s,1024,&len);
    Обратите внимание, что для хранения одного символа применяется
    тип unsigned short. Причина в том, что для представления строковых данных в .NET используется 16-разрядная кодировка Unicode.

    Анализ кода на CIL

    CIL и системное программирование в Microsoft .NET

    4.4.2.1. Чтение метаданных через рефлексию
    Метаинструмент, использующий библиотеку рефлексии, получает доступ к метаданным сборки через экземпляр класса Assembly, который создается при загрузке сборки в память. Затем через методы класса Assembly можно
    получить объекты класса Module, которые соответствуют модулям, входящим
    в сборку. Класс Module, в свою очередь, позволяет получить набор экземпляров класса Type для входящих в модуль типов, а уже через эти объекты можно добраться до конструкторов (класс ConstructorInfo), методов (класс
    MethodInfo) и полей (класс FieldInfo) типов. То есть библиотека рефлексии
    спроектирована таким образом, что создание объектов рефлексии, соответствующих элементам метаданных, осуществляется путем вызова методов
    других объектов рефлексии (схема получения доступа к объектам рефлексии
    показана на рис. 4.5, при этом сплошные стрелки обозначают основные пути получения доступа, а пунктирные – дополнительные пути). Другими словами, конструкторы классов, входящих в библиотеку, для пользователя библиотеки недоступны. Информацию о любом элементе метаданных можно
    прочитать из свойств соответствующего ему объекта рефлексии.
    Экземпляр класса Assembly, с создания которого, как правило, начинается работа со сборкой через рефлексию, можно получить разными способами:
    1. Если нас интересует текущая работающая сборка, мы можем
    вызвать статический метод GetExecutingAssembly класса
    Assembly:
    Assembly assembly = Assembly.GetExecutingAssembly().
    2. Если мы хотим получить доступ к сборке, в которой объявлен
    некоторый тип данных, мы можем воспользоваться статическим
    методом GetAssembly:
    Assembly assembly = Assembly.GetAssembly(typeof(int)).

    Библиотека рефлексии содержит классы для работы с метаданными
    на высоком уровне (эти классы располагаются в пространствах имен
    System.Reflection и System.Reflection.Emit). Она соответствует спецификации CLS, поэтому использующий ее метаинструмент может быть написан на любом языке платформы .NET, который является потребителем
    CLS-библиотек.

    4.4.2. Reflection API

    Генерация метаданных осуществляется через методы интерфейса
    IMetadataEmit, которые можно условно разделить на две группы:
    1. Методы DefineXXX добавляют новые элементы метаданных;
    2. Методы SetXXX устанавливают свойства элементов метаданных.
    Сгенерированные метаданные могут быть сохранены на диске при
    помощи метода Save.

    156

    PropertyInfo

    ConstructorInfo

    MethodInfo

    FieldInfo

    ParameterInfo

    157

    Assembly assembly = Assembly.LoadFrom(“test.exe”);
    Module[] modules = assembly.GetModules(false);

    3. И, наконец, если нам надо загрузить внешнюю сборку, мы используем статический метод LoadFrom:
    Assembly assembly = Assembly.LoadFrom(“test.exe”).
    Сборка .NET, как правило, состоит из одного модуля, хотя существует возможность включения в сборку сразу нескольких модулей. Получение доступа к объектам класса Module, описывающим модули, из которых
    состоит сборка, осуществляется одним из двух способов:
    1. Если мы знаем имя модуля, то мы можем использовать метод
    GetModule класса Assembly:
    Module module = assembly.GetModule(“SomeModule.exe”).
    2. Кроме того, мы можем вызвать метод GetModules для получения
    массива объектов класса Module, соответствующих всем модулям
    в сборке:
    Module[] modules = assembly.GetModules(false).
    Через объект класса Module мы можем обратиться к глобальным полям и функциям модуля (глобальные поля и функции не принадлежат ни
    одному классу), используя методы GetField и GetFields для полей, а также
    GetMethod и GetMethods для функций. При этом полям будут соответствовать объекты класса FieldInfo, а методам – объекты класса MethodInfo. В
    следующем примере мы выводим на экран сигнатуры всех глобальных
    функций некоторой сборки «test.exe»:

    Рис. 4.5. Получение доступа к объектам рефлексии

    Type

    Module

    Assembly

    Анализ кода на CIL

    CIL и системное программирование в Microsoft .NET

    foreach (Module mod in modules)
    {
    MethodInfo[] methods = mod.GetMethods();
    foreach (MethodInfo met in methods)
    Console.WriteLine(met);
    }
    Особое место в библиотеке рефлексии занимает класс Type, представляющий типы. Область применения этого класса значительно шире, чем
    области применения остальных классов рефлексии, и этот факт отражен в
    том обстоятельстве, что класс Type входит в пространство имен System, а
    не System.Reflection.
    В каждом классе существует унаследованный от класса System.Object
    метод GetType, возвращающий экземпляр класса Type, который описывает
    этот класс. В следующем примере метод GetType будет использован для динамического определения типа объекта o (так как этот объект представляет собой строку, то на печать будет выведено сообщение «Sytem.String»):
    object o = new String(“qwerty”);
    Type t = o.GetType();
    Console.WriteLine(t);
    В C# определен специальный оператор typeof, который возвращает
    объект класса Type. Например, если нам нужно получить объект Type,
    представляющий тип массива строк, мы можем записать:
    Type t = typeof(string[]);
    Кроме этого, доступ к информации о типе можно получить, зная имя
    этого типа:
    1. путем вызова статического метода GetType класса Type:
    Type t = Type.GetType(“System.Char”);
    2. через объекты классов Assembly или Module, которые соответствуют сборке или модулю, содержащему нужный тип:
    • Type t = module.GetType(“Class1“);
    • Type t = assembly.GetType(“Class1“).
    Мы также можем воспользоваться методами GetTypes классов
    Assembly и Module для получения массива типов, объявленных в сборке или
    модуле. В следующем примере на экран выводится список типов, содержащихся в сборке «test.exe»:
    Assembly assembly = Assembly.LoadFrom(“test.exe”);
    Type[] types = assembly.GetTypes();
    foreach (Type t in types)
    Console.WriteLine(t);
    Имея объект рефлексии, соответствующий некоторому типу, мы
    имеем возможность получить доступ к объектам рефлексии, описывающим его поля, конструкторы, методы, свойства и вложенные типы. При

    158

    159

    4.4.2.2. Управление объектами
    Кроме чтения метаданных, библиотека рефлексии позволяет создавать экземпляры типов, входящих в обрабатываемую сборку, вызывать методы этих типов, читать и изменять значения полей.
    Рассмотрим класс TestClass:
    class TestClass
    {
    private int val;
    public TestClass() { val = 7; }
    protected void print() { Console.WriteLine(val); }
    }
    В следующем примере мы используем метод Invoke класса MethodInfo
    для вызова метода print объекта класса TestClass. Обратите внимание, что
    метод print объявлен с модификатором доступа protected.
    static void CallMethodDemo()
    {
    TestClass a = new TestClass();
    BindingFlags flags = (BindingFlags)
    (BindingFlags.NonPublic | BindingFlags.Instance);

    этом, если мы знаем их имена, мы можем воспользоваться методами
    GetField, GetConstructor, GetMethod, GetProperty и GetNestedType класса
    Type. А если мы хотим получить массивы объектов рефлексии, описывающих члены типа, то нам нужно вызвать методы GetFields, GetConstructors,
    GetMethods, GetProperties и GetNestedTypes. В следующем примере на экран выводится список типов, содержащихся в сборке, вместе с сигнатурами их методов:
    Assembly assembly = Assembly.LoadFrom(“test.exe”);
    Type[] types = assembly.GetTypes();
    foreach (Type t in types)
    {
    Console.WriteLine(“”+t+”:”);
    MethodInfo[] methods = t.GetMethods();
    foreach (MethodInfo m in methods)
    Console.WriteLine(“ “+m);
    }
    Для каждого метода и конструктора мы можем получить доступ к
    массиву объектов рефлексии, описывающих его параметры. Для этого
    служит метод GetParameters, объявленный в классах MethodInfo и
    ConstructorInfo. Данный метод возвращает массив объектов класса
    ParameterInfo:
    ParameterInfo[] parms = method.GetParameters();

    Анализ кода на CIL

    MethodInfo printer =
    typeof(TestClass).GetMethod(“print”,flags);
    printer.Invoke(a, null);

    CIL и системное программирование в Microsoft .NET

    4.4.2.3. Генерация метаданных и CIL-кода
    Для генерации метаданных в библиотеке рефлексии предназначены
    классы пространства имен System.Reflection.Emit. Создание новой сборки
    начинается с создания экземпляра класса AssemblyBuilder. Далее путем вызова методов класса AssemblyBuilder создается нужное количество модулей
    (объектов класса ModuleBuilder), в модулях создаются типы (объекты класса TypeBuilder), а в типах – конструкторы, методы и поля (объекты классов
    ConstructorBuilder, MethodBuilder и FieldBuilder, соответственно). То есть
    при генерации метаданных идеология такая же, как и при чтении их из готовой сборки (на рис. 4.6 представлена схема, показывающая последовательность создания метаданных).

    }
    Путем внесения небольших изменений в CallMethodDemo мы можем
    добиться того, что объект класса TestClass будет также создаваться через
    рефлексию (с помощью вызова его конструктора):
    static void CallMethodDemo2()
    {
    Type t = typeof(TestClass);
    BindingFlags flags = (BindingFlags)
    (BindingFlags.NonPublic | BindingFlags.Instance);
    ConstructorInfo ctor = t.GetConstructor(new Type[0]);
    MethodInfo printer = t.GetMethod(“print”,flags);
    object a = ctor.Invoke(null);
    printer.Invoke(a, null);
    }
    А теперь продемонстрируем, как через рефлексию можно получить
    значение поля объекта. Это достигается путем вызова метода GetField объекта класса Type:
    static void GetFieldDemo()
    {
    TestClass a = new TestClass();
    BindingFlags flags = (BindingFlags)
    (BindingFlags.NonPublic |
    BindingFlags.Instance);
    FieldInfo val =
    typeof(TestClass).GetField(“val”,flags);
    Console.WriteLine(val.GetValue(a));
    }

    160

    161

    MethodBuilder

    ConstructorBuilder

    ModuleBuilder

    TypeBuilder

    В следующем примере библиотека рефлексии используется для генерации сборки .NET, состоящей из одного глобального метода main, выводящего на экран сообщение «Hello, World!»:
    using System;
    using System.Threading;
    using System.Reflection;
    using System.Reflection.Emit;
    class HelloGenerator
    {
    static void Main(string[] args)
    {
    AppDomain appDomain = Thread.GetDomain();
    AssemblyName assemblyName = new AssemblyName();
    assemblyName.Name = “hello.exe”;
    AssemblyBuilder assembly =
    appDomain.DefineDynamicAssembly(
    assemblyName,
    AssemblyBuilderAccess.RunAndSave
    );
    ModuleBuilder module =
    assembly.DefineDynamicModule(
    “hello.exe”, “hello.exe”

    ILGenerator

    ParameterBuilder

    Рис. 4.6. Последовательность создания метаданных

    PropertyBuilder

    FieldBuilder

    AssemblyBuilder

    В отличие от Metadata Unmanaged API, библиотека рефлексии содержит средства для генерации CIL-кода. Для этого предназначен класс
    ILGenerator. Он позволяет после создания объекта MethodBuilder (или
    ConstructorBuilder) сгенерировать для соответствующего метода (или
    конструктора) поток инструкций.

    Анализ кода на CIL

    }

    }

    );
    MethodBuilder mainMethod =
    module.DefineGlobalMethod(
    “main”,
    MethodAttributes.Static |
    MethodAttributes.Public,
    typeof(void),
    null
    );
    MethodInfo ConsoleWriteLineMethod =
    ((typeof(Console)).GetMethod(“WriteLine”,
    new Type[] { typeof(string) } ));
    ILGenerator il = mainMethod.GetILGenerator();
    il.Emit(OpCodes.Ldstr,”Hello, World!”);
    il.Emit(OpCodes.Call,ConsoleWriteLineMethod);
    il.Emit(OpCodes.Ret);
    module.CreateGlobalFunctions();
    assembly.SetEntryPoint(
    mainMethod,PEFileKinds.ConsoleApplication
    );
    assembly.Save(“hello.exe”);

    CIL и системное программирование в Microsoft .NET

    Reflection API
    +
    +

    +

    Из таблицы следует, что ни одна из библиотек, поставляемых вместе
    с .NET Framework, не позволяет читать CIL-код, хотя эта возможность
    требуется целому ряду метаинструментов (например, верификаторам и
    оптимизаторам кода).

    Чтение метаданных
    Генерация метаданных
    Чтение CIL-кода
    Генерация CIL-кода

    Metadata Unmanaged API
    +
    +



    Таблица 4.3. Сравнение возможностей библиотек

    Возможности, предоставляемые библиотекой Metadata Unmanaged
    API и библиотекой рефлексии, представлены в таблице 4.3.

    4.4.3. Сравнение возможностей библиотек

    162

    163

    Динамическая генерация кода – это прием программирования, заключающийся в том, что фрагменты кода порождаются и запускаются
    непосредственно во время выполнения программы. Этот прием был известен достаточно давно, но усложнение архитектуры компьютеров, и, что
    особенно важно, усложнение наборов команд процессоров привело к тому, что в последние 10-15 лет динамическая генерация кода в некоторой
    степени потеряла популярность.
    Целью динамической генерации кода является использование информации, доступной только во время выполнения программы, для повышения качества исполняемого кода. В терминах метавычислений можно
    сказать, что динамическая генерация кода позволяет специализировать
    фрагменты программы по данным, известным во время выполнения.
    В некотором смысле, любой JIT-компилятор как раз использует динамическую генерацию кода: имея некоторую программу, записанную на
    промежуточном языке (байт-коде), и зная, какой процессор работает в системе, JIT-компилятор динамически транслирует программу в инструкции этого процессора. При этом можно считать, что тип процессора – эта
    как раз та часть информации, которая становится известной только во
    время выполнения программы.
    Естественно, не стоит чересчур увлекаться динамической генерацией
    кода: этот прием далеко не всегда дает ускорение программы. Можно сказать, что применение динамической генерации оправдано, если:
    1. процесс вычислений в некотором фрагменте программы
    преимущественно определяется информацией, известной только во время выполнения;
    2. запуск этого фрагмента осуществляется многократно;
    3. выполнение фрагмента связано с существенными затратами
    времени процессора.
    В .NET доступно два способа организации динамической генерации
    кода:
    1. порождение программы на языке C# и вызов компилятора C#;
    2. непосредственное порождение метаданных и CIL-кода.

    5.1. Введение в динамическую генерацию кода

    Глава 5.
    Динамическая генерация кода

    Динамическая генерация кода

    CIL и системное программирование в Microsoft .NET

    Для интегрирования функций нам потребуется некое представление
    функции, которое бы не зависело от конкретного способа вычисления
    значения функции. Идеальным вариантом такого представления является
    абстрактный класс Function:
    public abstract class Function
    {
    public abstract double Eval(double x);
    }
    Объявив такой класс, мы предполагаем, что от него будут наследоваться другие классы, реализующие в методе Eval конкретный способ вычисления значения функции в заданной точке.
    Имея класс Function, мы можем записать обобщенный алгоритм интегрирования методом прямоугольников. В качестве параметров этот алгоритм принимает объект f, представляющий интегрируемую функцию,
    пределы интегрирования a и b, а также количество разбиений n:

    5.1.1. Обобщенный алгоритм интегрирования

    Если сравнить эти два способа, можно придти к выводу, что порождение C#-программы несколько проще, нежели генерация CIL-кода. Однако,
    наличие
    в
    библиотеке
    классов
    пространства
    имен
    System.Reflection.Emit позволяет избежать рутинных и трудоемких операций по работе с физическим представлением метаданных и CIL-кода.
    Кроме того, генерация CIL-кода выполняется на порядок быстрее и дает
    большую гибкость. Поэтому для программиста, знакомого с набором инструкций CIL, второй способ является более предпочтительным.
    В этом разделе мы рассмотрим простой пример программы на языке
    C#, выполняющей численное интегрирование функции, которую пользователь вводит с клавиатуры (то есть интегрируемая функция становится
    известной только в процессе выполнения программы). Исходный код
    примера приведен в Приложении B. Характерной особенностью задачи
    численного интегрирования является необходимость многократного вычисления значения функции в разных точках. При этом, так как функция
    представлена в виде строки, это вычисление связано со значительными
    затратами времени процессора. Таким образом, данная задача по всем
    признакам подходит для использования динамической генерации кода.
    Мы будет выполнять вычисление значения функции тремя способами:
    1. Без динамической генерации кода (путем непосредственной интерпретации выражения).
    2. Путем динамической генерации программы на языке C#.
    3. Путем динамической генерации метаданных и CIL-кода.
    Затем мы сравним эффективность каждого способа.

    164

    165

    В нашем примере пользователь будет вводить выражение с клавиатуры, то есть оно будет представлено в виде текстовой строки. Такое представление неудобно ни для непосредственного вычисления значения
    функции, ни для генерации кода, вычисляющего ее значение. Поэтому
    нам понадобится парсер и некоторое представление, в которое этот парсер
    будет переводить введенную с клавиатуры текстовую строку.
    Детали синтаксического анализа и представления выражений мы
    рассматривать не будем: интересующиеся могут обратиться к полным исходным текстам примера. Скажем лишь, что анализ осуществляется методом рекурсивного спуска и транслирует выражение в дерево, в узлах которого расположены объекты, представляющие арифметические операции и
    их операнды. Каждый из этих объектов является экземпляром одного из
    четырех классов, обозначающих числовые константы, переменные, унарные и бинарные операции. Причем все эти классы наследуют от абстрактного класса Expression:
    public abstract class Expression
    {
    public abstract string GenerateCS();
    public abstract void GenerateCIL(ILGenerator il);
    public abstract double Evaluate(double x);
    }
    В классе Expression объявлены три абстрактных метода, которые каждый класс-наследник реализует по-своему. Метод Evaluate выполняет

    5.1.2. Представление выражений

    static double Integrate(Function f, double a, double b, int n)
    {
    double h = (b-a)/n, sum = 0.0;
    for (int i = 0; i < n; i++)
    sum += h*f.Eval((i+0.5)*h);
    return sum;
    }
    Для проверки работоспособности алгоритма можно объявить тестовый класс TestFunction, реализующий вычисление функции f(x) = x *
    sin(x):
    public class TestFunction: Function
    {
    public override double Eval(double x)
    {
    return x * Math.Sin(x);
    }
    }

    Динамическая генерация кода

    CIL и системное программирование в Microsoft .NET

    Как уже говорилось, самым простым способом динамической генерации кода является порождение текста C#-программы и компиляция
    этой программы с помощью компилятора C#, доступного через библиотеку классов .NET.
    В нашем примере динамическую генерацию сборки осуществляет
    статический метод CompileToCS, который получает транслируемое выражение в виде объекта класса Expression и возвращает объект Function:
    static Function CompileToCS(Expression expr)
    {
    ICodeCompiler compiler = new CSharpCodeProvider().CreateCompiler();
    CompilerParameters parameters = new CompilerParameters();

    5.1.3. Трансляция выражений в C#

    непосредственное вычисление значения выражения, метод GenerateCS
    транслирует выражение в фрагмент программы на C#, а метод GenerateCIL
    транслирует выражение в CIL-код.
    Вопросы генерации кода будут обсуждаться в следующих разделах,
    поэтому сейчас мы только приведем пример CIL-кода, генерируемого методом GenerateCIL для дерева объектов, которые представляют выражение
    ”2*x*x*x+3*x*x+4*x+5”:
    ldc.r8
    2.0
    ldarg.1
    mul
    ldarg.1
    mul
    ldarg.1
    mul
    ldc.r8
    3.0
    ldarg.1
    mul
    ldarg.1
    mul
    add
    ldc.r8
    4.0
    ldarg.1
    mul
    add
    ldc.r8
    5.0
    add
    Метод GenerateCS фактически восстанавливает из дерева строковое
    представление выражения и в особых комментариях не нуждается.

    166

    167

    }
    Классы, отвечающие за компиляцию исходного кода, относятся к
    пространству имен System.CodeDom.Compiler.
    Основную функциональность, необходимую нам для компиляции
    сгенерированной C#-программы, обеспечивает класс CSharpCodeProvider.
    Метод CreateCompiler этого класса создает экземпляр компилятора C#, к
    которому можно обращаться через интерфейс ICodeCompiler.
    Параметры компиляции задаются через объект класса
    CompilerParameters. В нашем случае это имена сборок, импортируемых генерируемой программой: System.dll и Integral.exe. Обратите внимание,
    что Integral.exe – это сборка, получаемая при компиляции рассматриваемого нами примера. Она импортируется по причине того, что динамически генерируемый класс FunctionCS должен наследовать от определенного
    в ней абстрактного класса Function.
    Параметры компиляции и строка, содержащая текст программы, передаются методу CompileAssemblyFromSource экземпляра компилятора C#.
    Метод компилирует программу и возвращает объект класса
    CompilerResults, содержащий результаты компиляции. Из этого объекта
    мы можем получить объект рефлексии Assembly, представляющий созданную в памяти динамическую сборку. Используя данный объект, мы создаем экземпляр определенного в динамической сборке класса FunctionCS,
    который в дальнейшем может быть использован для вычисления значения
    функции в процессе интегрирования.

    CompilerResults compilerResults =
    compiler.CompileAssemblyFromSource(parameters,code);
    Assembly assembly = compilerResults.CompiledAssembly;
    return assembly.CreateInstance(“FunctionCS”) as Function;

    string e = expr.GenerateCS();
    string code = “public class FunctionCS: Function\n”+
    “{\n”+
    “ public override double Eval(double x)\n”+
    “ {\n”+

    return “+e+”;\n”+
    “ }\n”+
    “}\n”;

    parameters.ReferencedAssemblies.Add(“System.dll”);
    parameters.ReferencedAssemblies.Add(“Integral.exe”);
    parameters.GenerateInMemory = true;

    Динамическая генерация кода

    CIL и системное программирование в Microsoft .NET

    Более сложный, но эффективный способ динамической генерации
    кода предоставляется классами, относящимися к пространству имен
    System.Reflection.Emit. Эти классы в нашем примере используются в статическом методе CompileToCIL, который осуществляет трансляцию выражения напрямую в CIL:
    static Function CompileToCIL(Expression expr)
    Метод начинается с создания заготовки для будущей динамической
    сборки. Сборка будет выполняться в том же домене приложений, что и основная программа, поэтому объектную ссылку на домен приложений мы
    получаем путем вызова статического метода Thread.GetDomain. Затем вызываем метод DefineDynamicAssembly домена приложений и получаем объект класса AssemblyBuilder, позволяющий строить динамическую сборку:
    AppDomain appDomain = Thread.GetDomain();
    AssemblyName assemblyName = new AssemblyName();
    assemblyName.Name = “f”;
    AssemblyBuilder assembly =
    appDomain.DefineDynamicAssembly(
    assemblyName,
    AssemblyBuilderAccess.RunAndSave
    );
    Теперь мы можем создать в сборке модуль и добавить в него класс
    FunctionCIL, наследующий от класса Function. Обратите внимание, что
    при
    генерации
    кода
    через
    классы
    пространства
    имен
    System.Reflection.Emit явно прописывать импортируемые сборки не надо
    (например, не надо прописывать сборку Integral.exe, из которой импортируется класс Function), так как это выполняется автоматически:
    ModuleBuilder module =
    assembly.DefineDynamicModule(“f.dll”, “f.dll”);
    TypeBuilder typeBuilder =
    module.DefineType(
    “FunctionCIL”,
    TypeAttributes.Public | TypeAttributes.Class,
    typeof(Function)
    );
    В каждом классе должен быть конструктор. Компилятор C# создает
    конструкторы без параметров по умолчанию, поэтому при генерации C#кода нам не надо было явно объявлять конструктор в классе FunctionCS.
    Однако, при генерации динамической сборки через классы пространства
    имен System.Reflection.Emit конструкторы автоматически не добавляются, и нам придется сделать это самостоятельно:

    5.1.4. Трансляция выражений в CIL

    168

    169

    Давайте оценим эффективность рассмотренных способов вычисления выражений при интегрировании. Для этого будем интегрировать
    функцию «2*x*x*x+3*x*x+4*x+5» от 0.0 до 10.0 с 10000000 разбиений.
    В таблице 5.1 представлены результаты измерений, проведенных на
    компьютере с процессором Intel Pentium 4 с тактовой частотой 3000 МГц и
    1 Гб оперативной памяти.

    5.1.5. Сравнение эффективности трех способов вычисления
    выражений

    ILGenerator il = evalMethod.GetILGenerator();
    expr.GenerateCIL(il);
    il.Emit(OpCodes.Ret);
    Итак, мы закончили формирование класса FunctionCIL. Осталось создать для него объект рефлексии и через этот объект вызвать конструктор:
    Type type = typeBuilder.CreateType();
    ConstructorInfo ctor = type.GetConstructor(new Type[0]);
    return ctor.Invoke(null) as Function;
    Таким образом, получается объект класса FunctionCIL, который в
    дальнейшем можно использовать для вычисления значения функции в
    процессе интегрирования.

    ConstructorBuilder cons =
    typeBuilder.DefineConstructor(
    MethodAttributes.Public,
    CallingConventions.Standard,
    new Type[] { }
    );
    ILGenerator consIl = cons.GetILGenerator();
    consIl.Emit(OpCodes.Ldarg_0);
    consIl.Emit(OpCodes.Call,
    typeof(object).GetConstructor(new Type[0]));
    consIl.Emit(OpCodes.Ret);
    Разобравшись с конструктором, переходим к методу Eval. Как уже
    говорилось, код этого метода почти полностью генерируется в методе
    GenerateCIL выражения, остается лишь добавить в конец инструкцию ret:
    MethodBuilder evalMethod =
    typeBuilder.DefineMethod(
    “Eval”,
    MethodAttributes.Public | MethodAttributes.Virtual,
    typeof(double),
    new Type[] { typeof(double) }
    );

    Динамическая генерация кода

    CIL и системное программирование в Microsoft .NET

    172

    63

    ::=
    ::=
    ::=
    ::=

    Expr field f
    minus Expr
    Expr BinOp Expr
    local x assign Expr

    ArgList ::= Expr ArgList

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

    BinaryOp
    BinaryOp
    BinaryOp
    BinaryOp

    ::=
    ::=
    ::=
    ::=

    plus
    minus
    mul
    div

    ArgList ::= пусто

    Expr ::= Expr index Expr
    assign Expr
    Expr ::= Expr field f
    assign Expr
    Expr ::= Expr call s ArgList

    Expr ::= arg x assign Expr

    Expr
    Expr
    Expr
    Expr

    Expr ::= local x
    Expr ::= arg x
    Expr ::= Expr index Expr

    Правило
    Expr ::= const c

    171

    Описание
    Некоторая константа. Мы не уточняем тип константы, так как для
    дальнейшего изложения это несущественно. Это может быть целое
    число, число с плавающей запятой,
    строка, значение null
    Локальная переменная с именем x
    Параметр метода с именем x
    Доступ к элементу массива. Здесь первое выражение должно возвращать
    ссылку на массив, а второе – целое
    число, означающее индекс элемента
    Доступ к полю f объекта
    Унарный минус
    Бинарная арифметическая операция
    Операция присваивания
    переменной x значения выражения
    Операция присваивания параметру
    x значения выражения
    Операция присваивания элементу
    массива значения выражения
    Операция присваивания полю f
    объекта значения выражения
    Вызов экземплярного метода s для
    некоторого объекта с передачей
    списка фактических параметров
    Непустой список фактических параметров метода
    Пустой список фактических параметров
    Сложение
    Вычитание
    Умножение
    Деление

    Таблица 5.2. Абстрактный синтаксис выражений

    Динамическая генерация кода

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

    5.2.1. Генерация кода для выражений

    В этом разделе мы рассмотрим специфику генерации линейных участков кода на языке CIL.
    Линейными участками мы будем называть участки кода, не содержащие развилок и защищенных блоков.

    5.2. Генерация линейных участков кода для
    стековой машины

    Результаты показывают, что динамическая генерация кода может на
    два порядка уменьшить время работы программы.

    172

    Время вычисления
    интеграла функции, мс
    29422

    547

    Время на создание
    динамической сборки, мс


    Таблица 5.1. Результаты измерений эффективности трех способов вычисления выражений

    Способ вычисления
    значения функции
    Интерпретация
    дерева выражения
    Предварительная
    компиляция в C#
    Предварительная
    компиляция в CIL

    170

    CIL и системное программирование в Microsoft .NET

    5.2.1.2. Отображение абстрактного синтаксиса выражений в CIL
    Определим набор функций, отображающих различные деревья абстрактного синтаксиса в соответствующие им последовательности инструкций CIL.
    Будем считать, что каждая функция принимает в качестве параметра
    дерево абстрактного синтаксиса (оно записывается в квадратных скобках)
    и возвращает последовательность инструкций (при этом запятые обозначают операцию объединения последовательностей):
    GenExpr[const c] = нужный вариант инструкции ldc;
    GenExpr[local x] = ldloc x;
    GenExpr[arg x] = ldarg x;
    GenExpr[Expr1 index Expr2] =
    GenExpr[Expr1],
    GenExpr[Expr2],
    ldelem.нужный тип;
    GenExpr[Expr field f] =
    GenExpr[Expr],
    ldfld f;
    GenExpr[minus Expr] =
    GenExpr[Expr],
    neg;
    GenExpr[Expr1 BinOp Expr2] =
    GenExpr[Expr1],
    GenExpr[Expr2],
    GenBinOp[BinOp];
    GenExpr[local x assign Expr] =
    GenExpr[Expr],
    dup,
    stloc x;
    GenExpr[arg x assign Expr] =
    GenExpr[Expr],
    dup,
    starg x;
    GenExpr[Expr1 index Expr2 assign Expr3] =
    GenExpr[Expr1],
    GenExpr[Expr2],
    GenExpr[Expr3],
    dup,
    stloc временная переменная,
    stelem.нужный тип,
    ldloc временная переменная;
    GenExpr[Expr1 field f assign Expr2] =

    172

    173

    Зачастую бывает удобно разделить фазы генерации и оптимизации
    кода. Это позволяет существенно упростить генератор, сделать его независимым от оптимизатора и, кроме того, повторно использовать оптимизатор с другими генераторами.
    Существует большое количество методов оптимизации, но в контексте динамической генерации кода, требующей быстрой работы оптимизатора, не все эти методы применимы. Поэтому мы рассмотрим один из самых простых методов – так называемую peephole-оптимизацию.
    Суть peephole-оптимизации заключается в том, что оптимизатор
    ищет в коде метода сравнительно короткую последовательность инструкций, удовлетворяющую некоторому образцу, и заменяет ее более эффективной последовательностью инструкций.
    Алгоритм peephole-оптимизации использует понятие фрейма. Фрейм
    можно представить как окошко, двигающееся по коду метода. Содержимое фрейма сравнивается с образцом, и в случае совпадения выполняется
    преобразование (см. рис. 5.1).
    Peephole-оптимизация линейного участка кода должна выполняться
    многократно до тех пор, пока на очередном проходе фрейма по этому участку кода не будет найдено ни одного образца. С другой стороны, алгоритм peephole-оптимизации может быть остановлен в любой момент, что
    позволяет добиться требуемой скорости работы оптимизатора.

    5.2.2. Оптимизация линейных участков кода

    GenArgList[Expr ArgList] =
    GenExpr[Expr],
    GenArgList[ArgList];
    GenArgList[пусто] = ;
    GenBinOp[plus] = add;
    GenBinOp[minus] = sub;
    GenBinOp[mul] = mul;
    GenBinOp[div] = div;

    GenExpr[Expr1],
    GenExpr[Expr2],
    dup,
    stloc временная переменная,
    stfld f,
    ldloc временная переменная;
    GenExpr[Expr call s ArgList] =
    GenExpr[Expr],
    GenArgList[ArgList],
    call(callvirt) s;

    Динамическая генерация кода

    dup
    stloc.0
    stloc.1

    stloc.0

    stloc.1

    ldloc.0

    Рис. 5.1. Peephole-оптимизация

    dup

    dup

    CIL и системное программирование в Microsoft .NET

    Генерация кода, содержащего инструкции переходов, представляет
    некоторую сложность по сравнению с генерацией линейного кода. Дело в
    том, что появляются переходы вперед по коду, то есть переходы на инструкции, которые еще не были сгенерированы. Общий метод решения этой
    проблемы заключается в том, что такие инструкции переходов генерируются частично, то есть сначала вместо них в код вставляются заглушки, в
    которых не прописаны адреса переходов, а затем, когда адрес становится
    известен, заглушки заменяются на настоящие инструкции переходов.
    Интересен факт, что генерация развилок существенно упрощается,
    если в процессе генерации придерживаться определенных требований
    структурированной парадигмы в программировании. Эти требования заключаются в том, что в генерируемой программе используются только
    пять структурных конструкций, а именно: последовательность (рис. 5.2a),
    выбор (рис. 5.2b), множественный выбор (рис. 5.2c), цикл с предусловием
    (рис. 5.2d) и цикл с постусловием (рис. 5.2e). При этом конструкции могут
    быть вложены друг в друга.

    5.3. Генерация развилок

    В таблице 5.3 приведен список некоторых образцов и замен, которые
    можно использовать для peephole-оптимизации CIL-кода.

    174

    175

    ldc.i4.0
    stloc(starg) x
    dup
    stloc(starg) x
    ldloc(ldarg) y
    add (или любая коммутативная
    бинарная операция)

    pop
    pop


    pop

    Замена
    dup
    stloc(starg) x
    ldloc (ldarg) x
    dup
    ckfinite

    Логические выражения отличаются от рассмотренных ранее в этой
    главе арифметических выражений тем, что могут вычисляться не полностью. Например, в выражении
    (a = 10) and (sin(x) = 0.5)
    второе равенство имеет смысл вычислять, только если первое равенство
    истинно (то есть если значение переменной a равно 10).
    Это означает, что в коде, вычисляющем логические выражения,
    должны активно использоваться условные переходы.

    5.3.1. Генерация кода для логических выражений

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

    Образец
    stloc(starg) x
    ldloc(ldarg) x
    ldloc (ldarg) x
    ldloc (ldarg) x
    ckfinite
    ckfinite
    not(neg)
    pop
    add(sub,mul,div,...)
    pop
    ldc.i4.0
    add(sub)
    ldloca(ldarga) x
    initobj int32
    stloc(starg) x
    ldloc(ldarg) y
    ldloc(ldarg) x
    add (или любая коммутативная
    бинарная операция)

    Таблица 5.3. Некоторые образцы и замены для peephole-оптимизации
    CIL-кода

    Динамическая генерация кода

    b)

    c)

    Рис. 5.2. Структурные конструкции

    a)

    d)

    e)

    CIL и системное программирование в Microsoft .NET

    5.3.1.2. Отображение абстрактного синтаксиса логических выражений в CIL
    Аналогично функциям GenExpr из раздела 5.2.1.2, определим набор
    функций GenLogExpr, которые отображают деревья абстрактного синтаксиса, соответствующие логическим выражениям, в CIL.
    Напомним, что каждая функция принимает в качестве параметра
    дерево абстрактного синтаксиса и возвращает последовательность инструкций:
    GenLogExpr[Expr] = GenExpr[Expr];
    GenLogExpr[LogExpr1 ComparisonOp LogExpr2] =
    GenLogExpr[LogExpr1],
    GenLogExpr[LogExpr2],
    GenComparisonOp[ComparisonOp];
    GenLogExpr[LogExpr1 and LogExpr2] =

    5.3.1.1. Абстрактный синтаксис логических выражений
    Будем рассматривать логические выражения, которые содержат
    арифметические выражения, рассмотренные в разделе 5.2.1, в качестве
    подвыражений. Пусть также логические выражения содержат операции
    сравнения (равно, меньше, больше) и логические операции (логическое
    И, логическое ИЛИ, логическое НЕ).
    Дополним абстрактный синтаксис выражений, приведенный ранее в
    данной главе, новым нетерминалом LogExpr. Правила для этого нетерминала приведены в таблице 5.4.

    176

    177

    Применение логического НЕ
    Равенство
    Меньше
    Больше

    Применение логического ИЛИ

    Применение логического И

    Описание
    Вырожденный случай, когда логическое выражение не содержит ни
    одной логической операции или
    операции сравнения
    Сравнение двух выражений

    GenLogExpr[LogExpr1],
    dup,
    brfalse LABEL,
    GenLogExpr[LogExpr2],
    and,
    LABEL: ;
    GenLogExpr[LogExpr1 or LogExpr2] =
    GenLogExpr[LogExpr1],
    dup,
    brtrue LABEL,
    GenLogExpr[LogExpr2],
    or,
    LABEL: ;
    GenLogExpr[not LogExpr] =
    GenLogExpr[LogExpr],
    not;
    ComparisonOp[equal] = ceq;
    ComparisonOp[less] = нужный вариант инструкции clt;
    ComparisonOp[greater] = нужный вариант инструкции cgt;

    LogExpr ::= LogExpr
    ComparisonOp LogExpr
    LogExpr ::= LogExpr
    and LogExpr
    LogExpr ::= LogExpr
    or LogExpr
    LogExpr ::= not LogExpr
    ComparisonOp ::= equal
    ComparisonOp ::= less
    ComparisonOp ::= greater

    Правило
    LogExpr ::= Expr

    Таблица 5.4. Абстрактный синтаксис логических выражений

    Динамическая генерация кода

    CIL и системное программирование в Microsoft .NET

    Непустая последовательность
    предложений
    Пустая последовательность предложений

    Цикл с постусловием

    Цикл с предусловием

    Описание
    Предложение является выражением. Это может быть, например, выражение, содержащее операцию
    присваивания или вызов метода
    объекта
    Выбор с двумя альтернативами

    5.3.2.2. Отображение абстрактного синтаксиса управляющих
    конструкций в CIL
    Как уже говорилось, структурные управляющие конструкции допускают рекурсивный алгоритм генерации. Поэтому мы можем определить
    набор функций GenStatement, транслирующих деревья абстрактного синтаксиса в последовательности инструкций.

    Statement ::= if LogExpr
    StatementList else
    StatementList
    Statement ::= while
    LogExpr StatementList
    Statement ::= do
    StatementList
    while LogExpr
    StatementList ::=
    Statement StatementList
    StatementList ::= пусто

    Правило
    Statement ::= Expr

    Таблица 5.5. Абстрактный синтаксис управляющих конструкций

    5.3.2.1. Абстрактный синтаксис управляющих конструкций
    В таблице 5.5 приведен абстрактный синтаксис для последовательности, выбора и циклов с предусловием и постусловием. При записи абстрактного синтаксиса используется определенный ранее нетерминал
    LogExpr для представления условий выбора и циклов.

    Воспользовавшись уже отработанной схемой генерации кода, перейдем на уровень выше и рассмотрим генерацию основных структурных управляющих конструкций.

    5.3.2. Генерация кода для управляющих конструкций

    178

    179

    5.3.3.1. Удаление избыточных инструкций сохранения значений в переменных
    Это преобразование уменьшает количество присваиваний. Оно осуществляется только для переменных, адреса которых не используются.

    Рассмотрим несколько простых методов оптимизации кода, содержащего развилки, а именно:
    • удаление избыточных инструкций сохранения значений в переменных;
    • удаление псевдонимов переменных;
    • воспроизведение констант;
    • удаление неиспользуемых переменных.
    Хороших результатов можно достичь, если применять эти методы в совокупности с peephole-оптимизацией. При этом получаемая цепочка оптимизирующих преобразований должна выполняться над одним и тем же кодом многократно до тех пор, пока не будет достигнута неподвижная точка.

    5.3.3. Оптимизация кода, содержащего развилки

    GenStatement[Expr] =
    GenExpr[Expr],
    pop;
    GenStatement[if LogExpr StatementList1 else StatementList2] =
    GenLogExpr[LogExpr],
    brfalse LABEL1,
    GenStatementList[StatementList1],
    br LABEL2,
    LABEL1: GenStatementList[StatementList2],
    LABEL2: ;
    GenStatement[while LogExpr StatementList] =
    LABEL1: GenLogExpr[LogExpr],
    brfalse LABEL2,
    GenStatementList[StatementList],
    br LABEL1,
    LABEL2: ;
    GenStatement[do StatementList while LogExpr] =
    LABEL: GenStatementList[StatementList],
    GenLogExpr[LogExpr],
    brtrue LABEL;
    GenStatementList[Statement StatementList] =
    GenStatement[Statement],
    GenStatementList[StatementList];
    GenStatementList[пусто] = ;

    Динамическая генерация кода

    CIL и системное программирование в Microsoft .NET

    stloc X

    stloc X

    ldloc X

    Рис. 5.3. Пример графа использования переменной

    stloc X

    stloc X

    Под переменными мы будем понимать как локальные переменные, так и
    параметры методов.
    Другими словами, избыточные инструкции stloc и starg удаляются
    только для переменных, не использующихся в инструкциях ldloca и ldarga.
    Для обнаружения избыточных инструкций сохранения значений выполняется анализ использования переменной, состоящий из двух фаз:
    1. Построение графа использования переменной.
    2. Анализ графа использования переменной.
    Инструкции ldloc(ldarg) X и stloc(starg) X будем называть инструкциями использования переменной X.
    Мы будем говорить, что в графе потока управления инструкция использования B следует за инструкцией использования A на пути w, если:
    1. инструкции A и B используют одну и ту же переменную X;
    2. путь w соединяет A и B;
    3. путь w не содержит ни одной инструкции использования переменной X, кроме инструкций A и B.
    Граф использования переменной X – это ориентированный граф, в узлах которого находятся инструкции использования переменной X, а дуги
    задают отношение следования для этих инструкций. То есть, если инструкция B следует за инструкцией A на каком-либо пути в графе потока управления, то в графе использования переменной X имеется дуга от инструкции A к инструкции B.
    Анализ графа использования переменной заключается в нахождении
    таких инструкций stloc(starg), за которыми не следует ни одной инструкции ldloc(ldarg). Эти инструкции являются избыточными и заменяются инструкциями pop.
    На рисунке 5.3 изображен пример графа использования переменной.
    Серым цветом обозначены избыточные инструкции stloc.

    180

    181

    5.3.3.3. Воспроизведение констант
    Это преобразование позволяет избавиться от переменных, имеющих
    константное значение.
    Переменная Y имеет константное значение C тогда и только тогда,
    когда:

    Рис. 5.4. Удаление псевдонима Y переменной X

    ldloc X

    pop

    stloc Y

    ldloc Y

    ldloc X

    ldloc X

    5.3.3.2. Удаление псевдонимов переменных
    Это преобразование позволяет избавиться от лишних присваиваний и уменьшает количество локальных переменных. Под переменной
    будем понимать, опять же, как локальные переменные, так и параметры
    методов.
    Переменная Y является псевдонимом переменной X тогда и только
    тогда, когда:
    1. переменная X используется в теле метода только один раз, причем в инструкции ldloc(ldarg) X;
    2. за инструкцией ldloc(ldarg) X непосредственно следует инструкция stloc(starg) Y (впрочем, допускается наличие между ними любого количества инструкций dup). Причем инструкция
    stloc(starg) Y является первым использованием переменной Y
    (назовем ее инструкцией инициализации переменной Y).
    Схема удаления псевдонима Y переменной X показана на рис. 5.4.
    При удалении осуществляются два действия:
    1. Инструкция инициализации переменной Y заменяется инструкцией pop.
    2. Все использования переменной Y заменяются использованиями
    переменной X.

    Динамическая генерация кода

    CIL и системное программирование в Microsoft .NET

    pop

    ldc C

    stloc Y

    ldloc Y

    Рис. 5.5. Схема воспроизведения константы C, являющейся значением переменной Y

    ldc C

    ldc C

    5.3.3.4. Удаление неиспользуемых переменных
    Если некоторая переменная не используется в графе метода или
    встречается только в инструкциях stloc(starg), то она удаляется. При
    этом все инструкции stloc(starg), использующие эту переменную, заменяются инструкциями pop.

    1. первым использованием переменной Y является инструкция
    stloc(starg) Y (назовем ее инструкцией инициализации переменной Y);
    2. инструкция инициализации переменной Y непосредственно
    следует за инструкцией ldc C (любая инструкция загрузки константы на стек вычислений). Впрочем, допускается наличие между ними любого количества инструкций dup;
    3. за исключением инструкции инициализации, переменная Y используется только в инструкциях ldloc(ldarg) Y.
    Схема воспроизведения константы C, являющейся значением переменной Y, показана на рисунке 5.5. При воспроизведении осуществляются два действия:
    1. Инструкция инициализации переменной Y заменяется инструкцией pop.
    2. Инструкции ldloc(ldarg) Y заменяются инструкциями ldc C.

    182

    183

    Начиная знакомство с многозадачными системами, необходимо выделить понятия мультипроцессирования и мультипрограммирования.
    • Мультипроцессирование – использование нескольких процессоров для одновременного выполнения задач.
    • Мультипрограммирование – одновременное выполнение
    нескольких задач на одном или нескольких процессорах.

    6.1.1. Основные понятия

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

    6.1. Многозадачность в Windows

    Глава 6.
    Основы многозадачности

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

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

    Рис. 6.1. Разные подходы к реализации мультипроцессирования

    Ассиметричное ультипроцессирование

    Симметричное мультипроцессирование

    Одинаковые устройства

    Различные устройства

    Мультипроцессирование

    6.1.1.1. Мультипроцессирование
    Говоря о мультипроцессировании, необходимо выделить ситуации, когда используются различные виды оборудования, например, одновременная
    работа центрального процессора и графического ускорителя видеокарты;
    либо когда организуется одновременная работа равноправных устройств,
    выполняющих сходные задачи. Последний случай (см. рис. 6.1) также предполагает различные подходы – с выделением управляющего и подчиненных
    устройств (асимметричное мультипроцессирование), либо с использованием
    полностью равноправных (симметричное мультипроцессирование).

    В завершение знакомства укажем некоторые термины, определяющие базовые понятия, которыми оперируют операционные системы.

    184

    185

    ОЗУ

    ШИНА

    ЦПУ 2

    ...

    ЦПУ N

    • при необходимости создания систем с качественно большим
    числом процессоров прибегают к MPP (Massively Parallel
    Processors) системам. Для этого используют несколько однопроцессорных или SMP-систем, объединяемых с помощью некоторого коммуникационного оборудования в единую сеть (см. рис.
    6.3). При этом может применяться как специализированная высокопроизводительная среда передачи данных, так и обычные
    сетевые средства – типа Ethernet. В MPP системах оперативная
    память каждого узла обычно изолирована от других узлов, и для
    обмена данными требуется специально организованная пересылка данных по сети. Для MPP систем критической становится среда передачи данных; однако в случае мало связанных между собой процессов возможно одновременное использование
    большого числа процессоров. Число процессоров в MPP системах может измеряться сотнями и тысячами.

    Рис. 6.2. SMP компьютер

    ЦПУ 1

    • наиболее массовыми являются так называемые SMP (Shared
    Memory Processor или Symmetric MultiProcessor) машины. В таких компьютерах несколько процессоров подключены к общей
    оперативной памяти и имеют к ней равноправный и конкурентный доступ (см. рис. 6.2). По мере увеличения числа процессоров производительность оперативной памяти и коммутаторов,
    связывающих процессоры с памятью, становится критически
    важной. Обычно в SMP используются 2-8 процессоров; реже
    число процессоров достигает десятков. Взаимодействие одновременно выполняющихся процессов осуществляется посредством использования общей памяти, к которой имеют равноправный доступ все процессоры.

    Основы многозадачности

    186

    Среда передачи данных

    SMP 2

    SMP N

    а

    н

    и

    Ш

    ОЗУ

    ЦПУ 2

    ШИНА

    а

    н

    и

    Ш
    ...
    ОЗУ

    ЦПУ N

    Рис. 6.4. NUMA или cc-NUMA система

    ОЗУ

    ЦПУ 1

    а

    н

    и

    Ш

    • иногда используют так называемые NUMA и cc-NUMA архитектуры; они являются компромиссом между SMP и MPP системами: оперативная память является общей и разделяемой между
    всеми процессорами, но при этом память неоднородна по времени доступа. Каждый процессорный узел имеет некоторый объем
    оперативной памяти, доступ к которой осуществляется максимально быстро; для доступа к памяти другого узла потребуется
    значительно больше времени (см. рис. 6.4). cc-NUMA отличается от NUMA тем, что в ней на аппаратном уровне решены вопросы когерентности кэш-памяти (cache-coherent) различных
    процессоров. Формально на NUMA системах могут работать
    обычные операционные системы, созданные для SMP систем,
    хотя для обеспечения высокой производительности приходится
    решать нетипичные для SMP задачи оптимального размещения
    данных и планирования с учетом неоднородности памяти.

    Рис. 6.3. MPP система

    SMP 1

    ...

    CIL и системное программирование в Microsoft .NET

    187

    6.1.1.2. Мультипрограммирование
    Мультипрограммирование (то есть одновременное выполнение разного кода на одном или нескольких процессорах) возможно и без реального мультипроцессирования. Конечно, при наличии только одного процессора должен существовать некоторый механизм, обеспечивающий переключение процессора между разными выполняемыми потоками. Такой
    режим разделения процессорного времени позволяет одному процессору
    обслуживать несколько задач «как бы одновременно»: осуществляя быстрое переключение между разными задачами и выполняя в данный момент времени код только одной задачи, процессор создает иллюзию одновременного выполнения кода разных задач. Более того, даже на многопроцессорных системах при реальной возможности распараллеливания
    задач по разным процессорам, обычно используют механизм разделения
    времени на каждом из доступных процессоров. Формально мультипрограммирование предполагает именно разделение процессорного времени,
    поэтому иногда его противопоставляют мультипроцессированию: реали-

    Таким образом, с точки зрения реализации мультипроцессирования,
    для разработчиков ПО важно иметь представление о том, каковы средства
    взаимодействия между параллельно работающими ветвями кода – общая
    память с равноправным или неравноправным доступом, либо некоторая
    коммуникационная среда с механизмом пересылки данных. Наибольшее
    распространение получили SMP и MPP системы, соответственно, большинство операционных систем содержат необходимые средства для эффективного управления SMP системами. Для реализации MPP систем,
    как правило, используются обычные операционные системы на всех узлах
    и либо обычные сетевые технологии, с применением распространенных
    стеков протоколов, либо специфичное коммуникационное оборудование
    со своими собственными драйверами и собственными средствами взаимодействия с приложениями. NUMA системы распространены в меньшей
    степени, хотя выпускаются серийно. Нормальным при этом является применение массовых операционных систем, рассчитанных на SMP установки, несмотря на то, что это несколько снижает эффективность использования NUMA.
    Windows содержит встроенные механизмы, необходимые для работы
    на SMP; также возможна установка этой ОС на cc-NUMA системах (современные версии Windows имеют механизмы поддержки cc-NUMA систем). Специальных, встроенных в ОС средств для исполнения приложений на MPP системах в Windows не предусмотрено. Windows предполагает
    альтернативное применение MPP систем, построенных на обычных сетях,
    для реализации web- или файловых серверов с балансировкой нагрузки по
    узлам кластера.

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

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

    188

    Вытесняющая многозадачность

    С абсолютными приоритетами

    С использованием квантов
    (постоянных или динамических)

    189

    Понятия абсолютных и относительных приоритетов связаны с их
    влиянием на момент переключения с одной задачи на другую: в системах
    с абсолютными приоритетами такое переключение выполняется, как
    только в очереди готовых к исполнению задач появляется задача с более
    высоким приоритетом, чем выполняемая. В системах с относительными
    приоритетами появление более приоритетной задачи не приводит к
    немедленному переключению – момент переключения задач будет определяться по каким-либо иным критериям.
    Выделяют понятия вытесняющей и невытесняющей многозадачности: в случае невытесняющей многозадачности решение о переключении
    принимает выполняемая в данный момент задача, а в случае вытесняющей многозадачности такое решение принимается операционной системой (или иным арбитром), независимо от работы активной в данный момент задачи.
    На приведенном графе состояний задачи (см. рис. 6.6) прямая линия
    от состояния «выполнение» к состоянию «готовность» нарисована пунктиром, чтобы выделить отличие невытесняющей многозадачности от вытесняющей. В случае невытесняющей многозадачности выполняющаяся
    задача может либо завершиться, либо перейти в состояние «ожидание».
    И тот, и другой переходы определены логикой работы самой задачи. На

    Рис. 6.5. Планирование задач

    Невытесняющая многозадачность

    С относительными
    приоритетами

    С использованием
    приоритетов

    Динамическое

    Статическое

    Планирование

    Основы многозадачности

    Запуск
    задачи

    Выполнение

    Готовность

    Задача
    вытеснена из
    состояния
    выполнения

    графе состояний задачи в случае невытесняющей многозадачности пунктирной линии от состояния «выполнение» к состоянию «готовность» не
    будет. В случае вытесняющей многозадачности вытеснение осуществляется по решению системы, а не только по инициативе задачи.
    Для невытесняющей многозадачности характерно, что операционная
    система передает задаче управление и далее ожидает от нее сигнала, информирующего о возможности переключения на другую задачу; сама по
    себе операционная система выполняемую задачу не прерывает. Именно
    поэтому невытесняющая многозадачность рассматривается как многозадачность с относительными приоритетами – пока задача сама не сообщит,
    что настал подходящий для переключения момент, система не сможет передать управление никакой другой, даже высокоприоритетной, задаче.
    Невытесняющая многозадачность проста в реализации, особенно на
    однопроцессорных машинах, и, кроме того, обеспечивает очень малый
    уровень накладных расходов на реализацию плана. Недостатками являются повышенная сложность разработки приложений и невысокая защищенность системы от некачественных приложений.
    Характерный пример невытесняющей многозадачности – 16-ти разрядные Windows (включая собственно 16-ти разрядные версии Windows,
    выполнение 16-ти разрядных приложений в Windows-95, 98, ME и выполнение 16-ти разрядных приложений в рамках одной Windows-машины в

    Задача
    переходит в
    режим
    ожидания

    Ожидание

    Выполнение задачи в некоторых
    случаях может быть возобновлено
    немедленно по завершении ожидания

    Ожидаемое событие произошло и
    задача может выполняться

    CIL и системное программирование в Microsoft .NET

    Рис. 6.6. Граф состояний задачи

    Завершение
    задачи

    Задача
    выбрана для
    выполнения

    190

    191

    dt=квант
    Перепланирование по исчерпанию кванта

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

    Рис. 6.7. Моменты перепланирования задач

    Задача 1

    Задача 2

    Задача 3

    Задача 4

    Задача 5

    Понижение приоритета
    Запуск новой задачи
    задачей 2
    Начало операции
    Вызов функции
    Повышение приоритета
    ввода-вывода
    явно вызывающей
    задачей 2
    задачей 3
    препланирование

    NT, 2000, XP и 2003). В таких приложениях операционная система не прерывает выполнение текущей задачи до вызова ею функций типа
    GetMessage или WaitMessage, во время которых Windows осуществляет при
    необходимости переключение на другую задачу.
    Вытесняющая многозадачность предполагает наличие некоторого
    арбитра, принадлежащего обычно операционной системе, который принимает решение о вытеснении текущей выполняемой задачи какой-либо
    другой, готовой к выполнению, асинхронно с работой текущей задачи.
    В качестве некоторого обобщения можно выделить понятие «момент
    перепланирования», когда активируется планировщик задач и принимает
    решение о том, какую именно задачу в следующий момент времени надо
    начать выполнять. Принципы, по которым назначаются моменты перепланирования, и критерии, по которым осуществляется выбор задачи, определяют способ реализации многозадачности и его сильные и слабые стороны.

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

    Большинство современных операционных систем используют комбинированные планировщики, одновременно применяющие квантование
    с переменной продолжительностью кванта и абсолютные или относительные приоритеты (см. рис. 6.7).
    Выбор задачи, которая начнет выполняться вследствие срабатывания
    планировщика, также определяется многими факторами. Среди важнейших – приоритет готовых к исполнению задач. Однако, помимо этого, часто принимают во внимание текущее и предыдущее состояния задачи. Такой
    подход позволяет реализовать достаточно сложный и тщательно сбалансированный планировщик задач. Очень часто применяют такой прием: назначенный задаче приоритет рассматривается в качестве некоторого «базового», а планировщик операционной системы может в определенных рамках
    корректировать реальный приоритет в зависимости от истории выполнения
    задачи. Типичными причинами коррекции приоритета являются:
    • запуск задачи (для возможно скорейшего начала исполнения);
    • досрочное освобождение процессора до исчерпания отведенного кванта (велик шанс, что задача и в этот раз так же быстро отдаст управление);
    • частый вызов операций ввода-вывода (при этом задача чаще находится в ожидании завершения операции, нежели занимает
    процессорное время);
    • продолжительное ожидание в очереди (приоритет ожидающей
    задачи часто постепенно начинают увеличивать);
    • и многие другие.
    Работа планировщика существенно усложняется в случае SMP машин, когда необходимо принимать во внимание привязку задач к процессорам (иногда задаче можно назначить конкретный процессор) и то, на
    каком процессоре задача выполнялась до того (это позволяет эффективнее использовать кэш-память процессора).
    Реализации Windows NT, 2000, XP, 2003+ предусматривают достаточно
    развитый и сложный планировщик, учитывающий множество факторов и
    корректирующий как назначение и длительность отводимых задаче квантов, так и приоритеты задач. При этом планировщик является настраиваемым, и его логика работы несколько отличается в зависимости от настроек
    системы (некоторые доступны через панель управления) и от назначения
    системы (работа планировщика различна у серверов и рабочих станций).
    Важно отметить, что Windows является гибкой системой разделения
    времени с вытесняющей многозадачностью и не может рассматриваться в
    качестве системы реального времени. Даже те процессы, которые с точки
    зрения Windows относятся к классу процессов так называемого «реального времени», на самом деле требованиям, предъявляемым к системам реального времени, не удовлетворяют. Такие процессы получат приоритет-

    192

    193

    6.1.1.3. Базовая терминология
    В операционных системах сложилось несколько подходов к реализации многозадачности, и, соответственно, принятая в разных операционных системах терминология несколько отличается. Так, обычно разделяют
    понятия задачи, процесса и потока. При этом понятие задачи является в
    большей степени историческим, либо очень специфичным. Это понятие
    сформировалось, когда единицей выделения процессорного времени была сама задача; планировщик же мог только переключать задачи. В современных системах большее распространение получил подход, в котором в
    рамках одной «задачи» может быть выделено несколько одновременно выполняемых ветвей кода, соответственно термин «задача» заместился терминами «процесс» и «поток».
    Процесс является объектом планирования адресного пространства и
    некоторых ресурсов, выделенных задаче. Но при этом процесс не является потребителем процессорного времени и не подлежит планированию.
    Поток является объектом планирования процессорного времени.
    Дополнительно к этому с потоком ассоциируют некоторые другие свойства, например, пользователя, – это позволяет потокам даже одного процесса действовать от имени разных пользователей (воплощение) и использовать механизмы ограничения доступа к ресурсам операционной системы.
    Например, сервер, предоставляющий доступ к каким-либо файлам, может
    создать поток, обслуживающий конкретного клиента, и воплотить его с
    правами доступа, назначенными этому клиенту. Однако в данный момент
    для нас важно, что управление процессорным временем осуществляется
    применительно к потокам, а управление адресным пространством – применительно к процессам; каждый процесс содержит, как минимум, один
    поток.

    ное распределение процессорного времени и будут обрабатываться планировщиком с учетом их «особого статуса»; однако при этом нельзя гарантировать строгого выполнения временных ограничений. Более того, в силу
    используемых механизмов управления памятью, нельзя точно предсказать
    время, необходимое для выполнения той или иной операции. В любой момент времени при самом невинном обращении к какой-либо переменной
    или функции может потребоваться обработка ошибок доступа, подкачка
    выгруженных страниц, освобождение памяти и т.д. – то есть действия,
    время завершения которых предсказать крайне трудно. Фактически можно давать лишь вероятностные прогнозы по времени выполнения той или
    иной операции. До определенных рамок Windows можно применять в мягких системах реального времени – с достаточно свободными ограничениями, но даже незначительная вероятность превышения временных ограничений иногда просто недопустима.

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

    В современных полновесных реализациях Windows (Windows 2000,
    Windows XP, Windows 2003) планировщик ядра выделяет процессорное
    время потокам. Управление волокнами возложено на приложения пользователя: Windows предоставляет набор функций, с помощью которых приложение может управлять созданными волокнами. Фактически для волокон реализуется невытесняющая многозадачность средствами приложения; с точки зрения операционной системы, все волокна должны быть созданы в рамках потоков (один поток может быть «расщеплен» на множество волокон средствами приложения) и система никак не вмешивается в
    их планирование.

    6.1.2. Реализация в Windows

    Принято также деление потоков на потоки ядра и потоки пользователя (эти термины тоже неоднозначны). Потоки ядра в данном контексте являются потоками, для управления которыми предназначен планировщик,
    принадлежащий ядру операционной системы. Потоки пользователя при
    этом рассматриваются как потоки, которые управляются планировщиком
    пользовательского процесса. Строго говоря, потоки пользователя являлись переходным этапом между «задачами» и «процессами»: с точки зрения операционной системы использовались «задачи», которым выделялись и ресурсы, и процессорное время, тогда как разделение «задачи» на
    «потоки» осуществлялось непосредственно в самом приложении.
    В Windows для обозначения этих понятий использованы термины
    process (процесс), thread (поток) и fiber (волокно). Достаточно часто термин
    «thread» переводится на русский язык как «нить», а не «поток». Термин
    «fiber» также может переводиться либо как «нить», либо как «волокно».
    Поток соответствует потоку ядра и планируется ядром операционной системы, а волокно соответствует потоку пользователя и планируется в приложении пользователя.
    В операционной системе для описания потоков используются объекты
    двух типов – так называемые дескрипторы и контекст. Дескрипторы содержат информацию, описывающую поток, но не его текущее состояние исполнения. Контекст потока содержит информацию, описывающую непосредственно состояние исполнения потока. Так, например, дескриптор должен содержать переменные окружения, права доступа, назначенные потоку,
    приоритет, величину кванта и так далее, тогда как контекст должен сохранять информацию о состоянии стека, регистров процессора и т.д. Дескрипторы содержат актуальную в каждый момент информацию, а контекст обновляется в тот момент, когда поток выходит из исполняемого состояния.
    Из контекста восстанавливается состояние потока при возобновлении исполнения. Пока поток выполняется, содержимое контекста не является актуальным и не соответствует его реальному состоянию.

    194

    195

    Выполнение

    Выбран для
    выполнения

    Готовность

    Рис. 6.8. Граф состояний потока

    Завершение
    потока

    Переключение
    контекста на
    выбранный
    поток

    Выбор для
    выполнения

    Запуск
    потока

    Ожидание

    Переходное
    состояние

    Стек потока
    загружен

    Ожидаемое событие
    произошло и поток
    может
    выполняться,
    Поток переходит
    но
    его
    стек был
    в режим
    выгружен
    ожидания

    Ожидаемое
    событие
    произошло и
    поток может
    выполняться

    Поток
    вытеснен

    В Windows определен список событий, которые приводят к перепланированию потоков:
    • создание и завершение потока;
    • выделенный потоку квант исчерпан;
    • поток вышел из состояния ожидания;
    • поток перешел в состояние ожидания;
    • изменен приоритет потока;
    • изменена привязка к процессору.
    В целях уменьшения затрат на планирование потоков несколько изменен граф состояний потока. На рис. 6.6 приведен «классический» вид
    графа состояний задачи, а на рис. 6.8 – граф состояний потока в Windows.
    Переход из состояния «готовность» в состояние «выполнение» сделан в
    два этапа – выбранный к выполнению поток подготавливается к выполнению и переводится в состояние «выбран»; эта подготовка может осуществляться до наступления момента перепланирования, и в нужный момент достаточно просто переключить контекст выполняющегося потока
    на выбранный.
    Также в два этапа может происходить переход из состояния «ожидание» в «готовность»: если ожидание было долгим, то стек потока может
    быть выгружен из оперативной памяти. В этом случае поток переводится
    в промежуточное состояние до завершения загрузки стека – в списке готовых к выполнению потоков находятся только те, которые можно начать
    выполнять без лишнего ожидания.

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

    6.1.2.1. Управление квантованием
    Квантование потоков осуществляется по тикам системного таймера,
    продолжительность одного тика составляет обычно 10 или 15 мс, больший
    по продолжительности тик назначают многопроцессорным машинам. Каждый тик системного таймера соответствует 3 условным единицам; величина кванта может варьироваться от 2 до 12 тиков (от 6 до 36 единиц).
    Параметр реестра
    HKLM\SYSTEM\CurrentControlSet\Control\PriorityControl\
    Win32PrioritySeparation предназначен для управления квантованием. На

    При выборе потока для выполнения учитываются приоритеты потоков (абсолютные приоритеты) – система начинает выполнять код потока
    с наибольшим приоритетом из числа готовых к исполнению.
    Процесс выбора потока для выполнения усложняется в случае SMP
    систем, когда помимо приоритета готового к исполнению потока учитывается, на каком процессоре ранее выполнялся код данного потока.
    В Windows выделяют понятие «идеального» процессора – им назначается процессор, на котором запускается приложение в первый раз. В
    дальнейшем система старается выполнять код потока именно на этом процессоре – для SMP систем это решение улучшает использование кэш-памяти, а для NUMA систем позволяет, по большей части, ограничиться использованием оперативной памяти, локальной для данного процессора.
    Заметим, что диспетчер памяти Windows при выделении памяти для запускаемого процесса старается учитывать доступность памяти для назначенного процессора в случае NUMA системы.
    В многопроцессорной системе используется либо первый простаивающий процессор, либо, при необходимости вытеснения уже работающего
    потока, проверяются идеальный процессор, последний использовавшийся и процессор с наибольшим номером. Если на одном из них работает поток с меньшим приоритетом, то последний вытесняется и заменяется новым потоком; в противном случае выполнение потока откладывается (даже если в системе есть процессоры, занятые потоками с меньшим приоритетом).
    Современные реализации Windows в рамках единого дерева кодов
    могут быть использованы для различных классов задач – от рабочих станций, обслуживающих преимущественно интерфейс пользователя, до серверных установок на многопроцессорных машинах. Чтобы можно было
    эффективно использовать одну ОС в столь разных классах систем, планировщик Windows динамически изменяет длительность квантов и приоритеты, назначаемые потокам. Администратор системы может в некоторой
    степени изменить поведение системы при назначении длительности квантов и приоритетов потоков.

    196

    197

    5

    3

    2

    1

    0

    1 := длинный квант
    2 := короткий квант
    0 или 3 := значения по умолчанию
    (1 для рабочих станций и 2 для серверов)

    1 := переменная длительность кванта
    2 := фиксированная длительность кванта
    0 или 3 := значения по умолчанию
    (1 для рабочих станций и 2 для серверов)

    Динамическое приращение длительности кванта
    допустимые значения 0, 1 и 2

    4

    24
    36

    36
    36

    12
    36

    18
    18

    6
    18

    12
    18

    Длинный квант
    0
    1
    2

    Короткий квант
    0
    1
    2

    Этот параметр может быть изменен с помощью панели управления,
    однако, лишь в очень ограниченных рамках:
    «System Properties|Advanced|Performance:Settings|Advanced|Adjust for
    best performance of:» позволяет выбрать только:
    «applications»
    короткие кванты переменной длины, значение 0x26 т.е. 10 01 10 (короткие кванты переменной длительности, 18 ед.).

    Значение младших 2-х бит
    параметра Win32PrioritySeparation
    Переменная длительность
    Фиксированная длительность

    Таблица 6.1. Длительность кванта

    Рис. 6.9. Управление квантованием в Windows (длительность кванта
    показана в табл. 6.1)

    ...

    HKLM\SYSTEM\CurrentControlSet\Control\PriorityControl\Win32PrioritySeparation

    рис. 6.9 дан формат этого параметра для Windows 2000-2003, а в таблице
    6.1 приводятся длительности квантов в условных единицах для разных
    значений полей параметра Win32PrioritySeparation.

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

    6.1.2.2. Управление приоритетами
    В Windows выделяется 32 уровня приоритетов. 0 соответствует самому низкому приоритету (с таким приоритетом работает только специальный поток обнуления страниц), 31 – самому высокому. Этот диапазон делится на три части:
    • Приоритет 0 – соответствует приоритету потока обнуления
    страниц.
    • Приоритеты с 1 по 15 – соответствуют динамическим уровням
    приоритетов. Большинство потоков работают именно в этом
    диапазоне приоритетов, и Windows может корректировать в
    некоторых случаях приоритеты потоков из этого диапазона.
    • Приоритеты с 16 по 31 – соответствуют приоритетам «реального времени». Этот уровень достаточно высок для того, чтобы поток, работающий с таким приоритетом, мог реально помешать
    нормальной работе других потоков в системе – например, помешать обрабатывать сообщения от клавиатуры и мыши. Windows
    самостоятельно не корректирует приоритеты этого диапазона.
    Для некоторого упрощения управления приоритетами в Windows выделяют «классы приоритета» (priority class), которые задают базовый уровень приоритета, и «относительные приоритеты» потоков, которые корректируют указанный базовый уровень. Операционная система предостав-

    «background services»
    длинные кванты фиксированной длины, значение 0x18 т.е.
    01 10 00 (длинные кванты фиксированной длительности, 36 ед.).
    Более тонкая настройка возможна с помощью редактора реестра.
    Управление длительностью кванта связано с активностью процесса,
    которая определяется наличием интерфейса пользователя (GUI или консоль) и его активностью. Если процесс находится в фоновом режиме, то
    длительность назначенного ему кванта соответствует «нулевым» колонкам
    таблицы 6.1 (выделены серым цветом; т.е. длительности 6 или 24 – для переменной длины кванта или 18 и 36 – для фиксированной). Когда процесс
    становится активным, то ему назначается продолжительность квантов, исходя из значения двух младших бит параметра Win32PrioritySeparation в
    соответствии с приведенной таблицей.
    Еще один случай увеличения длительности кванта – процесс долгое
    время не получал процессорного времени (это может случиться, если все
    время есть активные процессы более высокого приоритета). В этой ситуации система раз в 3-4 секунды (в зависимости от продолжительности тика) назначает процессу повышенный приоритет и квант удвоенной длительности. По истечении этого кванта приоритет возвращается к прежнему значению и восстанавливается рекомендуемая длительность кванта.

    198

    199

    ляет набор функций для управления классами и относительными приоритетами потоков.
    Планировщик операционной системы также может корректировать
    уровень приоритета (из диапазона 1-15), однако базовый уровень
    (т.е. класс) не может быть изменен. Такая коррекция приоритета выполняется в случае:
    • Завершения операции ввода-вывода – в зависимости от устройства, приоритет повышается на 1 – 8 уровней.
    • По окончании ожидания события или семафора (см. далее) – на
    один уровень.
    • При пробуждении GUI потоков – на 2 уровня.
    • По окончании ожидания потоком активного процесса (определяется по активности интерфейса) – на величину, указанную
    младшими 2 битами параметра Win32PrioritySeparation (см. управление длительностью кванта).
    В случае коррекции приоритета по одной из перечисленных причин,
    повышенный приоритет начинает постепенно снижаться до начального
    уровня потока – с каждым тиком таймера на один уровень.
    Еще один случай повышения приоритета (вместе с увеличением длительности кванта) – процесс долгое время не получал процессорного времени. В этой ситуации система раз в 3-4 секунды назначает процессу приоритет, равный 15, и квант удвоенной длительности. По истечении этого
    кванта приоритет возвращается к прежнему значению и восстанавливается рекомендуемая длительность кванта.
    Разработчик приложения может изменять класс приоритета, назначенный процессу, и относительный приоритет потоков в процессе в соответствии с приведенной таблицей 6.2. Планировщик при выборе потока
    для исполнения учитывает итоговый уровень приоритета.
    В зависимости от настройки планировщика, NORMAL_PRIORITY_CLASS с
    базовым уровнем приоритета 8 может быть «расщеплен» на два базовых
    уровня – для потоков активных процессов (базовый уровень 9) и для потоков фоновых процессов (базовый уровень 7). Для класса HIGH_PRIORITY_CLASS относительные приоритеты потока THREAD_PRIORITY_HIGHEST и
    THREAD_PRIORITY_TIME_CRITICAL дают одинаковое значение приоритета 15.

    Основы многозадачности

    Таблица 6.2. Соответствие классов приоритета и относительных приоритетов потоков уровням

    IDLE
    4

    BELOW_NORMAL

    200

    6

    Background
    7

    NORMAL
    Normal
    8

    Foreground
    9

    ABOVE_NORMAL
    10

    HIGH
    13

    CIL и системное программирование в Microsoft .NET

    TIME_CRITICAL

    TIME_CRITICAL

    TIME_CRITICAL

    TIME_CRITICAL

    TIME_CRITICAL

    TIME_CRITICAL

    TIME_CRITICAL
    ABOVE_NORMAL
    NORMAL (13)
    BELOW_NORMAL
    LOWEST

    HIGHEST
    ABOVE_NORMAL
    NORMAL (10)
    BELOW_NORMAL
    LOWEST

    HIGHEST
    ABOVE_NORMAL
    NORMAL (9)
    BELOW_NORMAL
    LOWEST

    HIGHEST
    ABOVE_NORMAL
    NORMAL (8)
    BELOW_NORMAL
    LOWEST

    HIGHEST
    ABOVE_NORMAL
    NORMAL (7)
    BELOW_NORMAL
    LOWEST

    HIGHEST
    ABOVE_NORMAL
    NORMAL (6)
    BELOW_NORMAL
    LOWEST

    HIGHEST
    ABOVE_NORMAL
    NORMAL (4)
    BELOW_NORMAL
    LOWEST
    IDLE
    IDLE
    IDLE
    IDLE
    IDLE
    приоритет потока обнуления страниц
    IDLE
    IDLE

    15
    14
    13
    12
    11
    10
    9
    8
    7
    6
    5
    4
    3
    2
    1
    0

    Не рекомендуется создавать потоки с таким классом приоритета, так как это может повлиять на обработку
    сообщений от клавиатуры, мыши, сохранение на диске данных кэша и т.д.

    Уровни приоритета с 16 по 31 отведены только для класса REALTIME_PRIORITY_CLASS

    TIME_CRITICAL
    6
    5
    4
    3
    HIGHEST
    ABOVE_NORMAL
    NORMAL (24)
    BELOW_NORMAL
    LOWEST
    -3
    -4
    -5
    -6
    -7
    IDLE

    31
    30
    29
    28
    27
    26
    25
    24
    23
    22
    21
    20
    19
    18
    17
    16

    REALTIME
    24

    Класс приоритета (задает базовый уровень приоритета)

    Уровень

    201

    Как уже отмечалось, современные вычислительные системы содержат один или несколько центральных процессоров и, как правило,
    несколько специализированных устройств, способных к параллельной работе с центральными процессорами. Операционная система, функционирующая на таком компьютере, должна предоставлять и средства мультипрограммирования, и средства мультипроцессирования, в том числе мультипроцессирования на функционально различных устройствах.
    Используемые в операционной системе средства реализации многозадачности можно разделить на несколько групп:
    • Взаимодействие с устройствами.
    Существуют как специализированные средства взаимодействия, специфичные для конкретного вида устройств (например,
    для графических устройств), так и относительно универсальные
    средства, применимые к устройствам разных типов. Наиболее
    типичным примером являются средства асинхронного вводавывода.
    • Средства управления потоками и волокнами.
    Когда процесс запускается, операционная система в нем самостоятельно создает первичный поток, начинающий исполнение
    кода этого процесса. Создание всех остальных потоков процесса требует специальных действий. Операционная система должна предоставлять средства для создания потоков, волокон, их
    завершения, приостановки или возобновления, изменения их
    характеристик (например, приоритетов, прав доступа и т.д.).
    В Windows существует очень интересный гибрид средств управления потоками и асинхронного ввода-вывода под названием
    «порт завершения ввода-вывода». Несмотря на такое название,
    это, по большей части, именно специализированный механизм
    управления потоками.
    • Взаимодействие потоков в рамках одного процесса.
    Потоки, работающие в рамках одного процесса, имеют возможность взаимодействовать друг с другом, используя общее адресное пространство процесса. Это взаимодействие может, с одной
    стороны, приводить к конфликтам одновременного доступа
    (нужны средства разрешения конфликтов) и, с другой стороны,
    требовать средств изоляции некоторых данных одного потока от
    данных другого (нужны механизмы организации памяти, локальной для потока).

    6.2. Общие подходы к реализации приложений
    с параллельным выполнением операций

    Основы многозадачности

    • Взаимодействие между процессами одного компьютера.
    Чуть более сложный случай – необходимость взаимодействия
    различных процессов в рамках одной вычислительной системы.
    Особенность этого случая связана с тем, что разные процессы
    работают в изолированных друг от друга адресных пространствах, однако при этом существует возможность осуществить обмен данными через общую, разделяемую несколькими процессами, память. Диспетчер памяти операционной системы должен
    предусмотреть средства организации разделяемой между процессами памяти. Кроме того, многие операционные системы
    предоставляют дополнительные средства межпроцессного взаимодействия; многие из них являются надстройками над средствами работы с разделяемой памятью.
    В данной книге рассматриваются базовые средства для организации разделяемой памяти, так как большая часть остальных
    средств либо является надстройкой над ними (например, обмен
    оконными сообщениями), либо использует обмен данными через
    файловые объекты (к примеру, почтовые ящики, каналы и пр.).
    • Взаимодействие между процессами разных компьютеров.
    В общем случае память разных компьютеров можно рассматривать как изолированную друг от друга. В этом случае для взаимодействия разных процессов потребуется пересылка сообщений,
    содержащих данные и некоторую управляющую информацию,
    между разными компьютерами (узлами сети). Операционная
    система должна предоставить некоторый базовый набор функций по пересылке данных по коммуникационной сети и, зачастую, богатый набор различных средств, работающих «поверх»
    этого базового уровня.
    В данной книге межузловое взаимодействие не рассматривается.

    CIL и системное программирование в Microsoft .NET

    #include
    int sequential_io( char *filename, char *buffer, int size )
    {
    FILE *fp;
    int done;

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

    6.2.1. Асинхронный ввод-вывод

    202

    203

    }
    Такой подход прост в реализации, как с точки зрения операционной
    системы, так и с точки зрения пользователя («пользователем» операционной системы в данном случае выступает разработчик). Однако во время
    выполнения операций ввода-вывода сама программа не выполняется –
    она ожидает завершения ввода-вывода. Во многих случаях такие операции, во-первых, занимают достаточно много времени и, во-вторых, выполняются специализированным оборудованием без участия центрального процессора, который в это время находится в состоянии ожидания и
    не выполняет никакой полезной работы (по крайней мере, с точки зрения
    данной программы, так как в общем случае начало ожидания приводит к
    перепланировке потоков).
    Эффективность использования процессора можно было бы повысить, если бы существовала возможность выполнять код программы во
    время выполнения операций ввода-вывода. Конечно, это не всегда возможно или целесообразно. Например, если для продолжения работы программы необходимы данные, которые еще не получены, то нам все равно
    надо ожидать завершения ввода-вывода. Более того, структура приложения должна быть разработана с учетом специфики использования асинхронных операций ввода-вывода.
    В Windows для реализации асинхронного ввода-вывода предусмотрены функции типа ReadFile, WriteFile, ReadFileEx, WriteFileEx и др. и специальная структура OVERLAPPED, которая используется для взаимодействия с
    асинхронной операцией. Асинхронные операции применяются следующим образом: перед началом операции заполняется структура
    OVERLAPPED и вызывается нужная функция для выполнения ввода-вывода,
    которая ставит операцию в очередь и немедленно возвращает управление
    вызвавшей программе. После этого программа продолжает свою работу независимо от хода выполнения операции ввода-вывода. При необходимости
    можно выяснить состояние асинхронной операции, дождаться ее завершения или отменить ее, не дожидаясь завершения. Для этого предназначен
    специальный набор функций, например, CancelIo, GetOverlappedResult,

    fp = fopen( filename, “rb” );
    if ( fp ) {
    done = fread( fp, 1, size, buffer );
    /* код не будет выполняться, пока чтение не завершится*/
    fclose( fp );
    } else {
    done = 0;
    }
    buffer[done] = '\0';
    return done;

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

    ov.Offset = 12345;
    if (
    WriteFile( fh, buffer, sizeof(buffer), &dwWritten, &ov ) ||
    GetLastError() == ERROR_IO_PENDING
    ) {
    /* пока операция ввода-вывода выполняется,
    выполняем некоторые операции.
    дожидаемся завершения операции ввода-вывода */
    while (!GetOverlappedResult(fh, &ov, &dwWritten, FALSE)){}
    } else {
    /* возникла ошибка */
    }
    Функция GetOverlappedResult в данном случае проверяет состояние операции ввода-вывода и возвращает признак ее завершения.
    Опрос состояния операции в цикле позволяет наиболее быстро

    1. Выполнение асинхронных операций с опросом состояния; этот
    способ может обеспечить самую быструю реакцию на завершение операции ввода-вывода, но ценой более высокой загрузки
    процессора:

    ZeroMemory( &ov, sizeof(OVERLAPPED) );
    FillMemory( buffer, sizeof(buffer), 123 );

    HANDLE fh = CreateFile(
    “file.dat”, FILE_READ_DATA|FILE_WRITE_DATA,
    FILE_SHARE_READ, (LPSECURITY_ATTRIBUTES)NULL, OPEN_ALWAYS,
    FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED, NULL
    );
    if ( fh == INVALID_HANDLE_VALUE ) {
    /* возникла ошибка */
    }

    OVERLAPPED ov;
    DWORD
    dwWritten;
    BYTE
    buffer[ 5000000 ];

    HasOverlappedIoCompleted и некоторые другие. Существует несколько вариантов использования асинхронного ввода-вывода; рассмотрим их на
    небольшом примере.
    Для начала надо описать необходимые переменные и открыть файл с
    разрешением асинхронных операций (FILE_FLAG_OVERLAPPED):

    204

    205

    if (
    WriteFile( fh, buffer, sizeof(buffer), &dwWritten, &ov ) ||
    GetLastError() == ERROR_IO_PENDING
    ) {
    /* пока операция ввода-вывода выполняется, выполняем некоторые
    операции...
    дожидаемся завершения операции ввода-вывода */
    GetOverlappedResult( fh, &ov, &dwWritten, TRUE );
    } else {
    /* возникла ошибка */
    }
    Функция GetOverlappedResult в данном случае проверяет состояние операции и, если она еще не завершена, вызывает функцию WaitForSingleObject для ожидания завершения операции
    ввода-вывода. Объект «событие» можно не создавать – в этом
    случае функция будет ожидать освобождения объекта «файл»;
    однако в случае нескольких, накладывающихся друг на друга,
    асинхронных операций будет непонятно, какая именно операция завершилась, и использование специфичных для каждой
    операции событий снимает эту проблему.
    Ожидание на объектах ядра является наиболее экономным, но
    реакция приложения на завершение ввода-вывода связана с работой планировщика, поэтому для достижения малых задержек
    иногда надо дополнительно повышать приоритеты потоков, переходящих в режим ожидания завершения ввода-вывода.
    3. Выполнение асинхронных операций с использованием функций завершения операций ввода-вывода. Эта функция будет автоматически вызвана после завершения ввода-вывода и может

    ov.Offset = 12345;
    ov.hEvent = CreateEvent((LPSECURITY_ATTRIBUTES)NULL, TRUE, FALSE, 0);

    отреагировать на завершение операции (особенно на многопроцессорных машинах), однако ценой увеличения загрузки процессора, что может снизить общую производительность системы.
    Функция WriteFile возвращает значение TRUE, если операция записи завершена синхронно, а в случае ошибки или начатой
    асинхронной операции она возвращает FALSE, поэтому требуется анализ кода возникшей «ошибки», которая может и не являться ошибкой.
    2. Выполнение асинхронных операций с ожиданием на объектах
    ядра:

    Основы многозадачности

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

    CIL и системное программирование в Microsoft .NET

    ov.Offset = 12345;
    if ( WriteFileEx( fh, buffer, sizeof(buffer), &ov, io_done ) ) {
    /* пока операция ввода-вывода выполняется, выполняем некоторые
    операции и переходим в режим ожидания оповещения,
    например, так: */
    if ( SleepEx( INFINITE, TRUE ) != WAIT_IO_COMPLETION ) {
    /* ввод-вывод пока не завершен, возможно, ошибка */
    }
    } else {
    /* возникла ошибка */
    }
    /* нам еще надо предоставить собственную процедуру
    завершения ввода-вывода. В простейшем варианте
    она может ничего не делать: */
    VOID CALLBACK io_done(
    DWORD dwErr, DWORD dwWritten, LPOVERLAPPED lpOv
    ) {
    ...
    }
    Этот подход наиболее трудоемок и наименее распространен; на
    практике самым эффективным является механизм выполнения
    асинхронных операций с ожиданием на объектах ядра. Однако
    механизм вызова функций завершения (расширенный возможностью автоматического выбора потока, осуществляющего обработку функции завершения) послужил основой для реализации одного из очень эффективных механизмов взаимодействия
    потоков – порта завершения ввода-вывода.
    При использовании асинхронного ввода-вывода необходимо очень
    внимательно следить за выделением и освобождением ресурсов – особенно памяти, занятой структурами OVERLAPPED, и буферами, участвующими в
    операциях ввода-вывода.
    Асинхронный ввод-вывод является примером мультипроцессирования с использованием функционально различных устройств.

    206

    207

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

    6.2.3. Процессы, потоки и объекты ядра

    VOID CALLBACK ApcProc( ULONG_PTR dwData )
    {
    /* ... */
    }
    int main( void )
    {
    QueueUserAPC( ApcProc, GetCurrentThread(), 0 );
    /* ... */
    SleepEx( 1000, TRUE );
    return 0;
    }
    Обычно APC используются самой системой для реализации асинхронного ввода-вывода и некоторых других операций, но разработчикам
    также предоставлена функция QueueUserAPC, с помощью которой можно
    ставить в APC очередь запросы для вызова собственных функций.

    Для реализации асинхронного ввода-вывода в операционной системе предусмотрен специальный механизм, основанный на так называемых
    асинхронных вызовах процедур (Аsynchronous Procedure Call, APC). Это один
    из базовых механизмов, необходимый для нормального функционирования операционной системы.
    Практика показала, что такой механизм был бы эффективен и для реализации самих приложений. Более того, для реализации асинхронного
    ввода-вывода с поддержкой функции завершения система уже обязана
    была предоставить этот механизм. Для реализации этого механизма операционная система ведет списки процедур, которые она должна вызывать
    в контексте данного потока, с тем ограничением, что прерывать работу занятого потока в произвольный момент времени система не должна. Поэтому для обслуживания накопившихся в очереди процедур необходимо
    перевести поток в специальное состояние ожидания оповещения (alertable
    waiting) – для этого Win32 API предусматривает специальный набор функций: например, SleepEx, WaitForSingleObjectEx и др.
    Здесь и во многих примерах далее, чтобы не загромождать код примера, опущена проверка ошибок:

    6.2.2. Асинхронные вызовы процедур

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

    6.2.3.1. Объекты ядра
    Объекты ядра представлены в качестве некоторых структур и типов
    данных, управляемых ядром операционной системы и размещенных в памяти, принадлежащей ядру. Пользовательские процессы, как правило,
    не имеют возможности доступа к этим объектам напрямую. Для того чтобы
    пользовательский процесс мог оперировать такими объектами, введено понятие описатель (handle) объекта ядра. Так, например, объектами ядра являются файлы, процессы, потоки, события, почтовые ящики и многое другое.
    Все созданные описатели объектов ядра должны удаляться с помощью функции
    BOOL CloseHandle( HANDLE hKernelObject ),
    которая уменьшает счетчик использования объекта и уничтожает его,
    когда на объект никто больше не ссылается.
    Доступ к защищаемым объектам в Windows задается так называемыми дескрипторами безопасности (Security Descriptor). Дескриптор содержит
    информацию о владельце объекта и первичной группе пользователей и два
    списка управления доступом (ACL, Access Control List): один список задает
    разрешения доступа, другой – необходимость аудита при доступе к объекту. Список содержит записи, указывающие права выполнения действий, и
    запреты, назначенные конкретным пользователям и группам. При доступе к защищаемым объектам для начала проверяются запреты – если для
    данного пользователя и группы имеется запрет доступа, то дальнейшая
    проверка не выполняется и попытка доступа отклоняется. Если запретов
    нет, то проверяются права доступа – при отсутствии разрешений доступ
    отклоняется. Запрет обладает более высоким «приоритетом», чем наличие
    разрешений – это позволяет разрешить доступ, к примеру, целой группе
    пользователей и выборочно запретить некоторым ее членам.
    Объект, осуществляющий доступ (выполняющийся поток), обладает так называемым маркером доступа (access token). Маркер идентифицирует пользователя, от имени которого предпринимается попытка доступа, а также его привилегии и умолчания (например, стандартный ACL
    объектов, создаваемых этим пользователем). В Windows маркерами доступа обладают как потоки, так и процессы. С процессом связан так называемый первичный маркер доступа, который используется при созда-

    ядро операционной системы вынуждено проверять полномочия каждого
    процесса при их попытках доступа к защищаемым объектам.
    Для того чтобы ядро операционной системы могло контролировать
    доступ к тем или иным объектам, сами объекты должны управляться
    ядром системы. Это приводит к понятию объектов ядра (kernel objects), которые создаются по запросу процессов ядром системы, управляются
    ядром, и доступ к которым также контролируется ядром системы.

    208

    209

    нии потоков, а вот в дальнейшем поток может работать от имени какоголибо иного пользователя, используя собственный маркер воплощения
    (impersonation).
    Процессы и потоки в Windows являются с одной стороны «представителями» пользователя, выступающими от его имени, а с другой стороны –
    защищаемыми объектами, при доступе к которым выполняется проверка
    прав, то есть они обладают одновременно и маркерами доступа, и дескрипторами безопасности.
    Описатели объектов ядра в Windows позволяют разным процессам и
    потокам взаимодействовать с объектами с учетом их контекстов безопасности, располагаемых прав и требуемого режима доступа. Примерами защищаемых объектов являются процессы, потоки, файлы, большинство
    синхронизирующих примитивов (события, семафоры и т.д.), проекции
    файлов в память и многое другое.
    Для создания большинства объектов ядра используются функции,
    начинающиеся на слово «Create» и возвращающие описатель созданного
    объекта, например функции CreateFile, CreateProcess, CreateEvent и т.д.
    Многие объекты при их создании могут получить собственное имя или остаться неименованными.
    Любой процесс или поток может ссылаться на объекты ядра, созданные другим процессом или потоком. Для этого предусмотрено три механизма:
    • Объекты могут быть унаследованы дочерним процессом при его
    создании. В этом случае объекты ядра должны быть «наследуемыми», и родительский процесс должен принять меры к тому, чтобы
    потомок мог узнать их описатели. Возможность передавать описатель потомкам по наследованию явно указывается в большинстве функций, так или иначе создающих объекты ядра (обычно
    такие функции содержат аргумент «BOOL bInheritHandle», который указывает возможность наследования).
    • Объект может иметь собственное уникальное имя – тогда можно получить описатель этого объекта по его имени. Для разных
    типов объектов Win32 API предоставляет набор функций, начинающийся на Open...например, OpenMutex, OpenEvent и т.д.
    • Процесс-владелец объекта может передать его описатель любому другому процессу. Для этого процесс-владелец объекта должен получить специальный описатель объекта для «экспорта» в
    указанный процесс. В Win32 API для этого предназначена функция DuplicateHandle, создающая для объекта, заданного описателем в контексте данного процесса, новый описатель, корректный в контексте нового процесса:

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

    6.2.3.2. Описатели процесса и потока
    Для взаимодействия потоков и процессов между собой необходимы
    средства, обеспечивающие идентификацию соответствующих объектов.
    В Windows для идентификации процессов и потоков используют их описатели (HANDLE) и идентификаторы (DWORD). Описатели идентифицируют в
    данном случае объект ядра, представляющий процесс или поток, при доступе к которому, как ко всякому объекту ядра, учитывается контекст защиты, проверяются права доступа и т.д. Идентификаторы процесса и потока,
    назначаемые при их создании, исполняют роль уникальных имен.
    Описатели и идентификаторы процессов и потоков можно получить
    при создании соответствующих объектов. Кроме того, можно узнать идентификаторы текущего процесса и потока (GetCurrentThreadId,
    GetCurrentProcessId), или по описателю узнать соответствующий идентификатор (GetProcessId и GetThreadId). Функции OpenProcess и OpenThread
    позволяют получить описатели этих объектов по их идентификатору.
    Функции GetCurrentProcess и GetCurrentThread возвращают описатели текущего процесса и потока, однако возвращаемое ими значение не является настоящим описателем, а представлено некоторой константой, получившей название «псевдоописатель». Эта константа, использованная
    вместо описателя потока или процесса, рассматривается как описатель
    процесса/потока, сделавшего вызов системной функции. Псевдоописателями можно свободно пользоваться в рамках процесса (потока), в котором
    они получены, а при попытке передать их другому процессу или потоку
    они будут рассматриваться как описатели того процесса (потока), в контексте которого используются.
    В тех случаях, когда необходимо дать другому процессу или потоку
    доступ к данным описателям, нужно с помощью DuplicateHandle сделать с
    них «копии», которые будут являться настоящими описателями в контексте процесса-получателя. Так, например, с помощью этой функции мож-

    BOOL DuplicateHandle(
    HANDLE hFromProcess, HANDLE hSourceHandle,
    HANDLE hToProcess, LPHANDLE lpResultHandle,
    DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwOptions
    );
    Существенно проследить, чтобы все создаваемые описатели закрывались вызовом функции CloseHandle, включая описатели, созданные функцией DuplicateHandle. Хорошая практика при разработке приложений –
    проводить мониторинг выделяемых описателей и количества объектов в
    процессе (например, с помощью таких стандартных средств как менеджер
    задач или оснастка «производительность» панели управления).

    210

    211

    У процессов и потоков есть интересная особенность – объекты ядра,
    представляющие процесс и поток, сразу после создания имеют счетчик
    использования не менее двух: во-первых, это описатель, возвращенный
    функцией, и, во-вторых, объект используется работающим потоком.
    В итоге завершение потока и завершение последнего потока в процессе
    не приводят к удалению соответствующих объектов – они будут сохраняться все время, пока существуют их описатели. Это сделано для того,
    чтобы уже после завершения работы потока или процесса можно было получить от него какую-либо информацию, чаще всего – код завершения
    (функции GetExitCodeThread и GetExitCodeProcess);
    Объекты ядра «процесс» и «поток» поддерживают также интерфейс
    синхронизируемых объектов, так что их можно использовать для синхронизации работы: поток считается занятым до завершения, а процесс занят
    до тех пор, пока в нем есть хоть один работающий поток.
    Если ни синхронизация с этими объектами, ни получение кодов завершения не требуются разработчику, надо сразу после создания соответствующего объекта закрывать его описатель.
    В Windows для задания приоритета работающего потока используют
    понятия классов приоритетов и относительных приоритетов в классе. При
    этом класс приоритета связывается с процессом, а относительный приоритет – с потоком, исполняющимся в данном процессе.
    Соответственно Win32 API предоставляет функции для изменения
    класса приоритета для процесса (GetPriorityClass, SetPriorityClass) и
    для изменения относительного приоритета потока (GetThreadPriority и
    SetThreadPriority).
    Планировщик операционной системы может динамически корректировать приоритет потока, кратковременно повышая уровень. Разработчикам предоставлена возможность отказаться от этой возможности или,
    наоборот, задействовать ее (функции GetProcessPriorityBoost,
    SetProcessPriorityBoost, GetThreadPriorityBoost и
    SetThreadPriorityBoost).

    HANDLE hrealThread;
    DuplicateHandle(
    GetCurrentProcess(), GetCurrentProcess(),
    GetCurrentProcess(), &hrealThread,
    DUPLICATE_SAME_ACCESS, FALSE, 0
    );

    но превратить псевдоописатель процесса в настоящий описатель, действующий только в текущем процессе:

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

    6.2.4.1. Потоко-безопасные и небезопасные функции
    При реализации многопоточного приложения следует учитывать возможные побочные эффекты. Наличие таких эффектов обусловлено реализацией библиотеки времени исполнения: она содержит много функций
    (в том числе внутренних, обеспечивающих семантику языка программирования), являющихся потоко-небезопасными. Примеры таких функций –
    стандартная процедура strtok, операторы new и delete или функции
    malloc, calloc, free и так далее. Фактически любая стандартная функция,
    оперирующая статическими переменными, объектами или данными, может являться потоко-небезопасной в силу того, что два потока могут получить одновременный конкурирующий доступ к этим данным и в итоге разрушить их. Существует несколько подходов к решению этой проблемы:
    • Можно предоставить потоко-безопасные аналоги (например,
    strtok_r, являющийся в Linux потоко-безопасным аналогом
    функции strtok).
    • Можно переписать код всех потоко-небезопасных функций так,
    чтобы они вместо глобальных объектов использовали локальную для потока память или синхронизировали доступ к общим
    данным.
    В Windows принят второй подход, однако, с некоторой оговоркой.
    Потоко-безопасные версии функций более ресурсо- и время- емкие, чем
    обычные. В итоге используется два вида библиотек: один для однопоточных приложений, другой для многопоточных. Следует отметить, что выбор той или иной библиотеки определяется, как правило, свойствами проекта (параметрами компилятора), а вовсе не кодом приложения. Поэтому
    при разработке многопотокового приложения важно проследить, чтобы
    при компиляции использовалась правильная версия библиотеки, во избежание возникновения трудно диагностируемых ошибок, проявляющихся
    в самых разных и совершенно «невинных» на первый взгляд местах кода.
    В случае Visual Studio однопоточные версии библиотек выбираются ключами /ML или /MLd компилятора, а многопоточные ключами /MT, /MD, /MTd

    Для реализации мультипрограммирования или мультипроцессирования на однотипных устройствах можно применять процессы, потоки и волокна. Разница между ними связана с возможностью обмена данными: адресное пространство процессов изолировано друг от друга, поэтому взаимодействие затруднено; потоки находятся в общем адресном пространстве процесса и могут легко взаимодействовать друг с другом; волокна отчасти аналогичны потокам, но планирование волокон выполняется непосредственно приложением, а не операционной системой.

    6.2.4. Основы использования потоков и волокон

    212

    213

    int main( void )
    {
    HANDLE
    hThread;
    unsigned dwThread;
    /* создаем новый поток */
    hThread = (HANDLE)_beginthreadex
    (NULL, 0, ThreadProc, new int [128], 0, &dwThread
    );
    /* код в этом месте может выполняться
    одновременно с кодом функции потока ThreadProc,
    планирование потоков осуществляется системой
    */
    /* дождаться завершения созданного потока */
    WaitForSingleObject( hThread, INFINITE );
    CloseHandle( hThread );
    return 0;
    }

    unsigned __stdcall ThreadProc( void *param )
    {
    /* вновь созданный поток будет выполнять эту функцию */
    Sleep( 1000 );
    delete[] (int*)param;
    return 0;
    /* завершение функции = завершение потока */
    }

    6.2.4.2. Работа с потоками
    Наличие специальных потоко-безопасных версий библиотек требует
    использования специальных функций для создания и завершения потоков,
    принадлежащих не системному API, а библиотеке времени исполнения.
    Так, вместо функций Win32 API CreateThread, ExitThread необходимо использовать
    библиотечные
    функции
    _beginthread,
    _endthread или _beginthreadex, _endthreadex. Это требование связано с тем,
    что при создании нового потока необходимо, помимо выполнения определенных действий по созданию потока со стороны операционной системы,
    инициализировать специфичные структуры данных, обслуживающих потоко-безопасные версии функций библиотеки времени исполнения:

    или /MDd (свойства проекта Configuration Properties|C/C++|Code
    Generation|Runtime Library).

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

    В данном примере можно было бы создавать поток не вызовом функции _beginthreadex (или _beginthread), а вызовом функции API
    CreateThread. Но при незначительном усложнении примера, скажем, создании не одного, а двух потоков, уже было бы возможно возникновение
    ошибки при одновременном обращении к операторам new или delete в
    разных потоках (причем именно «возможно», так как ничтожные временные задержки могут изменить поведение потоков – это крайне осложняет
    выявление таких ошибок). Применение функций библиотеки времени исполнения для создания потоков решает эту проблему.
    Windows содержит достаточно богатый набор функций для управления потоками, включающий функции создания и завершения потоков
    (функции API CreateThread, ExitThread, TerminateThread и их «обертки» в
    библиотеке времени исполнения _beginthread, _endthread, _beginthreadex
    и _endthreadex).
    Функция Sleep(DWORD dwMilliseconds) может переводить поток в
    «спячку» на заданное время. Продолжительность задается с точностью до
    кванта работы планировщика, то есть не лучше, чем 10-15 мс, несмотря на
    то, что при вызове функции задать можно до 1 мс. Измерение времени реальной паузы, заданной, например, вызовом Sleep(1), позволяет получить
    косвенную информацию о работе планировщика.
    В Windows существует интересная особенность, связанная с работой
    планировщика и измерением интервалов времени. Система предоставляет три способа измерения интервалов:
    • таймер низкого разрешения, основанный на квантах планировщика (GetTickCount);
    • «мультимедийный», с разрешением до 1 мс (timeGetTime,
    timeBeginPeriod и пр.);
    • высокоточный, использующий счетчик тактов процессора и с
    разрешением ощутимо лучше микросекунды на современных
    процессорах (QueryPerformanceCounter,
    QueryPerformanceFrequency).
    Обычно мультимедийный таймер работает с разрешением от 1-5 мс и хуже (зависит от аппаратуры), однако функция timeBeginPeriod позволяет изменить разрешение вплоть до 1 мс. Если стандартное разрешение мультимедийного таймера на данном компьютере хуже 5-10 мс, то у функции
    timeBeginPeriod есть побочный эффект – улучшение разрешения повлияет на
    работу планировщика во всей системе, а не только в процессе, вызвавшем эту
    функцию. В результате, если один процесс повысит разрешение мультимедийного таймера, то функция Sleep также получит возможность задавать интервалы вплоть до 1 мс и эффект наблюдается даже в других процессах. Если мультимедийный таймер на данной аппаратуре стандартно работает с разрешением
    порядка 1 мс, то такого влияния на планировщик не наблюдается.

    214

    215

    6.2.4.3. Работа с волокнами
    Работа с волокнами в приложении в чем-то сложнее, в чем-то проще.
    Сложнее, потому что необходимо реализовать собственный планировщик
    волокон. Сложность разработки планировщика резко возрастает при
    необходимости синхронизации волокон – стандартные средства синхронизации Windows переводят в режим ожидания поток целиком, даже если
    он должен планировать множество волокон. Проще, потому что все волокна могут разделять один поток – в этом случае легко избежать проблем
    конкурирующего доступа к данным и можно применять любую библиотеку времени исполнения, в том числе потоко-небезопасную.
    При работе с волокнами используется функция ConvertThreadToFiber
    для предварительного создания необходимых операционной системе структур данных. Функция ConvertFiberToThread выполняет обратную задачу и
    уничтожает выделенные данные. После того как необходимые структуры созданы (поток «превращен» в волокно), появляется возможность создавать новые волокна (CreateFiber), удалять существующие (DeleteFiber) и планировать их исполнение (SwitchToFiber).
    Приведем пример применения двух рабочих волокон, выполняющих
    целевую функцию, и одного управляющего, удаляющего рабочие волокна
    по их завершении.
    Функция main превращает текущий поток в волокно (инициализация
    внутренних структур данных для работы с волокнами), затем создает рабочие волокна и организует цикл, в котором ожидает их завершения и удаляет. Цикл завершается тогда, когда все рабочие волокна удалены, после чего функция main принимает меры к корректному завершению работы с волокнами.
    Собственно целевая функция FiberProc эпизодически вызывает
    функцию SwitchToFiber для переключения выполняемого волокна. В данном примере для определения нового волокна, подлежащего исполнению,
    реализован простейший планировщик (функция schedule, инкапсулирующая вызов функции SwitchToFiber).

    Есть частный случай применения функции Sleep – при задании интервала 0 вызов функции просто приводит к срабатыванию планировщика и, при наличии других готовых потоков, к их активации. Аналогичного
    эффекта можно добиться, применяя функцию SwitchToThread, вызывающую перепланирование потоков.
    Поток может быть создан в приостановленном (suspended) состоянии с
    помощью задания специального флага CREATE_SUSPENDED при вызове функций _beginthreadex или CreateThread, а также переведен в это состояние
    (функция SuspendThread) или, наоборот, пробужден с помощью функции
    ResumeThread.

    Основы многозадачности

    216

    fiberEnd;
    fiberCtl;
    fiber[ FIBERS ];

    int main( void )
    {
    int i;
    fiberCtl = ConvertThreadToFiber( NULL );
    fiberEnd = NULL;
    for ( i = 0; i < FIBERS; i++ ) {
    fiber[i] = CreateFiber( 10000, FiberProc, NULL );
    }

    VOID CALLBACK FiberProc( PVOID lpParameter )
    { /* волокно будет выполнять код этой функции */
    int i;
    for ( i = 0; i < 100; i++ ) {
    Sleep( 1000 );
    shedule( TRUE ); /* выполнение продолжается */
    }
    shedule( FALSE ); /* волокно завершается */
    }

    static void shedule( BOOL fDontEnd )
    {
    int
    n, current;
    if ( !fDontEnd ) { /* волокно надо завершить */
    fiberEnd = GetCurrentFiber();
    SwitchToFiber(fiberCtl );
    }
    /* выбираем следующее волокно для выполнения */
    for ( n = 0; n < FIBERS; n++ ) {
    if ( fiber[n] && fiber[n] != GetCurrentFiber() ) break;
    }
    if ( n >= FIBERS ) return; /* нет других готовых волокон*/
    SwitchToFiber( fiber[n] );
    }

    static LPVOID
    static LPVOID
    static LPVOID

    #define FIBERS 2

    #define _WIN32_WINNT 0x0400
    #include

    CIL и системное программирование в Microsoft .NET

    217

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

    }

    for ( i = 0; i < FIBERS;) {
    SwitchToFiber( fiber[i] );
    if ( fiberEnd ) {
    DeleteFiber(fiberEnd );
    for ( i = 0; i < FIBERS; i++ ) {
    if ( fiber[i] == fiberEnd ) fiber[i] = NULL;
    }
    fiberEnd = NULL;
    }
    for ( i = 0; i < FIBERS; i++ ) if ( fiber[i] ) break;
    }
    ConvertFiberToThread();
    return 0;

    Основы многозадачности

    CIL и системное программирование в Microsoft .NET

    Одна из типовых задач – разработка серверов, обслуживающих асинхронно поступающие запросы. Реализация однопоточного сервера для такой задачи нецелесообразна, во-первых, потому что запросы могут приходить в то время, пока сервер занят выполнением предыдущего, а, во-вторых, потому что такой сервер не сможет эффективно задействовать многопроцессорную систему. Можно, конечно, запускать несколько экземпляров однопоточного сервера – но в этом случае потребуется разработка специального диспетчера, поддерживающего очередь запросов и их распределение по списку доступных экземпляров сервера. Альтернативным решением является разработка многопоточного сервера, создающего по специальному рабочему потоку для обработки каждого запроса. Этот вариант
    также имеет свои недостатки: создание и удаление потоков требует затрат
    времени, которые будут иметь место в обработке каждого запроса; сверх
    того, создание большого числа одновременно выполняющихся потоков
    приведет к общему снижению производительности (и значительному увеличению времени обработки каждого конкретного запроса).
    Эти соображения приводят к решению, получившему название пула
    потоков (thread pool). Для реализации пула потоков необходимо создание
    некоторого количества потоков, занятых обслуживанием запросов, и диспетчера с очередью запросов. При наличии необработанных запросов диспетчер находит свободный поток и передает запрос этому потоку; если
    свободных потоков нет, то диспетчер ожидает освобождения какого-либо

    7.1.1. Пулы потоков, порт завершения ввода-вывода

    При разработке параллельных приложений недостаточно создать
    несколько параллельных ветвей кода – необходимо обеспечить их согласованное выполнение и своевременный обмен данными. При этом возникает
    необходимость использования как общих для разных потоков и процессов
    данных, так и создания собственных локальных данных, недоступных другим параллельным ветвям кода, может потребоваться синхронизация и даже
    учет особенностей аппаратуры, на которой данный код будет исполняться.

    7.1. Применение потоков и волокон

    Глава 7.
    Разработка параллельных приложений
    для ОС Windows

    218

    219

    HANDLE CreateIoCompletionPort(
    HANDLE FileHandle, HANDLE ExistingCompletionPort,
    ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads
    );

    из занятых потоков. Такой подход обеспечивает, с одной стороны, малые
    затраты на управление потоками, с другой – достаточно высокую загрузку
    процессоров и хорошую масштабируемость приложения.
    Обычно имеет смысл ограничивать общее число рабочих потоков либо числом доступных процессоров, либо кратным этому числом. Если потоки занимаются исключительно вычислительной работой, то их число не
    должно превышать число процессоров. Если потоки, сверх того, проводят
    некоторое время в состоянии ожидания (например, при выполнении операций ввода-вывода), то число потоков следует увеличить – решение следует принимать, исходя из доли времени, которое поток проводит в состоянии простоя, и из полного времени обработки запроса.
    Достаточно типичная рекомендация: ограничивать число потоков
    удвоенным числом процессоров. В случае вычислительных потоков накладные потери будут достаточно малы; в случае потоков, занятых вводом-выводом, утилизация процессоров будет близка к полной. Предполагается, что потоки, не занятые вводом-выводом и при этом проводящие
    много времени в состоянии ожидания, встречаются весьма редко.
    Реализация пула потоков является, на самом деле, нетривиальной задачей – необходимо поддерживать очередь запросов и учитывать состояния потоков из пула (поток простаивает; поток выполняется; поток выполняется, но находится в состоянии ожидания). Также следует учитывать
    возможность выгрузки части данных в файл подкачки (например, выгрузка стека давно не используемого потока) – иногда быстрее подождать завершения работающего потока, чем активировать простаивающий. Для
    учета всех этих соображений необходимо реализовать поддержку пулов
    потоков ядром операционной системы, так как на уровне приложения
    некоторые нужные сведения просто недоступны.
    В Windows такая поддержка реализована в виде порта завершения ввода-вывода. Этот объект ядра берет на себя функциональность, необходимую для организации очереди запросов (используя для этого очередь APC)
    и списков рабочих потоков, обеспечивая оптимальное управление пулом.
    С точки зрения разработчика приложения необходимо:
    • создать порт завершения ввода-вывода;
    • создать пул потоков, ожидающий поступления запросов от этого порта;
    • обеспечить передачу запросов порту.
    Порт завершения создается с помощью функции

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    4

    В этом варианте функция CreateIoCompletionPort не создает нового порта, а возвращает переданный ей описатель уже существующего. Существующий порт завершения ввода-вывода можно связать с несколькими различными файлами одновременно;
    при этом процедура, обслуживающая завершение ввода-вывода,
    сможет различать, операция с каким именно файлом поставила
    в очередь данный запрос, с помощью параметра CompletionKey
    (здесь SOME_NUMBER), назначаемого разработчиком. Созданный
    порт можно не ассоциировать ни с одним файлом – тогда с по-

    #define SOME_NUMBER 123
    CreateIoCompletionPort( hFile, hCP, SOME_NUMBER, 0 );

    HANDLE
    hCP;
    hCP = CreateIoCompletionPort(
    INVALID_HANDLE_VALUE, NULL, NULL, CONCURRENTS
    );
    При простом создании порта завершения ввода-вывода достаточно указать только максимальное число одновременно работающих потоков (здесь CONCURRENTS, целесообразно ограничивать числом доступных процессоров). Далее, когда будет создаваться пул потоков, в нем можно будет создать и большее число
    потоков, чем указано при создании порта – система будет отслеживать, чтобы одновременно исполнялся код не более чем указанного числа потоков. При этом поток, перешедший в состояние ожидания, не считается исполняющимся, так что в случае
    потоков, проводящих часть времени в режиме ожидания, имеет
    смысл создавать пул потоков большего размера, чем указано
    при вызове функции CreateIoCompletionPort.
    2. Ассоциирование порта с файлом:

    #define CONCURRENTS

    Эта функция выполняет две разных операции – во-первых, она создает новый порт завершения, и, во-вторых, она ассоциирует порт с завершением операций ввода-вывода с заданным файлом. Обе эти операции
    могут быть выполнены одновременно одним вызовом функции, а могут
    быть исполнены раздельно. Более того, вторая операция – ассоциирование порта завершения ввода-вывода с реальным файлом – может вообще
    не выполняться. Две типичных формы применения функции
    CreateIoCompletionPort:
    1. Создание нового порта завершения ввода-вывода:

    220

    221

    /* создаем пул потоков */
    for ( i = 0; i < POOLSIZE; i++ ) {
    hthread[i] = (HANDLE)_beginthreadex(
    NULL,0,PoolProc,(void*)hcport,0,(unsigned*)&temp
    );
    }
    Если созданный порт ассоциирован с одним или несколькими файлами, то после завершения асинхронных операций ввода-вывода в очереди
    порта будут размещаться асинхронные запросы, которые система будет направлять для обработки потокам из пула. Однако порт завершения ввода-вывода можно и не связывать с файлами – тогда для размещения запроса можно воспользоваться функцией PostQueuedCompletionStatus, которая размещает запросы в очереди без выполнения реальных операций ввода-вывода.

    int main( void )
    {
    int
    i;
    HANDLE hcport, hthread[ POOLSIZE ];
    DWORD
    temp;
    /* создаем порт завершения ввода-вывода */
    hcport = CreateIoCompletionPort(
    INVALID_HANDLE_VALUE, NULL, NULL, CONCURENTS
    );
    После создания порта надо создать пул потоков. Число потоков в пуле обычно превышает число одновременно работающих потоков, задаваемое при создании порта:

    unsigned __stdcall PoolProc( void *arg );

    #define MAXQUERIES 15
    #define CONCURENTS 3
    #define POOLSIZE
    5

    #include
    #define _WIN32_WINNT 0x0500
    #include

    мощью функции PostQueuedCompletionStatus надо будет помещать в очередь порта запросы, имитирующие завершение вводавывода.
    Рассмотрим небольшой пример (проверка ошибок для упрощения
    пропущена):

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    unsigned __stdcall PoolProc( void *arg )
    {
    DWORD
    size;
    ULONG_PTR
    key;
    LPOVERLAPPED lpov;
    while (
    GetQueuedCompletionStatus(
    (HANDLE)arg, &size, &key, &lpov, INFINITE

    }
    В общем виде поток в пуле реализует цикл с выбором запросов из
    очереди с помощью функции GetQueuedCompletionStatus. Следует внимательно ознакомиться с описанием этой функции, чтобы грамотно обрабатывать возможные ошибочные ситуации и предусмотреть при необходимости завершение работы потока.
    В данном примере поток в течении 0.3 секунды просто ждет, то есть
    не исполняется, и порт завершения может передать запросы всем потокам
    пула, хотя их количество превышает максимальное число одновременно
    работающих потоков, указанное при создании порта:

    /* для завершения работы посылаем специальные запросы */
    for ( i = 0; i < POOLSIZE; i++ ) {
    PostQueuedCompletionStatus(hcport,0, (ULONG_PTR)-1,NULL);
    }
    /* дожидаемся завершения всех потоков пула
    и закрываем описатели */
    WaitForMultipleObjects( POOLSIZE, hthread, TRUE, INFINITE );
    for ( i = 0; i < POOLSIZE; i++ ) CloseHandle( hthread[i] );
    CloseHandle( hcport );
    return 0;

    /* посылаем несколько запросов в порт */
    for ( i = 0; i < MAXQUERIES; i++ ) {
    PostQueuedCompletionStatus( hcport, 1, i, NULL );
    Sleep( 60 );
    }
    Функция помещает в очередь запросов информацию о «как будто»
    выполненной операции ввода-вывода, полностью повторяя аргументы –
    такие как размер переданного блока данных, ключ завершения и указатель
    на структуру OVERLAPPED, содержащую сведения об операции. Мы можем
    передавать вместо этих значений произвольные данные. В данном примере, скажем, принято, что значение ключа завершения -1 совместно с длиной переданного блока 0 означает необходимость завершить поток:

    222

    223

    DWORD WINAPI QueryFunction( PVOID pContext )
    {
    ...
    return 0L;
    }
    Таким образом, управление пулом потоков сильно упрощается, хотя
    при этом теряется возможность связывания порта завершения ввода-вывода с конкретными файлами и все запросы должны размещаться в очереди явным вызовом функции QueueUserWorkItem.
    Есть и еще одна особенность у такого способа управления пулом –
    явного механизма задания числа потоков в пуле не предусмотрено. Однако у разработчика есть возможность управлять этим процессом с помощью последнего параметра функции, содержащего специфичные флаги.
    Так, с помощью флага WT_EXECUTEDEFAULT запрос будет направлен обычному потоку из пула, флаг WT_EXECUTEINIOTHREAD заставит систему обрабатывать запрос в потоке, который находится в состоянии ожидания оповещения (то есть, надо предусмотреть явные вызовы функции типа SleepEx или
    WaitForMultipleObjectsEx и т.д.). Флаг WT_EXECUTELONGFUNCTION предназна-

    BOOL QueueUserWorkItem(
    LPTHREAD_START_ROUTINE QueryFunction,
    PVOID pContext, ULONG Flags
    );
    Эта функция при необходимости создает пул потоков (число потоков
    в пуле определяется числом процессоров), создает порт завершения ввода-вывода и размещает в очереди порта запрос. Если нужный порт и пул
    потоков уже созданы, то она просто размещает новый запрос в очереди
    порта. При обработке запроса будет вызвана указанная параметром
    QueryFunction процедура с аргументом pContext:

    }
    Рассмотренный механизм управления пулом потоков весьма эффективен, однако требует некоторого объема ручной работы по созданию порта, по созданию пула потоков и по управлению этими потоками.
    В современных реализациях Windows предусмотрена возможность автоматического создания и управления пулом потоков с помощью функции

    )) {
    /* проверяем условия завершения цикла */
    if ( !size && key == (ULONG_PTR)-1 ) break;
    Sleep( 300 );
    }
    return 0L;

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    15
    3
    cnt;
    hEvent;

    int main( void )
    {
    int i;
    hEvent = CreateEvent( NULL, TRUE, FALSE, 0 );
    /* в пуле будет не менее POOLSIZE потоков */
    for ( i = 0; i < POOLSIZE; i++ ) {
    QueueUserWorkItem( QProc, NULL, WT_EXECUTELONGFUNCTION );
    Sleep( 60 );
    }
    /* остальные запросы будут распределяться между потоками
    пула,даже если их больше, чем число процессоров */
    for ( ; i < MAXQUERIES; i++ ) {
    QueueUserWorkItem( QProc, NULL, WT_EXECUTEDEFAULT );
    Sleep( 60 );
    }
    /* со временем система может уменьшить число потоков пула */
    /* дожидаемся обработки последнего запроса */
    WaitForSingleObject( hEvent, INFINITE );
    CloseHandle( hEvent );
    return 0;
    }

    DWORD WINAPI QProc( LPVOID lpData )
    {
    int r = InterlockedIncrement( &cnt );
    Sleep( 300 );
    if ( r >= MAXQUERIES ) SetEvent( hEvent );
    return 0L;
    }

    #define MAXQUERIES
    #define POOLSIZE
    static LONG
    static HANDLE

    #include
    #define _WIN32_WINNT 0x0500
    #include

    чен для случаев, когда обработка запроса может привести к продолжительному ожиданию – тогда система может увеличить число потоков в пуле:

    224

    225

    При разработке многопоточных приложений возникает необходимость обеспечивать не только параллельное исполнение кода потоков, но
    также их взаимодействие – обмен данными, доступ к общим, разделяемым всеми потоками данным и изоляцию некоторых данных одного потока от других.
    Поскольку все потоки разделяют общее адресное пространство процесса, то все они имеют общий и равноправный доступ ко всем данным,
    хранимым в адресном пространстве. Поэтому для потоков, как правило,
    не существует проблем с передачей данных друг другу – нужна лишь организация корректного взаимного доступа и изоляция собственных данных
    от данных других потоков.
    Очень часто для изоляции данных достаточно их размещать в стеке –
    тогда другие потоки смогут получить к ним доступ либо по явно переданным указателям, либо путем сканирования памяти в поисках стеков других потоков и нужных данных в этих стеках, что относится уже к достаточно трудоемким хакерским технологиям. Однако таким образом трудно организовать постоянное хранение данных, и необходимо постоянно явным
    образом передавать эти данные (или указатель на них) во все вызываемые
    процедуры; это достаточно неудобно и не всегда возможно.
    Для решения подобных задач в Windows предусмотрен механизм управления данными, локальными для потока (TLS память, Thread Local
    Storage). Система предоставляет небольшой специальный блок данных,
    ассоциированный с каждым потоком. В таком блоке возможно в общем
    случае хранение произвольных данных, однако, так как размеры этого
    блока крайне малы, то обычно там размещаются указатели на данные
    большего объема, выделяемые в приложении для каждого потока; в связи
    с этим ассоциированную с потоком память можно рассматривать как массив двойных слов или массив указателей.
    ОС Windows предоставляет четыре функции, необходимые для работы с локальной для потока памятью. Функция DWORD TlsAlloc(void) выделяет в ассоциированной с потоком памяти двойное слово, индекс которого возвращается вызвавшей процедуре. Если ассоциированный массив
    полностью использован, возвращаемое значение будет равно
    TLS_OUT_OF_INDEXES, что сообщает об ошибке выделения ячейки. Функция
    TlsFree освобождает выделенную ячейку.
    Если поток выделил некоторую ячейку в ассоциированном массиве,
    то все потоки данного процесса могут обращаться к ячейке с этим индек-

    7.1.2. Память, локальная для потоков и волокон

    Последний пример качественно проще, чем пример с явным созданием порта завершения ввода-вывода, хотя часть возможностей порта завершения при этом не может быть использована.

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    unsigned __stdcall ThreadProc( void *param )
    {
    TlsSetValue( dwTlsData, (LPVOID)new int array[100] );
    /* выделенные потоком данные размещены в общей куче,
    используемой всеми потоками, однако указатель на
    эти данные известен только потоку-создателю, так
    как сохраняется в локальной для потока области */
    ProcA( (int)param );
    Sleep( 0 );
    if ( ProcB() != 100*(int)param ) { /* ОШИБКА!!! */ }
    delete[] (int*)TlsGetValue( dwTlsData );
    return 0;
    }

    int ProcB( void )
    {
    int i, x;
    int *iptr = (int*)TlsGetValue( dwTlsData );
    for ( i = x = 0; i < 100; i++ ) x += iptr[i];
    return x;
    }

    void ProcA( int x )
    {
    int i;
    int *iptr = (int*)TlsGetValue( dwTlsData );
    for ( i = 0; i < 100; i++ ) iptr[i] = x;
    }

    #define THREADS 18
    static DWORD dwTlsData;

    #include
    #include

    сом – они получат доступ к ячейкам своих собственных ассоциированных
    массивов и не смогут узнать или изменить значения, сохраненные в этих
    ячейках другими потоками. Для доступа к данным зарезервированной
    ячейки используется функция TlsGetValue, возвращающая значение данной ячейки (в виде указателя, т.к. предполагается, что в ячейках хранятся
    указатели на некоторые структуры данных) и функция TlsSetValue, изменяющая значение в соответствующей ячейке:

    226

    227

    }

    for ( i = 0; i < 100; i++ ) iptr[i] = x;

    void ProcA( int x )
    {
    int i;

    #define THREADS 18
    __declspec(thread) static int iptr[ 100 ];

    #include
    #include

    int main( void )
    {
    HANDLE
    hThread[ THREADS ];
    unsigned dwThread;
    dwTlsData = TlsAlloc();
    /* создаем новые потоки */
    for ( i = 0; i < THREADS; i++ )
    hThread[i]=(HANDLE)_beginthreadex(
    NULL, 0, ThreadProc, (void*)i, 0, &dwThread
    );
    /* дождаться завершения созданных потоков */
    WaitForMultipleObjects( THREADS, hThread, TRUE, INFINITE );
    for ( i = 0; i < THREADS; i++ ) CloseHandle( hThread[i] );
    TlsFree( dwTlsData );
    return 0;
    }
    В приведенном примере в функции main выделяется ячейка в ассоциированном списке, индекс которой сохраняется в глобальной переменной
    dwTlsData, после чего потоки могут сохранять в этой ячейке свои данные.
    В Visual Studio работа с локальной для потока памятью может быть упрощена при использовании _declspec(thread) при описании переменных.
    В этом случае компилятор будет размещать эти переменные в специальном
    сегменте данных (_TLS), который будет создаваться библиотекой времени
    исполнения и ссылки на который будут разрешаться с использованием ассоциированной с потоком памяти. Этот способ во многих случаях предпочтительнее явного управления локальной для потока памятью, так как
    независимо от числа модулей, использующих такой сегмент, будет задействован только один указатель в ассоциированной памяти (построитель объединит в один большой сегмент все _TLS сегменты модулей).

    Разработка параллельных приложений для ОС Windows

    unsigned __stdcall ThreadProc( void *param )
    {
    ProcA( (int)param );
    Sleep( 0 );
    if ( ProcB() != 100*(int)param ) { /* ОШИБКА!!! */ }
    return 0;
    }

    int ProcB( void )
    {
    int i, x;
    for ( i = x = 0; i < 100; i++ ) x += iptr[i];
    return x;
    }

    CIL и системное программирование в Microsoft .NET

    int main( void )
    {
    HANDLE
    hThread[THREADS];
    unsigned dwThread;
    int
    i;
    /* создаем новые потоки */
    for ( i = 0; i < THREADS; i++ )
    hThread[i] = (HANDLE)_beginthreadex(
    NULL, 0, ThreadProc, (void*)i, 0, &dwThread
    );
    /* дождаться завершения созданных потоков */
    WaitForMultipleObjects( THREADS, hThread, TRUE, INFINITE );
    for ( i = 0; i < THREADS; i++ ) CloseHandle( hThread[i] );
    TlsFree( dwTlsData );
    return 0;
    }
    Следует внимательно следить за выделением и освобождением данных, указатели на которые сохраняются в TLS памяти (как в случае явного управления, так и при использовании _declspec(thread)). Могут возникнуть две потенциально ошибочных ситуации:
    1. TLS память резервируется в то время, когда уже существуют потоки. Это возможно при явном управлении TLS памятью, и для
    существующих потоков будут зарезервированы ячейки, но придется предусмотреть специальные меры для их корректной инициализации или для исключения их использования до этого.
    2. Все случаи завершения потока. Если TLS память содержит какие-либо указатели, то сама TLS память будет освобождена, а

    228

    229

    VOID WINAPI FlsCallback( PVOID lpFlsData )
    {
    ...
    }
    Функция отличается от ее аналога TlsAlloc указателем на специальную необязательную процедуру FlsCallback, предоставляемую разработчиком. Эта процедура будет вызвана автоматически при освобождении
    ячейки FLS памяти (как при завершении волокна, так и при завершении
    потока или возникновении ошибки), и разработчик может легко предоставить средства для освобождения памяти, указатели на которую были сохранены в ячейках FLS памяти.

    DWORD FlsAlloc( PFLS_CALLBACK_FUNCTION lpCallback );

    вот те данные, указатели на которые хранились в TLS памяти, –
    нет. Необходимо специально отслеживать все возможные случаи завершения потоков, включая завершение по ошибке, и
    принимать меры для освобождения выделенной памяти. При
    использовании _declspec(thread) эта ситуация встречается реже, так как позволяет хранить в _TLS сегментах данные любого
    фиксированного размера.
    Следует отметить еще один нюанс, связанный с использованием TLS
    памяти, волокон и оптимизации. В частных случаях волокна могут исполняться разными потоками – при этом одно и то же волокно должно иметь
    доступ к TLS памяти именно того потока, в котором оно в данный момент
    исполняется. А если компилятор генерирует оптимизированный код, то
    он может разместить указатель на данные TLS памяти в каком-либо регистре или временной переменной, что при переключении волокна на другой поток приведет к ошибке – будет использована TLS память предыдущего потока. Чтобы избежать такой ситуации, компилятору можно указать специальный ключ /GT, отключающий некоторые виды оптимизации
    при работе с TLS памятью. Это может потребоваться в крайне редких случаях – когда приложение использует несколько волокон, исполняемых в
    нескольких потоках, и при этом волокна должны использовать TLS память потоков.
    Аналогично TLS памяти, Windows поддерживает память, локальную
    для волокон, – так называемую FLS память, или Fiber Local Storage. При
    этом FLS память не зависит от того, какой именно поток выполняет данную нить. Для работы с FLS памятью Windows предоставляет набор функций, аналогичный Tls-функциям, отличие заключается только в функции
    выделения ячейки FLS памяти:

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    ОС Windows предоставляет небольшой набор функций, предназначенных для поддержки систем с неоднородным доступом к памяти
    (NUMA). К таким функциям относятся средства, обеспечивающие выполнение потоков на конкретных процессорах, и функции, позволяющие
    получить информацию о структуре NUMA машины. В некоторых случаях
    привязка потоков к процессорам может преследовать и иные цели, чем
    поддержка NUMA архитектуры. Так, например, привязка потока к процессору может улучшить использование кэша; на некоторых SMP машинах могут возникать проблемы с использованием таймеров высокого разрешения (опирающихся на счетчики процессоров) и т.д.
    Привязка потоков к процессору задается с помощью специального
    битового вектора (affinity mask), сохраняемого в целочисленной переменной. Каждый бит этого вектора указывает на возможность исполнения потока на процессоре, номер которого совпадает с номером бита. Таким образом, заданием маски сродства можно ограничить множество процессоров, на которых будет выполняться данный поток. В Windows такие маски
    назначаются
    процессу
    (функции
    GetProcessAffinityMask
    и
    SetProcessAffinityMask) и потоку (функция SetThreadAffinityMask). Маска, назначаемая потоку, должна быть подмножеством маски процесса. Помимо ограничения множества процессоров, на которых может исполняться поток, может быть целесообразно назначить потоку самый «удобный»

    7.1.3. Привязка к процессору и системы с неоднородным
    доступом к памяти

    DWORD dwFlsID;
    VOID WINAPI FlsCallback( PVOID lpFlsData )
    {
    /* при завершении волокна или потока память будет освобождена */
    delete[] (int*)lpFlsData;
    }
    void initialize( void )
    {
    dwFlsID = FlsAlloc( FlsCallback );
    ...
    }
    void fiberstart( void )
    {
    FlsSetValue( dwFlsID, new int [ 100 ] );
    /* здесь мы можем не следить за освобождением выделенной памяти */
    }
    Остальные функции для работы с FLS аналогичны Tls-функциям как
    по описаниям, так и по применению.

    230

    231

    #define THREADS 10
    #define ASIZE 10000000
    static LONG
    array[ASIZE];

    При реализации мультипрограммирования существует проблема одновременного конкурирующего доступа нескольких потоков к общим
    разделяемым данным. В многопоточных приложениях она особенно актуальна, так как вся память процесса является общей и разделяемой всеми
    потоками, поэтому конфликты при одновременном доступе могут возникать достаточно часто.
    Рассмотрим простейшую программу, в которой несколько потоков
    увеличивают на 1 значение элементов общего массива. Так как начальные
    значения элементов массива 0, то в результате весь массив должен быть заполнен числами, соответствующими числу потоков:
    #include
    #include
    #include

    7.2.1. Синхронизация потоков

    Материалы, рассматриваемые в этом разделе, могут быть разделены
    на две категории – синхронизация потоков и работа с процессами. Они
    сгруппированы в силу того, что большая часть методов синхронизации потоков может быть успешно применена для потоков, принадлежащих разным процессам. Кроме того, многие средства взаимодействия потоков,
    даже основанные на совместном доступе к данным в едином адресном
    пространстве, могут быть использованы в рамках одного вычислительного узла – в случае применения общей, разделяемой процессами, памяти.

    7.2. Взаимодействие процессов и потоков

    для него процессор (по умолчанию – тот, на котором поток был запущен
    первый раз). Для этого предназначена функция SetThreadIdealProcessor.
    При использовании NUMA систем следует учитывать, что распределение доступных процессоров по узлам NUMA системы не обязательно
    последовательное – узлы со смежными номерами могут быть с аппаратной точки зрения весьма удалены друг от друга. Функция
    GetNumaHighestNodeNumber позволяет определить число NUMA узлов, после чего с помощью обращений к функциям GetNumaProcessorNode,
    GetNumaNodeProcessorMask и GetNumaAvailableMemoryNode можно определить
    размещение узлов NUMA системы на процессорах и доступную каждому
    узлу память.

    Разработка параллельных приложений для ОС Windows

    unsigned __stdcall ThreadProc( void *param )
    {
    int i;
    for ( i = 0; i < ASIZE; i++ ) array[i]++;
    return 0;
    }

    CIL и системное программирование в Microsoft .NET

    int main( void )
    {
    HANDLE
    hThread[THREADS];
    unsigned dwThread;
    int
    i, errs;
    for ( i = 0; i < THREADS; i++ )
    hThread[i] = (HANDLE)_beginthreadex(
    NULL, 0, ThreadProc, NULL, 0, &dwThread
    );
    WaitForMultipleObjects( THREADS, hThread, TRUE, INFINITE );
    for ( i = 0; i < THREADS; i++ ) CloseHandle( hThread[i] );
    for ( errs=i=0; i if ( array[i] != THREADS ) errs++;
    if ( errs ) printf(“Detected %d errors!\n”, errs );
    return 0;
    }
    Все потоки перебирают массив в одном и том же порядке, и шанс
    конфликтов при доступе к элементам массива сравнительно невелик; однако, даже в такой ситуации при достаточно большом размере массива
    (если массив маленький, то потоки, скорее всего, будут работать последовательно и конфликтов вообще не возникнет) будут появляться ошибки.
    Причина в том, что операция увеличения на 1 элемента массива на самом
    деле не элементарная – она требует запроса для считывания данных из памяти, увеличения значения на 1 и сохранения данных в памяти. Если поток будет прерван планировщиком между операциями считывания и последующей записи, то после возобновления его исполнения уже может оказаться так, что какой-либо другой поток успел изменить значение в массиве, и запись значения, вычисленного на основе старого значения, будет
    некорректной. На многопроцессорных машинах такие ошибки могут возникать еще чаще, и при массивах меньшего размера.
    Поэтому при осуществлении одновременного доступа разных потоков к общим данным необходимо предпринимать специальные меры, исключающие возникновение конфликтов. В Windows предусмотрено
    несколько разных способов решения подобных проблем:

    232

    233

    7.2.1.1. Атомарные операции
    Атомарные операции обычно имеют более-менее близкое соответствие с командами процессора. Так, например, они могут сводиться к операциям с блокировкой шины (префикс lock) и специальным командам (типа cmpxchg) процессора. ОС Windows предоставляет функции для увеличения (InterlockedIncrement, InterlockedIncrement64) или уменьшения
    (InterlockedDecrement, InterlockedDecrement64) значения целочисленных
    переменных и изменения их значений (InterlockedExchange,
    InterlockedExchange64, InterlockedExchangeAdd, InterlockedExchangePointer),

    • Атомарные операции.
    В достаточно частых случаях необходимо обеспечить конкурентный доступ к какой-либо целочисленной переменной, являющейся счетчиком. Тогда бывает достаточно просто обеспечить атомарность выполнения операций увеличения, уменьшения или изменения значения переменной.
    • Критические секции.
    В более сложном случае необходимо гарантировать, что какойлибо фрагмент кода будет выполняться в монопольном режиме.
    Критические секции могут быть использованы только в рамках
    одного процесса, однако при этом критическая секция может
    работать чуть быстрее, чем синхронизация с использованием
    объектов ядра.
    • Синхронизация с использованием объектов ядра.
    Объекты ядра должны поддерживать специальный интерфейс
    синхронизируемых объектов, чтобы они могли быть использованы для взаимной синхронизации потоков или процессов.
    Этот интерфейс поддерживают многие объекты ядра, например,
    файлы, процессы, потоки, консоли, задания и пр. Кроме того,
    Windows предоставляет специальный набор объектов, предназначенный именно для взаимной синхронизации потоков. Объекты ядра могут быть использованы как для синхронизации потоков в рамках одного процесса, так и для синхронизации потоков в разных процессах.
    • Ожидающие таймеры.
    В некоторых случаях необходимо обеспечить выполнение каких-либо операций либо в заданное время, либо с определенной
    периодичностью. Для решения этих задач в Windows предусмотрены ожидающие таймеры, которые могут использоваться или
    средствами синхронизации (ожидающий таймер является объектом ядра, поддерживающим интерфейс синхронизируемых
    объектов), или для вызова асинхронных процедур.

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    7.2.1.2. Критические секции
    Термин «критическая секция» обозначает некоторый фрагмент кода,
    который должен выполняться в исключительном режиме – никакие другие потоки и процессы не должны выполнять этот же фрагмент в то же
    время. В некоторых операционных системах для однопроцессорных машин вход в критическую секцию просто блокирует работу планировщика
    до выхода из секции. Этот подход не всегда эффективен: нахождение потока в критической секции не должно влиять на возможность исполнения
    кода, не принадлежащего именно данной критической секции, другими
    потоками, особенно на многопроцессорных машинах.
    В Windows предусмотрен специальный тип данных, называемый
    CRITICAL_SECTION и предназначенный для реализации критических секций. В приложении может существовать произвольное количество данных
    этого типа, реализующих различные критические секции; равно как
    несколько секций кода могут использовать один общий объект
    CRITICAL_SECTION.
    Существуют четыре основных функции для работы с критическими
    секциями; перед использованием критическая секция должна быть инициализирована с помощью функции InitializeCriticalSection. Объект
    CRITICAL_SECTION, принадлежащий пользовательскому процессу, может
    использовать в своей реализации объекты ядра для ожидания; эти объекты могут создаваться по мере надобности, и для окончательного освобождения ресурсов, после использования критической секции она должна
    быть удалена с помощью функции DeleteCriticalSection.

    unsigned __stdcall ThreadProc( void *param )
    {
    int i;
    for ( i = 0; i < ASIZE; i++ ) InterlockedIncrement(array+i);
    return 0;
    }
    Еще несколько функций предназначены для работы с односвязными
    LIFO списками. Функция InitializeSListHead подготавливает начальный
    указатель на LIFO список, функция InterlockedPushEntrySList добавляет новую запись в список, функция InterlockedPopEntrySList извлекает из списка
    последнюю добавленную запись и функция InterlockedFlushSList очищает
    список. Операции изменения указателей в списке являются атомарными.

    в
    том
    числе
    со
    сравнением
    (InterlockedCompareExchange,
    InterlockedCompareExchangePointer).
    В приведенном выше примере было бы достаточно заменить оператор array[i]++ на вызов функции InterlockedIncrement:

    234

    235

    THREADS
    ASIZE
    LONG
    CRITICAL_SECTION

    10
    10000000
    array[ASIZE];
    CS;

    int main( void )
    {
    HANDLE
    hThread[THREADS];
    unsigned dwThread;
    int
    i, errs;
    InitializeCriticalSectionAndSpinCount( &CS, 100 );
    for ( i = 0; i < THREADS; i++ )
    hThread[i] = (HANDLE)_beginthreadex(
    NULL, 0, ThreadProc, (void*)i, 0, &dwThread );
    WaitForMultipleObjects( THREADS, hThread, TRUE, INFINITE );
    for ( i = 0; i < THREADS; i++ ) CloseHandle( hThread[i] );

    unsigned __stdcall ThreadProc( void *param )
    {
    int i;
    for ( i = 0; i < ASIZE; i++ ) {
    EnterCriticalSection( &CS );
    array[i]++;
    LeaveCriticalSection( &CS );
    }
    return 0;
    }

    #define
    #define
    static
    static

    #include
    #include
    #define _WIN32_WINNT 0x0403
    #include

    Собственно работа с критическими секциями сводится к двум основным функциям: функция EnterCriticalSection, которая соответствует
    входу в критическую секцию, при необходимости с ожиданием, не ограниченным по времени (!); и функция LeaveCriticalSection, которая соответствует выходу из этой секции, возможно с пробуждением потоков,
    ожидающих ее освобождения. При этом система исключает вход в критическую секцию всех остальных потоков процесса, в то время как поток,
    уже вошедший в данную секцию, может входить в нее рекурсивно – надо
    лишь, чтобы число выходов из секции соответствовало числу входов:

    Разработка параллельных приложений для ОС Windows

    for ( errs=i=0; i if ( array[i] != THREADS ) errs++;
    if ( errs ) printf(“Detected %d errors!\n”, errs );
    DeleteCriticalSection( &CS );
    return 0;

    CIL и системное программирование в Microsoft .NET

    DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMsecs );
    DWORD WaitForMultipleObjects(
    DWORD nCount, const HANDLE* lpHandles,
    BOOL bWaitAll, DWORD dwMsecs
    );
    С точки зрения операционной системы объекты ядра, поддерживающие интерфейс синхронизируемых объектов, могут находиться в одном из
    двух состояний: свободном (signaled) и занятом (nonsignaled). Функции про-

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

    }
    Помимо этих основных функций, Windows предоставляет еще
    несколько, например, TryEnterCriticalSection, которая позволяет при необходимости не входить в секцию, если она занята (так как при использовани EnterCriticalSection ожидание будет неограниченным по времени,
    что может быть неприемлемым).
    Кроме того, эффективность критических секций на многопроцессорных машинах может быть повышена, если перед началом ожидания занятой
    секции, вместо перехода к ожиданию в режиме ядра, выполнить предварительный кратковременный цикл с опросом состояния секции. Если секция
    занимается другим потоком на небольшое время, то такой цикл позволяет
    дождаться ее освобождения, не переходя в режим ядра. Для этого предназначены
    функции:
    InitializeCriticalSectionAndSpinCount
    и
    SetCriticalSectionSpinCount. Они позволяют задавать число опросов состояния занятой критической секции перед переходом в режим ядра для ожидания. На однопроцессорных машинах опросы не выполняются и функция
    InitializeCriticalSectionAndSpinCount ничем не отличается от обычной
    функции InitializeCriticalSection.

    236

    237

    веряют состояние ожидаемого объекта или ожидаемых объектов и продолжают выполнение, только если объекты свободны. В зависимости от типа
    ожидаемого объекта, система может предпринять специальные действия
    (например, как только поток дожидается освобождения объекта исключительного владения, он сразу должен захватить его).
    Функция WaitForSingleObject осуществляет ожидание одного объекта, а функция WaitForMultipleObjects может ожидать как освобождения
    любого из указанных объектов (bWaitAll = FALSE), так и всех сразу
    (bWaitAll = TRUE). Ожидание завершается либо по освобождении объекта(ов), либо по истечении указанного интервала времени (dwMsecs) в миллисекундах (бесконечное при dwMsecs = INFINITE). Код возврата функции
    позволяет определить причину – таймаут, освобождение конкретного объекта либо ошибка.
    Если функция WaitForMultipleObjects (или ее клон) ожидает сразу
    все объекты группы, то до освобождения всех ожидаемых объектов одновременно никаких мер по занятию ранее освободившихся объектов функция не предпринимает.
    В тех случаях, когда поток переходит в состояние ожидания, его исполнение блокируется до конца ожидания. Для реализации APC (и функций завершения ввода-вывода) необходимо было предусмотреть в операционной системе возможность приостановки потока с вызовом асинхронных процедур. Это связано с тем, что система должна гарантировать выполнение функций в контексте конкретного потока для соблюдения норм
    безопасности. Ожидание оповещения – это такое состояние ожидания,
    которое может быть завершено либо по достижении таймаута, либо при
    освобождении указанного объекта, либо после обработки APC. При этом
    в контексте потока, находящегося в состоянии ожидания оповещения, обрабатывается APC вызов и только затем завершается состояние ожидания.
    Для перехода в ожидание оповещения предусмотрены функции
    SleepEx,
    WaitForMultipleObjectsEx,
    WaitForSingleObjectEx
    и
    SignalObjectAndWait.
    Еще несколько функций предназначены для разработки GUI-приложений Win32: MsgWaitForMultipleObjects и MsgWaitForMultipleObjectsEx
    выполняют ожидание указанных объектов и, кроме того, могут контролировать состояние очереди потока, предназначенной для обработки оконных сообщений. Функция WaitForInputIdle ожидает, пока GUI-приложение не закончит свою инициализацию и не перейдет в ожидание в цикле
    обработки сообщений.
    Несколько типов объектов в Win32 предназначены только для взаимной синхронизации потоков: события (event), семафоры (semaphore), объекты исключительного владения (мьютексы, mutex, mutual exclusion) и ожидающие таймеры (waitable timer). Для изменения их состояния предусмотре-

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    VOID CALLBACK ApcProc( ULONG_PTR dwData )
    {
    SetEvent( (HANDLE)dwData );
    }

    #define DEFAULT_SECURITY (LPSECURITY_ATTRIBUTES)NULL

    ны специальные функции, так что потоки могут явным образом управлять
    состоянием таких объектов, обеспечивая взаимную синхронизацию. Эти
    объекты являются базовыми примитивами, на которых часто строятся более сложные синхронизирующие объекты.
    При проектировании составных объектов иногда возникает задача
    изменения состояния одного из примитивных объектов при переходе к
    ожиданию другого. Если такие операции выполнять поочередно, то между вызовами функции, изменяющей состояние объекта, и функции ожидания возможно срабатывание планировщика и переключение потоков;
    то есть время, проходящее между изменением состояния одного объекта и
    началом ожидания другого, оказывается непредсказуемым. Чтобы избежать такой ситуации, предусмотрена функция SignalObjectAndWait, переводящая один объект в свободное состояние и ожидающая другой.
    Стандартные синхронизирующие объекты могут быть именованными (функции, создающие эти объекты, имеют параметр, указывающий их
    имя), а могут быть «безымянными», если вместо имени указать NULL. Попытка создания именованных объектов с совпадающими именами приведет или к получению нового описателя существующего объекта (если типы существующего объекта и вновь создаваемого одинаковы), или к
    ошибке (если типы разные). Именованные объекты обычно используют
    для межпроцессного взаимодействия, а безымянные – для взаимодействия потоков в одном процессе.
    События
    Обычно событие – некоторый объект, который может находиться в
    одном из двух состояний: занятом или свободном. Переключение состояний осуществляется явным вызовом соответствующих функций; при этом
    любой процесс/поток, имеющий необходимые права доступа к объекту
    «событие», может изменить его состояние.
    В Windows различают события с ручным и с автоматическим сбросом
    (тип и начальное состояние задаются при создании события функцией
    CreateEvent). События с ручным сбросом ведут себя обычным образом:
    функция SetEvent переводит событие в свободное (сигнальное) состояние,
    а функция ResetEvent – в занятое (не сигнальное). События с автоматическим сбросом переводятся в занятое состояние либо явным вызовом
    функции ResetEvent, либо ожидающей функцией WaitFor....

    238

    239

    int main( void )
    {
    HANDLE hEvent;
    hEvent = CreateEvent( DEFAULT_SECURITY, TRUE, FALSE, NULL );
    QueueUserAPC(ApcProc, GetCurrentThread(), (ULONG_PTR)hEvent);
    WaitForSingleObject(hEvent,100);
    /* код завершения WAIT_TIMEOUT */
    SleepEx( 100, TRUE );
    /* APC процедура освобождает событие */
    WaitForSingleObject(hEvent,100);
    /* код завершения WAIT_OBJECT_0 */
    WaitForSingleObject(hEvent,100);
    /* код завершения WAIT_OBJECT_0 */
    CloseHandle( hEvent );
    return 0;
    }
    В примере создается событие с ручным сбросом в занятом состоянии.
    При первом вызове функции WaitForSingleObject событие все еще занято,
    поэтому выход из функции осуществляется по таймауту. При втором вызове уже успевает сработать APC и событие свободно – поэтому функция
    ожидания завершится с успехом. К третьему вызову состояние события
    не меняется, поэтому результат аналогичный.
    Если в приведенном примере изменить событие с ручным сбросом на
    событие с автоматическим сбросом (второй параметр функции CreateEvent
    должен быть FALSE), то при втором обращении к WaitForSingleObject функция завершится также с кодом WAIT_OBJECT_0, но при этом событие автоматически будет переведено в занятое состояние. В результате третий вызов
    функции WaitForSingleObject завершится по таймауту.
    Поведение событий с ручным и автоматическим сбросом особенно
    различаются в случае, если несколько потоков ждут одного события: для
    события с ручным сбросом, при установке его в свободное состояние, выполнение могут продолжить все ожидающие потоки; а для события с автоматическим сбросом – только один, так как событие будет сразу переведено в занятое состояние.
    В некоторых случаях бывает надо перевести событие в свободное состояние, чтобы ожидающие на данный момент времени потоки могли
    продолжить свое выполнение, после чего снова вернуть в занятое. Вместо
    пары вызовов SetEvent...ResetEvent, во время выполнения которых планировщик вполне может переключиться на другие потоки (в итоге время
    нахождения события в свободном состоянии непредсказуемо), целесообразно использовать функцию PulseEvent, которая как бы выполняет сброс
    и установку события, но в рамках одной операции.

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    int main( void )
    {
    HANDLE hSem;
    LONG
    lPrev;
    hSem = CreateSemaphore( DEFAULT_SECURITY, 0, 5, NULL );
    WaitForSingleObject( hSem, 100 );
    /* код завершения WAIT_TIMEOUT */
    ReleaseSemaphore( (HANDLE)dwData, 1, &lPrev );
    WaitForSingleObject( hSem, 100 );
    /* код завершения WAIT_OBJECT_0 */
    WaitForSingleObject( hSem, 100 );
    /* код завершения WAIT_TIMEOUT */
    ReleaseSemaphore( (HANDLE)dwData, 2, &lPrev );
    WaitForSingleObject( hSem, 100 );
    /* код завершения WAIT_OBJECT_0 */
    WaitForSingleObject( hSem, 100 );
    /* код завершения WAIT_OBJECT_0 */
    WaitForSingleObject( hSem, 100 );
    /* код завершения WAIT_TIMEOUT */
    CloseHandle( hSem );
    return 0;
    }
    В примере создается семафор в первоначально занятом состоянии,
    поэтому первое ожидание завершается по таймауту. После этого его счетчик увеличивается на 1, и следующее ожидание завершается успехом, при
    этом счетчик семафора уменьшается и он снова становится занятым. В результате третье ожидание завершается по таймауту. После этого счетчик
    увеличивается на 2, и два последующих ожидания завершаются успехом,
    после чего счетчик оказывается снова нулевым и третье ожидание завершается по таймауту.

    #define DEFAULT_SECURITY (LPSECURITY_ATTRIBUTES)NULL

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

    240

    241

    (LPSECURITY_ATTRIBUTES)NULL
    unsigned __stdcall TProc( void *pdata )
    {
    WaitForSingleObject( hObject, 2000 );
    WaitForSingleObject( hObject, 2000 );
    Sleep( 1000 );
    ReleaseMutex( (HANDLE)pdata );
    ReleaseMutex( (HANDLE)pdata );
    return 0;
    }

    #include
    #include
    #define DEFAULT_SECURITY

    LONG lCounter;
    lCounter = 0;
    if ( WaitForSingleObject( hSem, 0 ) == WAIT_OBJECT_0 )
    ReleaseSemaphore( hSem, 1, &lCounter );
    /* теперь переменная lCounter содержит значение счетчика */
    Семафоры предназначены для ограничения числа потоков, имеющих одновременный доступ к какому-либо ресурсу.
    Мьютексы
    Объекты исключительного владения могут быть использованы в одно время не более чем одним потоком. В этом отношении мьютексы подобны критическим секциям, с той оговоркой, что работа с ними выполняется в режиме ядра (при использовании критических секций переход в
    режим ядра необязателен) и что мьютексы могут быть использованы для
    межпроцессного взаимодействия, тогда как критические секции реализованы для применения внутри процесса.
    Для захвата мьютекса используется ожидающая функция WaitFor..., а
    для освобождения – функция ReleaseMutex. При создании мьютекса функцией CreateMutex можно указать, чтобы он создавался сразу в занятом состоянии:

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

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    int main( void )
    {
    unsigned id;
    HANDLE
    hMutex, hThread;
    hMutex = CreateMutex( DEFAULT_SECURITY, TRUE, NULL );
    hThread = (HANDLE)_beginthreadex(
    (void*)0, 0, TProc, (void*)hMutex, 0, &id
    );
    Sleep( 1000 );
    ReleaseMutex( hMutex );
    WaitForSingleObject( hThread, INFINITE );
    CloseHandle( hThread );
    CloseHandle( hMutex );
    return 0;
    }
    ОС Windows предоставляет достаточно удобный набор объектов,
    пригодных для синхронизации. Однако, в большинстве случаев эти объекты эффективны с точки зрения операционной системы, представляя самые базовые примитивы. В реальных задачах часто возникают ситуации,
    когда необходимо создавать на основе этих примитивов более сложные
    составные синхронизирующие объекты. Достаточно распространенными
    примерами таких задач являются задачи с барьерной синхронизацией или
    задачи с множественным доступом к ресурсу по чтению и исключительным доступом по записи.
    При реализации барьерной синхронизации надо обеспечить не только возможность проконтролировать достижение барьера всеми потоками,
    но также снова «поставить барьер» сразу после того, как потоки начнут
    свое выполнение (иначе какой-либо поток может быстро справиться со
    своей работой и снова придет к барьеру, пока тот еще открыт). При этом
    потоки, прошедшие барьер, могут начинать свое выполнение со значительной задержкой.
    Синхронизирующие объекты, обслуживающие ресурс с множественным доступом по чтению и исключительным по записи, должны отслеживать число потоков, осуществляющих чтение данного ресурса, и потоки,
    требующие изменения ресурса. Реализация такого объекта должна предусматривать работу в условиях высокой нагрузки – когда несколько потоков
    могут одновременно считывать ресурс и при этом не возникает пауз, когда
    ресурс является свободным; при этом к тому же ресурсу должны обращаться изменяющие его потоки, требуя при этом исключительного доступа.
    Стандартных типов объектов, решающих такие задачи, в Windows
    нет.

    242

    243

    Процессы в Windows определяют адресное пространство, которое будет использоваться всеми потоками, работающими в этом процессе. В отличие от UNIX-подобных систем системного вызова типа fork в Windows
    не предусмотрено – новый процесс создается заново, с выделением для
    него адресного пространства, проецирования на него компонент системы,
    образа исполняемого файла, необходимых динамических библиотек и
    других компонент. Эта операция требует чуть больше ресурсов, чем в
    UNIX-подобных системах, однако выполняется достаточно быстро – диспетчер памяти позволяет просто проецировать на адресное пространство
    компоненты из других процессов с режимом «копирование при записи».
    Основные механизмы взаимодействия процессов могут быть разделены на несколько групп:

    7.2.2 Процессы

    int main()
    {
    HANDLE
    hTimer = NULL;
    LARGE_INTEGER
    liDueTime;
    hTimer = CreateWaitableTimer(NULL, TRUE, “WaitableTimer”);
    /* задать срабатывание через 5 секунд */
    liDueTime.QuadPart=-50000000;
    SetWaitableTimer( hTimer, &liDueTime, 0, NULL, NULL, 0 );
    WaitForSingleObject( hTimer, INFINITE );
    return 0;
    }
    Таймеры могут служить в качестве синхронизирующих объектов, как
    в данном примере, а могут вызывать указанную разработчиком функцию,
    если поток в нужное время находится в ожидании оповещения.
    Следует подчеркнуть, что ожидающие таймеры обладают ограниченной точностью работы. При необходимости точно планировать время выполнения (например, в случае обработки потоков мультимедиа данных) надо использовать специальный таймер, предназначенный для работы с мультимедиа (см. функции timeGetSystemTime, timeBeginPeriod и др.).

    #include
    #include

    7.2.1.4. Ожидающие таймеры
    Эти объекты предназначены для выполнения операций через заданные промежутки времени или в заданное время. Таймеры бывают периодическими или однократными, также их разделяют на таймеры с ручным
    сбросом и синхронизирующие:

    Разработка параллельных приложений для ОС Windows

    • Использование объектов ядра для взаимной синхронизации. Рассмотрено при обсуждении взаимной синхронизации потоков.
    При использовании именованных объектов или передаче описателей объектов ядра другим процессам рассмотренные средства
    могут использоваться для межпроцессной синхронизации.
    • Проецирование файлов в адресное пространство процесса (File
    Mapping). Один из базовых механизмов, рассматривается ниже.
    • Использование файловых объектов. Каналы (Pipes), почтовые
    ящики (Mailslots) и сокеты (Sockets). Еще один базовый механизм; чаще применяется для организации межузлового взаимодействия, за исключением анонимных каналов (unnamed pipes,
    anonymous pipes), которые используются для межпроцессного
    взаимодействия в рамках одного узла. В данном курсе эти механизмы не затрагиваются.
    • Механизмы, ориентированные на обмен оконными сообщениями (буфер обмена, DDE, сообщение WM_COPYDATA и др.). В своей основе используют механизм проецирования файлов для передачи данных между адресными пространствами процессов.
    В данном курсе эти механизмы не затрагиваются.
    • Вызов удаленных процедур (Remote Procedure Call, RPC). Является надстройкой, использующей проецирование для реальной
    передачи данных. Позволяет описать процедуры, реализованные в других процессах, и обращаться к ним как к обычным
    процедурам, локальным для данного процесса. RPC инкапсулирует вопросы нахождения реальной процедуры, выполняющей
    необходимую работу, передачу данных в эту процедуру и получение от нее ответа. RPC позволяет организовать не только межпроцессное взаимодействие, но также межузловое с передачей
    данных по сети. В данном курсе не рассматривается.
    • COM. Является еще более высокоуровневой абстракцией, в данном курсе также не рассматривается.

    CIL и системное программирование в Microsoft .NET

    7.2.2.1. Создание процессов
    Для создания процессов используются функции CreateProcess,
    CreateProcessAsUser, CreateProcessWithLogonW и CreateProcessWithTokenW.
    Функция CreateProcess создает новый процесс, который будет исполняться от имени текущего пользователя потока, вызвавшего эту функцию.
    Функция CreateProcessAsUser позволяет запустить процесс от имени другого пользователя, который идентифицируется его маркером безопасности (security token); однако вызвавший эту функцию поток должен принять
    меры к правильному использованию реестра, так как профиль нового
    пользователя не будет загружен. Функции CreateProcessWithTokenW и

    244

    245

    (LPSECURITY_ATTRIBUTES)NULL

    7.2.2.2. Адресное пространство процесса и проецирование файлов
    В Windows потоки работают в одном адресном пространстве процесса, поэтому обмен данными между ними в большинстве случаев сводится
    к доступу к общей разделяемой памяти. Иная ситуация в случае взаимодействия потоков, принадлежащим разным процессам: так как адресное
    пространство процессов изолировано друг от друга, то приходится прини-

    int main( void )
    {
    STARTUPINFO
    si;
    PROCESS_INFORMATION pi;
    memset( &si, 0, sizeof(si) );
    memset( &pi, 0, sizeof(pi) );
    si.cb = sizeof(si);
    CreateProcess(
    NULL, “cmd.exe”, DEFAULT_SECURITY, DEFAULT_SECURITY,
    FALSE, NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi
    );
    CloseHandle( pi.hThread );
    WaitForSingleObject( pi.hProcess, INFINITE );
    CloseHandle( pi.hProcess );
    return 0;
    }
    При создании процесса ему можно передать описатели каналов
    (CreatePipe), предназначенные для перенаправления stdin, stdout и stderr.
    Описатели каналов должны быть наследуемыми.
    Для завершения процесса рекомендуется применять функцию
    ExitProcess, которая завершит процесс, сделавший этот вызов. В крайних
    случаях можно использовать функцию TerminateProcess, которая может
    завершить процесс, заданный его описателем. Этой функцией пользоваться не рекомендуется, так как при таком завершении разделяемые библиотеки будут удалены из адресного пространства уничтожаемого процесса
    без предварительных уведомлений – это может привести в некоторых случаях к утечке ресурсов.

    #include
    #define DEFAULT_SECURITY

    CreateProcessWithLogonW позволяют при необходимости загрузить профиль пользователя и, кроме того, функция CreateProcessWithLogonW сама
    получает маркер пользователя по известному учетному имени, домену и
    паролю:

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    мать специальные меры для передачи данных из адресного пространства
    одного процесса в адресное пространство другого.
    Доступная пользователю часть адресного пространства процесса выделяется реально не в физической оперативной памяти, а в файлах, данные которых проецируются на соответствующие фрагменты адресного
    пространства. Выделение памяти без явного указания проецируемого
    файла приведет к тому, что область для проецирования будет автоматически выделяться в файле подкачки страниц. Физическая оперативная память в значительной степени является кэшем для данных файлов. Выделение физической оперативной памяти применяется очень редко, например, средствами расширения адресного пространства (Address Windowing
    Extension, AWE), позволяющими использовать более чем 4Г адресного
    пространства в 32-х разрядном приложении Win32.
    Важная особенность средств управления адресным пространством и
    проецированием файлов – они используют так называемую гранулярность
    выделения ресурсов (ее можно узнать с помощью функции GetSystemInfo);
    в современных версиях Win32 гранулярность составляет 64К. Это означает,
    что если вы попробуете спроецировать в память файл размером 100 байт, то
    в адресном пространстве будет занят фрагмент в 65536 байт длиной, из которого реально будут заняты только первые 100 байт.
    Для управления адресным пространством предназначены функции
    VirtualAlloc, VirtualFree, VirtualAllocEx, VirtualFreeEx, VirtualLock и
    VirtualUnlock. С их помощью можно резервировать пространство в адресном пространстве процесса (без передачи физической памяти из файла) и
    управлять передачей памяти из файла подкачки страниц указанному диапазону адресов.
    Механизмы проецирования файлов в память различаются для обычных и для исполняемых файлов. Функции создания процессов
    (CreateProcess...) и загрузки библиотек (LoadLibrary, LoadLibraryEx), помимо специфичных действий, выполняют проецирование исполняемых
    файлов в адресное пространство процесса; при этом учитывается их деление на сегменты, наличие секций импорта, экспорта и релокаций и др.
    Обычные файлы проецируются как непрерывный блок данных на непрерывный диапазон адресов.
    Для явного проецирования файлов используется специальный объект ядра проекция файла (file mapping object). Этот объект предназначен для
    описания файла, который может быть спроецирован в память, но реального отображения файла или его части в память при создании проекции
    не происходит. Описатель объекта «проекция файла» можно получить с
    помощью функций CreateFileMapping и OpenFileMapping. Для проецирования файла или его части в память предназначены функции MapViewOfFile,
    MapViewOfFileEx и UnmapViewOfFile:

    246

    247

    7.2.2.3. Межпроцессное взаимодействие с использованием проецирования файлов
    Проецирование файлов используется для создания разделяемой памяти: для этого один процесс должен создать объект «проекция файла», а
    другой – открыть его. После этого система будет гарантировать когерентность данных в этой проекции во всех процессах, которые ее используют,
    хотя проекции могут размещаться в разных диапазонах адресов.
    В примере ниже приводится текст двух приложений (first.cpp и second.cpp), которые обмениваются между собой данными через общий объект «проекция файла»:
    /* FIRST.CPP */
    #include
    #define DEFAULT_SECURITY (LPSECURITY_ATTRIBUTES)NULL
    int main( void )
    {
    HANDLE
    hMapping;
    LPVOID
    pMapping;
    STARTUPINFO
    si;
    PROCESS_INFORMATION
    pi;

    #include
    #define DEFAULT_SECURITY (LPSECURITY_ATTRIBUTES)NULL
    int main( void )
    {
    HANDLE hFile, hMapping;
    LPVOID pMapping;
    LPSTR
    p;
    int
    i;
    hFile = CreateFile(
    “abc.dat”, GENERIC_WRITE|GENERIC_READ,
    FILE_SHARE_WRITE, DEFAULT_SECURITY,
    CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL
    );
    hMapping = CreateFileMapping(
    hFile, DEFAULT_SECURITY, PAGE_READWRITE, 0, 256, NULL
    );
    pMapping = MapViewOfFile( hMapping, FILE_MAP_WRITE, 0,0, 0 );
    for ( p = (LPSTR)pMapping, i=0; i<256; i++ ) *p++ = (char)i;
    UnmapViewOfFile( hMapping );
    CloseHandle( hMapping );
    CloseHandle( hFile );
    return 0;
    }

    Разработка параллельных приложений для ОС Windows

    248

    /* SECOND.CPP */
    #include
    int main( int ac, char **av )
    {
    HANDLE
    hMapping;
    LPVOID
    pMapping;
    int
    *p;
    int
    i;
    hMapping = OpenFileMapping(
    FILE_MAP_READ, FALSE, “FileMap-AB-874436342”
    );
    pMapping = MapViewOfFile( hMapping, FILE_MAP_READ, 0,0, 0 );
    for ( p = (int*)pMapping, i=0; i<256; i++ )
    if ( *p++ != i ) break;
    if ( i != 256 ) { /* ОШИБКА! */ }
    UnmapViewOfFile( hMapping );
    CloseHandle( hMapping );
    return 0;
    }

    }

    int
    *p;
    int
    i;
    memset( &si, 0, sizeof(si) );
    memset( &pi, 0, sizeof(pi) );
    si.cb = sizeof(si);
    hMapping = CreateFileMapping(
    INVALID_HANDLE_VALUE, DEFAULT_SECURITY, PAGE_READWRITE,
    0, 1024, “FileMap-AB-874436342”
    );
    pMapping = MapViewOfFile( hMapping, FILE_MAP_WRITE, 0,0, 0 );
    CreateProcess(
    NULL, “second.exe”, DEFAULT_SECURITY,
    DEFAULT_SECURITY, FALSE, NORMAL_PRIORITY_CLASS,
    NULL, NULL, &si, &pi);
    CloseHandle( pi.hThread );
    for (p=(int*)pMapping,i=0; i<256; i++) *p++=i;
    WaitForSingleObject( pi.hProcess, INFINITE );
    CloseHandle( pi.hProcess );
    UnmapViewOfFile( hMapping );
    CloseHandle( hMapping );
    return 0;

    CIL и системное программирование в Microsoft .NET

    249

    (LPSECURITY_ATTRIBUTES)NULL

    int main( int ac, char **av )
    {
    STARTUPINFO
    si;
    PROCESS_INFORMATION
    pi;
    int
    i;
    memset( &si, 0, sizeof(si) );
    memset( &pi, 0, sizeof(pi) );

    #pragma section(“SHRD_DATA”,read,write,shared)
    __declspec(allocate(“SHRD_DATA”)) int shared[ 256 ];

    /* FSEC.CPP */
    #include
    #define DEFAULT_SECURITY

    7.2.2.4. Межпроцессное взаимодействие с использованием общих секций
    Еще одна разновидность работы с разделяемыми данными посредством проецирования файлов связана с объявлением специальных разделяемых сегментов в приложении – такие сегменты будут общими для всех
    копий этого приложения. В своей основе такой способ является частным
    случаем проецирования – с той оговоркой, что проецируется исполняемый файл (или разделяемая библиотека) и управление проекциями осуществляется декларативным способом.
    В Microsoft Visual C++ для объявления разделяемого сегмента и помещаемых в него данных используются директивы #pragma и __declspec,
    как показано в примере ниже:

    Важно отметить, что если разные процессы откроют один и тот же
    файл, а затем каждый создаст свой собственный объект «проекция файла»,
    то система не будет гарантировать когерентности данных в проекциях; когерентность обеспечивается только в рамках одного объекта «проекция файла». В некоторых случаях можно воспользоваться функцией FlushViewOfFile
    для явного сброса данных из оперативной памяти в файл, что, однако, еще
    не гарантирует автоматического обновления данных в других проекциях.
    Именно механизм проецирования файлов является базовым средством для передачи данных между адресными пространствами процессов.
    Он является основой для построения многих других средств межпроцессного взаимодействия. Так, например, обмен оконными сообщениями (если в сообщении содержится указатель на данные) между разными процессами приводит к тому, что система создает внутренний объект «проекция»,
    помещает данные в него, проецирует на второй процесс и посылает сообщение получателю с указателем на проекцию в процессе-получателе.

    Разработка параллельных приложений для ОС Windows

    WaitForSingleObject( pi.hProcess, INFINITE );
    CloseHandle( pi.hProcess );
    } else {
    /* второй экземляр с общими данными */
    for ( i = 0; i < 256; i++ )
    if ( shared[i] != 256-i ) break;
    if ( i != 256 ) { /* ОШИБКА! */ }
    }
    return 0;

    si.cb = sizeof(si);
    if ( ac != 2 || strcmp( av[1], “slave” ) ) {
    for ( i = 0; i < 256; i++ ) shared[i] = 256-i;
    /* первый экземляр с общими данными */
    CreateProcess(
    NULL, “fsec.exe slave”, DEFAULT_SECURITY,
    DEFAULT_SECURITY, FALSE, NORMAL_PRIORITY_CLASS,
    NULL, NULL, &si, &pi
    );
    CloseHandle( pi.hThread );

    CIL и системное программирование в Microsoft .NET

    Реализация параллельного выполнения кода в .NET основана на базовых механизмах, предоставляемых ядром операционной системы
    Windows. Аналогично средствам операционной системы средства .NET
    Framework могут быть разделены на следующие группы:
    • Обработка асинхронных запросов. Сюда относятся средства для
    выполнения асинхронных операций ввода-вывода, средства для
    работы с очередями сообщений, некоторые надстройки для работы в сетевой среде (ASP, XML и др.) и средства поддержания
    инфраструктуры COM объектов.
    • Организация многопоточных приложений. Сюда относятся средства создания потоков, управления ими, включая пулы потоков,

    7.3. Параллельные операции в .NET

    }
    Некоторое неудобство, связанное с необходимостью использовать одно и то же приложение, можно легко обойти, если разделяемый сегмент поместить в разделяемую библиотеку – тогда один и тот же образ библиотеки
    может быть подключен к разным приложениям, что позволит им обмениваться данными. В этом варианте надо только учитывать, что адреса, в которые будет помещена библиотека, в разных процессах могут отличаться.

    250

    251

    Потоки в .NET реализованы на основе модели потоков операционной системы и не предусматривают средств управления волокнами. Если
    система не поддерживает многопоточные приложения, то PAL должен будет реализовать собственный планировщик потоков режима пользователя
    (и в этом случае окажется, что потоки .NET являются аналогами волокон,
    а не потоков ядра).

    7.3.1. Потоки и пул потоков

    средства взаимной синхронизации, а также организация локальной для потоков памяти. По большей части .NET Framework предоставляет надстройку над средствами операционной системы.
    • Работа с различными процессами и доменами приложений.
    В основном это надстройки над абстракциями высокого уровня,
    такими как RPC, COM объекты и пр.
    Как и в случае API системы, такое деление не является строгим – реальные средства «пересекают» границы этих категорий. При этом не все механизмы, предоставляемые операционной системой, нашли свое отражение
    в .NET Framework; равно как многие механизмы, оставаясь внешне схожими с механизмами операционной системы, существенным образом изменились. Так, например, .NET не поддерживает волокна; операции асинхронного ввода-вывода основываются на использовании отдельных потоков,
    выполняющих фоновые синхронные операции ввода-вывода, и др.
    Вообще говоря, использование фоновых потоков для выполнения
    специфических задач поддержки инфраструктуры .NET стало общим местом. При запуске .NET приложения автоматически создается пул потоков,
    используемый CLR по мере надобности. Этот пул применяется, в частности, в ситуациях, связанных с обработкой асинхронных запросов и асинхронного ввода-вывода, когда в рамках операционной системы реально использовался бы основной поток в состоянии ожидания оповещения.
    .NET разрабатывался с учетом возможности переноса на другие платформы. Для облегчения этого процесса в архитектуру CLR включен специальный уровень адаптации к платформе (PAL, Platform Adaptation Layer),
    являющийся прослойкой между основными механизмами CLR и уровнем
    операционной системы. В случае платформы Windows уровень PAL достаточно прост – считается, что PAL должен предоставить для CLR функциональность, аналогичную Win32 API. Однако в случае иных платформ PAL
    может оказаться достаточно сложным и многоуровневым. Например, в
    случае платформ, не поддерживающих многопоточные приложения, PAL
    должен самостоятельно реализовать недостающую функциональность.
    В данном курсе рассматриваются основные средства реализации
    многопоточных приложений и не затрагиваются вопросы создания ASP,
    COM-объектов и многого другого.

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    m_size = 600;
    m_stripsize = 50;
    m_stipmax = 12;
    m_stripused = 0;
    m_A = new double[m_size,m_size],
    m_B = new double[m_size,m_size],
    m_C = new double[m_size,m_size];
    public static void ThreadProc()
    {
    int i,j,k, from, to;
    from = ( m_stripused++ ) * m_stripsize;
    to = from + m_stripsize;
    if ( to > m_size ) to = m_size;
    for ( i = 0; i < m_size; i++ ) {
    for ( j = 0; j < m_size; j++ ) {

    namespace TestNamespace {
    class TestApp {
    const int
    const int
    const int
    private static int
    private static double[,]

    using System;
    using System.Threading;

    Основные классы для реализации многопоточных приложений определены в пространстве имен System.Threading. Для описания собственных
    потоков предназначен класс Thread. При создании потока ему необходимо
    указать делегата, реализующего процедуру потока. К сожалению, в .NET,
    во-первых, не предусмотрено передачи аргументов в эту процедуру, вовторых, процедура должна быть статическим методом, и в-третьих, класс
    Thread является опечатанным. В результате передача каких-либо данных в
    процедуру потока вызывает определенные трудности и требует явного или
    косвенного использования статических полей, что не слишком удобно,
    зачастую нуждается в дополнительной синхронизации и плохо соответствует парадигме ООП.
    Приведенный ниже пример демонстрирует работу с созданием
    нескольких потоков для параллельного перемножения двух квадратных
    матриц. Начальные значения всех элементов матриц равны 1, поэтому результирующая матрица должна быть заполнена числами, равными размерности перемножаемых матриц. В процессе умножения и суммирования
    элементов матриц синхронизация не выполняется, поэтому при достаточно большом размере матриц гарантированно будут возникать ошибки (необходимо синхронизировать выполнение некоторых действий в потоках,
    чтобы избежать возникновения ошибок, – об этом ниже, при рассмотрении средств синхронизации):

    252

    }

    253

    }
    }
    Поток в .NET может находиться в одном из следующих состояний:
    незапущенном, исполнения, ожидания, приостановленном, завершенном
    и прерванном. Возможные переходы между этими состояниями изображены на рис. 7.1.
    Сразу после создания и до начала выполнения потока он находится в
    незапущенном состоянии (Unstarted). Текущее состояние можно определить с помощью свойства Thread.ThreadState. После запуска поток можно
    перевести в состояние исполнения (Running) вызовом метода Thread.Start.
    Работающий поток может быть переведен в состояние ожидания
    (WaitSleepJoin) явным или неявным вызовом соответствующих методов
    (Thread.Sleep, Thread.Join и др.) или приостановлен (Suspended) с помощью
    метода Thread.Suspend(). Исполнение приостановленного потока можно

    }
    public static void Main()
    {
    Thread[] T = new Thread[ m_stripmax ];
    int
    i,j,errs;
    for ( i = 0; i < m_size; i++ ) {
    for ( j = 0; j < m_size; j++ ) {
    m_A[i,j] = m_B[i,j] = 1.0;
    m_C[i,j] = 0.0;
    }
    }
    for ( i = 0; i < m_stripmax; i++ ) {
    T[i] = new Thread(new ThreadStart(ThreadProc));
    T[i].Start();
    }
    // дожидаемся завершения всех потоков
    for ( i = 0; i < m_stripmax; i++ ) T[i].Join();
    // проверяем результат
    errs = 0;
    for ( i = 0; i < m_size; i++ )
    for ( j = 0; j < m_size; j++ )
    if ( m_C[i,j] != m_size ) errs++;
    Console.WriteLine(“Error count = {0}”, errs );
    }

    }

    for ( k = from; k < to; k++ )
    m_C[i,j] += m_A[i,k] * m_B[k,j];

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    Aborted

    Stopped

    Sleep(),
    Join() и
    переход к
    ожиданию

    WaitSleepJoin

    Interrupt() и завершение ожидания

    Resume()

    Suspended

    Завершение функции потока нормальным образом переводит поток
    в состояние «завершен» (Stopped), а досрочное прекращение работы вызовом метода Thread.Abort переведет его в состояние «прерван» (Aborted).
    Кроме того, .NET поддерживает несколько переходных состояний
    (AbortRequested, StopRequested и SuspendRequested). Состояния потока в
    общем случае могут комбинироваться, например, вполне корректно сочетание состояния ожидания (WaitSleepJoin) и какого-либо переходного,
    скажем, AbortRequested.
    Для выполнения задержек в ходе выполнения потока предназначены
    два метода – Sleep, переводящий поток в состояние ожидания на заданное
    время, и SpinWait, который выполняет некоторую задержку путем многократных повторов внутреннего цикла. Этот метод дает высокую загрузку
    процессора, однако позволяет реализовать очень короткие паузы. К сожалению, продолжительность пауз зависит от производительности и загруженности процессора.
    Для получения и задания приоритета потока используется свойство
    Thread.Priority. Приоритеты потока в .NET базируются на подмножестве

    Рис. 7.1. Состояния потока

    Уничтожение
    объекта Thread

    Abort()

    Suspend()

    return

    Running

    Start()

    Unstarted

    Создание объекта
    Thread

    возобновить вызовом метода Thread.Resume. Также можно досрочно вывести поток из состояния ожидания вызовом метода Thread.Interrupt.

    254

    255

    Для реализации асинхронного ввода-вывода в .NET предназначен
    абстрактный класс System.IO.Stream. В этом классе определены абстрактные синхронные методы чтения Read и записи Write, а также реализация
    асинхронных методов BeginRead, EndRead, BeginWrite и EndWrite. Асинхронные методы реализованы с помощью обращения к синхронным операциям фоновыми потоками пула.
    На основе абстрактного класса Stream в .NET Framework реализуются потомки, осуществляющие взаимодействие с разного рода потоками
    данных. Так, например, System.IO.FileStream реализует операции с файлами, System.IO.MemoryStream предоставляет возможность использования

    7.3.2. Асинхронный ввод-вывод

    относительных приоритетов Win32 API так, что при переносе на другие
    платформы существует возможность предоставить их корректные аналоги. В .NET используются приоритеты Highest, AboveNormal, Normal,
    BelowNormal и Lowest.
    Когда .NET приложение начинает исполняться в среде Windows,
    CLR создает внутренний пул потоков, используемый средой для реализации асинхронных операций ввода-вывода, вызова асинхронных процедур,
    обработки таймеров и других целей. Потоки могут добавляться в пул по
    мере надобности. Этот пул реализуется на основе пула потоков, управляемого операционной системой (построенного на основе порта завершения
    ввода-вывода). Для взаимодействия с пулом потоков предусмотрен класс
    ThreadPool, и единственный объект, принадлежащий этому классу, создается CLR при запуске приложения. Все домены приложений в рамках одного процесса используют общий пул потоков.
    Разработчики могут использовать несколько статических методов
    класса ThreadPool. Так, например, существует возможность связать внутренний порт завершения ввода-вывода с файловым объектом, созданным
    неуправляемым кодом, для обработки событий, связанных с завершением
    ввода-вывода этим объектом (см. методы ThreadPool.BindHandle и описание порта завершения ввода-вывода в главе 7.1.1). Можно управлять числом потоков в пуле (методы GetAvailableThreads, GetMaxThreads,
    GetMinThreads и SetMinThreads), можно ставить в очередь асинхронных вызовов собственные процедуры (метод QueueUserWorkItem) и назначать процедуры, которые будут вызываться при освобождении какого-либо объекта (метод RegisterWaitForSingleObject). Эти два метода имеют «безопасные» и «небезопасные» (Unsafe...) версии; последние отличаются тем, что
    в стеке вызовов асинхронных методов не будут присутствовать данные о
    реальном контексте безопасности потока, поставившего в очередь этот
    вызов, – в подобном случае будет использоваться контекст безопасности
    самого пула потоков.

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    m_size = 100000000;
    m_data = new byte [m_size];

    }
    }
    Данный пример демонстрирует использование FileStream для выполнения асинхронной операции записи большого объема данных.

    public static void DoneWritting( IAsyncResult state ) {
    }
    static void Main(string[] args) {
    TestApp
    ta = new TestApp();
    IAsyncResult state;
    Stream
    st = new FileStream(
    “test.dat”, FileMode.OpenOrCreate,
    FileAccess.ReadWrite, FileShare.Read, 1, true
    );
    state = st.BeginWrite(
    m_data, 0, m_size,
    new AsyncCallback(DoneWritting), null
    );
    // код в этом месте будет выполняться
    // одновременно с выводом данных
    st.EndWrite( state );
    st.Close();
    }

    public TestApp()
    {
    int i;
    for ( i = 0; i < m_size; i++ ) m_data[i] = (byte)i;
    }

    using System;
    using System.IO;
    namespace TestNamespace {
    class TestApp {
    private const int
    private static byte[]

    байтового массива в качестве источника или получателя данных,
    System.IO.BufferedStream является «надстройкой» над другими объектами,
    производными от System.IO.Stream, и обеспечивает буферизацию запросов чтения и записи. Некоторые классы вне пространства имен Sytem.IO
    также являются потомками Stream. Так, например, класс
    System.NET.Sockets.NetworkStream обеспечивает сетевое взаимодействие:

    256

    257

    }

    }

    public static void Main()
    {
    GreetingData gd = new GreetingData(“Hello, world!”);
    ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncProc), gd);
    Thread.Sleep( 1000 );
    }

    class TestApp {
    static void AsyncProc( Object arg )
    {
    GreetingData gd = (GreetingData)arg;
    gd.Invoke();
    }

    using System;
    using System.Threading;
    namespace TestNamespace {
    class GreetingData {
    private string
    m_greeting;
    public GreetingData( string text ) { m_greeting = text; }
    public void Invoke() { Console.WriteLine( m_greeting ); }
    }

    Для реализации вызова асинхронных процедур в .NET используются
    фоновые потоки пула, так же как для обработки асинхронных операций
    ввода-вывода. Класс ThreadPool предлагает два способа для вызова асинхронных процедур: явное размещение вызовов в очереди
    (QueueUserWorkItem) и связывание вызовов с переводом некоторых объектов в свободное состояние (RegisterWaitForSingleObject). Кроме того,
    .NET позволяет осуществлять асинхронные вызовы любых процедур с помощью метода BeginInvoke делегатов.
    Статический метод ThreadPool.QueueUserWorkItem ставит вызов указанной процедуры в очередь для обработки. Если пул содержит простаивающие потоки, то обработка этой функции начнется немедленно:

    7.3.3. Асинхронные процедуры

    При реализации собственных потомков класса Stream, возможно, будет
    иметь смысл переопределить не только абстрактные методы Read и Write, но
    также некоторые базовые (например, BeginRead, ReadByte и др.), универсальная реализация которых может быть неэффективной в конкретном случае.

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    namespace TestNamespace {
    class GreetingData {
    private string
    m_greeting;
    private RegisteredWaitHandle
    m_waithandle;
    public GreetingData( string text ) { m_greeting = text; }
    public void Invoke() { Console.WriteLine( m_greeting ); }
    public RegisteredWaitHandle WaitHandle {
    set {
    if (value==null) m_waithandle.Unregister( null );
    m_waithandle = value;
    }
    }
    }
    class TestApp {
    static void AsyncProc( Object arg, bool isTimeout ) {
    GreetingData gd = (GreetingData)arg;
    if ( !isTimeout ) gd.WaitHandle = null;
    gd.Invoke();
    }
    public static void Main() {
    GreetingData
    gd = new GreetingData(“Hello”);
    AutoResetEvent
    ev = new AutoResetEvent(false);
    gd.WaitHandle=ThreadPool.RegisterWaitForSingleObject(
    ev, new WaitOrTimerCallback(AsyncProc),

    using System;
    using System.Threading;

    При постановке в очередь асинхронного вызова можно указать объект, который является аргументом асинхронной процедуры (при создании
    собственных потоков передача аргументов процедуре потока затруднительна).
    Второй способ вызова асинхронных процедур связан с использованием
    объектов, производных от класса System.Threading.WaitHandle (это события
    и мьютексы). При этом вызов асинхронной процедуры связывается с переводом объекта в свободное состояние. Данный метод может быть использован также для организации повторяющегося через определенные интервалы
    вызова асинхронных процедур – при регистрации делегата указывается максимальный интервал ожидания, и если он исчерпывается, то вызов размещается в очереди пула, даже если объект остался занятым. Если объект попрежнему остается занятым, то вызов процедуры будет периодически размещаться в очереди после исчерпания каждого интервала ожидания.

    258

    }

    259

    namespace TestNamespace {
    public class GreetingData {
    private string
    m_greeting;
    public GreetingData( string text ) { m_greeting = text; }
    public static void Invoke( GreetingData arg ) {
    Console.WriteLine( arg.m_greeting );
    }
    }

    using System;
    using System.Threading;

    }
    Приведенный пример демонстрирует использование периодического
    вызова асинхронной процедуры – при регистрации делегата
    (RegisterWaitForSingleObject) указывается максимальное время ожидания
    1 секунда (1000 миллисекунд), после чего основной поток переводится в
    состояние «спячки» на 2.5 секунды. За это время в очередь пула поступает
    два вызова асинхронных процедур (с признаком вызова по тайм-ауту). Через 2.5 секунды основной поток пробуждается, переводит событие в свободное состояние, и в очередь пула поступает третий вызов. При обработке этого вызова регистрация делегата отменяется.
    Последний способ связан с использованием методов BeginInvoke и
    EndInvoke делегатов. Когда определяется какой-либо делегат функции, для
    него будут определены методы: BeginInvoke (содержащий все аргументы
    делегата плюс два дополнительных – AsyncCallback, который может быть
    вызван по завершении обработки асинхронного вызова, и AsyncState, с
    помощью которого можно определить состояние асинхронной процедуры) и EndInvoke, содержащий все выходные параметры (т.е. описанные как
    inout или out), плюс IAsyncResult, позволяющий узнать результат выполнения процедуры.
    Таким образом, использование BeginInvoke позволяет не только поставить в очередь вызов асинхронной процедуры, но также связать с завершением ее обработки еще один асинхронный вызов. Метод EndInvoke
    служит для ожидания завершения обработки асинхронной процедуры:

    }

    gd, 1000, false
    );
    Thread.Sleep( 2500 );
    ev.Set();
    Console.ReadLine();

    Разработка параллельных приложений для ОС Windows

    public delegate void AsyncProcCallback ( GreetingData gd );
    class TestApp {
    public static void Main() {
    GreetingData gd = new GreetingData( “Hello!!!” );
    AsyncProcCallback apd = new AsyncProcCallback(
    GreetingData.Invoke );
    IAsyncResult ar = apd.BeginInvoke( gd, null, null );
    ar.AsyncWaitHandle.WaitOne();
    }
    }

    CIL и системное программирование в Microsoft .NET

    public static void ThreadProc()
    {

    7.3.4.1. Атомарные операции
    Платформа .NET предоставляет, аналогично базовой операционной
    системе Windows, набор некоторых основных операций над целыми числами
    (int и long), которые могут выполняться атомарно. Для этого предусмотрены четыре статических метода класса System.Threading.Interlocked, а именно Increment, Decrement, Exchange и CompareExchange. Применение этих методов аналогично соответствующим Interlocked... процедурам Win32 API.
    Возвращаясь к примеру использования потоков для умножения матриц, можно выделить один момент, требующий исправления: самое начало процедуры потока, там, где определяется номер полосы:

    Проблемы, встающие перед разработчиками многопоточных приложений .NET, очень похожи на проблемы разработчиков приложений Win32
    API. Соответственно, .NET предоставляет в значительной мере близкий
    набор средств взаимодействия потоков и их взаимной синхронизации.
    К этим средствам относятся атомарные операции, локальная для потока память, синхронизирующие примитивы и таймеры. Их применение в
    основе своей похоже на применение аналогичных средств API.

    7.3.4. Синхронизация и изоляция потоков

    }
    Данный пример иллюстрирует вызов асинхронной процедуры с использованием метода BeginInvoke и альтернативный механизм ожидания
    завершения – с использованием внутреннего объекта AsyncWaitHandle
    (класса WaitHandle), благодаря которому, собственно говоря, становится
    возможен вызов асинхронной процедуры, обслуживающей завершение
    обработки данной процедуры.
    В этом смысле асинхронный вызов процедур с помощью BeginInvoke
    очень близок к обработке асинхронных операций ввода-вывода.

    260

    261

    7.3.4.2. Синхронизация потоков
    Основные средства взаимной синхронизации потоков в .NET обладают заметным сходством со средствами операционной системы. Среди
    них можно выделить:
    • Мониторы, близкие к критическим секциям Win32 API.
    • События и мьютексы, имеющие соответствующие аналоги среди объектов ядра.
    • Плюс дополнительный достаточно универсальный синхронизирующий объект, обеспечивающий множественный доступ потоков по чтению и исключительный – по записи.
    Последний синхронизирующий объект ReaderWriterLock закрывает
    очень типичный класс задач синхронизации – для многих объектов является совершенно корректным конкурентный доступ для чтения и требуются исключительные права для изменения данных. Причина в том, что
    изменение сложных объектов осуществляется не атомарно, поэтому во
    время постепенного внесения изменений объект кратковременно пребывает в некорректном состоянии – при этом должен быть исключен
    не только доступ других потоков, пытающихся внести изменения, но даже
    потоков, осуществляющих чтение.
    Мониторы
    Мониторы в .NET являются аналогами критических секций в Win32
    API. Использование мониторов достаточно эффективно (это один из самых эффективных механизмов) и удобно настолько, что в .NET был предусмотрен механизм, который позволяет использовать практически любой объект, хранящийся в управляемой куче, для синхронизации доступа.

    public static void ThreadProc()
    {
    int
    i,j,k, from, to;
    from = (Interlocked.Increment(ref m_stripused) – 1 ) * m_stripsize;
    to = from + m_stripsize;
    ...

    int
    i,j,k, from, to;
    from = ( m_stripused++ ) * m_stripsize;
    to = from + m_stripsize;
    ...
    Здесь потенциально возможна ситуация, когда несколько потоков
    одновременно начнут выполнять этот код и получат идентичные номера
    полос. В этом месте самым эффективным было бы использование атомарных операций для увеличения значения поля m_stripused. Для этого фрагмент надо переписать:

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    Рис. 7.2. Использование кэша SyncBlock записей объектами управляемой кучи

    ...
    Данные объекта
    ...

    Индекс в SyncBlock кэше

    Информация об объекте

    Объект k

    ...
    Данные объекта
    ...

    ...

    SyncBlock[0] (пусто)
    SyncBlock[1] (используется)
    SyncBlock[2] (пусто)
    SyncBlock[3] (пусто)
    SyncBlock[4] (используется)
    SyncBlock[5] (пусто)
    ...

    Информация об объекте

    Индекс в SyncBlock (пусто)

    Кэш SyncBlock записей

    Информация о типах, таблицы
    указателей на методы и т.п.

    Внутренние данные CLR

    Объект j

    ...
    Данные объекта

    Индекс в SyncBlock кэше

    Информация об объекте

    Объект i

    Управляемая куча

    Для этого с каждым объектом ссылочного типа сопоставляется запись
    SyncBlock, являющаяся, по сути, аналогом структуры CRITICAL_SECTION в
    Win32 API. Добавление такой записи к каждому объекту в управляемой куче чересчур накладно, особенно если учесть, что используются они относительно редко. Поэтому все записи SyncBlock выносятся в отдельный
    кэш, а в информацию об объекте включается ссылка на запись кэша
    (см. рис. 7.2). Такой прием позволяет, с одной стороны, содержать кэш
    синхронизирующих записей минимального размера, а с другой – любому
    объекту при необходимости можно сопоставить запись.

    262

    263

    public static void ThreadProc()
    {
    int
    i,j,k, from, to;
    from = (Interlocked.Increment(ref m_stripused)–1)
    * m_stripsize;
    to = from + m_stripsize;
    if ( to > m_size ) to = m_size;
    for ( i = 0; i < m_size; i++ ) {
    for ( j = 0; j < m_size; j++ ) {
    for ( k = from; k < to; k++ )
    m_C[i,j] += m_A[i,k] * m_B[k,j];
    }
    }
    }
    Так как эта операция выполняется не атомарно, то вполне может быть
    так, что один поток считывает значение m_C[i,j], прибавляет к нему величину m_A[i,k] * m_B[k,j] и, прежде чем успевает записать в m_C[i,j] результат сложения, прерывается другим потоком. Второй поток успевает изменить величину m_C[i,j], потом первый снова пробуждается и записывает
    значение, вычисленное для предыдущего состояния элемента m_C[i,j], – то
    есть некорректную величину. Собственно говоря, именно эта ситуация и
    приводит к ошибкам, которые можно наблюдать в исходном примере.
    Ситуацию можно исправить, используя синхронизацию при доступе
    к элементу m_C[i,j] с помощью мониторов:

    Обычно объекты не имеют сопоставленной с ними SyncBlock записи, однако она автоматически выделяется при первом использовании
    монитора.
    Класс Monitor, определенный в пространстве имен System.Threading,
    предлагает несколько статических методов для работы с записями синхронизации. Методы Enter и Exit являются наиболее применяемыми и соответствуют функциям EnterCriticalSection и LeaveCriticalSection операционной системы. Аналогично критическим секциям Win32 API, мониторы могут использоваться одним потоком рекурсивно. Еще несколько методов класса Monitor – Wait, Pulse и PulseAll – позволяют при необходимости временно разрешить доступ к объекту другому потоку, ожидающему
    его освобождения, не покидая критической секции.
    Продолжим рассмотрение примера с многопоточным умножением
    матриц. Помимо уже рассмотренной проблемы с назначением полос, в
    процедуре потока есть еще одно некорректное место – прибавление накоплением к элементу результирующей матрицы произведения двух элементов исходной матрицы:

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    ...
    for ( j = 0; j < m_size; j++ ) {
    for ( k = from; k < to; k++ ) {
    Monitor.Enter( m_C );
    try {
    m_C[i,j] += m_A[i,k] * m_B[k,j];
    } finally {
    Monitor.Exit( m_C );
    }
    }
    }
    ...
    В этом фрагменте надо выделить два существенных момента: во-первых, использование метода Exit в блоке finally, а во-вторых – использование всего массива m_C, а не отдельного элемента m_C[i,j].
    Первое надо взять за правило, так как в случае возникновения исключения в критической секции блокировка может остаться занятой
    (т.е. в случае покидания секции без вызова метода Exit).
    Второе связано с тем, что элементы m_C[i,j] являются значениями, а
    не ссылочными типами. Для типов-значений соответствующее представление в управляемой куче не создается, и у них нет и не может быть ссылок на синхронизирующие записи SyncBlock.
    Самое плохое в этой ситуации то, что попытка собрать приложение,
    использующее типы-значения в качестве аргументов методов Enter и Exit
    (как в примере ниже), пройдет успешно:
    ...
    for ( j = 0; j < m_size; j++ ) {
    for ( k = from; k < to; k++ ) {
    Monitor.Enter( m_C[i,j] );
    try {
    m_C[i,j] += m_A[i,k] * m_B[k,j];
    } finally {
    Monitor.Exit( m_C[i,j] );
    }
    }
    }
    ...
    В прототипах методов Enter и Exit указано, что они должны получать
    ссылочный тип object; соответственно тип-значение будет упакован, и методу Enter будет передан свой экземпляр упакованного типа-значения, на
    который будет поставлена блокировка, а методу Exit – свой экземпляр, на
    котором блокировки никогда не было. Понятно, что все остальные потоки

    264

    265

    public static void ThreadProc()
    {
    int
    i,j,k, from, to;
    double R;
    from = (Interlocked.Increment(ref m_stripused) – 1) * m_stripsize;
    to = from + m_stripsize;

    Использование ключевого слова lock предпочтительно, так как при
    этом выполняется дополнительная синтаксическая проверка – попытка
    использовать для блокировки тип-значение приведет к диагностируемой
    компилятором ошибке, вместо трудно отлавливаемой ошибки во время
    исполнения:

    Monitor.Enter( obj ); try { ... }
    finally { Mointor.Exit( obj ); }

    эквивалентна

    lock ( obj ) { ... }

    будут создавать и множить свои собственные упакованные представления
    типов-значений, и никакой синхронизации не произойдет. Поэтому при
    использовании мониторов важно проследить, чтобы вызовы разных методов в разных потоках использовали один общий объект ссылочного типа.
    Можно выделить интересный момент – типы объектов сами являются экземплярами класса Type, и для них выделяется место в управляемой
    куче. Это позволяет использовать тип объекта в качестве владельца записи SyncBlock:
    ...
    for ( j = 0; j < m_size; j++ ) {
    for ( k = from; k < to; k++ ) {
    Monitor.Enter( typeof(double) );
    try {
    m_C[i,j] += m_A[i,k] * m_B[k,j];
    } finally {
    Monitor.Exit( typeof(double) );
    }
    }
    }
    ...
    Возможно неявное использование мониторов в C# с помощью ключевого слова lock:

    Разработка параллельных приложений для ОС Windows

    if ( to > m_size ) to = m_size;
    for ( i = 0; i < m_size; i++ ) {
    for ( j = 0; j < m_size; j++ ) {
    R = 0;
    for ( k = from; k < to; k++ ) R += m_A[i,k]*m_B[k,j];
    lock ( m_C ) { m_C[i,j] += R; }
    }
    }

    CIL и системное программирование в Microsoft .NET

    }
    Данный пример показывает процедуру потока, осуществляющего пополосное умножение матриц с необходимой синхронизацией. Следует заметить, что синхронизация доступа требует дополнительных ресурсов
    процессора (в данном случае, качественно превышающих затраты на умножение и сложение двух чисел с плавающей запятой), поэтому целесообразно как можно сильнее сократить число блокировок и время их наложения. В примере для этого использована промежуточная переменная R, накапливающая частичный результат.
    Следует особо подчеркнуть, что мониторы и блокировки доступа
    только лишь позволяют разработчику реализовать соответствующую синхронизацию, но ни в коем случае не осуществляют принудительное ограничение конкурентного обращения к полям и методам объектов. Любой
    параллельно выполняющийся фрагмент кода сохраняет полную возможность обращаться со всеми объектами, независимо от того, связаны они с
    какими-либо блокировками или нет. Для синхронизации и блокирования
    доступа необходимо, чтобы все участники синхронизации явным образом
    использовали критические секции.
    Ожидающие объекты
    .NET предоставляет базовый класс WaitHandle, служащий для описания объекта, который находится в одном из двух состояний: занятом или
    свободном. На основе этого класса строятся другие классы синхронизирующих объектов .NET, такие как события (ManualResetEvent и
    AutoResetEvent) и мьютексы (Mutex).
    Класс WaitHandle является, по сути, оберткой объектов ядра операционной системы, поддерживающих интерфейс синхронизации. Свойство
    Handle объекта WaitHandle позволяет установить (или узнать) соответствие
    этого объекта .NET с объектом ядра операционной системы.
    Существует три метода класса WaitHandle для ожидания освобождения объекта: метод WaitOne, являющийся методом объекта, и статические
    методы WaitAny и WaitAll. Метод WaitOne является оберткой вызова
    WaitForSingleObject Win32 API, а методы WaitAny и WaitAll – вызова
    WaitForMultipleObjects. Соответственно семантике конкретных объектов
    ядра, представленных объектом WaitHandle, методы Wait...могут изменять

    266

    267

    namespace TestNamespace {
    public class SomeData {
    public const int
    m_queries = 10;
    private static int
    m_counter = 0;
    private static Mutex
    m_mutex = new Mutex();
    private static ManualResetEvent m_event =
    new ManualResetEvent( false );
    public static void Invoke( int no ) {
    m_mutex.WaitOne();
    m_counter++;
    if ( m_counter >= m_queries ) m_event.Set();
    m_mutex.ReleaseMutex();
    m_event.WaitOne();
    }
    }
    public delegate void AsyncProcCallback( int no );
    class TestApp {
    public static void Main() {
    int
    i;
    WaitHandle[]
    wh;
    AsyncProcCallback apd;
    wh = new WaitHandle[ SomeData.m_queries ];
    apd = new AsyncProcCallback( SomeData.Invoke );
    for ( i = 0; i < SomeData.m_queries; i++ ) wh[i] =
    apd.BeginInvoke(i,null,null).AsyncWaitHandle;
    WaitHandle.WaitAll( wh );
    }
    }
    }

    using System;
    using System.Threading;

    или не изменять состояние ожидаемого объекта. Так, например, для событий с ручным сбросом (ManualResetEvent) состояние не меняется, а события с автоматическим сбросом и мьютексы (AutoResetEvent, Mutex) переводятся в занятое состояние.
    Объекты класса WaitHandle и производных от него, представляя объекты ядра операционной системы, могут быть использованы для межпроцессного взаимодействия. Конструкторы производных объектов (событий
    и мьютексов) позволяют задать имя объекта ядра, предназначенное для
    организации общего доступа к объектам процессами:

    Разработка параллельных приложений для ОС Windows

    CIL и системное программирование в Microsoft .NET

    Приведенный пример показывает синхронизацию с использованием
    мьютекса, события с ручным сбросом и объекта WaitHandle, представляющего состояние асинхронного вызова. В примере делается 10 асинхронных вызовов, после чего приложение ожидает завершения всех вызовов с помощью
    метода WaitAll. Каждый асинхронный метод в секции кода, защищаемой
    мьютексом (здесь было бы эффективнее использовать монитор или блокировку), подсчитывает число сделанных вызовов и переходит к ожиданию занятого события. Самый последний асинхронный вызов установит событие в
    свободное состояние, после чего все вызовы должны завершиться.
    Помимо использования разных синхронизирующих объектов, в этом
    примере интересно поведение CLR: асинхронные вызовы должны обрабатываться в пуле потоков, однако число вызовов превышает число потоков
    в пуле. CLR по мере необходимости добавляет в пул потоки для обработки поступающих запросов.
    Потоки не являются наследниками класса WaitHandle в силу того, что
    для разных базовых платформ потоки могут быть реализованы в качестве
    потоков операционной системы или легковесных потоков, управляемых
    CLR. В последнем случае потоки .NET не будут иметь никаких аналогов
    среди объектов ядра операционной системы. Для синхронизации с потоками надо использовать метод Join класса Thread.
    Один «писатель», много «читателей»
    Одной из типичных задач синхронизации потоков является задача, в
    которой допускается одновременный конкурентный доступ многих объектов для чтения данных («читатели») и исключительный доступ единственного потока, вносящего в объект изменения («писатель»). В Win32 API
    стандартного объекта, реализующего подобную логику, не существует, поэтому каждый раз его надо проектировать и создавать заново.
    .NET предоставляет весьма эффективное стандартное решение:
    класс ReaderWriterLock. В приводимом ниже примере демонстрируется
    применение методов Acquire... и Release... для корректного использования блокировки доступа при чтении и записи. Тестовый класс содержит
    две целочисленные переменные, которые считываются и увеличиваются
    на 1 с небольшими задержками по отношению друг к другу. Пока операции синхронизируются, попытка чтения или изменения всегда будет возвращать четный результат, а вот если бы синхронизация не выполнялась,
    то в некоторых случаях получались бы нечетные числа:
    using System;
    using System.Threading;
    namespace TestNamespace {
    public class SomeData {
    public const int
    m_queries = 10;
    private ReaderWriterLock m_rwlock = new ReaderWriterLock();

    268

    }

    }
    public delegate void AsyncProcCallback(SomeData sd, int no);
    class TestApp {
    public static void Main() {
    int
    i;
    SomeData
    sd = new SomeData();
    WaitHandle[]
    wh;
    AsyncProcCallback apd;
    wh = new WaitHandle[ SomeData.m_queries ];
    apd = new AsyncProcCallback( SomeData.Invoke );
    for ( i = 0; i < SomeData.m_queries; i++ ) wh[i] =
    apd.BeginInvoke(sd,i,null,null).AsyncWaitHandle;
    WaitHandle.WaitAll( wh );
    }
    }

    private int
    m_a = 0, m_b = 0;
    public int summ() {
    int r;
    m_rwlock.AcquireReaderLock( -1 );
    try {
    r = m_a; Thread.Sleep( 1000 ); return r + m_b;
    } finally {
    m_rwlock.ReleaseReaderLock();
    }
    }
    public int inc() {
    m_rwlock.AcquireWriterLock( -1 );
    try {
    m_a++; Thread.Sleep( 500 ); m_b++;
    return m_a + m_b;
    } finally {
    m_rwlock.ReleaseWriterLock();
    }
    }
    public static void Invoke( SomeData sd, int no ) {
    if ( no % 2 == 0 ) {
    Console.WriteLine( sd.inc() );
    } else {
    Console.WriteLine( sd.summ() );
    }
    }

    Разработка параллельных приложений для ОС Windows

    269

    CIL и системное программирование в Microsoft .NET

    class SomeData {
    private static LocalDataStoreSlot m_tls = Thread.AllocateDataSlot();
    public static void ThreadProc() {
    Thread.SetData( m_tls, ... );
    ...
    }
    public void Main() {
    SomeData sd = new SomeData();
    ...
    // создание и запуск потоков
    }
    }

    class SomeData {
    [ThreadStatic]
    public static double xxx;
    ...
    Поле класса SomeData.xxx будет размещено в локальной для каждого
    потока памяти.
    Императивный подход связан с применением методов
    AllocateDataSlot,
    AllocateNamedDataSlot,
    GetNamedDataSlot,
    FreeNamedDataSlot, GetData и SetData класса Thread. Использование этих
    методов очень похоже на использование Tls... функций Win32 API, с той
    разницей, что вместо целочисленного индекса в TLS массиве потока (как
    это было в Win32 API) используется объект типа LocalDataStoreSlot, который выполняет функции прежнего индекса:

    7.3.4.3. Локальная для потока память
    Применение локальной для потока памяти в .NET опирается на TLS
    память, поддерживаемую операционной системой. Аналогично Win32
    API, возможны декларативный и императивный подходы для работы с локальной для потока памятью.
    Декларативный подход сводится к использованию атрибута
    ThreadStaticAttribute перед описанием любого статического поля. Например, в следующем фрагменте:

    Конечно, аналогичного эффекта можно было бы добиться, просто
    используя блокировку (lock или методы класса Monitor) при доступе к объекту. Однако, такой подход потребует наложить блокировку исключительного доступа при чтении данных, что не эффективно. В обычных условиях вполне допустимо чтение данных несколькими одновременно выполняющимися потоками, что может дать заметное ускорение.

    270

    271

    namespace TestNamespace {
    class TestTimer : Timer {
    private int m_minimal, m_maximal, m_counter;
    public int count { get{ return m_counter – m_minimal; }}

    using System;
    using System.Timers;

    .NET предлагает два вида таймеров: один описан в пространстве имен
    System.Timers, а другой – в пространстве имен System.Threading.
    Таймер пространства имен System.Threading является опечатанным и
    предназначен для вызова указанной асинхронной процедуры с заданным
    интервалом времени.
    Таймер пространства имен System.Timers может быть использован
    для создания собственных классов-потомков – в нем вместо процедуры
    асинхронного вызова применяется обработка события, с которым может
    быть сопоставлено несколько обработчиков. Кроме того, этот таймер может вызывать обработку события конкретным потоком, а не произвольным потоком пула:

    7.3.5. Таймеры

    Методы Allocate... и GetNamedDataSlot позволяют выделить новую
    ячейку в TLS памяти (или получить существующую именованную), методы GetData и SetData позволяют получить или сохранить ссылку на объект
    в TLS памяти. Использование TLS памяти в .NET менее удобно и эффективно, чем в Win32 API, но это связано не с реализацией TLS, а с реализацией потоков:
    • Во-первых, возможно размещение данных в TLS памяти только
    текущего потока, то есть нельзя положить данные до запуска потока.
    • Во-вторых, процедура потока не получает аргументов, то есть
    требуется предусмотреть отдельный механизм для передачи данных в функцию потока, а этот неизбежно реализуемый механизм окажется конкурентом существующей реализации TLS памяти.
    • В-третьих, использование TLS памяти в асинхронно вызываемых процедурах может быть ограничено теми соображениями,
    что заранее нельзя предугадать поток, который будет выполнять
    эту процедуру.
    • В-четвертых, использование методов ООП часто позволяет сохранить специфичные данные в полях объекта, вообще не прибегая к выделению TLS памяти.

    Разработка параллельных приложений для ОС Windows

    public TestTimer( int mn, int mx ) {
    Elapsed += new ElapsedEventHandler(OnElapsed);
    m_minimal = m_counter = mn;
    m_maximal = mx;
    AutoReset = true;
    Interval = 400;
    }
    static void OnElapsed( object src, ElapsedEventArgs e ) {
    TestTimer tt = (TestTimer)src;
    if ( tt.m_counter < tt.m_maximal ) tt.m_counter++;
    if ( tt.m_counter >= tt.m_maximal ) tt.Stop();
    }
    static void Main(string[] args) {
    TestTimer tm = new TestTimer( 0, 10 );
    tm.Start();
    Thread.Sleep( 5000 );
    tm.Stop();
    }

    CIL и системное программирование в Microsoft .NET

    }
    }
    Приведенный выше пример иллюстрирует использование таймера
    пространства имен System.Timers.

    272

    273

    1.

    Common Language Infrastructure, Partition I: Concepts and Architecture.
    Microsoft. – .NET Framework SDK Tool Developer's Documentation.
    2. Common Language Infrastructure, Partition II: Metadata Definition and
    Semantics. – .NET Framework SDK Tool Developer's Documentation.
    3. Common Language Infrastructure, Partition III: CIL Instruction Set.
    – .NET Framework SDK Tool Developer's Documentation.
    4. Common Language Runtime: Metadata Unmanaged API.
    – .NET Framework SDK Tool Developer's Documentation.
    5. Microsoft Portable Executable and Common Object File Format Specification.
    – Microsoft Corporation, 1999.
    6. М. Бертран. Объектно-ориентированное конструирование программных систем. – М.: Издательско-торговый дом «Русская редакция», 2005. – 1232 с.
    7. Основы операционных систем. Курс лекций. Учебное пособие /
    В.Е. Карпов, К.А. Коньков / Под редакцией В.П. Иванникова. –
    М.: ИНТУИТ.РУ «Интернет- Университет Информационных Технологий», 2004. – 632 с.
    8. Дж. Рихтер. Windows для профессионалов: создание эффективных
    Win32-приложений с учетом специфики 64-разрядной версии
    Windows. – СПб: Питер; М.: Издательско-торговый дом «Русская
    редакция», 2001. – 752 с.
    9. Дж. Рихтер. Программирование на платформе Microsoft .NET
    Framework. – М.: Издательско-торговый дом «Русская редакция»,
    2003. – 512 с.
    10. Д. Соломон, М. Руссинович. Внутреннее устройство Microsoft
    Windows 2000. – СПб: Питер; М.: Издательско-торговый дом «Русская редакция», 2001. – 752 с.
    11. Д. Уоткинз, М. Хаммонд, Б. Эйбрамз. Программирование на платформе .NET. – М.: Издательский дом «Вильямс», 2003. – 368 с.

    Литература

    Литература

    CIL и системное программирование в Microsoft .NET

    2

    #define SIZEOF_TEXT_NOTALIGNED(params) \

    // Not aligned
    #define SIZEOF_HEADERS_NOTALIGNED \
    sizeof(struct HEADERS)

    #define SIZEOF_METHODS(params) \
    align(params->SizeOfCilCode, SECTION_ALIGNMENT)

    #define IMAGE_SUBSYSTEM_WINDOWS_GUI

    0x00000020
    0x00000040
    0x00000080
    0x02000000
    0x20000000
    0x40000000
    0x80000000

    #define SIZEOF_RELOC_M \
    align(sizeof(struct RELOC_SECTION), SECTION_ALIGNMENT)

    #define SIZEOF_CLI_M \
    align(sizeof(struct CLI_SECTION_IMAGE), SECTION_ALIGNMENT)

    #define SIZEOF_TEXT_M(params) \
    align(params->SizeOfMetadata + params->SizeOfCilCode, \
    SECTION_ALIGNMENT)

    // Aligned to SectionAlignment boundary
    #define SIZEOF_HEADERS_M(params) \
    align(sizeof(struct HEADERS), SECTION_ALIGNMENT)

    #define SIZEOF_RELOC(params) \
    align(sizeof(struct RELOC_SECTION), params->FileAlignment)

    #define SIZEOF_CLI_NOTALIGNED \
    sizeof(struct CLI_SECTION_IMAGE)

    CNT_CODE
    CNT_INITIALIZED_DATA
    CNT_UNINITIALIZED_DATA
    MEM_DISCARDABLE
    MEM_EXECUTE
    MEM_READ
    MEM_WRITE

    #define
    #define
    #define
    #define
    #define
    #define
    #define

    0x2000
    0x1
    0x0
    6
    82

    275

    #define SIZEOF_CLI(params) \
    align(sizeof(struct CLI_SECTION_IMAGE), params->FileAlignment)

    #define SIZEOF_TEXT(params) \
    align(params->SizeOfMetadata + params->SizeOfCilCode, \
    params->FileAlignment)

    // Aligned to FileAlignment boundary
    #define SIZEOF_HEADERS(params) \
    align(sizeof(struct HEADERS), params->FileAlignment)

    #define IMAGE_SUBSYSTEM_WINDOWS_CUI 3
    #define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9

    Исходный код программы pegen

    #define IMAGE_FILE_MACHINE_I386 0x014c

    SECTION_ALIGNMENT
    EXE_TYPE
    DLL_TYPE
    SIZEOF_JMP_STUB
    SIZEOF_IMPORT_TABLE

    #define
    #define
    #define
    #define
    #define

    #ifndef MACROS_H
    #define MACROS_H

    A.1. macros.h

    Исходный код программы pegen, выполняющей генерацию сборки
    .NET, размещен в четырех файлах:
    • macros.h
    Содержит макроопределения, которые используются в остальных файлах.
    • pe.h
    Интерфейс модуля генерации PE-файла.
    • pe.c
    Реализация модуля генерации PE-файла.
    • main.c
    Главный модуль, использующий модуль генерации для создания
    простейшей сборки .NET.

    Приложение A.
    Исходный код программы pegen

    274

    CIL и системное программирование в Microsoft .NET

    #endif

    #define OFFSETOF(s,m) \
    (size_t)&(((s *)0)->m)

    #define TYPE_OFFSET(a,b) \
    (a*0x1000 | b)

    #define RVA_OF_RELOC(params) \
    RVA_OF_CLI(params) + SIZEOF_CLI_M

    #define RVA_OF_CLI(params) \
    RVA_OF_TEXT +
    \
    align(params->SizeOfMetadata + params->SizeOfCilCode,
    SECTION_ALIGNMENT)

    // RVA of section
    #define RVA_OF_TEXT
    \
    align(sizeof(struct HEADERS), SECTION_ALIGNMENT)

    #define SIZEOF_DATA_DIRECTORY \
    sizeof(struct IMAGE_DATA_DIRECTORY)

    #define SIZEOF_RELOC_NOTALIGNED \
    sizeof(struct RELOC_SECTION)

    #define SIZEOF_CLI_HEADER \
    sizeof(struct IMAGE_COR20_HEADER)

    params->SizeOfMetadata + params->SizeOfCilCode

    // Block of input parameters
    struct INPUT_PARAMETERS {
    unsigned long
    Type;

    #include

    #ifndef PE_H

    A.2. pe.h

    276

    long
    long
    long
    short

    ImageBase;
    FileAlignment;
    EntryPointToken;
    Subsystem;

    SizeOfMetadata;
    SizeOfCilCode;

    *metadata;
    *cilcode;

    make_file

    (FILE *file, PINPUT_PARAMETERS inP);

    // MS-DOS header
    // PE signature
    // PE header

    char ms_dos_header[128];
    unsigned long signature;
    struct _IMAGE_FILE_HEADER {

    struct HEADERS {

    struct IMAGE_SECTION_HEADER {
    unsigned char Name[8];
    unsigned long VirtualSize;
    unsigned long VirtualAddress;
    unsigned long SizeOfRawData;
    unsigned long PointerToRawData;
    unsigned long PointerToRelocations;
    unsigned long PointerToLinenumbers;
    unsigned short NumberOfRelocations;
    unsigned short NumberOfLinenumbers;
    unsigned long Characteristics;
    };

    typedef struct IMAGE_DATA_DIRECTORY *PIMAGE_DATA_DIRECTORY;

    // Struct IMAGE_DATA_DIRECTORY
    struct IMAGE_DATA_DIRECTORY {
    unsigned long
    RVA;
    unsigned long
    Size;
    };

    void

    };
    typedef struct INPUT_PARAMETERS *PINPUT_PARAMETERS;

    unsigned
    unsigned
    unsigned
    unsigned

    unsigned long
    unsigned long

    unsigned char
    unsigned char

    Исходный код программы pegen

    277

    278

    short
    short
    long
    long
    long
    short
    short

    Machine;
    NumberOfSections;
    TimeDateStamp;
    PointerToSymbolTable;
    NumberOfSymbols;
    OptionalHeaderSize;
    Characteristics;

    struct _IMAGE_OPTIONAL_HEADER {
    // optional PE header
    unsigned short Magic;
    unsigned char
    LMajor;
    unsigned char
    LMinor;
    unsigned long
    CodeSize;
    unsigned long
    SizeOfInitializedData;
    unsigned long
    SizeOfUninitializedData;
    unsigned long
    EntryPointRVA;
    unsigned long
    BaseOfCode;
    unsigned long
    BaseOfData;
    unsigned long
    ImageBase;
    unsigned long
    SectionAlignment;
    unsigned long
    FileAlignment;
    unsigned short OSMajor;
    unsigned short OSMinor;
    unsigned short UserMajor;
    unsigned short UserMinor;
    unsigned short SubsysMajor;
    unsigned short SubsysMinor;
    unsigned long
    Reserved;
    unsigned long
    ImageSize;
    unsigned long
    HeaderSize;
    unsigned long
    FileCheckSum;
    unsigned short Subsystem;
    unsigned short DllFlags;
    unsigned long
    StackReserveSize;
    unsigned long
    StackCommitSize;
    unsigned long
    HeapReserveSize;
    unsigned long
    HeapCommitSize;
    unsigned long
    LoaderFlags;
    unsigned long
    NumberOfDataDirectories;
    }OptHdr;

    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    }PeHdr;

    CIL и системное программирование в Microsoft .NET

    IMAGE_DATA_DIRECTORY
    IMAGE_DATA_DIRECTORY
    IMAGE_DATA_DIRECTORY
    IMAGE_DATA_DIRECTORY
    IMAGE_DATA_DIRECTORY
    IMAGE_DATA_DIRECTORY
    IMAGE_DATA_DIRECTORY
    IMAGE_DATA_DIRECTORY
    IMAGE_DATA_DIRECTORY

    STUB1;
    IMPORT_DIRECTORY; // import directory
    STUB2[3];
    BASE_RELOC_DIRECTORY;
    STUB3[6];
    IAT_DIRECTORY;
    // IAT directory
    STUB4;
    CLI_DIRECTORY;
    // CLI directory
    STUB5;

    279

    // entry point
    JmpInstruction;
    JmpAddress;

    // Import table
    /* Import Address Table */
    unsigned long
    HintNameTableRVA2;
    unsigned long
    zero2;
    /* Import Directory Entry */

    struct _IMPORT_TABLE {

    struct _CLI_HEADER {
    // CLI header
    unsigned long
    cb;
    unsigned short
    MajorRuntimeVersion;
    unsigned short
    MinorRuntimeVersion;
    struct IMAGE_DATA_DIRECTORY MetaData;
    unsigned long
    Flags;
    unsigned long
    EntryPointToken;
    struct IMAGE_DATA_DIRECTORY NotUsed[6];
    }CLI_HEADER;

    struct _JMP_STUB {
    unsigned short
    unsigned long
    }JMP_STUB;

    // .CLI Section
    struct CLI_SECTION_IMAGE {

    typedef struct HEADERS *PHEADERS;

    };

    struct IMAGE_SECTION_HEADER TEXT_SECTION; // .text section header
    struct IMAGE_SECTION_HEADER CLI_SECTION; // .cli section header
    struct IMAGE_SECTION_HEADER RELOC_SECTION; // .reloc section header

    struct
    struct
    struct
    struct
    struct
    struct
    struct
    struct
    struct

    Исходный код программы pegen

    280

    ImportLookupTableRVA;
    TimeDateStamp;
    ForwarderChain;
    NameRVA;
    ImportAddressTableRVA;
    zero[20];

    struct IMAGE_COR20_HEADER
    {
    unsigned long
    cb;
    unsigned short
    MajorRuntimeVersion;
    unsigned short
    MinorRuntimeVersion;
    struct IMAGE_DATA_DIRECTORY MetaData;
    unsigned long
    Flags;
    unsigned long
    EntryPointToken;
    struct IMAGE_DATA_DIRECTORY Resources;
    struct IMAGE_DATA_DIRECTORY StrongNameSignature;
    struct IMAGE_DATA_DIRECTORY CodeManagerTable;
    struct IMAGE_DATA_DIRECTORY VTableFixups;

    PageRVA;
    BlockSize;
    TypeOffset;
    Padding;

    /* Dll name (“mscoree.dll”) */
    char
    DllName[12];
    }IMPORT_TABLE;

    /* Hint/Name Table */
    unsigned short
    Hint;
    char
    Name[12];

    //.reloc Section
    struct RELOC_SECTION
    {
    unsigned long
    unsigned long
    unsigned short
    unsigned short
    };

    };

    long
    long
    long
    long
    long
    char

    /* Import Lookup Table */
    unsigned long
    HintNameTableRVA1;
    unsigned long
    zero1;

    unsigned
    unsigned
    unsigned
    unsigned
    unsigned
    unsigned

    CIL и системное программирование в Microsoft .NET

    make_headers
    (FILE* file, PINPUT_PARAMETERS inP);
    make_text_section (FILE* file, PINPUT_PARAMETERS inP);
    make_cli_section (FILE* file, PINPUT_PARAMETERS inP);
    make_reloc_section (FILE* file, PINPUT_PARAMETERS inP);




    “pe.h”
    “macros.h”

    ExportAddressTableJumps;
    ManagedNativeHeader;

    unsigned char
    0x4D, 0x5A,
    0x04, 0x00,
    0xB8, 0x00,
    0x40, 0x00,
    0x00, 0x00,
    0x00, 0x00,
    0x00, 0x00,

    msdos_header[128]
    0x90, 0x00, 0x03,
    0x00, 0x00, 0xFF,
    0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,
    0x00, 0x00, 0x00,

    = {
    0x00,
    0xFF,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,

    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,

    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,

    void make_file (FILE* file, PINPUT_PARAMETERS inP)
    {
    make_headers(file, inP);
    // Stage 1
    make_text_section(file, inP); // Stage 2
    make_cli_section(file, inP); // Stage 3
    make_reloc_section(file, inP); // Stage 4
    }

    unsigned long align(unsigned long x, unsigned long alignment)
    {
    div_t t = div(x,alignment);
    return t.rem == 0 ? x : (t.quot+1)*alignment;
    };

    void
    void
    void
    void

    #include
    #include
    #include
    #include
    #include

    A.3. pe.c

    #endif

    };

    struct IMAGE_DATA_DIRECTORY
    struct IMAGE_DATA_DIRECTORY

    Исходный код программы pegen

    281

    282

    0x00,
    0x1F,
    0xB8,
    0x73,
    0x6D,
    0x20,
    0x69,
    0x6F,
    0x00,

    0x00,
    0xBA,
    0x01,
    0x20,
    0x20,
    0x62,
    0x6E,
    0x64,
    0x00,

    0x00,
    0x0E,
    0x4C,
    0x70,
    0x63,
    0x65,
    0x20,
    0x65,
    0x00,

    0x80,
    0x00,
    0xCD,
    0x72,
    0x61,
    0x20,
    0x44,
    0x2E,
    0x00,

    0x00,
    0xB4,
    0x21,
    0x6F,
    0x6E,
    0x72,
    0x4F,
    0x0D,
    0x00,

    0x00,
    0x09,
    0x54,
    0x67,
    0x6E,
    0x75,
    0x53,
    0x0D,
    0x00,

    0x00,
    0xCD,
    0x68,
    0x72,
    0x6F,
    0x6E,
    0x20,
    0x0A,
    0x00

    // Optional Header
    Hdr->OptHdr.Magic = 0x010B;
    Hdr->OptHdr.LMajor = 6;
    Hdr->OptHdr.LMinor = 0;
    Hdr->OptHdr.SizeOfUninitializedData
    = 0;
    Hdr->OptHdr.SectionAlignment
    = SECTION_ALIGNMENT;
    Hdr->OptHdr.OSMajor = 4;
    Hdr->OptHdr.OSMinor = 0;
    Hdr->OptHdr.UserMajor = 0;
    Hdr->OptHdr.UserMinor = 0;
    Hdr->OptHdr.SubsysMajor = 4;
    Hdr->OptHdr.SubsysMinor = 0;
    Hdr->OptHdr.Reserved = 0;
    Hdr->OptHdr.FileCheckSum = 0;
    Hdr->OptHdr.DllFlags = 0x400;
    Hdr->OptHdr.StackReserveSize = 0x100000;
    Hdr->OptHdr.StackCommitSize = 0x1000;
    Hdr->OptHdr.HeapReserveSize = 0x100000;
    Hdr->OptHdr.HeapCommitSize = 0x1000;
    Hdr->OptHdr.LoaderFlags = 0;

    Hdr->PeHdr.Machine = IMAGE_FILE_MACHINE_I386;
    Hdr->PeHdr.PointerToSymbolTable = 0;
    Hdr->PeHdr.NumberOfSymbols = 0;
    Hdr->PeHdr.OptionalHeaderSize = 0xe0;

    Hdr->signature = 0x00004550;

    // initialize constant fields in HEADERS structure
    void make_headers_const(PHEADERS Hdr){
    memcpy(Hdr->ms_dos_header, msdos_header, 128);

    };

    0x00,
    0x0E,
    0x21,
    0x69,
    0x61,
    0x74,
    0x20,
    0x6D,
    0x24,

    CIL и системное программирование в Microsoft .NET

    // initialize to 0
    memset(&Hdr->STUB1.RVA,
    memset(Hdr->STUB2, 0, 3
    memset(Hdr->STUB3, 0, 6
    memset(&Hdr->STUB4.RVA,
    memset(&Hdr->STUB5.RVA,

    0, SIZEOF_DATA_DIRECTORY);
    * SIZEOF_DATA_DIRECTORY);
    * SIZEOF_DATA_DIRECTORY);
    0, SIZEOF_DATA_DIRECTORY);
    0, SIZEOF_DATA_DIRECTORY);

    make_headers_const(&Hdr);
    Hdr.PeHdr.NumberOfSections = 3;
    Hdr.PeHdr.TimeDateStamp = (long)time(NULL);

    struct HEADERS Hdr;
    char * image;

    // initialize HEADERS structure
    void make_headers(FILE* file ,PINPUT_PARAMETERS inP){

    };

    = 0;
    = 0;
    = 0;
    = 0;
    0x60000020;

    = 0;
    = 0;
    = 0;
    = 0;
    0x60000020;

    // .RELOC section
    Hdr->RELOC_SECTION.PointerToRelocations = 0;
    Hdr->RELOC_SECTION.PointerToLinenumbers = 0;
    Hdr->RELOC_SECTION.NumberOfRelocations = 0;
    Hdr->RELOC_SECTION.NumberOfLinenumbers = 0;
    Hdr->RELOC_SECTION.Characteristics
    = 0x42000040;

    // CLI section
    Hdr->CLI_SECTION.PointerToRelocations
    Hdr->CLI_SECTION.PointerToLinenumbers
    Hdr->CLI_SECTION.NumberOfRelocations
    Hdr->CLI_SECTION.NumberOfLinenumbers
    Hdr->CLI_SECTION.Characteristics
    =

    // TEXT section
    Hdr->TEXT_SECTION.PointerToRelocations
    Hdr->TEXT_SECTION.PointerToLinenumbers
    Hdr->TEXT_SECTION.NumberOfRelocations
    Hdr->TEXT_SECTION.NumberOfLinenumbers
    Hdr->TEXT_SECTION.Characteristics
    =

    Hdr->OptHdr.NumberOfDataDirectories = 16;

    Исходный код программы pegen

    283

    284

    = RVA_OF_CLI(inP) + SIZEOF_JMP_STUB;
    = SIZEOF_CLI_HEADER;

    Hdr.TEXT_SECTION.VirtualSize
    = SIZEOF_TEXT_NOTALIGNED(inP);
    Hdr.TEXT_SECTION.VirtualAddress
    = SIZEOF_HEADERS_M(inP);

    //TEXT section
    memset(Hdr.TEXT_SECTION.Name, 0, sizeof(Hdr.TEXT_SECTION.Name));
    strcpy((char*)Hdr.TEXT_SECTION.Name, “.text”);

    // CLI Directory
    Hdr.CLI_DIRECTORY.RVA
    Hdr.CLI_DIRECTORY.Size

    // Base Reloc Directory
    Hdr.BASE_RELOC_DIRECTORY.RVA = RVA_OF_RELOC(inP);
    Hdr.BASE_RELOC_DIRECTORY.Size
    = 0x0C;

    // Import Address Directory
    Hdr.IAT_DIRECTORY.RVA
    = RVA_OF_CLI(inP) +
    OFFSETOF(struct CLI_SECTION_IMAGE,
    IMPORT_TABLE.HintNameTableRVA2);
    Hdr.IAT_DIRECTORY.Size
    = 0x08;

    // Import Directory
    Hdr.IMPORT_DIRECTORY.RVA
    = RVA_OF_CLI(inP) +
    OFFSETOF(struct CLI_SECTION_IMAGE,
    IMPORT_TABLE.ImportLookupTableRVA);
    Hdr.IMPORT_DIRECTORY.Size
    = 0x53;

    Hdr.OptHdr.CodeSize = SIZEOF_TEXT_M(inP);
    Hdr.OptHdr.SizeOfInitializedData = SIZEOF_TEXT_M(inP);
    Hdr.OptHdr.EntryPointRVA = RVA_OF_CLI(inP);
    Hdr.OptHdr.BaseOfCode = RVA_OF_TEXT;
    Hdr.OptHdr.BaseOfData = 0;
    Hdr.OptHdr.ImageBase = inP->ImageBase;
    Hdr.OptHdr.FileAlignment = inP->FileAlignment;
    Hdr.OptHdr.ImageSize = RVA_OF_RELOC(inP) + SIZEOF_RELOC_M;
    Hdr.OptHdr.HeaderSize = SIZEOF_HEADERS(inP);
    Hdr.OptHdr.Subsystem = inP->Subsystem;

    if(inP->Type == EXE_TYPE)
    Hdr.PeHdr.Characteristics = 0x010E;
    else
    Hdr.PeHdr.Characteristics = 0x210E;

    CIL и системное программирование в Microsoft .NET

    memset(image,0,SIZEOF_HEADERS(inP));
    memcpy(image,(char *)&Hdr, SIZEOF_HEADERS_NOTALIGNED);
    fwrite(image,1,SIZEOF_HEADERS(inP),file);
    free(image);

    image = malloc(SIZEOF_HEADERS(inP));

    Hdr.RELOC_SECTION.VirtualSize
    = SIZEOF_RELOC_NOTALIGNED;
    Hdr.RELOC_SECTION.VirtualAddress = RVA_OF_RELOC(inP);
    Hdr.RELOC_SECTION.SizeOfRawData
    = SIZEOF_RELOC(inP);
    Hdr.RELOC_SECTION.PointerToRawData = SIZEOF_HEADERS(inP) +
    SIZEOF_TEXT(inP) + SIZEOF_CLI(inP);
    //END of initializing .RELOC section

    //.RELOC section
    memset(Hdr.RELOC_SECTION.Name, 0, sizeof(Hdr.RELOC_SECTION.Name));
    strcpy((char*)Hdr.RELOC_SECTION.Name, “.reloc”);

    Hdr.CLI_SECTION.VirtualSize
    = SIZEOF_CLI_NOTALIGNED;
    Hdr.CLI_SECTION.VirtualAddress
    = SIZEOF_HEADERS_M(inP) +
    SIZEOF_TEXT_M(inP);
    Hdr.CLI_SECTION.SizeOfRawData
    = SIZEOF_CLI(inP);
    Hdr.CLI_SECTION.PointerToRawData = SIZEOF_HEADERS(inP) +
    SIZEOF_TEXT(inP);
    //END of initializing CLI section

    image = malloc(SIZEOF_TEXT(inP));
    memset(image, 0, SIZEOF_TEXT(inP));
    memcpy(image, inP->metadata, inP->SizeOfMetadata);

    // initialize .TEXT section
    void make_text_section(FILE * file, PINPUT_PARAMETERS inP) {
    char * image;

    };

    285

    //.cli section
    memset(Hdr.CLI_SECTION.Name, 0, sizeof(Hdr.CLI_SECTION.Name));
    strcpy((char*)Hdr.CLI_SECTION.Name, “.cli”);

    Hdr.TEXT_SECTION.SizeOfRawData
    = SIZEOF_TEXT(inP);
    Hdr.TEXT_SECTION.PointerToRawData = SIZEOF_HEADERS(inP);
    //END of initializing TEXT section

    Исходный код программы pegen

    286

    = 0;
    = 0;

    = RVA_OF_CLI(inP) +

    cls.IMPORT_TABLE.NameRVA
    = RVA_OF_CLI(inP) +
    OFFSETOF(struct CLI_SECTION_IMAGE, IMPORT_TABLE.DllName);

    cls.IMPORT_TABLE.TimeDateStamp
    cls.IMPORT_TABLE.ForwarderChain

    //Import Table
    cls.IMPORT_TABLE.ImportLookupTableRVA
    OFFSETOF(struct CLI_SECTION_IMAGE,
    IMPORT_TABLE.HintNameTableRVA1);

    memset(cls.CLI_HEADER.NotUsed, 0,
    6*sizeof(struct IMAGE_DATA_DIRECTORY));

    //CLI_HEADER
    cls.CLI_HEADER.cb
    = SIZEOF_CLI_HEADER;
    cls.CLI_HEADER.MajorRuntimeVersion
    = 2;
    cls.CLI_HEADER.MinorRuntimeVersion
    = 0;
    cls.CLI_HEADER.MetaData.RVA
    = RVA_OF_TEXT;
    cls.CLI_HEADER.MetaData.Size
    = inP->SizeOfMetadata;
    cls.CLI_HEADER.Flags
    = 1;
    cls.CLI_HEADER.EntryPointToken
    = inP->EntryPointToken;

    cls.JMP_STUB.JmpAddress
    = RVA_OF_CLI(inP) +
    OFFSETOF(struct CLI_SECTION_IMAGE, IMPORT_TABLE.Hint) +
    inP->ImageBase;

    //JMP_STUB
    cls.JMP_STUB.JmpInstruction = 0x25FF;

    // initialize .CLI section
    void make_cli_section(FILE * file, PINPUT_PARAMETERS inP) {
    struct CLI_SECTION_IMAGE cls;
    char * image;

    }

    fwrite(image, 1, SIZEOF_TEXT(inP), file);
    free(image);

    memcpy(image+inP->SizeOfMetadata, inP->cilcode,
    inP->SizeOfCilCode);

    CIL и системное программирование в Microsoft .NET

    rls.PageRVA
    = RVA_OF_CLI(inP);
    rls.BlockSize = SIZEOF_RELOC_NOTALIGNED;
    rls.TypeOffset = TYPE_OFFSET(0x3,0x2);
    rls.Padding
    = 0;

    // initialize .RELOC section
    void make_reloc_section(FILE* file, PINPUT_PARAMETERS inP) {
    struct RELOC_SECTION rls;
    char * image;

    };

    image = malloc(SIZEOF_CLI(inP));
    memset(image, 0, SIZEOF_CLI(inP));
    memcpy(image, (char *) &cls, SIZEOF_CLI_NOTALIGNED);
    fwrite(image,1, SIZEOF_CLI(inP),file);
    free(image);

    strcpy(cls.IMPORT_TABLE.DllName, “mscoree.dll”);

    if(inP->Type == EXE_TYPE)
    strcpy(cls.IMPORT_TABLE.Name, “_CorExeMain”);
    else
    strcpy(cls.IMPORT_TABLE.Name, “_CorDllMain”);

    cls.IMPORT_TABLE.Hint = 0;

    cls.IMPORT_TABLE.zero2 = 0;

    cls.IMPORT_TABLE.HintNameTableRVA2 = (RVA_OF_CLI(inP) +
    OFFSETOF(struct CLI_SECTION_IMAGE, IMPORT_TABLE.Hint));

    cls.IMPORT_TABLE.zero1 = 0;

    cls.IMPORT_TABLE.HintNameTableRVA1 = (RVA_OF_CLI(inP) +
    OFFSETOF(struct CLI_SECTION_IMAGE, IMPORT_TABLE.Hint)) ;

    memset(cls.IMPORT_TABLE.zero, 0, 20);

    cls.IMPORT_TABLE.ImportAddressTableRVA = RVA_OF_CLI(inP) +
    OFFSETOF(struct CLI_SECTION_IMAGE,
    IMPORT_TABLE.HintNameTableRVA2);

    Исходный код программы pegen

    287

    };

    CIL и системное программирование в Microsoft .NET

    // METADATA
    extern unsigned char metadata[]
    0x42, 0x53, 0x4A, 0x42, 0x01,
    0x00, 0x00, 0x00, 0x00, 0x0C,
    0x76, 0x31, 0x2E, 0x31, 0x2E,
    0x32, 0x00, 0x00, 0x00, 0x00,
    0xAC, 0x00, 0x00, 0x00, 0x80,
    0x23, 0x53, 0x74, 0x72, 0x69,
    0x00, 0x00, 0x00, 0x00, 0x2C,
    0x40, 0x00, 0x00, 0x00, 0x23,
    0x6C, 0x00, 0x00, 0x00, 0x40,
    0x23, 0x42, 0x6C, 0x6F, 0x62,
    0x6C, 0x01, 0x00, 0x00, 0x10,
    0x23, 0x47, 0x55, 0x49, 0x44,
    0x7C, 0x01, 0x00, 0x00, 0x96,
    0x23, 0x7E, 0x00, 0x00, 0x00,
    0x01, 0x04, 0x00, 0x01, 0x01,
    0x7A, 0x5C, 0x56, 0x19, 0x34,
    0x00, 0x00, 0x0E, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00,
    0x74, 0x68, 0x00, 0x61, 0x72,
    0x2E, 0x65, 0x78, 0x65, 0x00,

    #include “pe.h”
    #include “macros.h”

    #include
    #include

    = {
    0x00,
    0x00,
    0x34,
    0x00,
    0x00,
    0x6E,
    0x01,
    0x55,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x03,
    0x0E,
    0xE0,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x61,
    0x69,
    0x3C,
    0x01,
    0x00,
    0x33,
    0x05,
    0x00,
    0x67,
    0x00,
    0x53,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x08,
    0x89,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x72,
    0x74,
    0x4D,

    0x00,
    0x00,
    0x32,
    0x00,
    0x00,
    0x73,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0xB7,
    0x03,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x69,
    0x68,
    0x6F,

    image = malloc(SIZEOF_RELOC(inP));
    memset(image, 0, SIZEOF_RELOC(inP));
    memcpy(image, (char *)&rls, SIZEOF_RELOC_NOTALIGNED);
    fwrite(image,1, SIZEOF_RELOC(inP),file);
    free(image);

    A.4. main.c

    288

    0x64,
    0x6C,
    0x6C,
    0x65,
    0x6C,
    0x4C,
    0x64,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x65,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x33,
    0x26,
    0x01,
    0x09,
    0x00,
    0x01,
    0x01,
    0x01,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x01,
    0x13,
    0x04,
    0x01,
    0x00,
    0x00,
    0x00,

    0x75,
    0x63,
    0x69,
    0x6D,
    0x65,
    0x69,
    0x4C,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x03,
    0xD7,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x80,
    0x00,
    0x00,
    0x00,
    0x00,

    0x6C,
    0x00,
    0x62,
    0x00,
    0x00,
    0x6E,
    0x69,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x6C,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x08,
    0x2D,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x07,
    0x06,
    0x00,
    0x01,
    0x00,
    0x01,
    0x09,
    0x00,
    0x01,
    0x01,
    0x88,
    0x00,

    0x65,
    0x6D,
    0x00,
    0x43,
    0x57,
    0x65,
    0x6E,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x4F,
    0xDF,
    0x01,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x13,
    0x00,

    Исходный код программы pegen

    0x3E,
    0x73,
    0x53,
    0x6F,
    0x72,
    0x00,
    0x65,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x6C,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x1B,
    0xA8,
    0x00,
    0x47,
    0x00,
    0x01,
    0x01,
    0x02,
    0x01,
    0x01,
    0x2F,
    0x11,
    0x01,
    0x16,
    0x09,
    0x37,
    0x01,
    0x00,
    0x00,
    0x00,
    0x1F,

    0x00,
    0x63,
    0x79,
    0x6E,
    0x69,
    0x52,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x0A,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x2D,
    0x8A,
    0x00,
    0x04,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,

    0x63,
    0x6F,
    0x73,
    0x73,
    0x74,
    0x65,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x48,
    0x6F,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x61,
    0x85,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x28,
    0x00,
    0x14,
    0x1A,
    0x41,
    0x05,
    0x00,
    0x00,
    0x01,
    0x00,
    0x00,

    0x61,
    0x72,
    0x74,
    0x6F,
    0x65,
    0x61,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x86,
    0x73,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x22,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,
    0x00,

    289

    290

    0x00, 0x00, 0x00, 0x00

    }

    fclose(f);
    return 0;

    make_file(f, ¶ms);

    if(params.Type == EXE_TYPE){
    printf(“File: hello.exe generated\n”);
    f = fopen(“hello.exe”,”wb”);
    }
    else{
    printf(“File: hello.dll generated\n”);
    f = fopen(“hello.dll”,”wb”);
    }

    params.FileAlignment = 0x1000;
    params.SizeOfMetadata = sizeof(metadata);
    params.SizeOfCilCode = sizeof(cilcode);
    params.ImageBase = 0x400000;
    params.EntryPointToken = 0x06000001;
    params.Type = EXE_TYPE;
    params.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI;
    params.metadata = metadata;
    params.cilcode = cilcode;

    struct INPUT_PARAMETERS params;

    int main()
    {
    FILE *f;

    // CIL CODE
    extern unsigned char cilcode[] = {
    0x56, 0x72, 0x01, 0x00, 0x00, 0x70, 0x28, 0x02,
    0x00, 0x00, 0x0A, 0x28, 0x01, 0x00, 0x00, 0x0A,
    0x28, 0x02, 0x00, 0x00, 0x0A, 0x2A
    };

    };

    CIL и системное программирование в Microsoft .NET

    291

    System;
    System.Collections;
    System.Reflection;
    System.Reflection.Emit;

    private OpCode code;

    #region Private members

    public override string ToString()
    {
    return String.Format(“ IL_{0:X8}: “,Offset) +
    Code.ToString().PadRight(10) + “ “ + formatOperand();
    }

    public object Operand
    {
    get { return operand; }
    set { operand = value; }
    }

    public int Offset { get { return offset; } }

    public OpCode Code { get { return code; } }

    public class Instruction
    {
    public Instruction(OpCode code, int offset)
    {
    this.code = code;
    this.offset = offset;
    this.operand = null;
    }

    using
    using
    using
    using

    Исходный код программы CilCodec, выполняющей кодирование/декодирование потока инструкций языка CIL, располагается в файле
    CilCodec.cs:

    Приложение Б.
    Исходный код программы CilCodec

    Исходный код программы CilCodec

    292

    #endregion

    }

    case OperandType.InlineI: /* int32 */
    ins.Operand = BitConverter.ToInt32(cilStream,offset);
    offset += 4;

    default: /* token */
    s = String.Format(“{0:X8}”,operand);
    return “(“+s.Substring(0,2)+”)”+s.Substring(2);

    293

    case OperandType.ShortInlineBrTarget: /* int8 */
    ins.Operand = offset + 1 +
    (sbyte)cilStream[offset];
    offset++;
    break;

    switch (code.OperandType)
    {
    case OperandType.InlineNone: /* None */
    break;

    Instruction ins = new Instruction(code,offset);
    offset += code.Size;

    while (offset < cilStream.Length)
    {
    OpCode code;
    short s2 = cilStream[offset];
    if (s2 == 0xFE)
    {
    byte s1 = cilStream[offset+1];
    code = (OpCode)(codes[((s2 << 8) | s1)]);
    }
    else
    code = (OpCode)(codes[s2]);

    public class CilCodec
    {
    public static Instruction[] DecodeCil(byte[] cilStream)
    {
    int offset = 0;
    ArrayList instructions = new ArrayList();

    }

    }

    Исходный код программы CilCodec

    case OperandType.InlineSwitch: /* switch */
    s = “(“;
    foreach (int target in (int[])operand)
    s += String.Format(“IL_{0:X8}”,target) + “ “;
    return s + “)”;

    case OperandType.ShortInlineR: /* float32 */
    case OperandType.InlineR: /* float64 */
    return String.Format(“{0:e}”,operand);

    case OperandType.ShortInlineVar: /* unsigned int8 */
    case OperandType.InlineVar: /* unsigned int16 */
    return String.Format(“{0:d}”,operand);

    case OperandType.ShortInlineI: /* int8 */
    return String.Format(“0x{0:X2}”,operand);

    case OperandType.InlineI8: /* int64 */
    return String.Format(“0x{0:X16}”,operand);

    case OperandType.InlineI: /* int32 */
    return String.Format(“0x{0:X8}”,operand);

    case OperandType.ShortInlineBrTarget: /* int8 */
    case OperandType.InlineBrTarget: /* int32 */
    return String.Format(“IL_{0:X8}”,operand);

    switch (code.OperandType)
    {
    case OperandType.InlineNone: /* None */
    return “”;

    private string formatOperand()
    {
    string s;

    private object operand;
    private int offset;

    CIL и системное программирование в Microsoft .NET

    294

    case OperandType.InlineSwitch: /* switch */
    uint num =

    case OperandType.InlineR: /* float64 */
    ins.Operand =
    BitConverter.ToDouble(cilStream,offset);
    offset += 8;
    break;

    case OperandType.ShortInlineR: /* float32 */
    ins.Operand =
    BitConverter.ToSingle(cilStream,offset);
    offset += 4;
    break;

    case OperandType.InlineVar: /* unsigned int16 */
    ins.Operand =
    BitConverter.ToUInt16(cilStream,offset);
    offset += 2;
    break;

    case OperandType.ShortInlineVar: /* unsigned int8 */
    ins.Operand = cilStream[offset++];
    break;

    case OperandType.ShortInlineI: /* int8 */
    ins.Operand = (sbyte)cilStream[offset++];
    break;

    case OperandType.InlineI8: /* int64 */
    ins.Operand =
    BitConverter.ToInt64(cilStream,offset);
    offset += 8;
    break;

    case OperandType.InlineBrTarget: /* int32 */
    ins.Operand = offset + 4 +
    BitConverter.ToInt32(cilStream,offset);
    offset += 4;
    break;

    break;

    CIL и системное программирование в Microsoft .NET

    foreach(Instruction ins in instructions)
    {
    short codeValue = ins.Code.Value;

    public static byte[] EncodeCil(Instruction[] instructions)
    {
    ArrayList Result = new ArrayList();

    }

    instructions.Add(ins);

    Instruction[] instrArray = new Instruction [instructions.Count];
    instructions.CopyTo(instrArray);
    return instrArray;

    }

    }

    295

    ins.Operand = ((int)b1 << 24) | ((int)b2 << 16) |
    ((int)b3 << 8) | (int)b4;
    break;

    default: /* token */
    byte b1 = cilStream[offset++],
    b2 = cilStream[offset++],
    b3 = cilStream[offset++],
    b4 = cilStream[offset++];

    ins.Operand = targets;
    break;

    for (int i = 0; i < num; i++)
    targets[i] += offset;

    BitConverter.ToUInt32(cilStream,offset);
    offset += 4;
    int[] targets = new int[num];
    for (int i = 0; i < num; i++)
    {
    targets[i] =
    BitConverter.ToInt32(cilStream,offset);
    offset += 4;
    }

    Исходный код программы CilCodec

    296

    case OperandType.ShortInlineI: /* int8 */
    operands = BitConverter.GetBytes((sbyte)operand);
    foreach( byte op in operands )
    {
    Result.Add(op);

    case OperandType.InlineI8: /* int64 */
    operands = BitConverter.GetBytes((Int64)operand);
    foreach( byte op in operands )
    {
    Result.Add(op);
    }
    break;

    case OperandType.InlineBrTarget: /* int32 */
    operands = BitConverter.GetBytes((Int32)operand);
    foreach( byte op in operands )
    {
    Result.Add(op);
    }
    break;

    case OperandType.InlineI: /* int32 */
    byte[] operands = BitConverter.GetBytes((Int32)operand);
    foreach( byte op in operands )
    {
    Result.Add(op);
    }
    break;

    case OperandType.ShortInlineBrTarget: /* int8 */
    Result.Add((byte)((int)ins.Operand – dataOffset – 1));
    break;

    object operand = ins.Operand;
    int dataOffset = ins.Offset + ins.Code.Size;
    switch (ins.Code.OperandType)
    {
    case OperandType.InlineNone: /* None */
    break;

    Result.Add((byte)codeValue);

    CIL и системное программирование в Microsoft .NET

    case OperandType.InlineSwitch: /* switch */
    int[] targets = (int[])ins.Operand;
    //Write length of switching table
    operands = BitConverter.GetBytes(targets.Length);
    int nextOpOffset = dataOffset;
    foreach( byte op in operands )
    {
    Result.Add(op);
    nextOpOffset++;
    }
    nextOpOffset += targets.Length*4;

    case OperandType.ShortInlineR: /* float32 */
    operands = BitConverter.GetBytes((Single)operand);
    foreach( byte op in operands )
    {
    Result.Add(op);
    }
    break;
    case OperandType.InlineR: /* float64 */
    operands = BitConverter.GetBytes((double)operand);
    foreach( byte op in operands )
    {
    Result.Add(op);
    }
    break;

    case OperandType.InlineVar: /* unsigned int16 */
    operands = BitConverter.GetBytes((UInt16)operand);
    foreach( byte op in operands )
    {
    Result.Add(op);
    }
    break;

    case OperandType.ShortInlineVar: /* unsigned int8 */
    Result.Add((byte)operand);
    break;

    }
    break;

    Исходный код программы CilCodec

    297

    298

    private static Hashtable codes;

    }

    foreach (FieldInfo field in fields)
    {
    OpCode opCode = (OpCode)(field.GetValue(null));
    codes.Add(opCode.Value,opCode);
    }

    Type opCodesType =
    Type.GetType(“System.Reflection.Emit.OpCodes”);
    FieldInfo[] fields = opCodesType.GetFields(
    (BindingFlags.Static | BindingFlags.Public));

    class Demo
    {

    }

    return Result.ToArray(typeof(byte)) as byte[];

    static CilCodec()
    {
    codes = new Hashtable();

    }

    }

    }

    default: /* token */
    Result.Add((byte) ((int)ins.Operand >> 24));
    Result.Add((byte) ((int)ins.Operand >> 16));
    Result.Add((byte) ((int)ins.Operand >> 8));
    Result.Add((byte) ((int)ins.Operand));
    break;

    foreach(int target in targets)
    {
    operands =
    BitConverter.GetBytes(target-nextOpOffset);
    foreach( byte op in operands )
    Result.Add(op);
    }
    break;

    CIL и системное программирование в Microsoft .NET

    static byte[] test1 = new byte[]
    {
    0x23, 0, 0, 0, 0, 0, 0, 0xF0, 0x3F,
    /* IL_0000: ldc.r8
    1.
    0x0A,
    /* IL_0009: stloc.0
    0x2B, 0x1B,
    /* IL_000a: br.s
    IL_0027
    0x03,
    /* IL_000c: ldarg.1
    0x18,
    /* IL_000d: ldc.i4.2
    0x5D,
    /* IL_000e: rem
    0x17,
    /* IL_000f: ldc.i4.1
    0x33, 0x0B,
    /* IL_0010: bne.un.s IL_001d
    0x03,
    /* IL_0012: ldarg.1
    0x17,
    /* IL_0013: ldc.i4.1
    0x59,
    /* IL_0014: sub
    0x10, 0x01,
    /* IL_0015: starg.s 1
    0x06,
    /* IL_0017: ldloc.0

    #region Sample instruction streams

    }

    299

    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */

    Console.WriteLine(areEqual ?
    “Codec is correct” : “Codec is incorrect”);
    Console.WriteLine(“---------------------------------------”);

    byte[] cilStream2 = CilCodec.EncodeCil(instrArray);
    bool areEqual = cilStream.Length == cilStream2.Length;
    for (int i = 0; areEqual && i < cilStream.Length; i++)
    areEqual = cilStream[i] == cilStream2[i];

    Instruction[] instrArray = CilCodec.DecodeCil(cilStream);
    foreach (Instruction ins in instrArray)
    Console.WriteLine(ins);

    static void testCodec(byte[] cilStream)
    {
    Console.WriteLine(“Decoded CIL stream:”);

    static void Main()
    {
    testCodec(test1);
    testCodec(test2);
    }

    Исходный код программы CilCodec

    300

    0x30, 0xE8,
    0x06,
    0x0B,
    0x2B, 0x00,
    0x07,
    0x2A

    /* IL_0018: ldarg.0
    /* IL_0019: mul
    /* IL_001a: stloc.0
    /* IL_001b: br.s
    IL_0027
    /* IL_001d: ldarg.1
    /* IL_001e: ldc.i4.2
    /* IL_001f: div
    /* IL_0020: starg.s 1
    /* IL_0022: ldarg.0
    /* IL_0023: ldarg.0
    /* IL_0024: mul
    /* IL_0025: starg.s 0
    /* IL_0027: ldarg.1
    /* IL_0028: brtrue.s IL_000c
    /* IL_002a: br.s
    IL_0038
    /* IL_002c: ldloc.0
    0, 0, 0, 0, 0x24, 0x40,
    /* IL_002d: ldc.r8
    10.
    /* IL_0036: div
    /* IL_0037: stloc.0
    /* IL_0038: ldloc.0
    0, 0, 0, 0, 0x14, 0x40,
    /* IL_0039: ldc.r8
    5.
    /* IL_0042: bgt.s
    IL_002c
    /* IL_0044: ldloc.0
    /* IL_0045: stloc.1
    /* IL_0046: br.s
    IL_0048
    /* IL_0048: ldloc.1
    /* IL_0049: ret

    static byte[] test2 = new byte[]
    {
    0x02,
    /* IL_0000: ldarg.0
    0x0B,
    /* IL_0001: stloc.1
    0x07,
    /* IL_0002: ldloc.1
    0x45, 0x03, 0, 0, 0,
    /* IL_0003: switch
    (
    0x02, 0, 0, 0, /*
    IL_0016,
    0x06, 0, 0, 0, /*
    IL_001a,
    0x0A, 0, 0, 0, /*
    IL_001e)
    0x2B, 0x0C,
    /* IL_0014: br.s
    IL_0022

    };

    0, 0,

    0xE2,
    0x0C,

    0x00,

    0x01,

    0x0A,

    0x5B,
    0x0A,
    0x06,
    0x23, 0, 0,

    0x02,
    0x5A,
    0x0A,
    0x2B,
    0x03,
    0x18,
    0x5B,
    0x10,
    0x02,
    0x02,
    0x5A,
    0x10,
    0x03,
    0x2D,
    0x2B,
    0x06,
    0x23,

    */
    */
    */
    */
    */

    */
    */
    */

    */
    */
    */
    */
    */
    */
    */

    */
    */
    */
    */

    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */

    CIL и системное программирование в Microsoft .NET

    }

    0x00,

    0x04,

    0x08,

    0x0C,

    #endregion

    };

    0x17,
    0x0A,
    0x2B,
    0x16,
    0x0A,
    0x2B,
    0x17,
    0x0A,
    0x2B,
    0x16,
    0x0A,
    0x2B,
    0x06,
    0x2A

    /*
    /*
    /*
    /*
    /*
    /*
    /*
    /*
    /*
    /*
    /*
    /*
    /*
    /*

    IL_0016:
    IL_0017:
    IL_0018:
    IL_001a:
    IL_001b:
    IL_001c:
    IL_001e:
    IL_001f:
    IL_0020:
    IL_0022:
    IL_0023:
    IL_0024:
    IL_0026:
    IL_0027:

    Исходный код программы CilCodec

    ldc.i4.1
    stloc.0
    br.s
    IL_0026
    ldc.i4.0
    stloc.0
    br.s
    IL_0026
    ldc.i4.1
    stloc.0
    br.s
    IL_0026
    ldc.i4.0
    stloc.0
    br.s
    IL_0026
    ldloc.0
    ret

    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */
    */

    301

    CIL и системное программирование в Microsoft .NET

    System;
    System.Globalization;
    System.Reflection.Emit;
    System.Text.RegularExpressions;

    public override string GenerateCS()
    {
    return “-(“+a.GenerateCS()+”)”;
    }

    public UnaryExpression(Expression a) { this.a = a; }

    class UnaryExpression: Expression
    {
    private Expression a;

    public abstract class Expression
    {
    public abstract string GenerateCS();
    public abstract void GenerateCIL(ILGenerator il);
    public abstract double Evaluate(double x);
    }

    using
    using
    using
    using

    B.1. Expr.cs

    Исходный код программы Integral, демонстрирующей различные
    способы динамической генерации кода на примере вычисления определенного интеграла, состоит из двух файлов:
    • Expr.cs
    Содержит парсер арифметических выражений и классы для дерева абстрактного синтаксиса.
    • Integral.cs
    Содержит классы для динамической генерации кода и вычисления интеграла.

    Приложение B.
    Исходный код программы Integral

    302

    303

    public override void GenerateCIL(ILGenerator il)

    public override string GenerateCS()
    {
    return “(“+a.GenerateCS()+”)”+opCs()+”(“+b.GenerateCS()+”)”;
    }

    private string opCs()
    {
    if (op.Equals(OpCodes.Add))
    return “+”;
    else if (op.Equals(OpCodes.Sub))
    return “-”;
    else if (op.Equals(OpCodes.Mul))
    return “*”;
    else
    return “/”;
    }

    public BinaryExpression(Expression a, Expression b, OpCode op)
    {
    this.a = a;
    this.b = b;
    this.op = op;
    }

    class BinaryExpression: Expression
    {
    private Expression a, b;
    private OpCode op;

    }

    public override double Evaluate(double x)
    {
    return -a.Evaluate(x);
    }

    public override void GenerateCIL(ILGenerator il)
    {
    a.GenerateCIL(il);
    il.Emit(OpCodes.Neg);
    }

    Исходный код программы Integral

    304

    a.GenerateCIL(il);
    b.GenerateCIL(il);
    il.Emit(op);

    class VariableExpression: Expression

    }

    public override double Evaluate(double x)
    {
    return value;
    }

    public override void GenerateCIL(ILGenerator il)
    {
    il.Emit(OpCodes.Ldc_R8,value);
    }

    public override string GenerateCS()
    {
    return value.ToString(new CultureInfo(“”));
    }

    public ConstExpression(double value) { this.value = value; }

    if (isAddOp())

    public Expression Parse()
    {
    checkToken();
    Expression result = null;
    OpCode op = OpCodes.Add;

    }

    token = Regex.Match(expr,
    “x|” +
    // identifier x
    REGEXP_NUMBER+”|”+ // floating-point numbers
    “\\+|\\-|\\*|/|”+ // arithmetic operators
    “\\(|\\)”
    // parens
    );

    public Parser(string expr)
    {

    public class Parser
    {
    private const string REGEXP_NUMBER = “[0-9]+(.[0-9])?”;
    private Match token;

    class ConstExpression: Expression
    {
    private double value;

    public override double Evaluate(double x)
    {
    return x;
    }

    public override void GenerateCIL(ILGenerator il)
    {
    il.Emit(OpCodes.Ldarg_1);
    }

    public override string GenerateCS()
    {
    return “x”;
    }

    public VariableExpression() { }

    }

    {

    Исходный код программы Integral

    }

    public override double Evaluate(double x)
    {
    if (op.Equals(OpCodes.Add))
    return a.Evaluate(x) + b.Evaluate(x);
    else if (op.Equals(OpCodes.Sub))
    return a.Evaluate(x) – b.Evaluate(x);
    else if (op.Equals(OpCodes.Mul))
    return a.Evaluate(x) * b.Evaluate(x);
    else
    return a.Evaluate(x) / b.Evaluate(x);
    }

    }

    {

    CIL и системное программирование в Microsoft .NET

    305

    306

    return result;

    if (isNumber())
    {

    private Expression parseFactor()
    {
    checkToken();
    Expression result = null;

    }

    while (token.Success && isMulOp())
    {
    OpCode op = token.Value.Equals(“*”) ?
    OpCodes.Mul : OpCodes.Div;
    token = token.NextMatch();
    result = new BinaryExpression(result,parseFactor(),op);
    }

    private bool isAddOp()
    {
    return Regex.IsMatch(token.Value,”\\+|\\-”);
    }

    private bool isNumber()
    {
    return Regex.IsMatch(token.Value,REGEXP_NUMBER);
    }

    private void throwError()
    {
    throw new Exception(“syntax error”);
    }

    private void checkToken()
    {
    if (!token.Success)
    throwError();
    }

    return result;

    private Expression parseTerm()
    {
    checkToken();
    Expression result = parseFactor();
    }

    token = token.NextMatch();

    }
    else
    throwError();

    if (! token.Value.Equals(“)”))
    throwError();

    }
    else if (token.Value.Equals(“x”))
    result = new VariableExpression();
    else if (token.Value.Equals(“(“))
    {
    token = token.NextMatch();
    result = Parse();

    IFormatProvider provider = new CultureInfo(“”);
    double val = Convert.ToDouble(token.Value,provider);
    result = new ConstExpression(val);

    Исходный код программы Integral

    }

    return result;

    while (token.Success && isAddOp())
    {
    op = token.Value.Equals(“-”) ? OpCodes.Sub : OpCodes.Add;
    token = token.NextMatch();
    result = new BinaryExpression(result,parseTerm(),op);
    }

    result = parseTerm();
    if (op.Equals(OpCodes.Sub))
    result = new UnaryExpression(result);

    {
    op = token.Value.Equals(“-”) ? OpCodes.Sub : OpCodes.Add;
    token = token.NextMatch();
    }

    CIL и системное программирование в Microsoft .NET

    307

    }

    CIL и системное программирование в Microsoft .NET

    private bool isMulOp()
    {
    return Regex.IsMatch(token.Value,”\\*|/”);
    }

    System;
    System.CodeDom.Compiler;
    System.Reflection;
    System.Reflection.Emit;
    System.Threading;
    Microsoft.CSharp;

    public override double Eval(double x)
    {
    return expr.Evaluate(x);
    }

    public InterpretingFunction(Expression expr)
    {
    this.expr = expr;
    }

    public class InterpretingFunction: Function
    {
    private Expression expr;

    public class TestFunction: Function
    {
    public override double Eval(double x)
    {
    return x * Math.Sin(x);
    }
    }

    public abstract class Function
    {
    public abstract double Eval(double x);
    }

    using
    using
    using
    using
    using
    using

    B.2. Integral.cs

    308

    ModuleBuilder module =

    AssemblyBuilder assembly =
    appDomain.DefineDynamicAssembly(
    assemblyName,
    AssemblyBuilderAccess.RunAndSave
    );

    static Function CompileToCIL(Expression expr)
    {
    AppDomain appDomain = Thread.GetDomain();
    AssemblyName assemblyName = new AssemblyName();
    assemblyName.Name = “f”;

    }

    Assembly assembly = compilerResults.CompiledAssembly;
    return assembly.CreateInstance(“FunctionCS”) as Function;

    CompilerResults compilerResults =
    compiler.CompileAssemblyFromSource(parameters,code);

    class MainClass
    {
    static Function CompileToCS(Expression expr)
    {
    ICodeCompiler compiler =
    new CSharpCodeProvider().CreateCompiler();
    CompilerParameters parameters = new CompilerParameters();
    parameters.ReferencedAssemblies.Add(“System.dll”);
    parameters.ReferencedAssemblies.Add(“Integral.exe”);
    parameters.GenerateInMemory = true;
    string e = expr.GenerateCS();
    string code =
    “public class FunctionCS: Function\n”+
    “{\n”+
    “ public override double Eval(double x)\n”+
    “ {\n”+

    return “+e+”;\n”+
    “ }\n”+
    “}\n”;

    }

    Исходный код программы Integral

    309

    310

    static double Integrate(Function f, double a, double b, int n)
    {

    }

    ConstructorInfo ctor = type.GetConstructor(new Type[0]);
    return ctor.Invoke(null) as Function;

    Type type = typeBuilder.CreateType();

    ILGenerator il = evalMethod.GetILGenerator();
    expr.GenerateCIL(il);
    il.Emit(OpCodes.Ret);

    MethodBuilder evalMethod =
    typeBuilder.DefineMethod(
    “Eval”,
    MethodAttributes.Public | MethodAttributes.Virtual,
    typeof(double),
    new Type[] { typeof(double) }
    );

    ILGenerator consIl = cons.GetILGenerator();
    consIl.Emit(OpCodes.Ldarg_0);
    consIl.Emit(OpCodes.Call,typeof(object).GetConstructor(new
    Type[0]));
    consIl.Emit(OpCodes.Ret);

    ConstructorBuilder cons =
    typeBuilder.DefineConstructor(
    MethodAttributes.Public,
    CallingConventions.Standard,
    new Type[] { }
    );

    }

    }

    Console.WriteLine(“Interpreter: “+s1+” (“+(t2-t1)+”)”);
    Console.WriteLine(“C#:
    “+s2+” (“+
    (t2_2-t2)+” + “+(t3-t2_2)+”)”);
    Console.WriteLine(“CIL:
    “+s3+” (“+
    (t3_2-t3)+” + “+(t4-t3_2)+”)”);

    DateTime t4 = DateTime.Now;

    DateTime t3 = DateTime.Now;
    Function f3 = CompileToCIL(expr);
    DateTime t3_2 = DateTime.Now;
    double s3 = Integrate(f3,a,b,num);

    DateTime t2 = DateTime.Now;
    Function f2 = CompileToCS(expr);
    DateTime t2_2 = DateTime.Now;
    double s2 = Integrate(f2,a,b,num);

    DateTime t1 = DateTime.Now;
    Function f1 = new InterpretingFunction(expr);
    double s1 = Integrate(f1,a,b,num);

    Parser parser = new Parser(s);
    Expression expr = parser.Parse();

    static void Main()
    {
    int num = 10000000;
    double a = 0.0;
    double b = 10.0;
    string s = “2*x*x*x+3*x*x+4*x+5”;

    return sum;

    for (int i = 0; i < n; i++)
    sum += h*f.Eval((i+0.5)*h);

    TypeBuilder typeBuilder =
    module.DefineType(
    “FunctionCIL”,
    TypeAttributes.Public | TypeAttributes.Class,
    typeof(Function)
    );
    }

    double h = (b-a)/n, sum = 0.0;

    Исходный код программы Integral

    assembly.DefineDynamicModule(“f.dll”, “f.dll”);

    CIL и системное программирование в Microsoft .NET

    311

    Верификатор
    Верификация кода
    Виртуальная система выполнения
    Виртуальная страница
    Висящие указатели
    Волокно
    Встроенные типы-значения
    Встроенный операнд

    Бинарные операции
    Блок обработки исключений
    Блок тела метода

    Безопасный код
    Безопасный фрагмент программы
    Библиотека рефлексии

    Ассемблер
    Атомарные операции

    Асинхронный ввод-вывод

    Асинхронные вызовы процедур

    7
    32-64, 66,
    86
    8, 28
    22
    149-152
    29-30
    3-4

    185

    185, 230

    6
    3
    173-174
    6
    6

    2
    153-156

    2
    5

    5

    207,
    257-260
    201,
    202-206,
    255-257
    Assembler
    123-131,
    Atomic operation,
    233-234,
    Interlocked operation
    260-261
    Safe code
    9
    Safe program fragment
    10
    Reflection API
    77, 156-162,
    167, 169
    Binary operations
    91-97, 165
    Exception Handling Block
    136, 137, 143
    Method Body Block
    135, 140, 142,
    145
    Verifier
    2, 9, 147-148
    Code verification
    9, 147-152
    Virtual Execution System – VES 6, 21-28
    Virtual page
    34-36
    Dangling pointers
    8
    Fiber
    194, 215-217
    Built-in value types
    13-14
    Inline operand
    84

    .NET Framework Class
    Library
    Back-end
    Common Language
    Runtime – CLR
    Front-end
    Metadata Unmanaged
    API
    Mono
    p-System
    peephole optimization
    Portable .NET
    Shared Source CLI
    (Rotor)
    SMP, Shared Memory
    Processor
    MPP, Massively Parallel
    Processors
    JIT Compiler
    PE File
    Absolute instruction address
    Automatic memory management
    Active method
    Verification algorithm
    Garbage collection algorithm
    ANDF (Architectural
    Neutral Distribution Format)
    APC, asynchronous
    procedure call
    Asynchronous I/O

    Предметный указатель

    CIL и системное программирование в Microsoft .NET

    JIT-компилятор
    PE-файл
    Абсолютный адрес инструкции
    Автоматическое управление памятью
    Активный метод
    Алгоритм верификации
    Алгоритм сборки мусора
    Архитектурно-нейтральный формат

    312

    Общая секция
    Общая система типов

    Монитор
    Неверифицируемые инструкции
    Невытесняющая многозадачность
    Недопустимый код
    Неперехватываемые ошибки
    Область локальных данных
    Обработка исключений
    Обработчик finally
    Обработчик с пользовательской фильтрацией
    Обработчик с фильтрацией по типу
    Общая инфраструктура языков

    Метаинструменты

    Менеджер виртуальной памяти
    Метаданные

    Вытесняющая многозадачность
    Генератор ANDF
    Граф потока управления
    Двойное освобождение
    Демаршалинг
    Дерево блоков
    Дескриптор потока
    Динамическая генерация кода
    Динамическая проверка типов
    Динамические библиотеки
    Дополнительный заголовок PE-файла
    Допустимый код
    Заголовок CLI
    Заголовок MS-DOS
    Заголовок PE-файла
    Заголовок секции
    Задача
    Запрещенные ошибки
    Защищенная область
    Защищенный блок
    Идеальный процессор
    Инсталлятор ANDF
    Квантование
    Контекст потока
    Корень метаданных
    Критические секции
    Куча
    Локальные переменные
    Маршалинг
    Маска сродства
    Межпроцессное взаимодействие

    Предметный указатель

    34
    6,47,64-72, 76,
    77, 151, 156
    2, 6, 64, 133,
    140, 152, 153,
    156, 162
    261-266
    83,151
    189, 190
    9
    10
    27-28
    116-123
    119
    119
    119
    5-7

    189, 191
    4
    2
    8
    75
    138-140
    194
    163
    11
    32
    43-47
    9
    52-53
    40-42
    42-43
    48-49
    193, 194
    10
    117
    135, 142
    196, 230
    4
    188, 196-198
    194
    67
    234-236, 261
    13, 23, 29
    27
    75
    230
    243-244

    Monitor
    Nonverifiable instructions
    Non-preemptive multitasking
    Illegal code
    Untrapped errors
    Local memory storage
    Exception handling
    Finally handler
    User-filtered handler
    Type-filtered handler
    Common Language
    Infrastructure – CLI
    Shared section, shared segment 249-250
    Common Type System (CTS)
    6, 9-20

    Metainstruments

    Preemptive multitasking
    ANDF Producer
    Control-flow graph
    Double free
    Demarshalling
    Block tree
    Thread descriptor
    Dynamic code production
    Dynamic type checking
    Dynamic Link Libraries – DLL
    PE Optional Header
    Legal code
    CLI Header
    MS-DOS Header
    PE Header
    Section Header
    Task
    Forbidden errors
    Protected area
    Protected block
    Ideal processor
    ANDF Installer
    Timeslice
    Thread context
    Metadata Root
    Critical Section
    Heap
    Local variables
    Marshalling
    Affinity mask
    IPC, Interprocess
    communication
    Virtual-memory manager
    Metadata

    313

    Сборка мусора
    Связывание функций

    Разделяемый сегмент
    Распаковка
    Сборка .NET

    Псевдонимы переменных
    Псевдоописатель
    Пул потоков

    Проецирование файла
    Процесс

    6, 82

    36
    229-230
    225-229,
    270-271
    Trapped errors
    10
    Enumerations
    18-19
    sheduller
    188
    User value types
    18-19
    I/O completion port
    201, 219-223
    Thread
    193, 209,
    213-215,
    251-254
    Instruction stream
    83-87
    Metadata streams
    67-68
    User mode thread
    194
    Kernel mode thread
    194
    Thread-safety functions
    212
    Premature free
    8
    Affinity mask
    230
    Thread priority
    189, 198-200,
    211
    File Mapping
    245-249
    Process
    193, 209,
    243-245
    Variable aliases
    181
    Pseudo-handle
    210-211
    Thread pool
    218-219,
    223-224,
    251-255
    Shared segment, shared section 249-250
    Unboxing
    20
    .NET assembly
    7, 77, 125,
    152, 157, 167
    Garbage collection
    7, 8, 28-31
    Binding
    39

    Memory-mapped files
    FLS, Fiber Local Storage
    TLS, Thread Local Storage

    6, 83-131
    241-242,
    266-268
    Kernel objects
    208-210,
    236-238, 266
    Alertable Waiting
    206-207, 237
    Waitable Timer
    243
    Security decriptor
    25
    Thread handle
    210-211
    Process handle
    210-211
    Optimizer
    2, 152, 162,
    173,
    Debugger
    2, 152
    Relative Virtual Address – RVA 38

    Common Language
    Specification – CLS
    Common Intermediate
    Language – CIL
    Mutex

    CIL и системное программирование в Microsoft .NET

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

    Перехватываемые ошибки
    Перечисления
    Планировщик
    Пользовательские типы-значения
    Порт завершения ввода-вывода
    Поток

    Отладчик
    Относительный виртуальный адрес
    элемента в памяти
    Отображаемые в память файлы
    Память, локальная для волокон
    Память, локальная для потоков

    Ожидание оповещения
    Ожидающий таймер
    Описатель безопасности
    Описатель потока
    Описатель процесса
    Оптимизатор

    Объекты ядра

    Объект исключительного владения

    Общий промежуточный язык

    Общая спецификация языков

    314

    Таблица импортируемых сборок
    Таблица импортируемых типов
    Таблица методов
    Таблица модулей
    Таблица определенных в сборке типов
    Таблица сборок
    Таблица членов импортируемых типов
    Таблицы метаданных
    Таймеры
    Типизированные ссылки
    Типы-значения
    Типы-интерфейсы
    Указатели
    Унарные арифметические операции
    Упаковка
    Управляемые данные
    Утечки памяти
    Физическая страница
    Фрагментация адресного пространства
    Цель перехода
    Языки со строгой проверкой
    Ячейки

    Совместимость по присваиванию
    Состояние виртуальной машины
    Состояние возврата
    Состояние метода
    Состояние нити
    Ссылочные типы
    Статическая проверка типов
    Стек вычислений

    Секция в PE-файле
    Семафор
    Системы с неоднородным
    доступом к памяти
    Смещение элемента в файле
    Событие

    Предметный указатель

    AssemblyRef table
    TypeRef table
    Method table
    Module table
    TypeDef table
    Assembly table
    MemberRef table
    Metadata table
    Timer
    Typed references
    Value types
    Interface types
    Pointers
    Unary operations
    Boxing
    Managed data
    Memory leaks
    Physical page
    External fragmentation
    Jump target
    Strongly checked languages
    Locations

    Assignment compatibility
    Virtual machine state
    Return state handler
    Method state
    Thread state
    Reference types
    Static type checking
    Evaluation stack

    File offset
    Event

    PE Section
    Semaphore
    NUMA, cc-NUMA

    38
    240-241
    186,
    230-231
    38
    238-239,
    266-268
    16
    21-23
    25
    22, 23-25
    22-23
    12, 14-16
    11
    14, 24,
    25-27, 85,
    122, 129,
    151, 182
    71
    72
    71
    70
    70-71
    69-70
    72
    68-70
    243, 271-272
    115
    12
    16
    19-20
    98
    20
    8
    8
    34
    8
    86
    11
    13, 16

    315

    Отпечатано с готовых диапозитивов на ФГУП ордена «Знак Почета»
    Смоленская областная типография им. В.И. Смирнова.
    Адрес: 214000, г. Смоленск, проспект им.Ю. Гагарина, д. 2.

    ООО «ИНТУИТ.ру»
    Интернет-Университет Информационных Технологий, www.intuit.ru
    123056, Москва, Электрический пер., 8, стр. 3.

    Санитарно-эпидемиологическое заключение о соответствии санитарным
    правилам №77.99.02.953.Д.006052.08.03 от 12.08.2003

    Формат 60x90 1/16. Усл. печ. л. 20,5. Бумага офсетная.
    Подписано в печать 20.10.2005. Тираж 2000 экз. Заказ № .

    Литературный редактор С. Перепелкина
    Корректор Ю. Голомазова
    Компьютерная верстка Ю. Волшмид
    Обложка М. Автономова

    А.В. Макаров, С.Ю. Скоробогатов, А.М. Чеповский
    Common Intermediate Language и системное
    программирование в Microsoft .NET

    ОСНОВЫ ИНФОРМАТИКИ И МАТЕМАТИКИ