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.