Форум программистов, компьютерный форум, киберфорум
ОС на Assembler
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  
 
Рейтинг 4.86/258: Рейтинг темы: голосов - 258, средняя оценка - 4.86
Ушел с форума
Автор FAQ
 Аватар для Mikl___
16373 / 7685 / 1080
Регистрация: 11.11.2010
Сообщений: 13,759

Пишем загрузочный сектор

12.10.2013, 07:37. Показов 50932. Ответов 14
Метки нет (Все метки)

Студворк — интернет-сервис помощи студентам
Статья Пишем свой загрузочный сектор без указания автора найдена в разделе assembler на сайте http://www.cyberguru.ru. Пятиминутный поиск в Google привел на сайт автора этой статьи Алексея Золотова www.zolotov.h14.ru, у которого оказался целый цикл статей, которые, как пишет автор могут быть полезными тем, кто хочет написать свою ОС (К сожалению, проект заморожен на неопределенное время.)
Многозадачная ОС
  • О проекте. О проекте "Многозадачная ОС"
  • Начальная загрузка. В статье описывается процесс первоначальной загрузки компьютера.
  • Файловая система FAT. Описание файловой системы FAT.
  • Загрузочный сектор. Пишем свой загрузочный сектор для floppy.
  • Загрузчик ядра. Пишем загрузчик ядра.
  • Определение размера физической памяти. О том как определить количество доступной физической памяти
10
Programming
Эксперт
39485 / 9562 / 3019
Регистрация: 12.04.2006
Сообщений: 41,671
Блог
12.10.2013, 07:37
Ответы с готовыми решениями:

Загрузочный сектор
Здравствуйте! Компилирую (fasm) из данного кода бинарник и загружаю с него виртуальную машину (VMware), но на экране ничего нет. Помогите...

[NASM] Загрузочный сектор
Здравствуйте! Столкнулся с такой проблемой: самый простой бутлоадер работает не так, как ожидается. А именно: сначало прыгаем в main после...

Загрузочный сектор. Не загружается InsydeH20
Написал на FASM небольшой загрузочный сектор решил проверить, и он не загрузился, в BIOS видит флешку, на 1. USB в параметрах загрузки, вот...

14
Ушел с форума
Автор FAQ
 Аватар для Mikl___
16373 / 7685 / 1080
Регистрация: 11.11.2010
Сообщений: 13,759
12.10.2013, 07:46  [ТС]
статья взята на домашней страничке Алексея Золотова http://zolotov.h14.ru/doc/os/os.php
О проекте
Я давно хотел написать свою маленькую операционную систему. Мечтать невредно, но пора бы начать осуществлять свою мечту. В этом первом разделе я попытаюсь описывать этот процесс с подробными комментариями, чтобы любой желающий мог присоединиться. Конечно, можно взять исходники какой-нибудь Unix-подобной операционной системы и переделывать их на свой лад, но я намерен писать с нуля новую, свою операционную систему.
Процесс загрузки компьютера начинается с небольшой программы POST зашитой BIOS, его мы естественно менять не будем и начнем, как все нормальные люди, с загрузочного сектора. Так как мы с начала будем писать загрузочный сектор для трех дюймовой дискеты с файловой системой FAT, то советую прочитать немного про FAT.
Список сайтов
Здесь я буду приводить список ссылок на другие сайты подобной тематики, чтобы вам (и мне тоже) было проще найти нужную информацию. Если вы желаете добавить ссылку, напишите мне на e-mail zolotov-alex@mail.ru и если я сочту необходимым, то я добавлю сюда вашу ссылку.
______________________________________
статья взята на домашней страничке Алексея Золотова http://zolotov.h14.ru/doc/os/post.php
Начальная загрузка
Начнем с самого начала – аппаратного сброса процессора. При поступлении сигнала Reset процессор перестает исполнять инструкции и управлять системной шиной. В этот момент процессор пассивен, он принимает на свои входы сигналы, задающие его конфигурацию (коэффициент умножения, роль в много процессорных системах и некоторые другие параметры), регистры процессора (не все) приводятся в определенное состояние. После окончания сигнала Reset процессор исполняет первую инструкцию, расположенную по определенному адресу: на 15 байт меньше максимального физического адреса (у процессоров P6 возможен выбор между значением 0xFFFFFFF0 и 0xFFFF0). По этому адресу должна располагаться инструкция, с которой начинается инициализация процессора. Программа начальной инициализации, называемая POST (Power On Self Test – самотестирование по включению), хранится в постоянной памяти ROM BIOS (ПЗУ базовой системы ввода/вывода). Она выполняет инициализацию процессора – устанавливает необходимый режим и значения регистров. Далее выполняется проверка работоспособности и инициализации подсистем компьютера. После всех проверок и инициализации BIOS знает реальную конфигурацию компьютера и готова к загрузке операционной системы. Программа POST завершается вызовом процедуры начальной загрузки.
Процедура начальной загрузки (bootstrap loader) вызывается как программное прерывание (BIOS Int 19h). Эта процедура определяет первое готовое устройство из списка разрешенных и доступных (гибкий или жесткий диск, компакт-диск, сетевой адаптер) и пытается загрузить с него короткую программу загрузки. Эта программа может выполнятся в два этапа: сначала с жесткого диска загружается загрузчик MBR (Master Boot Record – главная загрузочная запись) и ему передается управление. Затем MBR определяет активный раздел и загружает с него загрузчик этого раздела и передает ему управление. В свою очередь загрузчик раздела загружает необходимые файлы операционной системы и передает ей управление.
Загрузчик загружается по физическому адресу 0x7C00 размером 512 байт. Процессор при этом находится в реальном режиме адресов и доступны системные вызовы BIOS. Загрузчик в виду его маленького размера должен найти на диске и загрузить в память другой код, который собственно и выполнит загрузку и инициализацию операционной системы.

Список литературы
  • Михаил Гук, Виктор Юров "Процессоры Pentium 4 Athlon и Duron" - СПб.: Питер, 2001. - 512 с.: ил.
______________________________________
статья взята на домашней страничке Алексея Золотова http://zolotov.h14.ru/doc/os/fat.php
Файловая система FAT
FAT (File Allocation Table – таблица размещения файлов) - этот термин относится к одному из способов организации файловой системы на диске. Эта таблица хранит информацию о файлах на жестком диске в виде последовательности чисел, определяющих, где находится каждая часть каждого файла. С ее помощью операционная система выясняет, какие кластеры занимает нужный файл. FAT - является самой распространенной файловой системой и поддерживается подавляющим большинством операционных систем. Сначала FAT была 12-разрядной и позволяла работать с дискетами и логическими дисками объемом не более 16 Мбайт. В MS-DOS версии 3.0 таблица FAT стала 16-разрядной для поддержки дисков большей емкости, а для дисков объемом до 2 047 Гбайт используется 32-разрядная таблица FAT.
Заголовок файловой системы FAT
Эта часть загрузочного сектора известна как BIOS Parameter Block (BPB) (блок параметров BIOS). Она содержит физические характеристики диска, которые MS-DOS и Windows используют при поиске определенного участка. Складывая или перемножая значения этих параметров, операционная система узнает, где находится таблица FAT, корневой каталог, где начинается и кончается область данных.
Общая часть заголовка файловой системы FAT
Эта часть общая для всех файловых систем семейства FAT (FAT12, FAT16 и FAT32):
СмещениеНазваниеРазмерОписание
0BS_jmpBoot3Команда перехода на загрузчик ОС
3BS_OEMName8Название и версия Windows
11BPB_BytsPerSec2Количество байтов в секторе (всегда 512)
13BPB_SecPerClus1Секторов на кластер всегда кратно степени двух
14BPB_RsvdSecCnt2Количество зарезервированных секторов перед первой FAT
16BPB_NumFATs1Количество таблиц FAT (всегда 2)
17BPB_RootEntCnt2Количество элементов в корневом каталоге (максимальный предел)
19BPB_TotSec162Общее число секторов (0 - если размер диска больше 32 Мб)
21BPB_Media1Тип устройства: F0 - гибкий диск, F8 - жесткий диск с любой емкостью
22BPB_FATsz162Количество секторов на элемент таблицы FAT
24BPB_SecPerTrk2Количество секторов на дорожку
26BPB_NumHeads2Число головок
28BPB_HiddSec4Количество скрытых секторов
32BPB_TotSec324Общее число секторов (0 - если размер диска меньше 32 Мб)
FAT12 и FAT16
FAT12 и FAT16 имеют одинаковый формат заголовка.
СмещениеНазваниеРазмерОписание
36BS_DrvNum1Номер диска
37BS_NtRsvd1Зарезервировано для Windows NT
38BS_BootSig1Расширенная сигнатура (если 29h, то следующие поля актуальны)
39BS_VolID4Серийный номер тома
43BS_VolLab11Метка тома
54BS_FilSysSize8Тип файловой системы (12- или 16-разрядная)
______________________________________
Загрузочный сектор
Мы будем писать загрузочный сектор для трехдюймовой дискеты с файловой системой FAT12. После окончания начальной загрузки программа POST находит активное устройство и загружает с него короткую программу загрузки ОС - загрузочный сектор. Загрузочный сектор это первый физический сектор устройства, в данном случае дискеты и его размет равен всего ничего 512 байт. С помощью этих 512 байт кода мы должны найти основную часть загрузчика операционной системы, загрузить его в память и передать ему управление. Заголовок файловой системы FAT находится в первом секторе дискеты, благодаря чему этот заголовок, содержащий всю необходимую информацию о файловой системе, загружается вместе нашим загрузчиком. Наш загрузочный сектор будет искать в корневом каталоге некоторый файл - загрузчик, загрузит его в память и передаст ему управление на его начало. А загрузчик уже сам разберется, что ему делать дальше. Я использую NASM, т.к. считаю, что он больше подходит для наших целей.

И так, приступим. Как я уже говорил, в начале нашего загрузочного сектора располагается заголовок FAT, опишем его:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
; Общая часть для всех типов FAT
BS_jmpBoot:
jmp short BootStart ; Переходим на код загрузчика
nop
BS_OEMName  db '*-v4VIHC'   ; 8 байт, что было на моей дискете, то и написал
BPB_BytsPerSec  dw 0x200    ; Байт на сектор
BPB_SecPerClus  db 1    ; Секторов на кластер
BPB_RsvdSecCnt  dw 1    ; Число резервных секторов
BPB_NumFATs db 2    ; Количество копий FAT
BPB_RootEntCnt  dw 224  ; Элементов в корневом каталоге (max)
BPB_TotSec16    dw 2880 ; Всего секторов или 0
BPB_Media   db 0xF0 ; код типа устройства
BPB_FATsz16 dw 9    ; Секторов на элемент таблицы FAT
BPB_SecPerTrk   dw 18   ; Секторов на дорожку
BPB_NumHeads    dw 2    ; Число головок
BPB_HiddSec dd 0    ; Скрытых секторов
BPB_TotSec32    dd 0    ; Всего секторов или 0
; Заголовок для FAT12 и FAT16
BS_DrvNum   db 0    ; Номер диска для прерывания int 0x13
BS_ResNT    db 0    ; Зарезервировано для Windows NT
BS_BootSig  db 29h  ; Сигнатура расширения
BS_VolID    dd 2a876CE1h    ; Серийный номер тома
BS_VolLab   db 'X boot disk'    ; 11 байт, метка тома
BS_FilSysType   db 'FAT12   '   ; 8 байт, тип ФС
; Структура элемента каталога
struc   DirItem
    DIR_Name:   resb 11
    DIR_Attr:   resb 1
    DIR_ResNT:  resb 1
    DIR_CrtTimeTenth    resb 1
    DIR_CrtTime:    resw 1
    DIR_CrtDate:    resw 1
    DIR_LstAccDate: resw 1
    DIR_FstClusHi:  resw 1
    DIR_WrtTime:    resw 1
    DIR_WrtDate:    resw 1
    DIR_FstClusLow: resw 1
    DIR_FileSize:   resd 1
endstruc ;DirItem
Большинство полей мы использовать не будем, и так мало места для полета. Загрузчик BIOS передает нам управление на начало загрузочного сектора, т.е. на BS_jmpBoot, поэтому в начале заголовка FAT на отводится 3 байта для короткой или длинной инструкции jmp. Мы в данном случае использовали короткую, указав модификатор short, и в третьем байте просто разместили однобайтовую инструкцию nop.
По инструкции jmp short BootStart мы переходим на наш код. Проведем небольшую инициализацию:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
; Наши не инициализированные переменные
; При инициализации они затрут не нужные нам
; поля заголовка FAT: BS_jmpBoot и BS_OEMName
struc   NotInitData
    SysSize:    resd 1  ; Размер системной области FAT
    fails:  resd 1  ; Число неудачных попыток при чтении
    fat:    resd 1  ; Номер загруженного сектора с элементами FAT
endstruc ;NotInitData
; По этому адресу мы будем загружать загрузчик
%define SETUP_ADDR  0x1000
; А по этому адресу нас должны были загрузить
%define BOOT_ADDR   0x7C00
%define BUF 0x500
BootStart:
    cld
    xor cx, cx
    mov ss, cx
    mov es, cx
    mov ds, cx
    mov sp, BOOT_ADDR
    mov bp, sp
    ; Сообщим о том что мы загружаемся
    mov si, BOOT_ADDR + mLoading
    call    print
Все сегментные регистры настраиваем на начало физической памяти. Вершину стека настраиваем на начало нашего сектора, стек растет вниз (т.е. в сторону младших адресов), так что проблем быть не должно. Туда же указывает регистр bp - нам нужно обращаться к полям заголовка FAT и паре наших переменных. Мы используем базовую адресацию со смещением, для чего используем регистр bp т.к. в этом случае можно использовать однобайтовые смещения, вместо двухбайтовых адресов, что позволяет сократить код. Процедуру print, выводящую сообщение на экран, рассмотрим позже.
Теперь нам нужно вычислить номера первых секторов корневого каталога и данных файлов.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    mov al, [byte bp+BPB_NumFATs]
    cbw
    mul word [byte bp+BPB_FATsz16]
    add ax, [byte bp+BPB_HiddSec]
    adc dx, [byte bp+BPB_HiddSec+2]
    add ax, [byte bp+BPB_RsvdSecCnt]
    adc dx, cx
    mov si, [byte bp+BPB_RootEntCnt]
    ; dx:ax - Номер первого сектора корневого каталога
    ; si - Количество элементов в корневом каталоге
    pusha
    ; Вычислим размер системной области FAT = резервные сектора +
    ; все копии FAT + корневой каталог
    mov [bp+SysSize], ax    ; осталось добавить размер каталога
    mov [bp+SysSize+2], dx
    ; Вычислим размер корневого каталога
    mov ax, 32
    mul si
    ; dx:ax - размер корневого каталога в байтах, а надо в секторах
    mov bx, [byte bp+BPB_BytsPerSec]
    add ax, bx
    dec ax
    div bx
    ; ax - размер корневого каталога в секторах
    add [bp+SysSize], ax    ; Теперь мы знаем размер системной
    adc [bp+SysSize+2], cx  ; области FAT, и начало области данных
    popa
    ; В dx:ax - снова номер первого сектора корневого каталога
    ; si - количество элементов в корневом каталоге
Теперь мы будем просматривать корневой каталог в поисках нужного нам файла
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NextDirSector:
    ; Загрузим очередной сектор каталога во временный буфер
    mov bx, 700h    ; es:bx - буфер для считываемого сектора
    mov di, bx  ; указатель текущего элемента каталога
    mov cx, 1   ; количество секторов для чтения
    call    ReadSectors
    jc  near DiskError  ; ошибка при чтении
RootDirLoop:
    ; Ищем наш файл
    ; cx = 0 после функции ReadSectors
    cmp [di], ch    ; byte ptr [di] = 0?
    jz  near NotFound   ; Да, это последний элемент в каталоге
    ; Нет, не последний, сравним имя файла
    pusha
    mov cl, 11  ; длина имени файла с расширением
    mov si, BOOT_ADDR + LoaderName  ; указатель на имя искомого файла
    rep cmpsb   ; сравниваем
    popa
    jz  short Found ; Нашли, выходим из цикла
    ; Нет, ищем дальше
    dec si  ; RootEntCnt
    jz  near NotFound   ; Это был последний элемент каталога
    add di, 32  ; Переходим к следующему элементу каталога
    ; bx указывает на конец прочтенного сектора после call ReadSectors
    cmp di, bx  ; Последний элемент в буфере?
    jb  short RootDirLoop   ; Нет, проверим следующий элемент
    jmp short NextDirSector ; Да последний, загрузим следующий сектор
Из этого кода мы можем выйти одну из трех точек: ошибка при чтении DiskError, файл найден Found или файл не найден NotFound.
Если файл найден, то загрузим его в память и передадим управление на его начало.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
Found:
    ; Загрузка загрузчика (извините, каламбур)
    mov bx, SETUP_ADDR
    mov ax, [byte di+DIR_FstClusLow]    ; Номер первого кластера файла
    ; Загружаем сектор с элементами FAT, среди которых есть FAT[ax]
    ; LoadFAT сохраняет значения всех регистров
    call    LoadFAT
ReadCluster:
    ; ax - Номер очередного кластера
    ; Загрузим его в память
    push    ax
    ; Первые два элемента FAT служебные
    dec ax
    dec ax
    ; Число секторов для чтения
    ; cx = 0 после ReadSectors
    mov cl, [byte bp+BPB_SecPerClus]    ; Секторов на кластер
    mul cx
    ; dx:ax - Смещение кластера относительно области данных
    add ax, [byte bp+SysSize]
    adc dx, [byte bp+SysSize+2]
    ; dx:ax - Номер первого сектора требуемого кластера
    ; cx еще хранит количество секторов на кластер
    ; es:bx - конец прошлого кластера и начало нового
    call    ReadSectors ; читаем кластер
    jc  near DiskError  ; Увы, ошибка чтения
    pop ax  ; Номер кластера
    ; Это конец файла?
    ; Получим значение следующего элемента FAT
    pusha
    ; Вычислим адрес элемента FAT
    mov bx, ax
    shl ax, 1
    add ax, bx
    shr ax, 1
    ; Получим номер сектора, в котором находится текущий элемент FAT
    cwd
    div word [byte bp+BPB_BytsPerSec]
    cmp ax, [bp+fat]    ; Мы уже читали этот сектор?
    popa
    je  Checked ; Да, читали
    ; Нет, надо загрузить этот сектор
    call    LoadFAT
Checked:
    ; Вычислим адрес элемента FAT в буфере
    push    bx
    mov bx, ax
    shl bx, 1
    add bx, ax
    shr bx, 1
    and bx, 511 ; остаток от деления на 512
    mov bx, [bx+0x700]  ; а вот и адрес
    ; Извлечем следующий элемент FAT
    ; В FAT16 и FAT32 все немного проще :(
    test    al, 1
    jnz odd
    and bx, 0xFFF
    jmp short done
odd:
    shr bx, 4
done:
    mov ax, bx
    pop bx
    ; bx - новый элемент FAT
    cmp ax, 0xFF8   ; EOF - конец файла?
    jb  ReadCluster ; Нет, читаем следующий кластер
    ; Наконец-то загрузили
    mov ax, SETUP_ADDR>>4   ; SETUP_SEG
    mov es, ax
    mov ds, ax
    ; Передаем управление, наше дело сделано :)
    jmp SETUP_ADDR>>4:0
    
LoadFAT ;proc
; Процедура для загрузки сектора с элементами FAT
; Элемент ax должен находится в этом секторе
; Процедура не должна менять никаких регистров
    pusha
    ; Вычисляем адрес слова содержащего нужный элемент
    mov bx, ax
    shl ax, 1
    add ax, bx
    shr ax, 1
    cwd
    div word [byte bp+BPB_BytsPerSec]
    ; ax - смещение сектора относительно начала таблицы FAT
    mov [bp+fat], ax    ; Запомним это смещение, dx = 0
    cwd         ; dx:ax - номер сектора, содержащего FAT[?]
    ; Добавим смещение к первой копии таблицы FAT
    add ax, [byte bp+BPB_RsvdSecCnt]
    adc dx, 0
    add ax, [byte bp+BPB_HiddSec]
    adc dx, [byte bp+BPB_HiddSec+2]
    mov cx, 1   ; Читаем один сектор. Можно было бы и больше, но не быстрее
    mov bx, 700h    ; Адрес буфера
    call    ReadSectors
    jc  DiskError   ; Ошибочка вышла
    popa
    ret
;LoadFAT    endp
В FAT12 на каждый элемент FAT отводится по 12 бит, что несколько усложняет нашу работу, в FAT16 и FAT32 на каждый элемент отводится по 16 и 32 бита соответственно и можно просто прочесть слово или двойное слово, а в FAT12 необходимо прочесть слово содержащее элемент FAT и правильно извлечь из него 12 бит.
Теперь разберем процедуру загрузки секторов. Процедура получает номер сектора в dx:ax (нумерация с нуля) и преобразует его к формату CSH (цилиндр, сектор, сторона), используемому прерыванием BIOS int 0x13.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
; *************************************************
; *          Чтение секторов с диска              *
; *************************************************
; * Входные параметры:                            *
; * dx:ax       - (LBA) номер сектора             *
; * cx          - количество секторов для чтения  *
; * es:bx       - адрес буфера                    *
; *************************************************
; * Выходные параметры:                           *
; * cx       - Количество не прочтенных секторов  *
; * es:bx    - Указывает на конец буфера          *
; * cf = 1   - Произошла ошибка при чтении        *
; *************************************************
ReadSectors ;proc
next_sector:
    ; Читаем очередной сектор
    mov byte [bp+fails], 3  ; Количество попыток прочесть сектор
try:
    ; Очередная попытка
    pusha
    ; Преобразуем линейный адрес в CSH
    ; dx:ax = a1:a0
    xchg    ax, cx      ; cx = a0
    mov ax, [byte bp+BPB_SecPerTrk]
    xchg    ax, si      ; si = Scnt
    xchg    ax, dx      ; ax = a1
    xor dx, dx
    ; dx:ax = 0:a1
    div si      ; ax = q1, dx = c1
    xchg    ax, cx      ; cx = q1, ax = a0
    ; dx:ax = c1:a0
    div si      ; ax = q2, dx = c2 = c
    inc dx      ; dx = Sector?
    xchg    cx, dx      ; cx = c, dx = q1
    ; dx:ax = q1:q2
    div word [byte bp+BPB_NumHeads] ; ax = C (track), dx = H
    mov dh, dl      ; dh = H
    mov ch, al
    ror ah, 2
    or  cl, ah
    mov ax, 0201h       ; ah=2 - номер функции, al = 1 сектор
    mov dl, [byte bp+BS_DrvNum]
    int 13h
    popa
    jc  Failure ; Ошибка при чтении
    ; Номер следующего сектора
    inc ax
    jnz next
    inc dx
next:
    add bx, [byte bp+BPB_BytsPerSec]
    dec cx  ; Все сектора прочтены?
    jnz next_sector ; Нет, читаем дальше
return:
    ret
Failure:
    dec byte [bp+fails] ; Последняя попытка?
    jnz try ; Нет, еще раз
    ; Последняя, выходим с ошибкой
    stc
    ret
;ReadSectors    endp
Осталось всего ничего:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
; Сообщения об ошибках
NotFound:   ; Файл не найден
    mov si, BOOT_ADDR + mLoaderNotFound
    call    print
    jmp short die
DiskError:  ; Ошибка чтения
    mov si, BOOT_ADDR + mDiskError
    call    print
    ;jmp    short die   
die:    ; Просто ошибка
    mov si, BOOT_ADDR + mReboot
    call    print
_die:   ; Бесконечный цикл, пользователь сам нажмет Reset
    jmp short _die
; Процедура вывода ASCIIZ строки на экран
; ds:si - адрес строки
print:  ; proc
    pusha
print_char:
    lodsb   ; Читаем очередной символ
    test    al, al  ; 0 - конец?
    jz  short pr_exit   ; Да конец
    ; Нет, выводим этот символ
    mov ah, 0eh
    mov bl, 7
    int 10h
    jmp short print_char    ; Следующий
pr_exit:
    popa
    ret
;print  endp
; Перевод строки
%define endl 10,13,0
; Строковые сообщения
mLoading    db 'Loading...',endl
mDiskError  db 'Disk I/O error',endl
mLoaderNotFound db 'Loader not found',endl
mReboot     db 'Reboot system',endl
; Выравнивание размера образа на 512 байт
times 499-($-$$) db 0
LoaderName  db 'BOOTOR     '    ; Имя файла загрузчика
BootMagic   dw 0xAA55   ; Сигнатура загрузочного сектора
Ну вот вроде бы и все. Компилируется все это до безобразия просто:
> nasm -f bin boot.asm -lboot.lst -oboot.bin
Осталось только как-то записать этот образ в загрузочный сектор вашей дискеты и разместить в корне этой дискеты файл загрузчика BOOTOR. Загрузочный сектор можно записать с помощью такой вот простой программы на Turbo (Borland) Pascal. Эта программа будет работать как в DOS, так и в Windows - пробовал на WinXP - работает как ни странно, но только с floopy. Но все же я рекомендую запускать эту утилиту из-под чистого DOS'а, т.к. WinXP обновляет не все поля в заголовке FAT и загрузочный сектор может работать некорректно.
Pascal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var
  fn:string;
  f:file;
  buf:array[0..511] of byte;
  ok:boolean;
begin
  fn:=ParamStr(1);
  if fn='' then writeln('makeboot bootsect.bin')
  else
  begin
    writeln('Making boot floppy');
    {$I-}
    assign(f,fn);
    reset(f,sizeof(buf));
    BlockRead(f,buf,1);
    close(f);
    {$I+}
    if IOResult<>0 then
    begin
      Writeln('Failed to read file "',fn,'"');
      Halt(1);
    end;
    ok:=false;
    asm
      mov       ax, 0301h
      mov       cx, 1
      mov       dx, 0
      mov       bx, seg buf
      mov       es, bx
      mov       bx, offset buf
      int       13h
      jc        @error
      mov       ok, true
    @error:
    end;
    if ok then writeln('Done :)')
    else begin
      writeln('Makeboot failed :(');
      Halt(1);
    end;
  end;
end.
Исходники: полный пакет, fat12.asm.zip(3,86Kb), Netwide Assembler for Win32. Следующий этап загрузка ядра операционной системы.
Вложения
Тип файла: zip fat12.asm.zip (3.9 Кб, 246 просмотров)
Тип файла: zip fat12.zip (8.7 Кб, 292 просмотров)
5
Ушел с форума
Автор FAQ
 Аватар для Mikl___
16373 / 7685 / 1080
Регистрация: 11.11.2010
Сообщений: 13,759
12.10.2013, 09:58  [ТС]
статья взята на домашней страничке Алексея Золотова http://zolotov.h14.ru/doc/os/bootor.php
Загрузчик ядра
Относительно процесса загрузки, ядро операционной системы состоит из двух частей 16-битной и 32-битной. Ядро получает управление в реальном режиме, проводит первичную инициализацию 32-битной части, переходит в защищенный режим и основная часть инициализации ядра происходит в защищенном режиме. В этой статье мы рассмотрим 16-битную часть - загрузчик ядра.
Пока наше ядро имеет небольшие размеры мы для простоты включим файл 32-битной части (далее просто ядро) в 16-битную часть (далее загрузчик ядра).
Основные задачи загрузчика ядра:
  • Получить информацию о физической памяти от BIOS и сохранить её в строго оговоренном месте: на первой физической странице;
  • Перейти в защищенный режим со страничным преобразованием;
  • Найти и отобразить ядро на тот виртуальный адрес, который ожидает ядро;
  • Передать управление ядру.
Данная статья находиться пока в разработке. Как найду время (скорее всего после экзаменов) доработаю ее и обновлю. (Читателю следует учесть, что прошло уже 9 лет с момента написания статьи) А пока, в связи проявленным большим интересом выложу здесь просто текст самого модуля. Пожалуй самое интересное в нем, так это включение страничной переадресации и отображение ядра на соответствующие адреса. Ядро является обычным PE-файлом.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
; -----------------------------------------------------
; MinOS bootor
; Золотов Алексей 2004
; e-mail:  [email]zolotov@mail.ru[/email]
; url:     [url]http://zolotov.h14.ru[/url]
; -----------------------------------------------------
 
; Загрузочный сектор должен был загрузить нас
; по адресу 0:0x1000. Мы еще в реальном режиме и нам
; нужно получить некоторую информацию от BIOS:
; карту доступной памяти.
 
%include "kernel.inc"
%include "memory.inc"
 
BOOTOR_MAGIC equ 0x12EF ; Сигнатура
 
org 0x1000
base:
; Определимся с регистрами:
    xor ax, ax
    mov es, ax
    mov ds, ax
    mov ss, ax
    mov sp, 0x1000
; Установим видеорежим, очиститься экран
    mov ax, 3
    int 10h
; Вообщим о том что мы загрузились
    mov si, .mVersion
    call    .print
    cmp word [BOOTOR_SIG], BOOTOR_MAGIC
    je  near .GoodSig
