1 Introduction
2 Ground Rules

Building a File System
3 File Systems
4 File Content Data Structure
5 Allocation Cluster Manager
6 Exceptions and Emancipation
7 Base Classes, Testing, and More
8 File Meta Data
9 Native File Class
10 Our File System
11 Allocation Table
12 File System Support Code
13 Initializing the File System
14 Contiguous Files
15 Rebuilding the File System
16 Native File System Support Methods
17 Lookups, Wildcards, and Unicode, Oh My
18 Finishing the File System Class

The Init Program
19 Hardware Abstraction and UOS Architecture
20 Init Command Mode
21 Using Our File System
22 Hardware and Device Lists
23 Fun with Stores: Partitions
24 Fun with Stores: RAID
25 Fun with Stores: RAM Disks
26 Init wrap-up

The Executive
27 Overview of The Executive
28 Starting the Kernel
29 The Kernel
30 Making a Store Bootable
31 The MMC
32 The HMC
33 Loading the components
34 Using the File Processor
35 Symbols and the SSC
36 The File Processor and Device Management
37 The File Processor and File System Management
38 Finishing Executive Startup

Users and Security
39 Introduction to Users and Security
40 More Fun With Stores: File Heaps
41 File Heaps, part 2
42 SysUAF
43 TUser
44 SysUAF API

UCL
45 Shells and UCL

Glossary/Index


Download sources
Download binaries

Fun with Stores: RAM Disks

I had hoped to wrap up the Init with this article, but the article ended up being too long. So, we will finish Init in the next article.

Non-sensical uses of stores
One of the consequences of allowing any store to be a member of a mirror set, is that the user can set up situations that don't really make much sense. For instance, one could mirror a RAM disk with a hard disk to provide faster read access to the disk. However, as we will see in the future, store caching is a much more efficient means of speeding up disk access. Or perhaps the user is mirroring two partitions on the same store. In that case, there is no performance improvement on read and a definite performance loss on writes (and in general due to extra seek operations). Further, if the partitions are next to each other and a spot on the disk goes bad at the boundary between the two, then both members of the mirror set could be corrupted at the same time (the start of one and the end of the other). But an operating system's job is to provide a general, abstract set of tools that the user can make use of in whatever way they want. The O/S should only get in the way if the user wants to do something that is prohibited from an authorization standpoint or which is logically (or physically) impossible. Certainly, there isn't any given configuration of mirror sets that would corrupt data. So we will allow it even if it doesn't provide a benefit. Who knows? Maybe there is some unique situation where one of these odd circumstances would provide the user with a benefit. It isn't our job to make that call for them. However, that doesn't mean that UOS can't provide the user with warnings or hints when they do something that seems silly. Init, however, doesn't.

Some would argue that the very use of RAM disks makes little sense. One use for a RAM disk is to provide faster disk access. However, any decent caching mechanism obviates that need. In fact, a RAM disk can make performance worse since the memory that would have been used for caching the data which is actually being used is, instead, occupied with data that may not be used very much. What if we only had files on the RAM disk that we wanted fast access to whenever they are required, even if they are not in the cache? That can be handled by resident images in UOS. In fact, there is only one circumstance where I can imagine a use for a RAM Disk: on diskless systems. Being able to do work on a RAM disk would be faster than using network storage. This scenario would require that the the network storage would be regularly updated from the RAM, but perhaps not until a given amount of work is done (ie if transaction-oriented).

