HotXLS gali sukelti Delphi darbinės gijos gedimą be jokios pagaunamos išimties, kai vienu kvietimu skaičiuoja didelės darbalapio XML dalies kontrolinę sumą: zlib-ng virš maždaug 119 KB įvesties pereina prie Chorba algoritmo, o bendroji C šio algoritmo versija steke paskiria pagalbinį masyvą, pakankamai didelį, kad perpildytų numatytąjį 1 MB gijos dėklą. Delphi negauna progos sureaguoti, nes dėklo perpildymas nėra tokio tipo išimtis, kuriai gaudyti buvo sukurtas try/except
HotXLS yra gimtoji Delphi ir C++Builder biblioteka Excel darbaknygėms skaityti ir rašyti, o gedimas buvo atsektas iki jos darbalapio rašytuvo. Pirmasis problemos ženklas buvo pagalbos užklausa: naktinis eksportavimo darbas sugesdavo maždaug du kartus per savaitę, visada vykdymo viduryje, be Delphi išimties dialogo ir be įrašytos klaidos, tik dingęs procesas ir Windows Error Reporting įrašas, kuris nenurodė nieko naudingo. Pakartoti tai prie darbo stalo buvo visiškai kitas reikalas. Mažos darbaknygės išsisaugodavo sėkmingai. Didelės darbaknygės taip pat išsisaugodavo sėkmingai, jei įrašymas vykdavo pagrindinėje gijoje su jau prijungtu derintuvu. Tikras gamybos dydžio failų paketas, paleistas per tikrą kelių gijų eksportavimo kelią, galiausiai atkūrė gedimą, o iki to laiko disko įvestis ir išvestis, atminties spaudimas bei įtartinas šablonas jau buvo atmesti
Kaip darbalapio įrašymas virsta vienu milžinišku CRC32 kvietimu
XLSX failai yra ZIP konteineriai, o ZIP formatas reikalauja CRC-32 kontrolinės sumos kiekvienam įrašui, saugomos ir vietinėje failo antraštėje, ir centriniame kataloge. HotXLS šią kontrolinę sumą apskaičiuoja kviesdamas mažą apvalkalą ZLibCRC32, kuris savo ruožtu kviečia paties zlib-ng crc32 procedūrą, kai SaveAs baigia surinkti darbalapio XML atmintyje, ir ilgą laiką šiam kvietimui visas nesuspaustas buferis buvo perduodamas vienu iškvietimu. Mažam darbalapiui tai pagrįstas sprendimas. Tačiau kvietimas tampa labai didelis, kai lapas yra toks, apie kokį rašoma mūsų HotXLS didelių darbaknygių našumo vadove, kur vieno lapo XML prieš suspaudimą įprastai viršija kelis šimtus kilobaitų
Kodėl zlib-ng CRC32 reikia milžiniško buferio dėkle?
zlib-ng ne visiems kvietimams naudoja tą pačią CRC-32 realizaciją. Žemiau dydžio ribos jis pereina buferį naudodamas lentelių paieškas ir lankstymo gudrybes, kurioms nereikia reikšmingos papildomos atminties, o virš šios ribos, maždaug 119 KB, tiksliau 118 960 baitų versijoje, su kuria susiejamas HotXLS, jis pereina prie specializuoto spartaus algoritmo, vadinamo Chorba. Bendroji C šio kelio realizacija keičia atmintį į spartą: pagalbinį masyvą ji paskiria steke, o ne krūvoje, ir jo dydis parenkamas taip, kad algoritmo vidinė kilpa veiktų sparčiai, o ne taip, kad patogiai tilptų į bet kokį kviečiančiosios gijos dėklo limitą. Iš kviečiančiosios pusės nieko iš to nesimato. Kontrolinės sumos funkcija paprastai yra lapinis kvietimas: perskaityti kelis baitus ir grąžinti skaičių, be jokio verto aptarimo paskirstymo, ir ši prielaida galioja didžiajai daugumai kvietimų į zlib-ng, kol į ją patenka pakankamai didelis buferis, peržengiantis Chorba ribą
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;
Kodėl darbinės gijos tai matė, o interaktyvus derinimas – niekada
Šiam gedimui sukelti vienu metu reikia dviejų sąlygų: pakankamai didelės darbalapio XML dalies, kad ji peržengtų zlib-ng Chorba ribą, ir gijos, turinčios tik įprastą numatytąjį dėklą, o ne didesnę atsargą. Gamybiniai eksportavimo darbai atitiko abi. Jie vykdomi kaip serverio paketų darbai, paskirstantys HotXLS įrašymus tarp darbinės gijos telkinio, kur kiekviena gija turi numatytąjį 1 MB dėklą, kurį Windows rezervuoja, nebent kvietėjas paprašytų daugiau, ir kiekviena apdoroja pakankamai dideles klientų darbaknyges. Derinimas prie darbo stalo patikimai neatitiko nė vienos sąlygos: pavyzdiniai failai paprastai būdavo mažesni už ribą, o vykdymas žingsnis po žingsnio dažniausiai vykdavo pagrindinėje gijoje, o ne naujai sukurtoje darbinėje gijoje, todėl abi sąlygos, kurios gamyboje turėjo sutapti, kūrėjo darbo vietoje beveik niekada nesutapdavo
Gedimo, kaltinusio netinkamą funkciją, paieška
Komandai prieinamos gedimų ataskaitos rodė vietą zlib-ng deflate funkcijoje, o ne HotXLS kode ir ne akivaizdžiai CRC-32 kode. Ši viena detalė pirmąjį tyrimo etapą nukreipė į suspaudimo kelią: deflate perduodamų buferių dydžiai, lango bitai, suspaudimo lygis – visi įprasti įtariamieji, kai savasis kodekas sukelia gedimą. Nė vienas jų nepasitvirtino
Klaidinantis viršutinis kadrų įrašas
Dėklo perpildymą simbolizuoti sudėtinga, nes iki pranešimo apie jį dėklo rodyklė jau būna peržengusi jai rezervuotą vietą. Kas sukūrė tą gedimo ataskaitą, greičiausiai susiejo gedimo adresą su artimiausiu simboliu, kurį dar galėjo rasti, o artimiausias eksportuotas įėjimo taškas šalia tikrojo kaltininko buvo deflate. Tikrasis gedimas įvyko Chorba pagalbinio buferio paskyrime CRC-32 kelyje, sukompiliuotame į tą pačią biblioteką, ir dvejetainiame faile buvo pakankamai arti, kad jį būtų galima palaikyti faktiškai vykdyta funkcija
Dvejetainė paieška naudojant laiko žymas, o ne derintuvą
Gedimas, nuverčiantis visą procesą, nepalieka ko pagauti įprastai Delphi derintuvo sesijai, todėl komanda grįžo prie GetTickCount kontrolinių taškų, įterptų aplink kiekvieną įtariamą kvietimą, ir rankinės dvejetainės paieškos įrašymo kelyje, siaurindama, kuri operacija vyko tuo momentu, kai procesas žlugo. Kartu žinoma gera bazinė versija tuos pačius gamybos failus vykdė greta dabartinės versijos, kad būtų atmestas regresinis poveikis iš to paties etapo pakeitimų prieš tiriant tolesnes grandis. Tik kai abu patikrinimai buvo sėkmingi, tyrimas apsistojo ties trečiosios šalies priklausomybe, netikėtai darančia kažką su visiškai tinkama įvestimi
Kodėl try/except nepagauna dėklo perpildymo?
Dėklo perpildymas nėra išimtis, kurią Delphi kodas tyčia sukelia, ir ji taip pat nepateikiama taip, kaip Windows pateikia prieigos pažeidimą ar dalybą iš nulio. Ji pasireiškia aparatūros apsauginio puslapio klaida, apie kurią pranešama per tą patį struktūrinių išimčių apdorojimo mechanizmą, kuriuo grindžiamas Delphi try/except, tačiau tuo metu, kai ji įvyksta, paprastai nebelieka dėklo vietos tvarkyklei paleisti, valymo kodui išvynioti ar net tinkamai užbaigti klaidos pranešimą. Darbinėje gijoje, turinčioje tik numatytąjį 1 MB rezervą, kai tokio dydžio pagalbinis buferis jau sunaudojo didžiąją likusios vietos dalį, vykdymo aplinkai nebelieka su kuo dirbti
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;
Šis except blokas atrodo kaip apsauginis tinklas, ir daugumos gedimų atveju toks ir yra, tačiau čia jis nieko nedaro. Komanda tai patvirtino praktiškai: try/except nieko nepagaudavo, finally blokas taip pat niekada neturėdavo patikimos progos įvykdyti, o operatorius matydavo užgesusį procesą be jokio programos lygmens žurnalo įrašo – tiksliai taip, kaip aprašyta pradinėje pagalbos užklausoje
Pataisa: CRC32 perduoti 64 KB dalimis, o ne vienu milžinišku kvietimu
HotXLS pateikta pataisa nieko nekeičia pačiame zlib-ng ir nekeičia darbaknygės įrašymui naudojamo suspaudimo lygio. ZLibCRC32 dabar pereina įvestį fiksuotomis 64 KB dalimis, po 65536 baitus, vienai daliai kviesdamas zlib-ng crc32 ir perduodamas kaupiamos kontrolinės sumos reikšmę iš vieno kvietimo į kitą. CRC-32 iš esmės yra prieauginis algoritmas, todėl per kelias dalis sukaupta kontrolinė suma bitas į bitą sutampa su apskaičiuotąja vienu kvietimu tiems patiems baitams: pataisa keičia darbo padalijimą, o ne tai, ką jis apskaičiuoja
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;
Kad tai veiktų, nereikėjo keisti nieko aplinkinio SaveAs kvietimo ir nepasikeitė jokie HotXLS rašomi ZIP įrašai: CRC-32 reikšmė, patenkanti į vietinę failo antraštę ir centrinį katalogą, yra lygiai tokia, kokią būtų gavęs vienas milžiniškas kvietimas, tik surinkta iš mažesnių dalių. zlib-ng ankstesnės versijos naudojimas arba lėtesnė, mažai paskirstymų naudojanti CRC-32 realizacija taip pat būtų išvengusi gedimo, tačiau už tikrą kainą kiekvienam failui, kuris nė iš tolo nepriartėjo prie ribos, todėl nė vienas iš šių sprendimų nebuvo išleistas
Ką tai reiškia, jei iš savo darbinių gijų kviečiate zlib-ng
Čia aprašytas dėklo perpildymo gedimo režimas nėra konkrečiai susijęs su skaičiuoklėmis. Bet kuri programa, perduodanti zlib-ng didelį buferį, nesvarbu, ar suspaudimui, išskleidimui, ar kontrolinei sumai, iš gijos, turinčios tik platformos numatytąjį dėklą, gali atsitrenkti į tą pačią ribą, nes biblioteka algoritmą pasirenka pagal įvesties dydį, o kai kurie algoritmai numano, kad dėkle liko pakankamai vietos. Du būdai veikia nekeičiant paties zlib-ng: didelių buferių perdavimas dydžiui jautrioms procedūroms fiksuotomis dalimis visiškai pašalina paleidimo sąlygą bet kuriam savaime prieauginiam algoritmui, o kai skaidymas į dalis neįmanomas, kitas svertas yra kviečiančiajai gijai suteikti didesnį nei platformos numatytasis dėklą. Bet kuris variantas pigesnis, nei iš gamybinės gedimo ataskaitos, kaltinančios netinkamą funkciją, sužinoti apie nedokumentuotą dydžio ribą
Ši konkreti riba liko nepastebėta, kol pakankamai didelė gamybinė darbaknygė peržengė ją netinkamo tipo gijoje – būtent toks gedimas išryškėja tik tada, kai kodas veikia su tikrais failais, o ne mažais testiniais pavyzdžiais. Skaidymo dalimis CRC-32 kelias dabar pateikiamas kaip standartinio įrašymo srauto dalis HotXLS Excel komponente, skirtoje Delphi ir C++Builder, todėl kvietėjui nieko nereikia konfigūruoti ir nėra savybės, kuri jį įjungtų arba išjungtų