Repository: thedenisnikulin/os-project
Branch: master
Commit: 33f060c4b5b9
Files: 66
Total size: 133.5 KB
Directory structure:
gitextract_w89a63ku/
├── .bashrc
├── .gitignore
├── README.md
├── guide/
│ ├── 00-BOOT-SECTOR/
│ │ ├── ex00/
│ │ │ ├── README.md
│ │ │ └── main.asm
│ │ ├── ex01/
│ │ │ └── main.asm
│ │ ├── ex02/
│ │ │ ├── main.asm
│ │ │ └── org_demo.asm
│ │ ├── ex03/
│ │ │ └── main.asm
│ │ ├── ex04/
│ │ │ ├── main.asm
│ │ │ └── print_string.asm
│ │ ├── ex05/
│ │ │ ├── main.asm
│ │ │ └── print_hex.asm
│ │ ├── ex06/
│ │ │ └── main.asm
│ │ ├── ex07/
│ │ │ ├── disk_load.asm
│ │ │ └── main.asm
│ │ └── ex08/
│ │ ├── gdt.asm
│ │ ├── main.asm
│ │ ├── print_string_pm.asm
│ │ └── switch.asm
│ └── 01-KERNEL/
│ ├── ex00/
│ │ ├── BUILD.md
│ │ ├── README.md
│ │ ├── boot/
│ │ │ ├── bootsect.asm
│ │ │ ├── disk_load.asm
│ │ │ ├── gdt.asm
│ │ │ ├── kernel_entry.asm
│ │ │ ├── print_hex.asm
│ │ │ ├── print_string.asm
│ │ │ ├── print_string_pm.asm
│ │ │ └── switch.asm
│ │ ├── build/
│ │ │ └── Makefile
│ │ └── kernel/
│ │ └── kernel.c
│ └── ex01/
│ ├── boot/
│ │ ├── bootsect.asm
│ │ ├── disk_load.asm
│ │ ├── gdt.asm
│ │ ├── kernel_entry.asm
│ │ ├── print_hex.asm
│ │ ├── print_string.asm
│ │ ├── print_string_pm.asm
│ │ └── switch.asm
│ ├── build/
│ │ └── Makefile
│ ├── common.c
│ ├── common.h
│ ├── drivers/
│ │ ├── lowlevel_io.c
│ │ ├── lowlevel_io.h
│ │ ├── screen.c
│ │ └── screen.h
│ └── kernel/
│ └── kernel.c
└── src/
├── boot/
│ ├── bootsect.asm
│ ├── disk_load.asm
│ ├── gdt.asm
│ ├── kernel_entry.asm
│ ├── print_hex.asm
│ ├── print_string.asm
│ ├── print_string_pm.asm
│ └── switch.asm
├── build/
│ └── Makefile
├── common.c
├── common.h
├── drivers/
│ ├── lowlevel_io.c
│ ├── lowlevel_io.h
│ ├── screen.c
│ └── screen.h
└── kernel/
├── kernel.c
├── utils.c
└── utils.h
================================================
FILE CONTENTS
================================================
================================================
FILE: .bashrc
================================================
function emu() { # compile and run emulator
nasm $1.asm -f bin -o temp.bin
qemu-system-x86_64 temp.bin
}
================================================
FILE: .gitignore
================================================
*.bin
*.raw
# -- automatically generated --
# Prerequisites
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.map
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
================================================
FILE: README.md
================================================
# os-project
>Пишем свою собственную операционную систему с нуля!
Идея написать ОС возникла у меня в процессе поиска идеи для сайд-проекта. Это исключительно хобби-проект, не рассчитанный на серьезность и достоверность, и хотя я пытался объяснить многие новые и неочевидные концепты, с которыми я столкнулся в процессе разработки, я мог что-то упустить, так как я сам только учусь - именно поэтому я настоятельно рекомендую пользоваться гуглом и любыми другими источниками информации когда вы познакомитесь с чем-то новым в гайде. Гуглите абсолютно всё. Я серьезно.
**Prerequisites: **Для комфортного прохождения гайда нужно уметь программировать на языке Си на базовом уровне (одно из обязательных требований: понимать принципы работы с указателями), иметь опыт разработки на высокоуровневых ЯП. С синтаксисом ассемблера можно ознакомиться по ссылке ниже, но все же рекомендую побольше почитать или посмотреть по нему туториалов.
## Навигация по репозиторию
**`guide/`** --- гайд с последовательными уроками, теорией и задокументированным кодом
* Гайд разделен на главы, например `00-BOOT-SECTOR`
* Главы разделены на упражнения, например `ex00`
* Упражнения содержат в себе код и теорию. Выглядят как `main.asm`
**`src/`** --- исходный код ОС
## Установка и запуск
1. Установить эмулятор QEMU (подробнее: https://www.qemu.org/download/)
```
sudo apt install qemu-kvm qemu
```
2. Собрать кросс-компилятор gcc для i386 архитектуры процессора. Удобнее использовать готовый отсюда: https://wiki.osdev.org/GCC_Cross-Compiler#Prebuilt_Toolchains. Для компьютеров на Linux с x86_64 архитектурой:
```
wget http://newos.org/toolchains/i386-elf-4.9.1-Linux-x86_64.tar.xz
mkdir /usr/local/i386elfgcc
tar -xf i386-elf-4.9.1-Linux-x86_64.tar.xz -C /usr/local/i386elfgcc --strip-components=1
export PATH=$PATH:/usr/local/i386elfgcc/bin
```
3. Клонировать и собрать проект
```
git clone https://github.com/thedenisnikulin/os-project
cd os-project/src/build
make
```
4. Запустить образ ОС с помощью эмулятора
```
qemu-system-i386 -fda os-image.bin
```
## Справочник по синтаксису ассемблера NASM
https://www.opennet.ru/docs/RUS/nasm/nasm_ru3.html
---
## Дополнительная информация
Ссылки на полезный материал которым я пользовался в качестве теории.
> На русском языке:
- Серия статей о ядре Linux и его внутреннем устройстве: https://github.com/proninyaroslav/linux-insides-ru
- Статья "Давай напишем ядро!": https://xakep.ru/2018/06/18/lets-write-a-kernel/
> На английском языке:
- Небольшая книга по разработке собственной ОС (70 страниц): https://www.cs.bham.ac.uk/~exr/lectures/opsys/10_11/lectures/os-dev.pdf
- Общее введене в разработку операционных систем: https://wiki.osdev.org/Getting_Started
- Туториал по разработке ядра операционной системы для 32-bit x86 архитектуры. Первые шаги в создании собсвтенной ОС: https://wiki.osdev.org/Bare_Bones
- Продолжение предыдущего туториала: https://wiki.osdev.org/Meaty_Skeleton
- Про загрузку ОС (booting): https://wiki.osdev.org/Boot_Sequence
- Список туториалов по написанию ядра и модулей к ОС: https://wiki.osdev.org/Tutorials
- Внушительных размеров гайд по разработке ОС с нуля: http://www.brokenthorn.com/Resources/OSDevIndex.html
- Книга, описывающая ОС xv6 (не особо вникал, но должно быть что-то годное): https://github.com/mit-pdos/xv6-riscv-book, сама ОС: https://github.com/mit-pdos/xv6-public
- "Небольшая книга о разработке операционных систем" https://littleosbook.github.io/
- Операционная система от 0 до 1 (книга): https://github.com/tuhdo/os01
- ОС, написанная как пример для предыдущей книги: https://github.com/tuhdo/sample-os
- Интересная статья про программирование модулей для Линукса и про системное программирование https://jvns.ca/blog/2014/09/18/you-can-be-a-kernel-hacker/
- Еще одна статья от автора предыдущей https://jvns.ca/blog/2014/01/04/4-paths-to-being-a-kernel-hacker/
- Пример простого модуля к ядру линукса: https://github.com/jvns/kernel-module-fun/blob/master/hello.c
- Еще один туториал о том, как написать ОС с нуля: https://github.com/cfenollosa/os-tutorial
- Статья "Давайте напишем ядро": https://arjunsreedharan.org/post/82710718100/kernels-101-lets-write-a-kernel
- Сабреддит по разработке ОС: https://www.reddit.com/r/osdev/
- Большой список идей для проектов для разных ЯП, включая C/C++: https://github.com/tuvtran/project-based-learning/blob/master/README.md
- Еще один список идей для проектов https://github.com/danistefanovic/build-your-own-x
- "Давайте напишем ядро с поддержкой ввода с клавиатуры и экрана": https://arjunsreedharan.org/post/99370248137/kernel-201-lets-write-a-kernel-with-keyboard-and
================================================
FILE: guide/00-BOOT-SECTOR/ex00/README.md
================================================
# Интро
Когда мы включаем компьютер, он должен каким-то образом загрузить операционную систему. Он может загрузить ее с дискеты, с флэшки, с жесткого диска или с каих-либо других носителей.
Среда, которую дает нам компьютер вне ОС, может предоставить нам не так уж и много. Например, мы имеем BIOS (англ. Basic Input/Output Software), набор програм которые изначально загружены в памят и инициализированы как только компьютер включается. БИОС предоставляет базовый контроль над важными девайсами компьютера: экран, клавиатура, жесткий диск.
Чтобы БИОСу загрузить ОС, ему нужно узнать, есть ли ОС на определенном носителе. Для этого он читает первые 512 байтов носителя которые называются загрузочным сектором (англ. boot sector) и если они кончаются магическим числов 0xaa55,то он загружает код бут сектора в память, а процессор его исполняет.
================================================
FILE: guide/00-BOOT-SECTOR/ex00/main.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex00 / main.asm
; Title: Простая программа загрузочного сектора (boot sector), которая
; запускает бесконечный цикл.
; ------------------------------------------------------------------------------
; Description:
; Чтобы дать понять BIOS, что на текущей флэшке, компакт диске или жестком
; диске расположена ОС, на этом носителе должен быть загрузочный сектор. BIOS
; узнает загрузочный сектор по "магическому числу" из 2-х байт, равное 0xaa55
; (в шестнадцатеричной системе исчисления). Простейший загрузочный сектор в
; машинном коде выглядит так:
;
; e9 fd ff 00 00 00 00 00 00 00 00 00 00 00 00 00
; 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
; *
; 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa
;
; Как видим, последние 2 байта действительно являются "магическим числом".
; Первые 3 байта являются машинными инструкциями, которые запускают
; бесконечный цикл, а остальное просто заполнено нулями т.к. наша программа
; должна быть размером ровно 512 байт.
;
; Чтобы перевести наш файл boot_sect.asm в машинный код, нужно написать:
; nasm boot_sect.asm -f bin -o boot_sect.bin
; Попробуйте запустить boot_sect.bin с помощью эмулятора (например, qemu), и
; вы увидите "Booting from Hard Disk...", хотя если вы измените "магическое
; число", соберете файл boot_sect.bin заново и запустите с помощью эмулятора,
; то BIOS попытается загрузить ОС, но в итоге напишет "No bootable device".
; А значит, у нас получилось создать загрузочный сектор!
; ------------------------------------------------------------------------------
; Бесконечный цикл:
loop: ; Определяем метку "loop"
jmp loop ; Инструкция "jmp" позволяет нам перейти к метке
; "loop" (англ. jump - прыжок), т.е. создается
; бесконечный цикл.
times 510-($-$$) db 0 ; Инструкция "times" заставляет идущую после нее
; команду выполняться опредленное количество раз, т.е.
; times <кол-во раз> <команда>.
; Токен $ высчитывает позицию начала текущей строки,
; токен $$ - позицию начала текущей секции.
; 510-($-$$) = 510-(2-0) = 508 байт. Чтобы лучше понять
; как это работает, рекомендую посмотреть как программа
; выглядит в машинном коде с помощью утилиты ghex.
; Таким образом мы заполняем пространство нулями (db 0),
; приводя нас к 510-му байту.
; db расшифровывается как "declare bytes", т.е.
; "объявить байты"
dw 0xaa55 ; В последние два байта кладем "магическое число",
; чтобы BIOS знал, что это загрузочный сектор
================================================
FILE: guide/00-BOOT-SECTOR/ex01/main.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex01 / main.asm
; Title: Простая программа загрузочного сектора (boot sector), которая
; выводит "Hello" на экран, используя рутину BIOS'а
; ------------------------------------------------------------------------------
; Description:
; Чтобы вывести символ на экран, мы воспользуемся "scrolling tele-type BIOS
; routine", то есть специальной рутиной BIOS, которая выводит символ на экран
; и перемещает курсор, чтобы быть готовым напечатать следующий символ. Чтобы
; воспользоваться этой рутиной, нужно переместить в регистр ah число 0x0e,
; а также использовать прерывания (синтаксис прерываний в ассемблере -
; "int <номер прерывания>"). Прерывания - это механизм, с помощью которого
; процессор может быть прерван от выполнения того, чем он сейчас занят, чтобы
; выполнить какую-то другую команду. Мы будем использовать прерывание 0x10
; (это число - индекс обработчика прерывания в ISR (interrupt service
; routines). ISR это последовательность команд, ответсвенных за прерывание.
; Нам нужна команда под индексом 0x10), функцией которого является
; предоставление графических сервисов, вывод строк на экран и т.д.
; ------------------------------------------------------------------------------
mov ah, 0x0e ; Перемещаем число 0x0e в регистр ah, указывая BIOS'у
; что нам нужна рутина tele-type, то есть режим вывода
; информации на экран
mov al, 'H' ; Перемещаем ASCII код символа 'H' в регистр al (команда
int 0x10 ; mov), вызываем прерывание 0x10, которое выводит
mov al, 'e' ; на экран значение регистра al
int 0x10
mov al, 'l'
int 0x10
mov al, 'l'
int 0x10
mov al, 'o'
int 0x10
; ------------------------------------------------------
jmp $ ; Знак $ вычисляет позицию начала строки, содержащей
; выражение, то есть мы прыгаем к началу этой же строки,
; создавая бесконечный цикл
times 510-($-$$) db 0 ; Заполняем ненужные байты нулями
dw 0xaa55 ; Вставляем в конец "магическое число"
================================================
FILE: guide/00-BOOT-SECTOR/ex02/main.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex02 / main.asm
; Title: Простая программа загрузочного сектора (boot sector), которая
; демонстрирует работу адресов
; ------------------------------------------------------------------------------
; Description:
; Наша цель - разобраться как работать с памятью на языке Ассемблер и узнать,
; по какому адресу хранится загрузочный сектор (спойлер: по адресу 0x7c00).
; Для этого мы попробуем вывести символ 'Х' на экран 4-мя разными способами.
; Подготовка: если addr - адрес, то [addr] - значение на которое указывает
; адрес
; ------------------------------------------------------------------------------
mov ah, 0x0e ; Перемещаем число 0x0e в регистр ah, указывая BIOS'у
; что нам нужна рутина tele-type, то есть режим вывода
; информации на экран
mov al, '1' ; Выводим номер способа (нужно лишь нам для понятности)
int 0x10
mov al, the_secret ; #1: перемещаем адрес the_secret.
int 0x10 ; На экран выведется мусор, т.к. мы переместили в al
; сам адрес, а не хранящееся в нем значение.
mov al, '2' ; Выводим номер способа
int 0x10
mov al, [the_secret] ; #2: перемещаем значение по адресу the_secret.
int 0x10 ; Может показаться вполне корректным, но на экран снова
; выведется мусор, т.к the_secret объявлен не в том же
; месте, что и наш загрузочный сектор, и поэтому мы не
; сможем таким образом забрать из него 'X'.
mov al, '3' ; Выводим номер способа
int 0x10
mov bx, the_secret ; #3: перемещаем адрес the_secret в регистр bx,
add bx, 0x7c00 ; добавляем к этому адресу адрес загрузочного сектора,
mov al, [bx] ; выводим значение по адресу bx.
int 0x10 ; Вот теперь то мы и сможем вывести 'X'. Суть в том, что
; чтобы мы смогли добраться до корректного значения
; которое мы объявили, адрес этого значения должен быть
; в пределах адресного пространства загрузочного
; сектора. Т.е. мы должны прибавить к адресу the_secret
; адрес, с которого начинается загрузочный сектор.
; то есть если the_secret = 0x2d, то прибавляя к нему
; адрес загрузочного сектора 0x7c00 мы получим 0x7c2d.
; Такая техника называется смещением (англ. offset)
; Именно по этому адресу мы сможем достать 'X'.
mov al, '4' ; Выводим номер способа
int 0x10
mov al, [0x7c2d] ; #4: перемещаем значение адреса, который мы
int 0x10 ; захардкодили (т.е. специально сами посчитали, заранее,
; не программно) Таким способом у нас тоже получится
; вывести 'X', но это плохой способ, т.к. адрес может
; в итоге отличаться.
; ------------------------------------------------------
jmp $ ; бесконечный цикл
the_secret: ; Объявляем метку the_secret,
db 'X' ; объявляем байт, и инициализируем его с ASCII кодом,
; соответствующим символу 'X'
times 510-($-$$) db 0 ; Заполняем ненужные байты нулями
dw 0xaa55 ; Вставляем в конец "магическое число"
================================================
FILE: guide/00-BOOT-SECTOR/ex02/org_demo.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex02 / org_demo.asm
; Title: Простая программа загрузочного сектора (boot sector), которая
; демонстрирует работу адресов, используя директиву [org <адрес>]
; ------------------------------------------------------------------------------
; Description:
; Мы выяснили в предыдущем упражнении, что чтобы процессор корректно нашел
; объявленное нами значение, нужно добавлять 0x7c00 к адресу этого значения
; (т.е. "сместить" адрес на 0x7c00, англ. offset - смещение). Это довольно
; неудобно, и поэтому ассемблер позволяет нам указать "глобальное смещение" с
; помощью директивы [org <адрес>]. Посмотрим как это работает.
; ------------------------------------------------------------------------------
[org 0x7c00] ; Указываем "глобальное смещение"
mov ah, 0x0e ; Перемещаем число 0x0e в регистр ah, указывая BIOS'у
; что нам нужна рутина tele-type, то есть режим вывода
; информации на экран
mov al, '1' ; Выводим номер способа для удобства
int 0x10
mov al, the_secret ; #1: перемещаем адрес the_secret.
int 0x10 ; На экран выведется мусор, т.к. мы переместили в al
; сам адрес, а не хранящееся в нем значение.
mov al, '2' ; Выводим номер способа
int 0x10
mov al, [the_secret] ; #2: перемещаем значение по адресу the_secret.
int 0x10 ; В этот раз мы сможем вывести 'X', т.к. [the_secret]
; будет разыменован по адресу the_secret + 0x7c00
; благодаря "глобальному смещению"
mov al, '3' ; Выводим номер способа
int 0x10
mov bx, the_secret ; #3: перемещаем адрес the_secret в регистр bx,
add bx, 0x7c00 ; добавляем к этому адресу адрес загрузочного сектора,
mov al, [bx] ; выводим значение по адресу bx.
int 0x10 ; Мы не сможем вывести 'X', т.к. лишний раз делаем
; смещение: 0x7c00 + the_secret + 0x7c00
mov al, '4' ; Выводим номер способа
int 0x10
mov al, [0x7c2d] ; #4: перемещаем значение адреса, который мы
int 0x10 ; захардкодили (т.е. специально сами посчитали, заранее,
; не программно) Таким способом у нас тоже получится
; вывести 'X', но это плохой способ, т.к. адрес может
; в итоге отличаться.
; ------------------------------------------------------
jmp $ ; бесконечный цикл
the_secret: ; Объявляем метку the_secret,
db 'X' ; объявляем байт, и инициализируем его с ASCII кодом,
; соответствующим символу 'X'
times 510-($-$$) db 0 ; Заполняем ненужные байты нулями
dw 0xaa55 ; Вставляем в конец "магическое число"
================================================
FILE: guide/00-BOOT-SECTOR/ex03/main.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex03 / main.asm
; Title: Простая программа загрузочного сектора, которая демонстрирует
; работу стека
; ------------------------------------------------------------------------------
; Description:
; Цель: разобраться как работать со стеком на языке Ассемблер.
; Теория:
; 1. Стек - структура данных, которая действует по принциу LIFO (last in first
; out). Со стеком можно провести две операции: добавить в стек элемент
; (добавлять можно только в конец) и взять элемент из стека (взять можно
; тоже только с конца). О стеке удобно думать как о стопке блинов.
; 2. Регистры для работы со стеком: BP и SP. BP содержит адрес начала стека,
; а SP - адрес конца стека (т.е. начала стопки блинов и ее макушка соответсв.)
; 3. Стек, с которым мы будем работать, "растет" вниз, то есть мы добавляем
; значения в стек по адресу меньше конца стека. Допустим, если адрес конца
; стека находится в регистре SP и равен 0x8000, то добавляя что-нибудь в стек,
; значение регистра SP станет 0x8000 - 0x2 (если размер этого "чего-нибудь"
; равен 2 байтам).
; 4. Операции со стеком: "push <значение>" чтобы добавить какое-либо значение
; в конец стека, "pop <регистр>" чтобы удалить последнее значение из стека и
; добавить его в указанный регистр.
; ------------------------------------------------------------------------------
mov ah, 0x0e ; Перемещаем число 0x0e в регистр AH, указывая BIOS'у
; что нам нужна рутина tele-type, то есть режим вывода
; информации на экран
mov bp, 0x8000 ; BP - регистр адреса начала стека, а SP - конца стека
mov sp, bp ; мы размещаем стек чуть выше адреса, из которого БИОС
; загружает наш загрузочный сектор (в адресе 0x8000),
; чтобы случайно не задеть загрузочный сектор.
; Переносим адрес BP в SP, т.к. изначально стек пустой,
; и поэтому адрес конца стека равен адресу начала
push 'A' ; Добавляем в стек символы
push 'B'
push 'C'
pop bx ; Переносим последнее значение из стека в регистр BX
; (помним, что значение sp увеличится, а не уменьшится,
; т.к. стек "растет" вниз)
mov al, bl ; в bl находится значение*, взятое из стека с помощью
; команды pop. Перемещаем его в регистр AL
int 0x10 ; Выводим 'C'
pop bx ; Переносим в регистр BX следующее значение из стека
mov al, bl
int 0x10 ; Выводим 'B'
mov al, [0x8000 - 0x2] ; Переносим в регистр AL значение по адресу 0x7ffe
; (0x8000 - 0x2), т.е. адрес начала стека минус 2 байта,
; для того чтобы подтвердить, что стек "растет" вниз
int 0x10 ; Выводим 'A'
; ------------------------------------------------------
jmp $ ; бесконечный цикл
the_secret: ; Объявляем метку the_secret,
db 'X' ; объявляем байт, и инициализируем его с числом,
; соответствующим символу 'X' в таблице ASCII
times 510-($-$$) db 0 ; Заполняем ненужные байты нулями
dw 0xaa55 ; Вставляем в конец "магическое число"
; ------------------------------------------------------------------------------
; * почему BL, а не BX? Регистр bx устроен так, что состоит из двух других
; регистров: BH и BL (оба размером 8 байт, а размер bx соответственно 16 байт)
; Подобным образом же устроен и регистр AX, т.е. AH и AL. Т.к. мы используем
; регистр AL, равный 8 байтам, чтобы выводить символы на экран, то нам нужно
; перемещать в этот регистр значение из регистра такого же размера. Поэтому
; мы перемещаем туда значение из BL. (гугли: регистры в ассемблере)
================================================
FILE: guide/00-BOOT-SECTOR/ex04/main.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex04 / main.asm
; Title: Простая программа загрузочного сектора, которая выводит на экран
; строку с помощью функции
; ------------------------------------------------------------------------------
; Description:
; Кратко о том, как работают функции:
; Чтобы вызвать функцию, используется команда call <метка>. Чтобы вернуть
; (выйти) из функции, используется команда ret.
; Команды call и ret используются в паре. Команда call помещает значение
; регистра EIP (в нем установлен адрес, следующий после call <метка>
; инструкции) в стек, а команда ret извлекает его и передаёт управление
; инструкции по этому адресу. Вы также можете определить аргументы, для
; вызываемой функции. Это можно сделать с помощью стека или регистров, т.е.
; перед вызовом функции занести параметры в стек или в определенный регистр,
; а в теле функции извлечь их.
; ------------------------------------------------------------------------------
[org 0x7c00]
mov bx, HELLO_MSG ; Перемещаем объявленную нами ниже строку в
; регистр BX. В ассемблере строки представляют
; из себя то же, что и строки в Си - идущие друг
; за другом ячейки памяти, в которых занесено
; значение символов этой строки, поэтому в BX
; в данный момент находится первый символ строки
; (а точнее его ASCII код).
call print_string ; Вызываем функцию print_string с помощью
; команды call
mov bx, GOODBYE_MSG
call print_string
; ------------------------------------------------------
jmp $ ; бесконечный цикл
%include "print_string.asm" ; директива %include вставляет весь код из
; файла print_string.asm в место, откуда она
; была вызвана.
HELLO_MSG:
db "Hello, world!", 0 ; Объявляем байты, содержащие строку с
; приветствием, заканчивающуюся нулем.
; На самом деле эти байты размещаются прямо в
; исполняемом файле, откомпилированном с помощью
; nasm. Вот так вот.
GOODBYE_MSG: ; Объявляем метку GOODBYE_MSG
db "Goodbye!", 0 ; Объявляем массив из символом, заканчивающийся
; нулем (гугли: нуль терминированная строка)
times 510-($-$$) db 0 ; Заполняем оставшиеся байты нулями
dw 0xaa55 ; Вставляем в конец "магическое число"
================================================
FILE: guide/00-BOOT-SECTOR/ex04/print_string.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex04 / print_string.asm
; Title: Функция вывода строки на экран
; ------------------------------------------------------------------------------
; Description: null
; ------------------------------------------------------------------------------
print_string: ; Функция вывода строки на экран.
pusha ; Когда мы используем функции, мы можем модифицировать
; регистры прямо в них, что нарушает чистоту функции,
; т.е. мы можем перезаписывать какие-то внешние
; данные. Для этого мы добавляем значение всех регистров
; в стек с помощью команды pusha, а в конце функции
; мы возвращаем регистрам их изначальные значения,
; которые возьмем из стека (команда popa).
mov ah, 0x0e ; tele-type mode
loop: ; Метка loop (= цикл)
mov al, [bx] ; Перемещаем значение BX в AL, т.к. мы помним что в BX
; лежит первый символ строки (см. ./main.asm)
cmp al, 0 ; Команда cmp для сравнения AL и 0.
je return ; (if) je = "jump if equal",
; т.е. перемещаемся к коду с меткой return если AL == 0
jmp put_char ; (else) в противном случае перемещаемся к put_char.
put_char: ; Метка put_char - вывод символа на экран.
int 0x10 ; Вызываем прерывание, которое позволяет вывести
; на экран значение регистра AL, в котором лежит [BX].
inc bx ; inc <регистр> - увеличить на 1.
jmp loop ; Возвращаемся обратно к циклу.
return: ; Метка return - завершаем функцию
popa ; Возвращаем регистрам их изначальные значения.
ret ; Заканчиваем выполнение функции.
================================================
FILE: guide/00-BOOT-SECTOR/ex05/main.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex05 / main.asm
; Title: Простая программа загрузочного сектора, которая выводит на экран
; шестнадцатеричное число (число 0x1fb6)
; ------------------------------------------------------------------------------
; Description: null
; ------------------------------------------------------------------------------
[org 0x7c00]
mov dx, 0x1fb6 ; Перемещаем 0x1fb6 в регистр DX чтобы потом
; задействовать его в функции print_hex
mov bx, HEX_OUT ; перемещаем HEX_OUT в BX
call print_hex ; Вызываем функцию print_hex
; ----------------------------------------------
jmp $ ; бесконечный цикл
%include "../ex04/print_string.asm" ; директива %include вставляет весь код из
%include "print_hex.asm" ; файла print_string.asm в место, откуда она
; была вызвана.
HEX_OUT: ; Объявляем метку HEX_OUT. Она нужна как шаблон,
; на котором мы будем отображать наше число.
db "0x0000", 0 ; Объявляем массив из символом, заканчивающийся
; нулем (гугли: нуль терминированная строка)
times 510-($-$$) db 0 ; Заполняем ненужные байты нулями
dw 0xaa55 ; Вставляем в конец "магическое число"
================================================
FILE: guide/00-BOOT-SECTOR/ex05/print_hex.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex05 / print_hex.asm
; Title: Функция вывода шестнадцатеричного числа на экран
; ------------------------------------------------------------------------------
; Description:
; Как выглядит регистр EAX (32 b = 32 бита):
;
; EAX (32 b)
; ---------------------------------------------
; | | | |
; | | AH (8 b) | AL (8 b) |
; | | | |
; ---------------------------------------------
; AX (16 b)
;
; Как видим, eax содержит в себе регистр AX, который в свою очередь разделен
; на AH (A HIGH, верхний) и AL (a low, нижний).
; ------------------------------------------------------------------------------
; Помним, что в main.asm:
; DX = 0x1fb6
; BX = "0x0000"
print_hex:
pusha ; Сохраняем значения регистров в стеке
mov cx, 0 ; Регистр CX будет служить счетчиком
loop1:
cmp cx, 4 ; if (CX < 4)
jl print ; Переходим к print
jmp end ; else переходим к end
print:
mov ax, dx ; В AX теперь 0x1fb6
and ax, 0x000f ; В AX теперь 6 (последняя цифра от 0x1fb6).
cmp ax, 9 ; if (AX > 9) (проверяем обозначается ли число
; буквой т.к. мы помним что цифра больше 9 в
; 16-ричной системе исчисления обозначается
; буквой латинского алфавита)
jg num_to_abc ; Переходим к num_to_abc
jmp next
num_to_abc: ; Перевод числа в букву (так, как она бы
; выглядела в 16-ричной СИ), например число 15
; в десятичной будет равно f в 16-ричной
add ax, 39 ; Добавляем к этому числу 39, чтобы затем еще
; добавить 48 ('0'), получая код
; соответсвующего символа в ASCII (например,
; f (как число, то есть 15) + 39 + 48 (код '0')
; = 102 (то есть 'f' в ASCII, как нам и нужно)
jmp next
next:
add ax, '0' ; Добавляем 48 в AX
mov bx, HEX_OUT + 5 ; Теперь bx указывает на последний символ строки
; HEX_OUT
sub bx, cx ; BX = BX - counter (для итерации)
mov [bx], al ; Так как мы разыменовываем BX (вот так: [BX]),
; то [BX] это не регистр, а ссылка на память,
; и так как AX = 16 бит (2 байта), чтобы нам не
; перезаписать лишнюю память, в [BX] мы помещаем
; не AX, а AL, размер которого равен 1-му байту.
ror dx, 4 ; было: 0x1fb6, стало: 0x61fb (переносим
; последнюю цифру в начало)
inc cx ; counter++
jmp loop1 ; переходим обратно к loop1
end:
mov bx, HEX_OUT ; Делаем так, чтобы bx снова указывал на первый
; символ строки HEX_OUT
call print_string ; выводи на экран строку из регистра BX
popa ; Возвращаем регистрам их изначальное значение
ret ; Заканчиваем выполнение функции
================================================
FILE: guide/00-BOOT-SECTOR/ex06/main.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex06 / main.asm
; Title: Простая программа загрузочного сектора (boot sector), которая
; демонстрирует работу сегментации
; ------------------------------------------------------------------------------
; Description:
; Когда процессор запущен в его начальном режиме (16-bit real mode),
; максимальный размер регистров = 16 бит, поэтому самый большой размер,
; который мы можем использовать это 0xffff, который равен примерно 64 KB.
; Чтобы преодолеть этот лимит, существуют специальные регистры, которые
; называются регистры сегментов - CS, DS, SS, ES, означающие Code, Data, Stack
; и Extra соответственно.
; Память разделена на сегменты, которые индексированы регистрами сегментов
; (т.е. к примеру в регистре DS лежит адрес начала Data-сегмента). Поэтому,
; когда мы указываем какой-либо 16-битный адрес, процессор автоматически
; высчитывает абсолютный адрес, сдвигая указанный нами адрес от начала нужного
; сегмента.
; Высчитывая абсолютный адрес, процессор умножает на 16 значение в регистре
; сегмента и добавляет указанный нами адрес. Например, если мы установим
; значение сегмента DS как 0x4d и попробуем сделать что-то вроде
; "mov ax, [0x20]", то значение, добавляемое в AX, будет загружено
; из адреса 0x4f0 (16 * 0x4d + 0x20).
; Как можно догадаться, с помощью сегментации мы можем добиться того же, что и
; с помощью директивы [org <адрес>], как мы делали в ex02/org_demo.asm.
; ------------------------------------------------------------------------------
mov ah, 0x0e ; Перемещаем число 0x0e в регистр AH, указывая BIOS'у
; что нам нужна рутина tele-type, то есть режим вывода
; информации на экран
mov al, '1' ; Выводим номер способа для удобства
int 0x10
mov al, [the_secret] ; #1: так как мы не указывали никакого смещения ни с
int 0x10 ; помощью org директивы ни с помощью сегментации,
; 'X' не выведется
mov al, '2' ; Выводим номер способа
int 0x10
mov bx, 0x7c0 ; т.к. мы не можем напрямую написать "mov ds, 0x7c0",
mov ds, bx ; используем BX как промежуточное хранилище
mov al, [the_secret] ; 'X' выведется, так как мы сделали смещение.
int 0x10 ; Стоит заметить, что мы переводим в DS 0x7c0, а не
; 0x7c00, так как помним, что значение, перемещаемое в
; AL будет загружено из адреса, который будет
; высчитываться как 16 * ds + the_secret,
; а 16 * 0x7c0 == 0x7c00, то есть так как нам и нужно.
mov al, '3' ; Выводим номер способа
int 0x10
mov al, [es:the_secret] ; Явно указываем процессору, использовать сегмент ES
int 0x10 ; (просто для демонстрации возможностей и чтобы понять
; как это работает)
; 'X' не выведется, т.к. мы не переместили в регистр ES
; число 0x7c0, и поэтому смещение на адрес 16 * 0x7c0
; выполнено не будет.
mov al, '4' ; Выводим номер способа
int 0x10
mov bx, 0x7c0 ; Используя BX как промежуточное хранилище, перемещаем
mov es, bx ; в регистр ES 0x7c0.
mov al, [es:the_secret] ; Явно указываем процессору сегмент ES
int 0x10 ; 'X' выведется.
; ------------------------------------------------------
jmp $ ; бесконечный цикл
the_secret:
db 'X'
times 510-($-$$) db 0 ; Заполняем ненужные байты нулями
dw 0xaa55 ; Вставляем в конец "магическое число"
================================================
FILE: guide/00-BOOT-SECTOR/ex07/disk_load.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / disk_load.asm
; Title: Функция чтения диска
; ------------------------------------------------------------------------------
; Description:
; Чтобы лучше понять, что здесь происходит, разберитесь с тем, что такое CHS
; по ссылке https://ru.wikipedia.org/wiki/CHS
; ------------------------------------------------------------------------------
disk_load:
push dx
mov ah, 0x02 ; Указвыаем БИОСу что нам нужна рутина чтения диска
; Указываем что нам нужно:
mov al, dh ; 1. Прочитать кол-во секторов, равное значению в DH
mov ch, 0x00 ; 2. Выбрать нулевой цилиндр
mov dh, 0x00 ; 3. Выбрать нулевую головку
mov cl, 0x02 ; 4. Начинать считывать со второго сектора (т.е.
; первый свободный сектор сразу после загруочного
; сектора, т.к. загрузочный сектор находится по
; адресу 0x01)
int 0x13 ; Вызываем прерывание для чтения
; У БИОСа может не получиться прочитать диск, и
; чтобы дать нам знать что произошла ошибка, он,
; во-первых, обновляет специальный флаг CF (carry
; flag) специальным значением, которое означает
; ошибку, а во-вторых, кладет в регистр AL кол-во
; секторов, которые у него получилось прочитать.
jc disk_error ; jc - инструкция для прыжка на указанную метку,
; которая выполняется только если CF (carry flag)
; сигнализирует об ошибке
pop dx ; Восстанавливаем регистр DX из стека
cmp dh, al ; если AL (кол-во прочитанных секторов) != DH
; (предполагаемое кол-во секторов),
jne disk_sectors_error ; то выводим на экран сообщение об ошибке и зависаем
; (то есть запускаем бесконечный цикл)
jmp disk_success
jmp disk_exit ; Заканчиваем выполнение функции
disk_success:
mov bx, SUCCESS_MSG
call print_string
jmp disk_exit
disk_error:
mov bx, DISK_ERR_MSG ; Перемещаем в BX сообщение об ошибке
call print_string ; Выводим его на экран
mov dh, al
call print_hex
jmp disk_loop ; бесконечный цикл
disk_sectors_error:
mov bx, SECTORS_ERR_MSG
call print_string
SUCCESS_MSG:
db "Disk was successfully read ", 0
DISK_ERR_MSG:
db "Disk read error! ", 0
SECTORS_ERR_MSG:
db "Incorrect number of sectors read ", 0
disk_loop:
jmp $
disk_exit:
ret
================================================
FILE: guide/00-BOOT-SECTOR/ex07/main.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex07 / main.asm
; Title: Простая программа загрузочного сектора, которая демонстрирует
; чтение с диска
; ------------------------------------------------------------------------------
; Description: Операционная система не уберется в 512 байтов, поэтому нам нужно
; уметь прочитать что-то с диска.
; ------------------------------------------------------------------------------
[org 0x7c00]
mov bp, 0x8000 ; Распологаем наш стек подальше в безопасное
mov sp, bp ; место
mov bx, 0x9000 ; Данные из секторов будут загружаться в
; адрес 0x0000(ES):0x9000(BX), т.е.
; (ES * 16 + BS), равный 0x90000
mov dh, 2 ; Загрузим 2 сектора
call disk_load ; Загружаем диск
mov dx, [0x9000] ; Выводим на экран первое загрузившееся
call print_hex ; "слово" (т.е. машинное слово = 2 байта)
; предполагая, что оно будет равно 0xdada
; (распологается по адресу 0x9000)
mov dx, [0x9000 + 512] ; Выводим на экран первое "слово" из 2-го
; загруженного нами сектора. Должно быть
; равно 0xface
call print_hex
jmp $
%include "../ex04/print_string.asm" ; Функция печати строки
%include "../ex05/print_hex.asm" ; Функция печати 16-ричного числа
%include "disk_load.asm" ; Функция чтения диска
HEX_OUT:
db "0x0000", 0
times 510-($-$$) db 0
dw 0xaa55
; БИОС загрузит только первые 512 байтов с диска, поэтому если мы специально
; добавим пару секторов (тоже по 512 байт), мы сможем убедиться в том что у
; нас получилось загрузить эти самые сектора.
; TODO: explain better.
times 256 dw 0xdada ; второй сектор
times 256 dw 0xface ; третий сектор
================================================
FILE: guide/00-BOOT-SECTOR/ex08/gdt.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / gdt.asm
; Title: Определяем GDT (глобальная таблица дескрипторов)
; ------------------------------------------------------------------------------
; Description:
; Способ, которым процессор переводит логический адрес в физический, в
; 32-битном защищенном режиме отличается от 16-битного реального режима.
; Вместо того, чтобы умножить значение регистра сегмента на 16 и прибавить к
; этому "смещение" (offset), регистр сегмента становится индексом
; определенного дескриптора сегмента в GDT.
; Дескриптор сегмента - это 8-битная структура, которая определяет свойства
; этого сегмента:
; - Base address (32 bits), определяющий откуда сегмент начинается в
; физической памяти.
; - Segment Limit (20 bits), определяющий размер сегмента
; - Различные флаги, которые устанавливают каким образом процессор будет
; "относиться" к сегментам, например уровень привилегий и т.д.
;
; Флаги:
; * 1-ые флаги:
; - present flag (флаг присутствия). Если его значение "1", то это
; указывает, что сегмент присутствует в памяти (это нужно для виртуальной
; памяти)
; - privilege flag (флаг привилегии). Значение "0" - самый высокий уровень
; привилегии
; - descriptor type (тип дескриптора). "1" - для сегмента кода или
; сегмента данных
; * Флаги типа:
; - code (флаг кода). "1" - для кода, "0" - для даннных
; - conformig (флаг подчинения). "0" - чтобы код в другом сегменте с
; более низким уровнем привилегий не смог вызвать код из этого сегмента -
; это ключ к защите памяти (memory protection).
; - readable (читаемость). "1" - если читаемый, "0" - только исполняемый.
; - writable. Разрешает сегменту данных быть записываемым, в противном
; случае, он будет доступен только для чтения.
; - accessed (флаг доступа). Этот флаг устанавливается, когда происходит
; обращение к сегменту.
; - expand down. Флаг (бит), позволяющий сегменту расширяться вниз.
; * 2-ые флаги:
; - granulariy (гранулярность). "0" - байтовая гранулярность, лимит
; задается в байтах, если "1" - страничная гранулярность, в 4кб блоках.
; Если выбрать страничную гранулярность и установить значение лимита как
; 0xfffff, то лимит умножится на 16*16*16 (4кб), и лимит станет 0xfffff000
; позволяя нашему сегменту занять 4гб места в памяти.
; - 32-bit default. "1" - т.к. наш сегмент будет содержать 32-битный код.
; - 64-bit code segment. "0" - т.к. не используется на 32-битных
; процессорах.
; - AVL (available). Определяет доступность сегмента для использования
; системным программным обеспечением (используются только ОС).
; ------------------------------------------------------------------------------
gdt_start: ; Эта пустая метка нужно для того чтобы удобнее
; посчитать размер GDT для ее дескриптора
; (end - start)
gdt_null: ; Необходимый нулевой дескриптор для GDT
dd 0x0 ; dd - define double (двойное "слово"", т.е.
dd 0x0 ; 4 байта).
gdt_code: ; Определяем дескриптор сегмента кода
dw 0xffff ; Limit (bits 0-15)
dw 0x0 ; Base (bits 0-15)
db 0x0 ; Base (bits 16-23)
db 10011010b ; Первые флаги + флаги типа (смотрим по битам)
; present: 1, privilege: 00, descriptor type: 1
; code: 1, conforming: 0, readable: 1, accessed: 0
db 11001111b ; Вторые флаги + длина сегмента (bits 16-19):
; granularity: 1, 32-bit default: 1,
; 64-bit default: 0, AVL: 0
db 0x0 ; Base (bits 24-31)
gdt_data: ; Определяем дескриптор сегмента кода
dw 0xffff ; Limit (bits 0-15)
dw 0x0 ; Base (bits 0-15)
db 0x0 ; Base (bits 16-23)
db 10010010b ; Первые флаги + флаги типа (смотрим по битам)
db 11001111b ; Вторые флаги + длина сегмента (bits 16-19)
db 0x0 ; Base (bits 24-31)
gdt_end: ; Пустая метка
gdt_descriptor: ; дескриптор GDT
dw gdt_end - gdt_start - 1 ; Размер GDT
dd gdt_start ; Адрес начала GDT
CODE_SEG equ gdt_code - gdt_start ; Определяем некоторые константы.
DATA_SEG equ gdt_data - gdt_start ; Они понадобятся для регистров сегментов в
; 32-битном защищенном режиме. Например,
; когда мы установим регистр DS = 0x10 (т.е
; 16 байтов) в этом режиме, процессор
; поймет что мы хотим использовать сегмент,
; находящийся в смещении 0x10 в GDT,
; т.е. в нашем случае это сегмент данных
; (0x0 -> NULL, 0x08 -> сегмент кода,
; 0x10 -> сегмент данных)
================================================
FILE: guide/00-BOOT-SECTOR/ex08/main.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / main.asm
; Title: Программа загрузочного сектора, которая входит в 32-битный
; Защищенный Режим
; ------------------------------------------------------------------------------
; Description:
; Было бы неплохо остаться в 16-битном реальном режиме, но чтобы использовать
; возможности процессора полностью, мы должны переключиться в 32-битный
; защищенный режим.
; Основные отличия 32-битного защищенного режима:
; 1. Регистры увеличены до 32 бит. Чтобы использовать увеличенные регистры
; нужно добавить букву "е" перед ними. Пример: AX -> EAX, BX -> EBX
; 2. Добавлены 2 новых регистров сегмента: FS и GS.
; 3. Доступны 32-битные смещения (размер сегмента сможет достигать 4гб)
; 4. Процессор поддерживает более сложную сегментацию памяти, у которой
; есть 2 достоинства:
; a. Коду в одном сегменте запрещено исполнять код в другом, более
; привилигированном сегменте, поэтому вы можете защитить код ядра
; от кода пользовательских приложений.
; b. Процессор может предоставлять виртуальную память для процессов
; пользователя.
; 5. Обработка прерываний также более сложна.
; Самая сложная часть перехода в 32PM - мы должны подготовить сложную
; структуру данных, которая называется глобальная таблица дескрипторов (GDT).
; ------------------------------------------------------------------------------
[org 0x7c00]
mov bp, 0x9000 ; Устанавливаем стек
mov sp, bp
mov bx, MSG_REAL_MODE
call print_string ; Печатаем сообщение на экран
call switch_to_pm ; Переключаемся на загрузочный режим
jmp $
%include "../ex06/print_string.asm" ; Вывод строки
%include "gdt.asm" ; GDT
%include "print_string_pm.asm" ; Вывод строки в 32 PM
%include "switch.asm" ; Переключиться на 32 PM
[bits 32]
BEGIN_PM: ; Сюда мы попадем после переключения в PM
mov ebx, MSG_PROT_MODE
call print_string_pm ; Печатаем сообщение на экран
jmp $
MSG_REAL_MODE:
db "Started in 16-bit Real Mode", 0
MSG_PROT_MODE:
db "Successfully landed in 32-bit Protected mode", 0
times 510-($-$$) db 0
dw 0xaa55
================================================
FILE: guide/00-BOOT-SECTOR/ex08/print_string_pm.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / print_string_pm.asm
; Title: Функция вывода строки на экран в 32-битном защищенном режиме
; ------------------------------------------------------------------------------
; Description:
; Плюсы 32-битного режима: возможность использовать 32-битные регистры и
; адрессацию памяти, защищенную память виртуальную память
; Минусы: отсутствие БИОС прерываний, требование наличия GDT (об этом позже)
; В этой программе мы напишем новую функцию печати строки, но без прерываний
; БИОСа, а напрямую манипулируя VGA видеопамятью, вместо вызова int 0x10.
; VGA размещена начиная с адреса 0xb8000, и у VGA имеется специальный
; текстовый режим, поэтому нам не придется напрямую рисовать пиксели.
; Особенности:
; 1. Символ представляется в виде 2-х байтов. Первый байт = сам символ,
; второй байт = 4 бита на цвет текста и еще 4 на цвет фона.
; Например, чтобы распечатать символ 'A' белым текстом на черном фоне, мы
; испольузуем 0x410f: 0x41 == 'A', 0 == белый, f == черный.
; ------------------------------------------------------------------------------
[bits 32] ; Используем 32-битный режим
; Определяем некоторые константы
VIDEO_MEMORY equ 0xb8000 ; = адрес начала памяти VGA
WHITE_ON_BLACK equ 0x0f ; = цвет символов (0x0f - белый на черном)
print_string_pm:
pusha
mov edx, VIDEO_MEMORY ; Перемещаем в EDX адрес начала массива видеопамяти
print_string_pm_loop:
; Помним, что AX (2б) = AH(1б) и AL(1б)
mov al, [ebx] ; Сохраняем символ из EBX в AL
mov ah, WHITE_ON_BLACK ; Устанавливаем цвет символов в AH
; таким образом AX получается равен символу + цвету
cmp al, 0 ; if (AL == 0), т.е. если конец массива, то
je print_string_pm_done ; заканчиваем выполнение функции
; else:
mov [edx], ax ; video_memory[EDX] = AX
add ebx, 1 ; переходим к следующему символу (+1, просто массив)
add edx, 2 ; переходим к следующему адресу в VGA (+2 т.к.
; два байта на символ)
jmp print_string_pm_loop
print_string_pm_done:
popa
ret
================================================
FILE: guide/00-BOOT-SECTOR/ex08/switch.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / switch.asm
; Title: Переключаемся в PM (Protected mode, защищенный режим)
; ------------------------------------------------------------------------------
; Description:
; Чтобы сделать свитч, нам нужно:
; 1. Отключить прерывания (процессор просто будет их игнорировать), т.к.
; в PM, прерывания обрабатываются совершенно по-другому в отличие от
; Real Mode. Даже если процессор и смог бы распределить сигналы прерываний
; по конкретным BIOS обработчикам прерываний, БИОС обработчики будут
; обрабатывать 16-битный код, что повлекло бы за собой ошибки.
; 2. Загрузить GDT дескриптор
; 3. Изменяем первый бит регистра управления cr0 на "1"
; https://en.wikipedia.org/wiki/Control_register#CR0
; 4. Т.к. процессор использует специальную технику, которая называется
; называется pipelining (гугли: вычислительный конвейер, полезная статья
; на хабре: https://habr.com/ru/post/182002/), и поэтому сразу после того,
; как перевести процессор в PM (что мы и сделали в предыдущем пункте),
; нам нужно заставить процессор завершить всю работу в конвейере, чтобы
; быть уверенным, что все будущие инструкции будут выполнены корректно.
; Конвейер загружает в себя некоторые количество последующих после текущей
; инструкций, но конвейеру не очень нравятся инструкции типа call и jmp,
; т.к. процессор не знает полностью какие инструкции будут следовать за
; ними, в особенности если мы вызовем jmp или call "прыгая" в другой
; сегмент. Поэтому нам нужно сделать "дальний прыжок", чтобы стереть
; обрабатываемые в конвейере инструкции.
; Сам прыжок: jmp <сегмент>:<адрес смещения>
; ------------------------------------------------------------------------------
[bits 16]
switch_to_pm:
cli ; Отключаем прерывания (cli = clear interrupts)
lgdt [gdt_descriptor] ; Загружаем GDT дескриптор (lgdt = load GDT)
mov eax, cr0 ; Чтобы перейти в PM, нужно чтобы первый бит
or eax, 0x1 ; регистра управления cr0 был 1
mov cr0, eax
jmp CODE_SEG:init_pm ; Делаем "дальний прыжок" в наш новый 32-битный
; сегмент кода. Это так же заставляет процессор
; завершить обрабатываемые в конвейере инструкции.
[bits 32]
init_pm: ; в PM, наши старые сегменты бесполезны, поэтому
mov ax, DATA_SEG ; мы делаем так, чтобы регистры всех сегментов
mov ds, ax ; указывали на сегмент данных, который мы определили
mov ss, ax ; в GDT (см. ./gdt.asm)
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000 ; Обновляем позицию стека, чтобы он был на самом
mov esp, ebp ; верху свободного места
call BEGIN_PM ; Вызываем функцию из ./main.asm
================================================
FILE: guide/01-KERNEL/ex00/BUILD.md
================================================
# Процесс сборки
Для сборки нам понадобится собрать кросс-компилятор gcc для i386 архитектуры процессора. Удобнее использовать готовый отсюда: https://wiki.osdev.org/GCC_Cross-Compiler#Prebuilt_Toolchains. Для компьютеров на Linux с x86_64 архитектурой:
```
wget http://newos.org/toolchains/i386-elf-4.9.1-Linux-x86_64.tar.xz
mkdir /usr/local/i386elfgcc
tar -xf i386-elf-4.9.1-Linux-x86_64.tar.xz -C /usr/local/i386elfgcc --strip-components=1
export PATH=$PATH:/usr/local/i386elfgcc/bin
```
Далее можно использовать установленный тулчейн с помощью команд `i386-elf-gcc` для gcc, `i386-elf-ld` для линкера, и т.д.
1. Итак, сначала компилируем загрузочный сектор. Получится бинарный файл.
```
nasm bootsect.asm -f bin -o bootsect.bin
```
Флаги:
- `-f bin` - формат бинарный
- `-o bootsect.bin` - output file = bootsect.bin
2. Далее компилируем ядро в объектный файл, чтобы потом собрать его вместе с kernel_entry.
```
i386-elf-gcc -ffreestanding -c kernel.c -o kernel.o
```
Флаги:
- `-ffreestanding` - в режиме `freestanding`, единственные доступные заголовочные файлы стандартной библиотеки это , , , , , и . Также эта опция указывает компилятору не полагаться на то, что стандартные функции имеют их обычное определение. Это предотвратит компилятор от оптимизации, которую он делает на основе предположений о поведении функций из стандартных библиотек. Например в `hosted` режиме (противополжен `freestanding`), gcc знает о том, что имеющаяся библиотека соответствует спецификации стандарта языка Си. Он может преобразовать `printf("hi\n")` в `puts("hi")`, т.к. имеет представление из определения стандартной IO библиотеки что эти две функции ведут себя одинаково в данном случае. А с флагом `-ffreestanding` gcc не проводит подобных оптимизаций. Подобнее: http://cs107e.github.io/guides/gcc/, https://stackoverflow.com/questions/18711719/freestanding-gcc-and-builtin-functions.
- `-c kernel.c` - флаг указывает на то, что файл после компиляции не нужно линковать
- `-o kernel.o` - output object file
3. Компилируем `kernel_entry.asm`
```
nasm kernel_entry.asm -f elf -o kernel_entry.o
```
Флаги:
- `-f elf` - формат: ELF (Executable and Linkable Format)
4. Линкуем `kernel_entry.o` и `kernel.o` вместе в один бинарный файл `kernel.bin`
```
i386-elf-ld -o kernel.bin -Ttext 0x1000 kernel_entry.o kernel.o --oformat binary
```
Флаги:
- `-o kernel.bin` - output file = kernel.bin
- `-Ttext 0x1000` - этот флаг распологает секцию `.text` по адресу `0x1000`.
- `--oformat binary` - output format = binary
5. Соединяем два файла `bootsect.bin` и `kernel.bin` в один `os-image.bin` с помощью утилиты cat
```
cat bootsect.bin kernel.bin > os-image.bin
```
6. Запускаем образ ОС с помощью эмулятора qemu
```
qemu-system-i386 -fda os-image.bin
```
Флаги:
- `-fda` - использовать файл как образ флоппи диска (дискеты).
Вуаля!
> Подробнее: [Makefile](build/Makefile)
================================================
FILE: guide/01-KERNEL/ex00/README.md
================================================
# `01-KERNEL / ex00`: Процесс сборки, структура проекта, дебаг
> Новые штучки: `Makefile`, `gcc`, `gdb`
Новые файлы упражнения, которые нужно посмотреть:
- `boot/newline.asm`
- `boot/bootsect.asm`
- `boot/kernel_entry.asm`
- `kernel/kernel.c`
- `build/Makefile`
## Процесс сборки
Описан тут [BUILD.md](BUILD.md)
## Структура проекта
- `boot` - файлы загрузочного сектора `.asm`
- `kernel` - файлы ядра `.c`
- `build` - мейкфайл и скомпилированные файлы, включая образ ОС `.o` `.bin` `.elf`
## Дебаг
1. Запускаем программу make с таргетом debug
```
$ make debug
```
2. Запускаем GDB в отдельном терминале
```
$ gdb
```
3. Подключаемся к порту 1234, который по дефолту прослушивает QEMU
```
(gdb) target remote localhost:1234
```
4. Загружаем символьный файл `kernel.elf` который предоставляет GDB полезную информацию для дебага. Это нужно для перевода символов (названия функций и переменных) в адреса, номеров строк в адреса кода и так далее.
```
(gdb) symbol-file kernel.elf
```
4. Ставим брейкпоинт на функции `kmain`
```
(gdb) b kmain
```
5. Запускаем программу
```
(gdb) continue
```
6. Идем читать гайд по GDB
```
Например тут https://habr.com/ru/post/491534/
```
================================================
FILE: guide/01-KERNEL/ex00/boot/bootsect.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 01-KERNEL
; File: ex00 / bootsect.asm
; Title: Программа загрузочного сектора, которая загружает ядро, написанное
; на C в 32-битный защищенный режим.
; ------------------------------------------------------------------------------
; Description:
; ------------------------------------------------------------------------------
[org 0x7c00]
KERNEL_OFFSET equ 0x1000 ; Смещение в памяти, из которого мы загрузим ядро
mov [BOOT_DRIVE], dl ; BIOS stores our boot drive in DL , so it ’s
; best to remember this for later. (Remember that
; the BIOS sets us the boot drive in 'dl' on boot)
mov bp, 0x9000 ; Устанавливаем стек
mov sp, bp
mov bx, MSG_REAL_MODE ; Печатаем сообщение
call print_string
call load_kernel ; Загружаем ядро
call switch_to_pm ; Переключаемся в Защищенный Режим
jmp $
%include "print_string.asm" ; ф. печати строки
%include "print_hex.asm" ; ф. печати 16-ричного числа
%include "disk_load.asm" ; ф. чтения диска
%include "print_string_pm.asm" ; ф. печати строки (32PM)
%include "switch.asm" ; ф. переключения в 32PM
%include "gdt.asm" ; таблица GDT
[bits 16]
load_kernel:
mov bx, MSG_LOAD_KERNEL
call print_string ; Печатаем сообщение о том, то мы загружаем ядро
; Устанавливаем параметры для функции disk_load:
mov bx, KERNEL_OFFSET ; Загрузим данные в место памяти по TODO: disk_load main lookup
; смещению KERNEL_OFFSET
mov dh, 16 ; Загрузим много секторов. *
mov dl, [BOOT_DRIVE] ; Загрузим данные из BOOT_DRIVE (Возвращаем BOOT_DRIVE)
call disk_load ; Вызываем функцию disk_load
ret
[bits 32] ; Сюда мы попадем после переключения в 32PM
BEGIN_PM:
mov ebx, MSG_PROT_MODE
call print_string_pm ; Печатаем сообщение об успешной загрузке в 32PM
call KERNEL_OFFSET ; Переходим в адрес, по которому загрузился код ядра
jmp $
BOOT_DRIVE: db 0
MSG_REAL_MODE: db "Started in 16-bit Real Mode", 0
MSG_PROT_MODE: db "Successfully landed in 32-bit Protected mode", 0
MSG_LOAD_KERNEL: db "Loading kernel into VIDEO_MEMORY", 0
times 510-($-$$) db 0
dw 0xaa55
; ------
; * - Забавный факт: если загрузить меньше секторов, то мы столкнемся со
; странными ошибками когда будем писать ядро на Си. Например, аргументы
; функции могут быть повреждены, а строки "обрезаны".
================================================
FILE: guide/01-KERNEL/ex00/boot/disk_load.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / disk_load.asm
; Title: Функция чтения диска
; ------------------------------------------------------------------------------
; Description:
; Чтобы лучше понять, что здесь происходит, разберитесь с тем, что такое CHS
; по ссылке https://ru.wikipedia.org/wiki/CHS
; ------------------------------------------------------------------------------
disk_load:
push dx
mov ah, 0x02 ; Указвыаем БИОСу что нам нужна рутина чтения диска
; Указываем что нам нужно:
mov al, dh ; 1. Прочитать кол-во секторов, равное значению в dh
mov ch, 0x00 ; 2. Выбрать нулевой цилиндр
mov dh, 0x00 ; 3. Выбрать нулевую головку
mov cl, 0x02 ; 4. Начинать считывать со второго сектора (т.е.
; первый свободный сектор сразу после загруочного
; сектора, т.к. загрузочный сектор находится по
; адресу 0x01)
int 0x13 ; Вызываем прерывание для чтения
; У БИОСа может не получиться прочитать диск, и
; чтобы дать нам знать что произошла ошибка, он,
; во-первых, обновляет специальный флаг CF (carry
; flag) специальным значением, которое означает
; ошибку, а во-вторых, кладет в регистр AL кол-во
; секторов, которые у него получилось прочитать.
jc disk_error ; jc - инструкция для прыжка на указанную метку,
; которая выполняется только если CF (carry flag)
; сигнализирует об ошибке
pop dx ; Восстанавливаем регистр DX из стека
cmp dh, al ; если AL (кол-во прочитанных секторов) != DH
; (предполагаемое кол-во секторов),
jne disk_sectors_error ; то выводим на экран сообщение об ошибке и зависаем
; (то есть запускаем бесконечный цикл)
jmp disk_success
jmp disk_exit ; Заканчиваем выполнение функции
disk_success:
mov bx, SUCCESS_MSG
call print_string
jmp disk_exit
disk_error:
mov bx, DISK_ERR_MSG ; Перемещаем в BX сообщение об ошибке
call print_string ; Выводим его на экран
mov dh, al
call print_hex
jmp disk_loop ; бесконечный цикл
disk_sectors_error:
mov bx, SECTORS_ERR_MSG
call print_string
SUCCESS_MSG:
db "Disk was successfully read ", 0
DISK_ERR_MSG:
db "Disk read error! ", 0
SECTORS_ERR_MSG:
db "Incorrect number of sectors read ", 0
disk_loop:
jmp $
disk_exit:
ret
================================================
FILE: guide/01-KERNEL/ex00/boot/gdt.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / gdt.asm
; Title: Определяем GDT (глобальная таблица дескрипторов)
; ------------------------------------------------------------------------------
; Description:
; Способ, которым процессор переводит логический адрес в физический, в
; 32-битном защищенном режиме отличается от 16-битного реального режима.
; Вместо того, чтобы умножить значение регистра сегмента на 16 и прибавить к
; этому "смещение" (offset), регистр сегмента становится индексом
; определенного дескриптора сегмента в GDT.
; Дескриптор сегмента - это 8-битная структура, которая определяет свойства
; этого сегмента:
; - Base address (32 bits), определяющий откуда сегмент начинается в
; физической памяти.
; - Segment Limit (20 bits), определяющий размер сегмента
; - Различные флаги, которые устанавливают каким образом процессор будет
; "относиться" к сегментам, например уровень привилегий и т.д.
;
; Флаги:
; * 1-ые флаги:
; - present flag (флаг присутствия). Если его значение "1", то это
; указывает, что сегмент присутствует в памяти (это нужно для виртуальной
; памяти)
; - privilege flag (флаг привилегии). Значение "0" - самый высокий уровень
; привилегии
; - descriptor type (тип дескриптора). "1" - для сегмента кода или
; сегмента данных
; * Флаги типа:
; - code (флаг кода). "1" - для кода, "0" - для даннных
; - conformig (флаг подчинения). "0" - чтобы код в другом сегменте с
; более низким уровнем привилегий не смог вызвать код из этого сегмента -
; это ключ к защите памяти (memory protection).
; - readable (читаемость). "1" - если читаемый, "0" - только исполняемый.
; - writable. Разрешает сегменту данных быть записываемым, в противном
; случае, он будет доступен только для чтения.
; - accessed (флаг доступа). Этот флаг устанавливается, когда происходит
; обращение к сегменту.
; - expand down. Флаг (бит), позволяющий сегменту расширяться вниз.
; * 2-ые флаги:
; - granulariy (гранулярность). "0" - байтовая гранулярность, лимит
; задается в байтах, если "1" - страничная гранулярность, в 4кб блоках.
; Если выбрать страничную гранулярность и установить значение лимита как
; 0xfffff, то лимит умножится на 16*16*16 (4кб), и лимит станет 0xfffff000
; позволяя нашему сегменту занять 4гб места в памяти.
; - 32-bit default. "1" - т.к. наш сегмент будет содержать 32-битный код.
; - 64-bit code segment. "0" - т.к. не используется на 32-битных
; процессорах.
; - AVL (available). Определяет доступность сегмента для использования
; системным программным обеспечением (используются только ОС).
; ------------------------------------------------------------------------------
gdt_start: ; Эта пустая метка нужно для того чтобы удобнее
; посчитать размер GDT для ее дескриптора
; (end - start)
gdt_null: ; Необходимый нулевой дескриптор для GDT
dd 0x0 ; dd - define double (двойное слово, т.е. 4 байта)
dd 0x0
gdt_code: ; Определяем дескриптор сегмента кода
dw 0xffff ; Limit (bits 0-15)
dw 0x0 ; Base (bits 0-15)
db 0x0 ; Base (bits 16-23)
db 10011010b ; Первые флаги + флаги типа (смотрим по битам)
; present: 1, privilege: 00, descriptor type: 1
; code: 1, conforming: 0, readable: 1, accessed: 0
db 11001111b ; Вторые флаги + длина сегмента (bits 16-19):
; granularity: 1, 32-bit default: 1,
; 64-bit default: 0, AVL: 0
db 0x0 ; Base (bits 24-31)
gdt_data: ; Определяем дескриптор сегмента кода
dw 0xffff ; Limit (bits 0-15)
dw 0x0 ; Base (bits 0-15)
db 0x0 ; Base (bits 16-23)
db 10010010b ; Первые флаги + флаги типа (смотрим по битам)
db 11001111b ; Вторые флаги + длина сегмента (bits 16-19)
db 0x0 ; Base (bits 24-31)
gdt_end: ; Пустая метка
gdt_descriptor: ; дескриптор GDT
dw gdt_end - gdt_start - 1 ; Размер GDT
dd gdt_start ; Адрес начала GDT
CODE_SEG equ gdt_code - gdt_start ; Определяем некоторые константы.
DATA_SEG equ gdt_data - gdt_start ; Они понадобятся для регистров сегментов в
; 32-битном защищенном режиме. Например,
; когда мы установим регистр DS = 0x10 (т.е
; 16 байтов) в этом режиме, процессор
; поймет что мы хотим использовать сегмент,
; находящийся в смещении 0x10 в нашем GDT,
; т.е. в нашем случае это сегмент данных
; (0x0 -> NULL, 0x08 -> сегмент кода,
; 0x10 -> сегмент данных)
================================================
FILE: guide/01-KERNEL/ex00/boot/kernel_entry.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 01-KERNEL
; File: ex00 / kernel_entry.asm
; Title: Код, служащий входной точкой для функции kmain из kernel.c
; ------------------------------------------------------------------------------
; Description:
; ------------------------------------------------------------------------------
[bits 32]
[extern kmain] ; Определяем 'внешнюю' штуку с названием kmain - она понадобится
; линкеру чтобы собрать все вместе
call kmain ; Вызываем определенную выше функцию, которая будет доступна
; после линковки. Это функция kmain из kernel.c
jmp $
================================================
FILE: guide/01-KERNEL/ex00/boot/print_hex.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex05 / print_hex.asm
; Title: Функция вывода шестнадцатеричного числа на экран
; ------------------------------------------------------------------------------
; Description:
; Как выглядит регистр EAX (32 b = 32 бита):
;
; EAX (32 b)
; ---------------------------------------------
; | | | |
; | | AH (8 b) | AL (8 b) |
; | | | |
; ---------------------------------------------
; AX (16 b)
;
; Как видим, EAX содержит в себе регистр AX, который в свою очередь разделен
; на AH (a high, верхний) и AL (a low, нижний).
; ------------------------------------------------------------------------------
; Помним, что в main.asm:
; DX = 0x1fb6
; BX = "0x0000" (а точнее, BX="0", т.к. указывает на первый элемент)
print_hex:
pusha ; Сохраняем значения регистров в стеке
mov cx, 0 ; Регистр CX будет служить счетчиком
loop1:
cmp cx, 4 ; if (CX < 4)
jl print ; Переходим к print
jmp end ; else переходим к end
print:
mov ax, dx ; В AX теперь 0x1fb6
and ax, 0x000f ; В AX теперь 6 (последняя цифра от 0x1fb6).
cmp ax, 9 ; if (AX > 9) (проверяем обозначается ли число
; буквой т.к. мы помним что цифра больше 9 в
; 16-ричной системе исчисления обозначается
; буквой латинского алфавита)
jg num_to_abc ; Переходим к num_to_abc
jmp next
num_to_abc: ; Перевод числа в букву (так, как она бы
; выглядела в 16-ричной СИ), например число 15
; в десятичной будет равно f в 16-ричной
add ax, 39 ; Добавляем к этому числу 39, чтобы затем еще
; добавить 48 ('0'), получая код
; соответсвующего символа в ASCII (например,
; f (как число, то есть 15) + 39 + 48 (код '0')
; = 102 (то есть 'f' в ASCII, как нам и нужно)
jmp next
next:
add ax, '0' ; Добавляем 48 в ax
mov bx, HEX_OUT + 5 ; Теперь bx указывает на последний символ строки
; HEX_OUT
sub bx, cx ; BX = BX - counter (для итерации)
mov [bx], al ; Так как мы разыменовываем bx (вот так: [bx]),
; то [bx] это не регистр, а ссылка на память,
; и так как ax = 16 бит (2 байта), чтобы нам не
; перезаписать лишнюю память, в [bx] мы помещаем
; не ax, а al, размер которого равен 1-му байту.
ror dx, 4 ; было: 0x1fb6, стало: 0x61fb (переносим
; последнюю цифру в начало)
inc cx ; counter++
jmp loop1 ; переходим обратно к loop1
end:
mov bx, HEX_OUT ; Делаем так, чтобы bx снова указывал на первый
; символ строки HEX_OUT
call print_string ; выводи на экран строку из регистра bx
popa ; Возвращаем регистрам их изначальное значение
ret ; Заканчиваем выполнение функции
HEX_OUT: db "0x0000", 0
================================================
FILE: guide/01-KERNEL/ex00/boot/print_string.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex04 / print_string.asm
; Title: Функция вывода строки на экран
; ------------------------------------------------------------------------------
; Description: null
; ------------------------------------------------------------------------------
print_string: ; Функция вывода строки на экран.
pusha ; Когда мы используем функции, мы можем модифицировать
; регистры прямо в них, что нарушает чистоту функции,
; т.е. мы можем перезаписывать какие-то внешние
; данные. Для этого мы добавляем значение всех регистров
; в стек с помощью команды pusha, а в конце функции
; мы возвращаем регистрам их изначальные значения,
; которые возьмем из стека (команда popa).
mov ah, 0x0e ; tele-type mode
loop: ; Метка loop (= цикл)
mov al, [bx] ; Перемещаем значение BX в AL, т.к. мы помним что в BX
; лежит первый символ строки (см. ./main.asm)
cmp al, 0 ; Команда cmp для сравнения AL и 0.
je newline ; (if) je = "jump if equal",
; т.е. перемещаемся к коду с меткой return если AL == 0
jmp put_char ; (else) в противном случае перемещаемся к put_char.
put_char: ; Метка put_char - вывод символа на экран.
int 0x10 ; Вызываем прерывание, которое позволяет вывести
; на экран значение регистра AL, в котором лежит [bx].
inc bx ; inc <регистр> - увеличить на 1.
jmp loop ; Возвращаемся обратно к циклу.
newline:
mov ah, 0x0e
mov al, 0x0a ; (0x0a) \n - new line
int 0x10
mov al, 0x0d ; (0x0d) \r - carriage return
int 0x10
jmp return
return: ; Метка return - завершаем функцию
popa ; Возвращаем регистрам их изначальные значения.
ret ; Заканчиваем выполнение функции.
================================================
FILE: guide/01-KERNEL/ex00/boot/print_string_pm.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / print_string_pm.asm
; Title: Функция вывода строки на экран в 32-битном защищенном режиме
; ------------------------------------------------------------------------------
; Description:
; Плюсы 32-битного режима: возможность использовать 32-битные регистры и
; адрессацию памяти, защищенную память виртуальную память
; Минусы: отсутствие БИОС прерываний, требование наличия GDT (об этом позже)
; В этой программе мы напишем новую функцию печати строки, но без прерываний
; БИОСа, а напрямую манипулируя VGA видеопамятью, вместо вызова int 0x10.
; VGA память размещена начиная с адреса 0xb8000, и у VGA имеется специальный
; текстовый режим, поэтому нам не придется напрямую рисовать пиксели.
; Особенности:
; 1. Символ представляется в виде 2-х байтов. Первый байт - сам символ,
; второй байт - 4 бита на цвет текста и еще 4 на цвет фона.
; Например, чтобы распечатать символ 'A' белым текстом на черном фоне, мы
; испольузуем 0x410f: 0x41 == 'A', 0 == белый, f == черный.
; ------------------------------------------------------------------------------
[bits 32] ; Используем 32-битный режим
; Определяем некоторые константы
VIDEO_MEMORY equ 0xb8000 ; = адрес начала памяти VGA
WHITE_ON_BLACK equ 0x0f ; = цвет символов (0x0f - белый на черном)
print_string_pm:
pusha
mov edx, VIDEO_MEMORY ; Перемещаем в EDX адрес начала массива видеопамяти
print_string_pm_loop:
; Помним, что AX (2б) = AH(1б) и AL(1б)
mov al, [ebx] ; Сохраняем символ из EBX в AL
mov ah, WHITE_ON_BLACK ; Устанавливаем цвет символов в AH
; таким образом AX получается равен символу + цвету
cmp al, 0 ; if (AL == 0), т.е. если конец массива, то
je print_string_pm_done ; заканчиваем выполнение функции
; else:
mov [edx], ax ; video_memory[EDX] = AX
add ebx, 1 ; переходим к следующему символу (+1, просто массив)
add edx, 2 ; переходим к следующему адресу в VGA (+2 т.к.
; два байта на символ)
jmp print_string_pm_loop
print_string_pm_done:
popa
ret
================================================
FILE: guide/01-KERNEL/ex00/boot/switch.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / switch.asm
; Title: Переключаемся в PM (Protected mode, защищенный режим)
; ------------------------------------------------------------------------------
; Description:
; Чтобы сделать свитч, нам нужно:
; 1. Отключить прерывания (процессор просто будет их игнорировать), т.к.
; в PM, прерывания обрабатываются совершенно по-другому в отличие от
; Real Mode. Даже если процессор и смог бы распределить сигналы прерываний
; по конкретным BIOS обработчикам прерываний, БИОС обработчики будут
; обрабатывать 16-битный код, что повлекло бы за собой ошибки.
; 2. Загрузить GDT дескриптор
; 3. Изменяем первый бит регистра управления cr0 на "1"
; https://en.wikipedia.org/wiki/Control_register#CR0
; 4. Т.к. процессор использует специальную технику, которая называется
; называется pipelining (гугли: вычислительный конвейер, полезная статья
; на хабре: https://habr.com/ru/post/182002/), и поэтому сразу после того,
; как перевести процессор в PM (что мы и сделали в предыдущем пункте),
; нам нужно заставить процессор завершить всю работу в конвейере, чтобы
; быть уверенным, что все будущие инструкции будут выполнены корректно.
; Конвейер загружает в себя некоторые количество последующих после текущей
; инструкций, но конвейеру не очень нравятся инструкции типа call и jmp,
; т.к. процессор не знает полностью какие инструкции будут следовать за
; ними, в особенности если мы вызовем jmp или call "прыгая" в другой
; сегмент. Поэтому нам нужно сделать "дальний прыжок", чтобы завершить
; обрабатываемые в конвейере инструкции.
; Сам прыжок: jmp <сегмент>:<адрес смещения>
; ------------------------------------------------------------------------------
[bits 16]
switch_to_pm:
cli ; Отключаем прерывания (cli = clear interrupts)
lgdt [gdt_descriptor] ; Загружаем GDT дескриптор (lgdt = load GDT)
mov eax, cr0 ; Чтобы перейти в PM, нужно чтобы первый бит
or eax, 0x1 ; регистра управления cr0 был 1
mov cr0, eax
jmp CODE_SEG:init_pm ; Делаем "дальний прыжок" в наш новый 32-битный
; сегмент кода. Это так же заставляет процессор
; завершить обрабатываемые в конвейере инструкции.
[bits 32]
init_pm: ; в PM, наши старые сегменты бесполезны, поэтому
mov ax, DATA_SEG ; мы делаем так, чтобы регистры всех сегментов
mov ds, ax ; указывали на сегмент данных, который мы определили
mov ss, ax ; в GDT (см. ./gdt.asm)
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000 ; Обновляем позицию стека, чтобы он был на самом
mov esp, ebp ; верху свободного места
call BEGIN_PM ; Вызываем функцию из ./main.asm
================================================
FILE: guide/01-KERNEL/ex00/build/Makefile
================================================
# флаг для дебага для gcc
CFLAGS = -g
run: os-image.bin
qemu-system-i386 -fda os-image.bin
make clean
debug: os-image.bin kernel.elf
# kernel.elf нужен для gdb как symbol-file
# флаг -s указывает qemu открыть и прослушивать 1234 порт
# чтобы gdb смог соединиться с ним для дебага
# флаг -S указыает QEMU не запускать образ и подождать подключений
qemu-system-i386 -s -S -fda os-image.bin
make clean
os-image.bin: bootsect.bin kernel.bin
cat bootsect.bin kernel.bin > os-image.bin
bootsect.bin:
cd ../boot/ && nasm bootsect.asm -f bin -o ../build/bootsect.bin && cd -
kernel.bin: kernel_entry.o kernel.o
i386-elf-ld -o kernel.bin -Ttext 0x1000 kernel_entry.o kernel.o --oformat binary
kernel_entry.o:
nasm ../boot/kernel_entry.asm -f elf -o kernel_entry.o
kernel.o:
i386-elf-gcc ${CFLAGS} -ffreestanding -c ../kernel/kernel.c
kernel.elf: kernel_entry.o kernel.o
# как kernel.bin только без --oformat binary
i386-elf-ld -o kernel.bin -Ttext 0x1000 kernel_entry.o kernel.o -o kernel.elf
clean:
rm *.bin *.o *.elf
================================================
FILE: guide/01-KERNEL/ex00/kernel/kernel.c
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex00 / kernel / kernel.c
* Title: Программа на Си, в которую мы загрузимся после boot'а.
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
void func() {} /* Эта функция нужна чтобы показать, что ядро будет
загружаться не с начала этого файла (0x00), а с
функции kmain. Если бы это было не так, то вместо kmain
код бы начал выполняться с этой функции, а т.к. она
ничего не делает, то мы бы не получили результата */
void kmain()
{
char *video_memory = (char *) 0xb8000; /* Распологаем символ 'x' по */
*video_memory = 'x'; /* адресу 0xb8000 */
}
================================================
FILE: guide/01-KERNEL/ex01/boot/bootsect.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 01-KERNEL
; File: ex00 / bootsect.asm
; Title: Программа загрузочного сектора, которая загружает ядро, написанное
; на C в 32-битный защищенный режим.
; ------------------------------------------------------------------------------
; Description:
; ------------------------------------------------------------------------------
[org 0x7c00]
KERNEL_OFFSET equ 0x1000 ; Смещение в памяти, из которого мы загрузим ядро
mov [BOOT_DRIVE], dl ; BIOS stores our boot drive in DL , so it ’s
; best to remember this for later. (Remember that
; the BIOS sets us the boot drive in 'dl' on boot)
mov bp, 0x9000 ; Устанавливаем стек
mov sp, bp
mov bx, MSG_REAL_MODE ; Печатаем сообщение
call print_string
call load_kernel ; Загружаем ядро
call switch_to_pm ; Переключаемся в Защищенный Режим
jmp $
%include "print_string.asm" ; ф. печати строки
%include "print_hex.asm" ; ф. печати 16-ричного числа
%include "disk_load.asm" ; ф. чтения диска
%include "print_string_pm.asm" ; ф. печати строки (32PM)
%include "switch.asm" ; ф. переключения в 32PM
%include "gdt.asm" ; таблица GDT
[bits 16]
load_kernel:
mov bx, MSG_LOAD_KERNEL
call print_string ; Печатаем сообщение о том, то мы загружаем ядро
; Устанавливаем параметры для функции disk_load:
mov bx, KERNEL_OFFSET ; Загрузим данные в место памяти по TODO: disk_load main lookup
; смещению KERNEL_OFFSET
mov dh, 16 ; Загрузим много секторов. *
mov dl, [BOOT_DRIVE] ; Загрузим данные из BOOT_DRIVE (Возвращаем BOOT_DRIVE)
call disk_load ; Вызываем функцию disk_load
ret
[bits 32] ; Сюда мы попадем после переключения в 32PM
BEGIN_PM:
mov ebx, MSG_PROT_MODE
call print_string_pm ; Печатаем сообщение об успешной загрузке в 32PM
call KERNEL_OFFSET ; Переходим в адрес, по которому загрузился код ядра
jmp $
BOOT_DRIVE: db 0
MSG_REAL_MODE: db "Started in 16-bit Real Mode", 0
MSG_PROT_MODE: db "Successfully landed in 32-bit Protected mode", 0
MSG_LOAD_KERNEL: db "Loading kernel into VIDEO_MEMORY", 0
times 510-($-$$) db 0
dw 0xaa55
; ------
; * - Забавный факт: если загрузить меньше секторов, то мы столкнемся со
; странными ошибками когда будем писать ядро на Си. Например, аргументы
; функции могут быть повреждены, а строки "обрезаны".
================================================
FILE: guide/01-KERNEL/ex01/boot/disk_load.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / disk_load.asm
; Title: Функция чтения диска
; ------------------------------------------------------------------------------
; Description:
; Чтобы лучше понять, что здесь происходит, разберитесь с тем, что такое CHS
; по ссылке https://ru.wikipedia.org/wiki/CHS
; ------------------------------------------------------------------------------
disk_load:
push dx
mov ah, 0x02 ; Указвыаем БИОСу что нам нужна рутина чтения диска
; Указываем что нам нужно:
mov al, dh ; 1. Прочитать кол-во секторов, равное значению в dh
mov ch, 0x00 ; 2. Выбрать нулевой цилиндр
mov dh, 0x00 ; 3. Выбрать нулевую головку
mov cl, 0x02 ; 4. Начинать считывать со второго сектора (т.е.
; первый свободный сектор сразу после загруочного
; сектора, т.к. загрузочный сектор находится по
; адресу 0x01)
int 0x13 ; Вызываем прерывание для чтения
; У БИОСа может не получиться прочитать диск, и
; чтобы дать нам знать что произошла ошибка, он,
; во-первых, обновляет специальный флаг CF (carry
; flag) специальным значением, которое означает
; ошибку, а во-вторых, кладет в регистр AL кол-во
; секторов, которые у него получилось прочитать.
jc disk_error ; jc - инструкция для прыжка на указанную метку,
; которая выполняется только если CF (carry flag)
; сигнализирует об ошибке
pop dx ; Восстанавливаем регистр DX из стека
cmp dh, al ; если AL (кол-во прочитанных секторов) != DH
; (предполагаемое кол-во секторов),
jne disk_sectors_error ; то выводим на экран сообщение об ошибке и зависаем
; (то есть запускаем бесконечный цикл)
jmp disk_success
jmp disk_exit ; Заканчиваем выполнение функции
disk_success:
mov bx, SUCCESS_MSG
call print_string
jmp disk_exit
disk_error:
mov bx, DISK_ERR_MSG ; Перемещаем в BX сообщение об ошибке
call print_string ; Выводим его на экран
mov dh, al
call print_hex
jmp disk_loop ; бесконечный цикл
disk_sectors_error:
mov bx, SECTORS_ERR_MSG
call print_string
SUCCESS_MSG:
db "Disk was successfully read ", 0
DISK_ERR_MSG:
db "Disk read error! ", 0
SECTORS_ERR_MSG:
db "Incorrect number of sectors read ", 0
disk_loop:
jmp $
disk_exit:
ret
================================================
FILE: guide/01-KERNEL/ex01/boot/gdt.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / gdt.asm
; Title: Определяем GDT (глобальная таблица дескрипторов)
; ------------------------------------------------------------------------------
; Description:
; Способ, которым процессор переводит логический адрес в физический, в
; 32-битном защищенном режиме отличается от 16-битного реального режима.
; Вместо того, чтобы умножить значение регистра сегмента на 16 и прибавить к
; этому "смещение" (offset), регистр сегмента становится индексом
; определенного дескриптора сегмента в GDT.
; Дескриптор сегмента - это 8-битная структура, которая определяет свойства
; этого сегмента:
; - Base address (32 bits), определяющий откуда сегмент начинается в
; физической памяти.
; - Segment Limit (20 bits), определяющий размер сегмента
; - Различные флаги, которые устанавливают каким образом процессор будет
; "относиться" к сегментам, например уровень привилегий и т.д.
;
; Флаги:
; * 1-ые флаги:
; - present flag (флаг присутствия). Если его значение "1", то это
; указывает, что сегмент присутствует в памяти (это нужно для виртуальной
; памяти)
; - privilege flag (флаг привилегии). Значение "0" - самый высокий уровень
; привилегии
; - descriptor type (тип дескриптора). "1" - для сегмента кода или
; сегмента данных
; * Флаги типа:
; - code (флаг кода). "1" - для кода, "0" - для даннных
; - conformig (флаг подчинения). "0" - чтобы код в другом сегменте с
; более низким уровнем привилегий не смог вызвать код из этого сегмента -
; это ключ к защите памяти (memory protection).
; - readable (читаемость). "1" - если читаемый, "0" - только исполняемый.
; - writable. Разрешает сегменту данных быть записываемым, в противном
; случае, он будет доступен только для чтения.
; - accessed (флаг доступа). Этот флаг устанавливается, когда происходит
; обращение к сегменту.
; - expand down. Флаг (бит), позволяющий сегменту расширяться вниз.
; * 2-ые флаги:
; - granulariy (гранулярность). "0" - байтовая гранулярность, лимит
; задается в байтах, если "1" - страничная гранулярность, в 4кб блоках.
; Если выбрать страничную гранулярность и установить значение лимита как
; 0xfffff, то лимит умножится на 16*16*16 (4кб), и лимит станет 0xfffff000
; позволяя нашему сегменту занять 4гб места в памяти.
; - 32-bit default. "1" - т.к. наш сегмент будет содержать 32-битный код.
; - 64-bit code segment. "0" - т.к. не используется на 32-битных
; процессорах.
; - AVL (available). Определяет доступность сегмента для использования
; системным программным обеспечением (используются только ОС).
; ------------------------------------------------------------------------------
gdt_start: ; Эта пустая метка нужно для того чтобы удобнее
; посчитать размер GDT для ее дескриптора
; (end - start)
gdt_null: ; Необходимый нулевой дескриптор для GDT
dd 0x0 ; dd - define double (двойное слово, т.е. 4 байта)
dd 0x0
gdt_code: ; Определяем дескриптор сегмента кода
dw 0xffff ; Limit (bits 0-15)
dw 0x0 ; Base (bits 0-15)
db 0x0 ; Base (bits 16-23)
db 10011010b ; Первые флаги + флаги типа (смотрим по битам)
; present: 1, privilege: 00, descriptor type: 1
; code: 1, conforming: 0, readable: 1, accessed: 0
db 11001111b ; Вторые флаги + длина сегмента (bits 16-19):
; granularity: 1, 32-bit default: 1,
; 64-bit default: 0, AVL: 0
db 0x0 ; Base (bits 24-31)
gdt_data: ; Определяем дескриптор сегмента кода
dw 0xffff ; Limit (bits 0-15)
dw 0x0 ; Base (bits 0-15)
db 0x0 ; Base (bits 16-23)
db 10010010b ; Первые флаги + флаги типа (смотрим по битам)
db 11001111b ; Вторые флаги + длина сегмента (bits 16-19)
db 0x0 ; Base (bits 24-31)
gdt_end: ; Пустая метка
gdt_descriptor: ; дескриптор GDT
dw gdt_end - gdt_start - 1 ; Размер GDT
dd gdt_start ; Адрес начала GDT
CODE_SEG equ gdt_code - gdt_start ; Определяем некоторые константы.
DATA_SEG equ gdt_data - gdt_start ; Они понадобятся для регистров сегментов в
; 32-битном защищенном режиме. Например,
; когда мы установим регистр DS = 0x10 (т.е
; 16 байтов) в этом режиме, процессор
; поймет что мы хотим использовать сегмент,
; находящийся в смещении 0x10 в нашем GDT,
; т.е. в нашем случае это сегмент данных
; (0x0 -> NULL, 0x08 -> сегмент кода,
; 0x10 -> сегмент данных)
================================================
FILE: guide/01-KERNEL/ex01/boot/kernel_entry.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 01-KERNEL
; File: ex00 / kernel_entry.asm
; Title: Код, служащий входной точкой для функции kmain из kernel.c
; ------------------------------------------------------------------------------
; Description:
; ------------------------------------------------------------------------------
[bits 32]
[extern kmain] ; Определяем 'внешнюю' штуку с названием kmain - она понадобится
; линкеру чтобы собрать все вместе
call kmain ; Вызываем определенную выше функцию, которая будет доступна
; после линковки. Это функция kmain из kernel.c
jmp $
================================================
FILE: guide/01-KERNEL/ex01/boot/print_hex.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex05 / print_hex.asm
; Title: Функция вывода шестнадцатеричного числа на экран
; ------------------------------------------------------------------------------
; Description:
; Как выглядит регистр EAX (32 b = 32 бита):
;
; EAX (32 b)
; ---------------------------------------------
; | | | |
; | | AH (8 b) | AL (8 b) |
; | | | |
; ---------------------------------------------
; AX (16 b)
;
; Как видим, EAX содержит в себе регистр AX, который в свою очередь разделен
; на AH (a high, верхний) и AL (a low, нижний).
; ------------------------------------------------------------------------------
; Помним, что в main.asm:
; DX = 0x1fb6
; BX = "0x0000" (а точнее, BX="0", т.к. указывает на первый элемент)
print_hex:
pusha ; Сохраняем значения регистров в стеке
mov cx, 0 ; Регистр CX будет служить счетчиком
loop1:
cmp cx, 4 ; if (CX < 4)
jl print ; Переходим к print
jmp end ; else переходим к end
print:
mov ax, dx ; В AX теперь 0x1fb6
and ax, 0x000f ; В AX теперь 6 (последняя цифра от 0x1fb6).
cmp ax, 9 ; if (AX > 9) (проверяем обозначается ли число
; буквой т.к. мы помним что цифра больше 9 в
; 16-ричной системе исчисления обозначается
; буквой латинского алфавита)
jg num_to_abc ; Переходим к num_to_abc
jmp next
num_to_abc: ; Перевод числа в букву (так, как она бы
; выглядела в 16-ричной СИ), например число 15
; в десятичной будет равно f в 16-ричной
add ax, 39 ; Добавляем к этому числу 39, чтобы затем еще
; добавить 48 ('0'), получая код
; соответсвующего символа в ASCII (например,
; f (как число, то есть 15) + 39 + 48 (код '0')
; = 102 (то есть 'f' в ASCII, как нам и нужно)
jmp next
next:
add ax, '0' ; Добавляем 48 в ax
mov bx, HEX_OUT + 5 ; Теперь bx указывает на последний символ строки
; HEX_OUT
sub bx, cx ; BX = BX - counter (для итерации)
mov [bx], al ; Так как мы разыменовываем bx (вот так: [bx]),
; то [bx] это не регистр, а ссылка на память,
; и так как ax = 16 бит (2 байта), чтобы нам не
; перезаписать лишнюю память, в [bx] мы помещаем
; не ax, а al, размер которого равен 1-му байту.
ror dx, 4 ; было: 0x1fb6, стало: 0x61fb (переносим
; последнюю цифру в начало)
inc cx ; counter++
jmp loop1 ; переходим обратно к loop1
end:
mov bx, HEX_OUT ; Делаем так, чтобы bx снова указывал на первый
; символ строки HEX_OUT
call print_string ; выводи на экран строку из регистра bx
popa ; Возвращаем регистрам их изначальное значение
ret ; Заканчиваем выполнение функции
HEX_OUT: db "0x0000", 0
================================================
FILE: guide/01-KERNEL/ex01/boot/print_string.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex04 / print_string.asm
; Title: Функция вывода строки на экран
; ------------------------------------------------------------------------------
; Description: null
; ------------------------------------------------------------------------------
print_string: ; Функция вывода строки на экран.
pusha ; Когда мы используем функции, мы можем модифицировать
; регистры прямо в них, что нарушает чистоту функции,
; т.е. мы можем перезаписывать какие-то внешние
; данные. Для этого мы добавляем значение всех регистров
; в стек с помощью команды pusha, а в конце функции
; мы возвращаем регистрам их изначальные значения,
; которые возьмем из стека (команда popa).
mov ah, 0x0e ; tele-type mode
loop: ; Метка loop (= цикл)
mov al, [bx] ; Перемещаем значение BX в AL, т.к. мы помним что в BX
; лежит первый символ строки (см. ./main.asm)
cmp al, 0 ; Команда cmp для сравнения AL и 0.
je newline ; (if) je = "jump if equal",
; т.е. перемещаемся к коду с меткой return если AL == 0
jmp put_char ; (else) в противном случае перемещаемся к put_char.
put_char: ; Метка put_char - вывод символа на экран.
int 0x10 ; Вызываем прерывание, которое позволяет вывести
; на экран значение регистра AL, в котором лежит [bx].
inc bx ; inc <регистр> - увеличить на 1.
jmp loop ; Возвращаемся обратно к циклу.
newline:
mov ah, 0x0e
mov al, 0x0a ; (0x0a) \n - new line
int 0x10
mov al, 0x0d ; (0x0d) \r - carriage return
int 0x10
jmp return
return: ; Метка return - завершаем функцию
popa ; Возвращаем регистрам их изначальные значения.
ret ; Заканчиваем выполнение функции.
================================================
FILE: guide/01-KERNEL/ex01/boot/print_string_pm.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / print_string_pm.asm
; Title: Функция вывода строки на экран в 32-битном защищенном режиме
; ------------------------------------------------------------------------------
; Description:
; Плюсы 32-битного режима: возможность использовать 32-битные регистры и
; адрессацию памяти, защищенную память виртуальную память
; Минусы: отсутствие БИОС прерываний, требование наличия GDT (об этом позже)
; В этой программе мы напишем новую функцию печати строки, но без прерываний
; БИОСа, а напрямую манипулируя VGA видеопамятью, вместо вызова int 0x10.
; VGA память размещена начиная с адреса 0xb8000, и у VGA имеется специальный
; текстовый режим, поэтому нам не придется напрямую рисовать пиксели.
; Особенности:
; 1. Символ представляется в виде 2-х байтов. Первый байт - сам символ,
; второй байт - 4 бита на цвет текста и еще 4 на цвет фона.
; Например, чтобы распечатать символ 'A' белым текстом на черном фоне, мы
; испольузуем 0x410f: 0x41 == 'A', 0 == белый, f == черный.
;
; Bit: | 15 14 13 12 11 10 9 8 | 7 6 5 4 | 3 2 1 0 |
; Content: | ASCII | FG | BG |
;
; ------------------------------------------------------------------------------
[bits 32] ; Используем 32-битный режим
; Определяем некоторые константы
VIDEO_MEMORY equ 0xb8000 ; = адрес начала памяти VGA
WHITE_ON_BLACK equ 0x0f ; = цвет символов (0x0f - белый на черном)
print_string_pm:
pusha
mov edx, VIDEO_MEMORY ; Перемещаем в EDX адрес начала массива видеопамяти
print_string_pm_loop:
; Помним, что AX (2б) = AH(1б) и AL(1б)
mov al, [ebx] ; Сохраняем символ из EBX в AL
mov ah, WHITE_ON_BLACK ; Устанавливаем цвет символов в AH
; таким образом AX получается равен символу + цвету
cmp al, 0 ; if (AL == 0), т.е. если конец массива, то
je print_string_pm_done ; заканчиваем выполнение функции
; else:
mov [edx], ax ; video_memory[EDX] = AX
add ebx, 1 ; переходим к следующему символу (+1, просто массив)
add edx, 2 ; переходим к следующему адресу в VGA (+2 т.к.
; два байта на символ)
jmp print_string_pm_loop
print_string_pm_done:
popa
ret
================================================
FILE: guide/01-KERNEL/ex01/boot/switch.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / switch.asm
; Title: Переключаемся в PM (Protected mode, защищенный режим)
; ------------------------------------------------------------------------------
; Description:
; Чтобы сделать свитч, нам нужно:
; 1. Отключить прерывания (процессор просто будет их игнорировать), т.к.
; в PM, прерывания обрабатываются совершенно по-другому в отличие от
; Real Mode. Даже если процессор и смог бы распределить сигналы прерываний
; по конкретным BIOS обработчикам прерываний, БИОС обработчики будут
; обрабатывать 16-битный код, что повлекло бы за собой ошибки.
; 2. Загрузить GDT дескриптор
; 3. Изменяем первый бит регистра управления cr0 на "1"
; https://en.wikipedia.org/wiki/Control_register#CR0
; 4. Т.к. процессор использует специальную технику, которая называется
; называется pipelining (гугли: вычислительный конвейер, полезная статья
; на хабре: https://habr.com/ru/post/182002/), и поэтому сразу после того,
; как перевести процессор в PM (что мы и сделали в предыдущем пункте),
; нам нужно заставить процессор завершить всю работу в конвейере, чтобы
; быть уверенным, что все будущие инструкции будут выполнены корректно.
; Конвейер загружает в себя некоторые количество последующих после текущей
; инструкций, но конвейеру не очень нравятся инструкции типа call и jmp,
; т.к. процессор не знает полностью какие инструкции будут следовать за
; ними, в особенности если мы вызовем jmp или call "прыгая" в другой
; сегмент. Поэтому нам нужно сделать "дальний прыжок", чтобы завершить
; обрабатываемые в конвейере инструкции.
; Сам прыжок: jmp <сегмент>:<адрес смещения>
; ------------------------------------------------------------------------------
[bits 16]
switch_to_pm:
cli ; Отключаем прерывания (cli = clear interrupts)
lgdt [gdt_descriptor] ; Загружаем GDT дескриптор (lgdt = load GDT)
mov eax, cr0 ; Чтобы перейти в PM, нужно чтобы первый бит
or eax, 0x1 ; регистра управления cr0 был 1
mov cr0, eax
jmp CODE_SEG:init_pm ; Делаем "дальний прыжок" в наш новый 32-битный
; сегмент кода. Это так же заставляет процессор
; завершить обрабатываемые в конвейере инструкции.
[bits 32]
init_pm: ; в PM, наши старые сегменты бесполезны, поэтому
mov ax, DATA_SEG ; мы делаем так, чтобы регистры всех сегментов
mov ds, ax ; указывали на сегмент данных, который мы определили
mov ss, ax ; в GDT (см. ./gdt.asm)
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000 ; Обновляем позицию стека, чтобы он был на самом
mov esp, ebp ; верху свободного места
call BEGIN_PM ; Вызываем функцию из ./main.asm
================================================
FILE: guide/01-KERNEL/ex01/build/Makefile
================================================
C_FILES = $(shell find ../drivers/*.c ../*.c)
temp = $(notdir $(C_FILES))
O_FILES = ${temp:.c=.o}
# флаг для дебага для gcc
CFLAGS = -g
run: os-image.bin
qemu-system-i386 -fda os-image.bin
make clean
debug: os-image.bin kernel.elf
# kernel.elf нужен для gdb как symbol-file
# флаг -s указывает qemu открыть и прослушивать 1234 порт
# чтобы gdb смог соединиться с ним для дебага
# флаг -S указыает QEMU не запускать образ и подождать подключений
qemu-system-i386 -s -S -fda os-image.bin
make clean
os-image.bin: bootsect.bin kernel.bin
cat bootsect.bin kernel.bin > os-image.bin
bootsect.bin:
cd ../boot/ && nasm bootsect.asm -f bin -o ../build/bootsect.bin && cd -
kernel.bin: kernel_entry.o kernel.o
i386-elf-ld -o kernel.bin -Ttext 0x1000 kernel_entry.o kernel.o $(O_FILES) --oformat binary
kernel_entry.o:
nasm ../boot/kernel_entry.asm -f elf -o kernel_entry.o
kernel.o:
i386-elf-gcc ${CFLAGS} -ffreestanding -c ../kernel/kernel.c $(C_FILES)
kernel.elf: kernel_entry.o kernel.o
# как kernel.bin только без --oformat binary
i386-elf-ld -o kernel.bin -Ttext 0x1000 kernel_entry.o kernel.o $(O_FILES) -o kernel.elf
clean:
rm *.bin *.o *.elf
================================================
FILE: guide/01-KERNEL/ex01/common.c
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / common.c
* Title: Всякие удобные константы, типы и функции
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
#include "common.h"
void memcpy(u8 *src, u8 *dest, u32 bytes)
{
u32 i;
i = 0;
while (i < bytes)
{
dest[i] = src[i];
i++;
}
}
================================================
FILE: guide/01-KERNEL/ex01/common.h
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / common.h
* Title: Всякие удобные константы, типы и функции
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
#ifndef COMMON_H
#define COMMON_H
// Указанная размерность характерна только для архитектуры x86
// Подробнее про типы данных: https://metanit.com/cpp/c/2.3.php
typedef unsigned int u32; // беззнаковое целое число размером 32 бита
typedef int s32; // целое число 32 бита со знаком
typedef unsigned short u16; // и т.д.
typedef short s16;
typedef unsigned char u8;
typedef char s8;
void memcpy(u8 *src, u8 *dest, u32 bytes);
#endif
================================================
FILE: guide/01-KERNEL/ex01/drivers/lowlevel_io.c
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / drivers / lowlevel_io.c
* Title: Низкоуровневые I/O функции для работы с девайсами
* ------------------------------------------------------------------------------
* Description:
* Обычно есть два способа взаимодействовать с hardware - memory-mapped I/O
* и I/O ports.
* Если девайс использует memory-mapped I/O, то чтобы передать ему
* какую-либо информацию, нужно записать ее в специальный адрес. Например,
* чтобы вывести символ на экран, мы записывали его в адрес 0xb8000.
* Если девайс использует I/O ports, то для взаимодействия с ним
* используются инструкции ассемблера in и out.
*
* Чтобы работать с дейвасом, нам достаточно знать что в нем есть некий
* контролирующий чип, который делает всю грязную работу за нас и позволяет
* процессору взаимодействовать с напрямую с железом дейваса. У такого чипа
* есть регистры в которые можно что-то записывать или что-то из них
* читать, и значение этих регистров указывет чипу что делать.
*
* Как мы будем читать/писать в регистры контролирующих чипов девайса? Вот
* что нужно знать:
* 1. В архитектуре процессоров Интел, регистры контроля девайсами
* расположены в специальном I/O адресном пространстве.
* 2. Инструкции ввода-ввывода используют такой синтаксис (intel syntax):
* in <записать результат сюда (регистр процессора)>, <прочитать
* содержимое отсюда (регистр девайса)>
* out <записать сюда (регистр девайса)>, <вот это>
* 3. К сожалению, в языке Си нет подобных конструкций, так что нам
* придется использовать inline assembly. Ща посмотрим как.
* ----------------------------------------------------------------------------*/
unsigned char port_byte_in(unsigned short port)
{
/* Функция-обертка над assembly, читающая 1 байт из параметра port */
/* unsigned short port: адрес регистра какого-либо девайса, из которого */
/* мы что-то прочтем. */
/* Используется другой синтаксис ассембли (GAS). Обратите внимание, что */
/* выражение "mov dest, src" в GAS мы запишем как "mov src, dest", т.е. */
/* "in dx, al" означает прочитать содержимое порта (адрес которого */
/* находится в DX) и положить в AL. */
/* Символ % означает регистр, а т.к. % - escape symbol, то мы */
/* пишем еще один %. */
/* Перемещаем результат в регистр AL т.к. размер AL == 1 байт */
unsigned char result;
__asm__("in %%dx, %%al" : "=a" (result) : "d" (port));
/* разберем только что вызванную функцию: */
/* "in %%dx, %%al" - Прочитать содержимое порта и положить это в AL */
/* : "=a" (result) - Положить значение AL в переменную result */
/* : "d" (port) - Загрузить port в регистр EDX (extended DX: 32b) */
return (result); /* Возвращаем прочитанное содержимое из port */
}
void port_byte_out(unsigned short port, unsigned char data)
{
/* Функция-обертка над assembly, пишущая data (1 байт) в port */
/* unsigned short port: адрес регистра девайса, в который что-то запишем */
/* unsigned char data: 1 байт какой-то информации (например, символ) */
__asm__("out %%al, %%dx" : : "a" (data), "d" (port));
/* разберем только что вызванную функцию: */
/* "out %%al, %%dx" - Записать data в port */
/* : : "a" (data) - Загрузить data в регистр EAX */
/* : "d" (port) - Загрузить port в регистр EDX */
}
unsigned char port_word_in(unsigned short port)
{
/* Функция-обертка над assembly, читающая 2 байта из параметра port */
/* Перемещаем результат в регистр AX т.к. размер AX == 2 байта */
unsigned short result;
__asm__("in %%dx, %%ax" : "=a" (result) : "d" (port));
return (result);
}
void port_word_out(unsigned short port, unsigned short data)
{
/* Функция-обертка над assembly, пишущая data (2 байта, т.е. word) в port */
__asm__("out %%ax, %%dx" : : "a" (data), "d" (port));
}
================================================
FILE: guide/01-KERNEL/ex01/drivers/lowlevel_io.h
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / drivers / lowlevel_io.h
* Title: Заголовочный файл для lowlevel_io.c
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
unsigned char port_byte_in(unsigned short port);
void port_byte_out(unsigned short port, unsigned char data);
unsigned char port_word_in(unsigned short port);
void port_word_out(unsigned short port, unsigned short data);
================================================
FILE: guide/01-KERNEL/ex01/drivers/screen.c
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / drivers / screen.c
* Title: Функции работы с экраном
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
#include "screen.h"
#include "lowlevel_io.h"
#include "../common.h"
void kprint(u8 *str)
{
/* Функция печати строки */
// u8 *str: указатель на строку (на первый символ строки). Строка должна
// быть null-terminated.
while (*str)
{
putchar(*str, WHITE_ON_BLACK);
str++;
}
}
void putchar(u8 character, u8 attribute_byte)
{
/* Более высокоуровневая функция печати символа */
// u8 character: байт, соответствующий символу
// u8 attribute_byte: байт, соответствующий цвету текста/фона символа
u16 offset;
offset = get_cursor();
if (character == '\n')
{
// Переводим строку.
if ((offset / 2 / MAX_COLS) == (MAX_ROWS - 1))
scroll_line();
else
set_cursor((offset - offset % MAX_COLS) + MAX_COLS*2);
}
else
{
if (offset == (MAX_COLS * MAX_ROWS * 2)) scroll_line();
write(character, attribute_byte, offset);
set_cursor(offset+2);
}
}
void scroll_line()
{
/* Функция скроллинга */
u8 i = 1; // Начинаем со второй строки.
u16 last_line; // Начало последней строки.
while (i < MAX_ROWS)
{
memcpy(
(u8 *)(VIDEO_ADDRESS + (MAX_COLS * i * 2)),
(u8 *)(VIDEO_ADDRESS + (MAX_COLS * (i-1) * 2)),
(MAX_COLS*2)
);
i++;
}
last_line = (MAX_COLS*MAX_ROWS*2) - MAX_COLS*2;
i = 0;
while (i < MAX_COLS)
{
write('\0', WHITE_ON_BLACK, (last_line + i * 2));
i++;
}
set_cursor(last_line);
}
void clear_screen()
{
/* Функция очистки экрана */
u16 offset = 0;
while (offset < (MAX_ROWS * MAX_COLS * 2))
{
write('\0', WHITE_ON_BLACK, offset);
offset += 2;
}
set_cursor(0);
}
void write(u8 character, u8 attribute_byte, u16 offset)
{
/* Функция печати символа на экран с помощью VGA по адресу 0xb8000 */
// u8 character: байт, соответствующий символу
// u8 attribute_byte: байт, соответствующий цвету текста/фона символа
// u16 offset: смещение (позиция), по которому нужно распечатать символ
u8 *vga = (u8 *) VIDEO_ADDRESS;
vga[offset] = character;
vga[offset + 1] = attribute_byte;
}
u16 get_cursor()
{
/* Функция, возвращающая позицию курсора (char offset). */
port_byte_out(REG_SCREEN_CTRL, 14); // Запрашиваем верхний байт
u8 high_byte = port_byte_in(REG_SCREEN_DATA); // Принимаем его
port_byte_out(REG_SCREEN_CTRL, 15); // Запрашиваем нижний байт
u8 low_byte = port_byte_in(REG_SCREEN_DATA); // Принимаем и его
// Возвращаем смещение умножая его на 2, т.к. порты возвращают смещение в
// клетках экрана (cell offset), а нам нужно в символах (char offset), т.к.
// на каждый символ у нас 2 байта
return (((high_byte << 8) + low_byte) * 2);
}
void set_cursor(u16 pos)
{
/* Функция, устаналивающая курсор по смещнию (позиции) pos */
/* Поиграться с битами можно тут http://bitwisecmd.com/ */
pos /= 2; // конвертируем в cell offset (в позицию по клеткам, а не
// символам)
// Устанавливаем позицию курсора
port_byte_out(REG_SCREEN_CTRL, 14); // Указываем, что будем
// передавать верхний байт
port_byte_out(REG_SCREEN_DATA, (pos >> 8)); // Передаем верхний байт
port_byte_out(REG_SCREEN_CTRL, 15); // Указываем, что будем
// передавать нижний байт
port_byte_out(REG_SCREEN_DATA, (pos & 0xff)); // передаем нижний байт
}
================================================
FILE: guide/01-KERNEL/ex01/drivers/screen.h
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / drivers / screen.h
* Title: Заголовочный файл для screen.c
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
#include "../common.h"
#define VIDEO_ADDRESS 0xb8000 // Адрес начала VGA для печати символов
#define MAX_ROWS 25 // макс. строк
#define MAX_COLS 80 // макс. столбцов
#define WHITE_ON_BLACK 0x0f // 0x0 == white fg, 0xf == black bg
// Адреса I/O портов для взаимодействия с экраном.
#define REG_SCREEN_CTRL 0x3d4 // этот порт для описания данных
#define REG_SCREEN_DATA 0x3d5 // а этот порт для самих данных
void kprint(u8 *str);
void putchar(u8 character, u8 attribute_byte);
void clear_screen();
void write(u8 character, u8 attribute_byte, u16 offset);
void scroll_line();
u16 get_cursor();
void set_cursor(u16 pos);
================================================
FILE: guide/01-KERNEL/ex01/kernel/kernel.c
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / kernel / kernel.c
* Title: Ядро
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
#include "../common.h"
#include "../drivers/screen.h"
s32 kmain()
{
/* Простая программа чтобы продемонстрировать чего мы добились. */
/* Поиграйтесь с кодом и добавьте больше логики, попробуйте заполнить все */
/* 25 строк чтобы посмотреть работу функции scroll_line. */
u8 i;
clear_screen();
i = 0;
while (i < 3)
{
kprint("Hello, world!\n");
i++;
}
return 0;
}
================================================
FILE: src/boot/bootsect.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 01-KERNEL
; File: ex00 / bootsect.asm
; Title: Программа загрузочного сектора, которая загружает ядро, написанное
; на C в 32-битный защищенный режим.
; ------------------------------------------------------------------------------
; Description:
; ------------------------------------------------------------------------------
[org 0x7c00]
KERNEL_OFFSET equ 0x1000 ; Смещение в памяти, из которого мы загрузим ядро
mov [BOOT_DRIVE], dl ; BIOS stores our boot drive in DL , so it ’s
; best to remember this for later. (Remember that
; the BIOS sets us the boot drive in 'dl' on boot)
mov bp, 0x9000 ; Устанавливаем стек
mov sp, bp
mov bx, MSG_REAL_MODE ; Печатаем сообщение
call print_string
call load_kernel ; Загружаем ядро
call switch_to_pm ; Переключаемся в Защищенный Режим
jmp $
%include "print_string.asm" ; ф. печати строки
%include "print_hex.asm" ; ф. печати 16-ричного числа
%include "disk_load.asm" ; ф. чтения диска
%include "print_string_pm.asm" ; ф. печати строки (32PM)
%include "switch.asm" ; ф. переключения в 32PM
%include "gdt.asm" ; таблица GDT
[bits 16]
load_kernel:
mov bx, MSG_LOAD_KERNEL
call print_string ; Печатаем сообщение о том, то мы загружаем ядро
; Устанавливаем параметры для функции disk_load:
mov bx, KERNEL_OFFSET ; Загрузим данные в место памяти по TODO: disk_load main lookup
; смещению KERNEL_OFFSET
mov dh, 16 ; Загрузим много секторов. *
mov dl, [BOOT_DRIVE] ; Загрузим данные из BOOT_DRIVE (Возвращаем BOOT_DRIVE)
call disk_load ; Вызываем функцию disk_load
ret
[bits 32] ; Сюда мы попадем после переключения в 32PM
BEGIN_PM:
mov ebx, MSG_PROT_MODE
call print_string_pm ; Печатаем сообщение об успешной загрузке в 32PM
call KERNEL_OFFSET ; Переходим в адрес, по которому загрузился код ядра
jmp $
BOOT_DRIVE: db 0
MSG_REAL_MODE: db "Started in 16-bit Real Mode", 0
MSG_PROT_MODE: db "Successfully landed in 32-bit Protected mode", 0
MSG_LOAD_KERNEL: db "Loading kernel into VIDEO_MEMORY", 0
times 510-($-$$) db 0
dw 0xaa55
; ------
; * - Забавный факт: если загрузить меньше секторов, то мы столкнемся со
; странными ошибками когда будем писать ядро на Си. Например, аргументы
; функции могут быть повреждены, а строки "обрезаны".
================================================
FILE: src/boot/disk_load.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / disk_load.asm
; Title: Функция чтения диска
; ------------------------------------------------------------------------------
; Description:
; Чтобы лучше понять, что здесь происходит, разберитесь с тем, что такое CHS
; по ссылке https://ru.wikipedia.org/wiki/CHS
; ------------------------------------------------------------------------------
disk_load:
push dx
mov ah, 0x02 ; Указвыаем БИОСу что нам нужна рутина чтения диска
; Указываем что нам нужно:
mov al, dh ; 1. Прочитать кол-во секторов, равное значению в dh
mov ch, 0x00 ; 2. Выбрать нулевой цилиндр
mov dh, 0x00 ; 3. Выбрать нулевую головку
mov cl, 0x02 ; 4. Начинать считывать со второго сектора (т.е.
; первый свободный сектор сразу после загруочного
; сектора, т.к. загрузочный сектор находится по
; адресу 0x01)
int 0x13 ; Вызываем прерывание для чтения
; У БИОСа может не получиться прочитать диск, и
; чтобы дать нам знать что произошла ошибка, он,
; во-первых, обновляет специальный флаг CF (carry
; flag) специальным значением, которое означает
; ошибку, а во-вторых, кладет в регистр AL кол-во
; секторов, которые у него получилось прочитать.
jc disk_error ; jc - инструкция для прыжка на указанную метку,
; которая выполняется только если CF (carry flag)
; сигнализирует об ошибке
pop dx ; Восстанавливаем регистр DX из стека
cmp dh, al ; если AL (кол-во прочитанных секторов) != DH
; (предполагаемое кол-во секторов),
jne disk_sectors_error ; то выводим на экран сообщение об ошибке и зависаем
; (то есть запускаем бесконечный цикл)
jmp disk_success
jmp disk_exit ; Заканчиваем выполнение функции
disk_success:
mov bx, SUCCESS_MSG
call print_string
jmp disk_exit
disk_error:
mov bx, DISK_ERR_MSG ; Перемещаем в BX сообщение об ошибке
call print_string ; Выводим его на экран
mov dh, al
call print_hex
jmp disk_loop ; бесконечный цикл
disk_sectors_error:
mov bx, SECTORS_ERR_MSG
call print_string
SUCCESS_MSG:
db "Disk was successfully read ", 0
DISK_ERR_MSG:
db "Disk read error! ", 0
SECTORS_ERR_MSG:
db "Incorrect number of sectors read ", 0
disk_loop:
jmp $
disk_exit:
ret
================================================
FILE: src/boot/gdt.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / gdt.asm
; Title: Определяем GDT (глобальная таблица дескрипторов)
; ------------------------------------------------------------------------------
; Description:
; Способ, которым процессор переводит логический адрес в физический, в
; 32-битном защищенном режиме отличается от 16-битного реального режима.
; Вместо того, чтобы умножить значение регистра сегмента на 16 и прибавить к
; этому "смещение" (offset), регистр сегмента становится индексом
; определенного дескриптора сегмента в GDT.
; Дескриптор сегмента - это 8-битная структура, которая определяет свойства
; этого сегмента:
; - Base address (32 bits), определяющий откуда сегмент начинается в
; физической памяти.
; - Segment Limit (20 bits), определяющий размер сегмента
; - Различные флаги, которые устанавливают каким образом процессор будет
; "относиться" к сегментам, например уровень привилегий и т.д.
;
; Флаги:
; * 1-ые флаги:
; - present flag (флаг присутствия). Если его значение "1", то это
; указывает, что сегмент присутствует в памяти (это нужно для виртуальной
; памяти)
; - privilege flag (флаг привилегии). Значение "0" - самый высокий уровень
; привилегии
; - descriptor type (тип дескриптора). "1" - для сегмента кода или
; сегмента данных
; * Флаги типа:
; - code (флаг кода). "1" - для кода, "0" - для даннных
; - conformig (флаг подчинения). "0" - чтобы код в другом сегменте с
; более низким уровнем привилегий не смог вызвать код из этого сегмента -
; это ключ к защите памяти (memory protection).
; - readable (читаемость). "1" - если читаемый, "0" - только исполняемый.
; - writable. Разрешает сегменту данных быть записываемым, в противном
; случае, он будет доступен только для чтения.
; - accessed (флаг доступа). Этот флаг устанавливается, когда происходит
; обращение к сегменту.
; - expand down. Флаг (бит), позволяющий сегменту расширяться вниз.
; * 2-ые флаги:
; - granulariy (гранулярность). "0" - байтовая гранулярность, лимит
; задается в байтах, если "1" - страничная гранулярность, в 4кб блоках.
; Если выбрать страничную гранулярность и установить значение лимита как
; 0xfffff, то лимит умножится на 16*16*16 (4кб), и лимит станет 0xfffff000
; позволяя нашему сегменту занять 4гб места в памяти.
; - 32-bit default. "1" - т.к. наш сегмент будет содержать 32-битный код.
; - 64-bit code segment. "0" - т.к. не используется на 32-битных
; процессорах.
; - AVL (available). Определяет доступность сегмента для использования
; системным программным обеспечением (используются только ОС).
; ------------------------------------------------------------------------------
gdt_start: ; Эта пустая метка нужно для того чтобы удобнее
; посчитать размер GDT для ее дескриптора
; (end - start)
gdt_null: ; Необходимый нулевой дескриптор для GDT
dd 0x0 ; dd - define double (двойное слово, т.е. 4 байта)
dd 0x0
gdt_code: ; Определяем дескриптор сегмента кода
dw 0xffff ; Limit (bits 0-15)
dw 0x0 ; Base (bits 0-15)
db 0x0 ; Base (bits 16-23)
db 10011010b ; Первые флаги + флаги типа (смотрим по битам)
; present: 1, privilege: 00, descriptor type: 1
; code: 1, conforming: 0, readable: 1, accessed: 0
db 11001111b ; Вторые флаги + длина сегмента (bits 16-19):
; granularity: 1, 32-bit default: 1,
; 64-bit default: 0, AVL: 0
db 0x0 ; Base (bits 24-31)
gdt_data: ; Определяем дескриптор сегмента кода
dw 0xffff ; Limit (bits 0-15)
dw 0x0 ; Base (bits 0-15)
db 0x0 ; Base (bits 16-23)
db 10010010b ; Первые флаги + флаги типа (смотрим по битам)
db 11001111b ; Вторые флаги + длина сегмента (bits 16-19)
db 0x0 ; Base (bits 24-31)
gdt_end: ; Пустая метка
gdt_descriptor: ; дескриптор GDT
dw gdt_end - gdt_start - 1 ; Размер GDT
dd gdt_start ; Адрес начала GDT
CODE_SEG equ gdt_code - gdt_start ; Определяем некоторые константы.
DATA_SEG equ gdt_data - gdt_start ; Они понадобятся для регистров сегментов в
; 32-битном защищенном режиме. Например,
; когда мы установим регистр DS = 0x10 (т.е
; 16 байтов) в этом режиме, процессор
; поймет что мы хотим использовать сегмент,
; находящийся в смещении 0x10 в нашем GDT,
; т.е. в нашем случае это сегмент данных
; (0x0 -> NULL, 0x08 -> сегмент кода,
; 0x10 -> сегмент данных)
================================================
FILE: src/boot/kernel_entry.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 01-KERNEL
; File: ex00 / kernel_entry.asm
; Title: Код, служащий входной точкой для функции kmain из kernel.c
; ------------------------------------------------------------------------------
; Description:
; ------------------------------------------------------------------------------
[bits 32]
[extern kmain] ; Определяем 'внешнюю' штуку с названием kmain - она понадобится
; линкеру чтобы собрать все вместе
call kmain ; Вызываем определенную выше функцию, которая будет доступна
; после линковки. Это функция kmain из kernel.c
jmp $
================================================
FILE: src/boot/print_hex.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex05 / print_hex.asm
; Title: Функция вывода шестнадцатеричного числа на экран
; ------------------------------------------------------------------------------
; Description:
; Как выглядит регистр EAX (32 b = 32 бита):
;
; EAX (32 b)
; ---------------------------------------------
; | | | |
; | | AH (8 b) | AL (8 b) |
; | | | |
; ---------------------------------------------
; AX (16 b)
;
; Как видим, EAX содержит в себе регистр AX, который в свою очередь разделен
; на AH (a high, верхний) и AL (a low, нижний).
; ------------------------------------------------------------------------------
; Помним, что в main.asm:
; DX = 0x1fb6
; BX = "0x0000" (а точнее, BX="0", т.к. указывает на первый элемент)
print_hex:
pusha ; Сохраняем значения регистров в стеке
mov cx, 0 ; Регистр CX будет служить счетчиком
loop1:
cmp cx, 4 ; if (CX < 4)
jl print ; Переходим к print
jmp end ; else переходим к end
print:
mov ax, dx ; В AX теперь 0x1fb6
and ax, 0x000f ; В AX теперь 6 (последняя цифра от 0x1fb6).
cmp ax, 9 ; if (AX > 9) (проверяем обозначается ли число
; буквой т.к. мы помним что цифра больше 9 в
; 16-ричной системе исчисления обозначается
; буквой латинского алфавита)
jg num_to_abc ; Переходим к num_to_abc
jmp next
num_to_abc: ; Перевод числа в букву (так, как она бы
; выглядела в 16-ричной СИ), например число 15
; в десятичной будет равно f в 16-ричной
add ax, 39 ; Добавляем к этому числу 39, чтобы затем еще
; добавить 48 ('0'), получая код
; соответсвующего символа в ASCII (например,
; f (как число, то есть 15) + 39 + 48 (код '0')
; = 102 (то есть 'f' в ASCII, как нам и нужно)
jmp next
next:
add ax, '0' ; Добавляем 48 в ax
mov bx, HEX_OUT + 5 ; Теперь bx указывает на последний символ строки
; HEX_OUT
sub bx, cx ; BX = BX - counter (для итерации)
mov [bx], al ; Так как мы разыменовываем bx (вот так: [bx]),
; то [bx] это не регистр, а ссылка на память,
; и так как ax = 16 бит (2 байта), чтобы нам не
; перезаписать лишнюю память, в [bx] мы помещаем
; не ax, а al, размер которого равен 1-му байту.
ror dx, 4 ; было: 0x1fb6, стало: 0x61fb (переносим
; последнюю цифру в начало)
inc cx ; counter++
jmp loop1 ; переходим обратно к loop1
end:
mov bx, HEX_OUT ; Делаем так, чтобы bx снова указывал на первый
; символ строки HEX_OUT
call print_string ; выводи на экран строку из регистра bx
popa ; Возвращаем регистрам их изначальное значение
ret ; Заканчиваем выполнение функции
HEX_OUT: db "0x0000", 0
================================================
FILE: src/boot/print_string.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex04 / print_string.asm
; Title: Функция вывода строки на экран
; ------------------------------------------------------------------------------
; Description: null
; ------------------------------------------------------------------------------
print_string: ; Функция вывода строки на экран.
pusha ; Когда мы используем функции, мы можем модифицировать
; регистры прямо в них, что нарушает чистоту функции,
; т.е. мы можем перезаписывать какие-то внешние
; данные. Для этого мы добавляем значение всех регистров
; в стек с помощью команды pusha, а в конце функции
; мы возвращаем регистрам их изначальные значения,
; которые возьмем из стека (команда popa).
mov ah, 0x0e ; tele-type mode
loop: ; Метка loop (= цикл)
mov al, [bx] ; Перемещаем значение BX в AL, т.к. мы помним что в BX
; лежит первый символ строки (см. ./main.asm)
cmp al, 0 ; Команда cmp для сравнения AL и 0.
je newline ; (if) je = "jump if equal",
; т.е. перемещаемся к коду с меткой return если AL == 0
jmp put_char ; (else) в противном случае перемещаемся к put_char.
put_char: ; Метка put_char - вывод символа на экран.
int 0x10 ; Вызываем прерывание, которое позволяет вывести
; на экран значение регистра AL, в котором лежит [bx].
inc bx ; inc <регистр> - увеличить на 1.
jmp loop ; Возвращаемся обратно к циклу.
newline:
mov ah, 0x0e
mov al, 0x0a ; (0x0a) \n - new line
int 0x10
mov al, 0x0d ; (0x0d) \r - carriage return
int 0x10
jmp return
return: ; Метка return - завершаем функцию
popa ; Возвращаем регистрам их изначальные значения.
ret ; Заканчиваем выполнение функции.
================================================
FILE: src/boot/print_string_pm.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / print_string_pm.asm
; Title: Функция вывода строки на экран в 32-битном защищенном режиме
; ------------------------------------------------------------------------------
; Description:
; Плюсы 32-битного режима: возможность использовать 32-битные регистры и
; адрессацию памяти, защищенную память виртуальную память
; Минусы: отсутствие БИОС прерываний, требование наличия GDT (об этом позже)
; В этой программе мы напишем новую функцию печати строки, но без прерываний
; БИОСа, а напрямую манипулируя VGA видеопамятью, вместо вызова int 0x10.
; VGA память размещена начиная с адреса 0xb8000, и у VGA имеется специальный
; текстовый режим, поэтому нам не придется напрямую рисовать пиксели.
; Особенности:
; 1. Символ представляется в виде 2-х байтов. Первый байт - сам символ,
; второй байт - 4 бита на цвет текста и еще 4 на цвет фона.
; Например, чтобы распечатать символ 'A' белым текстом на черном фоне, мы
; испольузуем 0x410f: 0x41 == 'A', 0 == белый, f == черный.
;
; Bit: | 15 14 13 12 11 10 9 8 | 7 6 5 4 | 3 2 1 0 |
; Content: | ASCII | FG | BG |
;
; ------------------------------------------------------------------------------
[bits 32] ; Используем 32-битный режим
; Определяем некоторые константы
VIDEO_MEMORY equ 0xb8000 ; = адрес начала памяти VGA
WHITE_ON_BLACK equ 0x0f ; = цвет символов (0x0f - белый на черном)
print_string_pm:
pusha
mov edx, VIDEO_MEMORY ; Перемещаем в EDX адрес начала массива видеопамяти
print_string_pm_loop:
; Помним, что AX (2б) = AH(1б) и AL(1б)
mov al, [ebx] ; Сохраняем символ из EBX в AL
mov ah, WHITE_ON_BLACK ; Устанавливаем цвет символов в AH
; таким образом AX получается равен символу + цвету
cmp al, 0 ; if (AL == 0), т.е. если конец массива, то
je print_string_pm_done ; заканчиваем выполнение функции
; else:
mov [edx], ax ; video_memory[EDX] = AX
add ebx, 1 ; переходим к следующему символу (+1, просто массив)
add edx, 2 ; переходим к следующему адресу в VGA (+2 т.к.
; два байта на символ)
jmp print_string_pm_loop
print_string_pm_done:
popa
ret
================================================
FILE: src/boot/switch.asm
================================================
; ------------------------------------------------------------------------------
; Guide: 00-BOOT-SECTOR
; File: ex08 / switch.asm
; Title: Переключаемся в PM (Protected mode, защищенный режим)
; ------------------------------------------------------------------------------
; Description:
; Чтобы сделать свитч, нам нужно:
; 1. Отключить прерывания (процессор просто будет их игнорировать), т.к.
; в PM, прерывания обрабатываются совершенно по-другому в отличие от
; Real Mode. Даже если процессор и смог бы распределить сигналы прерываний
; по конкретным BIOS обработчикам прерываний, БИОС обработчики будут
; обрабатывать 16-битный код, что повлекло бы за собой ошибки.
; 2. Загрузить GDT дескриптор
; 3. Изменяем первый бит регистра управления cr0 на "1"
; https://en.wikipedia.org/wiki/Control_register#CR0
; 4. Т.к. процессор использует специальную технику, которая называется
; называется pipelining (гугли: вычислительный конвейер, полезная статья
; на хабре: https://habr.com/ru/post/182002/), и поэтому сразу после того,
; как перевести процессор в PM (что мы и сделали в предыдущем пункте),
; нам нужно заставить процессор завершить всю работу в конвейере, чтобы
; быть уверенным, что все будущие инструкции будут выполнены корректно.
; Конвейер загружает в себя некоторые количество последующих после текущей
; инструкций, но конвейеру не очень нравятся инструкции типа call и jmp,
; т.к. процессор не знает полностью какие инструкции будут следовать за
; ними, в особенности если мы вызовем jmp или call "прыгая" в другой
; сегмент. Поэтому нам нужно сделать "дальний прыжок", чтобы завершить
; обрабатываемые в конвейере инструкции.
; Сам прыжок: jmp <сегмент>:<адрес смещения>
; ------------------------------------------------------------------------------
[bits 16]
switch_to_pm:
cli ; Отключаем прерывания (cli = clear interrupts)
lgdt [gdt_descriptor] ; Загружаем GDT дескриптор (lgdt = load GDT)
mov eax, cr0 ; Чтобы перейти в PM, нужно чтобы первый бит
or eax, 0x1 ; регистра управления cr0 был 1
mov cr0, eax
jmp CODE_SEG:init_pm ; Делаем "дальний прыжок" в наш новый 32-битный
; сегмент кода. Это так же заставляет процессор
; завершить обрабатываемые в конвейере инструкции.
[bits 32]
init_pm: ; в PM, наши старые сегменты бесполезны, поэтому
mov ax, DATA_SEG ; мы делаем так, чтобы регистры всех сегментов
mov ds, ax ; указывали на сегмент данных, который мы определили
mov ss, ax ; в GDT (см. ./gdt.asm)
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000 ; Обновляем позицию стека, чтобы он был на самом
mov esp, ebp ; верху свободного места
call BEGIN_PM ; Вызываем функцию из ./main.asm
================================================
FILE: src/build/Makefile
================================================
C_FILES = $(shell find ../kernel/*.c ../drivers/*.c ../*.c)
temp = $(notdir $(C_FILES))
O_FILES = ${temp:.c=.o}
# флаг для дебага для gcc
CFLAGS = -g
run: os-image.bin
qemu-system-i386 -fda os-image.bin
make clean
debug: os-image.bin kernel.elf
# kernel.elf нужен для gdb как symbol-file
# флаг -s указывает qemu открыть и прослушивать 1234 порт
# чтобы gdb смог соединиться с ним для дебага
# флаг -S указыает QEMU не запускать образ и подождать подключений
qemu-system-i386 -s -S -fda os-image.bin
make clean
os-image.bin: bootsect.bin kernel.bin
cat bootsect.bin kernel.bin > os-image.bin
bootsect.bin:
cd ../boot/ && nasm bootsect.asm -f bin -o ../build/bootsect.bin && cd -
kernel.bin: kernel_entry.o kernel.o
i386-elf-ld -o kernel.bin -Ttext 0x1000 kernel_entry.o $(O_FILES) --oformat binary
kernel_entry.o:
nasm ../boot/kernel_entry.asm -f elf -o kernel_entry.o
kernel.o:
i386-elf-gcc ${CFLAGS} -ffreestanding -c $(C_FILES)
kernel.elf: kernel_entry.o kernel.o
# как kernel.bin только без --oformat binary
i386-elf-ld -o kernel.bin -Ttext 0x1000 kernel_entry.o $(O_FILES) -o kernel.elf
clean:
rm *.bin *.o *.elf
================================================
FILE: src/common.c
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / common.c
* Title: Всякие удобные константы, типы и функции
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
#include "common.h"
void memcpy(u8 *src, u8 *dest, u32 bytes)
{
u32 i;
i = 0;
while (i < bytes)
{
dest[i] = src[i];
i++;
}
}
================================================
FILE: src/common.h
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / common.h
* Title: Всякие удобные константы, типы и функции
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
#ifndef COMMON_H
#define COMMON_H
// Указанная размерность характерна только для архитектуры x86
// Подробнее про типы данных: https://metanit.com/cpp/c/2.3.php
typedef unsigned int u32; // беззнаковое целое число размером 32 бита
typedef int s32; // целое число 32 бита со знаком
typedef unsigned short u16; // и т.д.
typedef short s16;
typedef unsigned char u8;
typedef char s8;
void memcpy(u8 *src, u8 *dest, u32 bytes);
#endif
================================================
FILE: src/drivers/lowlevel_io.c
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / drivers / lowlevel_io.c
* Title: Низкоуровневые I/O функции для работы с девайсами
* ------------------------------------------------------------------------------
* Description:
* Обычно есть два способа взаимодействовать с hardware - memory-mapped I/O
* и I/O ports.
* Если девайс использует memory-mapped I/O, то чтобы передать ему
* какую-либо информацию, нужно записать ее в специальный адрес. Например,
* чтобы вывести символ на экран, мы записывали его в адрес 0xb8000.
* Если девайс использует I/O ports, то для взаимодействия с ним
* используются инструкции ассемблера in и out.
*
* Чтобы работать с дейвасом, нам достаточно знать что в нем есть некий
* контролирующий чип, который делает всю грязную работу за нас и позволяет
* процессору взаимодействовать с напрямую с железом дейваса. У такого чипа
* есть регистры в которые можно что-то записывать или что-то из них
* читать, и значение этих регистров указывет чипу что делать.
*
* Как мы будем читать/писать в регистры контролирующих чипов девайса? Вот
* что нужно знать:
* 1. В архитектуре процессоров Интел, регистры контроля девайсами
* расположены в специальном I/O адресном пространстве.
* 2. Инструкции ввода-ввывода используют такой синтаксис (intel syntax):
* in <записать результат сюда (регистр процессора)>, <прочитать
* содержимое отсюда (регистр девайса)>
* out <записать сюда (регистр девайса)>, <вот это>
* 3. К сожалению, в языке Си нет подобных конструкций, так что нам
* придется использовать inline assembly. Ща посмотрим как.
* ----------------------------------------------------------------------------*/
unsigned char port_byte_in(unsigned short port)
{
/* Функция-обертка над assembly, читающая 1 байт из параметра port */
/* unsigned short port: адрес регистра какого-либо девайса, из которого */
/* мы что-то прочтем. */
/* Используется другой синтаксис ассембли (GAS). Обратите внимание, что */
/* выражение "mov dest, src" в GAS мы запишем как "mov src, dest", т.е. */
/* "in dx, al" означает прочитать содержимое порта (адрес которого */
/* находится в DX) и положить в AL. */
/* Символ % означает регистр, а т.к. % - escape symbol, то мы */
/* пишем еще один %. */
/* Перемещаем результат в регистр AL т.к. размер AL == 1 байт */
unsigned char result;
__asm__("in %%dx, %%al" : "=a" (result) : "d" (port));
/* разберем только что вызванную функцию: */
/* "in %%dx, %%al" - Прочитать содержимое порта и положить это в AL */
/* : "=a" (result) - Положить значение AL в переменную result */
/* : "d" (port) - Загрузить port в регистр EDX (extended DX: 32b) */
return (result); /* Возвращаем прочитанное содержимое из port */
}
void port_byte_out(unsigned short port, unsigned char data)
{
/* Функция-обертка над assembly, пишущая data (1 байт) в port */
/* unsigned short port: адрес регистра девайса, в который что-то запишем */
/* unsigned char data: 1 байт какой-то информации (например, символ) */
__asm__("out %%al, %%dx" : : "a" (data), "d" (port));
/* разберем только что вызванную функцию: */
/* "out %%al, %%dx" - Записать data в port */
/* : : "a" (data) - Загрузить data в регистр EAX */
/* : "d" (port) - Загрузить port в регистр EDX */
}
unsigned char port_word_in(unsigned short port)
{
/* Функция-обертка над assembly, читающая 2 байта из параметра port */
/* Перемещаем результат в регистр AX т.к. размер AX == 2 байта */
unsigned short result;
__asm__("in %%dx, %%ax" : "=a" (result) : "d" (port));
return (result);
}
void port_word_out(unsigned short port, unsigned short data)
{
/* Функция-обертка над assembly, пишущая data (2 байта, т.е. word) в port */
__asm__("out %%ax, %%dx" : : "a" (data), "d" (port));
}
================================================
FILE: src/drivers/lowlevel_io.h
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / drivers / lowlevel_io.h
* Title: Заголовочный файл для lowlevel_io.c
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
unsigned char port_byte_in(unsigned short port);
void port_byte_out(unsigned short port, unsigned char data);
unsigned char port_word_in(unsigned short port);
void port_word_out(unsigned short port, unsigned short data);
================================================
FILE: src/drivers/screen.c
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / drivers / screen.c
* Title: Функции работы с экраном
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
#include "screen.h"
#include "lowlevel_io.h"
#include "../common.h"
void kprint(u8 *str)
{
/* Функция печати строки */
// u8 *str: указатель на строку (на первый символ строки). Строка должна
// быть null-terminated.
while (*str)
{
putchar(*str, WHITE_ON_BLACK);
str++;
}
}
void putchar(u8 character, u8 attribute_byte)
{
/* Более высокоуровневая функция печати символа */
// u8 character: байт, соответствующий символу
// u8 attribute_byte: байт, соответствующий цвету текста/фона символа
u16 offset;
offset = get_cursor();
if (character == '\n')
{
// Переводим строку.
if ((offset / 2 / MAX_COLS) == (MAX_ROWS - 1))
scroll_line();
else
set_cursor((offset - offset % (MAX_COLS*2)) + MAX_COLS*2);
}
else
{
if (offset == (MAX_COLS * MAX_ROWS * 2)) scroll_line();
write(character, attribute_byte, offset);
set_cursor(offset+2);
}
}
void scroll_line()
{
/* Функция скроллинга */
u8 i = 1; // Начинаем со второй строки.
u16 last_line; // Начало последней строки.
while (i < MAX_ROWS)
{
memcpy(
(u8 *)(VIDEO_ADDRESS + (MAX_COLS * i * 2)),
(u8 *)(VIDEO_ADDRESS + (MAX_COLS * (i-1) * 2)),
(MAX_COLS*2)
);
i++;
}
last_line = (MAX_COLS*MAX_ROWS*2) - MAX_COLS*2;
i = 0;
while (i < MAX_COLS)
{
write('\0', WHITE_ON_BLACK, (last_line + i * 2));
i++;
}
set_cursor(last_line);
}
void clear_screen()
{
/* Функция очистки экрана */
u16 offset = 0;
while (offset < (MAX_ROWS * MAX_COLS * 2))
{
write('\0', WHITE_ON_BLACK, offset);
offset += 2;
}
set_cursor(0);
}
void write(u8 character, u8 attribute_byte, u16 offset)
{
/* Функция печати символа на экран с помощью VGA по адресу 0xb8000 */
// u8 character: байт, соответствующий символу
// u8 attribute_byte: байт, соответствующий цвету текста/фона символа
// u16 offset: смещение (позиция), по которому нужно распечатать символ
u8 *vga = (u8 *) VIDEO_ADDRESS;
vga[offset] = character;
vga[offset + 1] = attribute_byte;
}
u16 get_cursor()
{
/* Функция, возвращающая позицию курсора (char offset). */
port_byte_out(REG_SCREEN_CTRL, 14); // Запрашиваем верхний байт
u8 high_byte = port_byte_in(REG_SCREEN_DATA); // Принимаем его
port_byte_out(REG_SCREEN_CTRL, 15); // Запрашиваем нижний байт
u8 low_byte = port_byte_in(REG_SCREEN_DATA); // Принимаем и его
// Возвращаем смещение умножая его на 2, т.к. порты возвращают смещение в
// клетках экрана (cell offset), а нам нужно в символах (char offset), т.к.
// на каждый символ у нас 2 байта
return (((high_byte << 8) + low_byte) * 2);
}
void set_cursor(u16 pos)
{
/* Функция, устаналивающая курсор по смещнию (позиции) pos */
/* Поиграться с битами можно тут http://bitwisecmd.com/ */
// конвертируем в cell offset (в позицию по клеткам, а не символам)
pos /= 2;
// Указываем, что будем передавать верхний байт
port_byte_out(REG_SCREEN_CTRL, 14);
// Передаем верхний байт
port_byte_out(REG_SCREEN_DATA, (u8)(pos >> 8));
// Указываем, что будем передавать нижний байт
port_byte_out(REG_SCREEN_CTRL, 15);
// Передаем нижний байт
port_byte_out(REG_SCREEN_DATA, (u8)(pos & 0xff));
}
================================================
FILE: src/drivers/screen.h
================================================
/*------------------------------------------------------------------------------
* Guide: 01-KERNEL
* File: ex01 / drivers / screen.h
* Title: Заголовочный файл для screen.c
* ------------------------------------------------------------------------------
* Description:
* ----------------------------------------------------------------------------*/
#include "../common.h"
#define VIDEO_ADDRESS 0xb8000 // Адрес начала VGA для печати символов
#define MAX_ROWS 25 // макс. строк
#define MAX_COLS 80 // макс. столбцов
#define WHITE_ON_BLACK 0x0f // 0x0 == white fg, 0xf == black bg
// Адреса I/O портов для взаимодействия с экраном.
#define REG_SCREEN_CTRL 0x3d4 // этот порт для описания данных
#define REG_SCREEN_DATA 0x3d5 // а этот порт для самих данных
void kprint(u8 *str);
void putchar(u8 character, u8 attribute_byte);
void clear_screen();
void write(u8 character, u8 attribute_byte, u16 offset);
void scroll_line();
u16 get_cursor();
void set_cursor(u16 pos);
================================================
FILE: src/kernel/kernel.c
================================================
#include "../common.h"
#include "../drivers/screen.h"
#include "./utils.h"
s32 kmain()
{
clear_screen();
kprint_rick_and_morty();
kprint(
"- Look Rick, we are in an OS!\n"
"- Damn Morty, that's fantastic!\n\n"
);
kprint("So... Welcome to my OS!\n\n");
kprint(
"Actually, the functionality of this OS is limited only to displaying that creepy "
"ASCII art. No commands, no more features. The only purpose of this OS is "
"educational - you can follow easy steps in guide/ folder and create your own OS, "
"just like this. The guide is well-documented and all written in Russian.\n\n"
);
kprint("Thank you for your attention!\n");
kprint("Find me on GitHub: ");
kprint_colored("https://github.com/thedenisnikulin", 0xf0);
return 0;
}
================================================
FILE: src/kernel/utils.c
================================================
#include "./utils.h"
#include "../drivers/screen.h"
#include "../common.h"
void kprint_rick_and_morty()
{
u8 *char_map[] = {
"__#_#_#\n",
"__#####____", "#####\n",
"__#", "###", "#____", "#", "###", "#\n",
"__#", "#", "#", "#", "#____", "#", "#", "#", "#", "#\n",
"__#", "#", "#", "#", "#____", "#", "#", "#", "#", "#\n",
"__#####____", "#####\n",
"___###_____", "_###_\n",
"__##", "#", "##____", "#####\n",
"__##", "#", "##____", "#", "###", "#\n",
"__##", "#", "##____", "#", "###", "#\n",
"__#", "#", "#", "#", "#____", "#", "###", "#\n",
"___##", "#______", "###\n",
"___#_#______#_#\n"
};
u8 color_map[] = {
0xbb,
0xbb, 0x66,
0xbb, 0xff, 0xbb, 0x66, 0xff, 0x66,
0xff, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00, 0xff, 0x00, 0xff,
0xff, 0x00, 0xff, 0x00, 0xff, 0xff, 0x00, 0xff, 0x00, 0xff,
0xff, 0xff,
0xff, 0xff,
0x77, 0xbb, 0x77, 0xee,
0x77, 0xbb, 0x77, 0xff, 0xee, 0xff,
0x77, 0xbb, 0x77, 0xff, 0xee, 0xff,
0xff, 0x77, 0xbb, 0x77, 0xff, 0xff, 0xee, 0xff,
0x99, 0x77, 0x99,
0x99
};
u8 i = 0;
while (i < 61)
{
kprint_colored(char_map[i], color_map[i]);
i++;
}
}
void kprint_colored(u8 *str, u8 attr)
{
while (*str)
{
if (*str == '_')
putchar(*str, 0x00);
else
putchar(*str, attr);
str++;
}
}
================================================
FILE: src/kernel/utils.h
================================================
#include "../common.h"
void kprint_rick_and_morty();
void kprint_colored(u8 *str, u8 attr);