Последний раз обновлено 14.02.014
Унифицированное адресное пространство
Портируемая программа нуждается не только в унифицированных целых типах, но и в унифицированном адресном пространстве. Проблема тут та же самая, что была и для типов - невозможно написать программу, адресное пространство которой имеет неизвестные на момент написания размеры, структуру и свойства.
Повторим, что в хорошо структурированных программах плоская модель памяти не очень полезна. В данном случае возможность портируемости программы не потребует отказа от более качественной технологии программирования, поэтому современная абстрактная машина С не может не поддерживать технологию модульного программирования.
Логически, модульная программа разделяет память на сегменты, которые, в той или иной их аппаратной реализации, являются независимыми, изолированными друг от друга адресными пространствами модулей; поэтому программист должен задавать не размер адресного пространства вообще, а размер конкретного адресного пространства конкретного модуля, а модуль может представлять собой компилируемый файл, класс, отдельную функцию или блок данных.
Также как это было для целых типов, только от программиста зависит обоснованность выбора в каждом случае размера требуемого адресного пространства, чем он выбран меньше, тем лучше портируемая программа будет исполняться на конкретных машинах с аппаратным адресным пространством малого размера.
В результате, унифицированное адресное пространство абстрактной машины С всегда описывается парой {сегмент(селектор):смещение(указатель)}. Эта пара и есть адрес.
В абстрактной машине С "адрес" это не то же самое, что "указатель", также несмотря на применение терминов из архитектуры x86, по другому компоненты унифицированного адреса не назовешь, а "селектор" и "сегмент" не относятся к сегментным регистрам или дескрипторным таблицам процессоров x86, вся похожесть получается только оттого, что архитектура x86 имеет элементы аппаратной поддержки модулей.
Оба компонента адресной пары независимо друг от друга виртуализируются подобно унифицированным целым типам, в первом приближении, это абстрактные типы (классы):
В первом приближении, для типов selXX, ptrXX:
Унифицированное адресное пространство
Помимо того, что унифицированное адресное пространство можно охарактеризовать размерами сегментов модулей, оно имеет довольно сложную логическую структуру.
На схеме унифицированного адресного пространства, деление на сегменты принадлежащие модулям представляются горизонтальными линиями (модули образуют вертикальные связи), а вертикальные линии в пределах каждого модуля разделяют логические сегменты модуля (логическая структура модуля образует горизонтальные связи).
Сложность логической структуры обусловлена несовместимостью операций, которые можно совершать над разными логическими сегментами, эта разница дает нам возможность аппаратного контроля правильности таких операций, а наличие аппаратного контроля делает систему более устойчивой к сбоям отдельных модулей. Поддержка аппаратного контроля современных процессоров это то, что мы хотим добавить в абстрактную машину С.
Хорошо известно логическое деление на сегменты кода и данных: данные нельзя исполнять и т.п. Аппаратно подобное деление уже во времена создания языка С поддерживалось "битом защиты от выполнения". Еще хорошо известно, что данные логически можно разделять на константные и обычные: в константные данные нельзя записать. И также уже во времена создания языка С, подобное деление аппаратно поддерживалось "битом защиты от записи".
Для приведенных примеров в модуле есть уже три логических сегмента: кода, констант и данных. Теперь ясно, что представление адресного пространства модуля как плоской модели памяти с указателем типа ptrXX, которое было найдено в первом приближении, не может быть использовано на практике.
Унифицированное адресное пространство
В абстрактной машине С для портируемого приложения не существует "указателя указывающего неизвестно куда" (конструкция типа "char * ptr" недопустима), все указатели постоянно связаны с логическим сегментом, в пределах которого они имеют смысл (для пользователей архитектуры x86 подобная связь знакома по конструкциям типа "char *__ds ptr"). В пределах логического сегмента модель памяти плоская.
Старый код С, который использует "указатели указывающие неизвестно куда", не может быть перенесен на абстрактную машину С без переделок.
Для указателя такая связь с логическим сегментом называется "storage" (по отношению к объекту, на который указывают, а не к самой переменной указателя). Эта связь в чем-то аналогична формату вызова функции ("linkage") и в отечественной транскрипции в обоих случаях часто говорят о "линковке".
В абстрактной машине С, такой storage указателя называется "тип памяти" (тип логического сегмента, с которым он связан) и тип памяти неразрывен со значком объявления указателя "*", объявить указатель без типа памяти "*void" нельзя, также как нельзя объявить переменную типа "void". В свою очередь, типизированный памятью указатель уже ссылается на "тип данных", так же как указатель в С.
В абстрактной машине С тип памяти указателя это не только название, а полноценный тип, такой же как тип данных; можно сказать, что указатель всегда двойной: сначала он ссылается на логический сегмент, а внутри этого сегмента ссылается на тип данных. При этом выражение "типизированный указатель" продолжает применяться только по отношению к типу данных.
Как и переменные разных типов данных, указатели на разные типы памяти несовместимы между собой по присваиванию и сравнению, потому что в общем случае указывают на физически разную память. Конечно, язык С позволяет в принципе проинициализировать указатель произвольным числом, в том числе взяв это число от указателя на другой тип памяти, но автоматически такая операция не выполняется.
Концептуально, использование типизации памяти вместо "обычного указателя" это такой же плюс, как использование типизации данных вместо "обычного указателя на void". Хотя для работы с любыми данными можно написать только одну функцию, принимающую параметр "void*", так стараются не делать, чтобы иметь контроль типа во время компиляции.
С каждым логическим сегментом, прозрачно для указателей на этот сегмент, связан свой селектор. Именно поэтому полные адреса сами по себе непригодны для межмодульного обмена, поскольку собственное адресное пространство модуля описывается полными адресами его логических сегментов.
Вопрос, а сколько типов логических сегментов может быть в программе всего? Ответ, типов логических сегментов и аппаратного контроля над ними может быть неограниченно много, но из всего этого многообразия мы вполне можем выбрать некий разумный набор, который соответствует развитию реальных процессоров в настоящее время.
Задача минимизации при выборе компонент этого набора не ставится, поскольку на конкретной машине все многообразие логических сегментов может отображаться на единственную аппаратную плоскую модель памяти, никакой аппаратной защиты для логических сегментов при этом не будет, но программа, рассчитанная на абстрактную машину С, будет работать и на плоской модели памяти, наоборот нет.
Чем лучше конкретная машина (процессор), тем больше аппаратной защиты можно будет задействовать для абстрактной машины С.
Для того, чтобы перечислить логические сегменты абстрактной машины С, нам необходимо перечислить сущности, которые действуют в современном исполнимом окружении. Эти сущности прямо связаны с модульной структурой программы и многозадачностью: система, процесс, нить и модуль.
Попытки реализаций потребностей этих сущностей показали нам список необходимых логических сегментов унифицированного адресного пространства, это типы (классы):
основные сегменты кода
ptrXX_f (function)
основные сегменты данных
ptrXX_d (proc_data)
ptrXX_h (proc_heap)
ptrXX_x (proc_heap_array)
ptrXX_ud (proc_unit_data)
ptrXX_uh (proc_unit_heap)
ptrXX_ux (proc_unit_heap_array)
ptrXX_td (thread_data)
ptrXX_th (thread_heap)
ptrXX_tx (thread_array)
ptrXX_ld (thread_unit_data)
ptrXX_lh (thread_unit_heap)
ptrXX_lx (thread_unit_heap_array)
ptrXX_a (current_auto)
ptrXX_ax (current_auto_array)
ptrXX_b (thread_auto)
ptrXX_bx (thread_auto_array)
константные основные сегменты данных
ptrXX_D (const proc_data)
ptrXX_H (const proc_heap)
ptrXX_X (const proc_heap_array)
ptrXX_UD (const proc_unit_data)
ptrXX_UH (const proc_unit_heap)
ptrXX_UX (const proc_unit_heap_array)
ptrXX_TD (const thread_data)
ptrXX_TH (const thread_heap)
ptrXX_TX (const thread_array)
ptrXX_LD (const thread_unit_data)
ptrXX_LH (const thread_unit_heap)
ptrXX_LX (const thread_unit_array)
ptrXX_A (const auto)
ptrXX_AX (const auto_array)
ptrXX_B (const thread_auto)
ptrXX_BX (const thread_auto_array)
логические сегменты (не связаные с аппаратной защитой)
ptrXX_s (current stack)
ptrXX_S (const current stack)
ptrXX_z (thread B stack)
ptrXX_Z (const thread B stack)
ptrXX_g (global (thread) B stack)
ptrXX_G (const global (thread) B stack)
ptrXX_W (const code_wide_data)
ptrXX_sx (stack_array)
ptrXX_SX (const stack_array)
ptrXX_zx (stack_B_array)
ptrXX_ZX (const stack_B_array)
ptrXX_gx (global stack_array)
ptrXX_GX (const global stack_array)
ptrXX_WX (code_wide_data_array)
ptrXX_i (указатель на данные объекта (тот же тип как у this), для шаблонной реализации методов класса)
ptrXX_I (const вариант)
Всего удалось найти около 50 различных логических сегментов, применяемых в унифицированном адресном пространстве, но на самом деле их общее большое количество связано только с комбинацией пяти основных типов сегментов {f,d,a,t,u}.
Однажды меня спросили, а правда ли, что вы хотели добавить новое ключевое слово? На что я ответил, что нет, это неправда, я хотел добавить около 25 новых слов, но сейчас мы уже перешагнули психологический рубеж в 50 новых слов.
Такое большое число аппаратных типов защит не обязательно есть в каждом конкретном процессоре, но в портируемой программе нельзя заранее предсказать какие из защит будут, а какие не будут действовать, это зависит только от конкретной машины, на которой программа будет выполняться. Тем не менее, логически это 50 совершенно несовместимых между собой физических сегментов и указатели на них также несовместимы между собой уже на этапе компиляции.
Наличие 50 разных сегментов требует от программиста продумывать размещение данных. Подробнее эти типы рассмотрены далее, в разделе "реализация".
Унифицированное адресное пространство
Численное значение указателя (адрес) в абстрактной машине С это беззнаковое целое число от 0 до максимального значения, которое зависит от числа бит адресного пространства.
Каждый тип памяти может иметь свой собственный зарезервированный под NULL адрес указателя, доступ к которому аппаратно защищен. Целое число 0 в абстрактной машине С нельзя использовать для проверки на NULL, все операции сравнения С указателей должны быть адаптированы под типизированный памятью NULL:
определение NULL
#define nullXY (0x...U) //null16f, null16d...
сравнение указателя на NULL
!p //требует поддержку компилятора для С указателей
p!=nullXY
возврат NULL
return nullXY;
Допустимо сравнение указателя с беззнаковым целым числом операциями "==", "!=", "<", ">", "<=", ">="; в том числе со значением nullXY для проверки на null операциями "==", "!=".
Автоматически преобразовать численное значение указателя в целое нельзя, иначе указатели разных типов можно будет сравнивать между собой и не будет работать сравнение на null (nullXY численно не совпадает с 0). Для извлечения численного значения указателя используется операция get_ptr().
При сравнении указателя на null, заместо "шаблонного null", который бы автоматически подставлял правильный nullXY в зависимости от типа указателя, используется операция "преобразование в bool" if(p) и операция "!" if(!p). В компиляторе, в котором типа bool нет, его функцию выполняет тип char.
Автоматически преобразовать численное значение указателя в bool тоже нельзя, иначе указатели разных типов можно будет сравнивать между собой, а сравнение указателя операцией "!" или операцией "преобразование в bool" больше не совпадает с таким сравнением для численного значения указателя как обычного целого числа.
Операция "преобразование в bool" для указателя логическая, т.е. в итоге указатель преобразуется в bool так или иначе, но технически, для класса С++ представляющего указатель, эта операция реализуется с использованием автоматического преобразования к промежуточному типу, уникальному для указателя на каждый тип памяти, так что указатели разных типов памяти не смогут быть сравнены между собой, например, преобразование к фиктивному указателю С на промежуточный тип с NULL равным 0:
class ptrXY{
private: class Tcmp{};
public: operator Tcmp _mXY* ()const
{return (Tcmp _mXY*)((offs==nullXY)? 0: 1);}
};
Далее по тексту будет перечислен интерфейс класса С++, использующегося в качестве указателя.
Старый код С, который использует "0 в качестве NULL", не может быть перенесен на абстрактную машину С без переделок.
Унифицированное адресное пространство
Тип (класс) межмодульного адреса имеет вид:
adrXXptYY
Как уже ранее говорилось, для межмодульной адресации приходится применять специальные усилия, чтобы сделать адрес осмысленным за пределами модуля. В перечисленном выше многозадачном окружении абстрактной машины С этих "пределов за модулем" больше одного, а именно:
p|"none" (proc) контекст процесса
u (proc_unit) контекст процесса модуля
t (thread) контекст нити
l (thread_unit) контекст нити модуля (не что иное, как упрощенный по числу типов локальный указатель)
q (sys) глобальный IPC (в ОС с "горизонтальной" защитой в пределах уровня привилегий практически не применяется)
Типы сегментов памяти при межмодульной адресации немного отличаются от типов локальных селекторов, это типы (классы):
основные межмодульные типы адреса
f (function)
d (data)
D (const data)
логические межмодульные типы адреса (не связано с аппаратной защитой)
h (heap)
H (const heap)
x (heap array)
X (const heap array)
Подробнее типы полных адресов рассмотрены далее, в разделе "реализация".
Унифицированное адресное пространство
Чтобы не откладывать, сразу полные адреса и рассмотрим. Некоторые их свойства.
Пара {selXX:ptrXX} как адрес аналогична комплексному числу, для которого определена только операция сравнения на равно.
Тип "d" это статические данные, тип "D" это статические константы, а тип "f" это код. Все как в старые, добрые времена, только указатель на статические данные (статическую переменную) не может иметь тип "f" и код всегда защищен от записи.
Типы "h" и "x" используются для указания на динамические данные С. Разница между ними в том, что через указатель типа "x" можно во время выполнения получить размер динамического массива С (структуры данных в памяти "h" и "x" не совпадают). Выделять и уничтожать межмодульные динамические данные можно только средствами IPC (вид IPC зависит от контекста адреса).
Типы "H" и "X" указывают на константные динамические данные С. Константность адреса можно повысить (преобразовать из "d" в "D" и т.п.), но выполнить обратную операцию нельзя. Хотя С всегда позволяет это сделать обходными путями, никакого смысла в этом нет, поскольку данные могут быть защищены от записи аппаратно.
Контекст полного адреса (префикс) зависит от контекста его селектора. Приведем пример простейшей реализации на совершенно обычном компиляторе С++ портируемых 16 битных селекторов абстрактной машины С, прямо совместимых с архитектурой х86:
/*
abstract address space
selectors
_IU address areas
p prefix
Как видим, все эти 50 типов не будут особенно сложны в простых реализациях.
Теперь о некоторых свойствах указателей. Указатель типа "f" это указатель на код модуля, как и для межмодульной адресации, те же самые свойства.
Указатели на данные "dhx" принадлежат разным контекстам. Процесс, нить и модуль втроем образуют четыре раздельных контекста (каждый имеет свой "префикс"), два из этих контекстов это старые добрые процесс "none" и нить "t", которые являются общими для всех модулей процесса/нити; два других контекста это опять процесс "u" и нить "l", но они уже принадлежат только одному модулю; т.е. модуль разделяет контексты "процесс" и "нить" на две части.
Статические данные могут размещаться в этих сегментах с помощью linkage "static" или "_dd", "_du", "_dt", "_dl" (при совместимом компиляторе).
Динамические данные выделяются с помощью библиотечных вызовов типа new/delete. Наличие разных динамических сегментов улучшает в общем неэффективное управление динамической памятью.
Константные сегменты опять кодируются большими буквами. Правила преобразования константности те же самые, что и для межмодульной адресации.
Стек данных образует отдельный сегмент. Модульная программа подразумевает такое понятие, как "фрейм стека". Наличие фрейма стека означает, что каждая функция в общем случае имеет свой независимый сегмент стека, такой стек обозначается как "текущий", тип "a".
Локальные переменные (storage "auto" или "_da"), объявленные в функции С, размещаются в этом стеке.
Для стека "a" есть своя, очень эффективно выделяемая динамическая память. Выделять память можно с помощью "alloca". Освобождается такая память тоже очень эффективно, но только при выходе из функции. Правильная поддержка alloca очень проста и зависит исключительно от компилятора.
Компилятор для архитектуры x86, который адресует через "?bp+offset" что-либо, кроме локальных переменных функции, такой поддержкой не обладает, несмотря на возможное наличие в библиотеке функции "alloca". Эта проблема вызвана исторически недостатком в системе команд x86, из-за которого возможности адресации через [?sp] ограничены, поэтому для работы с динамическими данными на стеке, компилятору надо использовать базовый регистр [?bx] или аналогичные приемы.
Помимо этого стека, как и в плоской модели памяти, с каждой нитью связан свой отдельный собственный стек, тип "b". Этот стек является общим для всех модулей, его наличие позволяет модулям обмениваться между собой параметрами, а нити выполняться.
В стеке типа "b" память можно динамически выделять с помощью "allocb". Поведение аналогично alloca. Локальные переменные в этом стеке можно размещать с помощью linkage "_db" (при совместимом компиляторе).
В отличие от плоской модели памяти, в модульной программе для расширения стека надо порождать новые фреймы (делать новые вызовы функции), поэтому в модульной программе стек фрагментирован, но по размеру ограничен только количеством виртуальной памяти.
Функции абстрактной машины С, которые при вызове получают свой собственный фрейм стека имеют специальный linkage типа . Правила передачи параметров для таких функций отличаются от обычных. Функции типа frame также используют стек "b" для обмена общими данными.
Обычные функции (без frame), работают в физически одном и том же фрейме стека "a", в этом их назначение. Это значит, что адреса стека таких функций имеют один и тот же селектор (селектор определяется или последней frame функцией или исходным селектором стека "a" для нити).
Совершенно стандартной ошибкой является передача адреса из сегмента стека "a" в статическую переменную, время жизни которой больше, чем время жизни этого стека и наличие несовместимых указателей помогает устранять эту ошибку.
Однако, при работе программы, адреса из стека "a" иногда должны быть переданы в вызываемую функцию и возвращены из нее. Для этого служит логический сегмент типа "s". Тип "s" указывает, что данный адрес принадлежит верхнему по уровню стеку "a".
Есть правила использования "a" и "s" при вызовах функций, описании параметров функций и т.п. (правила далее), но в общем контроль над правильностью использования типа "s" и преобразованию данных из "a" в "s" лежит на программисте.
Также традиционным для С++ является способ создавать статические переменные динамически, внутри функции main и т.п. Такие данные имеют время жизни сравнимое со статическими переменными. Нет смысла размещать такие данные в h/x сегментах и загружать систему динамической памяти, поскольку реально эта память не будет перераспределяться и такие данные можно эффективно размещать на стеке "b". Для работы с такими данными используется логический сегмент Некоторые из правил сочетаний a,s:
Для упрощения отслеживания правильности стека на базе адресов s можно иметь структурный тип пользователя s2{s_addr;id_stack} и хранить в нем id_стека_адреса (id от 0 до uregX_max), а также иметь локальную переменную id_текущего_стека, это позволит гарантированно отслеживать конверсию s2a:
Логический сегмент "W" используется для доступа к константным данным, размещенным в сегменте кода. Отметим, что данные в сегменте кода имеют видимость кода, т.е. они общие для всех пользователей такого кода в системе.
Логический сегмент "i" используется для типизации адреса "this". Объект класса может быть создан в любом из логических сегментов. Методы класса, которым нужен тип логического сегмента своего объекта, ссылаются на него через логический сегмент "i", как шаблоны.
В качестве варианта будущего развития типов можно уже сегодня анонсировать типы указателей формата "ptrX_Y_Z". В этом формате компонент Z отвечает за поле индекса сегмента, которое хранится в одном регистре с полем указателя и имеет вид "yHL":
Например, объявление типа "p32dy12" означает, что из 32 бит ureg32 хранящих численное значение адреса типа p32d, самый старший бит и самые младшие 2 бита отведены под индекс сегмента (всего восемь сегментов индексируются):
Применение такой индексации удобно для исполнимых окружений с сегментами больших размеров (для архитектуры x86 32 бит и более) и позволяет уменьшить размер памяти, отведенный под хранение указателей, но немного усложняет работу с такими указателями. Без такого хранения индексации, параметр шаблона кода времени выполнения, указывающий на сегмент используемого указателя, будет занимать в памяти размер в общем равный "машинному слову" (размер указателя удваивается, хотя хранится не значение селектора, а индекс "прозрачного селектора").
Для указателей в виде класса С++, значок "*" при объявлении указателя не используется, но для объявлений в стиле С нужен конкретный синтаксис объявления указателя, типизированного памятью. Полный синтаксис при объявлении указателя стиля С использует обе звездочки:
В упрощенном синтаксисе одну из "*" можно убрать. Если убрать левую звездочку, то у нас появляется "data_type" размещенный в "memory_type", на который указывает "pointer_name" тоже где-то размещенный, т.е. memory_type мы соединяем с традиционным data_type и "*", как и в обычном С, разделяет описание указателя и того, на что он указывает:
Для описания "memory_type" используем префикс "_m", после которого следуют разрядность адреса "XX" и буквы типа локального указателя: "d", "td", "lh" ... :
Если убрать правую звездочку из полного описания, то у нас появляется "data_type", на который указывает тоже где-то размещенный "pointer_name" имеющий тип "pointer_type" (ptrX_Y), т.е. memory_type мы соединяем с типом указателя и "*", как и в обычном С, разделяет описание указателя и того, на что он указывает:
Для описания "pointer_type" используем префикс "cp" (от C pointer), после которого следуют разрядность адреса "XX" и буквы типа локального указателя: "d", "lh" и т.п.:
В целом оба типа идентичны, первый тип традиционен и водит все равно необходимый тип памяти; второй тип переносит разрядность адресного пространства от типа данных ближе к переменной указателя.
Для указателей в виде класса С++ "pXY Операции ++, -- только префиксные, поскольку повсеместно программисты до сих пор не могут освоить правильное применение таких постфиксных операторов в С++. Для указателей абстрактной машины С такие постфиксные операторы С++ для указателей явно задать или использовать нельзя.
На аппаратуре, которая постфиксные ++, -- не поддерживает аппаратно (на х86, например), их применение для классов С++ порождает поражающий воображение своей неэффективностью код, который выполняет десяток операций вместо одной, поэтому программист (пользуясь только префиксными операциями ++, --) должен явно выделять временные хранилища, когда они действительно нужны, компилятор сам определить такую необходимость не может.
В иных случаях, когда постфиксные операторы ++, -- пользователем не заданы, совместимый компилятор может поддержать постфиксные операторы сам, упорядочивая операции и применяя префиксный оператор в тех выражениях, где это возможно.
Операции -,[], ++, --, +=, -= требуют sizeof(T), операция -> определена только для тех T, для которых такая операция существует, поэтому надо ввести дополнительный суффикс типа интерфейса указателя по двухбитовой комбинации:
В десятичном и используемом в программе виде, дополнительный суффикс типа указателя:
Вот кусок кода инициализации простой реализаций lh, созданный по правилам абстрактной машины С:
Получается очень похоже на обычный С, только добавляется портируемость. Для сравнения код выделения памяти для alloca(ureg32 size) на архитектуре х86 и совместимом компиляторе:
Пример простой реализации унифицированных указателей на х86 абсолютно аналогичен примеру для селекторов на х86, только занимает больше места, поэтому приводить его нет смысла.
Унифицированное адресное пространство
Главное требование к реализации унифицированного адресного пространства на конкретной машине заключается в том, чтобы наличие логических сегментов было полностью прозрачным для программы. В терминах архитектуры x86 это означает, что сегментные регистры за все время работы модуля загружаются только один раз, при вызове модуля.
Конечно, отличие от плоской модели памяти при этом все равно есть, отличие не меньшее, чем увеличение для модульной программы числа параметров при вызове функций, но это плата за пользование аппаратной защитой времени выполнения и раздельными адресными пространствами. В правильно спроектированном процессоре при смене модулей передается минимум "лишних" данных, нужных только для поддержки аппаратной защиты.
Вопрос, который при этом может взволновать нового читателя: как тогда на вполне себе обычной аппаратной системе практически выразить 50 разных логических сегментов?
Надо признать, что унифицированное адресное пространство можно полностью реализовать даже на 286 процессоре, у которого всего четыре аппаратных сегментных регистра. Ясно, что это можно сделать только тогда, когда некоторые из этих сегментов (в нашем примере это должно быть 46 из них) не получат аппаратной реализации.
Проблемы как кажется минимальны, в системе с плоской моделью памяти все 50 из этих 50 логических сегментов не получат аппаратной реализации, но тут есть один подводный камень.
Если портируемая программа затребовала 50 "прозрачных" сегментных регистров по 16 бит, то несмотря на то, что 286 поддерживает 16-битные сегменты, он может предоставить только 3 "прозрачных" сегментных регистра и такая 16-битная программа на нем просто не сможет быть выполнена. Аналогичный пример для 50 "прозрачных" сегментных регистров по 32 бита. Их в 386 только 5.
Заметим, что даже 3 сегмента это по размеру все же больше чем один сегмент. Если портируемая программа умещает каждый свой модуль в три 16-ти битных или пять 32-битных сегментов, с учетом их неравномерного использования, то независимо от логического разбиения этих аппаратных сегментов, программа будет работать на 286 и 386.
Но этот подводный камень заключается в несовпадении затребованного размера адресного пространства с предоставляемым при такой схеме отображения, что совершенно недопустимо для портируемой программы.
Программист, затребовав 16 битные сегменты, вправе ожидать их полный 16 битый размер, но при таком их отображении на общие 16 битные аппаратные сегменты 286, этого полного размера нет и во время выполнения может возникнуть неожиданная нехватка памяти. Более того, нехватка памяти может возникать редко и в общем, что хуже всего, невозможно предсказать когда она возникнет.
Нельзя исключать из использования "лишние" сегменты, если они требуются по логике программы, ради выполнения на "реальном 286". Это лишает программу портируемости и надежности. Возможны такие решения:
Путь с помощью DEF файла самый хороший, сохраняется как единственность определения сегментов (в системе только 16 бит сегменты), так и не требуется повышения битности исполнимого окружения (только 16 бит исполнимое окружение).
Модульная портируемая программа для абстрактной машины С требует от программиста не только явно выбирать размеры используемых целых типов, но и явно выбирать кластеры памяти для модуля, которые потом не могут быть перераспределены в сторону уменьшения или увеличения. Это плата за портируемость и надежность.
Такое требование к памяти как заставляет программиста тщательно выяснять предполагаемый максимальный размер используемых сегментов, так и оставляет малоиспользуемые "хвосты" в пределах сегментов, которые могут быть убраны из физической памяти в общем только аппаратной системой страничной адресации.
Если перераспределение памяти нужно, то портируемая программа использует сервис IPC для довольно малоэффективного перераспределения из системной памяти дополнительных внешних сегментов в адресном пространстве модуля, пользование которыми уже не прозрачно, нужны полные адреса (на процессоре х86 эти сегменты будут доступны через перезагрузку регистра ES).
data_type *memory_type *pointer_name;
data_type memory_type *pointer_name;
//статическая переменная data в сегменте нити
//описание _dt см. далее
_dt ureg32 data;
//константный указатель с именем ptr на стеке А
//указывающий на константную переменную ureg32
//в сегменте нити размером 32 бита
//проинициализирован адресом data
//это C вариант указателя ptr32_td
const ureg32 _m32td* auto const ptr=&data;
//C++
p32td
data_type *pointer_type pointer_name;
//статическая переменная data в сегменте нити
//описание _dt см. далее
_dt ureg32 data;
//константный указатель типа cp32td на стеке А с именем ptr
//указывающий на константную переменную ureg32
//в сегменте нити размером 32 бита
//проинициализирован адресом data
//это C вариант указателя ptr32_td
const ureg32 *cp32td auto const ptr=&data;
//data
uregX offs;
//ctor
ctor()
copy_ctor
pXY& =(const pXY&)
ctor(T _mXY*)
pXY& =(T _mXY*)
pXY& set_ptr(T _mXY*)
//access
T _mXY* get_ptr()const
T _mXY& *()const
//access2
T _mXY& [](uregX)const //idx
uregX -(const pXY&)const //возвращает idx между двумя указателями
//access3
T _mXY* ->
//comparison
//если компилятор может bool (){ return (offs!=nullXY); }
//то для операций (!, ==, !=, <, >, <=, >=)
class ptrXY<T>::Tcmp{};
operator Tcmp _mXY*()const
//modification
pXY& ++()
pXY& --()
pXY& +=(uregXX)
pXY& -=(uregXX)
pXY +(uregXX)
pXY -(uregXX)
включен
бит 0 нет операции sizeof(T)
бит 1 есть операция ->
нет указатель на POD тип (есть sizeof(T), нет ->)
2 полный указатель (есть ->, есть sizeof(T))
3 указатель внутри структуры на себя (нет sizeof(T), есть ->)
1 самый простой доступ (нет sizeof(T), нет ->)
class Theap_mcb{
...
public:
//controlled memory size (excluding self sizeof(Theap_mcb))
uregX size;
//?allocated
char is_busy;
//to faster free
Tptr prev;
...
//
(*_p_data).total_heap_size=
(*_p_base).n_heap.heap_top-(*_p_base).n_heap.heap_base;
//?no space to place mcbs
if( (*_p_data).total_heap_size <= 2*sizeof(Theap_mcb) )return;
(*_p_data).mcbs.set_ptr( (uregX)((*_p_base).n_heap.heap_base) );
(*_p_data).mcbs->size=
(*_p_data).total_heap_size - 2*sizeof(Theap_mcb);
(*_p_data).mcbs->is_busy= 0;
(*_p_data).mcbs->prev= 0;
(*_p_data).free_mcb= (*_p_data).mcbs;
(*_p_data).free_heap_size= (*_p_data).mcbs->size;
//
Tptr
last_mcb;
last_mcb.set_ptr( (uregX)(
(*_p_base).n_heap.heap_base
+ ((*_p_data).total_heap_size - sizeof(Theap_mcb))
));
last_mcb->size= 0;
last_mcb->is_busy= 1;
last_mcb->prev= (*_p_data).mcbs;
if(size&_align4_mask)size+= (4-(size&_align4_mask));
__sp32-= size;
return __sp32;
Конкретная реализация