Я давно хотел научиться писать операционную систему или хотя бы попробовать эту задачу на зуб. Почему ОС начинают писать на языке ассемблера? Дело в том, что работа ОС обеспечивается определенными особенностями архитектуры процессора. Это, например, наличие нескольких уровней привилегий (режим ядра, режим пользователя), поддержка виртуальной памяти, поддержка многозадачности. Работа с этими особенностями архитектуры процессора подразумевает использование таких машинных команд и регистров процессора, о которых компиляторы высокоуровневых языков программирования (например C/C++) ничего не знают. Поэтому — ассемблер. В следующей серии заметок я буду писать о своих опытах исследования архитектуры процессоров Intel как то: работа в реальном режиме, переход в защищенный режим, написание загрузчика (bootloader), обработка прерываний, включение механизма виртуальной памяти и пр.
В Интернете я нашел несколько хороших ресурсов по теме:
- Руслан Аблязов — Программирование на ассемблере на платформе х86-64 — 2011
- Intel 64 and IA-32 Architectures Software Developer’s Manual
- BrokenThorn OS Development Series
- OSDev.org
- OSDever.net
Кроме того, если вы не знакомы с языком ассемблера для процессоров семейства x86, рекомендую вам книгу Kip Irvine — Assembly Language for x86 Processors, 7th edition — 2014.
Начнем с того, что напишем программу HelloWorld, запускающуюся на голом железе (без операционной системы) и запустим ее на виртуальной машине.
Программа HelloWorld
Ниже показан исходный код программы HelloWorld на языке ассемблера Flat Assembler (FASM), которая будет стартовать на голом железе в реальном режиме работы x86-совместимого процессора (комментарии пишу на английском — привычка, которая возникла у меня в связи с проблемами с кодировками кириллицы).
use16 ; generate 16-bit code
org 7C00h ; the code starts at 0x7C00 memory address
start:
jmp far dword 0x0000:entr ; makes CS=0, IP=0x7c05
entr:
xor ax, ax ; ax = 0
mov ds, ax ; setup data segment ds=ax=0
cli ; when we set up stack we need disable interrupts because stack is involved in interrupts handling
mov ss, ax ; setup stack segment ss=ax=0
mov sp, 0x7C00 ; stack will grow starting from 0x7C00 memory address
sti ; enable interrupts
mov si, message
cld ; clear direction flag (DF is a flag used for string operations)
mov ah, 0Eh ; BIOS function index (write a charachter to the active video page)
puts_loop:
lodsb ; load to al the next charachter located at [si] address in memory (si is incremented automatically because the direction flag DF = 0)
test al, al ; zero in al denotes the end of the string
jz puts_loop_exit
int 10h ; call BIOS standard video service's function
jmp puts_loop
puts_loop_exit:
cli ; disable interrupts before halting the processor
hlt ; halt the processor
;jmp $ ; alternatively to hlt we could run an infinite loop
message db 'Hello World!', 0
finish:
; The size of a disk sector is 512 bytes. Boot sector signature occupies the two last bytes.
; The gap between the end of the source code and the boot sector signature is filled with zeroes.
times 510-finish+start db 0
db 55h, 0AAh ; boot sector signature
Компиляция исходного кода
Объяснения того, как работает программа — потом, сначала давайте скомпилируем исходный код. Компилировать будем ассемблером Flat Assembler (FASM). Скачиваем с официального сайта архив с дистрибутивом FASM. Внутри архива находится компилятор fasm.exe. Я рекомендовал бы распаковать архив в папку C:\FASM
и добавить путь к этой папке в переменную окружения PATH. Далее запускаем командную строку и компилируем исходный код:
HelloWorld.asm — это приведенный выше файл с исходным кодом, HelloWorld.bin — это скомпилированный машинный код.
Создание образа дискеты
Операционная система должна загружаться с какого-то носителя (жесткого диска, CD-ROM, флешки, дискеты и т. д.). Создадим образ загрузочной дискеты с нашим машинным кодом. Виртуальная машина сможет загрузиться с виртуальной дискеты, используя этот образ. Создавать образ дискеты умеет unix’овая утилита под названием dd. Существует версия этой утилиты под Windows. Если вы пользуетесь средой Cygwin, то утилита dd есть в ее составе. Итак, запускаем командную строку:
dd if="HelloWorld.bin" of="floppy.img" conv=notrunc
1-я команда создает образ дискеты floppy.img и заполняет его нулями, 2-я — записывает в самое начало образа нашу программу.
Установка и запуск виртуальной машины Bochs
Я имел дело всего с тремя виртуальными машинами: Oracle VM VirtualBox, VMware Workstation Player и Bochs. Bochs хотя и обладает очень скромным графическим интерфейсом, хорош тем, что он легкий и может выполнять машинный код пошагово, т. е. с ним можно производить отладку исходного кода программы. Скачиваем с официального сайта программу установки (файл с расширением .exe) и запускаем его (все настройки я оставлял по-умолчанию). После установки запускаем Bochs. Возникает диалоговое окно Bochs Start Menu. В списке Edit Options выбираем Disk & Boot и нажимаем кнопку Edit. Открывается диалог Bochs Disk Options. На вкладке Floppy Options в группе First Floppy Drive устанавливаем следующие настройки:
Type of floppy drive | 3.5 1.44M |
---|---|
First floppy image/device | жмем Browse и выбираем ранее созданный нами файл floppy.img |
Type of floppy media | 1.44M |
Write Protection | галка снята |
Status | inserted |
Жмем кнопку OK. В окне Bochs Start Menu жмем кнопку Start (предварительно можно сохранить сделанные нами настройки в текстовом файле с расширением .bxrc нажав кнопку Save, впоследствии их можно будет загрузить, нажав кнопку Load). Открывается окно, в котором мы видим надпись
Особенности FASM
Прежде всего обратите внимание на документацию Flat Assembler Documentation and Tutorials, которая также поставляется в дистрибутиве FASM в виде файла PDF. Синтаксис FASM имеет особенности, которые отличают его например от MASM, которым мне доводилось пользоваться до сих пор. Особенности касаются обращений к памяти и работы с метками:
MASM | FASM |
---|---|
metka dword 1234h |
metka dd 1234h |
mov eax, metka |
mov eax, [metka] mov eax, ptr metka |
mov eax, offset metka |
mov eax, metka |
mov ax, word ptr metka |
mov ax, word ptr metka mov ax, word [metka] |
Объяснение исходного кода программы HelloWorld
Теперь в двух словах о том, как работает программа HelloWorld. Когда процессор выполняет нашу программу, он считывает машинные команды из оперативной памяти. Откуда в памяти возьмется наша программа? Ответ: с одного из носителей, коими могут быть жесткий диск, дискета, флешка, компакт-диск или локальная сеть. Кто загрузит программу с носителя в оперативную память? Ответ: процессор, который сразу после включения начинает выполнять программу, записанную в BIOS — микросхеме памяти, которая хранит программу, которая проецируется на адресное пространство процессора и которая заставляет процессор перебирать все носители и искать на них т. н. загрузочный сектор — блок данных размером 512 байт, в последних двух байтах которого содержится т. н. сигнатура загрузочного сектора. Найдя загрузочный сектор, процессор копирует его с носителя в оперативную память по адресу 0x7C00 и переходит к выполнению машинной инструкции, расположенной по этому адресу. Чтобы рассчитать адреса меток, компилятор должен знать, по какому адресу будет расположена наша программа — для этого мы используем директиву
Заметим, что директива org
никак не влияет на расположение кода внутри двоичного файла, она влияет только на адреса, на которые указывают метки.
Чтобы поместить в последние два байта (всего в секторе 512 байт) сектора сигнатуру загрузочного сектора (байты 55h, AAh), используем в конце файла директиву times:
times 510-finish+start db 0
db 55h, 0AAh ; boot sector signature
После включения процессор работает в 16-разрядном режиме (т. н. реальный режим работы процессора). Две ассемблерные команды, имеющие одно и то же мнемоническое обозначение, будут иметь разные машинный код в зависимости от режима работы процессора (режимов на сегодняшний день существует три: реальный (16-разрядный), защищенный (32-разрядный) и длинный (64-разрядный)). Поэтому компилятор должен знать, какой машинный код ему генерировать, 16-, 32- или 64-разрядный — для этого мы используем директиву
Настройка сегментных регистров:
jmp far dword 0x0000:entr ; makes CS=0, IP=0x7c00
entr:
xor ax, ax ; ax = 0
mov ds, ax ; setup data segment ds=ax=0
mov ss, ax ; setup stack segment ss=ax=0
mov sp, 0x7C00 ; stack will grow starting from 0x7C00 memory address
Зачем помещать нули в сегментные регистры? Дело в том, что мы не знаем, каковы значения этих регистров в момент запуска программы. Единственное, что мы знаем — это что наша программа будет загружена по адресу 0x7C00 и что первая машинная команда будет выполнена. В то же время, наша программа рассчитана на то, что в сегментных регистрах cs и ds будут нули, т. е. все метки в программе обозначают смещения относительного базового адреса, равного нулю. Стек мы пока не используем, но это пока — нелишним будет проинициализировать и регистры ss и sp.
Как вывести текстовую строку на экран. После старта процессора в реальном режиме в оперативную память из BIOS загружена таблица прерываний и загружены обработчики прерываний. Среди них есть программные прерывания — те, обработчики которых можно вызвать командой int. Эти прерывания по сути являются функциями, которые BIOS предоставляет нашей программе, и эти функции позволяют осуществлять ввод-вывод, в том числе на экран компьютера. Функции BIOS могут принимать параметры через регистры. Например функция int 10h принимает два параметра: параметр ah=0Eh уточняет задачу функции — «записать символ в видеопамять»; параметр al — ASCII-код символа, который надо записать в видеопамять. И мы последовательно в цикле помещаем в регистр al символы строки «Hello World!» и вызываем прерывание int 10h:
lodsb ; load to al the next charachter located at [si] address in memory (si is incremented automatically because direction flag DF = 0)
test al, al ; zero in al means the end of the string
jz puts_loop_exit
int 10h ; call BIOS standard video service's function
jmp puts_loop
puts_loop_exit:
Некоторые функции BIOS приведены на странице OsDev.org — BIOS.
Заканчивается программа инструкцией hlt
, которая останавливает работу процессора. Из этого состояния он может быть выведен только прерыванием (как немаскируемым, так и маскируемым) или перезагрузкой (reset).
В следующей заметке я расскажу о настройке проекта osdevlearning, который размещен на BitBucket, и в который я буду помещать наши эксперименты по изучению архитектуры Intel x86-64.