Archive for the ‘System Programming’ Category

Mapeando Arquivos em Memória

26 de June de 2010

Depois de ilustrar algumas das características do Memory Manager sendo um provedor de serviços ao Cache Manager no post anterior, hoje vou demonstrar que meras aplicações User-Mode também podem utilizar tais serviços. Mapeando arquivos em memória a aplicação ganha um intervalo de endereços virtuais que contém o conteúdo do arquivo. O acesso ao conteúdo do arquivo se dá simplesmente desreferenciando um ponteiro, sem a necessidade de chamar as funções ReadFile() ou WriteFile().

Quer uma necessidade para isso? Imagine que sua aplicação precise fazer a busca por uma determinada string em um arquivo, digamos “DriverEntry”. Em um desevolvimento “arroz com feijão”, o handle do arquivo é obtido através da chamada à função CreateFile(), e um buffer recebe o conteúdo parcial do arquivo, vamos supor 200 bytes. Uma simples função de busca da API poderia fazer tal busca no buffer.

Essa solução seria perfeita se não houvesse a possibilidade de a palavra buscada cair nas extremidades do buffer tal como ilustrado abaixo.

Um algoritmo mais espertinho teria que ser utilizado para identificar o prefixo e continuar a busca na próxima leitura do arquivo.

Este é apenas um simples exemplo, mas que ilustra com clareza uma das vantagens de se mapear arquivos. Se houvesse uma simples função que recebesse o path de um arquivo e nos retornasse um ponteiro para o conteúdo dele,  a busca seria bem simples.

Arquivos mapeados em memória também podem facilitar a escrita em seu conteúdo. Escrevendo no ponteiro recebido por tal mapeamento, o Memory Manager vai se encarregar de fazer o I/O necessário para que este novo conteúdo chegue ao disco.

Uma simples função de mapeamento

Aqui vou exemplificar o uso das rotinas que mapeiam um arquivo em memória. Os comentários seguem na explicação.

/****
***     MapFileToMemory
**
**      Rotina que recebe o path de um arquivo que será
**      mapeado em memória para leitura. Um endereço é
**      retornado à rotina chamadora bem como o tamanho
**      do arquivo.
*/
 
DWORD MapFileToMemory(LPCTSTR   tzFileName,
                      LPVOID*   ppMemory,
                      LPDWORD   pdwSize)
{
    HANDLE  hFile = NULL,
            hMapping = NULL;
    DWORD   dwError = ERROR_SUCCESS;
 
    __try
    {
        __try
        {
            //-f--> Zera variáveis de saída.
            *pdwSize = NULL;
            *ppMemory = NULL;
 
            //-f--> Aqui abrimos o arquivo a ser mapeado
            hFile = CreateFile(tzFileName,
                               GENERIC_READ,
                               FILE_SHARE_READ | FILE_SHARE_DELETE,
                               NULL,
                               OPEN_EXISTING,
                               FILE_ATTRIBUTE_NORMAL,
                               NULL);
 
            //-f--> Vericamos se o arquivo foi aberto, senão
            //      o homem do saco vem e nos leva.
            if (hFile == INVALID_HANDLE_VALUE)
                RaiseException(GetLastError(),
                               0,
                               0,
                               NULL);
 
            //-f--> Embora o tamanho do arquivo não seja necessário
            //      nesta função, vamos aproveitar que temos o handle
            //      do arquivo em mãos para obter essa informação para
            //      a rotina chamadora que vai precisar dela.
            *pdwSize = GetFileSize(hFile,
                                   NULL);
 
            //-f--> Aqui criamos um mapeamento do arquivo.
            //      Em kernel seria o equivalente a se criar uma
            //      section do arquivo.
            hMapping = CreateFileMapping(hFile,
                                         NULL,
                                         PAGE_READONLY,
                                         0,
                                         0,
                                         NULL);
 
            //-f--> Prevenindo o homem do saco.
            if (!hMapping)
                RaiseException(GetLastError(),
                               0,
                               0,
                               NULL);
 
            //-f--> Aqui sim o mapeamento é feito e ganhamos o
            //      intervalo de endereços que conterá o conteúdo
            //      do arquivo.
            *ppMemory = MapViewOfFile(hMapping,
                                      FILE_MAP_READ,
                                      0,
                                      0,
                                      0);
 
            //-f--> A mesma treta do saco que já comentei.
            if (!*ppMemory)
                RaiseException(GetLastError(),
                               0,
                               0,
                               NULL);
        }
        __finally
        {
            //-f--> Aqui é onde faremos toda a faxina
            //      fechando os handles que foram abertos.
            if (hFile)
                CloseHandle(hFile);
 
            if (hMapping)
                CloseHandle(hMapping);
        }
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //-f--> Oops! Alguma coisa não saiu como foi ensaiado.
        //      Encontre um culpado e faça de conta que não é com você.
        dwError = GetExceptionCode();
    }
 
    return dwError;
}
 

Este exemplo é realmente bem simples, mas sinta-se à vontade para adicionar parâmetros que tornem essa função mais flexível e complexa.

“Fernando, mesmo que o arquivo tenha sido mapeado com succeso, você fecha o handles do arquivo e do mapeamento. Isso não deveria liberar as refências que este programa tem com o arquivo?”

Na verdade, depois de criarmos o mapeamento de arquivo utilizando a rotina CreateFileMapping() que recebe o handle do arquivo, uma referência extra já foi feita ao arquivo e assim já poderíamos fechar o handle dele se quisessemos. O mesmo acontece com a chamada da rotina MapViewOfFile(), que recebe o handle do mapeamento, e que por sua vez possui uma referência indireta ao arquivo mapeado. Ou seja, depois de tudo mapeado podemos fechar todos os handles e deixar as referências indiretas tomarem conta disso.

No próximo código fonte veremos um exemplo simples de utilização dessa função.

/****
***     _tmain
**
**      Simples utilização da função de mapeamento de arquivo.
**      Só pra não dizer que não fiz tudim tudim...
*/
 
