PDA

View Full Version : una fork per Windows [x ilsensine, ma non solo :)]


71104
12-12-2005, 20:45
l'altro giorno sulla mailing list del corso di Laboratorio di SO arriva un niubbo e fa: "ho fatto per esercitarmi un programma che usa la funzione fork bla bla e su Windows non compila (lol :asd: ) esiste una funzione analoga alla fork su Windows?"; dopodiché arriva un altro niubbo (un po' meno niubbo del precedente però) e fa "ah bla bla bla la fork è una prerogativa dei sistemi Unix ecc. ecc.".
allora io prendo spunto e realizzo una mini-fork per Windows :asd:
non vi immaginate che sia un granché, è molto rudimentale: altro non fa che creare il processo figlio sospeso, ricopiarci tutte le sezioni non shared e non discardable impostate con permessi di scrittura oltre che di lettura, settare opportunamente il contesto del thread interrotto, ed infine riesumarlo; una fork simile funziona solo a patto che siano verificate le seguenti condizioni:
1) il programma è single-threaded
2) il programma non chiama VirtualAlloc e HeapAlloc prima della fork (dopo volendo si, ma prima no)
3) il programma non alloca handles non ereditabili prima della fork (dopo magari si, ma prima no :p)

comunque quel poco che fa lo fa bene :)
siccome ricordo che ilsensine una volta chiese a fek di spiegargli come hanno fatto quelli di Cygwin a realizzare la fork su Windows, ora la incollo qui di seguito; BTW, per l'occasione mi sono anche letto il codice della fork di Cygwin: la loro funziona un po' meglio della mia perché hanno un vantaggio: se ho ben capito come funziona questo Cygwin, si assume che un programma usi solamente le funzioni di Cygwin, quindi loro nel realizzare la fork avevano controllo sull'heap; ma per fare un bel crash anche su Cygwin è sufficiente tenere memoria di un handle non ereditabile prima di una fork :)


#include <windows.h>

int fork();

int main() {
TCHAR pszMsg[0x100];
wsprintf(pszMsg, TEXT("questo è il processo padre, PID = %d"), GetCurrentProcessId());
MessageBox(NULL, pszMsg, TEXT("fork on Windows"), MB_ICONINFORMATION);

if (fork()) {
wsprintf(pszMsg, TEXT("questo invece è il figlio, PID = %d"), GetCurrentProcessId());
MessageBox(NULL, pszMsg, TEXT("fork on Windows"), MB_ICONINFORMATION);
}

return 0;
}


#define TESTFLAG(mask, flag) ((flag) == ((mask) & (flag)))

HANDLE hStarted = NULL;

__declspec(naked) void Stub() {
SetEvent(hStarted);
CloseHandle(hStarted);
SuspendThread(GetCurrentThread());
}

BYTE pbJmp[5] = {0xE9};

