Ожидайте..

 
 
Новости
Документы
Прочее
Авторизоваться
 

Теория

Во-первых, нужно определить или поставить конечную цель. Задайтесь вопросом какая ОС у вас будет и что она должна делать? В нашем случае для примера будем строить самую простую ОС, поэтому она должна делать минимум.

Для создания самой минимальной работоспособной версии ОС нужно уметь делать следующие шаги:

1.       Выделить память под задачу

2.       Прочитать код в память

3.       «Прыгнуть» на выполнение задачи

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

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

4.       Попросим создать дескриптор для другой задачи

5.       Загрузим другую задачу в память «задача1»

6.       Осуществим переключение задачи

В этом месте надо понимать что при переключении начинает выполняться «задача1», до некоторого момента. Этот момент может быть:

·         тик таймера

·         программный вызов переключения задачи

·         перевод задачи в спящий режим

·         удаление текущей задачи

·         авария в выполняемой задаче

После обхода всех задач, «задача0» продолжает выполняться с момента переключения. После этого в цикле начнем загрузку и выполнение «задача2» и т.д. до тех пор пока не выполним запуск всех задач. После этого мы как бы выполнив все этапы завершим выполнение «задачи0». Но т.к. «задача0» имеет части кода для обработки прерываний и пр. – нам нельзя разрушать «задачу0». Поэтому сделаем в конце зацикливание либо переведем в состояние глубокого сна.

Алгоритм

Для решения такой задачи построим следующий алгоритм.

1.       Выделить память под задачу

Нужно понимать, что такое физический адрес, логический адрес, страница памяти. Для выделения памяти под задачу нужно так же понимать механизм. Вся физическая память доступна «задаче0» и она будет распределять, высвобождать и если нужно перемещать память. Память для «задачи0» описывается в таблицах PD (page directory) и PT (page table). Т.е. у нас есть запись 0 в PD указывающая на таблицу PT. Следующая запись в PD указывает на следующую таблицу PT. Запись 0 в таблице PT указывает на местонахождение страницы с логическими адресами с 0x00000000 по 0x00000FFF. Запись 1 в таблице PT указывает на местонахождение страницы с логическими адресами с 0x00001000 по 0x00001FFF. И т.д.

 

PD

PT

 

 

CR3 ->

0x000

0x000

0x00000000

0x00000FFF

0x001

0x00001000

0x00001FFF

0x002

0x00002000

0x00002FFF

 

 

0x1FF

0x001FF000

0x001FFFFF

0x001

0x000

0x00200000

0x00200FFF

0x001

0x00201000

0x00201FFF

0x002

0x00202000

0x00202FFF

 

 

0x1FF

0x002FF000

0x002FFFFF

 

 

 

0x1FF

0x000

0x1FF00000

0x1FF00FFF

0x001

0x1FF01000

0x1FF01FFF

0x002

0x1FF02000

0x1FF02FFF

 

 

0x1FF

0x1FFFF000

0x1FFFFFFF

 

Примерно это выглядит именно так. Более подробно смотрите в документации, но вначале именно это я не совсем понимал. Так вот. Это дерево страниц для «задачи0». Т.е. физический адрес равен логическому. Для других задач мы должны строить свое дерево для каждой из них. Поэтому:

a.       Получим следующий адрес для размещения PD

b.      Пометим, что эта страница теперь занята

Т.к. в «задаче0» располагаются обработчики прерываний и прочие функции, то спроецируем используемые страницы «задачи0» в PD «задачи1»:

c.       Заполним PD теми страницами, которые использует «задача0»

                                                               i.      Добавим страницу PT

                                                             ii.      Пометим, что эта станица теперь занята

                                                            iii.      Заполним соответствующие записи в PT

d.      Заполним соответствующую запись в PD

 

2.       Прочитать код в память

В связи с дополнениями этот пункт перетекает в пункт 5.

 

3.        «Прыгнуть» на выполнение задачи

Т.к. мы добавили многозадачность то в этом пункте нам нужно лишь сделать переключение задачи. Поэтому этот пункт трансформируется в пункт 6.

4.       Попросим создать дескриптор для другой задачи

Создаем запись в таблице задач. Заполняем необходимые параметры для выполнения задачи.

a.       Создать запись задачи

                                                               i.      Выделить память под эту запись

                                                             ii.      Заполнить указатель на PD

                                                            iii.      Заполнить EIP

                                                           iv.      Выделить память под стек, добавить запись в PD и PT, заполнить ESP, EBP

                                                             v.      Установить состояние как NORMAL

                                                           vi.      Заполнить поле next_task у предыдущей записи

Тут возникает потребность в использовании кучи. Поэтому я добавил реализацию кучи пунктом 7.

 

5.       Загрузим другую задачу в память «задача1»

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

Начало

Размер в секторах

Описание

1

1

Boot-сектор загружаемый при старте

2

TAIL_LENGTH (1)

Хвост загрузчика

3

2

Таблица файлов

4

~

Файлы

 

Таблица файлов:

Начало

Размер в байтах

Описание

0х00

8

Имя файла

0х08

4

Начальный сектор файла

0х0с

4

Размер файла

 

Т.к. компилятор делает код с выравниванием 0x40000, то будем загружать в это логическое адресное пространство.

a.       Выделим необходимое количество страниц (размер_бинарника/4096)

b.      Пометим их как занятые и пропишем их в PD и PT

c.       Загрузим файл в память эти страницы

 

6.       Осуществим переключение задачи

Здесь нужно сохранить регистры и заполнить их значениями другой задачи.

a.       Получим указатель на следующую задачу

b.      Сохраним регистры

c.       Загрузим регистры значениями, сохраненными для новой задачи

d.      Загрузим CR3, т.е. переключимся в PD задачи

e.      Сохраним EIP для возврата

f.        Прыгнем в новый EIP

 

7.       Работа с кучей

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

При выделении будем придерживаться следующего алгоритма:

a.       Ищем свободное место в куче требуемого размера

b.      Если место не подходит, то ищем дальше

c.       Если подходит или больше чем нужно, то

d.      Помечаем как занятое место нашим размером, а остаток как незанятое

e.      Возвращаем указатель

При освобождении:

a.       Помечаем место как свободное

b.      Запускаем функцию слепления рядомстоящих свободных мест

Запись кучи будет простой. 1 байт статус, 4 байта размер области и сама область. Если первая запись будет примерно такой:
Свободно, 0х100000_байт, ~

То при выделении будут две записи:
Занято, 0х100_байт, ~, Свободно, 0хFFF00 байт, ~

Подробный алгоритм запуска

1.       Загрузка «задачи0»

a.       Перенесем загрузчик в другую область

b.      Подгрузим хвост загрузчика

c.       Подготовим GDT и перейдем в PM

d.      Загрузим «задачу0»

e.      Прыгнем в нее

2.       Запуск остальных задач

a.       Инициализируем кучу

                                                               i.      Сделаем запись в куче

b.      Инициализируем процессор

                                                               i.      Настроим GDT

                                                             ii.      Настроим IDT

                                                            iii.      Настроим CR3

c.       Инициализируем диспетчер задач

                                                               i.      Создадим дескриптор «задачи0»

d.      Выполним задачи

                                                               i.      Создадим PD&PT

                                                             ii.      Прочитаем размер файла и выделим в PD задачи

                                                            iii.      Загрузим задачу в физические адреса

                                                           iv.      Создадим дескриптор задачи

e.      Зациклим «задачу0»

Реализация

1.       Загрузка «задачи0»

На этом этапе нужно понимать, что система при обнаружении загрузчика, помещает его код по адресам 0x07c0:0000 или 0x0000:0x7c00 и передает ему управление. На этом этапе бывают подводные камни и чтобы не тратить время на их поиск лучше всего перенести загруженные 512 байт в какое-нибудь место памяти. Перейти туда и догрузить остаток загрузчика, если он не влез в 510 байт кода. Процессор находится в реальном режиме, так что для пользования всеми благами 32-х разрядного режима процессор нужно перевести в защищенный режим. Но прежде чем перевести я рекомендую загрузить сначала хвост загрузчика средствами BIOS.

Для компиляции я использую ассемблер nasm, он входит в cygwin, так что достать его будет не трудно. Строка компиляции выглядит так:

nasm -f bin boot.asm -o boot.bin

a.       Перенесем загрузчик в другую область

%define DEF_INITSEG 0x0800
       mov    ax,0x07c0
       mov    ds,ax
       mov    ax, DEF_INITSEG
       mov    es,ax
       mov    cx,256
       sub    si,si
       sub    di,di
       cld
       rep    movsw
       jmp    DEF_INITSEG:go