int _tmain(int argc, _TCHAR* argv[])
{
    PBYTE   pBuffer;
    DWORD   dwError, dwSize, i;
 
    //-f--> Passa o nome do arquivo e obtém o ponteiro
    //      com seu conteúdo mapeado. Simples assim...
    dwError = MapFileToMemory(_T("C:\\Temp\\Test.txt"),
                              (LPVOID*)&pBuffer,
                              &dwSize);
 
    //-f--> Testar erro nunca é demais.
    if (dwError == ERROR_SUCCESS)
    {
        //-f--> Momentos de suspense antes de tocar o endereço.
        printf("Hit any key to access the buffer at 0x%p...\n", pBuffer);
        _getch();
 
        //-f--> Imprime cada caractere armzenado no arquivo.
        //      "Olha mamãe! Sem o ReadFile()!"
        for (i=0; i
            printf("%c", pBuffer[i]);
 
        //-f--> Aqui o mapeamento é desfeito.
        UnmapViewOfFile(pBuffer);
    }
 
    return dwError;
}
 

Ao final deste exemplo podemos observar a chamada à rotina UnmapViewOfFile(), que recebe simplesmente o ponteiro base do mapeamento do arquivo. Com essa chamada, todas as referências internas são desfeitas e o arquivo finalmente é fechado.

Testando o brinquedo

Para que possamos fazer um teste besta, crie um arquivo texto utilizando o Notepad.exe.

Rodando a aplicação de teste temos a saída como ilustrada abaixo.

Agora na câmera lenta do replay

Com o WinDbg podemos observar o exato momento em que a aplicação acessa o intervalo de endereços referente ao conteúdo do arquivo. Para isso vamos colocar um breakpoint na rotina de leitura de arquivo do driver Ntfs.sys, desta forma poderemos ver a requisição do Memóry Manager ser atendida. Para que isso aconteça, o arquivo texto não pode estar no cache do sistema, então se você já rodou a aplicação de teste ao menos uma vez, você deverá reiniciar o sistema.

Caso você ainda não tenha utilizado o WinDbg e não sabe como conectá-lo ao sistema, então leia este post para um quick start. Depois de conectar o WinDbg ao Kernel do sistema, vá até o diretório onde está a aplicação de teste e a execute, mas ainda não pressione qualquer tecla deixando-a parada como mostra a seguir:

Depois disso pressione Ctrl+Break no WinDbg para que você adquira o controle sobre o sistema depurado, que neste momento vai permanecer congelado.

Para colocar o tal breakpoint na rotina de leitura de arquivo do Ntfs.sys, precisaremos saber onde está essa rotina dentro do driver. Podemos obter essa informação utilizando a extenção !drvobj do WinDbg como exibido abaixo.

kd> !drvobj \FileSystem\Ntfs 2
Driver object (843fd650) is for:
 \FileSystem\Ntfs
DriverEntry:   828f5b75    Ntfs!GsDriverEntry
DriverStartIo: 00000000    
DriverUnload:  00000000    
AddDevice:     00000000    
 
Dispatch routines:
[00] IRP_MJ_CREATE                      8289400a    Ntfs!NtfsFsdCreate
[01] IRP_MJ_CREATE_NAMED_PIPE           8165a013    nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE                       82896fcf    Ntfs!NtfsFsdClose
[03] IRP_MJ_READ                        82818514    Ntfs!NtfsFsdRead
[04] IRP_MJ_WRITE                       82815638    Ntfs!NtfsFsdWrite
[05] IRP_MJ_QUERY_INFORMATION           82895a88    Ntfs!NtfsFsdDispatchWait
[06] IRP_MJ_SET_INFORMATION             8281e950    Ntfs!NtfsFsdSetInformation
[07] IRP_MJ_QUERY_EA                    82895a88    Ntfs!NtfsFsdDispatchWait
[08] IRP_MJ_SET_EA                      82895a88    Ntfs!NtfsFsdDispatchWait
[09] IRP_MJ_FLUSH_BUFFERS               82884349    Ntfs!NtfsFsdFlushBuffers
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    828b5fc6    Ntfs!NtfsFsdDispatch
[0b] IRP_MJ_SET_VOLUME_INFORMATION      828b5fc6    Ntfs!NtfsFsdDispatch
[0c] IRP_MJ_DIRECTORY_CONTROL           828b5d41    Ntfs!NtfsFsdDirectoryControl
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         8289970e    Ntfs!NtfsFsdFileSystemControl
[0e] IRP_MJ_DEVICE_CONTROL              82879466    Ntfs!NtfsFsdDeviceControl
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     8165a013    nt!IopInvalidDeviceRequest
[10] IRP_MJ_SHUTDOWN                    8282b36b    Ntfs!NtfsFsdShutdown
[11] IRP_MJ_LOCK_CONTROL                82823b7a    Ntfs!NtfsFsdLockControl
[12] IRP_MJ_CLEANUP                     828a1d42    Ntfs!NtfsFsdCleanup
[13] IRP_MJ_CREATE_MAILSLOT             8165a013    nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY              828b5fc6    Ntfs!NtfsFsdDispatch
[15] IRP_MJ_SET_SECURITY                828b5fc6    Ntfs!NtfsFsdDispatch
[16] IRP_MJ_POWER                       8165a013    nt!IopInvalidDeviceRequest
[17] IRP_MJ_SYSTEM_CONTROL              8165a013    nt!IopInvalidDeviceRequest
[18] IRP_MJ_DEVICE_CHANGE               8165a013    nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA                 82895a88    Ntfs!NtfsFsdDispatchWait
[1a] IRP_MJ_SET_QUOTA                   82895a88    Ntfs!NtfsFsdDispatchWait
[1b] IRP_MJ_PNP                         8286137b    Ntfs!NtfsFsdPnp
 
Fast I/O routines:
FastIoCheckIfPossible                   8288187b    Ntfs!NtfsFastIoCheckIfPossible
FastIoRead                              82880c38    Ntfs!NtfsCopyReadA
FastIoWrite                             82881f53    Ntfs!NtfsCopyWriteA
FastIoQueryBasicInfo                    82888c3a    Ntfs!NtfsFastQueryBasicInfo
FastIoQueryStandardInfo                 82888aa6    Ntfs!NtfsFastQueryStdInfo
FastIoLock                              8287bf41    Ntfs!NtfsFastLock
FastIoUnlockSingle                      8287bd75    Ntfs!NtfsFastUnlockSingle
FastIoUnlockAll                         828cd7b3    Ntfs!NtfsFastUnlockAll
FastIoUnlockAllByKey                    828cd958    Ntfs!NtfsFastUnlockAllByKey
ReleaseFileForNtCreateSection           8281e904    Ntfs!NtfsReleaseForCreateSection
FastIoQueryNetworkOpenInfo              8287ad84    Ntfs!NtfsFastQueryNetworkOpenInfo
AcquireForModWrite                      8280c892    Ntfs!NtfsAcquireFileForModWrite
MdlRead                                 828cd0d8    Ntfs!NtfsMdlReadA
MdlReadComplete                         81650af6    nt!FsRtlMdlReadCompleteDev
PrepareMdlWrite                         828cd31f    Ntfs!NtfsPrepareMdlWriteA
MdlWriteComplete                        817f5a9a    nt!FsRtlMdlWriteCompleteDev
FastIoQueryOpen                         82874d03    Ntfs!NtfsNetworkOpenCreate
AcquireForCcFlush                       8281ab35    Ntfs!NtfsAcquireFileForCcFlush
ReleaseForCcFlush                       8281aa9c    Ntfs!NtfsReleaseFileForCcFlush
 

Como você deve estar imaginando, a rotina de leitura do Ntfs é utilizada com muita frequência, o que faria este breakpoint parar muitas vezes sem ter a menor relação com o nosso teste. Para limitar o escopo do breakpoint, vamos fazer com que ele se aplique somente à thread que fará a solicicação que estamos esperando.

Como já expliquei no post anterior, quando o endereço de memória é obtido, o arquivo ainda não foi lido. Quando a aplicação desreferenciar este ponteiro buscando os dados, um page fault será gerado e o Memory Manager vai tomar o controle sobre a thread por meio de um trap de sistema. Essa é a thread que será utilizada para realizar a leitura do arquivo que vai abastecer a página de memória à pedido do Memory Manager. Esta é a razão pela qual nosso programa de teste espera uma tecla ser pressionada antes de acessar o buffer. Isso nos dá a oportunidade de obter a identificação da thread que está aguardando esse evento.

Utilizamos a extenção !process para localizar o nosso programa de teste e também listará suas threads, que em nosso caso é uma única.

kd> !process 0 2 MapFile.exe
PROCESS 84bf6d90  SessionId: 1  Cid: 0b60    Peb: 7ffdf000  ParentCid: 0b40
    DirBase: 1f09b4c0  ObjectTable: 8e77ccd0  HandleCount:   5.
    Image: MapFile.exe
 
        THREAD 84f6cb50  Cid 0b60.0b64  Teb: 7ffde000 Win32Thread: 00000000 WAIT: (WrLpcReply) ...
            84f6cd64  Semaphore Limit 0x1
 
kd> bp /1 /t 84f6cb50 Ntfs!NtfsFsdRead
kd> g
 

Depois de colocado o breakpoint, podemos liberar a execução do sistema e teclar algo na aplicação de teste. Isso fará com que nosso breakpoint interrompa o sistema bem como pretendíamos. Olhando para a pilha de chamadas que temos no momento, podemos evidenciar a execução do trap que foi gerado pela aplicação de teste. Este trap está sendo atendido pelo Memory Manager.

Breakpoint 0 hit
Ntfs!NtfsFsdRead:
82818514 6a40
 
kd> kb
ChildEBP RetAddr  Args to Child
8f395b6c 816f00c3 84438020 84f40290 84f40290 Ntfs!NtfsFsdRead
8f395b84 821a3ba7 84437730 84f40290 00000000 nt!IofCallDriver+0x63
8f395ba8 821a3d64 8f395bc8 84437730 00000000 fltmgr!FltpLegacyProcessingAfterPreCallbacksCompleted+0x251
8f395be0 816f00c3 84437730 84f40290 00000000 fltmgr!FltpDispatch+0xc2
8f395bf8 8167bf2e 84f6cb50 8443a18c 8443a158 nt!IofCallDriver+0x63
8f395c14 816b8d51 00000043 84f6cb50 8443a198 nt!IoPageRead+0x172
8f395cd0 816db03f 00020000 90825810 00000000 nt!MiDispatchFault+0xd18
8f395d4c 8168ebf4 00000000 00020000 00000001 nt!MmAccessFault+0x1fb7
8f395d4c 0018d972 00000000 00020000 00000001 nt!KiTrap0E+0xdc
0015fbc0 0018ec36 00000001 00281a28 00281a78 MapFile!wmain+0x72
0015fc0c 0018eb0f 0015fc20 77554911 7ffdf000 MapFile!__tmainCRTStartup+0x116
0015fc14 77554911 7ffdf000 0015fc60 77ace4b6 MapFile!wmainCRTStartup+0xf
0015fc20 77ace4b6 7ffdf000 77a26775 00000000 kernel32!BaseThreadInitThunk+0xe
0015fc60 77ace489 0018b532 7ffdf000 00000000 ntdll!__RtlUserThreadStart+0x23
0015fc78 00000000 0018b532 7ffdf000 00000000 ntdll!_RtlUserThreadStart+0x1b
 

Como conhecemos o protótipo que uma rotina de dispatch precisa ter, sabemos que o segundo parâmetro da rotina NtfsFsdRead() é o endereço da IRP que o driver recebeu. Utilizando a extensão !irp podemos obter detalhes sobre a IRP. Isso nos permite conhecer o FileObject ao qual está destinado essa solicitação.

kd> !irp 84f40290 
Irp is active with 8 stacks 8 is current (= 0x84f403fc)
 Mdl=8443a1d8: No System Buffer: Thread 84f6cb50:  Irp stack trace.  
     cmd  flg cl Device   File     Completion-Context
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
>[  3, 0]   0  0 84438020 84bd0028 00000000-00000000    
           \FileSystem\Ntfs
            Args: 00001000 00000000 00000000 00000000
 

Com o endereço do FileObject em mãos, a extensão !fileobj nos mostrará mais detalhes sobre esse objeto. Assim podemos verificar que de fato o arquivo a ser lido é o nosso arquivo texto que foi mapeado.

kd> !fileobj 84bd0028 
 
\Temp\Test.txt
 
Device Object: 0x84439030   \Driver\volmgr
Vpb: 0x84436e28
Access: Read SharedRead SharedDelete 
 
Flags:  0x44042
    Synchronous IO
    Cache Supported
    Cleanup Complete
    Handle Created
 
FsContext: 0x92a4cd80    FsContext2: 0x92a4ced8
CurrentByteOffset: 0
Cache Data:
  Section Object Pointers: 84d24e74
  Shared Cache Map: 00000000
 

Sabendo que estamos no contexto da thread que fez o acesso, podemos dar uma espiadinha no endereço acessado antes do page fault ser atendido. Esse endereço foi exibido na saída da aplicação de teste antes o breakpoint interromper a execução do sistema.

kd> db 0x00020000
00020000  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020010  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020020  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020030  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020040  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020050  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020060  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020070  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
 

“Fernando, por que aparecem sinais de interrogação? Tudo bem que o conteúdo do arquivo ainda não foi copiado para o buffer da aplicação, mas não deveríamos ver lixo ou mesmo zeros?”

Olha, essa sua pergunta foi realmente muito boa, acho que eu mesmo não poderia ter pensado em uma pergunta melhor. Na verdade a resposta para essa pergunta está vinculada àquela reposta tosca do post anterior. O que acontece é que um intervalo de endereços foi reservado para conter as páginas de memória com o conteúdo do arquivo. Como nenhum acesso ainda foi feito, o esse endereço virtual ainda não aponta para nenhuma página física de memória. Sem essa página física não se pode determinar seus dados. Podemos verificar isso utilizando a extenção !vtop do WinDbg, que faz a tradução de endereços virtuais para endereços físicos.

kd> !vtop 0 0x00020000
X86VtoP: Virt 00020000, pagedir 1f09b4c0
X86VtoP: PAE PDPE 1f09b4c0 - 000000001876d801
X86VtoP: PAE PDE 1876d000 - 0000000018908867
X86VtoP: PAE PTE 18908100 - ffffffff00000420
X86VtoP: Virt ffffffff, pagedir 1f09b4c0
X86VtoP: PAE PDPE 1f09b4d8 - 00000000187b0801
X86VtoP: PAE PDE 187b0ff8 - 0000000000128063
X86VtoP: PAE PTE 128ff8 - 0000000000000000
X86VtoP: PAE zero PTE
Virtual address 20000 translation fails, error 0x8007001E.
 
kd> !error 0x8007001E
Error code: (HRESULT) 0x8007001e (2147942430) - The system cannot read from the specified device.
 

A tentativa de tradução desse endereço virtual resulta em um erro. Vamos tentar fazer essa tradução novamente depois que o page fault for atendito. Vamos liberar a execução do sistema até o endereço de retorno para a rotina MmAccessFault(). Este endereço foi obtido na pilha de chamadas da thread e foi destacado nos resultados do comando kd já ilustrado acima.

kd> ga 8168ebf4 
 
nt!KiTrap0E+0xdc:
8168ebf4 85c0            test    eax,eax
 
kd> !vtop 0 0x00020000
X86VtoP: Virt 00020000, pagedir 1f09b4c0
X86VtoP: PAE PDPE 1f09b4c0 - 000000001876d801
X86VtoP: PAE PDE 1876d000 - 0000000018908867
X86VtoP: PAE PTE 18908100 - 8000000018844025
X86VtoP: PAE Mapped phys 18844000
Virtual address 20000 translates to physical address 18844000.
 

Aqui o page fault já foi antendido e o controle será devolvido à aplicação. Neste ponto o Memory Manager realizou as tarefas necessárias para que esse endereço virtual agora pudesse ser traduzido para uma página física. Repetindo a mesma tentativa de tradução que falhou anteriormente, teremos a seguinte saída.

kd> db 0x00020000
00020000  54 65 73 74 65 20 64 65-20 6d 61 70 65 61 6d 65  Teste de mapeame
00020010  6e 74 6f 20 64 65 20 61-72 71 75 69 76 6f 2e 2e  nto de arquivo..
00020020  2e 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020030  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020040  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020050  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020060  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020070  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

Liberando a execução do sistema, a aplicação fará o acesso ao buffer e teremos a mesma saída exibida anteriormente. Vale lembrar que para repetir essa experiência, é necessário reiniciar o sistema, pois o conteúdo do arquivo texto agora está no cache do sistema. Isso significa que o page fault não ocorrerá novamente até que esta página seja descartada pelo Cache Manager. Tal evento depende de muitos fatores e pode não acontecer até que a máquina desligue.

Enfim, este post além de trazer uma simples função de mapeamento de arquivo, também traz o mesmo blá-blá-blá técnico de sempre. Espero que tenham gostado.
Até mais! 😉

MapFile.zip

Enumerando dispositivos

1 de May de 2009

Dentre minhas tarefas atuais, estava a de ler as amostras de um giroscópio utilizando uma porta serial e gerar um arquivo com elas. Este arquivo seria lido por um driver semelhante ao demonstrado no post anterior a este. No datasheet do giroscópio há uma descrição do protocolo, sem falar do exemplo feito em Visual Basic que existe no site do fabricante, que demonstra a aceleração angular em cada eixo bem como as acelerações lineares deles como mostra abaixo.


Toda a interface de configuração e obtenção dos dados do giroscópio é realizada através da serial. Mesmo sem uma aplicação dedicada pode-se utilizar um programa qualquer, como o HyperTerminal, para ter acesso aos menus oferecidos pelo dispositivo. Utilizando uma porta serial, qualquer kit de microcontrolador que não tenha display e nem mesmo um teclado pode oferecer uma interface bem amigável.

Escrever protocolos seriais não é algo que me assuste. Duro é ter que lembrar como programar uma aplicação User-Mode que tenha janelas, botões e outros controles, mas nada que um pouco de MSDN não resolva. Programadores de drivers normalmente testam tudo que podem utilizando aplicações console, então desenvolvi um pequeno software de terminal para lidar diretamente com o dispositivo. O fonte dele segue abaixo e fica de brinde para quem precisar brincar com portas seriais qualquer dia.

/****
***     main
**
**      E lá vamos nós...
*/
int _tmain(int argc, _TCHAR* argv[])
{
    DWORD           dwBytes,
                    dwError = ERROR_SUCCESS;
    HANDLE          hCom;
    ULONG           i;
    UCHAR           ucByteIn[512], ucByteOut;
    DCB             dcb;
    COMMTIMEOUTS    CommTimeouts;
 
    //-f--> Aqui abrimos aporta serial desejada
    hCom = CreateFile(L"COM3",
                      GENERIC_READ | GENERIC_WRITE,
                      0,
                      NULL,
                      OPEN_EXISTING,
                      0,
                      NULL);
 
    //-f--> Verifica se obtivemos sucesso
    if (hCom == INVALID_HANDLE_VALUE) 
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf("CreateFile failed with error %d.\n",
                dwError);
        return dwError;
    }
 
    //-f--> Existem 7 kilos de configurações da porta serial.
    //      Ao invés de configirar cada uma delas, vamos apenas
    //      obter as configurações padrão do sistema e modificar
    //      apenas as que nos interessa.
    if (!GetCommState(hCom, &dcb))
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf ("GetCommState failed with error %d.\n",
                dwError);
        CloseHandle(hCom);
        return dwError;
    }
 
    //-f--> Aqui modificamos as configurações para setar as
    //      configurações exigidas pelo giroscópio.
    //      57600, 8 N 1 (sem controle de fluxo)
    dcb.DCBlength = sizeof(DCB);
    dcb.BaudRate = CBR_57600;
    dcb.ByteSize = 8;
    dcb.Parity = NOPARITY;
    dcb.StopBits = ONESTOPBIT;
    dcb.fDtrControl = DTR_CONTROL_DISABLE;
    dcb.fRtsControl = RTS_CONTROL_DISABLE;
 
    //-f--> Aqui aplicamos as configurações que modificamos
    if (!SetCommState(hCom, &dcb))
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf ("SetCommState failed with error %d.\n",
                dwError);
        CloseHandle(hCom);
        return dwError;
    }
 
    //-f--> Vou configurar os timeouts de forma que uma
    //      leitura seja completada depois de 100ms mesmo
    //      nenhum caracter seja recebido pela aplicação.
    //      Isso evita da aplicação ficar travada em ReadFile
    //      até que um byte seja recebido pela porta serial
    CommTimeouts.ReadIntervalTimeout = 0;
    CommTimeouts.ReadTotalTimeoutMultiplier = 0;
    CommTimeouts.ReadTotalTimeoutConstant = 100;
    CommTimeouts.WriteTotalTimeoutConstant = 0;
    CommTimeouts.WriteTotalTimeoutMultiplier = 0;
 
    //-f--> Aplica as configurações de timeouts
    if (!SetCommTimeouts(hCom, &CommTimeouts))
    {
        //-f--> Oops!
        dwError = GetLastError();
        printf ("SetCommTimeouts failed with error %d.\n",
                dwError);
        CloseHandle(hCom);
        return dwError;
    }
 
    //-f--> Aqui começa o loop infinito que vai executar
    //      para todo o sempre até o fim dos dias.
    //      Na verdade, se você teclar [ESC] ele termina.
    while(1)
    {
        //-f--> Verifica se há um byte a ser recebido pela
        //      aplicação no buffer de teclado.
        if (_kbhit())
        {
            //-f--> Obtém a tecla
            ucByteOut = _getch();
 
            //-f--> Verifica se a tecla recebida é [ESC]
            if (ucByteOut == 27)
                break;
 
            //-f--> Envia o byte recebido para a porta serial
            if (!WriteFile(hCom,
                           &ucByteOut,
                           1,
                           &dwBytes,
                           NULL))
            {
                //--> Oops!
                dwError = GetLastError();
                break;
            }
        }
 
        //-f--> Verifica se algum byte foi recebido pela
        //      porta serial. Observe que neste loop não
        //      existe o típico Sleep() para não levar a CPU
        //      a 100%. Esta espera é realizada dentro da
        //      chamada à ReadFile(). A função espera por um
        //      byte por até 100ms, conforme configurado nos
        //      timeouts.
        if (!ReadFile(hCom,
                      ucByteIn,
                      sizeof(ucByteIn),
                      &dwBytes,
                      NULL))
        {
            //-f--> Oops!
            dwError = GetLastError();
            break;
        }
 
        //-f--> Imprime na tela a sequência de caracteres recebida
        for (i=0; i
        {
            if (ucByteIn[i] == 0x0d)
                puts("");
            else
                printf("%c", ucByteIn[i]);
        }
    }
 
    //-f--> Bom se chegamos neste ponto é porque chegamos
    //      ao fim dos dias ou a tecle ESC foi pressionada.
    //      Vamos fechar a porta serial e sair correndo.
    CloseHandle(hCom);
    return dwError;
}

