WinDBG para el depurador del kernel de Windows
Una gran diferencia entre WinDBG y el depurador de usuario es que el depurador del kernel se inicia en una máquina y depura otro sistema relacionado iniciado en modo de depuración a través del puerto serie. o puede ser un sistema en otra máquina (este es solo un método recomendado e implementado por Microsoft. De hecho, los depuradores del kernel como SoftICE pueden lograr una depuración independiente). Mucha gente piensa que las funciones principales están implementadas en WinDBG. De hecho, ese no es el caso. Windows ha integrado el mecanismo de depuración del kernel en el kernel. Lo que los depuradores del kernel como WinDBG y kd solo deben hacer es enviar paquetes de datos. formatos específicos para comunicarse, como interrumpir el sistema, puntos de interrupción, mostrar datos de la memoria, etc. Luego, WinDBG procesa y muestra el paquete de datos recibido.
Antes de presentar más WinDBG, permítanme presentarles dos funciones: KdpTrace y KdpStub. Mencioné brevemente estas dos funciones en el artículo "Proceso de manejo de excepciones de Windows". Ahora permítanme mencionar nuevamente, cuando ocurre una excepción en el modo kernel, se llamará a KiDebugRoutine dos veces. Cuando ocurre una excepción en el modo de usuario, se llamará a KiDebugRoutine una vez, y la primera llamada es cuando la excepción recién comienza a procesarse. .
Cuando WinDBG no está cargado, KiDebugRoutine es KdpStub, y el procesamiento también es muy simple, principalmente para excepciones causadas por int 0x2d como DbgPrint, DbgPrompt, carga y descarga de SÍMBOLOS (las anomalías causadas por int 0x2d serán se detalla más adelante) Introducción), etc., agregue 1 a Context.Eip y omita la instrucción int 0x3 después de int 0x2d.
La función que realmente implementa la función WinDBG es KdpTrap, que es responsable de manejar todas las excepciones STATUS_BREAKPOINT y STATUS_SINGLE_STEP (de un solo paso). Las excepciones en STATUS_BREAKPOINT incluyen int 0x3, DbgPrint, DbgPrompt y carga y descarga de SÍMBOLOS. El procesamiento de DbgPrint es el más simple. KdpTrap envía directamente el paquete que contiene la cadena al depurador. Debido a que DbgPrompt debe generar y recibir cadenas, primero envía el paquete que contiene la cadena y luego cae en un bucle esperando recibir el paquete que contiene la cadena de respuesta del depurador. SYMBOLS se carga y descarga llamando a KdpReportSymbolsStateChange, excepción de punto de interrupción int 0x3 y excepción de un solo paso int 0x1 (estas dos excepciones son básicamente las excepciones más manejadas por el depurador del kernel) llamando a KdpReportExceptionStateChange, estas dos funciones son muy similares, ambas Al llamar a Función KdpSendWaitContinue. Se puede decir que KdpSendWaitContinue es el administrador de la función de depuración del kernel y es responsable del envío de varias funciones. Esta función envía la información que se enviará al depurador del kernel, como el estado actual de todos los registros. Después de cada paso, podemos encontrar que la información del registro se actualiza, es decir, el depurador del kernel acepta el paquete que envía que contiene. último estado de la máquina; y el estado de SÍMBOLOS, de modo que cuando se cargan y descargan SÍMBOLOS, podamos ver la reacción correspondiente en el depurador del kernel. KdpSendWaitContinue luego espera un paquete que contiene un comando del depurador del kernel para decidir qué hacer a continuación.
Echemos un vistazo a lo que KdpSendWaitContinue puede hacer:
case DbgKdReadVirtualMemoryApi:
KdpReadVirtualMemory(&ManipulateState,&MessageData,ContextRecord);
break;
caso DbgKdReadVirtualMemory64Api:
KdpReadVirtualMemory64(&ManipulateState,&MessageData,ContextRecord);
rotura;
caso DbgKdWriteVirtualMemoryApi:
KdpWriteVirtualMemory( &ManipulateState ,&MessageData,ContextRecord);
romper;
caso DbgKdWriteVirtualMemory64Api:
KdpWriteVirtualMemory64(&ManipulateState,&MessageData,ContextRecord);
romper ;
caso DbgKdReadPhysicalMemoryApi:
KdpReadPhysicalMemory(&ManipulateState,&MessageData,ContextRecord);
rotura;
caso DbgKdWritePhysicalMemoryApi:
KdpWritePhysicalMemory(&ManipulateState,&MessageData,ContextRecord);
break;
caso DbgKdGetContextApi:
KdpGetContext(&ManipulateState,&MessageData,ContextRecord);
descanso;
caso DbgKdSetContextApi:
KdpSetContext(&ManipulateState,&MessageData,ContextRecord);
descanso;
caso DbgKdWriteBreakPointApi:
KdpWriteBreakpoint(&ManipulateState,&MessageData,ContextRecord);
break;
caso DbgKdRestoreBreakPointApi:
KdpRestoreBreakpoint(&ManipulateState,&MessageData,ContextRecord ) ;
descanso;
caso DbgKdReadControlSpaceApi:
KdpReadControlSpace(&ManipulateState,&MessageData,ContextRecord);
descanso;
caso DbgKdWriteControlSpac
eApi:
KdpWriteControlSpace(&ManipulateState,&MessageData,ContextRecord);
break;
case DbgKdReadIoSpaceApi:
KdpReadIoSpace(&ManipulateState,&MessageData, ContextRecord);
descanso;
caso DbgKdWriteIoSpaceApi:
KdpWriteIoSpace(&ManipulateState,&MessageData,ContextRecord);
descanso;
case DbgKdContinueApi:
if (NT_SUCCESS(ManipulateState.u.Continue.ContinueStatus) != FALSE) {
return ContinueSuccess;
} else {
return ContinuarError;
}
romper;
caso DbgKdContinueApi2:
if (NT_SUCCESS(ManipulateState .u.Continue2.ContinueStatus) != FALSE) {
KdpGetStateChange(&ManipulateState,ContextRecord);
return ContinueSuccess;
} else {
devolver ContinuarError;
}
romper;
caso DbgKdRebootApi:
KdpReboot();
break;
caso DbgKdReadMachineSpecificRegister:
KdpReadMachineSpecificRegister(&ManipulateState,&MessageData,ContextRecord);
break;
caso DbgKdWriteMachineSpecificRegister: p> p>
KdpWriteMachineSpecificRegister(&ManipulateState,&MessageData,ContextRecord);
break;
caso DbgKdSetSpecialCallApi:
KdSetSpecialCall(&ManipulateState,ContextRecord);
romper;
caso DbgKdClearSpecialCallsApi:
KdClearSpecialCalls();
romper;
caso DbgKdSetInternalBreakPointApi: p>
KdSetInternalBreakpoint(&ManipulateState);
descanso;
caso DbgKdGetInternalBreakPointApi:<
/p>
KdGetInternalBreakpoint(&ManipulateState);
descanso;
caso DbgKdGetVersionApi:
KdpGetVersion(&ManipulateState);
descanso ;
caso DbgKdCauseBugCheckApi:
KdpCauseBugCheck(&ManipulateState);
rotura;
caso DbgKdPageInApi:
KdpNotSupported (&ManipulateState);
rotura;
caso DbgKdWriteBreakPointExApi:
Estado = KdpWriteBreakPointEx(&ManipulateState,
&MessageData,
ContextRecord);
if (Estado) {
ManipulateState.ApiNumber = DbgKdContinueApi;
ManipulateState.u.Continue.ContinueStatus = Estado;
devolver ContinuarError;
}
romper;
caso DbgKdRestoreBreakPointExApi:
KdpRestoreBreakPointEx(&ManipulateState,&MessageData,ContextRecord);
romper;
caso DbgKdSwitchProcessor:
KdPortRestore ();
ContinueStatus = KeSwitchFrozenProcessor(ManipulateState.Processor);
KdPortSave ();
return ContinueStatus;
caso DbgKdSearchMemoryApi:
KdpSearchMemory(&ManipulateState,&MessageData,ContextRecord);
break;
Leer y escribir memoria, buscar memoria, establecer/restaurar puntos de interrupción, continuar la ejecución, reiniciar, etc. ¿Se pueden realizar todas las funciones en WinDBG Jaja?
Cada vez que el depurador del kernel se hace cargo del sistema, llama a KiDebugRoutine(KdpTrace) en KiDispatchException, pero sabemos que para que el sistema ejecute KiDispatchException, debe ocurrir una excepción en el sistema. El depurador del kernel y el sistema que se está depurando solo están conectados a través del puerto serie. El puerto serie solo interrumpirá y no provocará que el sistema genere una excepción. Entonces, ¿cómo genera el sistema una excepción? La respuesta está en KeUpdateSystemTime. Cada vez que ocurre una interrupción del reloj, HalpClockInterrupt realiza algún procesamiento subyacente y saltará a esta función para actualizar la hora del sistema (porque es un salto en lugar de una llamada, por lo tanto, la dirección de HalpClockInterrupt no se encontrará al retroceder la pila después de desconectar WinDBG), que es una de las funciones llamadas con más frecuencia en el sistema.
En KeUpdateSystemTime, se juzgará si KdDebuggerEnable es VERDADERO. Si es VERDADERO, se llamará a KdPollBreakIn para juzgar si hay un paquete que contiene información de interrupción del depurador del núcleo. Si es así, se llamará a DbgBreakPointWithStatus y se ejecutará una instrucción int 0x3. ejecutado Después de que el proceso de manejo de excepciones ingresa a KdpTrace Dependiendo del procesamiento, los paquetes se enviarán al depurador del kernel y esperarán en un bucle infinito una respuesta del depurador del kernel. Ahora puedo entender por qué después de interrumpir el sistema en WinDBG, el rastreo de la pila puede encontrar KeUpdateSystemTime->RtlpBreakWithStatusInstruction. El sistema se detuvo en la instrucción int 0x3 (de hecho, se ejecutó int 0x3, pero el Eip se redujo en 1). De hecho, ingresó a KiDispatchException->KdpTrap, dando control al depurador del kernel.
Además de int 0x3, los métodos para que el sistema interactúe con el depurador incluyen DbgPrint, DbgPrompt, símbolos de carga y descarga, y todos obtienen servicios llamando a DebugService.
NTSTATUS DebugService(
ULONG ServiceClass,
PVOID Arg1,
PVOID Arg2
)
{
Estado NTSTATUS;
__asm {
mov eax,ServiceClass
mov ecx,Arg1 p>
mov edx,Arg2
int 0x2d
int 0x3
mov Estado,eax
}
estado de devolución;
}
ServiceClass puede ser BEAKPOINT_PRINT(0x1), BREAKPOINT_PROMPT(0x2), BREAKPOINT_LOAD_SYMBOLS(0x3), BREAKPOINT_UNLOAD_SYMBOLS(0x4). ¿Por qué va seguido de int 0x3? M$ es para compartir el código con int 0x3*** (no entiendo lo que significa -_-), porque el controlador de trampas de int 0x2d realiza un procesamiento y luego salta a int. 0x3 El procesamiento continúa en el controlador de capturas. Pero, de hecho, no hay procesamiento para esta instrucción int 0x3, simplemente agrega 1 a Eip y lo omite. Entonces este int 0x3 puede ser reemplazado por cualquier byte.
Los resultados del registro de excepción (EXCEPTION_RECORD)ExceptionRecord.ExceptionCode generado por int 0x2d e int 0x3 son STATUS_BREAKPOINT(0x80000003). La diferencia es que la excepción generada por int 0x2d tiene ExceptionRecord.NumberParameters>0 y ExceptionRecord. ExceptionInformation corresponde a la ServiceClass correspondiente, como BREAKPOINT_PRINT, etc. De hecho, una vez montado el depurador del kernel, el procesamiento de DbgPrint y otros caracteres enviados al depurador del kernel ya no se realiza a través del servicio de captura int 0x2d, sino que envía el paquete directamente. En palabras de M$, esto es más seguro porque no es necesario llamar a KdEnterDebugger y KdExitDebugger.
Finalmente, hablemos de la comunicación entre el sistema depurado y el depurador del kernel. El sistema depurado y el depurador del kernel se comunican enviando paquetes de datos a través del puerto serie. La dirección del puerto IO de Com1 es 0x3f8 y la dirección del puerto IO de Com2 es 0x2f8.
Antes de que el sistema depurado esté listo para enviar un paquete al depurador del kernel, primero llamará a KdEnterDebugger para pausar la ejecución de otros procesadores y obtener el bloqueo de giro del puerto Com (por supuesto, esto es para multiprocesadores) y configurar el puerto. bandera para guardar el estado. Una vez enviado el paquete, llame a KdExitDebugger para restaurar. Cada paquete es como un paquete de datos en la red y contiene un encabezado y contenido específico. El formato del encabezado del paquete es el siguiente:
typedef struct _KD_PACKET {
ULONG PacketLeader;
USHORT PacketType;
USHORT ByteCount ;
ULONG PacketId;
Suma de comprobación ULONG;
} KD_PACKET,*PKD_PACKET;
PacketLeader se envía mediante un identificador de cuatro idénticos bytes El paquete general es 0x30303030, el paquete de control es 0x69696969 y el paquete que interrumpe el sistema depurado es 0x62626262. Lea un byte a la vez y lea 4 veces seguidas para identificar el paquete. El paquete que interrumpe el sistema es muy especial. Los datos del paquete son solo 0x62626262. El identificador del paquete va seguido del tamaño del paquete, el tipo, el ID del paquete, el código de detección, etc. El encabezado del paquete va seguido de datos específicos. Esto es muy similar a los paquetes transmitidos en la red. Existen algunas similitudes, por ejemplo, cada vez que se envía un paquete al depurador, se recibirá un paquete de respuesta ACK para determinar si el depurador lo ha recibido. Si se recibe un paquete de RESENVIO o no se recibe respuesta durante un tiempo prolongado, se enviará nuevamente. Para los paquetes que envían cadenas de salida al depurador, informan el estado del SÍMBOLO, etc., se devuelven inmediatamente tan pronto como se recibe el paquete ACK, el sistema reanuda la ejecución y el rendimiento del sistema está a punto de estancarse. Solo los paquetes que informan el estado esperarán cada paquete de control del depurador del kernel y completarán la función correspondiente hasta que el paquete enviado contenga un comando para continuar la ejecución. Independientemente de si el paquete se envía o se recibe, se agregará un 0xaa al final del paquete para indicar el final.
Ahora usamos algunos ejemplos para observar el proceso de depuración.
Recuerdo que antes le pregunté a jiurl por qué el paso único de WinDBG es tan lento (en comparación con softICE), y en realidad dijo que no sentía que fuera lento.*$&$^$^(&(& ;(I ft. Ahora puedo entender por qué el paso único de WinDBG y la interrupción de la ejecución normal del sistema operativo son tan lentos. El paso único es lento porque además del procesamiento necesario, cada paso tiene que enviar y recibir paquetes del serial ¿Cómo es posible que el sistema de interrupción no sea lento? Esto se debe a que el sistema depurado no aceptará el paquete de interrupción de WinDBG hasta que se produzca la interrupción del reloj y se alcance KeUpdateSystemTime. Ahora estudiemos por qué no se pueden establecer puntos de interrupción en KiDispatchException, pero podemos usar single. Seguimiento de pasos de KiDispatchException Se establece un punto de interrupción, y cuando se alcanza el punto de interrupción, el sistema encuentra una excepción y regresa a KiDispatchException, y luego ejecuta a int 0x3. Este ida y vuelta crea un bucle infinito y el código original es modificado. el punto de interrupción int 0x3 no se puede restaurar excepto para int 0x1, porque se debe a que el bit TF en el registro EFLAG se establece y se reinicia automáticamente cada vez, por lo que el sistema puede continuar ejecutándose sin un bucle infinito. Para el mecanismo interno, podemos llamar a la función KdXXX. Implementar un depurador de kernel como WinDBG, o incluso reemplazar KiDebugRoutine (KdpTrap) con su propia función para implementar un depurador más potente, jaja.