int fork() {
DWORD dwDummy;

TCHAR pszFileName[0x400];
if (!GetModuleFileName(NULL, pszFileName, 0x400)) {
return -1;
}

SECURITY_ATTRIBUTES sa = {
sizeof(SECURITY_ATTRIBUTES),
NULL,
TRUE
};
hStarted = CreateEvent(&sa, FALSE, FALSE, NULL);

STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory((PVOID)&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);

if (!CreateProcess(pszFileName, NULL, NULL, NULL, TRUE, GetPriorityClass(GetCurrentProcess()) |
CREATE_SUSPENDED, NULL, NULL, &si, &pi))
{
return -1;
}

WriteProcessMemory(pi.hProcess, (PVOID)&hStarted, (PVOID)&hStarted, 4, &dwDummy);


PBYTE pbInstBase = (PBYTE)GetModuleHandle(NULL);
PIMAGE_NT_HEADERS pinth = (PIMAGE_NT_HEADERS)(pbInstBase + *(PDWORD)(pbInstBase + 0x3C));

PVOID pvEntryAddress = (PVOID)(pbInstBase + pinth->OptionalHeader.AddressOfEntryPoint);

DWORD dwPrevProtect;
VirtualProtectEx(pi.hProcess, pvEntryAddress, 5, PAGE_READWRITE, &dwPrevProtect);

*(PDWORD)(pbJmp + 1) = (DWORD)&Stub - (DWORD)pvEntryAddress - 5;
WriteProcessMemory(pi.hProcess, pvEntryAddress, (PVOID)pbJmp, 5, &dwDummy);

ResumeThread(pi.hThread);
WaitForSingleObject(hStarted, INFINITE);
CloseHandle(hStarted);

WriteProcessMemory(pi.hProcess, pvEntryAddress, pvEntryAddress, 5, &dwDummy);

VirtualProtectEx(pi.hProcess, pvEntryAddress, 5, dwPrevProtect, &dwDummy);


PIMAGE_SECTION_HEADER pSecHdrs = (PIMAGE_SECTION_HEADER)((PBYTE)pinth +
sizeof(IMAGE_NT_HEADERS));
for (UINT u = 0; u < pinth->FileHeader.NumberOfSections; u++) {
DWORD dwFlags = pSecHdrs[u].Characteristics;
if (!TESTFLAG(dwFlags, IMAGE_SCN_MEM_DISCARDABLE) &&
!TESTFLAG(dwFlags, IMAGE_SCN_MEM_SHARED) &&
TESTFLAG(dwFlags, IMAGE_SCN_MEM_READ) &&
TESTFLAG(dwFlags, IMAGE_SCN_MEM_WRITE))
{
PVOID pvSecAddr = (PVOID)(pbInstBase + pSecHdrs[u].VirtualAddress);
WriteProcessMemory(pi.hProcess, pvSecAddr, pvSecAddr, pSecHdrs[u].Misc.VirtualSize,
&dwDummy);
}
}


BOOL InTheChild = FALSE;

CONTEXT c;
ZeroMemory((PVOID)&c, sizeof(CONTEXT));
c.ContextFlags = CONTEXT_FULL;
GetThreadContext(GetCurrentThread(), &c);

__asm {
call near here
here:
pop eax
inc eax
mov c.Eip,eax
mov c.Esp,esp
mov c.Ebp,ebp
}

if (InTheChild) {
return GetCurrentProcessId();
}

/*
PUSH EBP
MOV EBP,ESP
*/

DWORD dwFramePtr;
__asm {
mov dwFramePtr,ebp
}
DWORD dwKernel32 = (DWORD)LoadLibrary(TEXT("kernel32.dll"));
PIMAGE_NT_HEADERS pinthKernel32 = (PIMAGE_NT_HEADERS)(dwKernel32 +
*(PDWORD)(dwKernel32 + 0x3C));
UINT uKernel32Size = pinthKernel32->OptionalHeader.SizeOfImage;
while (dwFramePtr) {
DWORD dwRetAddress = *(((PDWORD)dwFramePtr) + 1);
if ((UINT)(dwRetAddress - dwKernel32) <= uKernel32Size) {
break;
}
dwFramePtr = *(PDWORD)dwFramePtr;
}

DWORD dwStackPtr;
__asm {
mov dwStackPtr,esp
}
DWORD dwDelta = dwFramePtr - dwStackPtr;

WriteProcessMemory(pi.hProcess, (PVOID)dwStackPtr, (PVOID)dwStackPtr, dwDelta, &dwDummy);


BOOL RemoteChildFlag = TRUE;
WriteProcessMemory(pi.hProcess, (PVOID)&InTheChild, (PVOID)&RemoteChildFlag, 4, &dwDummy);

SetThreadContext(pi.hThread, &c);
ResumeThread(pi.hThread);

CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);

return 0;
}

71104
12-12-2005, 20:49
dimenticavo: a causa di una particolarità del mio codice, il programma che chiama quella fork non può neanche chiamarla da una funzione di callback che è stata a sua volta richiamata da Kernel32.dll: altre DLL vanno bene, User32 ad esempio va bene, ma Kernel32 no :(
insomma sta fork fa abbastanza schifo :D
ma provate voi a farne una migliore su Windows :Prrr:

però c'è di buono che in teoria un programma che usa quella fork può anche avere un'interfaccia grafica perché gli handles delle finestre sono globalmente visibili, e quindi sarebbero validi anche nei processi figli... con l'unico problema che il processo figlio utilizzerebbe le stesse finestre del padre :D ci sarebbe un'attimino di interferenza :p

The3DProgrammer
12-12-2005, 21:51
certo ke tu e kernel32.dll vi parlate eh?
:D

ciau

VegetaSSJ5
12-12-2005, 21:59
:eek:


:cry:

jappilas
12-12-2005, 22:17
bello, bello, tutta "arte che entra" :)

