📜 ⬆️ ⬇️

How to run a program without an operating system: part 6. Support for working with disks with the FAT file system

In the fifth part of our series of articles, we showed how you can use BIOS interrupts after switching to protected mode, and determined the size of RAM as an example. Today we will develop this success and implement full support for working with disks with the FAT16 and FAT32 file system. Work with files on the disk can be divided into 2 parts: work with the file system and work with the disk at the read / write level of sectors. We can say that for this we need to write the “driver” of the file system and the “driver” of the disk.

Work with the disk at the level of read / write sectors


To begin to learn how to work with the disk.
So we can cause BIOS interrupts. Among other features, the BIOS provides an interface for working with the disk, namely, the int 0x13 interrupt. The list of services provided by the interruption can be found on Wikipedia . We are interested in the services of reading and writing disks.

There are two ways to address a sector on a disk that the BIOS works with - CHS ( cylinder-head-sector ) and LBA ( logical block addressing ). CHS addressing is based on the use of disk geometry, and the sector address is a set of three coordinates: a cylinder, a head, a sector. The method allows you to address up to 8GB. The int0x13 interrupt provides the ability to read and write to disk using this addressing.
')
It is clear that 8GB is very small, and this addressing method is obsolete, and all modern (and not so) hard disk controllers support LBA addressing. Addressing LBA abstracts from the disk geometry and assigns each sector its own number. Sector numbering starts from zero. LBA uses 48 bits to set the block number, which allows addressing 128 pi, taking into account the sector size of 512 bytes. The int0x13 interrupt provides two services for reading and writing sectors to disk using LBA. We will use them. To read the sector, the in0x13 interrupt expects the following parameters:



DAP structure:



The interrupt returns the following values:



One of the parameters is the disk number. We need to somehow find out the number of the disk with which we are going to work. The numbering is as follows: floppy disks (fdd), and everything that is emulated as floppy is numbered from scratch, and hard disks (hdd), and everything that is emulated as they (usb flash drives, for example), are numbered from 0x80. This number is not related to the boot sequence in the BIOS settings. In our case, the disk with which we are going to work is the disk from which we booted.

When the BIOS transfers control to the MBR, it loads it at the address 0000h: 7C00h, and in the DL register it transfers the number of the boot device we need. This is part of the interface between the BIOS and the MBR. Thus, this number falls into GRUB, where it is further used to work with the disk. GRUB, in turn, passes this OS number as part of the Multiboot information structure.

Immediately after the transfer of control from GRUB to the OS, the EBX register contains a pointer to this structure. The first field of the structure is flags, and if the 2nd bit is set in it, then the boot_device field is correct. This field also belongs to the Multiboot information structure and in its high byte (the field size is 4 bytes) we store the required disk number, which is understood by the int0x13 interrupt. Thus, using GRUB, we got the missing parameter for reading / writing sectors to disk.

We learned to read and write sectors to disk, this is certainly important. But the file system is not tied to the whole disk, but only to its part - the partition. To work with the file system, you need to find the sector from which the section on which it is located begins. Information about the sectors on the disk is stored in the first sector of the disk, in the same place where the MBR is located. There are many different MBR formats , but the following structure is true for all of them:



Information about the sections is stored in the partition table. There can be only 4 primary partitions on the disk from which you can boot. There are 8 bytes for a partition record. The first byte is the flags, if its value is 0x80, then the partition is bootable. The MBR code in the course of its work runs through these 4 sections in the search for a boot partition. After its detection, the MBR copies the contents of the first sector of this section to the address 0000h: 7C00h and transfers control there. We are interested in the LBA address of the first sector of the boot partition, since that is where our kernel resides, and there is a file system that we are going to read. In order to get this address, you need to read the first sector of the disk, find the partition table on it, find the boot partition in the partition table, and read the required field from its entry.

So, we have a mechanism for reading the sector from the disk and knowledge of the location of the partition we need on the disk. It remains to learn how to work with the file system in this section.

Work with file system


To work with the file system, we will use the fat_io_lib library. The library is available under the GPL license. It provides an interface for working with files and directories, similar to that found in libc. Functions such as fopen (), fgets (), fputc (), fread (), fwrite (), etc. are implemented. The library requires only two functions for its work: to write down a sector and read a sector, the first being optional. Functions have the following prototype:

int media_read(uint32 sector, uint8 *buffer, uint32 sector_count); int media_write(uint32 sector, uint8 *buffer, uint32 sector_count); Return: int, 1 = success, 0 = failure. 