Abrir uma porta serial é a parte mais simples desta história, o problema é decidir qual porta abrir. Como qualquer programa decente, deveria haver um combo list com as portas seriais disponíveis no computador, onde o usuário escolheria uma e pronto. A partir daí é só obter a porta selecionada pelo usuário e montar uma chamada para a rotina CreateFile() como ilustrado no cógido acima.

Tudo bem, arrastei o controle para a janela que eu estava programando e agora é só preenchê-lo. Então pensei: "Deve haver alguma função do tipo EnumerateCommPorts() na API", mas não foi o que a página da referência me mostrava. - Como assim não tem? O Google deve saber algo a respeito. - Acabei descobrindo que esta é uma dúvida bem comum por aí. Uns resolvem este problema fazendo um loop que tenta abrir as portas seriais em sequência (COM1, COM2,... ), outros utilizam a função QueryDosDevice() e filtram os Symbolic Links que iniciam com "COM(n)", mas o método que vou mostrar aqui é capaz de enumerar qualquer tipo de interface utilizando a SetupAPI.

Setup quem?

A SetuAPI é uma parte do Plug-And-Play que fornece serviços às aplicações User-Mode. O Plug-And-Play tem como um dos seus objetivos, unificar a configuração, uso e a enumeração de dispositivos e serviços semelhantes. Desta forma todos os fabricantes de placas que oferecem serviços de porta serial podem ter seus dispositivos configurados de uma única maneira. Dispositivos que oferecem serviços de porta serial devem implementar uma interface pré-definida, ou seja, devem se mostrar dispostos a receber IOCTLs e responder a eles de maneira prevista na documentação.

