Своя ОС?

от автора

Дарова! Сегодня я поделюсь с вами опытом, как я пытался написать собственную ОС и, что из этого вышло. Запасайтесь чайком с печеньками и присаживайтесь поудобнее! Пора окунуться в 16ти битный мир…


С чего начать?

Я начал с изучения ЯП ассемблера. Далее нам понадобится hex редактор (да, я его тоже использовал) и редактор образов дисков. И последнее, что понадобится виртуальная машина. Конкретных рекомендаций давать не буду, но я использовал:

  • HxD hex-редактор

  • ЯП — fasm

  • ultraISO в качестве программы для создания и редактирования образов дисков

  • VMBox — виртуальная машина, хотя во многих туториалах и гайдах использовали qemu (я просто с ним не разобрался)

На_стройки

Я надеюсь вы уже установили необходимые программы, а так же редактор кода? Тогда приступим!

Для начала, создадим структуру проекта, например так:

пример структуры проекта

Os/
bin/
obj/
src/
boot/
makefile.txt

Далее напишем простой makefile:

#################################################################################### #Create Date: 03.01.2024 18:50 #Goal: create a simple bootloader and simple core #Author: As_Almas #Description: wait for write... # #Status:  ####################################################################################  TARGET = As_OS.img  SRC_BOOT_PREF = ./src/boot/  BOOT_OBJ_F  = ./obj/ BIN_PREFIX  = ./bin/  ISO_app = UltraISO.exe HEX_EDIT = HxD.exe  VBOX = VBoxManage.exe startvm  OS_NAME = "AS_OS"  DEBUG_FLAGS =  -E VBOX_GUI_DBG_ENABLED=true  ASM = FASM ASM_FLAGS =   boot:  $(ASM) $(ASM_FLAGS) $(SRC_BOOT_PREF)bootloader.asm $(BOOT_OBJ_F)bootloader.bin  hex: $(BOOT_OBJ_F)bootloader.bin $(HEX_EDIT) $(BIN_PREFIX)$(TARGET) $(BOOT_OBJ_F)bootloader.bin  fs: $(ISO_app) $(BIN_PREFIX)$(TARGET)  clean: del "$(BOOT_OBJ_F)\*.bin"  debug: $(BIN_PREFIX)$(TARGET) $(VBOX) $(OS_NAME) $(DEBUG_FLAGS)

Ну, я думаю здесь всё просто:
boot — выполняет компиляцию файла с исходным кодом загрузчика ядра (bootloader — о нём позже).
hex — сделано для моего удобства, открывается hex-редактор, в котором я спокойно и с удовольствием заменяю сектор с bootloader’ом в образе диска на мой bootloader (не переживайте, вы скоро поймёте что за образы и bootloader’ы)
fs — открывает образ диска в программе для его редактирования. Это нам понадобится на этапе загрузки ядра в образ диска.
clean — очищает папку obj от мусора (P.S. я этой командой не пользовался, но может быть вам пригодится)
debug — запускает виртуальную машину с нашей ОС в режиме отладке (debug-mode)

Далее можно создать образ диска, с помощью специальных программ. Нам требуется образ дискеты размером 1.44мб. Я создал образ диска так:

Способ создания образа

Открываем программу UltrsISO:

главная UltraISO

главная UltraISO

Далее переходим в файл -> новый:

Меню инструментов UltraISO

Меню инструментов UltraISO

Здесь выбираем образ дискеты. Откроется следующее меню, в котором всё оставляем как на картинке:

настройки образа дискеты

настройки образа дискеты

На этом создание образа дискеты завершено, не забудьте сохранить!

Настроим виртуальную машинку?

VMbox - создание машины

VMbox — создание машины

Нажимаем создать. Открывается меню как на картинке, где имя пишем что пожелаем, остальные поля оставляем пустым. Тип устанавливаем other, а версию DOS . Нажимаем далее. В следующем окне выбираете так как пожелаете. Нажимаем далее, и можете не подключать виртуальный жёсткий диск. Нажимаем далее и готово.
Следующим делом необходимо выбрать нашу только, что созданную виртуальную машину в главном меню VMbox и нажать настроить. В настройках переходим в носители и где контроллер Floppy нажимаете плюс. Нажимаем добавить и выбираем недавно созданный нами образ диска:

Картинки настроек
настройки

настройки
меню выбора гибкого диска (дискеты)

меню выбора гибкого диска (дискеты)

Непосредственно код

Перед запуском любой ОС в процессор загружается программа bios. Она проверяет наличие и исправность компонентов необходимых для нормальной работы компьютера. А затем ищет среди устройств хранения данных (жёсткие/гибкие диски и т.п.), тот, в последних двух байтах первого сектора которого находится специальная запись двухбайтовая 0x55, 0xAA, указывающая, что он содержит загрузчик ядра. Обычно сектор равняется 512 байт, но на некоторых устройствах оооочень редко может быть другое количество байт на сектор.

Bootloader или же загрузчик ядра

Коротко, он запускается после проверки систем ПК и нахождения устройства хранения данных (например, дискеты) с возможным к запуску кодом по сигнатуре 0x55, 0xAA в последних байтах первого сектора. BootLoader должен после своего запуска найти на диске ядро системы, загрузить его с диска в оперативную память, перейти из реального режима (x16) в защищённый режим (x32) и передать управление ядру.

Перейдём к написанию кода загрузчика:

use16 ; код для 16 битного режима org 0x7C00 ; расположение кода с адресса 0x7C00  start: ; начало кода  ; ... some code  finish:      times 0x200-finish+start-2 db 0 ; заполняем до 510 байта всё нулями     db 0x55, 0xAA ; сигнатура сектора

Так, как код загрузчика выгружается в оперативную память именно по адресу 0x7C00, мы должны указать компилятору, чтобы он позиционировал код относительно этого адреса. Например, у нас в коде есть переменная str по адресу 0x00CA, но так как наш код будет находиться по адресу 0x7C00, то компилятор должен добавить его к адресу переменной и итоговая позиция переменной в оперативной памяти получается 0x7CCA.
Далее мы должны заполнить все пустые байты вплоть до 510 (включительно) нулями, для того, чтобы сигнатура 0x55, 0xAA была именно в последних двух байтах сектора.

start: jmp  boot_entry nop ; запись BPB boot_entry: ;..

В самом начале загрузчика (не считая команд jmp и nop в сумме занимающих 3 байта), должна находиться специальная запись, которая называется блок параметров биос (BPB), о нём пойдёт речь далее. Для того чтобы процессор не начал чудить пытаясь исполнить, как код область данных BPB, необходимо сразу выполнить короткий прыжок в область с исполняемым кодом.

BPB
; !!!! BPB BLOCK START !!!! OEM_NAME                db "ASOS2024" ;8 байт - название OEM BYTES_PER_SECTOR        dw 0x200 ; количество байт на сектор (512 байт на сектор) SECTORS_PER_CLUSTER     db 1 ; количество секторов на кластер (о кластерах позже) RSVD_SECTORS            dw 1 ; зарезервированные секторы (1 - сектор загрузчика) FATS_CNT                db 2 ; количество FAT таблиц (2 - 1 оригинал, 2 копия) ROOT_DIR_ENTRIES        dw 224 ; количество записей в корневом каталоге LOW_SECTORS_COUNT       dw 2880 ; количество секторов (нижнее слово) MEDIA_TYPE              db 0xF0 ; тип носителя (0xF0 - дискета) SECTORS_PER_FAT         dw 9 ; количество секторов на одну FAT таблицу SECTORS_PER_TRACK       dw 18 ; количество секторов на дорожку (о ней позже) HEADS_COUNT             db 2 ; количество считывающих головок  HEADEN_SECTORS          dd 0 ; скрытые секторы (таких у нас нет) HIGHT_SECTOR_COUNT      dd 0 ; количество секторов (верхнее слово) - в fat12 и fat16 - пустое ; !!!! BPB BLOCK END !!!!  ; !!!! EXTENDED BPB START !!!! DRIVE_NUMBER            db 0 ; номер устройства (предоставляется в регистре dl биосом) WIN_RTFLAGS             db 0 ; зарезервировано  BOOT_SIG                db 0x29 ; сам не разобрался, но пусть будет VOLUME_ID               dd 0 ; серийный номер устройства  VOLUME_LABEL            db "AS OS start" ; метка тома  SYS_LABEL               db "FAT12   "  ; используемая файловая система ; !!!! EXTENDED BPB END !!!!