go:
       mov    ax, cs
       mov    ds, ax
       mov    es, ax
       mov    ss, ax
       mov    sp, 0x0000

b.      Тут не лишним будет сохранить диск загрузки

mov    [BOOT_DRIVE], dl

c.       Подгрузим хвост загрузчика

mov al, [Boot_length]      ; количество читаемых секторов
mov ah, 0x02
mov bx, DEF_INITSEG ; куда грузим (сегмент)
mov es, bx
mov bx, 0x0200      ; куда грузим (смещение)
mov cx, 0x0002      ; с какого сектора начинать
mov dh, 0x00
mov dl, [BOOT_DRIVE]
int 0x13
jc fail

Код до этого места должен поместиться в первые 512 байт. В конце 512 байт нужно поместить сигнатуру 0xaa55:

times 510-($-$$) db 0
dw 0AA55h

a.       Загрузим «задачу0»

Есть два варианта загрузки. Первый это пересчитывать расположение сектора из LBA в CHS. Второй использовать продвинутую функцию 0x42 прерывания 0x13. Рекомендую второй вариант т.к. это проще. Для этого нам нужна структура, где указывается LBA сектор:

DAP
   
db 0x10 ; size of DAP
    db 0
DAP_count
   
db 0x01 ; count of blocks
    db 0
DAP_seg
   
dw 0x0000   ; offset
    dw 0x0000   ; segment
DAP_lba
   
dw 0x0000
   
dw 0x0000
   
dw 0x0000
   
dw 0x0000

Т.к. мы точно знаем, что первый файл это файл «задачи0», то просто получим местоположение и размер из первой записи. И соответственно чтение будет таким:

    mov word [DAP_lba], 2
   
mov word [DAP_lba+2], 0x0000
   
mov word [DAP_lba+4], 0x0000
   
mov word [DAP_lba+6], 0x0000
   
mov word [DAP_seg], 0x0000
   
mov word [DAP_seg+2], KERNEL_CODE_BASE/0x10
   
mov ah, 0x42
   
mov dl, [BOOT_DRIVE]
   
mov si, DAP
   
int 0x13
   
jc fail

    mov      ax, KERNEL_CODE_BASE/0x10
    mov      es, ax
    mov ax, [es:0x000c]
    mov dx, [es:0x000e]
    mov bx, 512
    div bx

    mov word [DAP_count], ax
    mov      ax, [es:0x0008]
    mov word [DAP_lba], ax
    mov word [DAP_lba+2], 0x0000
    mov word [DAP_lba+4], 0x0000
    mov word [DAP_lba+6], 0x0000
    mov word [DAP_seg], 0x0000
    mov word [DAP_seg+2], KERNEL_CODE_BASE/0x10
    mov ah, 0x42
    mov dl, [BOOT_DRIVE]
    mov si, DAP
    int 0x13
    jc fail

b.      Подготовим GDT и перейдем в PM

Сам GDT у меня выглядит так:

gdtr:
        dw gdt_end - gdt - 1   
; GDT limit
        dd DEF_INITSEG*0x10 + gdt                  ; GDT base
; -----------------------------------------------
;                      GDT
; -----------------------------------------------
gdt:
times 8 db 0           
; NULL Descriptor
SYS_CODE_SEL  equ  $-gdt
        dw 0xFFFF      
; limit 15:0
        dw 0            ; base 15:0
        db 0            ; base 23:16
        db 0x9A         ; type = present, ring 0, code, non-conforming, readable
        db 0xCF         ; page granular, 32-bit
        db 0            ; base 31:24
SYS_DATA_SEL  equ  $-gdt
        dw 0xFFFF      
; limit 15:0
        dw 0            ; base 15:0
        db 0            ; base 23:16
        db 0x92         ; type = present, ring 0, data, expand-up, writable
        db 0xCF         ; page granular, 32-bit
        db 0            ; base 31:24
gdt_end:
    dw 0

А перевод процессора в режим PM такой:

    ; enable A20
    in  al, 0x92
    or  al, 2
    out 0x92, al
    ; cli
    cli
    in  al, 0x70
    or  al, 0x80
    out 0x70, al
    lgdt [gdtr]
    mov eax, cr0
    or al, 1
    mov cr0, eax