O driver que desejar criar devices que implementem a interface de porta serial deverá declarar isso através da chamada à rotina IoRegisterDeviceInterface(). Aqui um device é associado à uma classe de interface de dispositivo, a qual é identificada por um GUID. Existem diversas classes de interfaces de dispositivos pré-definidas no sistema, como listado aqui, e a classe de portas seriais é uma delas. A partir daí, seu device será enumerado por rotinas do Plug-and-Play como provedor de uma determinada interface.

Então tá. O que temos que fazer é utilizar estas funções de enumeração de interfaces para descobrir quais os dispositivos que implementam a interface de porta serial. A SetupAPI vai nos ajudar com essa tarefa. Mas antes de darmos uma olhada no fonte, vamos resolver uma coisinha. Tenho visto diferentes maneiras de usar GUID_DEVINTERFACE_COMPORT, uns incluem o header de Kernel-Mode Ntddser.h, outros definem o GUID na unha, mas qual seria o jeito correto?

Definindo o GUID de interface

Tudo começa com a chamada à rotina SetupDiGetClassDevs() que vai reunir informações sobre o grupo de dispositivos que correspondem aos critérios de busca adotados nos parâmetros. Vamos querer dispositivos que implementem a interface identificada por GUID_DEVINTERFACE_COMPORT, mas se simplesmente fizermos a chamada como mostra abaixo...

hDevInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT,
                                  NULL,
                                  NULL,
                                  DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);

...obteremos o erro exibido em seguida.

1>z:\sources\samples\enumserialport.obj : error LNK2001: unresolved external
 symbol _GUID_DEVINTERFACE_COMPORT

Isso ocorre porque GUID_DEVINTERFACE_COMPORT está declarado em WinIoCtl.h. Lembre-se que este header é indiretamente incluído por Windows.h quando o símbolo WIN32_LEAN_AND_MEAN não é definido, como já comentei neste outro post. Mas mesmo incluindo este header ainda temos o mesmo problema. Vamos olhar isso um pouco mais de perto.

No header WinIoCtl.h temos:

DEFINE_GUID(GUID_DEVINTERFACE_COMPORT, 0x86e0d1e0L, 0x8089, 0x11d0,
            0x9c, 0xe4, 0x08, 0x00, 0x3e, 0x30, 0x1f, 0x73);

Mas o que é DEFINE_GUID afinal?

Em GuidDef.h temos:

#ifdef INITGUID
#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
        EXTERN_C const GUID DECLSPEC_SELECTANY name \
                = { l, w1, w2, { b1, b2,  b3,  b4,  b5,  b6,  b7,  b8 } }
#else
#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \
    EXTERN_C const GUID FAR name
#endif // INITGUID

Então agora nos vem o sonoro "Aaaah táaa!". GUID_DEVINTERFACE_COMPORT é uma constante que é definida quando o símbolo INITGUID é definido, caso contrário, esta constante é apenas declarada. O símbolo INITGUID é definido no header InitGuid.h. Assim, quando você quiser utilizar os GUIDs declarados com DEFINE_GUID, você terá que em um dos seus módulos incluir o header Initguid.h antes de Windows.h. Como nosso exemplo só tem um módulo, então fica fácil. Como de costume, todo código fonte está contido num exemplo disponível para download.

//-f--> Vamos ter que colocar estes includes na ordem certa
//      para que o GUID que identifica a interface
//      GUID_DEVINTERFACE_COMPORT seja definido.
#include 
#include 
#include 

Enumerando interfaces

/****
***     EnumSerialInterfaces
**
**      Rotina que enumera dispositivos que
**      implementam a interface de porta serial
*/
 
DWORD EnumSerialInterfaces(void)
{
    CHAR                        szFriendlyName[100];
    HDEVINFO                    hDevInfoSet = NULL;
    SP_DEVICE_INTERFACE_DATA    DevInterfaceData;
    SP_DEVINFO_DATA             DevInfoData;
    DWORD                       dwReturn,
                                dwInterfaceIndex = 0;
    try
    {
        //-f--> Reunindo informações sobre dispositivos que implementam
        //      a interface desejada que estejam presentes no
        //      momento em que esta rotina é chamada.
        hDevInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT,
                                          NULL,
                                          NULL,
                                          DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
 
        if (hDevInfoSet == INVALID_HANDLE_VALUE)
            throw GetLastError();
 
        DevInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
 
        //-f--> Agora enumera cada uma das interfaces
        while (SetupDiEnumDeviceInterfaces(hDevInfoSet,
                                           0,
                                           &GUID_DEVINTERFACE_COMPORT,
                                           dwInterfaceIndex++,
                                           &DevInterfaceData))
        {
            DevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
 
            //-f--> Para cada uma das interfaces, obtemos o
            //      device que a implementa.
            if (SetupDiGetDeviceInterfaceDetail(hDevInfoSet,
                                                &DevInterfaceData,
                                                NULL,
                                                0,
                                                NULL,
                                                &DevInfoData))
                throw GetLastError();
 
            //-f--> Agora apenas obtemos o nome camarada do dispositivo
            if (!SetupDiGetDeviceRegistryProperty(hDevInfoSet,
                                                  &DevInfoData,
                                                  SPDRP_FRIENDLYNAME,
                                                  NULL,
                                                  (PBYTE)szFriendlyName,
                                                  sizeof(szFriendlyName),
                                                  NULL))
                throw GetLastError();
 
            //-f--> Printf neles...
            printf("%d) %s\n",
                   dwInterfaceIndex,
                   szFriendlyName);
        }
    }
    catch(DWORD dwError)
    {
        //-f--> Oops!
 
        printf("Error %d on trying enumerate device interfaces.\n",
               dwError);
 
        dwReturn = dwError;
    }
 
    //-f--> Libera as informações obtidas
    if (hDevInfoSet)
        SetupDiDestroyDeviceInfoList(hDevInfoSet);
 
    return dwReturn;
}

Com essa implementação, teremos a seguinte saída.


Legal, mas não era bem isso...

Muito bem. As portas foram enumeradas, mas como eu passaria uma string dessas para a função CreateFile()? Terei que ficar interpretando essa string para pegar a parte "COM1" que está entre parênteses?

Na verdade existem meios de você obter o Symbolic Link dos dispositivos também utilizando funções da SetupAPI, mas eu imagino que o que você queria é o mesmo que eu quero. Preencher um Combo Box com o nome simples das portas seriais, como qualquer programa normal.

O port name, que é a string "COMx" que estamos procurando, está escrito como um valor de registro na chave do dispotivo. Todo driver de porta serial tem que ter esse valor conforme mostra esta página. Para obter a chave de registro referente ao dispositivo, utilizaremos uma outra rotina da SetupAPI. O fonte abaixo vai enumerar as portas seriais da maneira que estamos querendo.

/****
***     EnumSerialPorts
**
**      Rotina que enumera dispositivos que
**      implementam a interface de porta serial e
**      imprime um nome que não é camarada mas que
**      ainda servem para alguma coisa.
*/
 
