FICHIER : FATdoc#1.txt DATE : 25/05/2002 VERSION : 1.0 AUTEUR : Kaze LIRE LE FAT VIA LES PORTS --------------------------- [ PARTIE 1: ACCEDER AU DISQUE ] [ INTRO ] Voila un tut que j'aurais bien aime trouver il y a quelques semaines. Il a pour but de vous permettre d'integrer la gestion de la FAT a votre os. Pour beaucoup, la premiere grande difficulte rencontree pendant l'elaboration d'un os reside dans la gestion d'un fs. Je n'ai pas echappe a cette regle, et j'ai particulierement ete frappe par le manque de doc et surtout de tuts sur le sujet. J'espere que ce texte y remediera quelque peu. La premiere partie de ce tut vous montrera comment piloter le disque dur, et comment localiser une partition FAT, c'est dans le deuxieme que l'on parlera vraiment de la FAT en elle-meme... (Tout le code a ete ecrit sous nasm) 1) ACCEDER AU DISQUE -------------------- Comme l'indique le titre de ce tut, nous n'allons pas utiliser l'int 13h ou autre API. Il faudra donc acceder au dique dur par les ports. Le code qui va suivre ne marchera que pour des disques durs IDE (cad avec une interface ATA). Tout d'abord, voyons comment controler notre disque IDE. Nous pouvons commander le disque dur via 8 registres/ports: +----+----------------------------+---------------------------+ |Port| Lecture | Ecriture | +----+----------------------------+---------------------------+ |1F0 | Donnees | Donnees | |1F1 | Erreurs | Inusite | |1F2 | Nombre de secteurs | Nombre de secteurs | |1F3 | Numero du secteur | Numero du secteur | |1F4 | Numero du cylindre (LSB) | Numero du cylindre (LSB) | |1F5 | Numero du cylindre (MSB) | Numero du cylindre (MSB) | |1F6 | Numero du Disque/Tete | Numero du Disque/Tete | |1F7 | Statut | Commande | +----+----------------------------+---------------------------+ [ Port 0x01F0 ] C'est par ce port que s'effectuent toutes les lectures et ecritures. C'est le plus utilise. [ Port 0x01F1 ] En lecture, il contient le resultat de la derniere commande effectuee: Bit 7 6 5 4 3 2 1 0 Signification BBK UNC 0 IDNF 0 ABRT TK0NF AMNF BBK : Bad block mark, secteur defectueux. UNC : ??? IDNF : Secteur non trouve (ID Not Found) ABRT : Commande non effectuee (ABoRTed): commande invalide ou erreure disque. TK0NF: TraK 0 Not Found: piste 0 non detectee lors du recalibrage. AMNF : ??? [ Port 0x01F2 ] En ecriture, contient le nombre de secteurs a ecrire lors de la prochaine lecture/ecriture. Les valeurs acceptees vont de 0 a 255, 0 correspondant a 256 secteurs. En lecture, contient le nombre de secteurs restant a lire/ecrire apres une lecture/ecriture. Si 0, tour va bien: tout a ete lu. [ Port 0x01F3 ] Contient le numero du secteur ou la prochaine lecture/ecriture va avoir lieu. A mettre en relation avec le numero de la tete et le numero du cylindre bien enetendu. [ Port 0x01F4 ] Contient la partie basse (LSB) du numero du cylindre ou la prochaine lecture/ ecriture va avoir lieu. [ Port 0x01F5 ] Contient la partie haute (MSB) du numero du cylindre ou la prochaine lecture/ ecriture va avoir lieu. [ Port 0x01F6 ] Contient le numero de la tete, et le numero du disque.Ce registre se presente comme suit: Bit 7 6 5 4 3 2 1 0 Signification 1 0 1 DRV H e a d n u m b e r DRV : Numero du disque, 0 pour le disque dur primaire et 1 pour le secondaire Head number: Numero de la tete, de 0 a 15 donc. [ Port 0x01F7 ] En lecture, contient l'etat du controlleur disque, a savoir: Bit 7 6 5 4 3 2 1 0 Signification BUSY DRDY DWF DSC DRQ CORR INDEX ERROR BUSY : Indique si le controlleur est occupe, auquel cas, aucun des registre ne peut etre accede, que ce soit en lecture ou en ecriture. DRDY : Drive ReaDY, disque pret. Indique que le controlleur est pret a recevoir une commande. DWF : Drive Write Fault, mis a un lorsqu'une erreur apparait. DSC : Drive Seek Complete, mis a un lorsque apres une commande de repositionnement d'une tete (seek), la tete est en route vers la bonne piste. DRQ : Data ReQuest, mis a un quand le disque est pret a transferer des donnees. CORR : Quand une erreur corrigeable a lieu et qu'elle est corrigee, ce bit est mis a 1. INDEX: ??? ERROR: Mis a un si une erreur est apparue lors de l'execution de la derniere commande. En ecriture, ce port nous permet de passer les numeros des commandes. Voila les ports qui vont nous interesser. Il en existe d'autre, mais nous n'en aurons pas besoin. Maintenant, voyons les commandes que l'on peut passer au port 01F7h: +--------+---------------------------------+ | Command| Command Description | | Code | | +--------+---------------------------------+ | 20h | Read Sectors With Retry | | 21h | Read Sectors | | 22h | Read Long With Retry | | 23h | Read Long | | 30h | Write Sectors With Retry | | 31h | Write Sectors | | 32h | Write Long With Retry | | 33h | Write Long | | ECh | Identify Drive | +--------+---------------------------------+ La aussi il y en a bien d'autres, mais seules celles-la nous interesseront. 20h : Read Sector with retry 21h : Read Sector Cette commande lit le nombre de secteur (1-256) specifie par le registre SectorCount (1F2h) commancant au secteur specifie par Secteur Number (1F3h) et au cylindre Cylinder (1F4h et 1F5h), la tete et le disque etant specifies par Disc / Head (1F6h). Si SectorCount est egal a 0, alors 256 secteurs sont lus. A la fin de la commande, une interruption materielle est executee. SecteurCount contient alors le nombre de secteurs lus, et les ports de 1F3h a 1F6h contiennent la position du dernier secteur lu. La difference entre la commande 20h et la commande 21h est qu'avec la commande 21h le controlleur reessayera de lire le secteur si une erreur a eu lieu (dans une certaine limite bien sur). 30h : Write Sector with retry 31h : Write Sector Idem en ecriture. ECh: Identify Drive Cette commande nous donne de precieuses informations sur le disque. Apres l'envoi de cette commande, un certain temps de latence est requis. Normalement il faut attendre une interruption materielle du controlleur, mais j'ai remarque qu'elle ne se produisait pas toujours selon les disques. Une solution simple est d'attendre 4 retraces verticales du vga, ce qui donne assez de temps au controlleur pour repondre a la commande. Ensuite, un buffer est accessible via le port 01F0h (comme si on lisait un secteur). Voila a quoi ressemble ce buffer: (La ou il y a des *, c'est que ca nous interesse) +-------+-----------------------------------------------------------------+ | Word | Description | +-------+-----------------------------------------------------------------+ | 00h | Bit mapped general configuration information. True when bit set | | | Bit 15: Reserved for non magnetic drives. | | | Bit 14: Format speed tolerance gap not required. | | | Bit 13: Track offset option not available. | | | Bit 12: Data strobe offset option not available. | | | Bit 11: Rotational speed tolerance is < 0.5% | | | Bit 10: Disk transfer rate not > 10 MB/s | | | Bit 09: Disk transfer rate > 5 MB/s and < 10 MB/s | | | Bit 08: Disk transfer rate > 5 MB/s | | | Bit 07: Removable cartridge drive. | | | Bit 06: Fixed drive. | | | Bit 05: Spindle motor control option not implemented. | | | Bit 04: Head switch time > 15 microseconds. | | | Bit 03: Not MFM encoded. | | | Bit 02: Not soft sectored. | | | Bit 01: Hard Sectored. | | | Bit 00: Reserved. | | | | | 01h | Number of logical cylinders in the default translation mode. | | | | | 02h | Reserved. | | | | * | 03h | Number of logical heads in the default translation mode. | | | | | 04h | Number of unformatted bytes per logical track. | | | | | 05h | Number of unformatted bytes per sector. | | | | * | 06h | Number of logical sectors per track. | | | | | 07h | Bits 15...08: Inter Sector Gap after Index & before splice. | | | Bits 07...00: Inter Sector Gap bytes. | | | | | 08h | Bits 15...08: Reserved. | | | Bits 07...00: Bytes in Phase Lock Oscillator field. | | | | | 09h | Number of vendor unique status words. | | | | | 0Ah | Serial number, 20 ASCII chars, right aligned & padded with 20h. | | | | | 14h | Controller type: | | | 0000h: Not specified. | | | 0001h: Single ported, single sector buffer capable of data | | | transfers only to or from the host or the disk at one | | | time. | | | 0002h: Dual ported, multiple sector buffer capable of | | | simultaneous data transfers to and from the host, or | | | from the host and the disk. | | | 0003h: Dual ported, multiple sector buffer capable of | | | simultaneous data transfers with read caching. | | | 0004h-FFFFh: Reserved. | | | | | 15h | Buffer size in 512 byte increments. | | | | | 16h | Number of ECC bytes passed to host on R/W long operations. | | | | | 17h | Firmware revision, 8 ASCII chars, left aligned & space padded. | | | | | 1Bh | Model Number, 40 ASCII chars, left aligned & space padded. | | | | | 2Fh | READ/WRITE multiples implemented. | | | | | 30h | Supports double word I/O transfer. | | | | | 31h | Reserved. | | | | | 32h | Reserved. | | | | | 33h | Minimum PIO data transfer cycle time in nsec. | | | | | 34h | Minimum DMA data transfer cycle time in nsec. | | | | | 35h | All words past this point are reserved. | +-------+-----------------------------------------------------------------+ 2) LIRE UN SECTEUR ------------------ Pour lire un secteur, il nous faut suivre les etapes suivantes: 1) Attendre que le bit BUSY soit a 0 2) Mettre a jours les different registres (secteur, tete,disque ...) 3) Attendre que le bit DRDY soit a 1 4) Envoyer la commande au port 0x01F7 5) Attendre que les bits BUSY DSC soient a 0 6) Lire 512 bytes sur le port 0x01F7 autant de fois que specifiees par la valeure rentree dans "Nombre de secteurs" (0x01F2) Voila la theorie, maintenant passons a la pratique. Le code qui va suivre est en grande partie une adaptation du code d'Alan Martin. Je n'ai pas grand merite, mais bon, les variantes sont rares et le temps precieux, alors merci a Mr Alan Martin. La premiere partie est une simple application de ce que nous avons vu, la deuxieme elle concerne la gestion des erreurs. Je n'en ai pas parle mais le code est suffisament commente a mon avis. ========>8========>8========>8========>8========>8========>8========>8========>8========>8 ; Registres HD_DATA EQU 0x01F0 HD_MAIN EQU 0x01F1 HD_NBR_SECTOR EQU 0x01F2 HD_SECTOR EQU 0x01F3 HD_CYLINDREL EQU 0x01F4 HD_CYLINDREH EQU 0x01F5 HD_HEAD_DRIVE EQU 0x01F6 HD_STATUS EQU 0x01F7 ; Operations HD_RECALIBRATE EQU 0x10 HD_READ EQU 0x20 ; Masques HD_READY_FOR_READ_BIT EQU 0x10 HD_READY_BIT EQU 0x40 HD_BUSY_BIT EQU 0x80 ; Codes d'erreurs HD_ALL_OK EQU 0x00 HD_BUSY EQU 0x01 HD_NOT_READY EQU 0x02 HD_CANNOT_READ EQU 0x03 HD_DEVICE_FAULT EQU 0x04 ; Ca ce sont les arguments, on va les reutilise souvent Disque db 0 ;0 pour le premier disque, 1 pour le deuxieme Cylindre dw 0 ;numero de cylindre Head db 0 ;numero de la tete (0-16) Secteur db 1 ;numero du secteur NbrSecteurs db 1 ;nombre de secteurs a lire consecutivement (0=256) ;============================================================================ ; hd_read_ : Lis des secteurs. ; ; IN: Head db numero de la tete ; Secteur db numero de secteur ; Disque db numero du disque (1 ou 0) ; Cylindre dw numero du cylindre ; NbrSecteurs db nombre de secteurs a lire (0=256) ; edi--> buffer ; ; OUT: eax=statut ;============================================================================ hd_read_: cli pushad ; Save all registers. push ds call hd_fd_wait_ ; Wait for HDC not busy. jnc ir_ok0 ; Continue if not busy. mov al,HD_BUSY jmp ir_err ; ...done. ir_ok0:mov al,[Head] ; Get head number. mov ah,[Disque] ; Get drive number. and ax,010Fh ; Mask out extra bits. shl ah,04h ; Adjust AH. or al,ah ; Combine data. or al,0A0h ; Set 512 bytes + ECC. mov dx,HD_HEAD_DRIVE ; Write drive/head numbers... out dx,al ; ...done. call hd_fd_wait_ ; Wait for HDC not busy. jnc ir_ok1 ; Continue if not busy. mov al,HD_BUSY ; Error 1: Controller busy... jmp ir_err ; ...done. ir_ok1:mov ecx,000C0000h ; 3s delay. mov dx,HD_STATUS ; HDC status register. ir_l1: in al,dx ; Read status. test al,HD_READY_BIT ; Drive ready? jnz ir_ok2 ; Continue if so. loop ir_l1 ; Loop for 3s. mov al,HD_NOT_READY ; Error 2: Drive not ready... jmp ir_err ; ...done. ir_ok2:test al,HD_READY_FOR_READ_BIT ; Drive ready for read? jnz ir_ok3 ; Continue if so. loop ir_l1 ; Loop for 3s. mov al,HD_CANNOT_READ ; Error 3: Cannot read data... jmp ir_err ; ...done. ir_ok3:mov al,10h ; Set to >8 heads... mov dx,03F6h ; . out dx,ax ; ...done. mov dx,HD_NBR_SECTOR ; Write read parameters... mov al,[NbrSecteurs] out dx,al ; . inc dx ; . mov al,[Secteur] ; . out dx,al ; . inc dx ; . mov ax,[Cylindre] ; . out dx,al ; . inc dx ; . mov al,ah ; . out dx,al ; . inc dx ; . mov al,[Head] ; . mov ah,[Disque] ; . and ax,010Fh ; . shl ah,04h ; . or al,ah ; . or al,0A0h ; . out dx,al ; ...done. mov dx,HD_MAIN ; Write Precompensation = 0... xor al,al ; . out dx,al ; ...done. call hd_fd_wait_ ; Wait for HDC not busy. jnc ir_ok4 ; Continue if not busy. mov al,HD_BUSY ; Error 1: Controller busy... jmp ir_err ; ...done. ir_ok4:xor cx,cx ; Get sector count... mov cl,[NbrSecteurs] ; ...done. mov dx,HD_STATUS ; Send read command... mov al,HD_READ ; . out dx,al ; ...done. ir_l2: mov dx,HD_STATUS ; Get status port. delay 000Ah ; Delay for >400ns. in al,dx ; Get status. test al,HD_BUSY_BIT ; Busy? jnz ir_l2 ; Loop if so. test al,029h ; Loop if no change... jz ir_l2 ; ...done. test al,08h ; Ready for data? jnz ir_rda ; If so, read it. test al,21h ; Error in command? jnz ir_dev ; If so, return device error. jmp ir_l2 ; Continue loop. ir_rda:push cx ; Save CX. mov cx,0x100 ; Repeat count. mov dx,HD_DATA ; 16-bit transfer port. rep insw ; Read data. pop cx ; Restore CX. loop ir_l2 ; Loop until done. mov al,12h ; Deactivate controller... mov dx,03F6h ; . out dx,ax ; ...done. xor al,al ; No error - return 0... clc ; No error: CF=0. pop ds popad ; Restore all registers. sti ret ; Return. ir_dev:mov al,HD_DEVICE_FAULT ; Error 4: Device fault... mov dx,HD_MAIN ; Get error code... in al,dx ; . mov dx,HD_HEAD_DRIVE ; Recalibrate head... mov al,[Head] ; . mov ah,[Disque] ; . and ax,010Fh ; . shl ah,04h ; . or al,ah ; . or al,0A0h ; . out dx,al ; . inc dx ; . mov al,HD_RECALIBRATE ; . out dx,al ; ...done. call hd_fd_wait_ ; Wait for HDC not busy. mov al,12h ; Deactivate controller... mov dx,03F6h ; . out dx,ax ; ...done. ir_err: stc ; Error: CF=1. pop ds popad ; Restore all registers. sti ret ; Return. %macro delay 1 ; Long delay (for 400ns transition). push cx ; Save CX. mov cx,%1 ; Get repeat count. .loo: loop .loo ; Loop X times. pop cx ; Restore CX. %endm ; End of DELAY macro. hd_wait_: ; Attends que le hd soit prêt. push eax push ecx push edx mov ecx,00040000h ; 1s delay on 486DX-50. mov dx,HD_STATUS ; HDC status register. in al,dx ; Read status. test al,HD_BUSY_BIT ; Is the HDC busy? jz hhok ; If not, end immediately. hhloop: in al,dx ; Read status. delay 000Ah ; 50-100 clock delay. test al,HD_BUSY_BIT ; Is the HDC busy? jz hhok ; If not, end loop. loop hhloop ; Otherwise, continue. pop edx pop ecx pop eax ; Restore registers. stc ; Set CF. jmp hherr ; Exit with error. hhok: pop edx pop ecx pop eax clc ; Clear CF. hherr: ret ========>8========>8========>8========>8========>8========>8========>8========>8========>8 Voila, ca nous fait un joli driver disque. Je pense que ce code pourrait etre quelque peut accelere, mais en tout cas pour l'instant il marche parfaitement et ce sur tous les pc sur lesquels je l'ai teste. 3 ) UTILISER LE LBA -------------------- Voila, maintenant nous avons acces au disque sans APIs. Neanmoins, le mode d'adressage utilise, aussi appele P-CHS (pour physical Cylindre-Head- Sector) n'est pas le mode utilise par dos/Windows. Windows (et dos) utilisent un mode plus simple appele LBA, qui se resume en fait a acceder, le premier secteur ayant pour numero 0. Certains disques durs supportent directement ce mode d'adressage, mais un maximum de compatibilite etant souhaite, nous convertirons nous-meme. Pour cela, la methode est plutot simple: cylindre = LBA / (tete_par_cylindre * secteurs_pazr_piste) temp = LBA % (tete_par_cylindre * secteurs_par_piste) tete = temp / secteurs_par_piste secteur = temp % secteurs_par_piste + 1 Voila la fonction qui s'en charge: ;Quelques infos nécessaire sur la geométrie du disque. Disqueinfo: D1Cylindres dw 0 ;infos pour le HD nø1 D1Heads dw 0 D1Sectpartrack dw 0 D1Flags dw 0 D2Cylindres dw 0 ;infos pour le HD nø2 D2Heads dw 0 D2Sectpartrack dw 0 D2Flags dw 0 ;============================================================================ ; hd_lba2pchs_ : convertit un lba en p-chs ; ; IN: eax= LBA ; ; OUT: variables Head,Secteur et Cylindre ;============================================================================ hd_lba2pchs_: push ebx push edx push edi movzx edx,byte [Disque] lea edi,[D1Cylindres+edx*8] ;choisi selon le disque mov dx,[edi+4] ;edx=sectpertrack movzx ebx,word [edi+2] ;heads imul bx,dx ;ebx= sectpercylindre xor edx,edx div ebx mov [Cylindre],ax ;eax=nø de cylindre mov eax,edx mov bx,[edi+4] ;sectpertracks xor edx,edx div ebx mov [Head],al inc edx mov [Secteur],dl pop edi pop edx pop ebx ret Ce qui nous permet d'avoir une fonction qui lit un certain nombre de secteurs avec comme parametre le numéro LBA du secteur. C'est cette fonction qui sera toujours utiliser dans la gestion des FS: ;============================================================================ ; hd_LBAread_ : Lis des secteurs ; ; IN: eax= numéro LBA du secteur ; edi--> buffer ; NbrSecteurs db nombre de secteurs a lire ; ;============================================================================ hd_LBAread_: ;eax=LBA, ebx=nombre de secteurs ,edi-->buffer NbrSecteurs=... mov byte [NbrSecteurs],bl call hd_lba2pchs_ call hd_read_ ret Bien entendu, libre a vous de presenter les arguments autrement. Vous aurez remarque qu'un certain nombre d'infos sur le disque sont necessaires (il faut remplire Disqueinfo oui :). Ces infos sont accessibles via la commande ECh (celles ou il y a la petite etoile =). Comme je suis gentil, je vais vous montrer comment obtenir ces infos (bien entendu, c'est UNE maniere, et p-e pas la meilleure): ;============================================================================ ; hd_getvitalinfo_ : Lis des secteurs ; ; IN: edi--> structure DisqueInfo (voir plus haut) ; Disque contient le numéro du disque. ; ; OUT: eax=0 si erreur sinon structure DisqueInfo remplie ;============================================================================ hd_getvitalinfo_: push edi fsv1: mov edi,IO_buffer call hd_read_ call hd_fd_wait_ mov dx,HD_STATUS mov al,0xEC ; Get Drive Parameters out dx,al mov ecx,7 hd_wait1: call VESA_vretrace_ loop hd_wait1 mov edi,IO_buffer mov dx,0x1F0 ; Lit le buffer de données mov cx,0x100 ; pour avoir les caracteristiques du disque rep insw mov esi,IO_buffer pop edi mov ax,[esi+2] ; Nombres de cylindres test ax,ax ; si 0 alors erreur (ou disque non présent) jz fsver stosw mov ax,[esi+6] ; Nombre de têtes test ax,ax jz fsver stosw test ah,ah jnz fsver mov ax,[esi+12] ; Nombre de secteurs par piste (ou tête) stosw test ah,ah jz fsvfin fsver: xor eax,eax fsvfin: ret VESA_vretrace_: mov dx,0x3DA ; Attend la retrace verticale (temporisation quoi) ret1: in al,dx and al,8 jnz ret1 ret2: in al,dx and al,8 jz ret2 ret Voila. Vous devez etre en mesure de remplacer l'int 13h desormais, le plus dur est fait.