Оглавление [01] - введение / основные сведения о ядре [02] - организация работы с памятью [03] - этапы загрузки различных ОС [04] - создание bootsector'а [05] - основы защищенного режима [06] - шлюзы / виртуальный режим процессора 8086 [07] - исключения защищенного режима / микроядерные системы [08] - файловые системы [09] - чтение ext2fs [10] - форматы файлов ELF и PE [11] - процесс загрузки [12] - определение количества памяти ***********************************************
[01] - введение / основные сведения о ядре И начнем мы наше дело с написания ядра. Ядро, которое мы напишем, будет ориентированно на UNIX-подобные операционные системы. Чтобы нам было проще, мы с самого начала будем стремиться к совместимости с существующими системами. Задача наша будет состоять в следующем: Сделать, по возможности, компактное, надежное и быстрое ядро, с максимальным эффектом используя возможности процессора. Писать будем в основном на Ассемблере. Для начала разберемся, как устроены системы. Ядро состоит из следующих компонентов: 1. "Собственно ядро" 2. Драйвера устройств 3. Системные вызовы В зависимости от организации внутренних взаимодействий, ядра подразделяются на "микроядра" (microkernel) и монолитные ядра. Системы с "микроядром" строятся по модульному принципу, имеют обособленное ядро, и механизм взаимодействия между драйверами устройств и процессами. По такому принципу строятся системы реального времени. Примерно так сделан QNX или HURD. Монолитное ядро имеет более жесткую внутреннюю структуру. Все установленные драйвера жестко связываются между собой, обычно прямыми вызовами. По таким принципам строятся обыкновенные операционные системы типа Linux, FreeBSD. Естественно, не все так четко, идеального монолитного или "микроядра" нет, наверное, ни в одной системе, просто системы приближаются к тому или иному типу ядра. Мне бы очень хотелось, чтобы то, что мы будем делать, больше походило на первый тип ядер. Немного углублюсь в аппаратные возможности компьютеров. Один, отдельно взятый, процессор, в один момент времени, может исполнять только одну программу. Но к компьютерам предъявляются более широкие требования. Мало кто, в настоящее время, удовлетворился однозадачной операционной системой (к каким относился DOS, например). В связи с этим разработчики процессоров предусмотрели мультизадачные возможности. Возможность эта заключается в том, что процессор выполняет какую-то одну программу (их еще называют процессами или задачами). Затем, по истечении некоторого времени (обычно это время меряется микросекундами), операционная система переключает процессор на другую программу. При этом все регистры текущей программы сохраняются. Это необходимо для того, чтобы через некоторое время вновь передать управление этой программе. Программа при этом не замечает каких либо изменений, для нее процесс переключения остается незаметен. Для того чтобы программа не могла, каким либо образом, нарушить работоспособность системы или других программ, разработчики процессоров предусмотрели механизмы защиты. Процессор предоставляет 4 "кольца защиты" (уровня привилегий), можно было бы использовать все, но это связано со сложностями взаимодействия программ разного уровня защиты. Поэтому в большинстве существующих систем используют два уровня. 0 - привилегированный уровень (ядро) и 3 - непривилегированный (пользовательские программы). Всем этим обеспечивается надежное функционирование системы и независимость программ друг от друга. Теперь немного поподробнее про устройство ядра. На "Собственно ядро" возлагаются функции менеджера памяти и процессов. Переключение процессов - это основной момент нормального функционирования системы. Драйвера не должны "тормозить", а тем более блокировать работу ядра. Windows - наглядный пример того, что этого нельзя допустить! Теперь о драйверах. Драйвера - это специальные программы, обеспечивающие работу устройств компьютера. В существующих системах (во FreeBSD это точно есть, про Linux не уверен) предусматриваются механизмы прерывания работы драйверов по истечении какого-то времени. Правда, все зависит от того, как написан драйвер. Можно написать драйвер под FreeBSD или Linux, который полностью блокирует работу системы. Избежать этого при двухуровневой защите не представляется возможным, поэтому драйвера надо будет тщательно программировать. В нашей работе драйверам мы уделим очень много внимания, поскольку от этого в основном зависит общая производительность системы. Системные вызовы - это интерфейс между процессами и ядром (читайте-железом). Никаких других методов взаимодействия процессов с устройствами компьютера быть не должно. Системных вызовов достаточно много, на Linux их 190, на FreeBSD их порядка 350, причем большей частью они совпадают, соответствуя стандарту POSIX (стандарт, описывающий системные вызовы в UNIX). Разница заключается в передаче параметров, что легко будет предусмотреть. Естественно, мы не сможем сделать ядро, работающее одновременно на Linux и на FreeBSD, но по отдельности совместимость вполне реализуема. Прикладным программам абсолютно безразлично, как системные вызовы реализуются в ядре. Это облегчает для нас обеспечение совместимости с существующими системами. В следующем выпуске мы поговорим про защищенный режим процессора, распределение памяти, менеджер задач и рассмотрим, как это сделано в существующих системах.
[02] - организация работы с памятью Как процессор работает с памятью? Для начала небольшое предисловие. В процессорах имеются базовые регистры, которые могут задавать смещение. На 16-битной архитектуре максимальное смещение могло быть до 64 килобайт, что, в общем-то, не много и вызывало определенные трудности (разные модели памяти, разные форматы файлов). Так же, в 16-битной архитектуре присутствовали сегментные регистры, которые указывали адрес сегмента в памяти. В процессорах, начиная с i386, базовые регистры стали 32-х битными, что позволяет адресовать до 4 гигабайт. Сегментные регистры остались 16-битными, и в защищенном режиме они не содержат адреса! они содержат индекс дескриптора. В реальном режиме сегментные регистры работают так же, как и на 16-битных процессорах. В реальном режиме сегментные регистры непосредственно указывают на адрес начала сегмента в памяти. Это позволяет нам, без каких либо преград, адресовать 1 мегабайт памяти. Но создает определенные трудности для защиты. А защищать нам нужно многое. Например, мы не можем пользовательским программам дать возможность непосредственно обращаться к коду или данным ядра. Так же мы не можем дать возможность пользовательским программам обращаться к коду или данным других пользовательских программ, поскольку это может нарушить их работоспособность. Для этого был изобретен защищенный режим работы процессора, который появился в процессорах i286. Защищенность этого режима заключается в следующем: Сегментный регистр больше не указывает на адрес в памяти. В этом регистре теперь задается индекс в таблице дескрипторов. Таблица дескрипторов может быть глобальная или локальная (применяется в многозадачных системах для изоляции адресного пространства задач) и представляет собой массив записей, по 8 байт в каждой, где описываются адреса, пределы и права доступа к сегментам. Про адрес ничего не буду говорить, и так все ясно. Что такое предел? В этом Поле описывается размер сегмента. При обращении за пределы сегмента процессор генерирует исключение (специальное прерывание защищенного режима). Так же исключение генерируется в случае нарушения прав доступа к сегменту. Поле прав доступа описывает возможность чтения/записи сегмента, возможность выполнения кода сегмента, уровень привилегий для доступа к сегменту. При обращении к сегменту из дескриптора берется базовый адрес сегмента и складывается со смещением сегмента. Так получается линейный 32-х разрядный (в i286 - 24-х разрядный) адрес. Для i286 на этом процесс получения адреса завершается, линейный адрес там равен физическому. Для i386 или выше это справедливо не всегда. Страничная организация памяти. В процессорах, начиная с i386, появилась, так называемая, страничная организация памяти. Страница имеет размер 4 килобайта или 4 мегабайта. Большие страницы могут быть только в pentium или выше. Не знаю только, какой толк от таких страниц. Если возможность страничной адресации не используется, то линейный адрес, как и на i286, равен физическому. Если используется - то линейный адрес разбивается на три части. Первая, 10-битная, часть адреса является индексом в каталоге страниц, который адресуется системным регистром CR3. Запись в каталоге страниц указывает адрес таблицы страниц. Вторая, 10-битная, часть адреса является индексом в таблице страниц. Запись в таблице страниц указывает физический адрес нахождения страницы в памяти. последние 12 бит адреса указывают смещение в этой странице. В страничных записях, как и в дескрипторных записях, есть служебные биты, описывающие права доступа, и некоторые другие тонкости страниц. Одной из важных тонкостей является бит присутствия страницы в памяти. В случае не присутствия страницы, процессор генерирует исключение, в котором можно считать данную страницу из файла или из swap раздела. Это сильно облегчает реализацию виртуальной памяти. Чуть ниже мы про это поговорим. Для начала небольшое предисловие. В процессорах имеются базовые регистры, которые могут задавать смещение. На 16-битной архитектуре максимальное смещение могло быть до 64 килобайт, что, в общем-то, не много и вызывало определенные трудности (разные модели памяти, разные форматы файлов). Так же, в 16-битной архитектуре присутствовали сегментные регистры, которые указывали адрес сегмента в памяти. В процессорах, начиная с i386, базовые регистры стали 32-х битными, что позволяет адресовать до 4 гигабайт. Сегментные регистры остались 16-битными, и в защищенном режиме они не содержат адреса! они содержат индекс дескриптора. В реальном режиме сегментные регистры работают так же, как и на 16-битных процессорах. В реальном режиме сегментные регистры непосредственно указывают на адрес начала сегмента в памяти. Это позволяет нам, без каких либо преград, адресовать 1 мегабайт памяти. Но создает определенные трудности для защиты. А защищать нам нужно многое. Например, мы не можем пользовательским программам дать возможность непосредственно обращаться к коду или данным ядра. Так же мы не можем дать возможность пользовательским программам обращаться к коду или данным других пользовательских программ, поскольку это может нарушить их работоспособность. Для этого был изобретен защищенный режим работы процессора, который появился в процессорах i286. Защищенность этого режима заключается в следующем: Сегментный регистр больше не указывает на адрес в памяти. В этом регистре теперь задается индекс в таблице дескрипторов. Таблица дескрипторов может быть глобальная или локальная (применяется в многозадачных системах для изоляции адресного пространства задач) и представляет собой массив записей, по 8 байт в каждой, где описываются адреса, пределы и права доступа к сегментам. Про адрес ничего не буду говорить, и так все ясно. Что такое предел? В этом Поле описывается размер сегмента. При обращении за пределы сегмента процессор генерирует исключение (специальное прерывание защищенного режима). Так же исключение генерируется в случае нарушения прав доступа к сегменту. Поле прав доступа описывает возможность чтения/записи сегмента, возможность выполнения кода сегмента, уровень привилегий для доступа к сегменту. При обращении к сегменту из дескриптора берется базовый адрес сегмента и складывается со смещением сегмента. Так получается линейный 32-х разрядный (в i286 - 24-х разрядный) адрес. Для i286 на этом процесс получения адреса завершается, линейный адрес там равен физическому. Для i386 или выше это справедливо не всегда. Многозадачность. Многозадачные возможности в процессорах так же появились в процессорах, начиная с i286. Для реализации этого, процессор для каждой задачи использует, так называемый, "сегмент состояния задачи" ("Task State Segment", сокращенно TSS). В этом сегменте, при переключении задач, сохраняются все базовые регистры процессора, сегменты и указатели стека для трех уровней защиты (для каждого уровня используется свой стек), сегментный адрес локальной таблицы дескрипторов ("Local descriptor table", сокращенно LDT). В процессорах, начиная с i386, там еще хранится адрес каталога страниц (регистр CR3). Так же этот сегмент обеспечивает некоторые другие механизмы защиты, но о них мы пока не будем говорить. Операционная система может расширить TSS, и использовать его для хранения регистров и состояния сопроцессора. Процессор при переключении задач не сохраняет этого. Так же возможны другие применения. Что из всего этого следует? В своей работе мы не будем ориентироваться на процессор i286, поскольку 16-битная архитектура и отсутствие механизма страничного преобразования сильно усложняет программирование операционной системы. К тому же, таких процессоров давно уже никто не использует. Ориентироваться мы будем на i386 или более старшие модели процессоров, вплоть до последних. Ядро системы при распределении памяти оперирует 4-х килобайтными страницами. Страницы могут использоваться самим ядром, для нужд драйверов (кэширование, например), или для процессов. Программа или процесс состоит из следующих частей: * Сегмент кода. Может только выполняться, сама программа его не прочитать, не переписать не может! Использовать для этого сегмента swap не нужно, при необходимости код считывается прямо из файла; * Сегмент данных состоит из трех частей: o Константные данные, их тоже можно загружать из файла, так как они не меняются при работе программы; o Инициализированные данные. Участвует в процессе свопинга; o Не инициализированные данные. Так же участвует в свопинге; * Сегмент стека. Так же участвует в свопинге. Но, обычно, системы делят сегмент данных на две части: инициализированные данные и не инициализированные данные. Все сегменты разбиваются на страницы. Сегмент кода имеет постоянный размер. Сегмент данных может увеличиваться в сторону больших адресов. Сегмент стека, поскольку растет вниз, увеличивается в сторону уменьшения адресов. Страницы памяти для дополнительных данных или стека выделяются системой по мере необходимости. Очень интересный момент: При выполнении программы операционная система делает следующие действия: * Готовит для программы локальную таблицу дескрипторов; * Готовит для программы каталог страниц, все страницы помечаются как не присутствующие в памяти. * Все. При передаче управления этой программе процессор генерирует исключение по отсутствию страницы, в котором нужная страница загружается из файла или инициализируется. Еще один интересный момент: Когда в системе загружается две или более одинаковых программы - нет необходимости для каждой из них выделять место для кодового сегмента, они спокойно могут использовать один код на всех.
[03] - этапы загрузки различных ОС Процесс загрузки, естественно, начинается с BIOS. При старте процессор находится в реальном режиме, следовательно больше одного мегабайта памяти адресовать не может. Но это и не обязательно. BIOS проверяет устройства, с которых может производиться загрузка. Порядок проверки в современных BIOS устанавливается. В список устройств могут входить Floppy disk, IDE disk, CDROM, SCSI disk... Вне зависимости от типа устройства суть загрузки одна... На устройстве обнаруживается boot sector. Для CDROM это не совсем справедливо, но про них мы пока не будем говорить. BootSector загружается в память по адресу 0:7с00. Дальнейшее поведение BootSector'а зависит от системы. Загрузка Linux. Для Linux свойственно два способа загрузки: * Загрузка через boot sector ядра; * Загрузка через boot manager LILO (Linux Loader); Процесс загрузки через ядро используется обычно на Floppy дисках и происходит в следующем порядке: 1. boot sector переписывает свой код по адресу 9000h:0; 2. Загружает с диска Setup, который записан в нескольких последующих секторах, по адресу: 9000h:0200h; 3. Загружает ядро по адресу 1000h:0. Ядро так же следует в последующих секторах за Setup. Ядро не может быть больше чем 508 килобайт, но так как оно, чаще всего, архивируется - это не страшно; 4. Запускается Setup; 5. Проверяется корректность Setup; 6. Производится проверка оборудования средствами BIOS. Определяется размер памяти, инициализируется клавиатура и видеосистема, наличие жестких дисков, наличие шины MCA (Micro channel bus), PC/2 mouse, APM BIOS (Advanced power management); 7. Производится переход в защищенный режим; 8. Управление передается по адресу 1000h:0 на ядро; 9. Если ядро архивировано, оно разархивируется. иначе просто переписывается по адресу 100000h (за пределы первого мегабайта); 10. Управление передается по этому адресу; 11. Активируется страничная адресация; 12. Инициализируются idt и gdt, при этом в кодовый сегмент и в сегмент данных ядра входит вся виртуальная память; 13. Инициализируются драйвера; 14. Управление передается неуничтожимому процессу init; 15. init запускает все остальные необходимые программы в соответствии с файлами конфигурации; В случае загрузки через LILO: 1. boot sector LILO переписывает свой код по адресу 9a00h:0; 2. До адреса 9b00h:0 размещает свой стек; 3. Загружает вторичный загрузчик по адресу 9b00h:0 и передает ему управление; 4. Вторичный загрузчик загружает boot sector ядра по адресу 9000h:0; 5. Загружает Setup по адресу 9000h:0200h; 6. Загружает ядро по адресу 1000h:0; 7. Управление передается программе Setup. Зачем загружает boot sector из ядра? не понятно; В Linux есть такое понятие как "big kernel". Такой kernel сразу загружается по адресу 100000h. Загрузка FreeBSD. Принципиальных отличий для FreeBSD, конечно, нет. основное отличие состоит в том, что ядро, как и модули ядра являются перемещаемыми и могут быть загружены или выгружены в процессе загрузки системы. Порядок загрузки примерно следующий: 1. BootSector загружает вторичный загрузчик; 2. Вторичный загрузчик переводит систему в защищенный режим и запускает loader; 3. loader предоставляет пользователю возможность выбрать необходимые модули или запустить другое ядро; 4. После чего управление передается ядру и начинается инициализация драйверов; В прошлом выпуске я писал: В следующем выпуске мы рассмотрим процессы загрузки разных операционных систем (Windows не предлагать!). Почему Windows не предлагать? Windows пока что еще никто не отменял Не хотите загружаться как Windows, но тогда расскажите, почему и приведите сравнение, но все равно расскажите, как это делает Windows. Не хотел рассказывать, но придется... Если что-то я напутаю, уж извините... Давайте по порядку рассмотрим, как грузятся системы от Microsoft. Загрузка DOS. boot sector DOS загружает в память два файла: io.sys и msdos.sys. Названия этих файлов в разных версиях DOS различались, не важно. Файл io.sys содержит в себе функции прерывания int 21h, файл msdos.sys обрабатывает config.sys, и запускает командный интерпретатор command.com, который в свою очередь обрабатывает командный файл autoexec.bat. Загрузка Windows 9x. Отличие от DOS заключается в том, что функции msdos.sys взял на себя io.sys. msdos.sys остался ради совместимости как конфигурационный файл. После того как командный интерпретатор command.com обрабатывает autoexec.bat вызывается программа win.com, которая осуществляет перевод системы в защищенный режим, и запускает различные другие программы, обеспечивающие работу системы. Загрузка Windows NT. boot sector NT - зависти от формата FS, для FAT устанавливается один, для NTFS - другой, в нем содержиться код чтения FS, без обработки подкаталогов. 1. boot sector загружает NTLDR из корневой директории, который запускается в real mode; 2. NTLDR певодит систему в защищенный режим; 3. Создаются необходимые таблицы страниц для доступа к первому мегабайту памяти; 4. Активируется механизм страничного преобразования; 5. Далее NTLDR читает файл boot.ini, для этого он использует встроенный read only код FS. В отличии от кода бутсектора он может читать подкаталоги; 6. На экране выводится меню выбора вида загрузки; 7. После выбора, или по истечении таймаута, NTLDR из файла boot.ini определяет нахождение системной директории Windows, она может находиться в другом разделе, но обязательно должна быть корневой; 8. Если в boot.ini указана загрузка DOS (или Win9x), то файл bootsect.dos загружается в память и выполняется горячая перезагрузка; 9. Далее обрабатывается boot.ini; 10. Загружается ntdetect.com, который выводит сообщение "NTDETECT V4.0 Checking Hardware", и детектит различные устройства... Вся информация собирается во внешней структуре данных, которая в дальнейшем становиться ключем реестра "HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION"; 11. NTLDR выводит сообщение "OSLOADER V4.0"; 12. Из директории winnt\system32 загружается ntoskrnl.exe, содержащий в себе ядро и подсистемы выполнения (менеджер памяти, кэш менеджер, менеджер объектов), и файл hal.dll, который содержит в себе интерфейс с аппаратным обеспечением; 13. Далее NTLDR предоставляет возможность выбрать "последние известные хорошие" конфигурации. В зависимости от выбора выбираются копии реестра используемые для запуска; 14. Загружает все драйвера и другие необходимые для загрузки файлы; 15. В завершение он запускает функцию main из ntoskrnl.exe и завершает свою работу; ___________________________________________________________________________________________________________ Не могу гарантировать полную достоверность представленной информации, NT я знаю плохо, тем более не знаю что у нее внутри. Так же не могу что-либо более конкретного сказать про распределение памяти в процессе загрузки Windows NT. некоторые неточности могут быть связаны с моим плохим знанием английского, желающие могут посмотреть на оригинал по адресу: Inside the Boot Proccess, Part 1 Ну вот, мы узнали как загружаются системы. В своей системе мы не будем слепо следовать какому либо из представленных здесь путей. Ради совместимости обеспечим формат ядра, аналогичный Linux. Мне кажется, в этой системе все сделано достаточно понятно и просто.
[04] - создание bootsector'а Как я уже упоминал, boot sector загружается в память по адресу 0:7c00h и имеет длину 512 байт. Это не слишком много, поэтому возможности boot sector'a ограничиваются загрузкой какого либо вторичного загрузчика. Наш boot sector, по образу и подобию linux, будет загружать в память два блока. Первым является тот самый вторичный загрузчик, у нас он, как и в linux, называется setup. Вторым является собственно ядро. Этот boot sector служит для загрузки ядра с дискет, поэтому, на первых порах, он жестко привязан к диску "a:". BIOS предоставляет возможность читать по нескольку секторов сразу, но не более чем до границы дорожки. Такая возможность, конечно, ускоряет чтение с диска, но представляет собой большие сложности в программировании, так как надо учитывать границы сегментов (в реальном режиме сегмент может быть не больше, чем 64к) и границы дорожек, получается достаточно хитрый алгоритм. Я пошел немного другим путем. Я читаю с диска по секторам. Это, конечно, медленнее, но я думаю, что здесь скорость не очень критична. За то это гораздо проще и компактнее реализуется. А теперь давайте разбираться, как это все работает. Code: %define SETUP_SEG 0x07e0 %define SETUP_SECTS 10 %define KERNEL_SEG 0x1000 %define KERNEL_SECTS 1000 Для начала описываем место и размер для каждого загружаемого блока. Размеры пока произвольные, поскольку все остальное еще предстоит написать. Code: section .text BITS 16 org 0x7c00 Как я уже говорил, boot sector загружается и запускается по адресу 0:7c00h Содержимое регистров при старте таково: * cs содержит 0 * ip содержит 7с00h Прерывания запрещены! Про содержание остальных регистров мне ничего не известно, если кто-то, что-то знает, напишите мне. Остальные регистры мы будем инициализировать самостоятельно. Code: entry_point: mov ax, cs cli mov ss, ax mov sp, entry_point sti mov ds, ax Стек у нас будет располагаться перед программой, до служебной области BIOS еще остается порядка 30 килобайт, для стека больше чем достаточно. Прерывания изначально запрещены, но я все равно сделаю это самостоятельно, на всякий случай. и разрешу после установки стека. Никаких проблем это вызвать, по-моему, не должно. Так же, нулевым значением, инициализируем сегментный регистр ds. Code: ; Сохpаняем фоpму куpсоpа mov ah, 3 xor bh, bh int 0x10 push cx ; отключаем куpсоp mov ah, 1 mov ch, 0x20 int 10h Чтобы все было красиво и радовало глаз, мы на время чтения отключим курсор. Иначе он будет мелькать на экране. Чтобы его потом восстановить, как и был, мы сохраняем его форму в стеке. Code: ; Заг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). Процедуру загрузки блока мы рассмотрим немного позже. А в остальном здесь, по-моему, все понятно. Code: ; заг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). Здесь все в точности аналогично первому блоку. Code: ; Восстанавливаем куpсоp pop cx mov ah, 1 int 0x10 Восстанавливаем форму курсора. Code: ; Пеpедаем упpавление на setup jmp SETUP_SEG:0 На этом работа boot sector'а заканчивается. Дальним переходом мы передаем управление программе setup. Далее располагаются функции. ; Загрузка блока ; cx - количество сектоpов ; ax - начальный сектоp ; es - указатедь на память ; si - loading message Функция загрузки блока. Она же занимается выводом на экран процентного счетчика. Code: load_block: mov di, cx ; сохpаняем количество блоков .loading: xor bx, bx call load_sector inc ax mov bx, es add bx, 0x20 mov es, bx ; Выводим сообщение о загpузке. call outstring push ax ; Выводим пpоценты ; ((di - cx) / di) * 100 mov ax, di sub ax, cx mov bx, 100 mul bx div di call outdec push si mov si, persent_msg call outstring pop si pop ax loop .loading ret В этой функции, по-моему, ничего сложного нет. Обыкновенный цикл. А вот следующая функция загружает с диска отдельный сектор, при этом оперируя его линейным адресом. Есть так называемое int13 extension, разработанное совместно фирмами MicroSoft и Intel. Это расширение BIOS работает почти аналогичным образом, Считывая сектора по их линейным адресам, но оно поддерживается не всеми BIOS, имеет несколько разновидностей и работает в основном для жестких дисков. Поэтому нам не подходит. В своей работе мы пока ориентируемся только на чтение с floppy диска, размером 1,4 мегабайта. Поэтому будем использовать старомодную функцию, которой в качестве параметров задается номер дорожки, головки и сектора. [/code] ; Загрузка сектора ; ax - номеp сектоpа (0...max (2880)) ; es:bx - адpес для pазмещения сектоpа. [/code] Абсолютный номеp сектоpа вычисляется по фоpмуле: AbsSectNo = (CylNo * SectPerTrack * Heads) + (HeadNo * SectPerTrack) + (SectNo - 1) Значит обpатное спpаведливо так: CylNo = AbsSectNo / (SectPerTrack * Heads) HeadNo = остаток / SectorPerTrack SectNo = остаток + 1 load_sector: push ax push cx cwd mov cx, 18 ; SectPerTrack div cx mov cx, dx inc cx ; количество сектоpов Поделив номер сектора на количество секторов на дорожке, мы в остатке получаем номер сектора на дорожке. Это значение хранится в 6 младших битах регистра cl. Code: xor dx, dx ; dl - диск - 0! Номер диска храниться в dl и устанавливается в 0 (это диск a Code: shr ax, 1 rcl dh, 1 ; номер головки Младший бит частного определяет для нас номер головки. (0 или 1) Code: mov ch, al shl ah, 4 or cl, ah ; количество доpожек Оставшиеся биты частного определяют для нас номер цилиндра (или дорожки). восемь младших бит номера хранятся в регистре ch, два старших бита номера хранятся в двух старших битах регистра cl. Code: .rept: mov ax, 0x201 int 0x13 jnc .read_ok push si mov si, read_error call outstring movzx ax, ah call outdec mov si, crlf call outstring xor dl, dl xor ah, ah int 0x13 pop si jmp short .rept В случае ошибки чтения мы не будем возвращать из функции какие-либо результаты, а будем повторять чтение, пока оно не окажется успешным. Ведь в случае неуспешного чтения у нас все равно ничего не будет работать! Для верности мы, в случае сбоя, производим сброс устройства. Code: .read_ok: pop cx pop ax ret Далее идет две интерфейсные функции, обеспечивающие вывод на экран строк и десятичных цифр. Ничего особенного они из себя не представляют а для вывода пользуются телетайпным прерыванием BIOS (ah = 0eh, int 10h), которое обеспечивает вывод одного символа с обработкой некоторых служебных кодов. Code: ; Вывод стpоки. ; ds:si - стpока. outstring: push ax push si mov ah, 0eh jmp short .out .loop: int 10h .out: lodsb or al, al jnz .loop pop si pop ax ret Эта функция ограничена выводом чисел до 99 включительно, случай с большим числом обрабатывается как переполнение и отображается как '##'. Code: ; Вывод десятичных чисел от 0 до 99 ; ax - число! outdec: push ax push si mov bl, 10 div bl cmp al, 10 jnc .overflow add ax, '00' push ax mov ah, 0eh int 0x10 pop ax mov al, ah mov ah, 0eh int 0x10 jmp short .exit .overflow: mov si, overflow_msg call outstring .exit: pop si pop ax ret Далее располагаются несколько служебных сообщений. Code: load_setup_msg: db 'Setup loading: ', 0 load_kernel_msg: db 'Kernel loading: ', 0 complete_msg: db 'complete.' crlf: db 0ah, 0dh, 0 persent_msg: db '%', 0dh, 0 overflow_msg: db '##', 0 read_error: db 0ah, 0dh db 'Read error #', 0 TIMES 510-($-$$) db 0 Эта комбинация заполняет оставшееся место в секторе нулями. А остается у нас еще около 200 байт. Code: dw 0aa55h Последние два байта называются "Partition table signature", что не совсем корректно. Фактически эта сигнатура говорит BIOS'у о том, что этот сектор является загрузочным. Этот boot sector, помимо того, что читает по секторам, отличается от линуксового еще и размещением в памяти. После загрузки он не перемещает себя в памяти, и работает по тому же адресу, по которому его загрузил BIOS. Так же setup загружается непосредственно следом за boot sector'ом, с адреса 7e00h, что в принципе не помешает ему работать в других адресах, если мы будем загружать наше ядро через LILO, например. Скомпилированную версию boot sector'а вы можете найти в файловом архиве (секция "наработки"). Надеюсь, что я достаточно доходчиво объясняю, если кому-то что-то не понятно - пишите. В следующем выпуске мы перейдем к программе setup и рассмотрим порядок перехода в защищенный режим. А заодно я более подробно расскажу про этот режим процессора.
[05] - основы защищенного режима История организации памяти. Ранние модели процессоров от Intel имели 16 бит шины данных и 20 бит шины адреса. Это налагало определенные ограничения на адресацию памяти, ибо 16-бинтный регистр невозможно было использовать для адресации более чем 64 килобайт памяти. Чтобы обойти это препятствие разработчики предусмотрели сегментные регистры. Сегментный регистр хранит в себе старшие 16 бит адреса и для получения полного адреса к сегментному адресу прибавляется смещение в сегменте. Таким образом, стало возможным адресовать до 1 мегабайта памяти. Это же позволило делать программы, не настолько привязанными к памяти и упростило адресацию. Сегменты могут начинаться с любого адреса, кратного 16 байтам, эти 16-байтные блоки памяти получили название параграфов. Но это и создает определенные неудобства. Первое неудобство состоит в том, что на один адрес памяти указывает 4096 различных комбинаций сегмент/смещение. Второе неудобство состоит в том, что нет возможности ограничить программам доступ к тем или иным частям памяти, что в некоторых случаях может быть существенно! Введение защищенного режима решило эти проблемы, но ради совместимости любой из современных процессоров может работать в реальном или виртуальном режиме процессора i8086. Защита. Для обеспечения надежной работы операционных систем и прикладных программ разработчики процессоров предусмотрели в них механизмы защиты. В процессорах фирмы Intel предусмотрено четыре уровня привилегий для программ и данных. Нулевой уровень считается наиболее привилегированным, третий уровень - наименее. Так же в защищенном режиме совсем иначе работает механизм преобразования адресов. в сегментном регистре теперь хранится не старшие биты адреса, а селектор. селектор представляет из себя индекс в таблице дескрипторов. И кроме этого содержит в себе несколько служебных бит. Формат селектора такой: Поле 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) определяет запрашиваемые привилегии... об этом мы поговорим чуть позже. Дескрипторы сегментов. Дескрипторные таблицы состоят из записей по 64 бита (8 байт) в каждой. Формат дескриптора таков: Сразу бросается в глаза очень странная организация дескриптора, но это связано с совместимостью с процессором i286, формат дескриптора в котором был таков: Что же содержится в дескрипторе: Базовый адрес - 32 бита (24 бита для i286). Определяет линейный адрес памяти, с которого начинается сегмент. В отличие от реального режима этот адрес может быть указан с точностью до байта. Предел - 20 бит (16 бит для i286). Определяет размер сегмента (максимальный адрес, по которому может быть произведено обращение, это справедливо не всегда но об этом чуть позже). 20-битное поле может показаться не очень то большим для 32-х битного процессора, но это не так. Оно не всегда показывает размер в байтах. Но и об этом чуть позже. Байт прав доступа: Бит P (present) - Указывает на присутствие сегмента в памяти. обращение к отсутствующему сегменту вызывает особый случай не присутствия сегмента в памяти. Двух битное поле DPL определяет уровень привилегий сегмента. Про Уровни привилегий мы поговорим чуть позже. Бит S (Segment)- Будучи установленным в 1, определяет сегмент памяти, к которому может быть получен доступ на чтение (запись) или выполнение. Три бита Type - в зависимости от бита S определяет либо возможности чтения/записи, выполнения сегмента или определяет тип системных данных, хранимых в селекторе. Подробнее это выглядит так: Если бит S установлен в 1, о поле Type делится на биты: Если сегмент расширяется вниз (это используется для стековых сегментов) то поле предела показывает адрес, выше которого допустима запись. ниже запись недопустима и вызовет нарушение пределов сегмента. Бит А (Accessed) устанавливается в 1, если к сегменту производилось обращение. Если бит S установлен в 0, то в сегменте находится служебная информация определяемая полем Typе и битом A. 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 Остальные комбинации либо недопустимы, либо зарезервированы. TSS - это сегмент состояния задачи (Task state segment) о них мы поговорим позже, возможно в следующем выпуске. Шестой байт дескриптора, помимо старших бит предела, содержит в себе несколько битовых полей. Бит G (Granularity) - определяет размер элементов, в которых измеряется предел. если 0 - предел в байтах, если 1 - размер в страницах. Бит D (Default size) - размер операндов в сегменте. Если 0 - 16 бит. если 1 - 32 бита. Бит U (User) - доступен для пользователя (вернее для программиста операционной системы) И снова защита. Немного терминологии: Уровень привилегий может быть от 0(высший) до 3(низший). Следовательно повышение уровня привилегий соответствует его уменьшению в численном эквиваленте, понижение - наоборот. В дескрипторе содержатся биты DPL, которые определяют максимальный уровень привелегий для доступа к сегменту. В селекторе содержится RPL - то есть запрашиваемый уровень привилегий. RPL секущего кодового сегмента (хранится в регистре cs) является уровнем привилегий данного процесса и называется текущим уровнем привилегий (CPL) Прямые обращения к сегментам возможны при соблюдении следующих условий: * В случае если запрашиваемый уровень привилегий больше текущего, то запрашиваемый уровень понижается до текущего. * При обращении к сегменту данных RPL селектора должен быть не ниже DPL сегмента. * При обращении к сегменту кода возможно только при равенстве CPL, RPL и DPL. * Если сегмент кода помечен как подчиненный, то для обращения к нему необходимо иметь уровень привилегий не ниже уровня сегмента. При этом выполнение сегмента происходит с текущим уровнем привилегий. Косвенные вызовы возможны только через шлюзы при соблюдении следующих условий: * DPL шлюза должен быть не выше, чем CPL сегмента, из которого производится вызов шлюза. * DPL сегмента, на который указывает шлюз, должно быть не ниже чем DPL шлюза.
[06] - шлюзы / виртуальный режим процессора 8086 Шлюзы В прошлом выпуске, когда я говорил о дескрипторах и дескрипторных таблицах я ни слова не упомянул о дескрипторной таблице прерываний (Interrupt description table или IDT). Эта таблица так же состоит из дескрипторов, но в отличии от LDT и GDT в этой таблице могут размечаться только шлюзы. В защищенном режиме все прерывания происходят через IDT. Традиционная таблица векторов прерываний здесь не используется. Формат дескрипторов шлюзов отличается от дескриптора сегмента. Для начала рассмотрим шлюз вызова. В поле прав доступа задается уровень привилегий, который должен быть ниже CPL текущего процесса, бит присутствия и соответствующий тип в остальных полях. Селектор и смещение задают адрес вызываемой функции, при этом селектор должен присутствовать либо в GDT либо в активной LDT. Параметр "Количество слов стека" служит для передачи аргументов в вызываемую функцию, при этом соответствующее количество слов копируется из стека текущего уровня привилегий в стек уровня привилегий вызываемой функции. Это поле использует только младшие 5 бит четвертого байта. Остальные биты должны быть нулевыми. Обращаться к такому шлюзу, если дескриптор не расположен в IDT, можно только командой call far, при этом указываемое в команде смещение игнорируется. А селектор должен указывать на дескриптор шлюза вызова. Шлюз прерывания и шлюз ловушки имеют одинаковый формат, отличаются между собой типами в байте прав доступа. В отличии от шлюза вызова эти шлюзы не содержат в себе Количества слов стека, поскольку прерывания бывают аппаратными и передача в них параметров через стек - бессмысленна. Эти шлюзы используются обычно только в IDT. Шлюз задачи содержит в себе значительно меньше информации. Во втором и третьем байте дескриптора записывается селектор TSS (Сегмента состояния задачи). Поле прав доступа заполняется аналогично другим шлюзам, но с соответствующим типом. Остальные поля дескриптора не используются. При вызове такого шлюза происходит переключение контекста задачи. При этом вызывающая задача блокируется и не может быть вызвана до тех пор, пока вызванная задача не вернет ей управление командой iret. Про правила доступа к шлюзам я говорил в прошлом выпуске, и в этот раз я закончу на этом. в следующем выпуске расскажу про прерывания более подробно. Виртуальный режим процессора 8086. Для возможности запуска из защищенного режима программ, предназначенных для реального, существует так называемый "Виртуальный режим процессора 8086". При этом полноценно работают механизмы преобразования адресов защищенного режима. А так же многозадачные системы, которые могут одновременно выполнять как защищенные задачи, так и виртуальные. При этом адресация в виртуальной задаче осуществляется традиционным для 8086 методом - сегмент/смещение. Обращение к прерываниям осуществляется через IDT, но таблица прерываний реального режима может быть обработана из функций, шлюзы которых размещаются в IDT. Обращение виртуальной задачи к портам так же может быть отслежено через прерывания защищенного режима. При обращении к запрещенным портам происходит исключение. При желании может быть обеспечена абсолютно прозрачная работа нескольких виртуальных задач в одной мультизадачной среде. Но мы этой возможностью не будем пользоваться, и в своей работе будем рассчитывать на программы исключительно защищенного режима.
[07] - исключения защищенного режима / микроядерные системы Исключения защищенного режима. Я уже неоднократно упоминал это слово в предидущих выпусках. Но думаю что не всем было понятно что это такое. Сейчас мы рассмотрим это поподробнее. Исключения или системные прерывания существовали еще в самых первых моделях процессоров от Intel. Вот их список: 0. Division by zero (деление на ноль или переполнение при делении); 1. Single step (пошаговая отладка); 3. Breakpoint; 4. Overflow (срабатывает при команде into в случае установленного флага overflow в регистре flags); 6. Invalid opcode (i286+); 7. No math chip; Исключения располагаются в начале таблицы прерываний. В реальном режиме занимают 8 первых векторов прерываний. Введение защищенного режима потребовало введения дополнительных исключений. В защищенном режиме первые 32 вектора прерываний зарезервированы для исключений. Не все они используются в существующих процессорах, в будующем возможно их будет больше. Системные прерывания в защищенном режиме делятся на три типа: нарушения (fault), ловушки (trap) и аварии (abort). Итак в защищенном режиме у нас существуют следующие исключения: 0. Divide error (fault); 1. Debug (fault/trap); 3. Breakpoint (trap); 4. Overflow (trap); 5. Bounds check (fault); 6. Invalid opcode (fault); 7. Coprocessor not available (fault); 8. Double fault (abort); 9. Coprocessor segment overrun (fault); 10. Invalid tss (fault); 11. Segment not present (fault); 12. Stack fault (fault); 13. General protection fault (fault); 14. Page fault (fault); 16. Coprocessor error (fault); 17. Alignument check (fault) (i486+); 18. Hardware check (abort) (Pentium+); 19. SIMD (fault) (Pentium III+). Нарушения возникают вследствии несанкционированных или неправильных действий программы, предполагается, что ошибки можно исправить и продолжить выполнение программы с инструкции, которая вызвала ошибку. Ловушки возникают после выполнения инструкции, но тоже подразумевают исправление ошибочной ситуации и дальнейшую работу программы. Аварии возникают в случае критических нарушений, после этого программа уже не может быть перезапущена и должна быть закрыта. Но иногда в случае ошибки или ловушки программа тем не менее не может продолжить свое выполнение. Это зависит от тяжести нарушения и от организации операционной системы, которая обрабатывает исключения. И если ошибка или ловушка не может быть исправлена, программу так же следует закрыть. При возникновении исключения процессор иногда помещает в стек код ошибки, по которому обработчик исключения может проанализировать и, возможно, исправить возникшую ошибку. Все исключения обрабатываются операционной системой. В случае микроядерных систем этим занимается микроядро. Вот о микроядрах мы и поведем наш дальнейший разговор. Микроядерные системы. В первых выпусках я уже касался этой темы, но тогда я ограничился буквально несколькими словами. Но теперь мы решили двигаться именно в сторону микроядерности, значит стоит поподробнее рассказать, что это такое. Принцип микроядерности заключается в том, что ядро практически не выполняет операций, связанных с обслуживанием внешних устройств. Эту функцию выполняют специальные программы-сервера. Ядро лишь предоставляет им возможность обращаться к устройствам. Помимо этого ядро обеспечивает многозадачность (параллельное выполнение программных потоков), межпроцессное взаимодействие и менеджмент памяти. Приложения (как и сервера) у нас работают на третьем, непривилегированном кольце и не могут свободно обращаться к портам ввода/вывода или dma памяти. Тем более не могут сами устанавливать свои обработчики прерываний. Для использования ресурсов процессы обращаются к ядру с просьбой выделить необходимые ресурсы в их распоряжение. Осуществляется это следующим образом: Для обеспечения доступа к портам ввода/вывода используются возможности процессоров, впервые появившиеся intel 80386. У каждой задачи (в сегменте состояния задачи (TSS)) существует карта доступности портов ввода/вывода. Приложение обращается к ядру с "просьбой" зарегистрировать для нее диапазон портов. Если эти порты до тех пор никем не были заняты, то ядро предоставляет их в распоряжение процесса, помечая их как доступные в карте доступности ввода/вывода этого процесса. DMA память, опять таки после запроса у ядра, с помощью страничного преобразования подключается к адресному пространству процесса. Настройка каналов осуществляется ядром по "просьбе" процесса. Доступ к аппаратным прерываниям (IRQ) осуществляется сложнее. Для этого процесс порождает в себе поток (thread), и сообщает ядру, что этот поток будет обрабатывать какое-то IRQ. При возникновении аппаратного прерывания, которое обрабатывает всетаки ядро, данный процесс выходит из состояния спячки, в котором он находился в ожидании прерывания, и ставится в очередь к менеджеру процессов. Такие потоки должны иметь более высокий приоритет, чем все остальные, дабы вызываться как можно скорее. Но, как я говорил, ядро выполняет еще некоторые функции, немаловажная из которых - это межпроцессное взаимодействие. Оно представляет из себя возможность процессов обмениваться сообщениями между собой. В отличии от монолитных систем в микроядерных системах межпроцессное взаимодействие (Inter Process Communication или IPC) это едва ли не основное средство общения между процессами, и поскольку все драйвера у нас такие же процессы, микроядерное IPC должно быть очень быстрым. Быстродействие IPC достигается за счет передачи сообщений без промежуточного буферизирования в ядре. Либо непосредственным переписыванием процессу-получателю, либо с помощью маппинга страниц (если сообщения большого размера). Менеджер памяти имеет как бы две стороны. Первая сторона - внутренняя, распределение памяти между приложениями, организация свопинга (который тоже осуществляется не ядром непосредственно, а специальной программой-сервером) никаким образом не интересует остальные программы. Но другая сторона - внешняя служит именно для них. Программы могут запросить у ядра во временное пользование некоторое количество памяти, которое ядро им обязательно предоставит (в разумных пределах... гигабайта два... не больше... . Или же программы могут запросить у ядра какой-то определенный участок памяти. Это бывает необходимо программам-серверам. И это требование ядром также вполне может быть удовлетворено при условии, что никакая другая программа до того не забронировала этот участок памяти для себя.
[08] - файловые системы Есть много файловых систем, которые нам, в принципе, подойдут (EXT2FS, FFS, NTFS, RaiserFS и много других), есть так же файловые системы, которые нам вообще не подойдут (FAT). В процессе развития нашей операционной системы мы создадим поддержку и для них, но для начала надо остановиться на чем-то одном. Этой одной файловой системой будет EXT2FS. В этом выпуске я достаточно подробно рассмотрю файловые системы FAT, и более подробно файловую систему Linux (ext2). Поскольку наша операционная система будет юниксоподобная, то файловые системы FAT нам никак не подходят, поскольку они не обеспечивают мер ограничения доступа, и по сути своей не являются многопользовательскими. Про остальные файловые системы я ограничусь лишь основными моментами. Так же я не стану затрагивать тему разделов диска. Обсудим это в другой раз. Основные принципы файловых систем. Все устройства блочного доступа (к которым относятся жесткие или гибкие диски, компакт диски) при чтении/записи информации оперируют секторами. Для жестких или гибких дисков размер сектора равен 512 байт, в компакт-дисках размер сектора равен 2048 байт. Сектора являются физической единицей информации для носителя. Для файловых систем такое распределение часто бывает не очень удобно, и в них вводится понятие кластера. Кластеры часто бывают больше по размеру, чем сектора носителя. Кластеры являются логической единицей файловых систем. Правда, не всегда они называются кластерами. В ext2 кластеры называются просто блоками, но это не столь важно. Для организации кластеров файловые системы хранят таблицы кластеров. Таблицы кластеров, естественно, расходуют дисковое пространство. Помимо этого, дополнительное дисковое пространство расходуется под каталоги файлов. Эти неизбежные расходы в разных файловых системах имеют разную величину. Но об этом мы поговорим ниже. Файловые системы на базе FAT (File Allocation Table). Этот тип файловых систем разработала фирма 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 (корневая директория) * Область файлов. (Кластеры файловой системы) Boot sector имеет размер 512 байт, как мы уже знаем, может содержать в себе загрузчик системы, но помимо этого для FAT он содержит Блок параметров. Блок параметров размещается в boot sector'е по смещению 0x0b и содержит в себе следующую информацию: Code: struct FAT_Parameter_block { u_int16 Sector_Size; u_int8 Sectors_Per_Cluster; u_int16 Reserved_Sectors; u_int8 FAT_Count; u_int16 Root_Entries; u_int16 Total_Sectors; u_int8 Media_Descriptor; u_int16 Sectors_Per_FAT; u_int16 Sectors_Per_Track; u_int16 Heads; u_int32 Hidden_sectors; u_int32 Big_Total_Sectors; }; Размер кластера можно вычислить, умножив Sector_Size на Sectors_Per_Cluster. Общий размер диска определяется следующим образом: Если значение Total_Sectors равно 0, то раздел более 32 мегабайт и его длина в секторах храниться в Big_Total_Sectors. Иначе размер раздела показан в Total_Sectors. Таблица FAT начинается с сектора, номер которого храниться в Reserved_Sectors и имеет длину Sectors_Per_FAT; при 16-битном FAT размер таблицы может составлять до 132 килобайт (или 256 секторов) (в FAT12 до 12 килобайт). Вторая копия FAT служит для надежности системы... но может отсутствовать. После таблицы FAT следует корневая директория диска. Размер этой директории ограничен Root_Entries записями. Формат записи в директории таков: Code: struct FAT_Directory_entry { char Name[8]; char Extension[3]; u_int16 File_Attribute; char Reserved[10]; u_int16 Time; u_int16 Date; u_int16 Cluster_No; u_int32 Size; }; Размер записи - 32 байта, следовательно, общий размер корневой директории можно вычислить, умножив Root_Entries на 32. Далее на диске следуют кластеры файловой системы. Из записи в директории берется первый номер кластера, с него начинается файл. В FAT под этим номером может содержаться либо код последнего кластера (0xffff или 0xfff для FAT12) либо номер кластера, следующего за этим. При записи файла из FAT выбираются свободные кластеры по порядку от начала. В результате возникает фрагментация файловой системы, и существенно замедляется ее работа. Но это уже выходит за тему рассылки. Все выше сказанное про FAT справедливо для FAT12 и FAT16. FAT32 более существенно отличается, но общие принципы организации для нее примерно такие же. VFAT ничем не отличается от FAT16, для хранения длинных имен там используется однеа запись в директории для хранения короткого имени файла и несколько записей для хранения длинного. Длинное имя храниться в unicode, и на запись в директории приходится 13 символов длинного имени, причем они разбросаны по некоторым полям записи, остальные поля заполняются с таким расчетом, чтобы старые программы не реагировали на такую запись. С первого взгляда видна не высокая производительность таких файловых систем. Не буду поливать грязью Microsoft, у них и без меня достаточно проблем... К тому же и у них есть другие разработки, которые не столь плохи. Но о них мы поговорим ниже... А сейчас давайте посмотрим на ext2fs. Правда, эта файловая система несколько другого уровня, и сравнивать ее с FAT - нельзя. Но обо всем по порядку. Ext2fs (Расширенная файловая система версия 2) 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 содержит в себе информацию о файловой системе и имеет следующий формат: Code: 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 килобайта размером. Об остальных полях чуть попозже. А теперь рассмотрим группы дескрипторов файловой системы. Формат дескриптора группы таков: Code: 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]; }; Содержимое группы таково: * Block bitmap (Битовая карта занятости блоков) * Inode bitmap (Битовая карта занятости inode) * Inode table (Таблица inode) * Available blocks (блоки, доступные для размещения файлов) Блоки в файловой системе отсчитываются с начала раздела. В дескрипторе группы содержаться номер блока с битовой картой блоков группы, номер блока с битовой картой инодов, и номер блока с которого начинается таблица inode. Про inode мы поговорим чуть попозже, а сперва разберемся с битовыми картами. В суперблоке храниться количество блоков в группе (s_blocks_per_group). Битовая карта имеет соответствующий размер в битах (занимает она не более блока). и в зависимости от размера блока может содержать информацию об использовании 8, 32 или 132 мегабайт максимум. Дисковое пространство раздела разбивается на группы в соответствии с этими значениями. А групп, как я уже упоминал, может быть до 32... что позволяет создавать разделы, в зависимости от размера блока, 256, 1024 или 4096 мегабайт соответственно. В битовую карту блоков группы входят так же те блоки, которые используются под саму карту, под карту inode и под таблицу inode. Они сразу помечаются как занятые. Теперь давайте разберемся, что такое inode. В отличии от FAT информация о файле здесь храниться не в директории, а в специальной структуре, которая носит название inode (информационный узел). В записи директории содержится только адрес inode и имя файла. При этом на один inode могут ссылаться несколько записей директории. Это называется hard link. Формат inode таков: Code: struct ext2_inode { u_int16 i_mode; u_int16 i_uid; u_int32 i_size; u_int32 i_atime; u_int32 i_ctime; u_int32 i_mtime; u_int32 i_dtime; u_int16 i_gid; u_int16 i_links_count; u_int32 i_blocks; u_int32 i_flags; u_int32 i_reserved1; u_int32 i_block[14]; u_int32 i_version; u_int32 i_file_acl; u_int32 i_dir_acl; u_int32 i_faddr; u_int8 i_frag; u_int8 i_fsize; u_int16 i_pad1; u_int32 i_reserved2[2]; }; Как видно из приведенной выше структуры в inode содержится следующая информация: * Тип и права доступа файла (i_mode) * идентификатор хозяина файла (i_uid) * Размер (i_size) * Время доступа, создания, модификации и удаления файла (после удаления inode не удаляется, а просто перестает занимать блоки файловой системы) * Идентификатор группы * Количество записей в директориях, указывающих на этот inode... * Количество занимаемых блоков fs * дополнительные флаги ext2fs * таблица занимаемых блоков * Ну и другая, не столь существенная в данных момент информация. Остановимся поподробнее на таблице занимаемых блоков. Как видите там всего 14 записей. Но 14 блоков - это мало для одного файла. Дело в том, что не все записи содержат номера блоков. 13-я запись содержит косвенный блок, то есть блок, в котором содержится таблица блоков. А 14-я запись содержит номер блока в котором содержится таблица номеров блоков, в которых содержаться таблицы блоков занимаемых файлом... так что размер файла практически ничто не ограничивает. Первые 10 inode зарезервированы для специфического использования. Для корневой директории в этой файловой системе не отводится заранее отведенного места. Любая, в том числе и корневая директория в этой файловой системе является по сути своей обыкновенным файлом. Но для облегчения поиска корневой директории для нее зарезервирован inode номер 2. В этой файловой системе в отличие от FAT существуют методы защиты файлов, которые обеспечиваются указанием идентификаторов пользователя и группы, а так же правами доступа, которые указываются в inode в поле i_mode. За счет нескольких групп блоков уменьшается перемещение головки носителя при обращении к файлам, что увеличивает скорость обращения и уменьшает износ носителя. Да и сама файловая система организована так, что для чтения файлов не требуется загрузка больших объемов служебной информации, Что тоже не может не сказаться на производительности. Примерно так же устроены файловые системы FFS, HPFS, NTFS. Но в их устройство я не буду вдаваться. И так уже выпуск очень большой получается. Но в недавнее время появился еще один тип файловых систем. Эти системы унаследовали некоторые черты от баз данных и получили общее название "Журналируемые файловые системы". Особенность их заключается в том что все действия, производимые в файловой системе фиксируются в журнале, который правда съедает некоторый объем диска, но это позволяет значительно повысит надежность систем. В случае сбоя проверяется состояние файловой системы и сверяется с записями в журнале. В случае обнаружения несоответствий довести операцию до конца не составляет проблем, и отпадает необходимость в ремонте файловой системы. К таким файловым системам относятся ext3fs, RaiserFS и еще некоторые.
[09] - чтение ext2fs Чтение ext2fs В прошлом выпуске я описывал структуру этой файловой системы. Как вы поняли, (я надеюсь) в файловой системе присутствует Super Block и дескрипторы групп. Эта информация хранится в начале раздела. Super Block во 2-м килобайте, дескрипторы групп - в третьем. Стоит заметить, что первый килобайт для нужд файловой системы не используется и может быть целиком использован для boot sector'а (правда он уже будет не сектор, а килобайт . Но для этого следует подгрузить второй сектор boot'а. А для инициализации файловой системы нам нужно загрузить super block и дескрипторы групп, они же понадобятся нам для работы с файловой системой. Это все можно загрузить одновременно, как мы и сделаем. Code: 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 номера файловой системы и вычисляет размеры блока для работы. Code: 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. Code: .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 block_size: dw 1024 block_dword_size: dw 256 block_seg_size: dw 64 block_sect_size: dw 2 Все эти значения нам понадобятся для работы. А теперь рассмотрим процедуру загрузки одного блока файловой системы. Code: ext2_load_block: pusha mov cx, [block_sect_size] mul cx call load_block mov ax, es add ax, [block_seg_size] mov es, ax ; смещаем es popa ret При входе в эту процедуру ax содержит номер блока (блоки нумеруются с нуля), es содержит адрес памяти для загрузки содержимого блока. Номер блока нам надо преобразовать в номер сектора, для этого мы умножаем его на длину блока в секторах. А в cx у нас уже записана длина блока в секторах, то есть все готово для вызова процедуры load_block. После считывания блока мы модифицируем регистр es, чтобы последующие блоки грузить следом за этим... в принципе модифицирование указателя можно перенести в другое место, в процедуру загрузки файла, это будет наверное даже проще и компактнее, но сразу я об этом не подумал. Но пошли дальше... основной структурой описывающей файл в ext2fs является inode. Inode храняться в таблицах, по одной таблице на каждую группу. Количество inode в группе зафиксировано в супер блоке. Итак, процедура загрузки inode: Code: ext2_get_inode: pusha push es dec ax xor dx, dx div word [sb + ext2_sb.inodes_per_group] Поделив номер inode на количество inode в группе, в ax мы получаем номер группы, в которой находится inode, в dx получаем номер inode в группе. Code: shl ax, gd_bit_size mov bx, ax mov bx, [gd + bx + ext2_gd.inode_table] ax умножаем на размер записи о группе (делается это сдвигом, но, по сути, то же самое умножение) и получаем смещение группы в таблице дескрипторов групп. gd - базовый адрес таблицы групп. Последняя операция извлекает из дескриптора группы адрес таблицы inode этой группы (адрес задается в блоках файловой системы) который у нас пока будет храниться в bx. Code: mov ax, dx shl ax, inode_bit_size Теперь разберемся с inode. Определим его смещение в таблице inode группы. Code: xor dx, dx div word [block_size] add ax, bx Поделив это значение на размер блока мы получим номер блока относительно начала таблицы inode (ax), и смещение inode в блоке (dx). К номеру блока (bx) прибавим блок, в котором находится inode. Code: mov bx, tmp_block >> 4 mov es, bx call ext2_load_block Загрузим этот блок в память. Code: push ds pop es mov si, dx add si, tmp_block mov di, inode mov cx, ext2_i_size >> 1 rep movsw Восстановим содержимое сегментного регистра es и перепишем inode из блока в отведенное для него место. Code: pop es popa ret Inode загружен. Теперь по нему можно загружать файл. Здесь все не столь однозначно. Процедура загрузки файла состоит из нескольких модулей. Потому что помимо прямых ссылок inode может содержать косвенные ссылки на блоки. В принципе можно ограничить возможности считывающей подпрограммы необходимым минимумом, полная поддержка обеспечивает загрузку файлов до 4 гигабайт размером. Естественно в реальном режиме мы такими файлами оперировать не сможем, да это и не нужно. Но сейчас мы рассмотрим полную поддержку: Code: ext2_load_inode: pusha xor ax, ax mov si, inode + ext2_i.block mov cx, EXT2_NDIR_BLOCKS call dir_blocks cmp ax, [inode + ext2_i.blocks] jz short .exit В inode храняться прямые ссылки на 12 блоков файловой системы. Такие блоки мы загружаем с помощью процедуры dir_blocks (она будет описана ниже). Данный этап может загрузить максимум 12/24/48 килобайт файла (в зависимости от размера блока fs 1/2/4 килобайта). После окончания работы процедуры проверяем, все ли содержимое файла уже загружено или еще нет. Если нет, то загрузка продолжается по косвенной таблице блоков. Косвенная таблица - это отдельный блок в файловой системе, который содержит в себе таблицу блоков. Code: mov cx, 1 call idir_blocks cmp ax, [inode + ext2_i.blocks] jz short .exit В inode только одна косвенная таблица первого уровня (cx=1). Для загрузки блоков из такой таблицы мы используем процедуру idir_blocks. Это позволяет нам, в зависимости от размера блока загрузить 268/1048/4144 килобайта файла. Если файл еще не загружен до конца, то используется косвенная таблица второго уровня. Code: mov cx, 1 call ddir_blocks cmp ax, [inode + ext2_i.blocks] jz short .exit В inode также только одна косвенная таблица второго уровня (cx=1). Для загрузки блоков из такой таблицы мы используем процедуру ddir_blocks. Это позволяет нам, в зависимости от размера блока загрузить 64.2/513/4100 мегабайт файла. Если файл опять не загружен до конца (где же столько памяти взять??), то используется косвенная таблица третьего уровня. Ради этого мы уже не будем вызывать подпрограмм, а обработаем ее в этой процедуре. Code: push ax push es mov ax, tmp3_block >> 4 mov es, ax lodsw call ext2_load_block pop es pop ax mov si, tmp3_block mov cx, [block_dword_size] call ddir_blocks В inode и эта таблица присутствует только в одном экземпляре (куда же больше?). Это, крайняя возможность, позволяет нам, в зависимости от размера блока, загрузить 16/256.5/4100 гигабайт файла. Что уже является пределом даже для размера файловой системы (4 терабайта). Code: .exit: popa ret Конечно, такие крайности нам при старте будут не к чему, с учетом, что мы находимся в реальном режиме и не можем адресовать больше ~600к памяти. Кратко рассмотрю вспомогательные функции: Code: dir_blocks: .repeat: push ax lodsw call ext2_load_block add si, 2 pop ax inc ax cmp ax, [inode + ext2_i.blocks] jz short .exit loop .repeat .exit: ret Эта функция загружает прямые блоки. Ради простоты я пока не обрабатывал блоки номер которых превышает 16 бит. Это создает ограничение на размер файловой системы в 65 мегабайт, а реально еще меньше, поскольку load_block у нас тоже не оперирует с секторами, номер которых больше 16 бит, ограничение по размеру уменьшается до 32 мегабайт. В дальнейшем эти ограничения мы конечно обойдем, а пока достаточно. В этой функции стоит проверка количества загруженных блоков, для того чтобы вовремя выйти из процедуры считывания. Code: idir_blocks: .repeat: push ax push es mov ax, tmp_block >> 4 mov es, ax lodsw call ext2_load_block add si, 2 pop es pop ax push si push cx mov si, tmp_block mov cx, [block_dword_size] call dir_blocks pop cx pop si cmp ax, [inode + ext2_i.blocks] jz short .exit loop .repeat .exit: ret Эта функция обращается в свою очередь к функции dir_blocks, предварительно загрузив в память содержимое косвенного блока. так же имеет контроль длины файла. Функция ddir_blocks в точности аналогична этой, только для считывания вызывает не dir_blocks, а idir_blocks, поскольку адреса блоков в ней дважды косвенны. Но мы еще не рассмотрели самого главного. Процедуры, которая по пути файла может загрузить его с диска. Начнем. Code: ext2_load_file: pusha cmp byte [si], '/' jnz short .error_exit Если путь файла не начинается со слэш, то это в данном случае является ошибкой. Мы не оперируем понятием текущий каталог! Code: mov ax, INODE_ROOT ; root_inode call ext2_get_inode Загружаем корневой inode - он имеет номер 2. Code: .cut_slash: cmp byte [si], '/' jnz short .by_inode inc si jmp short .cut_slash Уберем лидирующий слэш... или несколько слэшей, такое не является ошибкой. Code: .by_inode: push es call ext2_load_inode pop es Загрузим содержимое файла. Директории, в том числе и корневая, являются такими же файлами, как и все остальные, только содержат в себе записи о находящихся в директории файлах. Code: mov ax, [inode + ext2_i.mode] and ax, IMODE_MASK cmp ax, IMODE_REG jnz short .noreg_file По inode установим тип файла. Если файл не регулярный, то это может быть директорией. Это проконтролируем ниже. Code: cmp byte [si], 0 jnz short .error_exit Если это файл, который нам надлежит скачать - то в [si] будет содержаться 0, означающий что мы обработали весь путь. Code: .ok_exit: clc jmp short .exit А поскольку содержимое файла уже загружено, то можем со спокойной совестью вернуть управление. Битом C сообщив, что все закончилось хорошо. Code: .noreg_file: cmp ax, IMODE_DIR jnz short .error_exit Если этот inode не является директорией, то это или не поддерживаемый тип файла или ошибка в пути. Code: mov dx, [inode + ext2_i.size] xor bx, bx Если то, что мы загрузили, является директорией, то со смещения 0 (bx) в этом файле содержится список записей о файлах. Нам нужно выбрать среди них нужную. В dx сохраним длину файла, по ней будем определять коней директории. Code: .walk_dir: lea di, [es:bx + ext2_de.name] mov cx, [es:bx + ext2_de.name_len] ; длина имени push si repe cmpsb mov al, [si] pop si test cx, cx jnz short .notfind Сравниваем имена из директории с именем, на которое указывает si. Если не совпадает - перейдем на следующую запись (чуть ниже) Code: cmp al, '/' jz short .normal_path test al, al jnz short .notfind Если совпал, то в пути после имени должно содержаться либо '/' либо 0 - символ конца строки. Если это не так, значит это не подходящий файл. Code: .normal_path: mov ax, [es:bx + ext2_de.inode] call ext2_get_inode Загружаем очередной inode. Code: add si, [es:bx + ext2_de.name_len] cmp byte [si], '/' jz short .cut_slash jmp short .by_inode И переходим к его обработке. Это продолжается до тех пор, пока не пройдем весь путь. Code: .notfind: sub dx, [es:bx + ext2_de.rec_len] add bx, [es:bx + ext2_de.rec_len] test dx, dx jnz short .walk_dir Если путь не совпадает, и если в директории еще есть записи - продолжаем проверку. Code: .error_exit: mov si, bad_dir call outstring stc Иначе выводим сообщение об ошибке Code: .exit: popa ret Вот и весь алгоритм. Не смотря на большой размер этого повествования, код занимает всего около 450 байт. А если убрать параноидальные функции, то и того меньше.
[10] - форматы файлов ELF и PE Формат ELF. В данном обзоре мы будем говорить только о 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 Безнаковое байтовое целое Теперь рассмотрим заголовок файла: Code: #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 содержит в себе информацию о системе и состоит из нескольких подполей. Code: 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 операционные системы хранят свою идентификационную информацию. Это поле может быть пустым. Для нас оно тоже не важно. Поле заголовка e_type может содержать несколько значений, для выполняемых файлов оно должно быть ET_EXEC равное 2 e_machine - определяет процессор на котором может работать данный выполняемый файл (Для нас допустимо значение EM_386 равное 3) Поле e_version соответствует полю ei_version из заголовка. Поле e_entry определяет стартовый адрес программы, который перед стартом программы размещается в eip. Поле e_phoff определяет смещение от начала файла, по которому располагается таблица программных секций, используемая для загрузки программ в память. Не буду перечислять назначение всех полей, не все нужны для загрузки. Лишь еще два опишу. Поле e_phentsize определяет размер записи в таблице программных секций. И поле e_phnum определяет количество записей в таблице программных секций. Таблица секций (не программных) используется для линковки программ. мы ее рассматривать не будем. Так же мы не будем рассматривать динамически линкуемые модули. Тема эта достаточно сложная, для первого знакомства не подходящая. Теперь про программные секции. Формат записи таблицы программных секций таков: Code: struct elf32_phdr { Elf32_Word p_type; Elf32_Off p_offset; Elf32_Addr p_vaddr; Elf32_Addr p_paddr; Elf32_Word p_filesz; Elf32_Word p_memsz; Elf32_Word p_flags; Elf32_Word p_align; }; Подробнее о полях. p_type - определяет тип программной секции. Может принимать несколько значений, но нас интересует только одно. PT_LOAD (1). Если секция именно этого типа, то она предназначена для загрузки в память. p_offset - определяет смещение в файле, с которого начинается данная секция. p_vaddr - определяет виртуальный адрес, по которому эта секция должна быть загружена в память. p_paddr - определяет физический адрес, по которому необходимо загружать данную секцию. Это поле не обязательно должно использоваться и имеет смысл лишь для некоторых платформ. p_filesz - определяет размер секции в файле. p_memsz - определяет размер секции в памяти. Это значение может быть больше предыдущего. Поле p_flag определяет тип доступа к секциям в памяти. Некоторые секции допускается выполнять, некоторые записывать. Для чтения в существующих системах доступны все. Загрузка формата ELF. С заголовком мы немного разобрались. Теперь я приведу алгоритм загрузки бинарного файла формата ELF. Алгоритм схематический, не стоит рассматривать его как работающую программу. Code: 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, и расставить на соответствующие страницы права доступа, да и просто копирование здесь не подойдет, но это уже не относится к формату, а к распределению памяти. Поэтому сейчас об этом не будем говорить. Формат PE. Во многом он аналогичен формату ELF, ну и не удивительно, там так же должны быть секции, доступные для загрузки. Как и все в Microsoft формат PE базируется на формате EXE. Структура файла такова: * 00h - EXE заголовок (не буду его рассматривать, он стар как Дос. * 20h - OEM заголовок (ничего существенного в нем нет); * 3сh - смещение реального PE заголовка в файле (dword). * таблица перемещения stub; * stub; * PE заголовок; * таблица объектов; * объекты файла; stub - это программа, выполняющаяся в реальном режиме и производящая какие-либо предварительные действия. Может и отсутствовать, но иногда может быть нужна. Нас интересует немного другое, заголовок PE. Структура его такая: Code: 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 заголовка. Записи в этой таблице имеют следующий формат: Code: 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; }; o_name - имя секции, для загрузки абсолютно безразлично; o_vsize - размер секции в памяти; o_vaddr - адрес в памяти относительно ImageBase; o_psize - размер секции в файле; o_poff - смещение секции в файле; o_flags - флаги секции; Вот на флагах стоит остановиться поподробнее. * 00000004h - используется для кода с 16 битными смещениями * 00000020h - секция кода * 00000040h - секция инициализированных данных * 00000080h - секция неинициализированных данных * 00000200h - комментарии или любой другой тип информации * 00000400h - оверлейная секция * 00000800h - не будет являться частью образа программы * 00001000h - общие данные * 00500000h - выравнивание по умолчанию, если не указано иное * 02000000h - может быть выгружен из памяти * 04000000h - не кэшируется * 08000000h - не подвергается страничному преобразованию * 10000000h - разделяемый * 20000000h - выполнимый * 40000000h - можно читать * 80000000h - можно писать Опять таки не буду с разделяемыми и оверлейными секциями, нас интересуют код, данные и права доступа. В общем, этой информации уже достаточно для загрузки бинарного файла. Загрузка формата PE. Code: 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; } Это опять таки не готовая программа, а алгоритм загрузки. И опять таки многие моменты не освещаются, так как выходят за пределы темы. Но теперь стоит немного поговорить про существующие системные особенности. Системные особенности. Не смотря на гибкость средств защиты, имеющихся в процессорах (защита на уровне таблиц дескрипторов, защита на уровне сегментов, защита на уровне страниц) в существующих системах (как в Windows, так и в Unix) полноценено используется только страничная защита, которая хотя и может уберечь код от записи, но не может уберечь данные от выполнения. (Может быть, с этим и связано изобилие уязвимостей систем?) Все сегменты адресуются с нулевого линейного адреса и простираются до конца линейной памяти. Разграничение процессов производится только на уровне страничных таблиц. В связи с этим все модули линкуются не с начальных адресов, а с достаточно большим смещением в сегменте. В Windows используется базовый адрес в сегменте - 0x400000, в юникс (Linux или FreeBSD) - 0x8048000. Некоторые особенности так же связаны со страничной организацией памяти. ELF файлы линкуются таким образом, что границы и размеры секций приходятся на 4-х килобайтные блоки файла. А в PE формате, не смотря на то, что сам формат позволяет выравнивать секции на 512 байт, используется выравнивание секций на 4к, меньшее выравнивание в Windows не считается корректным.
[11] - процесс загрузки Процесс загрузки. То, что я до сих пор сделал пока рассчитано только на работы с дисками 1,4Мб, то есть с флопами. Это конечно ограничение в некоторой степени, но пока система еще далеко не готова, этого достаточно. Естественно это еще не окончательный вариант. Да и можно ли говорить об окончательности программных продуктов? Нет предела совершенству. В обязанности бутсектора входит следующее: 1. Загрузить с диска дополнительные части кода и служебную информацию файловой системы. 2. Загрузить с диска файл сценария (конфигурации) загрузки. 3. Загрузить с диска ядро и модули. 4. Перейти в защищенный режим. 5. Передать управление ядру. Если с первым и двумя последними пунктами все просто и компактно, то второй и третий пункт требуют возможности работы с файловой системой, а третий пункт помимо этого должен знать структуру бинарных форматов. На все это не хватает 512 байт, отводимых для бутсектора. Наш бутсектор занимает больше - один килобайт. В файловой системе EXT2 с этим не возникает никаких проблем, поскольку первый килобайт файловой системы не используется. В FAT это немного сложнее. Служебная структура, именуемая Boot Sector Record (BSR), содержит в себе все необходимые поля для выделения для загрузочного сектора места более чем 512 байт. Но как это сделать при форматировании, стандартными средствами, я не нашел. И если формат диска не соответствует каким-то внутренним представлениям Windows, то содержимое такого нестандартного диска может быть испорчено. Выход был найден случайно. Как оказалось утилита format хоть и не имеет таких параметров командной строки, но перед форматированием берет информацию из BSR. И если предварительно заполнить эту структуру (с нужными нам параметрами), а потом уже форматировать, то все получается так, как хочется нам. Таким образом, у меня получилось сделать диск, у которого два сектора зарезервированы (там будет размещаться boot), и одна копия FAT. Ну теперь давайте по порядку рассмотрим все этапы работы бутсектора. Загрузка с диска дополнительной части кода и служебной информации файловой системы. Бутсектор загружается БИОСом по адресу 0:7c00h занимает он 512 байт. Память начиная с адреса 0:7e00h свободна. но в эту память мы загрузим второй сектор бута. Одновременно загружается информация необходимая для обслуживания файловой системы. Для EXT2 дополнительно необходимо загрузить два килобайта (суперблок и дескрипторы групп), для FAT немного больше - 4,5 килобайта (первая копия FAT). Code: mov ax, 0x7e0 mov es, ax Адрес 0:7e00h идентичен адресу 7e0h:0. Вторым вариантом мы и будем пользоваться, потому что наша процедура загрузки секторов размещает их по сегментному адресу, хранящемуся в es. Code: mov ax, 1 В ax номер сектора, с которого начинается чтение (первый сектор является нулевым (каламбур . И далее все зависит от файловой системы. Code: %ifdef EXT2FS mov cx, 5 Для EXT2 загружается 5 секторов - второй сектор бутсектора (1 сектор), суперблок файловой системы (2 сектора) и дескрипторы групп (2 сектора). Code: %elifdef FATFS mov cx, 10 Для FAT загружается 10 секторов - второй сектор бутсектора (1 сектор), таблица FAT - 9 секторов (такой размер она имеет на floppy дисках). Code: %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к). Перейдем к предпоследнему пункту. Переход в защищенный режим. Бутсектор не особо беспокоится об организации памяти в системе - это забота ядра. Для перехода в защищенный режим он описывает всего два сегмента: сегмент кода и сегмент данных. оба сегмента имеют базовый адрес - 0 и предел в 4 гигабайта (это нам пригодиться для проверки наличия памяти). Перед переходом в защищенный режим нам необходимо включить адресную линию A20. По моим сведениям этот механизм ввели в пору 286 для предотвращения несанкционированных обращений к памяти свыше одного мегабайта (непонятно зачем?). Но поскольку это имеет место быть - нам это нужно обрабатывать, иначе каждый второй мегабайт будет недоступен. Делается это почему-то через контроллер клавиатуры (еще одна загадка). Code: mov al, 0xd1 out 0x64, al mov al, 0xdf out 0x60, al После этого можно переходить в защищенный режим. Code: lgdt [gd_desc] В регистр gdtr загружается дескриптор GDT. Code: push byte 2 popf Очищается регистр флагов. Code: mov eax, cr0 or al, 1 mov cr0, eax Включается защищенный режим. Не смотря на то, что процессор уже находится в защищенном режиме, мы пока работаем в старых адресах. Чтобы от них уйти нам нужно сделать дальный переход в адреса защищенного режима. Code: jmp 16:.epm BITS 32 .epm: 16 в этом адресе перехода - это не сегмент. Это селектор сегмента кода. Code: mov ax, 8 mov ds, ax mov es, ax ; Ставим стек. mov ss, ax movzx esp, sp После всего этого мы инициализируем сегментные регистры соответствующими селекторами, в том числе и сегмент стека, но указатель стека у нас не меняется, только теперь он становится 32-х битным. Code: ... 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 байт выше. Code: gd_desc: dw 3 * descriptor_size - 1 dd gd_table - descriptor_size А это содержимое регистра GDTR. Здесь устанавливается предел и базовый адрес дескриптора. обратите внимание на базовый адрес, здесь происходит резервирование нулевого дескриптора. Теперь процессор находится в защищенном режиме и уже не оперирует сегментами, а оперирует селекторами. Селекторов у нас всего три. Нулевой - недопустим. восьмой является селектором данных и шестнадцатый - селектором кода. После этого управление можно передать ядру. дальше со всем этим будет разбираться оно. Передача управления ядру. Здесь вообще все просто. Когда мы загрузили ядро, в файле ядра мы определили адреса сегмента кода и сегмента данных. Не смотря на то, что ядро имеет вполне конкретные смещения в сегменте (которые задаются при компиляции), код инициализации ядра рассчитан на работу без привязки к адресам. Это нужно для определения количества памяти, после перевода ядра на свои адреса доступ ко всей памяти будет для ядра затруднен в связи с включением механизма страничного преобразования. Итак, переходим к выполнению кода ядра. Code: mov ebx, kernel_data mov eax, [ebx + module_struct.code_start] jmp eax В этом фрагменте в eax записывается адрес начала кодового сегмента ядра. Так как сегмент кода у нас занимает всю виртуальную память, нам не важно где находится ядро (хотя мы знаем, что оно было загружено в базовую память). Мы просто передаем ему управление.
[12] - определение количества памяти Определение количества памяти через BIOS. Ну, начнем с исторических функций. Давным-давно, когда даже Билл Гейтс говорил что 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 - Количество возвращенных байт; * буфер заполнен информацией; Эту функцию нужно вызывать в цикле до тех пор, пока не будет прочитана вся карта памяти. Формат структуры таков: Code: struct { long long base; long long length; long type; }; Поле type может содержать следующие значения: * 1 - Доступно для использования операционной системой; * 2 - Зарезервировано (например, ROM); * 3 - ACPI reclaim memory (Доступно для операционной системы после прочтения таблицы ACPI; * 4 - ACPI NVS memory (Операционной системе требуется сохранять эту память между NVS сессиями). Проверить как работает эта функция у меня не получилось, мой BIOS ее не поддерживает. Но в заключение скажу следующее. Все функции в случае ошибки (если функция не поддерживается) возвращают установленный флаг cf. В случае отсутствия новых функций необходимо обращаться к более старым. Функции BIOS не работают в защищенном режиме, поэтому все эти операции необходимо производить еще до перехода в защищенный режим. Определение размера памяти другими способами: Помимо функций BIOS есть еще много других способов. Самый простой - помереть память самому. Делается это из защищенного режима, страничное преобразование должно быть выключено, адресная линия A20 должна быть включена. Можно мереть от нуля, но поскольку в первом мегабайте есть дыры (видеопамять, биосы, просто дыры), удобнее делать это начиная с первого мегабайта. Вовсе не обязательно проверять каждый байт, достаточно проверять один байт на какое-то определенное количество памяти. Определенным количеством памяти можно посчитать мегабайт, но лучше (хотя и медленнее) за единицу памяти принять одну страницу памяти (4к). Во избежание неприятностей память лучше не разрушать, а восстанавливать в первоначальном виде. делается это примерно так: Code: 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Мб памяти. Не очень то подходят для использования. Динамическое распределение памяти. Почти любое приложение пользуется динамически выделяемыми блоками памяти (известная, наверное, всем функция malloc в c). Сейчас мы поговорим о том, как это все работает. Подходить к этому можно по разному, но принцип везде прослеживается один. На каждый блок памяти необходимо иметь структуру, описывающую занятось блока, его размер. В примитивной реализации это может выглядеть так, как это сделано в DOS. В ДОСе вся память на равных правах принадлежит всем запущенным программам. Но чтобы операционная система могла как-то контролировать использование памяти, в ДОСе применяются MCB (Memory Control Block). Формат этого блока таков: Code: 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, у которого количество блоков редко превышает несколько десятков, но в приложениях поиск по цепочке блоков может быть достаточно долгой процедурой. Поэтому в приложениях обычно применяется другой алгоритм, который заключается в следующем. (Я рассмотрю наиболее быстрый алгоритм, вариантов, конечно, может быть множество): У каждого блока, как я уже говорил, есть два основных параметра: размер и флаг занятости. Оба эти параметра размещаются в одном двойном слове памяти. Поскольку как начало блока, так и его размер обычно выравниваются на четное число байт, младшие биты размера остаются неиспользуемыми (всегда равны нулю) и флаг занятости размещается в одном из них. Этот параметр блока размещается перед началом и по окончанию блока. Начальный параметр следующего блока соответственно будет размещен непосредственно после конечного параметра предыдущего, что позволит анализировать цепочку блоков с одинаковым успехом в обоих направлениях. Свободные блоки памяти размещаются в списках в соответствии со своим размером. Размер блоков в списках увеличивается в геометрической прогрессии. К примеру, в первом списке хранятся блоки до 16 байт длиной, во втором до 32-х байт длиной и так далее. Такая система позволяет, зная размер необходимого блока, сразу же выбирать из соответствующего списка подходящий блок и не требует поиска по всем блокам. Для организации списков к блоку добавляются несколько параметров (поскольку блок свободен, и его внутреннее пространство может быть использовано для любых целей, эти параметры размещаются в самом блоке). К этим параметрам относятся ссылка на следующий свободный блок в списке, и номер списка в котором находится блок. (Это позволяет ускорить удаление блока из списка). Для выделения блока необходимого размера сперва проверяется список соответствующего размера, в котором может потребоваться поиск блока. Если соответствующий список пуст, то проверяется следующий список, в котором уже не требуется проводить поиска, поскольку любой блок заведомо больше нужного размера. Найденный пустой блок делится на две части, вторая - не нужная часть оформляется как свободная и помещается в соответствующий список, а первая часть оформляется как занятая и возвращается программе. Из-за необходимости введения дополнительных параметров для свободных блоков памяти минимальный размер блока не может быть меньше 8 байт. Даже если пользователь захочет получить блок меньшего размера, выделится блок в 8 байт длиной. При освобождении блока, если предыдущий или последующий блоки пусты, он объединяется с ними в один блок и добавляется в список соответствующего размера. Использованные окружающие блоки удаляются из тех списков, в которых они были записаны ранее. Для того, чтобы предотвратить попытку объединения первого блока памяти (при его освобождении) с предшествующим ему, перед первым блоком ставится параметр с флагом занятости. То же самое делается и для последнего блока памяти, но только после него.
Файловый архив disk2.zip (9 kb) - KernelNG PreAlpha-1. Новая организация ресурсов. imgtools.rar (32 kb) - Image tools - fdread.exe, fdwrite.exe (dos/win32) - утилиты для работы с имиджами дисков от Дрона i586-elf-gnu.rar (2.2 Mb) - i586-elf GNU (bin) - i586 elf binutils и gcc nd_gnutools.rar (449 kb) - ND GNU-Tools (bin) - гнутые тулзы, выдранные из cygwin для корректной работы компайлеров Tech Help 6.0 - Замечательный справочник по прерываниям BIOS и DOS. Имеет массу другой информации. Схож с HelpPC (если Вы знаете, что это такое). bootprog.zip (50 kb) - Пару примеров работы с бут сектором на паскале и ассемблере os.zip (377 kb) - Исходник операционной системы, написанной Алексеем Фрунзей SolarOS (3145 kb) - Исходник операционной системы, написанной моим земляком, Богданом Онтану. Kolibri OS 0.6.0.0 (2117 kb) - Kolibri OS - операционная система, развившаяся из Menuet 32. Отличается ориентированностью на ассемблере - большая часть приложений для неё также написаны на ассемблере. bin86-0.16.17.tar.gz (700 kb) - This is a simple assember and linker for 8086 - 80386 machine code. MS-DOS 6.0 (19 mb) - Исходник известной нам ОС, MS-DOS 6.0 . Сегодня только осилил залить. Пароль на архив: antichat.ru~!@# PS Пост будет обновляться. "Горячие" файлы, буду выделять красным, остальные - оливковым.
(C) Dron, 2001 г. Статью я нашел на просторах народа и побоялся, как-бы она не потерялась. Решил выложить ее здесь. Над оформлением буду еще работать. Статья не моя, но всетаки предлагаю супер-модератору, закрепить ее как важное.
В общем нарыл еще один интересный исходник ядра, для винды 9х, который работает в защищенном режиме. [DOWNLOAD]
На инаттаке по-свежее статейка лежит: Написание собственной Операционной Системы №1 Написание собственной Операционной Системы №2 Написание собственной Операционной Системы №3 PS А может собраться и AntichatOS накодить?
Запосчу-ка я сюда же примерчик =) The Real "Hello World" 1. Идея (hello.c) Изучение нового языка программирования начинается, как правило, с написания простенькой программы, выводящей на экран краткое приветствие типа "Hello World!". Например, для C это будет выглядить приблизительно так. Code: main() { printf("Hello World!\n"); } Показательно, но совершенно не интересно. Программа, конечно работает, режим защищенный, но ведь для ее функционирования требуется ЦЕЛАЯ операционная система. А что если написать такой "Hello World", для которого ничего не надо. Вставляем дискетку в компьютер, загружаемся с нее и ..."Hello World". Можно даже прокричать это приветствие из защищенного режима. Сказано - сделано. С чего бы начать?.. Набраться знаний, конечно. Для этого очень хорошо полазить в исходниках Linux и Thix. Первая система всем хорошо знакома, вторая менее известна, но не менее полезна. Подучились? ... Понятно, что сперва надо написать загрузочный сектор для нашей мини-опрерационки (а ведь это именно мини-операционка). Поскольку процессор грузится в 16-разрядном режиме, то для созджания загрузочного сектора используется ассемблер и линковщик из пакета bin86. Можно, конечно, поискать еще что-нибудь, но оба наших примера используют именно его и мы тоже пойдет по стопам учителей. Синтаксис этого ассемблера немколько странноватый, совмещающий черты, характерные и для Intel и для AT&T (за подробностями направляйтесь в Linux-Assembly-HOWTO), но после пары недель мучений можно привыкнуть. 2. Загрузочный сектор (boot.S) Сознательно не буду приводить листингов программ. Так станут понятней основные идеи, да и вам будет намного приятней, если все напишите своими руками. Для начала определимся с основными константами. START_HEAD = 0 - Головка привода, которою будем использовать. START_TRACK = 0 - Дорожка, откуда начнем чтение. START_SECTOR = 2 - Сектор, начиная с которого будем считывать наше ядрышко. SYSSIZE = 10 - Размер ядра в секторах (каждый сектор содержит 512 байт) FLOPPY_ID = 0 - Идентификатор привода. 0 - для первого, 1 - для второго HEADS = 2 - Количество головок привода. SECTORS = 18 - Количество дорожек на дискете. Для формата 1.44 Mb это количество равно 18. В процессе загрузки будет происходить следующее. Загрузчик BIOS считает первый сектор дискеты, положит его по адресу 0000:0x7c00 и передаст туда управление. Мы его получим и для начала переместим себя пониже по адресу 0000:0x600, перейдем туда и спокойно продолжим работу. Собственно вся наша работа будет состоять из загрузки ядра (сектора 2 - 12 первой дорожки дискеты) по адресу 0x100:0000, переходу в защищенный режим и скачку на первые строки ядра. В связи с этим еще несколько констант: BOOTSEG = 0x7c00 - Сюда поместит загрузочный сектор BIOS. INITSEG = 0x600 - Сюда его переместим мы. SYSSEG = 0x100 - А здесь приятно расположится наше ядро. DATA_ARB = 0x92 - Определитель сегмента данных для дескриптора CODE_ARB = 0x9A - Определитель сегмента кода для дескриптора. Первым делом произведем перемещение самих себя в более приемлемое место. Code: cli xor ax, ax mov ss, ax mov sp, #BOOTSEG mov si, sp mov ds, ax mov es, ax sti cld mov di, #INITSEG mov cx, #0x100 repnz movsw jmpi go, #0 ; прыжок в новое местоположение загрузочного сектора на метку go Теперь необходимо настроить как следует сегменты для данных (es, ds) и для стека. Это конечно неприятно, что все приходится делать вручную, но что делать. Ведь нет никого в памяти компьютера, кроме нас и BIOS. Code: go: mov ax, #0xF0 mov ss, ax mov sp, ax ; Стек разместим как 0xF0:0xF0 = 0xFF0 mov ax, #0x60 ; Сегменты для данных ES и DS зададим в 0x60 mov ds, ax mov es, ax Наконец можно вывести победное приветствие. Пусть мир узнает, что мы смогли загрузиться. Поскольку у нас есть все-таки еще BIOS, воспользуемся готовой функцией 0x13 прерывания 0x10. Можно конечно презреть его и написать напрямую в видеопамять, но у нас каждый байт команды на счету, а байт таких всего 512. Потратим их лучше на что-нибудь более полезное. Code: mov cx,#18 mov bp,#boot_msg call write_message Функция write_message выгдядит следующим образом Code: write_message: push bx push ax push cx push dx push cx mov ah,#0x03 ; прочитаем текущее положение курсора, дабы не выводить сообщения где попало. xor bh,bh int 0x10 pop cx mov bx,#0x0007 ; Параметры выводимых символов : видеостраница 0, аттрибут 7 (серый на черном) mov ax,#0x1301 ; Выводим строку и сдвигаем курсор. int 0x10 pop dx pop cx pop ax pop bx ret А сообщение так Code: boot_msg: .byte 13,10 .ascii "Booting data ..." .byte 0 К этому времени на дисплее компьютера появится скромное "Booting data ..." . Это в принципе уже "Hello World", но давайте добьемся чуточку большего. Перейдем в защищенный режим и выведем этот "Hello" уже из программы написаной на C. Ядро 32-разрядное. Оно будет у нас размещаться отдельно от загрузочного сектора и собираться уже gcc и gas. Синтаксис ассемблера gas соответсвует требованиям AT&T, так что тут уже все проще. Но для начала нам нужно прочитать ядро. Опять воспользуемся готовой функцией 0x2 прерывания 0x13. Code: recalibrate: mov ah, #0 mov dl, #FLOPPY_ID int 0x13 ; производим переинициализацию дисковода. jc recalibrate call read_track ; вызов функции чтения ядра jnc next_work ; если во время чтения не произошло ничего плохого то работаем дальше bad_read: ; если чтение произошло неудачно то выводим сообщение об ошибке mov bp,#error_read_msg mov cx,7 call write_message inf1: jmp inf1 ; и уходим в бесконечный цикл. Теперь нас спасет только ручная перезагрузка Сама функция чтения предельно простая: долго и нудно заполняем параметры, а затем одним махом считываем ядро. Усложнения начнуться, когда ядро перестанет помещаться в 17 секторах ( то есть 8.5 kb), но это пока только в будущем, а пока вполне достаточно такого молниеносного чтения. Code: read_track: pusha push es push ds mov di, #SYSSEG ; Определяем mov es, di ; адрес буфера для данных xor bx, bx mov ch, #START_TRACK ;дорожка 0 mov cl, #START_SECTOR ;начиная с сектора 2 mov dl, #FLOPPY_ID mov dh, #START_HEAD mov ah, #2 mov al, #SYSSIZE ;считать 10 секторов int 0x13 pop ds pop es popa ret Вот и все. Ядро успешно прочитано и можно вывести еще одно радостное сообщение на экран. Code: next_work: call kill_motor ; останавливаем привод дисковода mov bp,#load_msg ; выводим сообщение mov cx,#4 call write_message Вот содержимое сообщения Code: load_msg: .ascii "done" .byte 0 А вот функция остановки двигателя привода. Code: kill_motor: push dx push ax mov dx,#0x3f2 xor al,al out dx,al pop ax pop dx ret На данный момент на экране выведено "Booting data ...done" и лампочка привода флоппи-дисков погашена. Все затихли и готовы к смертельному номеру - прыжку в защищенный режим. Для начала надо включить адресную линию A20. Это в точности означает, что мы будем использовать 32-разрядную адресацию к данным. Code: mov al, #0xD1 ; команда записи для 8042 out #0x64, al mov al, #0xDF ; включить A20 out #0x60, al Выведем предупреждающее сообщение, о том, что переходим в защищенный режим. Пусть все знают, какие мы важные. Code: protected_mode: mov bp,#loadp_msg mov cx,#25 call write_message (Сообщение: loadp_msg: .byte 13,10 .ascii "Go to protected mode..." .byte 0 ) Пока еще у нас жив BIOS, запомним позицию курсора и сохраним ее в известном месте ( 0000:0x8000 ). Ядро позже заберет все данные и будет их использовать для вывода на экран победного сообщения. Code: save_cursor: mov ah,#0x03 ; читаем текущую позицию курсора xor bh,bh int 0x10 seg cs mov [0x8000],dx ;сохраняем в специальном тайнике Теперь внимание, запрещаем прерывания (нечего отвлекаться во время такой работы) и загружаем таблицу дескрипторов Code: cli lgdt GDT_DESCRIPTOR ; загружаем описатель таблицы дескрипторов. У нас таблица дескрипторов состоит из трех описателей: Нулевой (всегда должен присутствовать), сегмента кода и сегмента данных Code: .align 4 .word 0 GDT_DESCRIPTOR: .word 3 * 8 - 1 ; размер таблицы дескрипторов .long 0x600 + GDT ; местоположение таблицы дескрипторов .align 2 GDT: .long 0, 0 ; Номер 0: пустой дескриптор .word 0xFFFF, 0 ; Номер 8: дескриптор кода .byte 0, CODE_ARB, 0xC0, 0 .word 0xFFFF, 0 ; Номер 0x10: дескриптор данных .byte 0, DATA_ARB, 0xCF, 0 Переход в защищенный режим может происходить минимум двумя способами, но обе ОС , выбранные нами для примера (Linux и Thix) используют для совместимости с 286 процессором команду lmsw. Мы будем действовать тем же способом Code: mov ax, #1 lmsw ax ; прощай реальный режим. Мы теперь находимся в защищенном режиме. jmpi 0x1000, 8 ; Затяжной прыжок на 32-разрядное ядро. Вот и вся работа загрузочного сектора - немало, но и немного. Теперь мы попрощаемся с ним и направимся к ядру. В конце ассемблерного файла полезно добавить следующую инструкцию. Code: .org 511 end_boot: .byte 0 В результате скомпилированный код будет занимать ровно 512 байт, что очень удобно для подготовки образа загрузочного диска. 3. Первые вздохи ядра (head.S) Ядро к сожалению опять начнется с ассемблерного кода. Но теперь его будет совсем немного. Мы собственно зададим правильные значения сегментов для данных (ES, DS, FS, GS). Записав туда значение соответствующего дескриптора данных. Code: cld cli movl $(__KERNEL_DS),%eax movl %ax,%ds movl %ax,%es movl %ax,%fs movl %ax,%gs Проверим, нормально ли включилась адресная линия A20 простым тестом записи. Обнулим для чистоты эксперимента регистр флагов. Code: xorl %eax,%eax 1: incl %eax movl %eax,0x000000 cmpl %eax,0x100000 je 1b pushl $0 popfl Вызовем долгожданную функцию, уже написанную на С. Code: call SYMBOL_NAME(start_my_kernel) И больше нам тут делать нечего. Code: inf: jmp inf 4. Поговорим на языке высокого уровня (start.c) Вот теперь мы вернулись к тому с чего начинали рассказ. Почти вернулись, потому что printf() теперь надо делать вручную. поскольку готовых прерываний уже нет, то будем использовать прямую запись в видеопамять. Для любопытных - почти весь код этой части , с незначительными изменениями, повзаимствован из части ядра Linux, осуществляющей распаковку (/arch/i386/boot/compressed/*). Для сборки вам потребуется дополнительно определить такие макросы как inb(), outb(), inb_p(), outb_p(). Готовые определения проще всего одолжить из любой версии Linux. Теперь, дабы не путаться со встроенными в glibc функциями, отменим их определение Code: #undef memcpy Зададим несколько своих Code: static void puts(const char *); static char *vidmem = (char *)0xb8000; /*адрес видеопамати*/ static int vidport; /*видеопорт*/ static int lines, cols; /*количество линий и строк на экран*/ static int curr_x,curr_y; /*текущее положение курсора */ И начнем, наконец, писать код на языке высокого уровня... правда с небольшими ассемблерными вставками. Code: /*функция перевода курсора в положение (x,y). Работа ведется через ввод/вывод в видеопорт*/ void gotoxy(int x, int y) { int pos; pos = (x + cols * y) * 2; outb_p(14, vidport); outb_p(0xff & (pos >> 9), vidport+1); outb_p(15, vidport); outb_p(0xff & (pos >> 1), vidport+1); } /*функция прокручивания экрана. Работает, используя прямую запись в видеопамять*/ static void scroll() { int i; memcpy ( vidmem, vidmem + cols * 2, ( lines - 1 ) * cols * 2 ); for ( i = ( lines - 1 ) * cols * 2; i < lines * cols * 2; i += 2 ) vidmem[i] = ' '; } /*функция вывода строки на экран*/ static void puts(const char *s) { int x,y; char c; x = curr_x; y = curr_y; while ( ( c = *s++ ) != '\0' ) { if ( c == '\n' ) { x = 0; if ( ++y >= lines ) { scroll(); y--; } } else { vidmem [ ( x + cols * y ) * 2 ] = c; if ( ++x >= cols ) { x = 0; if ( ++y >= lines ) { scroll(); y--; } } } } gotoxy(x,y); } /*функция копирования из одной области памяти в другую. Заместитель стандартной функции glibc */ void* memcpy(void* __dest, __const void* __src, unsigned int __n) { int i; char *d = (char *)__dest, *s = (char *)__src; for (i=0;i<__n;i++) d[i] = s[i]; } /*функция издающая долгий и протяжных звук. Использует только ввод/вывод в порты поэтому очень полезна для отладки*/ make_sound() { __asm__(" movb $0xB6, %al\n\t outb %al, $0x43\n\t movb $0x0D, %al\n\t outb %al, $0x42\n\t movb $0x11, %al\n\t outb %al, $0x42\n\t inb $0x61, %al\n\t orb $3, %al\n\t outb %al, $0x61\n\t "); } /*А вот и основная функция*/ int start_my_kernel() { /*задаются основные параметры */ vidmem = (char *) 0xb8000; vidport = 0x3d4; lines = 25; cols = 80; /*считывается предусмотрительно сохраненные координаты курсора*/ curr_x=*(unsigned char *)(0x8000); curr_y=*(unsigned char *)(0x8001); /*выводится строка*/ puts("done\n"); /*уходим в бесконечный цикл*/ while(1); } Вот и вывели мы этот "Hello World" на экран. Сколько проделано работы, а на экране только две строчки Booting data ...done Go to proteсted mode ...done Немного, но и немало. Закричала новая операционная система. Мир с радостью воспринял ее. Кто знает, может быть это новый Linux ... 5. Подготовка загрузочного образа (floppy.img) Итак, подготовим загрузочный образ нашей системки. Для начала соберем загрузочный сектор. as86 -0 -a -o boot.o boot.S ld86 -0 -s -o boot.img boot.o Обрежем 32 битный заголовок и получим таким образом чистый двоичный код. dd if=boot.img of=boot.bin bs=32 skip=1 Соберем ядро gcc -traditional -c head.S -o head.o gcc -O2 -DSTDC_HEADERS -c start.c При компоновке НЕ ЗАБУДБЬТЕ параметр "-T" он указывает относительно которого смещения вести расчеты, в нашем случае поскольку ядро грузится по адресy 0x1000, то и смещение соотетствующее ld -m elf_i386 -Ttext 0x1000 -e startup_32 head.o start.o -o head.img Очистим зерна от плевел, то есть чистый двоичный код от всеческих служебных заголовков и комментариев objcopy -O binary -R .note -R .comment -S head.img head.bin И соединяем воедино загрузочный сектор и ядро cat boot.bin head.bin >floppy.img Образ готов. Записываем на дискетку (заготовьте несколько для экспериментов, я прикончил три штуки) перезагружаем компьютер и наслаждаемся. cat floppy.img >/dev/fd0 6. Е-мое, что ж я сделал (...) Здорово, правда? Приятно почувствовать себя будущим Торвальдсом или кем-то еще. Красная линия намечена, можно смело идти вперед, дописывать и переписывать систему. Описанная процедура пока что едина для множества операционных систем, будь то UNIX или Windows. Что напишете Вы? ... не знает не кто. Ведь это будет Ваша система. Stanislav Ievlev, linux.ru.net