DWORD EnumSerialPorts(void)
{
    CHAR                        szPortName[10];
    HDEVINFO                    hDevInfoSet = NULL;
    SP_DEVICE_INTERFACE_DATA    DevInterfaceData;
    SP_DEVINFO_DATA             DevInfoData;
    DWORD                       dwReturn,
                                dwSize,
                                dwInterfaceIndex = 0;
    HKEY                        hKey;
 
    try
    {
        //-f--> Reunindo informações sobre dispositivos que implementam
        //      a interface desejada que estejam presentes no
        //      momento em que esta rotina é chamada.
 
        hDevInfoSet = SetupDiGetClassDevs(&GUID_DEVINTERFACE_COMPORT,
                                          NULL,
                                          NULL,
                                          DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
 
        if (hDevInfoSet == INVALID_HANDLE_VALUE)
            throw GetLastError();
 
        DevInterfaceData.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA);
 
        //-f--> Agora enumera cada uma das interfaces
        while (SetupDiEnumDeviceInterfaces(hDevInfoSet,
                                           0,
                                           &GUID_DEVINTERFACE_COMPORT,
                                           dwInterfaceIndex++,
                                           &DevInterfaceData))
        {
            DevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
 
            //-f--> Para cada uma das interfaces, obtemos o
            //      device que a implementa.
            if (SetupDiGetDeviceInterfaceDetail(hDevInfoSet,
                                                &DevInterfaceData,
                                                NULL,
                                                0,
                                                NULL,
                                                &DevInfoData))
                throw GetLastError();
 
            //-f--> Aqui obtemos a chave de registro do
            //      device que implementa a interface.
            hKey = SetupDiOpenDevRegKey(hDevInfoSet,
                                        &DevInfoData,
                                        DICS_FLAG_GLOBAL,
                                        0,
                                        DIREG_DEV,
                                        KEY_QUERY_VALUE);
 
            if (hKey == INVALID_HANDLE_VALUE)
                throw GetLastError();
 
            //-f--> Aqui obtemos o valor PortName do Registry
            dwSize = sizeof(szPortName);
            dwReturn = RegQueryValueEx(hKey,
                                       "PortName",
                                       NULL,
                                       NULL,
                                       (LPBYTE)szPortName,
                                       &dwSize);
            RegCloseKey(hKey);
 
            if (dwReturn != ERROR_SUCCESS)
                throw dwReturn;
 
            //-f--> Printf neles...
            printf("%d) %s\n",
                   dwInterfaceIndex,
                   szPortName);
        }
    }
    catch(DWORD dwError)
    {
        //-f--> Oops!
        printf("Error %d on trying enumerate device interfaces.\n",
               dwError);
 
        dwReturn = dwError;
    }
 
    //-f--> Libera as informações obtidas
    if (hDevInfoSet)
        SetupDiDestroyDeviceInfoList(hDevInfoSet);
 
    return dwReturn;
}

Agora sim...


Colocar isso num combo box já é assunto para um outro blog. Até que para um desenvolvedor de drivers, a janela abaixo não está tão ruim assim.


Até mais! 😉

EnumSerialPort.zip

Uma pitada de Memória Virtual

11 de September de 2008

Agora vamos deixar de conversinha mole e vamos logo ao que interessa. O exemplo que descrevi em outro post mostra como implementar um driver bem simples que armazena uma lista de strings que são enviadas ao driver através de operações de escrita (WriteFile), e as mesmas strings são retornadas em subseqüentes operações de leitura (ReadFile). Esse é um bom exemplo de como os dados das aplicações vão para os drivers e vice-versa. Existem três maneiras de acessar os dados oferecidos por aplicações a partir de um driver. Neste post vamos dar uma olhadinha em memória virtual para que possamos ter uma base para posts futuros, onde poderei explicar as diferenças entre aquelas tais três maneiras. Vamos nos ater a apenas algumas características básicas da memória virtual para que as coisas que explicarei em seguida façam mais sentido para você, mas se você quiser mais detalhes a respeito de Memória Virtual, você pode ler o Capítulo 7, “Memory Management”, do Windows Internals, ou dar uma olhadinha em uma apresentação feita pelo meu amigo Strauss.

O que é uma página de memória

Página de memória é uma unidade utilizada pelo hardware nas proteções de acesso à memória. Mais sobre proteções de páginas de memória aqui. Essa é uma unidade definida pelo hardware que ajuda a subdividir a memória em espaços menores e gerenciáveis. Apesar de existirem dois tamanhos de páginas (uma pequena de 4KB bytes, e outra grande de 4MB), toda referência ao tamanho de página vista na documentação são referentes apenas às páginas pequenas. O tamanho da página de memória varia com a plataforma de hardware. Em sistemas x86 e x64, as paginas são de 4KB enquanto que em sistemas IA64 a página é de 8KB. O tamanho da página pode ser obtido pela constante PAGE_SIZE.

Espaço de endereçamento

De uma maneira bem resumida, memória virtual é um mecanismo, que em um trabalho conjunto entre hardware e software, permite que processos que rodem no Windows tenham seu próprio espaço de endereçamento. Espaço quem? Espaço de endereçamento, ou Address Space como normalmente vimos na referência, permite que cada processo tenha uma visão privada da memória que esteja utilizando. Ou seja, um processo não pode ler ou escrever em páginas de memória de outro processo. Isso evita que um programa mal escrito possa erroneamente escrever em páginas de memória de outro processo ou mesmo em páginas do sistema operacional, comprometendo assim a estabilidade de todo o sistema. Portanto, podemos assumir que o espaço de endereçamento de um processo só é acessível pelo processo ao qual ele pertence.

Nota: É possível compartilhar memória entre processos, mas mesmo neste caso, a mesma página física de memória é referenciada por dois ou mais espaços de endereçamentos diferentes.

Memória Virtual e Memória Física

Logo após a descoberta do fogo, a memória era endereçada diretamente em modo real. Isso significa que um ponteiro utilizado por uma aplicação acessava o dado exatamente onde ele se encontrava fisicamente nos chips de memória RAM. Páginas de memória virtual associadas aos processos refletem páginas físicas de memória através de um endereçamento virtual. Os bits que compõem o endereço virtual mapeam a memória física que se deseja acessar. Ao deferenciar um ponteiro que contem um endereço virtual, o processador, de maneria transparente ao processo, consulta estruturas preenchidas pelo sistema operacional de forma a traduzir o endereço virtual em real, e dessa forma, encontra a página física onde o dado realmente se encontra.


A disposição das páginas de memória virtual não tem relação com sua disposição física. Isso significa que você quando você faz uma grande alocação de memória, que é composta por várias páginas de memória, você ganha um ponteiro que, do ponto de vista da aplicação, aponta para páginas de memória que estão linearmente distribuídas (uma seguida da outra), enquanto que as páginas de memória física podem estar todas espalhadas pelos chips de RAM.

A memória pode não estar na RAM

A memória utilizada pelas aplicações não se limita à quantidade de memória oferecida pelos chips de RAM instalados em sua placa mãe. Quando os processos, cada vez mais sedentos por memória, vão sendo executados, o espaço disponível em RAM vai sendo compartilhado entre os vários processos. Páginas de memória acessadas com menos frequência são removidas dos chips de RAM e vão para disco (para o Pagefile.sys para ser mais preciso), dando lugar à uma outra página de memória que é necessária naquele momento. Esse processo recebe o nome de paginação. Quando a aplicação finalmente acessar o dado que está naquela página agora em disco, o sistema aloca espaço em RAM física para trazer de volta a página do disco para a RAM. Isso pode resultar em outras páginas que estavam em RAM a serem paginadas para o disco.


Para as aplicações, o processo de paginação é completamente transparente. O desenvolvedor não está nem um pouco preocupado se a região de memória que o programa vai acessar está em disco ou está em RAM. É só acessar o dado e o processador junto com o gerenciador de memória resolve isso para você. Infelizmente, a história não é tão florida para os desenvolvedores de drivers. Aplicações sempre rodam em uma prioridade de thread baixa (PASSIVE_LEVEL) e ao acessar um dado que esteja em uma página em disco, a thread é interrompida pelo gerenciador de memória para que este tenha a oportunidade de acionar os drivers de File System e por conseqüência os drivers de disco, aguardar o I/O ser realizado (apesar de levar um cacalhésimo de segundo, ainda temos que esperar) e então a thread é liberada. Embora haja pessoas que não acreditem nessas coisas, a plataforma NT é completamente preemptiva. Isso significa que uma thread pode ser interrompida por outra de maior prioridade. Em Kernel Mode, uma thread pode estar em prioridades superiores à APC_LEVEL, que é a prioridade em que as paginações são realizadas. Se a thread que acessará o dado estiver em um nível de prioridade muito alta (maior ou igual a DISPATCH_LEVEL), o processador não vai conseguir fazer a paginação e uma tela azul irá aflorar aos seus olhos. Um exemplo típico de execução em alta prioridade é o tratamento de interrupções, mas não vamos nos desviar do assunto.

