zoukankan      html  css  js  c++  java
  • 寒江独钓-键盘过滤学习1传统型的键盘过滤

    最顶层的设备对象是驱动  Kbdclass 生成的设备对象 

    中间层的设备对象是驱动i8042prt生成的设备对象

    最底层的设备对象是驱动ACPI生成的设备对象


    学习参考:   http://bbs.pediy.com/showthread.php?t=96205&highlight=IOAPIC

         http://bbs.pediy.com/showthread.php?t=135230&highlight=IOAPIC

    键盘过滤分为几种方式:

    1 传统型的键盘过滤(本次笔记)


    2 修改IDT + IOAPIC重定位表截获PS2键盘中断的例子

    CPP文件:

    #include <wdm.h>
    #include <ntddkbd.h>
    #include "ctrl2cap.h"
    
    typedef struct _C2P_DEV_EXT 
    { 
        // 这个结构的大小
        ULONG NodeSize; 
        // 过滤设备对象
        PDEVICE_OBJECT pFilterDeviceObject;
        // 同时调用时的保护锁
        KSPIN_LOCK IoRequestsSpinLock;
        // 进程间同步处理  
        KEVENT IoInProgressEvent; 
        // 绑定的设备对象
        PDEVICE_OBJECT TargetDeviceObject; 
        // 绑定前底层设备对象
        PDEVICE_OBJECT LowerDeviceObject; 
    } C2P_DEV_EXT, *PC2P_DEV_EXT;
    
    // flags for keyboard status
    #define	S_SHIFT				1
    #define	S_CAPS				2
    #define	S_NUM				4
    
    //这里是一个标记,用来保存当前键盘的状态,其中有3个位,
    //CAPS LOCK  NUM LOCK  SHIFT 是否按下
    //shift 是按下生效,释放则无效
    //CAPS LOCK 是按一次生效,再按一次无效
    static int kb_status = S_NUM;
    void __stdcall print_keystroke(UCHAR sch)
    {
    	UCHAR	ch = 0;
    	int		off = 0;
    //一个键是扫描码是X  那么弹起就是X+0x80
    	if ((sch & 0x80) == 0)	//按下
    	{
    		if ((sch < 0x47) || //如果按下了字母/数字等可见字符
    			((sch >= 0x47 && sch < 0x54) && (kb_status & S_NUM))) // Num Lock
    		{
    			//最终得到哪个字符由CAPS LOCK , NUM LOCK, SHIFT 这几个键的状态来决定
    			//所以写在一张表中
    			ch = asciiTbl[off+sch];
    		}
    
    		switch (sch)
    		{
    			//caps lock 和NUM lock 键类似,都是按下两次 = 没按
    		    //所以这里用XOR 来设备标志,按一次起作用,再按一次就不起作用了
    		case 0x3A:
    			kb_status ^= S_CAPS;
    			break;
    
    			//1 有两个键,左右各一个,扫描码互不相同
    			//2 shift 键是按下起作用,弹起则作用消失,这里用 | 来设置
    		case 0x2A:
    		case 0x36:
    			kb_status |= S_SHIFT;
    			break;
    
    			//num lock键
    		case 0x45:
    			kb_status ^= S_NUM;
    		}
    	}
    	else		//弹起
    	{
    		if (sch == 0xAA || sch == 0xB6)
    			kb_status &= ~S_SHIFT;
    	}
    
    	if (ch >= 0x20 && ch < 0x7F)//32-126
    	{
    		DbgPrint("%C 
    ",ch);
    	}
    
    }
    
    NTSTATUS 
    c2pDevExtInit( 
        IN PC2P_DEV_EXT devExt, 
        IN PDEVICE_OBJECT pFilterDeviceObject, 
        IN PDEVICE_OBJECT pTargetDeviceObject, 
        IN PDEVICE_OBJECT pLowerDeviceObject ) 
    { 
        memset(devExt, 0, sizeof(C2P_DEV_EXT)); 
    
        devExt->NodeSize = sizeof(C2P_DEV_EXT); 
        devExt->pFilterDeviceObject = pFilterDeviceObject; 
    
        KeInitializeSpinLock(&(devExt->IoRequestsSpinLock)); 
    	//是专为防止多处理器并发而引入的一种锁。
        KeInitializeEvent(&(devExt->IoInProgressEvent), NotificationEvent, FALSE); 
    
        devExt->TargetDeviceObject = pTargetDeviceObject; 
        devExt->LowerDeviceObject = pLowerDeviceObject; 
        return( STATUS_SUCCESS ); 
    }
    
    // 这个函数是事实存在的,只是文档中没有公开。声明一下
    // 就可以直接使用了。
    NTSTATUS
    ObReferenceObjectByName(
                            PUNICODE_STRING ObjectName,
                            ULONG Attributes,
                            PACCESS_STATE AccessState,
                            ACCESS_MASK DesiredAccess,
                            POBJECT_TYPE ObjectType,
                            KPROCESSOR_MODE AccessMode,
                            PVOID ParseContext,
                            PVOID *Object
                            );
    
    extern POBJECT_TYPE IoDriverObjectType;
    ULONG gC2pKeyCount = 0;
    PDRIVER_OBJECT gDriverObject = NULL;
    
    // 这个函数经过改造。能打开驱动对象Kbdclass,然后绑定
    // 它下面的所有的设备:
    NTSTATUS 
    c2pAttachDevices( 
                      IN PDRIVER_OBJECT DriverObject, 
                      IN PUNICODE_STRING RegistryPath 
                      ) 
    { 
        NTSTATUS status = 0; 
        UNICODE_STRING uniNtNameString; 
        PC2P_DEV_EXT devExt; 
        PDEVICE_OBJECT pFilterDeviceObject = NULL; 
        PDEVICE_OBJECT pTargetDeviceObject = NULL; 
        PDEVICE_OBJECT pLowerDeviceObject = NULL; 
    
        PDRIVER_OBJECT KbdDriverObject = NULL; 
    
        KdPrint(("MyAttach
    ")); 
    
        // 初始化一个字符串,就是Kdbclass驱动的名字。
        RtlInitUnicodeString(&uniNtNameString, KBD_DRIVER_NAME); 
        // 请参照前面打开设备对象的例子。只是这里打开的是驱动对象。
        status = ObReferenceObjectByName ( 
            &uniNtNameString, 
            OBJ_CASE_INSENSITIVE, 
            NULL, 
            0, 
            IoDriverObjectType, 
            KernelMode, 
            NULL, 
            &KbdDriverObject 
            ); 
        // 如果失败了就直接返回
        if(!NT_SUCCESS(status)) 
        { 
            KdPrint(("MyAttach: Couldn't get the MyTest Device Object
    ")); 
            return( status ); 
        }
        else
        {
            // 这个打开需要解应用。早点解除了免得之后忘记。
            ObDereferenceObject(KbdDriverObject);
        }
    
        // 这是设备链中的第一个设备	
        pTargetDeviceObject = KbdDriverObject->DeviceObject;
        // 现在开始遍历这个设备链
        while (pTargetDeviceObject) 
        {
            // 生成一个过滤设备,这是前面读者学习过的。这里的IN宏和OUT宏都是
            // 空宏,只有标志性意义,表明这个参数是一个输入或者输出参数。
            status = IoCreateDevice( 
                IN DriverObject, 
                IN sizeof(C2P_DEV_EXT), 
                IN NULL, 
                IN pTargetDeviceObject->DeviceType, 
                IN pTargetDeviceObject->Characteristics, 
                IN FALSE, 
                OUT &pFilterDeviceObject 
                ); 
    
            // 如果失败了就直接退出。
            if (!NT_SUCCESS(status)) 
            { 
                KdPrint(("MyAttach: Couldn't create the MyFilter Filter Device Object
    ")); 
                return (status); 
            } 
    
            // 绑定。pLowerDeviceObject是绑定之后得到的下一个设备。也就是
            // 前面常常说的所谓真实设备。
            pLowerDeviceObject = 
                IoAttachDeviceToDeviceStack(pFilterDeviceObject, pTargetDeviceObject); 
            // 如果绑定失败了,放弃之前的操作,退出。
            if(!pLowerDeviceObject) 
            { 
                KdPrint(("MyAttach: Couldn't attach to MyTest Device Object
    ")); 
                IoDeleteDevice(pFilterDeviceObject); 
                pFilterDeviceObject = NULL; 
                return( status ); 
            } 
    
            // 设备扩展!下面要详细讲述设备扩展的应用。
            devExt = (PC2P_DEV_EXT)(pFilterDeviceObject->DeviceExtension); 
            c2pDevExtInit( 
                devExt, 
                pFilterDeviceObject, 
                pTargetDeviceObject, 
                pLowerDeviceObject ); 
    
            // 下面的操作和前面过滤串口的操作基本一致。这里不再解释了。
            pFilterDeviceObject->DeviceType = pLowerDeviceObject->DeviceType; 
            pFilterDeviceObject->Characteristics = pLowerDeviceObject->Characteristics; 
            pFilterDeviceObject->StackSize = pLowerDeviceObject->StackSize+1; 
            pFilterDeviceObject->Flags |= pLowerDeviceObject->Flags & (DO_BUFFERED_IO | DO_DIRECT_IO | DO_POWER_PAGABLE) ; 
            //next device 
            pTargetDeviceObject = pTargetDeviceObject->NextDevice;
        }
        return status; 
    } 
    
    VOID 
    c2pDetach(IN PDEVICE_OBJECT pDeviceObject) 
    { 
    	PC2P_DEV_EXT devExt; 
    	BOOLEAN NoRequestsOutstanding = FALSE; 
    
    	devExt = (PC2P_DEV_EXT)pDeviceObject->DeviceExtension; 
    	__try 
    	{ 
    		__try 
    		{ 
    			IoDetachDevice(devExt->TargetDeviceObject);
    			devExt->TargetDeviceObject = NULL; 
    
    			IoDeleteDevice(pDeviceObject); 
    			devExt->pFilterDeviceObject = NULL; 
    			DbgPrint(("Detach Finished
    ")); 
    		} 
    		__except (EXCEPTION_EXECUTE_HANDLER)
    		{} 
    	} 
    	__finally{} 
    	return; 
    }
    
    
    #define  DELAY_ONE_MICROSECOND  (-10)
    #define  DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)
    #define  DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000)
    
    VOID 
    c2pUnload(IN PDRIVER_OBJECT DriverObject) 
    { 
        PDEVICE_OBJECT DeviceObject; 
        PDEVICE_OBJECT OldDeviceObject; 
        PC2P_DEV_EXT devExt; 
    
        LARGE_INTEGER	lDelay;
        PRKTHREAD CurrentThread;
        //delay some time 
        lDelay = RtlConvertLongToLargeInteger(100 * DELAY_ONE_MILLISECOND);
        CurrentThread = KeGetCurrentThread();
        // 把当前线程设置为低实时模式,以便让它的运行尽量少影响其他程序。
        KeSetPriorityThread(CurrentThread, LOW_REALTIME_PRIORITY);
    
        UNREFERENCED_PARAMETER(DriverObject); //换句话说 UNREFERENCED_PARAMETER 展开传递的参数或表达式。
    										  //其目的是避免编译器关于未引用参数的警告
        KdPrint(("DriverEntry unLoading...
    ")); 
    
        // 遍历所有设备并一律解除绑定
        DeviceObject = DriverObject->DeviceObject;
        while (DeviceObject)
        {
            // 解除绑定并删除所有的设备
            c2pDetach(DeviceObject);
            DeviceObject = DeviceObject->NextDevice;
        } 
        ASSERT(NULL == DriverObject->DeviceObject);
    
        while (gC2pKeyCount)
        {
            KeDelayExecutionThread(KernelMode, FALSE, &lDelay);
        }
        KdPrint(("DriverEntry unLoad OK!
    ")); 
        return; 
    } 
    
    NTSTATUS c2pDispatchGeneral( 
                                     IN PDEVICE_OBJECT DeviceObject, 
                                     IN PIRP Irp 
                                     ) 
    { 
        // 其他的分发函数,直接skip然后用IoCallDriver把IRP发送到真实设备
        // 的设备对象。 
        KdPrint(("Other Diapatch!")); 
        IoSkipCurrentIrpStackLocation(Irp); 
        return IoCallDriver(((PC2P_DEV_EXT)DeviceObject->DeviceExtension)->LowerDeviceObject, Irp); 
    } 
    
    NTSTATUS c2pPower( 
                           IN PDEVICE_OBJECT DeviceObject, 
                           IN PIRP Irp 
                           ) 
    { 
        PC2P_DEV_EXT devExt;
        devExt =
            (PC2P_DEV_EXT)DeviceObject->DeviceExtension; 
    
        PoStartNextPowerIrp( Irp ); 
        IoSkipCurrentIrpStackLocation( Irp ); 
        return PoCallDriver(devExt->LowerDeviceObject, Irp ); 
    } 
    
    NTSTATUS c2pPnP( 
                         IN PDEVICE_OBJECT DeviceObject, 
                         IN PIRP Irp 
                         ) 
    { 
        PC2P_DEV_EXT devExt; 
        PIO_STACK_LOCATION irpStack; 
        NTSTATUS status = STATUS_SUCCESS; 
        KIRQL oldIrql; 
        KEVENT event; 
    
        // 获得真实设备。
        devExt = (PC2P_DEV_EXT)(DeviceObject->DeviceExtension); 
        irpStack = IoGetCurrentIrpStackLocation(Irp); 
    
        switch (irpStack->MinorFunction) 
        { 
        case IRP_MN_REMOVE_DEVICE: 
            KdPrint(("IRP_MN_REMOVE_DEVICE
    ")); 
    
            // 首先把请求发下去
            IoSkipCurrentIrpStackLocation(Irp); 
            IoCallDriver(devExt->LowerDeviceObject, Irp); 
            // 然后解除绑定。
            IoDetachDevice(devExt->LowerDeviceObject); 
            // 删除我们自己生成的虚拟设备。
            IoDeleteDevice(DeviceObject); 
            status = STATUS_SUCCESS; 
            break; 
    
        default: 
            // 对于其他类型的IRP,全部都直接下发即可。 
            IoSkipCurrentIrpStackLocation(Irp); 
            status = IoCallDriver(devExt->LowerDeviceObject, Irp); 
        } 
        return status; 
    }
    
    // 这是一个IRP完成回调函数的原型
    NTSTATUS c2pReadComplete( 
                                  IN PDEVICE_OBJECT DeviceObject, 
                                  IN PIRP Irp, 
                                  IN PVOID Context 
                                  ) 
    {
         PIO_STACK_LOCATION IrpSp;
         ULONG buf_len = 0;
         PUCHAR buf = NULL;
         size_t i,numKeys;
    
    	 PKEYBOARD_INPUT_DATA KeyData; 
    
         IrpSp = IoGetCurrentIrpStackLocation( Irp );
    
         //  如果这个请求是成功的。很显然,如果请求失败了,这么获取
         //   进一步的信息是没意义的。
         if( NT_SUCCESS( Irp->IoStatus.Status ) ) 
         {
            // 获得读请求完成后输出的缓冲区
            buf = Irp->AssociatedIrp.SystemBuffer;
    		KeyData = (PKEYBOARD_INPUT_DATA)buf;
            // 获得这个缓冲区的长度。一般的说返回值有多长都保存在
            // Information中。
            buf_len = Irp->IoStatus.Information;
            numKeys = buf_len / sizeof(KEYBOARD_INPUT_DATA);
            //… 这里可以做进一步的处理。我这里很简单的打印出所有的扫
            // 描码。
            //for(i=0;i<buf_len;++i)
    		for(i=0;i<numKeys;++i)
            {
                //DbgPrint("ctrl2cap: %2x
    ", buf[i]);
    			DbgPrint("
    ");
    			DbgPrint("numKeys : %d",numKeys);
    			DbgPrint("ScanCode: %x ", KeyData->MakeCode ); 
    			DbgPrint("%s
    ", KeyData->Flags ?"Up" : "Down" );
    			print_keystroke((UCHAR)KeyData->MakeCode);
    
    			//这里是一个小测试,发现CAPS LOCK键 就改写为CTRL键
    			if( KeyData->MakeCode == CAPS_LOCK) 
    			{ 
    				KeyData->MakeCode = LCONTROL; 
    			} 
            }
        }
        gC2pKeyCount--;
    
    	if( Irp->PendingReturned )
    	{ 
    		IoMarkIrpPending( Irp ); 
    	} 
        return Irp->IoStatus.Status;
    }
    
    
    NTSTATUS c2pDispatchRead( 
                                  IN PDEVICE_OBJECT DeviceObject, 
                                  IN PIRP Irp ) 
    { 
        NTSTATUS status = STATUS_SUCCESS; 
        PC2P_DEV_EXT devExt; 
        PIO_STACK_LOCATION currentIrpStack; 
        KEVENT waitEvent;
    
        KeInitializeEvent( &waitEvent, NotificationEvent, FALSE );
    
    	if (Irp->CurrentLocation == 1) 
    	{ 
    		ULONG ReturnedInformation = 0; 
    		KdPrint(("Dispatch encountered bogus current location
    ")); 
    		status = STATUS_INVALID_DEVICE_REQUEST; 
    		Irp->IoStatus.Status = status; 
    		Irp->IoStatus.Information = ReturnedInformation; 
    		IoCompleteRequest(Irp, IO_NO_INCREMENT); 
    		return(status); 
    	} 
    
        // 全局变量键计数器加1
        gC2pKeyCount++;
    
        // 得到设备扩展。目的是之后为了获得下一个设备的指针。
        devExt =
            (PC2P_DEV_EXT)DeviceObject->DeviceExtension;
    
        // 设置回调函数并把IRP传递下去。 之后读的处理也就结束了。
        // 剩下的任务是要等待读请求完成。
        currentIrpStack = IoGetCurrentIrpStackLocation(Irp); 
        IoCopyCurrentIrpStackLocationToNext(Irp);
    	/*
    	1,完成例程本来就是设置在当前堆栈的下一层堆栈里,这相当于是一个规范,
    	也可以用实际的IRP的返回来理解。在完成例程里,根据返回不同的状态值,
    	IRP的控制流可能会发生相应的变化,比如:...STATUS_MORE_PROCESSING,这样,
    	下层堆栈执行完成例程后,会将IRP的控制权交付给本层堆栈。从这个意义上讲,
    	完成例程,只能放在下层堆栈,实际上,设计也是这样的。
    	  2,拷贝当前堆栈的内容到下层堆栈,只是为了保证执行环境一样。
    	在一个设备栈中,高层设备只能访问自己的设备栈或者下层设备栈,
    	这就要求这个驱动必须要为下层设置IO堆栈,但不是必须的。每个堆栈中,
    	context字段的值是唯一的,会标识一些pending等状态位,表示不同的完成状态,
    	所以这个字段不可以随意复制
    	*/
        IoSetCompletionRoutine( Irp, c2pReadComplete, 
            DeviceObject, TRUE, TRUE, TRUE ); 
        return  IoCallDriver( devExt->LowerDeviceObject, Irp ); 	
    }
    
    NTSTATUS DriverEntry( 
                         IN PDRIVER_OBJECT DriverObject, 
                         IN PUNICODE_STRING RegistryPath 
                         ) 
    { 
        ULONG i; 
        NTSTATUS status; 
        KdPrint (("c2p.SYS: entering DriverEntry
    ")); 
    
    /*	__asm int 3;*/
        // 填写所有的分发函数的指针
        for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) 
        { 
            DriverObject->MajorFunction[i] = c2pDispatchGeneral; 
        } 
    
        // 单独的填写一个Read分发函数。因为要的过滤就是读取来的按键信息
        // 其他的都不重要。这个分发函数单独写。
        DriverObject->MajorFunction[IRP_MJ_READ] = c2pDispatchRead; 
    
        // 单独的填写一个IRP_MJ_POWER函数。这是因为这类请求中间要调用
        // 一个PoCallDriver和一个PoStartNextPowerIrp,比较特殊。
        DriverObject->MajorFunction [IRP_MJ_POWER] = c2pPower; 
    
        // 我们想知道什么时候一个我们绑定过的设备被卸载了(比如从机器上
        // 被拔掉了?)所以专门写一个PNP(即插即用)分发函数
        DriverObject->MajorFunction [IRP_MJ_PNP] = c2pPnP; 
    
        // 卸载函数。
        DriverObject->DriverUnload = c2pUnload; 
        gDriverObject = DriverObject;
        // 绑定所有键盘设备
        status =c2pAttachDevices(DriverObject, RegistryPath);
    
        return status; 
    }

    H文件:

    #pragma once//概述这是一个比较常用的C/C++杂注,只要在头文件的最开始加入这条杂注,就能够保证头文件只被编译一次
    // Kbdclass驱动的名字
    #define KBD_DRIVER_NAME  L"\Driver\Kbdclass"
    
    #define KEY_UP 1 
    #define KEY_DOWN 0 
    
    #define LCONTROL ((USHORT)0x1D) 
    #define CAPS_LOCK ((USHORT)0x3A) 
    
    
    #define DELAY_ONE_MICROSECOND   (-10)
    #define DELAY_ONE_MILLISECOND   (DELAY_ONE_MICROSECOND*1000)
    #define DELAY_ONE_SECOND        (DELAY_ONE_MILLISECOND*1000)
    
    unsigned char asciiTbl[]={
    	0x00, 0x1B, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x2D, 0x3D, 0x08, 0x09,	//normal
    		0x71, 0x77, 0x65, 0x72, 0x74, 0x79, 0x75, 0x69, 0x6F, 0x70, 0x5B, 0x5D, 0x0D, 0x00, 0x61, 0x73,
    		0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x3B, 0x27, 0x60, 0x00, 0x5C, 0x7A, 0x78, 0x63, 0x76,
    		0x62, 0x6E, 0x6D, 0x2C, 0x2E, 0x2F, 0x00, 0x2A, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x38, 0x39, 0x2D, 0x34, 0x35, 0x36, 0x2B, 0x31,
    		0x32, 0x33, 0x30, 0x2E,
    		0x00, 0x1B, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x30, 0x2D, 0x3D, 0x08, 0x09,	//caps
    		0x51, 0x57, 0x45, 0x52, 0x54, 0x59, 0x55, 0x49, 0x4F, 0x50, 0x5B, 0x5D, 0x0D, 0x00, 0x41, 0x53,
    		0x44, 0x46, 0x47, 0x48, 0x4A, 0x4B, 0x4C, 0x3B, 0x27, 0x60, 0x00, 0x5C, 0x5A, 0x58, 0x43, 0x56,
    		0x42, 0x4E, 0x4D, 0x2C, 0x2E, 0x2F, 0x00, 0x2A, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x38, 0x39, 0x2D, 0x34, 0x35, 0x36, 0x2B, 0x31,
    		0x32, 0x33, 0x30, 0x2E,
    		0x00, 0x1B, 0x21, 0x40, 0x23, 0x24, 0x25, 0x5E, 0x26, 0x2A, 0x28, 0x29, 0x5F, 0x2B, 0x08, 0x09,	//shift
    		0x51, 0x57, 0x45, 0x52, 0x54, 0x59, 0x55, 0x49, 0x4F, 0x50, 0x7B, 0x7D, 0x0D, 0x00, 0x41, 0x53,
    		0x44, 0x46, 0x47, 0x48, 0x4A, 0x4B, 0x4C, 0x3A, 0x22, 0x7E, 0x00, 0x7C, 0x5A, 0x58, 0x43, 0x56,
    		0x42, 0x4E, 0x4D, 0x3C, 0x3E, 0x3F, 0x00, 0x2A, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x38, 0x39, 0x2D, 0x34, 0x35, 0x36, 0x2B, 0x31,
    		0x32, 0x33, 0x30, 0x2E,
    		0x00, 0x1B, 0x21, 0x40, 0x23, 0x24, 0x25, 0x5E, 0x26, 0x2A, 0x28, 0x29, 0x5F, 0x2B, 0x08, 0x09,	//caps + shift
    		0x71, 0x77, 0x65, 0x72, 0x74, 0x79, 0x75, 0x69, 0x6F, 0x70, 0x7B, 0x7D, 0x0D, 0x00, 0x61, 0x73,
    		0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x3A, 0x22, 0x7E, 0x00, 0x7C, 0x7A, 0x78, 0x63, 0x76,
    		0x62, 0x6E, 0x6D, 0x3C, 0x3E, 0x3F, 0x00, 0x2A, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x38, 0x39, 0x2D, 0x34, 0x35, 0x36, 0x2B, 0x31,
    		0x32, 0x33, 0x30, 0x2E
    };


















    2 修改IDT + IOAPIC重定位表截获PS2键盘中断的例子
  • 相关阅读:
    day 80 视图家族
    day 79 drf 多表关联操作
    day 78 drf 序列化
    day 77 drf中请求、渲染、解析、异常、响应模块的二次封装
    day 76 drf
    python小知识
    请求 渲染 解析 异常 响应模块
    子组件
    vue基础(2)
    vue基础
  • 原文地址:https://www.cnblogs.com/zcc1414/p/3982412.html
Copyright © 2011-2022 走看看