[BITS 32]   
; All code from now on will be 32-bit
db 0x66
    jmp SYS_CODE_SEL:do_pm+DEF_INITSEG*0x10
do_pm:
    mov ax, SYS_DATA_SEL     
; Update the segment registers
    mov ds, ax                ; To complete the transfer to
    mov es, ax                ; 32-bit mode
    mov ss, ax
    xor eax, eax
    mov gs, ax
    mov fs, ax
    ; Update ESP
    mov esp, KERNEL_STACK_BASE

c.       Прыгнем в нее

    mov dl, [DEF_INITSEG*0x10+BOOT_DRIVE]
    jmp SYS_CODE_SEL:KERNEL_CODE_BASE

Первая строчка нужна, чтоб передать задачи номер диска, с которого произошла загрузка. В этом месте в принципе, если есть необходимость можно передать и другие параметры.

2.       Запуск остальных задач

Так как теперь мы можем использовать любой язык программирования (на этом этапе без использования стандартных библиотек и вызовов ядра) поэтому будем писать для GCC. Использовать C++ можно, но для этого нужно сначала описать стандартные вызова типа malloc, _main/_WinMain@16 и прочие. В рамках нашей ОС это делать пока не будем, а для успешной реализации потребуется следующие строки для компиляции:

gccffreestandingc main.c

Здесь мы получим объектный файл, который должны скомпоноваться:

ld -Ttext 0x25000 –nostdlib main.o other.o

Здесь есть два замечания. Во-первых в файле main.c должна быть единственная функция в которой будет переход на реальную функцию main. При компоновке объект этого файла должен идти первый в списке.

Параметр –Ttext определяет выравнивание данных. Проще говоря, нужно указать место, где будет располагаться «задача0». После этого на выходе получается бинарный файл с точкой входа 0-ой байт от начала, т.е. нам будет достаточно прыгнуть в место расположения «задачи0». Т.к. у нас может быть несколько файлов кода, то откомпилируем их аналогично main.c и добавим в строку компоновки в конец.

После компоновки конвертируем в бинарный файл:

objcopy -O binary a.exe task0.bin

Итак, наш main.c:

void _main(){
       task0();
}

Имя главной процедуры должно быть именно таким, она вызывает процедуру task0 на выполнение. Соответственно _main идет с самого начала бинарника, а task0 может находится где угодно.

Для работы нам нужны некоторые функции, такие как вывод значения в порт, получения значения из порта, заполнение памяти значениями и т.д.

// Write len copies of val into dest.
void memset(u1 *dest, u1 val, u4 len)
{
    u1 *temp = (u1 *)dest;
    u4 i;
    for (i=0 ; i < len; i++) temp[i] = val;
}

u1 inb(u2 port){
       u1 data;
       asm("inb (%w1)" :"=a" (data):"Nd" (port));
       return data;
}
u2 inw(u2 port){
       unsigned short data;
       asm("inw (%w1)" :"=a" (data):"Nd" (port));
       return data;
}
u4 inl(u2 port){
       unsigned short data;
       asm("inl (%w1)" :"=a" (data):"Nd" (port));
       return data;
}
void outb(u2 port, u1 data){
       asm("outb (%w1)" : : "a" (data), "dN" (port));
}
void outw(u2 port, u2 data){
       asm("outw (%w1)" : : "a" (data), "dN" (port));
}
void outl(u2 port, u4 data){
       asm("outl (%w1)" : : "a" (data), "dN" (port));
}

А так же для того чтоб получать информацию напишем функции вывода:

u1 *screenbuffer = (u1*)0xb8000;

Это буфер экрана. Будем просто записывать байты символов в соответствующую позицию. Я не буду тут расписывать подробно как это сделать. Вариантов вывода множество. В файле debug.c я напишу несколько функций, которые выведут строку и в случае необходимости сдвинут экран для новой строки.

a.       Инициализируем кучу

В модуле будут такие используемые функции init_heap, halloc, ahalloc, hfree.

                                                               i.      Сделаем первую запись в куче

       u1 *state = (u1*)HEAP_ALLOCATE;
       u4 *size = (u4*)HEAP_ALLOCATE+1;
       state[0] = HS_FREE;
       size[0] = HEAP_SIZE-5;

                                                             ii.      Выделение в куче

                                                            iii.      Освобождение указателя