Uma página pode ser acessada não só por um software, mas também por hardware. Hein? Uma aplicação pode passar o ponteiro de uma região de memória para um driver, que por sua vez, irá utilizar um canal de DMA para preencher este buffer. A cópia de um buffer através do processo de DMA não utiliza ciclos de CPU para ser realizada. Preencher um buffer é algo tão simples que até um chimpanzé autista pode fazer. Assim um grupo de chimpanzés autistas engenheiros criaram um chip que faz isso, mas isso não vem ao caso agora. Este chip pode estar na placa mãe ou na própria placa controlada pelo driver. O chip é programado pelo driver e uma transferência é iniciada. Durante a cópia, nem a aplicação, nem o sistema operacional e nem mesmo o processador ficam cientes do que está acontecendo. O sistema pode então determinar que as páginas utilizadas na transferência, que não estão sendo acessadas por nenhum processo, devem ser paginadas para disco. Se isso ocorrer, o chip de DMA continuará tranferido bytes para aquele endereço físico de RAM e… Bom, já sabe né? Para evitar isso, existem meios de avisar o gerenciador de memória que uma ou mais páginas não devem ser paginadas.

Durante o desenvolvimento de um driver, você pode querer alocar um buffer que será utilizado por uma ISR (Interrupt Service Routine). Como uma ISR é executada em alta prioridade, podemos fazer uma alocação de memória solicitando um buffer que não seja paginável, e assim, garantir que o buffer obtido por esta alocação esteja sempre em RAM. Mas vá com calma, memória não paginável é um recurso escasso e deve ser utilizado com parcimônia. Caso contrário, algumas operações vão deixar de ser realizadas pelo sistema por falta de RAM, mesmo que ainda haja muita memória paginável disponível.


User Space e System Space

Adotando um sistema de 32 bits como exemplo, um ponteiro é capaz de endereçar 4 GB de memória. Destes, 2 GB são reservados para endereçar páginas privadas para cada processo, ou seja, vão compor o espaço de endereçamento privado de cada aplicação. Esta faixa, que vai de 0x00000000 ao 0x7FFFFFFF, recebe o nome de User Space e são os endereços que aplicações acessam em User Mode. Conforme vimos, esta faixa compõe o espaço de endereçamento privado de um processo, protegido contra acessos indevidos de outros processos. Um exemplo seria: Somente as threads do Processo A terão acesso ao User Space do Processo A. O mesmo endereço aponta para diferentes páginas físicas de RAM em diferentes processos, dependendo do contexto do processo que faz o acesso. Os outros 2 GB restantes do endereçamento de 32 bits são reservados ao endereçamento de páginas de sistema. A faixa de endereços de 0x80000000 ao 0xFFFFFFFF define o então chamado System Space, que diferente do User Space, endereçam páginas de memória que não são privados a um determinado processo. Isso significa que o mesmo dado (na mesma página física) pode ser acessado através do mesmo endereço virtual em diferentes processos. Um dado em System Space pode ser acessado apenas em Kernel Mode, enquanto que um dado em User Space pode ser acessado tanto em Kernel Mode quanto em User Mode.


Cagamba! Isso foi uma metralhadora de conceitos? Se você apresentar sintomas de visão turva ou vômito seguido de diarréia e tremedeira, larga a mão de ser frouxo e se prepare para os próximos posts. Caso você não tenha apresentado nenhum destes sintomas, não se preocupe, para algumas pessoas o processo é mais demorado. Em posts futuros vou explicar como os drivers acessam os endereços virtuais oferecidos pelas aplicações, como lidam com a paginação de memória e ainda testam buffers oferecidos pelas aplicações. Um driver mal escrito pode permitir que um parâmetro inválido em uma aplicação cause uma tela azul.

Por hoje chega, mas vale lembrar que o que foi apresentado aqui é apenas a ponta do iceberg. Essa pontinha é o que acredito ser o mais relevante para o desevolvimento de drivers, mas ainda existem toneladas de detalhes referentes à memória virtual.

Have fun! 🙂

Personal Gina Tabajara

8 de August de 2007

Em conseqüência à volta às aulas na faculdade, meu tempo para escrever posts já diminuiu, e mais uma vez, vocês vão ter que tolerar um post que não fala nada sobre drivers. Na verdade, foi enquanto eu escrevia um post sobre drivers que escrevi esta Gina de exemplo. Mas depois de ouvir meus amigos, Lesma e Thiago, falarem que eu deveria deixar esse tal post de forma pudesse ser aplicado ao Windows Vista. Sabendo que ginas não são mais suportadas pelo Windows Vista, então a gina acabou ficando de lado. Tadinha… Enfim, como achei que o resultado ficou no mínimo divertido, vou deixar aqui esta Gina Stub (incluindo os fontes) que permite que mudemos o título dos diálogos apresentados.

O que é uma Gina Stub?

Seria melhor ainda dizer o que é Gina. Já escrevi algumas poucas coisas a respeito de Ginas no post que fala como utilizar o SoftIce, mas de forma resumida, Gina é o compomente do sistema que implementa a interface gráfica para a autenticação dos usuários em uma estação. Quer mais resumido ainda? É a telinha de logon do sistema. A gina é responsável por receber os dados que identificam o usuário e os repassam para os componentes que podem validar a senha e gerar o token com as credenciais deste usuário. É a partir deste token que é criada a sessão onde o usuário está se logando e onde será criado seu desktop. A gina também implementa a interface que faz a troca de senha, bloqueio e o shutdown da estação. Não vou detalhar todos estes passos aqui, tudo está explicado na documentação do Platform SDK.

A gina original do Windows está implementada em na DLL de nome MsGina.dll que está no diretório System32 do Windows. Para implementar uma nova gina, você precisa criar uma nova DLL e informar ao sistema que esta DLL será a nova gina através de uma chave no registro. Mas criar uma gina não é algo tão simples assim. Já desenvolvi algumas e digamos que a documentação deixou um pouco a desejar. Pelo menos foi assim naquela época. A gina tem muitas responsabilidades e se você quer apenas complementar ou alterar algum comportamento da gina original, você poderia simplesmente criar uma gina stub. Gina Stub é uma DLL que exporta todas funções que uma gina deveria exportar, mas esta repassa as chamadas para a gina original do sistema, dando assim a opção que alterar apenas as funcionalidades desejadas.

Isso não é um tutorial

Se você quer saber como desenvolver uma gina stub e precisa de um ponto de partida, então vá até à pasta de exemplos do Platform SDK e utilize o exemplo da pasta C:\MSSDK\Samples\Security\GINA\GinaStub. O projeto que estou deixando aqui realiza alguns malabarismos para evitar código muito repetitivo e também não utiliza a C/C++ Run Time para poder ser compilada em Visual Studio 2005 e ainda assim poder rodar em Windows NT 4.0.

Instalando a Gina

Para cadastrar uma gina, você deve criar um valor chamado GinaDLL na chave Winlogon do registro, conforme mostra a figura abaixo. Esse valor é consultado pelo Winlogon.exe e caso este valor não exista, a gina padrão é carregada. O valor GinaTitle deve conter a mensagem que será exibida no título dos diálogos. Este valor não tem nada a ver com o Windows, é a nossa gina stub quem lê este valor. Junto dos arquivos fontes disponíveis para download no final deste post, existe um arquivo de script do registro que configura estas chaves para facilitar a sua vida.


Copie o arquivo Gina.dll para o diretório System32 do Windows. Certifique-se que de tudo está certo antes de reiniciar a máquina e fazer com que estas alterações tenham efeito. Caso algo esteja errado e o Winlogon.exe não consiga carregar a gina, a janela abaixo é exibida antes de qualquer coisa.


Até que o design deste MessageBox melhorou bastante a partir do Windows 2000. Se o mesmo problema acontecesse com o Windows NT 4.0, a seguinte mensagem seria exibida.

Duas dicas úteis para gina coders

Escrever ginas é a oportunidade que programadores User-Mode têm de gerar suas próprias telas azuis. Sua DLL é carregada pelo Winlogon.exe, e assim, é executada no address space deste processo. Isso significa que se você tiver uma excessão não tratada, isso vai derrubar este processo. O Winlogon é um processo crítico e não pode ser derrubado. Resumindo, tela azul.

