Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Школа ассемблера: расширение возможностей разработанной операционной системы

Оригинал: AsmSchool: Expand the self-made OS
Автор: Mike Saunders
Дата публикации: 18 апреля 2016 г.
Перевод: А.Панин
Дата перевода: 2 мая 2016 г.

Часть 5: Завершаем серию статей, реализуя доступный пользовательским приложениям механизм системных вызовов, такой же, как и в популярных операционных системах.

Для чего это нужно?

  1. Для понимания принципов работы компиляторов.
  2. Для понимания инструкций центрального процессора.
  3. Для оптимизации вашего кода в плане производительности.

В прошлой статье мы реализовали простейшую операционную систему, собрав воедино все техники и приемы, описанные в каждой из четырех статей серии. Получившаяся миниатюрная ОС состояла из небольшого системного загрузчика, который загружал ее ядро с флоппи-диска с файловой системой FAT 12 и самого ядра ОС, которое после загрузки выводило приветствие и обрабатывало пользовательский ввод. При вводе команды ls пользователь получал список файлов, расположенных на диске; если же вводилось имя какого-либо из файлов, операционная система загружала его и пыталась исполнить.

Это уже неплохо, но если посмотреть на нашу операционную систему под другим углом, можно сделать вывод, что на данном этапе она является всего лишь прослойкой для запуска программ. Полноценные операционные системы позволяют программам использовать некоторые из своих функций, для того, чтобы разработчикам каждой из программ не приходилось создавать реализации одних и тех же механизмов. Например, каждая отдельная программа не должна содержать реализацию механизма обработки строк - этот механизм может быть реализован в формате одной из функций нашей операционной системы. Таким образом, в данной, завершающей статье серии статей об использовании языка ассемблера мы будем добавлять в ядро операционной системы функции, которые впоследствии смогут использоваться обычными программами.

Предоставление доступа к системным функциям

В данной статье мы будем работать с кодовой базой месячной давности, поэтому перед началом работы следует загрузить архив исходного кода, расположенный по адресу www.linuxvoice.com/code/lv015/asmschool.zip. В качестве напоминания ниже приведен список всех используемых файлов исходного кода с их описаниями:

  • boot.asm - 512-байтовый системный загрузчик, добавляемый в начало файла образа флоппи-диска с файловой системой FAT 12.
  • bpb.asm - блок параметров BIOS, являющийся набором информации для идентификации диска, подключаемый к файлу исходного boot.asm в процессе ассемблирования.
  • disk.asm - подпрограммы для чтения данных из файловой системы FAT 12, используемые в рамках файла исходного кода boot.asm.
  • mykernel.asm - ядро нашей операционной системы, включающее реализацию простых механизмов обработки пользовательского ввода и исполнения программ.
  • lib.asm - набор полезных подпрограмм для работы с экраном, клавиатурой и диском, который подключается к файлу исходного кода mykernel.asm в процессе ассемблирования.
  • test.asm - тестовая программа, которая может загружаться и исполняться средствами нашей ОС - она всего лишь выводит символ 'X' и завершает свою работу.

Для ассемблирования системного загрузчика используется следующая команда:

nasm -f bin -o boot.bin boot.asm

Для создания файла виртуального образа флоппи-диска с именем floppy.img в формате MS-DOS и добавления бинарного файла системного загрузчика в первый 512-байтовый сектор - следующая последовательность команд:

mkdosfs -C floppy.img 1440
dd conv=notrunc if=boot.bin of=floppy.img

Для ассемблирования ядра и тестовой программы с последующим добавлением их в файл образа флоппи-диска - следующая последовательность команд:

nasm -f bin -o mykernel.bin mykernel.asm
nasm -f bin -o test.bin test.asm
mcopy -i floppy.img mykernel.bin ::/
mcopy -i floppy.img test.bin ::/

(Обратите внимание на то, что утилита mcopy является частью пакета GNU Mtools, который доступен из репозиториев большинства дистрибутивов.) Теперь вы можете загрузить файл образа флоппи-диска с именем floppy.img с помощью такого эмулятора ПК, как QEMU или VirtualBox, например, с помощью следующей команды:

qemu-system-i386 -fda floppy.img

Таким образом, приведенные выше команды могут успешно использоваться для сборки кодовой базы текущей версии нашего проекта. А теперь давайте приступим к созданию новой версии MyOS 2.0!

Каков наш вектор действий?

