1. Del nodo especial a una lectura de bytes
El tema 45 distinguió el dispositivo representado por st_rdev del sistema de archivos que contiene su nodo. Ahora abriremos un descriptor y leeremos una posición explícita. Una ruta de disco completo, una partición y una imagen regular tienen orígenes de coordenadas distintos.
LBA: dirección de un bloque lógico dentro del objeto observado. Desplazamiento en bytes: posición desde su comienzo. Para un sector lógico de S bytes, el inicio de LBA n está en n × S.
Al abrir una partición, su offset cero ya corresponde al comienzo de esa partición; no se suma de nuevo su posición en el disco completo. En cambio, interpretar la tabla de particiones del disco requiere observar el objeto que contiene esa tabla.
El acceso por una ruta de bloques evita la interpretación del sistema de archivos montado para esa lectura, pero no implica O_DIRECT ni ausencia de cachés. En el laboratorio principal usaremos archivos regulares estables y herramientas que los abren únicamente en lectura.
2. Sector lógico, sector físico y bloque de copia
| Concepto | Qué mide | Ejemplo |
|---|---|---|
| Sector lógico | Unidad de direccionamiento expuesta por el dispositivo. | 512 o 4096 bytes en este tema. |
| Sector físico informado | Granularidad física que anuncia la capa consultada. | Un dispositivo puede informar 512 lógico y 4096 físico. |
| Bloque del sistema de archivos | Unidad de organización propia del formato. | Un bloque ext4 de 4096 bytes. |
| bs de dd | Tamaño del búfer y unidad de copia elegida. | bs=512 o bs=4096; lo decide el comando. |
En un dispositivo de 512 lógico y 4096 físico, LBA 1 comienza en el byte 512. No se multiplica por el sector físico. Si el sector lógico es 4096, LBA 1 comienza en el byte 4096.
Una imagen regular no incorpora automáticamente esa geometría en stat. Sus bytes pueden contener una tabla que esperamos interpretar con un sector determinado, pero ese tamaño debe venir del contrato del experimento. No se adivina mirando una extensión .img.
3. Consultar capacidad y tamaños con ioctl
ioctl recibe un descriptor abierto, un código de operación y el argumento que exige esa operación. La ABI determina tipo, tamaño y significado del dato. No se deduce que todos los ioctl devuelvan un entero mediante el mismo puntero. Véase ioctl(2).
| Consulta Linux | Objeto de salida del ejemplo | Unidad |
|---|---|---|
| BLKSSZGET | int | Bytes por sector lógico. |
| BLKPBSZGET | int | Bytes por sector físico informado. |
| BLKGETSIZE64 | uint64_t de 8 bytes | Capacidad en bytes. |
Para contrastar existen blockdev --getss, --getpbsz y --getsize64. No se confunden con --getbsz, que consulta otro tamaño interno, ni con cifras de sectores contabilizados en unidades fijas de 512 bytes. La referencia blockdev(8) distingue esas opciones.
Los códigos se incluyen desde las cabeceras de Linux, sin copiar valores numéricos. Se comprueba el retorno antes de usar el resultado: ENOTTY u otro fallo no significa que el sector mida cero. El modo de imagen del programa no intenta estos ioctl sobre un archivo regular.
4. Convertir LBA y validar rangos
N = capacidad_bytes / S
inicio_bytes = LBA × S
longitud_bytes = k × S
Antes de formar los productos:
capacidad_bytes debe ser múltiplo de S en este contrato
LBA < N
1 ≤ k ≤ N − LBA
Último LBA de una partición: inicio + cantidad − 1
Cantidad desde límites inclusivos: último − primero + 1La resta N−LBA solo se usa después de comprobar LBA<N. Esa forma evita sumar primero un inicio y una longitud no confiables. El lector también comprueba que las posiciones que usa pread caben en off_t.
Una partición de 8192 sectores de 512 bytes desde LBA 2048 empieza en 1048576 y ocupa 4194304 bytes. Su último LBA es 10239. Con sectores de 4096, una región de los mismos bytes empieza en LBA 256 y ocupa 1024 sectores, hasta 1279.
MBR y GPT expresan sus campos de ubicación en LBA del dispositivo, no en bloques ext4. Los límites de una partición no incluyen automáticamente un sistema de archivos válido dentro de ella.
5. Inspeccionar una región con dd
En GNU dd, if selecciona la entrada, bs el tamaño de copia, skip cuántos bloques de entrada omitir y count cuántos copiar. iflag=fullblock acumula lecturas cortas para completar bloques de entrada. Estas opciones no validan una tabla de particiones.
# Primeros 512 bytes: firma y registros MBR.
dd if=mbr512.img bs=512 count=1 iflag=fullblock status=none | od -Ax -tx1
# LBA 1 con sector logico de 512 bytes.
dd if=gpt512.img bs=512 skip=1 count=1 iflag=fullblock status=none | od -Ax -tx1
# LBA 1 con sector logico de 4096 bytes.
dd if=gpt4096.img bs=4096 skip=1 count=1 iflag=fullblock status=none | od -Ax -tx1No se proporciona of hacia un dispositivo: la salida se representa con od. Los offsets que imprime od son relativos al flujo que recibe; después de skip=1 no se convierten por sí solos en offsets absolutos del archivo. La referencia de dd documenta las unidades y opciones.
Una salida vacía o corta requiere revisar el estado y la longitud. En Bash, set -o pipefail permite que falle una tubería si falla dd, aunque od termine normalmente. «Leer directamente el dispositivo» tampoco equivale a usar iflag=direct: este último corresponde a otro modo de E/S y sus restricciones.
6. MBR: firma y cuatro registros primarios
En los primeros 512 bytes del LBA 0 se encuentran la firma 55 AA en offsets 510 y 511 y cuatro registros de 16 bytes desde offset 446. Incluso si el sector lógico es mayor, esos offsets del formato no se multiplican por su tamaño.
| Campo del registro MBR | Offset dentro del registro | Interpretación del ejemplo |
|---|---|---|
| Indicador de arranque | 0, un byte. | 0 o 0x80. |
| Tipo | 4, un byte. | 0 indica entrada vacía; otros identifican categorías de partición. |
| Inicio LBA | 8, cuatro bytes. | Entero little-endian. |
| Cantidad de sectores | 12, cuatro bytes. | Longitud, no último LBA. |
No interpretaremos los campos CHS ni ejecutaremos código de arranque. Los campos LBA de 32 bits explican el límite de direccionamiento habitual asociado a MBR y sectores de 512 bytes; cambiar la unidad y las restricciones del entorno cambia el análisis. No se presenta una capacidad en bytes independiente de S.
El analizador admite particiones primarias ordinarias, comprueba rango y solapamientos y rechaza como no soportadas las extendidas de tipos 05, 0F y 85: recorrer su cadena EBR requiere otro algoritmo. Una firma 55 AA sola no demuestra que la tabla sea coherente.
7. GPT: protección, cabecera y entradas
GPT utiliza un MBR protector en LBA 0, una cabecera primaria en LBA 1 y una tabla de entradas cuya posición declara la cabecera. También dispone de estructuras de respaldo al final del disco. La firma de la cabecera son los ocho bytes ASCII EFI PART.
La cabecera declara límites utilizables, identidad del disco, posición de la tabla, cantidad y tamaño de entradas. Las entradas activas contienen GUID de tipo, GUID de partición, primer y último LBA inclusivos, atributos y nombre UTF-16LE. GUID de tipo cero marca una entrada sin uso.
La especificación UEFI de GPT define el formato. No se supone que toda GPT tenga exactamente la configuración de nuestras imágenes: el programa delimita un subconjunto y distingue lo no soportado de un error de integridad.
8. CRC32, límites y alcance de la comprobación
El CRC de la cabecera primaria cubre HeaderSize bytes, poniendo a cero el campo del propio CRC durante el cálculo. El de la tabla cubre cantidad × tamaño de entrada bytes, no todo el relleno del último sector. GPT emplea CRC32 convencional; no el CRC32C del superbloque ext4 del tema 40.
El analizador usa zlib.crc32 y contrasta el resultado con los campos almacenados. Antes de leer la tabla valida tamaño, cantidad y posición para no reservar o recorrer regiones arbitrarias indicadas por una imagen malformada. El lector GPT del kernel también separa validación de cabeceras, entradas y posiciones.
Después comprueba que las particiones activas caben en la zona utilizable y no se superponen. Un CRC correcto no sustituye esas relaciones; una estructura incoherente puede tener un CRC recalculado que coincide.
Este programa verifica solo la cabecera y la tabla primarias. Comprueba que se declara un LBA de respaldo esperado y que hay espacio reservado, pero no lee ni compara la copia de respaldo. Tampoco verifica el contenido de las particiones ni autentica el origen de la imagen. El resultado lo indica como NO_VERIFICADO.
9. Contrato de las herramientas del tema
- El lector C abre en solo lectura, comprueba con fstat el objeto abierto y entrega un único sector lógico completo.
- En modo imagen exige archivo regular, tamaño múltiplo del sector explícito y S=512 o 4096.
- En modo bloque exige nodo de bloque y consulta los tamaños mediante ioctl; admite los mismos sectores lógicos.
- Los objetos permanecen estables y sin otros escritores durante el ensayo. Abrir en lectura no congela sus bytes.
- El analizador Python solo admite imágenes regulares y las abre sin seguir el enlace final.
- MBR extendido e híbridos se informan como no soportados; no se interpretan cadenas EBR.
- GPT admitida: revisión 1.0, cabecera de 92 bytes, 128 entradas de 128 bytes y MBR protector convencional.
- No se reparan tablas, no se reproducen journals y no se formatean particiones.
Una característica no soportada no prueba corrupción. El generador escribe únicamente archivos nuevos mediante creación exclusiva y crea tres discos sintéticos sin sistemas de archivos dentro de las particiones.
10. Programa en C: geometría y lectura completa
Guarda leer-bloque.c. Uso: imagen ruta sector LBA o bloque ruta LBA. Devuelve 0 al leer, 1 ante error o rango inválido y 2 por argumentos incorrectos. El hexadecimal corresponde al sector completo, sin reinterpretar estructuras C sobre sus bytes.
#define _POSIX_C_SOURCE 200809L
#define _FILE_OFFSET_BITS 64
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <linux/fs.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
_Static_assert(sizeof(off_t) >= 8, "Se requieren offsets de al menos 64 bits");
_Static_assert(sizeof(uint64_t) == 8, "BLKGETSIZE64 requiere ocho bytes");
static int decimal(const char *s, uint64_t *valor)
{
uint64_t n = 0;
if (!*s) return -1;
for (; *s; ++s) {
if (*s < '0' || *s > '9') return -1;
unsigned d = (unsigned)(*s - '0');
if (n > (UINT64_MAX - d) / 10) return -1;
n = n * 10 + d;
}
*valor = n; return 0;
}
int main(int argc, char **argv)
{
int imagen = argc > 1 && !strcmp(argv[1], "imagen");
int bloque = argc > 1 && !strcmp(argv[1], "bloque");
uint64_t lba = 0, declarado = 0;
if ((!imagen && !bloque) || (imagen && argc != 5) || (bloque && argc != 4) ||
(imagen && (decimal(argv[3], &declarado) ||
(declarado != 512 && declarado != 4096))) ||
decimal(argv[imagen ? 4 : 3], &lba)) {
fprintf(stderr, "Uso: %s imagen ruta 512|4096 LBA | bloque ruta LBA\n", argv[0]);
return 2;
}
int fd = open(argv[2], O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK);
if (fd == -1) { perror("open"); return 1; }
struct stat st;
if (fstat(fd, &st)) { perror("fstat"); close(fd); return 1; }
uint64_t capacidad;
int logico = (int)declarado, fisico = 0;
if (imagen) {
if (!S_ISREG(st.st_mode) || st.st_size < 0) {
fprintf(stderr, "Se requiere imagen regular\n"); close(fd); return 1;
}
capacidad = (uint64_t)st.st_size;
} else {
if (!S_ISBLK(st.st_mode)) {
fprintf(stderr, "Se requiere dispositivo de bloque\n"); close(fd); return 1;
}
if (ioctl(fd, BLKSSZGET, &logico) == -1 ||
ioctl(fd, BLKPBSZGET, &fisico) == -1 ||
ioctl(fd, BLKGETSIZE64, &capacidad) == -1) {
perror("ioctl de consulta"); close(fd); return 1;
}
if (fisico < logico || logico <= 0 || fisico % logico != 0) {
fprintf(stderr, "Geometria fisica incoherente\n"); close(fd); return 1;
}
}
if ((logico != 512 && logico != 4096) || capacidad == 0 ||
capacidad % (uint64_t)logico != 0 || lba >= capacidad / (uint64_t)logico) {
fprintf(stderr, "Geometria no admitida o LBA fuera de rango\n"); close(fd); return 1;
}
uint64_t offset = lba * (uint64_t)logico;
if (offset > (uint64_t)INT64_MAX - ((uint64_t)logico - 1)) {
fprintf(stderr, "La lectura excede los offsets admitidos\n"); close(fd); return 1;
}
unsigned char buffer[4096];
size_t usados = 0, pedido = logico == 512 ? 512u : 4096u;
while (usados < pedido) {
size_t faltan = pedido - usados;
if (usados >= sizeof buffer || faltan > sizeof buffer || faltan > sizeof buffer - usados) {
fprintf(stderr, "Limite interno de buffer invalido\n"); close(fd); return 1;
}
ssize_t n = pread(fd, buffer + usados, faltan, (off_t)(offset + usados));
if (n > 0) {
if ((size_t)n > faltan) {
fprintf(stderr, "Retorno de lectura fuera de contrato\n"); close(fd); return 1;
}
usados += (size_t)n;
}
else if (n < 0 && errno == EINTR) continue;
else {
if (n == 0) fprintf(stderr, "Fin prematuro de la region\n");
else perror("pread");
close(fd); return 1;
}
}
if (close(fd)) { perror("close"); return 1; }
printf("SECTOR_LOGICO=%d\n", logico);
if (imagen) puts("SECTOR_FISICO=NA"); else printf("SECTOR_FISICO=%d\n", fisico);
printf("CAPACIDAD=%" PRIu64 "\nLBA=%" PRIu64 "\nOFFSET=%" PRIu64 "\nHEX=",
capacidad, lba, offset);
for (size_t i = 0; i < pedido; ++i) printf("%02X", (unsigned)buffer[i]);
putchar('\n');
if (fflush(stdout) == EOF) { perror("stdout"); return 1; }
return 0;
}El producto offset se forma después de probar que LBA cae dentro de capacidad/sector. No se usa st_size como capacidad del nodo de bloque. En modo imagen, el sector físico se informa como NA: no se inventa a partir del almacenamiento que aloja el archivo.
O_NOFOLLOW se aplica al componente final y O_NONBLOCK evita esperar al abrir un FIFO inesperado antes de rechazar su tipo. Son restricciones sobre un entorno de rutas confiables, no una congelación de toda su jerarquía.
11. Analizador Python de MBR y GPT primaria
Guarda examinar-tabla.py. Recibe imagen y sector lógico. Devuelve JSON solo tras validar su subconjunto: estado 0 para éxito parcial, 1 para lectura o incoherencia, 2 para uso incorrecto y 3 para formato fuera de su contrato.
import argparse
import json
import os
import stat
import struct
import sys
import uuid
import zlib
class NoSoportado(Exception):
pass
def examinar(ruta, s):
fd = os.open(ruta, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
with os.fdopen(fd, 'rb') as f:
st = os.fstat(f.fileno())
if not stat.S_ISREG(st.st_mode) or st.st_size < 2 * s or st.st_size % s:
raise ValueError('Se requiere imagen regular, estable y con sectores completos')
n = st.st_size // s
def leer(offset, cantidad):
if offset < 0 or cantidad < 0 or offset > st.st_size or cantidad > st.st_size - offset:
raise ValueError('Region fuera de la imagen')
f.seek(offset)
b = f.read(cantidad)
if len(b) != cantidad:
raise ValueError('Lectura truncada')
return b
def sin_solapamientos(partes):
ordenadas = sorted(partes, key=lambda p: p['inicio'])
for a, b in zip(ordenadas, ordenadas[1:]):
if b['inicio'] <= a['fin']:
raise ValueError('Particiones superpuestas')
mbr = leer(0, 512)
if mbr[510:512] != b'\x55\xaa':
raise ValueError('Firma MBR ausente')
registros = []
for i in range(4):
e = mbr[446 + 16 * i:462 + 16 * i]
inicio, cantidad = struct.unpack_from('<II', e, 8)
if e[0] not in (0, 0x80):
raise ValueError('Indicador de arranque MBR invalido')
if not e[4]:
if inicio or cantidad:
raise ValueError('Entrada MBR vacia con rango no nulo')
continue
registros.append((i + 1, e[4], inicio, cantidad, e[0]))
protector = [r for r in registros if r[1] == 0xEE]
if not protector:
partes = []
for i, tipo, inicio, cantidad, arranque in registros:
if tipo in (0x05, 0x0F, 0x85):
raise NoSoportado('MBR extendido: falta implementar la cadena EBR')
if inicio < 1 or inicio >= n or not cantidad or cantidad > n - inicio:
raise ValueError('Rango MBR invalido')
partes.append(dict(indice=i, tipo=f'{tipo:02X}', inicio=inicio,
fin=inicio + cantidad - 1, sectores=cantidad,
arranque=arranque == 0x80))
sin_solapamientos(partes)
return dict(formato='MBR', sector=s, particiones=partes)
if len(registros) != 1 or protector[0][0] != 1:
raise NoSoportado('MBR hibrido o protector fuera del subconjunto')
_, _, inicio, cantidad, arranque = protector[0]
if inicio != 1 or cantidad != min(n - 1, 0xFFFFFFFF) or arranque != 0:
raise ValueError('Rango o indicador del MBR protector invalido')
h = leer(s, s)
if h[:8] != b'EFI PART':
raise ValueError('Firma GPT primaria ausente')
revision, tamano, crc_h, reservado = struct.unpack_from('<IIII', h, 8)
if revision != 0x10000 or tamano != 92:
raise NoSoportado('Se admite revision 1.0 y cabecera de 92 bytes')
if reservado:
raise ValueError('Campo reservado GPT no nulo')
copia = bytearray(h[:tamano])
copia[16:20] = bytes(4)
if zlib.crc32(copia) != crc_h:
raise ValueError('CRC32 de cabecera primaria incorrecto')
propia, respaldo, primero, ultimo = struct.unpack_from('<QQQQ', h, 24)
tabla, cuenta, ancho, crc_e = struct.unpack_from('<QIII', h, 72)
if cuenta != 128 or ancho != 128:
raise NoSoportado('Se admiten 128 entradas de 128 bytes')
bloques_tabla = (cuenta * ancho + s - 1) // s
if (propia != 1 or respaldo != n - 1 or primero > ultimo or
tabla < 2 or tabla > primero or bloques_tabla > primero - tabla or
respaldo <= bloques_tabla or ultimo >= respaldo - bloques_tabla or
h[56:72] == bytes(16)):
raise ValueError('Geometria GPT primaria incoherente')
entradas = leer(tabla * s, cuenta * ancho)
if zlib.crc32(entradas) != crc_e:
raise ValueError('CRC32 de entradas primarias incorrecto')
partes, identidades = [], set()
for i in range(cuenta):
e = entradas[i * ancho:(i + 1) * ancho]
if e[:16] == bytes(16):
continue
a, b = struct.unpack_from('<QQ', e, 32)
identidad = str(uuid.UUID(bytes_le=e[16:32]))
if (a < primero or b > ultimo or b < a or
e[16:32] == bytes(16) or identidad in identidades):
raise ValueError('Entrada GPT fuera de rango o identidad duplicada')
identidades.add(identidad)
nombre = e[56:128].decode('utf-16-le').split('\0', 1)[0]
partes.append(dict(indice=i + 1, inicio=a, fin=b, sectores=b - a + 1,
tipo_guid=str(uuid.UUID(bytes_le=e[:16])),
uuid=identidad, nombre=nombre))
sin_solapamientos(partes)
return dict(formato='GPT', sector=s, particiones=partes,
crc_cabecera='OK', crc_entradas='OK', respaldo='NO_VERIFICADO')
def main():
p = argparse.ArgumentParser(description='MBR y GPT primaria en imagen regular')
p.add_argument('imagen')
p.add_argument('sector', type=int, choices=(512, 4096))
a = p.parse_args()
try:
resultado = examinar(a.imagen, a.sector)
except NoSoportado as e:
print('No soportado: ' + str(e), file=sys.stderr); return 3
except (OSError, ValueError, struct.error) as e:
print('Error: ' + str(e), file=sys.stderr); return 1
print(json.dumps(resultado, ensure_ascii=True, indent=2))
return 0
if __name__ == '__main__':
raise SystemExit(main())struct usa little-endian explícito y uuid interpreta la representación GPT mediante bytes_le. JSON escapa controles y nombres no ASCII. Los atributos de partición y CHS no se interpretan; el contrato tampoco revisa código de arranque ni todos los campos reservados de estructuras futuras.
12. Generador de imágenes reproducibles
Guarda crear-imagenes46.py. Crea discos de 64 MiB: mbr512.img, gpt512.img y gpt4096.img. Cada uno declara una partición de 4 MiB desde el byte 1048576. La GPT usa GUID de tipo ficticio para evitar inferir un formato de contenido. No hay ext4, FAT ni datos de usuario dentro.
from pathlib import Path
import os
import struct
import uuid
import zlib
casos = [('mbr512.img', 512, False), ('gpt512.img', 512, True),
('gpt4096.img', 4096, True)]
if any(os.path.lexists(nombre) for nombre, _, _ in casos):
raise SystemExit('Ya existe una ruta de salida; usa un directorio nuevo')
for nombre, s, gpt in casos:
capacidad = 64 * 1024 * 1024
n, inicio, cantidad = capacidad // s, 1048576 // s, 4194304 // s
mbr = bytearray(512)
mbr[510:512] = b'\x55\xaa'
mbr[450] = 0xEE if gpt else 0x83
struct.pack_into('<II', mbr, 454, 1 if gpt else inicio,
min(n - 1, 0xFFFFFFFF) if gpt else cantidad)
if gpt:
mbr[447:450] = b'\x00\x02\x00'
mbr[451:454] = b'\xff\xff\xff'
with Path(nombre).open('xb') as f:
f.truncate(capacidad)
f.write(mbr)
if gpt:
entradas = bytearray(128 * 128)
entradas[:16] = uuid.UUID('11111111-2222-3333-4444-555555555555').bytes_le
entradas[16:32] = uuid.UUID('aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee').bytes_le
struct.pack_into('<QQQ', entradas, 32, inicio, inicio + cantidad - 1, 0)
texto = 'datos46'.encode('utf-16-le')
entradas[56:56 + len(texto)] = texto
bloques = len(entradas) // s
tabla_respaldo = n - 1 - bloques
def cabecera(propia, otra, tabla):
h = bytearray(s)
h[:8] = b'EFI PART'
struct.pack_into('<IIIIQQQQ', h, 8, 0x10000, 92, 0, 0,
propia, otra, 2 + bloques, tabla_respaldo - 1)
h[56:72] = uuid.UUID('01234567-89ab-cdef-0123-456789abcdef').bytes_le
struct.pack_into('<QIII', h, 72, tabla, 128, 128, zlib.crc32(entradas))
struct.pack_into('<I', h, 16, zlib.crc32(h[:92]))
return h
for offset, b in ((2 * s, entradas), (tabla_respaldo * s, entradas),
(s, cabecera(1, n - 1, 2)),
((n - 1) * s, cabecera(n - 1, 1, tabla_respaldo))):
f.seek(offset)
f.write(b)
print(nombre, 'sector=', s, 'inicio=', inicio, 'sectores=', cantidad)El generador escribe también el respaldo para producir ejemplos completos. El lector sigue informando que no lo verificó: conocer cómo se creó una imagen no amplía silenciosamente el alcance de una consulta posterior.
13. Laboratorio offline con tres imágenes
En Bash crea un directorio nuevo y guarda los tres fuentes. Se requieren compilador C y Python 3. El laboratorio principal no monta nada y no necesita privilegios administrativos.
mkdir laboratorio46
cd laboratorio46
# Guardar leer-bloque.c, examinar-tabla.py y crear-imagenes46.py.
cc -std=c11 -O2 -Wall -Wextra -Wpedantic leer-bloque.c -o leer-bloque
python3 crear-imagenes46.py
./leer-bloque imagen mbr512.img 512 0
./leer-bloque imagen gpt512.img 512 1
./leer-bloque imagen gpt4096.img 4096 1
python3 examinar-tabla.py mbr512.img 512
python3 examinar-tabla.py gpt512.img 512
python3 examinar-tabla.py gpt4096.img 4096Las dos cabeceras GPT comienzan con el hexadecimal 4546492050415254. Sus offsets absolutos son 512 y 4096 respectivamente. La partición empieza en LBA 2048 con sectores de 512, y en LBA 256 con sectores de 4096, pero en ambos casos su inicio en bytes es 1048576.
Repite las consultas dd de la sección 5. Si está disponible, sfdisk --json mbr512.img permite contrastar la tabla MBR y sfdisk --json gpt512.img la GPT de 512. Las herramientas externas también necesitan conocer o admitir el sector del caso de 4096; no atribuyas un fallo de unidad a la tabla sin comprobarlo.
El inicio utilizable GPT de estas imágenes es 34 para S=512 y 6 para S=4096: 128 × 128 bytes ocupan 32 o 4 sectores. Esos valores se derivan de esta distribución concreta; no se codifican como constantes universales en el analizador.
14. Ampliación: consultar un loop en solo lectura
Si el entorno ofrece loop y privilegios para asociarlo, esta ampliación vincula únicamente gpt4096.img como dispositivo de bloque en lectura. El sector lógico se configura explícitamente en 4096. No se solicita montar ni volver a leer particiones del sistema.
(
set -euo pipefail
loop46=''
liberar46() {
if [[ -n "$loop46" ]]; then sudo losetup --detach "$loop46"; fi
}
trap liberar46 EXIT
loop46=$(sudo losetup --find --show --read-only --nooverlap --sector-size 4096 gpt4096.img)
printf 'Loop de laboratorio: %s\n' "$loop46"
sudo blockdev --getss "$loop46"
sudo blockdev --getpbsz "$loop46"
sudo blockdev --getsize64 "$loop46"
sudo ./leer-bloque bloque "$loop46" 1
)Realiza esta ampliación con una imagen propia recién preparada y sin una asociación previa. Conserva el nombre que devuelve losetup; no se fija /dev/loop0 ni se usa una desconexión global. El trap libera el dispositivo en salidas normales; SIGKILL o pérdida del entorno pueden requerir revisar manualmente la asociación registrada.
losetup(8) documenta modo de lectura, tamaño de sector y ciclo de vida. El sector físico que anuncie un loop no demuestra la organización física del archivo de respaldo. Si loop no está permitido, las pruebas offline siguen siendo completas para el lector de imágenes y el analizador.
15. Verificador de bytes y casos negativos
Desde laboratorio46 ejecuta este bloque después de crear las imágenes. Contrasta bytes con la lectura C, tamaños y particiones con el analizador y hashes antes y después. Las alteraciones se hacen en copias temporales, nunca en las imágenes originales.
python3 - <<'PY'
from pathlib import Path
from tempfile import TemporaryDirectory
import hashlib
import json
import shutil
import struct
import subprocess
import zlib
lector = str(Path('leer-bloque').resolve())
analizador = str(Path('examinar-tabla.py').resolve())
def ejecutar(args, estado=0):
r = subprocess.run(args, capture_output=True, text=True)
assert r.returncode == estado, (args, r.returncode, r.stderr)
return r.stdout
def sha(p):
h = hashlib.sha256()
with p.open('rb') as f:
for b in iter(lambda: f.read(1048576), b''): h.update(b)
return h.digest()
casos = [(Path('mbr512.img'), 512, 'MBR'), (Path('gpt512.img'), 512, 'GPT'),
(Path('gpt4096.img'), 4096, 'GPT')]
antes = {p: sha(p) for p, _, _ in casos}
for p, s, formato in casos:
n = p.stat().st_size // s
for lba in (0, 1, n - 1):
salida = ejecutar([lector, 'imagen', str(p), str(s), str(lba)])
campos = dict(linea.split('=', 1) for linea in salida.splitlines())
with p.open('rb') as f:
f.seek(lba * s); esperado = f.read(s)
assert bytes.fromhex(campos['HEX']) == esperado
assert int(campos['OFFSET']) == lba * s
r = json.loads(ejecutar(['python3', analizador, str(p), str(s)]))
assert r['formato'] == formato and len(r['particiones']) == 1
parte = r['particiones'][0]
assert parte['inicio'] * s == 1048576
assert parte['sectores'] * s == 4194304
if formato == 'GPT':
assert r['crc_cabecera'] == r['crc_entradas'] == 'OK'
assert r['respaldo'] == 'NO_VERIFICADO'
ejecutar([lector, 'imagen', str(p), str(s), str(n)], 1)
ejecutar([lector, 'imagen', 'mbr512.img', '512', '-1'], 2)
ejecutar([lector, 'imagen', 'mbr512.img', '512', '18446744073709551616'], 2)
ejecutar([lector, 'bloque', 'mbr512.img', '0'], 1)
ejecutar(['python3', analizador, 'gpt4096.img', '512'], 1)
with TemporaryDirectory(prefix='tabla46-') as d:
base = Path(d)
def alterada(nombre, offset, datos):
p = base / nombre
shutil.copyfile('gpt512.img', p)
with p.open('r+b') as f: f.seek(offset); f.write(datos)
return p
p = alterada('cabecera.img', 512 + 56, bytes(16))
ejecutar(['python3', analizador, str(p), '512'], 1)
p = alterada('entrada.img', 1024 + 56, b'X')
ejecutar(['python3', analizador, str(p), '512'], 1)
p = alterada('respaldo.img', 64 * 1024 * 1024 - 512, b'BAD PART')
r = json.loads(ejecutar(['python3', analizador, str(p), '512']))
assert r['respaldo'] == 'NO_VERIFICADO'
# CRC correcto con geometria invalida debe fallar por otra razon.
p = alterada('rango.img', 512 + 40, struct.pack('<Q', 0))
with p.open('r+b') as f:
f.seek(512); h = bytearray(f.read(92)); h[16:20] = bytes(4)
struct.pack_into('<I', h, 16, zlib.crc32(h))
f.seek(512); f.write(h)
ejecutar(['python3', analizador, str(p), '512'], 1)
p = base / 'corta.img'; p.write_bytes(bytes(513))
ejecutar([lector, 'imagen', str(p), '512', '0'], 1)
p = base / 'extendida.img'
shutil.copyfile('mbr512.img', p)
with p.open('r+b') as f: f.seek(450); f.write(b'\x05')
ejecutar(['python3', analizador, str(p), '512'], 3)
p = alterada('mas-entradas.img', 512 + 80, struct.pack('<I', 256))
with p.open('r+b') as f:
f.seek(512); h = bytearray(f.read(92)); h[16:20] = bytes(4)
struct.pack_into('<I', h, 16, zlib.crc32(h))
f.seek(512); f.write(h)
ejecutar(['python3', analizador, str(p), '512'], 3)
for p, _, _ in casos: assert sha(p) == antes[p]
print('OK: bytes, unidades, CRC, geometria y consultas sin modificar originales')
PYEl caso de respaldo alterado debe seguir informando éxito parcial de la primaria y NO_VERIFICADO para el respaldo. Es una prueba deliberada del límite de la herramienta, no un diagnóstico de GPT íntegra. Para ampliar el lector haría falta verificar ambas cabeceras y sus tablas y contrastar sus relaciones.
16. Simuladores de LBA y reservas GPT
Convertir un rango sin perder enteros de 64 bits
Usa BigInt para posiciones exactas. La capacidad se limita al rango uint64_t y debe ser múltiplo del sector lógico. El simulador calcula un rango dentro del objeto observado, no descubre particiones ni decide el origen por ti.
LBA, longitud y desplazamiento absoluto
La misma tabla de entradas ocupa distinto número de sectores
Distribución convencional del generador: 128 entradas de 128 bytes, cabecera primaria en LBA 1, tabla en LBA 2 y copia de respaldo contigua al final. Se fija la partición de 4 MiB desde el byte 1 MiB y se compara S=512 con S=4096. Los límites corresponden a esta distribución.
Reservas GPT y alineación de una partición
| S, bytes | Sectores de tabla | Primer utilizable | Último utilizable | LBA de respaldo | Partición: inicio y cantidad |
|---|
17. Diagnóstico y resumen
| Observación | Qué revisar |
|---|---|
| Se busca GPT en byte 512 y no aparece. | Si el sector lógico es 4096, la cabecera LBA 1 comienza en 4096. |
| Se usa el sector físico para convertir un LBA. | La unidad de direccionamiento es el sector lógico. |
| Un nodo de bloque informa st_size cero. | Consultar capacidad con la interfaz del dispositivo; no asumir archivo vacío. |
| 55 AA aparece, pero una partición sale de la imagen. | La firma no sustituye validación de rangos. |
| El CRC coincide y falla la geometría. | Integridad de bytes y coherencia semántica son comprobaciones distintas. |
| La primaria pasa, pero el respaldo está dañado. | El analizador declara que el respaldo no fue verificado. |
| Se suma dos veces el comienzo de la partición. | Al abrir su nodo, offset cero ya es el inicio de ese objeto. |
- Las direcciones LBA se convierten usando el sector lógico de la capa observada.
- Sector físico, bloque del sistema de archivos y bs de dd son conceptos diferentes.
- ioctl requiere el descriptor y los tipos que especifica cada operación.
- Se validan capacidad, cantidad y límites antes de multiplicar o leer.
- MBR contiene cuatro registros primarios; las cadenas extendidas requieren recorrido adicional.
- GPT declara posiciones y tamaños y protege cabecera y entradas mediante CRC32.
- Una firma o CRC correcto no verifica por sí solo geometría, respaldo ni contenido de particiones.
- Las imágenes propias permiten practicar sin montar ni modificar discos reales.
18. Preguntas de repaso y práctica
1. ¿Dónde comienza LBA 1?
En el byte S, donde S es el sector lógico del objeto observado.
2. ¿Qué consulta BLKSSZGET?
El tamaño de sector lógico en bytes.
3. ¿Qué consulta BLKGETSIZE64?
La capacidad del dispositivo en bytes, usando una salida de ocho bytes.
4. ¿Por qué no usar st_size como capacidad de un nodo de bloque?
Porque no tiene el mismo contrato que el tamaño de un archivo regular; se consulta la interfaz del dispositivo.
5. ¿bs=4096 en dd cambia el sector del dispositivo?
No. Define el tamaño de copia elegido por el comando.
6. ¿Leer una ruta de bloque equivale a O_DIRECT?
No. El modo de acceso al nodo y la política de caché son cuestiones distintas.
7. ¿Cuál es la posición de la firma MBR?
Bytes 510 y 511 del comienzo del disco: 55 AA.
8. ¿El campo longitud MBR contiene el último LBA?
No. Contiene cantidad de sectores; el último es inicio + cantidad − 1.
9. ¿Cuántas entradas primarias MBR se muestran?
Cuatro registros de 16 bytes desde offset 446.
10. ¿Este analizador recorre EBR?
No. Informa las particiones extendidas como no soportadas.
11. ¿Qué firma identifica la cabecera GPT?
Los ocho bytes ASCII EFI PART.
12. ¿Dónde está la GPT primaria con S=4096?
Su cabecera en LBA 1 comienza en el byte 4096.
13. ¿Qué campo se pone a cero para calcular el CRC de cabecera?
El propio campo HeaderCRC32, dentro de los HeaderSize bytes cubiertos.
14. ¿GPT usa el CRC32C de ext4?
No. Usa CRC32 convencional.
15. ¿La tabla de 128 entradas de 128 bytes ocupa siempre 32 sectores?
No. Ocupa 32 sectores de 512 o 4 de 4096 en los ejemplos.
16. ¿Primaria correcta significa GPT completa verificada?
No. Este programa no lee ni compara las estructuras de respaldo.
17. ¿Las particiones generadas contienen un sistema de archivos?
No. Solo se crean las tablas y regiones declaradas del disco sintético.
18. ¿Abrir en lectura impide cambios de otros procesos?
No. El laboratorio requiere mantener estable el objeto durante las consultas.
19. ¿Por qué leer un sector mediante un bucle?
Para completar lecturas cortas, reintentar EINTR y detectar EOF prematuro antes de interpretar datos.
20. ¿Qué cambia al abrir el nodo de una partición?
El origen: offset cero ya corresponde a su comienzo y no se añade otra vez su posición en el disco padre.
Para practicar
- Convierte la misma región de 1 MiB a 5 MiB a LBA con sectores de 512 y 4096.
- Explica por qué el último LBA es inclusivo y contrasta cantidad con longitud en bytes.
- Compila el lector y compara su hexadecimal con dd para LBA 0 y 1.
- Localiza los cuatro registros MBR y decodifica inicio y cantidad sin usar una estructura C nativa.
- Contrasta las tablas GPT de 512 y 4096 y relaciona su espacio reservado con 16384 bytes de entradas.
- Ejecuta el verificador y explica por qué la copia con respaldo alterado conserva el éxito parcial de la primaria.
- Recalcula el CRC tras alterar una geometría y justifica por qué el resultado sigue debiendo rechazarse.
- Prueba LBA igual al total de sectores y explica el rechazo sin formar un producto fuera de rango.
- Si hay soporte loop, consulta un dispositivo propio en lectura y verifica los tamaños con blockdev.
- Describe qué campos adicionales se deben contrastar al implementar verificación del respaldo.
- Explica qué falta para recorrer particiones extendidas o aceptar GPT con otro tamaño de entrada.
- Relaciona el offset de una partición con el offset de un superbloque ext4 dentro de ella, sin confundir ambos orígenes.
En el tema 47 estudiaremos E/S no bloqueante, poll e io_uring: esperar eventos y administrar varias operaciones sin confundir una interfaz asíncrona con ausencia de espera.