← Sistemas de Archivos y E/SBloque 11 · Sistemas de archivos reales · Tema 36 de 50

Tema 36

FAT

Una entrada de directorio proporciona el comienzo de un archivo; una tabla permite seguir sus clústeres. Estudiaremos cómo convertir esas referencias en posiciones de una imagen, qué cambia entre FAT12, FAT16, FAT32 y exFAT y cómo detectar cadenas incoherentes. Dos programas en C, una imagen reproducible y dos simuladores conectarán los bytes del formato con el contenido del archivo.

1. La tabla que enlaza los clústeres

Después de estudiar protocolos de publicación, comenzamos a observar formatos concretos. FAT significa File Allocation Table, tabla de asignación de archivos. En el modelo clásico, el archivo no necesita ocupar una única región contigua: cada entrada de la tabla indica cómo continuar su cadena o que esta terminó.

Clúster: unidad de asignación formada por uno o más sectores del volumen. Cadena FAT: secuencia de clústeres que se obtiene partiendo de un índice inicial y consultando la entrada correspondiente de la tabla.

Orden lógico y ubicación de un archivo fragmentadoEl directorio indica clúster inicial 2 y tamaño 1200. La tabla enlaza 2 con 5, 5 con 3 y 3 con fin de cadena. Con clústeres de 512 bytes se utilizan 512, 512 y 176 bytes. DATOS.BINinicio 2 · tamaño 1200 2 → 5 5 → 33 → EOC Orden lógico: datos de 2, después de 5, después de 3.512 + 512 + 176 = 1200 bytes útilesLos 336 bytes restantes del último clúster no son parte del archivo.
Los números de clúster no son posiciones lógicas de bytes dentro del archivo.

La entrada del directorio inicia el recorrido. Los enlaces de la FAT ordenan las unidades físicas. El tamaño lógico determina cuántos bytes son contenido y evita entregar como archivo el relleno de la última unidad.

2. Sectores, clústeres y regiones del volumen

volumen FAT12/16 · orden conceptual, no a escala
sector inicial del volumen
    región reservada: incluye sector de arranque y BPB
    primera FAT
    otras copias de la FAT, si existen
    región fija del directorio raíz
    región de datos: clúster 2, clúster 3, clúster 4, ...

Un sector del volumen es una unidad declarada por el formato. No debe suponerse que coincide con el sector físico del dispositivo ni con una página de memoria. Los clústeres de datos comienzan a numerarse en 2; las entradas 0 y 1 de la tabla tienen usos reservados.

Una imagen puede contener solo un volumen o un disco con tabla de particiones. En el segundo caso, antes de interpretar el BPB hay que localizar el comienzo de la partición. El programa de este tema acepta únicamente una imagen de volumen que comienza en el byte cero y rechaza dispositivos: no analiza MBR ni GPT.

Esta distinción evita sumar dos veces el desplazamiento de la partición o leer una tabla de particiones como si fuera un BPB. El campo de sectores ocultos no convierte por sí solo una imagen de volumen en una imagen de disco completo.

3. Leer el BPB sin convertir bytes en una estructura nativa

El BIOS Parameter Block describe la geometría necesaria para localizar las regiones. Los enteros multibyte se decodifican como little-endian. No se hace un cast del buffer a una estructura C: el relleno, la alineación y el orden de bytes de la máquina no definen el formato del disco.

Campo del BPB clásicoDesplazamiento en bytesUso
BytsPerSec11 · 2 bytesBytes por sector.
SecPerClus13 · 1 byteSectores por clúster.
RsvdSecCnt14 · 2 bytesSectores de la región reservada.
NumFATs16 · 1 byteNúmero de tablas.
RootEntCnt17 · 2 bytesEntradas del raíz fijo en FAT12/16.
TotSec16 / TotSec3219 / 32Total de sectores, según el campo aplicable.
FATSz1622 · 2 bytesSectores por FAT en FAT12/16.
Firma510 y 511Bytes 0x55 y 0xAA; no prueban por sí solos validez.

Para implementar un lector es necesario consultar la especificación FAT de Microsoft. Una firma reconocible no reemplaza las comprobaciones de geometría, capacidad de la tabla y límites del archivo que contiene la imagen.

Como referencia de una implementación real, boot.c de dosfstools muestra la importancia de validar los parámetros antes de utilizarlos. El lector didáctico admite un subconjunto declarado y no intenta corregir un encabezado ambiguo.

4. Calcular el comienzo de los datos

fórmulas · todas las posiciones siguientes son relativas al comienzo del volumen
RootDirSectors = techo(RootEntCnt × 32 / BytsPerSec)
FirstRootSector = RsvdSecCnt + NumFATs × FATSz
FirstDataSector = FirstRootSector + RootDirSectors
DataSectors = TotalSectors - FirstDataSector
CountOfClusters = piso(DataSectors / SecPerClus)

