Capítulo 4: Estructura de FAT12 y lectura de disco


Nuestro disco ya tiene un sistema de archivos, pero no tiene ningún archivo dentro. Sus primeros 512 bytes contienen información crucial para iniciar FAT12, pero no hemos escrito ningún archivo como tal. La pregunta con la que empezaremos este capítulo es una de mis favoritas: ¿qué ocurre exactamente cuando creamos un archivo en un disco?

Tabla de contenidos


  1. Estructura básica de FAT12
  2. Geometría del disco
  3. Secciones de FAT12: La sección reservada
  4. Secciones de FAT12: La sección de FAT
  5. Secciones de FAT12: La carpeta raíz
  6. Secciones de FAT12: La sección de datos
  7. Resumen

Estructura básica de FAT12


Tras añadir los headers y formatearlo, nuestro disco ha pasado a tener 4 secciones. Este es un mapa de los primeros 24 kilobytes (48 sectores) del disco, en el que se ven las 3 primeras secciones y el inicio de la cuarta:

8 KB 16 KB 24 KB
Sección reservada
Sección de FAT
Carpeta raíz
Sección de datos

Puedes pasar el ratón por encima de la tabla para ver una descripción más detallada de cada elemento.

Si has decidido emular un floppy que no sea de 3 pulgadas y media o has cambiado los headers de alguna forma, es probable que las fronteras entre secciones sean ligeramente distintas, pero las secciones serán las mismas. También es importante aclarar que la sección de datos ocupa todo el resto del disco, no solamente los 7,5 KB que se ven en la tabla.

Si tienes curiosidad por un mapa del disco entero, aquí lo tienes:

360 KB 720 KB 1080 KB 1440 KB
Sección reservada
Sección de FAT
Carpeta raíz
Sección de datos
Sección de datos
Sección de datos

Puedes pasar el ratón por encima de la tabla para ver una descripción más detallada de cada elemento.

Geometría del disco


Dentro de nuestros headers hemos definido muchas variables interesantes. Ahora mismo nos interesan solo algunas, entre las que se incluyen:

La definición más importante de estas 3 es la que indica que un sector equivale a 512 bytes. Un sector es una porción de la memoria que funciona como unidad mínima de lectura, y su longitud depende de la geometría del disco.

Cuando leemos datos que están localizados en el disco, lo que hacemos cargarlos en alguna parte de la RAM, sector a sector. Es decir, si queremos leer un archivo que ocupe 1 KB, el proceso a realizar será el siguiente:

  1. Localizar alguna parte de la RAM que esté libre y tenga espacio para escribir todo el archivo: pongamos que nos quedamos con la dirección 0x50000.
  2. Leer el primer sector del archivo hacia 0x50000 y apuntar nuestro puntero hacia 0x50200 (512 bytes para delante).
  3. Leer el segundo sector del archivo hacia 0x50200, y terminar la operación de lectura.

Volveremos a esto cuando tengamos que leer archivos, pero por ahora lo más importante es que entiendas que no se puede leer el disco byte a byte, la unidad mínima de lectura es un sector.

Existen dos formas de definir la dirección de un sector dentro del disco. La más moderna y fácil es el LBA o Logical Block Addressing, y la más antigua (que usaremos únicamente en el modo real del procesador) es el CHS o Cylinder-Head-Sector. El LBA indica el sector en el que se encuentra una parte del disco si empezamos a contar desde cero. El boot sector sería 0, el siguiente 1, 2... y así hasta 2879. Su cálculo es sorprendentemente simple.

Para hablar del direccionamiento CHS hay que tratar primero la arquitectura de un disco duro. El esquema de la izquierda, creado por el usuario de Wikipedia LionKimbro, la ilustra muy bien.