Разъяснения к коду даны в виде комментариев

boot_entry:     cli ; off interraps         xor ax, ax ; ax = 0         mov ds, ax ; ds = ax         mov es, ax ; es = ax         mov ss, ax ; ss = ax         mov sp, 0x7Bff ; sp = 0x7Bff      sti ; on interraps

Отключаем все прерывания. Быстро и коротко обнуляем ах и сегментные регистры. Регистр начала стека sp устанавливаем 0x7Bff — ближайшая пустая область в оперативной памяти. А регистр конца стека ss обнуляем (стек растёт вниз). Включаем прерывания.

    mov [DRIVE_NUMBER], dl ; dl have the hard-drive index from bios     jmp 0x0000:main ; jmp main | cs = 0, ip = main main:

Помещаем номер устройства (дискеты) в DRIVE_NUMBER — обычно биос передаёт номер устройства в регистре dl. Затем совершаем длинный прыжок в область с основным нашим кодом. Длинный прыжок необходим для задания регистры cs необходимого значения (обнуляем), для того чтобы наш код выполнялся корректно и без ошибок. При этом регистр ip будет указывать на положение исполняемого кода в оперативной памяти.

main:   .clear_screan:         xor ax, ax ; ax = 0          int 10h ; video interrap   .on_start:

Очищаем экран и выбираем режим отображения 40x25 символов. ah = 0 — код функции прерывания int 10h, а al = 0 — код выбираемого видеорежима.

Ну что же, надо бы научиться загружать данные с диска в оперативную память. Иначе это будет не загрузчик, а ерунда какая-та. В современном мире используется LBA запись, в то время, как биос для считывания использует систему CHS. LBA — линейная запись номера сектора на диске, начинается с 0 и идёт до максимального количества секторов на диске. CHS — запись номера сектора включающая в себя, номер считывающей головки, номер считываемого цилиндра (на которой расположен нужный сектор) и номер сектора. Номер сектора в CHS начинается с 1. Естественно, удобнее использовать LBA, поэтому нам нужен способ преобразования LBA в CHS. К счастью такой способ есть!
Код в студию:

; ax = lba ; es:bx = read data start addr (in) read_sector:     .LBA_to_CHS: ; linear sector address to  address of cylinder, head and sector     ; s = (LBA % SECTORS_PER_TRACK) + 1     ; h = ((LBA - (s-1)) / SECTORS_PER_TRACK) % HEADS_COUNT     ; c = ( (LBA - (s-1) - h*S) / (HEADS_COUNT*SECTORS_PER_TRACK) )         push ax ; save ax         push ax ; save ax         xor dx, dx  ; find 's'         mov cx, [SECTORS_PER_TRACK]         div cx          pop ax ; ret ax value         inc dx         mov [sector], dx ; sector = s         dec dx ; dx = s - 1                  sub ax, dx ; ax = LBA - (s-1)         push ax ; save ax          xor dx, dx         mov cx, [SECTORS_PER_TRACK]         div cx ; ax = ((LBA - (s-1)) / SECTORS_PER_TRACK)          mov cl, [HEADS_COUNT]         div cl          mov [head], ah ; head = h          xor ah, ah ; cylinder = c         mov cx, [SECTORS_PER_TRACK]         mul cx ; ax = h * SECTORS_PER_TRACK         pop cx ; ax value to cx = ( LBA - (s-1))         sub cx, ax ; cx = (LBA - (s-1) - h*SECTORS_PER_TRACK)         push cx ; save cx         mov ax, [SECTORS_PER_TRACK]         mov cl, [HEADS_COUNT]         mul cl ; ax = SECTORS_PER_TRACK * HEADS_COUNT                  mov cx, ax          pop ax          xor dx, dx          div cx  ; c = cylinder     .read:

Для точности вычислений, лучше периодически обнулять регистр dx (как сделано в коде). В начале лучше найти значение номера сектора (s+1), так как его значение используется и в других вычислениях. Далее стоит его сохранить для дальнейшего использования. Находим значение номера считывающей головки и так же сохраняем. Находим номер цилиндра. В целом ничего сложного в этом нету, всё делается по формулам. Самое интересное ещё впереди.

Формулы для перевода LBA в CHS
sector = (LBA \mod S) + 1 head = \frac{LBA - (sector - 1)}{S}\mod Hcylinder = \frac{LBA - (sector-1) -head \times S}{H \times S}

