Eigene Betriebssystementwicklung am PC  -  Teil 3


Stand Oktober 2009 - Dr. Erhard Henkes            

Teil 1    Teil 2  

Inhaltsübersicht
 

User-Space und Kernel-Design

In diesem Teil geht es vor allem um das Wechselspiel zwischen Kernel und User-Applikationen. Wir schaffen zunächst die notwendigen Grundlagen.

User-Programme in Assembler



Mit dieser Aufgabe hat der letzte Part des Tutorials Sie verabschiedet. Wir integrieren zusätzlich eine Übersicht über unser Filesystem auf der RAM Disk und eine Möglichkeit Text hinter dem Prompt einzugeben (allerdings nur provisorisch):

PrettyOS [Version 0.1.0001]
(C) 2009 henkessoft.de

<DIR> dev
35      file1
35      file2
35      file3
586     Test-Programm

$>


Dies gelingt uns in program.asm mit folgenden Syscalls:

[BITS 32]
start:
   mov ebx, 15   ; foreground color
   mov ecx, 0    ; background color
   mov eax, 2    ; settextcolor
   int 0x7F
  
   ;prompt
   mov ebx, 0x24 ; '$'
   mov eax, 1    ; putch
   int 0x7F
   mov ebx, 0x3e ; '>'
   mov eax, 1    ; putch
   int 0x7F
 
.loop:  
   mov eax, 6    ; k_checkKQ_and_print_char  
   int 0x7F
   jmp .loop

 


In ckernel.c wurde aufgeräumt und auch andere Test-Prints deaktiviert. Das nur als Test-Funktion geschaffene k_checkKQ_and_print_char müsste man nun allerdings in ein getchar() umwandeln und den Prompt vor Überschreiben schützen etc.

User-Programme in C

Um User-Programme  in der Sprache C anstelle direkt in Assembler schreiben zu können, benötigen wir ein zusätzliches Modul, das die Nahtstelle bildet zwischen den durch Interrupt 0x7F aufgerufenen Kernel-Funktionen (syscalls) und den in User-Applikationen üblichen Funktionsnamen. Wir schaffen auf diese Art eine Application Programming Interface (API), zu deutsch: eine Programmierschnittstelle. Im konkreten Fall handelt es sich noch um eine funktionsorientierte Programmierschnittstelle.

Nachstehend finden Sie die beiden Files userlib.h und userlib.c, die als Basis für die API dienen, und einen ersten Test der beiden jetzt auch in C zur Verfügung gestellten Funktionen mit program.c:


// userlib.h


#ifndef USERLIB_H
#define USERLIB_H

void settextcolor(unsigned int foreground, unsigned int background);
void putch(unsigned char val);

#endif


// userlib.c


#include "userlib.h"

void settextcolor(unsigned int foreground, unsigned int background)
{
    asm volatile( "int $0x7F" : : "a"(2), "b"(foreground), "c"(background) );
}

void putch(unsigned char val)
{
    asm volatile( "int $0x7F" : : "a"(1), "b"(val) );
}


// program.c


#include "userlib.h"

int main()
{
  settextcolor(14,1);
  putch('T');
  putch('e');
  putch('s');
  putch('t');
  return 0;
}


Nun haben wir zwei main-Funktionen, eine im Kernel und eine im User-Programm. Als Starter für unser kleines User-Programm verwenden wir ein winziges Assembler-Programm namens start.asm, das die Funktion main der User-Applikation aufruft:

[BITS 32]
extern _main
global _start

_start:
    call _main

end:
    jmp end

Ansonsten verwenden wir folgendes makefile und Linkerskript:


ASFLAGSOBJ= -O32 -f elf
NASM= nasmw
 
CFLAGS= -Wall -O -ffreestanding -fleading-underscore -nostdlib -nostdinc -fno-builtin
CC= i586-elf-gcc
 
LDFLAGS= -T user.ld -Map kernel.map -nostdinc
LD= i586-elf-ld
 
all: program.elf
 
program.elf:     start.o userlib.o program.o
    $(LD) $(LDFLAGS) $+ -o $@
 
%.o: %.c
     $(CC) -c $(CFLAGS) $< -o $@
 
%.o: %.asm
    $(NASM) $(ASFLAGSOBJ) $< -o $@
 
#    $<  Erste Abhängigkeit
#    $+  Liste aller Abhängigkeiten
#    $@  Name des Targets


ENTRY(_start)
OUTPUT_FORMAT(elf32-i386)
SECTIONS
{
    . = 0x400100;
    .text : { *(.text*)             }
    .data : { *(.data*) *(.rodata*) }
    .bss :  { *(.bss*)              }
}


In ckernel.c sieht es momentan wie folgt aus:

// ckernel.c

#include "os.h"

#include "kheap.h"
#include "task.h"
#include "initrd.h"
#include "syscall.h"

extern ULONG file_data_start;
extern ULONG file_data_end;

extern page_directory_t* current_directory;
extern task_t* current_task;
extern task_t* ready_queue;
extern tss_entry_t tss_entry;
extern heap_t* kheap;

ULONG ramdisk_start;              // at kernel heap

ULONG address_user  = 0x00400000; // begin of user space
UCHAR address_TEST[4096];

int main()
{
    k_clear_screen(); settextcolor(14,0);
    printformat("PrettyOS [Version 0.1.0001]\n(C) 2009 henkessoft.de\n\n");
    gdt_install();
    idt_install();
    isrs_install();
    irq_install();
    initODA();
    timer_install();
    keyboard_install();
    paging_install();
   
    ///TEST: kernel heap given free for users!!!
    kheap = create_heap(KHEAP_START, KHEAP_START+KHEAP_INITIAL_SIZE, KHEAP_MAX, 0, 0); // user and read/write
   
    tasking_install();

    sti();
   
    ramdisk_start = k_malloc(0x100000,0,0);
    k_memcpy((void*)ramdisk_start, &file_data_start, (ULONG)&file_data_end - (ULONG)&file_data_start); //process.asm

    fs_root = install_initrd(ramdisk_start);

    // search the content of files <- data from outside "loaded" via incbin ...
    settextcolor(15,0);
    ULONG i = 0;
    struct dirent* node = 0;
    while( (node = readdir_fs(fs_root, i)) != 0)
    {
        fs_node_t* fsnode = finddir_fs(fs_root, node->name);

        if((fsnode->flags & 0x7) == FS_DIRECTORY)
        {
            printformat("<DIR> %s\n",node->name);
        }
        else
        {
            UCHAR buf[4096];
            ULONG sz = read_fs(fsnode, 0, fsnode->length, buf);
            printformat("%d \t%s\n",sz,node->name);

            ULONG j;
            for(j=0; j<sz; ++j)
            {
                if( k_strcmp(node->name,"Test-Programm")==0 )
                {
                    address_TEST[j] = buf[j];
                }
            }
        }
        ++i;
    }
    printformat("\n\n");

    k_memcpy((void*)address_user, address_TEST, 4096); // Test-Programm ==> user space
    pODA->ts_flag = 1; // enable task_switching
    create_task ((void*)0x400060,3);

    while(TRUE) { /*  */ }
    return 0;
}



Hoffentlich klappt es bei Ihnen nun ebenso:





Allerdings mussten wir bei der Umsetzung des auf die C-API zugreifenden Programms noch einen "faulen Kompromiss" eingehen:

   
    //paging.c

   
    ///TEST: kernel heap given free for users!!!

    // Now allocate those pages we mapped earlier.
    for( i=KHEAP_START; i<KHEAP_START+KHEAP_INITIAL_SIZE; i+=0x1000 )
        alloc_frame( get_page(i, 1, kernel_directory), US, RW); // user and read/write


    //ckernel.c
   
    ///TEST: kernel heap given free for users!!!

    kheap = create_heap(KHEAP_START, KHEAP_START+KHEAP_INITIAL_SIZE, KHEAP_MAX, 0, 0); // user and read/write


Das liegt daran, dass unser C-Programm (getriggert durch den Compiler) im Gegensatz zu den Assembler-Programmen einen Stack einrichtet.
Da wir noch keinen User-Stack anbieten, haben wir hierfür als Übergangslösung den Kernel-Stack freigeben. Dies ist aber keine Lösung auf Dauer!


Daher werden wir unser kleines Programm start.asm zur Einrichtung eines User-Stacks nutzen. Dafür müssen wir aber auch den User-Bereich auf read-write umsetzen. Wir werden ihn auch etwas vergrößern von 0x400000 bis 0x600000.


; start.asm

[BITS 32]
extern _main
global _start

_start:
    mov esp, 0x600000
    call _main
end:
    jmp end


// paging.c

    //Allocate user space
    ULONG user_space_start = 0x400000;
    ULONG user_space_end   = 0x600000;
    i=user_space_start;
    while( i <= user_space_end )
    {
        alloc_frame( get_page(i, 1, kernel_directory), US, RW); // user and read-write
        i += PAGESIZE;
    }

Den bisherigen User-Programmen war es egal, wo sie im Speicher liegen. Diesmal werden wir aber einen String ausgeben, den wir aus der Sektion .data holen müssen. Diese hat eine Adresse, die wir exakt treffen müssen. Die benötigten Daten finden wir in der elf-Datei selbt, wie Sie bereits wissen. Da wir noch keinen elf-Parser besitzen, verwenden wir i586-elf-readelf.exe und suchen uns dort die LOAD-Info:

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000060 0x00400000 0x00400000 0x000bb 0x000bb RWE 0x10

Wir werden den benötigten Teil des elf-Files nach der Formel memcpy (
VirtAddr, source + Offset, FileSize) in unseren User-Space kopieren:
(Dank an Jos Kuijpers für die hilfreiche Diskussion über dieses Thema)

ULONG address_user  = 0x00400000; // begin of user space
UCHAR address_TEST[4096];
//...   
//Test-Programm ==> user space

k_memcpy((void*)address_user, address_TEST + 0x60, 0xbb); // ELF LOAD: Offset VADDR FileSize

 
Damit es mit der Einstiegsadresse 0x400000 auch hundertprozentig klappt, geben wir diese im Linkerskript an:

ENTRY(_start)
OUTPUT_FORMAT(elf32-i386)
SECTIONS
{
    . = 0x400000;
    .text : { *(.text*)             }
    .data : { *(.data*) *(.rodata*) }
    .bss :  { *(.bss*)              }
}


Nun zu unserem Programm:

#include "userlib.h"

int main()
{
  settextcolor(14,1);
  putch('T');
  putch('e');
  putch('s');
  putch('t');
  puts(" Hello User-World!");
  return 0;
}



Wir fügen die Ausgabe eines Strings hinzu. Wir müssen diese Funktion auch in userlib.h/.c beschreiben:


#ifndef USERLIB_H
#define USERLIB_H

void settextcolor(unsigned int foreground, unsigned int background);
void putch(unsigned char val);
void puts(unsigned char* pString);

#endif


#include "userlib.h"

void settextcolor(unsigned int foreground, unsigned int background)
{
    asm volatile( "int $0x7F" : : "a"(2), "b"(foreground), "c"(background) );
}

void putch(unsigned char val)
{
    asm volatile( "int $0x7F" : : "a"(1), "b"(val) );
}

void puts(unsigned char* pString)
{
    asm volatile( "int $0x7F" : : "a"(0), "b"(pString) );
}


Die Frage ist nun, wo sich der String im User-Programm tatsächlich befinden soll. Hierzu hilft ein Blick in kernel.map:

.data           0x004000a8       0x13
 *(.data*)
 .data          0x004000a8        0x0 userlib.o
 .data          0x004000a8        0x0 program.o
 *(.rodata*)
 .rodata.str1.1
                0x004000a8       0x13 program.o

Das Ergebnis lässt sich sehen:



Ein letzter Blick in die mit i586-elf-objdump -D program.elf >objdump.txt (hierfür am besten kleine bat-Datei anlegen) erzeugte Text-Datei:

program.elf:     file format elf32-i386

Disassembly of section .text:

00400000 <_start>:
  400000:    bc 00 00 60 00           mov    $0x600000,%esp
  400005:    e8 3a 00 00 00           call   400044 <_main>

0040000a <end>:
  40000a:    eb fe                    jmp    40000a <end>

0040000c <_settextcolor>:
  40000c:    55                       push   %ebp
  40000d:    89 e5                    mov    %esp,%ebp
  40000f:    53                       push   %ebx
  400010:    b8 02 00 00 00           mov    $0x2,%eax
  400015:    8b 4d 0c                 mov    0xc(%ebp),%ecx
  400018:    8b 5d 08                 mov    0x8(%ebp),%ebx
  40001b:    cd 7f                    int    $0x7f
  40001d:    5b                       pop    %ebx
  40001e:    5d                       pop    %ebp
  40001f:    c3                       ret   

00400020 <_putch>:
  400020:    55                       push   %ebp
  400021:    89 e5                    mov    %esp,%ebp
  400023:    53                       push   %ebx
  400024:    b8 01 00 00 00           mov    $0x1,%eax
  400029:    8a 5d 08                 mov    0x8(%ebp),%bl
  40002c:    cd 7f                    int    $0x7f
  40002e:    5b                       pop    %ebx
  40002f:    5d                       pop    %ebp
  400030:    c3                       ret   

00400031 <_puts>:
  400031:    55                       push   %ebp
  400032:    89 e5                    mov    %esp,%ebp
  400034:    53                       push   %ebx
  400035:    b8 00 00 00 00           mov    $0x0,%eax
  40003a:    8b 5d 08                 mov    0x8(%ebp),%ebx
  40003d:    cd 7f                    int    $0x7f
  40003f:    5b                       pop    %ebx
  400040:    5d                       pop    %ebp
  400041:    c3                       ret   
    ...

