Пишем свою операционную систему. Загрузка ядра и модулей
Приветствую всех!
В прошлом выпуске мы написали подпрограммы для работы с файловой системой 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