ClusterBytes = BytsPerSec × SecPerClus
FirstSectorOfCluster(N) = FirstDataSector + (N - 2) × SecPerClus
ByteOffset(N) = FirstSectorOfCluster(N) × BytsPerSec

El redondeo hacia arriba reserva sectores completos para las entradas del raíz. La división del área de datos redondea hacia abajo: un resto de sectores que no completa un clúster no añade otra unidad de asignación.

geometría exacta de la imagen del laboratorio
BytsPerSec = 512       SecPerClus = 1
RsvdSecCnt = 1         NumFATs = 2
FATSz = 128            RootEntCnt = 512
TotalSectors = 32768

RootDirSectors = 32
FirstRootSector = 257
FirstDataSector = 289
CountOfClusters = 32768 - 289 = 32479
Offset del clúster 2 = 147968 bytes
Offset del clúster 5 = 149504 bytes
Offset del clúster 3 = 148480 bytes

Antes de restar hay que comprobar que las regiones caben en el total. Antes de leer hay que comprobar que el total declarado cabe en la imagen real. Los cálculos del ejemplo C usan uint64_t para no desbordar multiplicaciones intermedias de campos pequeños.

5. Determinar FAT12, FAT16 o FAT32

En FAT clásico, el número de clústeres de datos determina la variante; la cadena de texto que dice FAT16 o FAT32 no es una prueba suficiente.

VarianteClústeres de datosRepresentación de entradas
FAT12Menos de 4085.12 bits empaquetados; dos entradas ocupan tres bytes.
FAT16Desde 4085 y menos de 65525.16 bits por entrada.
FAT32Desde 65525, dentro de los límites del formato.32 bits almacenados; se usan los 28 inferiores para el valor de la cadena.

Los límites son estrictos: 4084 y 4085 no pertenecen a la misma variante. Esto no significa que todos los patrones de bits sean índices utilizables: hay valores especiales reservados. El lector comprueba también el rango de cada índice antes de seguirlo.

El tamaño de un archivo FAT clásico se almacena en un campo de 32 bits sin signo: su máximo representable es 4 GiB menos un byte. No hay que confundir ese límite por archivo con la capacidad del volumen ni con los límites que impone una herramienta concreta al formatear.

6. Entradas libres, enlaces, reservas y fin de cadena

InterpretaciónFAT12FAT16FAT32, 28 bits
Libre0x0000x00000x0000000
Marcador de clúster defectuoso0xFF70xFFF70x0FFFFFF7
Fin de cadena, EOC0xFF8–0xFFF0xFFF8–0xFFFF0x0FFFFFF8–0x0FFFFFFF
Reservados altos0xFF0–0xFF60xFFF0–0xFFF60x0FFFFFF0–0x0FFFFFF6

Un valor no especial puede ser el siguiente índice, pero también debe caer dentro del área de datos del volumen. El valor 1 no es un enlace de datos válido. Un enlace libre, reservado o defectuoso dentro de un archivo no se interpreta como final normal.

Para FAT16, la entrada N comienza a 2 × N bytes desde el inicio de la FAT. La implementación debe comprobar que esos dos bytes caben en ella. fat.c de dosfstools permite contrastar el tratamiento de las distintas anchuras y de sus valores especiales.

Las copias de la FAT son metadatos de asignación redundantes, no copias de cada bloque de contenido. Si difieren, no basta con elegir siempre la primera y declarar sano el volumen. Nuestro lector rechaza diferencias en las entradas que recorre; no compara íntegramente todas las tablas.

7. Del directorio al primer clúster

Una entrada corta de FAT clásico ocupa 32 bytes. Contiene nombre, atributos, fechas, primer clúster y tamaño. En FAT16, para el archivo del ensayo tomamos el primer clúster de los bytes 26–27 y el tamaño de los bytes 28–31.

entrada corta del laboratorio · nombre almacenado en once bytes
Nombre base:    "DATOS   "    8 bytes
Extensión:      "BIN"         3 bytes
Atributos:      0x20          archivo, atributo archive
Primer clúster: 2
Tamaño:         1200

Nombre visible de la práctica: DATOS.BIN
Clave corta que compara el lector: "DATOS   BIN"

El primer byte 0x00 indica que no hay más entradas utilizadas después de ese punto. El marcador 0xE5 indica una entrada eliminada. Los registros de nombre largo tienen otro tratamiento; no deben interpretarse como archivos independientes.

En FAT12/16 el raíz tiene una región fija; los subdirectorios usan cadenas en el área de datos. Por eso el lector encuentra un nombre corto del raíz sin implementar navegación recursiva. No es un explorador general ni reconstruye archivos eliminados.

