Своя маленькая операционная система

А что если хочется написать программку, для которой ничего не надо? Вставляем дискетку в компьютер, загружаемся с нее и …»Hello World»! Можно даже прокричать это приветствие из защищенного режима…
Сказано — сделано. С чего бы начать?.. Набраться знаний, конечно. Для этого очень хорошо полазить в исходниках Linux и Thix. Первая система всем хорошо знакома, вторая менее известна, но не менее полезна.

Подучились? Теперь займемся. Понятно, что первым делом надо написать загрузочный сектор для нашей мини-операционки (а ведь это будет именно мини-операционка!). Поскольку процессор грузится в 16-разрядном режиме, то для создания загрузочного сектора используется ассемблер и линковщик из пакета bin86. Можно, конечно, поискать еще что-нибудь, но оба наших примера используют именно его; и мы тоже пойдем по стопам учителей. Синтаксис этого ассемблера немного странноватый, совмещающий черты, характерные и для Intel и для AT&T, но после пары недель мучений можно привыкнуть.

Загрузочный сектор (boot.S)

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

Для начала определимся с основными константами.
START_HEAD = 0 — Головка привода, которою будем использовать.
START_TRACK = 0 — Дорожка, откуда начнем чтение.
START_SECTOR = 2 — Сектор, начиная с которого будем считывать наше ядрышко.
SYSSIZE = 10 — Размер ядра в секторах (каждый сектор содержит 512 байт)
FLOPPY_ID = 0 — Идентификатор привода. 0 — для первого, 1 — для второго
HEADS = 2 — Количество головок привода.
SECTORS = 18 — Количество дорожек на дискете. Для формата 1.44 МБ это количество равно 18.

В процессе загрузки будет происходить следующее. Загрузчик BIOS считает первый сектор дискеты, положит его по адресу 0000:0x7c00 и передаст туда управление. Мы его получим и — для начала — переместим себя пониже по адресу 0000:0x600, перейдем туда и спокойно продолжим работу. Собственно вся наша работа будет состоять из загрузки ядра (сектора 2 — 12 первой дорожки дискеты) по адресу 0x100:0000, переходу в защищенный режим и скачку на первые строки ядра. В связи с этим еще несколько констант:
BOOTSEG = 0x7c00 — Сюда поместит загрузочный сектор BIOS.
INITSEG = 0x600 — Сюда его переместим мы.
SYSSEG = 0x100 — А здесь приятно расположится наше ядро.
DATA_ARB = 0x92 — Определитель сегмента данных для дескриптора
CODE_ARB = 0x9A — Определитель сегмента кода для дескриптора.

Первым делом произведем перемещение самих себя в более приемлемое место.

cli
xor ax, ax
mov ss, ax
mov sp, #BOOTSEG
mov si, sp
mov ds, ax
mov es, ax
sti
cld
mov di, #INITSEG
mov cx, #0x100
repnz
movsw
jmpi go, #0
;прыжок в новое местоположение загрузочного сектора на метку go
Теперь необходимо настроить как следует сегменты для данных (es, ds) и для стека. Неприятно, конечно, что все приходится делать вручную, но что поделаешь — ведь кроме нас и BIOS в памяти компьютера никого нет.

go:
mov ax, #0xF0
mov ss, ax
mov sp, ax ; Стек разместим как 0xF0:0xF0 = 0xFF0
mov ax, #0x60 ; Сегменты для данных ES и DS зададим в 0x60
mov ds, ax
mov es, ax
Наконец, можно вывести победное приветствие. Пусть мир узнает, что мы смогли загрузиться! Поскольку у нас есть все-таки целый BIOS, воспользуемся готовой функцией 0x13 прерывания 0x10. Можно, конечно, его презреть и написать напрямую в видеопамять, но у нас каждый байт команды на счету, а байт таких всего 512. Потратим их лучше на что-нибудь более полезное.

mov cx,#18
mov bp,#boot_msg
call write_message
Функция write_message выглядит следующим образом
write_message:
push bx
push ax
push cx
push dx
push cx
mov ah,#0x03 ; прочитаем текущее положение курсора,
; дабы не выводить сообщения где попало.
xor bh,bh
int 0x10
pop cx
mov bx,#0x0007 ; Параметры выводимых символов : видеостраница 0,
; атрибут 7 (серый на черном)
mov ax,#0x1301 ; Выводим строку и сдвигаем курсор.
int 0x10
pop dx
pop cx
pop ax
pop bx
ret
А сообщение так

boot_msg:
.byte 13,10
.ascii «Booting data …»
.byte 0
К этому времени на дисплее компьютера появится скромное «Booting data …». Это в принципе не хуже, чем «Hello World», но давайте добьемся чуть большего. Перейдем в защищенный режим и выведем этот «Hello» уже из программы, написанной на C.

