%define SETUP_SEG 0x07e0 %define SETUP_SECTS 10
%define KERNEL_SEG 0x1000 %define KERNEL_SECTS 1000
Для начала описываем место и размер для каждого загружаемого блока.
Размеры пока произвольные, поскольку все остальное еще предстоит написать.
section .text BITS 16
org 0x7c00
Как я уже говорил, boot sector загружается и запускается по адресу 0:7c00h Содержимое регистров при старте таково:
cs содержит 0 ip содержит 7с00h Прерывания запрещены! Про содержание остальных регистров мне ничего не известно, если кто-то, что-то знает, напишите мне. Остальные регистры мы будем инициализировать самостоятельно.
entry_point: mov ax, cs
cli mov ss, ax mov sp, entry_point sti
mov ds, ax
Стек у нас будет располагаться перед программой, до служебной области BIOS еще остается порядка 30 килобайт, для стека больше чем достаточно. Прерывания изначально запрещены, но я все равно сделаю это самостоятельно, на всякий случай. и разрешу после установки стека. Никаких проблем это вызвать, по-моему, не должно.
Так же, нулевым значением, инициализируем сегментный регистр ds.
; Сохpаняем фоpму куpсоpа mov ah, 3 xor bh, bh int 0x10
push cx
; отключаем куpсоp mov ah, 1 mov ch, 0x20 int 10h
Чтобы все было красиво и радовало глаз, мы на время чтения отключим курсор. Иначе он будет мелькать на экране. Чтобы его потом восстановить, как и был, мы сохраняем его форму в стеке.
; Загpужаем setup mov ax, SETUP_SEG mov es, ax
mov ax, 1 mov cx, SETUP_SECTS
mov si, load_setup_msg call load_block
call outstring
mov si, complete_msg call outstring
Загружаем первый блок (setup). Процедуру загрузки блока мы рассмотрим немного позже. А в остальном здесь, по-моему, все понятно.
; загpужаем ядpо. mov ax, KERNEL_SEG mov es, ax
mov ax, 1 + SETUP_SECTS mov cx, KERNEL_SECTS
mov si, load_kernel_msg call load_block
call outstring
mov si, complete_msg call outstring
Загружаем второй блок (kernel). Здесь все в точности аналогично первому блоку.
; Восстанавливаем куpсоp pop cx mov ah, 1 int 0x10
В прошлом выпуске я описывал структуру этой файловой системы. Как вы поняли, (я надеюсь) в файловой системе присутствует Super Block и дескрипторы групп. Эта информация хранится в начале раздела. Super Block во 2-м килобайте, дескрипторы групп - в третьем.
Стоит заметить, что первый килобайт для нужд файловой системы не используется и может быть целиком использован для boot sector'а (правда он уже будет не сектор, а килобайт :). Но для этого следует подгрузить второй сектор boot'а.
А для инициализации файловой системы нам нужно загрузить super block и дескрипторы групп, они же понадобятся нам для работы с файловой системой.
Это все можно загрузить одновременно, как мы и сделаем.
mov ax, 0x7e0 mov es, ax mov ax, 1 mov cx, 5 call load_block
Для этого мы используем уже знакомую процедуру загрузки блока, но эта процедура станет значительно короче, потому что никаких процентов мы больше не будем выводить.
В es засылается адрес, следующий за загруженным загрузочным сектором (Загружается он, как мы помним, по адресу 7c00h, и имеет длину 200h байт, следовательно свободная память начинается с адреса 7e00h, а сегмент для этого адреса равен 7e0h). В ax засылается номер сектора с которого начинается блок (в нашем случае это первый сектор, загрузочный сектор является нулевым). в cx засылается длина загружаемых данных в секторах (1 - дополнительная часть boot sector'а, 2 - Super Block ext2, 2 - дескрипторы групп. Всего 5 секторов).
Теперь вызовем процедуру инициализации файловой системы. Эта процедура достаточно проста, и проверяет только соответствие magic номера файловой системы и вычисляет размеры блока для работы.
sb equ 0x8000
ext2_init: pusha cmp word [sb + ext2_sb.magic], 0xef53 jz short .right
mov si, bad_sb call outstring
popa stc ret
bad_sb: db 'Bad ext2 super block!', 0ah, 0dh, 0
В случае несоответствия magic номера происходит вывод сообщения об ошибке и выход из подпрограммы. Чтобы сигнализировать об ошибке используется бит C регистра flags.
.right: mov ax, 1024 mov cl, [sb + ext2_sb.log_block_size] shl ax, cl mov [block_size], al ; Размер блока в байтах shr ax, 2 mov [block_dword_size], ax ; Размер блока в dword shr ax, 2 mov [block_seg_size], ax ; Размер блока в параграфах shr ax, 5 mov [block_sect_size], ax ; Размер блока в секторах popa clc ret
В своей работе мы не будем ориентироваться на процессор i286, поскольку 16-битная архитектура и отсутствие механизма страничного преобразования сильно усложняет программирование операционной системы. К тому же, таких процессоров давно уже никто не использует. :)
Ориентироваться мы будем на i386 или более старшие модели процессоров, вплоть до последних.
Ядро системы при распределении памяти оперирует 4-х килобайтными страницами.
Страницы могут использоваться самим ядром, для нужд драйверов (кэширование, например), или для процессов.
Программа или процесс состоит из следующих частей:
Сегмент кода. Может только выполняться, сама программа его не прочитать, не переписать не может! Использовать для этого сегмента swap не нужно, при необходимости код считывается прямо из файла; Сегмент данных состоит из трех частей:
Константные данные, их тоже можно загружать из файла, так как они не меняются при работе программы; Инициализированные данные. Участвует в процессе свопинга; Не инициализированные данные. Так же участвует в свопинге;
Сегмент стека. Так же участвует в свопинге.
Но, обычно, системы делят сегмент данных на две части: инициализированные данные и не инициализированные данные.
Все сегменты разбиваются на страницы. Сегмент кода имеет постоянный размер. Сегмент данных может увеличиваться в сторону больших адресов. Сегмент стека, поскольку растет вниз, увеличивается в сторону уменьшения адресов. Страницы памяти для дополнительных данных или стека выделяются системой по мере необходимости.
Очень интересный момент:
При выполнении программы операционная система делает следующие действия:
Готовит для программы локальную таблицу дескрипторов; Готовит для программы каталог страниц, все страницы помечаются как не присутствующие в памяти. Все. При передаче управления этой программе процессор генерирует исключение по отсутствию страницы, в котором нужная страница загружается из файла или инициализируется.
Еще один интересный момент:
Когда в системе загружается две или более одинаковых программы - нет необходимости для каждой из них выделять место для кодового сегмента, они спокойно могут использовать один код на всех.
Дескрипторные таблицы состоят из записей по 64 бита (8 байт) в каждой. Формат дескриптора таков:
7 | 6 | 5 | 4 |
Базовый адрес 31-24 | Предел 19-16 | Права доступа | Базовый адрес 23-16 |
3 | 2 | 1 | 0 |
Базовый адрес 15-0 | Предел 15-0 |
Сразу бросается в глаза очень странная организация дескриптора, но это связано с совместимостью с процессором i286, формат дескриптора в котором был таков:
7 | 6 | 5 | 4 |
Зарезервировано | Права доступа | Базовый адрес 23-16 | |
3 | 2 | 1 | 0 |
Базовый адрес 15-0 | Предел 15-0 |
Что же содержится в дескрипторе:
Базовый адрес - 32 бита (24 бита для i286). Определяет линейный адрес памяти, с которого начинается сегмент. В отличие от реального режима этот адрес может быть указан с точностью до байта.
Предел - 20 бит (16 бит для i286). Определяет размер сегмента (максимальный адрес, по которому может быть произведено обращение, это справедливо не всегда но об этом чуть позже). 20-битное поле может показаться не очень то большим для 32-х битного процессора, но это не так. Оно не всегда показывает размер в байтах. Но и об этом чуть позже.
Байт прав доступа:
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
P | DPL | S | Type | A |
Бит P (present) - Указывает на присутствие сегмента в памяти. обращение к отсутствующему сегменту вызывает особый случай не присутствия сегмента в памяти.
Двух битное поле DPL определяет уровень привилегий сегмента. Про Уровни привилегий мы поговорим чуть позже.
Бит S (Segment)- Будучи установленным в 1, определяет сегмент памяти, к которому может быть получен доступ на чтение (запись) или выполнение.
Три бита Type - в зависимости от бита S определяет либо возможности чтения/записи, выполнения сегмента или определяет тип системных данных, хранимых в селекторе. Подробнее это выглядит так:
Если бит S установлен в 1, о поле Type делится на биты:
2 | 1 | 0 |
1 - код | Подчиненный сегмент кода | Допустимо считывание |
0 - данные | Расширяется вниз | Допустима запись |
Если сегмент расширяется вниз (это используется для стековых сегментов) то поле предела показывает адрес, выше которого допустима запись. ниже запись недопустима и вызовет нарушение пределов сегмента.
TYPE | A | Описание |
000 | 1 | TSS для i286 |
001 | 0 | LDT |
001 | 1 | Занятый TSS для i286 |
010 | 0 | Шлюз вызова i286 |
010 | 1 | Шлюз задачи |
011 | 0 | Шлюз прерывания i286 |
011 | 1 | Шлюз исключения i286 |
100 | 1 | TSS для i386 |
101 | 1 | Занятый TSS i386 |
110 | 0 | Шлюз вызова i386 |
111 | 0 | Шлюз прерывания i386 |
111 | 1 | Шлюз ловушки i386 |
7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
G | D | 0 | U | Предел 19-16 |
Почти любое приложение пользуется динамически выделяемыми блоками памяти (известная, наверное, всем функция malloc в c). Сейчас мы поговорим о том, как это все работает.
Подходить к этому можно по разному, но принцип везде прослеживается один. На каждый блок памяти необходимо иметь структуру, описывающую занятось блока, его размер. В примитивной реализации это может выглядеть так, как это сделано в DOS.
В ДОСе вся память на равных правах принадлежит всем запущенным программам. Но чтобы операционная система могла как-то контролировать использование памяти, в ДОСе применяются MCB (Memory Control Block). Формат этого блока таков:
struct { char Signature; unsigned short OwnerId; unsigned short SizeParas; char Reserved[3]; char OwnerName[8]; };
Размер структуры 16 байт (1 параграф памяти) и эта структура непосредственно предшествует описываемому блоку памяти.
Размер блока указывается в параграфах в поле SizeParas. Такая структура вполне подходит для ограниченной по размерам памяти DOS, но для приложений она не очень то применима. Разница состоит в том, что в случае ДОС, чтобы найти блок свободной памяти (Такие блоки помечаются нулевым OwnerId), необходимо пройти по всем блокам от начала цепочки, до тех пор, пока не встретится свободный блок соответствующего размера. В ДОСе имеется функция, с помощью которой можно получить адрес первого блока (Base MCB) (int 21h, fn 52h).
Столь медленный поиск не страшен для DOS, у которого количество блоков редко превышает несколько десятков, но в приложениях поиск по цепочке блоков может быть достаточно долгой процедурой.
Поэтому в приложениях обычно применяется другой алгоритм, который заключается в следующем. (Я рассмотрю наиболее быстрый алгоритм, вариантов, конечно, может быть множество):
У каждого блока, как я уже говорил, есть два основных параметра: размер и флаг занятости. Оба эти параметра размещаются в одном двойном слове памяти. Поскольку как начало блока, так и его размер обычно выравниваются на четное число байт, младшие биты размера остаются неиспользуемыми (всегда равны нулю) и флаг занятости размещается в одном из них.
Ядро состоит из следующих компонентов:
"Собственно ядро" Драйвера устройств Системные вызовы
В зависимости от организации внутренних взаимодействий, ядра подразделяются на "микроядра" (microkernel) и монолитные ядра.
Системы с "микроядром" строятся по модульному принципу, имеют обособленное ядро, и механизм взаимодействия между драйверами устройств и процессами. По такому принципу строятся системы реального времени. Примерно так сделан QNX или HURD.
Монолитное ядро имеет более жесткую внутреннюю структуру. Все установленные драйвера жестко связываются между собой, обычно прямыми вызовами. По таким принципам строятся обыкновенные операционные системы типа Linux, FreeBSD.
Естественно, не все так четко, идеального монолитного или "микроядра" нет, наверное, ни в одной системе, просто системы приближаются к тому или иному типу ядра.
Мне бы очень хотелось, чтобы то, что мы будем делать, больше походило на первый тип ядер.
Linux разрабатывался на операционной системе Minix. В ней была (да и есть) файловая система minixfs. Система не очень гибкая и достаточно ограниченная. После появления Linux была разработана (на базе minixfs) файловая система extfs, которую в скором времени заменила ext2fs, которая и используется в большинстве Linux, по сей день.
Для начала давайте рассмотрим основное устройство этой файловой системы:
Boot sector (1 сектор) Свободно (1 сектор, может быть использован для расширения Boot sector'а до килобайта) Super block (2 сектора или 1024 байта длиной) Group descriptors (2 сектора максимум) Group 1 Group 2 ... и так далее... до Group 32 если необходимо.
Если ext2fs находится на каком ни будь разделе жесткого диска, или является не загрузочной, то boot sector'а там может вообще не быть.
Super block содержит в себе информацию о файловой системе и имеет следующий формат:
struct ext2_super_block { u_int32 s_inodes_count; u_int32 s_blocks_count; u_int32 s_r_blocks_count; u_int32 s_free_blocks_count; u_int32 s_free_inodes_count; u_int32 s_first_data_block; u_int32 s_log_block_size; int32 s_log_frag_size; u_int32 s_blocks_per_group; u_int32 s_frags_per_group; u_int32 s_inodes_per_group; u_int32 s_mtime; u_int32 s_wtime; u_int16 s_mnt_count; u_int16 s_max_mnt_count; u_int16 s_magic; u_int16 s_state; u_int16 s_errors; u_int16 s_pad; u_int32 s_lastcheck; u_int32 s_checkinterval; u_int32 s_reserved[238]; };
Не буду описывать значение всех полей этой структуры, ограничусь основными. Размер блока файловой системы можно вычислить так: 1024 * s_log_block_size. Размер блока может быть 1, 2 или 4 килобайта размером.
Об остальных полях чуть попозже.
А теперь рассмотрим группы дескрипторов файловой системы.
Формат дескриптора группы таков:
struct ext2_group_desc { u_int32 bg_block_bitmap; u_int32 bg_inode_bitmap; u_int32 bg_inode_table; u_int16 bg_free_blocks_count; u_int16 bg_free_inodes_count; u_int16 bg_used_dirs_count; u_int16 bg_pad; u_int32 bg_reserved[3]; };
Содержимое группы таково:
Этот тип файловых систем разработала фирма Microsoft достаточно давно. Вместе с первыми DOS... С тех пор неоднократно натыкались на различные препятствия и дорабатывались в соответствии с требованиями времени.
Теперь пойдет небольшой экскурс в историю. :)
В 1977 году Биллом Гейтсом и Марком МакДональдом была разработана первая файловая система FAT. Ради совместимости с CP/M в ней было ограничено имя файла. Максимальная длина имени составляла 8 символов, и 3 символа можно было использовать для расширения файла. Регистр букв не различался и не сохранялся. Размер кластера не превышал 4 килобайта. Размер диска не мог превышать 16 мегабайт. В 1981 году вышла первая версия MSDOS, которая базировалась на FAT. Начиная с MSDOS версии 3.0, в файловой системе появилось понятие каталога.
Для поддержки разделов более 16 мегабайт размер элемента FAT был увеличен до 16 бит, (первая версия была 12-битная) а максимальный размер кластера увеличен до 32 килобайт. Это позволило создавать разделы до 2 гигабайт. В таком состоянии FAT просуществовал до появления VFAT, появившегося вместе с выходом Windows'95, в которой появилась поддержка длинных имен файлов. Теперь имя файлов могло иметь длину до 255 символов, но ради совместимости старый формат имен так же остался существовать. Немного позже FAT был еще расширен, размер элемента FAT стал 32 бита, при этом максимальный размер кластера вновь уменьшился до 4 килобайт, но это позволило создавать разделы до 2 терабайт. Кроме того, была расширена информация о файлах. Теперь она позволяли хранить помимо времени создания файла время модификации и время последнего обращения к файлу.
Ну а теперь подробнее рассмотрим структуру этой файловой системы.
Общий формат файловой системы на базе FAT таков:
Boot sector (в нем так же содержится "Блок параметров FS") Reserved Sectors (могут отсутствовать) FAT (Таблица размещения файлов) FAT (вторая копия таблицы размещения файлов, может отсутствовать) Root directory (корневая директория) Область файлов. (Кластеры файловой системы)
В данном обзоре мы будем говорить только о 32-х битной версии этого формата, ибо 64-х битная нам пока ни к чему.
Любой файл формата ELF (в том числе и объектные модули этого формата) состоит из следующих частей:
Заголовок ELF файла; Таблица программных секций (в объектных модулях может отсутствовать); Секции ELF файла; Таблица секций (в выполняемом модуле может отсутствовать);
Ради производительности в формате ELF не используются битовые поля. И все структуры обычно выравниваются на 4 байта.
Теперь рассмотрим типы, используемые в заголовках ELF файлов:
Тип | Размер | Выравнивание | Комментарий |
Elf32_Addr | 4 | 4 | Адрес |
Elf32_Half | 2 | 2 | Беззнаковое короткое целое |
Elf32_Off | 4 | 4 | Смещение |
Elf32_SWord | 4 | 4 | Знаковое целое |
Elf32_Word | 4 | 4 | Беззнаковое целое |
unsigned char | 1 | 1 | Безнаковое байтовое целое |
Теперь рассмотрим заголовок файла:
#define EI_NIDENT 16
struct elf32_hdr { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; /* Entry point */ Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx; };
Массив e_ident содержит в себе информацию о системе и состоит из нескольких подполей.
struct { unsigned char ei_magic[4]; unsigned char ei_class; unsigned char ei_data; unsigned char ei_version; unsigned char ei_pad[9]; }
ei_magic - постоянное значение для всех ELF файлов, равное { 0x7f, 'E', 'L', 'F'}
ei_class - класс ELF файла (1 - 32 бита, 2 - 64 бита который мы не рассматриваем)
ei_data - определяет порядок следования байт для данного файла (этот порядок зависит от платформы и может быть прямым (LSB или 1) или обратным (MSB или 2)) Для процессоров Intel допустимо только значение 1.
ei_version - достаточно бесполезное поле, и если не равно 1 (EV_CURRENT) то файл считается некорректным.
В поле ei_pad операционные системы хранят свою идентификационную информацию. Это поле может быть пустым. Для нас оно тоже не важно.
Во многом он аналогичен формату ELF, ну и не удивительно, там так же должны быть секции, доступные для загрузки.
Как и все в Microsoft :) формат PE базируется на формате EXE. Структура файла такова:
00h - EXE заголовок (не буду его рассматривать, он стар как Дос. :) 20h - OEM заголовок (ничего существенного в нем нет); 3сh - смещение реального PE заголовка в файле (dword). таблица перемещения stub; stub; PE заголовок; таблица объектов; объекты файла;
stub - это программа, выполняющаяся в реальном режиме и производящая какие-либо предварительные действия. Может и отсутствовать, но иногда может быть нужна.
Нас интересует немного другое, заголовок PE.
Структура его такая:
struct pe_hdr { unsigned long pe_sign; unsigned short pe_cputype; unsigned short pe_objnum; unsigned long pe_time; unsigned long pe_cofftbl_off; unsigned long pe_cofftbl_size; unsigned short pe_nthdr_size; unsigned short pe_flags; unsigned short pe_magic; unsigned short pe_link_ver; unsigned long pe_code_size; unsigned long pe_idata_size; unsigned long pe_udata_size; unsigned long pe_entry; unsigned long pe_code_base; unsigned long pe_data_base; unsigned long pe_image_base; unsigned long pe_obj_align; unsigned long pe_file_align;
// ... ну и еще много всякого, неважного. };
Много всякого там находится. Достаточно сказать, что размер этого заголовка - 248 байт.
И главное что большинство из этих полей не используется. (Кто так строит?) Нет, они, конечно, имеют назначение, вполне известное, но моя тестовая программа, например, в полях pe_code_base, pe_code_size и тд содержит нули но при этом прекрасно работает. Напрашивается вывод, что загрузка файла осуществляется на основе таблицы объектов. Вот о ней то мы и поговорим.
Таблица объектов следует непосредственно после PE заголовка. Записи в этой таблице имеют следующий формат:
struct pe_ohdr { unsigned char o_name[8]; unsigned long o_vsize; unsigned long o_vaddr; unsigned long o_psize; unsigned long o_poff; unsigned char o_reserved[12]; unsigned long o_flags; };
Немного терминологии:
Уровень привилегий может быть от 0(высший) до 3(низший). Следовательно повышение уровня привилегий соответствует его уменьшению в численном эквиваленте, понижение - наоборот.
В дескрипторе содержатся биты DPL, которые определяют максимальный уровень привелегий для доступа к сегменту.
В селекторе содержится RPL - то есть запрашиваемый уровень привилегий.
RPL секущего кодового сегмента (хранится в регистре cs) является уровнем привилегий данного процесса и называется текущим уровнем привилегий (CPL)
Прямые обращения к сегментам возможны при соблюдении следующих условий:
В случае если запрашиваемый уровень привилегий больше текущего, то запрашиваемый уровень понижается до текущего. При обращении к сегменту данных RPL селектора должен быть не ниже DPL сегмента. При обращении к сегменту кода возможно только при равенстве CPL, RPL и DPL. Если сегмент кода помечен как подчиненный, то для обращения к нему необходимо иметь уровень привилегий не ниже уровня сегмента. При этом выполнение сегмента происходит с текущим уровнем привилегий.
Косвенные вызовы возможны только через шлюзы при соблюдении следующих условий:
DPL шлюза должен быть не выше, чем CPL сегмента, из которого производится вызов шлюза. DPL сегмента, на который указывает шлюз, должно быть не ниже чем DPL шлюза.
Я уже неоднократно упоминал это слово в предидущих выпусках. Но думаю что не всем было понятно что это такое. Сейчас мы рассмотрим это поподробнее.
Исключения или системные прерывания существовали еще в самых первых моделях процессоров от Intel. Вот их список:
Division by zero (деление на ноль или переполнение при делении); Single step (пошаговая отладка); Breakpoint; Overflow (срабатывает при команде into в случае установленного флага overflow в регистре flags); Invalid opcode (i286+); No math chip;
Исключения располагаются в начале таблицы прерываний. В реальном режиме занимают 8 первых векторов прерываний.
Введение защищенного режима потребовало введения дополнительных исключений. В защищенном режиме первые 32 вектора прерываний зарезервированы для исключений. Не все они используются в существующих процессорах, в будующем возможно их будет больше. Системные прерывания в защищенном режиме делятся на три типа: нарушения (fault), ловушки (trap) и аварии (abort). Итак в защищенном режиме у нас существуют следующие исключения:
Divide error (fault); Debug (fault/trap); Breakpoint (trap); Overflow (trap); Bounds check (fault); Invalid opcode (fault); Coprocessor not available (fault); Double fault (abort); Coprocessor segment overrun (fault); Invalid tss (fault); Segment not present (fault); Stack fault (fault); General protection fault (fault); Page fault (fault); Coprocessor error (fault); Alignument check (fault) (i486+); Hardware check (abort) (Pentium+); SIMD (fault) (Pentium III+).
Нарушения возникают вследствии несанкционированных или неправильных действий программы, предполагается, что ошибки можно исправить и продолжить выполнение программы с инструкции, которая вызвала ошибку.
Ловушки возникают после выполнения инструкции, но тоже подразумевают исправление ошибочной ситуации и дальнейшую работу программы.
Аварии возникают в случае критических нарушений, после этого программа уже не может быть перезапущена и должна быть закрыта.
Но иногда в случае ошибки или ловушки программа тем не менее не может продолжить свое выполнение. Это зависит от тяжести нарушения и от организации операционной системы, которая обрабатывает исключения. И если ошибка или ловушка не может быть исправлена, программу так же следует закрыть.
При возникновении исключения процессор иногда помещает в стек код ошибки, по которому обработчик исключения может проанализировать и, возможно, исправить возникшую ошибку.
Все исключения обрабатываются операционной системой. В случае микроядерных систем этим занимается микроядро. Вот о микроядрах мы и поведем наш дальнейший разговор.
Ранние модели процессоров от Intel имели 16 бит шины данных и 20 бит шины адреса. Это налагало определенные ограничения на адресацию памяти, ибо 16-бинтный регистр невозможно было использовать для адресации более чем 64 килобайт памяти. Чтобы обойти это препятствие разработчики предусмотрели сегментные регистры. Сегментный регистр хранит в себе старшие 16 бит адреса и для получения полного адреса к сегментному адресу прибавляется смещение в сегменте.
19 | 15 | 8 | 4 | 0 |
Сегмент | ||||
Смещение | ||||
Линейный адрес |
Таким образом, стало возможным адресовать до 1 мегабайта памяти. Это же позволило делать программы, не настолько привязанными к памяти и упростило адресацию. Сегменты могут начинаться с любого адреса, кратного 16 байтам, эти 16-байтные блоки памяти получили название параграфов. Но это и создает определенные неудобства. Первое неудобство состоит в том, что на один адрес памяти указывает 4096 различных комбинаций сегмент/смещение. Второе неудобство состоит в том, что нет возможности ограничить программам доступ к тем или иным частям памяти, что в некоторых случаях может быть существенно!
Введение защищенного режима решило эти проблемы, но ради совместимости любой из современных процессоров может работать в реальном или виртуальном режиме процессора i8086.
Тема конечно очень сложна для понимания. В дальнейших выпусках я буду концентрировать внимание на таких моментах. Интересующиеся люди могут почитать дополнительную, более подробную информацию по защите в литературе по микропроцессорам фирмы Intel. Мое изложение не претендует на полноту.
О чем мы будем говорить в следующем выпуске, пока не знаю. Надеюсь, что до конца недели он увидит свет.
Отправлено 2001-08-06 для 3542 подписчиков.
ведущий рассылки Dron
Архив Рассылки
При поддержке Kalashnikoff.ru
Для начала небольшое предисловие.
В процессорах имеются базовые регистры, которые могут задавать смещение. На 16-битной архитектуре максимальное смещение могло быть до 64 килобайт, что, в общем-то, не много и вызывало определенные трудности (разные модели памяти, разные форматы файлов). Так же, в 16-битной архитектуре присутствовали сегментные регистры, которые указывали адрес сегмента в памяти. В процессорах, начиная с i386, базовые регистры стали 32-х битными, что позволяет адресовать до 4 гигабайт. Сегментные регистры остались 16-битными, и в защищенном режиме они не содержат адреса! они содержат индекс дескриптора. В реальном режиме сегментные регистры работают так же, как и на 16-битных процессорах.
В реальном режиме сегментные регистры непосредственно указывают на адрес начала сегмента в памяти. Это позволяет нам, без каких либо преград, адресовать 1 мегабайт памяти. Но создает определенные трудности для защиты. А защищать нам нужно многое. Например, мы не можем пользовательским программам дать возможность непосредственно обращаться к коду или данным ядра. Так же мы не можем дать возможность пользовательским программам обращаться к коду или данным других пользовательских программ, поскольку это может нарушить их работоспособность.
Для этого был изобретен защищенный режим работы процессора, который появился в процессорах i286.
Защищенность этого режима заключается в следующем:
Сегментный регистр больше не указывает на адрес в памяти. В этом регистре теперь задается индекс в таблице дескрипторов.
Таблица дескрипторов может быть глобальная или локальная (применяется в многозадачных системах для изоляции адресного пространства задач) и представляет собой массив записей, по 8 байт в каждой, где описываются адреса, пределы и права доступа к сегментам.
Про адрес ничего не буду говорить, и так все ясно. Что такое предел? В этом Поле описывается размер сегмента. При обращении за пределы сегмента процессор генерирует исключение (специальное прерывание защищенного режима). Так же исключение генерируется в случае нарушения прав доступа к сегменту. Поле прав доступа описывает возможность чтения/записи сегмента, возможность выполнения кода сегмента, уровень привилегий для доступа к сегменту.
При обращении к сегменту из дескриптора берется базовый адрес сегмента и складывается со смещением сегмента. Так получается линейный 32-х разрядный (в i286 - 24-х разрядный) адрес. Для i286 на этом процесс получения адреса завершается, линейный адрес там равен физическому. Для i386 или выше это справедливо не всегда.
В первых выпусках я уже касался этой темы, но тогда я ограничился буквально несколькими словами. Но теперь мы решили двигаться именно в сторону микроядерности, значит стоит поподробнее рассказать, что это такое.
Принцип микроядерности заключается в том, что ядро практически не выполняет операций, связанных с обслуживанием внешних устройств. Эту функцию выполняют специальные программы-сервера. Ядро лишь предоставляет им возможность обращаться к устройствам. Помимо этого ядро обеспечивает многозадачность (параллельное выполнение программных потоков), межпроцессное взаимодействие и менеджмент памяти.
Приложения (как и сервера) у нас работают на третьем, непривилегированном кольце и не могут свободно обращаться к портам ввода/вывода или dma памяти. Тем более не могут сами устанавливать свои обработчики прерываний. Для использования ресурсов процессы обращаются к ядру с просьбой выделить необходимые ресурсы в их распоряжение. Осуществляется это следующим образом:
Для обеспечения доступа к портам ввода/вывода используются возможности процессоров, впервые появившиеся intel 80386. У каждой задачи (в сегменте состояния задачи (TSS)) существует карта доступности портов ввода/вывода. Приложение обращается к ядру с "просьбой" зарегистрировать для нее диапазон портов. Если эти порты до тех пор никем не были заняты, то ядро предоставляет их в распоряжение процесса, помечая их как доступные в карте доступности ввода/вывода этого процесса.
DMA память, опять таки после запроса у ядра, с помощью страничного преобразования подключается к адресному пространству процесса. Настройка каналов осуществляется ядром по "просьбе" процесса.
Доступ к аппаратным прерываниям (IRQ) осуществляется сложнее. Для этого процесс порождает в себе поток (thread), и сообщает ядру, что этот поток будет обрабатывать какое-то IRQ. При возникновении аппаратного прерывания, которое обрабатывает всетаки ядро, данный процесс выходит из состояния спячки, в котором он находился в ожидании прерывания, и ставится в очередь к менеджеру процессов. Такие потоки должны иметь более высокий приоритет, чем все остальные, дабы вызываться как можно скорее.
Многозадачные возможности в процессорах так же появились в процессорах, начиная с i286. Для реализации этого, процессор для каждой задачи использует, так называемый, "сегмент состояния задачи" ("Task State Segment", сокращенно TSS). В этом сегменте, при переключении задач, сохраняются все базовые регистры процессора, сегменты и указатели стека для трех уровней защиты (для каждого уровня используется свой стек), сегментный адрес локальной таблицы дескрипторов ("Local descriptor table", сокращенно LDT). В процессорах, начиная с i386, там еще хранится адрес каталога страниц (регистр CR3). Так же этот сегмент обеспечивает некоторые другие механизмы защиты, но о них мы пока не будем говорить.
Операционная система может расширить TSS, и использовать его для хранения регистров и состояния сопроцессора. Процессор при переключении задач не сохраняет этого. Так же возможны другие применения.
Один, отдельно взятый, процессор, в один момент времени, может исполнять только одну программу. Но к компьютерам предъявляются более широкие требования. Мало кто, в настоящее время, удовлетворился однозадачной операционной системой (к каким относился DOS, например). В связи с этим разработчики процессоров предусмотрели мультизадачные возможности.
Возможность эта заключается в том, что процессор выполняет какую-то одну программу (их еще называют процессами или задачами). Затем, по истечении некоторого времени (обычно это время меряется микросекундами), операционная система переключает процессор на другую программу. При этом все регистры текущей программы сохраняются. Это необходимо для того, чтобы через некоторое время вновь передать управление этой программе. Программа при этом не замечает каких либо изменений, для нее процесс переключения остается незаметен.
Для того чтобы программа не могла, каким либо образом, нарушить работоспособность системы или других программ, разработчики процессоров предусмотрели механизмы защиты.
Процессор предоставляет 4 "кольца защиты" (уровня привилегий), можно было бы использовать все, но это связано со сложностями взаимодействия программ разного уровня защиты. Поэтому в большинстве существующих систем используют два уровня. 0 - привилегированный уровень (ядро) и 3 - непривилегированный (пользовательские программы).
Всем этим обеспечивается надежное функционирование системы и независимость программ друг от друга.
Изменился адрес сайта. Сайт теперь располагается по адресу http://asmdev.narod.ru/asmos.html.
У проекта открылся сайт на sourceforge. http://dronix.sourceforge.net/, на котором пока пусто, но вскоре изменения можно будет заметить на http://sourceforge.net/projects/dronix/.
Ну, начнем с исторических функций.
Давным-давно, когда даже Билл Гейтс говорил что 640 килобайт хватит всем, но не у всех были эти 640 килобайт. :) в биосах существовала функция определения количества базовой памяти.
int 12h |
Выходные параметры:
ax - размер базовой памяти в килобайтах.
Сейчас уже вряд ли кому придет в голову, что базовой памяти может быть меньше 640 килобайт. но мало ли... ;)
Появлялись новые процессоры, и размеры памяти стали расти. в связи с чем появилась функция определения количества расширенной памяти.
int 15h fn 88h |
Входные параметры:
ah = 88hВыходные параметры:
ax - размер расширенной памяти в килобайтах.
Возможно из за архитектуры 286-х процессоров (которым размер шины адреса не позволяет иметь больше чем 16 мегабайт памяти) эта функция часто имеет аналогичное ограничение и результат в ax не может превышать 3с00h (Что составляет 15Мб).
Но, опять таки, появились новые процессоры. 16 мегабайт стало мало. Вследствие этого появилась еще одна функция BIOS:
int 15h fn e801h |
Входные параметры:
ax = e801h.Выходные параметры:
ax - размер расширенной памяти в килобайтах до 16Mb; bx - размер расширенной памяти в блоках по 64к свыше 16Мб; cx - размер сконфигурированный расширенной памяти в килобайтах до 16Mb; dx - размер сконфигурированной расширенной памяти в блоках по 64к свыше 16Мб.
Не знаю, что означает сконфигурированная память. Так написано в описании.
Здесь производители BIOS видимо оказались неединодушны. Некоторые версии в ax и bx возвращают 0, это значит что размер памяти следует определять из cx, dx.
Но видимо и 4 гигабайт оказалось мало. В новых BIOS появилась еще одна функция.
int 15h fn e820h |
Входные параметры:
eax = e820h; edx = 534d4150h ('SMAP'); ebx - смещение от начала карты памяти; eсx - Размер буфера; es:di - Адрес буфера для размещения карты памяти.Выходные параметры:
eax - 534d4150h ('SMAP'); ebx - следующее смещение от начала карты памяти, если = 0, то вся карта передана; ecx - Количество возвращенных байт; буфер заполнен информацией;
Помимо функций BIOS есть еще много других способов.
Самый простой - помереть память самому. :) Делается это из защищенного режима, страничное преобразование должно быть выключено, адресная линия A20 должна быть включена.
Можно мереть от нуля, но поскольку в первом мегабайте есть дыры (видеопамять, биосы, просто дыры), удобнее делать это начиная с первого мегабайта.
Вовсе не обязательно проверять каждый байт, достаточно проверять один байт на какое-то определенное количество памяти. Определенным количеством памяти можно посчитать мегабайт, но лучше (хотя и медленнее) за единицу памяти принять одну страницу памяти (4к).
Во избежание неприятностей память лучше не разрушать, а восстанавливать в первоначальном виде. делается это примерно так:
xchg [ebx], eax
xchg [ebx], eax
Если после этого в eax содержится то же значение, которое было до того, значит память присутствует по данному адресу. Если возвратилось 0ffffffffh, значит память отсутствует, если же что ни будь другое - то это может быть ROM, хотя после мегабайта вы вряд ли встретите какой либо BIOS. В любом случае если память по текущему адресу не обнаружена, значит, память закончилась и дальше искать чревато... существуют еще различные типы памяти (ACPI например) которую не стоит трогать.
Из защищенного режима можно воспользоваться содержимым CMOS, некоторые ячейки в нем BIOS заполняет определенными при начальном тесте системы значениями. Но здесь все не так однозначно как хотелось бы. Разные версии BIOS могут хранить значения в разных местах.
15h - Базовая память в килобайтах (младший байт) (IBM); 16h - Базовая память в килобайтах (старший байт) (IBM); 17h - Расширенная память в килобайтах (младший байт) (IBM); 18h - Расширенная память в килобайтах (старший байт) (IBM); 30h - Расширенная память в килобайтах (младший байт) (IBM); 31h - Расширенная память в килобайтах (старший байт) (IBM); 34h - Расширенная память более 16Мб (блоками по 64к) (младший байт) (AMI);
35h - Расширенная память более 16Мб (блоками по 64к) (старший байт) (AMI);
35h - Расширенная память (блоками по 64к) (младший байт) (AMI WinBIOS); 36h - Расширенная память (блоками по 64к) (старший байт) (AMI WinBIOS);
Байты 30-31 принято считать стандартными, но они определяют только 64Мб памяти. Не очень то подходят для использования.
Все устройства блочного доступа (к которым относятся жесткие или гибкие диски, компакт диски) при чтении/записи информации оперируют секторами. Для жестких или гибких дисков размер сектора равен 512 байт, в компакт-дисках размер сектора равен 2048 байт. Сектора являются физической единицей информации для носителя.
Для файловых систем такое распределение часто бывает не очень удобно, и в них вводится понятие кластера. Кластеры часто бывают больше по размеру, чем сектора носителя. Кластеры являются логической единицей файловых систем. Правда, не всегда они называются кластерами. В ext2 кластеры называются просто блоками, но это не столь важно. Для организации кластеров файловые системы хранят таблицы кластеров. Таблицы кластеров, естественно, расходуют дисковое пространство. Помимо этого, дополнительное дисковое пространство расходуется под каталоги файлов. Эти неизбежные расходы в разных файловых системах имеют разную величину. Но об этом мы поговорим ниже.
Здесь вообще все просто. Когда мы загрузили ядро, в файле ядра мы определили адреса сегмента кода и сегмента данных. Не смотря на то, что ядро имеет вполне конкретные смещения в сегменте (которые задаются при компиляции), код инициализации ядра рассчитан на работу без привязки к адресам. Это нужно для определения количества памяти, после перевода ядра на свои адреса доступ ко всей памяти будет для ядра затруднен в связи с включением механизма страничного преобразования.
Итак, переходим к выполнению кода ядра.
mov ebx, kernel_data mov eax, [ebx + module_struct.code_start] jmp eax
В этом фрагменте в eax записывается адрес начала кодового сегмента ядра.
Так как сегмент кода у нас занимает всю виртуальную память, нам не важно где находится ядро (хотя мы знаем, что оно было загружено в базовую память). Мы просто передаем ему управление.
О ядре мы начнем говорить в следующем выпуске. С загрузчиком мы практически закончили. Он уже обладает достаточной для нас функциональностью.
Как всегда вы можете задавать свои вопросы, высказывать пожелания по адресу, указанному ниже.
Хочу принести свои извинения читателям рассылки в текстовом формате. Что-то на субскрайбе с конвертацией не то.
Отправлено 2001-11-29 для 5592 подписчиков.
ведущий рассылки Dron
Сайт проекта
Архив Рассылки
При поддержке Kalashnikoff.ru
Бутсектор не особо беспокоится об организации памяти в системе - это забота ядра. Для перехода в защищенный режим он описывает всего два сегмента: сегмент кода и сегмент данных. оба сегмента имеют базовый адрес - 0 и предел в 4 гигабайта (это нам пригодиться для проверки наличия памяти).
Перед переходом в защищенный режим нам необходимо включить адресную линию A20. По моим сведениям этот механизм ввели в пору 286 для предотвращения несанкционированных обращений к памяти свыше одного мегабайта (непонятно зачем?). Но поскольку это имеет место быть - нам это нужно обрабатывать, иначе каждый второй мегабайт будет недоступен. Делается это почему-то через контроллер клавиатуры (еще одна загадка).
mov al, 0xd1 out 0x64, al mov al, 0xdf out 0x60, al
После этого можно переходить в защищенный режим.
lgdt [gd_desc]
В регистр gdtr загружается дескриптор GDT.
push byte 2 popf
Очищается регистр флагов.
mov eax, cr0 or al, 1 mov cr0, eax
Включается защищенный режим. Не смотря на то, что процессор уже находится в защищенном режиме, мы пока работаем в старых адресах. Чтобы от них уйти нам нужно сделать дальный переход в адреса защищенного режима.
jmp 16:.epm
BITS 32 .epm:
16 в этом адресе перехода - это не сегмент. Это селектор сегмента кода.
mov ax, 8 mov ds, ax mov es, ax
; Ставим стек. mov ss, ax movzx esp, sp
После всего этого мы инициализируем сегментные регистры соответствующими селекторами, в том числе и сегмент стека, но указатель стека у нас не меняется, только теперь он становится 32-х битным.
...
gd_table: ; пеpвый дескpиптоp - данные и стек istruc descriptor at descriptor.limit_0_15, dw 0xffff at descriptor.base_0_15, dw 0 at descriptor.base_16_23, db 0 at descriptor.access, db 0x92 at descriptor.limit_16_19_a, db 0xcf at descriptor.base_24_31, db 0 iend
; втоpой дескpиптоp - код istruc descriptor at descriptor.limit_0_15, dw 0xffff at descriptor.base_0_15, dw 0 at descriptor.base_16_23, db 0 at descriptor.access, db 0x9a ; 0x98 at descriptor.limit_16_19_a, db 0xcf at descriptor.base_24_31, db 0 iend
Это GDT - Глобальная таблица дескрипторов. Здесь всего два дескриптора, но во избежание ошибок в адресации обычно вводится еще один дескриптор - нулевой, который не считается допустимым для использования. Мы не будем резервировать для него место специально, просто начало таблицы сместим на 8 байт выше.
gd_desc: dw 3 * descriptor_size - 1 dd gd_table - descriptor_size
А это содержимое регистра GDTR. Здесь устанавливается предел и базовый адрес дескриптора. обратите внимание на базовый адрес, здесь происходит резервирование нулевого дескриптора.
Теперь процессор находится в защищенном режиме и уже не оперирует сегментами, а оперирует селекторами. Селекторов у нас всего три. Нулевой - недопустим. восьмой является селектором данных и шестнадцатый - селектором кода.
После этого управление можно передать ядру. дальше со всем этим будет разбираться оно.
То, что я до сих пор сделал пока рассчитано только на работы с дисками 1,4Мб, то есть с флопами. Это конечно ограничение в некоторой степени, но пока система еще далеко не готова, этого достаточно. Естественно это еще не окончательный вариант. Да и можно ли говорить об окончательности программных продуктов? Нет предела совершенству. :)
В обязанности бутсектора входит следующее:
Загрузить с диска дополнительные части кода и служебную информацию файловой системы. Загрузить с диска файл сценария (конфигурации) загрузки. Загрузить с диска ядро и модули. Перейти в защищенный режим. Передать управление ядру.
Если с первым и двумя последними пунктами все просто и компактно, то второй и третий пункт требуют возможности работы с файловой системой, а третий пункт помимо этого должен знать структуру бинарных форматов. На все это не хватает 512 байт, отводимых для бутсектора. Наш бутсектор занимает больше - один килобайт.
В файловой системе EXT2 с этим не возникает никаких проблем, поскольку первый килобайт файловой системы не используется.
В FAT это немного сложнее. Служебная структура, именуемая Boot Sector Record (BSR), содержит в себе все необходимые поля для выделения для загрузочного сектора места более чем 512 байт. Но как это сделать при форматировании, стандартными средствами, я не нашел. И если формат диска не соответствует каким-то внутренним представлениям Windows, то содержимое такого нестандартного диска может быть испорчено. Выход был найден случайно. Как оказалось утилита format хоть и не имеет таких параметров командной строки, но перед форматированием берет информацию из BSR. И если предварительно заполнить эту структуру (с нужными нам параметрами), а потом уже форматировать, то все получается так, как хочется нам. Таким образом, у меня получилось сделать диск, у которого два сектора зарезервированы (там будет размещаться boot), и одна копия FAT.
Ну теперь давайте по порядку рассмотрим все этапы работы бутсектора.
В прошлом выпуске, когда я говорил о дескрипторах и дескрипторных таблицах я ни слова не упомянул о дескрипторной таблице прерываний (Interrupt description table или IDT). Эта таблица так же состоит из дескрипторов, но в отличии от LDT и GDT в этой таблице могут размечаться только шлюзы. В защищенном режиме все прерывания происходят через IDT. Традиционная таблица векторов прерываний здесь не используется.
Формат дескрипторов шлюзов отличается от дескриптора сегмента.
Для начала рассмотрим шлюз вызова.
7 | 6 | 5 | 4 |
Смещение 31-16 | Права доступа | Количество слов стека | |
3 | 2 | 1 | 0 |
Селектор | Смещение 15-0 |
В поле прав доступа задается уровень привилегий, который должен быть ниже CPL текущего процесса, бит присутствия и соответствующий тип в остальных полях.
Селектор и смещение задают адрес вызываемой функции, при этом селектор должен присутствовать либо в GDT либо в активной LDT.
Параметр "Количество слов стека" служит для передачи аргументов в вызываемую функцию, при этом соответствующее количество слов копируется из стека текущего уровня привилегий в стек уровня привилегий вызываемой функции. Это поле использует только младшие 5 бит четвертого байта. Остальные биты должны быть нулевыми.
Обращаться к такому шлюзу, если дескриптор не расположен в IDT, можно только командой call far, при этом указываемое в команде смещение игнорируется. А селектор должен указывать на дескриптор шлюза вызова.
Шлюз прерывания и шлюз ловушки имеют одинаковый формат, отличаются между собой типами в байте прав доступа. В отличии от шлюза вызова эти шлюзы не содержат в себе Количества слов стека, поскольку прерывания бывают аппаратными и передача в них параметров через стек - бессмысленна. Эти шлюзы используются обычно только в IDT.
Шлюз задачи содержит в себе значительно меньше информации.
Во втором и третьем байте дескриптора записывается селектор TSS (Сегмента состояния задачи). Поле прав доступа заполняется аналогично другим шлюзам, но с соответствующим типом. Остальные поля дескриптора не используются.
При вызове такого шлюза происходит переключение контекста задачи. При этом вызывающая задача блокируется и не может быть вызвана до тех пор, пока вызванная задача не вернет ей управление командой iret.
Про правила доступа к шлюзам я говорил в прошлом выпуске, и в этот раз я закончу на этом. в следующем выпуске расскажу про прерывания более подробно.
Не смотря на гибкость средств защиты, имеющихся в процессорах (защита на уровне таблиц дескрипторов, защита на уровне сегментов, защита на уровне страниц) в существующих системах (как в Windows, так и в Unix) полноценено используется только страничная защита, которая хотя и может уберечь код от записи, но не может уберечь данные от выполнения. (Может быть, с этим и связано изобилие уязвимостей систем?)
Все сегменты адресуются с нулевого линейного адреса и простираются до конца линейной памяти. Разграничение процессов производится только на уровне страничных таблиц.
В связи с этим все модули линкуются не с начальных адресов, а с достаточно большим смещением в сегменте. В Windows используется базовый адрес в сегменте - 0x400000, в юникс (Linux или FreeBSD) - 0x8048000.
Некоторые особенности так же связаны со страничной организацией памяти.
ELF файлы линкуются таким образом, что границы и размеры секций приходятся на 4-х килобайтные блоки файла.
А в PE формате, не смотря на то, что сам формат позволяет выравнивать секции на 512 байт, используется выравнивание секций на 4к, меньшее выравнивание в Windows не считается корректным.
Ну, вот собственно и все на сегодня.
С файлами мы примерно разобрались, о чем пойдет речь дальше - пока не знаю. Обещаю только что речь, о чем нибудь, пойдет обязательно. :) Жду ваших отзывов, возможно, они помогут мне определиться с темой следующего выпуска.
Вновь приношу свои извинения за длительный перерыв.
Отправлено 2001-11-12 для 5364 подписчиков.
ведущий рассылки Dron
Сайт проекта
Архив Рассылки
При поддержке Kalashnikoff.ru
В процессорах, начиная с i386, появилась, так называемая, страничная организация памяти. Страница имеет размер 4 килобайта или 4 мегабайта. Большие страницы могут быть только в pentium или выше. Не знаю только, какой толк от таких страниц.
Если возможность страничной адресации не используется, то линейный адрес, как и на i286, равен физическому. Если используется - то линейный адрес разбивается на три части. Первая, 10-битная, часть адреса является индексом в каталоге страниц, который адресуется системным регистром CR3. Запись в каталоге страниц указывает адрес таблицы страниц. Вторая, 10-битная, часть адреса является индексом в таблице страниц. Запись в таблице страниц указывает физический адрес нахождения страницы в памяти. последние 12 бит адреса указывают смещение в этой странице.
В страничных записях, как и в дескрипторных записях, есть служебные биты, описывающие права доступа, и некоторые другие тонкости страниц. Одной из важных тонкостей является бит присутствия страницы в памяти. В случае не присутствия страницы, процессор генерирует исключение, в котором можно считать данную страницу из файла или из swap раздела. Это сильно облегчает реализацию виртуальной памяти. Чуть ниже мы про это поговорим.
Надеюсь, я не сильно вас утомил? более подробно про все это можно прочитать в книгах по архитектуре процессоров. А мы вернемся к операционным системам.
На "Собственно ядро" возлагаются функции менеджера памяти и процессов. Переключение процессов - это основной момент нормального функционирования системы. Драйвера не должны "тормозить", а тем более блокировать работу ядра. Windows - наглядный пример того, что этого нельзя допустить!
Теперь о драйверах. Драйвера - это специальные программы, обеспечивающие работу устройств компьютера. В существующих системах (во FreeBSD это точно есть, про Linux не уверен) предусматриваются механизмы прерывания работы драйверов по истечении какого-то времени. Правда, все зависит от того, как написан драйвер. Можно написать драйвер под FreeBSD или Linux, который полностью блокирует работу системы.
Избежать этого при двухуровневой защите не представляется возможным, поэтому драйвера надо будет тщательно программировать. В нашей работе драйверам мы уделим очень много внимания, поскольку от этого в основном зависит общая производительность системы.
Системные вызовы - это интерфейс между процессами и ядром (читайте-железом). Никаких других методов взаимодействия процессов с устройствами компьютера быть не должно. Системных вызовов достаточно много, на Linux их 190, на FreeBSD их порядка 350, причем большей частью они совпадают, соответствуя стандарту POSIX (стандарт, описывающий системные вызовы в UNIX). Разница заключается в передаче параметров, что легко будет предусмотреть. Естественно, мы не сможем сделать ядро, работающее одновременно на Linux и на FreeBSD, но по отдельности совместимость вполне реализуема.
Прикладным программам абсолютно безразлично, как системные вызовы реализуются в ядре. Это облегчает для нас обеспечение совместимости с существующими системами.
В следующем выпуске мы поговорим про защищенный режим процессора, распределение памяти, менеджер задач и рассмотрим, как это сделано в существующих системах.
Для возможности запуска из защищенного режима программ, предназначенных для реального, существует так называемый "Виртуальный режим процессора 8086". При этом полноценно работают механизмы преобразования адресов защищенного режима. А так же многозадачные системы, которые могут одновременно выполнять как защищенные задачи, так и виртуальные. При этом адресация в виртуальной задаче осуществляется традиционным для 8086 методом - сегмент/смещение.
Обращение к прерываниям осуществляется через IDT, но таблица прерываний реального режима может быть обработана из функций, шлюзы которых размещаются в IDT.
Обращение виртуальной задачи к портам так же может быть отслежено через прерывания защищенного режима. При обращении к запрещенным портам происходит исключение.
При желании может быть обеспечена абсолютно прозрачная работа нескольких виртуальных задач в одной мультизадачной среде. Но мы этой возможностью не будем пользоваться, и в своей работе будем рассчитывать на программы исключительно защищенного режима.
На этом я сегодня заканчиваю. До следующих выпусков. Пишите.
Отправлено 2001-08-10 для 3692 подписчиков.
ведущий рассылки Dron
Архив Рассылки
При поддержке Kalashnikoff.ru
Какой бы вы хотели видеть СВОЮ систему?
На какую систему она должна походить?
Сколько места на винчестере занимать?
Сколько памяти требовать для работы?
Ответы на эти вопросы присылайте по адресу: mailto:dron@infosec.ru?Subject=AsmOS
По всем вопросам вы можете обращаться ко мне по адресу: mailto:dron@infosec.ru?Subject=AsmOS.
При поддержке Kalashnikoff.ru
В 4-м выпуске, когда я расписывал вам, как писать boot sector, я допусил одну достаточно серьезную ошибку, которую признаю и благодарю Bug Maker'а, за то, что обратил на это мое внимаение. В процедуре load_sector я, первым делом, делю номер сектора на количество секоров на дорожке. Для деления используя беззнаковую команду div, предварительно расширяя ax в dx:ax знаковой командой cwd. Правда если учесть что максимальное количество секторов на гибком диске не превышает 2880, то старший, знаковый, бит ax всегда нулевой. Но, тем не менее, ошибка потенциальная. Этот фрагмент кода стоит писать так:
load_sector: push ax push cx
mov cl, 18 div cl
mov cx, dx inc cx
Исправившись, я вообще убрал команду cwd, и теперь делю на байт cl. Все это, к тому же, сэкономило мне два байта. :)
Но это еще не все.. при написании boot sector'а я говорил, что это совсем не окончательная версия. Так оно и получается. Из бутсектора мы уберем код загрузки kernel. Этим будет заниматься программа setup. Следовательно, boot sector'у осанется только считать setup и запусить его... Даже если сделать более корректную обработку ошибок чтения, у нас остается около 250 байт на всякие развлечения... :)
А setup должен будет уметь достаточно многое. В него будет встроена поддержка файловой системы, поддержка выполняемых форматов файлов. Мы собираемся делать микроядро, и setup'у придется загружать помимо ядра еще несколько дополнительных программ, которые понадобятся нам для нормального старта системы.
Но об этом позже. А теперь продолжаем разбираться с защищенным режимом.
boot sector DOS загружает в память два файла: io.sys и msdos.sys. Названия этих файлов в разных версиях DOS различались, не важно. Файл io.sys содержит в себе функции прерывания int 21h, файл msdos.sys обрабатывает config.sys, и запускает командный интерпретатор command.com, который в свою очередь обрабатывает командный файл autoexec.bat.
С заголовком мы немного разобрались. Теперь я приведу алгоритм загрузки бинарного файла формата ELF. Алгоритм схематический, не стоит рассматривать его как работающую программу.
int LoadELF (unsigned char *bin) { struct elf32_hdr *EH = (struct elf32_hdr *)bin; struct elf32_phdr *EPH;
if (EH->e_ident[0] != 0x7f || // Контролируем MAGIC EH->e_ident[1] != 'E' || EH->e_ident[2] != 'L' || EH->e_ident[3] != 'F' || EH->e_ident[4] != ELFCLASS32 || // Контролируем класс EH->e_ident[5] != ELFDATA2LSB || // порядок байт EH->e_ident[6] != EV_CURRENT || // версию EH->e_type != ET_EXEC || // тип EH->e_machine != EM_386 || // платформу EH->e_version != EV_CURRENT) // и снова версию, на всякий случай return ELF_WRONG;
EPH = (struct elf32_phdr *)(bin + EH->e_phoff);
while (EH->e_phnum--) { if (EPH->p_type == PT_LOAD) memcpy (EPH->p_vaddr, bin + EPH->p_offset, EPH->p_filesz);
EPH = (struct elf32_phdr *)((unsigned char *)EPH + EH->e_phentsize)); }
return ELF_OK; }
По серьезному стоит еще проанализировать поля EPH->p_flags, и расставить на соответствующие страницы права доступа, да и просто копирование здесь не подойдет, но это уже не относится к формату, а к распределению памяти. Поэтому сейчас об этом не будем говорить.
int LoadPE (unsigned char *bin) { struct elf32_hdr *PH = (struct pe_hdr *) (bin + *((unsigned long *)&bin[0x3c])); // Конечно комбинация не из понятных... просто берем dword по смещению 0x3c // И вычисляем адрес PE заголовка в образе файла struct elf32_phdr *POH;
if (PH == NULL || // Контролируем указатель PH->pe_sign != 0x4550 || // сигнатура PE {'P', 'E', 0, 0} PH->pe_cputype != 0x14c || // i386 (PH->pe_flags & 2) == 0) // файл нельзя запускать! return PE_WRONG;
POH = (struct pe_ohdr *)((unsigned char *)PH + 0xf8);
while (PH->pe_obj_num--) { if ((POH->p_flags & 0x60) != 0) // либо код либо инициализированные данные memcpy (PE->pe_image_base + POH->o_vaddr, bin + POH->o_poff, POH->o_psize);
POH = (struct pe_ohdr *)((unsigned char *)POH + sizeof (struct pe_ohdr)); }
return PE_OK; }
Это опять таки не готовая программа, а алгоритм загрузки.
И опять таки многие моменты не освещаются, так как выходят за пределы темы.
Но теперь стоит немного поговорить про существующие системные особенности.
Принципиальных отличий для FreeBSD, конечно, нет. основное отличие состоит в том, что ядро, как и модули ядра являются перемещаемыми и могут быть загружены или выгружены в процессе загрузки системы.
Порядок загрузки примерно следующий:
BootSector загружает вторичный загрузчик; Вторичный загрузчик переводит систему в защищенный режим и запускает loader; loader предоставляет пользователю возможность выбрать необходимые модули или запустить другое ядро; После чего управление передается ядру и начинается инициализация драйверов;
В прошлом выпуске я писал: В следующем выпуске мы рассмотрим процессы загрузки разных операционных систем (Windows не предлагать!).
Почему Windows не предлагать? Windows пока что еще никто не отменял :) Не хотите загружаться как Windows, но тогда расскажите, почему и приведите сравнение, но все равно расскажите, как это делает Windows.
Не хотел рассказывать, но придется... :)
Если что-то я напутаю, уж извините...
Давайте по порядку рассмотрим, как грузятся системы от Microsoft.
Для Linux свойственно два способа загрузки:
Загрузка через boot sector ядра; Загрузка через boot manager LILO (Linux Loader);
Процесс загрузки через ядро используется обычно на Floppy дисках и происходит в следующем порядке:
boot sector переписывает свой код по адресу 9000h:0; Загружает с диска Setup, который записан в нескольких последующих секторах, по адресу: 9000h:0200h; Загружает ядро по адресу 1000h:0. Ядро так же следует в последующих секторах за Setup. Ядро не может быть больше чем 508 килобайт, но так как оно, чаще всего, архивируется - это не страшно; Запускается Setup; Проверяется корректность Setup; Производится проверка оборудования средствами BIOS. Определяется размер памяти, инициализируется клавиатура и видеосистема, наличие жестких дисков, наличие шины MCA (Micro channel bus), PC/2 mouse, APM BIOS (Advanced power management); Производится переход в защищенный режим; Управление передается по адресу 1000h:0 на ядро; Если ядро архивировано, оно разархивируется. иначе просто переписывается по адресу 100000h (за пределы первого мегабайта); Управление передается по этому адресу; Активируется страничная адресация; Инициализируются idt и gdt, при этом в кодовый сегмент и в сегмент данных ядра входит вся виртуальная память; Инициализируются драйвера; Управление передается неуничтожимому процессу init; init запускает все остальные необходимые программы в соответствии с файлами конфигурации;
В случае загрузки через LILO:
boot sector LILO переписывает свой код по адресу 9a00h:0; До адреса 9b00h:0 размещает свой стек; Загружает вторичный загрузчик по адресу 9b00h:0 и передает ему управление;
Вторичный загрузчик загружает boot sector ядра по адресу 9000h:0; Загружает Setup по адресу 9000h:0200h; Загружает ядро по адресу 1000h:0; Управление передается программе Setup. Зачем загружает boot sector из ядра? не понятно;
В Linux есть такое понятие как "big kernel". Такой kernel сразу загружается по адресу 100000h.
Бутсектор загружается БИОСом по адресу 0:7c00h занимает он 512 байт. Память начиная с адреса 0:7e00h свободна. но в эту память мы загрузим второй сектор бута. Одновременно загружается информация необходимая для обслуживания файловой системы. Для EXT2 дополнительно необходимо загрузить два килобайта (суперблок и дескрипторы групп), для FAT немного больше - 4,5 килобайта (первая копия FAT).
mov ax, 0x7e0 mov es, ax
Адрес 0:7e00h идентичен адресу 7e0h:0. Вторым вариантом мы и будем пользоваться, потому что наша процедура загрузки секторов размещает их по сегментному адресу, хранящемуся в es.
mov ax, 1
В ax номер сектора, с которого начинается чтение (первый сектор является нулевым (каламбур :). И далее все зависит от файловой системы.
%ifdef EXT2FS mov cx, 5
Для EXT2 загружается 5 секторов - второй сектор бутсектора (1 сектор), суперблок файловой системы (2 сектора) и дескрипторы групп (2 сектора).
%elifdef FATFS mov cx, 10
Для FAT загружается 10 секторов - второй сектор бутсектора (1 сектор), таблица FAT - 9 секторов (такой размер она имеет на floppy дисках).
%else %error File system not specified %endif call load_block
Все. первый пункт загрузки выполнен.
Функции обслуживания файловых систем имеют одинаковый интерфейс. Cобственно их всего две fs_init и fs_load_file. Естественно у них различаются реализации, но в процессе компиляции выбирается используемая файловая система. Для совместного использования нам никак не хватит одного килобайта, да и не за чем это.
Из-за сложности VFAT (FAT с длинными именами) он не реализован. Все имена на диске FAT должна иметь формат 8.3
В файловой системе FAT я не оперирую принятыми в MS системах именами дисков и при указании пути использую путь относительно корневой директории диска (как это делается в юникс системах).
Файл конфигурации у нас пока называется boot.rc и находится в каталоге /etc. Формат у этого файла достаточно нестрогий. Из-за нехватки места в boot секторе там сделана реакция только на ключевые слова, которыми являются:
kern[el] - файл ядра; modu[le] - файл модуля; #end - конец файла конфигурации. Использование этих слов в другом контексте недопустимо.
Предварительно проинициализировав файловую систему
call fs_init
Мы загружаем этот файл с диска.
mov si, boot_config call fs_load_file
...
boot_config: db '/etc/boot.rc', 0
Содержимое файла конфигурации такое:
kernel /boot/kernel
#end
Модулей у нас пока никаких нет, да и ядро еще в зачаточном состоянии. Но речь сейчас не об этом.
Про этот момент я не буду особо расписывать, желающие могут посмотреть в исходниках, которые в скором времени появятся на сайте.
Скажу только, что программа анализирует файл конфигурации, в соответствии с ключевыми словами загружает ядро (которое может быть в единственном экземпляре) и любое количество модулей. Общий объем ядра и модулей ограничен свободным размером базовой памяти (около 600к).
Перейдем к предпоследнему пункту.
Отличие от DOS заключается в том, что функции msdos.sys взял на себя io.sys. msdos.sys остался ради совместимости как конфигурационный файл. После того как командный интерпретатор command.com обрабатывает autoexec.bat вызывается программа win.com, которая осуществляет перевод системы в защищенный режим, и запускает различные другие программы, обеспечивающие работу системы.
boot sector NT - зависти от формата FS, для FAT устанавливается один, для NTFS - другой, в нем содержиться код чтения FS, без обработки подкаталогов.
boot sector загружает NTLDR из корневой директории, который запускается в real mode; NTLDR певодит систему в защищенный режим; Создаются необходимые таблицы страниц для доступа к первому мегабайту памяти; Активируется механизм страничного преобразования; Далее NTLDR читает файл boot.ini, для этого он использует встроенный read only код FS. В отличии от кода бутсектора он может читать подкаталоги; На экране выводится меню выбора вида загрузки; После выбора, или по истечении таймаута, NTLDR из файла boot.ini определяет нахождение системной директории Windows, она может находиться в другом разделе, но обязательно должна быть корневой; Если в boot.ini указана загрузка DOS (или Win9x), то файл bootsect.dos загружается в память и выполняется горячая перезагрузка; Далее обрабатывается boot.ini; Загружается ntdetect.com, который выводит сообщение "NTDETECT V4.0 Checking Hardware", и детектит различные устройства... Вся информация собирается во внешней структуре данных, которая в дальнейшем становиться ключем реестра "HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION"; NTLDR выводит сообщение "OSLOADER V4.0"; Из директории winnt\system32 загружается ntoskrnl.exe, содержащий в себе ядро и подсистемы выполнения (менеджер памяти, кэш менеджер, менеджер объектов), и файл hal.dll, который содержит в себе интерфейс с аппаратным обеспечением; Далее NTLDR предоставляет возможность выбрать "последние известные хорошие" конфигурации. В зависимости от выбора выбираются копии реестра используемые для запуска; Загружает все драйвера и другие необходимые для загрузки файлы; В завершение он запускает функцию main из ntoskrnl.exe и завершает свою работу;
Не могу гарантировать полную достоверность представленной информации, NT я знаю плохо, тем более не знаю что у нее внутри. Так же не могу что-либо более конкретного сказать про распределение памяти в процессе загрузки Windows NT. некоторые неточности могут быть связаны с моим плохим знанием английского, желающие могут посмотреть на оригинал по адресу: Inside the Boot Process, Part 1
Ну вот, мы узнали как загружаются системы. В своей системе мы не будем слепо следовать какому либо из представленных здесь путей. Ради совместимости обеспечим формат ядра, аналогичный Linux. Мне кажется, в этой системе все сделано достаточно понятно и просто.
Я думаю никто не будет против того, что мы в первую очередь будем ориентироваться на Linux?
А в следующем выпуске мы поговорим о распределении памяти в системе и начнем писать свой boot sector.
Отправлено 2001-07-20 для 695 подписчиков.
ведущий рассылки Dron
Архив Рассылки
При поддержке Kalashnikoff.ru
Ну не знаю что еще написать, жара достала, не знаю как у вас, а у нас в Москве - за 30... :(
В следующем выпуске мы рассмотрим процессы загрузки разных операционных систем (Windows не предлагать!). Ну, может быть еще что-то, это я узнаю из ваших писем.
По всем вопросам вы можете обращаться ко мне по адресу: mailto:dron@infosec.ru?Subject=AsmOS.
При поддержке Kalashnikoff.ru
Для обеспечения надежной работы операционных систем и прикладных программ разработчики процессоров предусмотрели в них механизмы защиты. В процессорах фирмы Intel предусмотрено четыре уровня привилегий для программ и данных. Нулевой уровень считается наиболее привилегированным, третий уровень - наименее.
Так же в защищенном режиме совсем иначе работает механизм преобразования адресов. в сегментном регистре теперь хранится не старшие биты адреса, а селектор. селектор представляет из себя индекс в таблице дескрипторов. И кроме этого содержит в себе несколько служебных бит. Формат селектора такой:
15 | 2 | 0 |
Index | TI | RPL |
Поле Index определяет индекс в дескрипторной таблице.
В процессорах Intel одновременно в системе может существовать две дескрипторных таблицы: Глобальная (Global descriptor table или GDT) и Локальная (Local descriptor table или LDT).
GDT существует в единственном экземпляре. Адрес и предел GDT хранятся в специальном системном регистре (GDTR) в 48 бит длиной (6 байт).
LDT может быть индивидуальная для каждой задачи, или общая для системы, или же ее вообще может не быть. Адрес и размер LDT определяется в GDT, для обращения к LDT в процессоре существует специальный регистр (LDTR), но в отличии от GDTR он имеет размер 16 бит и содержит в себе селектор из GDT.
Поле TI (Table indicator) селектора определяет принадлежность селектора GDT (0) или LDT (1).
Поле RPL (Requested privilege level) определяет запрашиваемые привилегии... об этом мы поговорим чуть позже.