Progetto 3DCloudViz
NICE -DIEE
WP 1
Studio di tecnologie per l’esecuzione
accelerata di applicazioni DirectX in ambiente
virtuale
Deliverable D1
 R.3.1 Definizione dei requisiti tecnici e funzionali
e progettazione di un protocollo di
visualizzazione remota di applicazioni 3D basate
su DirectX in esecuzione su macchine virtuali
Progetto 3DCloudViz
Deliverable D3
Indice
1 R.1.1 Definizione dei requisiti tecnici e funzionali e progettazione di un protocollo di
visualizzazione remota di applicazioni 3D basate su DirectX in esecuzione su macchine virtuali . 3
1.1
Introduzione ................................................................................................................................. 3
1.2
Analisi di strategie per la Library Interposition per l’API DirectX ............................................ 3
1.3
Analisi di varie strategie per la Function interception per l’API DirectX .................................. 8
1.4
Limitazioni dell’approccio tramite Library Injection ................................................................ 17
1.5
Analisi di fattibilità del supporto per External Rendering......................................................... 18
1.6
Avanzamenti tecnologici della piattaforma di sviluppo ............................................................ 20
1.7
Analisi e implementazione di Screen Grabbing tramite NVDIA GRID ................................... 23
1.8
Conclusioni ed obiettivi raggiunti ............................................................................................. 24
2
Progetto 3DCloudViz
Deliverable D3
1 R.1.1 Definizione dei requisiti tecnici e funzionali e
progettazione di un protocollo di visualizzazione remota di
applicazioni 3D basate su DirectX in esecuzione su
macchine virtuali
1.1
Introduzione
NICE ha tra i propri prodotti un software client-server denominato DCV (Desktop Cloud Visualization), dedicato
alla remotizzazione del desktop. Consente all’utente di accedere via rete ad una workstation remota (fisica o
virtuale) capace di grafica ad alte prestazioni da una varietà di device, tra cui PC, tablets e thin clients.
Ad oggi il software è focalizzato all’accesso remoto a applicazioni grafic-intensive basate su OpenGL, in
particolare applicazioni tecnico-scientifiche, quali visualizzatori di modelli, applicazioni CAE e applicazioni CAD.
L’obiettivo di questo WP consiste nell’esplorare i possibili approcci per estendere NICE DCV per il supporto alla
remotizzazione di applicazioni basate su DirectX sulla piattaforma Microsoft Windows.
Requisiti Funzionali
In sintesi le attività di RI e SS punteranno a.
 consentire l’accesso remoto ad applicazioni DirectX su sistemi Microsoft Windows (Windows 7 sarà la
versione di riferimento del sistema operativo)
 compatibilità con i client NICE DCV esistenti
 impatto prestazionale limitato per applicazioni tecnico scientifiche (CAE, CAD, ...): le prestazioni devono
essere comparabili a quelle della remotizzazione di applicazioni OpenGL
 supporto per macchine fisiche
 supporto per Virtual Machines, possibilmente in modo indipendente dal tipo di hypervisor
 supporto per differenti versioni DirectX (9, 10, 11)
