Начала программирования в защищённом режиме (часть 1)

Дата публикации 24 авг 2017 | Редактировалось 10 янв 2018

Начала программирования в защищённом режиме. Часть 1


Предисловие


Данный цикл статей расчитан на пользователей персональный компьютеров типа IBM PC, которые хотели бы познакомиться с архитектурой процессоров семейства Intel в защищённом режиме и написать свою собственную программу, функционирующую как вполне самостоятельная операционная система.
От читателя требуется хотя бы минимальный опыт программирования на языках Assembler и C++, потому что будет предполагаться, что с некоторыми моментами читатель знаком, и очень подробно они рассматриваться не будут. Но даже новички, не знающие хорошо языки программирования, получат очень много информации для размышления и осознания.
Особенно ценным материал покажется людям, любящим поломать свою голову, потому что дальнейшее его изложение будет основано на методе проб и ошибок. Оно и ясно: если человек однажды наткнулся на проблему и смог её логично решить, то он практически никогда больше не будет с ней сталкиваться.
Введение
Процессоры семейства x86 наиболее распространены по всему миру. Если касаться истории раннего развития процессоров Intel, то можно выделить следующие этапы:
  • В 1971 году был выпущен первый процессор 4004, не получивший широкого распространения из-за малого набора команд.
  • В 1974 году появился широко распространённый по сей день процессор 8080. Но если в то время процессор управлял компьютером ZX-Spectrum, то на сегодняшний день его можно встретить в различных бытовых устройствах.
  • В 1978 году компания Intel выпустила революционный процессор 8086, положивший начало дальнейшему прогрессивному развитию компьютерной техники.
  • В 1985 году компания Intel сделала прорыв, выпустив процессор 80386. В нём была впервые реализована 32-битная шина данных и реализована полная поддержка так называемого "защищённого режима" работы процессора, о котором и пойдёт в дальнейшем речь.
  • Но нельзя считать, что "защищённый режим" появился в 386-ом процессоре. Впервые он был реализован в его предке ― Intel 80286, но его недостатками были сложность реализации и наличие множества недоработок. Появление процессора 80386 знаменовало, что пришло время многозадачных программ и операционных систем.
Конечно же, за многие годы своей эволюции процессоры Intel и AMD претерпели много изменений, основными из которых были повышение скорости обработки информации и выполнения команд. Сегодняшние процессоры (80486, Cyrix x586, AMD K5, Intel Pentium, Intel Pentium Pro, AMD K6, Intel Pentium 2, AMD K7, Intel Pentium 3, Intel Pentium 4, Intel Celeron, AMD Athlon, AMD Duron) ― прямые потомки процессора 80386, полностью совместимые с ним. Конечно, сейчас эпоха настольных компьютеров с процессорами, реализующими IA32 (Intel Architecture 32), подходит к концу, и обе компании-лидера (AMD и Intel) выдвигают новую архитектуру процессоров ― Intel Itanium (Intel Extended Memory 64 Technology) и AMD Athlon 64 (AMD 64 Technology), открывающими новые возможности. Но производители сохраняют новые процессоры полностью совместимыми с IA32. Однако стоит помнить о том, что большинство настольных компьютеров остаются пока ещё на базе 32-разрядных процессоров (бывают случаи, когда в России местами встречаются ещё и 386-ые процессоры), и резкого скачка к 64-разрядным процессорам не намечается. Поэтому тема "защищённого режима" по сей день остаётся актуальной.
Итак, с появлением процессора 80386, началась эпоха "защищённого режима". Что такое "защищённый режим"? Этому термину можно дать следующее определение: "новый механизм распределения и защиты памяти". Конечно, это очень узкое толкование, для обрисовки полной картины следует перечислить все новые возможности, предоставленные процессором Intel 80386:
  • Работа с памятью, большей 1 мегабайта, и возможность реализации виртуальной памяти, общий объём которой достигает 64 терабайт.
  • Механизм защиты памяти, который позволяет приложениям работать только с выделенной для них областью памяти.
  • Многозадачность, то есть организация одновременного выполнения нескольких программ или процессов, запущенных на компьютере.