8. Nombres cortos, nombres largos y atributos

VFAT incorpora entradas auxiliares para nombres largos asociadas con una entrada corta. Un lector completo valida su secuencia y su relación con la entrada correspondiente. Saltarlas es una limitación válida para este ensayo, pero no equivale a implementar nombres largos.

Los atributos de FAT no son una representación de propietario, grupo y permisos POSIX. Linux puede presentar esos valores mediante opciones de montaje como uid, gid y máscaras. Véase la documentación de VFAT del kernel. No se debe deducir que cada chmod queda representado como en ext4.

El ejemplo busca exactamente el alias corto DATOS.BIN en mayúsculas y evita problemas de conversión de caracteres. También omite marcas de tiempo en su salida: para interpretarlas habría que considerar sus campos y las convenciones de zona horaria aplicables.

9. Particularidades de FAT32

FAT32 no consiste únicamente en leer entradas de cuatro bytes. Cambian campos del BPB, se obtiene el tamaño de la tabla de FATSz32 y el raíz se localiza mediante RootClus: ya no ocupa la región fija del raíz FAT12/16.

Al interpretar una entrada se enmascaran los cuatro bits superiores con 0x0FFFFFFF. Los indicadores del volumen también pueden señalar si se reflejan las tablas o cuál está activa. Un programa que aplica siempre la política de FAT16 puede usar una copia incorrecta.

FSInfo almacena información auxiliar sobre espacio libre y búsqueda de asignación. No sustituye la FAT como fuente de enlaces y sus contadores requieren validación. El lector del tema rechaza FAT32, en lugar de intentar usar el raíz fijo y producir resultados aparentemente razonables.

10. exFAT: bitmap y asignación contigua

exFAT tiene un formato propio. Un bitmap registra la asignación de clústeres y sus entradas de directorio se organizan en conjuntos. El flujo incluye tamaños de 64 bits y datos que determinan cómo localizar su almacenamiento.

Cuando NoFatChain está activo, la asignación contigua permite calcular el recorrido a partir del primer clúster y la longitud sin seguir enlaces FAT. En otros casos se utiliza una cadena. Aplicar siempre el algoritmo clásico sería incorrecto.

ValidDataLength distingue los bytes inicializados dentro de DataLength. La parte posterior a la longitud válida debe exponerse como ceros en la lectura lógica; un lector de bytes crudos no puede entregar como contenido válido lo que encuentre allí.

Los checksums de ciertas estructuras de exFAT no constituyen una verificación general de todos los datos del usuario como la estudiada en otros sistemas. El alcance preciso está definido en la especificación oficial de exFAT.

11. Fragmentación, tamaño e inconsistencias

Con clústeres de 512 bytes, un archivo de 1200 necesita tres unidades, aunque no sean consecutivas. La capacidad asignada es 1536 bytes y el espacio restante en la última unidad es fragmentación interna. La secuencia 2 → 5 → 3 añade falta de continuidad física, un concepto diferente.

SituaciónQué debe detectar un lector acotado
La cadena termina antes de cubrir el tamaño.No hay suficientes clústeres para el archivo declarado.
La cadena continúa tras los clústeres esperados.Hay asignación adicional que el lector estricto del tema rechaza.
Un índice vuelve a aparecer.Existe un ciclo; no continuar indefinidamente.
Un enlace sale del rango de datos.No convertirlo en un acceso fuera de la imagen.
Dos archivos comparten un clúster sin corresponder.Es una referencia cruzada; requiere examinar más de una cadena.

Los nombres eliminados, los clústeres huérfanos y las referencias cruzadas exigen una revisión global que este lector no realiza. Que DATOS.BIN pueda recorrerse no demuestra que todo el volumen esté consistente.

FAT clásico no incorpora el journal de ext4 ni checksums generales de contenido. La integridad depende de mantener coherentes tabla, directorios y datos. Los protocolos del tema 35 siguen necesitando soporte y comprobación de las operaciones de persistencia del proveedor concreto.

12. Programa en C: desempaquetar FAT12

Guarda entradas-fat12.c. Dos valores de 12 bits se empaquetan en tres bytes. Para una entrada par se toman los doce bits inferiores de una ventana de dieciséis; para una impar, los doce superiores. El lector comprueba que la ventana cabe en el buffer.

entradas-fat12.c · demostración sobre bytes conocidos, sin acceder a discos
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>

static int leer12(const uint8_t *fat, size_t bytes, uint32_t indice,
                  uint16_t *valor)
{
    uint64_t offset = (uint64_t)indice + indice / 2;
    if (offset >= bytes || bytes - (size_t)offset < 2) return -1;
    uint16_t par = (uint16_t)((uint16_t)fat[offset] |
                             ((uint16_t)fat[offset + 1] << 8));
    *valor = (uint16_t)((indice & 1) ? par >> 4 : par & 0x0fff);
    return 0;
}

