Создание операционной системы на ассемблере

         

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


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

; Загрузка сектора ; ax - номеp сектоpа (0...max (2880)) ; es:bx - адpес для pазмещения сектоpа.

Абсолютный номе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 и рассмотрим порядок перехода в защищенный режим. А заодно я более подробно расскажу про этот режим процессора.

Отправлено 2001-07-27 для 3016 подписчиков.
ведущий рассылки Dron
Архив Рассылки

При поддержке Kalashnikoff.ru



Чтение 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 байт. А если убрать параноидальные функции, то и того меньше. Не стоит пытаться откомпилировать этот код, все эти модули вы сможете найти на нашем сайте, ссылка на который приведена ниже. Здесь я все это привел для того чтобы объяснить как и что. Надеюсь у меня это получается хоть как-то. Если кто-то что-то не понимает - пишите мне, мой адрес вы всегда можете найти чуть ниже.

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

И наберитесь немного терпения... скоро мы начнем писать ядро.

Отправлено 2001-09-06 для 4463 подписчиков.
ведущий рассылки Dron
Сайт проекта
Архив Рассылки

При поддержке Kalashnikoff.ru



Что из всего этого следует?


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

Ориентироваться мы будем на i386 или более старшие модели процессоров, вплоть до последних.

Ядро системы при распределении памяти оперирует 4-х килобайтными страницами.
Страницы могут использоваться самим ядром, для нужд драйверов (кэширование, например), или для процессов.

Программа или процесс состоит из следующих частей:

Сегмент кода. Может только выполняться, сама программа его не прочитать, не переписать не может! Использовать для этого сегмента swap не нужно, при необходимости код считывается прямо из файла; Сегмент данных состоит из трех частей:

Константные данные, их тоже можно загружать из файла, так как они не меняются при работе программы; Инициализированные данные. Участвует в процессе свопинга; Не инициализированные данные. Так же участвует в свопинге;

Сегмент стека. Так же участвует в свопинге.

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

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

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

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

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



Дескрипторы сегментов.


Дескрипторные таблицы состоят из записей по 64 бита (8 байт) в каждой. Формат дескриптора таков:

7 6 5 4
Базовый адрес 31-24 Предел 19-16 Права доступа Базовый адрес 23-16
3 2 1 0
Базовый адрес 15-0 Предел 15-0

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

7 6 5 4
Зарезервировано Права доступа Базовый адрес 23-16
3 2 1 0
Базовый адрес 15-0 Предел 15-0

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

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

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

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

7 6 5 4 3 2 1 0
P DPL S Type A

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

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

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

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

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

2 1 0
1 - код Подчиненный сегмент кода Допустимо считывание
0 - данные Расширяется вниз Допустима запись

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




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

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

7 6 5 4 3 2 1 0
G D 0 U Предел 19-16
Бит G (Granularity) - определяет размер элементов, в которых измеряется предел. если 0 - предел в байтах, если 1 - размер в страницах.

Бит D (Default size) - размер операндов в сегменте. Если 0 - 16 бит. если 1 - 32 бита.

Бит U (User) - доступен для пользователя (вернее для программиста операционной системы)


Динамическое распределение памяти.


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

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

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

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

Отправлено 2002-02-15 для 6542 подписчиков.
ведущий рассылки Dron
Сайт проекта
Архив Рассылки

При поддержке Kalashnikoff.ru



Для начала разберемся, как устроены системы.


Ядро состоит из следующих компонентов:

"Собственно ядро" Драйвера устройств Системные вызовы

В зависимости от организации внутренних взаимодействий, ядра подразделяются на "микроядра" (microkernel) и монолитные ядра.
Системы с "микроядром" строятся по модульному принципу, имеют обособленное ядро, и механизм взаимодействия между драйверами устройств и процессами. По такому принципу строятся системы реального времени. Примерно так сделан QNX или HURD.
Монолитное ядро имеет более жесткую внутреннюю структуру. Все установленные драйвера жестко связываются между собой, обычно прямыми вызовами. По таким принципам строятся обыкновенные операционные системы типа Linux, FreeBSD.
Естественно, не все так четко, идеального монолитного или "микроядра" нет, наверное, ни в одной системе, просто системы приближаются к тому или иному типу ядра.