1.2
Analisi di strategie per la Library Interposition per l’API DirectX
La nostra indagine di ricerca per un wrapper DirectX ci ha inizialmente spinti a voler riadattare le soluzioni già
adottare per il wrapper di OpenGL già adottato e collaudato all’interno del pacchetto DCV. Tuttavia il tentativo
per il riadattamento delle strategie già usate per OpenGL ha incontrato nelle DirectX delle difficoltà tecniche
così ampie che si è deciso, nel tempo, di optare per soluzioni alternative, fino a trovare quella funzionate per il
nostro caso d’uso. Il lavoro di analisi è nato dalla rivisitazione del wrapper di OpenGL già implementato in DCV,
passando attraverso lo studio del WDDM (Windows Display Driver Model) per una comprensione delle DirectX,
con tutte le difficoltà per il wrapping di una libreria grafica (DirectX) che fonda le sue radici più profonde nel
driver usato per la gestione dell’aspetto grafico dell’intero sistema operativo, per cui non dirottabile con le
classiche tecniche di library interposition.
Il wrapper OpenGL di DCV
Le OpenGL sono una libreria grafica strutturata secondo una logica client-server. L’intera interfaccia di funzioni
opengl implementate dall’installable client driver è racchiusa all’interno del modulo opengl32.dll, per cui è noto
che le applicazioni che utilizzano opengl fanno riferimento ai metodi contenuti all’interno di questa libreria e non
ai moduli del driver grafico che ne implementano le funzionalità. Per wrappare le opengl è necessario trovare un
sistema per dirottare le chiamate della opengl32.dll a quelle di una libreria proprietaria di interposizione, detta
anche proxy-dll.
L’approccio più semplice è quello di creare una proxy-dll che abbia gli stessi identici entry points pubblici della
libreria che si intende wrappare e farla caricare all’applicazione. Se il nostro scopo è reimplementare il client di
opengl per serializzare le chiamate e inviarle ad un rendering server esterno, potremmo creare ad esempio una
3
Progetto 3DCloudViz
Deliverable D3
opengl32.dll da sostituire all’originale all’interno della directory di sistema windows/system32/. Tuttavia questo
approccio è errato per diversi motivi:
● La opengl32.dll viene installata da tutti i driver grafici dei fornitori di tutto il mondo (i.e. NVIDIA, Amd,
Intel), per cui la sua sostituzione coatta rappresenta una corruzione del sistema e dell’installable client
driver.
● Essendo installata dai driver grafici, non c’è garanzia che la nostra libreria non venga rimpiazzata di
frequente all’update dei driver grafici, qualora fosse necessario effettuarli.
● E’ difficile che le OpenGL dei driver vengano rimpiazzate al 100% dal wrapper. Anche nel caso in cui si
tratta di un totale wrapping con rendering esterno, è pur sempre necessario effettuare delle chiamate al
client driver per l’enumerazione dei pfd (pixel format descriptor) o per un eventuale fallback a estensioni
native.
Un rimedio semplice a questi tre problemi si realizza lasciando intatta la opengl32.dll nella directory di sistema,
e copiando la proxy-dll all’interno della directory dove viene eseguita l’applicazione. Il funzionamento di questo
espediente risiede nel modo in cui il sistema operativo risolve la ricerca del path di una libreria nel momento in
cui un’applicazione ha bisogno di aprirla: 1) perchè la possiede nella IAT (Import Address Table); 2) perchè è
stata caricata a runtime tramite LoadLibrary. In entrambi i casi e qualora non fosse specificato il path della dll, la
libreria viene ricercata dal sistema operativo nel seguente ordine:
1. La directory dove viene eseguita l’applicazione.
2. La directory di sistema. Con GetSystemDirectory è possibile ottenere questo path
3. La directory di sistema a 16-bit. Non esiste alcuna funzione per ottenere il path di questa directory, ma è
ugualmente cercata.
4. La directory di windows. Con GetWindowsDirectory è possibile ottenere questo path
5. La directory corrente.
6. Le directory che sono elencate nella variabile d’ambiente PATH.
Come è possibile vedere, la copia della proxy-dll all’interno della directory dove viene eseguita l’applicazione
permette il caricamento della libreria a primo colpo, salvo delle eccezioni che non possiamo ignorare. Se
un’applicazione dovesse caricare le opengl32.dll specificando il path assoluto con una
LoadLibrary(“c:\windows\system32\opengl32.dll”), in questo caso la nostra proxy-dll non verrebbe presa in
considerazione. Si potrebbe installare un redirect creando un file .local così che il sistema verrebbe forzato a
scegliere la libreria all’interno del path dell’applicazione, tuttavia questo espediente potrebbe introdurre delle
alterazioni. Inoltre, esiste una lista di librerie conosciute che vengono caricate dal sistema operativo all’avvio e
che vengono riutilizzate qualora la dll da caricare fosse già inclusa nella chiave di registro:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
C’è dunque una concreta possibilità che la opengl32.dll si trovi in questa lista (solitamente non è mai così, ma
qualche admin o un’installazione potrebbe modificare i registri) e che la nostra proxy-dll non venga caricata
anche se si trova dentro la directory dell’applicazione. Ma anche se non fosse così, ci sono degli svantaggi
concreti nel copiare la proxy-dll all’interno della cartella dell’applicazione:
●
●
●
●
Alcune applicazioni potrebbero possedere una loro versione di opengl32.dll già presente nella cartella
dell’eseguibile. La nostra proxy-dll rischierebbe di alterare il comportamento dell’applicazione.
La maggior parte dei tool per il debugging di applicazioni opengl copiano una opengl32.dll proprietaria
all’interno della cartella dell’eseguibile per permettere il tracciamento delle funzioni. Se copiassimo la
nostra, non potremmo usare queste strategie di debugging.
Copiare una proxy-dll all’interno delle directory di tutte le applicazioni del sistema non è una scelta
valida se si intende fare wrapping a system-wide, come nel nostro caso.
4
Progetto 3DCloudViz
●
Deliverable D3
La disinstallazione del wrapper richiederebbe la cancellazione di tutte le proxy-dll installate nel sistema.
Un evento proibitivo che richiederebbe un lavorio eccessivo coi files, la chiusura di tutti i processi ed il
riavvio del sistema operativo.
Come abbiamo potuto vedere, nonostante gli strumenti del sistema operativo per l’interposizione e la
redirezione di librerie siano moltepici, non ce n’è uno solo che può essere utile nel nostro caso d’uso, neppure
per il wrapping di OpenGL.
L’unico modo per poter far caricare la nostra proxy-dll a system-wide, senza sovrascrivere la libreria originale o
copiare la proxy-dll nelle directory di tutti i processi del sistema ci richiede di andare oltre le funzionalità messe a
disposizione per l’utente di windows e scendere più in basso nel kernel, scrivendo un apposito filter driver che
gestisca la redirezione nel caso in cui il sistema operativo stia cercando di caricare una opengl32.dll nella
directory di sistema. Abbiamo deciso di chiamare dcvogl.dll l’intera proxy-dll che effettuerà il wrapping totale
delle OpenGL. Senza scendere nei dettagli implementativi, la redirezione implementata dal filter driver funziona
nel seguente modo:
●
●
Quando viene cercata la opengl32.dll nel path di sistema c:\windows\system32, il filter driver fa caricare
al sistema operativo la dcvogl.dll
Quando viene cercata la libreria dcvogl32.dll, viene caricata la opengl32.dll presente nel path di
sistema. Questo ci consente di caricare la libreria opengl installata dal driver grafici dei fornitori.
In questo modo, è possibile attivare e disattivare il filter driver senza procurare alcuna alterazione del sistema
operativo o dover copiare/cancellare proxy-dll in tutte le cartelle. L’approccio si rivela ottimo per un wrapping di
tipo system-wide per le OpenGL ed è utilizzato nella versione attuale di dcv per le macchine virtuali Windows,
con rendering server esterno.
DirectX e filter driver
Come già accennato in precedenza, il metodo di library interposition con filter driver non funziona con le
DirectX. Questo perché le DirectX sono ancorate al WDDM (Windows Display Driver Model) che bypassa il filter
driver, in quanto il contratto privato con dxgkrnl.sys non passa attraverso lo stack di Input/Output. Se prendiamo
in esame le DirectX9, come per OpenGL, l’intera libreria grafica è racchiusa in un’unico modulo d3d9.dll.
Tuttavia, in questo caso, se applicassimo le redirezioni del filter driver per caricare una eventuale dcvd3d9.dll
come proxy-dll, finiremmo sempre per caricare la d3d9.dll usata dal sistema operativo. Questo ci mette in guai
seri, in quanto ci pone di fronte come unica alternativa di copiare la proxy dll all’interno delle cartelle delle
processi, con tutti i lati negativi e le impossibilità implementative che abbiamo già visto. E non finisce qui. Le
versioni di DirectX successive alla 9 passano attraverso una libreria intermedia per la gestione dei modi grafici
che si chiama dxgi.dll e che non dipende più dalla versione delle DirectX installate nel sistema, ma che è
ancorata al driver grafico e più cambiare i punti di ingresso pubblici e la sua implementazione interna in
funzione dei driver grafici installati dai vari fornitori. Questo rende la tecnica di library interposition con proxy-dll
totalmente inapplicabile per DirectX e ci ha costretti a procastinare lo sviluppo di un wrapper serio per DirectX,
facendoci volgere lo sguardo verso strade alternative totalmente diverse, dalla programmazione di driver video
proprietari o tecniche di api hooking. La riscrittura di driver grafici è una scelta troppo onerosa dal punto di vista
delle risorse umane e dei costi aggiuntivi di cui si avrebbe bisogno per supportare l’ampia gamma di piattaforme
e versioni del sistema operativo presenti sul mercato, per cui si è deciso di fare uso delle tecniche di api
hooking.
Le tecniche di api hooking, in generale, consistono nell’iniezione forzata di una libreria all’interno di un processo
mentre è in esecuzione, per alterare parti di codice a runtime e modificarne il comportamento. Questo ci
permette di dirottare le funzioni di DirectX verso funzioni proprietarie senza far ricorso a tecniche di library
interposition.
5
Progetto 3DCloudViz
Deliverable D3
Hook server process
Di certo una tecnica molto popolare per l’iniezione di dll all’interno di un processo risiede nell’api di hooking
fornita da Windows. Come descritto nella guida MSDN, un hook è un “trucco” nel meccanismo per la gestione
dei messaggi di sistema. Un’applicazione può installare una funzione filtro proprietaria per monitorare il traffico
dei messaggi del sistema e processare certi tipi di messaggi prima che raggiungano la procedura della finestra
di interesse.
Un hook è normalmente implementato all’interno di una dll al fine di incontrare i requisiti di base per un campo
d’azione a system-wide. Il concetto che sta alla base di questi hook è che la procedura di callback dell’hook
viene eseguita nello spazio di indirizzamento di ciascun processo che viene agganciato all’interno del sistema.
Per installare un hook è necessario chiamare la funzione SetWindowsHookEx() che, con i giusti parametri,
riesce a installare un hook per il tracciamento dei messaggi per la gestione degli eventi del sistema. Una volta
che il processo installa un system-wide hook, il sistema operativo mappa la dll all’interno dello spazio di
indirizzamento di ciascuno dei suoi processi client. Dunque le variabili globali all’interno della dll saranno “perprocesso” e non protranno essere condivise tra i processi che hanno caricato l’hook della dll. Tutte le variabili
che contengono dati condivisi devono essere rimpiazzate nella sezione dei dati condivisi. Il diagrama di seguito
mostra un esempio di come un hook venga registrato dall’hook server e iniettato all’interno dello spazio di
indirizzamento chiamato “Application one” e “Application two”:
Un system-wide hook è registrato soltanto una volta quando viene eseguita una chiamata a
SetWindowsHookEx(). Se non si verifica alcun errore, viene ritornato un handle all’hook. Il valore di ritorno è
richiesto alla fine dell’hook proprietario quando è necessario effettuare una chiamata a CallNextHookEx() per
garantire il funzionamento del sistema. Se la chiamata a SetWindowsHookEx() ha avuto buon esito, allora il
sistema operativo inietta la dll automaticamente all’interno di tutti i processi che incontrano i requisiti per questo
particolare tipo di filtro, per come descritto al momento della chiamata. Ad esempio, un hook installato per il
messaggio WH_GETMESSAGE avrebbe il seguente codice:
LRESULT CALLBACK GetMsgProc(int code,
// hook code
WPARAM wParam, // removal option
LPARAM lParam) // message
{
// We must pass the all messages on to CallNextHookEx.
return ::CallNextHookEx(sg_hGetMsgHook, code, wParam, lParam);
}
6
Progetto 3DCloudViz
Deliverable D3
Un system-wide hook è caricato da più processi alla volta che non condividono lo stesso spazio di
indirizzamento. Ad sempio, l’handle all’hook sg_hGetMsgHook, che è ottenuto da SetWindowsHookEx() ed è
usato come parametro nella CallNextHookEx() dev’essere usato virtualmente in tutti gli spazi di indirizzamento.
Questo significa che il suo valore dev’essere condiviso tra i processi agganciati e l’applicazione che fa da server
di hook. Al fine di rendere questa variabile visibile a tutti i processi dobbiamo memorizzarla nella sezione dei
dati condivisi.
Una volta che la dll di hook è caricata dentro lo spazio di indirizzamento del processo di interesse, non c’è alcun
modo per scaricarla fino a quando non viene chiamata una UnhookWindowsHookEx() dal server di hook o
l’applicazione del server di hook viene terminata. Quando un server di hook chiama UnhookWindowsHookEx() il
sistema operativo itera attraverso una lista interna con tutti i processi che sono stati forzati a caricare la dll di
hook. Il sistema operativo decrementa il lock count della dll e quando raggiunge lo zero, la dll viene
automaticamente rimossa dello spazio di indirizzamento del processo.
Alcuni vantaggi nell’usare questo approccio:
●
●
●
Questo meccanismo è supportato dalla famiglia NT/2K e 9x Windows e si spera che sarà mantenuto
nelle future versioni di Windows.
Questo metodo fornisce un sistema per sganciare le dll quando il server di hook decide che non sono
più richieste, con una chiamata a UnhookWindowsHookEx()
Tuttavia, anche se possiamo considerare questo sistema di hook estremamente utile, ci sono degli svantaggi
che ci hanno costretti a non utilizzarlo:
●
I windows hook possono degradare in modo significativo le performance dell’intero sistema, visto che
incrementano il numero di processamenti che il sistema deve performare per ciascun messaggio.
●
La SetWindowsHookEx ha effetto se il processo chiamante giace nello stesso desktop dei processi
dentro i quali si vogliono installare gli hook. Questo significa che è necessario chiudere e riaprire il
processo ogni volta che si effettua un cambiamento di desktop su diverse sessioni utente, o lanciare un
processo in ascolto per ogni sessione utente. Tutto questo non fa che peggiorare le performance.
●
La SetWindowsHookEx installa hook solo per monitorare gli eventi legati ai messaggi all’interno dello
stesso desktop. Esiste una concreta possibilità che un’applicazione carichi e usi DirectX prima di aver
lanciato un solo messaggio, per cui questa tecnica si rivela nel complesso instabile per una interception
completa e funzionale delle DirectX su tutti i processi.
L’ultimo punto è lo svantaggio più enorme perché non ci permette di avere la totale garanzia che il nostro hook
potrà funzionare sui processi che usano DirectX prima ancora di aver aperto una finestra e generato un solo
messaggio del desktop, fatto che è ampiamente probabile visto che non è affatto necessario che vi sia una
finestra per creare un oggetto directx qualsiasi, ma solo il device per il rendering. Per questo motivo, abbiamo
deciso di abbandonare definitivamente questo approccio per tentare altre strade.
Registro AppInit_DLLs
E’ possibile iniettare i processi che linkano la user32.dll aggiungendo il path della libreria all’interno della chiave
di registro:
HKEY_LOCAL_MACHINE \ Software \ Microsoft \ Windows NT \ CurrentVersion \ Windows \ AppInit_DLLs
7
Progetto 3DCloudViz
Deliverable D3
Il valore di questa chiave può contenere il path di una singola dll da iniettare o più dll separate da spazi o da
virgole. Questo metodo non presenta alcun problema e sembra iniettare tutti i processi, a patto che linkino la
user32.dll, ma non esiste una sola applicazione DirectX che non lo faccia immediatamente all’avvio. L’unico
problema protrebbe essere l’iniezione di alcuni processi di vitale importanza per il sistema che potrebbero
degradare le performance o che non vi è alcun interesse nel tracciarli, per questo si può facilmente evitare di
installare un hook aggiungendo i processi vietati in una black list. Ma è un problema ben piccolo se
consideriamo che l’hook funziona sempre a prescindere dai messaggi di sistema, diversamente dalla
SetWindowsHookEx, e non esistendo alcuna alternativa valida per il nostro caso d’uso, abbiamo deciso di
utilizzare questo approccio in via definitiva.
Una volta iniettata la dll, è necessario intercettare le funzioni da wrappare e installare un hook che esegua delle
parti di codice alternative al posto della funzione di interesse. Abbiamo effettuato un’analisi approfondita delle
maggiori tecniche per poter raggiungere lo scopo, di cui di seguito ne riportiamo i fatti salienti con le scelte che
sono state effettuate per il nostro caso d’uso.
Analisi di varie strategie per la Function interception per l’API DirectX
1.3
Le tecniche di function interception devono alterare in qualche modo le istruzioni e le tabelle dell’applicazione
per fare in modo che la chiamata della funzione di interesse venga dirottata in funzione alternativa che è stata
iniettata all’interno del processo e che possiamo programmare a nostro piacimento per ottenere il
comportamento desiderato. L’alterazione generica dell’applicazione per il dirottamento di un metodo è nota
anche col termine di hook, che si può installare in due modi:
-
Modificando la IAT. Ogni applicazione o dll ha un header noto col termine di Portable Executable (PE)
dentro il quale viene memorizzata una Import Address Table (IAT) con gli indirizzi di tutte le funzioni
importate dai moduli utilizzati dall’applicazione (i.e. d3d9.dll). L’installazione dell’hook consiste nel
trovare la posizione nella tabella dov’è contenuto l’indirizzo della funzione di interesse e sostituirlo con
l’indirizzo della funzione alternativa. Questa tecnica ovviamente vale solo per i metodi importati da
moduli esterni e non per le funzioni contenute all’interno dell’applicazione.
-
Modificando le istruzioni assembly. E’ necessario sovrascrivere i primi 6 byte della funzione di interesse
con il seguente codice assembly:
- JMP addressDistance
- RET
Questa alterazione delle istruzioni farà in modo che alla chiamata della funzione di interesse avvenga un salto
diretto alla funzione alternativa. All’interno della funzione alternativa, se ci interesserà eseguire nuovamente la
funzione di interesse, dovremo fare in modo di ripristinare i vecchi 6 bytes di codice altrimenti l’alterazione si
tradurrebbe nell’esecuzione di un loop infinito, causando il crash dell’intero processo.
La manutenzione di un sistema custom per la function interception nasconde molte insidie, tra cui
l’attivazione/disattivazione degli hook per evitare l’insorgere di loop infiniti, il supporto dei processi a 64 bit ed il
multithreading, problemi che se non gestiti nel migliore dei modi possono tradursi in crash fatali e
compromettere l’intera stabilità del sistema se l’installazione degli hook viene effettuata in tutti i processi del
sistema operativo. Abbiamo preferito dunque rivolgerci a librerie già esistenti per l’api hooking, che siano
robuste, multithreading, license free e supportino tutti i tipi di processi, a 32 e 64 bit.
Easyhook vs. Detours
Per un hook robusto dei metodi abbiamo preferito rivolgerci a una libreria di Api Hooking già esistente e non
implementare in assembly gli hook delle funzioni, andando da caso a caso, per evitare sul nascere problemi di
instabilità e di compatibilità con i diversi tipi di architettura. Tuttavia non è stata una scelta semplice perchè, oltre
ad essere un problema poco affrontato in letteratura, una libreria in grado di supportare un wrapping importante
8
Progetto 3DCloudViz
Deliverable D3
come quello delle directx per tutte le applicazioni grafiche del sistema, oltre ad essere potente e stabile, deve
soddisfare
i
seguenti
requisiti:
●
●
●
Permettere di iniettare il codice in processi a 32 e 64 bit
Installare e rimuovere tutti gli hook dal processo, in modo stabile e su più threads
Godere di una licenza gratuita
La Microsoft mette a disposizione una libreria di api hooking chiamata Detours che è piuttosto decente in termini
di stabilità. Tuttavia questa libreria soffre di serie limitazioni in termini di licenza. Detours Express gode di una
licenza gratuita per progetti di ricerca non commerciali, tuttavia è limitata alla versione a 32 bit per processori
x86. Se si vogliono supportare anche i processi a 64 bit (come nel nostro caso) è necessario rivolgersi a
Detours Professional che include una licenza per scopi commerciali e il supporto per tutti i processori 32 e 64 bit
windows compatibili presenti sul mercato alla “modica” cifra di 10000 dollari. Peggio ancora, l’ultimo
aggiornamento della libreria Detours risale al lontano 2006, per cui alla luce dello sviluppo tecnologico avvenuto
nel frattempo è difficile capire quanto riesca ad essere affidabile con gli ultimi processori a 64 bit usciti sul
mercato. Per questo motivo, abbiamo deciso di abbandonare Detours per rivolgerci a una libreria molto simile
chiamata Easyhook, che oltre ad essere rilasciata sotto licenza LGPL sembra godere di un’ottima stabilità per
entrambi i processi a 32 e 64 bit su tutti i processori che abbiamo avuto modo di testare, può essere usata per
progetti commerciali ed è ancora mantenuta in vita dai suoi sviluppatori.
Detto questo possiamo dare un’occhiata ai metodi che servono per installare l’hook delle funzioni, di cui due
sono i più importanti:
●
LhInstallHook: Installa un hook per l’entry point di interesse, reindirizzando tutte le chiamate al metodo
dell’hook che si sta per installare. Come valore di ritorno di ha un handle che verrà rilasciato all’unload
della
libreria
o
esplicitamente
con
una
chiamataa
LhUninstallHook().
EASYHOOK_NT_EXPORT
LhInstallHook(void
void
void
TRACED_HOOK_HANDLE
●
*InEntryPoint,
*InHookProc,
*InCallback,
OutHandle);
LhSetExclusiveACL: Rende attivo un hook ACL locale esclusivo sulla base di una lista di thread IDs
fornita in ingresso. Viene sempre effettuata un’intersezione tra gli insiemi di ACL locali e globali. Se ad
esempio si passa una lista vuota, nessun thread sarà escluso dall’hooking della funzione. Questo
metodo è utile per avere un controllo degli hook su più thread in parallelo.
EASYHOOK_NT_EXPORT
LhSetExclusiveACL(ULONG*
ULONG
TRACED_HOOK_HANDLE InHandle);
InThreadIdList,
InThreadCount,
Component Object Model
Tutte le api di DirectX sono basate su un set astratto di interfacce che appartiene al Component Object Model,
per cui la costruzione del wrapper dovrà catturare e dirottare le chiamate contenute all’interno di questi tipi di
oggetti. Tutto questo, ovviamente, rende la costruzione di un wrapper DirectX più complessa e laboriosa
rispetto a OpenGL ed una conoscenza approfondita del Component Object Model.
Il concetto di fondo è che l’intero software è costruito tramite un’impalcatura modulare di componenti COMcompatibili. I differenti tipi di componenti sono identificati da diversi class IDs (CLSIDs) in modo univoco e
globale, con il termine più ricorrente di Globally Unique Identifiers (GUIDs). I GUID sono memorizzati come
valori a 128 bit e possono essere rappresentati con 32 cifre esadecimali in gruppi separati da trattini (i.e.
21EC2020 - 3AEA - 1069 - A2DD - 08002B30309D). Ciascun componente espone le sue funzionalità attraverso
9
Progetto 3DCloudViz
Deliverable D3
una o più interfacce. Le diverse interfacce supportate da un componente sono distinte l’una dall’altra usando
degli ID di Interfaccia (IIDs), che sono anche dei GUIDs.
Interfacce
Tutti i componenti COM implementano un’interfaccia IUnknown che espone i metodi QueryInterface, AddRef e
Release:
●
QueryInterface consente al chiamante di ottenere i reference a diversi tipi di interfaccia implementate
dal componente. E’ simile al dynamic_cast<> del C++ oppure i cast usati in Java o C#. Nello specifico,
è usato per ottenere un puntatore a un’altra interfaccia dato un GUID che identifica univocamente quel
tipo di interfaccia. Se il componente non implementa quel tipo di interfaccia, allora verrà restituito in
uscita un errore di tipo E_NOINTERFACE
●
AddRef è usato per incrementare il reference count quando un nuovo client sta acquisendo l’oggetto. In
uscita viene restituito il valore del nuovo reference count.
●
Release è usato per decrementare il reference count quando il client ha finito di usare l’oggetto. In
uscita viene restituito il valore del nuovo reference count. Il componente verrà cancellato quando il
reference count avrà raggiunto lo zero.
interface IUnknown {
virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
virtual ULONG AddRef () = 0;
virtual ULONG Release () = 0;
};
L’ID dell’interfaccia IUnknown interface ID è definita come un GUID col valore di {00000000 - 0000 - 0000 C000 - 000000000046}.
Le interfacce del componente sono richieste per esibire proprietà riflessiva, transitiva e simmetrica. La proprietà
riflessiva si riferisce all’abilità di un’interfaccia di restituire un’istanza alla stessa interfaccia tramite una chiamata
QueryInterface. La proprietà simmetrica richiede che quando un’interfaccia B è ricavata da un’interfaccia A
attraverso la QueryInterface, allo stesso modo è possibile ricavare un’interfaccia A da un’interfaccia B. La
proprietà transitiva richiede che se è possibile ottenere un’interfaccia B da un interfaccia A e un’interfaccia C da
un’interfaccia B, allora è possibile ottenere un’interfaccia C da un’interfaccia A.
Classi
Una classe COM (coclass) è l’implementazione concreta di una o più interfacce e rispecchia le classi di un
qualsiasi lunguaggio di programmazione orientato agli oggetti. Le classi sono create utilizzando i rispettivi class
ID oppure le rispettive stringhe di identificatori programmatici (progid). Come molti altri linguaggi orientati agli
oggetti, COM fornisce un criterio di separazione tra interfacce e implementazione. Questa distinzione è
specialmente forte nel COM, dove non è possibile accedere direttamente agli oggetti ma solo attraverso le loro
interfacce. COM ha anche il supporto per implementazioni multiple della stessa interfaccia, così che i clienti
possono scegliere a runtime l’implementazione da istanzare.
Reference counting
10
Progetto 3DCloudViz
Deliverable D3
Tutti gli oggetti COM utilizzano un reference counting per gestire la vita degli oggetti. I reference count sono
controllati dai client attraverso l’uso dei metodi AddRef e Release implementati all’interno dell’interfaccia
IUnknown dalla quale derivano tutti gli altri oggetti. Gli oggetti COM sono dunque responsabili per il rilascio della
loro stessa memoria quando il reference count avrà raggiunto lo zero. In definitiva, possiamo dire che è
necessario chiamare AddRef o Release solo nei seguenti casi:
●
Funzione e metodi che ritornano riferimenti a interfaccia (via valore di ritorno o via parametri di output
passati come doppio puntatore) possono incrementare il reference count dell’oggetto di ritorno.
●
La Release dev’essere chiamata sul puntatore a un’interfaccia prima che il puntatore è sovrascritto o
viene
perso
in
altro
modo.
●
Se una copia è fatta su un puntatore che fa riferimento a un’interfaccia, allora una AddRef dev’essere
chiamata su quel puntatore.
●
AddRef e Release devono essere chiamate su una specifica interfaccia a cui viene fatto riferimento in
quanto un oggetto può implementare il numero di riferimenti per interfaccia al fine di allocare le risorse
interne solo per le interfacce che vengono referenziate.
Tutte le chiamate al reference count non sono inviate a un oggetto remoto attraverso il wire; un proxy tiene solo
un
riferimento
a
un
oggetto
remoto
e
mantiene
il
suo
reference
count
locale.
Wrapping dei componenti
Per la costruzione di un wrapper DirectX, è necessario eseguire lo wrapping delle interfacce dei componenti
programmando delle interfacce identiche che si preoccupano di istanziare e chiamare le funzioni delle
interfacce originali (Interface Wrapping), oppure dirottare le funzioni delle interface facendo un hook dei
puntatori a funzione delle rispettive vtables (Hooking VTable function addresses). Come vedremo, entrambi i
metodi rappresentano due tecniche diverse con i rispettivi pro e contro.
Interface wrapping
Tipicamente nelle DirectX9 la maggior parte delle chiamate vengono eseguite nel contesto di un componente
con interfaccia IDirect3D9, tramite il quale è poi possibile enumerare i display, creare i device directx e tutto il
resto. Per poter wrappare tutte le interfacce di interesse è necessario dunque wrappare l’interfaccia IDirect3D9
sostituendo l’unica funzione utilizzata per istanziarla, ossia la Direct3DCreate9.
Il miglior approccio per farlo consiste nell’installazione di un hook per il metodo Direct3DCreate9 non appena la
d3dhook.dll è stata iniettata dal sistema operativo alla partenza del processo e dunque prima che la funzione
venga eseguita per la prima volta:
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
installProcessHooks();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
11
Progetto 3DCloudViz
Deliverable D3
break;
}
return TRUE;
}
Questo procedimento nasconde un’insidia: anche se dobbiamo fare l’hook della Direct3DCreate9 non appena il
processo viene fatto partire, non possiamo fare l’hook della funzione se il processo non ha caricato la d3d9.dll
per la prima volta, è dunque importante capire quando ciò avviene. Tipicamente in un processo è possibile
linkare le librerie dinamiche in due modi:
Link statico a libreria dinamica. Le chiamate ad un modulo esterno vengono incluse a build time nella import
address table (IAT) del portable executable (PE) dell’applicazione. Questo significa che quando la nostra
d3dhook.dll viene iniettata all’interno del processo, la d3d9.dll è già presente ed è possibile installare l’hook
della Direct3DCreate9. Appena la d3dhook.dll è iniettata, è necessario eseguire un check della d3d9.dll tramite
una GetModuleHandle ed installare l’hook della Direct3DCreate9, altrimenti bisogna procedere in modo diverso
per coprire il secondo caso.
static void checkDirectXHooks()
{
dcv::Mutex &hookDirectXMutex = getHookDirectXMutex();
hookDirectXMutex.lock();
HMODULE hMod = GetModuleHandle("d3d9.dll");
if (hMod && !hModD3d9) {
installDirectX9Hooks(hMod);
}
hookDirectXMutex.unlock();
}
Link dinamico a libreria dinamica. Il modulo d3d9.dll può essere caricato a runtime in un momento qualsiasi
durante la vita del processo tramite una chiamata alla funzione LoadLibrary. In realtà si potrebbe rimediare
caricando la d3d9.dll alla partenza di tutti i processi del sistema e facendo un hook della Direct3DCreate9,
tuttavia non è possibile perchè il caricamento delle DirectX9 per tutti i processi è troppo dispendioso e
rischierebbe di compromettere la stabilità del sistema in modo serio. E’ necessario dunque introdurre un criterio
intelligente per intercettare l’istante in cui viene caricata la d3d9.dll che sia leggero e non comprometta il
corretto funzionamento del sistema operativo.
Il metodo più stabile da noi testato finora consiste nell’hook immediato della LoadLibrary non appena viene
iniettata la libreria, visto che tale funzione è sempre presente al momento dell’iniezione. Con un hook della
LoadLibrary possiamo metterci in ascolto di tutti i moduli che vengono caricati in seguito e installare l’hook della
Direct3DCreate9 non appena si verifica il caricamento della d3d9.dll, per la prima volta:
HMODULE WINAPI dcv_LoadLibraryA(LPCSTR lpFileName)
{
HMODULE hMod = LoadLibraryA(lpFileName);
checkDirectXHooks(); //check if d3d9.dll is loaded and hook methods
return hMod;
}
HMODULE WINAPI dcv_LoadLibraryW(LPWSTR lpFileName)
{
HMODULE hMod = LoadLibraryW(lpFileName);
12
Progetto 3DCloudViz
Deliverable D3
checkDirectXHooks(); //check if d3d9.dll is loaded and hook methods
return hMod;
}
static bool installProcessHooks()
{
if (!initEasyHook()) {
return false;
}
resetDirectXHooks();
checkDirectXHooks(); //check if d3d9.dll is loaded and hook methods
ULONG ACLEntries[1] = {0}, nf;
hModKernel32 = GetModuleHandle(TEXT("kernel32.dll"));
if (hModKernel32) {
nf = 0;
sys_LoadLibraryA = (LoadLibraryAPROC)GetProcAddress(hModKernel32,
"LoadLibraryA");
if (sys_LoadLibraryA) {
hHookSys[nf] = new HOOK_TRACE_INFO();
sys_LhInstallHook(sys_LoadLibraryA,
dcv_LoadLibraryA,
(PVOID)0x12345678,
hHookSys[nf]);
sys_LhSetExclusiveACL(ACLEntries, 0, hHookSys[nf]); nf++;
}
sys_LoadLibraryW = (LoadLibraryWPROC)GetProcAddress(hModKernel32,
"LoadLibraryW");
if (sys_LoadLibraryW) {
hHookSys[nf] = new HOOK_TRACE_INFO();
sys_LhInstallHook(sys_LoadLibraryW,
dcv_LoadLibraryW,
(PVOID)0x12345678,
hHookSys[nf]);
sys_LhSetExclusiveACL(ACLEntries, 0, hHookSys[nf]); nf++;
}
}
return true;
}
Una volta installato l’hook della Direct3DCreate9, possiamo ritornare un’istanza della nostra implementazione di
IDirect3D9 che wrappa l’originale e che contiene come membro un’istanza dell’interfaccia originale per poter
eseguire le chiamate native:
// dcvIDirect3D9.h
class dcvIDirect3D9 : public IDirect3D9
{
public:
dcvIDirect3D9(IDirect3D9 *pOriginal);
13
Progetto 3DCloudViz
Deliverable D3
virtual ~dcvIDirect3D9(void);
HRESULT __stdcall QueryInterface(REFIID riid, void **ppvObj);
ULONG __stdcall AddRef(void);
ULONG __stdcall Release(void);
HRESULT __stdcall RegisterSoftwareDevice(void *pInitializeFunction);
[...]
private:
IDirect3D9 *m_pIDirect3D9;
};
// dcvIDirect3D9.cpp
dcvIDirect3D9::dcvIDirect3D9(IDirect3D9 *pOriginal)
{
m_pIDirect3D9 = pOriginal;
}
dcvIDirect3D9::~dcvIDirect3D9(void)
{
}
HRESULT __stdcall dcvIDirect3D9::QueryInterface(REFIID riid, void **ppvObj)
{
*ppvObj = NULL;
HRESULT hRes = m_pIDirect3D9->QueryInterface(riid, ppvObj);
if (hRes == NOERROR) {
*ppvObj = this;
}
return hRes;
}
ULONG __stdcall dcvIDirect3D9::AddRef(void)
{
return m_pIDirect3D9->AddRef();
}
ULONG __stdcall dcvIDirect3D9::Release(void)
{
ULONG count = m_pIDirect3D9->Release();
if (count == 0) {
delete(this);
}
return count;
}
HRESULT __stdcall dcvIDirect3D9::RegisterSoftwareDevice(void *pInitializeFunction)
{
return m_pIDirect3D9->RegisterSoftwareDevice(pInitializeFunction);
}
Questo ci permette di intercettare la chiamata IDirect3D9::CreateDevice che crea a sua volta un device con
interfaccia IDirect3DDevice9 così da poter passare la nostra interfaccia custom come già fatto per IDirect3D9 e
14
Progetto 3DCloudViz
Deliverable D3
così via. Questo procedimento, eseguito in maniera attenta e metodica, ci permette di wrappare tutte le
interfacce DirectX ed averne il totale controllo:
HRESULT __stdcall dcvIDirect3D9::CreateDevice(UINT Adapter, D3DDEVTYPE DeviceType, HWND
hFocusWindow, DWORD BehaviorFlags, D3DPRESENT_PARAMETERS *pPresentationParameters,
IDirect3DDevice9 **ppReturnedDeviceInterface)
{
dcvIDirect3DDevice9 *pdcvIDirect3DDevice9;
IDirect3DDevice9 *pIDirect3DDevice9_ = NULL;
HRESULT hres = m_pIDirect3D9->CreateDevice(Adapter, DeviceType, hFocusWindow, BehaviorFlags,
pPresentationParameters, &pIDirect3DDevice9_);
pdcvIDirect3DDevice9 = new dcvIDirect3DDevice9(pIDirect3DDevice9_);
*ppReturnedDeviceInterface = pdcvIDirect3DDevice9;
return hres;
}
Il vantaggio nell’utilizzo di questa tecnica sta nella facilità di inclusione di risorse aggiuntive per interfaccia, che
possono essere incluse come membri al suo interno, inizializzate alla chiamata del costruttore e rilasciate alla
chiamata del distruttore. Uno svantaggio enorme, invece, sta proprio nel dover tenere traccia di tutte le funzioni
COM che istanziano nuove interfacce o restituiscono interfacce già istanziate, incrementando o decrementando
il reference count, nel rispetto della proprietà transitiva, riflessiva e simmetrica delle interfacce. In questo caso,
la complessità dell’interface wrapping è direttamente proporzionale alla complessità dell’architettura COM della
libreria da wrappare (i.e. DirectX10 e 11 sono di gran lunga più complesse di DirectX9). Bisogna dunque
prevedere tutte le combinazioni possibili e immaginabili di interface wrapping previste dalla libreria, così da
poter passare le interfacce giuste al sistema quando richiesto, evitando di perdere la traccia dell’hook o
addirittura di crashare l’intero processo.
Questo metodo, seppur complesso e difficile da mantenere, è l’unica strada che può essere percorsa per il
wrapping dell’intera libreria DirectX9/10/11, per la serializzazione e l’invio di tutte le chiamate da un client ad un
rendering server esterno. Tuttavia, con l’avvento delle vGPU ed il pieno supporto delle DirectX9/10/11 su
macchina virtuale, si è deciso di abbandonare questo approccio in quanto potenzialmente instabile, enorme e
complesso da mantenere alla luce dell’esistenza di metodi già esistenti e ben testati, come appunto quelli
implementati dai driver vGPU (nei capitoli successivi vedremo un’analisi più approfondita del problema). Si è
deciso dunque di utilizzare una tecnica alternativa che permette di arrivare direttamente alla chiamata Present(),
per catturare lo schermo renderizzato tramite vGPU e inviarlo al client, senza dover tenere traccia di tutte le
interfacce intermedie.
Hooking VTable function addresses
Questo approccio prevede l’uso della Virtual Method Table (VTable) di un’instanza di IDirect3DDevice9 per
determinare l’indirizzo di ciascun metodo virtuale così da poterli dirottare in funzioni alternative installando un
hook sui rispettivi puntatori a funzione. Per accedere alla vtable abbiamo bisogno di creare una istanza
temporanea di IDirect3DDevice9 poco prima di installare gli hook, e dunque creare un dummy device directx9
che non fa praticamente nulla se non regalarci l’istanza di cui abbiamo bisogno:
IDirect3D9 *pIDirect3D9 = Direct3DCreate9_fn(D3D_SDK_VERSION);
IDirect3DDevice9 *pIDirect3DDevice9;
ZeroMemory(&dxPresentParams, sizeof(dxPresentParams));
dxPresentParams.BackBufferCount = 1;
15
Progetto 3DCloudViz
Deliverable D3
dxPresentParams.MultiSampleType = D3DMULTISAMPLE_NONE;
dxPresentParams.MultiSampleQuality = 0;
dxPresentParams.SwapEffect = D3DSWAPEFFECT_DISCARD;
dxPresentParams.hDeviceWindow = hWnd;
dxPresentParams.Flags = 0;
dxPresentParams.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
dxPresentParams.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;
dxPresentParams.BackBufferFormat = D3DFMT_X8R8G8B8;
dxPresentParams.EnableAutoDepthStencil = FALSE;
dxPresentParams.Windowed = TRUE;
pIDirect3D9->CreateDevice(D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hWnd,
D3DCREATE_HARDWARE_VERTEXPROCESSING,
&dxPresentParams,
&pIDirect3DDevice9);
Da qua in poi bisogna semplicemente ricavare il puntatore alla vtable e scorrere la lista per ottenere i puntatori
ai metodi. Per ottenere il giusto puntatore alle funzioni per le quali si intende installare l’hook, è necessario
conoscere l’ordine esatto delle funzioni all’interno della vtable e gli argomenti da passare, che sono deducibili
dalla dichiarazione dell’interfaccia all’interno dell’header della libreria:
uintptr_t* pInterfaceVTable = (uintptr_t*)*(uintptr_t*)pIDirect3DDevice9;
Present9_fn = (Present9_Type)pInterfaceVTable[17];
TRACED_HOOK_HANDLE hHook = new HOOK_TRACE_INFO();
ULONG ACLEntries[1] = {0};
LhInstallHook_fn(Present9_fn, dcvPresent9, (PVOID)0x12345678, hHook);
LhSetExclusiveACL_fn(ACLEntries, 0, hHook);
Visto che stiamo parlando delle funzioni virtuali di un oggetto, è necessario aggiungere un primo argomento a
tutti gli altri argomenti che è un puntatore all’oggetto (IDirect3DDevice9 *) per il quale si effettua la chiamata:
HRESULT STDMETHODCALLTYPE dcvPresent9(IDirect3DDevice9 *pIDirect3DDevice9,
CONST RECT* pSourceRect,
CONST RECT* pDestRect,
HWND hDestWindowOverride,
CONST RGNDATA* pDirtyRegion)
{
D3DDEVICE_CREATION_PARAMETERS cparams;
pIDirect3DDevice9->GetCreationParameters(&cparams);
dcvDirect3D9Readback(pIDirect3DDevice9,
hDestWindowOverride ?
hDestWindowOverride :
cparams.hFocusWindow,
pSourceRect,
pDestRect,
pDirtyRegion);
return Present9_fn(self,
16
Progetto 3DCloudViz
Deliverable D3
pSourceRect,
pDestRect,
hDestWindowOverride,
pDirtyRegion);
}
Questa tecnica è efficace se si intende wrappare solo un set limitato di funzionalità DirectX. Al contrario, per il
wrapping dell’intera libreria DirectX9 e la serializzazione delle chiamate da inviare a un rendering server
esterno, si rivela totalmente inutile per i seguenti motivi:
- L’installazione di un hook per ogni funzione della vtable di tutte le interfacce è artificiosa e compromette
la stabilità del sistema.
-
Non è possibile installare l’hook di funzioni appartenenti a interfacce che non sono supportate sulla
macchina virtuale. Se le directx sono assenti o con supporto parziale, l’intero api hooking rischia di non
funzionare.
-
Non è possibile creare e passare interfacce custom, requisito fondamentale se si intende supportare il
rendering esterno con features e interfacce non supportate nella macchina virtuale, inclusa la totale
assenza delle directx.
Tuttavia, questa tecnica si rivela particolarmente robusta, efficace e facile da mantenere nel caso in cui si
intenda catturare lo schermo di un’applicazione che sta girando su macchina fisica o macchina virtuale con
vGPU, per poi comprimerlo e inviarlo a un client che faccia da endstation in un sistema di remotizzazione. Più
avanti vedremo come, assieme alla facilità di manutenzione di un sistema leggero e facile da mantenere, unito
allo sviluppo di nuove tecnologie emergenti per la remotizzazione tipo NVIDIA GRID e vGPU, si è preferito
usare questo tipo di approccio optando per una macchina virtuale (Guest) che tramite vGPU virtualizza le
DirectX9/10/11 in una macchina fisica (Host), che cattura e invia le immagini compresse via gpu ad un client
(Endstation), con un comportamento tuttosommato simile a quello di opengl con rendering server esterno e dei
costi di manutenzione di gran lunga inferiori, visto che la virtualizzazione di DirectX in questo caso è
implementata e mantenuta dai programmatori di NVIDIA.
1.4
Limitazioni dell’approccio tramite Library Injection
Per quanto la library injection si sia rivelata la miglior scelta per il wrapping di DirectX fino a questo momento, la
tecnica presenta delle limitazioni che farebbero preferire delle strade alternative, se solo fosse possibile averne.
Un problema riguarda il modo in cui le funzioni del wrapper possono chiamare le funzioni della libreria originale.
Abbiamo già detto che è necessario disattivare e riattivare l’hook della funzione qualora si intenda eseguire la
funzione originale, e questo già va a discapito delle performance, ma non è finita. E’ possibile che una funzione
debba chiamare altre funzioni senza passare dai rispettivi wrapper, per cui è necessario disattivare l’hook per
queste funzioni, senza toccare gli altri thread che verrebbero penalizzati. Questo lavoro di
attivazione/disattivazione e sincronizzazione per ogni chiamata si traduce in un lieve calo di performance. In
questo caso il calo è solo lieve perché le funzioni di cui fare hook sono poche visto che la maggior parte delle
chiamate sono gestite da componenti COM, ma nel caso in cui le chiamate fossero centinaia, o addirittura
migliaia, la tecnica non sarebbe affatto da utilizzare se le perfomance sono un punto critico.
Una limitazione, invece, risiede proprio nei principi che stanno alla base dell’injection e dell’api hooking.
L’alterazione del comportamento delle applicazioni non è una pratica ben vista dal sistema operativo per scopi
che esulino dal debugging delle applicazioni, e potrebbe essere intercettata da alcuni antivirus come un
tentativo di infezione del sistema.
Le librerie iniettate, oltre ad alterare il comportamento dei processi, li appesantisticono con parti di codice che la
maggior parte delle volte sono inutili, se i processi iniettati non sono applicazioni da remotizzare. Per questo
17
Progetto 3DCloudViz
Deliverable D3
motivo si è deciso di fare uso di una blacklist, per evitare quanto meno l’iniezione indesiderata dei processi da
non remotizzare, le cui performance e stabilità rappresentano un punto critico per il funzionamento dell’intero
sistema. Tuttavia è impossibile stilare una blacklist che minimizzi l’utilizzo di injection in tutti i casi possibili e
immaginabili, per cui la maggior parte delle volte questa tecnica rappresenta uno spreco sensibile di risorse.
Un altro problema riguarda la rimozione delle librerie iniettate. Diversamente dal metodo che fa uso di
SetWindowsHookEx che prevede un sistema automatico di rimozione, nel nostro caso è un requisito
praticamente impossibile da raggiungere, per i seguenti motivi:
●
non abbiamo modo di segnalare ai processi in funzione l’istante in cui è necessario espellere la libreria
iniettata. Si potrebbero usare gli eventi globali del sistema, ma un loop che si mette in ascolto e rilascia
per ogni processo, oltre ad essere letale in termini di stabilità, promette di degradare le performance
dell’intero sistema.
●
nel caso in cui l’applicazione stia facendo ancora uso di DirectX, non è possibile deiniettare il wrapper,
pena il crash istantaneo del processo. Si dovrebbero implementare delle funzioni bloccanti che
controllano per ogni chiamata, su ogni thread del processo, se è pervenuto un segnale di deinjection.
Tuttavia, oltre al degrado di performance, per deiniettare correttamente il wrapper si dovrebbe avere la
certezza che tutte le attività interne siano state portate a termine. Tutto questo si traduce in problemi di
stabilità e calo di performance che non possiamo permetterci.
Per colpa di questi problemi, i processi sono costretti a restare iniettati per tutta la loro vita. Questo significa che
la rimozione del wrapper per un’eventuale disinstallazione o upgrade non può essere eseguito se non
programmando degli script di installer che prevedono almeno un reboot del sistema.
1.5
Analisi di fattibilità del supporto per External Rendering
Sebbene riportato come ultimo punto tra i requisiti funzionali, abbiamo deciso di esplorare all’inizio del progetto
la fattibilità del supporto all’External Rendering, in quanto tale supporto influenza in modo strutturale le scelte
implementative.
NICE DCV supporta la delega del rendering OpenGL ad una macchina Linux definita Rendering Server. NICE
DCV si occupa di serializzare e inviare al rendering server lo stream di comandi OpenGL; il rendering server si
occupa di eseguire il rendering dell’immagine facendo uso della GPU e di inviare il risultato al client, che ha poi
il compito di comporre l’immagine OpenGL renderizzata con il desktop remoto che viene ricevuto direttamente
dall’host Windows.
Come visto in precedenza, le DirectX non sono strutturate su un’interfaccia di comandi ma di componenti COM.
In questo caso la situazione si complica, perché oltre alla serializzazione dello stream dei comandi per il
rendering, è necessario gestire la creazione ed il rilascio dei componenti COM sia lato client, sia lato server. E’
un lavoro molto oneroso se si considera:
●
Ogni componente implementa una o più interfacce che devono essere wrappate e devono rispettare la
proprietà riflessiva, simmetrica e transitiva.
●
Ogni singolo oggetto delle DirectX (device, adapter, rendering target, texture, surface, swap chain,
etc...) è stato implementato come un componente COM, con una o più interfacce.
●
Esistono funzioni che manipolano il reference count dei componenti COM. Per ottenere un
comportamento corretto della libreria, bisognerebbe andare da caso a caso e implementare il corretto
reference counting.
18
Progetto 3DCloudViz
●
Deliverable D3
Diversamente da OpenGL, le DirectX sono multithreading con più contesti sullo thread o su più thread
in parallelo. Questo aggiunge una complicazione in più.
Un’applicazione di solito genera migliaia di chiamate per i vari QueryInterface e la gestione del reference
counting, visto che i componenti COM sono molteplici e sono svariati i metodi per poterli gestire. Serializzare le
chiamate dirette di ciancun componente COM farebbe precipitare verso il basso le performance
dell’applicazione e finirebbe per saturare il network. Per ovviare a tutto questo, si dovrebbe escogitare uno
strato di astrazione che faccia da intermediario tra il client e il wire sull’application host, e tra il wire e il server
sul rendering server, che gestisca i componenti COM e serializzi sul wire solo un set indispensabile di chiamate
per il rendering, il mapping delle risorse grafiche, la gestione degli adapters, la presentazione degli schermi, gli
shaders, insomma, bisognerebbe reinventare da capo un core leggero e serializzabile di DirectX che
somiglierebbe molto all’implementazione delle funzionalità del driver grafico distribuito dai fornitori. Un lavoro
troppo oneroso in termini di risorse umane e tempo a disposizione.
●
challenge della traduzione da DIrcetX a OpenGL
Il rendering server di NICE DCV ad oggi richiede di essere in esecuizione su una macchina GNU/Linux e su tale
sistema operativo la libreria per la grafica 3D è OpenGL. DirectX è disponibile nativamente solo su sistemi
operativi Windows. Ne consegue che per implementare l’external rendering per DirectX sia necessario tradurre
le chiamate DirectX nelle corrispondenti chiamate OpenGL, tuttavia tale traduzione presenta svariati problemi in
quanto le due librerie non hanno una corrispondenza diretta ed espongono modelli di programmazione diversi.
L’alternativa sarebbe portare il rendering server su Windows, ma tale strada non è stata perseguita in quanto
considerata non strategica durante questo progetto, poichè vogliamo che il rendering server possa continuare
ad essere eseguito su di un host che funga da hypervisor KVM.
In realtà esiste già un progetto che implementa uno strato intermedio simile a quello che andiamo cercando: il
WineD3D, che è un sottocomponente del Wine.
Il Wine è un software scritto in C nato originariamente per sistemi operativi GNU/Linux, poi esteso ad altri
sistemi operativi, con lo scopo di permettere il funzionamento dei programmi sviluppati per il sistema operativo
windows. Non esistendo una versione DirectX per questi sistemi operativi, ed essendo al contrario le OpenGL
crossplatform, si è deciso di implementare un wrapper dal nome WineD3D che converta le chiamate DirectX in
OpenGL, così da poter utilizzare questa libreria grafica, già presente nei sistemi operativi dove si intende far
partire le applicazioni windows.
Avendo già programmato un wrapper di OpenGL e un interception di DirectX, la nostra idea è stata quello di
fondere le nostre componenti a quelle del Wine per poter implementare un wrapper DirectX che comuni con un
rendering server OpenGL, mettendo le componenti in serie nel seguente modo:
DirectX Interception -> Wine D3D -> OpenGL Wrapper -> Wire -> External Rendering
In pratica, per DirectX9, abbiamo dirottato la CreateDevice9 cercata dall’applicazione nella versione
implementata da WineD3D che a sua volta ha utilizzate le funzioni OpenGL, che a loro volta vengono
intercettate dal wrapper OpenGL di DCV che serializza le chiamate, le manda sul wire ad un rendering server
esterno su linux che effettua il rendering delle immagini da inviare a un Endstation. Per quanto possa sembrare
entusiasmante, un procedimento così complesso si è dimostrato troppo debole per i seguenti motivi:
●
Il Wine D3D è studiato principalmente per far funzionare videogiochi. Non esiste alcuna tabella di
stabilità associata alle pro application e nessun tentativo concreto di farle partire. Ed infatti, la maggior
parte di loro, tipo Autocad o 3DStudio, non hanno funzionato. Hanno funzionato invece i demo dell’SDK
DirectX o i videogiochi supportati da WineD3D, che non sono di nostro interesse.
19
Progetto 3DCloudViz
Deliverable D3
●
La traduzione eccessiva di chiamate si traduce in un calo di performance già evidente nella versione
che non fa uso del wrapper OpenGL, con il wrapper opengl le performance erano così basse da
rasentare la manciatina di frame nelle poche applicazioni in grado di eseguire correttamente.
●
DirectX a dispetto della sua architettura macchinosa, riesce a regalare performance e features superiori
rispetto a quelle di OpenGL. Creare una versione di DirectX scalata a OpenGL oltre che introdurre delle
limitazioni ed un calo di performance, non sarebbe piaciuta ai client che confidano nelle features più
avanzate di DirectX.
Dunque, l’unica strada apparentemente percorribile per il rendering esterno di DirectX si è rivelata troppo
debole per poter essere presa in considerazione. Non esistendo nessun altra alternativa, abbiamo deciso di non
intraprendere questa strada.
Fortunatamente, l’abbandono del rendering esterno di DirectX da parte nostra ha visto in parallelo la nascita di
più tecnologie per la virtualizzazione che lo supportano in modo nativo, seppur con delle limitazioni. Tra queste
tecnologie, quella di nostro interesse e che prenderemo in esame da ora in avanti riguarda NVIDIA GRID.
Questa tecnologia comprende i driver vGPU che consentono ad un massimo di 8 macchine virtuali guest di
utilizzare la scheda grafica della macchina host che le ospita. Anche se non passa attraverso il wire ma un
canale di comunicazione interno, è la soluzione ideale per macchine virtuali windows che girano su una
macchina xen di host, che può essere vista un po’ come il rendering server su linux nel caso di OpenGL I test
con questa nuova tecnologia hanno regalato una semplicità di implementazione assieme a performance e
stabilità tali da averci orientato definitvamente verso questa soluzione. Più avanti vedremo lo sviluppo delle
tecnologie che fanno parte del pacchetto NVIDIA GRID e come il loro utilizzo ci abbia consentito di
implementare un wrapper DirectX con vtable che gira su macchina fisica e su macchina virtuale con vGPU.
1.6
Avanzamenti tecnologici della piattaforma di sviluppo
Inizialmente Monterey, in seguito rinominata NVIDIA GRID, la tecnologia di NVIDIA ha evoluto negli anni delle
librerie che facilitano l’implementazione e rendono più performante il processo di remotizzazione e
virtualizzazione delle applicazioni 3D.
NvIFR
La prima libreria del pacchetto che abbiamo preso in considerazione, sin dalla nascita e durante il suo sviluppo,
è la NvIFR. Dato un device DirectX o un rendering context OpenGL in ingresso, la libreria NvIFR consente una
cattura degli schermi più performante rispetto a quella fornita dalle rispettive librerie grafiche.
NvIFRToSys. Consente la cattura di schermi su memoria di sistema. Dai nostri test, si è rivelata più performante
della glReadPixels di OpenGL e della GetRenderTargetData su offscreen surface di DirectX. Per questo motivo,
abbiamo deciso di usarla già da subito per la cattura degli schermi nel wrapper con vtable di DirectX (in grado di
girare solo su macchina fisica) e nel wrapper di OpenGL (su macchina fisica e rendering esterno).
NvIFRH264. In questa variante, gli schermi vengono catturati, compressi h264 con la GPU ed il frame dello
stream video viene restituito in uscita. Di solito noi catturiamo gli schermi dalla memoria video alla memoria di
sistema, li passiamo a un compressor che comprime utilizzando la codifica jpeg o zrle, con un dispendio di CPU
non indifferente. La cosa migliore della compressione h264, invece, non è soltanto la compressione via
hardware ma avere l’opportunità di poter comprimere al volo dallo schermo nella memoria video senza passare
per la memoria di sistema, con un guadagno enorme in termini di utilizzo di CPU. L’intruduzione del formato
h264 ha richiesto degli sforzi implementativi per evolvere il canale immagini di dcv e permettergli di supportare i
flussi video. NvIFRH264 è stato adottato in entrambi i wrapper OpenGL e DirectX.
20
Progetto 3DCloudViz
Deliverable D3
Supporto GRID in passthrough
La tecnologia GPU passthrough di NVIDIA permette di creare una workstation virtuale che da agli utenti con
tutti i benefici di un processore grafico dedicato sulla loro macchina. Connettendo direttamente una GPU
dedidcata a una macchina virtuale attraverso l’hypervisor (VMWare ESX, XenServer, KVM), è possibile allocare
l’intera GPU e le capacità di memoria grafica su una singola macchina virtuale senza compromettere alcuna
risorsa.
L’uso di un driver grafico nella virtual machine assicura la compatibilità col 100% delle applicazioni e l’accesso
alle più recenti versioni di NVIDIA di OpenGL, DirectX, Cuda ed NVIDIA GRID. Questo ha permesso di
utilizzare il wrapper DirectX nella sua versione vtable con NvIFR per h264, pur girando su una macchina
virtuale. L’enorme limitazione del passthrough è che non si può creare più di una macchina virtual per GPU
della macchina host, e di solito si spera di avere molte macchine virtuali che condividono le stesse risorse
hardware. Fortunatamente, la tecnologia vGPU sembra far fronte al problema.
vGPU
La tecnologia vGPU di NVIDIA permette a più macchine virtuali di avere accesso in modo simultaneo e diretto a
una singola GPU sulla macchina fisica.
21
Progetto 3DCloudViz
Deliverable D3
Questo ci consente di avere più macchine virtuali windows sulla stessa macchina host, che eseguono
applicazioni 3D con le versioni 9, 10 e 11 di DirectX. Le immagini sono catturate dal wrapper di DirectX con
NvIFR, codificate h264 ed inviate alla endstation. Tuttavia ci sono delle limitazioni:
●
●
●
●
●
E’ possibile riservare la stessa GPU fisica per un massimo di 8 macchine virtuali
Cuda e OpenCL non sono supportati
Funziona solo con GPU della serie NVIDIA GRID (i.e. NVIDIA K2)
Per il momento gli unici hypervisor supportati sono XenServer da 6.2 SP1 in su e VMWare ESXi da 6.0
in su
Linux non è supportato per il momento (NVIDIA ha annunciato di farlo in futuro)
Queste limitazioni non rappresentano comunque un ostacolo al corretto funzionamento della soluzione adottata
col wrapper di DirectX e NvIFRH264.
NvFBC
NvFBC è una API che mette a disposizione delle funzionalità interessanti per la cattura dell’intero desktop e non
del frame buffer della scena 3D della singola applicazione. La possibilità di catturare l’intero desktop mette in
discussione l’effettivo vantaggio di catturare gli schermi delle singole applicazioni, con tutti problemi legati alle
tecnologie dei wrapper, e non l’intero desktop.
Fino a tempi recenti si è deciso di catturare lo schermo delle singole applicazioni perchè la cattura dell’intero
desktop è troppo costosa in termini di compressione e fino a quando si poteva comprimere con jpeg, l’invio
dell’intero schermo rappresentava un uso troppo importante della banda e l’introduzione di delay lato client
troppo importanti, uniti a un eccessivo uso della CPU. Era necessario avere dei criteri per non catturare,
comprimere ed inviare le immagini di continuo e capire quali porzioni di schermo cambiassero per comprimere
ed inviare al client solo quelle, ma non c’era nulla se non programmare degli algoritmi via software che
aggiungevano complessità e un calo delle performance. NvFBC sembra fornire una soluzione anche per questo
attraverso la cattura delle diffmap. Una diffmap è una mappa dove il valore associato ad ogni byte se diverso da
zero rappresenta un cambiamento dei pixel in un blocco grande 128x128. Suddividendo l’intero desktop in una
griglia di blocchi 128x128, è possibile capire quali pixel di questi blocchi siano effettivamente cambiati da un
frame e il successivo, comprimere ed inviare al client solo questi. Ma non basta.
22
Progetto 3DCloudViz
Deliverable D3
La compressione dei tile tramite jpeg è troppo onerosa se il cambiamento tra un frame e il successivo dovesse
riguardare i pixel dell’intera immagine. Per ovviare a questo problema, si è possibile passare l’immagine del
desktop come texture ad un device DirectX per poi comprimere in h264 via GPU tramite NvIFRH264. Questa
soluzione ci ha permesso di elaborare un prototipo che cattura l’intero desktop e fa a meno dei wrapper con
library interposition, col vantaggio di avere un sistema non modificato con un solo processo in background che
cattura le immagini e le invia ad un client.
1.7
Analisi e implementazione di Screen Grabbing tramite NVDIA GRID
Facendo uso delle tecnologie NVIDIA GRID, abbiamo proceduto ad implementare un prototipo con una
architettura completamente differente dai metodi di library interposition.
L’obiettivo del prototipo è di catturare l’intero schermo in modo indipendente dalle applicazioni e per tanto
supportando trasparentemente applicazioni DirectX, OpenGL o anche applicazioni 2D. Per soddisfare l’obiettivo
di retrocompatibilità con client esistenti, il meccanismo di cattura, compressione e trasmissione deve andare ad
integrarsi con il processo VNC esistente.
L’architettura che abbiamo definito è la seguente: un processo demone dcvd usa NvFBC per catturare gli
schermi ed apre una finestra invisibile di overlay per permettere al client di VNC di tracciare l’intero contenuto
del desktop. In base ai cambiamenti del desktop, l’immagine catturata o alcune sue parti vengono compresse
con zrle o jpeg via software oppure con h264 tramite NvIFRH264. I frame compressi vengono inviati al client,
decompressi e visualizzati sulla finestra del client vnc, perfettamente compatibile alla versione di dcv con
wrapper OpenGL. Il diagramma che descrive il funzionamento di dcv in questo caso, è il seguente:
E’ stato necessario introdurre dei criteri per ottimizzare l’utilizzo della banda, migliorare le performance e
minimizzare il delay. NvFBC è in grado di catturare lo schermo se c’è stato un cambiamento nel desktop ed una
mappa dei blocchi del desktop hanno subito questo cambiamento. Se un solo blocco è stato modificato, allora
l’intero desktop viene catturato da uno screen grabber tramite la funzione bloccante NvFBCToSysGrabFrame
assieme alla rispettiva diffmap. L’immagine e la diffmap vengono inviate dallo screen grabber ad un tiler, che
decide quale tipo di compressione effettuare in base alla quantità dei cambiamenti. Se più di 9 ≤ N/10 ≤ 16 degli
N blocchi dello schermo stanno cambiando, allora l’intero frame viene compresso con h264. Altrimenti dalla
diffmap vengono estratti dei rettangoli (o tile) che coprono i blocchi per le porzioni di schermo che hanno subito
il cambiamento, vengono compressi con jpeg e inviati sul client. Se non si verifica alcun cambiamento nel giro di
300 ms, allora viene inviato un quality update di tipo lossless con zrle per ciancun blocco che ha subito il
cambiamento tra l’invio della quality update precedente e quella corrente. Nel caso dei monitor multipli, un unico
desktop virtuale è composto da più schermi, uno per ogni monitor. In questo caso viene tracciata una finestra di
overlay in grado di comprire l’intero dekstop virtuale. Il desktop capture di dcvd si preoccupa di enumerare i
monitor e di instanzare uno screen grabber con un NvFBC per ciascun monitor. I frame h264 ed i tiles vengono
23
Progetto 3DCloudViz
Deliverable D3
inviati al client sottoforma di slice da ricomporre e disegnare nelle rispettive posizioni occupate dai monitor nel
desktop virtuale.
Le strategie di screen capture hanno permesso di diminuire il delay, il processamento delle immagini a carico
della cpu così come l’utilizzo di banda. Questo ha reso possibile la cattura dell’intero desktop anche con monitor
multipli e la remotizzazione delle applicazioni 3D senza l’utilizzo dei wrapper con library injection ed
interposition. I vantaggi rispetto al vecchio sistema coi wrapper sono i seguenti:
●
Il sistema non è modificato da filter driver e librerie iniettate nei processi.
●
Non c’è un wrapper che possa crashare l’applicazione o degradarne le performance.
●
E’ possibile catturare, comprimere e inviare sul client solo ciò che si vede sull’intero desktop. Questo
consente di aprire un numero arbitrario di applicazioni 3D. Col vecchio sistema, 100 dcvtest avrebbero
inviato 100 immagini, saturando la cpu sul server, la banda e la cpu sul client. In questo modo, 100
dcvtest provocano al più l’invio dell’intero desktop in h264.
●
Non c’è più bisogno di tenere il passo tecnologico delle librerie grafiche per tenere i wrapper aggiornati.
●
E’ possibile fornire all’utente un’esperienza di remotizzazione completa su tutte le applicazioni del
desktop, comprese quelle del browser.
1.8
Conclusioni ed obiettivi raggiunti
Il progetto ha permesso di raggiungere i seguenti obiettivi che soddisfano pienamente i requisiti funzionali che ci
eravamo prefissi:
●
L’approccio basato su screen grabbing si è dimostrato il più stabile e performante: consente la
remotizzazione di applicazioni OpenGL e DirectX, assicurando una compatibiltà estesa con tutte le
applicazioni testate. L’uso di NVIDIA GRID permette di sfruttare la codifica h264 e il fast frame
grabbing, assicurando prestazioni elevate. L’uso di GPU passthrough e vGPU consente il supporto di
macchine virtuali. Infine Il prototipo implementato si integra pienamente con la versione esistente di
VNC usata da NICE DCV, garantendo la compatibilità con client esistenti
●
Lo studio delle tecniche di interposition per DirectX ha permesso di approfondire la nostra conoscenza
sia della libreria DirectX stessa, sia delle tecniche di intercettamento a nostra disposizione: tale knowhow sarà estremamente utile in futuro sia per il debugging di applicazioni sia per implementare soluzioni
in ambiti in cui la library interposition sia necessaria
●
Lo studio di fattibilità sul supporto per external rendering ha permesso di mettere in evidenza una serie
di limitazioni che allo stata attuale sono state considerate bloccanti; se in futuro fosse necessario
intraprendere nuovamente questa strada saremmo già a conoscenza dei problemi da affrontare
24