int main(void)
{
    const uint8_t fat[] = {0xf8, 0xff, 0xff, 0xbc, 0xfa, 0xde};
    for (uint32_t i = 2; i <= 3; i++) {
        uint16_t valor;
        if (leer12(fat, sizeof fat, i, &valor) == -1) return 1;
        printf("FAT[%u] = 0x%03x\n", (unsigned)i, (unsigned)valor);
    }
    uint16_t fuera;
    if (leer12(fat, sizeof fat, 4, &fuera) != -1) return 1;
    puts("Acceso fuera del buffer rechazado.");
    return 0;
}

El resultado esperado es FAT[2] = 0xabc y FAT[3] = 0xdef. Este ejemplo explica empaquetado; no afirma que esos índices sean válidos para un volumen determinado. Una entrada FAT12 puede cruzar un límite de sector: un lector por sectores tendría que proporcionar ambos bytes.

13. Programa en C: recorrer y leer DATOS.BIN en FAT16

Guarda leer-fat16.c. Abre en lectura una imagen regular y estable, valida su geometría y busca el alias corto DATOS.BIN en el raíz. Recorre exactamente los clústeres necesarios para el tamaño, compara las entradas usadas de las copias FAT y lee únicamente los bytes útiles del archivo.

Calcula FNV-1a de 32 bits como huella didáctica del contenido lógico, no como verificación criptográfica ni como checksum almacenado por FAT. Su salida permite comprobar que leer 2, 5 y 3 en ese orden reproduce los bytes del archivo original.

leer-fat16.c · lector acotado para imágenes de volumen FAT16
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

static int imagen;
static uint64_t longitud;

static void error(const char *mensaje)
{
    fprintf(stderr, "%s\n", mensaje);
    exit(EXIT_FAILURE);
}

static uint16_t le16(const unsigned char *p)
{
    return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}

static uint32_t le32(const unsigned char *p)
{
    return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
           ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}

static void leer(uint64_t offset, void *destino, size_t bytes)
{
    if (offset > longitud || bytes > longitud - offset)
        error("Lectura fuera de la imagen.");
    unsigned char *p = destino;
    while (bytes > 0) {
        ssize_t n = pread(imagen, p, bytes, (off_t)offset);
        if (n > 0) { p += n; bytes -= (size_t)n; offset += (uint64_t)n; }
        else if (n == -1 && errno == EINTR) continue;
        else {
            if (n == -1) perror("pread");
            error("No se pudo completar la lectura.");
        }
    }
}

static int potencia2(uint32_t n)
{
    return n != 0 && (n & (n - 1)) == 0;
}