Мне бы очень хотелось, чтобы то, что мы будем делать, больше походило на первый тип ядер.



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

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

Отправлено 2001-08-24 для 4145 подписчиков.
ведущий рассылки Dron
Сайт проекта
Архив Рассылки

При поддержке Kalashnikoff.ru



Файловые системы на базе 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 - нельзя. Но обо всем по порядку.


Формат 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 определяет тип доступа к секциям в памяти. Некоторые секции допускается выполнять, некоторые записывать. Для чтения в существующих системах доступны все.


Формат 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 - можно писать

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


И снова защита.


Немного терминологии:

Уровень привилегий может быть от 0(высший) до 3(низший). Следовательно повышение уровня привилегий соответствует его уменьшению в численном эквиваленте, понижение - наоборот.

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

В селекторе содержится RPL - то есть запрашиваемый уровень привилегий.

RPL секущего кодового сегмента (хранится в регистре cs) является уровнем привилегий данного процесса и называется текущим уровнем привилегий (CPL)

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

В случае если запрашиваемый уровень привилегий больше текущего, то запрашиваемый уровень понижается до текущего. При обращении к сегменту данных RPL селектора должен быть не ниже DPL сегмента. При обращении к сегменту кода возможно только при равенстве CPL, RPL и DPL. Если сегмент кода помечен как подчиненный, то для обращения к нему необходимо иметь уровень привилегий не ниже уровня сегмента. При этом выполнение сегмента происходит с текущим уровнем привилегий.

Косвенные вызовы возможны только через шлюзы при соблюдении следующих условий:

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



Исключения защищенного режима.


Я уже неоднократно упоминал это слово в предидущих выпусках. Но думаю что не всем было понятно что это такое. Сейчас мы рассмотрим это поподробнее.

Исключения или системные прерывания существовали еще в самых первых моделях процессоров от Intel. Вот их список:

Division by zero (деление на ноль или переполнение при делении); Single step (пошаговая отладка); Breakpoint; Overflow (срабатывает при команде into в случае установленного флага overflow в регистре flags); Invalid opcode (i286+); No math chip;

Исключения располагаются в начале таблицы прерываний. В реальном режиме занимают 8 первых векторов прерываний.

Введение защищенного режима потребовало введения дополнительных исключений. В защищенном режиме первые 32 вектора прерываний зарезервированы для исключений. Не все они используются в существующих процессорах, в будующем возможно их будет больше. Системные прерывания в защищенном режиме делятся на три типа: нарушения (fault), ловушки (trap) и аварии (abort). Итак в защищенном режиме у нас существуют следующие исключения:

Divide error (fault); Debug (fault/trap); Breakpoint (trap); Overflow (trap); Bounds check (fault); Invalid opcode (fault); Coprocessor not available (fault); Double fault (abort); Coprocessor segment overrun (fault); Invalid tss (fault); Segment not present (fault); Stack fault (fault); General protection fault (fault); Page fault (fault); Coprocessor error (fault); Alignument check (fault) (i486+); Hardware check (abort) (Pentium+); SIMD (fault) (Pentium III+).

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

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

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

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

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

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



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


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

19 15 8 4 0
Сегмент
Смещение
Линейный адрес

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



Эпилог.


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

О чем мы будем говорить в следующем выпуске, пока не знаю. Надеюсь, что до конца недели он увидит свет.

Отправлено 2001-08-06 для 3542 подписчиков.
ведущий рассылки Dron
Архив Рассылки

При поддержке Kalashnikoff.ru



Как процессор работает с памятью?