00400044 <_main>:
  400044:    8d 4c 24 04              lea    0x4(%esp),%ecx
  400048:    83 e4 f0                 and    $0xfffffff0,%esp
  40004b:    ff 71 fc                 pushl  -0x4(%ecx)
  40004e:    55                       push   %ebp
  40004f:    89 e5                    mov    %esp,%ebp
  400051:    51                       push   %ecx
  400052:    83 ec 0c                 sub    $0xc,%esp
  400055:    6a 01                    push   $0x1
  400057:    6a 0e                    push   $0xe
  400059:    e8 ae ff ff ff           call   40000c <_settextcolor>
  40005e:    c7 04 24 54 00 00 00     movl   $0x54,(%esp)
  400065:    e8 b6 ff ff ff           call   400020 <_putch>
  40006a:    c7 04 24 65 00 00 00     movl   $0x65,(%esp)
  400071:    e8 aa ff ff ff           call   400020 <_putch>
  400076:    c7 04 24 73 00 00 00     movl   $0x73,(%esp)
  40007d:    e8 9e ff ff ff           call   400020 <_putch>
  400082:    c7 04 24 74 00 00 00     movl   $0x74,(%esp)
  400089:    e8 92 ff ff ff           call   400020 <_putch>
  40008e:    c7 04 24 a8 00 40 00     movl   $0x4000a8,(%esp)
  400095:    e8 97 ff ff ff           call   400031 <_puts>
  40009a:    b8 00 00 00 00           mov    $0x0,%eax
  40009f:    8b 4d fc                 mov    -0x4(%ebp),%ecx
  4000a2:    c9                       leave 
  4000a3:    8d 61 fc                 lea    -0x4(%ecx),%esp
  4000a6:    c3                       ret   

Disassembly of section .data:

004000a8 <.data>:
  4000a8:    20 48 65
...     (Das ist der String)         
 





Verbesserter Bootloader - 63 Sektoren-Kernel-Begrenzung überwinden

Es gibt da eine Herausforderung, der wir uns noch stellen müssen. Unser bisheriger bootloader kann nämlich - beschränkt durch 6 Bit - nur einen Kernel mit max. 63 Sektoren, d.h. 63 * 512 = 32256 Byte = 31,5 KByte, in den Speicher laden. Sie können dies leicht nachprüfen, indem Sie hier die nicht erlaubte 64 eingeben:

    mov [bootdrive], dl
load_kernel:
    xor ax, ax         ; mov ax, 0  => function "reset"
    int 0x13        
    jc load_kernel     ; trouble? try again

    mov bx, 0x8000     ; set up start address of kernel

    mov dl,[bootdrive] ; select boot drive
    mov al, 63         ; read n sectors, max. 63!
    mov ch,  0         ; cylinder = 0
    mov cl,  2         ; sector   = 2
    mov dh,  0         ; head     = 0
    mov ah,  2         ; function "read"
    int 0x13        
    jc load_kernel     ; trouble? try again

Da wir mit unserer Kernelgröße nun in diesem Bereich angelangt sind, stoßen Sie bei eigenen Experimenten vielleicht schon an diese Grenze.
Man muss für eine Abhilfe wissen, dass eine typische 1,44 MB Floppy Disk auf einem Track 18 Sektoren (=512 Byte) besitzt, und weiterhin zwei Heads und 80 Cylinder hat. Eine Adressierung eines bestimmten Sektors erfolgt daher über die CHS-Adressierung. Diese läuft wie folgt:

C     H     S

0     0     1
...
0     0    18

0     1     1
...
0     1    18

1     0     1
...
1     0    18

1     1     1
...
1     1    18

2     0     1
...
2     0    18
usw.

Beachten Sie, dass Cylinder und Head bei 0 beginnen, während die Sektoren jeweils bei 1 starten!
Die lineare Adressierung der Sektoren (häufig auch Blocks genannt) mittels LBA beginnt bei 0!
siehe: http://en.wikipedia.org/wiki/CHS_conversion

Wir zählen im Sinne von LBA +1. Sektor 1 ist der Bootloader, ab Sektor 2 beginnt unser Kernel.

Wenn wir bei CHS = 0,0,2 starten ...

    mov al, 63         ; read n sectors, max. 63!
    mov ch,  0         ; cylinder = 0
    mov cl,  2         ; sector   = 2
    mov dh,  0         ; head     = 0

...  und 63 Sektoren als zu lesende "Blocks" (= Sektoren) eingeben, dann berechnet unser BIOS selbst die korrekten C- und H-Werte, nachdem jeweils die 18 Sektoren pro Track enden! Das werden wir nun selbst übernehmen, damit wir hier weiter kommen, denn die 63 kommen von einer Beschränkung bei der Eingabe auf 6 Bit.

Hinzu kommt noch, dass der ISA DMA auf eine "harte" Segment-Grenze bei jeweils 64 KByte stößt. Daher müssen wir die Zieladresse am unteren Ende eines solchen Segmentes wählen.

Im nächsten Ansatz werden wir daher folgende Schritte unternehmen:

- Wir verlegen unseren Kernel probeweise von 0x8000 nach 0x10000 (= 0x01000:0x0000 in Real Mode Segment:Offset-Adressierung)
- Wir laden die Sektoren track-weise
- Wir berechnen zu Beginn eines neuen Tracks unsere CHS-Werte selbst

Damit kann unser Kernel größer als 64 KByte groß werden. Bootloader und Linker-Skript sehen nun experimentell wie folgt aus:

org 0x7C00  ; set up start address of bootloader

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; setup a stack and segment regs ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, ax

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;   read kernel from floppy disk                           ;
;   starting from C(ylinder) H(ead) S(ector): 0 0 2        ;
;   17 + 18 + 18 + 18 + 57 = 128 (hard segment limit)      ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
    mov si,loadmsg     ; show loading message
    call print_string
    call Waitingloop
    mov [bootdrive], dl ; boot drive stored by BIOS in DL.
                        ; we save it to [bootdrive] to play for safety.
load_kernel:
    xor ax, ax         ; mov ax, 0  => function "reset"
    int 0x13        
    jc load_kernel     ; trouble? try again

    mov bx, 0x1000    
    mov es, bx         ; target in memory: segment
   
xor bx, bx         ; target in memory: offset
    mov dl,[bootdrive] ; select boot drive
    mov al, 17         ; read n sectors
    mov ch,  0         ; cylinder = 0
    mov cl,  2         ; sector   = 2
    mov dh,  0         ; head     = 0
    mov ah,  2         ; function "read"
    int 0x13        
    jc load_kernel     ; trouble? try again

load_kernel2:
    xor ax, ax         ; mov ax, 0  => function "reset"
    int 0x13        
    jc load_kernel2    ; trouble? try again

    mov bx, 0x2200     ; target in memory: offset
    mov dl,[bootdrive] ; select boot drive
    mov al, 18         ; read n sectors
    mov ch,  0         ; cylinder
    mov cl,  1         ; sector  
    mov dh,  1         ; head    
    mov ah,  2         ; function "read"
    int 0x13        
    jc load_kernel2    ; trouble? try again

load_kernel3:
    xor ax, ax         ; mov ax, 0  => function "reset"
    int 0x13        
    jc load_kernel3    ; trouble? try again

    mov bx, 0x4600     ; target in memory: offset
    mov dl,[bootdrive] ; select boot drive
    mov al, 18         ; read n sectors
    mov ch,  1         ; cylinder
    mov cl,  1         ; sector  
    mov dh,  0         ; head    
    mov ah,  2         ; function "read"
    int 0x13        
    jc load_kernel3    ; trouble? try again   

load_kernel4:
    xor ax, ax         ; mov ax, 0  => function "reset"
    int 0x13        
    jc load_kernel4    ; trouble? try again

    mov bx, 0x6A00     ; target in memory: offset
    mov dl,[bootdrive] ; select boot drive
    mov al,  18        ; read n sectors
    mov ch,  1         ; cylinder
    mov cl,  1         ; sector  
    mov dh,  1         ; head    
    mov ah,  2         ; function "read"
    int 0x13        
    jc load_kernel4    ; trouble? try again   

load_kernel5:
    xor ax, ax         ; mov ax, 0  => function "reset"
    int 0x13        
    jc load_kernel5    ; trouble? try again

    mov bx, 0x8E00     ; target in memory: offset
    mov dl,[bootdrive] ; select boot drive
    mov al,  57        ; read n sectors (max. 57!!! due to max. 128)
    mov ch,  2         ; cylinder
    mov cl,  1         ; sector  
    mov dh,  0         ; head    
    mov ah,  2         ; function "read"
    int 0x13        
    jc load_kernel5    ; trouble? try again   
   
    call Waitingloop