ancora inpùt, ancora...

The3DProgrammer
12-12-2005, 22:21
a grandi linee l'ho capita, mi manca solo una parte:





*(PDWORD)(pbJmp + 1) = (DWORD)&Stub - (DWORD)pvEntryAddress - 5;
WriteProcessMemory(pi.hProcess, pvEntryAddress, (PVOID)pbJmp, 5, &dwDummy);



Ad occhio, &Stub-pvEntryAddress-5(?) restituisce il numero di byte che comprende la sezione tra l'entry point e la funzione Stub (di cui nn capisco la necessità di dichiararla naked) di cui vai ad effettuare una copia nel nuovo processo... o no?

ciau

71104
12-12-2005, 23:10
a grandi linee l'ho capita, mi manca solo una parte:





*(PDWORD)(pbJmp + 1) = (DWORD)&Stub - (DWORD)pvEntryAddress - 5;
WriteProcessMemory(pi.hProcess, pvEntryAddress, (PVOID)pbJmp, 5, &dwDummy);



Ad occhio, &Stub-pvEntryAddress-5(?) restituisce il numero di byte che comprende la sezione tra l'entry point e la funzione Stub (di cui nn capisco la necessità di dichiararla naked) di cui vai ad effettuare una copia nel nuovo processo... o no? hum, se non hai capito quello ti manca un pezzo piuttosto grosso... ti do un aiutino: guarda il nome del buffer che conteine i dati che vado a copiare nell'altro processo, e guarda anche la dimensione di tale buffer e il valore del primo byte ;)

ciao :D

71104
12-12-2005, 23:14
:eek:


:cry: capisco lo stupore, ma come mai le lacrime? :D

ilsensine
13-12-2005, 08:17
1) il programma è single-threaded

Come mai questa limitazione? Una fork non clona i thread

comunque quel poco che fa lo fa bene :)
Non dovrebbe fare molto altro, in effetti. Tieni conto che il 90% delle fork viene fatto prima di una exec (visto che nei sistemi unix non esiste un equivalente di CreateProcess), e il 9.9% sono situazioni che potrebbero essere meglio gestite tramite i thread. Direi quindi che se si intende scrivere un programma multipiattaforma che giri anche su windows, ha molto senso aggiungere quel paio di righe su #ifdef per chiamare CreateProcess oppure fork+exec a seconda della piattaforma (nascoste magari in un metodo StartNewProcess() ). Anche perché con le varie WriteProcessMemory elimini il principale punto di forza della fork (ovvero la COW), rendendo quindi estremamente inefficiente la funzione.

Grazie per la spiegazione comunque :)

ilsensine
13-12-2005, 08:50
3) il programma non alloca handles non ereditabili prima della fork (dopo magari si, ma prima no :p)

Anche questo non dovrebbe essere un problema, se la fork viene usata nella maniera classica (fork+exec). Anche su linux gli fd possono essere marcati "non ereditabili da una exec". In questo caso, gli handler non ereditabili dovrebbero passare inosservati/inuilizzati.

VegetaSSJ5
13-12-2005, 18:31
capisco lo stupore, ma come mai le lacrime? :D
ops hai ragione! ho dimenticato anche questo --> :muro: :help:

71104
13-12-2005, 22:56
Come mai questa limitazione? Una fork non clona i thread appunto :p
il nuovo programma non avrà i thread del primo, avrà solo il thread primario, quindi o il programma è single threaded oppure il programmatore deve essere consapevole della limitazione.

Non dovrebbe fare molto altro, in effetti. Tieni conto che il 90% delle fork viene fatto prima di una exec (visto che nei sistemi unix non esiste un equivalente di CreateProcess), e il 9.9% sono situazioni che potrebbero essere meglio gestite tramite i thread. Direi quindi che se si intende scrivere un programma multipiattaforma che giri anche su windows, ha molto senso aggiungere quel paio di righe su #ifdef per chiamare CreateProcess oppure fork+exec a seconda della piattaforma (nascoste magari in un metodo StartNewProcess() ). si ma che utilità ha secondo te questa fork che non ti permette neanche di usare malloc e free?? :D