Для начала небольшое предисловие.
В процессорах имеются базовые регистры, которые могут задавать смещение. На 16-битной архитектуре максимальное смещение могло быть до 64 килобайт, что, в общем-то, не много и вызывало определенные трудности (разные модели памяти, разные форматы файлов). Так же, в 16-битной архитектуре присутствовали сегментные регистры, которые указывали адрес сегмента в памяти. В процессорах, начиная с i386, базовые регистры стали 32-х битными, что позволяет адресовать до 4 гигабайт. Сегментные регистры остались 16-битными, и в защищенном режиме они не содержат адреса! они содержат индекс дескриптора. В реальном режиме сегментные регистры работают так же, как и на 16-битных процессорах.

В реальном режиме сегментные регистры непосредственно указывают на адрес начала сегмента в памяти. Это позволяет нам, без каких либо преград, адресовать 1 мегабайт памяти. Но создает определенные трудности для защиты. А защищать нам нужно многое. Например, мы не можем пользовательским программам дать возможность непосредственно обращаться к коду или данным ядра. Так же мы не можем дать возможность пользовательским программам обращаться к коду или данным других пользовательских программ, поскольку это может нарушить их работоспособность.
Для этого был изобретен защищенный режим работы процессора, который появился в процессорах i286.

Защищенность этого режима заключается в следующем:
Сегментный регистр больше не указывает на адрес в памяти. В этом регистре теперь задается индекс в таблице дескрипторов.
Таблица дескрипторов может быть глобальная или локальная (применяется в многозадачных системах для изоляции адресного пространства задач) и представляет собой массив записей, по 8 байт в каждой, где описываются адреса, пределы и права доступа к сегментам.
Про адрес ничего не буду говорить, и так все ясно. Что такое предел? В этом Поле описывается размер сегмента. При обращении за пределы сегмента процессор генерирует исключение (специальное прерывание защищенного режима). Так же исключение генерируется в случае нарушения прав доступа к сегменту. Поле прав доступа описывает возможность чтения/записи сегмента, возможность выполнения кода сегмента, уровень привилегий для доступа к сегменту.

При обращении к сегменту из дескриптора берется базовый адрес сегмента и складывается со смещением сегмента. Так получается линейный 32-х разрядный (в i286 - 24-х разрядный) адрес. Для i286 на этом процесс получения адреса завершается, линейный адрес там равен физическому. Для i386 или выше это справедливо не всегда.



Микроядерные системы.


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

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

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

Для обеспечения доступа к портам ввода/вывода используются возможности процессоров, впервые появившиеся intel 80386. У каждой задачи (в сегменте состояния задачи (TSS)) существует карта доступности портов ввода/вывода. Приложение обращается к ядру с "просьбой" зарегистрировать для нее диапазон портов. Если эти порты до тех пор никем не были заняты, то ядро предоставляет их в распоряжение процесса, помечая их как доступные в карте доступности ввода/вывода этого процесса.

DMA память, опять таки после запроса у ядра, с помощью страничного преобразования подключается к адресному пространству процесса. Настройка каналов осуществляется ядром по "просьбе" процесса.

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




Но, как я говорил, ядро выполняет еще некоторые функции, немаловажная из которых - это межпроцессное взаимодействие. Оно представляет из себя возможность процессов обмениваться сообщениями между собой. В отличии от монолитных систем в микроядерных системах межпроцессное взаимодействие (Inter Process Communication или IPC) это едва ли не основное средство общения между процессами, и поскольку все драйвера у нас такие же процессы, микроядерное IPC должно быть очень быстрым. Быстродействие IPC достигается за счет передачи сообщений без промежуточного буферизирования в ядре. Либо непосредственным переписыванием процессу-получателю, либо с помощью маппинга страниц (если сообщения большого размера).

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

На этом я заканчиваю сегодняшний выпуск. Если вам что-то не понятно, задавайте вопросы, адрес моей электронной почты вы всегда можете найти внизу выпуска.

Отправлено 2001-08-17 для 3870 подписчиков.
ведущий рассылки Dron
Сайт проекта
Архив Рассылки

При поддержке Kalashnikoff.ru



Многозадачность.