где S — количество секторов на дорожку (цилиндр), H — количество считывающих головок

С этим разобрались, а как же считывать данные с диска? Для этого есть прерывание int 13h и его функция 0x02:

.read:          mov dl, [DRIVE_NUMBER]         mov dh, [head]         mov cx, ax ; cylinder         shl cx, 6 ; cx << 6         or cx, [sector] ; hight 10 bits - cylinder; low 6 bits - sector         mov ax, 0x0201 ; al - count to read; ah - interrap function         int 13h ; read         jb _err ; on read error         pop ax ; ax = LBA ret

Эта функция принимает следующие параметры:

регистр

значение

dl

номер устройства (диска), с которого нужно считать данные

dh

номер считывающей головки

al

количество секторов к считыванию

ah

номер функции (0x02 — считывание с диска)

es:bx

адрес оперативной памяти, куда заносятся считанные данные

cx

самое интересное:) старшие 10 бит — номер дорожки; cl — номер сектора

Это ещё не всё. Что же делать, если нужно считать несколько секторов с диска, а не 1? Некоторые могут подумать, что можно указать количество секторов к считыванию в al более одного, НО, если вдруг считываемый сектор будет находиться в другом цилиндре или под другой головкой, то вернётся ошибка! Поэтому гораздо лучше каждый сектор считывать отдельно. Пример кода:

;ax = lba ;es:bx = read data start addr (in) ;cx = count of sectors to read read_sectors:      .read_loop:         push cx          call read_sector         inc ax          add bx, [BYTES_PER_SECTOR]         pop cx          loop .read_loop     ret

cx необходимо сохранить в стеке, а ax нет, потому что ax не изменяется внутри внутри функции read_sector.

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

Пару слов про то, как хранятся файлы

В мире уже давно существуют системы хранения данных (в нашем случае файлов). Будем использовать систему хранения файлов FAT. А точнее его версию FAT12.
Основные части FAT — это FAT таблица, в которой хранится информация о цепочках кластеров файлов (один кластер — минимальное количество секторов, выделяемых для хранений файла размером 1 байт), а так же корневой каталог, размер которого задаётся в BPB. В корневом каталоге хранятся данные о файлах и других каталогах в файловой системе, а точнее их объявления. Каталоги представлены так же, как файлы за несколькими исключениями в флагах при объявление. А непосредственно в области данных каталогов хранятся объявления вложенных файлов и каталогов, а так же объявление каталога-«родителя» и о самого каталоге ( . — сам каталог, .. — каталог-родитель, но в корневом каталоге этих записей нет).
Файловое объявление (запись) состоит из 32 байт:

название

смещение (байт)

размер (байт)

описание

название

0

11

8 — байт название файла, 3 байта — расширение. Точка между расширением и названием не вставляется

атрибуты

11

1

Верхние два бита зарезервированы и за редким исключением — обнулены. Значения: 0x01 — только для чтения, 0x02 — скрытый, 0x04 — системный, 0x20 — архивный, 0x10 — каталог, 0x08 — метка тома, 0x0F — часть имени другого файла (об этом упоминать не буду), 0x40 — зарезервировано (устройство).

NT_byte

12

1

Зарезервировано для Windows NT

время мс.

13

1

время создания файла в миллисекундах. Часто игнорируется

время с.

14

2

время создания файла с точностью в 2 секунды

дата создания

16

2

дата создания файла

последний доступ

18

2

дата последнего доступа к файлу (чтения или записи)

кластер ст.

20

2

старшие два байта номера первого кластера файла. В FAT12 и FAT16 равен 0. P.S. Число после FAT показывает сколько бит используется для обозначения номера кластера в FAT

последняя запись время

22

2

время последней записи в файл

последняя запись дата

24

2

дата последней записи в файл

кластер мл.

26

2

младшие два байта номера первого кластера файла в таблице FAT

размер

28

4

размер файла в байтах

В таблице FAT каждый предыдущий кластер указывает на следующий кластер файла. Номер кластера на диске и номер записи кластера в таблице FAT совпадают. Если кластер пустой, то он в таблице FAT указывается, как пустой (то есть 0), если кластер повреждён, то указывается 0x0FF7, если кластер — последний кластер файла, то его значение указывается больше или равным 0x0FF8. Это в случае с FAT12, которую и будем использовать.