Anche perché con le varie WriteProcessMemory elimini il principale punto di forza della fork (ovvero la COW), rendendo quindi estremamente inefficiente la funzione. era solo un esperimento didattico :p

Grazie per la spiegazione comunque :) prego

71104
13-12-2005, 22:58
Anche questo non dovrebbe essere un problema, se la fork viene usata nella maniera classica (fork+exec). Anche su linux gli fd possono essere marcati "non ereditabili da una exec". In questo caso, gli handler non ereditabili dovrebbero passare inosservati/inuilizzati. il problema è il seguente:
1) il processo padre crea un handle non ereditabile per, diciamo, un file, e non lo chiude
2) fork
3) il processo figlio fa riferimento a quell'handle... :D

al punto 3 qualsiasi funzione API semplicemente ritorna errore, ma il processo ne deve essere "aware"; cioè, se tu mi dici che il problema esiste anche su Linux e che i programmatori che usano la fork ne sono coscienti allora va bene, volevo solo dire che è un punto da tenere presente...

ilsensine
14-12-2005, 07:45
appunto :p
il nuovo programma non avrà i thread del primo, avrà solo il thread primario, quindi o il programma è single threaded oppure il programmatore deve essere consapevole della limitazione.
Anche la fork funziona così; un processo forkato contiene un solo thread.

si ma che utilità ha secondo te questa fork che non ti permette neanche di usare malloc e free?? :D
<me gnurant>
Vuoi dire che questo:
2) il programma non chiama VirtualAlloc e HeapAlloc prima della fork
implica che non puoi chiamare malloc/free? Oh bè questo è un problema...
3) il processo figlio fa riferimento a quell'handle...
Se l'handle non è ereditabile è corretto che il figlio lo veda chiuso

71104
14-12-2005, 10:02
implica che non puoi chiamare malloc/free? Oh bè questo è un problema... esatto... ^^
il problema è che su Windows un processo crea uno o più heap usando HeapCreate (poi li distruggerà con HeapDestroy), e in ciascuno di questi può allocare e riallocare memoria con HeapAlloc e HeapRealloc; siccome è anche possibile enumerare tutti gli heap presenti nel processo, io potrei anche provare ad enumerarli e allocare tanti blocchi nel nuovo quanti ne vedo nel vecchio, ma non è detto che avrebbero tutti gli stessi indirizzi virtuali: servirebbero delle rilocazioni... il problema si può risolvere utilizzando VirtualAlloc al posto di HeapAlloc per allocare i blocchi dell'heap: VirtualAlloc infatti permette di specificare l'indirizzo virtuale desiderato, ma c'è un problema anche qui: gli heap in Windows sono oggetti del kernel, ovvero sono identificati da un handle, il quale ovviamente viene creato come non ereditabile; se fosse ereditabile sarebbe un gran macello perché il processo figlio accederebbe alla stessa memoria dinamica del padre senza sincronizzarsi... d'altra parte però, non essendo ereditabile, il processo figlio non vede gli handle degli heap che aveva il padre, e quindi (siccome si suppone che esso tenga memoria dell'handle dell'heap, magari all'interno del CRT di Visual C++) qualsiasi HeapAlloc, HeapRealloc ed HeapFree per lui fallirà.
in user mode insomma non si va molto lontano, a meno che non si faccia come hanno fatto su Cygwin, ovvero non si realizza un intero framework che abbia controllo su queste cose e quindi abbia anche controllo sugli handle degli heap e li possa cambiare a piacere.
quindi stavo pensando l'altro giorno a quale potrebbe essere una soluzione un po' migliore realizzata in kernel mode: in kernel mode dopo aver creato il processo si potrebbe fare un po' quello che fa Linux, ovvero ricopiare pari pari la directory e le tabelle delle pagine mettendole in protezione Copy-On-Write; il problema degli handle degli heap però rimane comunque... uqesta soluzione altro non sarebbe che una versione più efficiente della soluzione in cui uso VirtualAlloc per ricopiare gli heap nel processo figlio.

DanieleC88
14-12-2005, 14:06
O bella questa... :)