Многозадачные возможности в процессорах так же появились в процессорах, начиная с i286. Для реализации этого, процессор для каждой задачи использует, так называемый, "сегмент состояния задачи" ("Task State Segment", сокращенно TSS). В этом сегменте, при переключении задач, сохраняются все базовые регистры процессора, сегменты и указатели стека для трех уровней защиты (для каждого уровня используется свой стек), сегментный адрес локальной таблицы дескрипторов ("Local descriptor table", сокращенно LDT). В процессорах, начиная с i386, там еще хранится адрес каталога страниц (регистр CR3). Так же этот сегмент обеспечивает некоторые другие механизмы защиты, но о них мы пока не будем говорить.

Операционная система может расширить TSS, и использовать его для хранения регистров и состояния сопроцессора. Процессор при переключении задач не сохраняет этого. Так же возможны другие применения.



Немного углублюсь в аппаратные возможности компьютеров.


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

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

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



Новости.


Изменился адрес сайта. Сайт теперь располагается по адресу http://asmdev.narod.ru/asmos.html.

У проекта открылся сайт на sourceforge. http://dronix.sourceforge.net/, на котором пока пусто, но вскоре изменения можно будет заметить на http://sourceforge.net/projects/dronix/.



Определение количества памяти через 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Мб памяти. Не очень то подходят для использования.



Основные принципы файловых систем.


Все устройства блочного доступа (к которым относятся жесткие или гибкие диски, компакт диски) при чтении/записи информации оперируют секторами. Для жестких или гибких дисков размер сектора равен 512 байт, в компакт-дисках размер сектора равен 2048 байт. Сектора являются физической единицей информации для носителя.

Для файловых систем такое распределение часто бывает не очень удобно, и в них вводится понятие кластера. Кластеры часто бывают больше по размеру, чем сектора носителя. Кластеры являются логической единицей файловых систем. Правда, не всегда они называются кластерами. В ext2 кластеры называются просто блоками, но это не столь важно. Для организации кластеров файловые системы хранят таблицы кластеров. Таблицы кластеров, естественно, расходуют дисковое пространство. Помимо этого, дополнительное дисковое пространство расходуется под каталоги файлов. Эти неизбежные расходы в разных файловых системах имеют разную величину. Но об этом мы поговорим ниже.



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


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

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

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

О ядре мы начнем говорить в следующем выпуске. С загрузчиком мы практически закончили. Он уже обладает достаточной для нас функциональностью.
Как всегда вы можете задавать свои вопросы, высказывать пожелания по адресу, указанному ниже.
Хочу принести свои извинения читателям рассылки в текстовом формате. Что-то на субскрайбе с конвертацией не то.

Отправлено 2001-11-29 для 5592 подписчиков.
ведущий рассылки Dron
Сайт проекта
Архив Рассылки

При поддержке Kalashnikoff.ru



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


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

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

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

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

lgdt [gd_desc]

В регистр gdtr загружается дескриптор GDT.

push byte 2 popf

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

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

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

jmp 16:.epm

BITS 32 .epm:

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

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

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

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

...

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

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

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

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

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

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

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



Процесс загрузки.


То, что я до сих пор сделал пока рассчитано только на работы с дисками 1,4Мб, то есть с флопами. Это конечно ограничение в некоторой степени, но пока система еще далеко не готова, этого достаточно. Естественно это еще не окончательный вариант. Да и можно ли говорить об окончательности программных продуктов? Нет предела совершенству. :)

В обязанности бутсектора входит следующее:

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

Если с первым и двумя последними пунктами все просто и компактно, то второй и третий пункт требуют возможности работы с файловой системой, а третий пункт помимо этого должен знать структуру бинарных форматов. На все это не хватает 512 байт, отводимых для бутсектора. Наш бутсектор занимает больше - один килобайт.
В файловой системе EXT2 с этим не возникает никаких проблем, поскольку первый килобайт файловой системы не используется.
В FAT это немного сложнее. Служебная структура, именуемая Boot Sector Record (BSR), содержит в себе все необходимые поля для выделения для загрузочного сектора места более чем 512 байт. Но как это сделать при форматировании, стандартными средствами, я не нашел. И если формат диска не соответствует каким-то внутренним представлениям Windows, то содержимое такого нестандартного диска может быть испорчено. Выход был найден случайно. Как оказалось утилита format хоть и не имеет таких параметров командной строки, но перед форматированием берет информацию из BSR. И если предварительно заполнить эту структуру (с нужными нам параметрами), а потом уже форматировать, то все получается так, как хочется нам. Таким образом, у меня получилось сделать диск, у которого два сектора зарезервированы (там будет размещаться boot), и одна копия FAT.