;;;;;;;;;;;;;
; A20-Gate  ;
;;;;;;;;;;;;;   

    in  al, 0x92      ; switch A20 gate via fast A20 port 92
    cmp al, 0xff      ; if it reads 0xFF, nothing's implemented on this port
    je .no_fast_A20
  
    or  al, 2         ; set A20_Gate_Bit (bit 1)
    and al, ~1        ; clear INIT_NOW bit (don't reset pc...)
    out 0x92, al
    jmp .A20_done
  
.no_fast_A20:         ; no fast shortcut -> use the slow kbc...
    call empty_8042
  
    mov al, 0xD1      ; kbc command: write to output port
    out 0x64, al
    call empty_8042
  
    mov al, 0xDF      ; writing this to kbc output port enables A20
    out 0x60, al
    call empty_8042

.A20_done:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Load GDT, switch to PM, and jump to kernel ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;       
   
    cli               ; clear interrupts
    lgdt [gdtr]       ; load GDT via GDTR (defined in file "gtd.inc")   

    mov eax, cr0      ; switch-over to Protected Mode
    or  eax, 1        ; set bit 0 of CR0 register
    mov cr0, eax      ;   

    jmp dword 08:0x10000   ; dword is important!

;;;;;;;;;
; Calls ;
;;;;;;;;;
 
print_string:
    mov ah, 0x0E      ; VGA BIOS fnct. 0x0E: teletype
.loop:  
    lodsb             ; grab a byte from SI
    test al, al       ; NUL?
    jz .done          ; if the result is zero, get out
    int 0x10          ; otherwise, print out the character!
    jmp .loop
.done:
    ret

empty_8042:
    call Waitingloop
    in al, 0x64
    cmp al, 0xff      ; ... no real kbc at all?
    je .done
  
    test al, 1        ; something in input buffer?
    jz .no_output
    call Waitingloop
    in al, 0x60       ; yes: read buffer
    jmp empty_8042    ; and try again
  
.no_output:
    test al, 2        ; command buffer empty?
    jnz empty_8042    ; no: we can't send anything new till it's empty
.done:
ret

Waitingloop:                  
    mov cx,0xFFFF
.loop_start:
    dec cx    
    jnz .loop_start
    ret      
    
;;;;;;;;;;;;
; Includes ;
;;;;;;;;;;;;

%include "gdt.inc"
   
;;;;;;;;
; data ;
;;;;;;;;

    bootdrive db 0   
    loadmsg db "bootloader message: loading kernel ...",13,10,0
   
    times 510-($-$$) hlt
    db 0x55
    db 0xAA

// Linker-Skript kernel.ld

OUTPUT_FORMAT("binary")
ENTRY(KernelStart)
SECTIONS
{
  . = 0x00010000;
  .text   : { *(.text)    }
  .data   : { *(.data)    }    
  .rodata : { *(.rodata)  }
  .bss    : { *(.bss)     } 
}

Mit diesem Umbau unseres Bootloaders haben wir nun Freiheit für unseren Kernel gewonnen. Bei 0x20000 müssen wir nun lediglich ES in ES:BX (Puffer-Adresse) von 0x1000 auf 0x2000 umsetzen. Dann kann man das Spiel erneut beginnen. Das zweite Segment beginnt mit LBA = 129 --> CHS = 3,1,4.

Sektoren:   1 | 2 ... 129 | 130 ... 257    
----------------------------------------   
LBA:        0 | 1 ... 128 | 129 ... 256


1 = Bootloader  2 ... 129 erstes Segment  130 ... 257 zweites Segment

; new segment
load_kernel10:
    xor ax, ax         ; mov ax, 0  => function "reset"
    int 0x13        
    jc load_kernel10   ; trouble? try again

    mov bx, 0x2000     ; next segment
    mov es, bx         ; target in memory: segment
    xor bx, bx         ; target in memory: offset
    mov dl,[bootdrive] ; select boot drive
    mov al, 63         ; read n sectors
    mov ch,  3         ; cylinder
    mov cl,  4         ; sector  
    mov dh,  1         ; head    
    mov ah,  2         ; function "read"
    int 0x13        
    jc load_kernel10   ; trouble? try again   


Wir haben hier z.B. vereinfacht die möglichen 63 Sektoren mit Auto-CHS-Berechnung durch das BIOS angewendet.
Einen einfachen LBA <-> CHS - Umrechner zur Kontrolle findet man hier: http://homepage2.nifty.com/cars/misc/chs2lba.html

Irgendwann gehen uns aber die 512 Byte des Bootsektors aus. Außerdem möchten wir an unserem Kernel ab 0x8000 festhalten, solange es geht.
Daher versuchen wir, einen Algorithmus anzuwenden, der sektorenweise in einer Schleife liest und schreibt.

Hier ist eine Möglichkeit (vielen Dank an PorkChicken und Cjreek aus dem LowLevel-Forum für die Unterstützung bei diesem Thema) für unseren Bootloader:

org 0x7C00  ; set up start address of bootloader

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; setup a stack and segment regs ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ax, 0x7000   ; stack address
    mov ss, ax      
    xor sp, sp       ; set stackpointer to 0
   
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;   read kernel from floppy disk                           ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

read_kernel:
    mov si,loadmsg    
    call print_string
    mov [bootdrive], dl ; boot drive stored by BIOS in DL.

    ; di: number of sectors, bx: segment, ch, dh, cl: cylinder, head, sector
    mov bx, 500
    mov di, bx
    mov bx, 0x0800 ; kernel start address (cf. linker script)
    mov ch, 0
    mov dh, 0
    mov cl, 2
    call read_sectors
                       
;;;;;;;;;;;;;
; A20-Gate  ;
;;;;;;;;;;;;;   

switch_a20:
    in  al, 0x92      ; switch A20 gate via fast A20 port 92
    cmp al, 0xff      ; if it reads 0xFF, nothing's implemented on this port
    je .no_fast_A20
  
    or  al, 2         ; set A20_Gate_Bit (bit 1)
    and al, ~1        ; clear INIT_NOW bit (don't reset pc...)
    out 0x92, al
    jmp .A20_done
  
.no_fast_A20:         ; no fast shortcut -> use the slow kbc...
    call empty_8042
  
    mov al, 0xD1      ; kbc command: write to output port
    out 0x64, al
    call empty_8042
  
    mov al, 0xDF      ; writing this to kbc output port enables A20
    out 0x60, al
    call empty_8042

.A20_done:

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Load GDT, switch to PM, and jump to kernel ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
       
load_gdt:   
    cli               ; clear interrupts
    lgdt [gdtr]       ; load GDT via GDTR (defined in file "gtd.inc")   

switch_to_PM:   
    mov eax, cr0      ; switch-over to Protected Mode
    or  eax, 1        ; set bit 0 of CR0 register
    mov cr0, eax      ;   

jmp_to_PM:
    jmp dword 08:0x00008000    ; this is a 16-bit-FAR-Jmp, but CS is loaded with "index" 8 in GDT
                      ; hence, the code will be interpreted as 32 bit from here on
                      ; the address can be found in the linker script
;;;;;;;;;
; Calls ;
;;;;;;;;;
 
print_string:
    mov ah, 0x0E      ; VGA BIOS fnct. 0x0E: teletype
.loop:  
    lodsb             ; grab a byte from SI
    test al, al       ; NUL?
    jz .done          ; if the result is zero, get out
    int 0x10          ; otherwise, print out the character!
    jmp .loop
.done:
    ret

empty_8042:
    call Waitingloop
    in al, 0x64
    cmp al, 0xff      ; ... no real kbc at all?
    je .done
  
    test al, 1        ; something in input buffer?
    jz .no_output
    call Waitingloop
    in al, 0x60       ; yes: read buffer
    jmp empty_8042    ; and try again
  
.no_output:
    test al, 2        ; command buffer empty?
    jnz empty_8042    ; no: we can't send anything new till it's empty
.done:
    ret

Waitingloop:                  
    mov cx,0xFFFF
.loop_start:
    dec cx    
    jnz .loop_start
    ret      

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;    read sectors from floppy disk to buffer ES:BX in memory    ;
;                                                               ;
;    input:                                                     ;
;    di         - number of sectors                             ;
;    bx         - segment                                       ;
;    ch, dh, cl - cylinder, head, sector                        ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

read_sectors:

.reset_floppy:
    xor ax, ax        ; mov ax, 0  => function "reset"
    int 0x13          
    jc .reset_floppy  ; trouble? try again

.next:

    mov si, progressmsg   ; show progress
    call print_string   
    mov es, bx

    xor bx, bx

.again:
    mov dl, [bootdrive]
    mov ax, 0x0201
    int 0x13
    jc .again

    inc cl
    cmp cl, 19
    jl .loop
    mov cl, 1

    inc dh
    cmp dh, 2
    jl .loop
    mov dh, 0

    inc ch
    cmp ch, 80
    jae .error

.loop:
    mov bx, es
    add bx, 512/16
    sub di, 1
    jnz .next

.done:
    push dx
    mov dx,0x3F2       ; switch off floppy disk motor
    mov al,0x0C
    out dx,al
    pop dx
   
ret   

.error:
    mov si, errormsg   ; show error message
    call print_string
.end:
    jmp .end   
    
;;;;;;;;;;;;
; Includes ;
;;;;;;;;;;;;

%include "gdt.inc"
   
;;;;;;;;
; data ;
;;;;;;;;

    bootdrive db 0   
    loadmsg db "bootloader message: loading kernel ...",13,10,0
    errormsg db "bootloader message: sector read error ...",13,10,0
    progressmsg db "*",0
   
    times 510-($-$$) hlt
    db 0x55
    db 0xAA


Den Stack mussten wir allerdings von 0x10000 nach 0x7000 verlegen, da er dem wachsenden Kernel nun im Weg war.


Informationen aus dem ELF-Format heraus lesen

Bisher haben wir die Startdaten unseres User-Programms in Form von Konstanten verwendet. Diese Informationen erhalten wir aber im elf-Format. Wir besorgen uns nun zumindest die für den Programm-Start notwendigsten Daten aus der Datei selbst. Dadurch, dass wir unsere elf-Datei momentan in einem Array liegen haben, können wir mit der Pointer-Technik gezielt auf die gewünschten Informationen zugreifen:

    /// peeking data from elf exec format ///
    ULONG elf_vaddr    = *((ULONG*)(address_TEST+0x3C));
    ULONG elf_offset   = *((ULONG*)(address_TEST+0x38));
    ULONG elf_filesize = *((ULONG*)(address_TEST+0x44));
    printformat("Virt. Addr.: %x Offset: %x Filesize: %x", elf_vaddr, elf_offset, elf_filesize);
   
    ...
    //Test-Programm ==> user space
    address_user = elf_vaddr;
    k_memcpy((void*)address_user, address_TEST+elf_offset, elf_filesize); // ELF LOAD: Offset VADDR FileSize

    ...
    create_task ((void*)getAddressUserProgram(),3); //user space beginning



Wir schaffen uns darüber hinaus in einer definierten Page eine "shared library" shared_pages, die Kernel und User beide benutzen  können. Dort legen wir z.B. eine Funktion getAddressUserProgram()an, die den Inhalt von address_user zurück liefert:

// Linker-Skript kernel.ld

OUTPUT_FORMAT("binary")

ENTRY(KernelStart)
SECTIONS
{
  . = 0x00010000;
  .text   : { *(EXCLUDE_FILE(shared_pages.o).text) }
  . = 0x00017000;
  .text1  : { shared_pages.o(.text)                }
  . = 0x00018000;
  .data   : { *(.data)                             }    
  .rodata : { *(.rodata)                           }
  .bss    : { *(.bss)                              } 
}

// shared_pages.h

#ifndef SHARED_PAGES_H
#define SHARED_PAGES_H

#include "os.h"

extern ULONG address_user;
ULONG getAddressUserProgram();

#endif

// shared_pages.c

#include "shared_pages.h"

ULONG getAddressUserProgram()
{
    return address_user;
}

// kernel.map
                0x00017000                . = 0x17000

.text1          0x00017000        0xa
 shared_pages.o(.text)
 .text          0x00017000        0xa shared_pages.o
                0x00017000                _getAddressUserProgram
                0x00018000                . = 0x18000

i=0;

while( i < placement_address + 0x2000 ) //important to add more!
{
    if( ((i>=0xb8000) && (i<=0xbf000)) || ((i>=0x17000) && (i<0x18000)) )
    {
        alloc_frame( get_page(i, 1, kernel_directory), US, RW); // user and read-write
    }
    else
    {
        alloc_frame( get_page(i, 1, kernel_directory), SV, RO); // supervisor and read-only
    }
    i += PAGESIZE;
}


Wir haben im Linkerskript hier im Download den Kernel "künstlich" aufgepumpt (shared_pages: 0x17000 bis 0x40000), damit Sie das Nachladen eines großen Kernels (500 Sektoren werden geladen) testen können. Verwenden Sie zum Schreiben des Disk-Images ein Tool wie z.B. Rawwritewin.




Probleme mit VFS / RAM Disk

Es gibt verschiedene Probleme:

1) Bochs 2.4.1 ist gegenüber Bochs 2.3.7 verbessert worden und zeigt beim Simulieren nun Fehler an, die vorher nicht entdeckt wurden.
    Der beste Test ist das Booten auf einem oder mehreren realen PC.

2) Da wir bisher aus didaktischen Gründen einen eigenen Bootloader anstelle von GRUB verwenden, sind diesbezügliche Problemen nicht völlig auszuschließen.

3) Der Code von James Molloy bezüglich VFS/RAM Disk ist leider nicht ausreichend robust und führt zur Instabilität der aus dem Filesystem "geladenen" Daten.

Nachstehend finden Sie eine Version, die bisher problemlos auf verschiedensten "Real PC" (ab 386er) lief:

//...
extern ULONG file_data_start;
extern ULONG file_data_end;
ULONG address_user;
UCHAR address_TEST[4096];
UCHAR buf[4096];
UCHAR flag1 = 0; // status of user-space-program
extern ULONG placement_address;
heap_t* kheap = 0;

static void init()
{
    kheap = 0;
    page_directory_t* kernel_directory  = 0;
    page_directory_t* current_directory = 0;
    placement_address = 0x200000;
}

int main()
{
    init();
    //...

    kheap = create_heap(KHEAP_START, KHEAP_START+KHEAP_INITIAL_SIZE, KHEAP_MAX, 1, 0); // SV and RW
   
    settextcolor(15,0);
    ULONG ramdisk_start = k_malloc(0x30000, 0, 0); // Ram Disk at the kernel heap
    //fs_root = install_initrd(ramdisk_start); // implement Ram Disk into VFS /// ASSERT at kheap
    tasking_install(); // ends with sti()

    // program provided by data.asm
    k_memcpy((void*)address_TEST, &file_data_start, (ULONG)&file_data_end - (ULONG)&file_data_start);

    /// peeking data from elf exec format ///
    ULONG elf_vaddr     = *( (ULONG*)( address_TEST + 0x3C ) );
    ULONG elf_offset    = *( (ULONG*)( address_TEST + 0x38 ) );
    ULONG elf_filesize  = *( (ULONG*)( address_TEST + 0x44 ) );

    if( (elf_vaddr == 0x400000) || (elf_offset<0x130) || (elf_filesize<0x1000) )
    {
        flag1=1;
    }

    // address_TEST (ring 0) ==> address_user (ring 3)
    if(flag1==1)
    {
        address_user = elf_vaddr;
        k_memcpy((void*)address_user, address_TEST + elf_offset, elf_filesize); // ELF LOAD: Offset VADDR FileSize
    }

    pODA->ts_flag = 1; // enable task_switching

    if(flag1==1)
    {
        create_task ((void*)getAddressUserProgram(),3); // program in user space (ring 3) takes over
    }
    else
    {
        settextcolor(4,0);
        printformat("Program not found.\n");
        settextcolor(15,0);
    }

    while(TRUE){/*  */}
    return 0;
}





Dieser Code verwendet den Code bezüglich VFS und RAM Disk (von James Molloy) - wegen noch unerkannter Probleme - nicht zum Starten des ersten Programms in der User-Ebene (Ring 3).





Physischen Speicher analysieren


Eine der Aufgaben eines Betriebssystems besteht darin, sich einen Überblcik über das "Physical Memory" - also den physischen Speicher - zu verschaffen. Mit der seit spätestens 2002 zur Verfügung stehenden BIOS-Routine  int 15h  eax = e820h  klappt das hervorragend. Man muss nur die anfallenden Daten entsprechend speichern und im C-Kernel auswerten.

Hier finden Sie ein erstes einfaches Beispiel auf Basis dieses Assemblercodes aus dem OSDEV.org wiki als Anregung:

in boot.asm

...

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; Determine physical memory                  ;
; INT 0x15, eax = 0xE820                     ;
;                                            ;
; input: es:di -> destination buffer         ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

xor ax, ax
mov es, ax
mov ax, 0x8000
mov di, ax
call get_memory_by_int15_e820

...

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;    INT 0x15, eax = 0xE820 BIOS function to get a memory map    ;
;                                                                ;
;    input:                                                      ;
;    es:di -> destination buffer for 24 byte entries             ;
;    outputs: bp = entry count, trashes all registers except esi ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
   
get_memory_by_int15_e820:
    xor ebx, ebx                      ; ebx must be 0 to start
    xor bp, bp                        ; keep an entry count in bp
    mov edx, 0x0534D4150              ; Place "SMAP" into edx
    mov eax, 0xe820
    mov [es:di + 20], dword 1         ; force a valid ACPI 3.X entry
    mov ecx, 24                       ; ask for 24 bytes
    int 0x15
    jc short .failed                  ; carry set on first call means "unsupported function"
    mov edx, 0x0534D4150              ; Some BIOSes apparently trash this register?
    cmp eax, edx                      ; on success, eax must have been reset to "SMAP"
    jne short .failed
    test ebx, ebx                     ; ebx = 0 implies list is only 1 entry long (worthless)
    je short .failed
    jmp short .jmpin
.e820lp:
    mov eax, 0xe820                   ; eax, ecx get trashed on every int 0x15 call
    mov [es:di + 20], dword 1         ; force a valid ACPI 3.X entry
    mov ecx, 24                       ; ask for 24 bytes again
    int 0x15
    jc short .e820f                   ; carry set means "end of list already reached"
    mov edx, 0x0534D4150              ; repair potentially trashed register
