Teknik Makale

HotXLS, zlib-ng CRC32 ve Delphi İş Parçacıklarında Yığın Taşması

HotXLS, büyük bir çalışma sayfası XML parçasını tek bir çağrıda sağlama toplamına tabi tuttuğunda yakalanabilir bir istisna olmadan bir Delphi işçi iş parçacığını çökertebilir: zlib-ng, yaklaşık 119 KB girdinin üzerinde Chorba algoritmasına geçer ve bu algoritmanın genel-C varyantı, varsayılan 1 MB iş parçacığı yığınını patlatacak kadar büyük bir taslak dizi ayırır. Delphi'nin tepki verme şansı hiç olmaz, çünkü bir yığın taşması try/except'in yakalamak için tasarlandığı türden bir istisna değildir

HotXLS, Excel çalışma kitaplarını okumak ve yazmak için yerel bir Delphi ve C++Builder kütüphanesidir ve çökme, çalışma sayfası yazıcısına kadar izlendi. İlk sorun belirtisi bir destek biletiydi: gece boyunca çalışan bir dışa aktarım işi haftada yaklaşık iki kez, her zaman çalışma ortasında, Delphi istisna diyaloğu olmadan ve günlüğe kaydedilmiş bir hata olmadan çöküyordu, yalnızca kaybolan bir süreç ve hiçbir yere işaret etmeyen bir Windows Hata Bildirimi girdisi vardı. Bunu masada yeniden üretmek tamamen başka bir konuydu. Küçük çalışma kitapları sorunsuz kaydediliyordu. Büyük çalışma kitapları da, kayıt ana iş parçacığında ve zaten bağlı bir hata ayıklayıcıyla çalıştığı sürece sorunsuz kaydediliyordu. Gerçek çok iş parçacıklı dışa aktarım yolundan geçen üretim boyutunda gerçek bir dosya toplusu, disk G/Ç'si, bellek baskısı ve şüpheli bir şablon her biri zaten elenmiş olduğu noktada çökmeyi eve getirdi

Bir çalışma sayfası kaydı nasıl tek bir dev CRC32 çağrısına dönüşür?

XLSX dosyaları ZIP kapsayıcılarıdır ve ZIP formatı, hem yerel dosya başlığında hem de merkezi dizinde kaydedilen her girdi için bir CRC-32 sağlama toplamı gerektirir. HotXLS bu sağlama toplamını, SaveAs bir çalışma sayfasının XML'ini bellekte birleştirmeyi bitirdikten sonra zlib-ng'nin kendi crc32 rutinini çağıran ZLibCRC32 adlı küçük bir sarmalayıcıyı çağırarak hesaplar ve uzun süre bu çağrı tüm sıkıştırılmamış arabelleği tek bir çağrıda taşıdı. Bu, küçük bir çalışma sayfası için makul bir tasarımdır. Bir sayfa, HotXLS'te büyük çalışma kitabı performansı rehberimizde ele alınan türden olduğu an, tek bir sayfanın XML'i sıkıştırılmadan önce rutin olarak birkaç yüz kilobaytı aştığında çok büyük bir çağrı haline gelir

zlib-ng CRC32 için neden dev bir yığın arabelleğine ihtiyaç duyar?

zlib-ng her çağrı için tek bir CRC-32 uygulaması kullanmaz. Bir boyut eşiğinin altında, anlamlı bir ekstra bellek gerektirmeyen tablo aramaları ve katlama numaralarıyla arabelleği dolaşır ve bu eşiğin üzerinde -yaklaşık 119 KB, HotXLS'in bağlandığı derlemede tam olarak 118.960 bayt- Chorba adlı özel bir hızlı algoritmaya geçer. O yolun genel-C uygulaması belleği hız için değiş tokuş eder: yığında değil yığın (stack) üzerinde bir taslak dizi ayırır, çağıran iş parçacığının taşıdığı yığın bütçesine rahatça sığması için değil, algoritmanın iç döngüsünü hızlı yapması için boyutlandırılmış. Bunların hiçbiri çağıran tarafından görünmez. Bir sağlama toplamı fonksiyonu normalde bir yaprak çağrıdır, birkaç bayt oku, bir sayı döndür, tartışmaya değer bir tahsis yok, ve bu varsayım, Chorba eşiğini aşacak kadar büyük bir arabellek birine girene kadar zlib-ng'ye yapılan çağrıların ezici çoğunluğu için geçerlidir

function BuildWorksheetPartCrc(const XmlBytes: TBytes): LongWord;
begin
  // One call over the whole worksheet XML buffer: fine for a small
  // sheet, but a large enough input pushes zlib-ng onto its Chorba
  // fast path and that path's stack-hungry scratch buffer
  Result := ZLibCRC32(0, XmlBytes[0], Length(XmlBytes));
end;

İşçi iş parçacıkları bunu neden gördü, interaktif hata ayıklama neden hiç görmedi?