RAM Disks
Note that RAM disks may also be ROM disks, meaning that an image of a disk can exist within read-only memory. This is used when booting UOS on embedded controllers that have no disks. Whether they are read-only or not, we call all memory disks "RAM disks", even though that may not be technically correct. Why would an embedded system want to boot from ROM? Because it may be that the applications it runs do not need to write any data (or only write it to some logging device). The UOS configuration, and the applications it runs, may be fixed in ROM both for security (you can't introduce a virus into ROM) and simplicity of hardware.
Here is the class definition, which largely matches our previous store class definitions.

type TRAM_Disk64 = class( TCOM_Store64 )
                       public // Constructors and destructors...
                           destructor Destroy ; override ;

                       private // Instance data...
                           _Buffer : PChar ;
                           _Cache : TCOM_Cache64 ;
                           _Minimum : longint ;
                           _Size : longint ;
                           _Bytes_Read : longint ;
                           _Bytes_Written : longint ;
                           _Reads : longint ;
                           _Writes : longint ;
                           _Error_Count : longint ;
                           _Foreign_Buffer : boolean ;

                       public // API...
                           function Is_Class( Name : PChar ) : boolean ;
                               override ;

                           function Read_Data( var Data ; Address, _Size : TStore_Address64 ;
                               var UEC : TUnified_Exception ) : TStore_Address64 ;
                               override ;

                           function Write_Data( var Data ; Address, _Size : TStore_Address64 ;
                               var UEC : TUnified_Exception ) : TStore_Address64 ;
                               override ;

                           function Max_Storage : TStore_Size64 ; override ;

                           function Min_Storage : TStore_Address64 ; override ;

                           function Extend( Amount : TStore_Address64 ) : TStore_Address64 ;
                               override ;

                           function Get_Read_Only : boolean ; override ;

                           function Get_Write_Only : boolean ; override ;

                           procedure Format ; override ;

                           function Get_Name : PChar ; override ;

                           function Get_Cache : TCOM_Cache64 ; override ;

                           procedure Set_Cache( Value : TCOM_Cache64 ) ;
                               override ;

                           function Contiguous_Store : boolean ; override ;

                           procedure Set_Max_Storage( Value : TStore_Address64 ;
                               var Res : TUnified_Exception ) ;
                               override ;

                           function Extended_Size : TStore_Address64 ; override ;
                           procedure Set_Read_Only( Value : boolean ) ;
                               override ;
                           procedure Set_Write_Only( Value : boolean ) ;
                               override ;

                           function Get_Bytes_Read : longint ; override ;
                           function Get_Bytes_Written : longint ; override ;
                           function Get_Reads : longint ; override ;
                           function Get_Writes : longint ; override ;
                           function Get_Error_Count : longint ; override ;
                           procedure Set_Bytes_Read( Value : longint ) ;
                               override ;
                           procedure Set_Bytes_Written( Value : longint ) ;
                               override ;
                           procedure Set_Reads( Value : longint ) ; override ;
                           procedure Set_Writes( Value : longint ) ; override ;
                           procedure Set_Error_Count( Value : longint ) ;
                               override ;

                       public // API...
                           procedure Set_Buffer( P : pointer ;
                               Value : TStore_Address64 ;
                               var Res : TUnified_Exception ) ;
                   end ; // TRAM_Disk
The only differences are Buffer, which is the memory buffer for the store; Size, which indicates the length of the buffer (the maximum store size); and _Foreign_Buffer, which is false to indicate we allocated the buffer within the class or true if the buffer was passed to the class.

Here are the methods which are (nearly) identical to previous store classes:

// Constructors and destructors...

destructor TRAM_Disk64.Destroy ;

begin
    if( not _Foreign_Buffer ) then
    begin
        freemem( _Buffer ) ; // Only free if we allocated it
    end ;
    _Buffer := nil ;

    inherited Destroy ;
end ;


// API...

function TRAM_Disk64.Is_Class( Name : PChar ) : boolean ;

var S : string ;

begin
    S := Edit( string( Name ), 512 ) ;
    Result := ( S = 'tram_disk64' ) ;
    if( not Result ) then
    begin
        Result := inherited Is_Class( Name ) ;
    end ;
end ;


function TRAM_Disk64.Extend( Amount : TStore_Address64 ) : TStore_Address64 ;

begin
    Result := Max_Storage ;
end ;


function TRAM_Disk64.Get_Read_Only : boolean ;

begin
    Result := False ;
end ;


function TRAM_Disk64.Get_Write_Only : boolean ;

begin
    Result := False ;
end ;


procedure TRAM_Disk64.Format ;

begin
    // Nothing to do
end ;


function TRAM_Disk64.Get_Name : PChar ;

begin
    Result := nil ;
end ;


function TRAM_Disk64.Get_Cache : TCOM_Cache64 ;

begin
    Result := _Cache ;
end ;


procedure TRAM_Disk64.Set_Cache( Value : TCOM_Cache64 ) ;

begin
    if( Value <> nil ) then
    begin
        Value.Attach ;
    end ;
    if( _Cache <> nil ) then
    begin
        _Cache.Detach ;
    end ;
    _Cache := Value ;
end ;


function TRAM_Disk64.Contiguous_Store : boolean ;

begin
    Result := True ;
end ;


function TRAM_Disk64.Extended_Size : TStore_Address64 ;

begin
    Result := Max_Storage ;
end ;


procedure TRAM_Disk64.Set_Read_Only( Value : boolean ) ;

begin
    // Do nothing
end ;


procedure TRAM_Disk64.Set_Write_Only( Value : boolean ) ;

begin
    // Do nothing
end ;


function TRAM_Disk64.Get_Bytes_Read : longint ;

begin
    Result := _Bytes_Read ;
end ;


function TRAM_Disk64.Get_Bytes_Written : longint ;

begin
    Result := _Bytes_Written ;
end ;


function TRAM_Disk64.Get_Reads : longint ;

begin
    Result := _Reads ;
end ;


function TRAM_Disk64.Get_Writes : longint ;

begin
    Result := _Writes ;
end ;


function TRAM_Disk64.Get_Error_Count : longint ;

begin
    Result := _Error_Count ;
end ;


procedure TRAM_Disk64.Set_Bytes_Read( Value : longint ) ;

begin
    _Bytes_Read := Value ;
end ;


procedure TRAM_Disk64.Set_Bytes_Written( Value : longint ) ;

begin
    _Bytes_Written := Value ;
end ;


procedure TRAM_Disk64.Set_Reads( Value : longint ) ;

begin
    _Reads := Value ;
end ;


procedure TRAM_Disk64.Set_Writes( Value : longint ) ;

begin
    _Writes := Value ;
end ;


procedure TRAM_Disk64.Set_Error_Count( Value : longint ) ;

begin
    _Error_Count := Value ;
end ;


function TRAM_Disk64.Get_Buffer : pointer ;

begin
    Result := _Buffer ;
end ;

Here are the overrides for the RAM disk:

procedure TRAM_Disk64.Set_Max_Storage( Value : TStore_Address64 ;
    var Res : TUnified_Exception ) ;

var P : Pointer ;

begin
    if( _Minimum = 0 ) then
    begin
        _Minimum := 16 ;
    end ;
    Value := Round_Up( Value, _Minimum ) ;
    P := _Buffer ;
    if( _Foreign_Buffer ) then
    begin
        P := allocmem( Value ) ;
        _Foreign_Buffer := False ;
    end else
    begin
        Reallocmem( P, Value ) ;
    end ;
    if( P = nil ) then
    begin
        Res := Create_Exception( RD_Error_Creation_Failure, nil ) ; { Untested }
    end else
    begin
        _Buffer := P ;
        _Size := Value ;
        Res := nil ;
    end ;
    Set_Last_Error( Res ) ;
end ; // TRAM_Disk64.Set_Max_Storage
When the max storage size is set, we first make sure that the minimum size is defaulted to 16 (if it is 0, then it was never set by the code that created the class). Then we make sure the size is an integral number of min storage units. If we are using a foreign buffer, we cannot reallocate it (since we do not "own" it), so we allocate a new one and reset the flag. otherwise, we reallocate our internal buffer. We then report any failure, and otherwise set the (max) size of the store. Note that this code is not used by Init when creating RAM disks. That is done via the Set_Buffer method.

procedure TRAM_Disk64.Set_Buffer( P : pointer ; Value : TStore_Address64 ;
    var Res : TUnified_Exception ) ; { Untested }

begin
    if( P = nil ) then
    begin
        Res := Create_Exception( RD_Invalid_Pointer, nil ) ;
    end else
    begin
        if( ( _Buffer <> nil ) and ( not _Foreign_Buffer ) ) then
        begin
            freemem( _Buffer ) ;
        end ;
        _Buffer := P ;
        _Size := Value ;
        Res := nil ;
        _Foreign_Buffer := True ;
    end ;
    Set_Last_Error( Res ) ;
end ;
Init calls this method when it has been assigned a chunk of memory by the HAL. If we use the Set_Max_Storage method instead, then the drive data will be allocated in the heap. First, that uses up valuable system heap, but second, we cannot be guaranteed that the heap used by Init will even be available once UOS starts. This method ensures that the RAM disk points to an independent memory buffer. Passing Nil is an error. Otherwise, we free any existing non-foreign buffer, then assign the buffer and set the size. Finally, we set the foreign flag so that the rest of the class doesn't try to free or reallocate the passed buffer - we can't be sure that the passed buffer is part of the heap, so trying to use the heap to manipulate it could be fatal. In fact, although this code doesn't know it, Init is passing a buffer that isn't part of the heap.

function TRAM_Disk64.Read_Data( var Data ; Address, _Size : TStore_Address64 ;
    var UEC : TUnified_Exception ) : TStore_Address64 ;

begin
    Result := 0 ;
    if( _Size <= 0 ) then
    begin
        exit ;
    end ;
    Address := Round_Up( Address, _Minimum ) ;
    _Size := Round_Up( _Size, _Minimum ) ;
    if( ( Address < 0 ) or ( Address + _Size > Max_Storage ) ) then
    begin
        UEC := Create_Exception( RD_Error_Address_Out_Of_Range, nil ) ;
    end else
    begin
        UEC := nil ;
        move( _Buffer[ Address ], Data, _Size ) ;
    end ;
    Set_Last_Error( UEC ) ;
    if( UEC = nil ) then
    begin
        Result := _Size ;
        inc( _Reads ) ;
        _Bytes_Read := _Bytes_Read + _Size
    end ;
end ; // TRAM_Disk64.Read_Data
The approach for the Read_Data method is simply a matter of copying from the memory buffer into the passed buffer. We make sure that the address and size map entire clusters, then validate that we aren't out of the range of the buffer, and then do the copy. If there was no error, we also update the I/O counts.

function TRAM_Disk64.Write_Data( var Data ; Address, _Size : TStore_Address64 ;
    var UEC : TUnified_Exception ) : TStore_Address64 ;

begin
    Result := 0 ;
    if( _Size <= 0 ) then
    begin
        exit ;
    end ;
    Address := Round_Up( Address, _Minimum ) ;
    _Size := Round_Up( _Size, _Minimum ) ;
    if( ( Address < 0 ) or ( Address + _Size > Max_Storage ) ) then
    begin
        UEC := Create_Exception( RD_Error_Address_Out_Of_Range, nil ) ;
    end else
    begin
        UEC := nil ;
        move( Data, _Buffer[ Address ], _Size ) ;
    end ;
    Set_Last_Error( UEC ) ;
    if( UEC = nil ) then
    begin
        Result := _Size ;
        inc( _Writes ) ;
        _Bytes_Written := _Bytes_Written + _Size
    end ;
end ; // TRAM_Disk64.Write_Data
The Write_Data function is almost exactly like the Read_Data except that the method copies from the passed buffer to the memory buffer, instead of the other way around.

Now let's take a look at the Init code that supports RAM disks. This involves two sub-commands under the DISK command: CREATE and DELETE, which (respectively) create a new RAM disk and delete an existing one. Like RAID sets, RAM disks are a software construct and the HAL knows nothing about them. Thus, we keep track of them in Init via a list (RAM_Disks). We use controller 24 (the "Y" controller) for all dynamic disks, other than RAID sets. Note that dynamic disks are those not associated with a specific controller. This includes not only RAM disks, but also network attached storage. We will talk about network storage in the future. For now, Init doesn't support network storage. However, we will add it in the future to support diskless devices.

var RAM_Disks : TList = nil ;


function Disk_Create : boolean ;

var Address : pointer ;
    ClusterSize : int64 ;
    E : TUnified_Exception ;
    S, S1 : string ;
    Size : int64 ;
    Store : TRAM_Disk64 ;

label Ask_Size, Ask_Clustersize ;

begin
    Result := False ;
Ask_Size:
    Output( 'RAM disk size <1 Mb> ' ) ;
    case( Process_Input_Prompt( '1Mb', S, S1, True ) ) of
        1 : begin
                Result := True ;
                exit ;
            end ;
        2 : begin
                exit ;
            end ;
    end ;
    Size := Get_Value( S ) ;
    if( Size <= 0 ) then
    begin
        Output_Line( 'Invalid size' ) ;
        goto Ask_Size ;
    end ;
    if( Size < 4096 ) then
    begin
        Output_Line( 'Minimum permitted RAM disk size is 4,096' ) ;
        goto Ask_Size ;
    end ;

Ask_Clustersize:
    Output( 'RAM disk cluster size <256> ' ) ;
    case( Process_Input_Prompt( '256', S, S1, True ) ) of
        1 : begin
                Result := True ;
                exit ;
            end ;
        2 : begin
                goto Ask_Size ;
            end ;
    end ;
    ClusterSize := Get_Value( S ) ;
    if( ( ClusterSize <= 0 ) or ( ClusterSize > Size ) ) then
    begin
        Output_Line( 'Invalid size' ) ;
        goto Ask_Size ;
    end ;
    if( ClusterSize < 32 ) then
    begin
        Output_Line( 'Minimum permitted clustersize is 32' ) ;
        goto Ask_Size ;
    end ;
    Size := Round_Up( Size, Clustersize ) ; // Make sure last cluster is full

    // Allocate the RAM for the disk...
    Address := HAL.Allocate_RAM( Size ) ;
    if( Address = nil ) then
    begin
        Output_Line( 'Not enough RAM' ) ;
        goto Ask_Clustersize ;
    end ;
    if( Clustersize < 256 ) then
    begin
        Output_Line( 'Warning: clustersize is too small for a UOS file system' ) ;
    end ;

    if( RAM_Disks = nil ) then
    begin
        RAM_Disks := TList.Create ;
    end ;
    Store := TRAM_Disk64.Create ;
    Store.Set_Min_Storage( ClusterSize ) ;
    Store.Set_Buffer( Address, Size, E ) ;
    if( Show_Error( E ) ) then
    begin
        goto Ask_Clustersize ;
    end ;
    RAM_Disks.Add( Store ) ;
end ; // Disk_Create
Init calls this function to create a new RAM disk. We prompt the user for the size and the cluster size. Since we cannot create a file system on a disk that is too small, we will not allow the creation of a RAM disk less than 4K in size. Further, we will not allow a minimum cluster size of less than 32. But we default to 256, since that is the smallest minimum clustersize supported by our file system. This is an inherent limit based on the way it was coded. We could change this, but it really doesn't make a lot of sense to have stores with smaller minimums, due to the overhead of the allocation table. We will allow cluster sizes between 32 and 255, but we will warn the user that they won't be able to put a UOS file system on it.
We create the RAM_Disks list, if it hasn't been already created. We request a RAM allocation from the HAL for the requested size. If that fails, there isn't enough contiguous memory available for the request. Then we create the RAM Disk, passing the buffer, size, and clustersize to it. Finally, we add the new store to the RAM_Disks list.

You may have noticed the Process_Input_Prompt function. We replaced much of the input processing throughout Init with this function in order to make the code smaller and ensure that the operation was more consistent. Here is that, and a related, function:

// Process input and return parsed/formatted string in S, S1, and the result:
// 0 = Normal
// 1 = Cancel
// 2 = Backup
// Single is true if no additional parameters are expected, false to parse S, S1
// S is formatted to trimmed, with no control codes
// Upper is true to format S to uppercase as well
function Process_Input( var S, S1 : string ; Single : boolean = False ;
    Upper : boolean = true ) : integer ;

var Dummy : integer ;

begin
    Result := 0 ;
    if( S = #3 ) then
    begin
        Result := 1 ;
        exit ;
    end ;
    if( ( S = ESC ) or ( S = #26 ) ) then
    begin
        Result := 2 ;
        exit ;
    end ;
    S := Edit( S, 4 or 8 or 128 ) ;
    if( Upper ) then
    begin
        S := Edit( S, 32 ) ;
    end ;
    if( not Single ) then
    begin
        Dummy := pos( ' ', S + ' ' ) ;
        S1 := copy( S, Dummy + 1, length( S ) ) ;
        S := copy( S, 1, Dummy - 1 ) ;
    end ;
end ;


function Process_Input_Prompt( Default : string ; var S, S1 : string ;
    Single : boolean = False ; Upper : boolean = true ) : integer ;

begin
    S := Input( Default ) ;
    Result := Process_Input( S, S1, Single, Upper ) ;
end ;
The first function gets, formats, and parses the input. The second function does the same, but allows for a default. The code is essentially what we've looked at before, just with a little bit of generalization. Nearly all input is now processed via these functions.

function Disk_Delete( S : string ) : boolean ;

var S1 : string ;
    Prompt : boolean ;
    Store : TRAM_Disk64 ;

begin
    Result := False ;
    Prompt := S = '' ;
    while( True ) do
    begin
        if( Prompt ) then
        begin
            Output( 'Disk to delete <DISKY0> : ' ) ;
            S := Input( 'DISKY0' ) ;
        end ;
        case( Process_Input( S, S1 ) ) of
            1 : begin
                    Result := True ;
                    exit ;
                end ;
            2 : begin
                    exit ;
                end ;
        end ;
        if( S <> '' ) then
        begin
            if( copy( S, 1, 5 ) = 'DISKY' ) then
            begin
                Store := TRAM_Disk64( Parse_Device( S ) ) ;
                if( Store = nil ) then
                begin
                    Output_Line( 'Invalid device' ) ;
                end else
                begin
                    RAM_Disks.Remove( Store ) ;
                    HAL.Deallocate( Store.Get_Buffer, Store.Max_Storage ) ;
		    Store.Free ;
                end ;
            end else
            begin
                Output_Line( 'Not a RAM disk' ) ;
            end ;
        end ;
        if( not Prompt ) then
        begin
            exit ;
        end ;
    end ; // while( True )
end ; // Disk_Delete
This function is called by Init to delete an existing RAM disk. If the store isn't passed to us, we prompt the user for it. Either way, we make sure that it is a RAM disk. Then we get the store and display an error if the device is invalid. Otherwise, we remove the store from RAM_Disks, tell the HAL to deallocate the previously allocated buffer for the RAM disk, and finally destroy the instance.

We have to rewrite the wrappers for the HAL Device and Store methods to account for dynamic disks. Here are the new versions of these functions:

function HAL_Store( Index : integer ) : TCOM_Store64 ;

var I : integer ;
    Last_HAL_Device_Index : integer ;

begin
    if( First_Time ) then
    begin
        First_Time := False ;
        Locate_RAID ;
    end ;
    if( RAM_Disks = nil ) then
    begin
        RAM_Disks := TList.Create ;
    end ;
    Result := HAL.Store( Index ) ;
    if( Result <> nil ) then
    begin
        exit ;
    end ;

    I := 0 ;
    while( I <= Index ) do
    begin
        Result := HAL.Store( I ) ;
        if( Result = nil ) then
        begin
            if( I - Last_HAL_Device_Index - RAM_Disks.Count > MStores.Count ) then
            begin
                exit ;
            end ;
            if( I = Index ) then
            begin
                if( I - Last_HAL_Device_Index <= RAM_Disks.Count ) then
                begin
                    Result := TRAM_Disk64( RAM_Disks[ I - Last_HAL_Device_Index - 1 ] ) ;
                    exit ;
                end ;
                Result := MStores.Get( I - Last_HAL_Device_Index - RAM_Disks.Count - 1 ) ;
                exit ;
            end ;
        end else
        begin
            Last_HAL_Device_Index := I ;
        end ;
        inc( I ) ;
    end ; // while( I <= Index )
end ; // HAL_Store


function HAL_Device( Index : integer ) : TDevice_Info ;

var I : integer ;
    Last_HAL_Device_Index : integer ;

begin
    if( First_Time ) then
    begin
        First_Time := False ;
        Locate_RAID ;
    end ;
    if( RAM_Disks = nil ) then
    begin
        RAM_Disks := TList.Create ;
    end ;
    Result := HAL.Device( Index ) ;
    if( Result.Device_Type <> DT_Non_Existant ) then
    begin
        if( Disabled_Devices <> nil ) then
        begin
            if( Disabled_Devices.Indexof( Device_Name( Result ) ) <> -1 ) then
            begin
                Result.Disabled := True ;
            end ;
        end ;
        exit ;
    end ;

    I := 0 ;
    while( I <= Index ) do
    begin
        Result := HAL.Device( I ) ;
        if( Result.Device_Type = DT_Non_Existant ) then
        begin
            if( I - Last_HAL_Device_Index > MStores.Count + RAM_Disks.Count ) then
            begin
                exit ;
            end ;
            if( I = Index ) then
            begin
                if( I - Last_HAL_Device_Index <= RAM_Disks.Count ) then
                begin
                    Result.Device_Type := DT_Store ;
                    Result.Controller := 24 ;
                    Result.Device_Unit := I - Last_HAL_Device_Index - 1 ;
                    Result.Media_Present := True ;
                    exit ;
                end ;
                Result.Device_Type := DT_Store ;
                Result.Controller := 25 ;
                Result.Device_Unit := I - Last_HAL_Device_Index - RAM_Disks.Count - 1 ;
                Result.Media_Present := MStores.Count <> 0 ;
                exit ;
            end ;
        end else
        begin
            Last_HAL_Device_Index := I ;
        end ;
        inc( I ) ;
    end ; // while( I <= Index )
end ; // HAL_Device

One last Init change is made to the memory list, which will show the parts of memory allocated to RAM disks. Again, the HAL doesn't know anything about these, so Init has to expand upon the data returned by the HAL.

function Hardware_Memory_List( S : string ) : boolean ;

    function Build_Range( L, H : int64 ) : string ;

    var S : string ;

    begin
        S := '' ;
        if( HAL.Radix <> 10 ) then
        begin
            S := CVTB( 10, HAL.Radix, inttostr( H ) ) ;
            while( length( S ) < 16 ) do
            begin
                S := '0' + S ;
            end ;
            S := CVTB( 10, HAL.Radix, inttostr( L ) ) + '-' + S ;
            while( length( S ) < 33 ) do
            begin
                S := '0' + S ;
            end ;
            S := ' (' + S + ')' ;
        end ; // if( HAL.Radix <> 10 )

        Result := inttostr( H ) ;
        while( length( Result ) < 16 ) do
        begin
            Result := ' ' + Result ;
        end ;
        Result := inttostr( L ) + '-' + Result ;
        while( length( Result ) < 33 ) do
        begin
            Result := ' ' + Result ;
        end ;
        Result := Result + S ;
    end ; // Hardware_Memory_List.Build_Range


    procedure Show_Range( L, H : int64 ; Memory_Type : word ) ;

    var S : string ;
        Size : int64 ;

        procedure _Show_Sub_Range( L, H : int64 ; Memory_Type : word ) ;

        begin
            S := Build_Range( L, H ) ;
            case Memory_Type of
                MT_RAM : Output( 'RAM  ' ) ;
                MT_ROM : Output( 'ROM  ' ) ;
                MT_WOM : Output( 'WOM  ' ) ;
                $FFFF : Output( 'DSK  ' ) ;
            end ; // case Info.Device_Type
            Output( S ) ;
            Size := H - L + 1 ;
            S := Abbreviate_Size( Size ) ;
            Output_Line( '   ' + S ) ;
        end ; // Hardware_Memory_List.Show_Range._Show_Sub_Range

    var I : integer ;
        Store : TRAM_Disk64 ;
        Store_Low : int64 ;

    begin
        if( ( RAM_Disks <> nil ) and ( RAM_Disks.Count > 0 ) ) then
        begin
            while( L < H ) do
            begin
                // Find lowest RAM disk address that is >= L...
                Store_Low := $7FFFFFFFFFFFFFFF ;
                for I := 0 to RAM_Disks.Count - 1 do
                begin
                    Store := TRAM_Disk64( RAM_Disks[ I ] ) ;
                    if(
                        ( cardinal( Store.Get_Buffer ) < Store_Low )
                        and
                        ( L <= cardinal( Store.Get_Buffer ) )
                      ) then
                    begin
                        Store_Low := cardinal( Store.Get_Buffer ) ;
                    end ;
                end ; // for I := 0 to RAM_Disks.Count - 1

                if( Store_Low > H ) then // All stores are before our Low address
                begin
                    _Show_Sub_Range( L, H, Memory_Type ) ;
                    break ;
                end ;
                if( L < cardinal( Store_Low ) ) then // Space before store
                begin
                    _Show_Sub_Range( L, cardinal( Store_Low ) - 1, Memory_Type ) ;
                    L := cardinal( Store_Low ) ;
                end else
                if( L > cardinal( Store_Low ) ) then // After last store
                begin
                    _Show_Sub_Range( L, H, Memory_Type ) ;
                    break ;
                end else
                begin
                    // Show store...
                    _Show_Sub_Range( L, L + Store.Max_Storage - 1, $FFFF ) ;
                    L := L + Store.Max_Storage ;
                end ;
            end ; // while( L < H )
        end else
        begin
            _Show_Sub_Range( L, H, Memory_Type ) ;
        end ;
    end ; // Hardware_Memory_List.Show_Range


var Index : integer ;
    Info : TMemory_Info ;
    Last : int64 ;

begin // Hardware_Memory_List
    Result := False ;
    Last := 0 ;
    Index := 0 ;
    Info := HAL.Memory( Index ) ;
    while( Info.Memory_Type <> MT_Non_Existant ) do
    begin
        if( Info.Low > Last ) then
        begin
            S := Build_Range( Last, Info.Low - 1 ) ;
            Output_Line( 'NXT  ' + S ) ;
        end ;
        Show_Range( Info.Low, Info.High, Info.Memory_Type ) ;
        Last := Info.High + 1 ;
        inc( Index ) ;
        Info := HAL.Memory( Index ) ;
    end ; // while( Info.Device_Type <> DT_Non_Existant )
end ; // Hardware_Memory_List
The main body of the function is similar to before, except that part of the loop is extracted to the Show_Range local function, which loops through the RAM disks to see if the current memory range overlaps a RAM disk. if so, we split the range into multiple parts. Then for each sub-range, we show the line of information.

That's it for RAM disks. In the next article, we will finally wrap up the Init.