int main(int argc, char **argv)
{
    if (argc != 2) { fprintf(stderr, "Uso: %s volumen.img\n", argv[0]); return 2; }
    imagen = open(argv[1], O_RDONLY | O_CLOEXEC | O_NOFOLLOW);
    if (imagen == -1) { perror("open"); return 1; }
    struct stat st;
    if (fstat(imagen, &st) == -1) { perror("fstat"); return 1; }
    if (!S_ISREG(st.st_mode) || st.st_size < 512)
        error("Se requiere una imagen regular de al menos 512 bytes.");
    longitud = (uint64_t)st.st_size;
    unsigned char boot[512];
    leer(0, boot, sizeof boot);
    if (!memcmp(boot + 3, "EXFAT   ", 8)) error("exFAT no soportado por este lector.");
    if (boot[510] != 0x55 || boot[511] != 0xaa) error("Firma de arranque incorrecta.");
    uint32_t bps = le16(boot + 11), spc = boot[13];
    uint32_t reservados = le16(boot + 14), tablas = boot[16];
    uint32_t entradas = le16(boot + 17), sectores_fat = le16(boot + 22);
    uint32_t total = le16(boot + 19);
    if (total == 0) total = le32(boot + 32);
    if (!potencia2(bps) || bps < 512 || bps > 4096 ||
        !potencia2(spc) || spc > 128 || reservados == 0 ||
        (tablas != 1 && tablas != 2) || entradas == 0 || sectores_fat == 0)
        error("BPB incompatible con el subconjunto FAT16 admitido.");
    uint64_t raiz_sectores = ((uint64_t)entradas * 32 + bps - 1) / bps;
    uint64_t raiz = reservados + (uint64_t)tablas * sectores_fat;
    uint64_t datos = raiz + raiz_sectores;
    if (datos >= total || (uint64_t)total * bps > longitud)
        error("Geometria fuera del volumen o imagen truncada.");
    uint64_t cantidad = ((uint64_t)total - datos) / spc;
    if (cantidad < 4085 || cantidad >= 65525)
        error("El conteo de clusters no corresponde a FAT16.");
    uint64_t fat_bytes = (uint64_t)sectores_fat * bps;
    if ((cantidad + 2) * 2 > fat_bytes) error("La FAT no tiene capacidad suficiente.");
    uint64_t cluster_bytes = (uint64_t)bps * spc;
    printf("FAT16: clusters=%" PRIu64 " cluster_bytes=%" PRIu64
           " first_data_sector=%" PRIu64 "\n", cantidad, cluster_bytes, datos);

    unsigned char entrada[32];
    uint32_t primero = 0, tamano = 0;
    int encontrado = 0;
    for (uint32_t i = 0; i < entradas; i++) {
        leer(raiz * bps + (uint64_t)i * 32, entrada, sizeof entrada);
        if (entrada[0] == 0) break;
        if (entrada[0] == 0xe5 || entrada[11] == 0x0f || (entrada[11] & 0x18))
            continue;
        if (!memcmp(entrada, "DATOS   BIN", 11)) {
            if (encontrado) error("Alias DATOS.BIN duplicado en el raiz.");
            encontrado = 1;
            primero = le16(entrada + 26); tamano = le32(entrada + 28);
        }
    }
    if (!encontrado) error("No se encontro DATOS.BIN en el raiz.");
    uint64_t necesarios = ((uint64_t)tamano + cluster_bytes - 1) / cluster_bytes;
    if (necesarios > cantidad) error("Tamano mayor que el area de datos.");
    if (tamano == 0 && primero != 0) error("Archivo vacio con asignacion inesperada.");
    printf("DATOS.BIN: first=%u size=%" PRIu32 "\n", (unsigned)primero, tamano);
    unsigned char *visitado = calloc((size_t)cantidad + 2, 1);
    if (!visitado) { perror("calloc"); return 1; }
    uint32_t actual = primero, huella = UINT32_C(2166136261);
    uint64_t pendientes = tamano;
    for (uint64_t i = 0; i < necesarios; i++) {
        if (actual < 2 || actual > cantidad + 1 || actual >= 0xfff0)
            error("Indice fuera de los clusters de datos utilizables.");
        if (visitado[actual]) error("Ciclo en la cadena.");
        visitado[actual] = 1;
        uint64_t offset = (datos + (uint64_t)(actual - 2) * spc) * bps;
        uint64_t utiles = pendientes < cluster_bytes ? pendientes : cluster_bytes;
        printf("cluster=%u offset=%" PRIu64 " utiles=%" PRIu64 "\n",
               (unsigned)actual, offset, utiles);
        unsigned char buffer[4096];
        for (uint64_t leidos = 0; leidos < utiles; ) {
            size_t n = utiles - leidos < sizeof buffer ?
                       (size_t)(utiles - leidos) : sizeof buffer;
            leer(offset + leidos, buffer, n);
            for (size_t j = 0; j < n; j++) {
                huella ^= buffer[j]; huella *= UINT32_C(16777619);
            }
            leidos += n;
        }
        pendientes -= utiles;
        uint16_t siguiente = 0;
        for (uint32_t copia = 0; copia < tablas; copia++) {
            unsigned char par[2];
            leer(((uint64_t)reservados + (uint64_t)copia * sectores_fat) * bps +
                 (uint64_t)actual * 2, par, sizeof par);
            if (copia == 0) siguiente = le16(par);
            else if (siguiente != le16(par)) error("Copias FAT diferentes en la cadena.");
        }
        if (siguiente == 0) error("Cluster libre dentro de la cadena.");
        if (siguiente == 0xfff7) error("Marcador de cluster defectuoso.");
        if (siguiente == 1 || (siguiente >= 0xfff0 && siguiente <= 0xfff6))
            error("Valor reservado dentro de la cadena.");
        if (i + 1 == necesarios) {
            if (siguiente < 0xfff8) error("La cadena continua despues del tamano esperado.");
        } else {
            if (siguiente >= 0xfff8) error("Fin de cadena prematuro.");
            actual = siguiente;
        }
    }
    printf("FNV32=%08" PRIx32 " bytes=%" PRIu32 "\n", huella, tamano);
    free(visitado);
    if (close(imagen) == -1) { perror("close"); return 1; }
    return 0;
}

Los errores terminan el proceso; el sistema operativo libera los descriptores y memoria que aún conserve. Ningún camino abre la imagen para escritura. Es necesario mantenerla estable durante toda la lectura: abrir un descriptor no congela su contenido.

El lector es estricto respecto al número de clústeres y a los archivos vacíos. Un volumen con preasignación adicional puede ser rechazado por esa política sin que se haya probado una corrupción global. Tampoco se validan todas las entradas reservadas, fechas, directorios ni referencias entre archivos.