Кроме защищённого режима в процессорах третьего поколения появились ещё три режима работы: реальный, виртуальный 8086 и нереальный режим. Об этих режимах речь пойдёт в следующих статьях.
Конечно, ограничиваться этими статьями не следует, и есть ещё много литературы, которую обязательно следует почитать. Например, документация от разработчика даёт самые обширные знания по архитектуре 386-х процессоров и их потомков. У нас же будет больше практики, всего того, что автор собственными силами смог добиться, и, что самое ценное, эти статьи предназначены для того, чтобы дать читателю больше практики, нежели теории. Порой автору встречались теоретики, которые довольно хорошо знали архитектуру защищённого режима, но не могли написать элементарной программы, корректно работающей в нём, по причине недостаточного практического опыта. Наша же задача ― твёрдо встать на ноги, после чего будет легче идти дальше самостоятельно. Но чтобы встать на ноги, нужно сначала приложить максимум усилий и внимательности. Помогут в этом нам подручные средства, которые и будут перечислены в следующей статье.
Инструментарий
Порой для достижения целей приходится пользоваться многими подручными средствами, которые позволяют исключить множество ошибок на этапе разработки программы. Надо помнить, что, допустим, встроенный в среду Watcom C++ дизассемблер не покажет ошибку, которую покажет профессиональный дизассемблер IDA, но порой даже дизассемблер не сможет отыскать ошибку, которую "словит" отладчик. Поэтому лучший способ решения проблем ― быть во всеоружии и не удивляться тому, что, вроде бы, текст программы написан грамотно (с точки зрения алгоритма), а она не хочет корректно функционировать.
Итак, приступим: эта статья предназначена для того, чтобы познакомить читателя с минимальным инструментарием, необходимым для разработки программ, функционирующих в защищённом режиме.
Язык программирования Assembler
Для дальнейшего изучения материала и практики потребуется компилятор языка Assembler, который способен генерировать 32-х битный код. Для этой цели хорошо подходят такие известные компиляторы как MASM (Macro Assembler от компании Microsoft), TASM (Turbo Assembler от компании Borland) и WASM (Watcom Assembler от компании Watcom). Конечно, мир ассемблера огромен и ограничиваться лишь этими тремя компиляторами не следует. Найти компиляторы языка Assembler сейчас совсем не сложно ― достаточно посетить Internet, откуда можно скачать совершенно различные их версии и реализации. Но особое внимание следует уделить компилятору FASM (Flat Assembler), специально созданного для написания операционной системы (и не только). Взять последнюю версию дистрибутива программы можно с официального сайта FASM: http://www.flatassembler.net. Она распространяется как самокомпилирующийся OpenSource-проект. Забегая вперёд, сообщу, что знакомство с защищённым режимом у нас начнётся именно с этого компилятора (я пользовался версией 1.51).
В дальнейшем мы будем больше пользоваться компилятором WASM, потому что мы будем иметь дело со средой программирования Watcom C++, о которой пойдёт речь в одной из следующих статей. Итак, вот перечень файлов, которые нам могут потребоваться:
  • fasm.exe ― компилятор языка FASM.
  • wasm.exe ― компилятор языка WASM.
  • wlink.exe ― компоновщик языка WASM.
Дизассемблер
Одним из важных компонентов разработки полноценных программ является дизассемблер. Именно с помощью него можно найти практически все ошибки, возникающие при компиляции проекта или написании программы. Конечно, одним дизассемблером не обойтись, и порой приходится прибегать к работе с отладчиком. Но об отладчике речь пойдёт позже, а сейчас следует уделить внимание хорошему дизассемблеру. Самым лучшим, на мой взгляд, оказался Interactive Disassembler Pro, который я использовал для исследования сгенерированного компилятором кода (версию 6.8.150423). Внешне интерфейс IDA выглядит так:
[​IMG]
При очередной загрузке дизассемблер предлагает открыть файл для дизассемблирования, после выбора он предлагает способ дизассемблирования файла из всех возможных. Когда файл и способ дизассемблирования выбран, перед нами открывается окошко с готовым текстом программы на ассемблере. В этом тексте нам порой и придётся разбираться.
Отладчик
Как было уже сказано в начале статьи, одним дизассемблером не обойтись, поэтому порой приходится прибегать к работе с отладчиком. Одним из удобных отладчиков можно назвать Turbo Debugger от компании Borland. Это классическое DOS-приложение, выглядящее следующим образом:
[​IMG]
Опять же, с работой в отладчике мы встретимся в следующих статьях. Пока достаточно того факта, что он должен быть в нашем наборе инструментов.

Среда разработки Watcom C++