Ядро 32-разрядное. Оно будет у нас размещаться отдельно от загрузочного сектора и собираться уже с помощью gcc и gas. Синтаксис ассемблера gas соответствует требованиям AT&T, так что тут все будет попроще. Но для начала нам нужно прочитать ядро. Опять воспользуемся готовой функцией 0x2 прерывания 0x13.

recalibrate:
mov ah, #0
mov dl, #FLOPPY_ID
int 0x13 ; проведем реинициализацию дисковода.
jc recalibrate
call read_track ; вызов функции чтения ядра
jnc next_work ; если во время чтения не произошло
; ничего плохого, то работаем дальше

bad_read:
; если чтение произошло неудачно — выводим сообщение об ошибке
mov bp,#error_read_msg
mov cx,7
call write_message
inf1: jmp inf1 ; и уходим в бесконечный цикл.
; Теперь нас спасет только ручная перезагрузка
Сама функция чтения предельно простая: долго и нудно заполняем параметры, а затем одним махом считываем ядро. Сложности начнутся, когда ядро перестанет помещаться в 17 секторах (то есть 8.5КБ); но это пока в будущем, а сейчас вполне достаточно такого молниеносного чтения

read_track:
pusha
push es
push ds
mov di, #SYSSEG ; Определяем
mov es, di ; адрес буфера для данных
xor bx, bx
mov ch, #START_TRACK ;дорожка 0
mov cl, #START_SECTOR ;начиная с сектора 2
mov dl, #FLOPPY_ID
mov dh, #START_HEAD
mov ah, #2
mov al, #SYSSIZE ;считать 10 секторов
int 0x13
pop ds
pop es
popa
ret
Вот и все. Ядро успешно прочитано, и можно вывести еще одно радостное сообщение на экран.

next_work:
call kill_motor ; останавливаем привод дисковода
mov bp,#load_msg ; выводим сообщение
mov cx,#4
call write_message
Вот содержимое сообщения
load_msg:
.ascii «done»
.byte 0
А вот функция остановки двигателя привода.
kill_motor:
push dx
push ax
mov dx,#0x3f2
xor al,al
out dx,al
pop ax
pop dx
ret
На данный момент на экране выведено «Booting data …done» и лампочка привода флоппи-дисков погашена. Все затихли и готовы к смертельному номеру — прыжку в защищенный режим.

Для начала надо включить адресную линию A20. Это в точности означает, что мы будем использовать 32-разрядную адресацию к данным.

mov al, #0xD1 ; команда записи для 8042
out #0x64, al
mov al, #0xDF ; включить A20
out #0x60, al
Выведем предупреждающее сообщение — о том, что переходим в защищенный режим. Пусть все знают, какие мы важные.

protected_mode:
mov bp,#loadp_msg
mov cx,#25
call write_message
Сообщение:

loadp_msg:
.byte 13,10
.ascii «Go to protected mode…»
.byte 0
Пока у нас еще жив BIOS, запомним позицию курсора и сохраним ее в известном месте (0000:0x8000 ). Ядро позже заберет все данные и будет их использовать для вывода на экран победного сообщения.

save_cursor:
mov ah,#0x03 ; читаем текущую позицию курсора
xor bh,bh
int 0x10
seg cs
mov [0x8000],dx ;сохраняем в специальном тайнике
Теперь внимание, запрещаем прерывания (нечего отвлекаться во время такой работы) и загружаем таблицу дескрипторов

cli
lgdt GDT_DESCRIPTOR ; загружаем описатель таблицы дескрипторов.
У нас таблица дескрипторов состоит из трех описателей: нулевой (всегда должен присутствовать), сегмента кода и сегмента данных .

align 4
.word 0
GDT_DESCRIPTOR: .word 3 * 8 — 1 ; размер таблицы дескрипторов
.long 0x600 + GDT ; местоположение таблицы дескрипторов
.align 2
GDT:
.long 0, 0 ; Номер 0: пустой дескриптор
.word 0xFFFF, 0 ; Номер 8: дескриптор кода
.byte 0, CODE_ARB, 0xC0, 0
.word 0xFFFF, 0 ; Номер 0x10: дескриптор данных
.byte 0, DATA_ARB, 0xCF, 0
Переход в защищенный режим может происходить минимум двумя способами, но обе ОС, выбранные нами для примера (Linux и Thix) используют для совместимости с 286 процессором команду lmsw. Мы будем действовать тем же способом

mov ax, #1
lmsw ax ; прощай реальный режим.
; Мы теперь находимся в защищенном режиме.
jmpi 0x1000, 8 ; Затяжной прыжок на 32-разрядное ядро.
Вот и вся работа загрузочного сектора – не мало, но и не много. Теперь с ним мы попрощаемся и направимся к ядру.

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

