Un programa Delphi se detiene con ERangeError y el mensaje escueto "Range check error", a menudo solo en una de sus dos configuraciones de compilación. Delphi lanza esta excepción cuando código compilado con la comprobación de rangos activada ({$R+}) indexa un arreglo o una cadena fuera de sus límites, o guarda un valor que no cabe en el tipo ordinal de destino. La corrección casi siempre pertenece al lugar donde se calculó el índice o el valor erróneo, que rara vez es la línea donde se detiene el debugger
¿Qué significa "Range check error" en Delphi?
ERangeError viene de comprobaciones que el compilador inserta cuando la comprobación de rangos está activada. Con {$R+} (forma larga {$RANGECHECKS ON}, o Project Options › Building › Delphi Compiler › Compiling › Range checking), cada subíndice de arreglo y de cadena se compara contra los límites declarados, y cada asignación a un ordinal más pequeño, a un subrango o a una variable enumerada se compara contra el rango de ese tipo. El estado por defecto es {$R-}, y el código compilado así no contiene ninguna comprobación, así que una escritura fuera de los límites simplemente aterriza en la memoria que tenga al lado. Una comprobación fallida lanza ERangeError con el resource string "Range check error" cuando SysUtils está enlazado; un programa que nunca usa SysUtils termina con el runtime error 201. El error de compilación E1012 (Constant expression violates subrange bounds) solo cubre índices que el compilador ve como constantes; todo lo que se calcula en tiempo de ejecución depende de {$R+}. El switch también funciona por unidad: los DCUs precompilados conservan el estado con el que se construyeron, y un {$R-} al inicio de una unidad sobrescribe la opción del proyecto para esa unidad
La comprobación de overflow es un switch aparte con una excepción aparte. {$Q+} ({$OVERFLOWCHECKS ON}) comprueba aritmética de enteros como +, -, *, Inc y Dec y lanza EIntOverflow cuando un resultado no cabe; {$R+} comprueba índices y almacenamientos. Las dos se encuentran en los tipos pequeños. La aritmética de Byte se evalúa como Integer, así que B := B + 1 con B = 255 produce 256 sin ningún overflow, y es el almacenamiento de vuelta en el Byte lo que falla la comprobación de rango. Inc(B) sobre el mismo valor es una operación con comprobación de overflow en cambio: con {$Q+} lanza EIntOverflow, y con solo {$R+} envuelve silenciosamente a 0. Qué configuraciones tienen estos switches activados depende de la versión de Delphi y de cómo se creó el proyecto. Lea los valores reales en Project Options para cada configuración de compilación antes de fiarse de una teoría sobre por qué el error aparece solo en una compilación
Las causas habituales, con repros mínimos
La mayoría de los errores de rango en código Delphi real caen en un puñado de patrones, cada uno con una corrección pequeña y mecánica. Los arreglos son el caso más común. Un array[0..9] estático acepta índices de 0 a 9, un arreglo dinámico o TBytes acepta 0..Length(A) - 1, y las strings van de 1 en adelante, así que escriba los bucles contra Low y High en lugar de límites fijados a mano. El caso vacío es donde el código se rompe. High de un arreglo dinámico vacío es -1, así que un bucle for I := 0 to High(A) con contador Integer correctamente corre cero veces, mientras que A[High(A)] o A[Count - 1] usados para traer "el último elemento" leen el índice -1. Casos relacionados se comportan distinto:
- Las clases contenedor comprueban sus propios índices, sea cual sea el estado de
{$R}:TListyTStringListlanzanEListErroroEStringListError, yTList<T>lanzaEArgumentOutOfRangeException - Los conteos que llegan como
CardinaloDWORDdesde un encabezado de archivo o una API son riesgosos en cero, porque a qué evalúaCount - 1depende de cómo el compilador tipifica la expresión; mantenga los contadores de bucle comoInteger, valide el conteo una vez, conviértalo, y pruebeCount > 0explícitamente
procedure ArrayRepros(const Data: TBytes; Buf: Pointer);
var
Last: Byte;
P: PByte;
I: Integer;
begin
// Arreglo vacío: High(Data) = -1, así que esto lee Data[-1]
Last := Data[High(Data)]; // ERangeError bajo {$R+}
// Corrección: pruebe la longitud antes de pedir el último elemento
if Length(Data) > 0 then
Last := Data[High(Data)];
// TByteArray se declara como array[0..32767] of Byte, así que bajo {$R+}
// cualquier índice por encima de 32767 falla aunque Buf contenga megabytes
I := 40000;
Last := PByteArray(Buf)^[I]; // ERangeError en I = 40000
// Corrección: indexe el buffer real, o use aritmética de PByte
P := PByte(Buf);
Inc(P, I);
Last := P^; // sin comprobación de rango, valide I usted mismo
end;
El caso PByteArray atrapa a desarrolladores con experiencia porque el límite declarado no tiene nada que ver con el buffer. Tanto el SysUtils de Delphi como el de Free Pascal declaran TByteArray como array[0..32767] of Byte, una ventana de 32 KB que se remonta al código de 16 bits. Bajo {$R+} la comprobación compara su índice con 32767, así que un offset válido dentro de un stream de 100 KB lanza ERangeError; bajo {$R-} no hay comprobación, y un índice más allá del final real del buffer jamás se detecta. Indexar el TBytes mismo le da una comprobación contra la longitud real, lo que lo hace el mejor valor por defecto. La aritmética de PByte no se comprueba en absoluto. Empárejela con una prueba explícita contra el tamaño del buffer
Los almacenamientos ordinales son la otra gran familia. Asignar 256 a un Byte, 70000 a un Word, o 13 a un subrango 1..12 falla la comprobación, y también asignar un Integer negativo a un Cardinal. Delphi evalúa operandos mixtos con signo y sin signo de 32 bits como Int64, así que la aritmética en sí suele ser correcta y el error aparece en el almacenamiento, lo que le señala la variable cuyo tipo es demasiado estrecho. Los typecasts duros son el hueco en esta red: Byte(I), Cardinal(I) y TMyEnum(I) no generan ninguna comprobación de rango y truncan o reinterpretan en silencio. Cuando un valor de 32 bits leído de un archivo selecciona un miembro de un enum, compárelo como Cardinal contra Ord(High(TMyEnum)) antes del cast
var
B: Byte;
C: Cardinal;
I: Integer;
Addr: Integer;
Obj: TObject;
begin
B := 255;
B := B + 1; // evaluado como Integer 256, el almacenamiento falla: ERangeError
I := -1;
C := I; // negativo hacia Cardinal: ERangeError
C := Cardinal(I); // cast duro: sin comprobación, C = 4294967295
// Win64: los punteros son de 8 bytes, Integer sigue de 4
Obj := TObject.Create;
Addr := NativeInt(Obj); // Integer: falla por encima de 2 GB (Cardinal: por encima de 4 GB)
// Corrección: mantenga los valores del tamaño de un puntero en NativeInt / NativeUInt
end;
// Aritmética módulo 2^32 que debe comportarse igual bajo Delphi y FPC
function AddMod32(A, B: Cardinal): Cardinal; inline;
begin
Result := Cardinal((UInt64(A) + UInt64(B)) and $FFFFFFFF);
end;
Win64 agrega una variante de tamaño de puntero del mismo error. Pointer, NativeInt, TComponent.Tag (un NativeInt desde XE2), LPARAM y WPARAM son todos de 8 bytes en Win64, mientras que Integer, Longint y Cardinal siguen de 4. Almacenar una dirección mediante una asignación con comprobación de rango lanza ERangeError en cuanto la dirección queda fuera del rango del tipo de destino: por encima de 2 GB para Integer, por encima de 4 GB para Cardinal. Almacenarla mediante un cast duro la trunca y produce un access violation más adelante. Los programas de prueba pequeños a menudo asignan memoria baja en el espacio de direcciones, así que esta clase de bug tiende a aparecer en procesos de larga vida en lugar de en pruebas unitarias. Declare como NativeInt o NativeUInt todo lo que guarde un puntero, un handle o un parámetro de mensaje. Los problemas de Longint y punteros en Linux64 siguen la misma lógica para objetivos LP64, donde el propio Longint cambia de tamaño
¿Por qué el error aparece solo en Debug, o solo bajo FPC?
Un error de rango que aparece en una sola compilación es casi siempre un acceso real fuera de los límites que la otra compilación no está comprobando. Las compilaciones Release con {$R-} corren el mismo índice erróneo sin queja y o leen la memoria del vecino o la corrompen, lo cual es peor que la excepción. Free Pascal agrega sus propios switches. {$mode delphi} cambia sintaxis y semántica del lenguaje pero no activa ninguna comprobación; la comprobación de rangos bajo FPC viene de {$R+} o de la opción de línea de comandos -Cr, y la comprobación de overflow de -Co, que Lazarus muestra bajo Project Options › Compiler Options › Debugging. FPC también evalúa expresiones enteras en el tamaño del entero nativo o más grande, así que una expresión que se envuelve en silencio en Delphi de 32 bits puede producir un valor por encima de High(Cardinal) en un objetivo FPC de 64 bits, y almacenarlo de vuelta en el Cardinal falla la comprobación. Nosotros nos topamos exactamente con esto en nuestro propio código SHA-256: las sumas módulo 232 escritas como sumas Cardinal corrientes lanzaban Range check error en una compilación FPC con comprobaciones. Sumar en UInt64 y enmascarar, como hace AddMod32 arriba, mantiene la salida byte a byte idéntica en ambos compiladores sin apagar las comprobaciones para toda la unidad. Más trampas de portabilidad de este tipo están reunidas en código Delphi que solo funciona por accidente bajo FPC
Hacer que el debugger se detenga en la línea que lo lanzó
La manera más rápida de encontrar un error de rango es dejar que el debugger del IDE se detenga en él y subir por la pila de llamadas hasta el código que produjo el valor erróneo. La excepción marca dónde se disparó la comprobación. La causa suele estar unos marcos más abajo:
- Corra la configuración Debug con la comprobación de rangos activada, y asegúrese de que
ERangeErrorno esté en la lista de ignorados de la página Language Exceptions de las opciones del debugger - Cuando el diálogo reporta "raised exception class ERangeError with message 'Range check error'", elija Break y abra la ventana Call Stack; los marcos por debajo del primero muestran de dónde salió el índice o el valor
- Si los marcos superiores están dentro de la RTL o la VCL, active "Use debug .dcus" para que esos marcos resuelvan a código fuente
- Ponga un breakpoint condicional en la asignación que calcula el índice (por ejemplo
Index < 0) para atrapar el error un paso antes que la excepción - Para errores que ocurren solo en la máquina de un cliente, distribuya la compilación con la comprobación de rangos activada y un logger de stack traces como madExcept, EurekaLog o JCL Debug, que escribe la misma pila de llamadas a un archivo de log
¿Debe apagar la comprobación de rangos?
Mantenga la comprobación de rangos activada en cada compilación Debug y de CI, y apáguela solo localmente, alrededor de un bucle caliente medido cuyos límites usted mismo valida. Apagarla globalmente no elimina el bug; elimina el reporte. Un ejemplo clásico es una rutina de formateo hexadecimal que llama a SetLength(Result, 16) y escribe los caracteres 17 a 24: bajo {$R+} falla de inmediato, bajo {$R-} escribe ocho caracteres más allá del final de la cadena en cada llamada, hacia la memoria del heap que siga, y crashea en un lugar sin relación mucho después. Cuando una rutina de verdad es demasiado caliente para comprobaciones, acote el cambio correctamente. {$R-} seguido de {$R+} se ve ordenado pero fuerza la activación de las comprobaciones para el resto de la unidad aunque el proyecto las tuviera apagadas, así que guarde y restaure el estado previo, y deje la región sin comprobaciones como una rutina pequeña completa cuyos llamadores hacen cumplir los límites
{$IFOPT R+}
{$DEFINE RANGECHECKS_WERE_ON}
{$R-}
{$ENDIF}
procedure XorBlock(Dst, Src: PByte; Count: Integer);
var
I: Integer;
begin
// Los llamadores garantizan que Count cabe en ambos buffers; comprobado una vez fuera del bucle
for I := 0 to Count - 1 do
Dst[I] := Dst[I] xor Src[I];
end;
{$IFDEF RANGECHECKS_WERE_ON}
{$UNDEF RANGECHECKS_WERE_ON}
{$R+}
{$ENDIF}
Errores de rango en código de parseo de PDF
El código que parsea archivos PDF está especialmente expuesto, porque casi cada número que usa como índice viene del propio archivo. Los offsets de bytes en la tabla de referencias cruzadas, los valores /Length de los streams, los anchos de campo en el arreglo /W de un cross-reference stream (ISO 32000-1 §7.5.8), los conteos de objetos y las entradas /ByteRange de firmas son todos datos no confiables en un archivo mal formado u hostil. Un guard ingenuo como if A + B > Length(Data) falla contra un /ByteRange [9223372036854775807 1 0 0] diseñado a mano: con la comprobación de overflow apagada, A + B se envuelve a un Int64 negativo, la prueba pasa, y Data[A] falla más tarde. Pruebe cada valor contra la longitud restante ((A >= 0) and (B >= 0) and (A <= Len) and (B <= Len - A)) para que ninguna suma pueda envolverse. Los guards compuestos esconden otra trampa. Delphi evalúa and con cortocircuito de izquierda a derecha, pero una prueba de límite superior sola no rechaza un índice negativo, porque -1 < Length(FPages) es True; el límite inferior con signo tiene que probarse antes del superior
// Parece seguro, pero un PageIndex negativo pasa la prueba de límite superior:
// if FDocOpen and (PageIndex < Length(FPages)) and
// (FPages[PageIndex].PageObj <> nil) then ...
if (PageIndex >= 0) and (PageIndex < Length(FPages)) then
begin
if FDocOpen and (FPages[PageIndex].PageObj <> nil) then
Result := FPages[PageIndex].PageObj
else
Result := nil;
end
else
raise ERangeError.CreateFmt(
'Page index %d is out of range (0..%d)',
[PageIndex, Length(FPages) - 1]);
El guard detiene el crash pero no explica por qué el índice estaba mal, y en código de manejo de páginas la razón habitual es un desacuerdo entre dos ordenamientos. ISO 32000-1 §7.7.3 define el orden de páginas mediante los arreglos Kids del árbol de páginas; los números de objeto y los offsets de bytes no dicen nada sobre él, y el objeto 20 puede contener la página 1 mientras el objeto 3 contiene la página 2. El código que construye su lista de páginas escaneando objetos en el orden del archivo funciona en cada documento cuyo generador escribió las páginas en secuencia, y se equivoca con cualquiera que no lo hizo, como un archivo editado al que se le agregó una portada después. Los índices que los llamadores calculan en orden de visualización caen en la página equivocada o más allá del final de la lista. El caso de estudio del orden de páginas PDF muestra cómo confirmar el desacuerdo con qpdf y reconstruir la lista de páginas desde el árbol de páginas
Para sacudir estos errores de su propio parser, compílelo con {$R+} y {$Q+} y córralo sobre un corpus variado: archivos de Word, LaTeX, firmware de escáneres, utilidades de split y merge, y algunos deliberadamente truncados. Cada archivo que lanza una excepción es un punto de datos sobre un supuesto que hace su código, así que corrija el supuesto una vez en lugar de agregar un caso especial por generador que se disparará en el momento equivocado con la siguiente fuente. Para código que llama a una biblioteca PDF, trate el conteo de páginas de la biblioteca como la autoridad. En el componente PDF de Delphi HotPDF, LoadFromFile devuelve el conteo de páginas del documento cargado y LoadedPageCount lo reporta después; los índices de página siguen el orden de visualización del árbol de páginas, así que 0..LoadedPageCount - 1 es el rango seguro para cualquier índice que derive de datos externos