Вот мы и дошли до самого главного ― компилятора языка C++. Почему именно Watcom C++? Дело в том, что языков, на которых можно написать самостоятельную операционную систему, не так много, и их можно перечислить по пальцам. К ним можно причислить GNU C/C++ и Watcom C++. Остальные либо не столь популярны, либо являются коммерческой тайной фирм-разработчиков операционных систем. Другие же компиляторы не способны генерировать самостоятельный код, а создают программы, адаптированные под конкретную операционную систему. Хорошие качества Watcom C++ заключаются в следующем:
  • GNU C++ больше используется для написания операционных систем на базе ядра UNIX, и у него есть свой, специфический синтаксис. Во-вторых, ассемблерные вставки в этом языке UNIX-стандарта, что, конечно, приводит к некоторым неудобствам, допустим, при переносе какого-либо кода из уже написанных программ под MS-DOS.
  • Watcom C++ прекрасно генерирует 32-битный код, при чём с превосходной оптимизацией. В этом можно убедиться, допустим, дизассемблировав полученный исполняемый файл. Конечно, у оптимизации есть свои "подводные камни", которые будут обязательно рассмотрены в статьях, по чему можно будет сделать вывод: "Самая лучшая оптимизация не всегда самая качественная". Но по некоторым параметрам Watcom C++ проигрывает в оптимизации GNU C++.
  • Для Watcom C++ очень легко пишется startup-код (код запуска системы), однако, как всякому системному программисту, нам придётся отказаться от стандартных библиотек.
  • Код других компиляторов хорошо переносится на Watcom C++. Достаточно привести пример, что код на Borland C++ вносится в проект и компилируется с незначительными изменениями текста программы.
  • У Watcom C++ есть собственный (конечно, не совсем удобный) редактор кода. Конечно, можно написать свой компилятор языка C++, но это будет титаническим трудом, нежели воспользоваться разработками целых компаний.
  • Ещё одним плюсом Watcom C++ (как и GNU C++) является его Freeware-распространение. Скачать последнюю версию компилятора и среду разработки можно с официального сайта http://www.openwatcom.org. Я пользовался версией 1.3.
Рекомендуемая литература
  1. Зубков С.В. Assembler для DOS, Windows и UNIX. ― М:. ДМК Пресс, 2000. ― 608 с.: ил. (Серия "Для программистов").
  2. Юров В.И. Assembler. Учебник для вузов. 2-е изд. ― СПб.: Питер, 2004. ― 637 с.: ил.
  3. Финогенов К.Г. Основы языка Ассемблера. ― М.: Радио и связь, 2000. ― 288 с.: ил.
  4. Страуструп Б. Язык программирования C++, 3-е изд./Пер. с англ. ― СПб.; М.: "Невский диалект" ― "Издательство БИНОМ", 1999 г. - 991 с., ил.
  5. Фридман А.Л. Основы объектно-ориентированного программирования на языке Си++. ― М.: Горячая линия ― Телеком, Радио и связь, 1999. - 208 с.: ил.

Начала программирования в защищённом режиме. Часть 2


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

Режимы работы процессора Intel 80386


С появлением процессора Intel 80386 возникла архитектура IA32. Она предполагала появление нового режима работы процессора ― защищённого ("Protected Mode"). Для совместимости с предыдущими процессорами линейки Intel 80x86 процессор 80386 не запускался сразу в защищённом режиме, а работал в так называемом реальном режиме ("Real Mode"). Кроме этого, у каждого режима есть один или несколько подрежимов. Разберём их.

Реальный режим


В этом режиме процессор находится сразу после нажатия кнопки "Power" компьютера. Доступ к памяти в реальном режиме осуществляется конструкцией "сегмент:смещение", которая описывает логический адрес. Значение сегмента, как и смещения, лежит в пределах от 0 до 0FFFFh.
Так как адресоваться можно только в пределах одного сегмента, то максимальный размер сегмента равен 64 килобайт. Физический адрес, который выставляется на адресную шину процессора, считается по формуле:
линейный адрес = сегмент × 16 + смещение​
В реальном режиме процессоров 80186 и 8086 значение сегмента лежало в пределах от 0 до 0F000h. Таким образом, максимальный выставленный адрес на адресную шину равен 0FFFFFh, что соответствует 220―1, т.е. 1 мегабайту.
Конечно, поначалу объём такой памяти казался колоссальным, но со временем одного мегабайта стало не хватать. С появлением процессора 80286 стал доступен так называемый блок памяти UMB, начинающийся с адреса 0FFFFh:0010h и заканчивающийся адресом 0FFFFh:0FFFFh (65520 байт за пределами одного мегабайта). Теперь можно было переконфигурировать операционную систему MS-DOS так, чтобы она занимала этот блок, освобождая в оперативной памяти 64 килобайт.

