Основы разработки ядра ОС [можно не делать] / подсказка к 2 задачке

Возможно вы сильно страдали, когда выводили текст побуквено. Очевидно что должен быть способ выводить текст как-то проще.

Он есть но придется сначла немного помутить. И так, в ассемблере вы можете объявить просто данные, и прописать в них например строку которой вы хотите оперировать. Вернемся к нашему базовому коду

mov ah, 0x0e
mov al, 'X'
int 0x10

; использовать loop для зависание экрана не самый хороший способо
; так как оно нагружает процессор
;loop:
;        jmp loop
; поэтому заменим на
cli ; отключить обработку прерываний
hlt ; перевести процессор в спящий режим

times 510 - ($-$$) db 0

dw 0xaa55

добавим строку данных

mov ah, 0x0e
mov al, 'X'
int 0x10

cli
hlt

hello_str: ; добавил
        db "Hello" ; на самом деле это означает что вставь в это место символы Hello

times 510 - ($-$$) db 0

dw 0xaa55

подправим передав метку в качестве параметра

mov ah, 0x0e
mov al, hello_str ; передал
int 0x10

cli
hlt

hello_str:
        db "Hello"

times 510 - ($-$$) db 0

dw 0xaa55

давайте глянем во что это скомпилируется

nasm -f bin kernel.s  
ndisasm kernel | less

увидим что-то такое

00000000  B40E              mov ah,0xe
00000002  B008              mov al,0x8 # сюда загналась цифра 8, то есть номер байта по которому лежит строка
00000004  CD10              int 0x10
00000006  EBFE              jmp short 0x6
00000008  48                dec ax # вот он этот байт, это непосредственно буква H, дизасемблер не распознал тут что это просто данные, поэтому пытается парсить символы как команды процессора
00000009  656C              gs insb # это lo
0000000B  6C                insb # это l
0000000C  6F                outsw # это o
...

То есть оно действительно вставило просто набор байт после исполняемого кода, которые теоретически даже можно запустить. Но мы не будем так как вторая строка явна выведет какую-то ерунду. Она выведет восьмой ascii символ, а если глянуть таблицу ascii кодов, то увидим, что это совсем не H, а есть символ backspace

в общем не будем запускать, мы точно не увидим там букву H

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

mov ah, 0x0e
mov al, [hello_str] ; вот так
int 0x10

cli
hlt

hello_str:
        db "Hello"

times 510 - ($-$$) db 0

dw 0xaa55

глянем что скомпилируется

ndisasm kernel | less

увидим

00000000  B40E              mov ah,0xe
00000002  A00900            mov al,[0x9] # взять байт по адресу 0x9, то есть не байт 9, а байт по адресу
00000005  CD10              int 0x10
00000007  EBFE              jmp short 0x7
00000009  48                dec ax # а вот он сам байт
0000000A  656C              gs insb
0000000C  6C                insb
0000000D  6F                outsw
...

запустим

увидим почему-то что-то другое, у меня вот буква Г, а у вас может вообще что-то третье получится.

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

то есть вначале идет таблица сопоставления прерываний с обработчиками, затем данные биоса, потом пустая область, и только затем размещается наш загрузчик с начальным адресом 0x7c00, его туда загнал биос после того как прочитал первый сектор mbr диска.

Когда мы пишем код для ассемблера, все его адреса считаются относительно начала программы, в том числе и адреса данных расположенных в коде.

Поэтому чтобы получить реальный адрес нашей буквы надо сдвинуться на 0x7c00 байт, делается это так:

mov ah, 0x0e
mov al, [hello_str + 0x7c00] ; вот так
int 0x10

cli
hlt

hello_str:
        db "Hello"

times 510 - ($-$$) db 0

dw 0xaa55

дизасемблируем:

00000000  B40E              mov ah,0xe
00000002  A0097C            mov al,[0x7c09] # тут теперь адрес сдвинутый на 0x7c00 байт
00000005  CD10              int 0x10
00000007  EBFE              jmp short 0x7
00000009  48                dec ax
0000000A  656C              gs insb
0000000C  6C                insb
0000000D  6F                outsw

пробуем запустить:

qemu-system-x86_64 kernel

красота =)

В принципе неплохо, можно попробовать выводить остальные буквы

mov ah, 0x0e
mov al, [hello_str + 0x7c00]
int 0x10
mov al, [hello_str + 0x7c00 + 1]
int 0x10
mov al, [hello_str + 0x7c00 + 2]
int 0x10
mov al, [hello_str + 0x7c00 + 3]
int 0x10
mov al, [hello_str + 0x7c00 + 4]
int 0x10

cli
hlt

hello_str:
    db 'Hello'

times 510 - ($-$$) db 0

dw 0xaa55

и оно даже выведется, но что тут плохо? Во-первых, нам все время приходится сдвигать вручную на 0x7c00

К счастью это быстро правится если добавить org 0x7c00 в самое начало кода, тогда можно будет записать код так:

org 0x7c00 ; значит сдвигай все адреса на 0x7c00