Давайте же загрузим корневой каталог:

.on_start:         mov dl, [DRIVE_NUMBER]     .loadRoot:         xor ax, ax          mov al, [FATS_CNT]         mov cx, [SECTORS_PER_FAT]         mul cx    ; dx:ax = FATS_CNT*SECTORS_PER_FAT = size of fat in sectors         add ax, [RSVD_SECTORS] ; ax =rootDirPos-hideSectors         add ax, word [HEADEN_SECTORS] ; ax = rootDirPos         push ax ; save ax                  mov ax, [ROOT_DIR_ENTRIES]         mov cx, 32          mul cx ; dx:ax = 32*ROOT_DIR_ENTRIES         mov cx, [BYTES_PER_SECTOR] ; 512b         div cx ; ax = (32*ROOT_DIR_ENTRIES):BYTES_PER_SECTOR         pop cx          xchg ax, cx          mov bx, 0x0500 ; es:bx = 0x0000:0x0500 | ax = rootDirPos | cx = rootDirSize         call read_sectors ; read root dir .find_file:

Для начала этот код вычисляет положение корневого каталога. Корневой каталог расположен после загрузочного сектора и таблиц FAT. Для этого необходимо вычислить размер таблиц FAT, далее прибавить загрузочный сектор, скрытые секторы и зарезервированные секторы. Сохраняем на будущее, и вычисляем размер корневого каталога в секторах (у нас 1 сектор = 1 кластер). Так, как в BPB указывается только количество файловых записей в корневом каталоге, требуется умножить на 32 и разделить на размер сектора в байтах. Загружаем положение корневого каталога и его размер в нужные регистры и считываем по адресу 0x0000:0x0500.

Зная, как устроены файловые записи, можно и нужно найти запись файла ядра в корневом каталоге (для простоты ядро хранится прямо в корневом каталоге):

.find_file:         mov ax, bx ; mov ax - rootDir max addr in the memory         mov bx, 0x0500 ; bx = rootDir min addr in the memory         .check_name:             mov cx, 10 ; kernel name length             mov si, sys  ; kernel name pos             .lp1:                 push bx ; save last position of bx                 add si, cx ; si - symbol position in sys                 add bx, cx ; bx - symbol position in rootDirAddr (bx)                 mov dl, [si] ; dl - symbol from si                 mov dh, [bx] ; dh - symbol from bx                 cmp dl, dh ; check symbols (strcmp)                 pop bx ; recive last position of bx                 jne .next_fn                 mov si, sys ; kernel name pos                 loop .lp1 ; loop while cx > 0             mov dl, [si] ; check last symbol             mov dh, [bx] ; check last symbol             cmp dh, dl ; check last symbol             jne .next_fn ; if not equal             mov ax, [bx + 26] ; ax = firstFileClusterAddr             push ax ; save cluster addr             .load_fat: ; load fat addr table ;.................... дальнейший код (увидите его ниже) ...............;         .next_fn:                  cmp ax, bx                  jb _err                 add bx, 32                  jmp .check_name ; some code.... sys db "SYSTEM16BIN"

Загружаем в bx адрес по которому загрузили корневой каталог, в ax сохраняем адрес верхнего предела (конца) корневого каталога. Пройдёмся по корневому каталогу в поисках записи нужного файла (файла ядра). Для этого: загружаем в dh символ из переменной sys (название требуемого файла) со смещением cx , а в dl символ из названия файла корневого каталога по смещению cx. Сравниваем посимвольно, если хоть один символ не соответствует, то переходим к следующей записи в корневом каталоге и так до тех пор, пока не найдём запись файла или не закончится корневой каталог. Если нужная файловая запись найдена, то загружаем в ax адрес первого кластера файла, если нет заканчиваем программу с ошибкой:

_err:     mov ax, 0x0E45     mov bx, 0x0007     int 10h ; выводит на экран символ "E"     jmp _end 

Ну что ж, файл найден, давайте же загрузим его с диска в оперативную память:

.load_fat: ; load fat addr table    mov ax, [SECTORS_PER_FAT]     mov cl, [FATS_CNT]    mul cl ; ax = FATs size    mov cx, ax     mov ax, [RSVD_SECTORS]     mov bx, 0x0500 ; cx - fats size; ax - fats LBA; bx - load addr    call read_sectors ; load FAT table to 0x0000:0x0500     mov bx, 0x7E00   .next_Clust:       pop si ; si = firstFileClusterAddr or fileNextClusterAddr       add si, 0x0500 ; si = file cluster position in FAT table - 1       inc si ; si = = file cluster position in FAT table       mov ax, [si] ; ax = FAT[fileClusterAddr]       sub si, 0x0500       test si, 1 ; check odd or even       jz .even       and ax, 0x0fff ; if odd: to null higth 4 bits       jmp .load ; load cluster from disc       .even:            and ax, 0xfff0 ; if even: to null low 4 bits           shr ax, 4 ; ax >> 4   .load: ; load file sector       ; .... здесь код, который разберём далее ....       cmp ax, 0x0ff7 ; cmp to last cluster       mov si, ax ; si = NextFileClustAddr       jc .next_Clust ; if not endClust

Для начала загрузим таблицу FAT: вычисляем размер таблиц FAT в секторах и загружаем их в оперативную память по адресу 0x0500. Здесь в bx я загрузил адрес, по которому в дальнейшем будет «лежать» наше ядро — 0x7E00. Восстанавливаем из стека в si адрес первого кластера файла (сохраняли его в стек в предыдущем коде), добавляем адрес, по которому загрузили FAT (сомневаюсь что надёжно и безопасно, но я болт клал). Увеличиваем на 1 (для точности LBA), загружаем в ax значение кластера файла (номер следующего кластера) из таблицы FAT. Проверяем является ли загруженный кластер чётным или нечётным (в FAT12 это важно), если он нечётный обнуляем старшие четыре бита, а если чётный — младшие 4 бита и сдвигаем «вправо» на 4 бита. Вот мы и получили адрес следующего кластера таблицы FAT. После загрузки этого кластера (разберём далее) необходимо проверить является он последним или следом за ним есть ещё кластер: сравниваем значение кластера в таблице FAT с 0x0ff7, если оно больше или равно — значит это последний кластер файла, если меньше — то загружаем следующий. Немного запутанно, но объяснил 🙂

От si (текущий кластер в si, следующий в ax) отнимаем адрес, по которому загрузили таблицу FAT. И пожалуй начнём загрузку этого кластера файла:

  .load: ; load file sector     push ax ; save next cluster addr     sub si, 3 ; cluster addr -> LBA     mov ax, [ROOT_DIR_ENTRIES] ; ax = ROOT_DIR_ENTRIES * 32 / 512     mov cx, 32 ; /     mul cx  ; /     mov cx, [BYTES_PER_SECTOR] ;      div cx ; ax = count of sectors for ROOT_DIR     push ax ; save ROOT_DIR_SECTORS_CNT     mov ax, [SECTORS_PER_FAT] ; ax = FATS_CNT * SECTORS_PER_FAT     mov cl, [FATS_CNT]     mul cl ; ax = FAT_SIZE     add ax, [RSVD_SECTORS] ; ax = FATS + RSVDS     add ax, si ; ax = LBA + ax     pop cx ; pop ROOT_DIR_SECTORS_CNT     add ax, cx ; ax = ax + ROOT_DIR_SECTORS_CNT | READ CLUSER NUM     mov cx, 1 ; sectors to read     call read_sectors ; read file sector     pop ax ; pop NextFileClustAddr ; ............... Отрезок кода ниже - уже разбирали ...............     cmp ax, 0x0ff7 ; cmp to last cluster     mov si, ax ; si = NextFileClustAddr     jc .next_Clust ; if not endClust  ; ............... Отрезок кода выше - уже разбирали ...............     .start_kernel: ; startup kernel 

Отнимаем от si 3 — первые две записи в FAT заняты корневым каталогом и меткой тома, а три отнимаем потому, что мы увеличивали si на 1 в предыдущем коде (для простоты). Вычисляем размер корневого каталога в кластерах и прибавляем его, а так же прибавляем к si размер таблиц FAT в секторах и количество резервных секторов (напоминаю, у меня 1 сектор = 1 кластеру). Вот мы и вычислили LBA адрес считываемого нами сектора файла. Считываем, предварительно сохранив (в самом начале этого кода) адрес следующего кластера в таблице FAT. После считывания восстанавливаем адрес следующего кластера из стека. Проверяем последний ли это кластер и всё «по новой», а если кластер последний, то стоит уже запустить ядро передав ему управление.

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