The library is written in pure C, which is again to our advantage. For use in our mini-OS, we do not have to change a single line in it. The library expects the reading of sectors to occur within the file system section.

So, we have functions of reading / writing sector per section and there is a library for working with FAT16 / 32, which uses these functions. It remains to collect all together and demonstrate the result. But before turning to the code, I would like to show that the approach we are going to use is quite applicable in real life. Below is a small part of VBR windows 7, in which the disk sector is read by interrupting int0x13. This code is repeatedly called during the system boot process, until the boot animation is drawn.



To call this code, Windows 7, just as we do, goes from protected mode to real mode, and vice versa. This is easy to verify by running Windows 7 in QEMU. QEMU should wait for a debugger connection. After connecting the debugger (gdb), set the breakpoint to the address (0x7c00 + 0x11d). Triggering a breakpoint will mean calling this function. By the way, this mechanism is absent in Windows XP, in order to trigger BIOS interrupts there, they switch to VM86 mode.

! IMPORTANT! All further actions can be successfully carried out only after successful completion of all steps from the fifth part of our series of articles.

Step 1. Change the main logic in kernel.c



1. Add the following declarations in the kernel.c file:

 #include "multiboot.h" #include "fat_io_lib/fat_filelib.h" //    loader.s extern u32 mbd; extern u32 magic; 


Code printing the size of RAM

 u64 ram_size = GetRamsize(); printf("ram_size = %llu(%lluMb)\n", ram_size, ram_size / 0x100000); 


replace with the following code:

 // ,    grub- if (magic != MULTIBOOT_BOOTLOADER_MAGIC) { printf("Invalid magic number: 0x%x\n", magic); return; } multiboot_info_t *p_multiboot_info = (multiboot_info_t*)mbd; // Is boot_device valid? if ((p_multiboot_info->flags & 2) == 0) { printf("Error: boot_device(2) flag is clear\n"); return; } //      if (InitBootMedia(p_multiboot_info->boot_device >> 24) == 0) { printf("Error: InitBootMedia failed.\n"); return; } //   fat_io_lib fl_init(); if (fl_attach_media(ReadBootMedia, WriteBootMedia) != FAT_INIT_OK) { printf("Error: Media attach failed.\n"); return; } //      /boot/grub fl_listdirectory("/boot/grub"); //   /boot/grub/menu.lst   char str[64]; void *file = fl_fopen("/boot/grub/menu.lst", "r"); if (file == 0) { printf("Error: can not open file.\n"); return; } printf("\nConntent of the file /boot/grub/menu.lst:\n"); while (fl_fgets(str, sizeof(str), file)) { printf("%s", str); } 


The memory for the variables mbd and magic is reserved in the file loader.s, so that they can be used similarly to the global variables from the C code. The magic variable contains a signature confirming that the Multiboot standard was used for loading, the reference implementation of which is GRUB. The mbd variable points to the multiboot_info_t structure, which is declared in multiboot.h. The boot disk number is determined by the following expression - p_multiboot_info-> boot_device >> 24. The InitBootMedia function remembers the disk number and searches for the first sector of the file system so that all offsets can be read from it.

The fat_io_lib library for initialization requires calling two functions: fl_init and fl_attach_media. The first function resets the internal structures of the library, and the second receives the parameters of the function of reading and writing sectors to disk, which are then used to access files. Next is a demonstration of working with the library: a list of files in the / boot / grub folder is displayed, and the contents of the menu.lst file are printed.

2. Add the multiboot.h file to the include folder. The contents of the file we take from the site specifications of the previous version.

Step 2. Add functions to work with the disk



1. In the file include \ callrealmode.h add prototypes of the following functions:

 u32 InitBootMedia(u8 bootDevice); int ReadBootMedia(unsigned long sector, unsigned char *buffer, unsigned long sectorCount); int WriteBootMedia(unsigned long sector, unsigned char *buffer, unsigned long sectorCount); 


2. In the include \ callrealmode_asm.h file, add a new value to enum callrealmode_Func so that the following happens:

 enum callrealmode_Func { CALLREALMODE_FUNC_GETSYSMEMMAP = 0, CALLREALMODE_FUNC_READ_DISK = 1 };  ,       : <source lang="c"> struct callrealmode_read_disk { u64 start_sector_lba; u32 buff_addr; u32 sectors_count; u16 disk_number; u8 ret_code; } __attribute__ ((packed)); 