; Загрузочный сектор неправидьно нас загрузил
    mov si, .mFAIL
    call    .print
    mov si, .mBadSig
    call    .print
.die:   jmp short .die
 
.mBadSig    db 'Major problem: Loader did not load kernel completely.',10,13
    db 'Reboot computer',10,13,0
.mVersion   db KERNEL_VERSION,10,13,"checking sig... ",0
.mOK    db '[ OK ]',10,13,0
.mFAIL  db '[FAIL]',10,13,0
 
; -----------------------------------------------------
.print:
; Print ASCIIZ string
    cld
    pusha
.PrintChar:
    lodsb
    test    al, al
    jz  short .Exit
    mov ah, 0eh
    mov bl, 7
    int 10h
    jmp short .PrintChar
.Exit:
    popa
    ret
; End of print
; -----------------------------------------------------
 
.GoodSig:   mov si, .mOK
    call    .print
    
    ; Остановить мотор флоппи
    mov dx, 0x03f2
    xor al, al
    out dx, al
 
; -----------------------------------------------------
; * Method E820h:
; * E820 returns memory classified into a whole bunch
; * of different types, and allows memory holes and
; * everything. We scan through this memory map and
; * build a list of the first 32 memory areas, which
; * we return at [E820_MAP]
; * This is documented at [url]http://www.teleport.com/~acpi/acpihtml/topic245.htm[/url]
.memE820:
    xor ebx, ebx    ; Continuation counter
    mov [E820_CNT], ebx ; Count of rectords in E820 map
    mov di, E820_MAP
    
.jmpE820:
    mov eax, 0xE820
    mov edx, SMAP
    mov ecx, 20     ; Size of E820 record
    int 0x15
    jc  .bailE820
    
    cmp eax, SMAP   ; Check the return is 'SMAP'
    jne .bailE820
    
.goodE820:
    mov eax, [E820_CNT]
    cmp eax, E820_MAX
    jnl .bailE820
    
    inc dword [E820_CNT]
    add di, 24  ; alignment
    
    test    ebx, ebx
    jnz .jmpE820
    
.bailE820:
 
; -----------------------------------------------------
; * Method E801:
; * Memory size is in 1K chunksizes
.memE801:
    mov [E801_MEM], dword 0
    mov ax, 0xE801
    int 0x15
    jc  .bailE801
    
    and edx, 0xFFFF ; Clear sign extend
    shl edx, 6      ; go form 64K to 1K chunks
    and ecx, 0xFFFF ; Clear sing extend
    add edx, ecx
    mov [E801_MEM], edx
    
.bailE801:
 
; -----------------------------------------------------
; * Method 88h
; * Returns the memory size (up to 16mb or 64mb, depending
; * on BIOS) in ax
.mem88:
    xor eax, eax
    mov [MEM_88], eax
    mov ah, 0x88
    int 0x15
    mov [MEM_88], ax
 
; -----------------------------------------------------
; * Enable A20
    call    .empty_8042
    
    mov al, 0xD1    ; command write
    out 0x64, al
    call    .empty_8042
    
    mov al, 0xDF    ; A20 on
    out 0x60, al
    call    .empty_8042
    
; * You must preserve the other bits here. Otherwise
; * embarrasing things like laptops powering on boot
; * happen. Corrected version byte Kira Brown from
; * Linux 2.2
    in  al, 0x92
    or  al, 2       ; "fast A20" version
    out 0x92, al    ; some chips have only this
    
; * Wait until A20 really *is* enabled; it can take
; * a fair amount of time on certain systems; Toshiba
; * Tecras are known to have this problem. The memory
; * localtion used here [0x200] is the int 0x80
; * vector, which should be safe to use.
    ; ds=0000
    mov ax, 0xFFFF  ; segment 0xFFFF (HMA)
    mov gs, ax
    
.A20_wait:
    inc ax
    mov [0x200], ax
    cmp ax,[gs:0x210]
    je  .A20_wait
    
; * Make sure any possible coprocessor is propertly reset
    xor ax, ax
    out 0xF0, al
    call    .delay
    
    out 0xF1, al
    call    .delay
    
; * Mask all interrupts
    mov al, 0xFF
    out 0xA1, al
    call    .delay
    
    out 0x21, al
    call    .delay
    
    cli
    ; Jump to protected mode switch
    jmp near .Switch
 
; -----------------------------------------------------
.delay:
    jmp short $+2
    jmp short $+2
    retn
    
; -----------------------------------------------------
.empty_8042:
    pusha
    mov ecx, 100000
.wait_loop:
    dec ecx
    jz  .wait_end
    
    call    .delay
    in  al, 0x64
    test    al, 1       ; output buffer
    jz  .no_output
    
    call    .delay
    in  al, 0x60
    jmp short .wait_loop
    
.no_output:
    test    al, 2       ; is input buffer full
    jnz .wait_loop
.wait_end:
    popa
    retn
; End Wait8042BufferEmtry
 
.mJumpPM    db 'Moving to protected mode',10,13,0
 
; -----------------------------------------------------
; * Jump to protected mode
align   16
.GDT:
; Dummy selector
.sel_dummy:
    dw 0,0,0,0
 
; Code selector
.sel_code:
    dw 0xFFFF   ; limit 4G
    dw 0x0000   ; base addres
    dw 0x9A00   ; code read/exec
    dw 0x00CF   ; granularity = 4096, 
 
; Data selector
.sel_data:
    dw 0xFFFF   ; limit 4G
    dw 0x0000   ; base addres
    dw 0x9200   ; data read/write
    dw 0x00CF   ; granuarity = 4096
 
.gdt_48:
    dw 0x8000   ; GTD limit
    dd .GDT     ; GTD base addres
    
.Switch:
    mov si, .mJumpPM
    call    .print
    lgdt    [.gdt_48]
    cli
    mov eax, 1
    mov cr0, eax
    jmp dword 0x8:Protected
    
; -----------------------------------------------------
; * From this time we have a 32 bit protected mode
[BITS   32]
Protected:
; Now we are in protected mode
    xor eax, eax
    mov al, 0x10
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x1000
 
    call    vga_init
    
; Включим страничное преобразование
PDE_PHY equ 0x00100000  ; Каталог таблиц
PTE_PHY equ 0x00101000  ; Каталог страниц первых 4 Мб
FREE_PHY    equ 0x00102000
init_pagging:
    cld
    ; Инициализируем каталог таблиц
    mov edi, PDE_PHY
    mov eax, PTE_PHY | 7
    stosd
    mov ecx, 1023
    xor eax, eax
    rep stosd
    ; Отобразим один в один первые 4 Мб
    mov eax, 7
    mov ecx, 1024
.cycl:  stosd
    add eax, 0x1000
    loop    .cycl
 
    mov eax, dword [PDE_PHY+4*(KERNEL_BASE>>22)]
    bt  eax, 0
    jc  .exists
    call    phy_page_alloc
    or  al, 7
    mov dword [PDE_PHY+4*(KERNEL_BASE>>22)], eax
.exists:    and eax, -4*K
    mov edi, eax
    mov ecx, 1024
    mov eax, 4*M | 7
.cycl2: stosd
    add eax, 0x1000
    loop    .cycl2
 
    ; Загрузим каталог таблиц
    mov eax, PDE_PHY
    mov cr3, eax
    ; Включим страничную переадресацию
    mov eax, 0x80000001
    mov cr0, eax
; Теперь пора сбросить конвейер и загрузить ядро
    jmp debug
mPagging    db "Pagging enabled",endl,0
mKermap db "Kernel mapped",endl,0
mSecErr db "Error: wrong number of sections in kernel",endl,0
die32:  jmp short die32
    
debug:
    mov eax, mPagging
    call    print
 
 
    jmp LoadKernel
 
_wait:  pusha
    mov ecx, 1024*1024*1024
.cycl:  loop    .cycl
    popa
    ret
    
free_page   dd FREE_PHY ; Первая свободная страница
 
phy_page_alloc:
; Выделить свободную физическую страницу
; eax
    mov eax, [free_page]
    add dword [free_page], 4*K
    ret
 
page_clear:
; Очистить страницу
; eax
    pusha
    mov edi, eax
    xor eax, eax
    mov ecx, 1024
    rep stosd
    popa
    ret
 
page_validate:
; Проверить страницу на доступность и если она не
; доступна, то сделать ее таковой
; !!! будет работать пока ядро меньше 4 Мб, 
; !!! но загрузочный сектор пока не способен загрузить файл более 60 Кб
; !!! все расчеты проводились из расчета что размер bootor'а меньше 28 Кб
; edi
    pusha
    mov ebx, PDE_PHY
    push    edi
    shr edi, 22
    bt  dword [ebx+4*edi], 0
    jc  .next
    call    phy_page_alloc
    call    page_clear
    or  al, 7
    mov [ebx+4*edi], eax
.next:  mov ebx, [ebx+4*edi]
    and ebx, ~(4*K-1)
    pop edi
    shr edi, 12
    and edi, 1023
    bt  dword [ebx+4*edi], 0
    jc  .exit
    call    phy_page_alloc
    call    page_clear
    or  al, 7
    mov [ebx+4*edi], eax
.exit:  popa
    ret
    
LoadKernel:
; Теперь надо правильно отобразить ядро
; ebp - адрес образа файла
    mov ebp, kernel
PE_NSECTIONS    equ 4+0x02
PE_ENTRY_POINT  equ 4+0x14+0x10
PE_IMAGE_BASE   equ 4+0x14+0x1C
PE_SECTION_ALIGN    equ 4+0x14+0x20
PE_FILE_ALIGN   equ 4+0x14+0x24
 
PE_SECTION_VSIZE    equ 0x08
PE_SECTION_BASE equ 0x0C
PE_SECTION_SIZE equ 0x10
PE_SECTION_RAW  equ 0x14
PE_SECTION_FLAGS    equ 0x24
    mov ebx, [ebp+0x3C]
    add ebx, ebp    ; PE header
    lea edx, [ebx+0xF8] ; First section
    movzx   ecx, word [ebx+PE_NSECTIONS]    ; Number of sections
    test    ecx, ecx
    jz  near .error
    cld
    
.sect:  push    ecx
    ; Обработать секцию, edx -> section header
;   test    dword [edx+PE_SECTION_FLAGS], 0x60  ; Данные или код
;   jz  .next
    mov edi, [edx+PE_SECTION_BASE]
    add edi, [ebx+PE_IMAGE_BASE]
    mov esi, [edx+PE_SECTION_RAW]
    add esi, ebp
    mov ecx, [edx+PE_SECTION_SIZE]
    shr ecx, 12 ; Количество страниц по 4*Кб
    jz  .step2
    
.step1: call    page_validate; edi
    push    ecx
    mov ecx, 1024
    rep movsd
    pop ecx
    loop    .step1
    
.step2: mov ecx, [edx+PE_SECTION_SIZE]
    and ecx, 4*K-1  ; Остаток
    jz  .bss
    shr ecx, 2
    call    page_validate; edi
    rep movsd
    
.bss:   ; Подумаем о неинициализироанных секциях
    mov edi, [edx+PE_SECTION_BASE]
    add edi, [ebx+PE_IMAGE_BASE]
    mov ecx, [edx+PE_SECTION_VSIZE]
    shr ecx, 12
    jz  .bss2
    
.bss1:  call    page_validate; edi
    add edi, 4*K
    loop    .bss1
    
.bss2:  mov ecx, [edx+PE_SECTION_VSIZE]
    and ecx, 4*K-1
    jz  .next
    call    page_validate; edi
    
.next:  pop ecx
    add edx, 40
    loop    .sect
    
.ok:    
    pusha
    mov eax, mKermap
    call    print
    ;jmp    die32
    popa
    
;   call    _wait
    
    mov eax, [ebx+PE_ENTRY_POINT]
    add eax, [ebx+PE_IMAGE_BASE]
    jmp eax ; ПОЕХАЛИ !!!
    
.error: mov eax, mSecErr
    call    print
    jmp die32
; -----------------------------------------------------
vk_return   equ 13
endl    equ vk_return
%include "vga.inc"
; -----------------------------------------------------
align   4
kernel:
incbin "kernel.exe"
kernel_end:
; -----------------------------------------------------
BOOTOR_SIG dw BOOTOR_MAGIC
align   16
3
1127 / 261 / 9
Регистрация: 11.06.2010
Сообщений: 1,049
12.10.2013, 11:37
статья взята на домашней страничке Алексея Золотова http://www.zolotov.h14.ru/doc/os/get_mem.php
Определение размера физической памяти
В этой статье мы рассмотрим, как узнать размер физической памяти.

Функции BIOS
Все функции с случае ошибки устанавливают флаг CF.

int 12h
Выход:
  • ax - размер базовой памяти в килобайтах
Эта функция имеет серьезное ограничение: возвращает размер до 640 Кб

int 15h функция 88h
Вход:
  • ah = 88h
Выход:
  • ax - размер расширенной памяти в килобайтах
Эта процедура часто имеет ограничение: значение не может превышать 3C00h (15 Мб)

int 15h функция 0E801h
Вход:
  • ax = 0E801h
Выход:
  • ax - размер расширенной памяти в килобайтах до 16 Мб
  • bx - размер расширенной памяти в блоках по 64 Кб свыше 16 Мб
  • cx - размер сконфигурированный расширенной памяти в килобайтах до 16 Мб
  • dx - размер сконфигурированной расширенной памяти в блоках по 64 Кб свыше 16 Мб
Некоторые версии BIOS возвращают в ax и bx нули, так что лучше полагаться на cx и dx

int 15h функция 0E820h
Вход:
  • eax = 0E820h;
  • edx = 534D4150h ('SMAP');
  • ebx - Смещение от начала карты памяти;
  • eсx - Размер буфера;
  • es:di - Адрес буфера для размещения карты памяти;
Выход
  • eax - 534d4150h ('SMAP');
  • ebx - Следующее смещение от начала карты памяти, если = 0, то вся карта передана;
  • ecx - Количество возвращенных байт;
  • Буфер заполнен информацией;