Представьте, что у вас возникла необходимость в использовании подпрограммы lib_print_string, находящейся в составе ядра ОС, из кода приложения, находящегося в файле test.asm. Что нужно сделать для этого? Скорее всего, следует использовать инструкцию call с адресом подпрограммы lib_print_string в оперативной памяти. Но в этом случае возникнет одна серьезная проблема: на уровне файла исходного кода test.asm просто неоткуда получить информацию об адресе подпрограммы lib_print_string в памяти. Если мы дизассемблируем бинарный код ядра ОС с помощью команды ndisasm mykernel.bin и осуществим поиск инструкции 'pushaw' (то есть, первой инструкции подпрограммы lib_print_string), мы обнаружим, что код подпрограммы lib_print_string начинается с позиции 577 (позиции отображаются в шестнадцатеричном формате в колонке слева).

Следовательно, найденное значение и будет адресом подпрограммы lib_print_string в памяти - вы можете просто использовать данное значение в рамах файла исходного кода test.asm, разместив указатель на строку в регистре SI и задействовав инструкцию call 0x557 для вывода строки на экран, не правда ли? Да, это сработает, но в случае использования подобной техники мы столкнемся с еще одной проблемой: адрес подпрограммы lib_print_string в памяти будет меняться после модификации исходного кода ядра ОС. Он может сместиться как на несколько байт даже после небольшой правки исходного кода, так и на несколько килобайт, если мы добавим большой объем кода в файл исходного кода lib.asm перед рассматриваемой подпрограммой.

К счастью, существует способ обхода данной проблемы: мы можем использовать векторы. Если мы разместим множество инструкций jmp в начале исходного кода нашего ядра ОС, они позволят осуществлять переходы к различным подпрограммам в рамках файла исходного кода lib.asm, причем их адреса будут оставаться неизменными при любых обстоятельствах. Эти инструкции перехода называются векторами и позволяют осуществлять корректную переадресацию вызовов, поступающих от программ.

Давайте рассмотрим описанный механизм в действии. Отредактируйте файл исходного кода mykernel.asm, добавив в самое начало три инструкции jmp, а также метку 'start' следующим образом:

	jmp start             ; 0x0000
	jmp lib_print_string  ; 0x0002
	jmp lib_input_string  ; 0x0005

start:
	mov ax, 2000h
	mov ds, ax
	mov es, ax

loop:
	mov si, prompt
	call lib_print_string
	...

Шестнадцатеричные значения в комментариях рядом с этими инструкциями jmp указывают их точные адреса в памяти после загрузки ядра ОС. Таким образом, инструкция jmp start расположена по адресу 0 (так как она располагается в самом начале кода ядра ОС), инструкция jmp lib_input_string - по адресу 0x0002, а следующая инструкция jmp - по адресу 0x0005. (Первая инструкция jmp занимает всего два байта, так как осуществляется переход к очень близкому адресу, удаленному всего на несколько байт, при этом все остальные инструкции jmp занимают по три байта и позволяют осуществлять переходы достаточно удаленным фрагментам кода, для указания на которые приходится использовать более длинные адреса.) Вы можете выяснить адреса, к которым осуществляется переход с помощью рассматриваемых инструкций, воспользовавшись командой ndisasm mykernel.bin - адреса отображаются в колонке слева.

Упрощение жизни сторонних разработчиков

На текущий момент мы создали три вектора: если программа желает использовать подпрограмму lib_print_string, то вместо использования ее точного адреса в памяти, она может просто вызвать вектор по адресу 0x0002, который осуществит переход к адресу подпрограммы lib_print_string. Этот вектор будет всегда находиться по адресу 0x0002, конечно же, если мы не решим изменить порядок следования векторов - но, как всегда красноречиво говорит Линус Торвальдс, мы НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ НЕ ДОЛЖНЫ нарушать работу программ пространства пользователя - поэтому адреса векторов не будут изменяться. В будущем мы сможем добавить дополнительные векторы, но это никоим образом не повлияет на адреса уже существующих векторов.

Попробуйте модифицировать файл исходного кода test.asm следующим образом:

	ORG 32768

	mov si, string
	call 0x0002
	ret

	string db "Ciao!", 0

Осуществите ассемблирование файлов исходного кода ядра ОС и test.asm, добавьте результирующие бинарные файлы в файл образа диска в соответствии с приведенными выше инструкциями и загрузите ОС. Выполните команду test.bin и вуаля: программа задействует подпрограмму lib_print_string из состава ядра ОС. Программе абсолютно не нужно знать точный адрес упомянутой подпрограммы в памяти, ведь она всегда может вызвать вектор с адресом 0x0002, с помощью которого будет осуществлен переход к необходимому адресу бинарного кода.

Это же утверждение актуально и для подпрограммы lib_input_string - ведь мы добавили вектор и для нее. Теперь вы можете сделать шаг вперед и самостоятельно добавить другие векторы для подпрограмм из библиотеки lib.asm, превратив свой проект в полноценную операционную систему!