14. Laboratorio con una imagen propia y determinista

Usa Linux con compilador C y Python 3. No hacen falta privilegios ni montar la imagen. El generador crea un volumen FAT16 de práctica de 16 MiB lógicos, con dos tablas iguales y una cadena fragmentada conocida. No es un formateador general ni genera un sistema arrancable.

Bash · guardar previamente los dos fuentes C
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 entradas-fat12.c -o entradas-fat12
$ cc -std=c11 -Wall -Wextra -Wpedantic -O2 leer-fat16.c -o leer-fat16
$ BIN36="$PWD"
$ LAB36=$(mktemp -d "$HOME/fat36.XXXXXX")
$ "$BIN36/entradas-fat12"

Construir un volumen cuyo mapa conocemos

La creación exclusiva evita sobrescribir un archivo existente. El relleno del último clúster se fija a 0xCC para demostrar que no pertenece a los 1200 bytes del archivo. También se guarda el contenido lógico esperado fuera de la imagen.

Python 3 · imagen de volumen, sin MBR ni GPT
$ python3 - "$LAB36" <<'PY'
from pathlib import Path
import struct
import sys

base = Path(sys.argv[1])
bps, total, fat_sectors, root_entries = 512, 32768, 128, 512
first_root = 1 + 2 * fat_sectors
first_data = first_root + root_entries * 32 // bps
contenido = bytes((i * 17 + 3) % 256 for i in range(1200))
with (base / 'esperado.bin').open('xb') as f:
    f.write(contenido)
boot = bytearray(bps)
boot[0:3] = bytes.fromhex('eb 3c 90')
boot[3:11] = b'CURSO36 '
struct.pack_into('<H', boot, 11, bps)
boot[13] = 1
struct.pack_into('<H', boot, 14, 1)
boot[16] = 2
struct.pack_into('<HH', boot, 17, root_entries, total)
boot[21] = 0xf8
struct.pack_into('<HHH', boot, 22, fat_sectors, 32, 64)
boot[36], boot[38] = 0x80, 0x29
struct.pack_into('<I', boot, 39, 0x36363636)
boot[43:54], boot[54:62] = b'CURSO36    ', b'FAT16   '
boot[510:512] = b'\x55\xaa'
fat = bytearray(fat_sectors * bps)
for cluster, valor in ((0, 0xfff8), (1, 0xffff), (2, 5), (5, 3), (3, 0xffff)):
    struct.pack_into('<H', fat, cluster * 2, valor)
root = bytearray(root_entries * 32)
root[0:11], root[11] = b'DATOS   BIN', 0x20
fecha = ((2026 - 1980) << 9) | (1 << 5) | 1
struct.pack_into('<H', root, 24, fecha)
struct.pack_into('<HI', root, 26, 2, len(contenido))
root[32:43], root[43] = b'CURSO36    ', 0x08
with (base / 'volumen.img').open('xb') as f:
    f.truncate(total * bps)
    f.seek(0)
    f.write(boot)
    for inicio in (1, 1 + fat_sectors):
        f.seek(inicio * bps)
        f.write(fat)
    f.seek(first_root * bps)
    f.write(root)
    for i, cluster in enumerate((2, 5, 3)):
        bloque = contenido[i * bps:(i + 1) * bps]
        f.seek((first_data + cluster - 2) * bps)
        f.write(bloque.ljust(bps, b'\xcc'))
print('Imagen creada:', base / 'volumen.img')
print('Primer sector de datos:', first_data)
PY
$ "$BIN36/leer-fat16" "$LAB36/volumen.img"

La salida debe identificar FAT16, 32479 clústeres y 512 bytes por clúster; después mostrar 2, 5 y 3 con 512, 512 y 176 bytes útiles. La huella corresponde solo a esos bytes, sin el relleno 0xCC.

Contrastar con herramientas externas, si están instaladas

mtools permite consultar y copiar archivos de una imagen sin montar el volumen. Usa un nombre de extracción nuevo para no sobrescribir una salida previa. fsck.fat -n comprueba sin aplicar reparaciones; la imagen debe permanecer sin otros usuarios.

terminal · comprobaciones opcionales con mtools y dosfstools
$ mdir -i "$LAB36/volumen.img" ::
$ mcopy -i "$LAB36/volumen.img" ::DATOS.BIN "$LAB36/extraido.bin"
$ cmp -- "$LAB36/esperado.bin" "$LAB36/extraido.bin"
$ fsck.fat -n "$LAB36/volumen.img"

Para crear otro ensayo con un formateador real, usa exclusivamente un archivo nuevo en el directorio de práctica. El alias DATOS.BIN sigue siendo la condición del lector; la cadena que el formateador asigne no tiene por qué ser 2 → 5 → 3.

