Bir Delphi programı ERangeError ve çıplak "Range check error" mesajıyla durur; çoğunlukla yalnızca iki derleme yapılandırmanızdan birinde. Delphi, aralık kontrolü açık ({$R+}) derlenmiş kod bir diziyi ya da dizgiyi sınırlarının dışında indekslediğinde veya hedef sıralı türe sığmayan bir değer sakladığında bu istisnayı yükseltir. Düzeltme neredeyse her zaman kötü indeksin ya da değerin hesaplandığı yere aittir ve hata ayıklayıcının durduğu satır genellikle orası değildir
Delphi'de "Range check error" ne anlama gelir?
ERangeError, aralık kontrolü açıkken derleyicinin eklediği kontrollerden gelir. {$R+} ile (uzun biçim {$RANGECHECKS ON}, ya da Project Options › Building › Delphi Compiler › Compiling › Range checking) her dizi ve dizgi alt yazımı bildirilen sınırlarla karşılaştırılır ve daha küçük bir sıralı türe, alt aralığa veya sayılana (enum) yapılan her atama, o türün aralığıyla karşılaştırılır. Varsayılan durum {$R-}'dir ve bu şekilde derlenen kod hiçbir kontrol içermez; dolayısıyla sınırların dışına bir yazım, hemen yanındaki belleğe gider. Başarısız bir kontrol, SysUtils bağlıyken "Range check error" kaynak dizgesiyle ERangeError yükseltir; SysUtils kullanmayan bir program ise yerine çalışma hatası 201 ile sonlanır. Derleme zamanı hatası E1012 (Constant expression violates subrange bounds) yalnızca derleyicinin sabit olarak gördüğü indeksleri kapsar; çalışma zamanında hesaplanan her şey {$R+}'a bağlıdır. Anahtar unit başına da çalışır: önderlenmiş DCU'lar derlendikleri durumu korur ve bir unit'in tepesindeki {$R-}, o unit için proje seçeneğini geçersiz kılar
Taşma kontrolü, ayrı bir anahtar ve ayrı bir istisnayla gelir. {$Q+} ({$OVERFLOWCHECKS ON}), +, -, *, Inc ve Dec gibi tamsayı aritmetiğini denetler ve sonuç sığmadığında EIntOverflow yükseltir; {$R+} ise indeksleri ve saklamaları denetler. İkisi küçük türlerde kesişir. Byte aritmetiği Integer olarak değerlendirilir; dolayısıyla B = 255 iken B := B + 1 hiç taşma olmadan 256 üretir ve aralık kontrolünü geçiren, Byte'a geri saklamadır. Aynı değer üzerindeki Inc(B) ise taşma denetimli bir işlemdir: {$Q+} ile EIntOverflow yükseltir, yalnızca {$R+} ile sessizce 0'a sarar. Bu anahtarların hangi yapılandırmalarda açık olduğu, Delphi sürümüne ve projenin nasıl kurulduğuna bağlıdır. Hatanın neden yalnızca bir derlemede göründüğüne dair kuramıza güvenmeden önce Project Options'tan her derleme yapılandırmasının gerçek değerlerini okuyun
Sıradan nedenler, küçük yeniden üretimlerle
Gerçek Delphi kodundaki aralık kontrolü hatalarının çoğu bir avuç örüntüye düşer; her birinin küçük, mekanik bir düzeltmesi vardır. En yaygın durum dizilerdir. Statik bir array[0..9] 0-9 arası indeksleri kabul eder; dinamik bir dizi ya da TBytes 0..Length(A) - 1 kabul eder; dizgiler 1 tabanlıdır, dolayısıyla döngüleri sabit sınırlara karşı değil Low ve High'a karşı yazın. Kodu kıran durum boşluktur. Boş bir dinamik dizinin High'u -1'dir; dolayısıyla Integer sayaçlı bir for I := 0 to High(A) döngüsü doğru şekilde sıfır kez koşar, oysa "son elemanı" almak için kullanılan A[High(A)] ya da A[Count - 1], -1 indeksini okur. İlgili durumlar farklı davranır:
- Kapsayıcı sınıfları kendi indekslerini kendileri denetler,
{$R}durumuna bakmaksızın:TListveTStringListEListErrorya daEStringListErroryükseltir,TList<T>iseEArgumentOutOfRangeExceptionyükseltir - Dosya başlığından ya da bir API'den
CardinalveyaDWORDolarak gelen sayılar sıfırda risklidir; çünküCount - 1'in neye değerlendiği, derleyicinin ifadeyi nasıl türlediğine bağlıdır. Döngü sayaçlarınıIntegertutun, sayıyı bir kez doğrulayın, çevirin veCount > 0'ı açıkça test edin
procedure ArrayRepros(const Data: TBytes; Buf: Pointer);
var
Last: Byte;
P: PByte;
I: Integer;
begin
// Boş dizi: High(Data) = -1, dolayısıyla bu Data[-1] okur
Last := Data[High(Data)]; // {$R+} altında ERangeError
// Düzeltme: son elemanı istemeden önce uzunluğu test edin
if Length(Data) > 0 then
Last := Data[High(Data)];
// TByteArray, array[0..32767] of Byte olarak bildirilmiştir; dolayısıyla {$R+}
// altında Buf megabaytlar tutsa bile 32767 üstü her indeks başarısız olur
I := 40000;
Last := PByteArray(Buf)^[I]; // I = 40000'de ERangeError
// Düzeltme: gerçek tamponu indeksleyin ya da PByte aritmetiği kullanın
P := PByte(Buf);
Inc(P, I);
Last := P^; // aralık kontrolü yok; dolayısıyla I'yi kendiniz doğrulayın
end;
PByteArray durumu deneyimli geliştiricileri yakalar; çünkü bildirilen sınırın tamponla bir ilgisi yoktur. Delphi'nin SysUtils'i de Free Pascal'ınkisi de TByteArray'i array[0..32767] of Byte olarak bildirir; 16 bitlik koddan kalma 32 KB'lik bir pencere. {$R+} altında kontrol, indeksinizi 32767 ile karşılaştırır; dolayısıyla 100 KB'lik bir akıştaki geçerli bir ofset ERangeError yükseltir. {$R-} altında kontrol yoktur ve tamponun gerçek sonunun ötesindeki bir indeks asla yakalanmaz. TBytes'in kendisini indekslemek size gerçek uzunluğa karşı bir kontrol verir; bu da onu daha iyi varsayılan yapar. PByte aritmetiği hiç aralık denetlenmez; tampon boyutuna karşı açık bir testle eşleştirin
Sıralı saklamalar öteki büyük ailedir. Bir Byte'a 256, bir Word'e 70000 ya da 1..12 alt aralığına 13 atamak kontrolü geçemez; negatif bir Integer'ı Cardinal'a atamak da öyle. Delphi, karışık işaretli/işaretsiz 32 bit işlenenleri Int64 olarak değerlendirir; dolayısıyla aritmetiğin kendisi genellikle doğrudur ve hata saklamada görünür; bu da türü fazla dar olan değişkeni işaret eder. Bu ağın boşluğu sert tür dönüşümleridir: Byte(I), Cardinal(I) ve TMyEnum(I) aralık kontrolü üretmez ve sessizce kırpır ya da yeniden yorumlar. Bir dosyadan okunan 32 bitlik bir değer bir enum üyesi seçecekse, dönüşümden önce onu Ord(High(TMyEnum)) ile Cardinal olarak karşılaştırın
var
B: Byte;
C: Cardinal;
I: Integer;
Addr: Integer;
Obj: TObject;
begin
B := 255;
B := B + 1; // Integer 256 olarak değerlendirilir, saklama başarısız: ERangeError
I := -1;
C := I; // Cardinal'a negatif: ERangeError
C := Cardinal(I); // sert dönüşüm: kontrol yok, C = 4294967295
// Win64: işaretçiler 8 bayt, Integer 4 kalır
Obj := TObject.Create;
Addr := NativeInt(Obj); // Integer: 2 GB üstünde başarısız (Cardinal: 4 GB üstünde)
// Düzeltme: işaretçi boyutlu değerleri NativeInt / NativeUInt'te tutun
end;
// Delphi ve FPC altında aynı davranması gereken mod 2^32 aritmetiği
function AddMod32(A, B: Cardinal): Cardinal; inline;
begin
Result := Cardinal((UInt64(A) + UInt64(B)) and $FFFFFFFF);
end;
Win64, aynı hatanın işaretçi boyutlu bir türünü ekler. Pointer, NativeInt, TComponent.Tag (XE2'den beri bir NativeInt), LPARAM ve WPARAM Win64'te 8 bayttır; Integer, Longint ve Cardinal ise 4 kalır. Aralık denetimli bir atamayla bir adres saklamak, adres hedef türün aralığının dışına çıktığında ERangeError yükseltir: Integer için 2 GB üstü, Cardinal için 4 GB üstü. Sert bir dönüşümle saklamak ise onu kırpır ve daha sonra bir erişim ihlali üretir. Küçük test programları adres uzayının alt kesimlerinde yer alır; dolayısıyla bu sınıf hata, unit testlerinden çok uzun ömürlü süreçlerde görünür. İşaretçi, tanıtıcı (handle) ya da ileti parametresi tutan her şeyi NativeInt ya da NativeUInt olarak bildirin. Linux64'te Longint ve işaretçi tuzakları, Longint'in kendisinin boyut değiştirdiği LP64 hedefleri için aynı mantığı izler
Hata neden yalnızca Debug'da ya da yalnızca FPC altında görünüyor?
Yalnızca bir derlemede görünen aralık kontrolü hatası, neredeyse her zaman öbür derlemenin denetlemediği gerçek bir sınırlar-dışı erişimdir. {$R-}'li Release derlemeleri aynı hatalı indeksi şikâyetsiz koşturur ve ya komşunun belleğini okur ya da onu bozar; bu, istisnadan daha kötüdür. Free Pascal kendi anahtarlarını ekler. {$mode delphi}, dil sözdizimini ve anlambilimini değiştirir ama hiçbir kontrolü açmaz; FPC altında aralık kontrolü {$R+}'dan ya da -Cr komut satırı seçeneğinden, taşma kontrolü ise Lazarus'un Project Options › Compiler Options › Debugging altında gösterdiği -Co'dan gelir. FPC ayrıca tamsayı ifadelerini yerel tamsayı boyutunda ya da daha genişte değerlendirir; dolayısıyla 32 bitlik Delphi'de sessizce saran bir ifade, 64 bitlik bir FPC hedefinde High(Cardinal) üstü bir değer üretebilir ve onu Cardinal'a geri saklamak kontrolü geçemez. Kendi SHA-256 kodumuzda tam olarak buna çarptık: düz Cardinal toplamları olarak yazılan 232 modulu eklemeler, denetimli bir FPC derlemesinde Range check error yükseltti. Yukarıdaki AddMod32'nin yaptığı gibi UInt64'te toplamak ve masklemek, bütün unit için kontrolleri kapatmadan çıktıyı iki derleyicide de bayt bayt özdeş tutar. Bu türdeki daha çok taşınabilirlik tuzağı FPC altında yalnızca tesadüfen çalışan Delphi kodu yazısında toplanmıştır
Hata ayıklayıcının yükselten satırda durmasını sağlamak
Bir aralık kontrolü hatasını bulmanın en hızlı yolu, IDE hata ayıklayıcısının üzerinde kırılmasını sağlamak ve çağrı yığınında kötü değeri üreten koda çıkmaktır. İstisna, kontrolün nerede tetiklendiğini işaretler. Neden genellikle birkaç çerçeve aşağıda oturur:
- Debug yapılandırmasını aralık kontrolü açıkken koşturun ve
ERangeError'un hata ayıklayıcı seçeneklerinin Language Exceptions sayfasındaki yoksayılanlar listesinde olmadığından emin olun - İletişim kutusu "raised exception class ERangeError with message 'Range check error'" bildirdiğinde Break'i seçin ve Call Stack penceresini açın; tepedeki çerçevenin altındakiler, indeksin ya da değerin nereden geldiğini gösterir
- En üstteki çerçeveler RTL ya da VCL içindeyse "Use debug .dcus" seçeneğini açın ki o çerçeveler kaynağa çözülsün
- İndeksi hesaplayan atamaya koşullu bir kırılma noktası koyun (örneğin
Index < 0); hatayı istisyondan bir adım önce yakalarsınız - Yalnızca müşteri makinesinde görünen hatalar için derlemeyi aralık kontrolü açık ve madExcept, EurekaLog ya da JCL Debug gibi bir yığın izleme günlükçüsüyle gönderin; aynı çağrı yığınını bir günlük dosyasına yazarlar
Aralık kontrolünü kapatmalı mısınız?
Aralık kontrolünü her Debug ve CI derlemesinde açık tutun ve yalnızca yerel olarak, sınırlarını kendiniz doğruladığınız ölçülmüş bir sıcak döngünün çevresinde kapatın. Genelinde kapatmak hatayı yok etmez; raporu yok eder. Klasik örnek, SetLength(Result, 16) çağırıp 17'den 24'e karakter yazan bir on altılık biçimlendirme yordamıdır: {$R+} altında anında başarısız olur; {$R-} altında her çağrıda dizginin sonunun sekiz karakter ötesine, arkasından gelen hangi yığın belleğine yazar ve çok sonra ilgisiz bir yerde çöker. Bir yordam kontroller için gerçekten fazla sıcaksa değişikliği doğru kapsamlayın. {$R-} ardından {$R+} düzgün görünür ama unit'in geri kalanı için kontrolü zorlar; proje kapalı tutuyorsa bile. Bunun yerine önceki durumu saklayıp geri yükleyin ve kontrolsüz bölgeyi, çağıranlarının sınırları zorladığı küçük bir bütün yordamda tutun
{$IFOPT R+}
{$DEFINE RANGECHECKS_WERE_ON}
{$R-}
{$ENDIF}
procedure XorBlock(Dst, Src: PByte; Count: Integer);
var
I: Integer;
begin
// Çağıranlar Count'un her iki tampona sığdığını garanti eder; döngü dışında bir kez denetlenir
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}
PDF ayrıştırma kodunda aralık kontrolü hataları
PDF dosyalarını ayrıştıran kod özellikle açıktır; çünkü indeks olarak kullandığı neredeyse her sayı dosyanın kendisinden gelir. Çapraz başvuru tablosundaki bayt ofsetleri, akışların /Length değerleri, bir çapraz başvuru akışının /W dizisindeki alan genişlikleri (ISO 32000-1 §7.5.8), nesne sayıları ve imzalardaki /ByteRange girdileri, bozuk ya da düşmanca bir dosyada tümüyle güvensizdir. if A + B > Length(Data) gibi naif bir koruma, işlenmiş bir /ByteRange [9223372036854775807 1 0 0] karşısında başarısız olur: taşma kontrolü kapalıyken A + B negatif bir Int64'e sarar, test geçer ve Data[A] daha sonra başarısız olur. Her değeri kalan uzunluğa karşı test edin ((A >= 0) and (B >= 0) and (A <= Len) and (B <= Len - A)); hiçbir toplama saramaz. Bileşik korumalar başka bir tuzağı gizler. Delphi and'i soldan sağa kısa devre yapar ama tek başına bir üst sınır testi negatif indeksi reddetmez; çünkü -1 < Length(FPages) True'dur. İşaretli alt sınır, üst sınırdan önce test edilmek zorundadır
// Güvenli görünür ama negatif bir PageIndex üst sınır testini geçer:
// 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]);
Koruma çökmeyi durdurur ama indeksin neden yanlış olduğunu açıklamaz; sayfa işleme kodunda sıradan neden, iki sıralama arasındaki uyumsuzluktur. ISO 32000-1 §7.7.3, sayfa sırasını sayfa ağacının Kids dizileri üzerinden tanımlar; nesne numaraları ve bayt ofsetleri bu konuda hiçbir şey söylemez ve nesne 20, sayfa 1'i tutabilirken nesne 3, sayfa 2'yi tutabilir. Sayfa listesini dosya sırasında nesneleri tarayarak kuran kod, üreticisi sayfaları sırayla yazmış her belgede çalışır ve yapmayan tek bir belgede yanılır; örneğin sonradan eklenmiş kapak sayfalı düzenlenmiş bir dosya. Çağıranların gösterim sırasında hesapladığı indeksler, yanlış sayfaya ya da listenin sonunun ötesine düşer. PDF sayfa sırası vaka çalışması, uyumsuzluğu qpdf ile nasıl doğrulayacağınızı ve sayfa listesini sayfa ağacından nasıl yeniden kuracağınızı gösterir
Bu hataları kendi ayrıştırıcınızdan arındırmak için onu {$R+} ve {$Q+} ile derleyin ve çeşitli bir derlem (korpus) üzerinden koşturun: Word'den, LaTeX'ten, tarayıcı aygıt yazılımından, böl-birleştir araçlarından gelen dosyalar ve birkaç bilerek kırpılmış dosya. İstisna yükselten her dosya, kodunuzun yaptığı bir varsayım hakkında bir veri noktasıdır; o yüzden varsayımı bir kez düzeltin, bir sonraki kaynakta yanlış zamanda tetiklenecek üreticiye özel bir istisna eklemeyin. Bir PDF kütüphanesi çağıran kod için kütüphanenin sayfa sayısını yetkili sayın. HotPDF Delphi PDF bileşeninde LoadFromFile, yüklenen belgenin sayfa sayısını döndürür ve LoadedPageCount sonradan bildirir; sayfa indeksleri sayfa ağacının gösterim sırasını izler, dolayısıyla harici veriden türettiğiniz her indeks için güvenli aralık 0..LoadedPageCount - 1'dir