Своя ОС?

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


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

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

  • HxD hex-редактор
  • ЯП — fasm
  • ultraISO в качестве программы для создания и редактирования образов дисков
  • VMBox — виртуальная машина, хотя во многих туториалах и гайдах использовали qemu (я просто с ним не разобрался)

На_стройки

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

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

Далее напишем простой 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. Она проверяет наличие и исправность компонентов необходимых для нормальной работы компьютера. А затем ищет среди устройств хранения данных (жёсткие/гибкие диски и т.п.), тот, в последних двух байтах первого сектора которого находится специальная запись двухбайтовая 0x550xAA, указывающая, что он содержит загрузчик ядра. Обычно сектор равняется 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























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 H
cylinder = \frac{LBA - (sector-1) -head \times S}{H \times S}

С этим разобрались, а как же считывать данные с диска? Для этого есть прерывание 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.

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


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

.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


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

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

Добавить комментарий