terminal · variante opcional con mkfs.fat, sin dispositivos reales
$ (set -o noclobber; : > "$LAB36/formateada.img")
$ truncate -s 32M -- "$LAB36/formateada.img"
$ mkfs.fat -F 16 "$LAB36/formateada.img"
$ mcopy -i "$LAB36/formateada.img" "$LAB36/esperado.bin" ::DATOS.BIN
$ "$BIN36/leer-fat16" "$LAB36/formateada.img"

Detén la secuencia si una orden falla, especialmente si el nombre ya existía. La imagen de volumen y la copia extraída son archivos del anfitrión; ninguno de estos comandos necesita acceso a un disco físico.

15. Verificación y errores controlados en copias

Este verificador confirma la cadena y la huella. Luego crea copias independientes alterando únicamente los campos conocidos del volumen determinista: un ciclo, un fin prematuro y una discrepancia entre tablas. No se aplica a una imagen ajena ni intenta reparar esos casos.

Python 3 · ejecutar tras crear volumen.img y esperado.bin
$ python3 - "$BIN36/leer-fat16" "$LAB36" <<'PY'
from pathlib import Path
import hashlib
import re
import struct
import subprocess
import sys

programa = str(Path(sys.argv[1]).resolve())
base = Path(sys.argv[2])
imagen = base / 'volumen.img'
contenido = (base / 'esperado.bin').read_bytes()
antes = hashlib.sha256(imagen.read_bytes()).digest()
r = subprocess.run([programa, str(imagen)], capture_output=True, text=True)
if r.returncode != 0:
    raise SystemExit(r.stderr)
cadena = [int(n) for n in re.findall(r'^cluster=(\d+) ', r.stdout, re.M)]
if cadena != [2, 5, 3]:
    raise SystemExit('Cadena inesperada: ' + repr(cadena))
huella = 2166136261
for byte in contenido:
    huella = ((huella ^ byte) * 16777619) & 0xffffffff
if f'FNV32={huella:08x} bytes={len(contenido)}' not in r.stdout:
    raise SystemExit('Huella o longitud inesperada')
casos = {
    'ciclo': ([(0, 2, 2), (1, 2, 2)], 'Ciclo'),
    'corta': ([(0, 2, 0xffff), (1, 2, 0xffff)], 'prematuro'),
    'copias': ([(1, 2, 4)], 'Copias FAT diferentes'),
}
for nombre, (cambios, mensaje) in casos.items():
    copia = base / (nombre + '.img')
    datos = bytearray(imagen.read_bytes())
    for tabla, cluster, valor in cambios:
        struct.pack_into('<H', datos, (1 + tabla * 128) * 512 + cluster * 2, valor)
    with copia.open('xb') as f:
        f.write(datos)
    prueba = subprocess.run([programa, str(copia)], capture_output=True, text=True)
    if prueba.returncode == 0 or mensaje not in prueba.stderr:
        raise SystemExit('No se rechazo correctamente: ' + nombre)
    print('OK rechazo:', nombre)
if hashlib.sha256(imagen.read_bytes()).digest() != antes:
    raise SystemExit('La imagen original cambio')
print('OK: cadena, huella, errores controlados y original sin modificaciones')
PY

Las copias se crean con exclusión: para repetir, prepara otro laboratorio o cambia sus nombres. El éxito comprueba los casos previstos, no acredita que el lector implemente todos los detalles de FAT. El hash externo detecta cambios de la imagen durante esta prueba; no proviene del formato.

16. Simuladores de geometría y cadenas

Del BPB a una posición de datos

Introduce valores enteros en decimal. El modelo calcula geometría FAT12/16 con raíz fijo y determina la variante por conteo; si obtiene FAT32, indica que sus campos específicos deben analizarse con otro esquema. No valida una imagen real ni todos los requisitos del formato.

Sectores y clústeres

resultados relativos al comienzo del volumen

Recorrer una tabla FAT16 sin salir de sus límites

El modelo tiene clústeres de datos del 2 al 9, de 512 bytes cada uno. Los valores especiales se interpretan como FAT16, aunque el modelo es demasiado pequeño para ser un volumen FAT16 real. La tabla se fija por escenario; el tamaño esperado se puede cambiar.

Cadena lógica y tamaño

ClústerValor FAT16Bytes útiles leídos
recorrido acotado y diagnóstico

17. Diagnóstico y resumen

