In un precedente articolo abbiamo visto come aggirare un pericoloso bug della libreria IBxpress, creando un package contenente un componente derivato dall’originale TIBDataSet fornito da Embarcadero.
In questo articolo esploreremo una tecnica ancora più raffinata per risolvere il medesimo problema: useremo il cosiddetto hooking per patchare a runtime il componente originale, senza creare package aggiuntivi.
Cominciamo con un po’ di sana teoria. Con il termine hooking si identifica un gruppo di tecniche di manipolazione, volte a modificare o estendere le funzionalità di un software tramite l’intercettazione e il dirottamento di chiamate a funzione. I metodi per ottenere ciò sono almeno un paio: iniezione di istruzioni JMP e manipolazione delle virtual table.
La prima tecnica, come accennato, prevede l’iniezione di un’istruzione JMP all’inizio della funzione originale. L’istruzione JMP dirotta immediatamente l’esecuzione verso la nostra funzione, ed il gioco è fatto. Le cose si complicano se abbiamo bisogno anche della funzione originale. Ciò può accadere, ad esempio, se la nostra funzione è un wrapper e deve quindi chiamare l’originale. In questo caso, non è sufficiente sovrascrivere i primi 5 byte della funzione originale con il nostro JMP; essi vanno preservati, e quindi devono essere spostati da qualche altra parte in memoria prima della sovrascrittura. Il layout di una chiamata prima del dirottamento assomiglia a questo schema:
Dopo il dirottamento, invece, potrebbe assomigliare a questo schema:
Come vedete, i primi 5 byte della funzione originale sono stati spostati; al loro posto viene inserita un’istruzione JMP che rimanda alla nostra funzione modificata. Se la nostra funzione vuole chiamare l’originale, troverà i primi 5 byte nella porzione che è stata spostata; essi vengono fatti seguire da un ulteriore JMP, che fa saltare l’esecuzione al 6° byte della funzione originale. Il ritorno riporterà l’esecuzione dentro la nostra funzione modificata, che potrà restituire il controllo al chiamante originale.
L’altra tecnica prevede la manipolazione della virtual table. La virtual table è una struttura che ha a che fare con i concetti di polimorfismo ed ereditarietà tipici dei linguaggi di programmazione orientata agli oggetti. Essa contiene semplicemente dei puntatori a funzione, per la precisione i puntatori alle funzioni dichiarate virtuali. Quando si dichiara un tipo, il compilatore crea una virtual table per quel tipo, contenente i puntatori alle funzioni virtuali, che possono essere quelle della classe base (se non è stato fatto un override) oppure quelle della classe derivata. Quando si crea un oggetto di un certo tipo, il compilatore inserisce nell’istanza dell’oggetto un puntatore alla virtual table di quel tipo. In questo modo, l’oggetto avrà un comportamento dettato dal suo tipo di appartenenza. Ecco uno schema di come si presentano in memoria le virtual table di tre oggetti (il primo è l’istanza di una classe base, gli altri due sono istanze di classi derivate con un override delle funzioni virtuali):
Ma passiamo dalla teoria alla pratica. Il nostro obbiettivo è patchare la funzione TIBCustomDataSet.InternalSetFieldData, che come abbiamo discusso nel precedente articolo contiene un grossolano errore. Se andiamo a vedere la sua dichiarazione, scopriamo che essa è dichiarata virtuale, ma non è stato fatto alcun override nella classe derivata TIBDataSet:
procedure InternalSetFieldData(Field: TField; Buffer: Pointer); virtual;
Questo significa che le virtual table di TIBCustomDataSet e di TIBDataSet conterranno entrambe un puntatore a questa funzione. Ma allora, è sufficiente che noi modifichiamo quel puntatore nella virtual table della classe TIBDataSet, per far sì che ogni istanza di TIBDataSet chiami la nostra funzione anziché quella originale!
Fortunatamente, il compito ci è anche facilitato dalle splendide funzioni scritte da Andreas Hausladen nel suo straordinario VCL Fix Pack, dal quale partiremo per realizzare la nostra patch.
Scarichiamo quindi il file VCLFixPack.pas, e iniziamo le modifiche.
Cominciamo con il definire una macro per condizionare la compilazione del codice alla versione di Delphi.
{$IF CompilerVersion = 21.0} // Delphi 2010 {$IFDEF VCLFIXPACK_DB_SUPPORT} {$DEFINE IBDataSetClear21Fix} {$ENDIF VCLFIXPACK_DB_SUPPORT} {$IFEND}
Bisognerebbe verificare se il problema abbraccia anche altre versioni di Delphi, e nel caso estendere il campo d’azione della patch. Purtroppo non dispongo di altre versioni se non RAD Studio 2010, quindi la mia patch si ferma a questa versione.
Aggiungiamo una manciata di unit alla clausola uses:
uses ... {$IFDEF VCLFIXPACK_DB_SUPPORT} DB, DBClient, DBGrids, DBCtrls, IBCustomDataSet, IBDatabase, IBExternals, IBHeader, IBSQL, IBIntf, FMTBcd, {$ENDIF VCLFIXPACK_DB_SUPPORT} ...
Ed ora viene il bello. Gli ingredienti della ricetta sono grossomodo gli stessi già usati nel precedente articolo:
- un TIBCustomDataSetHelper, per ottenere gli indirizzi delle funzioni private e protette
- un TIBCustomDataSetHack, per ottenere accesso ai campi privati
In più, aggiungeremo:
- un TFix21IBCustomDataSet contenente la funzione patchata e gli stub assembler per le funzioni WriteRecordCache, CheckEditState e AdjustRecordOnInsert
- una funzione di inizializzazione (InitIBDataSetClear21Fix) e una di finalizzazione (FiniIBDataSetClear21Fix) della patch
Ecco il listato della patch:
{ ---------------------------------------------------------------------------- } { Workaround TIBCustomDataSet.InternalSetFieldData bug } {$IFDEF IBDataSetClear21Fix} type TIBCustomDataSetHelper = class helper for TIBCustomDataSet function GetWriteRecordCacheAddress: Pointer; function GetCheckEditStateAddress: Pointer; function GetAdjustRecordOnInsertAddress: Pointer; function GetInternalSetFieldDataAddress: Pointer; end; {$HINTS OFF} TIBCustomDataSetHack = class(TWideDataSet) private FNeedsRefresh: Boolean; FForcedRefresh: Boolean; FIBLoaded: Boolean; FBase: TIBBase; FBlobCacheOffset: Integer; FBlobStreamList: TList; FBufferChunks: Integer; FBufferCache, FOldBufferCache: TRecordBuffer; FBufferChunkSize, FCacheSize, FOldCacheSize: Integer; FFilterBuffer: TRecordBuffer; FBPos, FOBPos, FBEnd, FOBEnd: DWord; FCachedUpdates: Boolean; FCalcFieldsOffset: Integer; FCurrentRecord: Long; FDeletedRecords: Long; FModelBuffer, FOldBuffer, FTempBuffer: TRecordBuffer; FOpen: Boolean; FInternalPrepared: Boolean; FQDelete, FQInsert, FQRefresh, FQSelect, FQModify: TIBSQL; FRecordBufferSize: Integer; FRecordCount: Integer; FRecordSize: Integer; FUniDirectional: Boolean; FUpdateMode: TUpdateMode; FUpdateObject: TIBDataSetUpdateObject; FParamCheck: Boolean; FUpdatesPending: Boolean; FUpdateRecordTypes: TIBUpdateRecordTypes; FMappedFieldPosition: array of Integer; FDataLink: TIBDataLink; FStreamedActive : Boolean; FLiveMode: TLiveModes; FGeneratorField: TIBGeneratorField; FRowsAffected: Integer; FBeforeDatabaseDisconnect, FAfterDatabaseDisconnect, FDatabaseFree: TNotifyEvent; FOnUpdateError: TIBUpdateErrorEvent; FOnUpdateRecord: TIBUpdateRecordEvent; FBeforeTransactionEnd, FAfterTransactionEnd, FTransactionFree: TNotifyEvent; FGDSLibrary : IGDSLibrary; end; {$HINTS ON} TFix21IBCustomDataSet = class(TIBCustomDataSet) private procedure AdjustRecordOnInsert(Buffer: Pointer); procedure CheckEditState; procedure WriteRecordCache(RecordNumber: Integer; Buffer: TRecordBuffer); private procedure NewInternalSetFieldData(Field: TField; Buffer: Pointer); end; var fnWriteRecordCache: Pointer; fnCheckEditState: Pointer; fnAdjustRecordOnInsert: Pointer; procedure TFix21IBCustomDataSet.AdjustRecordOnInsert(Buffer: Pointer); asm push EAX push EDX mov EAX, Self mov EDX, Buffer call fnAdjustRecordOnInsert pop EDX pop EAX end; procedure TFix21IBCustomDataSet.CheckEditState; asm push EAX mov EAX, Self call fnCheckEditState pop EAX end; procedure TFix21IBCustomDataSet.WriteRecordCache(RecordNumber: Integer; Buffer: TRecordBuffer); asm push EAX push EDX push ECX mov EAX, Self mov EDX, RecordNumber mov ECX, Buffer call fnWriteRecordCache pop ECX pop EDX pop EAX end; procedure TFix21IBCustomDataSet.NewInternalSetFieldData(Field: TField; Buffer: Pointer); var Buff, TmpBuff: TRecordBuffer; hack: TIBCustomDataSetHack; isString: Boolean; begin hack := TIBCustomDataSetHack(Self); Buff := GetActiveBuf; if Field.FieldNo < 0 then begin TmpBuff := Buff + hack.FRecordSize + Field.Offset; Boolean(TmpBuff[0]) := LongBool(Buffer); if Boolean(TmpBuff[0]) then Move(Buffer^, TmpBuff[1], Field.DataSize); WriteRecordCache(PRecordData(Buff)^.rdRecordNumber, Buff); end else begin CheckEditState; with PRecordData(Buff)^ do begin { If inserting, Adjust record position } AdjustRecordOnInsert(Buff); if (hack.FMappedFieldPosition[Field.FieldNo - 1] > 0) and (hack.FMappedFieldPosition[Field.FieldNo - 1] <= rdFieldCount) then begin Field.Validate(Buffer); isString := Field is TIBStringField; if (Buffer = nil) or isString and (PChar(Buffer)[0] = #0) then if not isString or TIBStringField(Field).EmptyAsNull then rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := True else begin rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataLength := 0; rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := False; end else begin Move(Buffer^, Buff[rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataOfs], rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataSize); if (rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataType = SQL_TEXT) or (rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataType = SQL_VARYING) then rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdDataLength := StrLen(PChar(Buffer)) * 2; rdFields[hack.FMappedFieldPosition[Field.FieldNo - 1]].fdIsNull := False; if rdUpdateStatus = usUnmodified then begin if CachedUpdates then begin hack.FUpdatesPending := True; if State = dsInsert then rdCachedUpdateStatus := cusInserted else if State = dsEdit then rdCachedUpdateStatus := cusModified; end; if State = dsInsert then rdUpdateStatus := usInserted else rdUpdateStatus := usModified; end; WriteRecordCache(rdRecordNumber, Buff); SetModified(True); end; end; end; end; if not (State in [dsCalcFields, dsFilter, dsNewValue]) then DataEvent(deFieldChange, Longint(Field)); end; function TIBCustomDataSetHelper.GetAdjustRecordOnInsertAddress: Pointer; begin Result := @TIBCustomDataSet.AdjustRecordOnInsert; end; function TIBCustomDataSetHelper.GetCheckEditStateAddress: Pointer; begin Result := @TIBCustomDataSet.CheckEditState; end; function TIBCustomDataSetHelper.GetWriteRecordCacheAddress: Pointer; begin Result := @TIBCustomDataSet.WriteRecordCache; end; function TIBCustomDataSetHelper.GetInternalSetFieldDataAddress: Pointer; begin Result := @TIBCustomDataSet.InternalSetFieldData; end; procedure InitIBDataSetClear21Fix; begin fnWriteRecordCache := TIBCustomDataSet(nil).GetWriteRecordCacheAddress; fnCheckEditState := TIBCustomDataSet(nil).GetCheckEditStateAddress; fnAdjustRecordOnInsert := TIBCustomDataSet(nil).GetAdjustRecordOnInsertAddress; ReplaceVmtField(TIBCustomDataSet, TIBCustomDataSet(nil).GetInternalSetFieldDataAddress, @TFix21IBCustomDataSet.NewInternalSetFieldData ); ReplaceVmtField(TIBDataSet, TIBCustomDataSet(nil).GetInternalSetFieldDataAddress, @TFix21IBCustomDataSet.NewInternalSetFieldData ); end; procedure FiniIBDataSetClear21Fix; begin ReplaceVmtField(TIBCustomDataSet, @TFix21IBCustomDataSet.NewInternalSetFieldData, TIBCustomDataSet(nil).GetInternalSetFieldDataAddress ); ReplaceVmtField(TIBDataSet, @TFix21IBCustomDataSet.NewInternalSetFieldData, TIBCustomDataSet(nil).GetInternalSetFieldDataAddress ); end; {$ENDIF IBDataSetClear21Fix} { ---------------------------------------------------------------------------- }
Per i dettagli, si rimanda al precedente articolo dove tutto il funzionamento è spiegato per bene. Qui mi soffermo solo sulle aggiunte, ovvero le due funzioni InitIBDataSetClear21Fix e FiniIBDataSetClear21Fix.
Come vedete, viene preso l'indirizzo delle tre funzioni protette WriteRecordCache, CheckEditState e AdjustRecordOnInsert attraverso l'helper. Quindi viene chiamata la funzione ReplaceVmtField, che sostituirà, nella virtual table, l'indirizzo della funzione originale (individuata da TIBCustomDataSet(nil).GetInternalSetFieldDataAddress) con quello della nostra funzione sostitutiva (identificata da @TFix21IBCustomDataSet.NewInternalSetFieldData). La funzione FiniIBDataSetClear21Fix non fa altro che eseguire l'operazione inversa. La sostituzione avviene sia sulla virtual table di TIBCustomDataSet, sia su quella di TIBDataSet, anche se tecnicamente ci serve solo quest'ultima (infatti un TIBCustomDataSet non verrà mai istanziato direttamente, quindi della sua virtual table potremmo fregarcene, ma a me piace essere preciso).
Non ci rimane che chiamare le nostre funzioni nelle sezioni initialization e finalization:
initialization ... {$IFDEF IBDataSetClear21Fix} InitIBDataSetClear21Fix; {$ENDIF IBDataSetClear21Fix}
finalization ... {$IFDEF IBDataSetClear21Fix} FiniIBDataSetClear21Fix; {$ENDIF IBDataSetClear21Fix}
Voilà! Possiamo riaprire il nostro vecchio progetto di test, ma questa volta dobbiamo tornare indietro: dobbiamo rimettere al suo posto un TIBDataSet laddove avevamo messo un TWrapIBDataSet (ricordate?).
Nel file dfm:
... object IBDataSet1: TIBDataSet ...
Nel file pas:
... type TForm2 = class(TForm) IBDataSet1: TIBDataSet; ...
E infine, dobbiamo includere il VCLFixPack nel progetto. Mi raccomando: deve essere in cima all'elenco degli uses, secondo solo a FastMM, eventualmente:
uses FastMM4, VCLFixPack, Forms, ...
Vogliamo provare se funziona? Vai con F9!
Due a zero per noi! Come avrete intuito, queste tecniche possono aprire la strada a tantissime possibilità di intervento. Laddove individuiamo un bug, non saremo più costretti ad attendere che Embarcadero ponga rimedio, ma potremo arrangiarci. Certo, tutto questo se paragonato con la libertà offerta dall'Open Source, è un po' grottesco...
Buon hacking a tutti.