Защищённый режим


Этот режим имеет сложную конструкцию по сравнению с реальным. Логический адрес представляется конструкцией "селектор:смещение". Селектор находится в пределах от 0 до 0FFFFh (на самом деле, селекторов в 4 раза меньше ― об этом подробнее в следующих статьях). Смещение, в отличие от реального режима, является 32-разрядным, что позволяет адресовать сегменты размером 4 гигабайт. Логический адрес преобразуется в линейный по следующей схеме:
линейный адрес = база сегмента + смещение​
Линейный адрес в дальнейшем выставляется на адресную шину, если не включен режим страничной адресации. В противном случае линейный адрес преобразуется в физический, и только после этого выставляется на адресную шину. Кроме этого, защищённый режим позволяет организовать виртуальную память, достигающую размера до 64 терабайт и зависящую лишь от объёма жёсткого диска (например, тот же файл подкачки в Windows реализует виртуальную память). В защищённом режиме функционируют практически все современные операционные системы.

Мультизадачный подрежим защищённого режима


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

Режим виртуального 8086


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

Нереальный режим


Это особый подрежим реального режима. Процессор находится в реальном режиме, но адресуется к памяти путём конструкции "селектор: смещение". Таким образом доступна память выше 1 мегабайта. В дальнейшем будет рассматриваться защищённый режим и его подрежимы.

Первая программа: переход в защищённый режим


Переход в защищённый режим осуществляется установкой бита 0 регистра CR0. Переход в реальный режим осуществляется сбросом того же нулевого бита. Рассмотрим программу, которая выполняет эту операцию (язык ― Flat Assembler):
Код (ASM):
  1. use16 ; Используются 16-разрядные команды
  2. org 100h
  3. start:
  4. ; Мы находимся в реальном режиме
  5. mov eax, cr0 ; Считываем значение регистра CR0
  6. or    al, 1      ; Устанавливаем нулевой бит
  7. mov cr0, eax ; Записываем новое значение CR0
  8. ; Мы в защищённом режиме
  9. mov eax, cr0 ; Считываем значение CR0
  10. and  al, 0FEh  ; Сбрасываем нулевой бит в 0
  11. mov cr0, eax ; Переходим в реальный режим
  12. ; Мы в реальном режиме
  13. ret ; Выходим из программы
Однако эта программа является совсем "сырой", потому что в ней нельзя реализовать зацикливание. Если написать команды вроде hlt или jmp $, то по срабатыванию первого же прерывания компьютер перезагрузится.
В нашем случае команды срабатывают достаточно быстро, но не исключено, что в промежутке между выполнением каких-либо команд нашей программы прерывание всё-таки сработает, что приведёт к моментальному сбою и перезагрузке. Поэтому следует позаботиться о прерываниях. Итак, посмотрим ещё раз листинг. Это нельзя назвать первой программой (скорее, она пошла бы за нулевую), поскольку в ней не реализуются основные действия по переходу в защищённый режим. Чтобы полноценно перейти в защищённый режим с минимальными настройками, нужно выполнить следующие действия:
  1. проверить, возможно ли переключиться в защищённый режим;
  2. инициализировать таблицы дескрипторов;
  3. запретить прерывания (как маскируемые, так и не маскируемые);
  4. открыть линию A20;
  5. загрузить регистры управления памятью;
  6. установить нулевой бит (далее он будет называться PE) регистра CR0;
  7. выполнить переход на 32-битный сегмент кода, переопределив регистр CS.
Но первой программе достаточно выполнить действия 3, 4, 5. Тогда её зацикливание не приведёт к перезагрузке компьютера. Разберёмся в каждом действии.
Запрещение прерываний предохраняет нас от перезагрузки. Прерывания делятся как на маскируемые, так и немаскируемые. Чтобы запретить маскируемые прерывания, надо сбросить флаг IF регистра EFLAGS командой cli, разрешение же прерываний выполняется командой sti. Немаскируемые прерывания запрещаются несколько иначе. Для этого существуют два способа: программирование регистров контроллера прерываний (этот способ будет рассмотрен несколько позже) или изменение седьмого бита порта 70h: если бит установлен, то прерывания запрещены, если бит сброшен ― прерывания могут выполняться.
Теперь зададимся вопросом, в чём заключается функция линии A20, и что это такое. Линия A20 ― одна из 32 адресных линий. При загрузке компьютера линия A20 закрыта. Это приводит к генерации 20-разрядных адресов (то есть всё адресное пространство получается равным 220=1 мегабайт). Введено это для совместимости с процессором 8086: таким образом, пытаясь записать по линейному адресу 12345678h, мы, на самом деле, запишем по адресу (12345678h)&0FFFFFh=00045678h, что может привести к совершенно неожиданному результату. Поэтому для полноценного функционирования 32-разрядного приложения линия A20 обязательно должна быть открыта. Осуществляется это установкой бита 1 порта 92h, закрытие линии A20 ― сброс этого бита.
С последним действием читатель уже знаком, и оно уже не должно вызывать у него вопросов.
Итак, рассмотрим листинг нашей новой, первой, программы, в которой уже реализуется небольшой цикл. Строки, которые добавлены к предыдущему листингу, помечены звёздочкой (*).
Код (ASM):
  1. use16
  2. org 100h
  3. start:
  4. ; Мы находимся в реальном режиме
  5. ; Запретить маскируемые прерывания
  6. cli ;*
  7. ; Запретить немаскируемые прерывания (NMI)
  8. in   al, 70h     ;*
  9. or   al, 80h     ;*
  10. out 70h, al     ;*
  11. ; Открыть линию A20
  12. in   al, 92h     ;*
  13. or   al, 2        ;*
  14. out 92h, al     ;*
  15. ; Переключиться в защищённый режим
  16. mov eax, cr0
  17. or    al, 1
  18. mov cr0, eax
  19. ; Теперь находимся в защищённом режиме
  20. ; Небольшой двойной цикл
  21. mov cx, 20     ;*
  22. cycle: ;*
  23. mov ax, cx     ;*
  24. or cx, -1 ;*
  25. loop $            ;*
  26. mov cx, ax     ;*
  27. loop cycle      ;*
  28. ; Переключиться в реальный режим
  29. mov eax, cr0
  30. and  al, 0FEh
  31. mov cr0, eax
  32. ; Закрыть линию A20
  33. in    al, 92h     ;*
  34. and al, 0FDh    ;*
  35. out  92h, al     ;*
  36. ; Разрешить немаскируемые прерывания (NMI)
  37. in    al, 70h     ;*
  38. and al, 7Fh      ;*
  39. out  70h, al     ;*
  40. ; Разрешить маскируемые прерывания
  41. sti ;*
  42. ; Мы снова находимся в реальном режиме
  43. ret ; завершить программу
Убедиться в том, что программа работает, можно, запустив исполняемый файл из чистого MS-DOS. Если программа корректно завершается, то всё в порядке.
Однако могут возникнуть и следующие проблемы:
  1. компьютер "зависает";
  2. компьютер перезагружается.
Это может возникнуть из-за следующих причин:
  1. программа запущена в режиме V86 (режим виртуального 8086);
  2. программа запущена в защищённом режиме или под конкретной операционной системой.
Поэтому перед запуском любой программы, работающей в защищённом режиме, следует проверить возможность дальнейшей работы программы. Об этом будет рассказано дальше.

Проверка возможности перехода в защищённый режим


В предыдущей главе у нас возникла следующая проблема: программа не распознаёт то, что она находится в защищённом режиме или режиме V86, что приводит к зависанию системы или её перезагрузке. Если же мы попытаемся запустить программу под управлением операционной системы Windows, то Windows отловит попытку программы перейти в защищённый режим и предложит перезагрузиться в режиме эмуляции MS-DOS (для платформы 9x), либо завершит работу программы принудительно (платформа NT).
Итак, для проверки того, что мы действительно находимся в реальном режиме, следует выполнить следующие операции:
  1. проверить нулевой бит регистра CR0;
  2. убедиться, что не загружена операционная система Windows.