mov ah, 0x0e
mov al, [hello_str]
int 0x10
mov al, [hello_str + 1]
int 0x10
mov al, [hello_str + 2]
int 0x10
mov al, [hello_str + 3]
int 0x10
mov al, [hello_str + 4]
int 0x10

cli
hlt

hello_str:
    db 'Hello'

times 510 - ($-$$) db 0

dw 0xaa55

вторая проблема думаю более очевидна. У нас тут явно строки кандидаты на цикл. К сожалению, в ассемблере нет цикла в чистом виде, зато есть оператор jmp который позволяет перемещаться на указанию метку в коде. Нам будет особо интересна модификация этой команды, именуемая jne.

В ассемблере есть комана cmp которая позволяет сравнить два значения и если два значения не равны она ставит специальный флаг (бит) процессора в состояние 0, так вот, команда jne если этот флаг равен нулю переходит на указанную метку. Кстати есть двойственная команда jz которая переходит на метку если значения равны.

Давайте попробуем вывести последовательно все строку в цикле пока не достигнем символа o, пишем:

org 0x7c00

mov ah, 0x0e

mov bx, hello_str ; положим в регистр hello_str адрес строки 

print:
        mov al, [bx] ; положили в регистр al символ по адресу [bx]
        int 0x10 ; рисуем
        inc bx ; увеличиваем значение bx на 1 байт
        cmp byte [bx-1], 'o'  ; проверяем не рисовали ли мы только что o
        jne print ; если не рисовали то повторяем процедуру

cli
hlt

hello_str:
    db 'Hello'

times 510 - ($-$$) db 0

dw 0xaa55

если запустить, то даже сработает:

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

org 0x7c00

mov ah, 0x0e

mov bx, hello_str

print:
        mov al, [bx]
        int 0x10
        inc bx
        cmp byte [bx], 0 ; проверяем на равенство нулевому символу
        jne print

cli
hlt

hello_str:
    db 'Hello, world! =)', 0 ; добавили нулевой символ в конец 

times 510 - ($-$$) db 0

dw 0xaa55

тестим:

Выносим код печати в отдельный файл

наш код с вывод уже становится достаточно хитрым, так что имеет смысл вынести его в отдельный файл, чтобы он не мозолил глаза. Создадим файл print.s загоним в него код, вместе с ret

print:
        mov ah, 0x0e ; включение режима вывода переносим тоже
        mov al, [bx]
        int 0x10
        inc bx
        cmp byte [bx], 0 ; проверяем на равенство нулевому символу
        jne print
        ret

теперь пойдем в файлик ядра и там подключим файл

org 0x7c00

mov bx, hello_str
call print ; вызываем код печати

cli
hlt

%include "print.s"  ; подключаем файл

hello_str:
    db 'Hello, world! =)', 0

times 510 - ($-$$) db 0

dw 0xaa55

должно работать как обычно

Чуток улучшаем код

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

Важно уметь сохранять и восстанавливать состояние регистров. Для этого у процессора реализован механизм работы со стеком. Стек это специальная область памяти в которую можно последовательно добавлять разные данные а потом в обратном порядке их вытаскивать. То есть если пололжить сначала тут байт A, затем байт B, затем байте C. То вытащить их можно будет только в обратном порядке, сначала байте С, затем B и только потом A.

Мы пока не будем сильно на этом заморачиваться, просто воспользуемся командой pusha и popa, для сохранения и восстановления всех регистров процессора.

И так, сначала скажем процессору где находится область данных стека, для этого добавим команды после org 0x7c00

org 0x7c00

mov bp, 0x8000  ; указывем что стек начинается с области памяти 0x8000
mov sp, bp ; sp -- это текущее состояние стека, так как стек пустой просто скопируем туда значение bp
; ...

Подкрутим нашу функцию печати следующим образом

print:
        pusha ; сохраняем все регистры в стек
.print_loop: ; использование метки с точкой гарантирует что она будет видан только внутри данной функции
        mov ah, 0x0e
        mov al, [bx]
        int 0x10
        inc bx

        cmp byte [bx], 0 ; проверяем на равенство нулевому символу
        jne .print_loop
        popa ; восстанавливаем все регистры из стека
        ret 

еще у нас тут косяк, что нулевый символ рисуется все равно, давайте раскидаем немного код чтобы он работал корректно

print:
        pusha ; сохраняем все регистры в стек
.print_loop:
        cmp byte [bx], 0 ; тепепрь тут проверяем на равенство нулевому символу
        jz .done ; если равен то идем на метку .done
        mov ah, 0x0e ; иначе рисуем символ
        mov al, [bx]
        int 0x10
        inc bx
        jmp .print_loop ; безусловный переход, то есть идем в начало
.done
        popa ; восстанавливаем все регистры из стека
        ret

в остальном должно работать как обычно

3.2

Доработайте функцию печати чтобы она могла обрабатывать строки с символами переноса “\n”

И выведете текст с помощью этой функции текст из предыдущего задания. Чтобы такой текст нормально отобразился

hello_str:
    db 'If you gaze\nlong into abyss\nthe abyss will gaze\nback into you =O', 0