A próxima dica é meio café com leite, mas vale a pena ser comentada. Durante o processo de desenvolvimento da gina, é natural ter vários builds e a necessidade se substituir a gina que está sendo utilizada no momento sempre aparece. Você já deve ter tentado sobrescrevê-la, mas como o Winlogon.exe está sempre com ela carregada, você não consegue apagar a gina atual. Como qualquer DLL nestas condições, você pode renomeá-la mesmo enquanto está sendo utilizada pelo Winlogon. Isso permite colocar uma nova versão da gina no diretório System32 sem ter que apagar a versão que está sendo executada no momento. Quando o sistema reiniciar, o Winlogon vai pegar a gina nova e largar a velha.

Espero que tenham gostado do brinquedinho. Agora preciso continuar aquele post.
Have fun!

TitleGina.zip

Prog2Svc – Serviço sem trabalho

2 de February de 2007

Hoje em dia, por mais contraditório que pareça, fazer um serviço não requer muito trabalho. Alguns cliques com o Wizard do Visual Studio 2005 e pronto, já teremos uma aplicação ATL capaz de fornecer interfaces COM e que seja de fato um serviço do Windows. Neste post vou falar sobre uma pequena ferramenta que estou oferecendo como brinde para aqueles que têm a paciência de ler este Blog.

Na primeira empresa que trabalhei como programador, isso há uns 11 anos atrás, eu era responsável por manter o software de comunicação da rede de coletores de dados da Provectus. No início, um programa MFC era responsável por interagir com o driver que controlava a placa SS140, que era a interface que o PC tinha para fazer parte dessa rede de coletores. Logo que o programa se estabilizou, modificamos este software para torná-lo um serviço do Windows.

Mas o que é um serviço?

Um serviço é um módulo executável, que é registrado para que seja executado no sistema mesmo que ninguém faça logon no Windows. Hoje muitas aplicações e componentes do Windows são implementados como serviços. Um serviço não é apenas um executável comum cadastrado para ter seu início automatizado. Um serviço precisa fornecer uma interface com rotinas de CallBack para o sistema, a fim de responder aos comandos de inicialização, parada e suspensão da execução. Esta interface não é uma interface COM como alguns de vocês devem ter imaginado. Dê uma olhada na função StartServiceCtrlDispatcher para que você tenha uma idéia do tipo de interface que estou me referindo. Esta é apenas uma das funções necessárias para se construir um serviço. Dê uma passeada pela referência para saber os detalhes de como se implementa um serviço na unha. Serviços normalmente são executados em conta de sistema, mas podem opcionalmente utilizar uma conta de usuário pré-definida. Veja mais detalhes sobre serviços no site do MSDN.

De volta aos anos 90, nos surgiu então a necessidade de fazer com que o Supervisor, um dos nossos principais programas na Provectus, trabalhasse como um serviço. O Supervisor era um programa gigante construído em Visual Basic 4.0. Ele era responsável por receber os comandos da rede de coletores, e a partir deles, executar Stored Procedures no SQL Server. O Visual Basic 4.0 ainda não implementava o operador AddressOf, que surgiu somente na versão 5.0. Este operador pode ser utilizado para se obter o ponteiro de funções escritas em VB. Isso possibilitou tais programas registrarem rotinas de CallBack no sistema. Enfim, com este operador, alguns quilos de paciência e uma arma apontada para a sua cabeça, seria possível construir um serviço em Visual Basic. Chegamos a cogitar a possibilidade de reescrever todo o Supervisor em C, para que fosse possível então transformá-lo em um serviço. Mas felizmente alguém teve um surto de sanidade e disse:

“Porque você não faz um serviço vazio que simplesmente chama o Supervisor?”


E não é que funciona mesmo? Esta necessidade de ter um programa qualquer se comportando com um serviço é mais comum do que se imagina, principalmente quando falamos de ambientes corporativos. Assim, logo apareceram ferramentas que tornaram isso possível, mas nada me impede de ter minha própria versão.

O programa Prog2Svc é um serviço que faz exatamente isso. Executando este programa sem qualquer parâmetro, é exibida a mensagem abaixo, que informa quais os possíveis parâmetros a serem utilizados.

Desta forma, para registrar a calculadora do Windows como um serviço, utilizaremos a seguinte linha de comando. Lembre-se que esta é uma operação que requer direito administrativo, sendo assim, no caso do Windows Vista, esta linha de comando deveria ser executada a partir de um Prompt de Comandos que foi iniciado como administrador.

Prog2Svc -add Calculadora c:\Windows\System32\calc.exe

Exibida a mensagem de sucesso, você já poderá visualizar seu serviço através gerenciador do Windows. Para ter acesso ao gerenciador de serviços, digite “services.msc” sem as aspas na janela Run… do Windows.

Você pode iniciar seu serviço tanto utilizando gerenciador como utilizando o bom e velho comando “net start Calculadora” na janela Run….

Quando iniciamos o serviço, não temos nenhum sinal de que ele esteja realmente funcionando. Isso porque o calc.exe foi executado em outro Desktop, mas podemos confirmar a sua execução utilizando o Process Explorer.

O parâmetro adicional -interactive pode fazer com que seu novo serviço tenha interação com o Desktop. Nota: O Windows Vista merece uma atenção especial neste ponto. O parâmetro -auto configura o início automático do serviço quando o sistema é ligado. E por último, o parâmetro -silent, que faz com que a instalação seja feita de forma silenciosa, ou seja, nenhuma mensagem de sucesso ou erro será exibida. Nota: Neste caso, o sucesso ou a falha pode ser verificada pelo código de saída do Prog2Svc. Outra possibilidade é o uso de variáveis de ambiente no path da aplicação que será executada. Veja este outro exemplo mais completo.

Prog2Svc -add -silent -auto BlocoDeNotas %SystemRoot%\System32\notepad.exe

Para remover o serviço é muito simples. Veja o exemplo abaixo. Note que para remover serviços, também podemos contar com o modo silencioso.

Prog2Svc -remove Calculadora
Prog2Svc -remove -silent BlocoDeNotas

Como o pseudo serviço termina?

Quando solicitamos a parada do serviço, o programa Prog2Svc recebe uma notificação via uma rotina de CallBack. Neste momento, poderíamos dar uma voadora no peito do processo, mas como isso não é de bom tom, uma rotina identifica a janela principal do processo que foi criado, e envia uma mensagem de WM_CLOSE para esta janela. Depois disso, são aguardados 30 segundos para que o processo tenha a oportunidade de finalizar suas tarefas e desalocar recursos do sistema. Caso este tempo expire e o processo ainda esteja rodando, bem, é como meu amigo Thiago sempre diz: “Só a violência constrói”, e o processo é derrubado via TerminateProcess.

Se você ainda não conseguiu imaginar como essa ferramenta lhe poderia ser útil, pense que você poderia criar um serviço que execute o Command Prompt, e assim, ter uma janela de comandos rodando em conta de sistema. Isso ajudaria a fazer testes e descobrir o que é possível fazer com privilégio desta conta.

Prog2Svc -add -interactive SysCmd %SystemRoot%\System32\cmd.exe

Para instalar este programa basta colocar uma cópia deste executável do diretório System32. Tecnicamente, seria possível colocá-lo em qualquer diretório, mas lembre-se de que a pasta onde ele ficará deveria ser acessível por uma conta de sistema. Assim, não o coloque em pastas como “Meus Documentos” ou outra pasta pessoal.

Como sabemos que a internet não é o lugar mais seguro de se obter um executável, certifique-se de que o programa que você está baixando possui uma assinatura válida.

Have fun!

  • Versão: 1.0.1.0
  • Windows: 2000 XP 2003 Vista

Prog2Svc.exe – x86 (58.7 KB)
Prog2Svc.exe – x64 (59.2 KB)
Prog2Svc.exe – IA64 (113 KB)