RPC ligero para llamar a un driver desde el modo de usuario …

publicado en: drivers | 0

En este artículo describimos una implementación de C++ RPC ligera. El RPC está destinado a cumplir el propósito bastante específico de llamar a las funciones de los controladores desde el código de modo de usuario. Es realmente ligero y no tiene ninguna característica avanzada, como muchas implementaciones RPC de propósito general. Sin embargo, la librería RPC (a la que llamamos intrincadamente RpcLib) resultó ser muy útil.

Es una tarea bastante común controlar un driver de modo de núcleo desde una aplicación o servicio de modo de usuario. Pero cualquiera que haya escrito controladores sabe que es imposible ejecutar simplemente algún trozo de código de controlador directamente desde el modo de usuario. Para hacer posible el envío de comandos de control a un controlador, el sistema operativo proporciona un mecanismo de códigos de control IO (IOCTL$0027s). La biblioteca descrita en este artículo utiliza IOCTL como transporte subyacente.

¿Por qué RPC?

«¿No estáis complicando las cosas, chicos?» – el lector puede preguntarse, – «¿Por qué el DeviceIoControl no es lo suficientemente bueno para ti?». A primera vista, RPC puede parecer un enfoque demasiado complicado. Pero puedes tener ciertas dificultades para usar el DeviceIoControl y las IOCTL desnudas.

La forma convencional de enviar comandos de control a un conductor es declarar un IOCTL para cada comando. Puedes ver el artículo Driver to Hide Processes and Files (Controlador para ocultar procesos y archivos) de Ivan Romanenko y Sergey Popenko o el artículo How to develop a virtual disk for Windows (Cómo desarrollar un disco virtual para Windows) de Gena Mairanichenko. Estos artículos son los ejemplos de este enfoque.

El problema es que un conductor del mundo real puede tener docenas de códigos de control. Significa que la rutina de despacho que procesa los IOCTL puede ser tan larga que puede perderse allí.

Otra cosa es la transferencia de datos. Si necesitas enviar algunos datos complejos entre un controlador y una aplicación, empaquetarlos en un búfer y desempaquetarlos de nuevo puede ser una especie de tarea difícil y molesta. RPC tiene un mecanismo de serialización que hace el trabajo sucio por ti.

Así que la RPC hace que las cosas complicadas sean un poco menos complicadas.

¿Por qué C++ en Controlador?

RpcLib fue diseñado para ser usado en el código C++ y se basa en gran medida en las características de C++ como plantillas y excepciones. Actualmente no soporta los controladores escritos en C plano (porque nunca lo hemos necesitado). El uso de C++ en los controladores del modo kernel tiene sus pros y sus contras, pero no son el tema de este artículo.

Para hacer posible escribir los drivers del modo kernel en C++ utilizamos las versiones modificadas de CppLib y STLPort.

Parte del cliente

Usar RpcLib es realmente simple. Veamos cómo se ve desde el modo de usuario. Cuando necesitas llamar una función RPC desde el kernel, creas el objeto rpc::RpcCall en el lado del modo de usuario. Luego lo inicializas con el nombre del objeto registrado en el servidor y con el nombre del método que debe ser llamado.

                rpc::RpcCall cl("Muestra", "TraducirDirecciónVirtual");cl.Pack(DirecciónVirtual);cl.Call(transport_);cl.Unpack(&DirecciónFísica);

Luego pasas los argumentos de entrada, ejecutas la llamada y obtienes los argumentos de salida. Es bastante similar a la forma en que ocurre una llamada de función normal.

La clase rpc::RpcCall incapsula la llamada RPC. Así es como se ve:

                class RpcCall{public:RpcCall(const std::cadena& objName, const std::cadena& fncName, size_t bufferSize = 16*1024);{rpc::Archivo fuenteArchivo(&datos_[0], datos_.size());rpc::Pack(sourceArchive, objName);rpc::Pack(sourceArchive, fncName);curPackSize_ = sourceArchive.GetUsedSize();}template <class P>void Pack(const P& p){rpc::Archivo sourceArchive(&data_[0], data_.size(), curPackSize_, 0);rpc::Pack(sourceArchive, p);curPackSize_ = sourceArchive.GetUsedSize();}template <class senderType>void Call(senderType* sender){sender->Send(&data_[0], curPackSize_, data_.size(), &answerSize_);}template <class P;};void Desempaquetar(P* p){rpc::Archive sourceArchive(&data_[0], answerSize_, answerSize_, curUnpackSize_);rpc::Desempaquetar(sourceArchive, p);curUnpackSize_ = sourceArchive.GetCurRange();}};

Tiene un búfer del tamaño especificado (16Kb por defecto) para los parámetros de entrada y salida. El objeto rpc::RpcCall serializa el nombre del objeto RPC y el nombre de la función y los parámetros de entrada en este búfer (la serialización se describe en la sección Serialización). El método Call envía los datos desde el buffer a través del transporte dado por el llamador. Después de la ejecución de la llamada, la memoria intermedia contiene los datos de salida a deserializar.

Transporte