Еще одно испытание на реальном компьютере, в ходе которого демонстрируются возможности подпрограмм для работы с графикой, добавленных в ядро нашей простейшей операционной системы

Еще одно испытание на реальном компьютере, в ходе которого демонстрируются возможности подпрограмм для работы с графикой, добавленных в ядро нашей простейшей операционной системы

А теперь представьте, что вы желаете привлечь сторонних разработчиков к процессу создания программного обеспечения для вашей ОС. Если вы предложите им осуществлять вызовы подпрограмм по различным шестнадцатеричным адресам, все будет выглядеть немного непонятно, поэтому мы можем облегчить их жизнь, сделав наш псевдо-API более привлекательным путем привязки псевдонимов к упомянутым шестнадцатеричным адресам. Создайте файл с именем myos.inc в директории с файлом исходного кода myos.asm и добавьте в него следующие строки:

lib_print_string equ 0x0002
lib_input_string equ 0x0005

Директива equ в данном случае обозначает следующее: строка слева эквивалентна числовому значению справа. Исходя из этого, вы можете модифицировать файл исходного кода test.asm следующим образом:

	%include "myos.inc"

	ORG 32768

	mov si, string
	call lib_print_string
	ret

	string db "Ciao! inc", 0

После подключения файла myos.inc к нашему файлу исходного кода test.asm, мы можем использовать читаемые имена подпрограмм, а не только шестнадцатеричные значения адресов векторов (разумеется, последние также будут работать). По мере добавления дополнительных подпрограмм в библиотеку lib.asm и создания векторов в начале файла исходного кода ядра ОС для их вызова из пользовательских программ, вы можете добавлять дополнительные директивы equ в файл с именем myos.inc для упрощения процесса создания программ для вашей ОС сторонними разработчиками.

Стив Баллмер был прав - все зависит от разработчиков, разработчиков и еще раз разработчиков! Вы сможете привлечь их, реализовав продуманный API вашей операционной системы

Стив Баллмер был прав - все зависит от разработчиков, разработчиков и еще раз разработчиков! Вы сможете привлечь их, реализовав продуманный API вашей операционной системы

Забавные подпрограммы для работы с графикой

Несмотря на то, что в рамках пяти статей серии нами уже была проведена серьезная работа, не помешает приложить еще немного усилий для реализации некоторых возможностей, связанных с выводом графических примитивов, в рамках ядра нашей ОС. Как и в случае с подпрограммами для работы с клавиатурой, экраном и дисками, нам на помощь придет прошивка BIOS - даже несмотря на то, что она является достаточно медленной. Если вы хотите разрабатывать динамичные игры, похожие на те, что выходили в золотые годы MS-DOS, вам придется работать непосредственно с оперативной памятью видеокарты, но в нашем случае мы можем просто попросить BIOS окрасить в соответствии с нашими предпочтениями заданные пиксели на экране и взять на себя всю дополнительную работу, связанную с выводом графики.

В первую очередь нам придется создать три подпрограммы: одну для перехода в графический режим, одну для окрашивания пикселя в заданной позиции на экране в заданный цвет и еще одну для выхода из графического режима. Откройте файл исходного кода lib.asm и добавьте в него следующий код подпрограмм:

lib_gfx_mode:
	pusha
	mov ah, 0
	mov al, 13h
	int 10h
	popa
	ret

lib_text_mode:
	pusha
	mov ah, 0
	mov al, 3
	int 10h
	popa
	ret

lib_plot_pixel:
	pusha
	mov ah, 0Ch
	mov bh, 0
	int 10h
	popa
	ret

Первая из трех подпрограмм, lib_gfx_mode, использует BIOS (инструкция int 10h, подпрограмма 0) для перехода в графический режим с разрешением экрана 320x200 с 256 цветами. Разумеется, это разрешение экрана является чрезвычайно низким по сегодняшним стандартам, но при этом стоит помнить и о том, что огромное количество отличных игр было написано для работы на машинах с такими ограниченными аппартыми ресурсами! Вторая подпрограмма предназначена для возврата в текстовый режим, а третья - для отрисовки пикселя на основе трех параметров: значения цвета в регистре AL, координаты по оси X (горизонтальной) в регистре CX и координаты по оси Y (вертикальной) в регистре DX.