Ну теперь давайте по порядку рассмотрим все этапы работы бутсектора.



Шлюзы


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

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

7 6 5 4
Смещение 31-16 Права доступа Количество слов стека
3 2 1 0
Селектор Смещение 15-0

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

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

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

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



Системные особенности.


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

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

В связи с этим все модули линкуются не с начальных адресов, а с достаточно большим смещением в сегменте. В Windows используется базовый адрес в сегменте - 0x400000, в юникс (Linux или FreeBSD) - 0x8048000.

Некоторые особенности так же связаны со страничной организацией памяти.
ELF файлы линкуются таким образом, что границы и размеры секций приходятся на 4-х килобайтные блоки файла.

А в PE формате, не смотря на то, что сам формат позволяет выравнивать секции на 512 байт, используется выравнивание секций на 4к, меньшее выравнивание в Windows не считается корректным.

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

Отправлено 2001-11-12 для 5364 подписчиков.
ведущий рассылки Dron
Сайт проекта
Архив Рассылки

При поддержке Kalashnikoff.ru



Страничная организация памяти.


В процессорах, начиная с i386, появилась, так называемая, страничная организация памяти. Страница имеет размер 4 килобайта или 4 мегабайта. Большие страницы могут быть только в pentium или выше. Не знаю только, какой толк от таких страниц.
Если возможность страничной адресации не используется, то линейный адрес, как и на i286, равен физическому. Если используется - то линейный адрес разбивается на три части. Первая, 10-битная, часть адреса является индексом в каталоге страниц, который адресуется системным регистром CR3. Запись в каталоге страниц указывает адрес таблицы страниц. Вторая, 10-битная, часть адреса является индексом в таблице страниц. Запись в таблице страниц указывает физический адрес нахождения страницы в памяти. последние 12 бит адреса указывают смещение в этой странице.
В страничных записях, как и в дескрипторных записях, есть служебные биты, описывающие права доступа, и некоторые другие тонкости страниц. Одной из важных тонкостей является бит присутствия страницы в памяти. В случае не присутствия страницы, процессор генерирует исключение, в котором можно считать данную страницу из файла или из swap раздела. Это сильно облегчает реализацию виртуальной памяти. Чуть ниже мы про это поговорим.

Надеюсь, я не сильно вас утомил? более подробно про все это можно прочитать в книгах по архитектуре процессоров. А мы вернемся к операционным системам.



Теперь немного поподробнее про устройство ядра.


На "Собственно ядро" возлагаются функции менеджера памяти и процессов. Переключение процессов - это основной момент нормального функционирования системы. Драйвера не должны "тормозить", а тем более блокировать работу ядра. Windows - наглядный пример того, что этого нельзя допустить!

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

Системные вызовы - это интерфейс между процессами и ядром (читайте-железом). Никаких других методов взаимодействия процессов с устройствами компьютера быть не должно. Системных вызовов достаточно много, на Linux их 190, на FreeBSD их порядка 350, причем большей частью они совпадают, соответствуя стандарту POSIX (стандарт, описывающий системные вызовы в UNIX). Разница заключается в передаче параметров, что легко будет предусмотреть. Естественно, мы не сможем сделать ядро, работающее одновременно на Linux и на FreeBSD, но по отдельности совместимость вполне реализуема.

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

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




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

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

На этом я сегодня заканчиваю. До следующих выпусков. Пишите.