.jmpin:
    jcxz .skipent                     ; skip any 0 length entries
    cmp cl, 20                        ; got a 24 byte ACPI 3.X response?
    jbe short .notext
    test byte [es:di + 20], 1         ; if so: is the "ignore this data" bit clear?
    je short .skipent
.notext:
    mov ecx, [es:di + 8]              ; get lower dword of memory region length
    test ecx, ecx                     ; is the qword == 0?
    jne short .goodent
    mov ecx, [es:di + 12]             ; get upper dword of memory region length
    jecxz .skipent                    ; if length qword is 0, skip entry
.goodent:
    inc bp                            ; got a good entry: ++count, move to next storage spot
    add di, 24
.skipent:
    test ebx, ebx                     ; if ebx resets to 0, list is complete
    jne short .e820lp
.e820f:
    mov [mmap_ent], bp                ; store the entry count
    ret                               ; test opcode cleared carry flag
.failed:
    stc                               ; "function unsupported" error exit
    ret

...

    times 508-($-$$) hlt
    mmap_ent dw 0
    db 0x55
    db 0xAA

    in ckernel.c
   
    ...

    // physical memory

    USHORT num_of_entries = *( (USHORT*)(0x7DFC) );
    printformat("# entries: %d\n\n",(num_of_entries) );

    printformat("base                length              type      extended\n");
    ULONG i,j;
    for(i=0; i<num_of_entries; ++i)
    {
        printformat("\n");
        for(j=0; j<24; j+=4)
        {
            printformat("%x ",/*0x8000+i*24+j,*/ *( (ULONG*)(0x8000+i*24+j) ) );
        }
    }


Da wir unseren Kernel ab 0x10000 ablegen, haben wir zur Zeit ausreichend Platz für solche Informationen im Bereich ab 0x8000. Dort legen wir im Bootloader die mit ES:DI adressierten Listen von jeweils 24 Byte (qword, qword, dword, dword) sicher ab, um später aus dem C-Kernel darauf zugreifen zu können. Damit wir auch die Zahl der Einträge "mmap_ent" finden, greifen wir zu einem Trick: Wir legen dieses word (USHORT) genau zwei Byte vor der Boot-Signatur ab. Die times-Anweisung müssen wir entsprechend von 510 auf 508 kürzen, damit die Boot-Signatur nicht verschoben wird. Damit finden wir diese Zählvariable exakt bei 0x7C00 + 508 = 0x7DFC.

Schauen Sie sich MyOS.bin nun mit dem Hex-Editor an: f4 f4 f4 00 00 55 aa (die letzten 7 Byte des Bootloaders)
An der Stelle der beiden Bytes mit Wert null werden wir nach dem Booten und erfolgreichen Ausführen von int 15h eax = 820h die Zahl der Einträge finden.

Beachten Sie, dass bei base oder length die beiden QWORD (zwei ULONG) in der üblichen Reihenfolge - zuerst low und nachfolgend high value - auftreten.
Hier finden Sie ein Screenshot von einem Rechner mit 512 KB RAM als Beispiel:



Die Zahl der Einträge mmap_ent ist in diesem Fall 5 (oft findet man auch 6 Einträge):

qword "base" (only low dword)
qword "length" (only low dword) dword "type"
dword "extended ..."
0x00000000 
0x000A0000  
0x1 (Usable RAM)
0x1 (bit0: ignore if clear)
0x000F0000 
0x00010000 
0x2 (Reserved - unusable ) 0x1 (bit0: ignore if clear)
0x00100000
0x1FEF0000
0x1 (Usable RAM) 0x1 (bit0: ignore if clear)
0x1FFF0000
0x00003000
0x4 (ACPI NVS memory) 0x1 (bit0: ignore if clear)
0x1FFF3000
0x0000D000
0x3 (ACPI reclaimable memory) 0x1 (bit0: ignore if clear)

Nun zurück zur Frage der Bestimmung des "Physical Memory" des untersuchten Rechners.
Man kann nun die "length" der Speicherbereiche addieren, die "type" gleich 1 (Usable RAM) haben.

Region "type":
-  1: Usable (normal) RAM
-  2: Reserved - unusable
-  3: ACPI reclaimable memory
-  4: ACPI NVS memory
-  5: Area containing bad memory

Das sind in diesem konkreten Fall (RAM 512 MB wird beim Booten angezeigt):
0xA0000 + 0x1FEF0000 = 0x1FF90000 = 536412160 Byte = 523840 KB = 511,5625 MB
Also ein akzeptables Ergebnis.

Die berühmten 0xA0000 sind genau die bekannten 640 KB "low memory", die man aus Zeiten von MS DOS noch gut kennt.
"640 KB ought to be enough for anybody." - Dieser berühmte Satz wurde Bill Gates "angedichtet".





Shell

Wir haben nun einen eigenen Bootloader, einen Kernel, der bereits einige wichtige Funktionen erfüllt, so dass Programme im User-Space auf Ring 3 geordnet ausgeführt werden können. Nun benötigen wir eine "Shell", die uns eine reibungslose Kommunikation mit unserem PrettyOS erlaubt.

Dazu muss man Instruktionen eingeben können, die mit dem in der Shell vorhandenen Befehlsvorrat verglichen werden und im Erfolgsfall entsprechende Aktionen auslösen. Das ist mit den vorhandenen Funktionen, die wir über syscall erreichen, kein Problem. Wir werden die Funktion  strcmp  nicht über syscall realisieren, sondern direkt in der user-lib realisieren, denn das spart Zeit. Syscalls sind bedingt durch den notwendigen Interrupt- und Ringwechsel-Mechanismus "teuer".

Wir bewegen uns momentan nur auf der User-Ebene. Der Kernel bleibt unangetastet. So sieht unser erstes Programm für eine Shell aus:

// userlib.h

#ifndef USERLIB_H
#define USERLIB_H

#define TRUE        1
#define FALSE       0

// syscalls
void settextcolor(unsigned int foreground, unsigned int background);
void putch(char val);
void puts(char* pString);
char getch();


// user functions
int strcmp( const char* s1, const char* s2 );

#endif


// userlib.c

// Compare two strings. Returns -1 if str1 < str2, 0 if they are equal or 1 otherwise.

int strcmp( const char* s1, const char* s2 )
{
    while ( ( *s1 ) && ( *s1 == *s2 ) )
    {
        ++s1; ++s2;
    }
    return ( *s1 - *s2 );
}


// shell.c

#include "userlib.h"

int main()
{
    while(TRUE)
    {
      settextcolor(15,0);
      puts("$> ");

      char entry[80];
      int i = 0;

      while(TRUE)
      {
        char input = getch();

        if(
             (input==32)                ||  // space key
             (input==45)                ||  // "-"   key
             (input==63)                ||  // "?"   key
            ((input>=65)&&(input<=90))  ||  // capital letters
            ((input>=97)&&(input<=122)) ||  // little letters
            ((input>=48)&&(input<=57))      // 0 ... 9
          )
        {
          if(i<70)
          {
            putch(input);
            entry[i]=input;
            ++i;
          }
        }
        if( input==8 )                      // Backspace
        {
            if(i>0)
            {
              putch('\b');
              entry[i-1]='\0';
              --i;
            }
        }

        if( input==10 )                     // Line Feed (ENTER-Key)
        {
            puts(" <--");
            putch('\n');
            entry[i]='\0';
            ++i;
            break;
        }
      }

      if( ( strcmp(entry,"help") == 0 ) || ( strcmp(entry,"?") == 0 ) )
      {
          settextcolor(2,0);
          puts("Implemented Instructions: help ? hi\n");
          settextcolor(15,0);
      }

      else
      {
          if( strcmp(entry,"hi") == 0)
          {
              settextcolor(2,0);
              puts("I am PrettyOS. Always at your service!\n");
              settextcolor(15,0);
          }
          else
          {
              settextcolor(2,0);
              puts("Sorry, I do not know this command.\n");
              settextcolor(15,0);
          }
      }
  }
  return 0;
}


Wir transferieren eingegebene Befehle in das char-Array entry[...]. Die Eingabe beschränken wir zunächst auf max. 70 Zeichen, damit es zu keinem Zeilenüberlauf kommt. Neben Groß- und Kleinbuchstaben und Ziffern lassen wir lediglich die Tasten BACKSPACE (ASCII 8 dec), ENTER (ASCII 10 dec), Fragezeichen und Bindestrich bei der Befehlseingabe zu. Alles andere blenden wir aus. Das ist eine Frage des Design und Workflow.

Nach der Bestätigung mit ENTER wird die Instruktion via strcmp mit dem zulässigen Instruktionsvorrat verglichen. Noch starten keine entsprechenden Programme, aber das kommt sicher noch. Der Phantasie sind hier wenige Grenzen gesetzt. Die "shell" ist das Bindeglied zwischen Benutzer und Kernel. Das "Gesicht" eines Betriebssystems wird durch nichts mehr geprägt als seine "Konsole" oder seine grafische Oberfläche, wenn man einmal von Fehlermeldungen (denken Sie an den schauerlichen "Bluescreen" von MS Windows) absieht.



Versuchen Sie z.B. die Reaktionen der Kommando-Konsole ("DOS-Box", cmd.exe) von MS Windows nachzubilden. Beachten Sie hierbei alle Details, vor allem die Reaktionen auf die Benutzung von Tasten wie Pfeil-hoch/runter/rechts/links, Bild hoch/runter, Pos1, Ende, Einfg, Entf ... und bilden sie deren Funktion nach. Wann erfolgt die Ausgabe von "Die eingegebene Zeile ist zu lang."?

Hier benötigen Sie zunächst noch keinen Download, sondern Phantasie. Prägen Sie Ihre eigene "API", ihre eigene "Shell".
Aufgrund von Schwächen (Probleme mit char/unsigned char) in den Vorversionen verwenden Sie bitte diese Version 88 als Basis:



Anmerkung: In Version 0.1.0091 (seit Version 0.1.0088) sind einige kleinere Schwächen im Kernel behoben, so dass nun das vertrauenerweckende  -Werror -Wall fehlerfrei funktioniert.

VFS/Initrd wird in 0.1.0091 zum Laden der Shell umgangen, bis der Schwachpunkt (Dateiinhalte werden zerstört/verfälscht) eindeutig festgestellt und eliminiert oder abgefangen ist. Hier ist ein zur Zeit stabiles experimentelles Stadium:
Version 0.1.0092
Wenn Sie die Ursache finden, warum es auf wenigen PCs zu Zeichen hinter den Filenamen in der initrd kommt, die die '\0' überschreiben und damit einen buffer overfolw ausgelöst haben (jetzt mit k_strncpy und Anhängen von '\0' anstelle k_strcpy auf max. 64 Zeichen beschränkt) bitte ich um entsprechende Information.



FAT12 - Filesystem der Diskette

Bisher haben wir unser PrettyOS auf der Diskette für "Explorer" unsichtbar gehalten. "Geschmiedet" vom Linker bildeten die Übersetzungen von boot.asm kernel.asm und dem eigentlichen Kernel ein "unsichtbares" MyOS.bin, das auf der Diskette im "raw format" residierte. Nun werden wir einen Schritt ins "Licht" gehen und unsere Daten in Form "sichtbarer" Files (Dateien) präsentieren. Da wir bereits mit realen oder simulierten Floppy Disks und Disketten arbeiten, bietet es sich an, dieses "Format" für die Organisation und Darstellung von Dateien zu verwenden.

Der zentrale Begriff ist die sogenannte FAT = File Allocation Table. Diese "Dateizuordnungstabelle" wurde von Microsoft in seinen Betriebssystemen genutzt, um Dateien zu verwalten. Jeder Cluster auf dem Datenträger hat einen Eintrag in der FAT. Eine Datei - also ein File - wird als eine einfach verlinkte Liste von Clustern (Clusterkette) aufgezeichnet. Diese Cluster müssen physisch nicht hintereinander liegen. Damit ergibt sich die Problematik der "Fragmentierung" der Cluster einer Datei. Es gibt nun verschiedene Versionen der FAT, die jeweils mit der Anzahl der Bitlänge der Nummer für die Cluster bezeichnet wird. Ein Cluster kann aus einem oder mehreren Sektoren bestehen.

Die Zwölf in FAT12 bedeutet, dass dieses Dateiverwaltungssystem 212 = 4096 Cluster verwalten kann. Geht man von der Sektorgröße von 512 Byte aus und nimmt diese auch als Clustergröße - also ein Cluster besteht aus einem Sektor, was sich bei einer Diskette anbietet - dann kann man mit dieser Organisation maximal 4096 * 512 = 2097152 Byte = 2048 KB = 2 MB auf einer Diskette verwalten. Will man größere Datenmengen speichern, so muss man entsprechend größere Cluster, die mehrere Sektoren beinhalten, einsetzen.

Dateinamen sind in FAT12 auf das altmodische und beengte Format 8.3 beschränkt, das nur acht Zeichen für den Namen und drei Zeichen für die Erweiterung (Extension) erlaubt. Schon in den Zeiten von MS-DOS war dies eine bedrückende Situation.

Die FAT wird aus Gründen der Datensicherheit in zweifacher Ausfertigung vorgehalten. In der FAT werden folgende Informationen über einen Cluster festgehalten:

BIOS Parameter Block (BPB)

Will man FAT12 im Bootloader einsetzen, dann muss man im First Stage Bootloader in Sektor 1 der Diskette folgende Struktur abbilden:

Offset (hex)  Byte 
Bedeutung und Inhalt
00 3 Sprunganweisung zum Entry Point des eigentlichenen Boot-Codes: jmp entry_point  Damit überspring man die Daten dieses Blocks.
Verwendet man einen  JMP SHORT , so muss man noch mit einem NOP das dritte Byte auffüllen!
03 8 OEM-Name (mit Leerzeichen auf 8 Byte aufgefüllt). MS-DOS ermittelte damit die Struktur der anderen Daten ("IBM  3.3" und "MSDOS5.0"). Der Name ist für ein eigenes OS frei definierbar.
0B 2 Bytes je Sektor (Mögliche Werte: 512, 1024, 2048 und 4096. Typisch bei Disketten: 512)
0D 1 Sektoren pro Cluster (Mögliche Werte: 1, 2, 4, 8, 16, 32, 64. Typisch bei Disketten: 1)
0E 2 Anzahl reservierter Sektoren (inkl. Bootsektor, also mindestens 1)
10 1 Anzahl der FATs (in der Regel: 2)
11 2 Maximale Anzahl an Verzeichniseinträgen im Stammverzeichnis (Typisch bei Disketten: 224)
13 2 Maximale Sektoren des Mediums (max. 65535; Diskette: 2880 = 80 Cylinders/Tracks  *  2 Heads  * 18 Sectors)
15 1 Media Descriptor Byte. Diskette: 0xF0
16 2 Anzahl Sektoren pro FAT. (Diskette: 9)
18 2 Sektoren pro Track (Diskette: 18)
1A 2 Anzahl der Seiten bzw. Schreib-Lese-Köpfe (Diskette: 2)
1C 4 Anzahl der „versteckten“ Sektoren vor dem Bootsektor
Festplatte: Anzahl der Sektoren zwischen Master Boot Record und dem Bootsektor der Partition
Diskette (nicht partitioniert, also ohne MBR): 0
20 4 Gesamtsektoranzahl, falls größer als 65535 (Diskette: 0)
      24     
    1     Physische BIOS-Laufwerksnummer (Diskette: 00hex Hard Disk: 80hex, 81hex, …).                                                           
25 1  Reserviert (ungenutzt)
26 1  Erweiterte Bootsignatur (Diskette: 0x29)
27 4  Dateisystem-ID (Seriennummer). Beim Anlegen des Dateisystems erzeugt. Wichtig zur Unterscheidung verschiedener Wechselmedien
2B 11  Name des Dateisystems (durch Leerzeichen aufgefüllt).
36 8  FAT-Variante, mit Leerzeichen aufgefüllt. Diskette: "FAT12   " (8 Zeichen)
3E 448  x86-Maschinencode des Bootloaders
1FE 2  BIOS-Bootsignatur. Enthält die beiden Werte 55hex AAhex

Wie sieht dies nun konkret für unserem Bootloader aus?

Zunächst muss man den Bootloader teilen, denn all diese Routinen und Datentabellen passen leider nicht in 512 Byte. Die klassische Lösung ist wie folgt:
Man unterteilt den Boot-Vorgang in den eigentlichen Bootsektor (First Stage Bootloader), der einen Second Stage Bootloader vom Datenträger mittels FAT12 nachlädt. Wir orientieren uns bezüglich des Aufbaus und der Hintergründe an dem ausführlichen Tutorial von "Mike" auf brokenthorn. Wir rüsten unseren bisherigen Code mit dem dort dargestellten Vorgehen bezüglich FAT12 nach, damit wir einen First und Second Stage Bootloader mit FAT12-Zugriff erhalten. Die ersten 512 Byte sind nach wie vor im Explorer unsichtbar. Das ist verständlich, denn hier sind wichtige Verwaltungsdaten für das Dateisystem der Diskette versteckt. Darstellen und Löschen ist unerwünscht!

First Stage Bootloader

boot.asm ==> unsichtbar in Sektor 1

; boot.asm
[Bits 16]
org 0x7C00                             ; start address of bootloader
jmp entry_point                        ; jump to bootloader entry point

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Memory Management:
; org                  7C00
; data/extra segments     0
; stack segment        7C00
; root dir -> memory   7E00
; boot2    -> memory    500
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

%include "Fat12_BPB.inc"

;******************************************************************************
;    bootloader entry point
;******************************************************************************
entry_point:
    xor     ax, ax                           ; set registers
    mov     ds, ax
    mov     es, ax
    mov     fs, ax
    mov     gs, ax
   
    mov     ax, 0x7C00                       ; set the stack
    mov     ss, ax
    xor     sp, sp
         
    mov  [bootdevice], dl                    ; store boot device
    mov si, msgLoading
    call print_string

Load_Root_Directory_Table:
    ; compute size of root directory and store in "cx"
    xor cx, cx
    xor dx, dx
    mov ax, 0x20                             ; 32 byte directory entry
    mul WORD [RootEntries]                   ; total size of directory
    div WORD [BytesPerSec]                   ; sectors used by directory
    xchg ax, cx
       
    ; compute location of root directory and store in "ax"
    mov al, BYTE [NumFATs]                    ; number of FATs
    mul WORD [FATSize]                        ; sectors used by FATs
    add ax, WORD [ReservedSec]                ; adjust for bootsector
    mov WORD [datasector], ax                 ; base of root directory
    add WORD [datasector], cx
         
    ; read root directory into memory (7E00h)
    mov bx, 0x7E00                            ; copy root dir above bootcode
    call ReadSectors

;******************************************************************************
;   Find stage2 bootloader
;******************************************************************************
    ; browse root directory for binary image
    mov cx, WORD [RootEntries]                ; load loop counter
    mov di, 0x7E00                            ; locate first root entry
.LOOP:
    push cx
    mov cx, 0xB                               ; name has 11 characters
    mov si, ImageName                         ; look for this image name
    push di
    rep cmpsb                                 ; test for entry match
    pop di
    je Load_FAT
    pop cx
    add di, 0x20                              ; queue next directory entry
    loop .LOOP
    jmp FAILURE

;******************************************************************************
;   Load File Allocation Table (FAT)
;******************************************************************************
Load_FAT:
    ; save starting cluster of boot image
    mov dx, WORD [di + 0x001A]
    mov WORD [cluster], dx                  ; file's first cluster
         
    ; compute size of FAT and store in "cx"
    xor ax, ax
    mov al, BYTE [NumFATs]                  ; number of FATs
    mul WORD [FATSize]                      ; sectors used by FATs
    mov cx, ax

    ; compute location of FAT and store in "ax"
    mov ax, WORD [ReservedSec]              ; adjust for bootsector
         
    ; read FAT into memory (7E00h)
    mov bx, 0x7E00                          ; copy FAT above bootcode
    call ReadSectors

    ; read image file into memory (0500h)
    xor ax, ax
    mov es, ax                              ; destination for image
    mov bx, 0x0500                          ; destination for image
    push bx

;******************************************************************************
;   Load stage2 bootloader
;******************************************************************************
Load_Image:
    mov ax, WORD [cluster]                  ; cluster to read
    pop bx                                  ; buffer to read into
    call Convert_Cluster_to_LBA             ; convert cluster to LBA
    xor cx, cx
    mov cl, BYTE [SecPerClus]               ; sectors to read
    call ReadSectors
    push bx
         
    ; compute next cluster
    mov ax, WORD [cluster]                  ; identify current cluster
    mov cx, ax                              ; copy current cluster
    mov dx, ax                              ; copy current cluster
    shr dx, 1                               ; divide by two
    add cx, dx                              ; sum for (3/2)
    mov bx, 0x7E00                          ; location of FAT in memory
    add bx, cx                              ; index into FAT
    mov dx, WORD [bx]                       ; read two bytes from FAT
    test ax, 1
    jnz .ODD_CLUSTER
         
.EVEN_CLUSTER:
    and dx, 0000111111111111b               ; take low twelve bits (mask: 0x0FFF)
    jmp .DONE
        
.ODD_CLUSTER:
    shr dx, 4                               ; take high twelve bits
         
.DONE:
    mov WORD [cluster], dx                  ; store new cluster
    cmp dx, 0x0FF0                          ; test for EOF
    jb Load_Image
         
DONE:
    mov si, msgCRLF
    call print_string
    mov dl, [bootdevice]
    push WORD 0x0000
    push WORD 0x0500
    retf
         
FAILURE:
    mov si, msgFailure
    call print_string
    mov ah, 0x00
    int 0x16                                ; wait for keypress
    int 0x19                                ; warm boot reset


;******************************************************************************
;   Convert CHS to LBA
;   LBA = (cluster - 2) * sectors per cluster
;******************************************************************************
Convert_Cluster_to_LBA:
    sub ax, 2                               ; zero base cluster number
    xor cx, cx
    mov cl, BYTE [SecPerClus]               ; convert byte to word
    mul cx
    add ax, WORD [datasector]               ; base data sector
    ret

;******************************************************************************
;   Convert LBA to CHS
;   AX    LBA Address to convert
;
;   sector = (logical sector / sectors per track) + 1
;   head   = (logical sector / sectors per track) MOD number of heads
;   track  = logical sector / (sectors per track * number of heads)
;
;******************************************************************************
Convert_LBA_to_CHS:
    xor dx, dx                              ; prepare dx:ax for operation
    div WORD [SecPerTrack]                  ; calculate
    inc dl                                  ; adjust for sector 0
    mov BYTE [Sector], dl
    xor dx, dx                              ; prepare dx:ax for operation
    div WORD [NumHeads]                     ; calculate
    mov BYTE [Head], dl
    mov BYTE [Cylinder], al
    ret

;******************************************************************************
;   Reads sectors
;   CX     Number of sectors to read
;   AX     Starting sector
;   ES:BX  Buffer to read to
;******************************************************************************
ReadSectors:
.NEXTSECTOR:
    mov di, 5                                ; five retries for error
.LOOP:
    push ax
    push bx
    push cx
    call Convert_LBA_to_CHS                  ; convert starting sector from LBA to CHS
    mov  ah, 2                               ; INT 0x13, AH=2 --> read in CHS mode
    mov  al, 1                               ; read one sector
    mov  ch, BYTE [Cylinder]                 ; track/cylinder
    mov  cl, BYTE [Sector]                   ; sector
    mov  dh, BYTE [Head]                     ; head
    mov  dl, BYTE [DriveNum]                 ; drive
    int  0x13                               
    jnc  .SUCCESS                            ; check read error
    xor  ax, ax                              ; INT 0x13, AH=0 --> reset floppy/hard disk
    int  0x13                               
    dec  di                                  ; decrement error counter
    pop  cx
    pop  bx
    pop  ax
    jnz  .LOOP                               ; read again
    int  0x18
.SUCCESS:
    mov  si, msgProgress
    call print_string
    pop  cx
    pop  bx
    pop  ax
    add  bx, WORD [BytesPerSec]              ; queue next buffer
    inc  ax                                  ; queue next sector
    loop .NEXTSECTOR                         ; read next sector
    ret
   
;******************************************************************************
;   Print String 
;    DS:SI   null-terminated string
;******************************************************************************
print_string:
    mov ah, 0x0E      ; BIOS function 0x0E: teletype
.loop:  
    lodsb             ; grab a byte from SI
    test al, al       ; NUL?
    jz .done          ; if the result is zero: get out
    int 0x10          ; else: print out the character
    jmp .loop
.done:
    ret   
   
;******************************************************************************
;    Parameters
;******************************************************************************
Sector         db 0
Head           db 0
Cylinder       db 0   
bootdevice     db 0
datasector     dw 0
cluster        dw 0
ImageName      db "BOOT2   SYS"
msgCRLF        db 0x0D, 0x0A, 0
msgProgress    db "*", 0
msgLoading     db "Loading Second Stage Bootloader", 0x0D, 0x0A, 0
msgFailure     db 0x0D, 0x0A, "BOOT2.SYS MISSING", 0x0D, 0x0A, 0
 
TIMES 510-($-$$) hlt                         ; fill bytes until boot signature
db 0x55                                      ; boot signature
db 0xAA                                      ; boot signature

Second Stage Bootloader 

boot2.asm ==> boot2.sys

;******************************************************************************
;    boot2.asm
;    Second Stage Bootloader
;******************************************************************************
[Bits 16]
org 0x500
jmp entry_point               ; go to entry point

;*******************************************************
;    Includes and Defines
;*******************************************************
%include "gdt.inc"            ; GDT definition
%include "A20.inc"            ; A20 gate enabling
%include "Fat12.inc"          ; FAT12 driver
%include "GetMemoryMap.inc"   ; INT 0x15, eax = 0xE820

%define IMAGE_PMODE_BASE 0x40000 ; where the kernel is to be loaded to in protected mode
%define IMAGE_RMODE_BASE 0x3000  ; where the kernel is to be loaded to in real mode
ImageName     db "CKERNEL SYS"
ImageSize     dw 0

;*******************************************************
;    Data Section
;*******************************************************
msgLoading db 0x0D, 0x0A, "Jumping to OS Kernel...", 0
msgFailure db 0x0D, 0x0A, "Missing CKERNEL.SYS", 0x0D, 0x0A, 0x0A, 0

entry_point:
   cli                  ; clear interrupts
   xor ax, ax           ; null segments
   mov ds, ax
   mov es, ax
   mov ss, ax
   mov sp, 0xFFFF       ; stack begins at 0xffff (downwards)
   sti                  ; enable interrupts

A20:   
   call EnableA20

;*******************************************************
;   Determine physical memory INT 0x15, eax = 0xE820                    
;   input: es:di -> destination buffer        
;*******************************************************
Get_Memory_Map:
    xor eax, eax
    mov ds, ax
    mov di, 0x1000
    call get_memory_by_int15_e820
    xor ax, ax
    mov es, ax ; important to null es!

Install_GDT:
    call InstallGDT
    sti   
   