ObservaciónInterpretación a comprobar
La firma es correcta, pero las regiones no caben.El BPB es incoherente o la imagen está truncada; no continuar por la firma.
El texto dice FAT16 y el conteo indica otra variante.La etiqueta de texto no determina el tipo.
Se lee relleno después del archivo.Se confundió capacidad asignada con tamaño lógico.
Un recorrido nunca termina.Puede haber un ciclo; registrar visitados y limitar el número de pasos.
Las dos tablas difieren.Existe una discrepancia de metadatos; no elegir una como válida sin criterio.
DATOS.BIN no aparece en el lector, pero la herramienta lo muestra.Revisar alias corto, ubicación en el raíz y límites del subconjunto soportado.
Un flujo exFAT no tiene enlaces útiles en FAT.Puede emplear NoFatChain; consultar el bitmap y su información de flujo.

18. Preguntas de repaso y práctica

1. ¿Qué relaciona una entrada FAT con la siguiente?

El valor guardado para el clúster actual puede indicar el índice del siguiente clúster de la cadena.

2. ¿Cuál es el primer número de clúster de datos?

El 2. Las entradas 0 y 1 tienen funciones reservadas.

3. ¿Los clústeres de un archivo deben ser consecutivos?

No. La cadena permite un orden lógico diferente de la ubicación física.

4. ¿Qué contiene el BPB que necesitamos para localizar los datos?

Parámetros como bytes por sector, sectores por clúster, región reservada, tablas, raíz y tamaño total.

5. ¿Por qué no hacer un cast del sector a una estructura C?

La alineación, el relleno y el orden de bytes nativos pueden no coincidir con la representación del volumen.

6. ¿La firma 0x55AA demuestra que el volumen es válido?

No. También hay que validar geometría, tamaños y límites de acceso.

7. ¿Qué determina el tipo FAT clásico?

El número calculado de clústeres de datos, no la etiqueta de texto del sector inicial.

8. ¿Cuántos bytes ocupan dos entradas FAT12?

Tres. Comparten un byte al empaquetar dos valores de doce bits.

9. ¿Todos los bits de una entrada FAT32 son parte del enlace?

No. Se interpretan los veintiocho inferiores y se consideran los superiores según las reglas del formato.

10. ¿Un valor FAT libre equivale a EOC?

No. Si aparece dentro de una cadena esperada se informa una inconsistencia.

11. ¿Cómo encuentra el lector DATOS.BIN?

Recorre el raíz fijo FAT16 y compara el alias corto de once bytes DATOS BIN.

12. ¿El ejemplo implementa nombres largos?

No. Omite sus entradas auxiliares y trabaja con un alias corto conocido.

13. ¿El raíz FAT32 tiene la misma región fija?

No. Se localiza a partir de su clúster inicial y su cadena.

14. ¿Cuántos clústeres de 512 bytes necesita un archivo de 1200?

Tres; del último solo se leen 176 bytes como contenido.

15. ¿Qué impide que el lector siga un ciclo indefinidamente?

El registro de clústeres visitados y el límite calculado a partir del tamaño.

16. ¿La segunda FAT contiene otra copia de DATOS.BIN?

No. Replica información de asignación, no los bytes del contenido.

17. ¿El FNV32 de la salida está almacenado en FAT?

No. Lo calcula el ejemplo como huella didáctica; no es criptográfico.

18. ¿Recorrer bien un archivo prueba que todo el volumen está sano?

No. Otros archivos pueden tener cruces, pérdidas o inconsistencias que el lector no examina.

19. ¿exFAT siempre necesita seguir una cadena FAT?

No. Con NoFatChain la asignación contigua se obtiene mediante la información del flujo.

20. ¿Por qué distinguir imagen de volumen e imagen de disco?

Porque la segunda puede requerir localizar una partición antes de interpretar su sistema de archivos.

Para practicar

  1. Calcula a mano los sectores del raíz y el comienzo de datos del laboratorio.
  2. Ubica el byte inicial de los clústeres 2, 3 y 5 y compara con la salida del lector.
  3. Explica por qué leer físicamente 2, 3 y 5 no reconstruye el archivo del ensayo.
  4. Ejecuta el desempaquetador FAT12 y dibuja los bits compartidos entre las entradas 2 y 3.
  5. Compila el lector con advertencias habilitadas y ejecuta el verificador.
  6. Contrasta la copia extraída por mtools con esperado.bin si dispones de esa herramienta.
  7. Introduce tamaños 512, 513, 1024 y 1200 en el simulador de cadenas y explica sus diagnósticos.
  8. Prueba los escenarios libre, defectuoso, fuera de rango y ciclo sin modificar una imagen real.
  9. Compara tamaño lógico y capacidad asignada para varios tamaños de clúster.
  10. Diseña una comprobación global de referencias cruzadas entre dos archivos.
  11. Enumera los cambios necesarios para localizar un archivo dentro de un subdirectorio.
  12. Explica por qué añadir soporte FAT32 y exFAT requiere más que cambiar el ancho de una lectura.

En el tema 37 estudiaremos ext4: grupos de bloques, extents, asignación retrasada, journal y herramientas para inspeccionar sus estructuras.