Rambler's
Top100

Разработка операционных систем. Выпуск 8

Автор: lonesome [TSH/Digital Daemons]
Дата: 26.05.2003
Раздел: Разработка ОС

ПРЕДЫДУЩИЙ ВЫПУСК      СЛЕДУЮЩИЙ ВЫПУСК

Разработка операционных систем

Выпуск 8 от 2003-05-25

Сегодня в номере:

Intro

Здравствуйте, уважаемые подписчики. У меня для вас две новости: хорошая и .. тоже хорошая :). Помните, что сказано в описании рассылки - "простейшую систему можно написать за несколько дней"? Мы уже изучили почти достаточно, чтобы написать самую простейшую из возможных 32-битных систем защищенного режима, некий "фундамент", и этому приятному занятию мы посвятим несколько следующих выпусков.
Новость вторая - монополия ассемблера на наших практических занятиях заканчивается. Сегодня мы напишем загрузчик, который будет способен загружать и выполнять 32-битный код. В связи с этим мы приступаем к использованию Си. Я буду использовать компилятор GCC, у которого есть версии, как под Linux, так и под Windows (
MinGW)

Ваши вопросы

ВОПРОС: в 6м выпуске вы писали:
"Фактически, за вход в защищенный режим отвечает нулевой бит регистра CR0, который также называется битом PE (Protection Enable). " В доках что я переводил совсем не так. На самом деле отвечает за переход сброс регистра CS, а установка PE ни к чему не приводит. там в доках еще пример этого есть, что в PMODE мы не переходим пока не сделаем flush CS

ОТВЕТ: после входа в защищенный режим (что все-таки осуществляется установкой бита PE в CR0), сегментные регистры (в том числе и отвечающий за текущий кодовый сегмент CS) содержат те же значения, что и в реальном режиме. Чтобы механизм защиты пришел в действие, необходимо загрузить в сегментные регистры селекторы защищенных сегментов

Загрузчик

Ниже приведен код загрузчика с подробными комментариями (в целях понятности кода, я не старался его оптимизировать - по размеру в 512 байт укладывается, а скорость в загрузчике никому не нужна. Я не имею ввиду чтение с дискеты, которое здесь оптимизировано по полной программе - считывание происходит сразу целыми цилиндрами):

; бутсектор работает так:
; чтение можно производить только в первые 64к,
; поэтому мы сначала считываем цилиндр по адресу 0x50:0 - 0x50:0x2400,
; затем копируем его в нужное место
; 
; Первый цилиндр считываем в самом конце 
; (он размещается как раз по адресу 0x50:0)


[BITS 16]
[ORG 0]

; сколько цилиндров считывать
%define CYLS_TO_READ 10	

; максимальное количество ошибок чтения
; (повторять операцию чтения необходимо как минимум три раза,
; т.к. ошибки возможны из-за того, что мотор привода не разогнался)
%define MAX_READ_ERRORS 5 
			
; Точка входа:
entry:	
		
	cli ;на время установки стека запретим прерывания
	mov ax, cs
	mov ds, ax
	mov es, ax
	mov ss, ax
	mov sp, start

	sti ;разрешим прерывания
	
	;; Необходимо скопировать загрузчик в 
	;; верхнюю часть доступной памяти, т.к.
	;; текущий код (находящийся по адресу 0x0:0x7c00
        ;; будет переписан загруженными с дискеты данными

	; В DS - адрес исходного сегмента
	mov ax, 0x07c0
	mov ds, ax
 
        ; В ES - адрес целевого сегмента
	mov ax, 0x9000
	mov es, ax

	; Копируем с 0
	xor si, si
	xor di, di
	
	; Копируем 128 двойных слов (512 байт)
	mov cx, 128
	rep movsd

	; Прыжок туда, где теперь находится бутсектор
	; (0x9000: 0)
	jmp 0x9000:start


	; следующий код выполняется по адресу 0x9000:0	
start:

        ; занесем новые значения в сегментные регистры
	mov ax, cs
	mov ds, ax
	mov ss, ax	
	
	; обрадуем пользователя сообщением
	mov si, msg_loading
	call kputs



	; Дальнейшая процедура выполняет чтение цилиндра
	; начиная с указанного в DI плюс нулевой цилиндр (в самом конце)
	; В AX - адрес сегмента в который будут записаны считанные данные

	mov di, 1
	mov ax, 0x290
	xor bx, bx

	
.loop:
	mov cx, 0x50
	mov es, cx

	push di

	; Вычисляем какую головку использовать	
	shr di, 1
	setc dh
	mov cx, di
	xchg cl, ch

	pop di
	
	; Уже все цилиндры считали?
	cmp di, CYLS_TO_READ
	je .quit

	call kread_cylinder

	;; Цилиндр считан по адресу 0x50:0x0 - 0x50:0x2400
	;; (линейный 0x500 - 0x2900)

	;; Выполним копирование этого блока по нужному адресу
	pusha
	push ds	

	mov cx, 0x50
	mov ds, cx
	mov es, ax
	xor di, di
	xor si, si
	mov cx, 0x2400
	rep movsb
	
	pop ds
	popa
	
	; Увеличим DI, AX и повторим все сначала
	inc di
	add ax, 0x240
	jmp short .loop
.quit:	
	
	; Мы считывали начиная с 1-го цилиндра! 
	; (т.к. участок 0x50:0 использовался как буфер данных)
	; теперь он свободен и мы можем считать нулевой цилиндр в него
	
	mov ax, 0x50
	mov es, ax
	mov bx, 0
	mov ch, 0
	mov dh, 0
	call kread_cylinder

	; Прыжок на загруженный код
	jmp 0x0000:0x0700
	
kread_cylinder:
        ;; процедура читает заданный цилиндр
	;; ES:BX - буфер
	;; CH - цилиндр
	;; DH - головка

	; Сбросим счетчик ошибок
	mov [.errors_counter], byte 0
	
	pusha
	
	; Сообщим пользователю, какой цилиндр и головку читаем
	mov si, msg_cylinder
	call kputs
	mov ah, ch
	call kputhex
	mov si, msg_head
	call kputs
	mov ah, dh
	call kputhex
	mov si, msg_crlf
	call kputs

	popa
	pusha
		
.start:	
	mov ah, 0x02
	mov al, 18
	mov cl, 1

	; Прерывание дискового сервиса BIOS
	int 0x13
	jc .read_error

		
	popa
	ret

.errors_counter:	db 0	
.read_error:
        ; Если произошла ошибка, то увеличим счетчик,
	; и выведем сообщение с кодом ошибки
	
	inc byte [.errors_counter]
	mov si, msg_reading_error
	call kputs
	call kputhex
	mov si, msg_crlf
	call kputs
	
	; Счетчик ошибок превысил максимальное значение?
	cmp byte [.errors_counter], MAX_READ_ERRORS
	jl .start
	
	; Ничего не получилось :( 
	mov si, msg_giving_up
	call kputs
	hlt
	jmp short $


	
	
	hex_table:	db "0123456789ABCDEF"
kputhex:
        ; Процедура преобразует число в ASCII-код 
	; его шестнадцатеричного представления и выводит его
	; (Да, я знаю, что это можно сделать четырьмя командами :))
	 
	pusha
	xor bx, bx
	mov bl, ah
	and bl, 11110000b
	shr bl, 4
	mov al, [hex_table+bx]
	call kputchar
	
	mov bl, ah
	and bl, 00001111b
	mov al, [hex_table+bx]
	call kputchar
	
	popa
	ret

	; Процедура выводит символ в AL на экран
kputchar:
	pusha
	mov ah, 0x0E
	int 0x10
	popa
	ret

	; Процедура выводит строку, на которую указывает SI, на экран
kputs:
	pusha
.loop:
	lodsb
	test al, al
	jz .quit
	mov ah, 0x0e
	int 0x10
	jmp short .loop
.quit:
	popa
	ret

	
	; Служебные сообщения
msg_loading:	db "the Operating System is loading...", 0x0A, 0x0D, 0
msg_cylinder:	db "Cylinder:", 0
msg_head:	db ", head:",0
msg_reading_error:	db "Error reading from floppy. Errcode:",0
msg_giving_up:	db "Too many errors, giving up",0x0A,0x0D, "Reboot your system, please", 0
msg_crlf:	db 0x0A, 0x0D,0 	
		
	; Сигнатура бутсектора:	
TIMES 510 - ($-$$) db 0
db 0xAA, 0x55

Этот загрузчик выполняет загрузку данных, которые следуют сразу за бутсектором - начиная с сектора 0:0:2

А вот пример кода для вторичного загрузчика, который переведет систему в защищенный режим (это усовершенствование программы из 6-го выпуска). После выполнения всех необходимых действий он передает управление на код, находящийся в файле 'kernel.bin'. В следующих выпусках, когда мы начнем писать ядро, в этом файле будет находится чистый бинарный код ядра (он уже может быть написан с использованием ЯВУ). Ну а пока - можете поэкспериментировать! Попробуйте записать в kernel.bin какой-нибудь код (он должен быть скомпонован по адресу 0x200000 - сюда загрузчик копирует ядро - т.е. при использовании NASM в программы должна быть директива ORG [0x200000]) и попробуйте как это все работает

[BITS 16]
[ORG 0x700]

	;; Загрузим в сегментные регистры 0 и установим стек

	cli	
	mov ax, 0
	mov ds, ax
	mov es, ax
	mov ss, ax
	mov sp, 0x700
	sti
	
	;; Выведем приветствие на экран
	mov si, msg_intro
	call kputs

	;; Сообщение о том, что собираемся в PM
	mov si, msg_entering_pmode
	call kputs

	;; Отключим курсор, чтобы не мозолил глаза
	mov ah, 1
	mov ch, 0x20
	int 0x10

	;; Установим базовый вектор контроллера прерываний в 0x20
	mov al,00010001b 
	out 0x20,al 
	mov al,0x20 
	out 0x21,al 
	mov al,00000100b 
	out 0x21,al
	mov al,00000001b	
	out 0x21,al 

	
	;; Отключим прерывания
	cli
	
	;; Загрузка регистра GDTR:	
	lgdt [gd_reg]

	;; Включение A20: 
	in al, 0x92
	or al, 2
	out 0x92, al

	;; Установка бита PE регистра CR0
	mov eax, cr0 
	or al, 1	
	mov cr0, eax  

	;; С помощью длинного прыжка мы загружаем
	;; селектор нужного сегмента в регистр CS
	;; (напрямую это сделать нельзя)
	jmp 0x8: _protected


	;; Эта функция вывода строки работает
	;; в реальном режиме!
	;; (использует прерывание 0x10 BIOS)
kputs:
	pusha
.loop:
	lodsb
	test al, al
	jz .quit
	mov ah, 0x0e
	int 0x10
	jmp short .loop
.quit:
	popa
	ret


	;; Следующий код - 32-битный
[BITS 32]
	;; Сюда будет передано управление
	;; после входа в PM
_protected:	
	;; Загрузим регистры DS и SS селектором
	;; сегмента данных
	mov ax, 0x10
	mov ds, ax
	mov es, ax
	mov ss, ax

	;; Наше ядро (kernel.bin) слинковано по адресу 2мб
	;; Переместим его туда

	;; kernel_binary - метка, после которой
	;; вставлено ядро
	;; (фактически - его линейный адрес)
	mov esi, kernel_binary

	;; адрес, по которому копируем
	mov edi, 0x200000


	;; Размер ядра в двойных словах
	;; (65536 байт)
	mov ecx, 0x4000

	;; Поехали :)
	rep movsd

	;; Ядро скопировано, передаем управление ему
	jmp 0x200000


gdt:
	dw 0, 0, 0, 0	; Нулевой дескриптор

	db 0xFF		; Сегмент кода с DPL=0 
	db 0xFF		; Базой=0 и Лимитом=4 Гб 
	db 0x00
	db 0x00
	db 0x00
	db 10011010b
	db 0xCF
	db 0x00
	
	db 0xFF		; Сегмент данных с DPL=0
	db 0xFF		; Базой=0 и Лимитом=4Гб	
	db 0x00	
	db 0x00
	db 0x00
	db 10010010b
	db 0xCF
	db 0x00


	;; Значение, которое мы загрузим в GDTR:	
gd_reg:
	dw 8192
	dd gdt

		
msg_intro:	db "Secondary bootloader received control", 0x0A, 0x0D, 0
msg_entering_pmode:	db "Entering protected mode...", 0x0A, 0x0D, 0

	
kernel_binary:	
	incbin 'kernel.bin'

Руководство по компиляции вышеприведенного кода (допустим что вы назвали файлы bootsect.asm и sb.asm соотвественно):

Первый файл - NASM'ом в чистый бинарный формат
nasm -fbin -o bootsect.bin bootsect.asm

Второй - тоже:
nasm -fbin -o sb.bin sb.asm

Эти два файла должны быть записаны на дискету СРАЗУ друг за другом. Один из самых простых способов создать нужный образ дискеты - включить в самый конец (после сигнатуры бутсектора) bootsect.asm директиву:
incbin 'sb.bin' - в результате NASM включит в bootsect.bin код файла sb.bin (который, разумеется, теперь надо компилировать первым)

UNIX'оиды, конечно использовали бы для этого дела dd, но вышеприведенный метод хорош своей кроссплатформенностью :)

Outro

На сегодня все, уважаемые подписчики. Разговор об исключениях придется перенести на следующий раз (уж слишком много сегодня было практики)
Как всегда, мой почтовый ящик открыт для вас:
lonesome@lowlevel.ru
Также вы можете задавать интересующие вас вопросы в форуме lowlevel.ru
Предыдущие выпуски рассылки вы можете найти по этому адресу:
http://subscribe.ru/archive/comp.soft.prog.osdev
Всего наилучшего!
Lonesome


ПРЕДЫДУЩИЙ ВЫПУСК      СЛЕДУЮЩИЙ ВЫПУСК

Rambler's Top100