Load_Root:
    call LoadRoot
    mov ebx, 0
    mov ebp, IMAGE_RMODE_BASE
    mov esi, ImageName
    call LoadFile       
    mov DWORD [ImageSize], ecx
    cmp ax, 0
    je EnterProtectedMode
    mov si, msgFailure
    call print_string
    xor ah, ah

;*******************************************************
;   Switch from Real Mode (RM) to Protected Mode (PM)             
;*******************************************************
EnterProtectedMode:
    mov si, msgLoading
    call print_string

    ; switch off floppy disk motor
    mov dx,0x3F2     
    mov al,0x0C
    out dx,al        
   
    ; switch to PM
    cli                              
    mov eax, cr0                          ; set bit 0 in cr0 --> enter PM
    or eax, 1
    mov cr0, eax
    jmp DWORD CODE_DESC:ProtectedMode     ; far jump to fix CS. Remember that the code selector is 0x8!

[Bits 32]
ProtectedMode:
    mov ax, DATA_DESC                     ; set data segments to data selector (0x10)
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov esp, 0x9000   

CopyImage:
    mov eax, DWORD [ImageSize]
    movzx ebx, WORD  [BytesPerSec]
    mul ebx
    mov ebx, 4
    div ebx
    cld
    mov esi, IMAGE_RMODE_BASE
    mov edi, IMAGE_PMODE_BASE
    mov ecx, eax
    rep movsd                             ; copy image to its protected mode address

;*******************************************************
;   Execute Kernel
;*******************************************************
EXECUTE:
    jmp DWORD CODE_DESC:IMAGE_PMODE_BASE
    cli
    hlt
   
;*******************************************************
;   calls, e.g. print_string
;*******************************************************
[BITS 16]
print_string:
   lodsb          ; grab a byte from SI
   or al, al      ; logical or AL by itself
   jz .done       ; if the result is zero, get out
   mov ah, 0x0E
   int 0x10       ; otherwise, print out the character!
   jmp print_string
.done:
   ret   


Als Interface zum Floppy Disk-Laufwerk dient die inkludierte Datei FAT12.inc:

%ifndef FAT12_INC
%define FAT12_INC

;******************************************************************************
;    Fat12.inc
;    FAT12 filesystem for 3,5" floppy disk
;******************************************************************************

[BITS 16]

%include "Fat12_BPB.inc"

datasector  dw 0x0000
cluster     dw 0x0000

Sector      db 0x00
Head        db 0x00
Cylinder    db 0x00

;******************************************************************************
; Convert CHS to LBA
; LBA = (cluster - 2) * sectors per cluster
;******************************************************************************

Convert_Cluster_to_LBA:
    sub ax, 2                                  ; zero base cluster number
    xor cx, cx
    mov cl, BYTE [SecPerClus]                  ; convert byte to word
    mul cx
    add ax, WORD [datasector]                  ; base data sector
    ret

;******************************************************************************
; Convert LBA to CHS
; AX      LBA Address to convert
;
; sector          = (logical sector / sectors per track) + 1
; head            = (logical sector / sectors per track) MOD number of heads
; cylinder/track  = logical sector / (sectors per track * number of heads)
;
;******************************************************************************

Convert_LBA_to_CHS:
    xor dx, dx                              ; prepare dx:ax for operation
    div WORD [SecPerTrack]                  ; calculate
    inc dl                                  ; adjust for sector 0
    mov BYTE [Sector], dl
    xor dx, dx                              ; prepare dx:ax for operation
    div WORD [NumHeads]                     ; calculate
    mov BYTE [Head], dl
    mov BYTE [Cylinder], al
    ret

;******************************************************************************
; Read sectors
; CX      Number of sectors to read
; AX      Starting sector
; ES:EBX  Buffer to read to
;******************************************************************************

ReadSectors:
.MAIN:
    mov     di, 0x0005                          ; five retries for error
.SECTORLOOP:
    push    ax
    push    bx
    push    cx
    call    Convert_LBA_to_CHS                  ; convert starting sector to CHS
    mov     ah, 0x02                            ; BIOS read sector
    mov     al, 0x01                            ; read one sector
    mov     ch, BYTE [Cylinder]                 ; track / cylinder
    mov     cl, BYTE [Sector]                   ; sector
    mov     dh, BYTE [Head]                     ; head
    mov     dl, BYTE [DriveNum]                 ; drive number
    int     0x13                                ; invoke BIOS
    jnc     .SUCCESS                            ; test for read error
    xor     ax, ax                              ; BIOS reset disk
    int     0x13                                ; invoke BIOS
    dec     di                                  ; decrement error counter
    pop     cx
    pop     bx
    pop     ax
    jnz     .SECTORLOOP                         ; attempt to read again
    int     0x18
.SUCCESS:
    pop     cx
    pop     bx
    pop     ax
    add     bx, WORD [BytesPerSec]              ; queue next buffer
    inc     ax                                  ; queue next sector
    loop    .MAIN                               ; read next sector
    ret
         
;******************************************************************************
;    Defines
;******************************************************************************
         
%define ROOT_OFFSET  0x2e00
%define FAT_SEG      0x02c0
%define ROOT_SEG     0x02e0

;******************************************************************************
; LoadRoot ()
; Transfers Root Directory Table into memory at 0x7e00
;******************************************************************************

LoadRoot:
    pusha
    push es

    ; compute size of root directory and store in "cx"
    xor     cx, cx                       
    xor     dx, dx
    mov     ax, 32                        ; 32 byte directory entry
    mul     WORD [RootEntries]            ; total size of directory
    div     WORD [BytesPerSec]            ; sectors used by directory
    xchg    ax, cx                        ; store in CX

    ; compute location of root directory and store in "ax"
    mov     al, BYTE [NumFATs]            ; number of FATs
    mul     WORD [FATSize]                ; sectors used by FATs
    add     ax, WORD [ReservedSec]
    mov     WORD [datasector], ax         ; base of root directory
    add     WORD [datasector], cx

    ; read root directory into 0x7e00
    push    word ROOT_SEG
    pop     es
    mov     bx, 0                         ; copy root dir
    call    ReadSectors                   ; read in directory table
    pop     es
    popa                                 
    ret

;******************************************************************************
; LoadFAT ()
; Loads FAT table into memory at 0x7c00
; ES:DI Root Directory Table
;******************************************************************************

LoadFAT:
    pusha                           
    push es

    ; compute size of FAT and store it into CX
    xor ax, ax
    mov al, BYTE [NumFATs]            ; number of FATs
    mul WORD [FATSize]                ; sectors used by FATs
    mov cx, ax

    ; compute location of FAT and store it into AX
    mov ax, WORD [ReservedSec]

    ; read FAT into memory (Overwrite bootloader at 0x7c00)
    push WORD FAT_SEG
    pop es
    xor bx, bx
    call ReadSectors
    pop es
    popa                           
    ret
   
;******************************************************************************
; FindFile ()
; Search for filename in root table
; DS:SI  File name
; AX     Return value: File index number in directory table (error: -1)
;******************************************************************************

FindFile:
    push cx                        ; store registers
    push dx
    push bx
    mov bx, si                     ; copy filename for later

    ; browse root directory for binary image
    mov cx, WORD [RootEntries]     ; load loop counter
    mov di, ROOT_OFFSET            ; locate first root entry at 1 MB mark
    cld                            ; clear direction flag

.LOOP2:
    push cx
    mov cx, 11                     ; eleven character name. Image name is in SI
    mov si, bx                     ; image name is in BX
    push di
    rep cmpsb                      ; test for entry match
    pop di
    je .Found
    pop cx
    add di, 32                     ; queue next directory entry
    loop .LOOP2

.NotFound:
    pop    bx                     
    pop    dx
    pop    cx
    mov    ax, -1                  ; set error code
    ret

.Found:
    pop    ax                      ; return value into AX contains entry of file
    pop    bx                     
    pop    dx
    pop    cx
    ret

;******************************************************************************
; LoadFile ()
; ES:SI   File to load
; EBX:BP  Buffer to load file to
; AX      Return value: success 0, error -1
;******************************************************************************

LoadFile:
    xor ecx, ecx                   ; size of file in sectors
    push ecx

.FIND_FILE:
    push bx                        ; BX=>BP points to buffer to write to; store it for later
    push bp
    call FindFile                  ; find the file. ES:SI contains the filename
    cmp    ax, -1
    jne    .LOAD_IMAGE_PRE
    pop    bp
    pop    bx
    pop    ecx
    mov    ax, -1
    ret

.LOAD_IMAGE_PRE:
    sub    edi, ROOT_OFFSET
    sub    eax, ROOT_OFFSET

    ; get starting cluster
    push word ROOT_SEG                ;root segment loc
    pop    es
    mov    dx, WORD [es:di + 0x001A]  ; DI points to file entry in root directory table. Reference the table...
    mov    WORD [cluster], dx         ; file's first cluster
    pop    bx                         ; get location to write to so we dont screw up the stack
    pop    es
    push bx                           ; store location for later again
    push es
    call LoadFAT

.LOAD_IMAGE:
    ; load the cluster
    mov  ax, WORD [cluster]           ; cluster to read
    pop  es                           ; bx:bp = es:bx
    pop  bx
    call Convert_Cluster_to_LBA
    xor  cx, cx
    mov cl, BYTE [SecPerClus]
    call ReadSectors
    pop  ecx
    inc  ecx
    push ecx
    push bx
    push es
    mov  ax, FAT_SEG                   ;start reading from FAT
    mov  es, ax
    xor  bx, bx

    ; get next cluster
    mov ax, WORD [cluster]             ; identify current cluster
    mov cx, ax                         ; copy current cluster
    mov dx, ax                         ; copy current cluster
    shr dx, 1                          ; divide by two
    add cx, dx                         ; sum for (3/2)
    mov bx, 0                          ;location of FAT in memory
    add bx, cx
    mov dx, WORD [es:bx]
    test ax, 1                         ; test for odd or even cluster
    jnz .ODD_CLUSTER

.EVEN_CLUSTER:
    and dx, 0000111111111111b          ; take low 12 bits
    jmp .DONE

.ODD_CLUSTER:
    shr dx, 4                          ; take high 12 bits

.DONE:
    mov WORD [cluster], dx
    cmp dx, 0x0ff0                     ; test for EOF
    jb .LOAD_IMAGE

.SUCCESS:
    pop es
    pop bx
    pop ecx
    xor ax, ax
    ret

%endif   


Um die Zusammenhänge besser zu verstehen, schaut man sich z.B. die Diskette mit dem Hexeditor an. Formatieren Sie die Diskette vorher, um den "Restmüll" zu beseitigen! Nur dann können Sie PrettyOS in Reinkultur analysieren. Anschließend kopieren Sie die beiden Bootloader (einen mit partcopy, einen mit copy) und den Kernel wie gewohnt auf die frische Diskette. Nun sieht man klar, welche Strukturen und Daten auf den Bootsektor folgen:



Direkt auf den Boot-Sektor folgt die erste Kopie der FAT im Bereich 0x200 bis 0x13FF. Anschließend folgt die zweite Kopie der FAT im Bereich von 0x1400 bis 0x25FF. Im Bereich 0x2600 bis 0x41FF liegt das Stammverzeichnis (Root Directory). Man erkennt das Volume Label "PRETTYOS" als ersten Eintrag, gefolgt von den beiden 8.3-Filenamen "BOOT2___SYS" und "CKERNEL_SYS" (wenn Sie eine bereits ältere Diskette verwenden, finden Sie hier eine Menge an Historie).

In der Programm-Routine "LoadRoot" in der Datei FAT12.inc erfolgt die Berechnung der Größe und der Lage des Root Directory sowie der Transfer von Diskette zum Hauptspeicher. Die folgende Abbildung zeigt die Zusammenhänge und erläutert die Rechenformeln:




Der inkludierte BIOS Parameter Block (BPB) für die Stammdaten der Dateiverwaltung ist identisch mit der Version im First Stage Bootloader. Erstellt wird die nachfolgende Version durch Ausführen von  build.bat  in den beiden Bootloader-Verzeichnissen und durch den bisher üblichen make-Prozess im Kernel.
Führen Sie dies bei bereits eingelegter Diskette durch, da die Kopiervorgänge zur Diskette in den Batch-Dateien bzw. im makefile bereits hinterlegt sind.



Werfen Sie anschließend bitte einen Blick auf diese Diskette:



Unser PrettyOS wird nun für den MS Explorer auch auf Datenträger "sichtbar". Allerdings besteht nun leider auch die Gefahr der Verwechslung der einzelnen Komponenten beim Zusammenbau.

Wir haben nun folgende Komponenten im Angebot:
- First Stage Bootloader
- Second Stage Bootloader
- Kernel
- User-Programme (Shell, ...)

Root Directory Table

Wie Sie gesehen haben, belegt die Root Directory Table (Stammverzeichnis) 14 Sektoren auf der Diskette und besteht aus 224 Einträgen mit jeweils 32 Byte.
Die Einträge (mit Ausnahme des ersten Eintrags, der das Volume Label beinhaltet) kennzeichnet Eigenschaften einer Datei (File) oder eines Verzeichnisses (Directory) und ist wie folgt aufgebaut:

Byte
Bedeutung
0-7
File Name (aufgefüllt mit Leerzeichen)
8-10
File Extension (aufgefüllt mit Leerzeichen)
11
Dateiattribute:

Bit 0 : Read Only (Schreibgeschützt)
Bit 1 : Hidden (Versteckt)
Bit 2 : System
Bit 3 : Volume Label
Bit 4 : Subdirectory (Unterverzeichnis)
Bit 5 : Archive
Bit 6 : Device (Internal use)
Bit 7 : Unused
12
Reserviert (ungenutzt)
13
Erstellungszeitpunkt (in 10 Millisekunden)
14-15 Erstellungszeitpunkt:

Bit 0-4   : Sekunden (0-29)
Bit 5-10  : Minuten  (0-59)
Bit 11-15 : Stunden  (0-23)
Auflösungsgenauigkeit der Sekunden: 2 s (0..29)
16-17
Erstellungszeitpunkt:

Bit 0-4  : Tag   (1-31)

Bit 5-8  : Monat (1 = Januar, 12 = Dezember)
Bit 9-15 : Jahr  (0 = 1980, 29 = 2009, 127 = 2107)
18-19
Letzter Zugriff (Format wie 16-17)
20-21
EA Index (OS/2 und NT)
22-23
Letzte Veränderung, Zeitpunkt: (wie 14-15)
24-25
Letzte Veränderung, Datum: (wie 16-17)
26-27
Erster Cluster: Offset des Start-Clusters + 2
Die Cluster-Nummerierung startet bei Disketten mit Nr. 2

28-31
Dateigröße in Byte

Am konkreten Beispiel von boot2.sys und ckernel.sys lesen sich die für uns wichtigsten Daten wie folgt:


File Allocation Table (FAT12)

Die FAT12 reiht Informationen im 12-Bit-Format formlos aneinander.
Die "Datei" entsteht dabei in Form einer einfach verketteten Liste der aufgeführten Cluster.

Beim Einlesen der 12-Bit-Werte geht man so vor, dass man ein ganzes WORD, also 2 Byte bzw. 16 Bit, liest und dann in Abhängigkeit, ob man bezüglich der Nummerierung beim Lesen einen "geradzahligen" (even) oder "ungeradzahligen" (odd) Cluster-Wert vorfindet, entweder die unteren 12 Bit oder die oberen 12 Bit verwendet (Intel: Little Endian).

.EVEN_CLUSTER:
    and dx, 0000111111111111b          ; take low 12 bits
    jmp .DONE

.ODD_CLUSTER:
    shr dx, 4                          ; take high 12 bits


Wenn man mit dem Hex-Editor auf die Bytes schaut und diese haben die Werte ab cd ef , dann liest man die beiden 12-Bit-Zahlen wie folgt: dab efc
Wir üben das mit der oben dargestellten FAT:



ff0 fff 003 fff 005 006 007 008 009 00a 00b 00c 00d 00e 00f 010 011 012 013 014 015 016 017 018 019 01a 01b 01c 01d 01e 01f 020 021 022 023 024 025 026 027 028 029 02a 02b 02c 02d 02e 02f 030 031 032 033 034 035 036 037 038 039 03a 03b 03c 03d 03e 03f 040 041 042 043 044 045 fff

Die Bedeutung der "Kettenglieder" folgt diesen Regeln:

 FAT12 Wert
 Bedeutung
 0x000  Freier Cluster
 0x001  Reserviert
 0x002 – 0xFEF  Cluster gehört zu einer Datei. Wert zeigt auf den nächsten Cluster in der Kette
 0xFF0 – 0xFF6  Reserviert
 0xFF7  "Bad Sector" im Cluster bzw. reservierter Cluster
 0xFF8 – 0xFFF  Letzter Cluster der Datei


Wir sehen erwartungsgemäß drei Clusterketten:
- Volume Label
- File boot2.sys
- File ckernel.sys

Die Cluster einer Datei bilden eine zusammenhängende Kette. Die beiden Dateien sind "unfragmentiert", haben also örtlich zusammenhängende Cluster, was bezüglich der Bewegungen des Lesekopfes des Laufwerkes ein Optimum darstellt. boot2.sys belegt die Cluster 002h und 003h, während ckernel.sys sich von Cluster 004h bis 045h erstreckt. Schauen Sie sich die beiden Dateien (mit dem Explorer) bezüglich ihrer "Eigenschaften" an:



Man erkennt einen Unterschied zwischen der wirklichen Dateigröße und der "Größe" auf dem Datenträger. Bei boot2.sys sind es 1024/512 = 2 Sektoren, nämlich 002h und 003h, und bei ckernel.sys sind dies 33792/512 = 66 Sektoren, nämlich von 004h bis 045h. Letzteres sind genau 66 Sektoren, wie Sie leicht nachrechnen können.

Wie Sie sehen, sind die Zusammenhänge klar nachvollziehbar.

Die Daten finden Sie übrigens, wie erwartet, ab 4200h (boot2.sys) und 4600h (ckernel.sys):



Die Einträge in der FAT bezüglich der Cluster werden übigens wie folgt umgerechnet:

physical sector number  =  33  +  FAT entry number  -  2  =  FAT entry number  +  31

Damit haben Sie nun (abgesehen von unserem initrd-Filesystem der Marke Eigenbau) einen ersten Einblick in den Aufbau eines einfachen aktuellen Dateisystems erhalten. Disketten werden heute immer noch mit FAT12 sowohl von MS Windows als auch von Linux formatiert und verwaltet. Wir werden dieses Format ebenfalls weitgehend für PrettyOS einsetzen, damit die Datenübergabe gewährleistet ist.


Floppy Driver

Read Sector

Da wir nun im First und Second Stage Bootloader das Dateisystem FAT12 eingesetzt haben, möchte man diese Technik natürlich auch im Kernel selbst, z.B. in C  programmiert einsetzen. Hier gibt es aber einen großen Unterschied. Im Bootloader-Bereich profitierten wir vom BIOS. Im Kernel stehen wir "alleine in der Wüste".

Da die Diskette beim modernen PC keine zentrale Rolle mehr spielt, werden wir uns die Aufgabe so einfach wie möglich machen. Wir verwenden eine bereits vorhandene Bibliothek. Bezüglich des Aufbaus passt das aktuell publizierte Modul von Mike von www.brokenthorn.com hervorragend zu PrettyOS. Durch Einbinden von Header plus Implementationsdatei  flpydsk.h / flpydsk.c  und nach einigen kleinen Modifikationen (versuchen Sie es doch selbst, bevor Sie den Download ziehen) kann man als ersten Test den Sektor 0 (LBA-Zählweise bei null beginnend) von Diskette via DMA (Direct Memory Access), also weitgehend an der CPU vorbei, in vier Paketen von je 128 Byte - hier dezimal - auf den Bildschirm zaubern.

Die detaillierten Grundlagen zum Floppy Driver kann man in Mike's Tutorial Kapitel 20 studieren. Allerdings benötigen wir auch das Verständnis für den DMA. Eine hervorragende Übersicht findet man hier: wiki.osdev.org  ISA DMA. Beachten Sie diesbezüglich vor allem das Kapitel Floppy_Disk_DMA_Programming, das wir vor allem auch für den späteren Schreibvorgang zum Verständnis benötigen.




Wenn alles glatt geht, sollte folgende Bildschirmausgabe erscheinen:

PrettyOS [Version 0.1.0098]  (C) 2009 henkessoft.de

Usable RAM: 1048124 KB

Ram Disk at: 4008100Ch

Floppy Driver Test!

Sector 0 contents:

233 59 0 77 83 68 79 83 53 46 48 0 2 1 1 0 2 224 0 64 11 240 9 0 18 0 2 0 0 0 0

0 0 0 0 0 0 0 41 131 162 205 176 78 79 32 78 65 77 69 32 32 32 32 70 65 84 49 50

 32 32 32 49 192 142 216 142 192 142 224 142 232 184 0 144 142 208 49 228 136 22

 147 125 190 168 125 232 42 1 49 201 49 210 184 32 0 247 38 17 124 247 54 11 124

 145 160 16 124 247 38 22 124 3 6 14 124 163 148 125 1 14 148 125 187 0 126 232

196

 

0 139 14 17 124 191 0 126 81 185 11 0 190 152 125 87 243 166 95 116 10 89 129 19

9 32 0 226 236 233 112 0 139 85 26 137 22 150 125 49 192 160 16 124 247 38 22 12

4 137 193 161 14 124 187 0 126 232 139 0 49 192 142 192 187 0 5 83 161 150 125 9

1 232 82 0 49 201 138 14 13 124 232 115 0 83 161 150 125 137 193 137 194 209 234

 1 209 187 0 126 1 203 139 23 169 1 0 117 7 129 226 255 15 233 3 0 193 234 4 137

 22 150 125 129 250 240 15 114 196 190 163

 

 125 232 127 0 138 22 147 125 104 0 0 104 0 5 203 190 202 125 232 110 0 180 0 205

 22 205 25 45 2 0 49 201 138 14 13 124 247 225 3 6 148 125 195 49 210 247 54 24

124 254 194 136 22 144 125 49 210 247 54 26 124 136 22 145 125 162 146 125 195 1

91 5 0 80 83 81 232 221 255 180 2 176 1 138 46 146 125 138 14 144 125 138 54 145

 125 138 22 36 124 205 19 115 12 49 192 205 19 79 89 91 88 117 216 205 24 190 16

6 125 232 11 0 89 91 88 3 30 11 124 64

 

226 195 195 180 14 172 132 192 116 5 205 16 233 246 255 195 0 0 0 0 0 0 0 0 66 7

9 79 84 50 32 32 32 83 89 83 13 10 0 42 0 76 111 97 100 105 110 103 32 83 101 99

 111 110 100 32 83 116 97 103 101 32 66 111 111 116 108 111 97 100 101 114 13 10

 0 13 10 66 79 79 84 50 46 83 89 83 32 77 73 83 83 73 78 71 13 10 0 244 244 244

244 244 244 244 244 244 244 244 244 244 244 244 244 244 244 244 244 244 244 244

244 244 244 244 244 244 244 85 170

 
Done.

<DIR> dev
35      file1
35      file2

35      file3
1693    shell

$>


Analysieren wir das Ganze von außen nach innen, so steht in ckernel.c folgender Code zum Lesen eines Sektors und zur Ausgabe in vier 128er Paketen:

    /// //////////////////////// TEST FLOPPY DRIVER
    printformat("\nFloppy Driver Test!\n");
    flpydsk_set_working_drive(0); // set drive 0 as current drive
    flpydsk_install(6);           // floppy disk uses IRQ 6
    ULONG sectornum = 0;
    unsigned char* sector = 0;
    printformat("\nSector %d contents:\n\n", sectornum);
    sector = flpydsk_read_sector( sectornum );
    if(sector)
    {
        int i=0;
        int c,j;
        for(c=0; c<4; ++c)
        {
            for(j=0; j<128; ++j)
                printformat("%d ", sector[i+j]);
            i+=128;
            sleepSeconds(10);
            printformat("\n\n");
        }
    }
    else
        printformat("\nError reading sector from disk");
    printformat("\nDone.\n");
    /// //////////////////////// TEST FLOPPY DRIVER


Hierbei setzen wir folgende Funktionen und Unterfunktionen des Floppy Disk-Moduls ein:

void flpydsk_set_working_drive(unsigned char drive)
{ if (drive < 4) _CurrentDrive = drive; }
Der Floppy Disk Controller (FDC) kann maximal 4 Laufwerke (FDD) ansteuern.
void flpydsk_install(int irq)
{
    irq_install_handler(irq, i86_flpy_irq);
    flpydsk_initialize_dma();
    flpydsk_reset();
    flpydsk_drive_data(13, 1, 0xF, TRUE);
}
In der Installationsroutine werden folgende Schritte erledigt:
- IRQ 6 zum Floppy Disk Handler i86_flpy_irq einrichten
- DMA starten
- FDD reset
- Konfiguration der Parameter
unsigned char* flpydsk_read_sector(int sectorLBA)
{
    if (_CurrentDrive >= 4) return 0;
    int head=0, track=0, sector=1;
    flpydsk_lba_to_chs(sectorLBA, &head, &track, &sector);
    flpydsk_control_motor(TRUE);
    if(flpydsk_seek (track, head)) return 0;
    flpydsk_read_sector_imp(head, track, sector);
    flpydsk_control_motor(FALSE);
    return (unsigned char*)DMA_BUFFER;
}
Das Einlesen eines Sektors beinhaltet:
- Check auf max. 4 FDD
- Umrechnung LBA --> CHS
- FDD Motor hoch fahren
- Gewünschten Track anfahren
- Sektor mittels DMA einlesen
- Motor ausschalten
- zeiger auf DMA-Datenpuffer im Hauptspeicher zurück liefern

Write Sector