El propósito del transporte es enviar los datos de entrada de la llamada RPC al servidor y devolver los datos de salida. Normalmente, para enviar los datos de una aplicación en modo usuario a un conductor, se utiliza rpc::IoctlTransport. Es simplemente un envoltorio sobre el DeviceIoControl.

                Plantilla de la plantilla de la RPC_IOCTL, clase IoctlTransport{public:IoctlTransport(HANDLE device){}: device_(device){}evitar Enviar(char* buf, size_t inBufSize, size_t maxBufSize, size_t* BytesCount){if (!DeviceIoControl(device_,RPC_IOCTL, (void*)buf, (DWORD)inBufSize, (void*)buf, (DWORD)maxBufSize, (DWORD*)BytesCount,NULL)){throw std::runtime_error("Device IO control failed");}}private:HANDLE device_;};

Parte del servidor

Veamos ahora, cómo escribir el código para procesar las llamadas desde el modo de usuario. Aquí hay un ejemplo del típico objeto RPC con un método RPC:

                class Sample: public rpc::RpcSkelBase{public:Sample(rpc::IRpcServer* rpcServer){RPC_FUNCTION(Sample, TranslateVirtualAddress);rpcServer->RegisterObject("Sample", this);}void TranslateVirtualAddress(rpc::Archive& inBuf, rpc::Archive& outBuf){// ...}};

Cada función RPC es un método de objeto que toma dos argumentos: el búfer de entrada y el búfer de salida. Cada búfer está representado por un objeto de la clase rpc::Archivo. Normalmente, es necesario deserializar los argumentos del archivo de entrada con la función rpc::Unpack. Una vez finalizado el trabajo, se puede serializar el resultado en el archivo de salida con la función rpc::Pack (la serialización se describe a continuación). Si hay un error, el método debe lanzar una excepción. Será recogida por la biblioteca, serializada al modo de usuario y relanzada allí. Así, el que llama sabrá que algo ha salido mal.

Para hacer accesible una función RPC, es necesario registrarla con la macro RPC_FUNCTION en el constructor.

También, necesitas registrar el objeto mismo en el servidor RPC. Es un objeto que envía llamadas entre el objeto RPC. En el código anterior, el objeto está siendo registrado en el constructor, pero en realidad, puedes hacerlo donde quieras.

Para despachar las llamadas RPC, necesitas escribir algo como esto en la función donde procesas tus solicitudes de IOCTL:

                g_RpcServer-;process(buf, inSize, outSize, &outSize);

Así es como se usa la biblioteca desde el modo kernel. Como puedes ver, todo es bastante simple. Ahora, echemos un vistazo dentro de la parte del servidor de la biblioteca.

Objetos RPC

Todos los objetos RPC deben implementar la interfaz rpc::IRpcSkel:

                estructura IRpcSkel{virtual ~IRpcSkel(){ } vacío virtual Call(const std::string& fncName, rpc::Archive& inBuf, rpc::Archive&amp outBuf) = 0;};

Esta interfaz tiene la llamada de método, que obtiene el nombre de la función (como una cadena) y los archivos de los parámetros de entrada y salida. Debe utilizar el nombre de la función para enviar la llamada a la función correcta. Es una tarea rutinaria, así que escribimos la clase rpc::RpcSkelBase para hacer esto. Se implementa de la siguiente manera:

                class RpcSkelBase : public rpc::IRpcSkel{typedef boost::function<void(rpc::Archive&, rpc::Archive&)> RpcImplMethodType;typedef std::map<std::string, RpcImplMethodType;RpcMethodMap;public:void Call(const std::string& fncName, rpc::Archive& inBuf, rpc::Archive& outBuf){RpcMethodMap::iterator it = rpcMethods_.find(fncName);RpcImplMethodType pMethod = it->second;pMethod(inBuf, outBuf);}void RegisterRpcFunction(const std::string& funcName, RpcImplMethodType pFunc){if (rpcMethods_.find(funcName) != rpcMethods_.end())throw std::runtime_error(std::string(__FUNCTION__) + " - method " + funcName + " already registered");rpcMethods_.insert(std::make_pair(funcName, pFunc));}private:RpcMethodMap rpcMethods_;};

Todo lo que el usuario tiene que hacer es heredar su objeto de rpc::RpcSkelBase y registrar la función RPC usando la macro RPC_FUNCTION que has visto arriba.

Servidor RPC

Como mencionamos, el servidor RPC es el objeto que mantiene la lista de los objetos RPC y envía llamadas entre ellos. Implementa la interfaz rpc::IRpcServer. Funciona de la siguiente manera:

                struct IRpcServer{virtual ~IRpcServer(){} vacío virtual RegisterObject(const std::cadena& objName, IRpcSkel* pObject) = 0;vacío virtual Process(char* buf, size_t inBufSize, size_t maxBufSize, size_t * outBufSize) = 0;};

Hay una implementación por defecto de esta interfaz en RpcLib.

                class RpcServer: public RpcServerImpl{public:void RegisterObject(const std::string&amp; objName, IRpcSkel* pObject){if (!objects_.insert(std::make_pair(objName, pObject)).second){throw std::runtime_error(std::string(__FUNCTION__) + <cod

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *