Перспективы создания ОС
На дворе наступила полноценная весна. Сегодня в наших широтах почти целый день лил весьма хороший дождь, что явно способствовало моему, если можно так выразиться, творческому процессу. Так что в свободное от работы время налейте себе чашечку кофе, сядьте в кресло и почитайте газету:). Сегодня мы рассмотрим некоторые более важные аспекты программирования операционных систем.
Как вы, возможно, поняли, наша сделанная на скорую руку операционная система не годится на многое, потому что размера предоставляемого ядру дискового пространства существенно не хватает, а максимум, что мы можем выжать из наших машин — это производительность их далеких предков. Нам доступен только один мегабайт ОЗУ независимо от вашей аппаратной конфигурации исключая те случаи, когда вы используете бородатый компьютер, в котором счет размера RAM идет на килобайты. Все это объясняется просто: максимальный адрес, который мы можем сообщить компьютеру, в лучшем случае равняется 0xFFFF:0xFFFF (или просто 0xFFFFF в его линейном эквиваленте). Это как раз и равняется одному мегабайту. Не много, правда? Как же современные операционные системы используют все "лошадиные силы" компьютеров? Для этого существует защищенный режим процессора.
Защищенный режим был реализован в процессорах фирмы Intel впервые в чипе 286 как замена реальному режиму. Защищенный режим иногда называют 32- битным, а реальный — 16-битным. Однако первым процессором, полностью поддерживающим 32-битную адресацию, был чип Intel 386, на который, кстати, ориентировалась тогда операционная система Windows 3. Именно в этом процессоре появилась 32-битная адресная шина, которая позволяла использовать 4 гигабайта оперативной памяти. Также в процессоре Intel 386 была введена страничная организация памяти, что позволяло намного лучше управлять памятью при наличии вместительного жесткого диска. Однако об этом в другой раз. Вернемся к защищенному режиму. После включения питания компьютера процессор находится в своем родном реальном режиме. BIOS также работает в реальном режиме. Следует заметить, что коды для 16-битного и 32-битного режимов различаются. Хотя бы поэтому, если вы вставите код, переводящий процессор в защищенный режим, в ту самую мини-ОС, она работать не будет. Более того: после перехода в защищенный режим нам становятся недоступны все сервисы, предоставляемые BIOS, так что их мы должны писать сами. До работы с контроллерами дисководов дело, я думаю, не дойдет, так как это слишком громоздкая тема, требующая существенных знаний. Кстати, могу на этот счет порекомендовать книгу Владимира Кулакова "Программирование дисковых подсистем", которая некогда была порекомендована мне. Уверяю: если вы действительно интересуетесь программированием ОС, покупайте — не прогадаете. Из вышесказанного следует вывод, что при надобности все требуемые операции с диском мы будем проводить из реального режима, как это делает, например, операционная система Menuet. Задача на сегодня — сделать такой примитивный "Hello, world!". Частенько, просматривая страницы форумов, на которых общаются программисты, пишущие ОС, я замечаю, что переход в защищенный режим вызывает очень много вопросов и массу позитивных эмоций после его реализации. Но для нас это обыденная вещь, правда;-)?
Суть защищенного режима заключается в том, что память разделяется на блоки — так называемые сегменты. Каждый сегмент описывается в особой таблице — Global descriptors table. Если в реальном режиме под понятием "сегмент" можно было понимать некоторое число, разбивающее память на кусочки и предоставляющее более удобный метод адресации, то в защищенном режиме сегмент — это совокупность правил и условий для каждого сегмента, что в случае использования многозадачности не позволит одному процессу залезть в область действия другого и навести там свои "порядки". Каждый дескриптор имеет размер в 32 бита. Что именно несут в себе дескрипторы, вы можете увидеть в таблице 1.
Заметьте, что первые два параметра формируют четырехбайтную величину, а потому положение байтов ниже второй строки рассматривается начиная со второго четырехбайтного куска дескриптора. Рассмотрим содержимое дескрипторов. Segment Limit обозначает ограничение сегмента в памяти. Значение задается в байтах или в четырех килобайтных величинах. Base Address — это базовый адрес сегмента, от которого начинается отсчет содержимого сегмента. Этот параметр также задается в байтах. Заметьте, что Base Address и Segment Limit задаются не целиком, а по частям. Параметр Type обозначает тип сегмента. Некоторые допустимые значения этого параметра приведены в таблице 2.
Я привел не все значения, т.к. некоторые сейчас просто не нужны. Далее флаг S при установленном значении 1 обозначает, что это сегмент кода/данных, или, иначе, системный сегмент. Интересен параметр DPL. Он определяет минимальный уровень привилегий процесса для доступа к сегменту. К слову, существуют так называемые кольца защиты (всего их четыре), характеризующие уровень процесса. Именно принадлежность к конкретному кольцу защиты характеризует уровень привилегий процесса. Наше ядро ОС всегда работает в нулевом (самом привилегированном) кольце, так что доступ к ресурсам компьютера для нас по-любому будет неограничен. Флаг P характеризует доступность сегмента. Если флаг P сброшен, то при первом же обращении к этому сегменту будет вызвано исключение #NP (Segment Not Present). Флаг D/B нам пока не нужен, так что пропускаем его. Флаг G характеризует гранулярность сегмента. Если он установлен, Segment Limit характеризуется в четырех килобайтных величинах (т.е., если Segment Limit равен 1, то на самом деле лимит будет равен четырем килобайтам). При сброшенном флаге G лимит сегмента исчисляется в байтах. Вот такая незатейливая картина. Страничная адресация включается нулевым битом регистра CR0 (этот бит еще называют PE — Protection Enabled). Для того, чтобы код, который должен исполняться после перехода в защищенный режим, исполнялся без проблем, надо, во-первых, отключить NMI (Non- Maskable Interrupts — немаскируемые прерывания), а во-вторых — сделать дальний прыжок с указанием сегмента кода. Первое необходимо для того, чтобы в случае возникновения прерывания от какой-то аппаратной части компьютера система не ушла в перезагрузку. Дело в том, что обработчики прерываний — как программных, так и аппаратных — необходимо задавать отдельно.
А так как при переходе в защищенный режим все обработчики пришли в негодность, поскольку были определены еще BIOS'ом в реальном режиме, мы должны определить их самостоятельно. А так как мы этого не сделали, и пока нашей целью не является обработка прерываний, мы просто их отключим. Также рекомендуется отключать программные прерывания командой cli (Clear Interrupts Flag). Делать дальний переход необходимо затем, чтобы установить нам нужный сегмент данных. Очень часто в качестве цели перехода указывается номер сегмента и через двоеточие — идентификатор, стоящий после команды перехода. Также важной деталью является включение линии A20 для реализации 32-битной адресации. Также для перехода в защищенный режим необходимо создать регистр таблицы глобальных дескрипторов. Регистр имеет размер в шесть байт и несет в первых двух байтах информацию о размере таблицы и в следующих четырех байтах — адрес регистра. Размер таблицы указывается в байтах и содержит значение, которое меньше реального размера на один байт. То есть, если у вас в таблице 3 элемента, то размер таблицы должен быть 3 умножить на 8 (размер одного дескриптора) минус 1. Нетрудно понять, что размер будет равен 23. Касаясь вопроса построения таблицы глобальных дескрипторов, следует упомянуть то, что первый дескриптор всегда заполняется нулями. При попытке использовать этот регистр возникает исключение. После перехода в защищенный режим необходимо заново установить все сегментные регистры (а точнее, ds, ss и — по надобности — es, fs, gs) и указатель стека (sp). При указании регистров ds и ss всегда должен использоваться сегмент данных, а при указании регистра ss должен обязательно быть использован регистр данных с правами Read/Write во избежание возникновения исключения. Регистр указывается смещением от начала таблицы дескрипторов. Если, допустим, мы хотим использовать второй регистр в сегменте кода, то должны выполнить команды mov ax, 0x8 и mov ds, ax. К слову, сегментные регистры напрямую не заполняются. Сейчас реализуем все вышесказанное в коде (см. листинг).
Листинг
1. org 0x7C00
2. use16
3. cli
4. mov ax, cs
5. mov ds, ax
6. mov ss, ax
7. mov sp, 0x7C00
8. xor al, al
9. out 0x70, al
10. lgdt [gdtr]
11. in al, 0x92
12. or al, 2
13. out 0x92, al
14. mov eax, CR0
15. or al, 1
16. mov CR0, eax
17. jmp 0x8:_protected_mode
18. use32
19. _protected_mode:
20. mov ax, 0x10
21. mov ds, ax
22. mov ss, ax
23. mov sp, 0x7C00
24. hlt
25. jmp $
26. gdt:
27. dd 0, 0
28. db 0xFF
29. db 0xFF
30. db 0x00
31. db 0x00
32. db 0x00
33. db 10011000b
34. db 11001111b
35. db 0x00
36. db 0xFF
37. db 0xFF
38. db 0x00
39. db 0x00
40. db 0x00
41. db 10010010b
42. db 11001111b
43. db 0x00
44. gdtr:
45. dw 3*8-1
46. dd gdt
47. times 510+0x7C00-$ db 0
48. db 0x55, 0xAA
Полный код "Hello, world'а" я не приводил, т.к. для этого пришлось бы переименовать всю газету в "Перспективы создания ОС", так что те, кому интересно, могут взять его на www.micronasp.com.ru, файл os.zip. Строки 1 и 2 не должны вызвать у вас вопросов: это директивы компилятора. В строке 3 мы сразу отключили прерывания. Строки 4-7 описывают инициализацию регистров кода, данных, стека и указателя на стек. Строки 8-9 как раз выполняют отключения немаскируемых прерываний. А в строке 10 мы загружаем нашу таблицу глобальных дескрипторов. Строки 11-13 нужны для того, чтобы включить линию A20. Есть очень много методов включения этой линии, но я выбрал этот как самый короткий. В строках 14-16 выполняется инвертирование бита PE в регистре CR0, который включает защищенный режим. К слову, MenuetOS тут же включает в регистре CR0 кэширование, ускоряющее работу процессора, хотя поначалу это совсем не обязательно. А в строке 17 собственно выполняется переход на нужный код с установкой регистра cs в значение 0x8 (то есть с установкой на указатель на сегмент кода). Не забывайте сообщать компилятору, где надо создавать 32-битный код, так как без этого система будет попросту исполнять неверные команды, что в лучшем члучае приведет к исключению. Делается это, опять же, директивой компилятора "use32", как написано в строке 18. В строках 20-23 опять инициализируются регистры, и в конце концов процессор "повисает" на строках 24-25. Но самое главное — это таблица.
Как вы можете заметить, нулевой дескриптор не используется. Строки 1-6 и 8 первого и второго дескрипторов одинаковы, потому что они нацелены на одинаковое пространство в памяти от нуля до четырех гигабайт. Внимательно рассмотрев строки 33 и 41, вы заметите, что дескрипторы различаются только по типу. В остальном все одинаково: гранулярность 1, DPL равен 0, флаг P равен 1. Откомпилировав программу с помощью FASM, записав ее на дискету и запустив на компьютере, вы не найдете ничего интересного: скучно мигающий курсор и никаких признаков жизни. Однако, если вы исследуете код в режиме отладки в эмуляторе Boch, то в логе заметите большие отличия от того, что могли бы видеть, запустив, например, нашу мини-ОС. Если вы хотите от защищенного режима чего-то конкретного, напишите процедуру, выводящую символы на экран. Только помните, что никаких сервисов BIOS уже быть не может, так что символы придется записывать прямо в видеобуфер (адрес 0xB8000).
Приведенный мною код не годится ни для чего, кроме демонстрации возможностей защищенного режима. Строить операционную систему, работающую в защищенном режиме и умещающуюся в 512 байт — просто сумашествие. Посмотрите хотя бы на размер оставшегося пространства в загрузчике, и все вопросы по этому поводу отпадут. Однако, если умело сочетать этот код с сервисами BIOS в реальном режиме, можно добиться гораздо больших успехов, примером чему — все та же бессмертная MenuetOS.
Влад Маслаков, Vladislav_1988@mail.ru
Как вы, возможно, поняли, наша сделанная на скорую руку операционная система не годится на многое, потому что размера предоставляемого ядру дискового пространства существенно не хватает, а максимум, что мы можем выжать из наших машин — это производительность их далеких предков. Нам доступен только один мегабайт ОЗУ независимо от вашей аппаратной конфигурации исключая те случаи, когда вы используете бородатый компьютер, в котором счет размера RAM идет на килобайты. Все это объясняется просто: максимальный адрес, который мы можем сообщить компьютеру, в лучшем случае равняется 0xFFFF:0xFFFF (или просто 0xFFFFF в его линейном эквиваленте). Это как раз и равняется одному мегабайту. Не много, правда? Как же современные операционные системы используют все "лошадиные силы" компьютеров? Для этого существует защищенный режим процессора.
Защищенный режим был реализован в процессорах фирмы Intel впервые в чипе 286 как замена реальному режиму. Защищенный режим иногда называют 32- битным, а реальный — 16-битным. Однако первым процессором, полностью поддерживающим 32-битную адресацию, был чип Intel 386, на который, кстати, ориентировалась тогда операционная система Windows 3. Именно в этом процессоре появилась 32-битная адресная шина, которая позволяла использовать 4 гигабайта оперативной памяти. Также в процессоре Intel 386 была введена страничная организация памяти, что позволяло намного лучше управлять памятью при наличии вместительного жесткого диска. Однако об этом в другой раз. Вернемся к защищенному режиму. После включения питания компьютера процессор находится в своем родном реальном режиме. BIOS также работает в реальном режиме. Следует заметить, что коды для 16-битного и 32-битного режимов различаются. Хотя бы поэтому, если вы вставите код, переводящий процессор в защищенный режим, в ту самую мини-ОС, она работать не будет. Более того: после перехода в защищенный режим нам становятся недоступны все сервисы, предоставляемые BIOS, так что их мы должны писать сами. До работы с контроллерами дисководов дело, я думаю, не дойдет, так как это слишком громоздкая тема, требующая существенных знаний. Кстати, могу на этот счет порекомендовать книгу Владимира Кулакова "Программирование дисковых подсистем", которая некогда была порекомендована мне. Уверяю: если вы действительно интересуетесь программированием ОС, покупайте — не прогадаете. Из вышесказанного следует вывод, что при надобности все требуемые операции с диском мы будем проводить из реального режима, как это делает, например, операционная система Menuet. Задача на сегодня — сделать такой примитивный "Hello, world!". Частенько, просматривая страницы форумов, на которых общаются программисты, пишущие ОС, я замечаю, что переход в защищенный режим вызывает очень много вопросов и массу позитивных эмоций после его реализации. Но для нас это обыденная вещь, правда;-)?
Суть защищенного режима заключается в том, что память разделяется на блоки — так называемые сегменты. Каждый сегмент описывается в особой таблице — Global descriptors table. Если в реальном режиме под понятием "сегмент" можно было понимать некоторое число, разбивающее память на кусочки и предоставляющее более удобный метод адресации, то в защищенном режиме сегмент — это совокупность правил и условий для каждого сегмента, что в случае использования многозадачности не позволит одному процессу залезть в область действия другого и навести там свои "порядки". Каждый дескриптор имеет размер в 32 бита. Что именно несут в себе дескрипторы, вы можете увидеть в таблице 1.
Данные | Биты |
Segment Limit 15:00 | 15:0 |
Base Address 15:00 | 31:16 |
Base Address 23:16 | 7:0 |
Type | 11:8 |
S | 12 |
DPL | 14:13 |
P | 15 |
Limit 19:16 | 19:16 |
AVL | 20 |
0 (zero) | 21 |
D/B | 22 |
G | 23 |
Base 31:24 | 31:24 |
Заметьте, что первые два параметра формируют четырехбайтную величину, а потому положение байтов ниже второй строки рассматривается начиная со второго четырехбайтного куска дескриптора. Рассмотрим содержимое дескрипторов. Segment Limit обозначает ограничение сегмента в памяти. Значение задается в байтах или в четырех килобайтных величинах. Base Address — это базовый адрес сегмента, от которого начинается отсчет содержимого сегмента. Этот параметр также задается в байтах. Заметьте, что Base Address и Segment Limit задаются не целиком, а по частям. Параметр Type обозначает тип сегмента. Некоторые допустимые значения этого параметра приведены в таблице 2.
Код | Тип | Доступ |
0 0 0 0 | данные | только чтение |
0 0 1 0 | данные | чтение/запись |
1 0 0 0 | код | только выполнение |
1 0 1 0 | код | чтение/выполнение |
Я привел не все значения, т.к. некоторые сейчас просто не нужны. Далее флаг S при установленном значении 1 обозначает, что это сегмент кода/данных, или, иначе, системный сегмент. Интересен параметр DPL. Он определяет минимальный уровень привилегий процесса для доступа к сегменту. К слову, существуют так называемые кольца защиты (всего их четыре), характеризующие уровень процесса. Именно принадлежность к конкретному кольцу защиты характеризует уровень привилегий процесса. Наше ядро ОС всегда работает в нулевом (самом привилегированном) кольце, так что доступ к ресурсам компьютера для нас по-любому будет неограничен. Флаг P характеризует доступность сегмента. Если флаг P сброшен, то при первом же обращении к этому сегменту будет вызвано исключение #NP (Segment Not Present). Флаг D/B нам пока не нужен, так что пропускаем его. Флаг G характеризует гранулярность сегмента. Если он установлен, Segment Limit характеризуется в четырех килобайтных величинах (т.е., если Segment Limit равен 1, то на самом деле лимит будет равен четырем килобайтам). При сброшенном флаге G лимит сегмента исчисляется в байтах. Вот такая незатейливая картина. Страничная адресация включается нулевым битом регистра CR0 (этот бит еще называют PE — Protection Enabled). Для того, чтобы код, который должен исполняться после перехода в защищенный режим, исполнялся без проблем, надо, во-первых, отключить NMI (Non- Maskable Interrupts — немаскируемые прерывания), а во-вторых — сделать дальний прыжок с указанием сегмента кода. Первое необходимо для того, чтобы в случае возникновения прерывания от какой-то аппаратной части компьютера система не ушла в перезагрузку. Дело в том, что обработчики прерываний — как программных, так и аппаратных — необходимо задавать отдельно.
А так как при переходе в защищенный режим все обработчики пришли в негодность, поскольку были определены еще BIOS'ом в реальном режиме, мы должны определить их самостоятельно. А так как мы этого не сделали, и пока нашей целью не является обработка прерываний, мы просто их отключим. Также рекомендуется отключать программные прерывания командой cli (Clear Interrupts Flag). Делать дальний переход необходимо затем, чтобы установить нам нужный сегмент данных. Очень часто в качестве цели перехода указывается номер сегмента и через двоеточие — идентификатор, стоящий после команды перехода. Также важной деталью является включение линии A20 для реализации 32-битной адресации. Также для перехода в защищенный режим необходимо создать регистр таблицы глобальных дескрипторов. Регистр имеет размер в шесть байт и несет в первых двух байтах информацию о размере таблицы и в следующих четырех байтах — адрес регистра. Размер таблицы указывается в байтах и содержит значение, которое меньше реального размера на один байт. То есть, если у вас в таблице 3 элемента, то размер таблицы должен быть 3 умножить на 8 (размер одного дескриптора) минус 1. Нетрудно понять, что размер будет равен 23. Касаясь вопроса построения таблицы глобальных дескрипторов, следует упомянуть то, что первый дескриптор всегда заполняется нулями. При попытке использовать этот регистр возникает исключение. После перехода в защищенный режим необходимо заново установить все сегментные регистры (а точнее, ds, ss и — по надобности — es, fs, gs) и указатель стека (sp). При указании регистров ds и ss всегда должен использоваться сегмент данных, а при указании регистра ss должен обязательно быть использован регистр данных с правами Read/Write во избежание возникновения исключения. Регистр указывается смещением от начала таблицы дескрипторов. Если, допустим, мы хотим использовать второй регистр в сегменте кода, то должны выполнить команды mov ax, 0x8 и mov ds, ax. К слову, сегментные регистры напрямую не заполняются. Сейчас реализуем все вышесказанное в коде (см. листинг).
Листинг
1. org 0x7C00
2. use16
3. cli
4. mov ax, cs
5. mov ds, ax
6. mov ss, ax
7. mov sp, 0x7C00
8. xor al, al
9. out 0x70, al
10. lgdt [gdtr]
11. in al, 0x92
12. or al, 2
13. out 0x92, al
14. mov eax, CR0
15. or al, 1
16. mov CR0, eax
17. jmp 0x8:_protected_mode
18. use32
19. _protected_mode:
20. mov ax, 0x10
21. mov ds, ax
22. mov ss, ax
23. mov sp, 0x7C00
24. hlt
25. jmp $
26. gdt:
27. dd 0, 0
28. db 0xFF
29. db 0xFF
30. db 0x00
31. db 0x00
32. db 0x00
33. db 10011000b
34. db 11001111b
35. db 0x00
36. db 0xFF
37. db 0xFF
38. db 0x00
39. db 0x00
40. db 0x00
41. db 10010010b
42. db 11001111b
43. db 0x00
44. gdtr:
45. dw 3*8-1
46. dd gdt
47. times 510+0x7C00-$ db 0
48. db 0x55, 0xAA
Полный код "Hello, world'а" я не приводил, т.к. для этого пришлось бы переименовать всю газету в "Перспективы создания ОС", так что те, кому интересно, могут взять его на www.micronasp.com.ru, файл os.zip. Строки 1 и 2 не должны вызвать у вас вопросов: это директивы компилятора. В строке 3 мы сразу отключили прерывания. Строки 4-7 описывают инициализацию регистров кода, данных, стека и указателя на стек. Строки 8-9 как раз выполняют отключения немаскируемых прерываний. А в строке 10 мы загружаем нашу таблицу глобальных дескрипторов. Строки 11-13 нужны для того, чтобы включить линию A20. Есть очень много методов включения этой линии, но я выбрал этот как самый короткий. В строках 14-16 выполняется инвертирование бита PE в регистре CR0, который включает защищенный режим. К слову, MenuetOS тут же включает в регистре CR0 кэширование, ускоряющее работу процессора, хотя поначалу это совсем не обязательно. А в строке 17 собственно выполняется переход на нужный код с установкой регистра cs в значение 0x8 (то есть с установкой на указатель на сегмент кода). Не забывайте сообщать компилятору, где надо создавать 32-битный код, так как без этого система будет попросту исполнять неверные команды, что в лучшем члучае приведет к исключению. Делается это, опять же, директивой компилятора "use32", как написано в строке 18. В строках 20-23 опять инициализируются регистры, и в конце концов процессор "повисает" на строках 24-25. Но самое главное — это таблица.
Как вы можете заметить, нулевой дескриптор не используется. Строки 1-6 и 8 первого и второго дескрипторов одинаковы, потому что они нацелены на одинаковое пространство в памяти от нуля до четырех гигабайт. Внимательно рассмотрев строки 33 и 41, вы заметите, что дескрипторы различаются только по типу. В остальном все одинаково: гранулярность 1, DPL равен 0, флаг P равен 1. Откомпилировав программу с помощью FASM, записав ее на дискету и запустив на компьютере, вы не найдете ничего интересного: скучно мигающий курсор и никаких признаков жизни. Однако, если вы исследуете код в режиме отладки в эмуляторе Boch, то в логе заметите большие отличия от того, что могли бы видеть, запустив, например, нашу мини-ОС. Если вы хотите от защищенного режима чего-то конкретного, напишите процедуру, выводящую символы на экран. Только помните, что никаких сервисов BIOS уже быть не может, так что символы придется записывать прямо в видеобуфер (адрес 0xB8000).
Приведенный мною код не годится ни для чего, кроме демонстрации возможностей защищенного режима. Строить операционную систему, работающую в защищенном режиме и умещающуюся в 512 байт — просто сумашествие. Посмотрите хотя бы на размер оставшегося пространства в загрузчике, и все вопросы по этому поводу отпадут. Однако, если умело сочетать этот код с сервисами BIOS в реальном режиме, можно добиться гораздо больших успехов, примером чему — все та же бессмертная MenuetOS.
Влад Маслаков, Vladislav_1988@mail.ru
Компьютерная газета. Статья была опубликована в номере 17 за 2005 год в рубрике soft :: ос