Práticas recomendadas de desenvolvimento de drivers da equipe do Surface

Introdução

Essas diretrizes de desenvolvimento de drivers foram desenvolvidas ao longo de muitos anos por desenvolvedores de drivers da Microsoft. Ao longo do tempo, quando os motoristas se comportaram mal e as lições foram aprendidas, essas lições foram capturadas e evoluíram para ser esse conjunto de orientações. Estas práticas recomendadas são utilizadas pela equipa de Hardware do Microsoft Surface para desenvolver e manter o código do controlador de dispositivo que suporta as experiências de hardware exclusivas do Surface.

Como qualquer conjunto de diretrizes, haverá exceções legítimas e abordagens alternativas que serão igualmente válidas. Considere incorporar essas diretrizes em seus padrões de desenvolvimento ou usá-las para iniciar suas diretrizes específicas de domínio para seu ambiente de desenvolvimento e seus requisitos exclusivos.

Erros comuns cometidos por desenvolvedores de drivers

Manipulando E/S

  1. Acessando buffers recuperados de IOCTLs sem validar o comprimento. Consulte Falha ao verificar o tamanho dos buffers.
  2. Executar o bloqueio de E/S no contexto de um thread de usuário ou contexto de thread aleatório. Consulte Introdução aos objetos do Kernel Dispatcher.
  3. Envio de E/S síncrona para outro driver sem tempo limite. Consulte Enviando solicitações de E/S de forma síncrona.
  4. Usando IOCTLs nem-io sem entender as implicações de segurança. Consulte Usando E/S direta ou em buffer.
  5. Não verificar o status de retorno de WdfRequestForwardToIoQueue ou não manipular a falha corretamente e resultar em WDFREQUESTs abandonados.
  6. Mantendo o WDFREQUEST fora da fila em um estado não cancelável. Consulte Gerenciando filas de E/S, Concluindo solicitações de E/S e Cancelando solicitações de E/S.
  7. Tentando gerenciar o cancelamento usando a função Mark/UnmarkCancelable em vez de usar IoQueues. Consulte Objetos de fila do framework.
  8. Não saber a diferença entre as operações Cleanup e Close do identificador de arquivo. Consulte Erros ao manipular operações de limpeza e fechamento.
  9. Ignorar possíveis recursões com conclusão de E/S e reenvio da rotina de conclusão.
  10. Não ser explícito sobre os atributos de gerenciamento de energia de WDFQUEUEs. Não documentar claramente a escolha de gerenciamento de energia. Esta é a principal causa da verificação de bugs 0x9F: DRIVER_POWER_STATE_FAILURE nos drivers WDF. Quando o dispositivo é removido, a estrutura limpa a E/S da fila gerenciada por energia e da fila gerenciada por não energia em diferentes estágios do processo de remoção. As filas não gerenciadas por energia são limpas quando o IRP_MN_REMOVE_DEVICE final é recebido. Portanto, se você estiver mantendo a E/S em uma fila gerenciada sem energia, é uma boa prática limpar explicitamente a E/S no contexto de EvtDeviceSelfManagedIoFlush para evitar o deadlock.
  11. Não seguir as regras de tratamento de IRPs. Consulte Erros ao manipular operações de limpeza e fechamento.

Sincronização

  1. Segurando bloqueios para código que não precisa de proteção. Não mantenha um bloqueio para uma função inteira quando apenas um pequeno número de operações precisar ser protegido.
  2. Chamamento de motoristas com travas seguradas. Essa é a principal causa dos impasses.
  3. O uso de primitivas intertravadas para criar um esquema de bloqueio em vez de usar o sistema apropriado forneceu primitivas de bloqueio, como mutex, semáforo e spinlocks. Consulte Introdução aos objetos Mutex, Objetos semáforos e Introdução aos bloqueios de rotação.
  4. Usando um spinlock onde algum tipo de bloqueio passivo seria mais apropriado. Consulte Mutexes rápidos e Mutexes protegidos e objetos de evento. Para obter uma perspectiva adicional sobre bloqueios, consulte o artigo OSR - O estado da sincronização.
  5. Optando pela sincronização WDF e modelo de nível de execução sem compreensão completa das implicações. Consulte Usando bloqueios de estrutura. A menos que seu driver seja monolítico de nível superior interagindo diretamente com o hardware, evite optar pela sincronização WDF, pois isso pode levar a bloqueios devido à recursão.
  6. Adquirindo KEVENT, Semaphore, ERESOURCE, UnsafeFastMutex no contexto de vários threads sem entrar na região crítica. Isso pode levar a um ataque do DOS porque um thread que contém um desses bloqueios pode ser suspenso. Consulte Introdução aos objetos do Kernel Dispatcher.
  7. Alocando KEVENT na pilha de threads e retornando ao chamador enquanto o EVENT ainda está em uso. Normalmente feito quando usado com IoBuildSyncronousFsdRequest ou IoBuildDeviceIoControlRequest. O chamador dessas chamadas deve certificar-se de que elas não se desenrolem da pilha até que o gerenciador de E/S tenha sinalizado o evento quando o IRP for concluído.
  8. Aguardando indefinidamente nas rotinas de despacho. Em geral, qualquer tipo de espera na rotina de despacho é uma prática ruim.
  9. Verificar inadequadamente a validade de um objeto (se blá == NULL) antes de excluí-lo. Isso normalmente significa que o autor não tem compreensão completa do código que controla o tempo de vida do objeto.

Gerenciamento de Objetos

  1. Não criar explicitamente objetos WDF parentais. Consulte Introdução aos objetos do Framework.
  2. Parenting WDF object to WDFDRIVER em vez de parenting to a object that provides better lifetime management and optimize memory usage. Por exemplo, parenting WDFREQUEST para um WDFDEVICE em vez de IOTARGET. Consulte Usando objetos de estrutura geral, ciclo de vida de objeto de estrutura e resumo de objetos de estrutura.
  3. Não fazer proteção de rundown de recursos de memória compartilhada acessados entre drivers. Consulte Função ExInitializeRundownProtection.
  4. Enfileirar por engano o mesmo item de trabalho enquanto o anterior já está na fila ou já está em execução. Isso pode ser um problema se o cliente assumir que cada item de trabalho enfileirado será executado. Consulte Usando itens de trabalho do framework. Para obter mais informações sobre como enfileirar WorkItems, consulte o módulo DMF_QueuedWorkitem no projeto DMF (Driver Module Framework) - https://github.com/Microsoft/DMF.
  5. Temporizador de enfileiramento antes de postar a mensagem que o temporizador deve processar. Consulte Usando temporizadores.
  6. Executar uma operação em um item de trabalho que pode bloquear ou levar indefinidamente muito tempo para ser concluída.
  7. Projetando uma solução que resulta em uma enxurrada de itens de trabalho a serem enfileirados. Isso pode levar a um sistema sem resposta ou a um ataque de DOS se o bandido puder controlar a ação (por exemplo, bombear E/S para um driver que enfileire um novo item de trabalho para cada E/S). Consulte Usando itens de trabalho do framework.
  8. Não se segue que os retornos de chamada DPC do item de trabalho tenham sido executados até a conclusão antes de excluir o objeto. Consulte Diretrizes para escrever rotinas DPC e a função WdfDpcCancel.
  9. Criar threads em vez de usar itens de trabalho para tarefas de curta duração/não sondagem. Consulte Threads de trabalho do sistema.
  10. Não garantir que os threads tenham sido executados até a conclusão antes de excluir ou descarregar o driver. Para obter mais informações sobre a sincronização de thread rundown, examine o código associado ao exame do módulo DMF_Thread no projeto DMF (Driver Module Framework) - https://github.com/Microsoft/DMF.
  11. Usando um único driver para gerenciar dispositivos diferentes, mas interdependentes e usando variáveis globais para compartilhar informações.

Memória

  1. Não marcar o código de execução passiva como PAGEABLE, quando possível. O código do driver de paginação pode reduzir o tamanho do espaço ocupado pelo código do driver, liberando espaço do sistema para outros usos. Seja cauteloso marcando código paginável que aumenta IRQL >= DISPATCH_LEVEL ou pode ser chamado em IRQL elevado. Veja quando o código e os dados devem ser pagináveis e tornar os drivers pagináveis e detectar o código que pode ser paginável.
  2. Declarando estruturas grandes na pilha, Deve usar o heap/poolem vez disso. Consulte Usando o KernelStack e alocando memória de espaço do sistema.
  3. Zerando desnecessariamente o contexto do objeto WDF. Isso pode indicar uma falta de clareza sobre quando a memória será zerada automaticamente.

Diretrizes gerais do driver

  1. Misturando primitivas WDM e WDF. Usando primitivas WDM onde primitivas WDF podem ser usadas. O uso de primitivas WDF protege você de gotchas, melhora a depuração e, mais importante, torna seu driver portátil para o modo de usuário.
  2. Nomear FDOs e criar links simbólicos quando não for necessário. Consulte Gerenciar controle de acesso de driver.
  3. Copie, colando e usando GUIDs e outros valores constantes de drivers de exemplo.
  4. Considere o uso do código-fonte aberto DMF (Driver Module Framework) em seu projeto de driver. DMF é uma extensão do WDF que permite funcionalidade extra para um desenvolvedor de driver WDF. Consulte Apresentando o Driver Module Framework.
  5. Usando o Registro como um mecanismo de notificação entre processos ou como uma caixa de correio. Para obter uma alternativa, consulte DMF_NotifyUserWithEvent e DMF_NotifyUserWithRequest módulos disponíveis no projeto DMF - https://github.com/Microsoft/DMF.
  6. Supondo que todas as partes do registro estarão disponíveis para acesso durante a fase inicial de inicialização do sistema.
  7. Assumindo a dependência da ordem de carga de outro driver ou serviço. Como a ordem de carga pode ser alterada fora do controle do driver, isso pode resultar em um driver que funciona inicialmente, mas depois falha em um padrão imprevisível.
  8. A recriação de bibliotecas de drivers que já estão disponíveis, como o WDF, fornece PnP descrito em Dando suporte a PnP e gerenciamento de energia em seu driver ou aqueles fornecidos na interface de barramento, conforme descrito no artigo OSR Usando interfaces de barramento para comunicação de driver para driver.

PnP/Potência

  1. Interface com outro driver de forma não amigável ao pnp - não se registrando para notificações de alteração de dispositivo pnp. Consulte Registrando-se para notificação de alteração da interface do dispositivo.
  2. Criar nós ACPI para enumerar dispositivos e criar dependências de energia entre eles em vez de usar o driver de barramento ou o sistema fornecido interfaces de criação de dispositivo de software para PNP e dependências de energia de uma maneira elegante. Consulte Suporte a PnP e gerenciamento de energia em drivers de função.
  3. Marcando o dispositivo como não desativado - forçando uma reinicialização na atualização do driver.
  4. Ocultando o dispositivo no gerenciador de dispositivos. Consulte Ocultando dispositivos do Gerenciador de dispositivos.
  5. Fazendo suposições de que o driver será usado para apenas uma instância do dispositivo.
  6. Fazendo suposições de que o driver nunca será descarregado. Consulte Rotina de descarga do driver PnP.
  7. Não lidar com notificação de chegada de interface espúria. Isso pode acontecer e espera-se que os motoristas lidem com essa condição com segurança.
  8. Não implementar uma política de energia ociosa S0, que é importante para dispositivos que são restrições DRIPS ou filhas delas. Consulte Suporte a desligamento ocioso.
  9. Não verificar o status de retorno do WdfDeviceStopIdle leva ao vazamento de referência de energia devido ao desequilíbrio WdfDeviceStopIdle/ResumeIdle e, eventualmente, à verificação de bugs do 9F.
  10. Sem saber que PrepareHardware/ReleaseHardware pode ser chamado mais de uma vez devido ao rebalanceamento de recursos. Esses retornos de chamada devem ser restritos à inicialização de recursos de hardware. Veja EVT_WDF_DEVICE_PREPARE_HARDWARE.
  11. Usando PrepareHardware/ReleaseHardware para alocar recursos de software. A alocação de recursos de software estáticos para o dispositivo deve ser feita em AddDevice ou em SelfManagedIoInit se a alocação de recursos for necessária interagindo com o hardware. Veja EVT_WDF_DEVICE_SELF_MANAGED_IO_INIT.

Codificando diretrizes

  1. Não usar funções de cadeia de caracteres seguras e inteiro. Consulte Usando funções de cadeia de caracteres seguras e Usando funções inteiras seguras.
  2. Não usar typedefs para definir constantes.
  3. Usando variáveis globais e estáticas. Evite armazenar o contexto por dispositivo em globais. Os globais destinam-se a compartilhar informações em várias instâncias de dispositivos. Como alternativa, considere o uso do contexto de objeto WDFDRIVER para compartilhar informações entre várias instâncias de dispositivos.
  4. Não usar nomes descritivos para variáveis.
  5. Não ser consistente na nomeação de variáveis - consistência do caso. Não seguir o estilo de codificação existente ao fazer atualizações no código existente. Por exemplo, usando nomes de variáveis diferentes para estruturas comuns em funções diferentes.
  6. Não comentar opções de design importantes - gerenciamento de energia, bloqueios, gerenciamento de estado, uso de itens de trabalho, DPCs, temporizadores, uso de recursos globais, pré-alocação de recursos, expressões complexas/instruções condicionais.
  7. Comentar sobre coisas que são óbvias a partir do nome da API que está sendo chamada. Tornar seu comentário o equivalente em inglês do nome da função (como escrever o comentário "Create the Device Object" ao chamar WdfDeviceCreate).
  8. Não crie macros que tenham uma chamada de retorno. Consulte Funções (C++).
  9. Anotações de código-fonte (SAL) ausentes ou incompletas. Consulte Anotações SAL 2.0 para drivers do Windows.
  10. Usando macros em vez de funções embutidas.
  11. Usando macros para constantes no lugar de constexpr ao usar C++
  12. Compilando seu driver com o compilador C, em vez do compilador C++ para garantir que você obtenha uma verificação de tipo forte.

Manipulação de Erro

  1. Não relatar erros críticos de driver e marcar graciosamente o dispositivo como não funcional.
  2. Não retornando o status de erro NT apropriado que se traduz em status de erro WIN32 significativo. Consulte Usando valores NTSTATUS.
  3. Não usar macros NTSTATUS para verificar o status retornado das funções do sistema.
  4. Não afirmar sobre variáveis de estado ou sinalizadores onde necessário.
  5. Verificar se o ponteiro é válido antes de acessá-lo para contornar as condições de corrida.
  6. ASSERTIVA em ponteiros NULL. Se você tentar usar um ponteiro NULL para acessar a memória, o Windows verificará um bug. Os parâmetros da verificação de bug fornecerão as informações necessárias para corrigir o ponteiro nulo. Ao longo do tempo, quando muitas instruções ASSERT desnecessárias são adicionadas ao código, elas consomem memória e tornam o sistema lento.
  7. ASSERTING no ponteiro de contexto do objeto. A estrutura do driver garante que o objeto sempre será alocado com contexto.

Rastreamento

  1. Não definir tipos personalizados do WPP e usá-los em chamadas de rastreamento para obter mensagens de rastreamentos legíveis por humanos. Consulte Adicionando rastreamento de software WPP a um driver do Windows.
  2. Não usar rastreamento IFR. Consulte Usando o Gravador de Rastreamento a Bordo (IFR) em Drivers KMDF e UMDF 2.
  3. Chamando nomes de função em chamadas de rastreamento WPP. O WPP já rastreia nomes de funções e números de linha.
  4. Não usar eventos ETW para medir o desempenho e outros eventos críticos que afetam a experiência do usuário. Consulte Adicionando rastreamento de eventos a drivers de modo kernel.
  5. Não relatar erros críticos no log de eventos e marcar graciosamente o dispositivo como não funcional.

Verificação

  1. Não executar o verificador de driver com configurações padrão e avançadas durante o desenvolvimento e teste. Consulte Verificador de Driver. Nas configurações avançadas, recomenda-se habilitar todas as regras, exceto aquelas relacionadas à simulação de poucos recursos. É preferível executar os testes de simulação de baixo recurso isoladamente para facilitar a depuração de problemas.
  2. Não executar o teste DevFund no driver ou na classe de dispositivo da qual o driver faz parte com as configurações avançadas do verificador habilitadas. Consulte Como executar os testes do DevFund por meio da linha de comando.
  3. Não verificar se o driver está em conformidade com HVCI. Consulte Implementar código compatível com HVCI.
  4. Não executar o AppVerifier em WUDFhost.exe durante o desenvolvimento e teste de drivers de modo de usuário. Consulte Verificador de aplicativos.
  5. Não verificar o uso da memória usando a extensão do depurador !wdfpoolusage em tempo de execução para garantir que os objetos WDF não sejam abandonados. Memória, solicitações e itens de trabalho são vítimas comuns desses problemas.
  6. Não usar a extensão do depurador !wdfkd para inspecionar a árvore de objetos para garantir que os objetos sejam parentados corretamente e verificar os atributos dos principais objetos, como WDFDRIVER, WDFDEVICE, IO.