Возможно вы сильно страдали, когда выводили текст побуквено. Очевидно что должен быть способ выводить текст как-то проще.
Он есть но придется сначла немного помутить. И так, в ассемблере вы можете объявить просто данные, и прописать в них например строку которой вы хотите оперировать. Вернемся к нашему базовому коду
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
глянем что скомпилируется
увидим
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
в остальном должно работать как обычно