Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Пишем свою операционную систему. Загрузка ядра и модулей


Приветствую всех!

В прошлом выпуске мы написали подпрограммы для работы с файловой системой ListFS и реализовали загрузку продолжения загрузчика из файла на диске. Теперь мы не ограничены 512 байтами и можем дописать начальный загрузчик. Я не буду его жёстко привязывать к своему ядру - загрузчик будет читать конфигурационный файл, в котором содержится информация о расположении ядра и модулей, загружать все необходимые файлы в память и передавать управление ядра, предварительно переведя процессор в нужный режим.

Для начала собственно загрузка конфигурационного файла:

; Дополнительные данные загрузчика
 ...
config_file_name db "boot.cfg",0
 ...
; Продолжение начального загрузчика
boot2:
        ; Загрузим конфигурационный файл загрузчика
	mov si, config_file_name
	mov bx, 0x1000 / 16
	call load_file 

Теперь по адресу 0000:1000 расположен текстовый конфиг загрузчика. Он имеет очень простой формат. Первый символ строки определяет её тип. Символ "#" в начале говорит о том, что вся строка далее является комментарием и должна быть пропущена. Также пропускаются пустые строки. Символ "L" (L:oad) обозначает, что за ним следует имя файла, подлежащего загрузки. Файл загружается в свободное пространство оперативной памяти. Первый загруженный таким образом файл считается ядром и на него будет передано управление, остальные файлы могут иметь произвольный формат - вся обработка этих файлов ложится на плечи ядра. Подразумевается, что эти файлы являются дополнительными модулям, которые необходимы для работы с диском, файловой системой и прочее, без чего система не сможет нормально работать. И наконец символ "S" (Start) приводит к немедленному запуску ядра. Символы следующие далее игнорируются (запустив ядро, загрузчик остаётся не у дел). После этого символа должны идти два символа - "16", "32" или "64". В зависимости от этого загрузчик может подготовить необходимое окружение. Загрузчик передаёт ядру в качестве параметров немного информации - список загруженных файлов, объём оперативной памяти, номер загрузочного диска.

Анализ загрузочного скрипта (пожалуй, конфиг можно называть и так, ведь он содержит команды) можно выполнять достаточно просто - перебираем символы файла, запомнив начало. Если натыкаемся на символ переноса строки, то время прерваться и посмотреть на первый символ. Выполнив действие, мы продолжаем перебор с предыдущей позиции.

        ; Выполним загрузочный скрипт
	mov dx, 0x1000
 .parse_line:
	mov si, dx
 .parse_char:
	lodsb
	test al, al
	jz .config_end
	cmp al, 10
	je .run_command
	cmp al, 13
	je .run_command
	jmp .parse_char
 .run_command:
	mov byte[si - 1], 0
	xchg dx, si ; Обмен значений DX и SI
	cmp byte[si], 0
	je .parse_line ; Пустая строка
	cmp byte[si], "#"
	je .parse_line ; Комментарий
	cmp byte[si], "L"
	je .load_file ; Загрузка файла
	cmp byte[si], "S"
	je .start ; Запуск ядра
	; Неизвестная команда
	mov al, [si]
	mov [.cmd], al
	call error
	db "Unknown boot script command '"
	.cmd db ?
	db "'!",13,10,0
 .config_end: ; При правильном конфигурационном файле мы не должны сюда попасть
	; Завершение
	jmp reboot 

 Осталось реализовать пару подпрограмм - загрузки файла и запуска ядра. Начнём с более простой - загрузки файла. Список файлов будет хранится под адресу 0000:6000 (таким образом для конфига останется 20 КБ, чего вполне достаточно) . Каждый элемент списка состоит из двух 64-битных целых чисел - базовый адрес загрузки файла (физический, а не сегмент:смещение) и размер файла в байтах. Последний элемент списка содержит нули (его созданием займётся .start).

Указатель на текущий элемент списка содержит BP (перед разбором конфига BP необходимо присвоить 0x6000), указатель на сегмент, куда следует загружать очередной файл содержит BX (перед разбором конфига ему следует присвоить 0x9000. 4 КБ хватит для продолжения загрузчика). Вот такой в итого получается код разбора конфигурационного файла:

; Продолжение начального загрузчика
boot2:
	; Загрузим конфигурационный файл загрузчика
	mov si, config_file_name
	mov bx, 0x1000 / 16
	call load_file
	; Выполним загрузочный скрипт
	mov bx, 0x9000 / 16
	mov bp, 0x6000
	mov dx, 0x1000
 .parse_line:
	mov si, dx
 .parse_char:
	lodsb
	test al, al
	jz .config_end
	cmp al, 10
	je .run_command
	cmp al, 13
	je .run_command
	jmp .parse_char
 .run_command:
	mov byte[si - 1], 0
	xchg dx, si
	cmp byte[si], 0
	je .parse_line ; Пустая строка
	cmp byte[si], "#"
	je .parse_line ; Комментарий
	cmp byte[si], "L"
	je .load_file ; Загрузка файла
	cmp byte[si], "S"
	je .start ; Запуск ядра
	; Неизвестная команда
	mov al, [si]
	mov [.cmd], al
	call error
	db "Unknown boot script command '"
	.cmd db ?
	db "'!",13,10,0
 .config_end: ; При правильном конфигурационном файле мы не должны сюда попасть
	; Завершение
	jmp reboot
; Загрузка файла
 .load_file:
	push dx
        inc si
	call load_file
	push ax
	mov cx, 512
	mul cx
	mov word[bp + 8], ax
	mov word[bp + 10], dx
	mov word[bp + 12], 0
	mov word[bp + 14], 0
	mov ax, bx
	mov cx, 16
	mul cx
	mov word[bp], ax
	mov word[bp + 2], dx
	mov word[bp + 4], 0
	mov word[bp + 6], 0
	pop ax
	shr ax, 9 - 4
	add bx, ax
	add bp, 16
	pop dx
	jmp .parse_line
; Запуск ядра
 .start:
	; Проверим, что загружен хотя бы один файл
	cmp bx, 0x9000 / 16
	ja @f
	call error
	db "NO KERNEL LOADED",13,10,0	
 @:
	; Заполняем последний элемент списка файлов
	xor ax, ax
	mov cx, 16
	mov di, bp
	rep stosw
        ... 

 Попутно, я заметил баг в функции split_file_name, из-за которого load_file не находил файлы в подкаталогах. Вот исправление:

; Разбиение строки DS:SI по символу слеша
split_file_name:
	push si
 @:
	lodsb
	cmp al, "/"
	je @f
	test al, al
	jz @f
	jmp @b
 @:
	mov byte[si - 1], 0
	mov ax, si
	pop si
	ret 

 Уже сейчас можно подготовить конфигурационный файл boot.cfg вроде такого:

# Loading kernel
Lsystem/kernel.bin
# Boot 64 bit kernel
S64 

Загрузчик загрузит все указанные файлы и приступит к инициализации ядра, которую нам осталось дописать.

Для полноты реализации напишем короткий код для старта 16-битного ядра, 32-битные ядра пока загружать не будем, потому что я планирую писать сразу 64-битную систему.

; Запуск ядра
 .start:
	; Проверим, что загружен хотя бы один файл
	cmp bx, 0x9000 / 16
	ja @f
	call error
	db "NO KERNEL LOADED",13,10,0	
 @:
	; Заполняем последний элемент списка файлов
	xor ax, ax
	mov cx, 16
	mov di, bp
	rep stosw
	; Переходим к процедуре инициализации ядра для нужной разрядности
	inc si
	cmp word[si], "16"
	je .start16
	cmp word[si], "32"
	je .start32
	cmp word[si], "64"
	je .start64
	; Неизвестная рязрядность ядра
	call error
	db "Invalid start command argument",13,10,0
; Запуск 16-разрядного ядра
 .start16:
	mov si, start16_msg
	mov bx, 0x6000
	mov dl, [disk_id]
	jmp 0x9000
; Запуск 32-разрядного ядра
 .start32:
	call error
	db "Starting 32 bit kernels is not implemented yet",13,10,0
; Запуск 64-разрядного ядра
 .start64:
	; Выводим уведомление о запуске 64-битного ядра
	mov si, start64_msg
	call write_str
	;
	jmp reboot 

Если ядро 16-разрядное (это может также пригодится опытным осеписателям, чтобы самостоятельно инициализировать всё). Код для запуска 64-битного ядра нам ещё нужно написать. Мы уже вышли на "финишную прямую", однако перед переходом в защищённый режим, мне необходимо рассказать больше теории, поэтому к написанию кода инициализации ядра мы приступим в следующем выпуске.

В заключение приведу полный код нашего начального загрузчика, который уже почти принял окончательный вид:

; Начальный загрузчик ядра для архитектуры x86
format Binary as "bin"
org 0x7C00
	jmp boot
; Заголовок ListFS
align 4
fs_magic dd ?
fs_version dd ?
fs_flags dd ?
fs_base dq ?
fs_size dq ?
fs_map_base dq ?
fs_map_size dq ?
fs_first_file dq ?
fs_uid dq ?
fs_block_size dd ?
; Заголовок файла
virtual at 0x800
f_info:
	f_name rb 256
	f_next dq ?
	f_prev dq ?
	f_parent dq ?
	f_flags dq ?
	f_data dq ?
	f_size dq ?
	f_ctime dq ?
	f_mtime dq ?
	f_atime dq ?
end virtual
; Данные начального загрузчика
label sector_per_track word at $$
label head_count byte at $$ + 2
label disk_id byte at $$ + 3
reboot_msg db "Press any key...",13,10,0
boot_file_name db "boot.bin",0
; Вывод строки DS:SI на экран
write_str:
	push si
	mov ah, 0x0E
 @:
	lodsb
	test al, al
	jz @f
	int 0x10
	jmp @b
 @:
	pop si
	ret
; Критическая ошибка
error:
	pop si
	call write_str
; Перезагрузка
reboot:
	mov si, reboot_msg
	call write_str
	xor ah, ah
	int 0x16
	jmp 0xFFFF:0
; Загрузка сектора DX:AX в буфер ES:DI
load_sector:
	push dx
	add ax, word[fs_base]
	adc dx, word[fs_base + 2]
	cmp byte[sector_per_track], 0xFF
	je .use_EDD
	push bx cx si
	div [sector_per_track]
	mov cl, dl
	inc cl
	div [head_count]
	mov dh, ah
	mov ch, al
	mov dl, [disk_id]
	mov bx, di
	mov al, 1
	mov si, 3
 @:
	mov ah, 2
	int 0x13
	jnc @f
	xor ah, ah
	int 0x13
	dec si
	jnz @b
 .error:
	call error
	db "DISK ERROR",13,10,0
 @:
	pop si cx bx dx
	ret
 .use_EDD:
	push si
	mov byte[0x600], 0x10
	mov byte[0x601], 0
	mov word[0x602], 1
	mov [0x604], di
	push es
	pop word[0x606]
	mov [0x608], ax
	mov [0x60A], dx
	mov word[0x60C], 0
	mov word[0x60E], 0
	mov ah, 0x42
	mov dl, [disk_id]
	mov si, 0x600
	int 0x13
	jc .error
	pop si dx
	ret
; Поиск файла с именем DS:SI в каталоге DX:AX
find_file:
	push cx dx di
 .find:
	cmp ax, -1
	jne @f
	cmp dx, -1
	jne @f
 .not_found:
	call error
	db "NOT FOUND",13,10,0
 @:
	mov di, f_info
	call load_sector
	push di
	mov cx, 0xFFFF
	xor al, al
	repne scasb
	neg cx
	dec cx
	pop di
	push si
	repe cmpsb
	pop si
	je .found
	mov ax, word[f_next]
	mov dx, word[f_next + 2]
	jmp .find
 .found:
	pop di dx cx
	ret
; Загрузка текущего файла в память по адресу BX:0. Количество загруженных секторов возвращается в AX
load_file_data:
	push bx cx dx si di
	mov ax, word[f_data]
	mov dx, word[f_data + 2]
 .load_list:
	cmp ax, -1
	jne @f
	cmp dx, -1
	jne @f
 .file_end:
	pop di si dx cx
	mov ax, bx
	pop bx
	sub ax, bx
	shr ax, 9 - 4
	ret
 @:
	mov di, 0x8000 / 16
	call load_sector
	mov si, di
	mov cx, 512 / 8 - 1
 .load_sector:
	lodsw
	mov dx, [si]
	add si, 6
	cmp ax, -1
	jne @f
	cmp dx, -1
	je .file_end	
 @:
	push es
	mov es, bx
	xor di, di
	call load_sector
	add bx, 0x200 / 16
	pop es
	loop .load_sector
	lodsw
	mov dx, [si]
	jmp .load_list
; Точка входа в начальный загрузчик
boot:
	; Настроим сегментные регистры
	jmp 0:@f
 @:
	mov ax, cs
	mov ds, ax
	mov es, ax
	; Настроим стек
	mov ss, ax
	mov sp, $$
	; Разрешим прерывания
	sti
	; Запомним номер загрузочного диска
	mov [disk_id], dl
	; Определим параметры загрузочного диска
	mov ah, 0x41
	mov bx, 0x55AA
	int 0x13
	jc @f
	mov byte[sector_per_track], 0xFF
	jmp .disk_detected
 @:
	mov ah, 0x08
	xor di, di
	push es
	int 0x13
	pop es
	jc load_sector.error
	inc dh
	mov [head_count], dh
	and cx, 111111b
	mov [sector_per_track], cx
 .disk_detected:
	; Загрузим продолжение начального загрузчика
	mov si, boot_file_name
	mov ax, word[fs_first_file]
	mov dx, word[fs_first_file + 2]
	call find_file
	mov bx, 0x7E00 / 16
	call load_file_data
	; Переходим на продолжение
	jmp boot2
; Пустое пространство и сигнатура
rb 510 - ($ - $$)
db 0x55,0xAA
; Дополнительные данные загрузчика
load_msg_preffix db "Loading '",0
load_msg_suffix db "'...",0
ok_msg db "OK",13,10,0
config_file_name db "boot.cfg",0
start16_msg db "Starting 16 bit kernel...",13,10,0
start64_msg db "Starting 64 bit kernel...",13,10,0
; Разбиение строки DS:SI по символу слеша
split_file_name:
	push si
 @:
	lodsb
	cmp al, "/"
	je @f
	test al, al
	jz @f
	jmp @b
 @:
	mov byte[si - 1], 0
	mov ax, si
	pop si
	ret
; Загрузка файла с именем DS:SI в буфер BX:0. Размер файла в секторах возвращается в AX
load_file:
	push si
	mov si, load_msg_preffix
	call write_str
	pop si
	call write_str
	push si
	mov si, load_msg_suffix
	call write_str
	pop si
	push si bp
	mov dx, word[fs_first_file + 2]
	mov ax, word[fs_first_file]
 @:
	push ax
	call split_file_name
	mov bp, ax
	pop ax
	call find_file
	test byte[f_flags], 1
	jz @f
	mov si, bp
	mov dx, word[f_data + 2]
	mov ax, word[f_data]
	jmp @b	
 @:
	call load_file_data
	mov si, ok_msg
	call write_str
	pop bp si
	ret
; Продолжение начального загрузчика
boot2:
	; Загрузим конфигурационный файл загрузчика
	mov si, config_file_name
	mov bx, 0x1000 / 16
	call load_file
	; Выполним загрузочный скрипт
	mov bx, 0x9000 / 16
	mov bp, 0x6000
	mov dx, 0x1000
 .parse_line:
	mov si, dx
 .parse_char:
	lodsb
	test al, al
	jz .config_end
	cmp al, 10
	je .run_command
	cmp al, 13
	je .run_command
	jmp .parse_char
 .run_command:
	mov byte[si - 1], 0
	xchg dx, si
	cmp byte[si], 0
	je .parse_line ; Пустая строка
	cmp byte[si], "#"
	je .parse_line ; Комментарий
	cmp byte[si], "L"
	je .load_file ; Загрузка файла
	cmp byte[si], "S"
	je .start ; Запуск ядра
	; Неизвестная команда
	mov al, [si]
	mov [.cmd], al
	call error
	db "Unknown boot script command '"
	.cmd db ?
	db "'!",13,10,0
 .config_end: ; При правильном конфигурационном файле мы не должны сюда попасть
	; Завершение
	jmp reboot
; Загрузка файла
 .load_file:
	push dx
	inc si
	call load_file
	push ax
	mov cx, 512
	mul cx
	mov word[bp + 8], ax
	mov word[bp + 10], dx
	mov word[bp + 12], 0
	mov word[bp + 14], 0
	mov ax, bx
	mov cx, 16
	mul cx
	mov word[bp], ax
	mov word[bp + 2], dx
	mov word[bp + 4], 0
	mov word[bp + 6], 0
	pop ax
	shr ax, 9 - 4
	add bx, ax
	add bp, 16
	pop dx
	jmp .parse_line
; Запуск ядра
 .start:
	; Проверим, что загружен хотя бы один файл
	cmp bx, 0x9000 / 16
	ja @f
	call error
	db "NO KERNEL LOADED",13,10,0	
 @:
	; Заполняем последний элемент списка файлов
	xor ax, ax
	mov cx, 16
	mov di, bp
	rep stosw
	; Переходим к процедуре инициализации ядра для нужной разрядности
	inc si
	cmp word[si], "16"
	je .start16
	cmp word[si], "32"
	je .start32
	cmp word[si], "64"
	je .start64
	; Неизвестная рязрядность ядра
	call error
	db "Invalid start command argument",13,10,0
; Запуск 16-разрядного ядра
 .start16:
	mov si, start16_msg
	mov bx, 0x6000
	mov dl, [disk_id]
	jmp 0x9000
; Запуск 32-разрядного ядра
 .start32:
	call error
	db "Starting 32 bit kernels is not implemented yet",13,10,0
; Запуск 64-разрядного ядра
 .start64:
	; Выводим уведомление о запуске 64-битного ядра
	mov si, start64_msg
	call write_str
	;
	jmp reboot 

В избранное