Home | Chi sono | Contattami
 

Progr. lineare

Delphi
 
Componenti
  Database
 
Miei articoli

Windows

Miei articoli 

 

Native API per la gestione di DLL


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

 

 
 
Your Ad Here