Дарова! Сегодня я поделюсь с вами опытом, как я пытался написать собственную ОС и, что из этого вышло. Запасайтесь чайком с печеньками и присаживайтесь поудобнее! Пора окунуться в 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:

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

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

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

Нажимаем создать. Открывается меню как на картинке, где имя
пишем что пожелаем, остальные поля оставляем пустым. Тип устанавливаем 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
где 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 |
номер функции ( |
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 |
Верхние два бита зарезервированы и за редким исключением - обнулены. Значения: |
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 - разрешено. |
2 (DC) |
если сегмент данных: 0 - сегмент "растёт" вверх, 1 - сегмент "растёт" вниз подобно стеку (в этом случае база должна быть больше лимита). |
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
и с помощью программы по работе с образами дисков переносим файл на образ. Запускаем виртуальную машину и наслаждаемся результатом кропотливого труда!
Спасибо за уделённое время, надеюсь вам хватило чая и конфет на прочтение статьи!
Исходный код (без используемых программ) можете найти здесь.
Использованная литература, указана в качестве ссылок по ходу статьи. Буду рад советам или конструктивной критике. До новых встреч!
Комментарии (9)
NeriaLab
30.07.2025 04:16Круто - слов нет, одни приятные эмоции. А что Вас побудило создать её?
Procher Автор
30.07.2025 04:16Спасибо ♥️
Приятно получать такие комментарии. К написанию меня побудило то, что я бросил себе вызов, "смогу ли я?". И "научный" интерес как ОС работают "изнутри"
В дальнейшем планирую улучшать её, или даже перейти в UEFI
protorus
30.07.2025 04:16"...Регистр начала стека
sp
устанавливаем0x7Bff
- ближайшая пустая область в оперативной памяти. А регистр конца стекаss
обнуляем (стек растёт вниз)..."Так неправильно, нужно 0x7С00, (выравнивание).
HardWrMan
Ох уж эти попытки структурирования в листинге ассемблера...