Функция 0E820h возвращает карту памяти разбитую на блоки разных типов и допускает дыры в памяти.
Формат структуры:
C
1
2
3
4
5
6
struct
{
  uint64 base;
  uint64 length;
  uint32 type;
};
Поле type может принимать следующие значения:
  1. Доступно для использования операционной системой;
  2. Зарезервировано (например, ROM);
  3. ACPI reclaim memory (Доступно для операционной системы после прочтения таблицы ACPI;
  4. ACPI NVS memory (Операционной системе требуется сохранять эту память между NVS сессиями).
Для получения полной информации о распределении физической памяти нужно вызывать эту функцию в цикле. При первом вызове нужно установить счетчик ebx в ноль и вызывать эту функцию до тех пор, пока в ebx вновь не будет ноль. Если функция установила флаг cf или edx не содержит соответствующей сигнатуры, то функция не поддерживается.
4
Ушел с форума
Автор FAQ
 Аватар для Mikl___
16373 / 7685 / 1080
Регистрация: 11.11.2010
Сообщений: 13,759
12.10.2013, 12:30  [ТС]
Взято здесь http://www.desy.de/~tigrank/rusfaq.html#boot
Как произвести запись в BOOT-сектор
Ниже следует программа, которая записывает в BOOT-сектор часть своего тела, как отдельную программу. Только надо учесть, что не всякую программу можно туда положить. Недоступность прерываний (за редким исключением) – только одна из проблем, следует учитывать размер записи (не более 512 байт) и так далее. А вообще, если хотите поработать с BOOT-сектором, было бы неплохо учиться этому на основе изучения вирусов.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
comment *
       Пример программы, пишущей в BOOT-сектор дискеты         
Вставьте дискету, запустите программу и загрузитесь с дискеты 
*
 
.model small
.stack
.data
; 0 = Дисковод A:
; 1 = Дисковод B:
laufwerk equ 0
;----------------------------------------------------------
; Хранилище под один сектор (размер занимаемой памяти 512b)
;----------------------------------------------------------
buffer db 16h dup (?) 
fatsek dw ?                                ;Partition Table
db 2bh-18h dup (?) 
laderstart db 200h-2bh dup (?)             ;Загрузчик системы
;----------------------------------------------------------
; Программа записи
;----------------------------------------------------------
.code
start: mov ax,@data                        ;Инициализация сегмента
mov ds,ax                                  ;данных
mov dx,0                                   ;Стартовый сектор (BOOT)
mov cx,1                                   ;Число секторов для чтения: 1
mov bx,offset buffer                       ;Адрес буфера в DS:BX
mov al,laufwerk                            ;Номер дисковода(A:)
int 25h                                    ;Считать в хранилище!
 
mov cx,3                                   ;Занести 3 байта от
mov si,offset bootsektor                   ;Начала нашего "сектора"
mov di,offset buffer                       ;В буфер
aenderesprungziel:
mov al,cs:[si]                             ;Копирование байта
mov ds:[di],al                             ;CS:[si]->DS:[di]
inc di 
inc si 
loop aenderesprungziel                     ;Лупим, пока CX > 0
 
mov cx,laderoutinelaenge                   ;В CX размер данных
mov di,offset laderstart                   ;Адрес буфера кода
mov si,offset startroutine                 ;Смещение загруз. кода
aenderelader:
mov al,cs:[si]                             ;Копирование байта
mov ds:[di],al                             ;CS:[si]->DS:[di]
inc di 
inc si
loop aenderelader                          ;Лупим, пока CX > 0
 
mov dx,0                                   ;Стартовый номер сектора
mov al,laufwerk                            ;Номер дисковода
mov bx,offset buffer                       ;Адрес буфера
mov cx,1                                   ;Число секторов
int 26h                                    ;ЗАПИСАТЬ !
 
abbruch: mov ah,4Ch                        ;Возврат управления
int 21h                                    ;Опер. системе
;----------------------------------------------------------
; Вот то, что мы будем сажать
;----------------------------------------------------------
bootsektor:
jmp short startroutine                     ;Перепрыгнуть
nop                                        ;на загрузчик системы
dummy db 28h dup (?)    ;Здесь как будто сидит Partition Table, на
;самом деле нужно только для правильного формирования адреса прыжка
startroutine: cli                                        ;Запрет прерываний
mov bx,cs
mov ss, bx                                 ;SS=DS
mov sp, 7C00h                              ;Программный код
mov ds,bx                                  ;DS=CS
sti                                        ;Разрешение прерываний
mov si,7C00h+textstart                     ;В SI адрес строки
zeichen: lodsb             ;Пересылка элемента строки в AL
cmp al,'$'                                 ;Конец строки?
je textfertig                              ;Если да,то выход из цикла
mov bx,7                                   ;Если нет, то вывод
mov ah,0Eh                                 ;В AL-символ 
int 10h                                    ;Вывести его на дисплей!
jmp short zeichen                          ;Прыгаем вверх
textfertig:                                ;Холостой цикл 
jmp short textfertig                       ;Вешаем систему
; Область данных и строка для вывода
textstart equ $-bootsektor+0
db 'Hello from Tigran!$'
; Конец области для записи в BOOT-сектор
laderoutinelaenge equ $-startroutine+0
end start
3
Ушел с форума
Автор FAQ
 Аватар для Mikl___
16373 / 7685 / 1080
Регистрация: 11.11.2010
Сообщений: 13,759
12.10.2013, 13:30  [ТС]
Раз пошла такая пьянка, то до кучи и статья Крис Касперски ака мыщъх (взято отсюда http://www.insidepro.com/kk/065/065r.shtml)
MBR своими руками
Сегодня мы напишем свой менеджер мультизагрузки. Это такая штука, что сидит в загрузочном секторе и грузит любую из нескольких установленных операционных систем по нашему выбору. Статья познакомит нас с прерыванием INT 13h, таблицей разделов и кое-чем еще.
Введение
Стандартный загрузчик, устанавливаемый большинством осей по умолчанию, слишком примитивен, чтобы его воспринимать всерьез, а нестандартные загрузчики от независимых разработчиков обычно слишком неповоротливы, монструозны и ненадежны. Вот и давайте напишем свой! Пока мы будет его писать, мы познаем дао и дзен ассемблера, научимся отлаживать программы без отладчика, и попробуем низкоуровневое железо винчестера на вкус.
Начальная теоретическая подготовка
Загрузка системы начинается с того, что BIOS считывает первый сектор жесткого диска, размещает его в памяти по адресу 0000:7С00h и передает сюда управление. Программисты называют его Главным Загрузочным Сектором (Master Boot Record), или, сокращенно, MBR. В начале MBR расположен машинный код загрузчика, за ним идет Таблица Разделов (Partition Table), описывающая схему разбиения логических дисков. В конце загрузочного сектора находится сигнатура 55h AAh, говорящая BIOS'у о том, что это действительно MBR, а не что-то еще.
Загрузчик должен проанализировать Таблицу Разделов, найти предпочтительный логический диск, считать его первый сектор (он называется загрузочным - boot) и передать ему бразды правления. Вот минимум требований, предъявляемых к стандартному загрузчику, главный недостаток которого заключается в том, что на каждом логическом диске может быть установлена только одна операционная система, причем она должна быть установлена непременно на Primary Master'е, в противном случае загрузчик ее просто "не увидит" и нам придется менять порядок загрузки в BIOS Setup, а это слишком хлопотно и утомительно. Наш загрузчик будет свободен от всех этих глупых ограничений, но прежде чем зарываться вглубь, окинем MBR беглым взглядом.
Воспользовавшись любым редактором диска (например, Microsoft Disk Probe из комплекта Resource Kit, прилагаемого к лицензионной Windows), считаем первый сектор физического диска. Он должен выглядеть приблизительно так:

Рисунок 1. Внешний вид MBR - очень похоже на Матрицу, не правда ли?

Первые 1BBh байт занимают код и данные загрузчика, среди которых отчетливо выделяются текстовые строки (кстати говоря, русифицировав сообщения загрузчика, Microsoft допустила грубейшую стратегическую ошибку, ведь никакого кириллического шрифта в BIOS'е нет и русские символы выглядят бессмысленной абракадаброй).

По смещению 1BBh расположен четырехбайтовый идентификатор диска, принудительно назначаемый Windows при запуске Disk Manager'а. Коварство Microsoft не знает границ! Еще со времен первых IBM PC (тогда они назывались XT), загрузчик владел первыми 1BEh байтами MBR-сектора, и достаточно многие загрузчики (и вирусы!) использовали эти байты на всю катушку. Нетрудно сообразить, что произойдет, если внутрь загрузчика вдруг запишется идентификатор. Это убьет его! Поэтому, байты 1BBh - 1BEh лучше не трогать.

Со смещения 1BEh начинается Таблица Разделов, представляющая собой массив из четырех записей типа partition. Каждая partition описывает свой логический диск, что позволяет нам создавать до четырех разделов на каждом HDD. Динамические диски, впервые появившиеся в W2K, хранятся в Базе Менеджера Логических Дисков (Logical Disk Manager Database) и в таблице разделов присутствовать не обязаны.

В общем, устройство Главного Загрузочного Сектора выглядит так:

СмещениеРазмерНазначение
000hПеременныйКод загрузчика
1BBh4hИдентификатор диска
1BEh10hpartition 1
1CEh10hpartition 2
1DEh10hpartition 3
1EEh10hpartition 4
1FEh0x2Признак таблицы разделов, сигнатура 55h AAh
Таблица 1. Устройство MBR.

Таблица Разделов - это святая святых операционной системы. Каждая запись partition состоит из: адресов начала и конца раздела, типа раздела (NTFS, FAT16, FAT32...), количество секторов в разделе и флага "загруженности" раздела.
Все адреса задаются либо CHS (Cylinder-Head-Sector - Цилиндр-Головка-Сектор), либо LBA (Logical Block Address - Логический Адрес Блока) формате. Конкретный формат определяется типом раздела (Boot ID), записанным в 04h байте. Количество существующих типов огромно и было бы слишком утомительно перечислять их здесь. В таблице 3 приведены лишь самые популярные из них.

В CHS-формате, 01h и 05h байты partition'а хранят номер первой и последней головки раздела (см. таблицу 2). Байты 02h и 06h хранят 5 младших бит начального/конечного сектора и по два старших бита номера цилиндра, а оставшиеся биты лежат в следующем байте. Получается довольно запутанная схема, да к тому же адресующая только первые 8 Гбайт дискового пространства (CHS адрес занимает три байта или 24 бита, что при длине сектора в 512 байт дает 512 * 224 = 8.388.608 байт). Ха! Да жесткие диски преодолели этот барьер еще в прошлом веке! Это было достигнуто за счет введения LBA-адресации, последовательно нумерующей все сектора от 0 до многодетной матери. Начало раздела хранится в 32-битном поле relative offset (относительное смещение), содержащим смещение первого сектора раздела от начала partition или, попросту говоря, расстояние между концом partition и началом раздела. Конец раздела в явном нигде не хранится, вместо этого в специальном 32-битном поле partition size записывается количество секторов в разделе. Как нетрудно подсчитать, предельно допустимый размер одного раздела составляет (512 * 232 = 2.199.023.255.552 байт или 2.048 Гбайт), а совокупный объем всего диска вообще неограничен! Так что, для сегодняшних нужд LBA-адресации вполне достаточно, а там уж мы что-нибудь придумаем.

СмещениеРазм.Назначение
0001BE1CE
0011BF1CF
0021C01D0
0031C11D1
0041C21D2
0051C31D3
0061C41D4
0071C51D5
0081C61D6
00С1CA1DA
Таблица 2. Формат partition.
Boot IDТип раздела
00hРаздел свободен
0x01FAT12 (менее чем 32.680 секторов в томе или 16 Мбайт), CHS
0x04FAT16 (32.680...65.535 секторов или 16-33 Мбайт), CHS
0x05Расширенный раздел (extended partition), CHS
0x06BIGDOS FAT16 раздел (33 Мбайт - 4 Гбайт), CHS
0x07NTFS-раздел, CHS
0x0BFAT32 раздел, CHS
0x0CFAT32 раздел с поддержкой расширенной BIOS INT 13h, LBA
0x0EBIGDOS FAT16 раздел с поддержкой расширенной BIOS INT 13h, LBA
0x0FРасширенный раздел с поддержкой расширенной BIOS int 13h, LBA
0x42Динамический диск, LBA
0x86Legacy FT FAT16 раздел, CHS
0x87Legacy FT NTFS раздел, CHS
0x8BLegacy FT volume formatted with FAT32, CHS
0x8CLegacy FT volume using BIOS INT 13h extensions formatted with FAT32, LBA
Таблица 3. Возможные значения Boot ID.

Рисунок 2. Основная Таблица Разделов, разбивающая винчестер на четыре логических диска.

Четыре раздела partition обслуживают до четырех логических дисков, а больше уже никак. На большее в MBR-секторе просто не хватает места! Но ведь хорошо известно, что FDISK может разбивать винчестер хоть на 26 разделов. Как же ему это удается? А вот как! Помимо Основной Таблицы Разделом, хранящейся в MBR, мы можем создавать любое количество Расширенных Таблиц Разделов (Extended Partition Table), разбросанных по всему диску (см. рис. 3):

Рисунок 3. Несколько Расширенных Таблиц Разделов, объединенных в одну цепочку, и разбивающих винчестер на любое количество логических дисков.

Если partition имеет тип 05h или 0Fh, то она указывает совсем не на начало раздела, а на следующий MBR. Точнее, не совсем MBR, но нечто очень на него похожее. В нем присутствует полноценная Таблица Разделов с четырьмя входами: partition 1, partition 2, partition 3 и partition 4, каждая из которых указывает либо на логический диск, либо на новый MBR. Длина такой цепочки практически неограниченна и может превышать 26. Однако назначить буквы всем последующим разделам уже не удаться и под Windows 9x они будут просто не видны. Windows NT поддерживает гибридный механизм наименования разделов - по буквам и по именам, поэтому ей эти ограничения не страшны.

Стандартный загрузчик позволяет запускать системы только из Основной Таблицы Разделов. Цепочку MBR'ов он не анализирует. В своем загрузчике мы исправим этот недостаток.

Рисунок 4. Структурная схема типичной Расширенной Таблицы Разделов.

Интерфейс INT 13h

Управлять дисками можно как через порты ввода/вывода, так и через BIOS. Порты намного более могущественны и интересны, однако BIOS программируется намного проще, к тому же она поддерживает большое количество разнокалиберных накопителей, абстрагируя нас от конструктивных особенностей каждой конкретной модели. Поэтому мы будем действовать через нее, а точнее через интерфейс прерывания INT 13h.

Попробуем прочитать сектор с диска в CHS-mode. Естественно, действовать нужно из самого MBR или из "голой" MS-DOS, иначе у нас ничего не получится, ведь Windows NT блокирует прямой доступ к диску даже из режима "Эмуляции MS-DOS"!

Номер функции заносится в регистр AH. В случае чтения он равен двум. Регистр AL отвечает за количество обрабатываемых секторов. Поскольку мы собираемся читать по одному сектору за раз, занесем сюда единицу. Регистр DH хранит номер головки, а DL - номер привода (80h - первый жесткий диск, 81h - второй и так далее). Пять младших битов регистра CL задают номер сектора, оставшиеся биты регистра CL и восемь битов регистра CH определяют номер цилиндра, который мы хотим прочитать. Регистровая пара ES:BX указывает на адрес буфера-приемника. Вот, собственно говоря, и все. После выполнения команды INT 13h считываемые данные окажутся в буфере, а если произойдет ошибка (например, головка споткнется о BAD-сектор) BIOS установит флаг переноса (carry flag) и мы будем вынуждены либо повторить попытку, либо вывести грустное сообщение на экран.

На ассемблерном языке это звучит так:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
MOV SI, 1BEh                    ; на первый partition
MOV AX, CS                      ; настраиваем ES
MOV ES, AX
MOV BX, buf                     ; смещение буфера
...
read_all_partitions:
        MOV AX, 0201h           ; читать 1 сектор с диска
        MOV DL, 80h             ; читать с первого диска
        MOV DH, [SI+1]          ; стартовый номер головки
        MOV CX, [SI+2]          ; стартовый сектор с цилиндром
        INT 13h
        JC error                ; ошибка чтения
 
        ; обрабатываем считанный boot-сектор или extended partitions
        ; ==========================================================
        ;
        CMP byte [SI], 80h
        JZ LOAD_BOOT            ; это загрузочный раздел
                                ; передаем на него управление
 
        CMP byte [SI+4], 05h
        JZ LOAD_CHS_EXT         ; это Расширенная Таблица Разделов в CHS-формате
 
        CMP byte [SI+4], 0Fh
        JZ LOAD_LBA_EXT         ; это Расширенная Таблица Разделов в LBA-формате
 
        ADD SI, 10h             ; переходим на следующую partition
        CMP SI, 1EEh
        JNA read_all_partitions ; читаем все партиции одну за другой
...
buf rb 512                      ; буфер на 512 байт
Листинг 1. Код, считывающий загрузочный сектор или Расширенную Таблицу Разделов.
Запись сектора в CHS-режиме происходит практически точно также, только регистр AH равен не 02h, а 03h. С LBA-режимом разобраться намного сложнее, но мы, как настоящие хакеры, его обязательно осилим. Вот только пива хлебнем.

Чтение сектора осуществляется функцией 42h (AH = 42h). В регистр DL, как и прежде, заносится номер привода, а вот регистровая пара DS:SI указывает на адресный пакет (disk address packet), представляющий собой продвинутую структуру следующего формата:

СмещениеТипНазначение
00hBYTEРазмер пакета 10h или 18h
01hBYTEЗарезервировано и должно быть равно нулю
02hWORDСколько секторов читать
04hDWORD32-разрядный адрес буфера-приемника в формате seg:offs
08hQWORDСтартовый номер сектора для чтения
10hQWORD64-разряный плоский адрес буфера приемника (используется, только если 32-разряный адрес равен FFFF:FFFF)
Таблица 4. Адресный пакет, используемый для чтения/записи секторов в режиме LBA.

Код, читающий сектор в LBA-режиме, в общем случае выглядит так:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
MOV DI, 1BEh                 ; на первый partition
MOV AX, CS                   ; настраиваем...
MOV buf_seg                  ;                 ...сегмент
MOV EAX, [DI+08h]            ; смещение partition относительно начала раздела
ADD EAX, EDI                 ; EDI должен содержать номер сектора текущего MBR
MOV [X_SEC]
...
read_all_partitions:
        MOV AH, 42h          ; читать сектор в LBA-режиме
        MOV DL, 80h          ; читать с первого диска
        MOV SI, dap          ; смещение адресного пакета
        INT 13h
        JC error             ; ошибка чтения
 
...
dap:
packet_size    db 10h        ; размер пакета 10h байт
reserved       db 00h        ; заначка для будущих расширений
N_SEC          dw 01h        ; читаем один сектор
buf_seg        dw 00h        ; сюда будет занесен сегмент буфера-приемника
buf_off        dw buf        ; смещение буфера-приемника
X_SEC          dd 0          ; сюда будет занесен номер сектора для чтения
               dd 0          ; реально неиспользуемый хвост 64-битного адреса
 
buf rb 512                   ; буфер на 512 байт
Листинг 2. Чтение сектора с диска в LBA-режиме.
Запись осуществляется аналогично, только регистр AH содержит не 42h, а 43h. Регистр AL определяет режим: если бит 0 равен 1, BIOS выполняет не запись, а ее эмуляцию. Бит 2, будучи взведенным, задействует запись с проверкой. Если AL равен 0, выполняется обыкновенная запись по умолчанию.

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

Как программируют загрузчики
Лучше всего загрузчики программируются на FASM. С точки зрения ассемблера загрузчик представляет собой обыкновенный двоичный файл, предельно допустимый объем которого составляет 1BBh (443) байт. Немного? Но не будет спешить с выводами. Всякий раздел всегда начинается с начала цилиндра, а это значит, что между концом MBR и началом раздела имеется, по меньшей мере, sector per track свободных секторов. Практически все современные винчестеры имеют по 64 секторов в треке, что дает нам: 443 + 63 * 512 = 32.699 байт или ~32 Кбайт. Да в этот объем даже графический интерфейс с мышью и голой красавицей на обоях уместить можно. Но мы не будем! Настоящие хакеры работают в текстовом режиме с командной строкой, а красавиц лучше иметь, чем смотреть.

Как уже говорилось, BIOS загружает MBR по адресу 7C00h, поэтому в начале ассемблерного кода должна стоять директива ORG 7C00h, а еще USE16 - ведь загрузчик выполняется в 16-разрядном реальном режиме. Позже, при желании он может перейти в защищенный режим, но это будет уже потом. Не будет лезть в такие дебри.

Обнаружив загрузочный раздел (а обнаружить это можно по флагу 80h, находящемуся по смещению от начала partition), загрузчик должен считать первый сектор этого раздела, разместив его в памяти по адресу 0000:7C000h, то есть аккурат поверх своего тела. А вот это уже нехорошо! И чтобы не вызвать крах системы, загрузчик должен заблаговременно перенести свою тушу в другое место, что обычно осуществляется командой MOVSB. Копироваться можно в любое место памяти - от 0080:0067h до 9FE00h. Память, расположенную ниже 0080:0067h лучше не трогать, т.к. здесь находятся вектора прерываний и системные переменные BIOS'а, а от A000h и выше начинается область отображения ПЗУ, так что предельно доступный адрес равен A000h - 200h (размер сектора) = 9FE00h.

Что еще? Ах да! Трогать DL-регистр ни в коем случае нельзя, поскольку в нем передается номер загрузочного привода. Некоторые загрузчики содержат ошибку, всегда загружаясь с первого жесткого диска. Стыдно не знать, что BIOS уже лет десять как позволяют менять порядок загрузки и потому загрузочным может быть любой привод.

Кстати говоря, FASM - единственный известный мне ассемблер, "переваривающий" команду дальнего вызова JMP 0000:7C000h напрямую. Все остальные ассемблеры заставляют извращаться приблизительно так: PUSH offset_of_target/PUSH segment_of_target/RETF. Здесь мы заталкиваем в стек сегмент и смещение целевого адреса и выполняем далекий RETF, переносящий нас на нужное место. Еще можно воспользоваться самомодифицирующимся кодом, собрав команду JMP FAR "вручную" или просто расположить целевой адрес в одном сегменте с исходным адресом (например, 0000:7C000h -> 0000:7E000h), но это все муторно и утомительно.

В общем, скелет нашего загрузчика будет выглядеть так:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
use16
ORG 7С00h
CLD                    ; копируем слева направо (в сторону увеличения адресов)
MOV SI,7C00h           ; откуда копировать
MOV DI,7E00h           ; куда копировать
MOV CX,200h            ; длина сектора
REP MOVSB              ; копируем
 
; выбираем раздел, который мы хотим загрузить,
; считываем его в память по адресу 0000:7C000h
; см. листинги 1, 2
 
JMP 0000:7C000h        ; передаем управление на boot-сектор
Листинг 3. Скелет простейшего загрузчика на FASM'е.

Инсталляция нашего загрузчика в MBR

Под старушкой MS-DOS записать свой загрузчик в MBR было просто - достаточно дернуть прерывание INT 13h, функцию 03h (запись сектора). Но под Windows NT этот прием уже не работает и приходится прибегать к услугам функции CreateFile. Если вместо имени открываемого фала указать название устройства, например, "\\.\PHYSICALDRIVE0" (первый физический диск), мы сможем свободно читать и записывать его сектора вызовами ReadFile и WriteFile, соответственно. При этом флаг dwCreationDisposition должен быть установлен в значение OPEN_EXISTING, а dwShareMode - в значение FILE_SHARE_WRITE. Еще потребуются права root'а или в терминологии Windows - администратора, иначе ничего не получится.

Законченный пример вызова CreateFile выглядит так:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
XOR EAX,EAX
PUSH EAX                                    ; hTemplateFile
PUSH dword FILE_ATTRIBUTE_NORMAL            ; dwFlagsAndAttributes
PUSH dword OPEN_EXISTING                    ; dwCreationDisposition
PUSH EAX                                    ; lpSecurityAttributes
PUSH dword FILE_SHARE_WRITE                 ; dwShareMode
PUSH dword (GENERIC_WRITE OR GENERIC_READ)  ; dwDesiredAccess
PUSH DEVICE_NAME                            ; имя устройства
CALL CreateFile                             ; открываем устройство
INC EAX
TEST EAX,EAX
JZ error
DEC EAX
...
DEVICE_NAME DB "\\.\PHYSICALDRIVE0",0
BUF RB 512                                  ; буфер
Листинг 4. Открытие непосредственного доступа к жесткому диску под Windows NT.

Открыв физический диск и убедившись в успешности этой операции, мы должны прочитать оригинальный MBR-сектор в буфер, перезаписать первые 1BBh байт, ни в коем случае не трогая Таблицу Разделов и сигнатуру 55h AAh (мы ведь не хотим, чтобы диск перестал загружаться, верно?). Остается записать обновленный MBR на место и закрыть дескриптор устройства. Все! После перезагрузки все изменения вступят в силу, а может быть и не вступят... Загрузчик жестоко мстит за малейшие ошибки проектирования и чтобы не потерять содержимое своих разделов, для начала лучше попрактиковаться на VM Ware или любом другом эмуляторе PC.

Под Windows 9x, кстати говоря, трюк с CreateFile не работает. Но там можно воспользоваться симуляцией прерываний из DMPI или обратится к ASPI-драйверу. Оба способа подробно описаны в моей книге "Техника защиты компакт-дисков от копирования". И хотя в ней речь идет о CD, а не о HDD, жесткие диски программируются аналогичным способом.
Отладка загрузчика
Отлаживать код загрузчиков невероятно трудно. Загрузчик получает управление задолго до запуска операционной системы, когда никакие отладчики еще не работают. Несколько лет назад это представляло огромную проблему и при разработке навороченных загрузчиков приходилось либо встраивать в них интегрированный мини-отладчик, либо выискивать ошибки руками, головой и карандашом. С появлением эмуляторов все изменилось. Достаточно запустить BOCHS и отлаживать загрузчик, как и любую другую программу!

Рисунок 5. Внешний вид эмулятора BOCHS, отлаживающего загрузочный сектор.

Заключение
Программирование загрузчиков - одна из тех немногих областей, в которых применение ассемблера действительно оправдано. Языки высокого уровня для этого слишком абстрагированы от оборудования и недостаточно гибки. Вот почему хакеры так любят возиться с загрузчиками, добавляя сюда множество новых фич - таких, например, как автоматическая загрузка с CD-ROM или SCSI-винтов, противодействие вирусам, парольная защита с шифрованием данных и т.д. Здесь действительно есть, где развернуться и показать себя. Но читать о новых идеях скучно и неинтересно. Намного приятнее генерировать их самостоятельно. Так чего же мы сидим?!
Интересные ссылки
  • MBR and OS Boot Records: Масса интересного материала по MBR (на английском языке): http://thestarman.narod.ru/asm... etail.htm;
  • BOCHS: Отличный эмулятор со встроенным отладчиком, значительно облегчающий процесс "пуско-наладки" загрузочных секторов, бесплатен, распространяется с исходными текстами: http://bochs.sourceforge.net;
  • www.koders.com: Отличный поисковик, нацеленный на поиск исходных кодов, по ключевому слову "MBR" выдает огромное количество загрузчиков на любой вкус;
  • Ralf Brown Interrupt List: Знаменитый Interrupt List Ральфа Брауна, описывающий все прерывания, включая недокументированные (на английском языке): http://www.pobox.com/~ralf;
  • OpenBIOS: Проект "Открытого BIOS", распространяемого в исходных текстах; помогает понять некоторые неочевидные моменты обработки системного загрузчика: http://www.openbios.info/docs/index.html;
Миниатюры
Пишем загрузочный сектор   Пишем загрузочный сектор   Пишем загрузочный сектор  

Пишем загрузочный сектор   Пишем загрузочный сектор  
3
 Аватар для Naydli
217 / 53 / 4
Регистрация: 03.08.2013
Сообщений: 278
12.10.2013, 16:53
Интересно было бы знать,как подобное реализовать на CD-R или CD-RW диске? на дискетах мозгодолбательства с файловой системой обычно не возникает.вот не удается мне записать на сидюк бинарник в сыром виде без всяких файловых систем. хотя образ диска вполне спокойно запускается в Virtualbox.монтирую в ultraISO.
1
 Аватар для Naydli
217 / 53 / 4
Регистрация: 03.08.2013
Сообщений: 278
12.10.2013, 23:32
Хм,вышло в общем одно интересное явление.Мой загрузчик все же загрузился с CD-R диска.С CD-RW этот фокус почему-то не удался.Не смотря на это, диск стал читабельным,что безусловно радует.Выходит,с CD-дисками работать даже легче,чем с дискетами.
0
780 / 412 / 75
Регистрация: 29.03.2013
Сообщений: 853
14.10.2013, 17:46
Нетрудно сообразить, что произойдет, если внутрь загрузчика вдруг запишется идентификатор. Это убьет его! Поэтому, байты 1BBh - 1BEh лучше не трогать.
Страшная-страшная страшилка от Криса...но не работает, совсем. Ну не трогает микрософт "нестандартные" загрузчики, по крайней мере мои
1
Просто Лис
Эксперт Python
 Аватар для Рыжий Лис
5973 / 3735 / 1099
Регистрация: 17.05.2012
Сообщений: 10,791
Записей в блоге: 9
16.11.2013, 11:54
Немного нубских вопросов про загрузочный сектор:
Assembler
1
BPB_BytsPerSec  dw 0x200    ; Байт на сектор
так создается переменная?
И дальше программа обращается к этой переменной? Например, здесь:
Assembler
1
mov bx, [byte bp+BPB_BytsPerSec]
Что случится, если значение этой переменной изменить извне? (Например, HEX редактором в скомпилированном бинарнике)
Программа будет нормально работать используя новое значение переменной или не будет работать?
0
85 / 61 / 29
Регистрация: 15.05.2013
Сообщений: 189
05.12.2013, 10:57
Линуксовый fdisk записывает идентификатор по смещению 1B8h. Или это я что-то путаю?

Добавлено через 7 минут
ps --

Disk identifier: 0x3e15e148

0001b0 00 00 00 00 00 00 00 00 48 e1 15 3e 00 00 00 00

>>fdisk.c<<
static void
dos_write_mbr_id(unsigned char *b, unsigned int id) {
store4_little_endian(&b[440], id);
}
0
Ушел с форума
Автор FAQ
 Аватар для Mikl___
16373 / 7685 / 1080
Регистрация: 11.11.2010
Сообщений: 13,759
06.12.2013, 08:41  [ТС]
Чтобы не было обвинений в плагиате предупреждаю сразу, всё нижеследующее взято на сайте asmdev.narod.ru, автор Андрей Валяев <dron@infosec.ru>, материалы были в форме рассылки, поэтому подвергнуты минимальной литобработке.
Создание операционной системы на ассемблере
В этой работе будут использоваться:
  • Многоплатформенный ассемблер nasm (есть версии для UNIX, DOS и Windows), поддерживающий команды практически всех современных процессоров и многообразием понимаемых форматов.
  • любой ANSI C компилятор.
Глава #1



В этой главе вы не увидите исходных текстов готовых программ, это все еще только предстоит написать при вашем активном участии.
Начнем с написания ядра. Ядро будет ориентированно на UNIX-подобные операционные системы. Для простоты с самого начала будем стремиться к совместимости с существующими системами.
Задача состоит в следующем:
Сделать, по возможности, компактное, надежное и быстрое ядро, с максимальным эффектом используя возможности процессора. Писать будем в основном на Ассемблере.

Для начала разберемся, как устроены системы
.


Ядро состоит из следующих компонентов:
  1. "Собственно ядро"
  2. Драйвера устройств
  3. Системные вызовы
В зависимости от организации внутренних взаимодействий, ядра подразделяются на "микроядра" (microkernel) и монолитные ядра.
Системы с "микроядром" строятся по модульному принципу, имеют обособленное ядро, и механизм взаимодействия между драйверами устройств и процессами. По такому принципу строятся системы реального времени. Примерно так сделан QNX или HURD.
Монолитное ядро имеет более жесткую внутреннюю структуру. Все установленные драйвера жестко связываются между собой, обычно прямыми вызовами. По таким принципам строятся обыкновенные операционные системы типа Linux, FreeBSD.
Естественно, не все так четко, идеального монолитного или "микроядра" нет, наверное, ни в одной системе, просто системы приближаются к тому или иному типу ядра.
Очень хотелось бы, чтобы то, что делаем, больше походило на первый тип ядер.


Немного углубляемся в аппаратные возможности компьютеров
.


Один, отдельно взятый, процессор, в один момент времени, может исполнять только одну программу. Но к компьютерам предъявляются более широкие требования. Мало кто, в настоящее время, удовлетворился однозадачной операционной системой (к каким относился DOS, например). В связи с этим разработчики процессоров предусмотрели мультизадачные возможности.
Возможность эта заключается в том, что процессор выполняет какую-то одну программу (их еще называют процессами или задачами). Затем, по истечении некоторого времени (обычно это время меряется микросекундами), операционная система переключает процессор на другую программу. При этом все регистры текущей программы сохраняются. Это необходимо для того, чтобы через некоторое время вновь передать управление этой программе. Программа при этом не замечает каких либо изменений, для нее процесс переключения остается незаметен.
Для того чтобы программа не могла, каким либо образом, нарушить работоспособность системы или других программ, разработчики процессоров предусмотрели механизмы защиты.
Процессор предоставляет 4 "кольца защиты" (уровня привилегий), можно было бы использовать все, но это связано со сложностями взаимодействия программ разного уровня защиты. Поэтому в большинстве существующих систем используют два уровня. 0 - привилегированный уровень (ядро) и 3 - непривилегированный (пользовательские программы).
Всем этим обеспечивается надежное функционирование системы и независимость программ друг от друга.

Теперь немного поподробнее про устройство ядра.


На "Собственно ядро" возлагаются функции менеджера памяти и процессов. Переключение процессов - это основной момент нормального функционирования системы. Драйвера не должны "тормозить", а тем более блокировать работу ядра. Windows - наглядный пример того, что этого нельзя допустить!
Теперь о драйверах. Драйвера - это специальные программы, обеспечивающие работу устройств компьютера. В существующих системах (во FreeBSD это точно есть, про Linux не уверен) предусматриваются механизмы прерывания работы драйверов по истечении какого-то времени. Правда, все зависит от того, как написан драйвер. Можно написать драйвер под FreeBSD или Linux, который полностью блокирует работу системы.
Избежать этого при двухуровневой защите не представляется возможным, поэтому драйвера надо будет тщательно программировать. В нашей работе драйверам уделим очень много внимания, поскольку от этого в основном зависит общая производительность системы.
Системные вызовы - это интерфейс между процессами и ядром (читайте-железом). Никаких других методов взаимодействия процессов с устройствами компьютера быть не должно. Системных вызовов достаточно много, на Linux их 190, на FreeBSD их порядка 350, причем большей частью они совпадают, соответствуя стандарту POSIX (стандарт, описывающий системные вызовы в UNIX). Разница заключается в передаче параметров, что легко будет предусмотреть. Естественно, нельзя сделать ядро, работающее одновременно на Linux и на FreeBSD, но по отдельности совместимость вполне реализуема.
Прикладным программам абсолютно безразлично, как системные вызовы реализуются в ядре. Это облегчает для нас обеспечение совместимости с существующими системами.
В следующей главе поговорим о защищенном режиме процессора, распределении памяти, менеджере задач и рассмотрим, как это сделано в существующих системах.

Вопросы
  • Какой бы вы хотели видеть СВОЮ систему?
  • На какую систему она должна походить?
  • Сколько места на винчестере занимать?
  • Сколько памяти требовать для работы?
Глава #2



В этой главе поговорим об архитектуре современных процессоров и о предоставляемых средствах защиты. Понимание этого будет необходимо нам, когда перейдем непосредственно к программированию операционной системы.

Как процессор работает с памятью?


Для начала небольшое предисловие.
В процессорах имеются базовые регистры, которые могут задавать смещение. На 16-битной архитектуре максимальное смещение могло быть до 64 килобайт, что, в общем-то, не много и вызывало определенные трудности (разные модели памяти, разные форматы файлов). Так же, в 16-битной архитектуре присутствовали сегментные регистры, которые указывали адрес сегмента в памяти. В процессорах, начиная с i386, базовые регистры стали 32-х битными, что позволяет адресовать до 4 гигабайт. Сегментные регистры остались 16-битными, и в защищенном режиме они не содержат адреса! они содержат индекс дескриптора. В реальном режиме сегментные регистры работают так же, как и на 16-битных процессорах.
В реальном режиме сегментные регистры непосредственно указывают на адрес начала сегмента в памяти. Это позволяет нам, без каких либо преград, адресовать 1 мегабайт памяти. Но создает определенные трудности для защиты. А защищать нужно многое. Например, не можем пользовательским программам дать возможность непосредственно обращаться к коду или данным ядра. Так же нельзя давать возможность пользовательским программам обращаться к коду или данным других пользовательских программ, поскольку это может нарушить их работоспособность.
Для этого был изобретен защищенный режим работы процессора, который появился в процессорах i286.
Защищенность этого режима заключается в следующем:
Сегментный регистр больше не указывает на адрес в памяти. В этом регистре теперь задается индекс в таблице дескрипторов.
Таблица дескрипторов может быть глобальная или локальная (применяется в многозадачных системах для изоляции адресного пространства задач) и представляет собой массив записей, по 8 байт в каждой, где описываются адреса, пределы и права доступа к сегментам.
Про адрес ничего не буду говорить, и так все ясно. Что такое предел? В этом Поле описывается размер сегмента. При обращении за пределы сегмента процессор генерирует исключение (специальное прерывание защищенного режима). Так же исключение генерируется в случае нарушения прав доступа к сегменту. Поле прав доступа описывает возможность чтения/записи сегмента, возможность выполнения кода сегмента, уровень привилегий для доступа к сегменту.
При обращении к сегменту из дескриптора берется базовый адрес сегмента и складывается со смещением сегмента. Так получается линейный 32-х разрядный (в i286 - 24-х разрядный) адрес. Для i286 на этом процесс получения адреса завершается, линейный адрес там равен физическому. Для i386 или выше это справедливо не всегда.

Страничная организация памяти.


В процессорах, начиная с i386, появилась, так называемая, страничная организация памяти. Страница имеет размер 4 килобайта или 4 мегабайта. Большие страницы могут быть только в pentium или выше. Не знаю только, какой толк от таких страниц.
Если возможность страничной адресации не используется, то линейный адрес, как и на i286, равен физическому. Если используется - то линейный адрес разбивается на три части. Первая, 10-битная, часть адреса является индексом в каталоге страниц, который адресуется системным регистром CR3. Запись в каталоге страниц указывает адрес таблицы страниц. Вторая, 10-битная, часть адреса является индексом в таблице страниц. Запись в таблице страниц указывает физический адрес нахождения страницы в памяти. последние 12 бит адреса указывают смещение в этой странице.
В страничных записях, как и в дескрипторных записях, есть служебные биты, описывающие права доступа, и некоторые другие тонкости страниц. Одной из важных тонкостей является бит присутствия страницы в памяти. В случае не присутствия страницы, процессор генерирует исключение, в котором можно считать данную страницу из файла или из swap раздела. Это сильно облегчает реализацию виртуальной памяти. Более подробно про все это можно прочитать в книгах по архитектуре процессоров. Вернемся к операционным системам.


Многозадачность.


Многозадачные возможности в процессорах так же появились в процессорах, начиная с i286. Для реализации этого, процессор для каждой задачи использует, так называемый, "сегмент состояния задачи" ("Task State Segment", сокращенно TSS). В этом сегменте, при переключении задач, сохраняются все базовые регистры процессора, сегменты и указатели стека для трех уровней защиты (для каждого уровня используется свой стек), сегментный адрес локальной таблицы дескрипторов ("Local descriptor table", сокращенно LDT). В процессорах, начиная с i386, там еще хранится адрес каталога страниц (регистр CR3). Так же этот сегмент обеспечивает некоторые другие механизмы защиты, но о них пока не будем говорить.
Операционная система может расширить TSS, и использовать его для хранения регистров и состояния сопроцессора. Процессор при переключении задач не сохраняет этого. Так же возможны другие применения.

Что из всего этого следует?


Не будем ориентироваться на процессор i286, поскольку 16-битная архитектура и отсутствие механизма страничного преобразования сильно усложняет программирование операционной системы. К тому же, таких процессоров давно уже никто не использует.
Ориентируемся на i386 или более старшие модели процессоров, вплоть до последних.
Ядро системы при распределении памяти оперирует 4-х килобайтными страницами.
Страницы могут использоваться самим ядром, для нужд драйверов (кэширование, например), или для процессов.
Программа или процесс состоит из следующих частей:
  • Сегмент кода. Может только выполняться, сама программа его не прочитать, не переписать не может! Использовать для этого сегмента swap не нужно, при необходимости код считывается прямо из файла;
  • Сегмент данных состоит из трех частей:
    • Константные данные, их тоже можно загружать из файла, так как они не меняются при работе программы;
    • Инициализированные данные. Участвует в процессе свопинга;
    • Не инициализированные данные. Так же участвует в свопинге;
  • Сегмент стека. Так же участвует в свопинге.
Но, обычно, системы делят сегмент данных на две части: инициализированные данные и не инициализированные данные.
Все сегменты разбиваются на страницы. Сегмент кода имеет постоянный размер. Сегмент данных может увеличиваться в сторону больших адресов. Сегмент стека, поскольку растет вниз, увеличивается в сторону уменьшения адресов. Страницы памяти для дополнительных данных или стека выделяются системой по мере необходимости.
Очень интересный момент
При выполнении программы операционная система делает следующие действия:
  • Готовит для программы локальную таблицу дескрипторов;
  • Готовит для программы каталог страниц, все страницы помечаются как не присутствующие в памяти.
При передаче управления этой программе процессор генерирует исключение по отсутствию страницы, в котором нужная страница загружается из файла или инициализируется.
Еще один интересный момент
Когда в системе загружается две или более одинаковых программы - нет необходимости для каждой из них выделять место для кодового сегмента, они спокойно могут использовать один код на всех.

Глава #3


В этой главе поговорим, о порядке загрузки операционных систем.

Процесс загрузки, естественно, начинается с BIOS.
При старте процессор находится в реальном режиме, следовательно больше одного мегабайта памяти адресовать не может. Но это и не обязательно.
BIOS проверяет устройства, с которых может производиться загрузка. Порядок проверки в современных BIOS устанавливается. В список устройств могут входить Floppy disk, IDE disk, CDROM, SCSI disk...
Вне зависимости от типа устройства суть загрузки одна...
На устройстве обнаруживается boot sector. Для CDROM это не совсем справедливо, но про них пока не будем говорить. BootSector загружается в память по адресу 0:7с00. Дальнейшее поведение BootSector'а зависит от системы.

Загрузка Linux.


Для Linux свойственно два способа загрузки:
  • Загрузка через boot sector ядра;
  • Загрузка через boot manager LILO (Linux Loader);
Процесс загрузки через ядро используется обычно на Floppy дисках и происходит в следующем порядке:
  1. boot sector переписывает свой код по адресу 9000h:0;
  2. Загружает с диска Setup, который записан в нескольких последующих секторах, по адресу: 9000h:0200h;
  3. Загружает ядро по адресу 1000h:0. Ядро так же следует в последующих секторах за Setup. Ядро не может быть больше чем 508 килобайт, но так как оно, чаще всего, архивируется - это не страшно;
  4. Запускается Setup;
  5. Проверяется корректность Setup;
  6. Производится проверка оборудования средствами BIOS. Определяется размер памяти, инициализируется клавиатура и видеосистема, наличие жестких дисков, наличие шины MCA (Micro channel bus), PC/2 mouse, APM BIOS (Advanced power management);
  7. Производится переход в защищенный режим;
  8. Управление передается по адресу 1000h:0 на ядро;
  9. Если ядро архивировано, оно разархивируется. иначе просто переписывается по адресу 100000h (за пределы первого мегабайта);
  10. Управление передается по этому адресу;
  11. Активируется страничная адресация;
  12. Инициализируются idt и gdt, при этом в кодовый сегмент и в сегмент данных ядра входит вся виртуальная память;
  13. Инициализируются драйвера;
  14. Управление передается неуничтожимому процессу init;
  15. init запускает все остальные необходимые программы в соответствии с файлами конфигурации;
В случае загрузки через LILO:
  1. boot sector LILO переписывает свой код по адресу 9a00h:0;
  2. До адреса 9b00h:0 размещает свой стек;
  3. Загружает вторичный загрузчик по адресу 9b00h:0 и передает ему управление;
  4. Вторичный загрузчик загружает boot sector ядра по адресу 9000h:0;
  5. Загружает Setup по адресу 9000h:0200h;
  6. Загружает ядро по адресу 1000h:0;
  7. Управление передается программе Setup. Зачем загружает boot sector из ядра? не понятно;
В Linux есть такое понятие как "big kernel". Такой kernel сразу загружается по адресу 100000h.


Загрузка FreeBSD.


Принципиальных отличий для FreeBSD, конечно, нет. основное отличие состоит в том, что ядро, как и модули ядра являются перемещаемыми и могут быть загружены или выгружены в процессе загрузки системы.
Порядок загрузки примерно следующий:
  1. BootSector загружает вторичный загрузчик;
  2. Вторичный загрузчик переводит систему в защищенный режим и запускает loader;
  3. loader предоставляет пользователю возможность выбрать необходимые модули или запустить другое ядро;
  4. После чего управление передается ядру и начинается инициализация драйверов;


Давайте по порядку рассмотрим, как грузятся системы от Microsoft.

Загрузка DOS.


boot sector DOS загружает в память два файла: io.sys и msdos.sys. Названия этих файлов в разных версиях DOS различались, не важно. Файл io.sys содержит в себе функции прерывания int 21h, файл msdos.sys обрабатывает config.sys, и запускает командный интерпретатор command.com, который в свою очередь обрабатывает командный файл autoexec.bat.


Загрузка Windows 9x.


Отличие от DOS заключается в том, что функции msdos.sys взял на себя io.sys. msdos.sys остался ради совместимости как конфигурационный файл. После того как командный интерпретатор command.com обрабатывает autoexec.bat вызывается программа win.com, которая осуществляет перевод системы в защищенный режим, и запускает различные другие программы, обеспечивающие работу системы.


Загрузка Windows NT.


boot sector NT - зависти от формата FS, для FAT устанавливается один, для NTFS - другой, в нем содержиться код чтения FS, без обработки подкаталогов.
  1. boot sector загружает NTLDR из корневой директории, который запускается в real mode;
  2. NTLDR певодит систему в защищенный режим;
  3. Создаются необходимые таблицы страниц для доступа к первому мегабайту памяти;
  4. Активируется механизм страничного преобразования;
  5. Далее NTLDR читает файл boot.ini, для этого он использует встроенный read only код FS. В отличии от кода бутсектора он может читать подкаталоги;
  6. На экране выводится меню выбора вида загрузки;
  7. После выбора, или по истечении таймаута, NTLDR из файла boot.ini определяет нахождение системной директории Windows, она может находиться в другом разделе, но обязательно должна быть корневой;
  8. Если в boot.ini указана загрузка DOS (или Win9x), то файл bootsect.dos загружается в память и выполняется горячая перезагрузка;
  9. Далее обрабатывается boot.ini;
  10. Загружается ntdetect.com, который выводит сообщение "NTDETECT V4.0 Checking Hardware", и детектит различные устройства... Вся информация собирается во внешней структуре данных, которая в дальнейшем становиться ключем реестра "HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION ";
  11. NTLDR выводит сообщение "OSLOADER V4.0";
  12. Из директории winnt\system32 загружается ntoskrnl.exe, содержащий в себе ядро и подсистемы выполнения (менеджер памяти, кэш менеджер, менеджер объектов), и файл hal.dll, который содержит в себе интерфейс с аппаратным обеспечением;
  13. Далее NTLDR предоставляет возможность выбрать "последние известные хорошие" конфигурации. В зависимости от выбора выбираются копии реестра используемые для запуска;
  14. Загружает все драйвера и другие необходимые для загрузки файлы;
  15. В завершение он запускает функцию main из ntoskrnl.exe и завершает свою работу;
Не могу гарантировать полную достоверность представленной информации, NT я знаю плохо, тем более не знаю что у нее внутри. Так же не могу что-либо более конкретного сказать про распределение памяти в процессе загрузки Windows NT. некоторые неточности могут быть связаны с моим плохим знанием английского, желающие могут посмотреть на оригинал по адресу: Inside the Boot Process, Part 1

Узнали как загружаются системы? В своей системе не будем слепо следовать какому либо из представленных здесь путей. Ради совместимости обеспечим формат ядра, аналогичный Linux. В этой системе все сделано достаточно понятно и просто. Ориентируемся на Linux.
А в следующей главе поговорим о распределении памяти в системе и начнем писать свой boot sector.
Глава #4


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

boot sector загружается в память по адресу 0:7c00h и имеет длину 512 байт. Это не слишком много, поэтому возможности boot sector'a ограничиваются загрузкой какого либо вторичного загрузчика.

Наш boot sector, по образу и подобию linux, будет загружать в память два блока. Первым является тот самый вторичный загрузчик, у нас он, как и в linux, называется setup. Вторым является собственно ядро.

Этот boot sector служит для загрузки ядра с дискет, поэтому, на первых порах, он жестко привязан к диску "a:".

BIOS предоставляет возможность читать по нескольку секторов сразу, но не более чем до границы дорожки. Такая возможность, конечно, ускоряет чтение с диска, но представляет собой большие сложности в программировании, так как надо учитывать границы сегментов (в реальном режиме сегмент может быть не больше, чем 64к) и границы дорожек, получается достаточно хитрый алгоритм.

Пойдем немного другим путем. Читаем с диска по секторам. Это, конечно, медленнее, но здесь скорость не очень критична. За то это гораздо проще и компактнее реализуется.

А теперь давайте разбираться, как это все работает.
Assembler
1
2
3
4
5
%define SETUP_SEG 0x07e0
%define SETUP_SECTS 10
 
%define KERNEL_SEG      0x1000
%define KERNEL_SECTS 1000
Для начала описываем место и размер для каждого загружаемого блока.
Размеры пока произвольные, поскольку все остальное еще предстоит написать.
Assembler
1
2
3
4
section .text
        BITS    16
 
        org     0x7c00
boot sector загружается и запускается по адресу 0:7c00h Содержимое регистров при старте таково:
  • cs содержит 0
  • ip содержит 7с00h
Прерывания запрещены! Про содержание остальных регистров пока ничего не известно. Остальные регистры инициализируем самостоятельно.
Assembler
1
2
3
4
5
6
7
entry_point: mov     ax, cs
        cli
        mov     ss, ax
        mov     sp, entry_point
        sti
 
        mov     ds, ax
Стек у нас будет располагаться перед программой, до служебной области BIOS еще остается порядка 30 килобайт, для стека больше чем достаточно. Прерывания изначально запрещены, но я все равно сделаю это самостоятельно, на всякий случай. и разрешу после установки стека. Никаких проблем это вызвать, по-моему, не должно.
Так же, нулевым значением, инициализируем сегментный регистр ds.
Assembler
1
2
3
4
5
6
7
8
9
10
11
        ; Сохpаняем фоpму куpсоpа
        mov     ah, 3
        xor     bh, bh
        int     0x10
 
        push    cx
 
        ; отключаем куpсоp
        mov     ah, 1
        mov     ch, 0x20
        int     10h
Чтобы все было красиво и радовало глаз, на время чтения отключаем курсор. Иначе он будет мелькать на экране. Чтобы его потом восстановить сохраним его форму в стеке.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
        ; Загpужаем setup
        mov     ax, SETUP_SEG
        mov     es, ax
 
        mov     ax, 1
        mov     cx, SETUP_SECTS
 
        mov     si, load_setup_msg
        call    load_block
 
        call    outstring
 
        mov     si, complete_msg
        call    outstring
Загружаем первый блок (setup). Процедуру загрузки блока рассмотрим немного позже.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
       ; загpужаем ядpо.
        mov     ax, KERNEL_SEG
        mov     es, ax
 
        mov     ax, 1 + SETUP_SECTS
        mov     cx, KERNEL_SECTS
 
        mov     si, load_kernel_msg
        call    load_block
 
        call    outstring
 
        mov     si, complete_msg
        call    outstring
Загружаем второй блок (kernel). Здесь все аналогично первому блоку.
Assembler
1
2
3
4
        ; Восстанавливаем куpсоp
        pop     cx
        mov     ah, 1
        int     0x10
Восстанавливаем форму курсора.
Assembler
1
2
        ; Пеpедаем упpавление на setup
        jmp     SETUP_SEG:0
На этом работа boot sector'а заканчивается. Дальним переходом передаем управление программе setup.

Далее располагаются функции.
Assembler
1
2
3
4
5
; Загрузка блока
; cx - количество сектоpов
; ax - начальный сектоp
; es - указатедь на память
; si - loading message
Функция загрузки блока. Она же занимается выводом на экран процентного счетчика.

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
load_block:
        mov     di, cx ; сохpаняем количество блоков
 
 .loading:
        xor     bx, bx
        call    load_sector
        inc     ax
        mov     bx, es
        add     bx, 0x20
        mov     es, bx
 
        ; Выводим сообщение о загpузке.
        call    outstring
 
        push    ax
 
        ; Выводим пpоценты
        ; ((di - cx) / di) * 100
        mov     ax, di
        sub     ax, cx
        mov     bx, 100
        mul     bx
        div     di
 
        call    outdec
 
        push    si
        mov     si, persent_msg
        call    outstring
        pop     si
 
        pop     ax
 
        loop    .loading
 
        ret
В этой функции ничего сложного нет. Обыкновенный цикл.

А вот следующая функция загружает с диска отдельный сектор, при этом оперируя его линейным адресом.
Есть так называемое int13 extension, разработанное совместно фирмами MicroSoft и Intel. Это расширение BIOS работает почти аналогичным образом, Считывая сектора по их линейным адресам, но оно поддерживается не всеми BIOS, имеет несколько разновидностей и работает в основном для жестких дисков. Поэтому нам не подходит.

В своей работе ориентируемся пока только на чтение с floppy диска, размером 1,4 мегабайта. Поэтому будем использовать функцию, которой в качестве параметров задается номер дорожки, головки и сектора.
Assembler
1
2
3
; Загрузка сектора
; ax - номеp сектоpа (0...max (2880))
; es:bx - адpес для pазмещения сектоpа.
Абсолютный номеp сектоpа вычисляется по фоpмуле:
AbsSectNo = (CylNo * SectPerTrack * Heads) + (HeadNo * SectPerTrack) + (SectNo - 1)
Значит обpатное спpаведливо:
CylNo = AbsSectNo / (SectPerTrack * Heads) HeadNo = остаток / SectorPerTrack SectNo = остаток + 1
Assembler
1
2
3
4
5
6
7
load_sector: push ax
 push cx
 cwd
 mov cx, 18 ; SectPerTrack
 div cx 
 mov cx, dx
 inc cx ; количество секторов
Поделив номер сектора на количество секторов на дорожке, в остатке получаем номер сектора на дорожке. Это значение хранится в 6 младших битах регистра cl.
Assembler
1
        xor     dx, dx  ; dl - диск - 0!
Номер диска храниться в dl и устанавливается в 0 (это диск a: )

Assembler
1
2
       shr     ax, 1
        rcl     dh, 1 ; номер головки
Младший бит частного определяет для нас номер головки. (0 или 1)
Assembler
1
2
3
        mov     ch, al
        shl     ah, 4
        or      cl, ah ; количество доpожек
Оставшиеся биты частного определяют номер цилиндра (или дорожки).
восемь младших бит номера хранятся в регистре ch, два старших бита номера хранятся в двух старших битах регистра cl.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 .rept:
        mov     ax, 0x201
        int     0x13
 
        jnc     .read_ok
 
        push    si
        mov     si, read_error
        call    outstring
 
        movzx   ax, ah
        call    outdec
 
        mov     si, crlf
        call    outstring
 
        xor     dl, dl
        xor     ah, ah
        int     0x13
 
        pop     si
 
        jmp     short .rept
В случае ошибки чтения не будем возвращать из функции какие-либо результаты, а повторяем чтение, пока оно не окажется успешным. В случае неуспешного чтения все равно ничего не будет работать! Для верности, в случае сбоя, производим сброс устройства.
Assembler
1
2
3
4
5
.read_ok:
 
        pop     cx
        pop     ax
        ret
Далее идет две интерфейсные функции, обеспечивающие вывод на экран строк и десятичных цифр. Ничего особенного они из себя не представляют а для вывода пользуются телетайпным прерыванием BIOS (ah = 0eh, int 10h), которое обеспечивает вывод одного символа с обработкой некоторых служебных кодов.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; Вывод строки.
; ds:si - стpока.
 
outstring:
        push    ax
        push    si
 
        mov     ah, 0eh
 
        jmp     short .out
 .loop:
        int     10h
 .out:
        lodsb
        or      al, al
        jnz     .loop
 
        pop     si
        pop     ax
        ret
Эта функция ограничена выводом чисел до 99 включительно, случай с большим числом обрабатывается как переполнение и отображается как '##'.

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
; Вывод десятичных чисел от 0 до 99
; ax - число!
outdec:
        push    ax
        push    si
        mov     bl, 10
        div     bl
        cmp     al, 10
        jnc     .overflow
        add     ax, '00'
        push    ax
        mov     ah, 0eh
        int     0x10
        pop     ax
        mov     al, ah
        mov     ah, 0eh
        int     0x10
        jmp     short .exit
 .overflow:
        mov     si, overflow_msg
        call    outstring
 .exit:
        pop     si
        pop     ax
        ret
Далее располагаются несколько служебных сообщений.

Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
load_setup_msg:
        db      'Setup loading: ', 0
 
load_kernel_msg:
        db      'Kernel loading: ', 0
 
complete_msg:
        db      'complete.'
 
crlf:
        db      0ah, 0dh, 0
 
persent_msg:
        db      '%', 0dh, 0
 
overflow_msg:
        db      '##', 0
 
read_error:
        db      0ah, 0dh
        db      'Read error #', 0
 
        TIMES   510-($-$$) db 0
Эта комбинация заполняет оставшееся место в секторе нулями. А остается у нас еще около 200 байт.
Assembler
1
        dw      0aa55h
Последние два байта называются "Partition table signature", что не совсем корректно. Фактически эта сигнатура говорит BIOS'у о том, что этот сектор является загрузочным.

Этот boot sector, помимо того, что читает по секторам, отличается от линуксового еще и размещением в памяти. После загрузки он не перемещает себя в памяти, и работает по тому же адресу, по которому его загрузил BIOS. Так же setup загружается непосредственно следом за boot sector'ом, с адреса 7e00h, что в принципе не помешает ему работать в других адресах, если будем загружать наше ядро через LILO, например.
Скомпилированную версию boot sector'а вы можете найти в файловом архиве (секция "наработки").
В следующей главе переходим к программе setup и рассматриваем порядок перехода в защищенный режим.
Глава #5


Рассмотрим немного подробнее организацию памяти в защищенном режиме и поговорим о концепциях защиты.

История организации памяти.


Ранние модели процессоров от Intel имели 16 бит шины данных и 20 бит шины адреса. Это налагало определенные ограничения на адресацию памяти, ибо 16-бинтный регистр невозможно было использовать для адресации более чем 64 килобайт памяти. Чтобы обойти это препятствие разработчики предусмотрели сегментные регистры. Сегментный регистр хранит в себе старшие 16 бит адреса и для получения полного адреса к сегментному адресу прибавляется смещение в сегменте.
191817161514131211109876543210
Сегмент
Смещение
Линейный адрес
Таким образом, стало возможным адресовать до 1 мегабайта памяти. Это же позволило делать программы, не настолько привязанными к памяти и упростило адресацию. Сегменты могут начинаться с любого адреса, кратного 16 байтам, эти 16-байтные блоки памяти получили название параграфов. Но это и создает определенные неудобства. Первое неудобство состоит в том, что на один адрес памяти указывает 4096 различных комбинаций сегмент/смещение. Второе неудобство состоит в том, что нет возможности ограничить программам доступ к тем или иным частям памяти, что в некоторых случаях может быть существенно!
Введение защищенного режима решило эти проблемы, но ради совместимости любой из современных процессоров может работать в реальном или виртуальном режиме процессора i8086.

Защита.


Для обеспечения надежной работы операционных систем и прикладных программ разработчики процессоров предусмотрели в них механизмы защиты. В процессорах фирмы Intel предусмотрено четыре уровня привилегий для программ и данных. Нулевой уровень считается наиболее привилегированным, третий уровень - наименее.
Так же в защищенном режиме совсем иначе работает механизм преобразования адресов. в сегментном регистре теперь хранится не старшие биты адреса, а селектор. селектор представляет из себя индекс в таблице дескрипторов. И кроме этого содержит в себе несколько служебных бит. Формат селектора такой:
151413121110987654321 0
Index TIRPL
Поле Index определяет индекс в дескрипторной таблице.

В процессорах Intel одновременно в системе может существовать две дескрипторных таблицы: Глобальная (Global descriptor table или GDT) и Локальная (Local descriptor table или LDT).
GDT существует в единственном экземпляре. Адрес и предел GDT хранятся в специальном системном регистре (GDTR) в 48 бит длиной (6 байт).
LDT может быть индивидуальная для каждой задачи, или общая для системы, или же ее вообще может не быть. Адрес и размер LDT определяется в GDT, для обращения к LDT в процессоре существует специальный регистр (LDTR), но в отличии от GDTR он имеет размер 16 бит и содержит в себе селектор из GDT.
Поле TI (Table indicator) селектора определяет принадлежность селектора GDT (0) или LDT (1).
Поле RPL (Requested privilege level) определяет запрашиваемые привилегии.

Дескрипторы сегментов


Дескрипторные таблицы состоят из записей по 64 бита (8 байт) в каждой. Формат дескриптора таков:
7654
Базовый адрес 31-24Предел 19-16Права доступаБазовый адрес 23-16
3210
Базовый адрес 15-0Предел 15-0
Сразу бросается в глаза очень странная организация дескриптора, но это связано с совместимостью с процессором i286, формат дескриптора в котором был таков:
7654
ЗарезервированоПрава доступаБазовый адрес 23-16
3210
Базовый адрес 15-0Предел 15-0
Что же содержится в дескрипторе:
Базовый адрес - 32 бита (24 бита для i286). Определяет линейный адрес памяти, с которого начинается сегмент. В отличие от реального режима этот адрес может быть указан с точностью до байта.
Предел - 20 бит (16 бит для i286). Определяет размер сегмента (максимальный адрес, по которому может быть произведено обращение, это справедливо не всегда но об этом чуть позже). 20-битное поле может показаться не очень то большим для 32-х битного процессора, но это не так. Оно не всегда показывает размер в байтах. Но и об этом чуть позже.
Байт прав доступа:
76543210
PDPLSTypeA
  • Бит P (present) - Указывает на присутствие сегмента в памяти. обращение к отсутствующему сегменту вызывает особый случай не присутствия сегмента в памяти.
  • Двух битное поле DPL определяет уровень привилегий сегмента. Про Уровни привилегий поговорим чуть позже.
  • Бит S (Segment)- Будучи установленным в 1, определяет сегмент памяти, к которому может быть получен доступ на чтение (запись) или выполнение.
  • Три бита Type - в зависимости от бита S определяет либо возможности чтения/записи, выполнения сегмента или определяет тип системных данных, хранимых в селекторе. Подробнее это выглядит так:
    Если бит S установлен в 1, то поле Type делится на биты:
    210
    1 - кодПодчиненный сегмент кодаДопустимо считывание
    0 - данныеРасширяется внизДопустима запись
    Если сегмент расширяется вниз (это используется для стековых сегментов) то поле предела показывает адрес, выше которого допустима запись. ниже запись недопустима и вызовет нарушение пределов сегмента.
  • Бит А (Accessed) устанавливается в 1, если к сегменту производилось обращение.
Если бит S установлен в 0, то в сегменте находится служебная информация определяемая полем Typе и битом A.
TYPEAОписание
000 1 TSS для i286
001 0 LDT
001 1 Занятый TSS для i286
010 0 Шлюз вызова i286
010 1 Шлюз задачи
011 0 Шлюз прерывания i286
011 1 Шлюз исключения i286
100 1 TSS для i386
101 1 Занятый TSS i386
110 0 Шлюз вызова i386
111 0 Шлюз прерывания i386
111 1 Шлюз ловушки i386
Остальные комбинации либо недопустимы, либо зарезервированы.
TSS - это сегмент состояния задачи (Task state segment) о них поговорим в следующей главе.
Шестой байт дескриптора, помимо старших бит предела, содержит в себе несколько битовых полей.
76543210
GD0UПредел 19-16
  • Бит G (Granularity) - определяет размер элементов, в которых измеряется предел. если 0 - предел в байтах, если 1 - размер в страницах.
  • Бит D (Default size) - размер операндов в сегменте. Если 0 - 16 бит. если 1 - 32 бита.
  • Бит U (User) - доступен для пользователя (вернее для программиста операционной системы)

И снова защита.


Немного терминологии:
Уровень привилегий может быть от 0(высший) до 3(низший). Следовательно повышение уровня привилегий соответствует его уменьшению в численном эквиваленте, понижение - наоборот.
В дескрипторе содержатся биты DPL, которые определяют максимальный уровень привелегий для доступа к сегменту.
В селекторе содержится RPL - то есть запрашиваемый уровень привилегий.
RPL секущего кодового сегмента (хранится в регистре cs) является уровнем привилегий данного процесса и называется текущим уровнем привилегий (CPL)
Прямые обращения к сегментам возможны при соблюдении следующих условий:
  • В случае если запрашиваемый уровень привилегий больше текущего, то запрашиваемый уровень понижается до текущего.
  • При обращении к сегменту данных RPL селектора должен быть не ниже DPL сегмента.
  • При обращении к сегменту кода возможно только при равенстве CPL, RPL и DPL.
  • Если сегмент кода помечен как подчиненный, то для обращения к нему необходимо иметь уровень привилегий не ниже уровня сегмента. При этом выполнение сегмента происходит с текущим уровнем привилегий.
Косвенные вызовы возможны только через шлюзы при соблюдении следующих условий:
  • DPL шлюза должен быть не выше, чем CPL сегмента, из которого производится вызов шлюза.
  • DPL сегмента, на который указывает шлюз, должно быть не ниже чем DPL шлюза.
Эпилог.
Тема конечно очень сложна для понимания. В следующих главах концентрируем внимание на таких моментах. Интересующиеся люди могут почитать дополнительную, более подробную информацию по защите в литературе по микропроцессорам фирмы Intel.
Глава #6


В этой главе продолжим разговор о защищенном режиме процессора, узнаете, что такое шлюзы. А так же вкратце поговорим о виртуальном режиме процессора 8086.

В 4-ой главе, когда я расписывал вам, как писать boot sector, я допустил одну достаточно серьезную ошибку, которую признаю и благодарю Bug Maker'а, за то, что обратил на это мое внимание. В процедуре load_sector я, первым делом, делю номер сектора на количество секторов на дорожке. Для деления используя беззнаковую команду div, предварительно расширяя ax в dx:ax знаковой командой cwd. Правда если учесть что максимальное количество секторов на гибком диске не превышает 2880, то старший, знаковый, бит ax всегда нулевой. Но, тем не менее, ошибка потенциальная. Этот фрагмент кода стоит писать так:
Assembler
1
2
3
4
5
6
7
load_sector:
        push    ax
        push    cx
        mov     cl, 18
        div     cl
        mov     cx, dx
        inc     cx
Исправившись, я вообще убрал команду cwd, и теперь делю на байт cl. Все это, к тому же, сэкономило мне два байта.
Но это еще не все.. при написании boot sector'а я говорил, что это совсем не окончательная версия. Так оно и получается. Из бутсектора мы уберем код загрузки kernel. Этим будет заниматься программа setup. Следовательно, boot sector'у осанется только считать setup и запусить его... Даже если сделать более корректную обработку ошибок чтения, у нас остается около 250 байт на всякие развлечения...
А setup должен будет уметь достаточно многое. В него будет встроена поддержка файловой системы, поддержка выполняемых форматов файлов. Мы собираемся делать микроядро, и setup'у придется загружать помимо ядра еще несколько дополнительных программ, которые понадобятся нам для нормального старта системы.

Но об этом позже. А теперь продолжаем разбираться с защищенным режимом.

Шлюзы


В предыдущей главе, когда говорили о дескрипторах и дескрипторных таблицах ни словом не упомянули о дескрипторной таблице прерываний (Interrupt description table или IDT). Эта таблица так же состоит из дескрипторов, но в отличии от LDT и GDT в этой таблице могут размечаться только шлюзы. В защищенном режиме все прерывания происходят через IDT. Традиционная таблица векторов прерываний здесь не используется.
Формат дескрипторов шлюзов отличается от дескриптора сегмента.
Для начала рассмотрим шлюз вызова.
7654
Смещение 31-16Права доступаКоличество слов стека
3210
СелекторСмещение 15-0
В поле прав доступа задается уровень привилегий, который должен быть ниже CPL текущего процесса, бит присутствия и соответствующий тип в остальных полях.
Селектор и смещение задают адрес вызываемой функции, при этом селектор должен присутствовать либо в GDT либо в активной LDT.
Параметр "Количество слов стека" служит для передачи аргументов в вызываемую функцию, при этом соответствующее количество слов копируется из стека текущего уровня привилегий в стек уровня привилегий вызываемой функции. Это поле использует только младшие 5 бит четвертого байта. Остальные биты должны быть нулевыми.
Обращаться к такому шлюзу, если дескриптор не расположен в IDT, можно только командой call far, при этом указываемое в команде смещение игнорируется. А селектор должен указывать на дескриптор шлюза вызова.
Шлюз прерывания и шлюз ловушки имеют одинаковый формат, отличаются между собой типами в байте прав доступа. В отличии от шлюза вызова эти шлюзы не содержат в себе Количества слов стека, поскольку прерывания бывают аппаратными и передача в них параметров через стек - бессмысленна. Эти шлюзы используются обычно только в IDT.
Шлюз задачи содержит в себе значительно меньше информации.
Во втором и третьем байте дескриптора записывается селектор TSS (Сегмента состояния задачи). Поле прав доступа заполняется аналогично другим шлюзам, но с соответствующим типом. Остальные поля дескриптора не используются.
При вызове такого шлюза происходит переключение контекста задачи. При этом вызывающая задача блокируется и не может быть вызвана до тех пор, пока вызванная задача не вернет ей управление командой iret.

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

Виртуальный режим процессора 8086.


Для возможности запуска из защищенного режима программ, предназначенных для реального, существует так называемый "Виртуальный режим процессора 8086". При этом полноценно работают механизмы преобразования адресов защищенного режима. А так же многозадачные системы, которые могут одновременно выполнять как защищенные задачи, так и виртуальные. При этом адресация в виртуальной задаче осуществляется традиционным для 8086 методом - сегмент/смещение.
Обращение к прерываниям осуществляется через IDT, но таблица прерываний реального режима может быть обработана из функций, шлюзы которых размещаются в IDT.
Обращение виртуальной задачи к портам так же может быть отслежено через прерывания защищенного режима. При обращении к запрещенным портам происходит исключение.
При желании может быть обеспечена абсолютно прозрачная работа нескольких виртуальных задач в одной мультизадачной среде. Но мы этой возможностью не будем пользоваться, и в своей работе будем рассчитывать на программы исключительно защищенного режима.
Глава #7


Исключения защищенного режима

Исключения или системные прерывания существовали еще в самых первых моделях процессоров от Intel. Вот их список:
0Division by zero (деление на ноль или переполнение при делении);
1Single step (пошаговая отладка);
3Breakpoint;
4Overflow (срабатывает при команде into в случае установленного флага overflow в регистре flags);
6Invalid opcode (i286+);
7No math chip;
Исключения располагаются в начале таблицы прерываний. В реальном режиме занимают 8 первых векторов прерываний.
Введение защищенного режима потребовало введения дополнительных исключений. В защищенном режиме первые 32 вектора прерываний зарезервированы для исключений. Не все они используются в существующих процессорах, в будующем возможно их будет больше. Системные прерывания в защищенном режиме делятся на три типа: нарушения (fault), ловушки (trap) и аварии (abort). Итак в защищенном режиме у нас существуют следующие исключения:
0Divide error fault
1Debug fault/trap
3Breakpoint trap
4Overflow trap
5Bounds check fault
6Invalid opcode fault
7Coprocessor not available fault
8Double fault abort
9Coprocessor segment overrun fault
10Invalid tss fault
11Segment not present fault
12Stack fault fault
13General protection fault fault
14Page fault fault
16Coprocessor error fault
17Alignument check fault (i486+);
18Hardware check abort (Pentium+);
19SIMD fault (Pentium III+)
.
Нарушения возникают вследствии несанкционированных или неправильных действий программы, предполагается, что ошибки можно исправить и продолжить выполнение программы с инструкции, которая вызвала ошибку.
Ловушки возникают после выполнения инструкции, но тоже подразумевают исправление ошибочной ситуации и дальнейшую работу программы.
Аварии возникают в случае критических нарушений, после этого программа уже не может быть перезапущена и должна быть закрыта.
Но иногда в случае ошибки или ловушки программа тем не менее не может продолжить свое выполнение. Это зависит от тяжести нарушения и от организации операционной системы, которая обрабатывает исключения. И если ошибка или ловушка не может быть исправлена, программу так же следует закрыть.
При возникновении исключения процессор иногда помещает в стек код ошибки, по которому обработчик исключения может проанализировать и, возможно, исправить возникшую ошибку.
Все исключения обрабатываются операционной системой. В случае микроядерных систем этим занимается микроядро.

Микроядерные системы


В первых главах уже касались этой темы, но тогда ограничились буквально несколькими словами. Теперь двигаемся именно в сторону микроядерности, значит стоит поподробнее рассказать, что это такое.
Принцип микроядерности заключается в том, что ядро практически не выполняет операций, связанных с обслуживанием внешних устройств. Эту функцию выполняют специальные программы-сервера. Ядро лишь предоставляет им возможность обращаться к устройствам. Помимо этого ядро обеспечивает многозадачность (параллельное выполнение программных потоков), межпроцессное взаимодействие и менеджмент памяти.
Приложения (как и сервера) у нас работают на третьем, непривилегированном кольце и не могут свободно обращаться к портам ввода/вывода или dma памяти. Тем более не могут сами устанавливать свои обработчики прерываний. Для использования ресурсов процессы обращаются к ядру с просьбой выделить необходимые ресурсы в их распоряжение. Осуществляется это следующим образом:
Для обеспечения доступа к портам ввода/вывода используются возможности процессоров, впервые появившиеся intel 80386. У каждой задачи (в сегменте состояния задачи (TSS)) существует карта доступности портов ввода/вывода. Приложение обращается к ядру с "просьбой" зарегистрировать для нее диапазон портов. Если эти порты до тех пор никем не были заняты, то ядро предоставляет их в распоряжение процесса, помечая их как доступные в карте доступности ввода/вывода этого процесса.
DMA память, опять таки после запроса у ядра, с помощью страничного преобразования подключается к адресному пространству процесса. Настройка каналов осуществляется ядром по "просьбе" процесса.
Доступ к аппаратным прерываниям (IRQ) осуществляется сложнее. Для этого процесс порождает в себе поток (thread), и сообщает ядру, что этот поток будет обрабатывать какое-то IRQ. При возникновении аппаратного прерывания, которое обрабатывает всетаки ядро, данный процесс выходит из состояния спячки, в котором он находился в ожидании прерывания, и ставится в очередь к менеджеру процессов. Такие потоки должны иметь более высокий приоритет, чем все остальные, дабы вызываться как можно скорее.
Но, как я говорил, ядро выполняет еще некоторые функции, немаловажная из которых - это межпроцессное взаимодействие. Оно представляет из себя возможность процессов обмениваться сообщениями между собой. В отличии от монолитных систем в микроядерных системах межпроцессное взаимодействие (Inter Process Communication или IPC) это едва ли не основное средство общения между процессами, и поскольку все драйвера у нас такие же процессы, микроядерное IPC должно быть очень быстрым. Быстродействие IPC достигается за счет передачи сообщений без промежуточного буферизирования в ядре. Либо непосредственным переписыванием процессу-получателю, либо с помощью маппинга страниц (если сообщения большого размера).
Менеджер памяти имеет как бы две стороны. Первая сторона - внутренняя, распределение памяти между приложениями, организация свопинга (который тоже осуществляется не ядром непосредственно, а специальной программой-сервером) никаким образом не интересует остальные программы. Но другая сторона - внешняя служит именно для них. Программы могут запросить у ядра во временное пользование некоторое количество памяти, которое ядро им обязательно предоставит (в разумных пределах... гигабайта два... не больше... . Или же программы могут запросить у ядра какой-то определенный участок памяти. Это бывает необходимо программам-серверам. И это требование ядром также вполне может быть удовлетворено при условии, что никакая другая программа до того не забронировала этот участок памяти для себя.
Глава #8


В этой главе поговорим о файловых системах.
Файловая система - это немаловажный момент в операционной системе, они бывают разные, различаются по производительности, надежности, по-разному экономно используют пространство.
Есть много файловых систем, которые нам, в принципе, подойдут (EXT2FS, FFS, NTFS, RaiserFS и много других), есть так же файловые системы, которые нам вообще не подойдут (FAT). В процессе развития нашей операционной системы мы создадим поддержку и для них, но для начала надо остановиться на чем-то одном. Этой одной файловой системой будет EXT2FS.
В этой главе подробно рассмотрим файловые системы FAT, и более подробно файловую систему Linux (ext2). Поскольку наша операционная система будет юниксоподобная, то файловые системы FAT нам никак не подходят, поскольку они не обеспечивают мер ограничения доступа, и по сути своей не являются многопользовательскими. Про остальные файловые системы я ограничусь лишь основными моментами.
Так же я не стану затрагивать тему разделов диска. Обсудим это в другой раз.

Основные принципы файловых систем


Все устройства блочного доступа (к которым относятся жесткие или гибкие диски, компакт диски) при чтении/записи информации оперируют секторами. Для жестких или гибких дисков размер сектора равен 512 байт, в компакт-дисках размер сектора равен 2048 байт. Сектора являются физической единицей информации для носителя.
Для файловых систем такое распределение часто бывает не очень удобно, и в них вводится понятие кластера. Кластеры часто бывают больше по размеру, чем сектора носителя. Кластеры являются логической единицей файловых систем. Правда, не всегда они называются кластерами. В ext2 кластеры называются просто блоками, но это не столь важно. Для организации кластеров файловые системы хранят таблицы кластеров. Таблицы кластеров, естественно, расходуют дисковое пространство. Помимо этого, дополнительное дисковое пространство расходуется под каталоги файлов. Эти неизбежные расходы в разных файловых системах имеют разную величину. Но об этом мы поговорим ниже.

Файловые системы на базе FAT (File Allocation Table)


Этот тип файловых систем разработала фирма Microsoft достаточно давно. Вместе с первыми DOS... С тех пор неоднократно натыкались на различные препятствия и дорабатывались в соответствии с требованиями времени.
Теперь пойдет небольшой экскурс в историю.
  • В 1977 году Биллом Гейтсом и Марком МакДональдом была разработана первая файловая система FAT. Ради совместимости с CP/M в ней было ограничено имя файла. Максимальная длина имени составляла 8 символов, и 3 символа можно было использовать для расширения файла. Регистр букв не различался и не сохранялся. Размер кластера не превышал 4 килобайта. Размер диска не мог превышать 16 мегабайт.
  • В 1981 году вышла первая версия MSDOS, которая базировалась на FAT.
  • Начиная с MSDOS версии 3.0, в файловой системе появилось понятие каталога.
  • Для поддержки разделов более 16 мегабайт размер элемента FAT был увеличен до 16 бит, (первая версия была 12-битная) а максимальный размер кластера увеличен до 32 килобайт. Это позволило создавать разделы до 2 гигабайт.
  • В таком состоянии FAT просуществовал до появления VFAT, появившегося вместе с выходом Windows'95, в которой появилась поддержка длинных имен файлов. Теперь имя файлов могло иметь длину до 255 символов, но ради совместимости старый формат имен так же остался существовать.
  • Немного позже FAT был еще расширен, размер элемента FAT стал 32 бита, при этом максимальный размер кластера вновь уменьшился до 4 килобайт, но это позволило создавать разделы до 2 терабайт. Кроме того, была расширена информация о файлах. Теперь она позволяли хранить помимо времени создания файла время модификации и время последнего обращения к файлу.
Ну а теперь подробнее рассмотрим структуру этой файловой системы.
Общий формат файловой системы на базе FAT таков:
  • Boot sector (в нем так же содержится "Блок параметров FS")
  • Reserved Sectors (могут отсутствовать)
  • FAT (Таблица размещения файлов)
  • FAT (вторая копия таблицы размещения файлов, может отсутствовать)
  • Root directory (корневая директория)
  • Область файлов. (Кластеры файловой системы)
Boot sector имеет размер 512 байт, как мы уже знаем, может содержать в себе загрузчик системы, но помимо этого для FAT он содержит Блок параметров. Блок параметров размещается в boot sector'е по смещению 0x0b и содержит в себе следующую информацию:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct FAT_Parameter_block {
  u_int16       Sector_Size;
  u_int8        Sectors_Per_Cluster;
  u_int16       Reserved_Sectors;
  u_int8        FAT_Count;
  u_int16       Root_Entries;
  u_int16       Total_Sectors;
  u_int8        Media_Descriptor;
  u_int16       Sectors_Per_FAT;
  u_int16       Sectors_Per_Track;
  u_int16       Heads;
  u_int32       Hidden_sectors;
  u_int32       Big_Total_Sectors;
};
Размер кластера можно вычислить, умножив Sector_Size на Sectors_Per_Cluster.
Общий размер диска определяется следующим образом: Если значение Total_Sectors равно 0, то раздел более 32 мегабайт и его длина в секторах храниться в Big_Total_Sectors. Иначе размер раздела показан в Total_Sectors.
Таблица FAT начинается с сектора, номер которого храниться в Reserved_Sectors и имеет длину Sectors_Per_FAT; при 16-битном FAT размер таблицы может составлять до 132 килобайт (или 256 секторов) (в FAT12 до 12 килобайт).
Вторая копия FAT служит для надежности системы... но может отсутствовать.
После таблицы FAT следует корневая директория диска. Размер этой директории ограничен Root_Entries записями. Формат записи в директории таков:
C
1
2
3
4
5
6
7
8
9
10
struct FAT_Directory_entry {
  char          Name[8];
  char          Extension[3];
  u_int16       File_Attribute;
  char          Reserved[10];
  u_int16       Time;
  u_int16       Date;
  u_int16       Cluster_No;
  u_int32       Size;
};
Размер записи - 32 байта, следовательно, общий размер корневой директории можно вычислить, умножив Root_Entries на 32.
Далее на диске следуют кластеры файловой системы. Из записи в директории берется первый номер кластера, с него начинается файл. В FAT под этим номером может содержаться либо код последнего кластера (0xffff или 0xfff для FAT12) либо номер кластера, следующего за этим.
При записи файла из FAT выбираются свободные кластеры по порядку от начала. В результате возникает фрагментация файловой системы, и существенно замедляется ее работа. Но это уже выходит за тему рассылки.
Все выше сказанное про FAT справедливо для FAT12 и FAT16. FAT32 более существенно отличается, но общие принципы организации для нее примерно такие же. VFAT ничем не отличается от FAT16, для хранения длинных имен там используется однеа запись в директории для хранения короткого имени файла и несколько записей для хранения длинного. Длинное имя храниться в unicode, и на запись в директории приходится 13 символов длинного имени, причем они разбросаны по некоторым полям записи, остальные поля заполняются с таким расчетом, чтобы старые программы не реагировали на такую запись.
С первого взгляда видна не высокая производительность таких файловых систем. Не буду поливать грязью Microsoft, у них и без меня достаточно проблем... К тому же и у них есть другие разработки, которые не столь плохи. Но о них мы поговорим ниже... А сейчас давайте посмотрим на ext2fs. Правда, эта файловая система несколько другого уровня, и сравнивать ее с FAT - нельзя. Но обо всем по порядку.

Ext2fs (Расширенная файловая система версия 2)


Linux разрабатывался на операционной системе Minix. В ней была (да и есть) файловая система minixfs. Система не очень гибкая и достаточно ограниченная. После появления Linux была разработана (на базе minixfs) файловая система extfs, которую в скором времени заменила ext2fs, которая и используется в большинстве Linux, по сей день.
Для начала давайте рассмотрим основное устройство этой файловой системы:
  • Boot sector (1 сектор)
  • Свободно (1 сектор, может быть использован для расширения Boot sector'а до килобайта)
  • Super block (2 сектора или 1024 байта длиной)
  • Group descriptors (2 сектора максимум)
  • Group 1
  • Group 2
  • ... и так далее... до Group 32 если необходимо
.Если ext2fs находится на каком ни будь разделе жесткого диска, или является не загрузочной, то boot sector'а там может вообще не быть.
Super block содержит в себе информацию о файловой системе и имеет следующий формат:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct ext2_super_block {
  u_int32  s_inodes_count;
  u_int32  s_blocks_count;
  u_int32  s_r_blocks_count;
  u_int32  s_free_blocks_count;
  u_int32  s_free_inodes_count;
  u_int32  s_first_data_block;
  u_int32  s_log_block_size;
  int32    s_log_frag_size;
  u_int32  s_blocks_per_group;
  u_int32  s_frags_per_group;
  u_int32  s_inodes_per_group;
  u_int32  s_mtime;
  u_int32  s_wtime;
  u_int16  s_mnt_count;
  u_int16  s_max_mnt_count;
  u_int16  s_magic;
  u_int16  s_state;
  u_int16  s_errors;
  u_int16  s_pad;
  u_int32  s_lastcheck;
  u_int32  s_checkinterval;
  u_int32  s_reserved[238];
};
Не буду описывать значение всех полей этой структуры, ограничусь основными. Размер блока файловой системы можно вычислить так: 1024 * s_log_block_size. Размер блока может быть 1, 2 или 4 килобайта размером.
Об остальных полях чуть попозже.
А теперь рассмотрим группы дескрипторов файловой системы.
Формат дескриптора группы таков:
C
1
2
3
4
5
6
7
8
9
10
struct ext2_group_desc {
  u_int32  bg_block_bitmap;
  u_int32  bg_inode_bitmap;
  u_int32  bg_inode_table;
  u_int16  bg_free_blocks_count;
  u_int16  bg_free_inodes_count;
  u_int16  bg_used_dirs_count;
  u_int16  bg_pad;
  u_int32  bg_reserved[3];
};
Содержимое группы таково:
  • Block bitmap (Битовая карта занятости блоков)
  • Inode bitmap (Битовая карта занятости inode)
  • Inode table (Таблица inode)
  • Available blocks (блоки, доступные для размещения файлов)
Блоки в файловой системе отсчитываются с начала раздела. В дескрипторе группы содержаться номер блока с битовой картой блоков группы, номер блока с битовой картой инодов, и номер блока с которого начинается таблица inode. Про inode мы поговорим чуть попозже, а сперва разберемся с битовыми картами.
В суперблоке храниться количество блоков в группе (s_blocks_per_group). Битовая карта имеет соответствующий размер в битах (занимает она не более блока). и в зависимости от размера блока может содержать информацию об использовании 8, 32 или 132 мегабайт максимум. Дисковое пространство раздела разбивается на группы в соответствии с этими значениями. А групп, как я уже упоминал, может быть до 32... что позволяет создавать разделы, в зависимости от размера блока, 256, 1024 или 4096 мегабайт соответственно.
В битовую карту блоков группы входят так же те блоки, которые используются под саму карту, под карту inode и под таблицу inode. Они сразу помечаются как занятые.
Теперь давайте разберемся, что такое inode. В отличии от FAT информация о файле здесь храниться не в директории, а в специальной структуре, которая носит название inode (информационный узел). В записи директории содержится только адрес inode и имя файла. При этом на один inode могут ссылаться несколько записей директории. Это называется hard link.
Формат inode таков:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct ext2_inode {
  u_int16  i_mode;
  u_int16  i_uid;
  u_int32  i_size;
  u_int32  i_atime;
  u_int32  i_ctime;
  u_int32  i_mtime;
  u_int32  i_dtime;
  u_int16  i_gid;
  u_int16  i_links_count;
  u_int32  i_blocks;
  u_int32  i_flags;
  u_int32  i_reserved1;
  u_int32  i_block[14];
  u_int32  i_version;
  u_int32  i_file_acl;
  u_int32  i_dir_acl;
  u_int32  i_faddr;
  u_int8   i_frag;
  u_int8   i_fsize;
  u_int16  i_pad1;
  u_int32  i_reserved2[2];
};
Как видно из приведенной выше структуры в inode содержится следующая информация:
  • Тип и права доступа файла (i_mode)
  • идентификатор хозяина файла (i_uid)
  • Размер (i_size)
  • Время доступа, создания, модификации и удаления файла (после удаления inode не удаляется, а просто перестает занимать блоки файловой системы)
  • Идентификатор группы
  • Количество записей в директориях, указывающих на этот inode...
  • Количество занимаемых блоков fs
  • дополнительные флаги ext2fs
  • таблица занимаемых блоков
  • Ну и другая, не столь существенная в данных момент информация.
Остановимся поподробнее на таблице занимаемых блоков. Как видите там всего 14 записей. Но 14 блоков - это мало для одного файла. Дело в том, что не все записи содержат номера блоков. 13-я запись содержит косвенный блок, то есть блок, в котором содержится таблица блоков. А 14-я запись содержит номер блока в котором содержится таблица номеров блоков, в которых содержаться таблицы блоков занимаемых файлом... так что размер файла практически ничто не ограничивает.
Первые 10 inode зарезервированы для специфического использования.
Для корневой директории в этой файловой системе не отводится заранее отведенного места. Любая, в том числе и корневая директория в этой файловой системе является по сути своей обыкновенным файлом. Но для облегчения поиска корневой директории для нее зарезервирован inode номер 2.
В этой файловой системе в отличие от FAT существуют методы защиты файлов, которые обеспечиваются указанием идентификаторов пользователя и группы, а так же правами доступа, которые указываются в inode в поле i_mode.
За счет нескольких групп блоков уменьшается перемещение головки носителя при обращении к файлам, что увеличивает скорость обращения и уменьшает износ носителя. Да и сама файловая система организована так, что для чтения файлов не требуется загрузка больших объемов служебной информации, Что тоже не может не сказаться на производительности.
Примерно так же устроены файловые системы FFS, HPFS, NTFS. Но в их устройство я не буду вдаваться. И так уже глава очень большой получается.
Но в недавнее время появился еще один тип файловых систем. Эти системы унаследовали некоторые черты от баз данных и получили общее название "Журналируемые файловые системы". Особенность их заключается в том что все действия, производимые в файловой системе фиксируются в журнале, который правда съедает некоторый объем диска, но это позволяет значительно повысит надежность систем. В случае сбоя проверяется состояние файловой системы и сверяется с записями в журнале. В случае обнаружения несоответствий довести операцию до конца не составляет проблем, и отпадает необходимость в ремонте файловой системы. К таким файловым системам относятся ext3fs, RaiserFS и еще некоторые.

Глава #9


Эта глава посвящена чтению файлов с файловой системы ext2fs. Эту систему мы, скорее всего, возьмем за базовую для начала. FAT для наших целей мало подходит. Тот boot sector, который публиковался до этого - можно забыть, от него уже почти ничего не осталось. Связано это с тем, что мы отошли от linux, наша система будет совсем другой. Я, конечно, предвижу трудности связанные с переносом программного обеспечения. Возможно продумаем возможность эмуляции существующих операционных систем. Время покажет.
Так же прошу меня простить, что мы занимаемся тут всякой ерундой с бутсекторами и файловыми системами, но до сих пор так и не начали писать собственно ядро. Задача эта не столь, тривиальна и нужно многое продумать, чтобы не было потом горько и обидно за бесцельно написанный код.

Чтение ext2fs


В предыдущей главе описывалась структура этой файловой системы. В файловой системе присутствует Super Block и дескрипторы групп. Эта информация хранится в начале раздела. Super Block во 2-м килобайте, дескрипторы групп - в третьем.
Первый килобайт для нужд файловой системы не используется и может быть целиком использован для boot sector'а (правда он уже будет не сектор, а килобайт . Но для этого следует подгрузить второй сектор boot'а.
А для инициализации файловой системы нам нужно загрузить super block и дескрипторы групп, они же понадобятся нам для работы с файловой системой.
Это все можно загрузить одновременно, как мы и сделаем.
Assembler
1
2
3
4
5
        mov     ax, 0x7e0
        mov     es, ax
        mov     ax, 1
        mov     cx, 5
        call    load_block
Для этого мы используем уже знакомую процедуру загрузки блока, но эта процедура станет значительно короче, потому что никаких процентов мы больше не будем выводить.
В es засылается адрес, следующий за загруженным загрузочным сектором (Загружается он, как мы помним, по адресу 7c00h, и имеет длину 200h байт, следовательно свободная память начинается с адреса 7e00h, а сегмент для этого адреса равен 7e0h). В ax засылается номер сектора с которого начинается блок (в нашем случае это первый сектор, загрузочный сектор является нулевым). в cx засылается длина загружаемых данных в секторах (1 - дополнительная часть boot sector'а, 2 - Super Block ext2, 2 - дескрипторы групп. Всего 5 секторов).
Теперь вызовем процедуру инициализации файловой системы. Эта процедура достаточно проста, и проверяет только соответствие magic номера файловой системы и вычисляет размеры блока для работы.
Assembler
1
2
3
4
5
6
7
8
9
10
11
sb      equ     0x8000
ext2_init:
        pusha
        cmp     word [sb + ext2_sb.magic], 0xef53
        jz      short .right
        mov     si, bad_sb
        call    outstring
        popa
        stc
        ret
bad_sb: db 'Bad ext2 super block!', 0ah, 0dh, 0
В случае несоответствия magic номера происходит вывод сообщения об ошибке и выход из подпрограммы. Чтобы сигнализировать об ошибке используется бит C регистра flags.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.right:
        mov     ax, 1024
        mov     cl, [sb + ext2_sb.log_block_size]
        shl     ax, cl
        mov     [block_size], al        ; Размер блока в байтах
        shr     ax, 2
        mov     [block_dword_size], ax  ; Размер блока в dword
        shr     ax, 2
        mov     [block_seg_size], ax    ; Размер блока в параграфах
        shr     ax, 5
        mov     [block_sect_size], ax   ; Размер блока в секторах
        popa
        clc
        ret
block_size:             dw 1024
block_dword_size:       dw  256
block_seg_size:         dw 64
block_sect_size:        dw 2
Все эти значения нам понадобятся для работы. А теперь рассмотрим процедуру загрузки одного блока файловой системы.
Assembler
1
2
3
4
5
6
7
8
9
10
ext2_load_block:
        pusha
        mov     cx, [block_sect_size]
        mul     cx
        call    load_block
        mov     ax, es
        add     ax, [block_seg_size]
        mov     es, ax ; смещаем es
        popa
        ret
При входе в эту процедуру ax содержит номер блока (блоки нумеруются с нуля), es содержит адрес памяти для загрузки содержимого блока.
Номер блока нам надо преобразовать в номер сектора, для этого мы умножаем его на длину блока в секторах. А в cx у нас уже записана длина блока в секторах, то есть все готово для вызова процедуры load_block.
После считывания блока мы модифицируем регистр es, чтобы последующие блоки грузить следом за этим... в принципе модифицирование указателя можно перенести в другое место, в процедуру загрузки файла, это будет наверное даже проще и компактнее, но сразу я об этом не подумал.
Но пошли дальше... основной структурой описывающей файл в ext2fs является inode. Inode хранятся в таблицах, по одной таблице на каждую группу. Количество inode в группе зафиксировано в супер блоке. Итак, процедура загрузки inode:
Assembler
1
2
3
4
5
6
7
ext2_get_inode:
        pusha
        push    es
 
        dec     ax
        xor     dx, dx
        div     word [sb + ext2_sb.inodes_per_group]
Поделив номер inode на количество inode в группе, в ax мы получаем номер группы, в которой находится inode, в dx получаем номер inode в группе.
Assembler
1
2
3
        shl     ax, gd_bit_size
        mov     bx, ax
        mov     bx, [gd + bx + ext2_gd.inode_table]
ax умножаем на размер записи о группе (делается это сдвигом, но, по сути, то же самое умножение) и получаем смещение группы в таблице дескрипторов групп. gd - базовый адрес таблицы групп. Последняя операция извлекает из дескриптора группы адрес таблицы inode этой группы (адрес задается в блоках файловой системы) который у нас пока будет храниться в bx.
Assembler
1
2
       mov     ax, dx
        shl     ax, inode_bit_size
Теперь разберемся с inode. Определим его смещение в таблице inode группы.
Assembler
1
2
3
        xor     dx, dx
        div     word [block_size]
        add     ax, bx
Поделив это значение на размер блока мы получим номер блока относительно начала таблицы inode (ax), и смещение inode в блоке (dx). К номеру блока (bx) прибавим блок, в котором находится inode.
Assembler
1
2
3
        mov     bx, tmp_block >> 4
        mov     es, bx
        call    ext2_load_block
Загрузим этот блок в память.
Assembler
1
2
3
4
5
6
7
        push    ds
        pop     es
        mov     si, dx
        add     si, tmp_block
        mov     di, inode
        mov     cx, ext2_i_size >> 1
        rep     movsw
Восстановим содержимое сегментного регистра es и перепишем inode из блока в отведенное для него место.
Assembler
1
2
3
        pop     es
        popa
        ret
Inode загружен. Теперь по нему можно загружать файл. Здесь все не столь однозначно. Процедура загрузки файла состоит из нескольких модулей. Потому что помимо прямых ссылок inode может содержать косвенные ссылки на блоки. В принципе можно ограничить возможности считывающей подпрограммы необходимым минимумом, полная поддержка обеспечивает загрузку файлов до 4 гигабайт размером. Естественно в реальном режиме мы такими файлами оперировать не сможем, да это и не нужно. Но сейчас мы рассмотрим полную поддержку:
Assembler
1
2
3
4
5
6
7
8
ext2_load_inode:
        pusha
        xor     ax, ax
        mov     si, inode + ext2_i.block
        mov     cx, EXT2_NDIR_BLOCKS
        call    dir_blocks
        cmp     ax, [inode + ext2_i.blocks]
        jz      short .exit
В inode хранятся прямые ссылки на 12 блоков файловой системы. Такие блоки мы загружаем с помощью процедуры dir_blocks (она будет описана ниже). Данный этап может загрузить максимум 12/24/48 килобайт файла (в зависимости от размера блока fs 1/2/4 килобайта). После окончания работы процедуры проверяем, все ли содержимое файла уже загружено или еще нет. Если нет, то загрузка продолжается по косвенной таблице блоков. Косвенная таблица - это отдельный блок в файловой системе, который содержит в себе таблицу блоков.
Assembler
1
2
3
4
        mov     cx, 1
        call    idir_blocks
        cmp     ax, [inode + ext2_i.blocks]
        jz      short .exit
В inode только одна косвенная таблица первого уровня (cx=1). Для загрузки блоков из такой таблицы мы используем процедуру idir_blocks. Это позволяет нам, в зависимости от размера блока загрузить 268/1048/4144 килобайта файла. Если файл еще не загружен до конца, то используется косвенная таблица второго уровня.
Assembler
1
2
3
4
        mov     cx, 1
        call    ddir_blocks
        cmp     ax, [inode + ext2_i.blocks]
        jz      short .exit
В inode также только одна косвенная таблица второго уровня (cx=1). Для загрузки блоков из такой таблицы мы используем процедуру ddir_blocks. Это позволяет нам, в зависимости от размера блока загрузить 64.2/513/4100 мегабайт файла. Если файл опять не загружен до конца (где же столько памяти взять??), то используется косвенная таблица третьего уровня. Ради этого мы уже не будем вызывать подпрограмм, а обработаем ее в этой процедуре.
Assembler
1
2
3
4
5
6
7
8
9
10
11
        push    ax
        push    es
        mov     ax, tmp3_block >> 4
        mov     es, ax
        lodsw
        call    ext2_load_block
        pop     es
        pop     ax
        mov     si, tmp3_block
        mov     cx, [block_dword_size]
        call    ddir_blocks
В inode и эта таблица присутствует только в одном экземпляре (куда же больше?). Это, крайняя возможность, позволяет нам, в зависимости от размера блока, загрузить 16/256.5/4100 гигабайт файла. Что уже является пределом даже для размера файловой системы (4 терабайта).
Assembler
1
2
3
 .exit:
        popa
        ret
Конечно, такие крайности нам при старте будут не к чему, с учетом, что мы находимся в реальном режиме и не можем адресовать больше ~600к памяти.
Кратко рассмотрю вспомогательные функции:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
dir_blocks:
 .repeat:
        push    ax
        lodsw
        call    ext2_load_block
        add     si, 2
        pop     ax
        inc     ax
        cmp     ax, [inode + ext2_i.blocks]
        jz      short .exit
        loop    .repeat
 .exit:
 ret
Эта функция загружает прямые блоки. Ради простоты я пока не обрабатывал блоки номер которых превышает 16 бит. Это создает ограничение на размер файловой системы в 65 мегабайт, а реально еще меньше, поскольку load_block у нас тоже не оперирует с секторами, номер которых больше 16 бит, ограничение по размеру уменьшается до 32 мегабайт. В дальнейшем эти ограничения мы конечно обойдем, а пока достаточно.
В этой функции стоит проверка количества загруженных блоков, для того чтобы вовремя выйти из процедуры считывания.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
idir_blocks:
 .repeat:
        push    ax
        push    es
        mov     ax, tmp_block >> 4
        mov     es, ax
        lodsw
        call    ext2_load_block
        add     si, 2
        pop     es
        pop     ax
        push    si
        push    cx
 
        mov     si, tmp_block
        mov     cx, [block_dword_size]
        call    dir_blocks
 
        pop     cx
        pop     si
 
        cmp     ax, [inode + ext2_i.blocks]
        jz      short .exit
 
        loop    .repeat
 .exit:
        ret
Эта функция обращается в свою очередь к функции dir_blocks, предварительно загрузив в память содержимое косвенного блока. так же имеет контроль длины файла.
Функция ddir_blocks в точности аналогична этой, только для считывания вызывает не dir_blocks, а idir_blocks, поскольку адреса блоков в ней дважды косвенны.
Но мы еще не рассмотрели самого главного. Процедуры, которая по пути файла может загрузить его с диска. Начнем.
Assembler
1
2
3
4
5
ext2_load_file:
        pusha
 
        cmp     byte [si], '/'
        jnz     short .error_exit
Если путь файла не начинается со слэш, то это в данном случае является ошибкой. Мы не оперируем понятием текущий каталог!
Assembler
1
2
       mov     ax, INODE_ROOT ; root_inode
        call    ext2_get_inode
Загружаем корневой inode - он имеет номер 2.
Assembler
1
2
3
4
5
6
 .cut_slash:
        cmp     byte [si], '/'
        jnz     short .by_inode
 
        inc     si
        jmp     short .cut_slash
Уберем лидирующий слэш... или несколько слэшей, такое не является ошибкой.
Assembler
1
2
3
4
 .by_inode:
        push    es
        call    ext2_load_inode
        pop     es
Загрузим содержимое файла. Директории, в том числе и корневая, являются такими же файлами, как и все остальные, только содержат в себе записи о находящихся в директории файлах.
Assembler
1
2
3
4
        mov     ax, [inode + ext2_i.mode]
        and     ax, IMODE_MASK
        cmp     ax, IMODE_REG
        jnz     short .noreg_file
По inode установим тип файла.
Если файл не регулярный, то это может быть директорией. Это проконтролируем ниже.
Assembler
1
2
        cmp     byte [si], 0
        jnz     short .error_exit
Если это файл, который нам надлежит скачать - то в [si] будет содержаться 0, означающий что мы обработали весь путь.
Assembler
1
2
3
 .ok_exit:
        clc
        jmp     short .exit
А поскольку содержимое файла уже загружено, то можем со спокойной совестью вернуть управление. Битом C сообщив, что все закончилось хорошо.
Assembler
1
2
3
 .noreg_file:
        cmp     ax, IMODE_DIR
        jnz     short .error_exit
Если этот inode не является директорией, то это или не поддерживаемый тип файла или ошибка в пути.
Assembler
1
2
        mov     dx, [inode + ext2_i.size]
        xor     bx, bx
Если то, что мы загрузили, является директорией, то со смещения 0 (bx) в этом файле содержится список записей о файлах. Нам нужно выбрать среди них нужную. В dx сохраним длину файла, по ней будем определять коней директории.
Assembler
1
2
3
4
5
6
7
8
9
 .walk_dir:
        lea     di, [es:bx + ext2_de.name]
        mov     cx, [es:bx + ext2_de.name_len]  ; длина имени
        push    si
        repe    cmpsb
        mov     al, [si]
        pop     si
        test    cx, cx
        jnz     short .notfind
Сравниваем имена из директории с именем, на которое указывает si. Если не совпадает - перейдем на следующую запись (чуть ниже)
Assembler
1
2
3
4
        cmp     al, '/'
        jz      short .normal_path
        test    al, al
        jnz     short .notfind
Если совпал, то в пути после имени должно содержаться либо '/' либо 0 - символ конца строки. Если это не так, значит это не подходящий файл.
Assembler
1
2
3
 .normal_path:
        mov     ax, [es:bx + ext2_de.inode]
        call    ext2_get_inode
Загружаем очередной inode.
Assembler
1
2
3
4
        add     si, [es:bx + ext2_de.name_len]
        cmp     byte [si], '/'
        jz      short .cut_slash
        jmp     short .by_inode
И переходим к его обработке. Это продолжается до тех пор, пока не пройдем весь путь.
Assembler
1
2
3
4
5
 .notfind:
        sub     dx, [es:bx + ext2_de.rec_len]
        add     bx, [es:bx + ext2_de.rec_len]
        test    dx, dx
        jnz     short .walk_dir
Если путь не совпадает, и если в директории еще есть записи - продолжаем проверку.
Assembler
1
2
3
4
 .error_exit:
        mov     si, bad_dir
        call    outstring
        stc
Иначе выводим сообщение об ошибке
Assembler
1
2
3
 .exit:
        popa
        ret
И прекращаем работу.
Вот и весь алгоритм. Не смотря на большой размер этого повествования, код занимает всего около 450 байт. А если убрать параноидальные функции, то и того меньше. Не стоит пытаться откомпилировать этот код, все эти модули вы сможете найти на нашем сайте, ссылка на который приведена ниже. Здесь я все это привел для того чтобы объяснить как и что. Надеюсь у меня это получается хоть как-то. Если кто-то что-то не понимает - пишите мне, мой адрес вы всегда можете найти чуть ниже.
В следующей главе рассмотрим форматы выполняемых файлов, используемые в unix. Это нам тоже потребуется на этапе загрузки.

В этой главе речь пойдет о форматах выполняемых файлов. Будут рассмотрены два формата: ELF и PE, и немного коснемся распределения памяти.

Формат ELF


В данном обзоре мы будем говорить только о 32-х битной версии этого формата, ибо 64-х битная нам пока ни к чему.
Любой файл формата ELF (в том числе и объектные модули этого формата) состоит из следующих частей:
  • Заголовок ELF файла;
  • Таблица программных секций (в объектных модулях может отсутствовать);
  • Секции ELF файла;
  • Таблица секций (в выполняемом модуле может отсутствовать);
Ради производительности в формате ELF не используются битовые поля. И все структуры обычно выравниваются на 4 байта.
Теперь рассмотрим типы, используемые в заголовках ELF файлов:
ТипРазмерВыравниваниеКомментарий
Elf32_Addr 44Адрес
Elf32_Half 22 Беззнаковое короткое целое
Elf32_Off 44Смещение
Elf32_SWord44Знаковое целое
Elf32_Word 44Беззнаковое целое
unsigned char11Безнаковое байтовое целое
Теперь рассмотрим заголовок файла:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define EI_NIDENT 16
 
struct elf32_hdr {
  unsigned char e_ident[EI_NIDENT];
  Elf32_Half e_type;
  Elf32_Half e_machine;
  Elf32_Word e_version;
  Elf32_Addr e_entry;  /* Entry point */
  Elf32_Off e_phoff;
  Elf32_Off e_shoff;
  Elf32_Word e_flags;
  Elf32_Half e_ehsize;
  Elf32_Half e_phentsize;
  Elf32_Half e_phnum;
  Elf32_Half e_shentsize;
  Elf32_Half e_shnum;
  Elf32_Half e_shstrndx;
};
Массив e_ident содержит в себе информацию о системе и состоит из нескольких подполей.
C
1
2
3
4
5
6
7
struct {
  unsigned char ei_magic[4];
  unsigned char ei_class;
  unsigned char ei_data;
  unsigned char ei_version;
  unsigned char ei_pad[9];
}
  • ei_magic - постоянное значение для всех ELF файлов, равное { 0x7f, 'E', 'L', 'F'}
  • ei_class - класс ELF файла (1 - 32 бита, 2 - 64 бита который мы не рассматриваем)
  • ei_data - определяет порядок следования байт для данного файла (этот порядок зависит от платформы и может быть прямым (LSB или 1) или обратным (MSB или 2)) Для процессоров Intel допустимо только значение 1.
  • ei_version - достаточно бесполезное поле, и если не равно 1 (EV_CURRENT) то файл считается некорректным.
  • В поле ei_pad операционные системы хранят свою идентификационную информацию. Это поле может быть пустым. Для нас оно тоже не важно.
  • Поле заголовка e_type может содержать несколько значений, для выполняемых файлов оно должно быть ET_EXEC равное 2
  • e_machine - определяет процессор на котором может работать данный выполняемый файл (Для нас допустимо значение EM_386 равное 3)
  • Поле e_version соответствует полю ei_version из заголовка.
  • Поле e_entry определяет стартовый адрес программы, который перед стартом программы размещается в eip.
  • Поле e_phoff определяет смещение от начала файла, по которому располагается таблица программных секций, используемая для загрузки программ в память.
Не буду перечислять назначение всех полей, не все нужны для загрузки. Лишь еще два опишу.
  • Поле e_phentsize определяет размер записи в таблице программных секций.
  • И поле e_phnum определяет количество записей в таблице программных секций.
Таблица секций (не программных) используется для линковки программ. мы ее рассматривать не будем. Так же мы не будем рассматривать динамически линкуемые модули. Тема эта достаточно сложная, для первого знакомства не подходящая.
Теперь про программные секции. Формат записи таблицы программных секций таков:
C
1
2
3
4
5
6
7
8
9
10
struct elf32_phdr {
  Elf32_Word p_type;
  Elf32_Off p_offset;
  Elf32_Addr p_vaddr;
  Elf32_Addr p_paddr;
  Elf32_Word p_filesz;
  Elf32_Word p_memsz;
  Elf32_Word p_flags;
  Elf32_Word p_align;
};
Подробнее о полях.
  • p_type - определяет тип программной секции. Может принимать несколько значений, но нас интересует только одно. PT_LOAD (1). Если секция именно этого типа, то она предназначена для загрузки в память.
  • p_offset - определяет смещение в файле, с которого начинается данная секция.
  • p_vaddr - определяет виртуальный адрес, по которому эта секция должна быть загружена в память.
  • p_paddr - определяет физический адрес, по которому необходимо загружать данную секцию. Это поле не обязательно должно использоваться и имеет смысл лишь для некоторых платформ.
  • p_filesz - определяет размер секции в файле.
  • p_memsz - определяет размер секции в памяти. Это значение может быть больше предыдущего.
  • Поле p_flag определяет тип доступа к секциям в памяти. Некоторые секции допускается выполнять, некоторые записывать. Для чтения в существующих системах доступны все.

Загрузка формата ELF


С заголовком мы немного разобрались. Теперь я приведу алгоритм загрузки бинарного файла формата ELF. Алгоритм схематический, не стоит рассматривать его как работающую программу.
int LoadELF (unsigned char *bin)
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
  struct elf32_hdr *EH = (struct elf32_hdr *)bin;
  struct elf32_phdr *EPH;
 
  if (EH->e_ident[0] != 0x7f ||         // Контролируем MAGIC
      EH->e_ident[1] != 'E' ||
      EH->e_ident[2] != 'L' ||
      EH->e_ident[3] != 'F' ||
      EH->e_ident[4] != ELFCLASS32 ||   // Контролируем класс
      EH->e_ident[5] != ELFDATA2LSB ||  // порядок байт
      EH->e_ident[6] != EV_CURRENT ||   // версию
      EH->e_type != ET_EXEC ||          // тип
      EH->e_machine != EM_386 ||        // платформу
      EH->e_version != EV_CURRENT)      // и снова версию, на всякий случай
     return ELF_WRONG;
 
  EPH = (struct elf32_phdr *)(bin + EH->e_phoff);
 
  while (EH->e_phnum--) {
        if (EPH->p_type == PT_LOAD)
           memcpy (EPH->p_vaddr, bin + EPH->p_offset, EPH->p_filesz);
 
        EPH = (struct elf32_phdr *)((unsigned char *)EPH + EH->e_phentsize));
  }
 
  return ELF_OK;
}
По серьезному стоит еще проанализировать поля EPH->p_flags, и расставить на соответствующие страницы права доступа, да и просто копирование здесь не подойдет, но это уже не относится к формату, а к распределению памяти. Поэтому сейчас об этом не будем говорить.

Формат PE


Во многом он аналогичен формату ELF, ну и не удивительно, там так же должны быть секции, доступные для загрузки.
Как и все в Microsoft формат PE базируется на формате EXE. Структура файла такова:
  • 00h - EXE заголовок (не буду его рассматривать, он стар как Дос.
  • 20h - OEM заголовок (ничего существенного в нем нет);
  • 3сh - смещение реального PE заголовка в файле (dword).
  • таблица перемещения stub;
  • stub;
  • PE заголовок;
  • таблица объектов;
  • объекты файла;
stub - это программа, выполняющаяся в реальном режиме и производящая какие-либо предварительные действия. Может и отсутствовать, но иногда может быть нужна.
Нас интересует немного другое, заголовок PE.
Структура его такая:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct pe_hdr {
  unsigned long  pe_sign;
  unsigned short pe_cputype;
  unsigned short pe_objnum;
  unsigned long  pe_time;
  unsigned long  pe_cofftbl_off;
  unsigned long  pe_cofftbl_size;
  unsigned short pe_nthdr_size;
  unsigned short pe_flags;
  unsigned short pe_magic;
  unsigned short pe_link_ver;
  unsigned long  pe_code_size;
  unsigned long  pe_idata_size;
  unsigned long  pe_udata_size;
  unsigned long  pe_entry;
  unsigned long  pe_code_base;
  unsigned long  pe_data_base;
  unsigned long  pe_image_base;
  unsigned long  pe_obj_align;
  unsigned long  pe_file_align;
 
  // ... ну и еще много всякого, неважного.
};
Много всякого там находится. Достаточно сказать, что размер этого заголовка - 248 байт.
И главное что большинство из этих полей не используется. (Кто так строит?) Нет, они, конечно, имеют назначение, вполне известное, но моя тестовая программа, например, в полях pe_code_base, pe_code_size и тд содержит нули но при этом прекрасно работает. Напрашивается вывод, что загрузка файла осуществляется на основе таблицы объектов. Вот о ней то мы и поговорим.
Таблица объектов следует непосредственно после PE заголовка. Записи в этой таблице имеют следующий формат:
C
1
2
3
4
5
6
7
8
9
struct pe_ohdr {
  unsigned char o_name[8];
  unsigned long o_vsize;
  unsigned long o_vaddr;
  unsigned long o_psize;
  unsigned long o_poff;
  unsigned char o_reserved[12];
  unsigned long o_flags;
};
  • o_name - имя секции, для загрузки абсолютно безразлично;
  • o_vsize - размер секции в памяти;
  • o_vaddr - адрес в памяти относительно ImageBase;
  • o_psize - размер секции в файле;
  • o_poff - смещение секции в файле;
  • o_flags - флаги секции;
Вот на флагах стоит остановиться поподробнее.
00000004h используется для кода с 16 битными смещениями
00000020h секция кода
00000040h секция инициализированных данных
00000080h секция неинициализированных данных
00000200h комментарии или любой другой тип информации
00000400h оверлейная секция
00000800h не будет являться частью образа программы
00001000h общие данные
00500000h выравнивание по умолчанию, если не указано иное
02000000h может быть выгружен из памяти
04000000h не кэшируется
08000000h не подвергается страничному преобразованию
10000000h разделяемый
20000000h выполнимый
40000000h можно читать
80000000h можно писать
Опять таки не буду с разделяемыми и оверлейными секциями, нас интересуют код, данные и права доступа.
В общем, этой информации уже достаточно для загрузки бинарного файла.

Загрузка формата PE


C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int LoadPE (unsigned char *bin)
{
  struct elf32_hdr *PH = (struct pe_hdr *)
                (bin + *((unsigned long *)&bin[0x3c]));
// Конечно комбинация не из понятных... просто берем dword по смещению 0x3c
// И вычисляем адрес PE заголовка в образе файла
  struct elf32_phdr *POH;
 
  if (PH == NULL ||              // Контролируем указатель
      PH->pe_sign != 0x4550 ||   // сигнатура PE {'P', 'E', 0, 0}
      PH->pe_cputype != 0x14c || // i386
      (PH->pe_flags & 2) == 0)     // файл нельзя запускать!
     return PE_WRONG;
 
  POH = (struct pe_ohdr *)((unsigned char *)PH + 0xf8);
 
  while (PH->pe_obj_num--) {
        if ((POH->p_flags & 0x60) != 0)
           // либо код либо инициализированные данные
           memcpy (PE->pe_image_base + POH->o_vaddr,
                        bin + POH->o_poff, POH->o_psize);
 
        POH = (struct pe_ohdr *)((unsigned char *)POH +
                                        sizeof (struct pe_ohdr));
  }
 
  return PE_OK;
}
Это опять таки не готовая программа, а алгоритм загрузки.
И опять таки многие моменты не освещаются, так как выходят за пределы темы.
Но теперь стоит немного поговорить про существующие системные особенности.

Системные особенности

Не смотря на гибкость средств защиты, имеющихся в процессорах (защита на уровне таблиц дескрипторов, защита на уровне сегментов, защита на уровне страниц) в существующих системах (как в Windows, так и в Unix) полноценено используется только страничная защита, которая хотя и может уберечь код от записи, но не может уберечь данные от выполнения. (Может быть, с этим и связано изобилие уязвимостей систем?)
Все сегменты адресуются с нулевого линейного адреса и простираются до конца линейной памяти. Разграничение процессов производится только на уровне страничных таблиц.
В связи с этим все модули линкуются не с начальных адресов, а с достаточно большим смещением в сегменте. В Windows используется базовый адрес в сегменте - 0x400000, в юникс (Linux или FreeBSD) - 0x8048000.
Некоторые особенности так же связаны со страничной организацией памяти.
ELF файлы линкуются таким образом, что границы и размеры секций приходятся на 4-х килобайтные блоки файла.
А в PE формате, не смотря на то, что сам формат позволяет выравнивать секции на 512 байт, используется выравнивание секций на 4к, меньшее выравнивание в Windows не считается корректным.
продолжение
4
Ушел с форума
Автор FAQ
 Аватар для Mikl___
16373 / 7685 / 1080
Регистрация: 11.11.2010
Сообщений: 13,759
06.12.2013, 12:15  [ТС]
Глава #11
Процесс загрузки.


То, что я до сих пор сделал пока рассчитано только на работы с дисками 1,4Мб, то есть с флопами. Это конечно ограничение в некоторой степени, но пока система еще далеко не готова, этого достаточно. Естественно это еще не окончательный вариант. Да и можно ли говорить об окончательности программных продуктов? Нет предела совершенству.
В обязанности бутсектора входит следующее:
  1. Загрузить с диска дополнительные части кода и служебную информацию файловой системы.
  2. Загрузить с диска файл сценария (конфигурации) загрузки.
  3. Загрузить с диска ядро и модули.
  4. Перейти в защищенный режим.
  5. Передать управление ядру.
Если с первым и двумя последними пунктами все просто и компактно, то второй и третий пункт требуют возможности работы с файловой системой, а третий пункт помимо этого должен знать структуру бинарных форматов. На все это не хватает 512 байт, отводимых для бутсектора. Наш бутсектор занимает больше - один килобайт.
В файловой системе EXT2 с этим не возникает никаких проблем, поскольку первый килобайт файловой системы не используется.
В FAT это немного сложнее. Служебная структура, именуемая Boot Sector Record (BSR), содержит в себе все необходимые поля для выделения для загрузочного сектора места более чем 512 байт. Но как это сделать при форматировании, стандартными средствами, я не нашел. И если формат диска не соответствует каким-то внутренним представлениям Windows, то содержимое такого нестандартного диска может быть испорчено. Выход был найден случайно. Как оказалось утилита format хоть и не имеет таких параметров командной строки, но перед форматированием берет информацию из BSR. И если предварительно заполнить эту структуру (с нужными нам параметрами), а потом уже форматировать, то все получается так, как хочется нам. Таким образом, у меня получилось сделать диск, у которого два сектора зарезервированы (там будет размещаться boot), и одна копия FAT.
Ну теперь давайте по порядку рассмотрим все этапы работы бутсектора.

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

Бутсектор загружается БИОСом по адресу 0:7c00h занимает он 512 байт. Память начиная с адреса 0:7e00h свободна. но в эту память мы загрузим второй сектор бута. Одновременно загружается информация необходимая для обслуживания файловой системы. Для EXT2 дополнительно необходимо загрузить два килобайта (суперблок и дескрипторы групп), для FAT немного больше - 4,5 килобайта (первая копия FAT).
Assembler
1
2
        mov ax, 0x7e0
        mov es, ax
Адрес 0:7e00h идентичен адресу 7e0h:0. Вторым вариантом мы и будем пользоваться, потому что наша процедура загрузки секторов размещает их по сегментному адресу, хранящемуся в es.
Assembler
1
       mov ax, 1
В ax номер сектора, с которого начинается чтение (первый сектор является нулевым (каламбур . И далее все зависит от файловой системы.
Assembler
1
2
%ifdef EXT2FS
        mov cx, 5
Для EXT2 загружается 5 секторов - второй сектор бутсектора (1 сектор), суперблок файловой системы (2 сектора) и дескрипторы групп (2 сектора).
Assembler
1
2
%elifdef FATFS
        mov cx, 10
Для FAT загружается 10 секторов - второй сектор бутсектора (1 сектор), таблица FAT - 9 секторов (такой размер она имеет на floppy дисках).
Assembler
1
2
3
4
%else
  %error File system not specified
%endif
        call load_block
Все. первый пункт загрузки выполнен.
Функции обслуживания файловых систем имеют одинаковый интерфейс. Cобственно их всего две fs_init и fs_load_file. Естественно у них различаются реализации, но в процессе компиляции выбирается используемая файловая система. Для совместного использования нам никак не хватит одного килобайта, да и не за чем это.

Загрузка с диска файла сценария (конфигурации) загрузки

Из-за сложности VFAT (FAT с длинными именами) он не реализован. Все имена на диске FAT должна иметь формат 8.3
В файловой системе FAT я не оперирую принятыми в MS системах именами дисков и при указании пути использую путь относительно корневой директории диска (как это делается в юникс системах).
Файл конфигурации у нас пока называется boot.rc и находится в каталоге /etc. Формат у этого файла достаточно нестрогий. Из-за нехватки места в boot секторе там сделана реакция только на ключевые слова, которыми являются:
  • kern[el] - файл ядра;
  • modu[le] - файл модуля;
  • #end - конец файла конфигурации.
Использование этих слов в другом контексте недопустимо.
Предварительно проинициализировав файловую систему
Assembler
1
        call fs_init
Мы загружаем этот файл с диска.
Assembler
1
2
3
4
5
6
7
        mov si, boot_config
        call fs_load_file
 
        ...
 
boot_config:
        db '/etc/boot.rc', 0
Содержимое файла конфигурации такое:
Code
1
2
kernel /boot/kernel
#end
Модулей у нас пока никаких нет, да и ядро еще в зачаточном состоянии. Но речь сейчас не об этом.

Загрузка с диска ядра и модулей

Про этот момент я не буду особо расписывать, желающие могут посмотреть в исходниках, которые в скором времени появятся на сайте.
Скажу только, что программа анализирует файл конфигурации, в соответствии с ключевыми словами загружает ядро (которое может быть в единственном экземпляре) и любое количество модулей. Общий объем ядра и модулей ограничен свободным размером базовой памяти (около 600к).
Перейдем к предпоследнему пункту.

Переход в защищенный режим

Бутсектор не особо беспокоится об организации памяти в системе - это забота ядра. Для перехода в защищенный режим он описывает всего два сегмента: сегмент кода и сегмент данных. оба сегмента имеют базовый адрес - 0 и предел в 4 гигабайта (это нам пригодиться для проверки наличия памяти).
Перед переходом в защищенный режим нам необходимо включить адресную линию A20. По моим сведениям этот механизм ввели в пору 286 для предотвращения несанкционированных обращений к памяти свыше одного мегабайта (непонятно зачем?). Но поскольку это имеет место быть - нам это нужно обрабатывать, иначе каждый второй мегабайт будет недоступен. Делается это почему-то через контроллер клавиатуры (еще одна загадка).
Assembler
1
2
3
4
        mov al, 0xd1
        out 0x64, al
        mov al, 0xdf
        out 0x60, al
После этого можно переходить в защищенный режим.
Assembler
1
        lgdt [gd_desc]
В регистр gdtr загружается дескриптор GDT.
Assembler
1
2
        push byte 2
        popf
Очищается регистр флагов.
Assembler
1
2
3
        mov eax, cr0
        or al, 1
        mov cr0, eax
Включается защищенный режим. Не смотря на то, что процессор уже находится в защищенном режиме, мы пока работаем в старых адресах. Чтобы от них уйти нам нужно сделать дальный переход в адреса защищенного режима.
Assembler
1
2
3
4
        jmp 16:.epm
 
        BITS 32
 .epm:
16 в этом адресе перехода - это не сегмент. Это селектор сегмента кода.
Assembler
1
2
3
4
5
6
7
        mov ax, 8
        mov ds, ax
        mov es, ax
 
        ; Ставим стек.
        mov ss, ax
        movzx esp, sp
После всего этого мы инициализируем сегментные регистры соответствующими селекторами, в том числе и сегмент стека, но указатель стека у нас не меняется, только теперь он становится 32-х битным.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
        ...
 
gd_table:
        ; пеpвый дескpиптоp - данные и стек
        istruc descriptor
        at descriptor.limit_0_15, dw 0xffff
        at descriptor.base_0_15, dw 0
        at descriptor.base_16_23, db 0
        at descriptor.access,  db 0x92
        at descriptor.limit_16_19_a,  db 0xcf
        at descriptor.base_24_31, db 0
        iend
 
        ; втоpой дескpиптоp - код
        istruc descriptor
        at descriptor.limit_0_15, dw 0xffff
        at descriptor.base_0_15, dw 0
        at descriptor.base_16_23, db 0
        at descriptor.access,  db 0x9a ; 0x98
        at descriptor.limit_16_19_a,  db 0xcf
        at descriptor.base_24_31, db 0
        iend
Это GDT - Глобальная таблица дескрипторов. Здесь всего два дескриптора, но во избежание ошибок в адресации обычно вводится еще один дескриптор - нулевой, который не считается допустимым для использования. Мы не будем резервировать для него место специально, просто начало таблицы сместим на 8 байт выше.
Assembler
1
2
3
gd_desc:
        dw 3 * descriptor_size - 1
        dd gd_table - descriptor_size
А это содержимое регистра GDTR. Здесь устанавливается предел и базовый адрес дескриптора. обратите внимание на базовый адрес, здесь происходит резервирование нулевого дескриптора.

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

Передача управления ядру

Здесь вообще все просто. Когда мы загрузили ядро, в файле ядра мы определили адреса сегмента кода и сегмента данных. Не смотря на то, что ядро имеет вполне конкретные смещения в сегменте (которые задаются при компиляции), код инициализации ядра рассчитан на работу без привязки к адресам. Это нужно для определения количества памяти, после перевода ядра на свои адреса доступ ко всей памяти будет для ядра затруднен в связи с включением механизма страничного преобразования.
Итак, переходим к выполнению кода ядра.
Assembler
1
2
3
        mov ebx, kernel_data
        mov eax, [ebx + module_struct.code_start]
        jmp eax
В этом фрагменте в eax записывается адрес начала кодового сегмента ядра.
Так как сегмент кода у нас занимает всю виртуальную память, нам не важно где находится ядро (хотя мы знаем, что оно было загружено в базовую память). Мы просто передаем ему управление.
О ядре мы начнем говорить в следующем выпуске. С загрузчиком мы практически закончили. Он уже обладает достаточной для нас функциональностью.
В этой главе
Определение количества памяти.
Динамическое распределение памяти в процессах.

Определение количества памяти через BIOS
.


Ну, начнем с исторических функций.
Давным-давно, когда даже Билл Гейтс говорил что 640 килобайт хватит всем, но не у всех были эти 640 килобайт. в биосах существовала функция определения количества базовой памяти.
int 12h
Выходные параметры:
  • ax - размер базовой памяти в килобайтах
.
Сейчас уже вряд ли кому придет в голову, что базовой памяти может быть меньше 640 килобайт. но мало ли...
Появлялись новые процессоры, и размеры памяти стали расти. в связи с чем появилась функция определения количества расширенной памяти.
int 15h fn 88h
Входные параметры:
  • ah = 88h
Выходные параметры:
  • ax - размер расширенной памяти в килобайтах
Возможно из за архитектуры 286-х процессоров (которым размер шины адреса не позволяет иметь больше чем 16 мегабайт памяти) эта функция часто имеет аналогичное ограничение и результат в ax не может превышать 3с00h (Что составляет 15Мб).
Но, опять таки, появились новые процессоры. 16 мегабайт стало мало. Вследствие этого появилась еще одна функция BIOS:
int 15h fn 0E801h
Входные параметры:
  • ax = 0E801h
.Выходные параметры:
  • ax - размер расширенной памяти в килобайтах до 16Mb;
  • bx - размер расширенной памяти в блоках по 64к свыше 16Мб;
  • cx - размер сконфигурированный расширенной памяти в килобайтах до 16Mb;
  • dx - размер сконфигурированной расширенной памяти в блоках по 64к свыше 16Мб
Не знаю, что означает сконфигурированная память. Так написано в описании.
Здесь производители BIOS видимо оказались неединодушны. Некоторые версии в ax и bx возвращают 0, это значит что размер памяти следует определять из cx, dx.
Но видимо и 4 гигабайт оказалось мало. В новых BIOS появилась еще одна функция.

int 15h fn 0E820h
Входные параметры:
  • eax = e820h;
  • edx = 534d4150h ('SMAP');
  • ebx - смещение от начала карты памяти;
  • eсx - Размер буфера;
  • es:edi - Адрес буфера для размещения карты памяти
Выходные параметры:
  • eax - 534d4150h ('SMAP');
  • ebx - следующее смещение от начала карты памяти, если = 0, то вся карта передана;
  • ecx - Количество возвращенных байт;
  • буфер заполнен информацией;
Эту функцию нужно вызывать в цикле до тех пор, пока не будет прочитана вся карта памяти.
Формат структуры таков:
C
1
2
3
4
5
struct {
  long long base;
  long long length;
  long      type;
};
Поле type может содержать следующие значения:
  1. Доступно для использования операционной системой;
  2. Зарезервировано (например, ROM);
  3. ACPI reclaim memory (Доступно для операционной системы после прочтения таблицы ACPI;
  4. ACPI NVS memory (Операционной системе требуется сохранять эту память между NVS сессиями).
Проверить как работает эта функция у меня не получилось, мой BIOS ее не поддерживает.
Но в заключение скажу следующее. Все функции в случае ошибки (если функция не поддерживается) возвращают установленный флаг cf. В случае отсутствия новых функций необходимо обращаться к более старым.
Функции BIOS не работают в защищенном режиме, поэтому все эти операции необходимо производить еще до перехода в защищенный режим.


Определение размера памяти другими способами


Помимо функций BIOS есть еще много других способов.
Самый простой - помсмотреть память самому. Делается это из защищенного режима, страничное преобразование должно быть выключено, адресная линия A20 должна быть включена.
Можно замерить объем памяти от нуля, но поскольку в первом мегабайте есть дыры (видеопамять, биосы, просто дыры), удобнее делать это начиная с первого мегабайта.
Вовсе не обязательно проверять каждый байт, достаточно проверять один байт на какое-то определенное количество памяти. Определенным количеством памяти можно посчитать мегабайт, но лучше (хотя и медленнее) за единицу памяти принять одну страницу памяти (4к).
Во избежание неприятностей память лучше не разрушать, а восстанавливать в первоначальном виде. делается это примерно так:
Assembler
1
2
xchg [ebx], eax
xchg [ebx], eax
Если после этого в eax содержится то же значение, которое было до того, значит память присутствует по данному адресу. Если возвратилось 0ffffffffh, значит память отсутствует, если же что ни будь другое - то это может быть ROM, хотя после мегабайта вы вряд ли встретите какой либо BIOS. В любом случае если память по текущему адресу не обнаружена, значит, память закончилась и дальше искать чревато... существуют еще различные типы памяти (ACPI например) которую не стоит трогать.
Из защищенного режима можно воспользоваться содержимым CMOS, некоторые ячейки в нем BIOS заполняет определенными при начальном тесте системы значениями. Но здесь все не так однозначно как хотелось бы. Разные версии BIOS могут хранить значения в разных местах.
  • 15h - Базовая память в килобайтах (младший байт) (IBM);
  • 16h - Базовая память в килобайтах (старший байт) (IBM);
  • 17h - Расширенная память в килобайтах (младший байт) (IBM);
  • 18h - Расширенная память в килобайтах (старший байт) (IBM);
  • 30h - Расширенная память в килобайтах (младший байт) (IBM);
  • 31h - Расширенная память в килобайтах (старший байт) (IBM);
  • 34h - Расширенная память более 16Мб (блоками по 64к) (младший байт) (AMI);
  • 35h - Расширенная память более 16Мб (блоками по 64к) (старший байт) (AMI);
  • 35h - Расширенная память (блоками по 64к) (младший байт) (AMI WinBIOS);
  • 36h - Расширенная память (блоками по 64к) (старший байт) (AMI WinBIOS);
Байты 30-31 принято считать стандартными, но они определяют только 64Мб памяти. Не очень то подходят для использования.

Динамическое распределение памяти


Почти любое приложение пользуется динамически выделяемыми блоками памяти (известная, наверное, всем функция malloc в c). Сейчас мы поговорим о том, как это все работает.
Подходить к этому можно по разному, но принцип везде прослеживается один. На каждый блок памяти необходимо иметь структуру, описывающую занятось блока, его размер. В примитивной реализации это может выглядеть так, как это сделано в DOS.
В ДОСе вся память на равных правах принадлежит всем запущенным программам. Но чтобы операционная система могла как-то контролировать использование памяти, в ДОСе применяются MCB (Memory Control Block). Формат этого блока таков:
C
1
2
3
4
5
6
7
struct {
  char           Signature;
  unsigned short OwnerId;
  unsigned short SizeParas;
  char           Reserved[3];
  char           OwnerName[8];
};
Размер структуры 16 байт (1 параграф памяти) и эта структура непосредственно предшествует описываемому блоку памяти.
Размер блока указывается в параграфах в поле SizeParas. Такая структура вполне подходит для ограниченной по размерам памяти DOS, но для приложений она не очень то применима. Разница состоит в том, что в случае ДОС, чтобы найти блок свободной памяти (Такие блоки помечаются нулевым OwnerId), необходимо пройти по всем блокам от начала цепочки, до тех пор, пока не встретится свободный блок соответствующего размера. В ДОСе имеется функция, с помощью которой можно получить адрес первого блока (Base MCB) (int 21h, fn 52h).
Столь медленный поиск не страшен для DOS, у которого количество блоков редко превышает несколько десятков, но в приложениях поиск по цепочке блоков может быть достаточно долгой процедурой.
Поэтому в приложениях обычно применяется другой алгоритм, который заключается в следующем. (Я рассмотрю наиболее быстрый алгоритм, вариантов, конечно, может быть множество):
У каждого блока, как я уже говорил, есть два основных параметра: размер и флаг занятости. Оба эти параметра размещаются в одном двойном слове памяти. Поскольку как начало блока, так и его размер обычно выравниваются на четное число байт, младшие биты размера остаются неиспользуемыми (всегда равны нулю) и флаг занятости размещается в одном из них.
Этот параметр блока размещается перед началом и по окончанию блока. Начальный параметр следующего блока соответственно будет размещен непосредственно после конечного параметра предыдущего, что позволит анализировать цепочку блоков с одинаковым успехом в обоих направлениях.
Свободные блоки памяти размещаются в списках в соответствии со своим размером. Размер блоков в списках увеличивается в геометрической прогрессии. К примеру, в первом списке хранятся блоки до 16 байт длиной, во втором до 32-х байт длиной и так далее. Такая система позволяет, зная размер необходимого блока, сразу же выбирать из соответствующего списка подходящий блок и не требует поиска по всем блокам. Для организации списков к блоку добавляются несколько параметров (поскольку блок свободен, и его внутреннее пространство может быть использовано для любых целей, эти параметры размещаются в самом блоке). К этим параметрам относятся ссылка на следующий свободный блок в списке, и номер списка в котором находится блок. (Это позволяет ускорить удаление блока из списка).
Для выделения блока необходимого размера сперва проверяется список соответствующего размера, в котором может потребоваться поиск блока. Если соответствующий список пуст, то проверяется следующий список, в котором уже не требуется проводить поиска, поскольку любой блок заведомо больше нужного размера. Найденный пустой блок делится на две части, вторая - не нужная часть оформляется как свободная и помещается в соответствующий список, а первая часть оформляется как занятая и возвращается программе.
Из-за необходимости введения дополнительных параметров для свободных блоков памяти минимальный размер блока не может быть меньше 8 байт. Даже если пользователь захочет получить блок меньшего размера, выделится блок в 8 байт длиной.
При освобождении блока, если предыдущий или последующий блоки пусты, он объединяется с ними в один блок и добавляется в список соответствующего размера. Использованные окружающие блоки удаляются из тех списков, в которых они были записаны ранее.
Для того, чтобы предотвратить попытку объединения первого блока памяти (при его освобождении) с предшествующим ему, перед первым блоком ставится параметр с флагом занятости. То же самое делается и для последнего блока памяти, но только после него.
4
Ушел с форума
Автор FAQ
 Аватар для Mikl___
16373 / 7685 / 1080
Регистрация: 11.11.2010
Сообщений: 13,759
11.02.2014, 06:28  [ТС]
Нестандартный загрузчик
Статья Рустэма Галеева aka Roustem от 21.07.2003 с сайта WASM.RU
Данная статья предназначена для тех, кто хочет подробнее узнать о процессе начальной загрузки компьютера и поэкспериментировать с ним. Никакого специального программного обеспечения не потребуется: все описываемые действия проделываются с использованием отладчика debug, входящего в состав Windows 95/98/ME/NT/XP. Проделав описанные в статье действия на своем компьютере, вы сможете создать вспомогательную программу wb.com и установить в реестре ее ассоциацию с расширением файлов .bot, что позволит одним щелчком мыши переносить созданные с помощью debug файлы на дискету, сделав ее загружаемой.
Начальная загрузка
После проверки программой BIOS подключенного оборудования управление передается процедуре начальной загрузки с дисков, доступ к которой можно получить через прерывание INT 19h. Данная процедура загружает всего один сектор (первый) с нулевой дорожки нулевой головки, размещая его в памяти по адресу 7С00h, и передавая управление после загрузки сектора по этому же адресу. Ответственность за дальнейшую загрузку компьютера ложится на код, содержащийся в этом секторе.
Совершенно очевидно, что из-за ограниченного объема сектора (512 байт) загрузочный код разнообразием не блещет и в основном сводится к операции загрузки с диска в оперативную память дополнительных секторов, содержащих ядро операционной системы, и передаче управления по соответствующему адресу памяти. Кроме кода загрузки, в стандартном загрузочном секторе содержится также приведенный ниже в таблице блок параметров BIOS с данными о форматировании диска.
Смещение
Размер,
байт
Содержание
3 8 Аббревиатура и номер версии ОС
0Bh 2 Число байтов в секторе (512)
0Dh 1 Число секторов в кластере
0Eh 2 Число резервных секторов в резервной области раздела
10h 1 Число копий FAT в разделе (2)
11h 2 Количество 32-байтных дескрипторов файлов в корневом каталоге
13h 2 Общее число секторов в разделе
15h 1 Тип носителя информации (для современных дискет F0h)
16h 2 Количество секторов, занимаемых одной копией FAT
18h 2 Число секторов на дорожке
1Ah 2 Число головок
1Ch 4 Число скрытых секторов перед началом раздела (для дискет 0)
20h 4 0 (используется FAT32)
24h 1 Номер дисковода (для дискет - от 0 до 3)
25h 1 0 (для Windows NT)
26h 1 Признак расширенной загрузочной записи (29h)
27h 4 Номер логического диска (создается при форматировании)
2Bh 11 Метка диска (текстовая строка)
36h 8 Аббревиатура типа файловой системы
3Eh  Начало загрузочного кода
Первые три байта загрузочного сектора содержат инструкцию безусловного перехода (JMP) на код, начинающийся после блока параметров BIOS. Кроме того, последними двумя байтами являются 55h и AAh (сигнатура - признак загрузочного сектора логического диска).
Загрузочный код
Приступим к созданию нашего загрузчика. Первым делом надо загрузить весь код с дискеты в оперативную память, затем передать ему управление. При этом необходимо учесть, что мы не можем воспользоваться услугами операционной системы - можно использовать лишь низкоуровневые функции BIOS. Для работы с дисками существуют функции прерывания INT 13h, при вызове которых используются следующие регистры:
регистрзначение
AH функция:
  • 0 - сброс
  • 2 - чтение сектора
  • 3 - запись сектора
AL число секторов (для чтения или записи)
BX буфер памяти для ввода-вывода данных (в паре ES:BX)
CH номер дорожки
CL номер начального сектора
DH номер головки (для дискет - 0 или 1)
DL номер дисковода (0=А, 1=B)
В случае успешного завершения функции флаг переноса (CF) сбрасывается; при ошибке он установлен.

Работа с дисководом для гибких дисков имеет свои особенности; это довольно медленное устройство, дискеты можно вынимать, поэтому ошибки случаются сравнительно часто (например, при обращении к дисководу мотор не успевает "разогнаться"), поэтому операции чтения или записи при ошибках обычно повторяют по 3 раза, и лишь после трех последовательных неудач выдают сообщение об ошибке конечному пользователю.

Для наших целей мы обойдемся без использования файловой системы. Используем простую линейную модель: весь код у нас будет записан последовательно за загрузочным сектором - сектора 2-18 нулевой дорожки со стороны нулевой головки (в предположении, что используется стандартная дискета на 1,44 Мб), затем сектора 1-18 нулевой дорожки со стороны первой головки; далее переходим на первую дорожку со стороны нулевой головки и т.д. Считывание, естественно, должно производиться в том же порядке. При этом необходимо где-то записать количество секторов, которые необходимо таким образом считать; сохраним это значение в двух байтах после блока параметров BIOS (со смещением 3Eh). Не забудьте, что загрузочный сектор размещается в памяти, начиная с адреса 7C00; это значит, что в памяти число секторов, которые необходимо прочитать с дискеты, будет находиться по адресу 7C3E, а с адреса 7C40 будет код. По адресу же 7C00 должна находится команда безусловного перехода
Assembler
1
JMP 7C40h
Для последовательного чтения секторов организуем цикл, после каждой итерации значение по адресу 7C3E будем уменьшать на 1, пока оно не достигнет 0 (это и будет сигналом выхода из цикла). Необходимо также предусмотреть сообщение об ошибке на случай трех последовательных безуспешных попыток чтения сектора; для этой цели используем незамысловатый текст "Read error" (необходимо помнить, что при начальной загрузке кодовая таблица с кириллицей в видеопамять не загружена, поэтому для сообщений нельзя использовать русский шрифт).
Итак, по адресу 7C40h размещается следующий код (все числа в коде - шестнадцатеричные):
Assembler
1
2
3
4
5
6
7
MOV AL,1 
MOV BX,7E00h ; разместить в памяти, начиная с адреса 7E00, т.е.    
            ; следующие 512 байт после 7C00
XOR CH,CH 
MOV CL,2 ; сектор 2 (1-й сектор уже считан)
XOR DX,DX ; DH=0 (головка 0), DL=0 (дисковод 0,   
            ; т.е. А:
Начальная инициализация произведена. Далее начинается цикл, в котором читается сектор (до трех попыток при ошибке). После успешного считывания данных номер сектора (AL) увеличивается на 1, и если он превысил значение 18, устанавливается равным 1, а номер головки (DH) увеличивается на 1. Аналогично номеру сектора, если номер головки превысит 1, устанавливается 0, и увеличивается номер дорожки (CH). Номер дорожки должен находиться в пределах 0-79, т.е. не должен достигать 80 (50h); если это произошло, выдается сообщение об ошибке. Значение счетчика считываемых секторов (по адресу 7C3E) уменьшается на 1, и если оно еще не достигло 0, цикл повторяется:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
LOOP: MOV SI,3 
REPEAT: MOV AH,2 
 INT 13h 
 JNC OK 
 ; Попадание сюда означает, что была ошибка чтения
 DEC SI 
 JZ ERROR ; 3 безуспешные попытки
 XOR AH,AH ; 
 INT 13h ; сброс дисковода для повторной попытки
 JMP REPEAT
OK: ; Сюда попадаем, если не было ошибки чтения
 INC CL 
 CMP CL,12h 
 JNG NEXT 
 ; (Число секторов превысило 18):
 MOV CL,1 
 INC DH 
 CMP DH,1 
 JNG NEXT 
 ; (Номер головки превысил 1):
 XOR DH,DH 
 INC CH 
 CMP CH,50h 
 JGE ERROR 
 
NEXT: DEC WORD PTR ds:[7C3Eh] ; уменьшить счетчик читаемых     
                            ; секторов (по адресу 7C3E) на 1
 JZ END ; если достигнут 0 - загрузка окончена    
 
 ; Сюда попадаем, если прочитаны не все сектора
 ADD BX,200h ; следующий сектор разместить в памяти непосредственно после настоящего
 JMP LOOP 
END: JMP 7E00h ; все сектора прочитаны: управление      
                    ; передается по адресу загрузки второго сектора
Осталось только реализовать вывод сообщения об ошибке. Для этого воспользуемся функцией 13h прерывания BIOS 10h. Данная функция выводит символьную строку.
регистрзначение
AH 13h (функция)
AL один из 4 возможных сервисов (мы используем 3 - вывод символа с атрибутом и перевод курсора)
BH страница видеопамяти (обычно 0)
BP адрес выводимой строки (в паре ES:BP)
CX длина строки (в символах)
DX координаты на экране (DH - строка, DL - столбец)
Итак, ошибку обрабатывает следующий код:
Assembler
1
2
3
4
5
6
7
ERROR:mov ax,1303h
 xor bx,bx
 mov es,bx 
 mov bp,offset TEXT ; адрес выводимой строки
 mov cx,0Ch 
 XOR DX,DX 
 INT 10h
После вывода текста необходимо приостановить выполнение программы до того момента, пока пользователь не прочтет сообщение и явным образом не даст сигнал, что можно продолжать дальше. Обычно для этого используют запрос на ввод данных с клавиатуры через прерывание BIOS 16h. Выполнение программы при этом приостанавливается до тех пор, пока не будет нажата какая-нибудь клавиша на клавиатуре. Итак:
Assembler
1
2
XOR AX,AX 
 INT 16h
Программа завершилась, что дальше? Обычно в этом месте ставят команду возврата в операционную систему. Однако, в нашем случае это не имеет смысла - операционная система не загружена. Поэтому просто поставим команду перезагрузки системы:
Assembler
1
INT 19h
Если дискета оставлена в дисководе, наша программа загрузится, и будет выполнена снова; если ее вытащить, загрузится обычная ОС (DOS или Windows).

Пора приступить к вводу и ассемблированию программы; для этого, как я уже говорил, используем отладчик debug. Однако сначала нужно вместо текстовых меток расставить действительные значения адресов памяти, а сделать это можно только с помощью "двух проходов". При первом проходе вместо реальных значений адресов подставляются произвольные числа; необходимо только следить, чтобы они отличались от настоящих адресов не слишком радикально (особенно в случаях команд ближних условных переходов: их можно использовать лишь при переходах в пределах 128 байт). Составляем таблицу, в которую вписываем все метки, встречающиеся в нашей программе; по мере набора программы и "прохождения" соответствующих меток проставляем в таблице рядом с каждой меткой ее действительный адрес. Затем повторно вводим все те команды, которые содержали метки, уже с реальными значениями адресов.

В случае с "реальным" ассемблером всей этой канителью занимается компилятор. В нашем случае, поскольку вся эта работа была уже проделана автором, вы можете просто воспользоваться уже готовыми адресами, поверив мне на слово, что это действительно те самые адреса, которые нужны. Итак, начинаем работать. Щелкните на кнопке "Start" ("Старт") и выберите пункт "Run" ("Выполнить"). Наберите в командной строке debug. Откроется окно сеанса DOS с черточкой - приглашением отладчика debug. Программа должна начинаться у нас с адреса 7C00, поэтому набираем:
Code
1
a 7C00
В ответ появится что-то вроде
Code
1
200E:7C00
Эти числа означают адрес вводимой команды в виде сегмент:смещение. Адрес сегмента в вашем случае будет другим, но это не играет роли. Внимание обращать следует лишь на смещение. Вводим нашу первую команду:
Code
1
JMP 7C40
После ввода каждой команды нажимаем <Enter>; в ответ debug выдаст адрес следующей вводимой команды. В нашем случае, после первой команды следует область блока параметров BIOS, которую следует пропустить. Для этого еще раз нажимаем <Enter>, затем, чтобы перейти к вводу команд с адреса 7C40h, набираем:
Code
1
а 7C40
Вот листинг оставшейся части программы (учтите, что некоторые одинаковые команды могут обозначаться по-разному, например, JNC и JNB, JNG и JLE и др., так что здесь нет ошибки):
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
200E:7C40 MOV     AL,01
200E:7C42 MOV     BX,7E00
200E:7C45 XOR     CH,CH
200E:7C47 MOV     CL,02
200E:7C49 XOR     DX,DX
200E:7C4B MOV     SI,0003
200E:7C4E MOV     AH,02
200E:7C50 INT     13
200E:7C52 JNB     7C5D
200E:7C54 DEC     SI
200E:7C55 JZ      7C85
200E:7C57 XOR     AH,AH
200E:7C59 INT     13
200E:7C5B JMP     7C4E
200E:7C5D INC     CL
200E:7C5F CMP     CL,12
200E:7C62 JLE     7C76
200E:7C64 MOV     CL,01
200E:7C66 INC     DH
200E:7C68 CMP     DH,01
200E:7C6B JLE     7C76
200E:7C6D XOR     DH,DH
200E:7C6F INC     CH
200E:7C71 CMP     CH,50
200E:7C74 JGE     7C85
200E:7C76 DEC     WORD PTR [7C3E]
200E:7C7A JZ      7C82
200E:7C7C ADD     BX,0200
200E:7C80 JMP     7C4B
200E:7C82 JMP     7E00
200E:7C85 MOV     AH,13
200E:7C87 MOV     AL,03
200E:7C89 MOV     BH,00
200E:7C8B MOV     BP,7C9B
200E:7C8E MOV     CX,000D
200E:7C91 XOR     DX,DX
200E:7C93 INT     10
200E:7C95 XOR     AX,AX
200E:7C97 INT     16
200E:7C99 INT     19
200E:7C9B
По адресу 7C9Bh у нас должен находиться текст "Read error", который (в коде ASCII с добавленными байтами-атрибутами) вводится следующим образом:
Code
1
2
200E:7C9B DW 752,765,761,764,720,765,772,772,76F,772
200E:7CAF
Наконец, в последних двух байтах первого сектора должна быть сигнатура. Нажимаем еще раз <Enter>, затем вводим:
Code
1
2
3
a 7CFE
200E:7CFE DB 55,AA
200E:7E00
Строго говоря, это еще не вся программа. Если вы вспомните, после загрузки всех секторов управление передается по адресу 7E00, а у нас по этому адресу ничего нет, да и никаких других секторов тоже нет. Тем не менее, мы можем сохранить этот кусок и использовать его в виде шаблона при создании других программ. В этом случае все, что нам потребуется, - это добавить код, начиная с адреса 7E00 - и независимо от того, сколько места он будет занимать, при записи на дискету наша программа сможет без всякой посторонней помощи сама себя загрузить и выполнить.

Сначала надо дать нашему шаблону имя, скажем, "template.bot". О расширении .bot немного позже, сейчас же присвоим это имя:
Code
1
n template.bot
И сохраним его; для этого сначала надо в регистр CX внести размер сохраняемой программы. Размер определяется вычитанием из последнего смещения, полученного нами при вводе программы, числа 100h (не забудьте, что все числа - шестнадцатеричные). В нашем случае это будет 7D00h:
Code
1
r cx <Enter>
Выводится текущее значение регистра CX и двоеточие для ввода нового значения. Вводим:
Code
1
7D00 <Enter>
Теперь записываем:
Code
1
w <Enter>
После успешной записи на экран будет выведено:
Code
1
Writing 7D00 bytes
Шаблон готов. Попробуем создать с его помощью простейшую тестовую программу. Скопируем файл шаблона под другим именем, например, "test.bot". Теперь откроем этот файл в отладчике debug. Это удобно сделать следующим образом. В файловом менеджере DOS (например, FAR или Norton Commander) перейдите в каталог, в котором вы сохранили файл "test.bot". В командной строке наберите:
Code
1
debug test.bot
Теперь по адресу 7E00 (набрав 'a 7E00') к этой программе можно добавить дополнительный код, например, такой:
Code
1
2
3
4
5
6
7
8
9
10
11
12
200D:7E00 MOV AH,13
200D:7E02 MOV AL,3
200D:7E04 XOR BH,BH
200D:7E06 MOV BP,7E17
200D:7E09 MOV CX,9
200D:7E0C XOR DX,DX
200D:7E0E INT 10
200D:7E10 XOR AX,AX
200D:7E12 INT 16
200D:7E14 JMP 8000
200D:7E17 DW  753,765,763,774,76F,772,720,732,720
200D:7E29
Этот код аналогичен коду, выводящему сообщение об ошибке на экран. В данном случае выводится сообщение "Sector 2"; управление передается по адресу 8000h. Ценность этого небольшого фрагмента кода в том, что его можно с небольшой модификацией разместить в различных секторах и сделать так, чтобы он выводил на экран номер сектора, в котором данный код находится. Это позволяет проконтролировать, что сектора загружаются, и передают друг другу управление нормально. Так, в третьем секторе (по адресу 8000) можно набрать этот же код, только по адресу 8006 на этот раз должна быть команда 'MOV BP, 8017' (т.е. необходимо соответственно увеличивать этот адрес на 200h). По адресу 8014 можно вставить команду 'INT 19', чтобы завершить программу; если же нужно задействовать еще один сектор, надо просто передать ему управление, например, 'JMP 8200', и т.д. В области данных необходимо также произвести изменения в соответствии с номером сектора: в третьем секторе предпоследнее число (732) должно быть 733, в четвертом - 734 и т.д. Для 10 сектора меняются уже два последних числа - '731,730' (что соответствует символам '1' и '0'). Не забудьте после добавления кода сохранить программу, предварительно записав в регистр CX ее новый размер (по последнему смещению минус 100).

Программа готова, но запустить ее в таком виде нам не удастся. Начало нашей программы должно быть записано в загрузочном секторе, причем так, чтобы сохранить блок параметров BIOS. Дело не только в том, что штатными средствами операционной системы этого сделать нельзя; дело еще и в том, что с помощью отладчика debug можно ассемблировать и сохранять небольшие исполняемые файлы, но они будут в формате COM. Формат COM предполагает, что начало файла соответствует смещению 100h, и debug записывает программу на диск соответствующим образом. Поскольку наш загрузчик (как и вообще любой загрузчик) размещается в памяти, начиная с адреса 7С00, debug сохранит в нашем bot-файле перед началом собственно программы 7B00h байт "мусора". Поэтому придется создать вспомогательную программу wb.com (от "Write Boot"), чтобы сохраненный нами файл в "формате" .bot адекватным образом переписать на дискету.

Вспомогательная программа
Алгоритм действий следующий. При запуске программы wb.com ей в командной строке в качестве параметра передается название bot-файла, который необходимо скопировать на дискету. Далее открываем этот файл с использованием функции 3Dh прерывания DOS 21h, и сохраняем в памяти дескриптор открытого файла. Файловый указатель перемещаем в позицию 7B00h (пропуская "мусор" в начале файла). Считываем первые 512 байт (будущий загрузочный сектор), и сохраняем его в отдельном буфере, поскольку, во-первых, нам надо будет добавить туда реальный блок параметров BIOS, считанный из загрузочного сектора дискеты (иначе для повторного использования в DOS или Windows дискету придется форматировать заново), а во-вторых, по смещению 3E (непосредственно после блока параметров) нам необходимо поместить число записанных на дискету секторов (после их успешной записи). Для этой цели загрузочный сектор дискеты также считываем и сохраняем в отдельном буфере; данные блока параметров BIOS (смещения с 3h по 3Eh) копируем из второго буфера в первый. Далее организуем цикл:
  • считываем 512 байт из открытого нами bot-файла в третий буфер;
  • записываем данные из этого буфера в соответствующий сектор дискеты;
  • увеличиваем значение счетчика секторов по смещению 3Eh в первом буфере;
  • когда при очередной итерации будет прочитано 0 байт (конец файла), запишем данные из первого буфера (с окончательным значением счетчика записанных секторов) в загрузочный (первый) сектор дискеты.
Обработка ошибок в программе сведена к минимуму, но при использовании файловых операций совсем обойтись без вывода сообщений невозможно. Первым делом в начале работы программы выводится сообщение: "Вставьте в дисковод А: чистую дискету и нажмите любую клавишу". Поскольку вывод осуществляется посредством функции 40h прерывания DOS 21h, можно использовать русские буквы, а также символы перевода строки, возврата каретки, табуляции и т.д. Второе сообщение выводится при ошибке открытия файла (например, если неправильно указано имя файла): "Ошибка открытия файла". Сообщение "Ошибка чтения дискеты" используется для экономии сразу в двух случаях: при ошибке чтения bot-файла выводится лишь часть сообщения ("Ошибка чтения"), при ошибке чтения с гибкого диска - все сообщение. Последнее сообщение - "Ошибка записи" - для случая безуспешной попытки записи сектора на дискету. В области данных отводятся также три буфера по 512 байт для операций чтения-записи, о которых говорилось выше. И, наконец, два байта отводятся для сохранения дескриптора открытого файла.

Приступим к написанию кода. Область данных расположим в начале файла, поэтому первой командой будет безусловный переход для обхода этих данных:
Code
1
JMP INVITE
Условно обозначим наши данные следующими метками (впоследствии при вводе с помощью debug подставим вместо них реальные адреса; пока же вместо адреса будем ставить соответствующую метку в скобках): выводимые сообщения - TXT1, TXT2, TXT3 и TXT4 соответственно; FIRST - первый буфер (для считывания первых (после "мусора") 512 байт bot-файла, FDD - второй буфер (для считывания загрузочного сектора дискеты), BUF - третий буфер (для последовательного копирования секторов из bot-файла на дискету); HANDLE - 2 байта (слово) для хранения дескриптора открытого файла.

Непосредственно после данных расположим универсальную процедуру вывода сообщений на экран. Для этой цели используем функцию 40h прерывания DOS 21h.
регистрзначение
AH 40h (функция)
BX устройство вывода:
  • 1 - экран
  • 3 - внешнее устройство
  • 4 - печать
CX максимальное число байтов
DX адрес области данных
Для каждого сообщения устанавливаются свои значения числа выводимых символов и адрес начала текста, общая же часть кода выглядит следующим образом:
Assembler
1
2
3
4
5
6
7
8
9
10
TXT_OUT: MOV AH,40h 
  MOV BX,1 
  INT 21h 
  XOR AX,AX 
  INT 16h 
  CMP DX,(TXT1) ; проверка, не выводится ли 1-е      
                  ; сообщение
  JE OPEN_FILE ; если да - открыть файл
; Сюда попадаем, если выводится одно из сообщений об ошибке
  JMP OUT ; выход из программы
Далее следуют специфические для каждого выводимого сообщения данные:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
INVITE: MOV DX,(TXT1) ; адрес первого сообщения
  MOV CX,3Fh  
  JMP TXT_OUT  ; вывести строку
FILE_ERR: MOV DX,(TXT2) ; при ошибке открытия файла
  MOV CX,17
  JMP TXT_OUT
READ_ERR: MOV DX,(TXT3) ; при ошибке чтения bot-файла
  MOV CX,0Eh  ; вывод лишь части TXT3
  JMP TXT_OUT
READ_FDD: MOV DX,(TXT3) ; при ошибке чтения дискеты
  MOV CX,17  ; вывод всего TXT3
  JMP TXT_OUT
WRITE_ERR: MOV DX,(TXT4) ; при ошибке записи на дискету
  MOV CX,0Fh
  JMP TXT_OUT
Для открытия файла используется функция 3Dh прерывания DOS 21h.
регистрзначение
AH 3Dh (функция);
AL код доступа:
  • 0 - для чтения
  • 1 - для записи
  • 2 - для чтения и записи
DX адрес строки с именем файла в ASCIIZ-формате
В случае ошибки устанавливается флаг переноса (CF); при успешном открытии файла он сброшен, а в регистре AX находится дескриптор открытого файла - по нему впоследствии можно обращаться к этому файлу при операциях чтения или записи.

ASCIIZ-формат представляет собой строку в кодировке ASCII, завершающуюся двоичным нулем. Имя bot-файла будет передаваться в командной строке; как получить к нему доступ? Здесь нам придется использовать так называемый префикс программного сегмента, который операционная система размещает в памяти перед каждой COM- или EXE- программой при ее запуске. Префикс программного сегмента имеет начальное смещение 0 и размер 256 (100h) байт (именно поэтому COM-файлы начинаются со смещения 100h). Начиная со смещения 80h в префиксе программного сегмента располагается область, называемая буфером передачи данных (DTA). В первом байте этого буфера размещается длина строки параметров программы. Начиная со второго байта размещаются введенные символы (если таковые имеются), а затем следует всевозможный "мусор".

Таким образом, в нашем случае после имени программы (wb.com) будет следовать пробел, затем имя bot-файла - по смещению 80h будет число, на 1 превышающее число букв в имени файла. Само имя начинается со смещения 82h. Этот адрес можно записать в регистр DX для функции открытия файла; однако сначала надо в конце имени файла (по смещению 81h + число символов в имени, т.е. число, хранящееся по адресу [80h]) поместить 0. Такую несколько громоздкую конструкцию закодируем следующим образом:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
OPEN_FILE: XOR BX,BX 
  MOV BL,ds:[80h] ; в BX - число по адресу 80h, то есть число введенных символов       
     ; (вместе с пробелом)
  XOR AX,AX 
  MOV [BX+81h],AX ; поместить 0 по адресу, равному 
                  ; сумме 81 и числа, хранящегося в BX - т.е. в конец имени файла
  MOV AH,3Dh 
; в AL уже находится 0 - открываем файл для чтения
  MOV DX,82h ; адрес начала имени файла (без      
              ; пробела) в DTA
  INT 21h 
  JC FILE_ERR 
  MOV [HANDLE],AX ; сохранить дескриптор файла
Следующим действием необходимо установить файловый указатель на значение 7B00h байт от начала файла. Для управления файловым указателем предназначена функция 42h прерывания DOS 21h.
регистрзначение
AH 42h (функция)
AL точка отсчета смещения:
  • 0 - от начала файла
  • 1 - от текущего значения файлового указателя
  • 2 - от конца файла
BX дескриптор файла
CX: DX смещение в байтах (DX - младшее слово, CX - старшее)
При ошибке устанавливается флаг переноса (CF).

Итак, продолжаем:
Assembler
1
2
3
4
5
6
7
MOV AH,42 
XOR AL,AL 
MOV BX,[HANDLE] 
XOR CX,CX 
MOV DX,7B00h  
INT 21h 
JC READ_ERR
Дальше нам нужно прочитать первые 512 байт bot-файла. Для чтения файла используется функция 3Fh прерывания DOS 21h.
регистрзначение
AH 3Fh (функция)
BX дескриптор файла
CX число байтов для чтения
DX адрес области ввода
При успешном выполнении функции флаг переноса CF сбрасывается, а в регистре AX содержится число действительно прочитанных байтов. Если это число равно 0, достигнут конец файла. Итак:
Assembler
1
2
3
4
5
6
7
MOV AH,3Fh 
; дескриптор файла сохранился в регистре BX после операции  
; установки файлового указателя
MOV CX,200h
MOV DX,(FIRST) ; адрес первого буфера
INT 21h 
JC READ_ERR
Следующим действием считаем первый (загрузочный) сектор дискеты. Для чтения физических секторов необходимо использовать низкоуровневые функции прерывания BIOS 13h. Мы уже проделывали это при создании файла "template.bot", поэтому это не должно вызвать проблем:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 MOV SI,3 ; 3 попытки при ошибках
RETRY:MOV AH,2 
 MOV AL,1 ; один сектор 
 MOV BX,(FDD); адрес второго буфера
 XOR CH,CH ; дорожка 0
 MOV CL,1 ; сектор 1
 XOR DX,DX ; диск А: (0), головка 0
 INT 13h 
 JNC OK 
 ; Сюда попадаем при ошибке чтения
 XOR AH,AH ; функция 0 - сброс дисковода
 DEC SI 
 CMP SI,0 
 JE READ_FDD 
; Сюда попадаем, если нужно еще раз попытаться прочесть сектор
 INT 13h 
 JMP RETRY
Если чтение загрузочного сектора с дискеты было успешным, необходимо скопировать блок параметров BIOS из второго сектора (FDD) в первый (FIRST). Для побайтного копирования данных из одной области памяти в другую используется инструкция MOVSB в сочетании с префиксом REP. Команда MOVSB копирует байт по адресу DS:SI в новое место по адресу ES: DI, при этом значения SI и DI после пересылки байта изменяются на 1: при сброшенном (0) флаге направления DF - увеличиваются на 1, при установленном (1) - уменьшаются на 1. Префикс REP заставляет команду MOVSB повторяться столько раз, сколько записано в регистре CX (при каждом повторе значение CX уменьшается на 1). Таким образом, в CX должно быть записано число байтов, которые необходимо скопировать из одного места в другое.
Assembler
1
2
3
4
5
6
7
8
9
10
MOV SI,(FDD)+3 ; исходный адрес - смещение 3 от начала     
                ; второго буфера (FDD)
MOV DI,(FIRST)+3 ; конечный адрес - смещение 3 от начала     
                    ; первого буфера (FIRST)
CLD   ; сбросить флаг направления (SI и DI будут    
            ; возрастать
MOV CX,3Bh  
REP MOVSB  
MOV WORD PTR [(FIRST)+3Eh],1 ; установить в счетчике секторов 
                            ; (по смещению 3Eh в первом буфере) значение 1
Можно считать, что первый сектор скопирован (хотя пока еще не записан физически на дискету). Для копирования оставшихся секторов организуется цикл, в котором в один и тот же буфер BUF производится сначала чтение очередных 512 байт из bot-файла с использованием уже знакомой нам функции 3Fh прерывания DOS 21h, а затем запись этих данных в соответствующий сектор на дискете уже с использованием низкоуровневой функции 3 прерывания BIOS 13h. Операция записи физического сектора аналогична операции чтения сектора; как и в случае чтения секторов в программе "template.bot", необходимо организовать смену головки и увеличение номера дорожки по мере заполнения секторов. Попытки записи также повторяются по 3 раза. Небольшая сложность лишь в том, что чередуются процесс чтения с использованием функции DOS и процесс записи сектора с использованием функции BIOS; необходимо сохранять в стеке текущие значения регистров для сектора, дорожки и головки (регистры CX и DX), а затем восстанавливать их оттуда. Код выглядит следующим образом:
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
 MOV CL,2 ; сектор 2,
 XOR CH,CH ; дорожка 0
 XOR DH,DH ; головка 0
LOOP: PUSH CX ; сохранить в стеке текущие значения дорожки    
                ; (CH), сектора (CL)
 PUSH DX ; и головки (DH)
 MOV AH,3Fh 
 MOV BX,[HANDLE] ; загрузить дескриптор файла
 MOV CX,200h 
 MOV DX,(BUF); адрес буфера ввода-вывода
 INT 21h 
 JNC M1 
 JMP READ_ERR
M1:
Последняя конструкция - как раз тот случай, когда переход по адресу READ_ERR превысил 128 байт, и пришлось использовать сочетание условного и безусловного переходов. При ближних переходах можно использовать просто 'JC READ_ERR'.
Assembler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
M1: CMP AX,00 ; проверка на конец файла
 JE WRITE_FIRST ; если конец - перейти на запись      
                 ; первого сектора (выход из цикла)
 
; Очередные данные считаны, конец файла не достигнут,   
; продолжаем
 POP DX ; восстановить сохраненные ранее значения
 POP CX ; дорожки, сектора и головки.     
 MOV SI,3 ; 3 раза для повторов
REWRITE: MOV AH,3 
 MOV AL,1 ; один сектор
 MOV BX,(BUF); адрес буфера
 XOR DL,DL ; диск А: (0)
 INT 13h 
 JNC OK2 
 ; Сюда попадаем, если была ошибка записи
 XOR AH,AH 
 DEC SI 
 CMP SI,0 
 JE WRITE_ERR; если да - выдать сообщение об ошибке
 ; Сюда попадаем, если необходимо повторить попытку записи
 INT 13h 
 JMP REWRITE ; повтор попытки записи
 
OK2: ; Очередной сектор был успешно записан
 INC CL 
 CMP CL,12h 
 JNG NEXT 
; номер сектора превысил 18:
 MOV CL,1 ; установить сектор 1
 INC DH ; увеличить номер головки
 CMP DH,1 
 JNG NEXT 
; номер головки превысил 1:
 XOR DH,DH ; установить головку 0
 INC CH ; увеличить номер дорожки
 CMP CH,50h 
 JG WRITE_ERR
 
NEXT: INC WORD PTR [(FIRST)+3Eh] ; увеличить счетчик  
                                    ; записанных секторов (по смещению 3Eh от начала первого буфера
 JMP LOOP 
 
WRITE_FIRST: ; все данные из bot-файла переписаны в  
                ; соответствующие сектора на дискете
    ; число записанных секторов 
    ; сохранено в первом буфере. Необходимо записать лишь сам  
    ; первый сектор. Процедура записи аналогична рассмотренной.
 MOV SI,3
N3: MOV AH,3
 MOV AL,1
 MOV BX,(FIRST)
 XOR DX,DX ; диск А: (0), головка 0
 XOR CH,CH ; дорожка 0
 MOV CL,1 ; сектор 1
 INT 13h
 JNC OUT ; если запись успешна - выход
 ; Ошибка записи:
 XOR AH,AH
 DEC SI
 CMP SI,0
 JNE N2 
 JMP WRITE_ERR ; число повторов = 0 - ошибка
N2: INT 13h ; сброс дисковода
 JMP N3 ; повтор попытки записи
 
OUT: ; Выход из программы. Необходимо закрыть файл, если он  
        ; был открыт
 CMP WORD PTR [HANDLE],0 ; если файл был открыт,        
                         ; дескриптор файла не равен 0
 JE END 
; Файл был открыт - необходимо его закрыть
 MOV AH,3Eh  
 MOV BX,[HANDLE] 
 INT 21hh
END: MOV AH,4Ch 
 INT 21h
Чтобы приступить к вводу программы с использованием debug, необходимо, как и в случае с программой "template.bot", вычислить все адреса и подставить их вместо меток. Разница же между двумя программами в том, что на этот раз мы создаем com-программу, поэтому вводить надо начинать со смещения 100h (команда отладчика 'a 100'):
Code
1
2
2039:0100 JMP     0795
2039:0103
Вводим текст первого сообщения (оно начинается со смещения 103h):
Code
1
2
2039:0103 DB " Вставьте в дисковод А: чистую дискету и нажмите любую клавишу",0D,0A 
2039:0142
Второе сообщение начинается со смещения 142h:
Code
1
2
]2039:0142 DB "Ошибка открытия файла",0D,0A 
2039:0159
]Третье сообщение со смещения 159h:
Code
1
2
2039:0159 DB "Ошибка чтения дискеты",0D,0A 
2039:0170
Четвертое сообщение со смещения 170h:
Code
1
2
2039:0170 DB "Ошибка записи",0D,0A
2039:017F
Далее должен следовать наш первый буфер (FIRST), его начальным смещением будет 17Fh, а его размер равен 512 (200h) байт, поэтому адресом второго буфера (FDD) будет смещение 37Fh, а третьего (BUF) - 57Fh. После него по смещению 77Fh будут два байта для дескриптора файла (HANDLE); здесь вначале должно быть число 0. Набираем 'a 77E <Enter>':
Code
1
2
2039:077E DW 0
2039:0781
Код начинается со смещения 781h:
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
2039:0781 MOV     AH,40
2039:0783 MOV     BX,0001
2039:0786 INT     21
2039:0788 XOR     AX,AX
2039:078A INT     16
2039:078C CMP     DX,0103
2039:0790 JZ      07BD
2039:0792 JMP     08AA
2039:0795 MOV     DX,0103
2039:0798 MOV     CX,003F
2039:079B JMP     0781
2039:079D MOV     DX,0142
2039:07A0 MOV     CX,0017
2039:07A3 JMP     0781
2039:07A5 MOV     DX,0159
2039:07A8 MOV     CX,000E
2039:07AB JMP     0781
2039:07AD MOV     DX,0159
2039:07B0 MOV     CX,0017
2039:07B3 JMP     0781
2039:07B5 MOV     DX,0170
2039:07B8 MOV     CX,000F
2039:07BB JMP     0781
2039:07BD XOR     BX,BX
2039:07BF MOV     BL,[0080]
2039:07C3 XOR     AX,AX
2039:07C5 MOV     [BX+0081],AX
2039:07C9 MOV     AH,3D
2039:07CB MOV     DX,0082
2039:07CE INT     21
2039:07D0 JB      079D
2039:07D2 MOV     [077F],AX
2039:07D5 MOV     AH,42
2039:07D7 XOR     AL,AL
2039:07D9 MOV     BX,[077F]
2039:07DD XOR     CX,CX
2039:07DF MOV     DX,7B00
2039:07E2 INT     21
2039:07E4 JB      07A5
2039:07E6 MOV     AH,3F
2039:07E8 MOV     CX,0200
2039:07EB MOV     DX,017F
2039:07EE INT     21
2039:07F0 JB      07A5
2039:07F2 MOV     SI,0003
2039:07F5 MOV     AH,02
2039:07F7 MOV     AL,01
2039:07F9 MOV     BX,037F
2039:07FC XOR     CH,CH
2039:07FE MOV     CL,01
2039:0800 XOR     DX,DX
2039:0802 INT     13
2039:0804 JNB     0812
2039:0806 XOR     AH,AH
2039:0808 DEC     SI
2039:0809 CMP     SI,+00
2039:080C JZ      07AD
2039:080E INT     13
2039:0810 JMP     07F5
2039:0812 MOV     SI,0382
2039:0815 MOV     DI,0182
2039:0818 CLD
2039:0819 MOV     CX,003B
2039:081C REPZ
2039:081D MOVSB
2039:081E MOV     WORD PTR [01BD],0001
2039:0824 MOV     CL,02
2039:0826 XOR     CH,CH
2039:0828 XOR     DH,DH
2039:082A PUSH    CX
2039:082B PUSH    DX
2039:082C MOV     AH,3F
2039:082E MOV     BX,[077F]
2039:0832 MOV     CX,0200
2039:0835 MOV     DX,057F
2039:0838 INT     21
2039:083A JNB     083F
2039:083C JMP     07A5
2039:083F CMP     AX,0000
2039:0842 JZ      0887
2039:0844 POP     DX
2039:0845 POP     CX
2039:0846 MOV     SI,0003
2039:0849 MOV     AH,03
2039:084B MOV     AL,01
2039:084D MOV     BX,057F
2039:0850 XOR     DL,DL
2039:0852 INT     13
2039:0854 JNB     0865
2039:0856 XOR     AH,AH
2039:0858 DEC     SI
2039:0859 CMP     SI,+00
2039:085C JNZ     0861
2039:085E JMP     07B5
2039:0861 INT     13
2039:0863 JMP     0849
2039:0865 INC     CL
2039:0867 CMP     CL,12
2039:086A JLE     0881
2039:086C MOV     CL,01
2039:086E INC     DH
2039:0870 CMP     DH,01
2039:0873 JLE     0881
2039:0875 XOR     DH,DH
2039:0877 INC     CH
2039:0879 CMP     CH,50
2039:087C JLE     0881
2039:087E JMP     07B5
2039:0881 INC     WORD PTR [01BD]
2039:0885 JMP     082A
2039:0887 MOV     SI,0003
2039:088A MOV     AH,03
2039:088C MOV     AL,01
2039:088E MOV     BX,017F
2039:0891 XOR     DX,DX
2039:0893 XOR     CH,CH
2039:0895 MOV     CL,01
2039:0897 INT     13
2039:0899 JNB     08AA
2039:089B XOR     AH,AH
2039:089D DEC     SI
2039:089E CMP     SI,+00
2039:08A1 JNZ     08A6
2039:08A3 JMP     07B5
2039:08A6 INT     13
2039:08A8 JMP     088A
2039:08AA CMP     WORD PTR [077F],+00
2039:08AF JZ      08B9
2039:08B1 MOV     AH,3E
2039:08B3 MOV     BX,[077F]
2039:08B7 INT     21
2039:08B9 MOV     AH,4C
2039:08BB INT     21
2039:08BD <Enter>
Дадим программе имя: 'n wb.com <Enter>', укажем ее размер - 'r cx <Enter>' и '7BD <Enter>', затем сохраним: 'w <Enter>'. Если при вводе не было сделано ошибок, программа должна сразу заработать в вывести на экран первое сообщение. Первое условие, однако, маловероятно, поэтому, скорее всего, программу придется отлаживать. Отладка больших (сравнительно) программ на ассемблере - занятие неблагодарное, особенно без специальных инструментальных средств; поэтому имеет смысл наращивать нашу программу "кусками", на каждом этапе проверяя набранное и добиваясь работоспособности программы. Для этого можно завершающий фрагмент кода ('MOV AH,4C' и 'INT 21') ставить после набора очередного смыслового блока, сохранить в таком виде и попробовать запустить (при этом не надо забывать подставлять в соответствующих местах вместо ссылок вперед на несуществующий еще код адрес этого завершающего фрагмента).

В частности, подобную процедуру можно проделать, набрав данные и процедуру вывода сообщений (до адреса 7BDh), затем набрав процедуру открытия файла (до адреса 7D5h), затем после перемещения файлового указателя (до адреса 7E6h), после чтения загрузочного сектора (до адреса 812h), после цикла копирования секторов (до адреса 887h). На каждом этапе "своя" часть функциональности должна быть обеспечена. В любом случае должно выводиться начальное сообщение. Затем, если в командной строке не было указано имя существующего файла (при необходимости с полным путем к нему), должно выводиться сообщение "Ошибка открытия файла". После реализации чтения загрузочного сектора должно появиться обращение к дисководу и т.д.

Для отладки используем тот же debug, для этого, собственно, он и предназначен. Чтобы вывести ассемблированный код, необходимо набрать 'u [адрес] <Enter>' и сравнить введенный код с тем, который должен быть. Особенно тщательно следует следить за соответствием адресов ссылок и данных. При попытке дизассемблировать область данных мы получим бессмыслицу - для отображения данных служит команда 'd [адрес] <Enter>', данные отображаются в виде шестнадцатиричных чисел.

Обработка ошибок в нашей программе сведена к минимуму, поэтому она требует корректного к себе обращения. При запуске вместе с названием файла wb (расширение указывать не обязательно) через один пробел должно следовать название bot-файла. Необходмо помнить, что формат загружаемого файла нашей программой не проверяется; она с одинаковым успехом сможет открыть файл любого формата - и txt, и bmp, и jpg, и exe и т.д. - и скопирует его на дискету, убрав начальные 7B00h байтов.

Отладка программы wb.com - это только полдела; она может успешно записать загрузочный сектор дискеты, а вот то, что мы туда записываем, само может потребовать отладки. Для этой цели и предназначается относительно простая программа "test.bot". Сначала эту программу можно составить так, чтобы в ней был всего один дополнительный сектор. После записи "test.bot" на дискету с помощью wb.com следует перезагрузить компьютер, оставив дискету в дисководе. При этом последовательность загрузки с помощью BIOS Setup должна быть установлена в порядке "A, C". Если надпись "Sector 2" отображается, можно добавить еще несколько секторов и проверить их работу. Данный шаг позволит убедиться, что программа wb.com правильно записывает последовательность секторов на дискету, а bot-программа - правильно их считывает.

Изменения в реестре

Если все работает нормально, можно несколько повысить для себя уровень сервиса при работе с этими программами. Для начала разместим wb.com в каталоге Windows (например, C:\WINDOWS). Теперь wb можно набирать, как обычную команду операционной системы, и она будет работать независимо от каталога, в котором мы находимся. Чтобы еще больше облегчить себе работу, создадим ассоциацию bot-файлов с программой wb.com. Желающие могут воспользоваться для этой цели проводником Windows, а для любителей программирования я привожу здесь соответствующий reg-файл.

Откройте "Блокнот" Windows и наберите в нем следующий текст:
Code
1
2
3
4
5
6
7
8
9
10
11
12
13
REGEDIT4
 
[HKEY_CLASSES_ROOT\.bot]
@="botfile"
 
[HKEY_CLASSES_ROOT\botfile\Shell]
@=""
 
[HKEY_CLASSES_ROOT\botfile\Shell\open]
@=""
 
[HKEY_CLASSES_ROOT\botfile\Shell\open\command]
@="C:\\WINDOWS\\wb.com %1"
В последней строке вы должны указать каталог Windows на своей машине, например, C:\\Win98\\wb.com, если у вас Windows размещена в каталоге Win98. Обратите внимание, что обратная косая черта должна быть продублирована. Еще раз тщательно проверьте правильность всех данных и сохраните файл с расширением .reg, например, "bot.reg". Из проводника Windows дважды щелкните на названии этого файла; появится запрос на подтверждение изменений в реестре. Щелкните "OK".

Теперь, если не было сделано ошибок, при двойном щелчке на имени файла с расширением .bot откроется окно DOS и появится знакомое нам приглашение вставить чистую дискету в дисковод А:. Вставив дискету и нажав любую клавишу, мы получим загрузочную дискету. Остается только пожелать успеха в ваших экспериментах с загрузочным сектором.


2002-2013 (©) wasm.ru
2
Ушел с форума
Автор FAQ
 Аватар для Mikl___
16373 / 7685 / 1080
Регистрация: 11.11.2010
Сообщений: 13,759
16.06.2014, 05:34  [ТС]
JobsOS
Пример ОС на ассемблере. Не совместима с 21h прерыванием и работает на прерываниях BIOS
Есть функции: shutdown,format,dir,example,cls. ОС имеет виртуальный диск, объемом 1024x9 (секторы по 1024, а их всего 9).
Взято здесь
Вложения
Тип файла: zip JobsOS.zip (2.2 Кб, 136 просмотров)
Тип файла: rar JobsOS_TESTING_UNDER_DOS.ver.(test).English.rar (2.9 Кб, 91 просмотров)
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
inter-admin
Эксперт
29715 / 6470 / 2152
Регистрация: 06.03.2009
Сообщений: 28,500
Блог
16.06.2014, 05:34
Помогаю со студенческими работами здесь

Загрузочный сектор, редактирование через Debug
Снял с флешки 0 сектор, и загружаю его в debug n c:\1.bin L 7c00 ;загружаю u 7c00 ; диассемблирую u 7c5a ; перехожу на прыжок a...

Загрузочный сектор на DVD-RW. Не происходит загрузки.
При отключении жесткого диска DVD-RW в биосе определяется как мастер. Или как там его. Главный. На сектор 0 скомпилировался ассемблер ...

Как записать данные в загрузочный сектор без использования int 13h
Подскажите, пожалуйста, как записать данные в загрузочный сектор без помощи 13h прерывания.

Не получается читать загрузочный сектор.
Пишу код: mov ax,0201h mov dl,2 mov dh, 0 mov ch,0 mov cl,1 push cs pop es mov bx,offset buf

Переписать загрузочный сектор флешки
Доброго времени суток! Задали лабу, переписать загрузочный сектор флешки и впихнуть туда свою прогу. С сектором все понятно. Вопрос...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
15
Ответ Создать тему
Новые блоги и статьи
SDL3 для Web (WebAssembly): Обработчик клика мыши в браузере ПК и касания экрана в браузере на мобильном устройстве
8Observer8 02.02.2026
Содержание блога Для начала пошагово создадим рабочий пример для подготовки к экспериментам в браузере ПК и в браузере мобильного устройства. Потом напишем обработчик клика мыши и обработчик. . .
Философия технологии
iceja 01.02.2026
На мой взгляд у человека в технических проектах остается роль генерального директора. Все остальное нейронки делают уже лучше человека. Они не могут нести предпринимательские риски, не могут. . .
SDL3 для Web (WebAssembly): Вывод текста со шрифтом TTF с помощью SDL3_ttf
8Observer8 01.02.2026
Содержание блога В этой пошаговой инструкции создадим с нуля веб-приложение, которое выводит текст в окне браузера. Запустим на Android на локальном сервере. Загрузим Release на бесплатный. . .
SDL3 для Web (WebAssembly): Сборка C/C++ проекта из консоли
8Observer8 30.01.2026
Содержание блога Если вы откроете примеры для начинающих на официальном репозитории SDL3 в папке: examples, то вы увидите, что все примеры используют следующие четыре обязательные функции, а. . .
SDL3 для Web (WebAssembly): Установка Emscripten SDK (emsdk) и CMake для сборки C и C++ приложений в Wasm
8Observer8 30.01.2026
Содержание блога Для того чтобы скачать Emscripten SDK (emsdk) необходимо сначало скачать и уставить Git: Install for Windows. Следуйте стандартной процедуре установки Git через установщик. . . .
SDL3 для Android: Подключение Box2D v3, физика и отрисовка коллайдеров
8Observer8 29.01.2026
Содержание блога Box2D - это библиотека для 2D физики для анимаций и игр. С её помощью можно определять были ли коллизии между конкретными объектами. Версия v3 была полностью переписана на Си, в. . .
Инструменты COM: Сохранение данный из VARIANT в файл и загрузка из файла в VARIANT
bedvit 28.01.2026
Сохранение базовых типов COM и массивов (одномерных или двухмерных) любой вложенности (деревья) в файл, с возможностью выбора алгоритмов сжатия и шифрования. Часть библиотеки BedvitCOM Использованы. . .
SDL3 для Android: Загрузка PNG с альфа-каналом с помощью SDL_LoadPNG (без SDL3_image)
8Observer8 28.01.2026
Содержание блога SDL3 имеет собственные средства для загрузки и отображения PNG-файлов с альфа-каналом и базовой работы с ними. В этой инструкции используется функция SDL_LoadPNG(), которая. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru