Ч А С Т Ь 3. TURBO PASCAL ВНУТРИ. ГЛАВА 16. ПАМЯТЬ. Эта глава описывает в деталях способы использования памяти программами на Turbo Pascal. Мы посмотрим карту памяти программ на Turbo Pascal, внутренние форматы данных, монитор кучи и прямой доступ к памяти. Карта памяти Turbo Pascal. Рис. 16.1 представляет распределение памяти для программы на Turbo Pascal. Префикс сегмента программы (Program Segment Prefix - PSP) - это 256-ти байтовая область, создаваемая DOS при загрузке программы. Адрес сегмента PSP хранится в переменной PrefixSeg. Каждый модуль (и главная программа и каждый модуль) имеет свой кодовый сегмент. Главная программа занимает первый кодовый сегмент; кодовые сегменты, которые следуют за ним, занимают модули (в порядке, обратном тому, как они следовали в операторе uses), и последний кодовый сегмент занимает библиотека времени выполнения (модуль System). Размер одного кодового сегмента не может превышать 64К, но общий размер кода ограничен только имеющейся памятью. Рис. 16.1. Распределение памяти в Turbo Pascal. Верхняя граница памяти DOS HeapEnd ──Ў┌────────────────────────────┐ │ │ │ свободная память │ │ │ HeapPtr ──Ў│............................│ │ куча (растет вверх) │ HeapOrg ──Ў├────────────────────────────┤ў── OvrHeapEnd │ оверлейный буфер │ ├────────────────────────────┤ў── OvrHeapOrg │ стек (растет вниз) │ SSeg:SPtr ──Ў│............................│ │ свободный стек │ SSeg:0000 ──Ў├────────────────────────────┤ │ глобальные переменные │ │............................│ў───────┐ │ типированные константы │ │ DSeg:0000 ──Ў├────────────────────────────┤ │ │ кодовый сегмент │ │ │ модуля System │ │ │............................│ │ │ кодовый сегмент │ │ │  первого модуля   │ │ │............................│ │ └────────────────────────────┘ │ . кодовый сегмент . содержат образ . других   модулей . .EXE файла ┌────────────────────────────┐ │ │............................│ │ │ кодовый сегмент │ │ │ последнего  модуля │ │ ├────────────────────────────┤ │ │ кодовый сегмент │ │ │ главной программы  │ │ ├────────────────────────────┤ў───────┘ │ префикс сегмента программы │ │ (PSP) │ PrefixSeg ──Ў└────────────────────────────┘ Нижняя граница памяти DOS Сегмент данных (адресуемый через DS) содержит все глобальные переменные и затем все типированные константы. Регистр DS никогда не изменяется во время выполнения программы. Размер сегмента данных не может превышать 64К. При запуске программы регистр сегмента стека (SS) и указатель стека (SP) устанавливаются так, что SS:SP указывает на первый байт после сегмента стека. Регистр SS никогда не изменяется во время выполнения программы, а SP может передвигаться вниз пока не достигнет конца сегмента. Размер стекового сегмента не может превышать 64К; размер по умолчанию - 16К, он может быть изменен директивой компилятора $M. Буфер оверлеев используется стандартным модулем Overlay для хранения оверлейного кода. Размер оверлейного буфера по умолчанию соответствует размеру наибольшего оверлея в программе; если в программе нет оверлеев, размер буфера оверлеев равен 0. Размер буфера оверлеев может быть увеличен с помощью вызова программы OvrSetBuf модуля Overlay; в этом случае размер кучи соответственно уменьшается, смещением вверх HeapOrg. Куча хранит динамические переменные, то есть переменные, распределенные через вызов стандартных процедур New и GetMem. Куча занимает всю или часть свободной памяти, оставшейся после загрузки программы. Фактически размер кучи зависит от минимального и максимального значений кучи, которые могут быть установлены директивой компилятора $M. Размер кучи никогда не будет меньше минимального значения и не превысит максимального. Если в системе нет памяти равного минимальному значению, программа не будет выполняться. Минимальное значение кучи по умолчанию равно 0 байт, максимальное - 640К; это означает, что по умолчанию куча будет занимать всю оставшуюся память. Управление кучей осуществляет монитор кучи (который является частью библиотеки времени выполнения Turbo Pascal). Он детально описан в следующем разделе. Монитор кучи. Куча имеет стековую структуру, растущую от нижних адресов памяти в сегменте кучи. Нижняя граница кучи хранится в переменной HeapOrg, а вершина кучи, соответствующая нижней границе свободной памяти, хранится в переменной HeapPtr. Каждый раз, когда динамическая переменная распределяется в куче (через New или GetMem), монитор кучи передвигает HeapPtr вверх на размер этой переменной, ставя динамические переменные одну за другой. HeapPtr нормализуется после каждой операции, устанавливая смещение в диапазоне от $0000 до $000F. Максимальный размер переменной, который может быть распределен в куче, равен 65519 байт ($10000 - $000F), поскольку каждая переменная должна полностью находиться в одном сегменте. Освобождение памяти. Динамические переменные, хранящиеся в куче, удаляются одним из двух путей: (1) через Dispose или FreeMem (2) через Mark и Release. Простейший способ - это с Mark и Release, если были выполнены следующие операторы: New(Ptr1); New(Ptr2); Mark(P); New(Ptr3); New(Ptr4); New(Ptr5); состояние кучи будет таким, как на рисунке 16.2 Рис.16.2: Освобождение памяти с помощью Mark и Release. Ptr1 ──Ў┌──────────────────────────┐ Нижняя граница памяти │ содержимое Ptr1^ │ Ptr2 ──Ў├──────────────────────────┤ │ содержимое Ptr2^ │ Ptr3 ──Ў├──────────────────────────┤ │ содержимое Ptr3^ │ Ptr4 ──Ў├──────────────────────────┤ │ содержимое Ptr4^ │ Ptr5 ──Ў├──────────────────────────┤ │ содержимое Ptr5^ │ HeapPtr ──Ў├──────────────────────────┤ │ │ Верхняя граница HeapEnd ──Ў└──────────────────────────┘ памяти Оператор Mark(P) помечает состояние кучи перед распределением Ptr3 (сохранением текущего HeapPtr в P). Если выполнить оператор Release(P), то состояние кучи станет как на рисунке 16.3, эффективно освобождая все указатели, распределенные после вызова Mark. Рис.16.3. Распределение кучи после выполнения Release(P). Ptr1 ──Ў┌────────────────────────────┐ Нижняя граница памяти │ содержимое Ptr1^ │ Ptr2 ──Ў├────────────────────────────┤ │ содержимое Ptr2^ │ HeapPtr ──Ў├────────────────────────────┤ │ │ │ │ │ │ Верхняя граница HeapEnd ──Ў└────────────────────────────┘ памяти Примечание: Выполнение оператора Release(HeapOrg) полностью очищает всю кучу, поскольку HeapOrg указывает на нижнюю границу кучи. Для программ, которые освобождают указатели в порядке, точно обратном порядку их распределения, процедуры Mark и Release очень эффективны. Однако большинство программ распределяют и освобождают указатели случайным образом, что требует более сложной техники управления, что и реализуется процедурами Dispose и FreeMem. Эти процедуры позволяют программе освобождать любой указатель в любое время. Когда динамическая переменная, которая не является последней (верхней) в куче, освобождается с помощью Dispose или FreeMem, куча становится фрагментированной. Если была выполнена та же последовательность операторов, а затем Dispose(Ptr3) - в середине кучи появится дырка (рис 16.4). Рис.16.4. "Дырка" в куче. Ptr1 ──Ў┌────────────────────────────┐ Нижняя граница памяти │ содержимое Ptr1^ │ Ptr2 ──Ў├────────────────────────────┤ │ содержимое Ptr2^ │ ├────────────────────────────┤ │ │ Ptr4 ──Ў├────────────────────────────┤ │ содержимое Ptr4^ │ Ptr5 ──Ў├────────────────────────────┤ │ содержимое Ptr5^ │ HeapPtr ──Ў├────────────────────────────┤ │ │ Верхняя граница HeapEnd ──Ў└────────────────────────────┘ памяти Если сейчас выполнить New(Ptr3), то он снова займет ту же область памяти. С другой стороны, выполнение Dispose(Ptr4) увеличит свободный блок, поскольку Ptr3 и Ptr4 были соседними блоками (рис 16.5). Рис.16.5. Увеличение свободного блока. Ptr1 ──Ў┌────────────────────────────┐ Нижняя граница памяти │ содержимое Ptr1^ │ Ptr2 ──Ў├────────────────────────────┤ │ содержимое Ptr2^ │ ├────────────────────────────┤ │ │ │ │ │ │ Ptr5 ──Ў├────────────────────────────┤ │ содержимое Ptr5^ │ HeapPtr ──Ў├────────────────────────────┤ │ │ Верхняя граница HeapEnd ──Ў└────────────────────────────┘ памяти Наконец, выполнение Dispose(Ptr5) во-первых создаст еще больший свободный блок, а затем переместит HeapPtr вниз. Кроме того, этот свободный блок сольется со свободной памятью кучи, так как последний значащий указатель сейчас - Ptr2 (рисунок 16.6). Рис.16.6. Удаление свободного блока. Ptr1 ──Ў┌────────────────────────────┐ Нижняя граница памяти │ содержимое Ptr1^ │ Ptr2 ──Ў├────────────────────────────┤ │ содержимое Ptr2^ │ HeapPtr ──Ў├────────────────────────────┤ │ │ │ │ │ │ │ │ Верхняя граница HeapEnd ──Ў└────────────────────────────┘ памяти Куча сейчас в таком же состоянии, как была после выполнения Release(P) (рис.16.2). Однако, свободные блоки, создаваемые и разрушаемые в этом режиме, фиксировались для последующего использования. Список свободных блоков. Адреса и размеры свободных блоков, создаваемых Dispose и FreeMem хранятся в списке свободных блоков, который растет сверху вниз от верхней границы сегмента кучи. Когда распределяется динамическая переменная, до размещения ее в куче проверяется список свободных блоков. Если есть свободный блок подходящего размера (размер больше или равен размеру распределяемого блока), то он используется. Примечание: Процедура Release всегда очищает список свободных блоков, что заставляет монитор кучи "забыть" о всех свободных блоках, которые могли быть ниже указателя кучи. Если Вы смешиваете вызовы Mark и Release с вызовами Dispose и FreeMem, то Вы должны быть уверены, что таких свободных блоков не существует. Переменная FreeList из модуля System указывает на первый свободный блок в куче. Этот блок содержит указатель на следующий свободный блок, который содержит указатель на следующий свободный блок и т.д. Последний свободный блок содержит указатель на вершину кучи (т.е. на положение, указываемое HeapPtr). Если в списке свободный блоков нет свободных блоков, FreeList равна HeapPtr. Формат первых 8 байт свободного блока определяется типом TFreeRec: type PFreeRec = ^TFreeRec; TFreeRec = record Next : PFreeRec; Size : Pointer; end; Поле Next указывает на следующий свободный блок, или на то же положение, что и HeapPtr, если блок - последний свободный блок. Поле Size хранит размер свободного блока. Значение Size не обычное 32-битовое значение; скорее это нормализованное значение указателя с числом свободных параграфов (16-байтовых блоков) в старшем слове и числом свободных байт (от 0 до 15) в младшем слове. Следующая функция BlockSize преобразует значение поля Size в нормальное значение LongInt: function BlockSize(Size: Pointer): Longint; type PtrRec = record Lo, Hi: Word; end; begin BlockSize := Longint(PtrRec(Size).Hi) * 16 + PtrRec(Size).Lo; end; Чтобы гарантировать, что всегда будет место для TFreeRec в начале свободного блока, монитор кучи округляет размер КАЖДОГО блока, распределяемого New или GetMem до 8-байтовой границы. Так для блоков, размером в 1..8 байт распределяется 8 байт, для блоков, размером 9..16 распределяется 16 байт и т.д. Это может показаться расточительным использованием памяти и в действительности будет таким, если каждый блок будет размером в 1 байт. Однако, обычно блоки имеют больший размер и относительный размер неиспользуемого пространства невелик. Более того, и это очень важно, 8-байтный коэффициент гранулированности гарантирует, что распределение и освобождение случайных блоков небольших размеров, как например для строк с переменной длиной в программах обработки текста, не будет сильно фрагментировать кучу. Например, допустим 50 -байтный блок распределяется и освобождается, становясь элементом в списке свободных блоков. Этот блок будет округлен до 56 байт (7*8) и последующий запрос на распределение от 49 до 56 байт будет полностью использовать этот блок, не оставляя от 1 до 7 байт свободными, которые будут фрагментировать кучу. Переменная HeapError. Переменная HeapError позволяет вам установить функцию обработки ошибок кучи, которая вызывается, когда монитор кучи не может обработать запрос на распределение памяти. HeapError указывает на функцию со следующим заголовком: function HeapFunc(Size: Word): Integer; far; Заметим, что директива компилятора far устанавливает дальнюю модель вызова для функции обработки ошибок. Функция обработки устанавливается присваиванием ее адреса переменной HeapError: HeapError := @HeapFunc; Функция обработки ошибок кучи вызывается, когда New или GetMem не могут обработать запрос. Параметр Size содержит размер блока, который не мог быть распределен и функция обработки должна попытаться освободить блок размером не меньшим этого. В зависимости от результата, функция обработки возвращает 0, 1 или 2. В случае 0 будет немедленно возникать ошибка времени выполнения в программе. В случае 1 вместо аварийного завершения программы New или GetMem возвращают указатель, равный Nil. Наконец, 2 означает успех и повторяет запрос на распределение памяти (который может опять вызвать функцию обработки ошибок). Стандартная функция обработки ошибок кучи всегда возвращает 0, что приводит к аварийному завершению программы, если New или GetMem не могут быть выполнены. Для многих программ будет удобна следующая функция обработки ошибок: function HeapFunc(Size: Word): Integer; far; begin HeapFunc := 1; end; Когда эта функция установлена, New и GetMem будут возвращать nil при невозможности распределить память, не приводя к аварийному завершению программы. Примечание: Вызов функции обработки ошибок кучи с параметром Size = 0 указывает, что для удовлетворения запроса на распределение монитор кучи расширил кучу, передвигая HeapPtr вверх. Это происходит, когда нет свободных блоков в списке свободных блоков, или когда все свободные блоки слишком малы для запроса на распределение. Вызов с Size = 0 не указывает на ошибку, поскольку существует достаточное место для распределения между HeapPtr и HeapEnd. Скорее это указывает, что неиспользованное пространство HeapPtr было уменьшено и монитор кучи игнорирует возвращаемое значение от вызовов этого типа. Внутренние форматы данных. Целые типы. Формат, используемый для представления целых чисел, зависит от их минимальной и максимальной границ: - граница в диапазоне -128 .. 127 (Shortint) - переменная хранится как знаковый байт. - граница в диапазоне 0 .. 255 (Byte) - переменная хранится как беззнаковый байт. - граница в диапазоне -32768 .. 32767 (Integer) - переменная хранится как знаковое слово. - граница в диапазоне 0 .. 65535 (Word) - переменная хранится как беззнаковое слово. - в других случаях переменная хранится как знаковое двойное слово (Longint). Символьный тип. Тип Char или поддиапазон типа Char хранится как беззнаковый байт. Логический тип. Тип Вoolean хранится как байт и принимает значения 0 (False) или 1 (True). Перечислимые типы. Перечислимый тип хранится в беззнаковом байте, если перечисление имеет 256 или меньше значений, в противном случае он хранится как беззнаковое слово. Вещественные типы. Вещественные типы (Real, Single, Double, Extended и Comp) хранят двоичное представление знака (+/-), экспоненты и мантиссы. Представление числа имеет вид : +/- мантисса х 2**экспонента где мантисса имеет один бит слева от точки двоичного числа (то есть 0 <= мантисса < 2). Примечание: На следующем рисунке msb означает наибольший значащий бит и lsb означает наименьший. Крайне левые элементы хранятся на больших адресах. Например, для значения типа Real, е - хранится в первом байте, f - в следующих 5 байтах и s - в наибольшем значащем бите последнего байта. Тип Real. 6-ти байтовый (48-ми битовый) тип Real делится на три поля: 1 39 8 ┌───┬──────..───────┬────────┐ │ s │ f │ e │ └───┴──────..───────┴────────┘ msb lsb msb lsb Значение числа v определяется как, если 0 < e <= 255, то v = (-1)**s x 2**(e-129) x (1.f). если е = 0, то v = 0. Примечание: Тип Real не может хранится денормализованным, NaN и неопределенным. Денормализованное становится 0, когда запоминается в Real. NaN, и неопределенное дают ошибку переполнения, при попытке запомнить их в Real. Тип Single. 4-х байтовый (32-х битный) тип Single делится на три поля: 1 8 23 ┌───┬──────┬───────..─────────┐ │ s │ e │ f │ └───┴──────┴───────..─────────┘ msb lsb msb lsb Значение числа v определяется как, если 0 < e < 255, то v = (-1)**s x 2**(e-127) x (1.f). если e = 0 и f <> 0, то v = (-1)**s x 2**(-126) x (0.f). если e = 0 и f = 0, то v = (-1)**s x 0 если e = 255 и f = 0, то v = (-1)**s x Inf. если e = 255 и f <> 0, то v = NaN. Тип Double. 8-х байтовый (64-х битный) тип Double делится на три поля: 1 11 52 ┌───┬──────┬───────..────────┐ │ s │ e │ f │ └───┴──────┴───────..────────┘ msb lsb msb lsb Значение числа v определяется как, если 0 < e < 2047, то v = (-1)**s x 2**(e-1023) x (1.f). если e = 0 и f <> 0, то v = (-1)**s x 2**(-1022) x (0.f). если e = 0 и f = 0, то v = (-1)**s x 0 если e = 2047 и f = 0, то v = (-1)**s x Inf. если e = 2047 и f <> 0, то v = NaN. Тип Extended. 10-х байтовый (80-х битный) тип Extended делится на три поля: 1 15 1 63 ┌───┬────────┬───┬────────..───────┐ │ s │ e │ i │ f │ └───┴────────┴───┴────────..───────┘ msb lsb msb lsb Значение числа v определяется как, если 0 <= e < 32767, то v = (-1)**s x 2**(e-16383) x (1.f). если e = 32767 и f = 0, то v = (-1)**s x Inf. если e = 32767 и f <> 0, то v = NaN. Тип Comp. 8-х байтовый (64-х битный) тип Comp делится на два поля: 1 63 ┌───┬───────────..──────────────┐ │ s │ d │ └───┴───────────..──────────────┘ msb lsb Значение числа v определяется как, если s = 1 и d = 0, то v = NaN. Иначе v - двоичное дополнение 64-битного значения. Тип Pointer. Тип Pointer хранится как двойное слово с частью смещения в младшем слове и с сегментной частью в старшем слове. Значение указателя nil хранится как 0 в обоих словах. Строковый тип. Длина строкового типа в байтах равна его максимальной длине плюс 1. Первый байт содержит текущую длину строки и следующие байты содержат символы строки. Байт длины и символы представлены беззнаковыми значениями. Максимальная длина строки 255 символов плюс байт длины (string[255]). Тип множества. Множество (set) - это битовый массив, где каждый бит показывает - есть данный элемент в множестве или нет. Максимальное число элементов в множестве - 256, поэтому множество никогда не занимает более 32 байт. Число байт занимаемых множеством вычисляется как : ByteSize = (Max div 8) - (Min div 8) + 1 где Min и Max - нижняя и верхняя границы базового типа этого множества. Элемент Е занимает байт с номером: ByteNumber = ( E div 8) - (Min div 8) и позиция бита внутри байта: BitNumber = E mod 8 где Е представляет порядковое значение элемента. Тип массива. Массив хранится как непрерывная последовательность переменных того типа, из которых объявлен массив. Компонента с наименьшим индексом хранится по наименьшему адресу памяти. Многомерный массив хранится так, что наиболее правый индекс увеличивается первым. Тип запись. Поля записи хранятся как непрерывная последовательность переменных. Первое поле хранится по наименьшему адресу памяти. Если запись содержит вариантные части, то каждая вариантная часть начинается с одного и того же адреса. Файловый тип. Файловый тип представлен как запись. Типированные и нетипированные файлы занимают 128 байт и имеют следующую структуру: type FileRec = record Handle: Word; Mode: Word; RecSize: Word; Private: array[1..26] of Byte; UserData: array[1..16] of Byte; Name: array[0..79] of Char; end; Текстовый файл занимает 256 байт и имеет структуру: type TextBuf = array[0..127] of Char; TextRec = record Handle: Word; Mode: Word; BufSize: Word; Private: Word; BufPos: Word; BufEnd: Word; BufPtr: ^TextBuf; OpenFunc: Pointer; InOutFunc: Pointer; FlushFunc: Pointer; CloseFunc: Pointer; UserData: array[1..16] of Byte; Name: array[0..79] of Char; Buffer: TextBuf; end; Hаndle содержит обработчик файла, возвращаемый из DOS. Поле Mode может принимать одно из значений: const fmClosed = $D7B0; fmInput = $D7B1; fmOutput = $D7B2; fmInOut = $D7B3; fmClosed означает, что файл закрыт. fmInput означает, что текстовый файл был открыт как входной (Reset), fmOutput - как выходной (Rewrite). fmInOut означает, что файловая переменная типированного или нетипированного файла была открыта на чтение или запись. Все другие значения означают, что файловая переменная не была инициализирована (с помощью Assign). Поле UserData никогда не используется Turbo Pascal и может свободно использоваться программистом. Name содержит имя файла, который представлен как последовательность символов, завершенная нулевым символом (#0). Для типированных и нетипированных файлов RecSize содержит длину записи в байтах, и поле Private не используется, но зарезервировано. Для текстовых файлов BufPtr - указатель на буфер длиной BufSize байт, BufPos - индекс следующего символа в буфере для чтения или записи, BufEnd - количество оставшихся символов в буфере. OpenFunc, InOutFunc, FlushFunc, CloseFunc - указатели на программы В/В, управляющие файлом. В разделе "Драйвер устройства текстового файла" главы 19 это описано более подробно. Процедурный тип. Процедурный тип хранится как двойное слово с частью смещения в слове с младшим адресом и сегментной частью в слове с большим адресом. Прямой доступ к памяти. Turbo Pascal реализует три предопределенных массива Mem, MemW, MemL, которые используются для прямого доступа к памяти. Каждый элемент Mem - один байт, каждый элемент MemW - одно слово и каждый элемент MemL - двойное слово (LongInt). Массив Mem использует специальный синтаксис для индекса: два выражения типа слово, разделенные двоеточием, используются для указания сегмента и смещения памяти. Примеры: Mem [$0040: $0049] := 7; Data := MemW [Seg(V):Ofs(V)]; MemLong := MemL[64:3*4]; Первый оператор записывает значение 7 в байт $0040:$0049. Второй оператор пересылает значение типа слово, записанное в первых двух байтах переменной V, в переменную Data. Третий оператор пересылает значения типа Longint, хранящиеся в $0040:$000C, в переменную MemLong.