b.      Инициализируем процессор

В модуле будут такие используемые функции init_sys.

                                                               i.      Настроим IDT

Настройки IDT скучны, поэтому я особо останавливаться не буду, это все описано в мануалах, но для нас интересным будет сам алгоритм. При поступлении прерывания процессор начинает выполнять код указанный в записи IDT. Тут мы сохраняем в стек номер прерывания и прыгаем на процедуру обработки всех прерываний (isr_common_stub). Сохраняем все регистры и вызываем обработчик прерываний на С (isr_handler). Дальше некоторая хитрость: если в таблице векторов назначен обработчик, то вызываем его, если нет, то завершаем вызов прерывания. Восстанавливаем регистры. Если у нас прерывание аппаратное то мы должны снять сигналы в PIC.

Для установки своего обработчика прерывания используется функция register_interrupt_handler. Для запрещения прерывания я использую функции cli(), sti() которая будет увеличивать или уменьшать счетчик, соответственно если этот счетчик не равен 0 то переключения задачи происходить не будет.

                                                             ii.      Настроим CR3

Тут тоже ничего особо умного. Получаем область памяти, выровненную по размеру страницы. Очистим ее для использования в качестве PD. Спроецируем логические адреса на физические. И наконец-то включим страничный режим:

       PD = (u4*)ahalloc(PAGE_SIZE,PAGE_SIZE);
      
memset(PD, 0, PAGE_SIZE);
      
make_physical_addr(PD, 0x00000000, maxmem-1);
   
asm("movl %0, %%cr3" :: "r" (PD));
   