Nach dem Lesen kommt das Schreiben, aber bevor man Daten auf einem Datenträger mit Dateiformat schreibend manipuliert, sollte man den Aufbau und die Zusammenhänge zuerst genau verstehen. Was passiert z.B. beim Formatieren der Diskette? Wie verändert man die Eigenschaftsdaten der Files. Wie erstellt / löscht man Files? (FAT #1, FAT #2, Stammverzeichnis, Datenbereich)

Diskette nach "format a."
Zunächst schauen wir uns an, wie eine Diskette aussieht, nachdem diese in der Konsole von MS Windows mittels "format a:" low level formatiert wurde. Bei der Nachfrage nach dem Volume Label geben Sie bitte "d00fcode001" o.ä. ein.

Befragt man das Laufwerk a: nun mit  dir a:, so erhält man folgende Ausgabe:

 Datenträger in Laufwerk A: ist D00FCODE001
 Volumeseriennummer: 24BD-8EB5

 Verzeichnis von A:\

 Datei nicht gefunden

Das reicht uns selbstverständlich nicht. So lassen wir uns nicht abspeisen. Nun benötigen wir einen Hex-Editor, mit dem man direkt von Diskette lesen kann. Ich verwende hierzu Hex-Editor Neo, WinHex (gibt zusätzlich statistische Infos über Diskette aus) oder einen anderen Hex-Editor, die diese direkte "Disk open"-Fähigkeit anbietet:


 
Bootet man diese Diskette anschließend z.B. mittels Bochs oder PC, dann findet man diese Aufforderung:

Datenträger entfernen
Neustart: Taste drücken

Die erste Kopie der FAT beginnend bei 0x200 ist "leer". Den entsprechenden Eintrag "f0 ff ff" findet man auch bei 0x1400, dem Platz der zweiten Kopie der FAT.

Unser "d00fc0de001" finden wir als ersten 32-Byte-Eintrag in der Root Directory, dem Stammverzeichnis bei 0x2600:



Der Datenbereich, der ab 0x4200 beginnt, enthält bei "richtiger" Formatierung (also kein Quick-Format) durchgehend die vom MS-Format gesetzte Lösch-Kennung "f6":



Wenn wir in unsere Shell einen eigenen Format-Befehl für FAT12 einbauen wollten, müssten wir die Diskette also entsprechend einrichten:
- Bootsektor mit BPB, entsprechendem Code / Meldungen und Boot-Signatur "0x55 0xAA" versehen
- FAT Nr. 1 (0x200) und FAT Nr. 2 (0x1400) mit "f0 ff ff" versehen
- Ersten Eintrag in die Root Directory mit passendem "Volume Label" (11 Zeichen) und entsprechenden "Beilagen" wie z.B. Attribut 8, erster Cluster 0 und Filegröße 0 versehen. Die Zeit-/Datums-Kennungen kann man alle auf Null oder eine andere gewünschte Info setzen
- Im Datenbereich von 0x4600 bis 0x163DFF jedes Byte - wie bei MS DOS und MS Windows üblich - auf "0xF6" (ASCII: ö) setzen.


Datum manipulieren
Wir setzen nach dem Aufspielen der beiden Bootloader Stages nun die Zeitdaten von boot2.sys komplett auf null. Was passiert?
Im MS Explorer werden überhaupt keine Zeitdaten mehr angezeigt. Die gute alte DOS-Konsole zeigt uns den 01.01.1601, 02:00 h, als Erstellungsdatum.
Erstellen Sie manuell im Hex-Editor ihr Lieblingsdatum. Ändern Sie die Attribute. Informationen finden Sie im Kapitel Root Directory und im Internet, aber Probieren geht vor Studieren.

Viel Spaß dabei!



Write-Anweisung
Wir wandeln die Read-Funktionen unseres Moduls in entsprechend ergänzte Write-Funktionen um. FDC_CMD_EXT_SKIP lassen wir beim Schreiben weg, sonst klappt es nicht.

// write a sector
void flpydsk_write_sector_imp(unsigned char head, unsigned char track, unsigned char sector)
{
    ULONG st0, cyl;
    flpydsk_dma_write(); // set the DMA for write transfer

    flpydsk_send_command(FDC_CMD_WRITE_SECT | FDC_CMD_EXT_MULTITRACK | FDC_CMD_EXT_DENSITY );  // write a sector
    flpydsk_send_command(head << 2 | _CurrentDrive );
    flpydsk_send_command(track);
    flpydsk_send_command(head);
    flpydsk_send_command(sector);
    flpydsk_send_command(FLPYDSK_SECTOR_DTL_512 );
    flpydsk_send_command(( ( sector + 1 ) >= FLPY_SECTORS_PER_TRACK ) ? FLPY_SECTORS_PER_TRACK : sector + 1 );
    flpydsk_send_command(FLPYDSK_GAP3_LENGTH_3_5 );
    flpydsk_send_command(0xFF);

    flpydsk_wait_irq();

    int j;
    for(j=0; j<7; ++j)
        flpydsk_read_data();      // read status info
    flpydsk_check_int(&st0,&cyl); // let FDC know we handled interrupt
}

// write a sector
int flpydsk_write_sector(int sectorLBA)
{
    if (_CurrentDrive >= 4) return -1;
    // convert LBA sector to CHS
    int head=0, track=0, sector=1;
    flpydsk_lba_to_chs(sectorLBA, &head, &track, &sector);
    // turn motor on and seek to track
    flpydsk_control_motor(TRUE);
    if(flpydsk_seek (track, head)) return -2;
    // write sector and turn motor off
    flpydsk_write_sector_imp(head, track, sector);
    flpydsk_control_motor(FALSE);
    return 0;
}
 
Erster Test: Wir überschreiben die Root Dir
Im Sine von "Spaß muss sein, sonst geht keiner mit der Beerdigung" (OS development ist ansonsten wenig spaßig!) überschreiben wir als ersten "Schreib-Test" unser Stammverzeichnis (root directory). Ich habe als Demonstration unserer neu gewonnenen Stärke das "@"-Zeichen gewählt, das wir der Diskette nun "einbrennen":

    /// //////////////////////// TEST FLOPPY DRIVER WRITE
    k_memset((void*)0x1000, 0x40, 0x200);  // 0x40: '@' 
    int retVal = flpydsk_write_sector(19); // sector 19: 0x4200, start of root directory
    if(retVal==0) printformat("\nWriting data to sector was successful.\n\n");
    /// //////////////////////// TEST FLOPPY DRIVER WRITE

 
Das Ergebnis unserer "Missetat" kann man sich zunächst im MS Explorer anschauen. Ein Versuch, eine Datei zu öffnen, misslingt selbstverständlich. Die Dateien boot2.sys und ckernel.sys sind im Stammverzeichnis nicht mehr auffindbar, da wir ihren Namen, ihre Größe, ihre Attribute und ihren ersten Cluster hemmungslos "übertüncht" haben.



Sie sollten sich solche Dinge unbedingt mit dem DIR-Befehl in der Konsole anschauen. Nette Details zeigt uns MS Windows nämlich nicht:



Wie gefällt Ihnen die Dateigröße, die belegte bzw. freie Kapazität sowie das Erstellungsdatum?
Hier ist viel Raum für eigene Ideen und Erkundungen. Beherrschen Sie dieses FAT12-Filesystem bis ins letzte Bit.
Mit unserem Programm oder dem Hex-Editor.

Die Diskette wurde von uns sozusagen "durch einen Schuss ins Herz ermordet". Nun kommt der "Leichenbeschauer" ins Spiel. Er findet heraus, dass es deshalb genau 16 Dateieinträge sind, weil 16 Einträge * 32 Byte/Eintrag = 512 Byte ergeben. Es wurde Sektor 19 (19 * 0x200 = 0x4200), also der Beginn der Root Dirtectory,  "böswillig" überschrieben. Als Beweis öffnet er die "Leiche" (Diskette) mit einem geeigneten Hex-Editor:



Die "Mordwaffe" kennen Sie ja bereits:

    k_memset((void*)0x1000, 0x40, 0x200);
    int retVal = flpydsk_write_sector(19);

Sie können ja mal als Übung versuchen, die "Leiche" manuell zu beleben. Sie müssen hierzu lediglich die ersten drei Einträge der Root Directory  wieder einrichten. Vorbild: siehe hier.
Die restlichen 13 setzen Sie einfach auf null. Denken Sie auch an die genaue Größe der Dateien (finden Sie am Original auf Ihrer Festplatte). Den Startcluster können Sie über die FAT checken.




Hinweis:

Das Schreiben von DMA-Puffer (im Hauptspeicher) zur Diskette funktioniert noch nicht optimal.
Die Anzahl der zu schreibenden Sektoren können Sie hier eingeben:

  // Tauschen Sie diese fragwürdige Zeile (aus dem Original von "brokenthorn" übernommen) ...
 
  flpydsk_send_command( ( ( sector + 1 ) >= FLPY_SECTORS_PER_TRACK ) ? FLPY_SECTORS_PER_TRACK : sector + 1 );

  // gegen eine sinnvolle Anweisung aus:
 
    flpydsk_send_command( 18 ); // Anzahl der Sektoren, die geschrieben werden sollen!

 
  // Sie müssen leider noch an einem Track-Anfang beginnen, also 0, 18, 36, ... (LBA-Zählweise der Sektoren),
  // wenn Sie weniger als 18 Sektoren schreiben wollen.

  // Aber dies hier geht mit vollen 18 Sektoren:

    /// //////////////////////// TEST FLOPPY DRIVER WRITE
    k_memset((void*)0x1000, 0xF6, 0x2400); // 0xF6 wird beim Formatieren verwendet
    for( i=33; i<=2847; i+=18 )            // Sektor 33 (LBA) entspricht 0x4200 (Start des Datenbereichs)
    {
        printformat("%d ",i);
        flpydsk_write_sector(i);
    }
    /// //////////////////////// TEST FLOPPY DRIVER WRITE
 
  // Ursache leider noch nicht klar.

Ich empfehle daher beim aktuellen Stand des Floppy Disk Treibers mit jeweils 18 Sektoren zu arbeiten und auch die Rückgabewerte zu prüfen. Informationen finden Sie hier:
http://www.brokenthorn.com/Resources/OSDev20.html (Kapitel "Write Sector" und "Read Sector")
http://lowlevel.brainsware.org/wiki/index.php/Floppy_Disk_Controller#ST0

Analyse der Root Directory Table

Nachdem wir nun Tracks lesen und schreiben können, gehen wir an eine erste Aufgabe heran. Wir wollen die Root Directory Table der Diskette analysieren und auf dem Bildschirm darstellen. Dazu müssen wir die Daten ab Sektor 19 (LBA-Zählweise), also ab 0x2600 auf Diskette, in den DMA-Puffer bei 0x1000 im Speicher einlesen und die maximal 224 Einträge von jeweils 32 Byte auf gültige Einträge analysieren.



Mit folgendem Sourcecode lesen wir das "Stammverzeichnis" bezüglich wichtiger Daten aus:

    /// //////////////////////// TEST FLOPPY DRIVER READ DIRECTORY
    k_memset((void*)DMA_BUFFER, 0x0, 0x2400);
    flpydsk_read_sector(19); // start at 0x2600: root directory (14 sectors)
    printformat("<Floppy Disc Root Dir>\n");
    for(i=0;i<224;++i)       // 224 Entries * 32 Byte
    {
        if(
            (( *((unsigned char*)(DMA_BUFFER + i*32)) )      != 0x00 ) && /* free from here on           */
            (( *((unsigned char*)(DMA_BUFFER + i*32)) )      != 0xE5 ) && /* 0xE5 deleted = free         */
            (( *((unsigned char*)(DMA_BUFFER + i*32 + 11)) ) != 0x0F )    /* 0x0F part of long file name */
          )
        {
            int start = DMA_BUFFER + i*32; // name
            int count = 8;
            char* end = (char*)(start+count);
            for(; count != 0; --count)
            {
                if( *(end-count) != 0x20 ) /* empty space in file name */
                    printformat("%c",*(end-count));
            }

            if(i!=0) printformat("."); // usual separator between file name and file extension

            start = DMA_BUFFER + i*32 + 8; // extension
            count = 3;
            end = (char*)(start+count);
            for(; count != 0; --count)
                printformat("%c",*(end-count));
            printformat("\t%d byte", *((ULONG*)(DMA_BUFFER + i*32 + 28)));
            printformat("\t");
            if((( *((unsigned char*)(DMA_BUFFER + i*32 + 11)) ) & 0x08 ) == 0x08 ) printformat(" (lab)");
            if((( *((unsigned char*)(DMA_BUFFER + i*32 + 11)) ) & 0x10 ) == 0x10 ) printformat(" (dir)");
            if((( *((unsigned char*)(DMA_BUFFER + i*32 + 11)) ) & 0x01 ) == 0x01 ) printformat(" (r/o)");
            if((( *((unsigned char*)(DMA_BUFFER + i*32 + 11)) ) & 0x02 ) == 0x02 ) printformat(" (hid)");
            if((( *((unsigned char*)(DMA_BUFFER + i*32 + 11)) ) & 0x04 ) == 0x04 ) printformat(" (sys)");
            if((( *((unsigned char*)(DMA_BUFFER + i*32 + 11)) ) & 0x20 ) == 0x20 ) printformat(" (arc)");
            printformat("\n");
        }
    }
    printformat("\n");
    /// //////////////////////// TEST FLOPPY DRIVER READ DIRECTORY


Zunächst lesen wir ab Sektor 19 (0x2600) die Daten in den DMA-Puffer ein. Anschließend untersuchen wir die einzelnen Einträge auf Gültigkeit.
Beginnt ein Name mit 0x00 (frei ab hier), 0xE5 (gelöscht, also frei) oder 0x0F (Teil eines langen Dateinamens), so überspringen wir diesen Eintrag.
Nun lesen wir die ersten acht Zeichen, also den eigentlichen Namen, ein. Das Leerzeichen drucken wir - wie üblich - nicht.
Als Separator zwischen Namen und Erweiterung setzen wir den von MS DOS gewöhnten Punkt. Man ist hier aber frei.
Das gleiche Auslese-Prozedere führen wir für die Erweiterung durch. Die Dateigröße finden wir ab Position 28 und die Attribute ab Position 11.

Vergleichen wir unseren Ausdruck mit dem des MS Windows Explorers (Anzeige der Attribute eingeschaltet):







Projektgruppe OS-Development


Motiviert Sie diese Tutorial-Serie, ein eigenes OS zu erstellen oder bei einem OS-Projekt basierend auf PrettyOS aktiv oder unterstützend mitzuwirken?
Wir laden Sie herzlich ein!



Aktiv und konstruktiv mit machen ist sicher der beste Weg für einen Einstieg. Die Hintergründe werde ich in einem vierten Teil näher erklären.

weiter zu:     Teil 4

zurück zu:    Teil 1    Teil 2