.start_kernel: ; startup kernel    cli  ; off interraps   xor eax, eax ; eax = 0   mov ax, ds ; ax = ds (0)   shl eax, 4 ; ax << 4   add eax, START_gdt ; ax = ds << 4 + START_gdt   mov [GDTR_+2], eax ; save gdt linear addr   mov eax, END_gdt    sub eax, START_gdt ; eax = gdt_end - gdt_start /|\ gdt_start   mov [GDTR_], ax ; save gdt_size   lgdt [GDTR_] ; load gdt    mov eax, cr0 ; go to 32bit mode   or al, 1 ; cr0 last byte on   mov cr0, eax ; 32 bit mode - turn on    jmp 08h:0x7E00 .next_fn: ; ... какой-то код который разбирали выше ...  START_gdt:     .null   dq 0     .Kcode  dq 0x00CF9A000000ffff     .Kdata  dq 0x00CF92000000ffff END_gdt:  GDTR_:     dw 0     dd 0 

Отключаем прерывания. Очищаем регистр eax, загружаем в ax значение сегмента данных (ds), сдвигаем «влево» eax на 4 бита. К получившемуся прибавляем адрес положения начала таблицы GDT в памяти. Эти все манипуляции необходимы для получения линейного адреса GDT в памяти. Сохраняем его в 32-битное поле переменной GDTR_ (именно она загружается в регистр gdt командой lgdt). В первом, 16-битном поле переменной GDTR_ хранится размер таблицы (в байтах) gdt — его вычисление гораздо проще: отнимаем от адреса концаgdt адрес его начала и сохраняем в 16-битное поле. Далее загружаем в регистр eax значение cr0 , устанавливаем младший бит в единицу (включаем линию a20) и сохраняем в cr0 новое значение. Всё, мы 32 битном режиму и спокойно передаём управление ядру, выполнив дальний прыжок. Теперь адресация сегментов идёт по таблице gdt , смещение в ней = адрес сегмента.

Как устроена gdt

Это очень интересная вещь, но новичкам (мне тоже) порой бывает трудно в ней разобраться. Я постараюсь объяснить максимально просто.