Отправлено 2001-08-10 для 3692 подписчиков.
ведущий рассылки Dron
Архив Рассылки

При поддержке Kalashnikoff.ru



Вопросы к подписчикам


Какой бы вы хотели видеть СВОЮ систему?
На какую систему она должна походить?
Сколько места на винчестере занимать?
Сколько памяти требовать для работы?

Ответы на эти вопросы присылайте по адресу: mailto:dron@infosec.ru?Subject=AsmOS

По всем вопросам вы можете обращаться ко мне по адресу: mailto:dron@infosec.ru?Subject=AsmOS.

При поддержке Kalashnikoff.ru



Возвращаясь к прошлым выпускам...


В 4-м выпуске, когда я расписывал вам, как писать boot sector, я допусил одну достаточно серьезную ошибку, которую признаю и благодарю Bug Maker'а, за то, что обратил на это мое внимаение. В процедуре load_sector я, первым делом, делю номер сектора на количество секоров на дорожке. Для деления используя беззнаковую команду div, предварительно расширяя ax в dx:ax знаковой командой cwd. Правда если учесть что максимальное количество секторов на гибком диске не превышает 2880, то старший, знаковый, бит ax всегда нулевой. Но, тем не менее, ошибка потенциальная. Этот фрагмент кода стоит писать так:

load_sector: push ax push cx

mov cl, 18 div cl

mov cx, dx inc cx

Исправившись, я вообще убрал команду cwd, и теперь делю на байт cl. Все это, к тому же, сэкономило мне два байта. :)

Но это еще не все.. при написании boot sector'а я говорил, что это совсем не окончательная версия. Так оно и получается. Из бутсектора мы уберем код загрузки kernel. Этим будет заниматься программа setup. Следовательно, boot sector'у осанется только считать setup и запусить его... Даже если сделать более корректную обработку ошибок чтения, у нас остается около 250 байт на всякие развлечения... :)

А setup должен будет уметь достаточно многое. В него будет встроена поддержка файловой системы, поддержка выполняемых форматов файлов. Мы собираемся делать микроядро, и setup'у придется загружать помимо ядра еще несколько дополнительных программ, которые понадобятся нам для нормального старта системы.

Но об этом позже. А теперь продолжаем разбираться с защищенным режимом.



Загрузка DOS.


boot sector DOS загружает в память два файла: io.sys и msdos.sys. Названия этих файлов в разных версиях DOS различались, не важно. Файл io.sys содержит в себе функции прерывания int 21h, файл msdos.sys обрабатывает config.sys, и запускает командный интерпретатор command.com, который в свою очередь обрабатывает командный файл autoexec.bat.



Загрузка формата 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.


int LoadPE (unsigned char *bin) { struct elf32_hdr *PH = (struct pe_hdr *) (bin + *((unsigned long *)&bin[0x3c])); // Конечно комбинация не из понятных... просто берем dword по смещению 0x3c // И вычисляем адрес PE заголовка в образе файла struct elf32_phdr *POH;

if (PH == NULL || // Контролируем указатель PH->pe_sign != 0x4550 || // сигнатура PE {'P', 'E', 0, 0} PH->pe_cputype != 0x14c || // i386 (PH->pe_flags & 2) == 0) // файл нельзя запускать! return PE_WRONG;

POH = (struct pe_ohdr *)((unsigned char *)PH + 0xf8);

while (PH->pe_obj_num--) { if ((POH->p_flags & 0x60) != 0) // либо код либо инициализированные данные memcpy (PE->pe_image_base + POH->o_vaddr, bin + POH->o_poff, POH->o_psize);

POH = (struct pe_ohdr *)((unsigned char *)POH + sizeof (struct pe_ohdr)); }

return PE_OK; }

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



Загрузка FreeBSD.


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

Порядок загрузки примерно следующий:

BootSector загружает вторичный загрузчик; Вторичный загрузчик переводит систему в защищенный режим и запускает loader; loader предоставляет пользователю возможность выбрать необходимые модули или запустить другое ядро; После чего управление передается ядру и начинается инициализация драйверов;

В прошлом выпуске я писал: В следующем выпуске мы рассмотрим процессы загрузки разных операционных систем (Windows не предлагать!).

Почему Windows не предлагать? Windows пока что еще никто не отменял :) Не хотите загружаться как Windows, но тогда расскажите, почему и приведите сравнение, но все равно расскажите, как это делает Windows.

Не хотел рассказывать, но придется... :)
Если что-то я напутаю, уж извините...
Давайте по порядку рассмотрим, как грузятся системы от Microsoft.



Загрузка Linux.


Для Linux свойственно два способа загрузки:

Загрузка через boot sector ядра; Загрузка через boot manager LILO (Linux Loader);

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

boot sector переписывает свой код по адресу 9000h:0; Загружает с диска Setup, который записан в нескольких последующих секторах, по адресу: 9000h:0200h; Загружает ядро по адресу 1000h:0. Ядро так же следует в последующих секторах за Setup. Ядро не может быть больше чем 508 килобайт, но так как оно, чаще всего, архивируется - это не страшно; Запускается Setup; Проверяется корректность Setup; Производится проверка оборудования средствами BIOS. Определяется размер памяти, инициализируется клавиатура и видеосистема, наличие жестких дисков, наличие шины MCA (Micro channel bus), PC/2 mouse, APM BIOS (Advanced power management); Производится переход в защищенный режим; Управление передается по адресу 1000h:0 на ядро; Если ядро архивировано, оно разархивируется. иначе просто переписывается по адресу 100000h (за пределы первого мегабайта); Управление передается по этому адресу; Активируется страничная адресация; Инициализируются idt и gdt, при этом в кодовый сегмент и в сегмент данных ядра входит вся виртуальная память; Инициализируются драйвера; Управление передается неуничтожимому процессу init; init запускает все остальные необходимые программы в соответствии с файлами конфигурации;

В случае загрузки через LILO:

boot sector LILO переписывает свой код по адресу 9a00h:0; До адреса 9b00h:0 размещает свой стек; Загружает вторичный загрузчик по адресу 9b00h:0 и передает ему управление;

Вторичный загрузчик загружает boot sector ядра по адресу 9000h:0; Загружает Setup по адресу 9000h:0200h; Загружает ядро по адресу 1000h:0; Управление передается программе Setup. Зачем загружает boot sector из ядра? не понятно;

В Linux есть такое понятие как "big kernel". Такой kernel сразу загружается по адресу 100000h.



Загрузка с диска дополнительной части кода и служебной информации файловой системы.


Бутсектор загружается БИОСом по адресу 0:7c00h занимает он 512 байт. Память начиная с адреса 0:7e00h свободна. но в эту память мы загрузим второй сектор бута. Одновременно загружается информация необходимая для обслуживания файловой системы. Для EXT2 дополнительно необходимо загрузить два килобайта (суперблок и дескрипторы групп), для FAT немного больше - 4,5 килобайта (первая копия FAT).

mov ax, 0x7e0 mov es, ax

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

mov ax, 1

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

%ifdef EXT2FS mov cx, 5

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

%elifdef FATFS mov cx, 10

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

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

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

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



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


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

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

kern[el] - файл ядра; modu[le] - файл модуля; #end - конец файла конфигурации. Использование этих слов в другом контексте недопустимо.

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

call fs_init

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

mov si, boot_config call fs_load_file

...

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

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

kernel /boot/kernel
#end

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



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


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

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



Загрузка Windows 9x.


Отличие от DOS заключается в том, что функции msdos.sys взял на себя io.sys. msdos.sys остался ради совместимости как конфигурационный файл. После того как командный интерпретатор command.com обрабатывает autoexec.bat вызывается программа win.com, которая осуществляет перевод системы в защищенный режим, и запускает различные другие программы, обеспечивающие работу системы.



Загрузка Windows NT.


boot sector NT - зависти от формата FS, для FAT устанавливается один, для NTFS - другой, в нем содержиться код чтения FS, без обработки подкаталогов.