Add the newly declared callrealmode_read_disk structure to the union inside the callrealmode_Data structure. You should have the following:

 struct callrealmode_Data { enum callrealmode_Func func : 16; union { struct callrealmode_GetSysMemMap getsysmemmap; struct callrealmode_read_disk readdisk; }; } __attribute__ ((packed)); 


3. Add the strncmp and strncpy functions used in the fat_io_lib library to the include \ string.h file.

 static inline int strncmp ( const char * str1, const char * str2, unsigned int num ) { for ( ; num > 0; str1++, str2++, --num) { if (*str1 != *str2) return ((*(unsigned char *)str1 < *(unsigned char *)str2) ? -1 : +1); else if (*str1 == '\0') return 0; } return 0; } static inline char* strncpy ( char * dst, const char * src, unsigned int num ) { if (num != 0) { char *d = dst; const char *s = src; do { if ((*d++ = *s++) == 0) { while (--num) *d++ = 0; break; } } while (--num); } return dst; } 


4. Add the following declarations to the callrealmode.c file:

 #include "fat_io_lib/fat_opts.h" #include "mbr.h" u64 g_BootPartitionStart = 0; //        u32 g_BootDeviceInt13Num = 0; //    


And several functions:

 //     int ReadBootMedia(unsigned long sector, unsigned char *buffer, unsigned long sectorCount) { struct callrealmode_Data param; // ,     //    RM,    param.func = CALLREALMODE_FUNC_READ_DISK; //         int13. //          //    1Mb,   "buffer"     . //         "low_mem_buff", //     RM ,    CALLREALMODE_OFFSET < 1Mb int i; void *low_mem_buff = CALLREALMODE_OFFSET + (&callrealmode_end - &callrealmode_start); for (i = 0; i < sectorCount; i++) { param.readdisk.start_sector_lba = sector + g_BootPartitionStart + i; param.readdisk.buff_addr = (u32)low_mem_buff; param.readdisk.disk_number = g_BootDeviceInt13Num; param.readdisk.sectors_count = 1; callrealmode_Call(¶m); // int 0x13    "param" if (param.readdisk.ret_code) { return 0; // error } memcpy(buffer + i * FAT_SECTOR_SIZE, low_mem_buff, FAT_SECTOR_SIZE); } return 1; // success } //    .  int WriteBootMedia(unsigned long sector, unsigned char *buffer, unsigned long sectorCount) { return 0; // error } //   u32 InitBootMedia(u8 bootDevice) { g_BootDeviceInt13Num = bootDevice; //     MBRSector_t mbr; if (ReadBootMedia(0, (u8*)&mbr, 1) == 0) { return 0; } //   if (mbr.mbr_sign[0] != 0x55 || mbr.mbr_sign[1] != 0xaa) { return 0; } //    int i; for (i = 0; i < 4; i++) { if (mbr.part[i].boot_indicator == 0x80) break; } if (i == 4) { return 0; } //       g_BootPartitionStart = mbr.part[i].start_lva; printf("start sector = %lld boot dev int13 num = 0x%x\n", g_BootPartitionStart, g_BootDeviceInt13Num); return 1; } 


The ReadBootMedia and WriteBootMedia functions are used by the fat_io_lib library to read / write sectors. The WriteBootMedia function is optional and is a stub, since in this example there is no writing to disk. Its implementation would look similar to the ReadBootMedia function. The ReadBootMedia function is similar to the GetRamsize function from the previous article, accurate to the type param.func, and param.readdisk is used instead of param.getsysmemmap. The InitBootMedia function must be called before the other two, since it initializes the g_BootPartitionStart and g_BootDeviceInt13Num values.

5. Change the callrealmode_asm.s. Add another type of CALLREALMODE_FUNC_READ_DISK called functions, you should get the following:

 #    enum callrealmode_Func CALLREALMODE_FUNC_GETSYSMEMMAP = 0x0 CALLREALMODE_FUNC_READ_DISK = 0x1 


Next, add another check on the type of the function and directly the code that reads from the disk. You should have the following:

 callrealmode_switch: OFF_FUNC = 44 #     %bp #   func  callrealmode_Data # Which function? movw OFF_FUNC(%bp),%ax cmp $CALLREALMODE_FUNC_GETSYSMEMMAP,%ax je getsysmemmap cmp $CALLREALMODE_FUNC_READ_DISK,%ax je readdisk ret readdisk: OFF_START_SECTOR = 50 #    start_sector_lba  callrealmode_Data OFF_BUFFER_ADDR = 58 #    buff_addr  callrealmode_Data OFF_SECTORS_COUNT = 62 #    sectors_count  callrealmode_Data OFF_DISK_NUMBER = 66 #    disk_number  callrealmode_Data OFF_RETURN_CODE = 68 #    ret_code  callrealmode_Data push %bp mov %sp,%bp #     DAP pushl OFF_START_SECTOR+4(%bp) pushl OFF_START_SECTOR+0(%bp) pushl OFF_BUFFER_ADDR(%bp) pushw OFF_SECTORS_COUNT(%bp) pushw $0x10 mov %sp,%si # ds:si    , ..  DAP mov OFF_DISK_NUMBER(%bp),%dl #    dl mov $0x42,%ah # EXTENDED READ int $0x13 # CALL DISK BIOS mov %ah,OFF_RETURN_CODE(%bp) #   add $0x10,%sp #    DAP pop %bp ret 


The readdisk label points to the code that forms the DAP structure from the callrealmode_Data structure and calls int0x13. In the code after the callrealmode_switch label, 2 instructions were added to check if readdisk should be called.

6. Add the file include \ mbr.h, containing definitions for working with the MBR. Its contents are:

 #ifndef _MBR_H_ #define _MBR_H_ #include "types.h" struct MBRPartitionEntry { unsigned char boot_indicator; unsigned char start_head; unsigned short start_sector : 6; unsigned short start_cylinder : 10; unsigned char sys_id; unsigned char end_head; unsigned short end_sector : 6; unsigned short end_cylinder : 10; unsigned int start_lva; unsigned int size_in_sectors; } __attribute__ ((packed)); typedef struct MBRPartitionEntry MBRPartitionEntry_t; struct MBRSector { u8 code[446]; MBRPartitionEntry_t part[4]; u8 mbr_sign[2]; } __attribute__ ((packed)); typedef struct MBRSector MBRSector_t; 


The MBRSector structure is used in the InitBootMedia function.

Step 3. Add the fat_io_lib library and run



1. Download the archive fat_io_lib.zip and unpack it into the folder fat_io_lib in the project root.
2. Add empty files assert.h and stdlib.h to the include folder. They are needed for the library to compile.
3. Fix the makefile. Add files from the library to the list of targets for compilation. You should have the following:

 FAT_LIB_OBJFILES = \ ./fat_io_lib/fat_access.o \ ./fat_io_lib/fat_cache.o \ ./fat_io_lib/fat_filelib.o \ ./fat_io_lib/fat_format.o \ ./fat_io_lib/fat_misc.o \ ./fat_io_lib/fat_string.o \ ./fat_io_lib/fat_table.o \ ./fat_io_lib/fat_write.o OBJFILES = \ loader.o \ common/printf.o \ common/screen.o \ common/string.o \ kernel.o \ callrealmode.o \ callrealmode_asm.o \ descriptor.o \ $(FAT_LIB_OBJFILES) 


Replace the line
 @dd if=/dev/zero of=./hdd.img bs=512 count=16065 1>/dev/null 2>&1 

On
 @dd if=/dev/zero of=./hdd.img bs=1M count=10 1>/dev/null 2>&1 


Now the image size is 10Mb. This is done so that the mkdosfs command will format the partition in FAT16 instead of FAT12. FAT12 is not supported by the fat_io_lib library.

Replace the line

 $(CC) -Iinclude $(CFLAGS) -o $@ -c $< 


On

 $(CC) -Iinclude -DFAT_PRINTF_NOINC_STDIO $(CFLAGS) -o $@ -c $< 


With this define, the library will not include stdio.h, but it will use a ready-made prototype of the printf function, which coincides with ours, and which is already implemented.

4. Reconstruct the project

 make rebuild 

sudo make image

5. Run

 sudo qemu-system-i386 -hda hdd.img 


You should have the following:



As in the previous parts, you can make the dd image of hdd.img on a flash drive and check the code on a real hardware by booting from it.

As a result, we implemented the work with the FAT16 and FAT32 file systems. We fooled a bit using the ready-made library, but it would be less interesting to understand the FAT device, and we would hardly have been able to do it in 1 article. I hope you enjoyed reading. Write in the comments, if you have problems in the passage of the steps described!

Selection of references to the previous parts:

Source: https://habr.com/ru/post/203706/


All Articles