org 511
end_boot: .byte 0
В результате скомпилированный код будет занимать ровно 512 байт, что очень удобно для подготовки образа загрузочного диска.

Первые вздохи ядра (head.S)

Ядро, к сожалению, опять начнется с ассемблерного кода. Но теперь его будет совсем немного.

Мы собственно зададим правильные значения сегментов для данных (ES, DS, FS, GS). Записав туда значение соответствующего дескриптора данных.

cld
cli
movl $(__KERNEL_DS),%eax
movl %ax,%ds
movl %ax,%es
movl %ax,%fs
movl %ax,%gs
Проверим, нормально ли включилась адресная линия A20 — простым тестом записи. Обнулим для чистоты эксперимента регистр флагов.

xorl %eax,%eax
1: incl %eax
movl %eax,0x000000
cmpl %eax,0x100000
je 1b
pushl $0
popfl
Вызовем долгожданную функцию, уже написанную на С.

call SYMBOL_NAME(start_my_kernel)
И больше нам тут делать нечего.

inf: jmp inf
Поговорим на языке высокого уровня (start.c)

Вот теперь мы вернулись к тому, с чего начинали рассказ. Почти вернулись, потому что printf() теперь надо делать вручную. Поскольку готовых прерываний уже нет, то будем использовать прямую запись в видеопамять. Для любопытных — почти весь код этой части, с незначительными изменениями, позаимствован из части ядра Linux, осуществляющей распаковку (/arch/i386/boot/compressed/*). Для сборки вам потребуется дополнительно определить такие макросы как inb(), outb(), inb_p(), outb_p(). Готовые определения проще всего одолжить из любой версии Linux.

Теперь, дабы не путаться со встроенными в glibc функциями, отменим их определение

#undef memcpy
Зададим несколько своих:
static void puts(const char *);
static char *vidmem = (char *)0xb8000; /*адрес видеопамяти*/
static int vidport; /*видеопорт*/
static int lines, cols; /*количество линий и строк на экран*/
static int curr_x,curr_y; /*текущее положение курсора */
И начнем, наконец, писать код на языке высокого уровня… правда, с небольшими ассемблерными вставками.

/* функция перевода курсора в положение (x,y).
* Работа ведется через ввод/вывод в видеопорт
*/
void gotoxy(int x, int y)
{
int pos;
pos = (x + cols * y) * 2;
outb_p(14, vidport);
outb_p(0xff & (pos >> 9), vidport+1);
outb_p(15, vidport);
outb_p(0xff & (pos >> 1), vidport+1);
}

/* функция прокручивания экрана. Работает,
* используя прямую запись в видеопамять
*/
static void scroll()
{
int i;
memcpy ( vidmem, vidmem + cols * 2, ( lines — 1 ) * cols * 2 );
for ( i = ( lines — 1 ) * cols * 2; i < lines * cols * 2; i += 2 ) vidmem[i] = ' '; } /* функция вывода строки на экран */ static void puts(const char *s) { int x,y; char c; x = curr_x; y = curr_y; while ( ( c = *s++ ) != '\0' ) { if ( c == '\n' ) { x = 0; if ( ++y >= lines ) {
scroll();
y—;
}
} else {
vidmem [ ( x + cols * y ) * 2 ] = c;
if ( ++x >= cols ) {
x = 0;
if ( ++y >= lines ) {
scroll();
y—;
}
}
}
}
gotoxy(x,y);
}
/* функция копирования из одной области
* памяти в другую. Заменитель стандартной функции glibc
*/
void* memcpy(void* __dest, __const void* __src,
unsigned int __n)
{
int i;
char *d = (char *)__dest, *s = (char *)__src;
for (i=0;i<__n;i++) d[i]="s[i];" }="}" /* функция, издающая долгий и протяжный звук. * Использует только ввод/вывод в порты поэтому * очень полезна для отладки */ make_sound() { __asm__(" movb $0xB6, %al\n\t outb %al, $0x43\n\t movb $0x0D, %al\n\t outb %al, $0x42\n\t movb $0x11, %al\n\t outb %al, $0x42\n\t inb $0x61, %al\n\t orb $3, %al\n\t outb %al, $0x61\n\t "); } /*А вот и основная функция*/ int start_my_kernel() { /*задаются основные параметры */ vidmem = (char *) 0xb8000; vidport = 0x3d4; lines = 25; cols = 80; /* считываются предусмотрительно * сохраненные координаты курсора */ curr_x=*(unsigned char *)(0x8000); curr_y=*(unsigned char *)(0x8001); /*выводится строка*/ puts("done\n"); /*уходим в бесконечный цикл*/ while(1); } Вот и вывели мы этот "Hello World" на экран. Сколько проделано работы, а на экране только две строчки Booting data ...done Go to proteсted mode ...done А что – плохо?! Закричала новая операционная система. Мир с радостью воспринял ее. Кто знает, может быть - это новый Linux ?... Подготовка загрузочного образа (floppy.img) Теперь подготовим загрузочный образ нашей системки.Для начала соберем загрузочный сектор. as86 -0 -a -o boot.o boot.S ld86 -0 -s -o boot.img boot.o Обрежем 32-битный заголовок и получим таким образом чистый двоичный код. dd if=boot.img of=boot.bin bs=32 skip=1 Соберем ядро gcc -traditional -c head.S -o head.o gcc -O2 -DSTDC_HEADERS -c start.c При компоновке НЕ ЗАБУДЬТЕ параметр "-T"! Он указывает, относительно какого смещения вести расчеты; в нашем случае, поскольку ядро грузится по адресy 0x1000, смещение соответствующее: ld -m elf_i386 -Ttext 0x1000 -e startup_32 head.o start.o -o head.img Отделим зерна от плевел, то есть чистый двоичный код от всяческих служебных заголовков и комментариев: objcopy -O binary -R .note -R .comment -S head.img head.bin И соединим воедино загрузочный сектор и ядро cat boot.bin head.bin >floppy.img
Образ готов. Записываем на дискетку (заготовьте несколько для экспериментов, я прикончил три штуки), перезагружаем компьютер и наслаждаемся…

cat floppy.img >/dev/fd0

Реклама

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

Мы будем писать загрузочный сектор для трехдюймовой дискеты с файловой системой FAT12. После окончания начальной загрузки программа POST находит активное устройство и загружает с него короткую программу загрузки ОС — загрузочный сектор. Загрузочный сектор это первый физический сектор устройства, в данном случае дискеты и его размет равен всего ничего 512 байт. С помощью этих 512 байт кода мы должны найти основную часть загрузчика операционной системы, загрузить его в память и передать ему управление. Заголовок файловой системы FAT находится в первом секторе дискеты, благодаря чему этот заголовок, содержащий всю необходимую информацию о файловой системе, загружается вместе нашим загрузчиком. Наш загрузочный сектор будет искать в корневом каталоге некоторый файл — загрузчик, загрузит его в память и передаст ему управление на его начало. А загрузчик уже сам разберется, что ему делать дальше. Я использую NASM, т.к. считаю, что он больше подходит для наших целей.
И так, приступим. Как я уже говорил, в начале нашего загрузочного сектора располагается заголовок FAT, опишем его:
; Общая часть для всех типов 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 мы переходим на наш код. Проведем небольшую инициализацию:
; Наши не инициализированные переменные
; При инициализации они затрут не нужные нам
; поля заголовка 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, выводящую сообщение на экран, рассмотрим позже.

Теперь нам нужно вычислить номера первых секторов корневого каталога и данных файлов.

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 — количество элементов в корневом каталоге

Теперь мы будем просматривать корневой каталог в поисках нужного нам файла
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.

Если файл найден, то загрузим его в память и передадим управление на его начало.

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.
; *************************************************
; * Чтение секторов с диска *
; *************************************************
; * Входные параметры: *
; * 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

Осталось всего ничего:
; Сообщения об ошибках
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 и загрузочный сектор может работать некорректно.

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.

Проверка готовности накопителя

Программа проверяет готовность устройства. Если устройство не готово, программа просит нажать клавишу ESC.

; Проверяем корректность и готовность устройства.
; Полностью завершенная COM-программа.
codeseg segment
assume CS:codeseg, DS:codeseg, ES:codeseg
org 100h

Begin: jmp Start
; —-переменные—-
Intvec dd ? ;старый вектор прерывания 24h
Luukku db ‘Disk not valid or ready. Hit Esc!’,10,13,’$’

Start:
;———— Основная программа ————
;Перехватываем прерывание 24h
push ES
mov AX,3524h ;вектор int 24h записывается в ES:BX
int 21h
mov word ptr Intvec,BX ;смещение
mov word ptr Intvec[2],ES ;сегмент
pop ES
;load a new int 24h
mov AX,2524h ;новый вектор 24h
mov DX,offset CError ;адрес
int 21h
;код для проверки готовности устройства
mov DL,1 ;1 — A:, 2 — B: и т.д..
mov AH,36h ;функция определения свободного места на диске
int 21h
cmp AX,-1 ;AX — число секторов в кластере -1
je Loppu ;выход если нет диска или не готов
;устройство готово
;здесь ваш код….
Loppu: int 20h ;завершаем COM-программу
;——— новое прерывание int 24h ——————
assume DS:nothing ;будут использоваться дальние вызовы
CError proc far
pushf ;сохраняем флаги
or AH,AH
js EiLevyke
push DX
push DS
mov AX,CS
mov DS,AX
assume DS:Codeseg
mov DX,offset Luukku
mov AH,9 ;выводим строку DS:DX
int 21h
mov AH,0
int 16h ;ждем нажатия клавиши
cmp AL,27 ;это Esc ?
jne EiEsc
mov AH,4Ch ;завершаем программу
int 21h
EiEsc: pop DS
assume DS:nothing
pop DX
popf
mov AL,1 ;еще раз
iret ;возвращаем управление главной программе
EiLevyke: popf ;восстанавливаем флаги
jmp CS:Intvec ;вызываем старый обработчик int 24h
CError endp
codeseg ends
end Begin

Рисуем пиксел в графическом режиме

Графические режимы могут быть разбиты на шесть групп в зависимости от количества бит, отводимых каждому пикселу:

1 бит/пиксел, 2 цвета, одна битовая плоскость:
CGA mode 6 разрешение 640*200
2 бит/пиксел, 4 цвета, одна битовая плоскость:
CGA mode 4 разрешение 320*200
4 бит/пиксел, 16 цветов, четыре битовых плоскости:
EGA mode 0Dh разрешение 320*200
EGA mode 0Eh разрешение 640*200
EGA mode 10h разрешение 640*350
VGA mode 12h разрешение 640*480
VESA mode 102h разрешение 800*600
VESA mode 104h разрешение 1024*768
VESA mode 106h разрешение 1280*1024
8 бит/пиксел, 256 цветов, одна битовая плоскость:
VGA mode 13h разрешение 320*200
VESA mode 100h разрешение 640*400
VESA mode 101h разрешение 640*480
VESA mode 103h разрешение 800*600
VESA mode 105h разрешение 1024*768
16 бит/пиксел, 65536 цветов, одна битовая плоскость(существуют также 32768-цветные режимы):
VESA mode 111h разрешение 640*480
VESA mode 114h разрешение 800*600
24 бит/пиксел, 16777216 цветов, одна битовая плоскость:
VESA mode 112h разрешение 640*480
Исключая 4-битные режимы пикселы в памяти располагаются на одной плоскости (plane), т.е., если координаты пиксела (x,y), то адрес, по которому располагается этот пиксел в памяти может быть вычислен как
Address = LineLength*y + Bits*x/8
где LineLength — количество байтов, занимаемых каждой строкой пикселов, а Bits — количество бит, занимаемым пикселом.

Исключениями являются режимы CGA номер 4 и 6, у которых четные и нечетные линии расположены в различных сегментах памяти.

В шестнадцатицветных режимах экранная память разделяется на 4 битовые плоскости. Каждый бит значения цвета пиксела расположен на своей плоскости. Адрес байта, хранящего пиксел с координатами x,y можно вычислить как
Address = LineLength*y + x/8
где LineLength — число байтов, занимаемых одной строкой.

Рисование пиксела с координатами x,y в 16-цветных режимах подразумевает установку бита во всех четырех плоскостях. Активная в данный момент плоскость выбирается записью в соответствующие порты видеокарты.

Режимы CGA, EGA и VGA поддерживаются всеми стандартными BIOS. Переключение в эти режимы обычно осуществляется простым вызовом функций BIOS.

Pixel$
Во всех режимах VGA следующая процедура Pixel$ может нарисовать пиксел. Нужно отметить, что процедура достаточно медленная, т.к. используются вызовы функций BIOS.

Pixel$ proc
;Рисует пиксел во всех режимах VGA.
;Входные данные: x в AX, y в BX, цвет в CX
;Выходные данные: регистры не сохраняются
mov DX,BX ;строка y
xchg AX,CX ;CX — колонка x, AL — цвет
sub BH,BH ;0 страница
mov AH,0Ch ;выводим пиксел
int 10h
ret
Pixel$ endp
Самый интересный режим VGA — это режим 13h с возможностью отображения 256 цветов и разрешением 320*200. Номер цвета 0…255 соответствуют значениям в палитре, где все цвета представлены в виде определенных сочетаний красной, зеленой и синей компонент. Следующая процедура VGApxl$ рисует пиксел в этом режиме. Она работает достаточно быстро, однако существуют еще более быстрые варианты.

;данные:
VGA_seg dw 0A000h ;сегмент памяти экрана VGA

VGApxl$ proc
;Рисует пиксел в режиме VGA 13h.
;Входные данные: x в AX, y в BX, цвет в CX
;Выходные данные: регистры AX и BX не сохраняются
xchg BH,BL ;умножаем y на 256, BL=0
add AX,BX ;AX = x+256y
shr BX,1 ;делим 256y на два
shr BX,1 ;BX = 256y/4 = 64y
add BX,AX ;BX = x+320y
mov AX,ES ;сохраняем значение ES
mov ES,VGA_seg ;сегмент памяти экрана VGA
mov ES:[BX],CL ;выводим байт на экран
mov ES,AX ;восстанавливаем значение регистра ES
ret
VGApxl$ endp

Функция синуса в 32-битной системе с фиксированной точкой
Процедура Rsin$ вычисляет тригонометрическую функцию sin от 32-битного аргумента. 32-битная система с фиксированной точкой определяется следующим образом:

;значение переменной F32bit = 4.750
F32bit dw 49152 ;дробная часть (0.75*65536)
dw 4 ;целая часть
Использование процедуры:
Входные данные: смещение аргумента в BX, смещение результата в AX. Аргумент задает угол в градусах.
Выходные данные: значение функции sin, записываемое в переменную, смещение которой определяется регистром AX. Значения регистров не сохраняются.

Например, sin(30.5°) вычисляется так:

Angle dd 001E8000h ;старший байт=30, младший байт=32768
Result dd ? ;сюда будет записан результат
….
mov AX,offset Result
mov BX,offset Angle
call Rsin$
В результате такого вызова вы получите результат 0.50752 в то время как правильное значени еравно 0.50754

Rsin$ proc
; значение синуса аргумента (двойное слово по смещению BX) вычисленное как двойное слово по смещению AX.
; Угол (по смещению BX) в градусах в диапазоне -360…360.
push AX ;сохраняем смещение результата
mov AX,[BX+2] ;целая часть угла
mov CX,[BX] ;дробная часть угла
mov DX,1 ;знак результата — +1 или -1
or AX,AX ;какой знак?
jns Rsin_1
not AX ;меняем знак
not CX
add CX,1
adc AX,0
neg DX ;также меняется знак результата
Rsin_1: ;теперь имеем угол в диапазоне 0…360
; уменьшаем диапазон до 0…180
cmp AX,180 ;угол больше 180?
jl Rsin_2
sub AX,180
neg DX ;изменяем знак результата
Rsin_2: ;теперь угол AX:CX в диапазоне 0…179.99998
cmp AX,90 ;угол больше 90?
jl Rsin_3
mov BX,180 ;вычисляем 180-угол
sub SI,SI
sub SI,CX
sbb BX,AX
mov AX,BX
mov CX,SI
Rsin_3: ;угол в AX:CX в диапазоне 0…90
push DX ;сохраняем знак результата в стеке
cmp AX,90 ;угол равен 90?
jne R_sin4
mov AX,1 ;возвращаем 1.0000
sub BX,BX
jmp short Rsin_9
R_sin4:
mov SI,offset Rsin_t ;таблица значений синусов
add SI,AX
add SI,AX
sub AX,AX ;целая часть значения синуса
mov BX,[SI] ;дробная часть
or CX,CX ;дробная чать равна 0?
jz Rsin_9
; интерполяция между значениями [SI] и [SI+2] mov AX,[SI+2] sub AX,BX ;AX = sin(a+1)-sin(a)
mul CX ;CX=0… 0.9999, результат DX= AX*CX/65536
add BX,DX ;добавим получившийся результат к значению из таблицы
sub AX,AX ;целая часть результата
Rsin_9:
pop DX ;корректируем знак результата AX:BX
or DX,DX
jns Rsin_loppu
not AX ;изменяем знак результата
not BX
add BX,1
adc AX,0
Rsin_loppu:
pop DI ;смещение результата
mov [DI+2],AX ;записываем целую часть
mov [DI],BX ;дробная часть
ret
Rsin$ endp
;таблица синусов
Rsin_t dw 0,1144,2287, 3430, 4572, 5712, 6850, 7987, 9121,10252
dw 11380,12505,13626,14742,15855,16962,18064,19161,20252,21336
dw 22415,23486,24550,25607,26656,27697,28729,29753,30767,31772
dw 32768,33754,34729,35693,36647,37590,38521,39441,40348,41243
dw 42126,42995,43852,44695,45525,46341,47143,47930,48703,49461
dw 50203,50931,51643,52339,53020,53684,54332,54963,55578,56175
dw 56756,57319,57865,58393,58903,59396,59870,60326,60764,61183
dw 61584,61965,62328,62672,62997,63303,63589,63856,64104,64332
dw 64540,64729,64898,65048,65177,65287,65376,65446,65496,65526
dw 0 ;для интерполяции

Пишем напрямую в видеопамять

; полностью завершенная COM-программа
codeseg segment
assume cs:codeseg, ds:codeseg, es:codeseg
org 100h
Code: jmp Start
x dw 50 ;координата x выводимого текста
y dw 20 ;координата y выводимого текста
Text db ‘string to be printed’,0 ;не забываем 0
Start:
mov AX,80 ;вычисляем адрес
mul y
add AX,x
shl AX,1 ;адрес в AX=160*y+2*x
mov DI,AX
mov SI,offset Text
push ES ;сохраняем ES
mov AX,0B800h ;сегмент экранной памяти в текстовом режиме
mov ES,AX
Print:
lodsb ;загружаем AL из DS:[SI] or AL,AL ;конец строки?
jz Ready ;да, AL=0
mov ES:[DI],AL ;символ для отображения
add DI,2 ;пропускаем байт атрибутов
jmp Print
Ready: pop ES ;восстанавливаем ES
;—————————————
int 20H
codeseg ends
end Code

Рисование в SVGA

Пикселы расположены линейно в памяти видеоадаптера. В 256-цветных режимах пиксел представляется одним байтом. Поэтому смещение точки с координатами (x,y) можно вычислить как 640*y+x в режиме с 640 пикселами по горизонтали. Единственное ограничение, связанное с такими вычисленими, — это то, что последний доступный пиксел, к которому может быть получен доступ, имеет координаты x=255, y=102, его смещение 65535. Это известное ограничение 64Kбайтных сегментов.

Чтобы обойти это ограничение, применяется переключение банков памяти. При этом переопределяется расположение физического адреса, которое соответствует логическому адресу. Так, логический адрес 0 соответствует физическому адресу 65536 если активен первый банк в видеодаптером с размером «окна» (granularity) 64 KB.

Логический адрес точки с координатами (x,y) определяется как 640*y+x-B*WG где B — номер банка и WG — размер «окна». Банк памяти может быть переключен с помощью функции AX=4F05h прерывания 10h в видеоадаптерах, поддерживающих стандарт VESA.

Следующая процедура рисует пиксел на экране. Координаты пиксела находятся в регистрах AX и BX, а в регистре CX передается цвет пиксела. В процедуре предполагается, что размер «окна» равен 64 KB, что справедливо, например, для чипов S3.

SVGA_bank dw 0 ;номер активного банка памяти
S_rivi dw 640 ;длина строки в байтах
VGA_seg dw 0A000h ;сегмент памяти экрана VGA
CBpxl$ proc ;рисует пиксел с координатами x=AX, y=BX, цвет=CX
xchg AX,BX ;теперь x=BX, y=AX
mul S_rivi
add AX,BX
adc DX,0 ;в DX помещается требуемый номер банка
mov DI,AX ;логический адрес
cmp DX,SVGA_bank ;банк корректен?
je Cxl256_OK
mov SVGA_bank,DX ;новый банк
mov AX,4F05h
xor BX,BX ;функция: устанавливаем банк DX, окно A
int 10h
Cxl256_OK:
mov BX,ES ;сохраняем сегмент
mov AL,CL ;цвет
mov ES,VGA_seg
stosb ;рисуем пиксел
mov ES,BX
ret
CBpxl$ endp

TSR: Завершаемся и остаемся в памяти

Инсталляция TSR-программы выполняется в три этапа:
Загрузка резидентной части в память. Проверка, не находится ли наша программа уже в памяти. Сохранение необходимой информации для дальнейшего удаления резидента из памяти. Освобождение памяти, занятой копией переменных окружения для экономии.
Установка параметров для работы резидентной части. Обычно на этом этапе перехватываются прерывания.
Завершение установочной программы, при этом резидентная часть остается в памяти.
;Структура программы TSR
Begin: ;Здесь начинается .COM-программа
jmp Install
;Сюда нужно поместить резидентную часть

Install:
;сюда поместите код установки
mov AH,31h ;завершиться и остаться резидентом
mov AL,0 ;возвращает результат =OK
mov DX,offset Install
mov CL,4
shr DX,CL ;делим на 16
add DX,1 ;объем резидентной части программы
int 21h

Чтение параметров командной строки

Параметры командной строки (сразу после имени файла) могут быть прочитаны с помощью следующей процедуры ReadCL.

Например, если ваша программа называется KOE.COM и вы запускаете ее, набрав команду

KOE 4abcs

в командной строке DOS, то процедура ReadCL вернет строку 4abcs в формате ASCIIZ.

ReadCL proc ;чтение параметров командной строки в буфер по адресу ES:[DI] ;DS должен остаться неизменным после запуска программы (=PSP)
mov SI,80h ;адрес парамтеров
xor CX,CX
mov CL,[SI] ;длина в байтах
inc SI ;игнорируем байт длины
rep movsb ;перемещаем строку в буфер
mov AL,0
stosb ;завершаем строку ASCIIZ нулем
ret
ReadCL endp

Таблица размещения файлов FAT

Первый сектор (с номером 0) диска — это загрузочный сектор. Его первые байты содержат следующую информацию:
byte
0-2 Переход на загрузочную программу
3-10 Имя в ASCII формате или что-нибудь еще
11-12 Байт на сектор
13 Секторов на кластер
14-15 Секторов в загрузочной записи =B
16 Количество копий FAT
17-18 Количество каталогов в корне диска
19-20 Секторов на диск
21 Тип диска =xx
22-23 Секторов на FAT =F
и т.д.
Первая таблица FAT начинается с B. Ее копия располагается в секторе B+F etc.
Можно детально рассмотреть FAT используя утилиту DEBUG. Не вносите изменений в таблицу FAT на жестком диске, если вы не уверены, что вы делаете.
Первая запись таблицы FAT выглядит так:

12 bit FAT: xx 0FFh 0FFh
16 bit FAT: xx 0FFh 0FFh 0FFh
xx — тип диска.
Затем, с кластера 2 начинаются элементы таблицы. Возможные значения перечислены в следующей таблице:
12-бит.FAT 16-бит. FAT
000h 0000h пусто
002h-0FEFh 0002h-0FFEFh использовано кластеров.
Значение-указатель на следующую запись в цепочке.
0FF0h-0FF6h 0FFF0h-0FFF6h зарезервировано
0FF7h 0FFF7h bad
0FF8h-0FFFh 0FFF8-0FFFFh последний кластер в цепочке
Вы можете читать сектора, используя прерывание 25h. Отметим, что это прерывание сохраняет флаги в стеке, так что после выполнения прерывания они должны быть восстановлены

Запуск дочерней программы
DOS выделяет всю доступную память текущей программе, независимо от того, какой объем реально необходим. Поэтому вы должны освободить часть памяти для того, чтобы загрузить и выполнить дочернюю программу. Это выполняется процедурой Setmem. Каждый параграф занимает 16 байт. Пространство, необходимое текущей программе вычисляется как размер в параграфах = Lseg — Psp + 1
где Lseg — сегмент, расположенный после последнего байта программы, а Psp — сегмент, в котором расположен psp программы.

Setmem proc
;Выделяет AX параграфов памяти текущей программе
:и очищает всю остальную память.
;Входные данные: количество выделяемых параграфов в AX
;Выходные данные: число реально выделенных параграфов в AX
mov BX,AX ;объем выделяемой памяти в 16-битных параграфах
mov AH,4Ah
int 21h ;ES должен указыват на сегмент PSP программы
mov AX,BX ;число выделенных параграфов
ret
Setmem endp
Следующий фрагмент кода запускает программу CHILD.COM с параметром /HELP.

;сегмент данныхt:
ChildName db ‘CHILD.COM’,0 ;имя файла в виде строки ASCIIZ

; сегмент кода:
mov AX,CS
mov SegCmdLine,AX
mov SegFCB1,AX
mov SegFCB2,AX
push DS ;сохраняем регистры
push ES
mov CS:Shell_SS,SS ;сохраняем только регистр CS
mov CS:Shell_SP,SP
;exec-function
mov DX,offset ChildName ;DS:DX — указатель на строку, содержащую имя файла
mov AX,CS
mov ES,AX
mov BX,offset CS:Parm_Table ;таблица параметров ES:BX
mov AX,4B00h ;загрузить и выполнить программу
int 21h
cli ;запрещаем прерывания
mov SS,CS:Shell_SS ;восстанавливаем регистры
mov SP,CS:Shell_SP
sti ;разрешаем прерывания
pop ES
pop DS
cld ;флаг направления (direction flag) = 0
jc ThereWasError ;ошибка

; эти данные должны быть определены в сегменте кода
CmdLineTail db 6,’ /HELP’,13 ;6 — число символов
even ;faster this way
Shell_SS dw 0 ;указатель стека
Shell_SP dw 0
Parm_Table dw 0 ;наследуем переменные окружения родительской программы
dw offset CmdLineTail
SegCmdLine dw 0 ;сюда будет записан CS
dw 5Ch ;блок управления файлом (FCB) #1
SegFCB1 dw 0 ;сюда будет записан CS
dw 6Ch ;блок управления файлом (FCB) #2
SegFCB2 dw 0 ;сюда будет записан CS

Линейные преобразования в системах с фиксированной точкой

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

Числа предствалены в 32-битном формате с фиксированной точкой. Старшее слово содержит целую часть числа, а младшее слово — дробную часть. Предполагается, что используются только положительные числа.

Код использует 32-битные инструкции, но может быть откомпилирован и 16-битным компилятором.

; данные
ConvFactor dw 26214 ;младший байт коэффициента преобразования 25.4
dw 25 ;старший байт
Inches dw 32768 ;младший байт представления 12.5 дюймов
dw 12 ;старший байт
mMeters dw ? ;младший байт результата в мм
dw ? ;старший байт
; код
db 66h
mov AX,Inches ;mov EAX,dword ptr ConvFactor
db 66h
mul ConvFactor ;результат в EDX:EAX
mov CL,16
db 66h
shr AX,CL ;shr EAX,16
mov mMeters,AX ;младший байт результата
mov mMeters+2,DX ;старший байт