GDT расшифровывается и переводится, как глобальная таблица дескрипторов. Это двоичная структура характерная для процессоров с архитектурой IA-32 и x86-64. В ней описываются сегменты памяти (данные о том, что и как хранят в себе эти сегменты, а так же как к ним возможно обратиться. При этом в регистр процессора gdt загружается не сама таблица, а структура, содержащая её линейный адрес в памяти и размер в байтах.

Таблица gdt состоит из множества 8-байтных записей, первая запись всегда равно нулю (иногда её используют, как некое хранилище данных о самой таблице, но я не рекомендую так делать — может выйти ошибка).

Каждая запись в таблице состоит из следующих частей: база — линейный 32-битный адрес начала сегмента в памяти; лимит — 20-битное число обозначающее максимальный адрес (база+лимит) сегмента (так сказать «потолок» сегмента);байт доступа и 4 бита флагов. Байт доступа состоит из:

бит

описание

0 (A)

бит доступа, устанавливается CPU. Оставьте его в 0 и забудьте

1 (RW)

если сегмент кода: 0 — чтение сегмента запрещено, 1 — разрешено.
если сегмент данных: 0 — запись в сегмент запрещена, 1 — разрешена.

2 (DC)

если сегмент данных: 0 — сегмент «растёт» вверх, 1 — сегмент «растёт» вниз подобно стеку (в этом случае база должна быть больше лимита).
если сегмент кода: 0 — код может быть вызван (исполнен) только с области с таким же DPL, как у этого сегмента, 1 — код может быть выполнен с области с таким же или ниже уровнем привилегии (DPL).

3 (E)

если 0 — сегмент данных, если 1 — сегмент кода

4 (S)

если 0 — системный сегмент (сегмент состояния задачи, например), 1 — сегмент данных или кода.

5-6 (DPL)

уровень привилегий сегмента, где 0 — высшие привилегии (ядро/система), 3 — низшие привилегии (пользователь)

7 (P)

для любого валидного (исправного) сегмента — 1

Биты флагов:

бит

описание

1 (RESERVED)

установите в 0 и забудьте

2 (L)

1 — сегмент 64-битный; 0 — другая разрядность

3 (DB)

0 — 16-битный сегмент; 1 — 32-битный сегмент

4 (G)

0 — число указанное в лимите, указано в байтах; 1 — число указанное в лимите, указано в блоках по 4 килобайта.

Подробнее рекомендую ознакомиться здесь.

Давайте напишем простейшее 32-битное ядро:

use32  org 0x7E00  _main:     mov ax, 0x10     mov fs, ax     mov ds, ax      mov es, ax      mov gs, ax      push ax     pop ss     mov sp, 0x7Bff  .white_screen:     mov eax, 40      mov ecx, 25     mul ecx      mov ecx, 2     mul ecx     mov bx, 0x7700     mov ecx, eax      .cycle:         mov eax, [VideoTextAddr]         add eax, ecx          mov [eax], bx         cmp ecx, 0          je .end         sub ecx, 2         jmp .cycle     .end:      push Hi_MSG     mov eax, 13     push eax      mov eax, 11     push eax     call print_str      jmp $ ; args: strAddr (4bytes) | xPos(4bytes) | yPos(4bytes) print_str:      push ebp     mov ebp, esp      push ecx     push edx      ; calc the CONSOLE_MAX_SIZE     sub esp, 4 ; ebp-4 = CONSOLE_SIZE = var1     mov eax, 40 ; columns (X)     mov ecx, 25 ; rows (Y)     mul ecx ; 40 * 25     mov ecx, 2 ; bytes per symbol in console     mul ecx ; eax = CONSOLE_SIZE     add eax, [VideoTextAddr] ;      mov [ebp-4], eax ; var1 = CONSOLE_SIZE      ; calc the cursor position in 40x25 console     mov ecx, [ebp+8]     mov eax, 40     mul ecx      add eax, dword [ebp+12]     mov ecx, 2      mul ecx       add eax, [VideoTextAddr] ; done      mov ecx, [ebp+16] ; strAddr     ; output string while cycle not meet 0-terminator     .while:         mov edx, [ebp-4] ; edx = MAX_CONSOLE_SIZE         cmp eax, edx  ; eax = CURRENT_POSITION | edx = MAX_CONSOLE_SIZE         jnb .err1_print_exit ; if eax >= edx         push eax ; save current position         mov al, byte [ecx] ; al = string symbol         cmp al, 0 ;          je .print_exit ; if al == 0         mov ah, 0xf0 ; ah = symbol color ( ax = color+symbol)         inc ecx ; *strAddr++;          pop edx ; edx = current position          mov word [edx], ax         mov eax, edx          add eax, 2         jmp .while          .err1_print_exit:         mov eax, 0xffffffff     .print_exit:         xor eax, eax         add esp, 4         pop edx         pop ecx         mov esp, ebp         pop ebp     ret  VideoTextAddr dd 0x000B8000 Hi_MSG db "HELLO WORLD!!!",0

Здесь всё максимально просто: сначала настраиваем регистры в соответствии с gdt. Далее, очищаем экран и изменяем цвет текста и фона его на белый (экран заполняется белым цветом). Начало области данных видеокарты, а конкретно выбранного нами режима вывода на экран находится по адресу 0xB80000. На каждый выводимый символ выделено 2 байта — 1 байт на цвет фона и символа, а второй на сам символ. Далее, начиная с 11 строчки и 13 столбца на экран выводится сообщение с переменной Hi_MSG.

Компилируем командой: FASM.exe system16.asm system16.bin и с помощью программы по работе с образами дисков переносим файл на образ. Запускаем виртуальную машину и наслаждаемся результатом кропотливого труда!


Спасибо за уделённое время, надеюсь вам хватило чая и конфет на прочтение статьи!
Исходный код (без используемых программ) можете найти здесь.

Использованная литература, указана в качестве ссылок по ходу статьи. Буду рад советам или конструктивной критике. До новых встреч!

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

У вас когда либо был опыт в написание своей ОС?

18.97%да22
56.03%нет65
25%думаю над этим29

Проголосовали 116 пользователей. Воздержались 13 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/921490/