Um programa Delphi para com ERangeError e a mensagem crua "Range check error", muitas vezes em apenas uma das suas duas configurações de build. O Delphi levanta essa exceção quando código compilado com range checking ativado ({$R+}) indexa um array ou string fora dos limites, ou armazena um valor que não cabe no tipo ordinal de destino. A correção quase sempre pertence ao lugar onde o índice ou valor inválido foi calculado, que raramente é a linha onde o debugger para
O que significa "Range check error" no Delphi?
ERangeError vem de verificações que o compilador insere quando o range checking está ligado. Com {$R+} (forma longa {$RANGECHECKS ON}, ou Project Options › Building › Delphi Compiler › Compiling › Range checking), todo subscrito de array e string é comparado contra os limites declarados, e toda atribuição a um ordinal, subrange ou variável enumerada menor é comparada contra o intervalo desse tipo. O estado padrão é {$R-}, e código compilado assim não contém verificação alguma, então uma escrita fora dos limites simplesmente cai na memória vizinha. Uma verificação que falha levanta ERangeError com a resource string "Range check error" quando SysUtils está linkado; um programa que nunca usa SysUtils termina com o runtime error 201. O erro de compilação E1012 (Constant expression violates subrange bounds) só cobre índices que o compilador vê como constantes; qualquer coisa calculada em runtime depende de {$R+}. A chave também funciona por unit: DCUs pré-compiladas mantêm o estado com que foram construídas, e um {$R-} no topo de uma unit sobrepõe a opção do projeto para aquela unit
Overflow checking é uma chave separada com uma exceção separada. {$Q+} ({$OVERFLOWCHECKS ON}) verifica aritmética de inteiros como +, -, *, Inc e Dec e levanta EIntOverflow quando um resultado não cabe; {$R+} verifica índices e armazenamentos. As duas se encontram em tipos pequenos. Aritmética de Byte é avaliada como Integer, então B := B + 1 com B = 255 produz 256 sem overflow nenhum, e é o armazenamento de volta no Byte que falha no range check. Inc(B) no mesmo valor é uma operação com verificação de overflow: com {$Q+} levanta EIntOverflow, e com apenas {$R+} volta silenciosamente para 0. Quais configurações têm essas chaves ligadas depende da versão do Delphi e de como o projeto foi criado. Leia os valores reais em Project Options para cada configuração de build antes de confiar em uma teoria sobre por que o erro aparece em apenas um build
As causas usuais, com repros mínimos
A maioria dos erros de range check em código Delphi real cai em um punhado de padrões, cada um com uma correção pequena e mecânica. Arrays são o caso mais comum. Um array[0..9] estático aceita índices 0 a 9, um array dinâmico ou TBytes aceita 0..Length(A) - 1, e strings são de base 1, então escreva loops contra Low e High em vez de limites fixos no código. O caso vazio é onde o código quebra. High de um array dinâmico vazio é -1, então um loop for I := 0 to High(A) com contador Integer corretamente roda zero vezes, enquanto A[High(A)] ou A[Count - 1] usado para buscar "o último elemento" lê o índice -1. Casos relacionados se comportam de forma diferente:
- Classes de contêiner verificam os próprios índices, seja qual for o estado do
{$R}:TListeTStringListlevantamEListErrorouEStringListError, eTList<T>levantaEArgumentOutOfRangeException - Contagens que chegam como
CardinalouDWORDde um header de arquivo ou de uma API são arriscadas em zero, porque o queCount - 1avalia depende de como o compilador tipa a expressão; mantenha contadores de loop comoInteger, valide a contagem uma vez, converta-a, e testeCount > 0explicitamente
procedure ArrayRepros(const Data: TBytes; Buf: Pointer);
var
Last: Byte;
P: PByte;
I: Integer;
begin
// Array vazio: High(Data) = -1, então isto lê Data[-1]
Last := Data[High(Data)]; // ERangeError sob {$R+}
// Correção: teste o comprimento antes de pedir o último elemento
if Length(Data) > 0 then
Last := Data[High(Data)];
// TByteArray é declarado como array[0..32767] of Byte, então sob {$R+}
// todo índice acima de 32767 falha mesmo quando Buf guarda megabytes
I := 40000;
Last := PByteArray(Buf)^[I]; // ERangeError em I = 40000
// Correção: indexe o buffer real, ou use aritmética de PByte
P := PByte(Buf);
Inc(P, I);
Last := P^; // sem range check, então valide I você mesmo
end;
O caso do PByteArray pega desenvolvedores experientes porque o limite declarado não tem nada a ver com o buffer. Tanto o SysUtils do Delphi quanto o do Free Pascal declaram TByteArray como array[0..32767] of Byte, uma janela de 32 KB que data do código de 16 bits. Sob {$R+} a verificação compara seu índice com 32767, então um offset válido em um stream de 100 KB levanta ERangeError; sob {$R-} não há verificação, e um índice além do fim real do buffer nunca é pego. Indexar o TBytes em si dá uma verificação contra o comprimento real, o que o torna a escolha padrão melhor. Aritmética de PByte não é verificada por range check nenhum. Combine-a com um teste explícito contra o tamanho do buffer
Armazenamentos ordinais são a outra grande família. Atribuir 256 a um Byte, 70000 a um Word, ou 13 a um subrange 1..12 falha na verificação, e atribuir um Integer negativo a um Cardinal também. O Delphi avalia operandos mistos de 32 bits com e sem sinal como Int64, então a aritmética em si geralmente está correta e o erro aparece no armazenamento, o que aponta para a variável cujo tipo é estreito demais. Typecasts duros são a brecha dessa rede: Byte(I), Cardinal(I) e TMyEnum(I) não geram range check e truncam ou reinterpretam silenciosamente. Quando um valor de 32 bits lido de um arquivo seleciona um membro de enum, compare-o como Cardinal contra Ord(High(TMyEnum)) antes do cast
var
B: Byte;
C: Cardinal;
I: Integer;
Addr: Integer;
Obj: TObject;
begin
B := 255;
B := B + 1; // avaliado como Integer 256, o armazenamento falha: ERangeError
I := -1;
C := I; // negativo em Cardinal: ERangeError
C := Cardinal(I); // hard cast: sem verificação, C = 4294967295
// Win64: ponteiros têm 8 bytes, Integer continua 4
Obj := TObject.Create;
Addr := NativeInt(Obj); // Integer: falha acima de 2 GB (Cardinal: acima de 4 GB)
// Correção: mantenha valores do tamanho de ponteiro em NativeInt / NativeUInt
end;
// Aritmética módulo 2^32 que precisa se comportar igual no Delphi e no FPC
function AddMod32(A, B: Cardinal): Cardinal; inline;
begin
Result := Cardinal((UInt64(A) + UInt64(B)) and $FFFFFFFF);
end;
O Win64 adiciona uma variante de tamanho de ponteiro do mesmo erro. Pointer, NativeInt, TComponent.Tag (um NativeInt desde o XE2), LPARAM e WPARAM têm todos 8 bytes no Win64, enquanto Integer, Longint e Cardinal ficam com 4. Armazenar um endereço através de uma atribuição com range check levanta ERangeError assim que o endereço fica fora do intervalo do tipo de destino: acima de 2 GB para Integer, acima de 4 GB para Cardinal. Armazená-lo através de um hard cast o trunca e produz um access violation mais tarde. Programas de teste pequenos geralmente alocam baixo no espaço de endereços, então essa classe de bug tende a aparecer em processos de longa duração em vez de testes de unit. Declare qualquer coisa que guarda um ponteiro, um handle ou um parâmetro de mensagem como NativeInt ou NativeUInt. As armadilhas de Longint e ponteiro no Linux64 seguem a mesma lógica para alvos LP64, onde o próprio Longint muda de tamanho
Por que o erro aparece só no Debug, ou só sob o FPC?
Um erro de range check que aparece em apenas um build é quase sempre um acesso real fora dos limites que o outro build não está verificando. Builds Release com {$R-} rodam o mesmo índice defeituoso sem reclamar e ou leem a memória do vizinho ou a corrompem, o que é pior que a exceção. O Free Pascal adiciona chaves próprias. {$mode delphi} muda sintaxe e semântica da linguagem mas não liga verificação alguma; range checking sob FPC vem de {$R+} ou da opção de linha de comando -Cr, e overflow checking de -Co, que o Lazarus mostra em Project Options › Compiler Options › Debugging. O FPC também avalia expressões inteiras no tamanho de inteiro nativo ou maior, então uma expressão que volta silenciosamente em 32 bits no Delphi pode produzir um valor acima de High(Cardinal) em um alvo FPC de 64 bits, e armazená-lo de volta no Cardinal falha na verificação. Nós acertamos exatamente isso em nosso próprio código SHA-256: as adições módulo 232 escritas como somas Cardinal simples levantavam Range check error em um build FPC verificado. Somar em UInt64 e mascarar, como o AddMod32 acima faz, mantém a saída byte a byte idêntica nos dois compiladores sem desligar as verificações da unit inteira. Mais armadilhas de portabilidade desse tipo estão coletadas em código Delphi que só funciona por acidente sob FPC
Fazendo o debugger parar na linha que levantou o erro
O jeito mais rápido de encontrar um erro de range check é deixar o debugger do IDE quebrar nele e subir a pilha de chamadas até o código que produziu o valor inválido. A exceção marca onde a verificação disparou. A causa geralmente fica alguns frames abaixo:
- Rode a configuração Debug com range checking ligado, e certifique-se de que
ERangeErrornão está na lista de ignorados na página Language Exceptions das opções do debugger - Quando o diálogo reporta "raised exception class ERangeError with message 'Range check error'", escolha Break e abra a janela Call Stack; os frames abaixo do primeiro mostram de onde o índice ou valor veio
- Se os primeiros frames estão dentro da RTL ou VCL, ative "Use debug .dcus" para que esses frames resolvam para fonte
- Defina um breakpoint condicional na atribuição que calcula o índice (por exemplo
Index < 0) para pegar o erro um passo antes da exceção - Para erros que ocorrem só na máquina de um cliente, entregue o build com range checking ligado e um logger de stack trace como madExcept, EurekaLog ou JCL Debug, que escreve a mesma pilha de chamadas em um arquivo de log
Você deveria desligar o range checking?
Mantenha o range checking ligado em todo build de Debug e CI, e desligue-o apenas localmente, em volta de um hot loop medido cujos limites você mesmo valida. Desligá-lo globalmente não remove o bug; remove o relato. Um exemplo clássico é uma rotina de formatação hex que chama SetLength(Result, 16) e escreve caracteres de 17 a 24: sob {$R+} falha imediatamente, sob {$R-} escreve oito caracteres além do fim da string a cada chamada, na memória de heap que vier depois, e quebra em um lugar não relacionado muito mais tarde. Quando uma rotina realmente é quente demais para verificações, delimitar o escopo da mudança corretamente importa. {$R-} seguido de {$R+} parece arrumado mas força a verificação ligada pelo resto da unit mesmo que o projeto a tivesse desligada, então salve e restaure o estado anterior, e mantenha a região sem verificação em uma rotina pequena inteira cujos chamadores garantem os limites
{$IFOPT R+}
{$DEFINE RANGECHECKS_WERE_ON}
{$R-}
{$ENDIF}
procedure XorBlock(Dst, Src: PByte; Count: Integer);
var
I: Integer;
begin
// Os chamadores garantem que Count cabe nos dois buffers; verificado uma vez fora do loop
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}
Erros de range check em código de parsing de PDF
Código que analisa arquivos PDF é especialmente exposto, porque quase todo número que usa como índice vem do próprio arquivo. Byte offsets na tabela de referência cruzada, valores /Length de streams, larguras de campo no array /W de um cross-reference stream (ISO 32000-1 §7.5.8), contagens de objetos e entradas /ByteRange em assinaturas são todos não confiáveis em um arquivo malformado ou hostil. Uma guarda ingênua como if A + B > Length(Data) falha contra um /ByteRange [9223372036854775807 1 0 0] forjado: com overflow checking desligado, A + B volta para um Int64 negativo, o teste passa, e Data[A] falha mais tarde. Teste cada valor contra o comprimento restante ((A >= 0) and (B >= 0) and (A <= Len) and (B <= Len - A)) para que nenhuma adição possa voltar. Guardas compostas escondem outra armadilha. O Delphi avalia and em curto-circuito da esquerda para a direita, mas um teste só de limite superior não rejeita um índice negativo, porque -1 < Length(FPages) é True; o limite inferior com sinal precisa ser testado antes do superior
// Parece seguro, mas um PageIndex negativo passa no teste de limite 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]);
A guarda para o crash mas não explica por que o índice estava errado, e em código de manipulação de páginas o motivo usual é uma incompatibilidade entre duas ordenações. A ISO 32000-1 §7.7.3 define a ordem de páginas através dos arrays Kids da árvore de páginas; números de objeto e byte offsets nada dizem sobre ela, e o objeto 20 pode guardar a página 1 enquanto o objeto 3 guarda a página 2. Código que constrói a lista de páginas dele varrendo objetos na ordem do arquivo funciona em todo documento cujo gerador escreveu as páginas em sequência, e erra em qualquer um que não o fez, como um arquivo editado com uma página de capa adicionada depois. Índices que os chamadores calculam na ordem de exibição caem na página errada ou além do fim da lista. O estudo de caso de ordem de páginas em PDF mostra como confirmar a incompatibilidade com o qpdf e reconstruir a lista de páginas a partir da árvore de páginas
Para sacudir esses erros do seu próprio parser, construa-o com {$R+} e {$Q+} e rode-o sobre um corpus variado: arquivos do Word, LaTeX, firmware de scanner, utilitários de divisão e mesclagem e alguns deliberadamente truncados. Cada arquivo que levanta uma exceção é um ponto de dados sobre uma suposição que seu código faz, então corrija a suposição uma vez em vez de adicionar um caso especial por gerador que vai disparar na hora errada na próxima fonte. Para código que chama uma biblioteca PDF, trate a contagem de páginas da biblioteca como a autoridade. No componente PDF Delphi HotPDF, LoadFromFile retorna a contagem de páginas do documento carregado e LoadedPageCount a reporta depois; os índices de página seguem a ordem de exibição da árvore de páginas, então 0..LoadedPageCount - 1 é o intervalo seguro para qualquer índice que você derive de dados externos