Далее следует добавить в начало файла исходного кода ядра ОС описания векторов для доступа ко всем этим подпрограммам из приложений. Если вы еще не создавали каких-либо дополнительных векторов для подпрограмм библиотеки lib.asm и в начале файла исходного кода ядра ОС присутствуют лишь описания трех векторов, рассмотренных выше, вы можете добавить описания новых векторов сразу же после них аналогичным образом:

	jmp lib_gfx_mode    ; 0x0008
	jmp lib_text_mode   ; 0x000B
	jmp lib_plot_pixel  ; 0x000E

(И снова инструкции перехода, использованные для реализации векторов, занимают по три байта; воспользуйтесь утилитой ndisasm для уточнения адресов бинарного кода ядра ОС из файла mykernel.asm, к которым осуществляются переходы с помощью инструкций jmp, если вы не уверены в корректности приведенных выше утверждений.) Теперь откройте файл myos.inc и добавьте в него соответствующие директивы equ для описанных подпрограмм для того, чтобы сторонние программы могли их использовать, после чего отредактируйте файл исходного кода программы test.asm. В данном случае мы будем разрабатывать программу, которая будет рисовать цветные полосы на экране сверху вниз, изменяя цвет каждой из полос. Эта программа будет использовать внутренний и внешний циклы, при реализации которых вы можете воспользоваться знаниями концепций, полученными при изучении более высокоуровневых языков программирования. А это исходный код программы:

	%include "myos.inc"
	
	ORG 32768

	call lib_gfx_mode

	mov al, 0
	mov cx, 0
	mov dx, 0

outerloop:
	inc al
	inc dx
	cmp dx, 199
	je $

	innerloop:
		call lib_plot_pixel
		inc cx
		cmp cx, 319
		jne innerloop

	mov cx, 0
	jmp outerloop

В данном коде программы после перехода в графический режим мы помещаем в регистры, предназначенные для хранения значений цвета, а также позиций по осям X и Y, нулевые значения. После этого мы начинаем наш внешний цикл, в рамках которого происходит увеличение значения цвета на единицу и перемещение к следующей строке. По достижении строки 199 (а именно, нижней строки экрана), происходит исполнение инструкции jmp $, которая позволяет осуществить переход к самой себе - таким образом реализуется бесконечный цикл. (Да, единственным способом завершения работы данной программы будет принудительная перезагрузка машины!)

Отличной идеей является документирование всех системных вызовов вашей операционной системы, ведь при наличии документации вы (а также сторонние разработчики) будете знать наверняка, в какие регистры следует помещать параметры подпрограмм, а также в каких регистрах следует искать возвращаемые ими результаты

Отличной идеей является документирование всех системных вызовов вашей операционной системы, ведь при наличии документации вы (а также сторонние разработчики) будете знать наверняка, в какие регистры следует помещать параметры подпрограмм, а также в каких регистрах следует искать возвращаемые ими результаты

Однако, если мы не достигли нижней строки экрана, начинается исполнение внутреннего цикла. В рамках данного цикла осуществляется отрисовка пикселя в текущей позиции, после которой значение в регистре CX увеличивается на 1, что подразумевает сдвиг текущей позиции по оси X на пиксель вправо. По достижении правого края экрана (пикселя 319) цикл завершается, значение позиции по оси X сбрасывается до 0 и в рамках внешнего цикла осуществляется обработка следующей строки. Соберите компоненты операционной системы, загрузите ее и вы увидите результат, аналогичный изображенному на приведенной выше иллюстрации - неплохо для такой компактной программы, не так ли?

Произвольные линии и геометрические фигуры

Обладая полученными в ходе чтения данной статьи знаниями, вы можете создавать подпрограммы, осуществляющие отрисовку прямых горизонтальных и вертикальных линий или даже залитых и обведенных прямоугольников. Но как быть при возникновении необходимости в отрисовке произвольной линии с началом и концом в заданных точках? Эта задача является немного более сложной, при этом одно из ее самых простых решений заключается в задействовании алгоритма Брезенхейма. Данный алгоритм позволяет определить координаты пикселей экрана, которые следует закрасить для отрисовки прямой линии из одной точки в другую. Рассмотрение математических операций, использованных в данном алгоритме, выходит за рамки данной статьи, но вам в любом случае стоит обратить внимание на псевдокод, приведенный по адресу http://tinyurl.com/k6qsxty, на основе которого вы можете создать собственную программную реализацию данного алгоритма.

