Урок ОСдева №4: работа с RAM, адресация в 16-битном режиме, регистры процессора.
Поздравим себя. В прошлый раз мы добавили блок параметров BIOS, после чего винда перестала
ругаться на дискету. Пора начинать писать загрузчик. Но перед этим надо подробнее
разобраться в специфике программирования на ассемблере. Всё-таки он сильно отличается от языков
высокого уровня. Давайте вспомним, как выглядела программа в конце прошлого поста.
.386p
CSEG segment use16
ASSUME cs:CSEG, ds:CSEG, es:CSEG, fs:CSEG, gs:CSEG, ss:CSEG
begin: jmp short execute ;Точка входа. Перейти к исполняемой части.
nop ;Пустой оператор. Заполняет 3-й байт перед BPB.
;БЛОК ПАРАМЕТРОВ BIOS===================================================================;
;=======================================;
;Блок параметров BIOS, 33 байта.
;Здесь хранятся характеристики
;носителя. Должен быть в 3-х байтах
;от начала загрузочного сектора.
;=======================================;
BPB_OEMname db 'BOOTDISK' ;0-7. Имя производителя. Может быть любым.
BPB_bytespersec dw 512 ;8-9. Размер сектора в байтаx.
BPB_secperclust db 1 ;10. Количество секторов в кластере.
BPB_reserved dw 1 ;11-12. Число зарезервированныx секторов (1, загрузочный).
BPB_numFATs db 2 ;13. Число FAT.
BPB_RDentries dw 224 ;14-15. Число записей Корневой Директории.
BPB_sectotal dw 2880 ;16-17. Всего секторов на носителе.
BPB_mediatype db 0F0h ;18. Тип носителя. 0F0 - 3,5-дюймовая дискета с 18 секторами в дорожке.
BPB_FATsize dw 9 ;19-20. Размер FAT в сектораx.
BPB_secpertrack dw 18 ;21-22. Число секторов в дорожке.
BPB_numheads dw 2 ;23-24. Число головок (поверxностей).
BPB_hiddensec dd 0 ;25-28. Число скрытыx секторов перед загрузочным.
BPB_sectotal32 dd 0 ;29-32. Число секторов, если иx больше 65535.
;===============================================;
;Расширенный блок параметров BIOS, 26 байт.
;Этот раздел используется в DOS 4.0.
;===============================================;
EBPB_drivenum db 0 ;0. Номер привода.
EBPB_NTflags db 0 ;1. Флаги в Windows NT. Бит 0 - флаг необxодимости проверки диска.
EBPB_extsign db 29h ;2. Признак расшренного BPB по версии DOS 4.0.
EBPB_volID dd 0 ;3-6. "Серийный номер". Любое случайное число или ноль, без разницы.
EBPB_vollabel db 'BOOTLOADER ' ;7-17. Название диска. Устарело.
EBPB_filesys db 'FAT12 ' ;18-25. Имя файловой системы.
;ИСПОЛНЯЕМЫЙ БЛОК=====================================================================;
execute: cli
hlt
org 1FEh ;Заполняет память нулями до 511-го байта.
dw 0AA55h ;Байты 511 и 512. Признак загрузочного сектора.
CSEG ends
end begin
Я снабдил всё подробными комментариями. Надеюсь, они помогут вам освежить память. Вкратце -
после запуска программы процессор выполняет переход к метке execute - и после этого останавливается
командами cli и hlt. Давайте добавим следующий код после execute, а потом разберём его.
execute: mov ax,07C0h
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
cli
mov ss,ax
mov sp,0FFFFh
sti
push ax
mov ax,offset stop
and ax,03FFh
push ax
retf
stop: cli
hlt
org 1FEh ;Заполняет память нулями до 511-го байта.
dw 0AA55h ;Байты 511 и 512. Признак загрузочного сектора.
Целая куча новых команд. Для того, чтобы их понять, придётся освоиться с новыми понятиями.
Регистр - ячейка памяти процессора, которая может выполнять какую-то конкретную задачу
или иметь общее назначение. Программируя на ассемблере, вы постоянно будете оперировать
регистрами: помещать в них данные, извлекать, модифицировать и т.д. В 16-битном режиме
процессор использует следующий набор регистров: AX, BX, CX, DX, SI, DI, BP, SP, flags, CS, DS, ES,
FS, GS, SS. С функциями каждого из них будем разбираться по мере надобности.
Сегмент:смещение - устаревшая система адресации, применявшаяся в эпоху 16-битных процессоров.
Тем не менее, для нас она важна, так как ради обратной совместимости именно в этом
режиме BIOS оставляет систему перед запуском загрузчика.
Постараюсь объяснить. 16-битная разрядность процессора подразумевает, что за раз он может обработать
16 бит данных. Максимальное значение, которое можно передать 16 битами - 65535. Это ограничение
касается и адресации памяти. Выходит, процессору доступно всего (65536/1024) 64 килобайта RAM. Чтобы
обойти это ограничение, была придумана модель адресации segment:offset. Сегмент в ней - это базовый адрес,
от которого считается смещение. Регистры процессора CS, DS, ES, FS, GS и SS - сегментные. Они используются
для указания адреса в памяти, от какого отсчитывается смещение. Например, DS:0050h означает байт 0050h
от значения, помещённого в DS. Вернее, от значения в DS*16. Это называется гранулярностью. Единица,
помещённая в регистр DS, устанавливает основание сегмента не в 1-й байт, а в 16-й. За счёт этого нам
становится доступен целый мегабайт оперативной памяти! (или даже больше с некоторыми ухищрениями,
но рассказывать о них я большого смысла не вижу, т.к. мы всё равно скоро покинем 16-битное царство)
Стек - область памяти, через которую можно передавать параметры процедурам в си-подобных языках
или сохранять состояние регистров при вызове прерывания. В случае ассемблера в стеке можно хранить
промежуточные результаты работы процедуры, если не хватило регистров. Кроме того, стек аппаратно
используется некоторыми командами процессора.
Команда mov op1,op2 используется для того, чтобы переместить значение op2 в op1. В качестве
операнда op1 может выступать адрес ячейки памяти или регистр. В качестве op2 может быть ячейка
памяти, регистр или конкретное значение. Есть два ограничения: операнды должны совпадать по
разрядности (нельзя поместить содержимое 16-битного регистра в 32-битный, например) и в качестве
обоих операндов не могут быть адреса в памяти. Так что делает этот код?
mov ax,07C0h
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
Правильно, он помещает значение 07С0h в регистр AX, потом копирует AX в сегментные регистры
DS, ES, FS и GS. Зачем? Затем, что BIOS копирует загрузочный сектор в 07С0h:0000h. Так как
наш загрузчик находится по этому адресу, будет правильным установить значения сегментных регистров
так, чтобы они указывали туда же. По какой-то причине (ей-богу не помню!) присваивать значения
сегментным регистрам напрямую нельзя, но можно через другой регистр - поэтому сначала мы загружаем
его в AX, а уже AX копируем в сегментные регистры. Вы наверое уже обратили внимание, что сегментные
регистры здесь не все. Для модификации оставшихся надо немного поплясать с бубном.
cli
mov ss,ax
mov sp,0FFFFh
sti
Что происходит здесь? Пара команд cli и sti запрещает и разрешает прерывания. Прерывания - то, при помощи
чего разные устройства в компьютере общаются с процессором. Они могут поступать от таймеров, дисковых
контроллеров и из множества других источников. Позже мы ещё поговорим о них подробно, а сейчас достаточно
знать, что команда cli вешает на процессор знак "не беспокоить". sti, соответственно, его снимает.
Дело в том, что SS - это сегментный регистр стека. При манипуляциях с ним лучше убедиться, что в
неподходящий момент не произойдёт переключение задачи. Обратите внимание: сегмент стека у нас там же,
где и загрузчик. Получается, помещая данные в стек, мы затрём часть собственного кода? Нет. Позиция стека
передаётся парой регистров SS:SP. SS - сегмент, а SP - смещение. mov sp, 0FFFFh устанавливает начало
стека в конец сегмента. Получается, ему некуда расти? Тоже нет. Стек растёт в обратном направлении.
Если мы командой push отправим в стек 16-битное слово, то указатель изменит значение на 0FFFDh. Таким
образом, загрузчик и стек находятся в разных концах 64-килобайтного сегмента, и расстояние между ними
вполне приличное.
push ax
mov ax,offset stop
and ax,03FFh
push ax
retf
Соберитесь, последний на сегодня кусок кода. Здесь мы модифицируем сегментный регистр кода, CS. К нему
тоже нужен особый подход. Кстати, самое время поговорить о том, как процессор узнаёт, какую команду выполнять
следующей. Как и в случае стека, существует указатель в виде пары регистров CS:IP. Каждый раз после
считывания из памяти инструкции IP увеличивается на её размер в байтах. Все модели BIOS помещают загрузчик
в 07C0h:0000h, но вот состояние CS:IP может быть разным: например, 07C0h:0000h и 0000h:7C00h указывают на
один и тот же байт в памяти, но во втором случае у нас могут быть проблемы. В каком именно состоянии
оказались регистры CS:IP при старте загрузчика, мы не знаем, поэтому лучше перестраховаться и установить
своё значение.
Как установить значение CS:IP? Например, при помощи инструкции дальнего возврата retf. Обычно она
используется для возврата из процедур, но подойдёт и нам, так кк делает именно то, что нужно: меняет
значения CS:IP. Сегмент и смещение для возврата должны быть в стеке. В AX у нас значение сегмента, 07C0h,
так что командой push отправляем его в стек. А вот с IP придётся повозиться. Щас объясню. CS в данный момент
может быть установлен либо в 07C0h, либо в 0000h. Значит, любое считанное нами смещение относительно его
начала будет равно или X или X+7C00h. Нам нужно однозначно привести его к X. Как это сделать? Команда
mov AX,offset stop помещает в AX смещение метки stop (то есть, конечно, команды cli, сами метки
в исполняемом файле физически не присутствуют и места не занимают). 7С00h, если его перевести в
двоичный вид, будет равно 111110000000000b. Соответственно, искомый X помещается в восьми нулях в начале
значения. обнуление старших пяти единиц будет эквивалентно уменьшению значения на 7С00h, что нам и нужно.
Про логические операции поговорим позже, но пока знайте, что команда and AX,03FFh делает как раз это:
обнуляет все старшие разряды AX, начиная с первой единицы в 111110000000000b. 03FFh, кстати, в
двоичном виде будет выглядеть так: 1111111111b. Заметили связь? В общем, если кто-то не разбирается в
логических операциях, то ДЗ на сегодня - просветиться по этой теме.
Фух, чёрт возьми, на сегодня всё! Теперь наш загрузчик будет работать в предсказуемой среде, что сэкономит
нам море усилий.