xref: /reactos/subsystems/mvdm/ntvdm/hardware/disk.c (revision 86eebc2a)
1 /*
2  * COPYRIGHT:       GPL - See COPYING in the top level directory
3  * PROJECT:         ReactOS Virtual DOS Machine
4  * FILE:            subsystems/mvdm/ntvdm/hardware/disk.c
5  * PURPOSE:         Generic Disk Controller (Floppy, Hard Disk, ...)
6  * PROGRAMMERS:     Hermes Belusca-Maito (hermes.belusca@sfr.fr)
7  *
8  * NOTE 1: This file is meant to be splitted into FDC and HDC
9  *         when its code will grow out of control!
10  *
11  * NOTE 2: This is poor-man implementation, i.e. at the moment this file
12  *         contains an API for manipulating the disks for the rest of NTVDM,
13  *         but does not implement real hardware emulation (IO ports, etc...).
14  *         One will have to progressively transform it into a real HW emulation
15  *         and, in case the disk APIs are needed, move them elsewhere.
16  *
17  * FIXME:  The big endian support (which is hardcoded here for machines
18  *         in little endian) *MUST* be fixed!
19  */
20 
21 /* INCLUDES *******************************************************************/
22 
23 #include "ntvdm.h"
24 
25 #define NDEBUG
26 #include <debug.h>
27 
28 #include "emulator.h"
29 #include "disk.h"
30 
31 // #include "io.h"
32 #include "memory.h"
33 
34 #include "utils.h"
35 
36 
37 /**************** HARD DRIVES -- VHD FIXED DISK FORMAT SUPPORT ****************/
38 
39 // http://citrixblogger.org/2008/12/01/dynamic-vhd-walkthrough/
40 // http://www.microsoft.com/en-us/download/details.aspx?id=23850
41 // https://projects.honeynet.org/svn/sebek/virtualization/qebek/trunk/block/vpc.c
42 // https://git.virtualopensystems.com/trescca/qemu/raw/40645c7bfd7c4d45381927e1e80081fa827c368a/block/vpc.c
43 // https://gitweb.gentoo.org/proj/qemu-kvm.git/tree/block/vpc.c?h=qemu-kvm-0.12.4-gentoo&id=827dccd6740639c64732418539bf17e6e4c99d77
44 
45 #pragma pack(push, 1)
46 
47 enum VHD_TYPE
48 {
49     VHD_FIXED           = 2,
50     VHD_DYNAMIC         = 3,
51     VHD_DIFFERENCING    = 4,
52 };
53 
54 // Seconds since Jan 1, 2000 0:00:00 (UTC)
55 #define VHD_TIMESTAMP_BASE 946684800
56 
57 // Always in BIG-endian format!
58 typedef struct _VHD_FOOTER
59 {
60     CHAR        creator[8]; // "conectix"
61     ULONG       features;
62     ULONG       version;
63 
64     // Offset of next header structure, 0xFFFFFFFF if none
65     ULONG64     data_offset;
66 
67     // Seconds since Jan 1, 2000 0:00:00 (UTC)
68     ULONG       timestamp;
69 
70     CHAR        creator_app[4]; // "vpc "; "win"
71     USHORT      major;
72     USHORT      minor;
73     CHAR        creator_os[4]; // "Wi2k"
74 
75     ULONG64     orig_size;
76     ULONG64     size;
77 
78     USHORT      cyls;
79     BYTE        heads;
80     BYTE        secs_per_cyl;
81 
82     ULONG       type;   // VHD_TYPE
83 
84     // Checksum of the Hard Disk Footer ("one's complement of the sum of all
85     // the bytes in the footer without the checksum field")
86     ULONG       checksum;
87 
88     // UUID used to identify a parent hard disk (backing file)
89     BYTE        uuid[16];
90 
91     BYTE        in_saved_state;
92 
93     BYTE Padding[0x200-0x55];
94 
95 } VHD_FOOTER, *PVHD_FOOTER;
96 C_ASSERT(sizeof(VHD_FOOTER) == 0x200);
97 
98 #pragma pack(pop)
99 
100 #if 0
101 /*
102  * Calculates the number of cylinders, heads and sectors per cylinder
103  * based on a given number of sectors. This is the algorithm described
104  * in the VHD specification.
105  *
106  * Note that the geometry doesn't always exactly match total_sectors but
107  * may round it down.
108  *
109  * Returns TRUE on success, FALSE if the size is larger than 127 GB
110  */
111 static BOOLEAN
112 calculate_geometry(ULONG64 total_sectors, PUSHORT cyls,
113                    PBYTE heads, PBYTE secs_per_cyl)
114 {
115     ULONG cyls_times_heads;
116 
117     if (total_sectors > 65535 * 16 * 255)
118         return FALSE;
119 
120     if (total_sectors > 65535 * 16 * 63)
121     {
122         *secs_per_cyl = 255;
123         *heads = 16;
124         cyls_times_heads = total_sectors / *secs_per_cyl;
125     }
126     else
127     {
128         *secs_per_cyl = 17;
129         cyls_times_heads = total_sectors / *secs_per_cyl;
130         *heads = (cyls_times_heads + 1023) / 1024;
131 
132         if (*heads < 4)
133             *heads = 4;
134 
135         if (cyls_times_heads >= (*heads * 1024) || *heads > 16)
136         {
137             *secs_per_cyl = 31;
138             *heads = 16;
139             cyls_times_heads = total_sectors / *secs_per_cyl;
140         }
141 
142         if (cyls_times_heads >= (*heads * 1024))
143         {
144             *secs_per_cyl = 63;
145             *heads = 16;
146             cyls_times_heads = total_sectors / *secs_per_cyl;
147         }
148     }
149 
150     *cyls = cyls_times_heads / *heads;
151 
152     return TRUE;
153 }
154 #endif
155 
156 
157 
158 /*************************** FLOPPY DISK CONTROLLER ***************************/
159 
160 // A Floppy Controller can support up to 4 floppy drives.
161 static DISK_IMAGE XDCFloppyDrive[4];
162 
163 // Taken from DOSBox
164 typedef struct _DISK_GEO
165 {
166     DWORD ksize;     /* Size in kilobytes */
167     WORD  secttrack; /* Sectors per track */
168     WORD  headscyl;  /* Heads per cylinder */
169     WORD  cylcount;  /* Cylinders per side */
170     WORD  biosval;   /* Type to return from BIOS & CMOS */
171 } DISK_GEO, *PDISK_GEO;
172 
173 // FIXME: At the moment, all of our diskettes have 512 bytes per sector...
174 static WORD HackSectorSize = 512;
175 static DISK_GEO DiskGeometryList[] =
176 {
177     { 160,  8, 1, 40, 0},
178     { 180,  9, 1, 40, 0},
179     { 200, 10, 1, 40, 0},
180     { 320,  8, 2, 40, 1},
181     { 360,  9, 2, 40, 1},
182     { 400, 10, 2, 40, 1},
183     { 720,  9, 2, 80, 3},
184     {1200, 15, 2, 80, 2},
185     {1440, 18, 2, 80, 4},
186     {2880, 36, 2, 80, 6},
187 };
188 
189 static BOOLEAN
MountFDI(IN PDISK_IMAGE DiskImage,IN HANDLE hFile)190 MountFDI(IN PDISK_IMAGE DiskImage,
191          IN HANDLE hFile)
192 {
193     ULONG  FileSize;
194     USHORT i;
195 
196     /*
197      * Retrieve the size of the file. In NTVDM we will handle files
198      * of maximum 1Mb so we can largely use GetFileSize only.
199      */
200     FileSize = GetFileSize(hFile, NULL);
201     if (FileSize == INVALID_FILE_SIZE && GetLastError() != ERROR_SUCCESS)
202     {
203         /* We failed, bail out */
204         DisplayMessage(L"MountFDI: Error when retrieving file size, or size too large (%d).", FileSize);
205         return FALSE;
206     }
207 
208     /* Convert the size in kB */
209     FileSize /= 1024;
210 
211     /* Find the floppy format in the list, and mount it if found */
212     for (i = 0; i < ARRAYSIZE(DiskGeometryList); ++i)
213     {
214         if (DiskGeometryList[i].ksize     == FileSize ||
215             DiskGeometryList[i].ksize + 1 == FileSize)
216         {
217             /* Found, mount it */
218             DiskImage->DiskType = DiskGeometryList[i].biosval;
219             DiskImage->DiskInfo.Cylinders = DiskGeometryList[i].cylcount;
220             DiskImage->DiskInfo.Heads     = DiskGeometryList[i].headscyl;
221             DiskImage->DiskInfo.Sectors   = DiskGeometryList[i].secttrack;
222             DiskImage->DiskInfo.SectorSize = HackSectorSize;
223 
224             /* Set the file pointer to the beginning */
225             SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
226 
227             DiskImage->hDisk = hFile;
228             return TRUE;
229         }
230     }
231 
232     /* If we are here, we failed to find a suitable format. Bail out. */
233     DisplayMessage(L"MountFDI: Floppy image of invalid size %d.", FileSize);
234     return FALSE;
235 }
236 
237 
238 /************************** IDE HARD DISK CONTROLLER **************************/
239 
240 // An IDE Hard Disk Controller can support up to 4 drives:
241 // Primary Master Drive, Primary Slave Drive,
242 // Secondary Master Drive, Secondary Slave Drive.
243 static DISK_IMAGE XDCHardDrive[4];
244 
245 static BOOLEAN
MountHDD(IN PDISK_IMAGE DiskImage,IN HANDLE hFile)246 MountHDD(IN PDISK_IMAGE DiskImage,
247          IN HANDLE hFile)
248 {
249     /**** Support for VHD fixed disks ****/
250     DWORD FilePointer, BytesToRead;
251     VHD_FOOTER vhd_footer;
252 
253     /* Go to the end of the file and retrieve the footer */
254     FilePointer = SetFilePointer(hFile, -(LONG)sizeof(VHD_FOOTER), NULL, FILE_END);
255     if (FilePointer == INVALID_SET_FILE_POINTER)
256     {
257         DPRINT1("MountHDD: Error when seeking HDD footer, last error = %d\n", GetLastError());
258         return FALSE;
259     }
260 
261     /* Read footer */
262     // FIXME: We may consider just mapping section to the file...
263     BytesToRead = sizeof(VHD_FOOTER);
264     if (!ReadFile(hFile, &vhd_footer, BytesToRead, &BytesToRead, NULL))
265     {
266         DPRINT1("MountHDD: Error when reading HDD footer, last error = %d\n", GetLastError());
267         return FALSE;
268     }
269 
270     /* Perform validity checks */
271     if (RtlCompareMemory(vhd_footer.creator, "conectix",
272                          sizeof(vhd_footer.creator)) != sizeof(vhd_footer.creator))
273     {
274         DisplayMessage(L"MountHDD: Invalid HDD image (expected VHD).");
275         return FALSE;
276     }
277     if (vhd_footer.version != 0x00000100 &&
278         vhd_footer.version != 0x00000500) // FIXME: Big endian!
279     {
280         DisplayMessage(L"MountHDD: VHD HDD image of unexpected version %d.", vhd_footer.version);
281         return FALSE;
282     }
283     if (RtlUlongByteSwap(vhd_footer.type) != VHD_FIXED) // FIXME: Big endian!
284     {
285         DisplayMessage(L"MountHDD: Only VHD HDD fixed images are supported.");
286         return FALSE;
287     }
288     if (vhd_footer.data_offset != 0xFFFFFFFFFFFFFFFF)
289     {
290         DisplayMessage(L"MountHDD: Unexpected data offset for VHD HDD fixed image.");
291         return FALSE;
292     }
293     if (vhd_footer.orig_size != vhd_footer.size)
294     {
295         DisplayMessage(L"MountHDD: VHD HDD fixed image size should be the same as its original size.");
296         return FALSE;
297     }
298     // FIXME: Checksum!
299 
300     /* Found, mount it */
301     DiskImage->DiskType = 0;
302     DiskImage->DiskInfo.Cylinders = RtlUshortByteSwap(vhd_footer.cyls); // FIXME: Big endian!
303     DiskImage->DiskInfo.Heads     = vhd_footer.heads;
304     DiskImage->DiskInfo.Sectors   = vhd_footer.secs_per_cyl;
305     DiskImage->DiskInfo.SectorSize = RtlUlonglongByteSwap(vhd_footer.size) / // FIXME: Big endian!
306                                      DiskImage->DiskInfo.Cylinders /
307                                      DiskImage->DiskInfo.Heads / DiskImage->DiskInfo.Sectors;
308 
309     /* Set the file pointer to the beginning */
310     SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
311 
312     DiskImage->hDisk = hFile;
313     return TRUE;
314 }
315 
316 
317 
318 /************************ GENERIC DISK CONTROLLER API *************************/
319 
320 BOOLEAN
IsDiskPresent(IN PDISK_IMAGE DiskImage)321 IsDiskPresent(IN PDISK_IMAGE DiskImage)
322 {
323     ASSERT(DiskImage);
324     return (DiskImage->hDisk != INVALID_HANDLE_VALUE && DiskImage->hDisk != NULL);
325 }
326 
327 BYTE
SeekDisk(IN PDISK_IMAGE DiskImage,IN WORD Cylinder,IN BYTE Head,IN BYTE Sector)328 SeekDisk(IN PDISK_IMAGE DiskImage,
329          IN WORD Cylinder,
330          IN BYTE Head,
331          IN BYTE Sector)
332 {
333     DWORD FilePointer;
334 
335     /* Check that the sector number is 1-based */
336     // FIXME: Or do it in the caller?
337     if (Sector == 0)
338     {
339         /* Return error */
340         return 0x01;
341     }
342 
343     /* Set position */
344     // Compute the offset
345     FilePointer = (DWORD)((DWORD)((DWORD)Cylinder * DiskImage->DiskInfo.Heads + Head)
346                     * DiskImage->DiskInfo.Sectors + (Sector - 1))
347                     * DiskImage->DiskInfo.SectorSize;
348     FilePointer = SetFilePointer(DiskImage->hDisk, FilePointer, NULL, FILE_BEGIN);
349     if (FilePointer == INVALID_SET_FILE_POINTER)
350     {
351         /* Return error */
352         return 0x40;
353     }
354 
355     return 0x00;
356 }
357 
358 BYTE
ReadDisk(IN PDISK_IMAGE DiskImage,IN WORD Cylinder,IN BYTE Head,IN BYTE Sector,IN BYTE NumSectors)359 ReadDisk(IN PDISK_IMAGE DiskImage,
360          IN WORD Cylinder,
361          IN BYTE Head,
362          IN BYTE Sector,
363          IN BYTE NumSectors)
364 {
365     BYTE Result;
366     DWORD BytesToRead;
367 
368     PVOID LocalBuffer;
369     BYTE StaticBuffer[1024];
370 
371     /* Read the sectors */
372     Result = SeekDisk(DiskImage, Cylinder, Head, Sector);
373     if (Result != 0x00)
374         return Result;
375 
376     BytesToRead = (DWORD)NumSectors * DiskImage->DiskInfo.SectorSize;
377 
378     // FIXME: Consider just looping around filling each time the buffer...
379 
380     if (BytesToRead <= sizeof(StaticBuffer))
381     {
382         LocalBuffer = StaticBuffer;
383     }
384     else
385     {
386         LocalBuffer = RtlAllocateHeap(RtlGetProcessHeap(), 0, BytesToRead);
387         ASSERT(LocalBuffer != NULL);
388     }
389 
390     if (ReadFile(DiskImage->hDisk, LocalBuffer, BytesToRead, &BytesToRead, NULL))
391     {
392         /* Write to the memory */
393         EmulatorWriteMemory(&EmulatorContext,
394                             TO_LINEAR(getES(), getBX()),
395                             LocalBuffer,
396                             BytesToRead);
397 
398         Result = 0x00;
399     }
400     else
401     {
402         Result = 0x04;
403     }
404 
405     if (LocalBuffer != StaticBuffer)
406         RtlFreeHeap(RtlGetProcessHeap(), 0, LocalBuffer);
407 
408     /* Return success or error */
409     return Result;
410 }
411 
412 BYTE
WriteDisk(IN PDISK_IMAGE DiskImage,IN WORD Cylinder,IN BYTE Head,IN BYTE Sector,IN BYTE NumSectors)413 WriteDisk(IN PDISK_IMAGE DiskImage,
414           IN WORD Cylinder,
415           IN BYTE Head,
416           IN BYTE Sector,
417           IN BYTE NumSectors)
418 {
419     BYTE Result;
420     DWORD BytesToWrite;
421 
422     PVOID LocalBuffer;
423     BYTE StaticBuffer[1024];
424 
425     /* Check for write protection */
426     if (DiskImage->ReadOnly)
427     {
428         /* Return error */
429         return 0x03;
430     }
431 
432     /* Write the sectors */
433     Result = SeekDisk(DiskImage, Cylinder, Head, Sector);
434     if (Result != 0x00)
435         return Result;
436 
437     BytesToWrite = (DWORD)NumSectors * DiskImage->DiskInfo.SectorSize;
438 
439     // FIXME: Consider just looping around filling each time the buffer...
440 
441     if (BytesToWrite <= sizeof(StaticBuffer))
442     {
443         LocalBuffer = StaticBuffer;
444     }
445     else
446     {
447         LocalBuffer = RtlAllocateHeap(RtlGetProcessHeap(), 0, BytesToWrite);
448         ASSERT(LocalBuffer != NULL);
449     }
450 
451     /* Read from the memory */
452     EmulatorReadMemory(&EmulatorContext,
453                        TO_LINEAR(getES(), getBX()),
454                        LocalBuffer,
455                        BytesToWrite);
456 
457     if (WriteFile(DiskImage->hDisk, LocalBuffer, BytesToWrite, &BytesToWrite, NULL))
458         Result = 0x00;
459     else
460         Result = 0x04;
461 
462     if (LocalBuffer != StaticBuffer)
463         RtlFreeHeap(RtlGetProcessHeap(), 0, LocalBuffer);
464 
465     /* Return success or error */
466     return Result;
467 }
468 
469 typedef BOOLEAN (*MOUNT_DISK_HANDLER)(IN PDISK_IMAGE DiskImage, IN HANDLE hFile);
470 
471 typedef struct _DISK_MOUNT_INFO
472 {
473     PDISK_IMAGE DiskArray;
474     ULONG NumDisks;
475     MOUNT_DISK_HANDLER MountDiskHelper;
476 } DISK_MOUNT_INFO, *PDISK_MOUNT_INFO;
477 
478 static DISK_MOUNT_INFO DiskMountInfo[MAX_DISK_TYPE] =
479 {
480     {XDCFloppyDrive, _ARRAYSIZE(XDCFloppyDrive), MountFDI},
481     {XDCHardDrive  , _ARRAYSIZE(XDCHardDrive)  , MountHDD},
482 };
483 
484 PDISK_IMAGE
RetrieveDisk(IN DISK_TYPE DiskType,IN ULONG DiskNumber)485 RetrieveDisk(IN DISK_TYPE DiskType,
486              IN ULONG DiskNumber)
487 {
488     ASSERT(DiskType < MAX_DISK_TYPE);
489 
490     if (DiskNumber >= DiskMountInfo[DiskType].NumDisks)
491     {
492         DisplayMessage(L"RetrieveDisk: Disk number %d:%d invalid.", DiskType, DiskNumber);
493         return NULL;
494     }
495 
496     return &DiskMountInfo[DiskType].DiskArray[DiskNumber];
497 }
498 
499 BOOLEAN
MountDisk(IN DISK_TYPE DiskType,IN ULONG DiskNumber,IN PCWSTR FileName,IN BOOLEAN ReadOnly)500 MountDisk(IN DISK_TYPE DiskType,
501           IN ULONG DiskNumber,
502           IN PCWSTR FileName,
503           IN BOOLEAN ReadOnly)
504 {
505     BOOLEAN Success = FALSE;
506     PDISK_IMAGE DiskImage;
507     HANDLE hFile;
508 
509     BY_HANDLE_FILE_INFORMATION FileInformation;
510 
511     ASSERT(DiskType < MAX_DISK_TYPE);
512 
513     if (DiskNumber >= DiskMountInfo[DiskType].NumDisks)
514     {
515         DisplayMessage(L"MountDisk: Disk number %d:%d invalid.", DiskType, DiskNumber);
516         return FALSE;
517     }
518 
519     DiskImage = &DiskMountInfo[DiskType].DiskArray[DiskNumber];
520     if (IsDiskPresent(DiskImage))
521     {
522         DPRINT1("MountDisk: Disk %d:%d:0x%p already in use, recycling...\n", DiskType, DiskNumber, DiskImage);
523         UnmountDisk(DiskType, DiskNumber);
524     }
525 
526     /* Try to open the file */
527     SetLastError(0); // For debugging purposes
528     hFile = CreateFileW(FileName,
529                         GENERIC_READ | (ReadOnly ? 0 : GENERIC_WRITE),
530                         (ReadOnly ? FILE_SHARE_READ : 0),
531                         NULL,
532                         OPEN_EXISTING,
533                         FILE_ATTRIBUTE_NORMAL,
534                         NULL);
535     DPRINT1("File '%S' opening %s ; GetLastError() = %u\n",
536             FileName, hFile != INVALID_HANDLE_VALUE ? "succeeded" : "failed", GetLastError());
537 
538     /* If we failed, bail out */
539     if (hFile == INVALID_HANDLE_VALUE)
540     {
541         DisplayMessage(L"MountDisk: Error when opening disk file '%s' (Error: %u).", FileName, GetLastError());
542         return FALSE;
543     }
544 
545     /* OK, we have a handle to the file */
546 
547     /*
548      * Check that it is really a file, and not a physical drive.
549      * For obvious security reasons, we do not want to be able to
550      * write directly to physical drives.
551      *
552      * Redundant checks
553      */
554     SetLastError(0);
555     if (!GetFileInformationByHandle(hFile, &FileInformation) &&
556         GetLastError() == ERROR_INVALID_FUNCTION)
557     {
558         /* Objects other than real files are not supported */
559         DisplayMessage(L"MountDisk: '%s' is not a valid disk file.", FileName);
560         goto Quit;
561     }
562     SetLastError(0);
563     if (GetFileSize(hFile, NULL) == INVALID_FILE_SIZE &&
564         GetLastError() == ERROR_INVALID_FUNCTION)
565     {
566         /* Objects other than real files are not supported */
567         DisplayMessage(L"MountDisk: '%s' is not a valid disk file.", FileName);
568         goto Quit;
569     }
570 
571     /* Success, mount the image */
572     if (!DiskMountInfo[DiskType].MountDiskHelper(DiskImage, hFile))
573     {
574         DisplayMessage(L"MountDisk: Failed to mount disk file '%s' in 0x%p.", FileName, DiskImage);
575         goto Quit;
576     }
577 
578     /* Update its read/write state */
579     DiskImage->ReadOnly = ReadOnly;
580 
581     Success = TRUE;
582 
583 Quit:
584     if (!Success) FileClose(hFile);
585     return Success;
586 }
587 
588 BOOLEAN
UnmountDisk(IN DISK_TYPE DiskType,IN ULONG DiskNumber)589 UnmountDisk(IN DISK_TYPE DiskType,
590             IN ULONG DiskNumber)
591 {
592     PDISK_IMAGE DiskImage;
593 
594     ASSERT(DiskType < MAX_DISK_TYPE);
595 
596     if (DiskNumber >= DiskMountInfo[DiskType].NumDisks)
597     {
598         DisplayMessage(L"UnmountDisk: Disk number %d:%d invalid.", DiskType, DiskNumber);
599         return FALSE;
600     }
601 
602     DiskImage = &DiskMountInfo[DiskType].DiskArray[DiskNumber];
603     if (!IsDiskPresent(DiskImage))
604     {
605         DPRINT1("UnmountDisk: Disk %d:%d:0x%p is already unmounted\n", DiskType, DiskNumber, DiskImage);
606         return FALSE;
607     }
608 
609     /* Flush the image and unmount it */
610     FlushFileBuffers(DiskImage->hDisk);
611     FileClose(DiskImage->hDisk);
612     DiskImage->hDisk = NULL;
613     return TRUE;
614 }
615 
616 
617 /* PUBLIC FUNCTIONS ***********************************************************/
618 
DiskCtrlInitialize(VOID)619 BOOLEAN DiskCtrlInitialize(VOID)
620 {
621     return TRUE;
622 }
623 
DiskCtrlCleanup(VOID)624 VOID DiskCtrlCleanup(VOID)
625 {
626     ULONG DiskNumber;
627 
628     /* Unmount all the present floppy disk drives */
629     for (DiskNumber = 0; DiskNumber < DiskMountInfo[FLOPPY_DISK].NumDisks; ++DiskNumber)
630     {
631         if (IsDiskPresent(&DiskMountInfo[FLOPPY_DISK].DiskArray[DiskNumber]))
632             UnmountDisk(FLOPPY_DISK, DiskNumber);
633     }
634 
635     /* Unmount all the present hard disk drives */
636     for (DiskNumber = 0; DiskNumber < DiskMountInfo[HARD_DISK].NumDisks; ++DiskNumber)
637     {
638         if (IsDiskPresent(&DiskMountInfo[HARD_DISK].DiskArray[DiskNumber]))
639             UnmountDisk(HARD_DISK, DiskNumber);
640     }
641 }
642 
643 /* EOF */
644