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);