В сети полно реализаций упомянутого алгоритма на языке ассемблера для архитектуры центрального процессора x86, таких, как реализация, размещенная по адресу http://bloerp.de/code/asm/x86/line.html, поэтому вы можете в любой момент воспользоваться поисковой системой Google для ознакомления деталями одной из них. Также в сети вы можете обнаружить аналогичные фрагменты кода, предназначенные для отрисовки кругов и других геометрических фигур. Если вас заинтересовала данная тема и у вас достаточно времени, вы можете попытаться добавить в свою операционную систему даже поддержку мыши. BIOS не предоставляет каких-либо функций для работы с мышью, но вы в любом случае можете написать простейший драйвер для взаимодействия с портом PS/2, причем большинство компьютеров будет эмулировать порт PS/2 даже при подключении мыши в порт USB (при работе в 16-битном режиме реальных адресов). Вы можете начать свою работу с ознакомления с материалом по адресу http://wiki.osdev.org/Mouse_Input - при этом стоит обратить особое внимание на ссылки внизу страницы, перейдя по которым вы можете обнаружить примеры реализации драйвера в формате фрагментов исходного кода.

На этом все! Наше путешествие в прекрасный мир языка ассемблера подошло к концу и я надеюсь, что оно пробудило в вас интерес к новым знаниям. Продолжайте работать с битами и байтами на реальном железе и в том случае, если вы получите предложение о работе над проектом на языке ассемблера с шестизначной зарплатой, автор статьи не откажется от небольшого вознаграждения, такого, как бесплатное пиво... удачной разработки!

Что дальше?

Какую же тему нам следует рассмотреть в будущем? Возможно, следующим шагом будет исследование других операционных систем для работы с центральными процессорами архитектуры x86 в 16-битном режиме реальных адресов, таких, как MikeOS (http://mikos.sf.net)? Нет, это не лучший вариант - операционная система MikeOS очень похожа на нашу простейшую операционную систему, хотя и содержит большое количество дополнительных библиотечных подпрограмм и функций ядра. В ней используется аналогичная карта распределения памяти и реализация векторов, при этом код отлично прокомментирован и вы можете использовать его фрагменты в своей операционной системе (код распространяется в соответствии с условиями лицензии BSD).

После самостоятельного исследования упомянутых операционных систем вам наверняка захочется познакомиться с более мощными экземплярами. KolobriOS (www.kolibrios.org) является впечатляющей операционной системой с графическим рабочим столом, разработанной с использованием языка ассемблера, в которой реализованы вытесняющая многозадачность, драйверы для различных файловых систем и другие полезные механизмы. KolibriOS является форком MenuetOS (www.menuetos.net) - операционной системы с открытым исходным кодом в своем 32-битном воплощении, содержащей дополнительные драйверы для звуковых и сетевых карт. Она является на удивление быстрой, но при этом крайне ограниченной в плане функциональности в сравнении с такими операционными системами, как Linux и *BSD.

Конечно же, архитектура x86 не является единственной архитектурой для языка ассемблера. Фактически, многие разработчики считают ее очень неудобной и нелогичной из-за большого количества инструкций, добавленных в центральные процессоры, выпускаемые с 1980-х годов (при этом ее сильной стороной является обратная совместимость). Сегодня чипы ARM встречаются практически везде и несмотря на то, что многие из них устанавливаются во встраиваемые или заблокированные производителем устройства, такие, как мобильные телефоны и планшеты, вы можете незамедлительно начать разрабатывать программы на языке ассемблера для этих чипов в том случае, если у вас имеется одноплатный компьютер Raspberry Pi.

Отличной отправной точкой может служить руководство, доступное по адресу http://tinyurl.com/nsgzq89, в котором подробно описывается процесс ассемблирования и связывания бинарных файлов, а также даются пояснения относительно специфики чипов ARM в последующих главах. Учтите, что несмотря на отличия инструкций и регистров архитектур ARM и x86, в случае использования языка ассемблера для разработки программ, большая часть изученных концепций будет применима вне зависимости от архитектуры. (Ну, конечно же в том случае, если вы не разрабатываете программы для урезанных в плане функций встраиваемых чипов, которые не поддерживают даже инструкции умножения!)

После того, как вы научитесь быстро писать программы для чипов ARM, вы можете попытаться разработать собственную операционную систему для Raspberry Pi. Это система будет определенно сложнее нашей системы, описанной в данной серии статей, так как в ARM не используется прошивка BIOS, функции которой облегчали нашу жизнь - да, вам придется разработать собственный драйвер видеоадаптера для простого вывода каких-либо данных на экран! Тем не менее, это очень интересная работа, для выполнения которой не помешает ознакомиться с руководством: www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os.

Одноплатный компьютер Raspberry Pi является отличным устройством по целому ряду причин, при этом он также является отличной платформой для изучения языка ассемблера для архитектуры ARM

Одноплатный компьютер Raspberry Pi является отличным устройством по целому ряду причин, при этом он также является отличной платформой для изучения языка ассемблера для архитектуры ARM


Предыдущие статьи из серии "Школа ассемблера":