Для перевода процессора в PM используется глобальная таблица дескрипторов, в которой хранится для каждого сегмента отдельный дескриптор. Его вид: 0,1 байты - размер 2-4 байты - база 5 байт - бит наличия сегмента, уровень привилегий, тип сегмента (0010 - данные, 1010 - код) 6 байт - 1, разрядность (0 - 16, 1 - 32), 0, 0, размер 7 байт - база Итак, задача для перехода в PM такова: построить GDT с дескрипторами для кода и данных (база - 0, размер - 4 Гб), включить адресную линию A20 (она отключена для совместимости со старыми процессорами), изменить в регистре CR0 один бит в 1 (флаг того, что мы в PM), после чего прыгнуть в 32-битный сегмент. Введение Итак, в прошлой части мы подготовили плацдарм для наших действий. Теперь перед нами обширные горизонты, от которых надо постараться не потерять голову. Давайте немного подумаем, какие первоочередные задачи сейчас перед нами стоят? Во-первых, мы умеем только загружать нашу ОС, у нас нету так ядра, которому бы передавалось управление. Далее наша ОС должна иметь минимальный набор функция для контакта с пользователем, а потому перед нами встает еще задача обеспечения функций работы с железом. Для этого нам будет необходимо написать таблицу прерываний, а так же научить наше детище работать, скажем, для начала с клавиатурой и жестким диском. Начнем с ядра. Кстати, немного о его концепции. Оно бывает нескольких видов: микроядро, монолитное, экзоядро. Различаются они по делению исполняемых задач по привилегиям. Так в микроядре на правах супервизора работает крайне малая часть функций, остальное - подключаемые библиотеки. Монолитное же ядро очень велико, все основные функции ОС сосредоточены именно в нем. Микроядерная архитектура имеет огромные преимущества над традиционной монолитной и только один серьезный недостаток - пониженное быстродействие. Экзоядерная архитектура превосходит все другие архитектуры по производительности в несколько раз, однако она слишком сложна. Она отличается тем, что если микро и монолитные ядра обеспечивают задачи распределения ресурсов и создания виртуальной машины, то экзоядра эти задачи разделают. В каком смысле? Например, дать возможность пользовательским программам обращаться к конкретной ячейке памяти, конкретному сектору диска, конкретным портам других внешних устройств. Мы будем рассматривать микроядро, т.е. в ядре будет сосредоточен базовый минимум функций, всю остальную работу будет обеспечивать набор драйверов. Хотя надо заметить, что в ближайших двух статьях ссылки на тип ядра у нас не будет. Ближе к делу. Как вы помните, написанный в предыдущем выпуске загрузчик обладает возможностью загружать и выполнять код, находящийся в файле kernel.bin и скомпонованный по адресу 0x200000. Теперь мы уже можем работать на С. Первой же проблемой, с которой мы столкнемся при создании kernal.bin, будет то, что содержимое функции main() вынесется компилятором на самый верх программы, т.е. мы будем из загрузчика передавать управление в никуда. Для того, чтобы от этого избавиться, надо передавать управление ядру через переходник (c.asm): [BITS 32] [EXTERN k_main] [GLOBAL _go] _go: mov esp, 0x200000-4 call k_main Само ядро у нас будет выглядеть вот так (kernel.c): void k_main() { for(;;); } Большое, не правда ли? Перейдем к работе с железом. Для начала нам потребуется написать несколько функций, позволяющих работать с портами и прерываниями, после чего можно будет уже самостоятельно описывать прерывания (что мы рассмотрим на примере клавиатуры). При возникновении прерывания, процессору известен только номер прерывания. Поскольку сам по себе этот номер ничего не говорит о том, где именно находится процедура-обработчик, эту информацию процессор должен почерпнуть из специальной таблицы (таблица прерываний). В режиме реальных адресов таблица прерываний находится по абсолютному адресу 0:0 - 0:0x400 и представляет из себя 256 абсолютных четырехбайтных адресов процедур-обработчиков. Процессору остается только взять из этой таблицы адрес соответствующего номеру прерывания обработчика, и выполнить переход на этот адрес. В защищенном режиме таблица прерываний называется таблицей дескрипторов прерываний IDT (Interrupt Descriptors Table) и на ее местонахождение указывает регистр IDTR (interrupt descriptors table register). Уже по названию можно судить, что в ней находятся не просто адреса обработчиков, а дескрипторы обработчиков прерывания. В качестве таких дескрипторов могут выступать дескрипторы трех типов: шлюз прерывания, шлюз ловушки и шлюз задачи. На данном этапе нас интересуют первые два. Они отличаются тем, что при выполнении обработчика прерывания все прерывания сразу запрещаются до завершения обработчика, а флаг трассировки сбрасывается. Дескриптор представляет собой следующую запись: 0,1 байты: 2,3 байты: селектор сегмента 4: пустой 5: 0-3 содержит тип шлюза (0110 - 16 битный шлюз прерывания, 0111 - 16 битный шлюз ловушки, 1110 - 32 битный шлюз прерывания, 1111 - 32 битный шлюз ловушки) 4 - тип дескриптора 5,6 - уровень привилегий сегмента 7 - наличие сегмента 6,7: биты смещения процедуры-обработчика Итак, в таблице должно быть 256 дескрипторов (по количеству возможных прерываний), каждый из которых будет иметь свой личный номер, использующийся для его вызова при соответствующем прерывании. При вызове обработчика прерываний процессор помещает в стек регистр флагов и адрес возврата Начнем мы с функций (inter.c), которые позволят нам устанавливать обработчики прерываний, а также запрещать и разрешать обработку прерываний: #define IT 0x100000 #define IR 0x100800 #define SCS 0x8 // В inst() some является обработчиком intr, тип шлюза задаем параметром kind; де-факто мы создаем/редактируем дескриптор в таблице. void inst(unsigned char int, void (*some)(), unsigned char kind) { char * it=IT; unsigned char i; unsigned char a[8]; a[0]= (unsigned int)some & 0x000000FF; a[1]=( (unsigned int)some & 0x0000FF00) >> 8; a[2]=SCS; a[3]=0; a[4]=0; a[5]=kind; a[6]=( (unsigned int)some & 0x00FF0000) >> 16; a[7]=( (unsigned int)some & 0xFF000000) >> 24; for(k=0;k<8;k++){ *(it+int*8+k)=a[k]; } } // Загружаем IDTR void int_l() { unsigned short *limit = IR; unsigned int *place = IR+2; *limit = 256*8 - 1; *place = IT; asm("lidt 0(,%0,)"::"a"(IR)); asm("sti"); } // Разрешаем прерывания void int_e() { asm("sti"); } // Запрещаем прерывания void int_d() { asm("cli"); } Необходимо так же помнить и о работе с портами, иначе прерывания нам становятся не нужны. Создадим port.c, главными строками в котором будут: asm("outb %b0,%w1":: "a"(value), "d"(port)); asm("inb %w1, %b0": "=a"(value): "d"(port)); (value - простое число, я надеюсь, что скелет вы напишете сами, раз читаете эту статью) Собственно, теперь мы готовы заниматься самими прерываниями. Как я и говорил, начнем мы с клавиатуры, однако не стоит забывать и о таймере, который тикает у нас каждую 1/18,3 секунды. Перед выполнением задачи обработчик прерывания должен сохранить регистры, а по завершении обработки - послать байт 0x20 в порт 0x20 (сигнал "конец прерывания" контроллеру прерываний), после чего восстановить регистры. Дабы не использовать в тексте каждый раз загромождений кодом из asm, я введу отдельный макрос: #define IRQ_HANDLER(func) void func (void);\ asm(#func ": pusha \n call _" #func " \n movb $0x20, %al \n outb %al, $0x20 \n popa \n iret \n");\ void _ ## func(void) (можно не пытаться в него вникнуть, главное понимайте зачем он нужен) Возьмем code.h (содержит таблицу символов): char codes[] = {0, 0, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 8,'\t','q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']','\n', 0,'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '`',0,'\\', 'z', 'x', 'c', 'v','b', 'n', 'm', ',', '.', '/',0,'*',0,' ',0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,'-', 0, 0, 0,'+', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; char codes_sh[] = {0, 0, '!', '@', '#', '$', '%', '^', '&', '*', '(', ')','_', '+', 8, '\t', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '\n', 0, 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '"', '~', 0, '|', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 0, '*', 0, ' ', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, '-', 0, 0, 0, '+', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // 8 - backspace, на ctrl, alt и пр. стоят заглушки - 0. Теперь напишем следующий finter.c: #include "code.h" // Я говорил вам о таймере, здесь пока будет простая заглушка, можно потом будет ввести сюда более серьезные функции. Например, подсчет времени. IRQ_HANDLER(irq_time) { } // Работа с клавиатурой довольно проста - мы должны считывать с нее коды клавиш, при этом не забывая, что у нас может быть два состояния - с shift и без него. char sh = 0; IRQ_HANDLER(irq_kb) { unsigned char code, asc; unsigned char reg; // С 60-го порта вы получаем данные code = ipb(0x60); // ipb, opb - мои названия функций из port.c switch(code) { // С shift case 0x36: case 0x2A: sh = 1; break; // Без shift case 0x36 + 0x80: case 0x2A + 0x80: sh = 0; break; default: // Если клавишу отпустили, то не делать ничего, а если нажали, то в зависимости от значения shift преобразовать скан-код в в разные символы ASCII if(code >= 0x80) { } else { if(sh){ asc = codes_sh[code]; } else { asc = codes[code]; } if(asc != 0) { // pc - выводит на экран символ, эту функцию я опишу ниже. pc(asc); } } break; } // После чего надо показать, что работа по считыванию символа проделана. Для этого считываем состояние клавиатуры (61 порт), преобразуем в нем старший бит в 1, после чего запишем назад. reg = ipb(0x61); reg |= 1; opb(0x61, creg); } // Теперь установка обработчиков прерываний void iint() { inst(0x20, &irq_time, 0x8e); inst(0x21, &irq_kb, 0x8e); int_l(); int_e(); } Вот, теперь у нас есть два прерывания. Надо бы проверить их работоспособность, да и вообще хотелось бы, чтобы наша ОС умела нам хоть что-то показывать, потому необходимо описать функции для работы с монитором. Это делать мы будет напрямую через видеопамять (video.c). #define VW 80 // Ширина экрана #define VH 25 // Его высота #define RAM 0xb8000 // Видеопамять int cur; // Где находится курсор int param; // Параметры // Идея проста - "бегаем" по экрану при прочтении очередного символа, заносим этот символ в видеопамять, меняем параметры символа. void initial() { cur = 0; param = 7; } void color(char c) { param = c; } void clear() { char *vid = RAM; int k; for (k = 0; k < VH*VW; k++) { *(vid + k*2) = ' '; } cur = 0; } // Единственная хоть чем-то интересная для нас функция, здесь мы как раз и записываем символ, меняем положение курсора. void pc(char c) { char *vid = RAM; int k; switch © { case '\n': cur+=VW; cur-=cur%VW; break; default: *(vid + cur*2) = c; *(vid + cur*2+1) = param; cur++; break; } // Здесь необходимо описать что делать, если положение курсора вышло за пределы экрана. Решение простое - сдвиг экрана на одну строку. if(cur>VW*VH){ for(i=VW*2;i<=VW*VH*2+VW*2;i++){ *(vid+i-VW*2)=*(vid+i); } cur-=VW; } } } На сегодня остался лишь один вопрос. У нас много кода (я описал все за исключением функций работы с портами), но его же надо как-то заставить работать. Как это сделать? Стоит добавить в k_main вызовы функций: initial(), iint(), а так же clear(). Компилируется все это довольно просто, достаточно следующей последовательности команд: gcc -ffreestanding -c -o keyboard.o keyboard.c gcc -ffreestanding -c -o inter.o inter.c gcc -ffreestanding -c -o finter.o finter.c gcc -ffreestanding -c -o port.o port.c gcc -ffreestanding -c -o video.o video.c gcc -ffreestanding -c -o kernel.o kernel.c nasm -felf -o c.o c.asm ld --oformat binary -Ttext 0x200000 -o kernel.bin c.o video.o inter.o port.o finter.o kernel.o nasm -fbin -o boot2.bin boot2.asm nasm -fbin -o all_image.bin boot1.asm (В текст второго загрузчика в конце теперь надо добавить kernel_binary: incbin 'kernel.bin') Итак, с этой частью статьи все. Мы научились работать с железом на простейшем уровне, смогли наконец-то заставить работать набор всех этих кодов. В следующей (уже более объемной и серьезной) части будет рассматриваться работа с жестким диском и файловой системой. Устройство жесткого диска На нем по физическому адресу 0-0-1 располагается главная загрузочная запись (MBR). В структуре MBR находятся следующие элементы: Внесистемный загрузчик (NSB); Таблица описания разделов диска (таблица разделов, PT). Располагается в MBR по смещению 0x1BE и занимает 64 байта; Таблица разделов описывает размещение и характеристики имеющихся на винчестере разделов. Разделы диска могут быть двух типов - primary (первичный, основной) и extended (расширенный). Максимальное число primary-разделов равно четырем. Наличие на диске хотя бы одного primary-раздела является обязательным. Extended-раздел может быть разделен на большое количество подразделов - логических дисков. Сигнатура MBR. Последние два байта MBR должны содержать число 0xAA55. Т.е. структура MBR выглядит так: программа анализа таблицы разделов и загрузки System Bootstrap с активного раздела (смещение 0, размер 446) --> Partition 1/2/3/4 entry ( элемент таблицы разделов) (Смещение для первого 0x1BE, размер каждого 16), Сигнатура 0xAA55 (Смещение - 0x1FE, размер 2) Partition entry выглядит так: - признак активности (0 - раздел не активный, 0x80 - раздел активный). Он служит для определения, является ли раздел системным загрузочным и есть ли необходимость производить загрузку операционной системы с него при старте компьютера. Активным может быть только один раздел. Элемент первичного раздела указывает сразу на загрузочный сектор логического диска (в первичном разделе всегда имеется только один логический диск), а элемент расширенного раздела - на список логических дисков, составленный из структур, которые именуются вторичными MBR (SMBR). SMBR имеет структуру, аналогичную MBR, но загрузочная запись у него отсутствует (заполнена нулями), а из четырех полей описателей разделов используются только два. Первый элемент раздела при этом указывает на логический диск, второй элемент указывает на следующую структуру SMBR в списке. Последний SMBR списка содержит во втором элементе нулевой код раздела. (1 байт) - Номер головки диска, с которой начинается раздел. (1 байт) - Номер цилиндра и номер сектора, с которых начинается раздел. Биты 0-5 содержат номер сектора, биты 6-7 - старшие два бита 10-разрядного номера цилиндра, биты 8-15 - младшие восемь битов номера цилиндра. (2 байта) - Код типа раздела System ID. Указываюет на принадлежность данного раздела к той или иной операционной системе. (1 байт) - Номер головки диска, на которой заканчивается раздел (1 байт) - Номер цилиндра и номер сектора, которыми заканчивается раздел Три байта на каждый номер. (2 байта) - Абсолютный (логический) номер начального сектора раздела, т.е. число секторов перед разделом (4 байта) - Размер раздела (число секторов). Размер раздела в секторах (4 байта) Теперь можно подобраться чуть ближе к практике. Рассмотрим модуль для Linux, показывающий информацию обо всех раздела жесткого диска. #define signt 0xAA55 #define DEV "/dev/hda" #define pt_s 0x10 // размер элемента таблицы разделов struct stype { u8 part_type; u8 *part_name; }; // Соответствие кода раздела с его символьным отображением struct stype 386_stype[] = { {0x00, "Empty"}, {0x01, "FAT12"}, {0x04, "FAT16 <32M"}, {0x05, "Extended"}, /* DOS 3.3 */ {0x06, "FAT16"}, /* DOS при >=32M */ {0x0b, "Win95 FAT32"}, {0x0c, "Win95 FAT32 (LBA)"}, {0x0e, "Win95 FAT16 (LBA)"}, {0x0f, "Win95 Ext'd (LBA)"}, {0x82, "Linux swap"}, /* и для солярис */ {0x83, "Linux"}, {0x85, "Linux extended"}, {0x07, "HPFS/NTFS"} }; // Именно такая таблица принята для файловых систем #define p_n (sizeof(386_stype) / sizeof(386_stype[0])) // определяем здесь число элементов в массиве, что определен выше. int disk; // дескриптор файла устройства u8 mbr[512]; // сюда считаем MBR struct pt { u8 boot; u8 startp[3]; u8 typep; u8 endp[3]; u32 sectbef; u32 secttot; } pt_k[max]; // Структура по таблице разделов #define max 20 // max логических дисков // Главная функция: int main() { int i = 0; u64 sk; // Открываем файл устройства, получаем таблицу разделов, потом сверяем сигнатуру: hard = open(DEV, O_RDONLY); if(disk < 0) { perror("open"); exit(-1); } rmaintab(); if(csign() < 0) { printf("Invalid sign!\n"); exit(-1); } // Поиск идентификатора расширенного раздела. Если есть – считаем смещение и получаем информацию по логическим разделам. for(; i < 4; i++) { if((pt_k[i].typep == 0xF) || \ (pt_k[i].typep == 0x5) || \ (pt_k[i].typep == 0x0C)) { sk = (u64)pt_k[i].sectbef * 512; rexttab(sk); break; } } // Отображаем информацию: showinf(); return 0; } // Проверка сигнатуры 0xAA55: int csign() { u16 sign = 0; memcpy((void *)&sign, (void *)(mbr + 0x1FE), 2); #ifdef DEBUG printf("Sign - 0x%X\n", sign); #endif if(sign != SIGNT) return -1; return 0; } // Чтение таблицы разделов: void rmaintab() { if(read(disk, mbr, 512) < 0) { perror("read"); close(disk); exit(-1); } memset((void *)pt_k, 0, (PT_SIZE * 4)); memcpy((void *)pt_k, mbr + 0x1BE, (pt_s * 4)); return; } // Чтения расширенной таблицы разделов: void rexttab(u64 sk) { int number = 4; // С этой позиции pt_k будет заполняться информацией о логических дисках u8 smbr[512]; // Она принимает один параметр sk - смещение к расширенному разделу от начала диска. Для получения информации о логических дисках воспользуемся циклом: for(;;number++) { // Читаем SMBR, по offset seek от начала диска: memset((void *)smbr, 0, 512); pread64(hard, smbr, 512, sk); // Заполним часть pt_k. Первый элемент будет указывать на логический диск, а следующий - на следующую структуру SMBR: memset((void *)&pt_k[number], 0, PT_SIZE * 2); memcpy((void *)&pt_k[number], smbr + 0x1BE, pt_size * 2); // Поправка в поле "Номер начального сектора" - отсчет ведется от начала диска: pt_k[number].sectbef += (sk / 512); // Код типа раздела равен нулю? Больше логических дисков нет! if(!(pt_k[number + 1].typep)) break; // Offset к следующей SMBR: sk = ((u64)(pt_k[number].sectbef + pt_k[number].secttot)) * 512; } return; } // Покажем информацию о найденных логических дисках: void showinf() { int i = 0, n; printf("Num - %d\n", P_N); for(; i < max; i++) { if(!pt_k[i].typep) break; printf("\nType %d - ", i); for(n = 0; n < P_N; n++) { if(pt_k[i].typep == 386_stype[n].typep) { printf("%s\n", 386_stype[n].namep); break; } } if(n == P_N) printf("Unknown\n"); printf(" Boot flag - 0x%X\n", pt_k[i].bootable); printf(" Number of sectors in partition%d - %d\n", i, pt_k[i].secttot); printf("Number of sector before partition%d - %d\n\n", i, pt_k[i].sectbef); } return; } Интерфейс АТА А теперь опять к теории… У каждого диска есть стандартный набор регистров, состоящий из двух блоков: командных регистров и управляющих регистров. Первый служит для посылки команд устройству и передачи информации о его состоянии. Состав блока командных регистров: 1. Регистр состояния/команд – в режиме read_only отражает текущее состояние устройства в процессе выполнения команды. Чтение регистра состояния разрешает дальнейшее изменение его бит и сбрасывает запрос аппаратного прерывания. В режиме write принимает коды команд для выполнения. Назначение бит регистра состояния: Бит 7 - BSY указывает на занятость устройства. При =1 игнорирует попытки записи в блок командных регистров. При =0 регистры командного блока доступны. Бит 6 - DRDY указывает на готовность устройства к восприятию любых кодов команд. Бит 5 - DF - индикатор отказа устройства. Бит 4 - DSC - индикатор завершения поиска трека. Бит 3 - DRQ - индикатор готовности к обмену словом или байтом данных. Бит 2 - CORR - индикатор исправленной ошибки данных. Бит 1 - IDX - индекс, трактуется специфично для каждого производителя. Бит 0 - ERR - индикатор ошибки выполнения предыдущей операции. Дополнительная информация содержится в регистре ошибок. 2. Регистр номера цилиндра и номера сектора. 3. Регистр номера устройства и головки. Биты 7 и 5 - зарезервированы. Бит 6 - единичным значением указывает на применение режима адресации LBA. При нулевом значении бита используется режим CHS. Бит 4 - DEV - выбор устройства. При DEV=0 выбрано устройство Master, при DEV=1 - устройство- Slave. Биты 3-0 имеют двоякое назначение в зависимости от выбранной системы адресации. В режиме CHS они содержат номер головки, в режиме LBA - старшие биты логического адреса. 4. Регистр данных. 5. Регистр ошибок. 6. Регистр свойств. 7. Регистр счетчика секторов содержит число секторов, участвующих в обмене. Блок управляющих регистров - для управления устройством и получения байта его состояния. В состав блока входят альтернативный регистр состояния и регистр управления устройством. Альтернативный регистр состояния имеет те же биты, что и основной, но его чтение не приводит ни к каким изменениям состояния устройства. В регистре управления устройством биты 7-3 зарезервированы, бит 0 всегда нулевой, используются только два бита: Бит 2 - SRST - программный сброс, действует все время, пока бит не будет сброшен. Бит 1 - IEN# - инверсный бит разрешения прерывания. Рассмотрим теперь взаимодействие хоста и устройства: 1. Хост читает регистр состояния устройства, дожидаясь нулевого значения бита BSY. 2. Дождавшись освобождения устройства, хост записывает в регистр номера устройства и головки байт, у которого бит DEV указывает на адресуемое устройство. 3. Хост читает основной регистр состояния адресованного устройства, дожидаясь признака его готовности (DRDY = 1). 4. Хост заносит требуемые параметры в блок командных регистров. 5. Хост записывает код команды в регистр команд. 6. Устройство устанавливает бит BSY и переходит к исполнению команды. Для команд, не требующих передачи данных (ND): 7. Завершив исполнение команды, устройство сбрасывает бит BSY и устанавливает запрос прерывания. К этому моменту в регистрах состояния и ошибок уже имеется информация о результате выполнения. Для команд, требующих чтения данных в режиме PIO: 7. Подготовившись к передаче первого блока данных по шине АТА, устройство устанавливает бит DRQ. Если была ошибка, она фиксируется в регистрах состояния и ошибок. Далее устройство сбрасывает бит BSY и устанавливает запрос прерывания. 8. Зафиксировав обнуление бита BSY (или по прерыванию), хост считывает регистр состояния, что приводит к сбросу прерывания от устройства. 9. Если хост обнаружил единичное значение бита DRQ, он производит чтение первого блока данных в режиме PIO (адресуясь к регистру данных). Если обнаружена ошибка, считанные данные могут быть недостоверными. Не утомились нудной частью? Возвращаемся к практике. Задача в общем-то такая же - получить информацию идентификации устройства и считать MBR. #include #include #include #include #define d_data 0x1f0 // регистр данных #define d_error 0x1f1 // регистр ошибок #define d_nsector 0x1f2 // регистр счетчика секторов #define d_sector 0x1f3 // регистр стартового сектора #define d_lcyl 0x1f4 // регистр младшего байта номера цилиндра #define d_hcy 0x1f5 // регистр старшего байта номера цилиндра #define d_current 0x1f6 // 101a???? , a=устройство, ????=головка #define d_status 0x1f7 // регистр состояния #define out(val,port) asm( "outb %%al, %%dx" ::"a"(val),"d"(port) ) #define in_byte(val,port) asm( "inb %%dx, %%al" :"=a"(val) :"d"(port) ) #define in_word(val,port) asm( "inw %%dx, %%ax" :"=a"(val) :"d"(port) ) // Чтение байта из порта, запись байта в порт и запись слова в порт соответственно. void d_busy() { unsigned char stat; do { in_b(stat,d_status); } while (stat & 0x80); return; } void d_ready() { unsigned char stat; do { in_b(stat,d_status); } while (!(stat & 0x40)); return; } // Занято устройство или свободно? void cerr() { unsigned char a; ib_b(a,d_status); if (a & 0x1) { perror("d_status"); exit(-1); } return; } // Не было ошибок? void gident(struct d_drive *hd) { unsigned short b = 0; int i = 0; unsigned short buf[0x100]; memset(buf,0,0x100); d_busy(); // Когда станет свободно, в регистр номера устройства и головки заносим значение 0xA0. Бит 4 равен 0, а значит, нами выбрано ведущее устройство. Бит 6 оставим нулевым: out_b(0xA0,d_current); hd_ready(); // Теперь диск полностью готов. В регистр команд заносим код команды идентификации устройства - 0xEC. Out_b(0xEC,d_status); // Считываем информацию по блоку данных: do { hd_busy(); cerr(); in_w(a,d_data); if((i>=10 && i<=19) || (i>=27 && i<=46)) asm( "xchgb %%ah, %%al" :"=a"(a) :"0"(a)); buf[i++] = a; } while(d_request()); // Считанную информацию сохраним в буфере buff1. Копируем полученную информацию из буфера buff1 в структуру dreg. После чего чистим буфер. memcpy(hd,(struct dreg *)buf,0x100); memset(buf,0,0x100); return; } Теперь рассмотрим чтение при адресация CHS void r_chs(unsigned short N, unsigned short sect, unsigned short cyl, unsigned short head, unsigned short *buf) // N - число секторов для чтения, sect - стартовый сектор, cyl - стартовый цилиндр, head - номер головки, buf - буфер, куда все помещается. { int i = 0; unsigned short a; if((!N) || (!sect)) return; hd_busy(); // В регистр номера устройства и головки заносим соответствующие данные. Out_b(0xA0|head,d_current); hd_ready(); // Заполним блок командных регистров: out_b(N,d_nsector); out_b(sect,d_sector); out_b(cyl,d_cyl); out_b((cyl >> 8),d_cyl); // В регистр команд записываем код команды чтения секторов с повторами - 0x20. out_b(0x20,d_status); // Считываем блок данных в буфер buf: do { hd_busy(); cerr(); in_w(a,d_data); buf[i++] = a; } while(d_request()); // Считываем последние 4 байта и выходим из функции: In_w(a,d_data); buf[i++] = a; in_w(a,d_data); buf[i] = a; return; } // Чтения сектора в режиме адресации LBA. void read_hd_sector_lba(unsigned short N, unsigned int lba, unsigned short *buf) { // N - число секторов для чтения, lba - номер блока, buf - буфер, куда все помещается int i = 0; unsigned short a; if(!N) return; hd_busy(); // В регистре номера устройства и головки бит 6 устанавливаем в 1, а биты 3-0 будут содержать старшие биты логического адреса (27-24): out_b(0xE0|((lba & 0x0F000000) >> 24),d_current); hd_ready(); // В блок командных регистров заносим требуемые параметры. В регистр младшего байта номера цилиндра - биты 15-8 логического адреса, а в регистр старшего байта номера цилиндра - биты 23-16 логического адреса. В регистр команд - команду чтения секторов с повторами. out_b(N,d_sector); out_b((lba & 0x000000FF),d_sector); out_b(((lba & 0x0000FF00) >> 8),d_cyl); out_b(((lba & 0x00FF0000) >> 16),d_cyl); out_b(0x20,d_status); do { d_busy(); cerr(); in_w(a,d_data); buf[i++] = a; } while(d_request()); in_w(a,d_data); buf[i++] = a; in_w(a,d_data); buf[i] = a; return; } Рассмотрим главную функцию, на этом и закончим текст программы. int main () { // N - число секторов для чтения, sect - номер сектора, cyl - номер цилиндра, head - номер головки, lba - номер логического блока struct d_drive hd; int out; unsigned short N = 1; unsigned int sect, cyl, head, lba; unsigned short buf[0x100*N]; memset(buf,0,0x100*N); memset(&hd,0,sizeof(struct d_drive)); // Во избежание лишних ошибок запросим у системы разрешения доступа к портам в диапазоне 0x1f0 - 0x1f7: ioperm(0x1f0,8,1); getident(&hd); // Вуаля: printf("Number - %s\n",hd.serial_no); printf("Model - %s\n",hd.model); printf("Cyl count - %d\n",hd.cur_cyls); printf("Head count - %d\n",hd.cur_heads); printf("Sector count - %d\n",hd.cur_sectors); printf("Blocks count - %d\n",hd.lba_capacity); Прочтение MBR в режиме CHS: sect = 1; cyl = 0; head = 0; r_chs(N,sect,cyl,head,buf); Тоже самое - в режиме LBA: lba = 0; r_lba(N,lba,buf); ioperm(0x1f0,8,0); Вот и вся программа. Пожалуй, на этом очередную часть можно закончить, в следующей части будет продолжение работы с жесткими дисками, а точнее описание работы с файловой системой, а так же, наверное, мы затронем работу с CD.
|