El cabezal es la unidad más grande de las 3. En el caso de nuestro disco (3½" DSHD 1.44 MB) existen únicamente 2 cabezales.

Los cilindros son las secciones concéntricas que incluye un cabezal. En nuestro disco hay un total de 80 cilindros por cabezal.

Los sectores son secciones cónicas que atraviesan todos los cilindros del disco. En nuestro caso, existen 18 sectores por cilindro.

Lo lógico sería que el direccionamiento fuera Cabezal-Cilindro-Sector (o HCS), pero... sencillamente no es así. Así que habrá que acostumbrarse.

Existe una equivalencia clara entre los dos sistemas de direccionamiento. Por ejemplo, el LBA 0 apunta al mismo sector que el CHS 0, 0, 1; el LBA 1 es el CHS 0, 0, 2; el LBA 19 es el CHS 0, 1, 1... y así sucesivamente.

Este sistema está completamente obsoleto hoy en día, pero es necesario conocerlo porque vamos a utilizar la función de la BIOS INT 13/AH=02h para leer sectores del disco, y esta función exige que le proporciones la dirección CHS del disco que quieres leer. Crearemos un "traductor" de LBA a CHS para librarnos de este problema cuanto antes.

Secciones de FAT12: La sección reservada


Los headers que hemos definido y todo el código que escribamos en nuestra stage1 se encuentra en el boot sector, que es el primer sector de la sección reservada. Merece la pena destacar que este sector es lo único que hemos cargado a la RAM hasta ahora. Aquí tienes un mapa de dicho sector:

128 B 256 B 384 B 512 B
Headers
Nuestro código
Firma

Puedes pasar el ratón por encima de la tabla para ver una descripción más detallada de cada elemento.

Merece la pena destacar los últimos 2 bytes (en naranja), que contienen la firma necesaria para que la BIOS detecte nuestro dispositivo como iniciable. Es decir, contiene el valor 0x55AA.

Dentro de la sección reservada también puede haber una cantidad arbitraria de sectores reservados para el sistema. Estos sectores serían los siguientes partiendo desde el boot sector, aunque nosotros no hemos reservado ninguno. Esto se puede configurar mediante la variable bdb_reserved_sectors, a la que hemos dado un valor de 1 (para reservar únicamente el boot sector).

Secciones de FAT12: La sección de FAT


La sección de FAT (File Allocation Tables) es un mapa de la sección de datos, que está dividida en clusters. A continuación, explicaremos qué es un cluster, su papel en la sección de datos y cómo esto afecta a la sección de FAT. Se puede calcular su LBA en el disco de la siguiente forma:

/**
 * Recuerda:
 * bdb_reserved_sectors = Nº de sectores reservados.
 *
 * Está definido en el header de FAT12.
 */

int fatlba = bdb_reserved_sectors;

Puedes configurar cuántos sectores conforman un cluster usando la variable bdb_sectors_per_cluster, declarada en el header. Nosotros le hemos dado el valor de 1, por lo que un cluster es equivalente a un sector. Igualmente, debemos programar nuestro bootloader teniendo en cuenta que esto podría cambiar cuando nuestro sistema operativo crezca.

La sección de FAT incluye una lista enlazada que nos muestra los clusters que ocupa cada archivo. Una lista enlazada (o linked list) es una secuencia lineal en la que cada valor contiene el siguiente al que hay que apuntar. Aquí tienes una linked list básica:

0 1 2 3 4 5 6 7
1 2 3 7 5 100 4 6

Para leer la lista en orden, debemos empezar en el índice 0 y dirigirnos al índice que hay guardado ahí, luego al índice guardado en el siguiente valor, y así sucesivamente. Vamos a asumir que el valor 100 marca el fin de la lista. En ese caso, el orden sería el siguiente:

0 1 2 3 7 6 4 5 [FIN DE LISTA]

Para entender cómo se traslada todo esto a la data section, vamos a imaginarnos una. Estos son los primeros 16 clusters de una sección de datos hipotética:

0x0 0x1 0x2 0x3 0x4 0x5 0x6 0x7
archivo.txt carpeta
0x8 0x9 0xA 0xB 0xC 0xD 0xE 0xF
programa.bin imagen.png

Aquí tenemos un archivo de texto (archivo.txt) que ocupa 6 clusters, una carpeta que ocupa 2 clusters, un ejecutable de 4 clusters y un archivo de imagen que ocupa otros 4. La sección de FAT resume esta información para que el sistema no tenga que recorrerse la data section entera si quiere buscar un archivo.

Teniendo en cuenta que los finales de archivo se suelen marcar con el número 0xFFF, la sección de FAT sería así:

0x0 0x1 0x2 0x3 0x4 0x5 0x6 0x7
0x001 0x002 0x003 0x004 0x005 0xFFF 0x007 0xFFF
0x8 0x9 0xA 0xB 0xC 0xD 0xE 0xF
0x009 0x00A 0x00B 0xFFF 0x00D 0x00E 0x00F 0xFFF

Si te fijas, cada valor nos indica el siguiente cluster de un archivo, hasta que llegamos al valor 0xFFF, que nos indica el cluster final de un archivo. Si no lo ves claro, compara las dos tablas y verás a lo que me refiero.

A lo mejor te preguntas por qué los números de la tabla tienen 3 cifras. Bueno, te presento una de las particularidades más molestas de FAT12. Los números incluidos en una FAT ocupan 12 bits cada uno. 1,5 bytes. Esto genera un problema de lectura que tendremos que solucionar más tarde. Por ahora solo tienes que tener en cuenta que esa tablita tan clara de arriba, al verla en el disco dividida por bytes, se ve de esta forma:

0x0 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xA 0xB
0x01 0x20 0x00 0x03 0x40 0x00 0x05 0xF0 0xFF 0x07 0x80 0x00
0xC 0xD 0xE 0xF 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
0x09 0xA0 0x00 0x0B 0xC0 0x00 0x0D 0xE0 0x00 0x0F 0xF0 0xFF

Explicamos cómo leer tablas como esta más adelante, pero si te puede la curiosidad, aquí te dejo la explicación de este fenómeno incluida en el artículo de Wikipedia del sistema de archivos FAT.

Secciones de FAT12: La carpeta raíz


Esta sección es mucho más simple que las anteriores. Simplemente, muestra la carpeta raíz del sistema de archivos. En Linux, esto serían los contenidos de la carpeta /. El equivalente en Windows es la carpeta C:. Se puede calcular su LBA en el disco de la siguiente forma:

/**
 * Recuerda:
 * fatlba = Definida en la sección anterior
 *
 * bdb_sectors_per_fat = Sectores que ocupa cada FAT
 * bdb_fat_count = Nº de FATs en el disco
 *
 * Estas 2 últimas están definidas en el header de FAT12.
 */

int rootlba = fatlba + (bdb_sectors_per_fat * bdb_fat_count);

En este sistema de archivos, una carpeta se representa como una sucesión de entradas de 32 bytes cada una, que definen el nombre de un archivo, sus atributos y la localización de su contenido dentro de la data section. Estos son los contenidos de una entrada, byte por byte:

0x0 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xA 0xB 0xC 0xD 0xE 0xF
nombre
Nombre del archivo (8 bytes, rellenados con espacios)

Ejemplo: FILE    .
extension
Extensión del archivo (3 bytes, rellenados con espacios)

Ejemplo: TXT.
atr
Atributos: 1 byte en el que cada bit define una propiedad.
  1. Reservado
  2. Dispositivo
  3. Archive bit
  4. Subcarpeta
  5. Etiqueta de dispositivo
  6. Archivo de sistema
  7. Oculto
  8. Solo lectura

Ejemplo: para un archivo de sistema en modo solo lectura, 00000101.
_res
Byte reservado para el sistema
Cms
Milisegundos de la creación del archivo
Ctime
Hora, minutos y segundos de la creación del archivo
  1. Hora (5 bits)
  2. Minutos (6 bits)
  3. Segundos/2 (5 bits)

Ejemplo: 01100 100010 11100.
0x0 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xA 0xB 0xC 0xD 0xE 0xF
Cdate
Fecha de creación
  1. Año (desde 1980, 7 bits)
  2. Mes (1-12, 4 bits)
  3. Día (1-31, 5 bits)

Ejemplo: 0000000 0000 00000.
accessed
Fecha del último acceso, formateada igual que Cdate.
Clhigh
Primer cluster del archivo (solo sus primeros 8 bits)

Ejemplo: para el cluster nº 00001111 11110000, este valor sería 00001111.
Mtime
Hora de modificación, formateada igual que Ctime.
Mdate
Fecha de modificación, formateada igual que Cdate.
Cllow
Primer cluster del archivo (solo sus últimos 8 bits)

Ejemplo: para el cluster nº 00001111 11110000, este valor sería 11110000.
size
Tamaño del archivo en bytes
Ejemplo: para un archivo de 15 bytes, este valor sería 00000000 00000000 00000000 00001111.

Puedes pasar el ratón por encima de la tabla para ver una descripción más detallada de cada elemento.

La carpeta raíz contiene muchas de estas entradas. Para ser exactos, contiene el número de entradas definido en la variable bdb_dir_entries_count, localizada en nuestro header. Si sigues la configuración del capítulo anterior, esta variable tendrá el valor 224, por lo que el directorio raíz ocupará exactamente 7168 bytes (224 * 32).

Secciones de FAT12: La sección de datos


Esta sección contiene el contenido de los archivos del disco, y está dividida en clusters, que son grupos de sectores contiguos. Se puede definir cuántos sectores ocupa cada cluster en la variable bdb_sectors_per_cluster, definida en el header. También podemos calcular el LBA de esta sección de la siguiente forma:

/**
 * Recuerda:
 * rootlba = Definida en el apartado anterior
 *
 * bdb_dir_entries_count = Entradas del directorio raíz
 * bdb_bytes_per_sector = Bytes por sector
 *
 * Estás 2 últimas variables están definidas en el header.
 */

/**
 * rootsize = Tamaño de la carpeta raíz en bytes
 * rootsec = Tamaño de la carpeta raíz en sectores
 */
int rootsize = bdb_dir_entries_count * 32;
int rootsec = (rootsize + bdb_bytes_per_sector - 1) / bdb_bytes_per_sector;

/* datalba = LBA de la sección de datos */
int datalba = rootlba + rootsec;

Vamos a usar esta sección para consultar el contenido de un archivo. Por ejemplo, pongamos que el disco contiene en su raíz un archivo llamado ejemplo.txt y su contenido ocupa 1.500 bytes. En algún punto de la carpeta raíz encontraremos la siguiente entrada:

0x0 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xA 0xB 0xC 0xD 0xE 0xF
'E' 'J' 'E' 'M' 'P' 'L' 'O' ' ' 'T' 'X' 'T' 0x20 0x18 -- --
0x0 0x1 0x2 0x3 0x4 0x5 0x6 0x7 0x8 0x9 0xA 0xB 0xC 0xD 0xE 0xF
-- -- 0x0000 -- -- 0x001C 0x000005DC

Podemos ver que Clhigh tiene el valor 0x0000 y Cllow tiene el valor 0x001C. Si juntamos el número de 32 bits completo, nos da el siguiente valor:

Hexadecimal Decimal
0x0000001C 28

Este valor nos indica el cluster en el que se encuentra el archivo. Los clusters se pueden traducir directamente a LBA de forma muy sencilla:

int LBA = lba_de_la_data_section + ((cluster * sectores_por_cluster) - 2);

Si hacemos la cuenta (33 + (28 * 1) = 61) sabremos que el primer sector del archivo está escrito en el sector nº 61 del disco. Pero en nuestro caso un sector son solo 512 bytes... ¿qué pasa con los archivos que pesan más de un sector? Pues, en ese caso, habría que consultar la FAT para saber dónde está el siguiente cluster del archivo.

Pongamos que la FAT de nuestro disco contiene las siguientes entradas:

0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B
0xFF0 0xFFF 0x003 0x004 0x005 0xFFF 0x007 0x008 0x009 0x00A 0x00B 0x00C
0x0C 0x0D 0x0E 0x0F 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
0x00D 0xFFF 0x00F 0x016 0x011 0x012 0x013 0x014 0x015 0xFFF 0x017 0xFFF
0x18 0x19 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F 0x20 0x21 0x22 0x23
0x019 0x01A 0x01B 0xFFF 0x022 0x01E 0x01F 0x020 0x021 0xFFF 0x023 0xFFF

Las 2 primeras entradas están en verde porque son entradas reservadas. No indican el contenido de ningún cluster de la sección de datos, solamente varían en función de si el disco está particionado o no. Después, las entradas indican el contenido de un archivo de 4 clusters y otro de 8. Tras estos, podemos ver que el archivo que comienza en el cluster 0x0E, marcado en azul oscuro, está fragmentado.

Si seguimos su cadena de clusters, obtendremos la siguiente lista: 0xE, 0xF, 0x16, 0x17. El archivo ocupa 4 clusters que no son contiguos en la memoria, por eso decimos que está fragmentado. En los clusters que se encuentran en medio, encontramos los datos de otro archivo. Si seguimos, tras otro archivo que ocupa 4 clusters, llegamos al índice que buscábamos: el 0x1C.

El archivo que buscamos está marcado en azul claro y también está fragmentado. Si queremos leer su contenido tendremos que leer, en orden, los clusters 0x1C, 0x22 y 0x23. Lo que en nuestro disco equivale a leer los sectores 61, 67 y 68.

Estos 3 clusters suman en nuestro disco 1.536 bytes, pero nuestro archivo ocupa 1.500 bytes. Por eso, los últimos 36 bytes del cluster 0x23 tendrán el valor 0. Al leerlo, conviene consultar el tamaño del archivo en su entrada para asegurarnos de que leemos el número de bytes correcto.

Resumen


Hemos visto en qué consisten las secciones reservada, de FAT, de directorio raíz y de datos de FAT12. También hemos aprendido cómo calcular su posición en el disco y cómo usarlas para leer el contenido de un archivo. En el proceso, hemos aprendido sobre la fragmentación de disco y los sistemas de direccionamiento LBA y CHS.

En el siguiente capítulo aprenderemos cómo leer parámetros del disco y cómo leer sus sectores hacia la memoria. Para ello, necesitarás tener conocimientos mínimos de assembly (como los explicados en el capítulo 1) y del sistema de archivos FAT12 (como los tratados en este capítulo).