Bu çökmeyi tetiklemek aynı anda iki koşul gerektirir: zlib-ng'nin Chorba eşiğini aşacak kadar büyük bir çalışma sayfası XML parçası ve daha ferah bir şey yerine yalnızca sıradan varsayılan yığına sahip bir iş parçacığı. Üretim dışa aktarım işleri her ikisine de çarpar. Bunlar, HotXLS yazmalarını bir işçi iş parçacığı havuzuna yayan sunucu tarafı toplu işler olarak çalışır; her biri, bir çağıran daha fazlasını istemedikçe Windows'un ayırdığı varsayılan 1 MB yığını taşır ve her biri önemli olacak kadar büyük müşteri çalışma kitaplarını işler. Masada hata ayıklama iki koşuldan da güvenilir bir şekilde çarpmadı: örnek dosyalar genellikle eşikten küçüktü ve tek adım çalıştırmalar taze bir şekilde oluşturulmuş bir işçinin içinde değil ana iş parçacığında olma eğilimindeydi; bu yüzden üretimde hizalanması gereken iki koşul bir geliştiricinin masasında neredeyse hiç hizalanmadı

Yanlış fonksiyonu suçlayan bir çökmeyi kovalamak

Ekibin eline geçen çökme raporları, herhangi bir HotXLS kodunda değil, zlib-ng'nin deflate fonksiyonunun içinde bir konuma işaret ediyordu ve açıkça CRC-32 koduna da değil. Bu tek ayrıntı, soruşturmanın ilk geçişini sıkıştırma yoluna gönderdi: deflate'e geçirilen arabellek boyutları, pencere bitleri, sıkıştırma düzeyi, bir codec'ten çıkan yerel bir çökme için tüm alışılmış şüpheliler. Hiçbiri tutmadı

Yanıltıcı bir üst çerçeve

Bir yığın taşması sembolize etmek için garip bir çökme türüdür, çünkü rapor edildiğinde, yığın işaretçisi kendisi için ayrılmış alanı zaten aşmıştır. O çökme raporunu her ne ürettiyse, hatalı adresi hâlâ bulabildiği en yakın sembole çözmüş olması en olasıdır ve gerçek suçlunun yanında oturan en yakın dışa aktarılan giriş noktası deflate olmuş oldu. Gerçek hata, aynı kütüphaneye derlenmiş CRC-32 yolu içindeki Chorba taslak arabellek tahsisinde oturuyordu; ikili dosyada gerçekte çalışan fonksiyonla karıştırılacak kadar yakın

Bir hata ayıklayıcı yerine zaman damgalarıyla ikiye bölme

Tüm süreci indiren bir çökme, normal bir Delphi hata ayıklayıcı oturumunun yakalayacağı hiçbir şey bırakmaz; bu yüzden ekip, her şüpheli çağrının etrafına bırakılmış GetTickCount kontrol noktalarına ve kayıt yolu boyunca manuel bir ikiye bölmeye başvurdu; süreç öldüğü anda hangi işlemin uçuşta olduğunu daraltarak. Bunun yanı sıra, bilinen iyi bir taban çizgisi derlemesi, o turun kendi değişikliklerindeki bir gerilemeyi daha ileriye bakmadan önce elemek için özellikle aynı üretim dosyalarını mevcut olanla yan yana çalıştırdı. Her iki kontrol de temiz döndükten sonra ancak soruşturma, mükemmel geçerli bir girdiyle beklenmedik bir şey yapan bir üçüncü taraf bağımlılığında karar kıldı

try/except neden bir yığın taşmasını yakalayamıyor?

Bir yığın taşması, Delphi kodunun kasıtlı olarak hiç fırlattığı bir istisna değildir ve Windows'un bir erişim ihlalini veya sıfıra bölmeyi teslim ettiği şekilde de teslim edilmez. Bir donanım koruma sayfası hatası olarak yüzeye çıkar, Delphi'nin try/except'inin üzerine kurulduğu aynı yapılandırılmış istisna işleme mekanizması üzerinden bildirilir, ama tam olarak tetiklendiği anda normalde bir işleyiciyi çalıştırmak, temizleme kodunu geri sarmak veya hatta hatayı temizce bildirmeyi bitirmek için hiçbir yığın alanı kalmaz. Yalnızca varsayılan 1 MB rezervasyonu taşıyan bir işçi iş parçacığında, o boyutta bir taslak arabellek zaten kalanın çoğunu tükettiğinden, çalışma zamanının çalışabileceği hiçbir şey kalmaz

procedure TExportWorker.Execute;
var
  Workbook: TXLSXWorkbook;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    try
      BuildWorksheet(Workbook);
      Workbook.SaveAs(FTargetFile);  // crashes the process here on a
                                      // large enough sheet: try/except
                                      // never gets a chance to run
    except
      on E: Exception do
        LogError('Export failed: ' + E.Message);
    end;
  finally
    Workbook.Free;
  end;