asm("movl %cr0, %eax\n\t"
        "
orl $0x80000000, %eax\n\t"
        "
movl %eax, %cr0");

c.       Инициализируем диспетчер задач

                                                               i.      Создадим дескриптор «задачи0»

Тут все просто: заполняем соответствующие поля.

    current_task = (task_t*)TR_ALLOCATE;
    tasks = current_task;
    tasks->id = next_pid++;
    tasks->timer_tick = 0;
    asm("movl %%cr3, %0" : "=r" (tasks->pdir));
    tasks->state = TS_NORMAL;

Здесь одно пояснение. pdir нужно заполнить текущим значением.

                                                             ii.      Реализуем переключение задач

Если не очень-то хочется разбираться с переключением, то в общих чертах это выглядит так. Ищем задачу у которой статус установлен в нормальный.

    task_t *new_task = current_task;
    do{
        if ( ((u4)new_task+sizeof(task_t)) > (TR_ALLOCATE+TR_SIZE) )
            new_task = tasks;
        else
            new_task = (task_t*)((u4)new_task+sizeof(task_t));
    }while(new_task->state != TS_NORMAL);

Получаем ESP, EBP, CR3. Получим EIP хитрым способом:

    eip = read_eip();

В файле sys_interrupts.s есть эта процедура

_read_eip:
  pop eax
  jmp eax

Проверим eip и если он помечен как 0x12345, то мы находимся в переключенной задаче. Иначе сохраняем ESP, EBP, CR3, EIP в дескрипторе задачи. Установим что текущая задача это новая. Загрузим ESP, EBP, CR3, EIP новой задачи и прыгнем в новую задачу.

    current_task->esp = esp;
    current_task->ebp = ebp;
    current_task->eip = eip;
    current_task->pdir = pdir;
    current_task = new_task;
    asm("movl %0, %%cr3" :: "r" (current_task->pdir));
    asm volatile("         \
        cli;                 \
        mov %2, %%esp;       \
        mov %3, %%ebp;       \
        mov %0, %%ecx;       \
        mov $0x12345, %%eax; \
        sti;                 \
             movl %1, %%cr3;      \
        jmp *%%ecx           "
            : : "r"(current_task->eip), "r"(current_task->pdir), "c"(current_task->esp), "a"(current_task->ebp));

d.      Выполним задачи

                                                               i.      Прочитаем размер файла и выделим в PD задачи

Тут еще один объемный как кажется с самого начала момент: как читать с диска если функции BIOS нам уже не доступны. И как всегда есть несколько вариантов.

·         Первый вариант – это возвращаться в реальный режим и делать пользоваться функциями BIOS, но этот путь не стоит использовать т.к. BIOS потихоньку себя изживает и не стоит пользоваться устаревшими методами.

·         Второй – использовать возможности виртуальной машины, но этот метод опять же устарел, да и для виртуальной машины нужно обеспечить доступ к областям памяти, подготовить отдельный сегмент GDT.

·         Третий вариант и насколько я могу трезво судить самый правильный – это использовать порты ввода-вывода, общаясь с оборудованием напрямую.

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

void hdd_read_sector_nosafe(u2 *buff, u4 sector){
    u1 state = inb(hdd_port+7);
    while ((state & 0x40) != 0x40){
        state = inb(hdd_port+7);
    }
    outb(hdd_port+2, 1);    // sector count
    outb(hdd_port+3, (sector&0x000000ff));
    outb(hdd_port+4, (sector&0x0000ff00)>>8);
    outb(hdd_port+5, (sector&0x00ff0000)>>16);
    outb(hdd_port+6,((sector&0x0f000000)>>24) | 0x40 | ((!hdd_port & 0x80)>>3));
    outb(hdd_port+7, 0x20);
    state = inb(hdd_port+7);
    u4 count = 0x4000;
    while (((state & 0x80) != 0x80) && ((state & 0x08) != 0x08)){
        state = inb(hdd_port+7);
        count--;
        if (count == 0){
            printk("HDD:\tdrive timeout.\n");
            return -1;
        }
    }
    for (count=0; count<0x100; count++){
        buff[count] = inw(hdd_port);
        state = inb(hdd_port+7);
    }

    return 0x100;
}

Мне кажется это очень немного кода для чтения с диска. Ну и для безопасного чтения будет служить следующая функция:

u4 hdd_read_sector(u1 *buff, u4 sector){
    u4 timeout=1000;
    if (hdc_in_use != 0){
        //printk(": HDC : We must wait previos read.\n");
    }
    u4 ii;
    while ((hdc_in_use != 0) && (timeout>0)){
        switch_task();
        timeout--;
    }
    if (hdc_in_use != 0){
        printk("HDD:\t!!! Time out for waiting HDC...\n");
        return -1;
    }
    hdc_in_use++;
    u4 size = hdd_read_sector_nosafe((u2*)buff, sector);
    hdc_in_use--;
    switch_task();
    return size;
}

Теперь можем спокойно прочитать размер файла и его местоположение:

       if (tablefs == 0){
             tablefs = (u4*)halloc(2*512);
             hdd_read_sector(tablefs, 3);
             hdd_read_sector(tablefs+512, 4);
       }
       u4 pos = tablefs[(index-1)*0x10+0x08];
       u4 size = tablefs[(index-1)*0x10+0x0c];

                                                             ii.      Создадим PD&PT

       u4 *PD = (u4*)make_PD();
       make_logical_addr(PD, 0x00040000, 0x00040000+size-1);

                                                            iii.      Загрузим задачу в физические адреса

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

       u4 i, j=0, addr;
       for ( i=0; i<size; i=i+512 ){
             addr = get_physical_addr(mPD, 0x40000+i);
             hdd_read_sector(addr, pos+j);
             j++;
       }

Не забудем про стек:

       make_logical_addr(mPD, 0x35000, 0x40000-1);

                                                           iv.      Создадим дескриптор задачи

       task_t *task = (task_t*)new_task();
       task->pdir = (u4)mPD;
       task->eip = 0x40000;
       task->esp = 0x40000;
       task->ebp = 0x40000;
       task->state = TS_NORMAL;

e.      Зациклим «задачу

Мы могли бы просто зациклить ее, но мы сделаем трюк благодаря которому мы будем видеть то что задача не зависла а работает:

       i = 0;
       u1 *sim = (u1*)(0xb8000+(10+3*80)*2);
       while(1){
             i++;
             if (i>10000000){
                    i=0;
                    sim[0]++;
             }
       }

3.       Прочие задачи

По аналогии с «задачей0» делаем файл main.c и task1.c или task2.c. Здесь стоит упомянуть момент. Так  как у других задач и другой PD, то помимо начальной области нужно отобразить и область видеобуфера.

a.       Задача1

Эта задача будет также в цикле инкрементировать символ на экране, что даст нам информацию о том, что задача не зависла.

       u4 i = 0;
       u1 *sim = (u1*)(0xb8000+(13+3*80)*2);
       while(1){
             i++;
             if (i>10001000){
                    i=0;
                    sim[0]++;
             }
       }

b.      Задача2

В ней можно сделать точно такое же решение, а можно и что-нибудь тестировать. Например, деление на 0.

       u4 i = 0;
      
u1 *sim = (u1*)(0xb8000+(15+3*80)*2);
      
u4 j = 100;
      
while(j>0){
            
i++;
            
if (i>10000000){
                   
i=0;
                   
sim[0]++;
            
j--;
             }
       }
      
asm("xor %bx, %bx\n\t"
             "
div %bx");
      
while(1);

Сборка и тестирование

Итак, соберем диск. Для этого нам нужно создать файл eduos.asm в котором будем вкладывать готовые бинарники. Если размер бинарника не дотягивает до границы сектора (512 байт) то дополним нулями. Чтобы не запудривать себе голову контроллером FDC будем сразу делать образ HDD. Для этого нужно создать образ с помощью утилиты из bochs «bximage.exe». Узнаем и прописываем параметры. И делаем такого же размера образ диска. В моем случае это образ размером 106831872 байта.

incbin "boot/boot.bin"
times 512*2-($-$$) db 0
; ---------------------------------------------------------------------
db "task0"
db 0,0,0
dd (start_task0-$$)/512
dd (end_task0-start_task0)
db "task1"
db 0,0,0
dd (start_task1-$$)/512
dd (end_task1-start_task1)
db "task2"
db 0,0,0
dd (start_task2-$$)/512
dd (end_task2-start_task2)
times 512*4-($-$$) db 0
; ---------------------------------------------------------------------
start_task0:
incbin "task0/task0.bin"
end_task0:
times ((end_task0-$$)/512+1)*512-($-$$) db 0
; ---------------------------------------------------------------------
start_task1:
incbin "task1/task1.bin"
end_task1:
times ((end_task1-$$)/512+1)*512-($-$$) db 0
; ---------------------------------------------------------------------
start_task2:
incbin "task2/task2.bin"
end_task2:
times ((end_task2-$$)/512+1)*512-($-$$) db 0
; ---------------------------------------------------------------------
;for vmware 102Mb
times 102*1024*1024-0x1e000-($ - $$) db 0

Для отладки можно использовать несколько способов.

1.       Bochs

Когда мы создавали образ диска то программа нам вывела строку для конфигурации bochs.

ata0-master: type=disk, mode=flat, translation=auto, path="D:\WORK\BUILD\OS\Edu.OS\eduos.hdd", cylinders=207, heads=16, spt=63, biosdetect=auto, model="Generic 1234"

Для дополнительной отладки не лишним будет использовать точки останова. Для этого нужно вставить в параметры строку «magic_break: enabled=1», а в коде добавить инструкцию «xchg bx, bx».

2.       VMWare + gdb

Для отладки нужно чуть больше действий но отладка намного интересней но, правда, только одной из задач. Для этого нужно откомпилировать код с ключом «-g». Добавить виртуальный жесткий диск с типом «monolithicFlat». В файле wmdk из папки с параметрами виртуальной машины нужно заменить строку расположения файла диска и параметрами самого диска. Для моего образа это выглядит так:

# Disk DescriptorFile
version=1
encoding="windows-1251"
CID=9e017bb4
parentCID=ffffffff
isNativeSnapshot="no"
createType="monolithicFlat"

# Extent description
RW 208656 FLAT "D:\WORK\BUILD\OS\Edu.OS\eduos.hdd" 0

# The Disk Data Base
#DDB

ddb.adapterType = "ide"
ddb.geometry.sectors = "63"
ddb.geometry.heads = "16"
ddb.geometry.cylinders = "207"
ddb.uuid = "60 00 C2 9d 18 05 d2 e3-49 0c 62 21 da b8 f5 99"
ddb.longContentID = "093b4dab4de9af4abfd963319e017bb4"
ddb.virtualHWVersion = "7"

А в файле vmx нужно добавить строку

debugStub.listen.guest32=1

После этого можно запускать машину и подключаться к ней, например, так:

gdb task0/a.exe -ex "target remote localhost:8832"

 

Исходники по тексту

 Если вы нашли ошибки или у вас есть замечания по тексту, то можете связаться со мной по емаилу qeos()qeos.ru