Первая операция осуществляется прямым чтением регистра CR0 с дальнейшей проверкой нулевого бита регистра EAX, AX или AL. Если бит не установлен, то мы находимся в реальном режиме. В противном случае дальнейшее выполнение программы становится бессмысленным.
Второе действие осуществляется вызовом функции 1600h прерывания 2Fh. Эта функция позволяет получить текущую версию операционной системы Windows. Если после вызова функции в регистре AL содержится ноль, то операционная система не загружена. В противном случае, опять же, нашей программе бессмысленно продолжать какие-либо дальнейшие действия.
Рассмотрим пример следующей программы. Она является модификацией предыдущей программы, все новые инструкции помечены звёздочкой (*).
Код (ASM):
  1. use16
  2. org 100h
  3. start:
  4. ; Настроить сегментные регистры
  5. mov ax, cs     ;*
  6. mov ds, ax     ;*
  7. ; Проверка того, что мы действительно в реальном режиме
  8. mov eax, cr0  ;* проверка нулевого бита
  9. test al, 1        ;* регистра CR0
  10. jz    no_pm     ;*
  11. ; Вывести сообщение об ошибке
  12. mov ah, 9       ;* функция DOS 09h
  13. mov dx, pm_msg ;* вывод строки
  14. int   21h            ;*
  15. ret                    ;* и выйти
  16. no_pm:
  17. ; Проверка: не запущена ли программа под Windows
  18. mov ax, 1600h     ;* функция 1600h мультиплексорного
  19. int   2Fh              ;* прерывания - получить версию Windows
  20. test al, al            ;* если не 0 - ошибка
  21. jz    no_windows
  22. ; Вывести сообщение об ошибке
  23. mov ah, 9       ;*
  24. mov dx, win_msg ;*
  25. int   21h             ;*
  26. ret                    ;*
  27. no_windows:
  28. ; Мы точно находимся в реальном режиме
  29. ; Запретить маскируемые прерывания
  30. cli
  31. ; Запретить немаскируемые прерывания (NMI)
  32. in   al, 70h
  33. or   al, 80h
  34. out 70h, al
  35. ; Открыть линию A20
  36. in   al, 92h
  37. or   al, 2
  38. out 92h, al
  39. ; Переключиться в защищённый режим
  40. mov eax, cr0
  41. or    al, 1
  42. mov cr0, eax
  43. ; Теперь находимся в защищённом режиме
  44. ; Небольшой двойной цикл
  45. mov cx, 20
  46. cycle:
  47. mov ax, cx
  48. or cx,-1
  49. loop $
  50. mov cx, ax
  51. loop cycle
  52. ; Переключиться в реальный режим
  53. mov eax, cr0
  54. and  al, 0FEh
  55. mov cr0, eax
  56. ; Закрыть линию A20
  57. in    al, 92h     ;*
  58. and al, 0FDh    ;*
  59. out  92h, al     ;*
  60. ; Разрешить немаскируемые прерывания (NMI)
  61. in    al, 70h    ;*
  62. and al, 7fh     ;*
  63. out 70h, al     ;*
  64. ; Разрешить маскируемые прерывания
  65. sti ;*
  66. ; Мы снова находимся в реальном режиме
  67. ret ; завершить программу
  68. ; Сообщения об ошибках
  69. pm_msg: ;*
  70. db "Error: already running in protected mode!$" ;*
  71. win_msg: ;*
  72. db "Error: Microsoft Windows detected!$" ;*
Этот пример тоже пока не показывает реализацию 32-разрядных команд. Для этого следует ещё познакомиться с материалом последующей главы. Кроме этого, у примера есть следующий недостаток: вызываются функции DOS (int 21h), что уже начинает противоречить независимости нашей программы от операционной системы MS-DOS. В дальнейшем придётся избавиться от использования функций операционной системы и перейти к использованию функций BIOS. Но пока достаточно ограничиться и таким кодом.

Сегментация памяти в защищённом режиме. Дескрипторы. Таблицы дескрипторов. Часть 3

В предыдущей статье была реализована программа, осуществляющая переход в защищённый режим с проверкой на возможное дальнейшее функционирование в защищённом режиме и реализующая простой цикл с использованием 16-разрядных команд. Теперь задача следующей программы – переход в защищённый режим и использование уже 32-разрядных команд. Для реализации этого следует познакомиться с новой информацией, касающейся защищённого режима.
Как говорилось раньше, сегментация памяти защищённого режима представляет собой совсем другую форму адресации: вместо привычной для реального режима адресации «сегмент:смещение» используется адресация «селектор:смещение». Рассмотрим структуру селектора:
[​IMG]
Рисунок 11. Формат селектора
Биты 3-15 указывают номер дескриптора в выбранной таблице дескрипторов (о таблицах чуть ниже). Таким образом, селектор может описать 213=8192 дескрипторов для одной таблицы. Каждый дескриптор в таблице описывает сегмент, при чём он не обязательно должен быть сегментом кода или данных (о типах сегментов будет написано ниже).
Бит 2 – флаг TI (Table Indicator). Этот флаг указывает, какая из двух таблиц дескрипторов будет использоваться для загрузки дескриптора. Если флаг равен нулю, то используется глобальная таблица дескрипторов (GDTGlobal Descriptor Table), в другом случае используется локальная таблица дескрипторов (LDTLocal Descriptor Table). Теперь поймём, что значит фраза «загрузка дескриптора». Дело в том, что, начиная с процессора 80286, размер сегментных регистров стал равным не двум байтам, а десяти, доступными из которых остались только два младших (в них и грузится селектор). Однако при загрузке селектора в младшие два байта сегментного регистра, в старшие 8 байт автоматически загружается дескриптор из используемой таблицы, по которому и ведётся дальнейшая адресация.
Биты 0-1 содержат запрашиваемый уровень привилегий (RPLRequested Privilege Level) доступа к сегменту, то есть с какими привилегиями программа обращается к сегменту, описанному дескриптором. О привилегиях доступа будет сказано позже, и пока будем считать их равными нулю (то есть, самыми высокими).
Пока в наших примерах достаточно пользоваться сегментными дескрипторами. Рассмотрим структуру сегментного дескриптора:
[​IMG]
Рисунок 12. Формат сегментного дескриптора
Теперь рассмотрим формат дескриптора поподробнее.
База – это линейный адрес, с которого начинается сегмент.
Лимит – это максимальное смещение от начала сегмента, то есть лимит равен размеру сегмента минус один байт. Таким образом, получается, что минимальный размер сегмента может быть равен одному байту, а максимальный – одному мегабайту или четырём гигабайтам (это зависит от, того, умножается размер сегмента на 4 килобайта или нет).
G – флаг гранулярности (Granularity), который, как раз, указывает, в чём измеряется размер сегмента: если G=0, то размер сегмента считается в байтах (то есть, максимальный размер сегмента получается 220=1 мегабайт), если G=1, то размер сегмента считается в 4-килобайтных блоках (страницах), то есть, максимальный размер сегмента равен 220×212=4 гигабайт, при чём размер сегмента всегда кратен 4 килобайтам.
D/B – флаг, указывающий разрядность сегмента:
  • 0 – сегмент 16-разрядный,
  • 1 – сегмент 32-разрядный (этот флаг ещё называют BIG).
Бит 53 зарезервирован и всегда должен быть равен нулю.
Бит AVL является свободным (Available) и может использоваться по усмотрению программы.
Биты DPL определяют привилегии доступа к сегменту (Descriptor Privelege Level). Всего существует 4 уровня доступа, в которых 0 – самый высокий приоритет, 3 – самый низкий приоритет. Таким образом, чтобы программе было возможным использовать сегмент, описанный данным дескриптором, в селекторе, используемом программой биты RPL должны принимать не меньшее значение, чем биты DPL дескриптора. В противном случае возникает аппаратная ошибка (прерывание), названная исключением, которая может быть обработана ядром ОС.
Флаг S и тип сегмента следует рассматривать вместе. Если S установлен, то это значит, что сегмент является системным (System), в противном случае сброшенный флаг S означает, что дескриптор описывает сегмент данных или кода. Если сегмент является системным, то указывается его тип (о типах системных дескрипторов будет написано позже), в другом случае дескриптор описывает либо сегмент данных, либо сегмент кода.
Поле «тип сегмента» для сегмента данных выглядит так:
[​IMG]
Рисунок 13. Поле «тип сегмента» сегмента данных
Для сегмента кода поле выглядит немного иначе:
[​IMG]
Рисунок 14. Поле «тип сегмента» сегмента кода
Флаг А также используется для организации виртуальной памяти и является флагом доступа (Accessed), то есть при любом обращении к описываемому сегменту флаг будет автоматически установлен (то есть, по этому биту операционная система решает, какие сегменты следует временно сбросить на диск в swap-файл, а с какими сегментами следует повременить). То есть, ОС может периодически сбрасывать этот бит, и если он через некоторое время не станет равным единице, то данный сегмент можно сбросить, допустим, на жёсткий диск, освободив тем самым память.
Для сегмента данных определяются два характерных флага:
  1. Флаг D задаёт направление роста сегмента (Direction). Обычно, если D=1, то выбранный сегмент является сегментом стека и растёт «задом наперёд». Если же D=0, то выбранный сегмент является сегментом данных. На самом деле, значение D=1 практически не используется.
  2. Флаг W определяет возможность записи в сегмент (Writable). То есть, сброшенный флаг указывает, что сегмент данных доступен только для чтения. Если флаг установлен, то сегмент доступен также и для записи.
Для сегмента кода определяются следующие два характерных флага:
  1. Бит C называется битом подчинения (Conforming), но рассмотрен он будет позже.
  2. Флаг R указывает на доступность сегмента кода для чтения (Readable). То есть, если флаг сброшен, читать из сегмента кода нельзя.
Из рассмотрения этих двух полей можно сделать вывод: писать прямо в сегмент кода категорически запрещено, как и запускать программу, находящуюся в сегменте данных.
Поля P, DPL, S и тип сегмента образуют единую группу – права доступа сегмента, и для операции с этой группой введены специальные команды процессора, которые будут рассмотрены позже.
Теперь перейдём к рассмотрению структуры таблиц дескрипторов. Таблицы LDT (Local Descriptor Table), IDT (Interrupt Descriptor Table) и GDT (Global Descriptor Table) имеют одинаковую структуру – это массивы дескрипторов. Сами таблицы не являются сегментами – это области физической памяти. Отличием IDT от GDT и LDT является то, что в ней хранятся особые дескрипторы, которые мы пока затрагивать не будем. Единственным отличием GDT от LDT является то, что таблица GDT может быть всего лишь одна, а таблиц LDT может быть много. Кроме этого, нулевой дескриптор в таблице GDT не может быть использован и должен содержать нули (так называемый «нулевой дескриптор»). Структуры таблиц выглядят следующим образом:
[​IMG]
Рисунок 15. Структура таблиц дескрипторов
Остаётся ещё один вопрос: каким образом процессор находит в памяти эти таблицы. Вспомним структуру реального режима: таблица прерываний хранится, начиная с нулевого логического адреса и занимает 400h байт. Процессору не составляло труда её обнаружить, так как на любой машине под таблицу были зарезервированы самые младшие 400h байт адресного пространства. В защищённом режиме всё несколько иначе: таблицы могут располагаться в любом месте оперативной памяти, и их адреса нужно где-то фиксировать. Для этого в процессорах Intel (начиная с модели 80286) появились новые машинно-специфичные регистры: GDTR(определяет расположение и размер таблицы GDT), LDTR (определяет селектор таблицы LDT), IDTR (определяет расположение и размер таблицы IDT). Регистры GDTR и LDTR схожи по строению: они имеют одинаковый размер (6 байт) и содержат в себе одинаковую структуру данных о расположении и размере таблицы в памяти. Регистр LDTR отличается от этих двух регистров по размеру (10 байт) и содержит в себе селектор дескриптора сегмента данных таблицы LDT и сам дескриптор, который находится в таблице GDT.
Рассмотрим структуру данных, хранящихся в регистрах GDTR и IDTR:
[​IMG]
Рисунок 16. Структура данных регистров GDTR и IDTR

8 12.475
SadKo

SadKo
Владимир Садовников

Регистрация:
4 июн 2007
Публикаций:
8

Комментарии


      1. SadKo 16 апр 2019
        Автор я, это не перевод какой-либо статьи с английского языка.
      2. mmc 8 фев 2019
        > В 1974 году появился широко распространённый по сей день процессор 8080. Но если в то время процессор управлял компьютером ZX-Spectrum, то на сегодняшний день его можно встретить в различных бытовых устройствах.

        Он не мог управлять ZX-Spectrum, потому как последний был выпущен лишь в 1982 году, и в качестве процессора там использовался Zilog Z80, который, хоть и был программно совместимый с 8080, но обладал расширенным набором команд и имел существенно более высокую производительность.
      3. TermoSINteZ 24 авг 2017
        Ок. Ну ладно не мешаю :)
      4. SadKo 24 авг 2017
        В 2006-ом году сам писал-сочинял, это точно :)
      5. TermoSINteZ 24 авг 2017
        А, я думал это ты сам пишешь - сочиняешь ) Сорян тогда
        yashechka нравится это.
      6. SadKo 24 авг 2017
        Статья 2006 года, чего вы хотите :)
        yashechka нравится это.
      7. TermoSINteZ 24 авг 2017
        Небольшие замечания (пока бегло смотрел)
        1) Эти циклы после переключения - они не нужны. Достаточно дальнего джампа. При чем лучше заранее заинитить селекторы. Но тут понятно - вы разговор о селекторах перенесли в следующие главы. Но все равно - не нужны эти циклы. По этому было бы логичнее код предоставить уже после рассказа про селекторы и таблицы. ИМХО
        2) Где проверка возможностей процессора? Перед тем как переключиться, неплохо бы проверить :)
        3) можно не убеждаться, что загружена Windows. если кто по подумает запустить под виндой 16 битную программу - получит достойный отпор))

        Пока так) не принимайте близко к сердцу
        yashechka нравится это.