boot sector загружает NTLDR из корневой директории, который запускается в real mode; NTLDR певодит систему в защищенный режим; Создаются необходимые таблицы страниц для доступа к первому мегабайту памяти; Активируется механизм страничного преобразования; Далее NTLDR читает файл boot.ini, для этого он использует встроенный read only код FS. В отличии от кода бутсектора он может читать подкаталоги; На экране выводится меню выбора вида загрузки; После выбора, или по истечении таймаута, NTLDR из файла boot.ini определяет нахождение системной директории Windows, она может находиться в другом разделе, но обязательно должна быть корневой; Если в boot.ini указана загрузка DOS (или Win9x), то файл bootsect.dos загружается в память и выполняется горячая перезагрузка; Далее обрабатывается boot.ini; Загружается ntdetect.com, который выводит сообщение "NTDETECT V4.0 Checking Hardware", и детектит различные устройства... Вся информация собирается во внешней структуре данных, которая в дальнейшем становиться ключем реестра "HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION"; NTLDR выводит сообщение "OSLOADER V4.0"; Из директории winnt\system32 загружается ntoskrnl.exe, содержащий в себе ядро и подсистемы выполнения (менеджер памяти, кэш менеджер, менеджер объектов), и файл hal.dll, который содержит в себе интерфейс с аппаратным обеспечением; Далее NTLDR предоставляет возможность выбрать "последние известные хорошие" конфигурации. В зависимости от выбора выбираются копии реестра используемые для запуска; Загружает все драйвера и другие необходимые для загрузки файлы; В завершение он запускает функцию main из ntoskrnl.exe и завершает свою работу;

Не могу гарантировать полную достоверность представленной информации, NT я знаю плохо, тем более не знаю что у нее внутри. Так же не могу что-либо более конкретного сказать про распределение памяти в процессе загрузки Windows NT. некоторые неточности могут быть связаны с моим плохим знанием английского, желающие могут посмотреть на оригинал по адресу: Inside the Boot Process, Part 1

Ну вот, мы узнали как загружаются системы. В своей системе мы не будем слепо следовать какому либо из представленных здесь путей. Ради совместимости обеспечим формат ядра, аналогичный Linux. Мне кажется, в этой системе все сделано достаточно понятно и просто.

Я думаю никто не будет против того, что мы в первую очередь будем ориентироваться на Linux?

А в следующем выпуске мы поговорим о распределении памяти в системе и начнем писать свой boot sector.

Отправлено 2001-07-20 для 695 подписчиков.
ведущий рассылки Dron
Архив Рассылки

При поддержке Kalashnikoff.ru



Заключение.


Ну не знаю что еще написать, жара достала, не знаю как у вас, а у нас в Москве - за 30... :(

В следующем выпуске мы рассмотрим процессы загрузки разных операционных систем (Windows не предлагать!). Ну, может быть еще что-то, это я узнаю из ваших писем.

По всем вопросам вы можете обращаться ко мне по адресу: mailto:dron@infosec.ru?Subject=AsmOS.

При поддержке Kalashnikoff.ru



Защита.


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

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

15 2 0
Index TI RPL

Поле Index определяет индекс в дескрипторной таблице.

В процессорах Intel одновременно в системе может существовать две дескрипторных таблицы: Глобальная (Global descriptor table или GDT) и Локальная (Local descriptor table или LDT).

GDT существует в единственном экземпляре. Адрес и предел GDT хранятся в специальном системном регистре (GDTR) в 48 бит длиной (6 байт).
LDT может быть индивидуальная для каждой задачи, или общая для системы, или же ее вообще может не быть. Адрес и размер LDT определяется в GDT, для обращения к LDT в процессоре существует специальный регистр (LDTR), но в отличии от GDTR он имеет размер 16 бит и содержит в себе селектор из GDT.

Поле TI (Table indicator) селектора определяет принадлежность селектора GDT (0) или LDT (1).

Поле RPL (Requested privilege level) определяет запрашиваемые привилегии... об этом мы поговорим чуть позже.