end;

O except bloğu bir güvenlik ağı gibi görünür ve çoğu hataya karşı öyledir de, ama burada hiçbir şey yapmaz. Ekip bunu pratikte doğruladı: try/except hiçbir şey yakalamadı, finally bloğu da güvenilir bir şekilde çalışma şansı hiç bulamadı ve operatör, hiçbir uygulama düzeyinde günlük girdisi olmayan ölü bir süreç gördü, orijinal destek biletinin tarif ettiği tam olarak buydu

Çözüm: CRC32'yi tek bir dev çağrı yerine 64 KB dilimler halinde beslemek

HotXLS'in gönderdiği düzeltme, zlib-ng'nin kendisi hakkında hiçbir şeyi ve çalışma kitabını yazmak için kullanılan sıkıştırma düzeyi hakkında hiçbir şeyi değiştirmez. ZLibCRC32 artık girdiyi sabit 64 KB dilimler halinde, her biri 65536 bayt, dolaşır; zlib-ng'nin crc32'sini dilim başına bir kez çağırır ve çalışan sağlama toplamı değerini bir çağrıdan diğerine iplik gibi geçirir. CRC-32 yapı gereği artımlı (incremental) bir algoritmadır; bu yüzden birkaç dilim üzerinden oluşturulan bir sağlama toplamı, aynı baytlar üzerinde tek bir çağrıda hesaplanan bir sağlama toplamıyla bayt bayt aynıdır: düzeltme işin nasıl bölündüğünü değiştirir, neyi hesapladığını değil

function ZLibCRC32(crc: LongWord; const buffer; count: Longint): LongWord;
const
  // 64 KB keeps every call comfortably under the Chorba threshold
  CrcChunkSize = 65536;
var
  Cursor: PByte;
  ThisChunk: Longint;
begin
  Result := crc;
  Cursor := PByte(@buffer);
  while count > 0 do
  begin
    ThisChunk := count;
    if ThisChunk > CrcChunkSize then
      ThisChunk := CrcChunkSize;
    Result := zng_crc32(Result, Cursor, Cardinal(ThisChunk));
    Inc(Cursor, ThisChunk);
    Dec(count, ThisChunk);
  end;
end;

Bunun çalışması için çevredeki SaveAs çağrısında hiçbir şeyin değişmesi gerekmedi ve HotXLS'in yazdığı ZIP girdileri hakkında da hiçbir şey değişmedi: yerel dosya başlığında ve merkezi dizinde sona eren CRC-32 değeri, tek bir dev çağrının üreteceği değerin tam olarak aynısıdır, yalnızca daha küçük parçalardan bir araya getirilmiştir. zlib-ng'yi eski bir sürüme düşürmek veya daha yavaş, tahsis konusunda hafif bir CRC-32 uygulamasına geri dönmek de çökmeyi önlerdi, ama eşiğe hiç yaklaşmamış her dosya için gerçek bir maliyetle, bu yüzden hiçbiri gönderilmedi

Bu, zlib-ng'yi kendi işçi iş parçacıklarınızdan çağırıyorsanız ne anlama geliyor?

Burada açıklanan yığın taşması başarısızlık modunun elektronik tablolarla özellikle hiçbir ilgisi yoktur. zlib-ng'ye sıkıştırma, kod çözme veya bir sağlama toplamı için büyük bir arabellek veren ve yalnızca platform varsayılan yığınını taşıyan bir iş parçacığından bunu yapan herhangi bir uygulama aynı türden bir duvara çarpabilir, çünkü kütüphane algoritmasını girdi boyutuna göre seçer ve bu algoritmaların bazıları yedekte yığın olduğunu varsayar. zlib-ng'nin kendisine dokunmadan iki savunma işe yarar: doğal olarak artımlı olan herhangi bir algoritma için büyük arabellekleri sabit yığınlar halinde boyuta duyarlı rutinlere beslemek tetikleme koşulunu tamamen ortadan kaldırır ve yığınlamanın bir seçenek olmadığı yerde, çağıran iş parçacığına platform varsayılanından daha büyük bir yığın vermek diğer koldur. Her ikisi de, yanlış fonksiyonu suçlayan bir üretim çökme raporundan belgelenmemiş bir boyut eşiği hakkında bilgi edinmekten daha ucuzdur

Bu belirli eşik, yeterince büyük bir üretim çalışma kitabı onu yanlış türden bir iş parçacığında aşana kadar görünmez kaldı; bu, kodun küçük test dosyaları yerine gerçek dosyalara karşı çalıştığında ancak ortaya çıkan türden bir başarısızlıktır. Yığınlanmış CRC-32 yolu artık Delphi ve C++Builder için HotXLS Excel Bileşeni'ndeki standart yazma boru hattının bir parçası olarak gönderiliyor; çağıranın yapılandıracağı hiçbir şey ve onu açıp kapatan hiçbir özellik olmadan