|
In questo articolo analizzeremo le API Native inerenti la gestione delle DLL.
Verranno esaminate una ad una tutte le funzioni interessate, verrà analizzata
l'API win32 corrispondente ad ognuna, etc... Bene, come al solito passiamo dalle
parole ai fatti.
1. Gestione degli errori per le API Native
Le API Native sono le funzioni esportate dalla dll ntdll.dll; questa dll è
caricata nello spazio di memoria di tutti i processi, è sempre la prima dll ad
essere caricata e viene caricata sempre allo stesso indirizzo. Prima di passare
all'esame delle singole API è opportuno esaminare quelli che sono i concetti
fondamentali associati alle API Native. Come prima cosa analizziamo la
situazione in cui una API Native restituisce un errore.
1.1 Win32
Le api Win32 sono le funzioni classiche che vengono utilizzate nella
realizzazione di un programma: sono implementate nelle varie dll del sistema
operativo tipo kernel32.dll, user32.dll, advapi.dll, etc... La prassi vuole che,
nel caso una di queste funzioni incontri un problema nella propria esecuzione,
prima di interrompere l'esecuzione viene salvato il codice relativo all'errore
(valore di 4 byte) in una zona di memoria prefissata ed associata al thread
chiamante. Tale valore può poi essere letto tramite l'API win32 GetLastError.
Nella maggior parte dei casi (non sempre però) se una API win32 ha incontrato un
errore nel corso della sua esecuzione, dopo aver salvato il codice di errore,
prima di uscire definitivamente, restituisce 0 come output (ripeto, quasi sempre
ma non sempre, consultare al riguardo il Platform SDK per vedere il
comportamento dell'api che ci interessa di volta in volta). Quindi in pratica
tutte le volte che si chiama una api win32 occorre memorizzarsi l'output e, nel
caso che corrisponda ad un fallimento di esecuzione (0 nella maggiorparte dei
casi, ma non sempre come già detto), si esegue una chiamata all'api win32
GetLastError per ottenere il codice di errore. Come già detto, il codice di
errore è un valore a 32 bit, di seguito la definizione

come si può notare, ogni gruppo di bit ha un significato particolare; senza
entrare troppo nei dettagli, si può vedere come i bit 30 e 31 rappresentino la
tipologia di risultato (successo, informazione, avvertimento o errore). Se il
bit 31 vale 1 allora si tratta di un errore o di un avvertimento; se il codice
di errore viene interpretato come integer (range di definizione [-2147483648..2147483647]),
il bit 31 rappresenta il segno: se impostato ad 1, il valore è negativo (quindi
se il valore interpretato come integer è negativo allora si tratta di un
errore). Il bit
29 è riservato per la definizione di nuovi codici di errore: ad esempio posso
crearmi delle procedure il cui comportamento (a livello di gestione errori di
esecuzione) sia equivalente a quello delle api win32; in caso di errore, prima
di uscire (e far restituire alla mia funzione un valore identificativo del
fallimento avvenuto), vado a salvare il codice di errore corrispondente che ho
definito: il salvataggio del mio codice di errore può essere effettuato con
l'api win32 SetLastError che prende appunto in input il valore a 32 bit
che rappresenta il mio codice di errore. Anche in questo caso, si potrà leggere
il codice di errore grazie alla GetLastError. La SetLastError è
appunto la funzione usata dalle api win32 per salvare il codice di errore. Come
si è già capito, non necessariamente vengono salvati codici relativi ad errore,
ma alcune api win32 possono anche ad esempio chiamare SetLastError con
parametro settato a 0 per indicare successo. Come ultima cosa è utile dire che
le api GetLastError e SetLastError sono esportate dalla
kernel32.dll ma non sono altro che un forward alle funzioni
RtlGetLastError e RtlSetLastError implementate in ntdll.dll.
Per ottenere una stringa descrittiva del codice in questione si può ricorrere
all'api win32 FormatMessage; Delphi fornisce una procedura che fa da
wrapper alla FormatMessage: SysErrorMessage.
Per finire definiamo la seguente procedura che ci consente di loggare gli errori
function ErrStr(nomeFunc: string): Boolean;
var
error_code: Cardinal;
error_description: string;
error_string: string;
begin
result := False;
error_code := GetLastError;
error_description := SysErrorMessage(error_code);
error_string := nomeFunc + ': ' +
'CODErr=' + IntToStr(error_code) +
' Descr=' + error_description;
ScriviLog(error_string);
result := True;
end;
1.2 Native Le api Native sono le funzioni esportate dalla ntdll.dll. A differenza
delle api win32, le api Native non salvano eventuali errori (tramite la
SetLastError), ma restituiscono l'errore stesso come output. Il codice di
errore ha la stessa struttura di quello delle api win32: 32 bit con i
significati precedentemente descritti. Nell'ambito Native si fa spesso uso del
tipo NTSTATUS: non è altro che un alias del tipo Integer; la prassi è
sempre quella di convertire in Integer (NTSTATUS) l'output
della funzione Native: se il valore è negativo (bit 31 settato a 1 come visto al
paragrafo precedente) allora si tratta di un errore. A questo punto ci viene in
aiuto una funzione esportata sempre da ntdll.dll: si tratta dell'api
Nativa RtlNtStatusToDosError che converte il valore restituito da una API
Native nel corrispondente valore nell'ambito win32 (ossia i valori che vengono
salvati dalle api win32 e poi restituiti dalla GetLastError); del resto
tantissime api win32 sono dei wrapper alle corrispondenti api Native e l'esempio
più lampante sono appunto le api per la gestione delle dll che andremo ad
analizzare in questo articolo. Il seguente pseudocodice definisce in linea di massima il funzionamento di un'api
win32 che chiama la corrispondente api Native
var
Status: Integer;
...
function Win32API(
//parametri
): boolean;
begin
...
...
Status := NativeAPI(
//parametri
);
if status < 0 then
begin
SetLastError(RtlNtStatusToDosError(Status));
Result := False;
Exit;
end;
end;
Bene, come ultima cosa anche in questo caso definiamo la seguente procedura che ci consente di loggare gli errori
function IntToHex(dwValue, dwDigits: Cardinal): String; stdcall;
const
hex: array[0..$F] of char = ('0','1','2','3','4','5','6','7',
'8','9','A','B','C','D','E','F');
begin
if (dwDigits > 8) then
dwDigits := 8;
Result := Copy(
hex[(dwValue and $F0000000) shr 28]+
hex[(dwValue and $0F000000) shr 24]+
hex[(dwValue and $00F00000) shr 20]+
hex[(dwValue and $000F0000) shr 16]+
hex[(dwValue and $0000F000) shr 12]+
hex[(dwValue and $00000F00) shr 8]+
hex[(dwValue and $000000F0) shr 4]+
hex[(dwValue and $0000000F) shr 0],9-dwDigits,dwDigits);
end;
function ErrStrNative(nomeFunc: string; status: Integer): Boolean;
var
error_code: Cardinal;
error_description: string;
error_string: string;
begin
result := False;
error_string := nomeFunc + ': ' +
'NTSTATUSCODErr=' + IntToHex(status,8);
error_code := RtlNtStatusToDosError(status);
error_description := SysErrorMessage(error_code);
error_string := error_string +
'; CODErr=' + IntToStr(error_code) +
'; Descr=' + error_description;
ScriviLog(error_string);
result := True;
end;
2. Strutture e funzioni che ricorrono nell'ambito delle api Native Bene. Dopo un breve preambolo inerente gli errori di sistema e la loro gestione in ambito sia Win32 sia Native, è arrivato il momento di analizzare alcune strutture e funzioni che useremo nella gestione delle api che dobbiamo trattare: 2.1 ANSI_STRING e UNICODE_STRING Sono le 2 strutture fondamentali: di seguito le definizioni
type
PANSI_STRING = ^ANSI_STRING;
ANSI_STRING = record
Length: Word;
MaximumLength: Word;
Buffer: PAnsiChar;
end;
PUNICODE_STRING = ^UNICODE_STRING;
UNICODE_STRING = record
Length: Word;
MaximumLength: Word;
Buffer: PWideChar;
end;
Di seguito andiamo ad esporre alcune delle funzioni che coinvolgono queste 2 strutture 2.2 Funzioni di conversione ANSI_STRING --> UNICODE_STRING
function RtlAnsiStringToUnicodeString(
DestinationString : PUNICODE_STRING;
SourceString : PANSI_STRING;
AllocateDestinationString : Boolean
): Integer; stdcall; external 'ntdll.dll';
UNICODE_STRING --> ANSI_STRING
function RtlUnicodeStringToAnsiString(
DestinationString : PANSI_STRING;
SourceString : PUNICODE_STRING;
AllocateDestinationString : Boolean
): Integer; stdcall; external 'ntdll.dll';
Vediamo i singoli parametri: - DestinationString: puntatore al buffer che conterrà il risultato della conversione
- SourceString: puntatore al buffer che contiene il valore da convertire
- AllocateDestinationString: specifica se la funzione deve allocare la memoria puntata da DestinationString
2.3 Deallocazione ANSI_STRING
procedure RtlFreeAnsiString(
AnsiString : PANSI_STRING
); stdcall; external 'ntdll.dll';
UNICODE_STRING
procedure RtlFreeUnicodeString(
UnicodeString : PUNICODE_STRING
); stdcall; external 'ntdll.dll';
2.4 Conversione dell'output di un'api Native nel corrispondente codice Win32
function RtlNtStatusToDosError(const Status : Integer
): Cardinal; stdcall; external 'ntdll.dll';
3. API Native per la gestione delle DLL Bene, dopo i dovuti preamboli, buttiamoci ora a capofitto sulle singole api. Parleremo di api per la gestione di dll quindi la prima cosa da fare è prepararsi una dll di esempio: eccola
library TestDll;
uses
Windows,
SysUtils,
Classes;
procedure Somma(i1: Cardinal; i2: Cardinal; var i3: Cardinal); stdcall;
begin
i3 := i1 + i2;
end;
procedure ScriviSuFile(nomefile: string; val: string); stdcall;
var
error_log_File : TextFile;
FileHandle: integer;
begin
if not FileExists(nomefile) then
begin
FileHandle := FileCreate(nomefile);
FileClose(FileHandle);
end;
AssignFile(error_log_File, nomefile);
Append(error_log_File);
WriteLn(error_log_File, val);
CloseFile(error_log_File);
end;
procedure EntryPointProc(reason: integer);
begin
case reason of
DLL_PROCESS_ATTACH: //1
begin
DisableThreadLibraryCalls(HInstance);
end;
DLL_THREAD_ATTACH: //2
begin
end;
DLL_PROCESS_DETACH: //3
begin
end;
DLL_THREAD_DETACH: //0
begin
end;
end;
end;
exports
ScriviSuFile name 'LogData',
Somma name 'Addizione';
begin
DllProc := @EntryPointProc;
DllProc(DLL_PROCESS_ATTACH);
end.
3.1 LdrLoadDll Carica un modulo (dll) nello spazio di memoria del processo chiamante. API Win32 corrispondenti: LoadLibrary, LoadLibraryEx
function LdrLoadDll(
PathToFile : PWideChar;
Flags : Cardinal;
ModulFileName : PUNICODE_STRING;
ModuleHandle : pCardinal
): integer; stdcall; external 'ntdll.dll';
- PathToFile:[input] opzionale: non lo usiamo (almeno in questa trattazione)
- Flags:[input] opzionale: non lo usiamo (almeno in questa trattazione). N.B. ha lo stesso significato del parametro Flags dell'api win32 LoadLibraryEx (rimando al Platform SDK per approfondimenti)
- ModuleFileName:[input] nome del modulo da caricare con percorso completo
- ModuleHandle:[output] indirizzo di base del modulo caricato, nello spazio di memoria del processo chiamante.
L'API Native LdrLoadDll viene chiamata dalle API Win32 LoadLibrary e LoadLibraryEx di cui di seguito vediamo le dichiarazioni
function LoadLibraryA(lpLibFileName: PAnsiChar
): Cardinal; stdcall;
function LoadLibraryW(lpLibFileName: PWideChar
): Cardinal; stdcall;
function LoadLibraryExA(
lpLibFileName: PAnsiChar;
hFile: Cardinal;
dwFlags: Cardinal
): Cardinal; stdcall;
function LoadLibraryExW(
lpLibFileName: PWideChar;
hFile: Cardinal;
dwFlags: Cardinal
): Cardinal; stdcall;
Come si può notare, entrambe le funzioni hanno una doppia implementazione: A per l'insieme dei caratteri ASCII e W per l'insieme dei caratteri UNICODE. LoadLibrary: - lpLibFileName: nome del modulo da caricare
LoadLibrarEx: - lpLibFileName: nome del modulo da caricare
- hFile: RISERVATO: va settato a 0
- dwFlags: comportamento adottato dalla funzione: rimando al Platform SDK per approfondimenti; la cosa importante da dire è che, se questo parametro è impostato a 0 allora la LoadLibraryEx è equivalente alla LoadLibrary. Inoltre il valore di questo parametro viene passato al parametro dwFlags della LdrLoadDll.
Il comportamento della LdrLoadDll è quindi quello descritto nel Platform SDK per le 2 api win32 LoadLibrary e LoadLibraryEx: tenta di caricare il modulo specificato, nello spazio di memoria del processo chiamante; in particolare, se il modulo è già caricato allora si limita ad incrementare di 1 il Load Count del medesimo, altrimenti carica il modulo e chiama il suo entrypoint. E' da osservare che, se dwFlags è settato a 2 (costante LOAD_LIBRARY_AS_DATAFILE), la dll viene caricata in memoria come un semplice file dati: non viene chiamato il suo entrypoint tantomeno potranno poi essere usate su di essa le normali api win32 tipo GetModuleHandle o GetProcAddress; è utile effettuare un tale tipo di caricamento quando si vuole far riferimento ad una dll solo per estrarre messaggi o risorse da essa. Tra i possibili output della funzione LdrLoadDll c'è il seguente $C0000135: corrisponde al codice di errore win32 126 (basta chiamare RtlNtStatusToDosError($C0000135)): sta ad indicare che non è stato possibile trovare il modulo specificato (stringa descrittiva ottenuta tramite SysErrorMessage: "Impossibile trovare il modulo specificato") 3.2 LdrGetDllHandle Ottiene l'indirizzo di base del modulo specificato, nello spazio di memoria del processo chiamante API win32 corrispondenti: GetModuleHandle
function LdrGetDllHandle(
pwPath : PWORD;
Unused : pointer;
ModuleFileName : PUNICODE_STRING;
pHModule : pCardinal
): Integer; stdcall; external 'ntdll.dll';
- pwPath: [input] opzionale: non lo usiamo
- Unused: [input] opzionale: un nome, una garanzia: col cavolo che lo usiamo
- ModuleFileName: [input] nome del modulo di cui si vuol prelevare l'indirizzo di base nello spazio di memoria del processo chiamante
- pHModule: [output] indirizzo di base del modulo nello spazio di memoria del processo chiamante
L'API Native LdrGetDllHandle viene chiamata dall'API Win32 GetModuleHandle di cui di seguito vediamo la dichiarazione
function GetModuleHandleA(
lpModuleName: PAnsiChar
): Cardinal; stdcall;
function GetModuleHandleW(
lpModuleName: PWideChar
): Cardinal; stdcall;
- lpModuleName: nome del modulo di cui si vuole determinare l'indirizzo di base nello spazio di memoria del processo chiamante (non c'è bisogno di specificare il percorso completo)
Se il modulo richiesto non è presente in memoria, la funzione restituisce il valore $C0000135 (126 in win32) 3.3 LdrUnloadDll Decrementa di 1 il Load Count del modulo specificato (modulo caricato nello spazio di memoria del processo chiamante) e, nel caso in cui tale valore diventi 0, allora provvede a scaricare il modulo medesimo API win32 corrispondenti: FreeLibrary
function LdrUnloadDll(
ModuleHandle: Cardinal
): Integer; stdcall; external 'ntdll.dll';
- ModuleHandle: indirizzo di base, nello spazio di memoria del processo chiamante, del modulo da scaricare
L'API Native LdrUnloadDll viene chiamata dall'API Win32 FreeLibrary di cui di seguito vediamo la dichiarazione
function FreeLibrary(hLibModule: Cardinal): Boolean; stdcall;
- hLibModule: indirizzo di base, nello spazio di memoria del processo chiamante, del modulo da scaricare
Se il modulo richiesto non è presente in memoria, la funzione restituisce il valore $C0000135 (126 in win32) 3.4 LdrGetProcedureAddress Ottiene l'indirizzo (nello spazio di memoria del processo chiamante) di una funzione esportata da una dll caricata nello spazio di memoria del processo chiamante. API Win32 corrispondenti: GetProcAddress
function LdrGetProcedureAddress(
hModule : Cardinal;
psName : PANSI_STRING;
dwOrdinal : Cardinal;
var pProcedure : Pointer
): Integer; stdcall; external 'ntdll.dll';
- hModule:[input] indirizzo di base, nello spazio di memoria del processo chiamante, della dll in cui si trova la funzione
- psName:[input] nome della funzione (inuitle se si usa l'ordinal della funzione (vedi parametro dwOrdinal))
- dwOrdinal:[input] ordinal della funzione (inutile se si usa il nome della funzione (vedi parametro psName))
- pProcedure:[output] indirizzo della funzione
L'API Native LdrGetProcedureAddress viene chiamata dall'API Win32 GetProcAddress di cui di seguito vediamo la dichiarazione
function GetProcAddress(
hModule: Cardinal;
lpProcName: PAnsiChar
): Pointer; stdcall;
- hModule: indirizzo di base, nello spazio di memoria del processo chiamante, della dll in cui si trova la funzione
- lpProcName: valore a 4 byte che può avere 2 significati distinti: 1) se il valore è compreso tra 0 e $FFFF (ossia la High-Order Word è pari a 0) allora il valore rappresenta l'ordinal della funzione; 2) altrimenti rappresenta un puntatore ad una stringa che mi definisce il nome della funzione
L'output della GetProcAddress è appunto l'indirizzo della funzione 4. Funzioni di esempio di utilizzo delle API Native
E' arrivato il momento di crearsi delle funzioni che utilizzano le API appena descritte. Dichiaro una variabile globale di nome status di tipo Integer che riceverà di volta in volta l'output delle API Native: se il valore è negativo allora c'è stato un errore (questo concetto lo si era già visto nel paragrafo 1. quando si è parlato della struttura del codice di errore). 4.1 MyLoadLibrary
function MyLoadLibrary(DLL: PAnsiChar): Boolean;
var
hModule: Cardinal;
AnsString: ANSI_STRING;
UncString: UNICODE_STRING;
begin
Result := False;
AnsString.Buffer := PAnsiChar(DLL);
AnsString.Length := Length(DLL);
AnsString.MaximumLength := AnsString.Length;
status := RtlAnsiStringToUnicodeString(
@UncString,
@AnsString,
True);
if status < 0 then
begin
ErrStrNative('RtlAnsiStringToUnicodeString', status);
Exit;
end;
status := LdrGetDllHandle(nil, nil, @UncString, @hModule);
if (status < 0) and (status <> Integer($C0000135)) then
begin
ErrStrNative('LdrGetDllHandle', status);
Exit;
end;
//la LdrGetDllHandle non ha trovato il modulo già caricato in memoria
if status = Integer($C0000135) then
begin
status := LdrLoadDll(nil, 0, @UncString, @hModule);
if status < 0 then
begin
ErrStrNative('LdrLoadDll', status);
Exit;
end;
end;
RtlFreeUnicodeString(@UncString);
result := True;
end;
4.2 MyFreeLibrary
function MyFreeLibrary(DLL: PAnsiChar): Boolean;
var
hModule: Cardinal;
AnsString: ANSI_STRING;
UncString: UNICODE_STRING;
begin
result := False;
AnsString.Buffer := PAnsiChar(DLL);
AnsString.Length := Length(DLL);
AnsString.MaximumLength := AnsString.Length;
status := RtlAnsiStringToUnicodeString(
@UncString,
@AnsString,
True);
if status < 0 then
begin
ErrStrNative('RtlAnsiStringToUnicodeString', status);
Exit;
end;
status := LdrGetDllHandle(nil, nil, @UncString, @hModule);
if status < 0 then
begin
ErrStrNative('LdrGetDllHandle', status);
Exit;
end;
if hModule <> 0 then
begin
status := LdrUnloadDll(hModule);
if status < 0 then
begin
ErrStrNative('LdrUnloadDll', status);
Exit;
end;
end;
RtlFreeUnicodeString(@UncString);
result := True;
end;
4.3 MyGetProcAddress
function MyGetProcAddress(DLL: PAnsiChar; procName: PAnsiChar; var pFunction: Pointer): Boolean;
var
hModule: Cardinal;
AnsString: ANSI_STRING;
UncString: UNICODE_STRING;
begin
result := False;
AnsString.Buffer := PAnsiChar(DLL);
AnsString.Length := Length(DLL);
AnsString.MaximumLength := AnsString.Length;
status := RtlAnsiStringToUnicodeString(
@UncString,
@AnsString,
True);
if status < 0 then
begin
ErrStrNative('RtlAnsiStringToUnicodeString', status);
Exit;
end;
status := LdrGetDllHandle(nil, nil, @UncString, @hModule);
if status < 0 then
begin
ErrStrNative('LdrGetDllHandle', status);
Exit;
end;
AnsString.Buffer := PAnsiChar(procName);
AnsString.Length := Length(procName);
AnsString.MaximumLength := AnsString.Length;
status := LdrGetProcedureAddress(hModule, @AnsString, 0, pFunction);
if status < 0 then
begin
ErrStrNative('LdrGetProcedureAddress', status);
Exit;
end;
RtlFreeUnicodeString(@UncString);
Result := True;
end;
Uhm.. bene. Di seguito un esempio che utilizza le funzioni appena descritte e anche le classiche api win32 LoadUnloadNative.7z
|