Форум АНТИЧАТ

Форум АНТИЧАТ (https://forum.antichat.xyz/index.php)
-   С/С++, C#, Delphi, .NET, Asm (https://forum.antichat.xyz/forumdisplay.php?f=24)
-   -   ОС с нуля (https://forum.antichat.xyz/showthread.php?t=68623)

z01b 28.04.2008 01:33

ОС с нуля
 
Оглавление

[01] - введение / основные сведения о ядре
[02] - организация работы с памятью
[03] - этапы загрузки различных ОС
[04] - создание bootsector'а
[05] - основы защищенного режима
[06] - шлюзы / виртуальный режим процессора 8086
[07] - исключения защищенного режима / микроядерные системы
[08] - файловые системы
[09] - чтение ext2fs
[10] - форматы файлов ELF и PE
[11] - процесс загрузки
[12] - определение количества памяти


***********************************************

z01b 28.04.2008 01:34

[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, но по отдельности совместимость вполне реализуема.

Прикладным программам абсолютно безразлично, как системные вызовы реализуются в ядре. Это облегчает для нас обеспечение совместимости с существующими системами.

В следующем выпуске мы поговорим про защищенный режим процессора, распределение памяти, менеджер задач и рассмотрим, как это сделано в существующих системах.

z01b 28.04.2008 01:41

[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 Не инициализированные данные. Так же участвует в свопинге;
* Сегмент стека. Так же участвует в свопинге.

Но, обычно, системы делят сегмент данных на две части: инициализированные данные и не инициализированные данные.

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

Очень интересный момент:
При выполнении программы операционная система делает следующие действия:

* Готовит для программы локальную таблицу дескрипторов;
* Готовит для программы каталог страниц, все страницы помечаются как не присутствующие в памяти.
* Все.

При передаче управления этой программе процессор генерирует исключение по отсутствию страницы, в котором нужная страница загружается из файла или инициализируется.

Еще один интересный момент:

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

z01b 28.04.2008 01:52

[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. Мне кажется, в этой системе все сделано достаточно понятно и просто.

z01b 28.04.2008 02:12

[04] - создание bootsector'а

Как я уже упоминал, boot sector загружается в память по адресу 0:7c00h и имеет длину 512 байт. Это не слишком много, поэтому возможности boot sector'a ограничиваются загрузкой какого либо вторичного загрузчика.

Наш boot sector, по образу и подобию linux, будет загружать в память два блока. Первым является тот самый вторичный загрузчик, у нас он, как и в linux, называется setup. Вторым является собственно ядро.

Этот boot sector служит для загрузки ядра с дискет, поэтому, на первых порах, он жестко привязан к диску "a:".

BIOS предоставляет возможность читать по нескольку секторов сразу, но не более чем до границы дорожки. Такая возможность, конечно, ускоряет чтение с диска, но представляет собой большие сложности в программировании, так как надо учитывать границы сегментов (в реальном режиме сегмент может быть не больше, чем 64к) и границы дорожек, получается достаточно хитрый алгоритм.

Я пошел немного другим путем. Я читаю с диска по секторам. Это, конечно, медленнее, но я думаю, что здесь скорость не очень критична. За то это гораздо проще и компактнее реализуется.


А теперь давайте разбираться, как это все работает.
Код:

%define SETUP_SEG 0x07e0
%define SETUP_SECTS 10

%define KERNEL_SEG      0x1000
%define KERNEL_SECTS 1000

Для начала описываем место и размер для каждого загружаемого блока.
Размеры пока произвольные, поскольку все остальное еще предстоит написать.
Код:

section .text
        BITS    16

        org    0x7c00

Как я уже говорил, boot sector загружается и запускается по адресу 0:7c00h Содержимое регистров при старте таково:

* cs содержит 0
* ip содержит 7с00h

Прерывания запрещены! Про содержание остальных регистров мне ничего не известно, если кто-то, что-то знает, напишите мне. Остальные регистры мы будем инициализировать самостоятельно.
Код:

entry_point:
        mov    ax, cs

        cli
        mov    ss, ax
        mov    sp, entry_point
        sti

        mov    ds, ax

Стек у нас будет располагаться перед программой, до служебной области BIOS еще остается порядка 30 килобайт, для стека больше чем достаточно. Прерывания изначально запрещены, но я все равно сделаю это самостоятельно, на всякий случай. и разрешу после установки стека. Никаких проблем это вызвать, по-моему, не должно.
Так же, нулевым значением, инициализируем сегментный регистр ds.
Код:

        ; Сохpаняем фоpму куpсоpа
        mov    ah, 3
        xor    bh, bh
        int    0x10

        push    cx

        ; отключаем куpсоp
        mov    ah, 1
        mov    ch, 0x20
        int    10h

Чтобы все было красиво и радовало глаз, мы на время чтения отключим курсор. Иначе он будет мелькать на экране. Чтобы его потом восстановить, как и был, мы сохраняем его форму в стеке.
Код:

        ; Загpужаем setup
        mov    ax, SETUP_SEG
        mov    es, ax

        mov    ax, 1
        mov    cx, SETUP_SECTS

        mov    si, load_setup_msg
        call    load_block

        call    outstring

        mov    si, complete_msg
        call    outstring

Загружаем первый блок (setup). Процедуру загрузки блока мы рассмотрим немного позже. А в остальном здесь, по-моему, все понятно.
Код:

        ; загpужаем ядpо.
        mov    ax, KERNEL_SEG
        mov    es, ax

        mov    ax, 1 + SETUP_SECTS
        mov    cx, KERNEL_SECTS

        mov    si, load_kernel_msg
        call    load_block

        call    outstring

        mov    si, complete_msg
        call    outstring

Загружаем второй блок (kernel). Здесь все в точности аналогично первому блоку.
Код:

        ; Восстанавливаем куpсоp
        pop    cx
        mov    ah, 1
        int    0x10

Восстанавливаем форму курсора.
Код:

        ; Пеpедаем упpавление на setup
        jmp    SETUP_SEG:0

На этом работа boot sector'а заканчивается. Дальним переходом мы передаем управление программе setup.

Далее располагаются функции.

; Загрузка блока
; cx - количество сектоpов
; ax - начальный сектоp
; es - указатедь на память
; si - loading message

Функция загрузки блока. Она же занимается выводом на экран процентного счетчика.
Код:

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.
Код:

        xor    dx, dx  ; dl - диск - 0!
Номер диска храниться в dl и устанавливается в 0 (это диск a:)
Код:

        shr    ax, 1
        rcl    dh, 1 ; номер головки

Младший бит частного определяет для нас номер головки. (0 или 1)
Код:

        mov    ch, al
        shl    ah, 4
        or      cl, ah ; количество доpожек

Оставшиеся биты частного определяют для нас номер цилиндра (или дорожки).
восемь младших бит номера хранятся в регистре ch, два старших бита номера хранятся в двух старших битах регистра cl.
Код:

.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

В случае ошибки чтения мы не будем возвращать из функции какие-либо результаты, а будем повторять чтение, пока оно не окажется успешным. Ведь в случае неуспешного чтения у нас все равно ничего не будет работать! Для верности мы, в случае сбоя, производим сброс устройства.
Код:

.read_ok:

        pop    cx
        pop    ax
        ret

Далее идет две интерфейсные функции, обеспечивающие вывод на экран строк и десятичных цифр. Ничего особенного они из себя не представляют а для вывода пользуются телетайпным прерыванием BIOS (ah = 0eh, int 10h), которое обеспечивает вывод одного символа с обработкой некоторых служебных кодов.
Код:

; Вывод ст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 включительно, случай с большим числом обрабатывается как переполнение и отображается как '##'.
Код:

; Вывод десятичных чисел от 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

Далее располагаются несколько служебных сообщений.
Код:

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 байт.
Код:

        dw      0aa55h
Последние два байта называются "Partition table signature", что не совсем корректно. Фактически эта сигнатура говорит BIOS'у о том, что этот сектор является загрузочным.

Этот boot sector, помимо того, что читает по секторам, отличается от линуксового еще и размещением в памяти. После загрузки он не перемещает себя в памяти, и работает по тому же адресу, по которому его загрузил BIOS. Так же setup загружается непосредственно следом за boot sector'ом, с адреса 7e00h, что в принципе не помешает ему работать в других адресах, если мы будем загружать наше ядро через LILO, например.

Скомпилированную версию boot sector'а вы можете найти в файловом архиве (секция "наработки").

Надеюсь, что я достаточно доходчиво объясняю, если кому-то что-то не понятно - пишите.

В следующем выпуске мы перейдем к программе setup и рассмотрим порядок перехода в защищенный режим. А заодно я более подробно расскажу про этот режим процессора.

z01b 28.04.2008 02:27

[05] - основы защищенного режима

История организации памяти.

Ранние модели процессоров от Intel имели 16 бит шины данных и 20 бит шины адреса. Это налагало определенные ограничения на адресацию памяти, ибо 16-бинтный регистр невозможно было использовать для адресации более чем 64 килобайт памяти. Чтобы обойти это препятствие разработчики предусмотрели сегментные регистры. Сегментный регистр хранит в себе старшие 16 бит адреса и для получения полного адреса к сегментному адресу прибавляется смещение в сегменте.

http://img212.imageshack.us/img212/66/16502738zx2.gif

Таким образом, стало возможным адресовать до 1 мегабайта памяти. Это же позволило делать программы, не настолько привязанными к памяти и упростило адресацию. Сегменты могут начинаться с любого адреса, кратного 16 байтам, эти 16-байтные блоки памяти получили название параграфов. Но это и создает определенные неудобства. Первое неудобство состоит в том, что на один адрес памяти указывает 4096 различных комбинаций сегмент/смещение. Второе неудобство состоит в том, что нет возможности ограничить программам доступ к тем или иным частям памяти, что в некоторых случаях может быть существенно!
Введение защищенного режима решило эти проблемы, но ради совместимости любой из современных процессоров может работать в реальном или виртуальном режиме процессора i8086.


Защита.

Для обеспечения надежной работы операционных систем и прикладных программ разработчики процессоров предусмотрели в них механизмы защиты. В процессорах фирмы Intel предусмотрено четыре уровня привилегий для программ и данных. Нулевой уровень считается наиболее привилегированным, третий уровень - наименее.

Так же в защищенном режиме совсем иначе работает механизм преобразования адресов. в сегментном регистре теперь хранится не старшие биты адреса, а селектор. селектор представляет из себя индекс в таблице дескрипторов. И кроме этого содержит в себе несколько служебных бит. Формат селектора такой:

http://img257.imageshack.us/img257/8186/31113869aa4.png

Поле 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 байт) в каждой. Формат дескриптора таков:

http://img329.imageshack.us/img329/1600/92223203yx5.gif

Сразу бросается в глаза очень странная организация дескриптора, но это связано с совместимостью с процессором i286, формат дескриптора в котором был таков:

http://img509.imageshack.us/img509/8577/70559176dc8.gif

Что же содержится в дескрипторе:

Базовый адрес - 32 бита (24 бита для i286). Определяет линейный адрес памяти, с которого начинается сегмент. В отличие от реального режима этот адрес может быть указан с точностью до байта.

Предел - 20 бит (16 бит для i286). Определяет размер сегмента (максимальный адрес, по которому может быть произведено обращение, это справедливо не всегда но об этом чуть позже). 20-битное поле может показаться не очень то большим для 32-х битного процессора, но это не так. Оно не всегда показывает размер в байтах. Но и об этом чуть позже.

Байт прав доступа:

http://img182.imageshack.us/img182/1...304935qu2.gif/

Бит P (present) - Указывает на присутствие сегмента в памяти. обращение к отсутствующему сегменту вызывает особый случай не присутствия сегмента в памяти.

Двух битное поле DPL определяет уровень привилегий сегмента. Про Уровни привилегий мы поговорим чуть позже.

Бит S (Segment)- Будучи установленным в 1, определяет сегмент памяти, к которому может быть получен доступ на чтение (запись) или выполнение.

Три бита Type - в зависимости от бита S определяет либо возможности чтения/записи, выполнения сегмента или определяет тип системных данных, хранимых в селекторе. Подробнее это выглядит так:

Если бит S установлен в 1, о поле Type делится на биты:

http://img329.imageshack.us/img329/1540/14646081le6.gif

Если сегмент расширяется вниз (это используется для стековых сегментов) то поле предела показывает адрес, выше которого допустима запись. ниже запись недопустима и вызовет нарушение пределов сегмента.

Бит А (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) о них мы поговорим позже, возможно в следующем выпуске.

Шестой байт дескриптора, помимо старших бит предела, содержит в себе несколько битовых полей.

http://img505.imageshack.us/img505/5215/97085370qw9.gif

Бит 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 шлюза.

z01b 28.04.2008 03:12

[06] - шлюзы / виртуальный режим процессора 8086

Шлюзы

В прошлом выпуске, когда я говорил о дескрипторах и дескрипторных таблицах я ни слова не упомянул о дескрипторной таблице прерываний (Interrupt description table или IDT). Эта таблица так же состоит из дескрипторов, но в отличии от LDT и GDT в этой таблице могут размечаться только шлюзы. В защищенном режиме все прерывания происходят через IDT. Традиционная таблица векторов прерываний здесь не используется.

Формат дескрипторов шлюзов отличается от дескриптора сегмента.
Для начала рассмотрим шлюз вызова.

http://img89.imageshack.us/img89/8879/41642218zz1.gif

В поле прав доступа задается уровень привилегий, который должен быть ниже CPL текущего процесса, бит присутствия и соответствующий тип в остальных полях.
Селектор и смещение задают адрес вызываемой функции, при этом селектор должен присутствовать либо в GDT либо в активной LDT.
Параметр "Количество слов стека" служит для передачи аргументов в вызываемую функцию, при этом соответствующее количество слов копируется из стека текущего уровня привилегий в стек уровня привилегий вызываемой функции. Это поле использует только младшие 5 бит четвертого байта. Остальные биты должны быть нулевыми.
Обращаться к такому шлюзу, если дескриптор не расположен в IDT, можно только командой call far, при этом указываемое в команде смещение игнорируется. А селектор должен указывать на дескриптор шлюза вызова.

Шлюз прерывания и шлюз ловушки имеют одинаковый формат, отличаются между собой типами в байте прав доступа. В отличии от шлюза вызова эти шлюзы не содержат в себе Количества слов стека, поскольку прерывания бывают аппаратными и передача в них параметров через стек - бессмысленна. Эти шлюзы используются обычно только в IDT.

Шлюз задачи содержит в себе значительно меньше информации.
Во втором и третьем байте дескриптора записывается селектор TSS (Сегмента состояния задачи). Поле прав доступа заполняется аналогично другим шлюзам, но с соответствующим типом. Остальные поля дескриптора не используются.
При вызове такого шлюза происходит переключение контекста задачи. При этом вызывающая задача блокируется и не может быть вызвана до тех пор, пока вызванная задача не вернет ей управление командой iret.

Про правила доступа к шлюзам я говорил в прошлом выпуске, и в этот раз я закончу на этом. в следующем выпуске расскажу про прерывания более подробно.


Виртуальный режим процессора 8086.


Для возможности запуска из защищенного режима программ, предназначенных для реального, существует так называемый "Виртуальный режим процессора 8086". При этом полноценно работают механизмы преобразования адресов защищенного режима. А так же многозадачные системы, которые могут одновременно выполнять как защищенные задачи, так и виртуальные. При этом адресация в виртуальной задаче осуществляется традиционным для 8086 методом - сегмент/смещение.

Обращение к прерываниям осуществляется через IDT, но таблица прерываний реального режима может быть обработана из функций, шлюзы которых размещаются в IDT.
Обращение виртуальной задачи к портам так же может быть отслежено через прерывания защищенного режима. При обращении к запрещенным портам происходит исключение.
При желании может быть обеспечена абсолютно прозрачная работа нескольких виртуальных задач в одной мультизадачной среде. Но мы этой возможностью не будем пользоваться, и в своей работе будем рассчитывать на программы исключительно защищенного режима.

z01b 28.04.2008 03:17

[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 достигается за счет передачи сообщений без промежуточного буферизирования в ядре. Либо непосредственным переписыванием процессу-получателю, либо с помощью маппинга страниц (если сообщения большого размера).

Менеджер памяти имеет как бы две стороны. Первая сторона - внутренняя, распределение памяти между приложениями, организация свопинга (который тоже осуществляется не ядром непосредственно, а специальной программой-сервером) никаким образом не интересует остальные программы. Но другая сторона - внешняя служит именно для них. Программы могут запросить у ядра во временное пользование некоторое количество памяти, которое ядро им обязательно предоставит (в разумных пределах... гигабайта два... не больше... :). Или же программы могут запросить у ядра какой-то определенный участок памяти. Это бывает необходимо программам-серверам. И это требование ядром также вполне может быть удовлетворено при условии, что никакая другая программа до того не забронировала этот участок памяти для себя.

z01b 28.04.2008 03:23

[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 и содержит в себе следующую информацию:
Код:

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 записями. Формат записи в директории таков:
Код:

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 содержит в себе информацию о файловой системе и имеет следующий формат:
Код:

struct ext2_super_block {
  u_int32  s_inodes_count;
  u_int32  s_blocks_count;
  u_int32  s_r_blocks_count;
  u_int32  s_free_blocks_count;
  u_int32  s_free_inodes_count;
  u_int32  s_first_data_block;
  u_int32  s_log_block_size;
  int32    s_log_frag_size;
  u_int32  s_blocks_per_group;
  u_int32  s_frags_per_group;
  u_int32  s_inodes_per_group;
  u_int32  s_mtime;
  u_int32  s_wtime;
  u_int16  s_mnt_count;
  u_int16  s_max_mnt_count;
  u_int16  s_magic;
  u_int16  s_state;
  u_int16  s_errors;
  u_int16  s_pad;
  u_int32  s_lastcheck;
  u_int32  s_checkinterval;
  u_int32  s_reserved[238];
};

Не буду описывать значение всех полей этой структуры, ограничусь основными. Размер блока файловой системы можно вычислить так: 1024 * s_log_block_size. Размер блока может быть 1, 2 или 4 килобайта размером.

Об остальных полях чуть попозже.
А теперь рассмотрим группы дескрипторов файловой системы.
Формат дескриптора группы таков:
Код:

struct ext2_group_desc {
  u_int32  bg_block_bitmap;
  u_int32  bg_inode_bitmap;
  u_int32  bg_inode_table;
  u_int16  bg_free_blocks_count;
  u_int16  bg_free_inodes_count;
  u_int16  bg_used_dirs_count;
  u_int16  bg_pad;
  u_int32  bg_reserved[3];
};

Содержимое группы таково:

* 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 таков:
Код:

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 и еще некоторые.

z01b 28.04.2008 03:44

[09] - чтение ext2fs

Чтение ext2fs

В прошлом выпуске я описывал структуру этой файловой системы. Как вы поняли, (я надеюсь) в файловой системе присутствует Super Block и дескрипторы групп. Эта информация хранится в начале раздела. Super Block во 2-м килобайте, дескрипторы групп - в третьем.
Стоит заметить, что первый килобайт для нужд файловой системы не используется и может быть целиком использован для boot sector'а (правда он уже будет не сектор, а килобайт :). Но для этого следует подгрузить второй сектор boot'а.
А для инициализации файловой системы нам нужно загрузить super block и дескрипторы групп, они же понадобятся нам для работы с файловой системой.
Это все можно загрузить одновременно, как мы и сделаем.
Код:

        mov    ax, 0x7e0
        mov    es, ax
        mov    ax, 1
        mov    cx, 5
        call    load_block

Для этого мы используем уже знакомую процедуру загрузки блока, но эта процедура станет значительно короче, потому что никаких процентов мы больше не будем выводить.
В es засылается адрес, следующий за загруженным загрузочным сектором (Загружается он, как мы помним, по адресу 7c00h, и имеет длину 200h байт, следовательно свободная память начинается с адреса 7e00h, а сегмент для этого адреса равен 7e0h). В ax засылается номер сектора с которого начинается блок (в нашем случае это первый сектор, загрузочный сектор является нулевым). в cx засылается длина загружаемых данных в секторах (1 - дополнительная часть boot sector'а, 2 - Super Block ext2, 2 - дескрипторы групп. Всего 5 секторов).

Теперь вызовем процедуру инициализации файловой системы. Эта процедура достаточно проста, и проверяет только соответствие magic номера файловой системы и вычисляет размеры блока для работы.
Код:

sb      equ    0x8000

ext2_init:
        pusha
        cmp    word [sb + ext2_sb.magic], 0xef53
        jz      short .right

        mov    si, bad_sb
        call    outstring

        popa
        stc
        ret

bad_sb: db 'Bad ext2 super block!', 0ah, 0dh, 0

В случае несоответствия magic номера происходит вывод сообщения об ошибке и выход из подпрограммы. Чтобы сигнализировать об ошибке используется бит C регистра flags.
Код:

.right:
        mov    ax, 1024
        mov    cl, [sb + ext2_sb.log_block_size]
        shl    ax, cl
        mov    [block_size], al        ; Размер блока в байтах
        shr    ax, 2
        mov    [block_dword_size], ax  ; Размер блока в dword
        shr    ax, 2
        mov    [block_seg_size], ax    ; Размер блока в параграфах
        shr    ax, 5
        mov    [block_sect_size], ax  ; Размер блока в секторах
        popa
        clc
        ret

block_size:            dw 1024
block_dword_size:      dw  256
block_seg_size:        dw 64
block_sect_size:        dw 2

Все эти значения нам понадобятся для работы. А теперь рассмотрим процедуру загрузки одного блока файловой системы.
Код:

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:
Код:

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 в группе.
Код:

        shl    ax, gd_bit_size
        mov    bx, ax
        mov    bx, [gd + bx + ext2_gd.inode_table]

ax умножаем на размер записи о группе (делается это сдвигом, но, по сути, то же самое умножение) и получаем смещение группы в таблице дескрипторов групп. gd - базовый адрес таблицы групп. Последняя операция извлекает из дескриптора группы адрес таблицы inode этой группы (адрес задается в блоках файловой системы) который у нас пока будет храниться в bx.
Код:

        mov    ax, dx
        shl    ax, inode_bit_size

Теперь разберемся с inode. Определим его смещение в таблице inode группы.
Код:

        xor    dx, dx
        div    word [block_size]
        add    ax, bx

Поделив это значение на размер блока мы получим номер блока относительно начала таблицы inode (ax), и смещение inode в блоке (dx). К номеру блока (bx) прибавим блок, в котором находится inode.
Код:

        mov    bx, tmp_block >> 4
        mov    es, bx
        call    ext2_load_block

Загрузим этот блок в память.
Код:

        push    ds
        pop    es

        mov    si, dx
        add    si, tmp_block
        mov    di, inode
        mov    cx, ext2_i_size >> 1
        rep    movsw

Восстановим содержимое сегментного регистра es и перепишем inode из блока в отведенное для него место.
Код:

        pop    es
        popa
        ret

Inode загружен. Теперь по нему можно загружать файл. Здесь все не столь однозначно. Процедура загрузки файла состоит из нескольких модулей. Потому что помимо прямых ссылок inode может содержать косвенные ссылки на блоки. В принципе можно ограничить возможности считывающей подпрограммы необходимым минимумом, полная поддержка обеспечивает загрузку файлов до 4 гигабайт размером. Естественно в реальном режиме мы такими файлами оперировать не сможем, да это и не нужно. Но сейчас мы рассмотрим полную поддержку:
Код:

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 килобайта). После окончания работы процедуры проверяем, все ли содержимое файла уже загружено или еще нет. Если нет, то загрузка продолжается по косвенной таблице блоков. Косвенная таблица - это отдельный блок в файловой системе, который содержит в себе таблицу блоков.
Код:

        mov    cx, 1
        call    idir_blocks

        cmp    ax, [inode + ext2_i.blocks]
        jz      short .exit

В inode только одна косвенная таблица первого уровня (cx=1). Для загрузки блоков из такой таблицы мы используем процедуру idir_blocks. Это позволяет нам, в зависимости от размера блока загрузить 268/1048/4144 килобайта файла. Если файл еще не загружен до конца, то используется косвенная таблица второго уровня.
Код:

        mov    cx, 1
        call    ddir_blocks

        cmp    ax, [inode + ext2_i.blocks]
        jz      short .exit

В inode также только одна косвенная таблица второго уровня (cx=1). Для загрузки блоков из такой таблицы мы используем процедуру ddir_blocks. Это позволяет нам, в зависимости от размера блока загрузить 64.2/513/4100 мегабайт файла. Если файл опять не загружен до конца (где же столько памяти взять??), то используется косвенная таблица третьего уровня. Ради этого мы уже не будем вызывать подпрограмм, а обработаем ее в этой процедуре.
Код:

        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 терабайта).
Код:

.exit:
        popa
        ret

Конечно, такие крайности нам при старте будут не к чему, с учетом, что мы находимся в реальном режиме и не можем адресовать больше ~600к памяти.
Кратко рассмотрю вспомогательные функции:
Код:

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 мегабайт. В дальнейшем эти ограничения мы конечно обойдем, а пока достаточно.
В этой функции стоит проверка количества загруженных блоков, для того чтобы вовремя выйти из процедуры считывания.
Код:

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, поскольку адреса блоков в ней дважды косвенны.

Но мы еще не рассмотрели самого главного. Процедуры, которая по пути файла может загрузить его с диска. Начнем.
Код:

ext2_load_file:
        pusha

        cmp    byte [si], '/'
        jnz    short .error_exit

Если путь файла не начинается со слэш, то это в данном случае является ошибкой. Мы не оперируем понятием текущий каталог!
Код:

        mov    ax, INODE_ROOT ; root_inode
        call    ext2_get_inode

Загружаем корневой inode - он имеет номер 2.
Код:

.cut_slash:
        cmp    byte [si], '/'
        jnz    short .by_inode

        inc    si
        jmp    short .cut_slash

Уберем лидирующий слэш... или несколько слэшей, такое не является ошибкой.
Код:

.by_inode:
        push    es
        call    ext2_load_inode
        pop    es

Загрузим содержимое файла. Директории, в том числе и корневая, являются такими же файлами, как и все остальные, только содержат в себе записи о находящихся в директории файлах.
Код:

        mov    ax, [inode + ext2_i.mode]
        and    ax, IMODE_MASK
        cmp    ax, IMODE_REG
        jnz    short .noreg_file

По inode установим тип файла.
Если файл не регулярный, то это может быть директорией. Это проконтролируем ниже.
Код:

        cmp    byte [si], 0
        jnz    short .error_exit

Если это файл, который нам надлежит скачать - то в [si] будет содержаться 0, означающий что мы обработали весь путь.
Код:

.ok_exit:
        clc
        jmp    short .exit

А поскольку содержимое файла уже загружено, то можем со спокойной совестью вернуть управление. Битом C сообщив, что все закончилось хорошо.
Код:

.noreg_file:
        cmp    ax, IMODE_DIR
        jnz    short .error_exit

Если этот inode не является директорией, то это или не поддерживаемый тип файла или ошибка в пути.
Код:

        mov    dx, [inode + ext2_i.size]
        xor    bx, bx

Если то, что мы загрузили, является директорией, то со смещения 0 (bx) в этом файле содержится список записей о файлах. Нам нужно выбрать среди них нужную. В dx сохраним длину файла, по ней будем определять коней директории.
Код:

.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. Если не совпадает - перейдем на следующую запись (чуть ниже)
Код:

        cmp    al, '/'
        jz      short .normal_path

        test    al, al
        jnz    short .notfind

Если совпал, то в пути после имени должно содержаться либо '/' либо 0 - символ конца строки. Если это не так, значит это не подходящий файл.
Код:

.normal_path:
        mov    ax, [es:bx + ext2_de.inode]
        call    ext2_get_inode

Загружаем очередной inode.
Код:

        add    si, [es:bx + ext2_de.name_len]
        cmp    byte [si], '/'
        jz      short .cut_slash
        jmp    short .by_inode

И переходим к его обработке. Это продолжается до тех пор, пока не пройдем весь путь.
Код:

.notfind:
        sub    dx, [es:bx + ext2_de.rec_len]
        add    bx, [es:bx + ext2_de.rec_len]

        test    dx, dx
        jnz    short .walk_dir

Если путь не совпадает, и если в директории еще есть записи - продолжаем проверку.
Код:

.error_exit:
        mov    si, bad_dir
        call    outstring
        stc

Иначе выводим сообщение об ошибке
Код:

.exit:
        popa
        ret

Вот и весь алгоритм. Не смотря на большой размер этого повествования, код занимает всего около 450 байт. А если убрать параноидальные функции, то и того меньше.

z01b 28.04.2008 03:52

[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 Безнаковое байтовое целое

Теперь рассмотрим заголовок файла:
Код:

#define EI_NIDENT 16

struct elf32_hdr {
  unsigned char e_ident[EI_NIDENT];
  Elf32_Half e_type;
  Elf32_Half e_machine;
  Elf32_Word e_version;
  Elf32_Addr e_entry;  /* Entry point */
  Elf32_Off e_phoff;
  Elf32_Off e_shoff;
  Elf32_Word e_flags;
  Elf32_Half e_ehsize;
  Elf32_Half e_phentsize;
  Elf32_Half e_phnum;
  Elf32_Half e_shentsize;
  Elf32_Half e_shnum;
  Elf32_Half e_shstrndx;
};

Массив e_ident содержит в себе информацию о системе и состоит из нескольких подполей.
Код:

struct {
  unsigned char ei_magic[4];
  unsigned char ei_class;
  unsigned char ei_data;
  unsigned char ei_version;
  unsigned char ei_pad[9];
}

ei_magic - постоянное значение для всех ELF файлов, равное { 0x7f, 'E', 'L', 'F'}
ei_class - класс ELF файла (1 - 32 бита, 2 - 64 бита который мы не рассматриваем)
ei_data - определяет порядок следования байт для данного файла (этот порядок зависит от платформы и может быть прямым (LSB или 1) или обратным (MSB или 2)) Для процессоров Intel допустимо только значение 1.
ei_version - достаточно бесполезное поле, и если не равно 1 (EV_CURRENT) то файл считается некорректным.
В поле ei_pad операционные системы хранят свою идентификационную информацию. Это поле может быть пустым. Для нас оно тоже не важно.

Поле заголовка e_type может содержать несколько значений, для выполняемых файлов оно должно быть ET_EXEC равное 2
e_machine - определяет процессор на котором может работать данный выполняемый файл (Для нас допустимо значение EM_386 равное 3)
Поле e_version соответствует полю ei_version из заголовка.
Поле e_entry определяет стартовый адрес программы, который перед стартом программы размещается в eip.
Поле e_phoff определяет смещение от начала файла, по которому располагается таблица программных секций, используемая для загрузки программ в память.
Не буду перечислять назначение всех полей, не все нужны для загрузки. Лишь еще два опишу.
Поле e_phentsize определяет размер записи в таблице программных секций.
И поле e_phnum определяет количество записей в таблице программных секций.

Таблица секций (не программных) используется для линковки программ. мы ее рассматривать не будем. Так же мы не будем рассматривать динамически линкуемые модули. Тема эта достаточно сложная, для первого знакомства не подходящая. :)

Теперь про программные секции. Формат записи таблицы программных секций таков:
Код:

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. Алгоритм схематический, не стоит рассматривать его как работающую программу.
Код:

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.
Структура его такая:
Код:

struct pe_hdr {
  unsigned long  pe_sign;
  unsigned short pe_cputype;
  unsigned short pe_objnum;
  unsigned long  pe_time;
  unsigned long  pe_cofftbl_off;
  unsigned long  pe_cofftbl_size;
  unsigned short pe_nthdr_size;
  unsigned short pe_flags;
  unsigned short pe_magic;
  unsigned short pe_link_ver;
  unsigned long  pe_code_size;
  unsigned long  pe_idata_size;
  unsigned long  pe_udata_size;
  unsigned long  pe_entry;
  unsigned long  pe_code_base;
  unsigned long  pe_data_base;
  unsigned long  pe_image_base;
  unsigned long  pe_obj_align;
  unsigned long  pe_file_align;

  // ... ну и еще много всякого, неважного.
};

Много всякого там находится. Достаточно сказать, что размер этого заголовка - 248 байт.
И главное что большинство из этих полей не используется. (Кто так строит?) Нет, они, конечно, имеют назначение, вполне известное, но моя тестовая программа, например, в полях pe_code_base, pe_code_size и тд содержит нули но при этом прекрасно работает. Напрашивается вывод, что загрузка файла осуществляется на основе таблицы объектов. Вот о ней то мы и поговорим.
Таблица объектов следует непосредственно после PE заголовка. Записи в этой таблице имеют следующий формат:
Код:

struct pe_ohdr {
  unsigned char o_name[8];
  unsigned long o_vsize;
  unsigned long o_vaddr;
  unsigned long o_psize;
  unsigned long o_poff;
  unsigned char o_reserved[12];
  unsigned long o_flags;
};

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.

Код:

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 не считается корректным.

z01b 28.04.2008 04:00

[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).
Код:

        mov ax, 0x7e0
        mov es, ax

Адрес 0:7e00h идентичен адресу 7e0h:0. Вторым вариантом мы и будем пользоваться, потому что наша процедура загрузки секторов размещает их по сегментному адресу, хранящемуся в es.
Код:

        mov ax, 1
В ax номер сектора, с которого начинается чтение (первый сектор является нулевым (каламбур :). И далее все зависит от файловой системы.
Код:

%ifdef EXT2FS
        mov cx, 5

Для EXT2 загружается 5 секторов - второй сектор бутсектора (1 сектор), суперблок файловой системы (2 сектора) и дескрипторы групп (2 сектора).
Код:

%elifdef FATFS
        mov cx, 10

Для FAT загружается 10 секторов - второй сектор бутсектора (1 сектор), таблица FAT - 9 секторов (такой размер она имеет на floppy дисках).
Код:

%else
  %error File system not specified
%endif
        call load_block

Все. первый пункт загрузки выполнен.

Функции обслуживания файловых систем имеют одинаковый интерфейс. Cобственно их всего две fs_init и fs_load_file. Естественно у них различаются реализации, но в процессе компиляции выбирается используемая файловая система. Для совместного использования нам никак не хватит одного килобайта, да и не за чем это.


Загрузка с диска файла сценария (конфигурации) загрузки.

Из-за сложности VFAT (FAT с длинными именами) он не реализован. Все имена на диске FAT должна иметь формат 8.3
В файловой системе FAT я не оперирую принятыми в MS системах именами дисков и при указании пути использую путь относительно корневой директории диска (как это делается в юникс системах).

Файл конфигурации у нас пока называется boot.rc и находится в каталоге /etc. Формат у этого файла достаточно нестрогий. Из-за нехватки места в boot секторе там сделана реакция только на ключевые слова, которыми являются:

* kern[el] - файл ядра;
* modu[le] - файл модуля;
* #end - конец файла конфигурации.

Использование этих слов в другом контексте недопустимо.

Предварительно проинициализировав файловую систему

call fs_init

Мы загружаем этот файл с диска.

mov si, boot_config
call fs_load_file

...

boot_config:
db '/etc/boot.rc', 0

Содержимое файла конфигурации такое:

kernel /boot/kernel
#end

Модулей у нас пока никаких нет, да и ядро еще в зачаточном состоянии. Но речь сейчас не об этом.


Загрузка с диска ядра и модулей.

Про этот момент я не буду особо расписывать, желающие могут посмотреть в исходниках, которые в скором времени появятся на сайте.
Скажу только, что программа анализирует файл конфигурации, в соответствии с ключевыми словами загружает ядро (которое может быть в единственном экземпляре) и любое количество модулей. Общий объем ядра и модулей ограничен свободным размером базовой памяти (около 600к).

Перейдем к предпоследнему пункту.


Переход в защищенный режим.

Бутсектор не особо беспокоится об организации памяти в системе - это забота ядра. Для перехода в защищенный режим он описывает всего два сегмента: сегмент кода и сегмент данных. оба сегмента имеют базовый адрес - 0 и предел в 4 гигабайта (это нам пригодиться для проверки наличия памяти).

Перед переходом в защищенный режим нам необходимо включить адресную линию A20. По моим сведениям этот механизм ввели в пору 286 для предотвращения несанкционированных обращений к памяти свыше одного мегабайта (непонятно зачем?). Но поскольку это имеет место быть - нам это нужно обрабатывать, иначе каждый второй мегабайт будет недоступен. Делается это почему-то через контроллер клавиатуры (еще одна загадка).
Код:

        mov al, 0xd1
        out 0x64, al
        mov al, 0xdf
        out 0x60, al

После этого можно переходить в защищенный режим.
Код:

        lgdt [gd_desc]
В регистр gdtr загружается дескриптор GDT.
Код:

        push byte 2
        popf

Очищается регистр флагов.
Код:

        mov eax, cr0
        or al, 1
        mov cr0, eax

Включается защищенный режим. Не смотря на то, что процессор уже находится в защищенном режиме, мы пока работаем в старых адресах. Чтобы от них уйти нам нужно сделать дальный переход в адреса защищенного режима.
Код:

        jmp 16:.epm

        BITS 32
 .epm:

16 в этом адресе перехода - это не сегмент. Это селектор сегмента кода.
Код:

        mov ax, 8
        mov ds, ax
        mov es, ax

        ; Ставим стек.
        mov ss, ax
        movzx esp, sp

После всего этого мы инициализируем сегментные регистры соответствующими селекторами, в том числе и сегмент стека, но указатель стека у нас не меняется, только теперь он становится 32-х битным.
Код:

        ...

gd_table:
        ; пеpвый дескpиптоp - данные и стек
        istruc descriptor
        at descriptor.limit_0_15, dw 0xffff
        at descriptor.base_0_15, dw 0
        at descriptor.base_16_23, db 0
        at descriptor.access,  db 0x92
        at descriptor.limit_16_19_a,  db 0xcf
        at descriptor.base_24_31, db 0
        iend

        ; втоpой дескpиптоp - код
        istruc descriptor
        at descriptor.limit_0_15, dw 0xffff
        at descriptor.base_0_15, dw 0
        at descriptor.base_16_23, db 0
        at descriptor.access,  db 0x9a ; 0x98
        at descriptor.limit_16_19_a,  db 0xcf
        at descriptor.base_24_31, db 0
        iend

Это GDT - Глобальная таблица дескрипторов. Здесь всего два дескриптора, но во избежание ошибок в адресации обычно вводится еще один дескриптор - нулевой, который не считается допустимым для использования. Мы не будем резервировать для него место специально, просто начало таблицы сместим на 8 байт выше.
Код:

gd_desc:
        dw 3 * descriptor_size - 1
        dd gd_table - descriptor_size

А это содержимое регистра GDTR. Здесь устанавливается предел и базовый адрес дескриптора. обратите внимание на базовый адрес, здесь происходит резервирование нулевого дескриптора.

Теперь процессор находится в защищенном режиме и уже не оперирует сегментами, а оперирует селекторами. Селекторов у нас всего три. Нулевой - недопустим. восьмой является селектором данных и шестнадцатый - селектором кода.

После этого управление можно передать ядру. дальше со всем этим будет разбираться оно.


Передача управления ядру.


Здесь вообще все просто. Когда мы загрузили ядро, в файле ядра мы определили адреса сегмента кода и сегмента данных. Не смотря на то, что ядро имеет вполне конкретные смещения в сегменте (которые задаются при компиляции), код инициализации ядра рассчитан на работу без привязки к адресам. Это нужно для определения количества памяти, после перевода ядра на свои адреса доступ ко всей памяти будет для ядра затруднен в связи с включением механизма страничного преобразования.
Итак, переходим к выполнению кода ядра.
Код:

        mov ebx, kernel_data
        mov eax, [ebx + module_struct.code_start]
        jmp eax

В этом фрагменте в eax записывается адрес начала кодового сегмента ядра.
Так как сегмент кода у нас занимает всю виртуальную память, нам не важно где находится ядро (хотя мы знаем, что оно было загружено в базовую память). Мы просто передаем ему управление.

z01b 28.04.2008 04:04

[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 - Количество возвращенных байт;
* буфер заполнен информацией;

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

Формат структуры таков:
Код:

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к).

Во избежание неприятностей память лучше не разрушать, а восстанавливать в первоначальном виде. делается это примерно так:
Код:

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). Формат этого блока таков:
Код:

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 байт длиной.

При освобождении блока, если предыдущий или последующий блоки пусты, он объединяется с ними в один блок и добавляется в список соответствующего размера. Использованные окружающие блоки удаляются из тех списков, в которых они были записаны ранее.

Для того, чтобы предотвратить попытку объединения первого блока памяти (при его освобождении) с предшествующим ему, перед первым блоком ставится параметр с флагом занятости. То же самое делается и для последнего блока памяти, но только после него.

z01b 28.04.2008 04:10

Файловый архив
 
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 Пост будет обновляться. "Горячие" файлы, буду выделять красным, остальные - оливковым.

z01b 28.04.2008 04:15

(C) Dron, 2001 г.

Статью я нашел на просторах народа и побоялся, как-бы она не потерялась. Решил выложить ее здесь. Над оформлением буду еще работать.
Статья не моя, но всетаки предлагаю супер-модератору, закрепить ее как важное.

z01b 28.04.2008 16:02

Цитата:

Сообщение от slesh
Статей довольно интересная. год назад еще читал, жаль что там почти не описано как работать по нормальному в защищенном режиме

В общем нарыл еще один интересный исходник ядра, для винды 9х, который работает в защищенном режиме.
Цитата:

;---------------------------------------------------;
; TINY OS KERNEL (C) BY ALEXEI A. FROUNZE ;
; 28th OF MAY, 2000 ;
; ;
; !!! ABSOLUTELY NO ANY WARRANTIES !!! ;
; ;
; ! AUTHOR CAN NOT BE RESPONSIBLE FOR ANY DAMAGE, ! ;
; ! DATA OR HEALTH/LIFE LOSS ! ;
; ;
; E-Mail: alexfru@chat.ru ;
; Homepage: http://www.chat.ru/~alexfru ;
; Mirror: http://members.xoom.com/alexfru ;
; ;
; FEATURES: ;
; - 32-BIT PROTECTED MODE PROGRAMMING WITH 386+ ;
; - EXCEPTIONS HANDLING ;
; - TIMER & KEYBOARD HARDWARE INTERRUPTS HANDLING ;
; - TASK SWITCHING (RING 0 AND RING 3) ;
; ;
; SYSTEM REQUIREMENTS: ;
; - 386 OR BETTER COMPUTER ;
; - EGA/VGA OR BETTER COLOR VIDEO SYSTEM ;
; - DOS 5.0 OR BETTER (WITHOUT ANY MEMORY ;
; MANAGERS SUCH AS EMM386.EXE) OR ;
; WINDOWS 9x LOADED IN COMMAND PROMPT ONLY MODE ;
;---------------------------------------------------;
[DOWNLOAD]

desTiny 29.04.2008 18:38

На инаттаке по-свежее статейка лежит:


Написание собственной Операционной Системы №1
Написание собственной Операционной Системы №2
Написание собственной Операционной Системы №3


PS А может собраться и AntichatOS накодить? :)

iv. 29.04.2008 18:57

Запосчу-ка я сюда же примерчик =)

The Real "Hello World"

1. Идея (hello.c)

Изучение нового языка программирования начинается, как правило, с написания простенькой программы, выводящей на экран краткое приветствие типа "Hello World!". Например, для C это будет выглядить приблизительно так.

Код:

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 - Определитель сегмента кода для дескриптора.

Первым делом произведем перемещение самих себя в более приемлемое место.

Код:

  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.

Код:

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. Потратим их лучше на что-нибудь более полезное.

Код:

  mov    cx,#18
  mov    bp,#boot_msg
  call    write_message

Функция write_message выгдядит следующим образом

Код:

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

А сообщение так

Код:

boot_msg:
                .byte 13,10
                .ascii "Booting data ..."
                .byte 0

К этому времени на дисплее компьютера появится скромное "Booting data ..." . Это в принципе уже "Hello World", но давайте добьемся чуточку большего. Перейдем в защищенный режим и выведем этот "Hello" уже из программы написаной на C.

Ядро 32-разрядное. Оно будет у нас размещаться отдельно от загрузочного сектора и собираться уже gcc и gas. Синтаксис ассемблера gas соответсвует требованиям AT&T, так что тут уже все проще. Но для начала нам нужно прочитать ядро. Опять воспользуемся готовой функцией 0x2 прерывания 0x13.

Код:

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), но это пока только в будущем, а пока вполне достаточно такого молниеносного чтения.

Код:

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

Вот и все. Ядро успешно прочитано и можно вывести еще одно радостное сообщение на экран.

Код:

next_work:
  call    kill_motor      ; останавливаем привод дисковода
  mov    bp,#load_msg    ; выводим сообщение
  mov    cx,#4
  call    write_message

Вот содержимое сообщения

Код:

load_msg:
  .ascii "done"
  .byte 0

А вот функция остановки двигателя привода.

Код:

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-разрядную адресацию к данным.

Код:

  mov    al, #0xD1      ; команда записи для 8042
  out    #0x64, al
  mov    al, #0xDF      ; включить A20
  out    #0x60, al

Выведем предупреждающее сообщение, о том, что переходим в защищенный режим. Пусть все знают, какие мы важные.

Код:

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 ). Ядро позже заберет все данные и будет их использовать для вывода на экран победного сообщения.

Код:

save_cursor:
  mov    ah,#0x03    ; читаем текущую позицию курсора
  xor    bh,bh
  int    0x10
  seg    cs
  mov    [0x8000],dx  ;сохраняем в специальном тайнике

Теперь внимание, запрещаем прерывания (нечего отвлекаться во время такой работы) и загружаем таблицу дескрипторов

Код:

  cli
  lgdt    GDT_DESCRIPTOR    ; загружаем описатель таблицы
                              дескрипторов.

У нас таблица дескрипторов состоит из трех описателей: Нулевой (всегда должен присутствовать), сегмента кода и сегмента данных

Код:

.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. Мы будем действовать тем же способом

Код:

  mov    ax, #1
  lmsw    ax          ; прощай реальный режим. Мы теперь
                        находимся в защищенном режиме.
  jmpi    0x1000, 8  ; Затяжной прыжок на 32-разрядное ядро.

Вот и вся работа загрузочного сектора - немало, но и немного. Теперь мы попрощаемся с ним и направимся к ядру.

В конце ассемблерного файла полезно добавить следующую инструкцию.

Код:

.org 511
end_boot:      .byte  0

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

3. Первые вздохи ядра (head.S)

Ядро к сожалению опять начнется с ассемблерного кода. Но теперь его будет совсем немного.

Мы собственно зададим правильные значения сегментов для данных (ES, DS, FS, GS). Записав туда значение соответствующего дескриптора данных.

Код:

  cld
  cli
  movl $(__KERNEL_DS),%eax
  movl %ax,%ds
  movl %ax,%es
  movl %ax,%fs
  movl %ax,%gs

Проверим, нормально ли включилась адресная линия A20 простым тестом записи. Обнулим для чистоты эксперимента регистр флагов.

Код:

    xorl %eax,%eax
1:  incl %eax
    movl %eax,0x000000
    cmpl %eax,0x100000
    je 1b
    pushl $0
    popfl

Вызовем долгожданную функцию, уже написанную на С.

Код:

  call SYMBOL_NAME(start_my_kernel)
И больше нам тут делать нечего.

Код:

inf:    jmp    inf
4. Поговорим на языке высокого уровня (start.c)

Вот теперь мы вернулись к тому с чего начинали рассказ. Почти вернулись, потому что printf() теперь надо делать вручную. поскольку готовых прерываний уже нет, то будем использовать прямую запись в видеопамять. Для любопытных - почти весь код этой части , с незначительными изменениями, повзаимствован из части ядра Linux, осуществляющей распаковку (/arch/i386/boot/compressed/*). Для сборки вам потребуется дополнительно определить такие макросы как inb(), outb(), inb_p(), outb_p(). Готовые определения проще всего одолжить из любой версии Linux.

Теперь, дабы не путаться со встроенными в glibc функциями, отменим их определение

Код:

#undef memcpy
Зададим несколько своих

Код:

static void puts(const char *);
static char *vidmem = (char *)0xb8000; /*адрес видеопамати*/
static int vidport;                    /*видеопорт*/
static int lines, cols;                /*количество линий и строк на экран*/
static int curr_x,curr_y;              /*текущее положение курсора */

И начнем, наконец, писать код на языке высокого уровня... правда с небольшими ассемблерными вставками.

Код:

/*функция перевода курсора в положение (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

De-visible 29.04.2008 19:51

Цитата:

Сообщение от z01b
(C) Dron, 2001 г.

Статью я нашел на просторах народа и побоялся, как-бы она не потерялась. Решил выложить ее здесь. Над оформлением буду еще работать.
Статья не моя, но всетаки предлагаю супер-модератору, закрепить ее как важное.

Цитата:

к сожалению, это было написано ранее и на специализированных форумах
Для новичков, да и не только будет полезно....

Jes 30.04.2008 21:00

Пишем игрушечную ОС

_Great_ 01.05.2008 23:48

статья в первых постах - нечто странное из серии "галопам по европам"..

GlOFF 01.05.2008 23:49

Цитата:

Сообщение от _Great_
статья в первых постах - нечто странное из серии "галопам по европам"..

Возможно, можешь что-то толковое предложить? :)

_Great_ 02.05.2008 07:53

могу - нормально изучить основы работы ОС, например Windows (такие книжки пишут минимум на 500 страниц) и изучить процессор Intel в защищенном режиме (то, что здесь расписано на страницу, в статьях на васме занимает несколько статей).
только тогда можно собраться писать ОС.

http://wasm.ru/article.php?article=pipm01
http://wasm.ru/article.php?article=pipm02
http://wasm.ru/article.php?article=pipm03
http://wasm.ru/article.php?article=pipm04
http://wasm.ru/article.php?article=pipm05
http://wasm.ru/article.php?article=pipm06
http://wasm.ru/article.php?article=pipm07
http://wasm.ru/article.php?article=pipm08
http://wasm.ru/article.php?article=pipm09
http://wasm.ru/article.php?article=pipm10
http://wasm.ru/article.php?article=pipm11
http://wasm.ru/article.php?article=pipm12
http://wasm.ru/article.php?article=pipm13

+ просите за саморекламу, моя статья в продолжение этого цикла:
http://wasm.ru/article.php?article=ia32int

М.Руссинович, Д.Соломон "Внутреннее устройство Microsoft Windows 2003/XP/2000"
http://rapidshare.com/files/27758180/vnutrennee_ustroystvo_microsoft_windows.rar
Пароль: www.booklandija.ru
http://rapidshare.com/files/27758180/vnutrennee_ustroystvo_microsoft_windows.rar

desTiny 02.05.2008 09:56

Цитата:

Сообщение от _Great_
могу - нормально изучить основы работы ОС, например Windows (такие книжки пишут минимум на 500 страниц) и изучить процессор Intel в защищенном режиме (то, что здесь расписано на страницу, в статьях на васме занимает несколько статей).
только тогда можно собраться писать ОС.

http://wasm.ru/article.php?article=pipm01
http://wasm.ru/article.php?article=pipm02
http://wasm.ru/article.php?article=pipm03
http://wasm.ru/article.php?article=pipm04
http://wasm.ru/article.php?article=pipm05
http://wasm.ru/article.php?article=pipm06
http://wasm.ru/article.php?article=pipm07
http://wasm.ru/article.php?article=pipm08
http://wasm.ru/article.php?article=pipm09
http://wasm.ru/article.php?article=pipm10
http://wasm.ru/article.php?article=pipm11
http://wasm.ru/article.php?article=pipm12
http://wasm.ru/article.php?article=pipm13

+ просите за саморекламу, моя статья в продолжение этого цикла:
http://wasm.ru/article.php?article=ia32int

М.Руссинович, Д.Соломон "Внутреннее устройство Microsoft Windows 2003/XP/2000"
http://rapidshare.com/files/27758180/vnutrennee_ustroystvo_microsoft_windows.rar
Пароль: www.booklandija.ru
http://rapidshare.com/files/27758180/vnutrennee_ustroystvo_microsoft_windows.rar

Можно ещё добавить
Свен Шрайбер "Недокументированные возможности Windows 2000"

_Great_ 02.05.2008 10:28

можно добавить много чего но к проектированию ОС это малое отношение имеет =) Всетаки нужно сначала определиться с структурой ядра, а тут неплохо было бы изучить руссиновича про ядро Windows и изучить ядро Linux (тут уже книги посоветовать не могу, ибо сам не знаю и не изучал.. надо будет исправить)

xcedz 02.05.2008 10:39

ядро linux можно почитать

разработка ядра в линукс

азбука ядра

GlOFF 02.05.2008 12:26

_Great_ Спс. за инфу.

Есть у кого-нибудь файлы к книге - Свен Шрайбер "Недокументированные возможности Windows 2000" ???

xcedz 02.05.2008 12:51

http://book.opensourceproject.org.cn/kernel/linux011/

_Great_ 02.05.2008 20:28

Цитата:

Сообщение от GlOFF
_Great_ Спс. за инфу.

Есть у кого-нибудь файлы к книге - Свен Шрайбер "Недокументированные возможности Windows 2000" ???

http://gr8.cih.ms/uploads/schreiber.zip

не помню откуда качал, поэтому залил скачанное

taha 02.05.2008 21:18

Вы пытаетесь собрать то, что давно уже собрано на вот этом сайте:
http://www.sysbin.com/
System Coders Zone

v4567 26.05.2008 20:07

Прочитал данную статью автора z01b видно что человек очень хорошо разбирается в данной теме.
z01b, у меня ктебе есть вопрос, помоги разобраться с загрузчиком. Написал свой загрузчик но он почемуто не работает.
Загрузчик записываю в первый сектор нулевого цилиндра на дискете, загружаюсь с неё. Далее информация с дискеты читается на аппаратном уровне, на уровне биоса, тоесть никакой файловой системы на ней нет, а информацию с неё читаю используя 13 прерывание и 2 функцию биоса, тоесть по секторам, цилиндрам и головкам.
Данная программа читает информацию с шести цилиндров и передаёт управление дальним прыжком в другой сегмент оперативной памяти с нулевым смещением, куда и были считаны данные.
Далее привожу текст программы:
Код:

;                          zagr.asm - Загрузчик.


cseg segment para 'code'

        org 100h

begin proc near

        assume cs:cseg, ds:cseg, ss:cseg

        org 7c00h
        jmp start
start:
       
        cli
        push ax
        push dx
        push cx
        push bx
        push es
        mov ax,cs
        mov ds,ax

;--------------Сброс дисковой системы.--------------------

        mov cx,3d
pov:
        mov ah,0h
        mov dl,0h
        int 13h
        cmp ah,0h
        jz povtor1
        loop pov
        pop es
        pop bx
        pop cx
        pop dx
        pop ax
        sti

;--------------Чтение с дискеты.--------------------------

povtor1:
        mov ax,1117h
        mov es,ax
        mov ch,0h
        mov ah,2h
        mov al,8d
        mov cl,2h
        mov dh,0h
        mov dl,0h
        mov bx,0h
        int 13h
gdem:
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        cmp ah,0h
        jz pr
        jmp gdem
pr:
        cmp al,8h
        jz pr1
        jmp gdem
pr1:
        mov ch,0h
pr4:
        mov ah,2h
        mov al,9d
        mov cl,1h
        not dh
        mov dl,0h
        int 13h
gdem1:
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        nop
        cmp ah,0h
        jz pr2
        jmp gdem1
pr2:
        cmp al,9h
        jz pr3
        jmp gdem1
pr3:
        cmp [zil],0h
        jz pr6
        jmp pr7
pr6:
        not [zil]
        jmp pr4
pr7:
        not [zil]
        inc ch
        cmp ch,6d
        jz pr5
        jmp pr4
pr5:
        pop es
        pop bx
        pop cx
        pop dx
        pop ax
        jmp dword ptr [perex]
       
perex dd 11170000h
zil db 0h

begin endp
cseg ends
end begin

Данный загрузчик не работает, z01b если ты видешь где у меня ошибки или знаешь в чём проблема помоги пожалуйста, за ответ заранее благодарен.

De-visible 26.05.2008 20:53

Автор не z01b!
Там есть копирайты если что...

v4567 26.05.2008 22:08

De-visible, если знаешь где у меня ошибка в программе или есть информация по даному вопросу или исходные тексты аналогичного загрузчика, помоги мне пожалуйста.

z01b 03.07.2008 01:49

Сегодня нарыл еще одну интересную статейку по сабжу, кому интересно, может скачать и читать.
http://wasm.ru/forum/attachment.php?item=2197

0verbreaK 03.07.2008 03:20

Цитата:

Сообщение от z01b
Сегодня нарыл еще одну интересную статейку по сабжу, кому интересно, может скачать и читать.
http://wasm.ru/forum/attachment.php?item=2197

ссылку на тему плз

z01b 03.07.2008 13:44

Цитата:

Сообщение от 0verbreaK
ссылку на тему плз

Вот перезалил http://slil.ru/25953519

Flame of Soul 03.07.2008 15:30

v4567 ответ по вопросу
 
v4567 - некогда проверять

интересен только один вопрос:
Сброс дисковой системы - то есть у тебя файловой системы там нет как таковой и соответственно компилируемый файл *.bin на выходе.
Вопрос собственно такой - А ты правильно пишешь бинарник на флопик без файловой системы?


Ниже просто приведу листинг рабочего загрузчика , программы записи бинарника на дискету!

Полная версия Мини ОС[ скачать ]
Код HTML:

для установки вставить дискетку и запустить батник - он там один! листинги имеются
PS: ниже даны листинги. Прошу прощения за множество копирайтов в этой версии просто это единственный экземпляр сохранившийся с тех времен когда я занималась данной темой, более поздние версии этой глупости невыжили.
PHP код:

name "loader"
#make_boot#
org 7c00h

пропустите данные и секцию декларации функции:
jmp start

секция данных
msg  db 
"welcome to MINI -os"0Dh,0Ah,
     
db "loading...",0Dh,0Ah0
err  db 
"invalid data at sector: 2, cylinder: 0, head: 0 - integrity check failed."0Dh,0Ah
     db 
"refer to tutorial 11 - making your own operating system."0Dh,0Ah
     db 
"system will reboot now... press any key..."0

start
:
push    cs
pop     ds

инициализируем стек:
mov     ax07c0h
mov     ss
ax
mov     sp
03feh вершина стека

set data segment:
push    cs
pop     ds

устанавливаем видео режим 80x25:
mov     ah00h
mov     al
03h
int     10h

выдаем сообщение приветствия:
lea     simsg
call    print_string

bios передает номер диска в dl:

mov     ah02h
mov     al
10  чтение секторов
mov     ch
0   цилиндр
mov     cl
2   сектор
mov     dh
0   головка
dl не меняется! - номер диска

буфер данных:
mov     bx0800h
mov     es
bx
mov     bx
0

чтение!
int     13h

проверка целостности:
cmp     es:[0000],0E9h  первый байт ядра должен быть 0E9 (jmp).
je     integrity_check_ok

ошибкапри проверки целостности
lea     si
err
call    print_string

ожидаем нажатия клавиши
mov     ah
0
int     16h

храним значение в 0040h:0072h:
;   
0000h холодная загрузка
;   1234h перезапуск из памяти
mov     ax
0040h
mov     ds
ax
mov     w
.[0072h], 0000h ;холодная загрузка
jmp    0ffffh
:0000h      ;reboot!

integrity_check_ok:
передача управления на ядро:
jmp     0800h:0000h

print_string proc near
push    ax
push    si
next_char
:
mov     al, [si]
cmp     al0
jz      printed
inc     si
mov     ah
0eh
int     10h
jmp     next_char
printed
:
pop     si
pop     ax
ret
print_string endp 

Запись бинарника на дискетку

PHP код:

name "writebin"
org  100h

jmp st

filename    db 128 dup
(0)
buffer      db 512 dup (0)
buffer_size =  $ - offset buffer
handle      dw 0
kernel_flag db 0

counter dw 0

sect  db 1
cyld  db 0
head  db 0
drive db 0

st
mov axcs
mov ds
ax
mov es
ax

call clear_screen

cp
: xor cxcx
mov cl
, [80h]
jcxz np
mov si
82h
mov di
offset filename
cld
rep movsb
mov 
[di-1], 0

cld
xor cxcx
mov cl
, [80h]
mov dioffset filename
mov al
'/'
repne scasb
jz  fs
jmp boot
fs
cmp [di-2], ' '
jne nsp
mov 
[di-2], 0
nsp
:mov [di-1], 0
or  [di], 0010_0000b
cmp 
[di], 'k'
jne wp
or kernel_flag1
mov sect
2

lea dx
s0
mov ah
9
int 21h
jmp s0s
s0 db 0Dh
,0Ah,"[/k] - start from sector: 2 " 0Dh,0Ah'$'
s0s:
jmp of

boot
:
lea dxs3
mov ah
9
int 21h
jmp s3s
s3 db 0Dh
,0Ah," boot record "0Dh,0Ah'$'
s3s:


open file
of
mov ah3dh
mov al
0
mov dx
offset filename
int 21h
jc  co
mov handle
ax


lea dx
m0
mov ah
9
int 21h
jmp m0m
m0 db 
"opened: $"
m0m:
mov sioffset filename
call print_string


read bytes from file
rd
mov ah3fh
mov bx
handle
mov cx
buffer_size
mov dx
offset buffer
int 21h
jc er

cmp ax
0  no bytes left?
jz  cf


write bytes to disk
wr
mov ah03h
mov al
write 1 sector (512 bytes).
mov clsect  sector (1..18)
mov chcyld  cylinder (0..79)
mov dhhead  head  (0..1)
mov dldrive always 0 (A:)
mov bxoffset buffer
int 13h
jc er

inc counter

set cursor at 2,5
mov ah
2
mov dh
5
mov bh
0
int 10h

show current progress:
lea dxs1
mov ah
9
int 21h
jmp s1s
s1 db 1Dh
,0Ah,"writing: $"
s1s:
mov axcounter
call print_num_uns




sectors... cylinders... heads...
cmp kernel_flag1
jne cf
inc sect
cmp sect
18
jbe rd           
; ^
mov sect1
inc cyld
cmp cyld
79
jbe rd           
; ^
mov cyld0
inc head
cmp head
1
jbe rd           
; ^



close file
cf
mov bxhandle
mov ah
3eh
int 21h
jc er

jmp ex


co
lea dxe0
mov ah
9
int 21h
jmp e0e
e0 db 
"  cannot open the file...",0Dh,0Ah,'$'
e0e:
jmp ex

np
lea dxe1
mov ah
9
int 21h
jmp e1e
e1 db 
"  no parameters...",0Dh,0Ah
db  
"==============================================================================",0Dh,0Ah
db 
"this program was designed especially to test VeHG -operating system",0Dh,0Ah
db 
"if you are emulating in emu8086, click file->set command line parameters.",0Dh,0Ah
db 
"from command prompt type:"0Dh,0Ah
db 
"                           writebin [filename] [/k]",0Dh,0Ah,
db  "when /k parameter is specified file is written to second sector (kernel area).",0Dh,0Ah,
db  "==============================================================================",0Dh,0Ah,'$'
e1ejmp ex


er
lea dxe2
mov ah
9
int 21h
jmp e2e
e2 db 
"   i/o error...",0Dh,0Ah,'$'
e2e:
jmp ex

wp
lea dxe3
mov ah
9
int 21h
jmp e3e
e3 db 
"   wrong parameter. only [/k] is supported. use 8.3 short file names only.",0Dh,0Ah,'$'
e3e:


exlea dxm1
mov ah
9
int 21h
jmp m1m
m1 db 0Dh
,0Ah,"   total sectors: $"
m1m:
mov axcounter
call print_num_uns

lea dx
m2
mov ah
9
int 21h
jmp m2m
m2 db 0Dh
,0Ah"thank your for using writebin!   - VeHG.com - 2008 (c) freeware",0Dh,0Ah
db 
" press any key...",0Dh,0Ah'$'
m2m:


wait for any key...
mov ah0
int 16h


ret        
; exit.



this macro prints a char in AL and advances
the current cursor position:
PUTC    MACRO   char
PUSH    AX
MOV     AL
char
MOV     AH
0Eh
INT     10h
POP     AX
PUTC    ENDM


prints out an unsigned value of AX register.
allowed values from 0 to 65535
print_num_uns   proc    near
push    ax
push    bx
push    cx
push    dx
flag to prevent printing zeros before number:
mov     cx1
; (result of "/ 10000" is always less or equal to 9).
mov     bx10000       2710h divider.
ax is zero?
cmp     ax0
jz      print_zero
begin_print
:
check divider (if zero go to end_print):
cmp     bx,0
jz      end_print
avoid printing zeros before number:
cmp     cx0
je      calc
; if ax<bx then result of div will be zero:
cmp     axbx
jb      skip
calc
:
mov     cx0   set flag.
mov     dx0
div     bx      
ax dx:ax bx   (dx=remainder).
; print 
last digit
ah is always zeroso it's ignored
add     al, 30h    ; convert to ascii code.
putc    al
mov     ax, dx  ; get remainder from last div.
skip:
; calculate bx=bx/10
push    ax
mov     dx, 0
mov     ax, bx
div     cs:ten  ; ax = dx:ax / 10   (dx=remainder).
mov     bx, ax
pop     ax
jmp     begin_print
print_zero:
putc    '
0'
end_print:
pop     dx
pop     cx
pop     bx
pop     ax
ret
ten             dw      10      ; used as divider.
print_num_uns   endp





; print null terminated string at ds:si
print_string proc near
push    ax      ; store registers...
push    si      ;

nxtch:  mov     al, [si]
cmp     al, 0
jz      printed
inc     si
mov     ah, 0eh ; teletype function.
int     10h
jmp     nxtch
printed:

pop     si      ; re-store registers...
pop     ax      ;

ret
print_string endp




clear_screen proc near
push    ax      ; store registers...
push    ds      ;
push    bx      ;
push    cx      ;
push    di      ;

mov     ax, 40h
mov     ds, ax  ; for getting screen parameters.
mov     ah, 06h ; scroll up function id.
mov     al, 0   ; scroll all lines!
mov     bh, 07  ; attribute for new lines.
mov     ch, 0   ; upper row.
mov     cl, 0   ; upper col.
mov     di, 84h ; rows on screen -1,
mov     dh, [di] ; lower row (byte).
mov     di, 4ah ; columns on screen,
mov     dl, [di]
dec     dl      ; lower col.
int     10h

; set cursor position to top
; of the screen:
mov     bh, 0   ; current page.
mov     dl, 0   ; col.
mov     dh, 0   ; row.
mov     ah, 02
int     10h

pop     di
pop     cx
pop     bx
pop     ds
pop     ax

ret
clear_screen endp 



Время: 04:20