Monday, December 03, 2007

NDIS Driver[2]

NDIS_MINIPORT_CHARACTERISTICS

這個資料結構只可以用在 NDIS 5.x 之前,之後已經被 NDIS 6.0 禁用了,不過 NDIS 6.0 是換湯不換藥啦,所以瞭解這個並無不妥。
typedef struct _NDIS_MINIPORT_CHARACTERISTICS {
    UCHAR MajorNdisVersion;
    UCHAR MinorNdisVersion;
    UINT Reserved;
    W_CHECK_FOR_HANG_HANDLER CheckForHangHandler;
    W_DISABLE_INTERRUPT_HANDLER DisableInterruptHandler;
    W_ENABLE_INTERRUPT_HANDLER  EnableInterruptHandler;
    W_HALT_HANDLER HaltHandler;
    W_HANDLE_INTERRUPT_HANDLER  HandleInterruptHandler;
    W_INITIALIZE_HANDLER InitializeHandler;
    W_ISR_HANDLER ISRHandler;
    W_QUERY_INFORMATION_HANDLER QueryInformationHandler;
    W_RECONFIGURE_HANDLER ReconfigureHandler;
    W_RESET_HANDLER ResetHandler;
    W_SEND_HANDLER SendHandler;
    W_SET_INFORMATION_HANDLER SetInformationHandler;
    W_TRANSFER_DATA_HANDLER TransferDataHandler;
    
    //
    // Version used is V4.0 or V5.0
    // with following members
    //
    
    W_RETURN_PACKET_HANDLER ReturnPacketHandler;
    W_SEND_PACKETS_HANDLER SendPacketsHandler;
    W_ALLOCATE_COMPLETE_HANDLER AllocateCompleteHandler;
    
    //
    // Version used is V5.0 with the following members
    //
    
    W_CO_CREATE_VC_HANDLER CoCreateVcHandler;
    W_CO_DELETE_VC_HANDLER CoDeleteVcHandler;
    W_CO_ACTIVATE_VC_HANDLER CoActivateVcHandler;
    W_CO_DEACTIVATE_VC_HANDLER CoDeactivateVcHandler;
    W_CO_SEND_PACKETS_HANDLER CoSendPacketsHandler;
    W_CO_REQUEST_HANDLER CoRequestHandler;
    
    //
    // Version used is V5.1 with the following members
    //
    
    W_CANCEL_SEND_PACKETS_HANDLER CancelSendPacketsHandler;
    W_PNP_EVENT_NOTIFY_HANDLER PnPEventNotifyHandler;
    W_MINIPORT_SHUTDOWN_HANDLER AdapterShutdownHandler;
} NDIS_MINIPORT_CHARACTERISTICS, *PNDIS_MINIPORT_CHARACTERISTICS;

基本上, W_CO_XXX 不用理會,直接給 NULL,用不到啊。從這個資料結構看來,NDIS 也是一步一步發展而來的。不過 NDIS 6.0 卻來個天翻地覆的改變。

需要實做的大概只有紅色的函式,如果 NIC 不會產生 interrupt 的話。ISR 相關的東西在下一集說明。

CheckForHangHandler

系統預設每兩秒會呼叫這個函式,如果回傳 TRUE,表示 NIC 已經不正常了,接著系統會呼叫 MiniportReset。 你可以在呼叫 NdisMSetAttributesEx() 的時候,修改系統呼叫這個函式的間隔。

BOOLEAN MiniportCheckForHang(
    IN NDIS_HANDLE MiniportAdapterContext
);

MiniportCheckForHang runs at IRQL = DISPATCH_LEVEL.

DisableInterruptHandler

系統呼叫這個函式,命令你的 Driver 關掉 NIC 的 interrutp。如果 NIC 支援動態開關 interrupt 但不共享 IRQ的話,Driver 可以提供這個函式。這個函式只是讓系統有機會開關你的 NIC 的 interrupt,換句話說,你也可以不提供。

VOID MiniportDisableInterrupt(
    IN NDIS_HANDLE MiniportAdapterContext
);

DIRQL

EnableInterruptHandler

系統呼叫這個函式,命令你的 Driver 打開 NIC 的 interrupt。如果 NIC 支援動態開關 interrupt 但不共享 IRQ的話,Driver 可以提供這個函式。這個函式只是讓系統有機會開關你的 NIC 的 interrupt,換句話說,你也可以不提供。

VOID MiniportEnableInterrupt(
    IN NDIS_HANDLE MiniportAdapterContext
);

DIRQL

HaltHandler

NDIS 會呼叫 MiniportHalt 的時機:
  • NDIS 無法設定 Multicast Address 或 MAC_OPTION。
  • Unbind 所有 bind 到 Miniport Driver 的 protocol drivers 之後。
  • 在 Driver Unload 的時候。
  • 系統關機的時候。
當 NIC 被移除或停用時,會呼叫這個函式。這個函式必須 deallocate 所有在 MiniportInitialize() 所 allocate 的資源。MiniportHalt() 在呼叫 NdisMDeregisterInterrupt() 都有可能被 MiniportISR() 或 MiniportHandleInterrupt() 所中斷,因此,儘快在一開使便關掉 interrupt,然後呼叫NdisMDeregisterInterrupt()。

Driver 如果有收到來自 NIC 的封包,並且已經往上層回報,但是 NDIS 還沒呼叫 MinoportReturnPacket() 的話, MiniportHalt 必須等到所有封包的 MinoportReturnPacket() 都已經被呼叫過,才可以返回。

VOID MiniportHalt(IN NDIS_HANDLE MiniportAdapterContext);

MiniportHalt runs at IRQL = PASSIVE_LEVEL.

HandleInterruptHandler

DSR(Deferred Service Routine)。如果 NIC 共享 IRQ 的話,只有當 MiniportISR() 把 QueueMiniportHandleInterrupt 設為 TRUE 的時候,系統之後才會呼叫 MiniportHandleInterrupt()。 通常在這裡面會做的事情有:
  • 1. 取得 NIC 的 Interrupt Event。
  • 2. 根據 NIC 的 Interrupt Event 採取對應的動作。大部分的動作都是去取得 NIC 上的封包。
  • 3. 清掉 NIC 的 Interrupt。
  • 4. Enable Interrupt Mask。因為這時候 Interrupt Mask 通常是關掉的。
為甚麼事情不在 MiniportISR() 做完就好了?那是因為很多事情很花時間,很不幸的,ISR 是執行在 DIRQL,這意味著如果 ISR 不趕快結束的話,其他執行在 DIRQL 的 Thread 根本就無法執行,系統可能會因此而 Hang 住,因為系統有些 Thread 也是在 DIRQL 上執行的。

VOID MiniportHandleInterrupt(
    IN NDIS_HANDLE MiniportAdapterContext
    );

MiniportHandleInterrupt runs at IRQL = DISPATCH_LEVEL.

InitializeHandler

MiniportInitialize() 是 Driver 的 DriverEntry() 離開之後會被系統呼叫的函式。它要做的事情很多,包括檢查 Medium Type 是不是符合(通常是 NdisMedium802_3)、初始化所有的資源(如 Timer 、SpinLock、Memory...等等)、讀取 Registry、初始化 NIC。

MiniportInitialize() 的傳入值 MiniportAdapterHandle 跟 WrapperConfigurationContext,要把它們記起來,以後常常會用到。 如果 NIC 是屬於 PCI 之類的介面的話,Driver 需要呼叫 NdisMQueryAdapterResources(),取得 NIC 的硬體資源。一般的硬體資源不外乎 Interrupt、DMA、Port(IoAddress)。

VOID NdisMQueryAdapterResources(
    OUT PNDIS_STATUS Status,
    IN NDIS_HANDLE WrapperConfigurationContext,
    OUT PNDIS_RESOURCE_LIST ResourceList,
    IN OUT PUINT BufferSize
);

ResourceList 是 Driver 要提供的空間(不固定大小)。NDIS_RESOURCE_LIST 也就是 CM_PARTIAL_RESOURCE_LIST:

typedef struct _CM_PARTIAL_RESOURCE_LIST {
  USHORT  Version;
  USHORT  Revision;
  ULONG  Count;
  CM_PARTIAL_RESOURCE_DESCRIPTOR PartialDescriptors[1];
} CM_PARTIAL_RESOURCE_LIST, *PCM_PARTIAL_RESOURCE_LIST;

Count:PartialDescriptors 陣列有幾個元素。

typedef struct _CM_PARTIAL_RESOURCE_DESCRIPTOR {
    UCHAR Type;
    UCHAR ShareDisposition;
    USHORT Flags;
    union {
        struct {
            PHYSICAL_ADDRESS Start;
            ULONG Length;
        } Generic;
        struct {
            PHYSICAL_ADDRESS Start;
            ULONG Length;
        } Port;
        struct {
            ULONG Level;
            ULONG Vector;
            KAFFINITY Affinity;
        } Interrupt;
        struct {
            PHYSICAL_ADDRESS Start;
            ULONG Length;
        } Memory;
        struct {
            ULONG Channel;
            ;
            ULONG Reserved1;
        } Dma;
        struct {
            ULONG Data[3];
        } DevicePrivate;
        struct {
            ULONG Start;
            ULONG Length;
            ULONG Reserved;
        } BusNumber;
        struct {
            ULONG DataSize;
            ULONG Reserved1;
            ULONG Reserved2;
        } DeviceSpecificData;
    } u;
} CM_PARTIAL_RESOURCE_DESCRIPTOR, *PCM_PARTIAL_RESOURCE_DESCRIPTOR;

Driver 需要做的事情,就是把這些硬體資訊記起來,以後會用到。 如果 NIC 支援 Port 資源的話,Driver 需要呼叫 NdisMRegisterIoPortRange(),之後才可以使用NdisRawReadPortXxx() 以及 NdisRawWritePortXxx()。這就是所謂的 PortMapping?

NDIS_STATUS NdisMRegisterIoPortRange(
    OUT PVOID *PortOffset,
    IN NDIS_HANDLE MiniportAdapterHandle,
    IN UINT InitialPort,
    IN UINT NumberOfPorts
);

PCMCIA 卡可能會有所謂的 Atrribute Memory,Driver 可以使用 NdisReadPcmciaAttributeMemory() 來讀取:

ULONG NdisReadPcmciaAttributeMemory(
    IN NDIS_HANDLE NdisAdapterHandle,
    IN ULONG Offset,
    IN PVOID Buffer,
    IN ULONG Length
);

接下來便是讀取 Registry。首先 Driver 先呼叫 NdisOpenConfiguration(),取得 Configuration Handle:

VOID NdisOpenConfiguration(
    OUT PNDIS_STATUS Status,
    OUT PNDIS_HANDLE ConfigurationHandle,
    IN NDIS_HANDLE WrapperConfigurationContext
);

接下來 Driver 便可以快快樂樂用這個 Configuration Handle 來讀取或寫入 Registry:

VOID NdisReadConfiguration(
    OUT PNDIS_STATUS Status,
    OUT PNDIS_CONFIGURATION_PARAMETER  *ParameterValue,
    IN NDIS_HANDLE ConfigurationHandle,
    IN PNDIS_STRING Keyword,
    IN NDIS_PARAMETER_TYPE ParameterType
);

VOID NdisWriteConfiguration(
    OUT PNDIS_STATUS Status,
    IN NDIS_HANDLE ConfigurationHandle,
    IN PNDIS_STRING Keyword,
    IN PNDIS_CONFIGURATION_PARAMETER ParameterValue
);

最後,再把這個 Configuration Handle 還給系統:

VOID NdisCloseConfiguration(
    IN NDIS_HANDLE ConfigurationHandle
);

在 MiniportInitialize() 裡面,Driver 一定要呼叫 NdisMSetAttributesEx()。其函式原型如下:

VOID NdisMSetAttributesEx(
    IN NDIS_HANDLE MiniportAdapterHandle,
    IN NDIS_HANDLE MiniportAdapterContext,
    IN UINT CheckForHangTimeInSeconds OPTIONAL,
    IN ULONG AttributeFlags,
    IN NDIS_INTERFACE_TYPE AdapterType
);

MiniportAdapterContext 很重要。要知道在 NDIS 或是 WDM 裡面,Driver 永遠都只有被呼叫的份,那表示 Driver 得維護自己的狀態才行,不然系統隨便呼叫 Driver 的函式,Driver 自己又不知道自己的狀態,那不是很容易就 BSOD (Blue Screen Of Death)了嗎。所以 MiniportAdapterContext 就是讓 Driver 提供一個地方,讓系統每次呼叫 Driver 的函式時,都會把這個地方的位址傳給 Driver,如此 Driver 就可以記住跟知道它需要的資訊。

CheckForHangTimeInSeconds 是告訴系統,多久來呼叫 Driver 的 MiniportCheckForHang()。

AttributeFlags 很重要。NDIS_ATTRIBUTE_BUS_MASTER、NDIS_ATTRIBUTE_DESERIALIZE、 NDIS_ATTRIBUTE_NO_HALT_ON_SUSPEND、NDIS_ATTRIBUTE_SURPRISE_REMOVE_OK 值得注意。
  • NDIS_ATTRIBUTE_SURPRISE_REMOVE_OK 表示這隻 Driver 可以處理 NIC 突然被移除,並且不需要通知使用者。這樣的 Driver 必須實做 MiniportPnPEventNotify ()。
  • NDIS_ATTRIBUTE_NO_HALT_ON_SUSPEND 表示系統轉移到低耗電的狀態(Sleeping)之前,NDIS 並不需要呼叫這隻 Driver 的 MiniportHalt()。
AdapterType 很有趣,基本上它是告訴 NDIS,這隻 Driver 的 Bus Type,也就是它下層要面對的對象是誰。 NDIS_INTERFACE_TYPE 的定義如下(from ndis.h):

typedef enum _NDIS_INTERFACE_TYPE {
    NdisInterfaceInternal = Internal,
    NdisInterfaceIsa = Isa,
    NdisInterfaceEisa = Eisa,
    NdisInterfaceMca = MicroChannel,
    NdisInterfaceTurboChannel = TurboChannel,
    NdisInterfacePci = PCIBus,
    NdisInterfacePcMcia = PCMCIABus
} NDIS_INTERFACE_TYPE, *PNDIS_INTERFACE_TYPE;

只有 ISA、EISA、PCI、PCMCIA,那 USB 跟 SDIO 勒?請使用 100

Windows CE 如果 Miniport Driver 沒有實做 MiniportReset() 的話,最好在 NdisMSetAttributesEx() 內把 CheckForHangInSeconds 設定為一個很大的數值,這樣系統就不會常常醒來呼叫 MiniportCheckForHang(),進而達到省電的目的。 CheckForHangInSeconds 也可以透過 HKEY_LOCAL_MACHINE\Comm\\Parms\CheckForHangTimeInSeconds來設定。

呼叫完 NdisMSetAttributesEx() 之後,如果 Driver 需要處理 Interrupt,請呼叫 NdisMRegisterInterrupt():


NDIS_STATUS NdisMRegisterInterrupt(
    OUT PNDIS_MINIPORT_INTERRUPT Interrupt,
    IN NDIS_HANDLE MiniportAdapterHandle,
    IN UINT InterruptVector,
    IN UINT InterruptLevel,
    IN BOOLEAN RequestIsr,
    IN BOOLEAN SharedInterrupt,
    IN NDIS_INTERRUPT_MODE InterruptMode
);

Interrupt:Driver 要保存下來,所有要跟 ISR 同步存取 NIC 資源的函式都得使用到這個 Handle(如 NdisMSynchronizeWithInterrupt())。
InterruptVector: 把之前取得的 Interrupt Vector 填上去就對了。
InterruptLevel: 把之前取得的 Interrupt Level 填上去就對了。
RequestIsr:如果 Driver 希望 NIC 每次產生 Interrupt 的時候,系統都會呼叫到 MiniportISR() 的話,設為 TRUE。如果 SharedInterrupt = TRUE,RequestIsr = TRUE。
SharedInterrupt:表示是否要共享 Interrutp Line。
InterruptMode:可以指定 Interrupt 的偵測是 Level Trigger 或 Edge Trigger。

最後,在 NDIS5.0 之前的版本,Driver 還必須 呼叫 NdisMRegisterAdapterShutdownHandler() ,註冊系統關機時要呼叫的函式。

VOID NdisMRegisterAdapterShutdownHandler(
    IN NDIS_HANDLE MiniportHandle,
    IN PVOID ShutdownContext,
    IN ADAPTER_SHUTDOWN_HANDLER ShutdownHandler
);

NDIS_STATUS MiniportInitialize(
    OUT PNDIS_STATUS     OpenErrorStatus,
    OUT PUINT            SelectedMediumIndex,
    IN PNDIS_MEDIUM      MediumArray,
    IN UINT              MediumArraySize,
    IN NDIS_HANDLE       MiniportAdapterHandle,
    IN NDIS_HANDLE       WrapperConfigurationContext
);

MiniportInitialize runs at IRQL = PASSIVE_LEVEL.

ISRHandler

如果要系統呼叫這個函式的話,Driver 必須在 MiniportInitialize() 內呼叫 NdisMRegisterInterrupt()。

VOID MiniportISR(
    OUT PBOOLEAN InterruptRecognized,
    OUT PBOOLEAN QueueMiniportHandleInterrupt,
    IN NDIS_HANDLE MiniportAdapterContext
);

InterruptRecognized:如果這個 interrupt 是這隻 Driver 的 NIC 所產生的話,回傳 TRUE。在共享 IRQ 的狀況下,有可能會回傳 FALSE。
QueueMiniportHandleInterrupt:回傳 TRUE的話,表示系統會在離開 MiniportISR() 之後再呼叫 MiniportHandleInterrupt()。
MiniportISR runs at DIRQL.

QueryInformationHandler

系統會透過 MiniportQueryInformation() 取得 NIC 的資訊,如 MAC address、TX/RX 統計數字、連線速度等等。

NDIS_STATUS MiniportQueryInformation(
    IN NDIS_HANDLE MiniportAdapterContext,
    IN NDIS_OID Oid,
    IN PVOID InformationBuffer,
    IN ULONG InformationBufferLength,
    OUT PULONG BytesWritten,
    OUT PULONG BytesNeeded
);

MiniportQueryInformation runs at IRQL = DISPATCH_LEVEL. ReconfigureHandler NDIS 不會呼叫 MiniportReconfigure() ,但是 Driver 可以在 MiniportInitialize() 內呼叫它。

NDIS_STATUS MiniportReconfigure(
    OUT PNDIS_STATUS OpenErrorStatus,
    IN NDIS_HANDLE MiniportAdapterContext,
    IN NDIS_HANDLE WrapperConfigurationContext
);

MiniportReconfigure runs at the same IRQL as MiniportIntialize.

ResetHandler

NDIS 會呼叫 MiniportReset 的時機:
  • MiniportCheckForHang() 回傳 TRUE。
  • NDIS 偵測到 Pending Sending Packet(for serialized miniport drivers only)。
  • NDIS 偵測到在某段時間內 Pending Sending Packet 一直無法被 complete。
Driver 必須在 MiniportReset() Reset NIC 跟 Driver 的狀態。

Deserialized Miniport Driver 必須 Complete 任何尚未送到 NIC 的 Ndis Packets。

Miniport Driver 不需要呼叫 NdisMIndicateStatus() 或 NdisMCoIndicateStatus()。

如果 Driver 有支援 MiniportCheckForHang() 的話,系統會定期呼叫 MiniportCheckForHang(),再決定是否要呼叫 MiniportReset()。如果 Driver 不支援 MiniportCheckForHang() 的話,系統會在呼叫 MiniportQueryInformation()、MiniportSetInformation()、MiniportSendPackets()、MiniportSend() 或 MiniportWanSend 逾時之後再呼叫 MiniportReset()。

NDIS_STATUS MiniportReset(
    OUT PBOOLEAN AddressingReset,
    IN NDIS_HANDLE MiniportAdapterContext
);

AddressingReset:如果 Driver 需要在 MiniportReset() 返回之後,讓 NDIS 透過 MiniportSetInformation() 重新設定 NIC 跟 Driver 的狀態(如 Packet Filter、Multicass Address)的話,設為 TRUE。
MiniportReset runs at IRQL = DISPATCH_LEVEL.

SendHandler

如果 Driver 不支援 MiniportSendPackets()、MiniportWanSend()、或 MiniportCoSendPackets() 的話,Driver 必須支援 SendHandler()。如果 Driver 支援 MiniportSendPackets() 跟 MiniportSend() 的話,系統將不會呼叫 MiniportSend()。

如果是 Deserialized Miniport Driver 的話,Driver 通常會把 Packet 放到 software queue 中,然後回傳 NDIS_STATUS_PENDING。等到 Driver 把 software queue 內的 Packet 送到 NIC 之後,再呼叫 NdisMSendComplete(),如此才算是完成整個 TX。

NDIS_STATUS MiniportSend(
    IN NDIS_HANDLE MiniportAdapterContext,
    IN PNDIS_PACKET Packet,
    IN UINT Flags
);

Packet:Driver 不可以直接存取這個參數。Driver 可以呼叫 NdisQueryPacket() 取得 Packet 的資訊。

typedef struct _NDIS_PACKET {
    NDIS_PACKET_PRIVATE  Private;
    union {
        struct {
             UCHAR       MiniportReserved[2*sizeof(PVOID)];
             UCHAR       WrapperReserved[2*sizeof(PVOID)];
        };
        struct {
             UCHAR       MiniportReservedEx[3*sizeof(PVOID)];
             UCHAR       WrapperReservedEx[sizeof(PVOID)];
        };
        struct {
             UCHAR       MacReserved[4*sizeof(PVOID)];
        };
    };
    ULONG_PTR            Reserved[2];
    UCHAR                ProtocolReserved[1];
} NDIS_PACKET, *PNDIS_PACKET, **PPNDIS_PACKET;

Private

typedef struct _NDIS_PACKET_PRIVATE {
    UINT                PhysicalCount;  // number of physical pages in packet.
    UINT                TotalLength;    // Total amount of data in the packet.
    PNDIS_BUFFER        Head;           // first buffer in the chain
    PNDIS_BUFFER        Tail;           // last buffer in the chain
    // if Head is NULL the chain is empty; Tail doesn't have to be NULL also
    PNDIS_PACKET_POOL   Pool;           // so we know where to free it back to
    UINT                Count;
    ULONG               Flags;
    BOOLEAN             ValidCounts;
    UCHAR               NdisPacketFlags;    // See fPACKET_xxx bits below
    USHORT              NdisPacketOobOffset;
} NDIS_PACKET_PRIVATE, * PNDIS_PACKET_PRIVATE;

VOID NdisQueryPacket(
    IN  PNDIS_PACKET            _Packet,
    OUT PUINT                   _PhysicalBufferCount OPTIONAL,
    OUT PUINT                   _BufferCount OPTIONAL,
    OUT PNDIS_BUFFER *          _FirstBuffer OPTIONAL,
    OUT PUINT                   _TotalPacketLength OPTIONAL
)
{
    if ((_FirstBuffer) != NULL) {
        PNDIS_BUFFER * __FirstBuffer = (_FirstBuffer);
        *(__FirstBuffer) = (_Packet)->Private.Head;
    }
    if ((_TotalPacketLength != NULL) || (_BufferCount != NULL) || (_PhysicalBufferCount != NULL)) {
        if (!(_Packet)->Private.ValidCounts) {
            PNDIS_BUFFER TmpBuffer = (_Packet)->Private.Head;
            UINT PTotalLength = 0, PPhysicalCount = 0, PAddedCount = 0;
            UINT PacketLength, Offset;
            while (TmpBuffer != (PNDIS_BUFFER)NULL) {
                NdisQueryBufferOffset(TmpBuffer, &Offset, &PacketLength);
                PTotalLength += PacketLength;
                PPhysicalCount += (UINT)NDIS_BUFFER_TO_SPAN_PAGES(TmpBuffer);
                ++PAddedCount;
                TmpBuffer = TmpBuffer->Next;
            }
            (_Packet)->Private.Count = PAddedCount;
            (_Packet)->Private.TotalLength = PTotalLength;
            (_Packet)->Private.PhysicalCount = PPhysicalCount;
            (_Packet)->Private.ValidCounts = TRUE;
        }
        if (_PhysicalBufferCount != NULL) {
            PUINT __PhysicalBufferCount = (_PhysicalBufferCount);
            *(__PhysicalBufferCount) = (_Packet)->Private.PhysicalCount;
        }
        if (_BufferCount != NULL) {
            PUINT __BufferCount = (_BufferCount);   
            *(__BufferCount) = (_Packet)->Private.Count;
        }
        if (_TotalPacketLength != NULL)  {
            PUINT __TotalPacketLength = (_TotalPacketLength);
            *(__TotalPacketLength) = (_Packet)->Private.TotalLength;
        }
    }
}

PhysicalBufferCount:這個 Ndis Packet 包含幾個 Page。
BufferCount:這個 Ndis Packet 包含幾個 MDL。
FirstBuffer:第一個 MDL 的位址。
TotalPacketLength:全部 MDL 所描述的記憶體大小。

直接看程式碼,NDIS_PACKET 的秘密可說破解一半,同時也把 MDL 的資料結構給搞清楚。NdisQueryPacket() 會找出它所包含的所有 MDL,然後透過 NdisQueryBufferOffset() 查詢每個 MDL 的大小,接著把這些資訊紀錄在 NDIS_PACKET_PRIVATE 內。

請注意,NDIS_BUFFER 是一個 MDL(Memory Description List),也因為它是個 List,這表示它有可能是由數個記憶體區塊串起來而形成的,因此 Driver 是不可以直接存取它的。所以 Driver 必須透過 NdisQueryBufferSafe() 來取得真正的記憶體位址,並且用 NdisGetNextBuffer() 取得下個 MDL。如此便可以把整個 MDL 的內容複製到一塊連續的記憶體中,整個送給 NIC。有的 Bus Type 可能不需要這個動作,因為它們的 Bus Driver 支援 MDL 的寫入方式。

VOID NdisGetNextBuffer(IN PNDIS_BUFFER CurrentBuffer, OUT PNDIS_BUFFER *NextBuffer) {
    *NextBuffer = CurrentBuffer->Next;
}

VOID NdisQueryBufferSafe(
    IN PNDIS_BUFFER  Buffer,
    OUT PVOID  *VirtualAddress  OPTIONAL,
    OUT PUINT  Length,
    IN MM_PAGE_PRIORITY Priority
) {
    if ((CHAR *)VirtualAddress != (CHAR *)NULL) {
        *(PVOID *)(_VirtualAddress) = MmGetSystemAddressForMdlSafe(Buffer, Priority);
    }
    *_Length = MmGetMdlByteCount(Buffer);
}

除此之外,Driver 可以透過 NDIS_PER_PACKET_INFO_FROM_PACKET() 取得 Packet 的額外資訊。Driver 可以拿它來判斷 Packet 是否支援 802.1Q(這個我也不知道是什麼,網路的東西太多了,學不完啊)。

PVOID NDIS_PER_PACKET_INFO_FROM_PACKET(
    IN/OUT PNDIS_PACKET Packet,
    IN NDIS_PER_PACKET_INFO InfoType
);

typedef enum _NDIS_PER_PACKET_INFO {
    TcpIpChecksumPacketInfo,
    IpSecPacketInfo,
    TcpLargeSendPacketInfo,
    ClassificationHandlePacketInfo,
    HeaderIndexInfo,                // Internal NDIS use only
    ScatterGatherListPacketInfo,
    Ieee8021pPriority,
    OriginalPacketInfo,
    NdisInternalExtension1,            // Internal NDIS use only
    NdisInternalExtension2,            // Internal NDIS use only
    NdisInternalExtension3,            // Internal NDIS use only
    NdisInternalPktDebug,            // Internal NDIS use only
    MaxPerPacketInfo
} NDIS_PER_PACKET_INFO, *PNDIS_PER_PACKET_INFO;

typedef struct _NDIS_PACKET_8021Q_INFO {
    union {
        struct {
            UINT32  UserPriority:3;
            UINT32  CanonicalFormatId:1;
            UINT32  VlanId:12;
            UINT32  Reserved:16;
        } TagHeader;
        PVOID  Value;
    };
} NDIS_PACKET_8021Q_INFO, *PNDIS_PACKET_8021Q_INFO;

The MiniportSend function of a serialized miniport driver runs at IRQL = DISPATCH_LEVEL.

SetInformationHandler

系統會透過 MiniportSetInformation() 設定 NIC 的資訊,如 Packet Filter、Protocol Option 等等。

NDIS_STATUS MiniportSetInformation(
    IN NDIS_HANDLE MiniportAdapterContext,
    IN NDIS_OID Oid,
    IN PVOID InformationBuffer,
    IN ULONG InformationBufferLength,
    OUT PULONG BytesRead,
    OUT PULONG BytesNeeded
);

MiniportSetInformation runs at IRQL = DISPATCH_LEVEL.

TransferDataHandler



NDIS_STATUS MiniportTransferData(
    OUT PNDIS_PACKET Packet,
    OUT PUINT BytesTransferred,
    IN NDIS_HANDLE MiniportAdapterContext,
    IN NDIS_HANDLE MiniportReceiveContext,
    IN UINT ByteOffset,
    IN UINT BytesToTransfer
);

ReturnPacketHandler

當 Driver 透過 NdisMIndicateReceivePacket() 或 NdisMCoIndicateReceivePacket() 告訴 NDIS 有封包由 NIC 收進來之後,因為 RX 的 Ndis Packet 是由 Driver 所配置,所以當 NDIS 把封包丟給 Application 之後,會呼叫 MinoportReturnPacket() ,讓 Driver 把這些 Ndis Packet 釋放或再利用。

Driver 必須在 MinoportReturnPacket() 裡面呼叫 NdisUnchainBufferAtXxx(),然後利用 NdisFreeBuffer() 釋放掉取得的 NDIS_BUFFER。最後如果你不要重新利用這個 NDIS_PACKET 的話,再呼叫 NdisFreePacket() 把這個 Packet 釋放掉。如果 Driver 要重新利用這個 NDIS_PACKET 的話,呼叫NdisReinitializePacket()。

這樣說明似乎有點模糊,這牽涉到 NDIS_PACKET 的配置。首先,NDIS_PACKET 的定義如下:

typedef struct _NDIS_PACKET {
    NDIS_PACKET_PRIVATE  Private;
    union {
        struct {
             UCHAR       MiniportReserved[2*sizeof(PVOID)];
             UCHAR       WrapperReserved[2*sizeof(PVOID)];
        };
        struct {
             UCHAR       MiniportReservedEx[3*sizeof(PVOID)];
             UCHAR       WrapperReservedEx[sizeof(PVOID)];
        };
        struct {
             UCHAR       MacReserved[4*sizeof(PVOID)];
        };
    };
    ULONG_PTR            Reserved[2];
    UCHAR                ProtocolReserved[1];
} NDIS_PACKET, *PNDIS_PACKET, **PPNDIS_PACKET;

當 Driver 由 NIC 收到封包時,Driver 需要呼叫 NdisAllocatePacket() 配置一個 NDIS_PACKET。

VOID NdisAllocatePacket(
    OUT PNDIS_STATUS Status,
    OUT PNDIS_PACKET *Packet,
    IN NDIS_HANDLE PoolHandle
);

問題來啦,PoolHandle 是甚麼?可以吃嗎?所以 Driver 之前得在 MiniportInitialize() 裡面透過 NdisAllocatePacketPool() 配置一個 Packet Pool 才行:

VOID NdisAllocatePacketPool(
    OUT PNDIS_STATUS Status,
    OUT PNDIS_HANDLE PoolHandle,
    IN UINT NumberOfDescriptors,
    IN UINT ProtocolReservedLength
);

ProtocolReservedLength:Miniport Driver 必須把它設為 PROTOCOL_RESERVED_SIZE_IN_PACKET。 有了 NDIS_PACKET 了,但是 Driver 還沒把 NDIS_BUFFER 串到 NDIS_PACKET。所以 Driver 得呼叫 NdisChainBufferAtBack() 或 NdisChainBufferAtFront() 把 NDIS_BUFFER 給串到 NDIS_PACKET。等等,NDIS_BUFFER 還沒配置呢。所以 Driver 得先呼叫 NdisAllocateBuffer ():

VOID NdisAllocateBuffer(
    OUT PNDIS_STATUS Status,
    OUT PNDIS_BUFFER *Buffer,
    IN NDIS_HANDLE PoolHandle,
    IN PVOID VirtualAddress,
    IN UINT Length
);

VirtualAddress:收到的封包的位址。 PoolHandle:Again,又遇到 PoolHandle 的問題。所以 Driver 得在 MiniportInitialize() 裡面先透過 NdisAllocateBufferPool() 把 Buffer Pool 配置好。

VOID NdisAllocateBufferPool(
    OUT PNDIS_STATUS Status,
    OUT PNDIS_HANDLE PoolHandle,
    IN UINT NumberOfDescriptors
);

所以事情比較清楚了。因為要把封包丟給 NDIS 之前,Driver 必須先配置 NDIS_PACKET,而 NDIS_PACKET 又包含一個以上的 NDIS_BUFFER,所以在 MinoportReturnPacket() 裡面,就算你要重新利用 NDIS_PACKET,你還是得先把裡面的 NDIS_BUFFER全部移除並釋放。因為 MDL映射到的 Virtual Address 有可能已經都不對了。

VOID MinoportReturnPacket(
    IN NDIS_HANDLE             MiniportAdapterContext,
    IN PNDIS_PACKET            Packet
);

MiniportReturnPacket runs at IRQL = DISPATCH_LEVEL.

SendPacketsHandler

跟 MiniportSend() 相同,只不過它可以一次處理多個 NDIS_PACKET。

VOID MiniportSendPackets(
    IN NDIS_HANDLE           MiniportAdapterContext,
    IN PPNDIS_PACKET         PacketArray,
    IN UINT                  NumberOfPackets
    );

AllocateCompleteHandler

Bus-Master DMA NIC 的 Driver 在呼叫 NdisMAllocateSharedMemoryAsync() 才需要的函式。因為我也沒寫過這類 Driver,所以也無從解釋。

VOID MiniportAllocateComplete(
    IN NDIS_HANDLE MiniportAdapterContext,
    IN PVOID VirtualAddress,
    IN PNDIS_PHYSICAL_ADDRESS PhysicalAddress,
    IN ULONG Length,
    IN PVOID Context
);

MiniportAllocateComplete runs at IRQL = DISPATCH_LEVEL.

CoCreateVcHandler

Ignore.

CoDeleteVcHandler

Ignore.

CoActivateVcHandler

Ignore.

CoDeactivateVcHandler

Ignore.

CoSendPacketsHandler

Ignore.

CoRequestHandler

Ignore.

CancelSendPacketsHandler

取消某個特定群組的封包傳送。 作法如下:
  • 在儲存 NDIS_PACKET 的 software queue 內搜尋,利用 NDIS_GET_PACKET_CANCEL_ID() 一個一個檢視封包的 ID 是否跟 CancelId 相同,如果相同的話,Driver 必須把這些 NDIS_PACKET 由 software queue 內移除,並呼叫 NdisMSendComplete()、NdisMSendWanComplete() 或 NdisMCoSendComplete(),並且把 Status 設為 NDIS_STATUS_ABORTED。


  • VOID MiniportCancelSendPackets(
        IN NDIS_HANDLE  MiniportAdapterContext,
        IN PVOID CancelId
    );
    

    System support for MiniportCancelSendPackets is available in Windows XP and later operating systems.
    MiniportCancelSendPackets runs at IRQL <= DISPATCH_LEVEL.

PnPEventNotifyHandler

  • 在處理 surprise removal 部份,所有在 Windows XP 版本之後,不會直接 access 硬體的 miniport driver,都應該在呼叫 NdisMSetAttributesEx() 的時候,傳入 NDIS_ATTRIBUTE_SURPRISE_REMOVE_OK 這個 flag。如果有設定這個 flag 的話,當使用者突然移除 NIC 時,就不會顯示出警告訊息。Driver 應該 completes pending Ndis Packets。 除此之外,在 MiniportPnPEventNotify() 之外的任何動作,都應該確認卡是否已經被移除(可以透過 register write/read 的方式,確認 register 是否可以被寫入)。如果不做這個確認的話,Driver 有可能會一直嘗試讀取已被移除的 NIC 的 register,這樣會導致 NDIS 不會呼叫到 MiniportPnPEventNotify() 。
  • 在處理 power source 部份,miniport 可以利用 MiniportPnPEventNotify() 傳入的參數,調整 NIC 的 power comsuption。例如,如果收到 NdisPowerProfileBattery 的話,就調低 power comsuption。,如果收到 NdisPowerProfileAcOnline 的話,便調高 power comsuption。


  • VOID MiniportPnPEventNotify(
        IN NDIS_HANDLE  MiniportAdapterContext,
        IN NDIS_DEVICE_PNP_EVENT  PnPEvent,
        IN PVOID  InformationBuffer,
        IN ULONG  InformationBufferLength,
    );
    

    System support for MiniportPnPEventNotify is available in Windows XP and later operating systems.
    MiniportPnPEventNotify runs synchronously at IRQL = PASSIVE_LEVEL and in the context of a system thread.

AdapterShutdownHandler

當系統關機的時候(不管是使用者選擇或是系統發生無法回覆的錯誤),MiniportShutdown() 會把 NIC 重置成初始的狀態。每個 Miniport Driver 都必須提供 MiniportShutdown(),不管是透過 NdisMRegisterAdapterShutdownHandler() (~NDIS5.0) 還是 NdisMRegisterMiniport() (NDIS5.1~)。

MiniportShutdown() 不用釋放任何配置過的資源,它只是 Stop the NIC。

VOID MiniportShutdown(
    IN PVOID ShutdownContext
);

MiniportShutdown should call no NdisXxx functions.

NDIS Driver[1]

NDIS(Network Driver Interface Specification) driver 就是所謂的網路卡 driver,網路卡分很多種,但是常用的就是那幾種:LAN 跟 WLAN。用比較通俗的說法,就是有線跟無線網卡。

而一般所謂的 NDIS driver,通常是指 NDIS miniport driver。miniport driver 聽起來很玄,其實就是 MS 不想讓寫 Driver 的人太痛苦,所以先把架構(framework)定好,再實做整個架構,然後把介面開出來,寫 driver 的人只要實做這些介面就可以了。miniport driver 就是指這類的 driver。每個 class 都幾乎會有相對應的 miniport driver,而且它們的架構也都不一樣。

miniport driver 有好有壞,好處顯而易見,不用做太多 dirty work。而壞處呢?使用者得先懂 miniport 的架構跟 DDK,另外 debug 也不是很方便,因為雜事都讓整個 framework 包走了,出問題的話根本不知道是為甚麼,因為你沒有 framework 的 source code。

NIC 指的是 Network Interface Card,也就是網路卡。很無聊的頭字語。

要注意的是,NDIS 還有分版本喔,現在已經到 6.0 了。而 5.x 到 6.0 的差別還不小,雖然說 NDIS 6.0 可以用模擬的方式來執行 5.x 的 driver,但是 MS 並不建議這樣做。所以說,痛苦的永遠是寫 driver 的人啊。

Deserialized and Serialized NDIS Miniport Drivers

Deserialized miniport driver 會把上層交代下來的 ndis packets 儲存起來,等到適當的時機再把這些 ndis packets 送到 NIC,讓 NIC 把這些封包傳出去。Serialized miniport driver 則是上層交代一個 ndis packet,則馬上把這個 ndis packet 送到 NIC 去。想當然爾,Deserialized miniport driver 會比較有效率,所以 DDK 強烈建議用 Deserialized miniport driver。

至於要怎樣做才可以讓系統知道你的 Driver 是 Deserialized Miniport Driver?
  • 在 MiniportInitialize() 內呼叫 NdisMSetAttributesEx(),並且把 AttributeFlags 加上 NDIS_ATTRIBUTES_DESERIALIZE 這個 flag。
當然,Deserialized Miniport Driver 還有一些需求,你必須讓你的 driver 符合這些需求。請參閱 DDK。

Connection-Oriented NDIS Miniport Drivers

Connection-oriented miniport drivers 必定是 deserialized miniport drivers。目前大多數的網路卡 driver 都是 Connection-less miniport driver,所以就不用看這段了。

Driver Entry

DriverEntry() 是所有 Windows Driver 的進入點。系統只要發現到新的 Device,它就會嘗試載入對應的 Driver(如果它找的到的話),接著就會呼叫這隻 Driver 的 DriverEntry()。在 DriverEntry 內,你必須先呼叫 NdisMInitializeWrapper(),初始化 Ndis Wrapper,然後再呼叫 NdisMRegisterMiniport(),跟系統註冊你支援的函式。

不用想太多,文件怎樣寫你就怎樣做。

NDIS_STATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath);

NdisMInitializeWrapper

NdisWrapperHandle 是系統給你的一個 handle,你得把它放在任何函式都可以存取它的地方,因為你會常常需要它。 程式碼如下所示:

NDIS_HANDLE                        hWrapper;
NdisMInitializeWrapper( &hWrapper, DriverObject, RegistryPath, NULL);

VOID NdisMInitializeWrapper(
    OUT PNDIS_HANDLE NdisWrapperHandle,
    IN PVOID SystemSpecific1,
    IN PVOID SystemSpecific2,
    IN PVOID SystemSpecific3
);

NdisMRegisterMiniport

NdisWrapperHandle 就是呼叫 NdisMInitializeWrapper() 之後,系統給你的 handle。MiniportCharacteristics 則是你需要提供給系統的資訊,也就是你支援哪些函式。CharacteristicsLength 則是 MiniportCharacteristics 的大小。

NDIS_STATUS NdisMRegisterMiniport(
    IN NDIS_HANDLE NdisWrapperHandle,
    IN PNDIS_MINIPORT_CHARACTERISTICS MiniportCharacteristics,
    IN UINT CharacteristicsLength
);

MiniportCharacteristics 是個很大的資料結構,下集再講吧。

[Windows Driver] 利用 IRP 內的 ListEntry

這是不小心在 Testcap driver sample 內看到的,原先不知道可以這樣用 IRP 內的 ListEntry(事實上是沒時間,也不想去惡搞這些東西)。

Irp->Tail.Overlay.DriverContext[0] = context;
InsertTailList( &List, &Irp->Tail.Overlay.ListEntry);

context 通常是 device extension,而 testcap 是用 SRB,因為 SRB 內含 device extension,所以用 SRB 比較好。
List 則是你要把這個 IRP 放入的 LIST_ENTRY。


這樣就免於還要用另外一個資料結構去包 LIST_ENTRY 跟 IRP 了,蠻方便的。

至於要取出嘛:

PUCHAR ptr = (PUCHAR) RemoveHeadList(&List);
PIRP pIrp = (PIRP) (((PUCHAR) ptr) - FIELDOFFSET(IRP, Tail.Overlay.ListEntry));

這樣就抓出 LIST_ENTRY 內的 IRP 了。

Wednesday, November 28, 2007

libusb under Ubuntu 7.10

啊啊,好鳥啊,usb null-packet 竟然不發,害我 debug 好幾天(也才兩天啦)。另外,程式要用 sudo 才可以 access usb device 相關的東西,這點也很機車,因為根本就搞不清楚為甚麼啊。然後又接了一個爛 hub,結果 bulk-in 可以,可是 bulk-out 就 timeout,一定要插拔 hub 才可以,這是怎樣....。

Monday, November 19, 2007

在 Testcap 內新增 Audio Capture Pin,並且讓 Windows 的音效裝置可以選擇它[2]

這個還要搭配 inf 啦。參考一下 MSVAD 的寫法:

;======================================================
; COMMON
;======================================================
[MSVAD.I.Wave]
AddReg=MSVAD.I.Wave.AddReg
[MSVAD.I.Wave.AddReg]
HKR,,CLSID,,%Proxy.CLSID%
HKR,,FriendlyName,,%MSVAD.Wave.szPname%

[MSVAD.I.Topo]
AddReg=MSVAD.I.Topo.AddReg
[MSVAD.I.Topo.AddReg]
HKR,,CLSID,,%Proxy.CLSID%
HKR,,FriendlyName,,%MSVAD.Topo.szPname%

;======================================================
; MSVAD_SIMPLE
;======================================================
[MSVAD_Simple.NT]
Include=ks.inf,wdmaudio.inf
Needs=KS.Registration, WDMAUDIO.Registration
CopyFiles=MSVAD_Simple.CopyList
AddReg=MSVAD_Simple.AddReg

[MSVAD_Simple.NT.Interfaces]
AddInterface=%KSCATEGORY_AUDIO%,%KSNAME_Wave%,MSVAD.I.Wave
AddInterface=%KSCATEGORY_RENDER%,%KSNAME_Wave%,MSVAD.I.Wave
AddInterface=%KSCATEGORY_CAPTURE%,%KSNAME_Wave%,MSVAD.I.Wave
AddInterface=%KSCATEGORY_AUDIO%,%KSNAME_Topology%,MSVAD.I.Topo

[MSVAD_Simple.NT.Services]
AddService=msvad_simple,0x00000002,msvad_Simple_Service_Inst

[msvad_Simple_Service_Inst]
DisplayName=%msvad_simple.SvcDesc%
ServiceType=1
StartType=3
ErrorControl=1
ServiceBinary=%10%\system32\drivers\vadSimpl.sys

;======================================================
; COMMON
;======================================================
[Strings]
MSFT="Microsoft"
MfgName="Microsoft Audio DDK"
MSVAD_Simple.DeviceDesc="Microsoft Virtual Audio Device (Simple) (WDM)"

MSVAD.Wave.szPname="MSVAD Wave"
MSVAD.Topo.szPname="MSVAD Topology"

Proxy.CLSID="{17CCA71B-ECD7-11D0-B908-00A0C9223196}"
KSCATEGORY_AUDIO="{6994AD04-93EF-11D0-A3CC-00A0C9223196}"
KSCATEGORY_CAPTURE="{65E8773D-8F56-11D0-A3B9-00A0C9223196}"

KSNAME_Wave="Wave"
KSNAME_Topology="Topology"

msvad_simple.SvcDesc="Microsoft Virtual Audio Device (Simple) (WDM)"
MediaCategories="SYSTEM\CurrentControlSet\Control\MediaCategories"

Simple.NameGuid="{946A7B1A-EBBC-422a-A81F-F07C8D40D3B4}"
Simple.Name="MSVAD (Simple)"

把藍色字改一下加到 Testcap 的 inf 內即可。之前以為不用是因為,亂 try 的時候已經把這些東西寫到 registry 內部了。

Wednesday, September 26, 2007

學編程的人不能不看的好文章

http://blog.csdn.net/dljhf/archive/2005/06/28/406271.aspx

看完只是覺得, 小子你還太嫩了. 看來是沒學過演算法吧, 連基本的演算法都不知道就開始寫程式, 難怪被定的滿頭包.

而且第一題是很基本的歸納法啊, 高中就學過了, 題目沒看仔細就開始作答了, 難怪不會上. 如果每個大陸人都是這樣, 那我應該還有幾年的飯可以吃.....

Thursday, September 20, 2007

Windows Driver Programming[8]

Synchronous and Asynchronous IRP

IRP 只有一種,就是 IRP。會有 Synchronous 跟 Asynchronous 之分,是源自於 IRP 傳送的方式。如果你寫過 Socket 程式,那你應該會知道 Asynchronous I/O 跟 Synchronous I/O 的概念。 簡言之,便是 Synchronous IRP 一傳送,Driver 就得等這個 IRP 完成才能夠繼續執行,而 Asynchronous IRP 會繼續執行下去。 這個考量在 User mode Application 當然沒問題(其實還是會有啦,不然不會有一堆 Asynchronous API 出來),只要把這些東西放到 Thread 內,然後再用一些同步機制就可以搞定。 但是這一招在 Driver 裡面是不行的,Windows Driver 裡有定義每個函式能夠執行的 IRQL,而跑在 Dispatch Level 之上的 thread,是不能夠使用 Synchronous IRP 的, 因為它們不可以睡覺,而 Synchronous IRP 會讓該 thread 去睡覺。

使用 Asynchronous IRP 也有很大的困擾:同步的問題。在 Driver 裡面同步可不比 Application mode,一不小心就 BSOD 了,麻煩。 另外,Asynchronous IRP 必須在 Driver 內部配置跟釋放,而 Synchronous IRP 則是 Driver 配置,系統(I/O manager)釋放。

在這邊以 USB 當例子。

Allocate IRP

  • Synchronous IRP
  • PIRP IoBuildDeviceIoControlRequest(
     IN ULONG IoControlCode,
     IN PDEVICE_OBJECT DeviceObject,
     IN PVOID InputBuffer OPTIONAL,
     IN ULONG InputBufferLength,
     OUT PVOID OutputBuffer OPTIONAL,
     IN ULONG OutputBufferLength,
     IN BOOLEAN InternalDeviceIoControl,
     IN PKEVENT Event,
     OUT PIO_STATUS_BLOCK IoStatusBlock
    );
    

    IoControlCode:這個 IRP 所代表的操作。如果是 USB 的話,請設定為 IOCTL_INTERNAL_USB_SUBMIT_URB。
    DeviceObject:NdisMGetDeviceProperty() 的 NextDeviceObject 參數
    InputBuffer:Device 要輸出的記憶體位置。
    OutputBuffer:要輸入到 Device 的記憶體位置。
    Event:IRP 完成/取消/錯誤之後要設定的 Event。
    IoStatusBlock:Specifies an I/O status block to be set when the request is completed by lower drivers.

    通常這樣就可以了。如果是 USB 的話,Driver 還需要先產生一個 URB:

    VOID UsbBuildInterruptOrBulkTransferRequest(
     IN OUT PURB  Urb,
     IN USHORT  Length,
     IN USBD_PIPE_HANDLE  PipeHandle,
     IN PVOID  TransferBuffer OPTIONAL,
     IN PMDL  TransferBufferMDL OPTIONAL,
     IN ULONG  TransferBufferLength,
     IN ULONG  TransferFlags,
     IN PURB  Link
    );
    

    如果 Driver 要建立的是一個 USB Bulk 或 Interrupt Transfer 的話,請呼叫 UsbBuildInterruptOrBulkTransferRequest()。
    然後再設定 IRP 的參數:

    PIO_STACK_LOCATION nextStack = IoGetNextIrpStackLocation(irp);
    nextStack->Parameters.Others.Argument1 = pUrb;
    

    至於為甚麼要這樣?因為當初根本沒想到會有這種東西出現吧,所以只好用很奇怪的欄位塞入 URB。
  • Asynchronous IRP


  • PIRP IoAllocateIrp(
     IN CCHAR StackSize,
     IN BOOLEAN ChargeQuota
    );
    

    StackSize:NdisMGetDeviceProperty() 的 NextDeviceObject ->StackSize + 1

    然後再設定 IRP 的參數:

    PIO_STACK_LOCATION nextStack = IoGetNextIrpStackLocation(irp);
    nextStack->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROL; // Internal Device Control for USB
    nextStack->Parameters.Others.Argument1 = pUrb; // for USB
    nextStack->Parameters.DeviceIoControl.IoControlCode = IOCTL_INTERNAL_USB_SUBMIT_URB; // for USB submition
    

    最後再設定 IoCompleteRoutine:

    VOID IoSetCompletionRoutine(
     IN PIRP Irp,
     IN PIO_COMPLETION_ROUTINE CompletionRoutine,
     IN PVOID Context,
     IN BOOLEAN InvokeOnSuccess,
     IN BOOLEAN InvokeOnError,
     IN BOOLEAN InvokeOnCancel
    );
    

    NTSTATUS IoCompletion(
     IN PDEVICE_OBJECT DeviceObject,
     IN PIRP Irp,
     IN PVOID Context
    );
    

Send IRP

丟出 IRP 倒是很容易:

NTSTATUS IoCallDriver(
 IN PDEVICE_OBJECT DeviceObject,
 IN OUT PIRP Irp
);

DeviceObject:NdisMGetDeviceProperty() 的 NextDeviceObject 參數
  • Synchronous IRP

  • 呼叫完 IoCallDriver() 之後,如果回傳值是 STATUS_PENDING 的話,還得呼叫 KeWaitForSingleObject() 等待 IRP 完成。

    NTSTATUS KeWaitForSingleObject(
     IN PVOID Object,
     IN KWAIT_REASON WaitReason,
     IN KPROCESSOR_MODE WaitMode,
     IN BOOLEAN Alertable,
     IN PLARGE_INTEGER Timeout OPTIONAL
    );
    

    Object:IoBuildDeviceIoControlRequest() 所傳入的 Event,也就是 Driver 所配置的 Event Object。
  • Asynchronous IRP
  • 不需額外處理。

Cancel IRP

  • Synchronous IRP/Asynchronous IRP
  • BOOLEAN IoCancelIrp(IN PIRP Irp);
    

    一般而言,Calcel IRP 的動作幾乎只會在 Asynchronous IRP 內進行,這是因為 Synchronous IRP 一定會呼叫 KeWaitForSingleObject(),並且指定其 Timeout 為 INFINITE。 但是如果 Timeout 不是 INFINITE 呢?那可能得先為這個 IRP 安裝一個 Complete Routine,如果 KeWaitForSingleObject() 失敗的話, 再呼叫 IoCancelIrp(),然後再呼叫 KeWaitForSingleObject(),等待這個 IRP 被 Cancel。

    為 Synchronous IRP 呼叫 IoCancelIrp() 的機會很少,只是我很倒楣,常常會遇到,因為硬體不穩啊,常常一個簡單的 IO command 就死掉了, 所以 KeWaitForSingleObject() 就得設 Timeout,理論上,Timeout 之後的 Synchronous IRP 是不需要去作特別處理的, 不過如果不取消它,Windows 會有機會 BSOD,所以還是乖乖呼叫 IoCancelIrp() 吧。

Release IRP

  • Asynchronous IRP
  • VOID IoFreeIrp(IN PIRP Irp);
    

    Callers of IoFreeIrp must be running at IRQL <= DISPATCH_LEVEL.
  • Synchronous IRP
  • 不需要呼叫 IoFreeIrp()。

Conclusion

IRP 不困難,困難的是 IRP 的流程。

舉例,如果你的 Driver 發了一堆 Asynchronous IRP,結果在這些 IRP 還沒完成之前,OS 要結束你的 Driver 了, 你得先確保這些 IRP 都已經被正確的完成或取消,才能呼叫 IoFreeIrp(),然後再離開。

又,如果你的 Driver 收到一堆 IRP,結果又把他們放到旁邊不管,在 Driver 結束之前,得先統統完成他們才行。

Wednesday, September 05, 2007

Windows Driver Programming[7]

Relationship between IRP and IO_STACK_LOCATION

通常,Driver programmer 會被IoSkipCurrentIrpStackLocation()、 IoGetCurrentIrpStackLocation() 以及 IoCopyCurrentIrpStackLocationToNext() 給搞得很頭大。 其實,這三個函式都是 macro,把 macro 給展開,秘密大概就揭開了一半。

但是,首先來看看這張圖:



這是 IRP traverse 的時候,資料結構的型態。請注意一點,stack 增加的方向跟一般記憶體不同, + 表示是往前一個 stack,而 - 是往下一個 stack。

先來看 IoGetCurrentIrpStackLocation() 吧:

#define IoGetCurrentIrpStackLocation( Irp ) {
 (Irp)->Tail.Overlay.CurrentStackLocation;
}

它只是取得 IRP.Tail.Overlay.CurrentStackLocation。注意,它的型別為 struct _IO_STACK_LOCATION *。 再來看 IoGetNextIrpStackLocation():

#define IoGetNextIrpStackLocation( Irp ) {
 (Irp)->Tail.Overlay.CurrentStackLocation - 1 )
}

它只是回傳 IRP.Tail.Overlay.CurrentStackLocation - 1。 那 IoSetNextIrpStackLocation() 呢?

#define IoSetNextIrpStackLocation( Irp ) { 
 (Irp)->CurrentLocation--;
 (Irp)->Tail.Overlay.CurrentStackLocation--;
}

它也只是把 IRP.Tail.Overlay.CurrentStackLocation - 1 跟把 IRP.CurrentLocation - 1。

而 IoCallDriver() 做了什麼事呢(由書提供)?

NTSTATUS IoCallDriver(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
 IoSetNextIrpStackLocation(Irp);
 PIO_STACK_LOCATION stack = IoGetCurrentStackLocation(Irp);
 stack->DeviceObject = DeviceObject;
 ULONG fcn = stack->MajorFunction;
 PDRIVER_OBJECT driver = DeviceObject->DriverObject;
 return (*driver->MajorFunction[func])(DeviceObject, Irp);
}

Forwarding IRPs

要丟一個 IRP 的話,得呼叫 IoCallDriver(),而 IoCallDriver() 第一件事情就是先往下一個 stack location 走,然後再設定這個 stack location 的 DeviceObject。 最後,再根據這個 stack location 的 major function,呼叫這個 Device Object 的 Driver Object 所對應的函式。 注意喔,DeviceObject 是指 next DeviceObject,所以根據上圖,如果是 filter driver 要丟 IRP 的話,DeviceObject 得是 functional device object。

當 Driver 收到一個 IRP 時,它最常做的選擇便是,把它往下丟。往下丟有兩種,射後不理跟射後管理。先說射後管理,Driver 得先呼叫 IoCopyCurrentIrpStackLocationToNext(),然後再呼叫 IoCallDriver()。

#define IoCopyCurrentIrpStackLocationToNext( Irp ) {
 PIO_STACK_LOCATION __irpSp;
 PIO_STACK_LOCATION __nextIrpSp;
 __irpSp = IoGetCurrentIrpStackLocation( (Irp) );
 __nextIrpSp = IoGetNextIrpStackLocation( (Irp) );
 RtlCopyMemory( __nextIrpSp, __irpSp, FIELD_OFFSET(IO_STACK_LOCATION, CompletionRoutine));
 __nextIrpSp->Control = 0;
}

只是把現在的 stack location 的內容複製到下一個 stack location(除了 CompletionRoutine 跟 Context 之外)。 注意,IoCopyCurrentStackLocationToNext() 會把 stack location 的 DeviceObject 也一起複製,不過沒關係,IoCallDriver() 會寫入正確的值。

如果是射後不理的方式呢?Driver 得先呼叫 IoSkipCurrentStackLocation(),然後再呼叫 IoCallDriver()。

#define IoSkipCurrentIrpStackLocation( Irp ) { 
 (Irp)->CurrentLocation++;
 (Irp)->Tail.Overlay.CurrentStackLocation++;
}

IoSkipCurrentStackLocation() 只是把 IRP.Tail.Overlay.CurrentStackLocation + 1 跟 IRP.CurrentLocation + 1,也就是往前一個 stack location。 而因為 IoCallDriver 會往下一個 stack location 走,所以結果就是 stack location 並沒有改變,但是 stack location 的 DeviceObject 會變成 lower Device Object。 而造成的後果是,你的 Driver Object 在 stack location 內消失了,而這表示你跟這個 IRP 會變成一點關係都沒有。

通常,Driver 會想知道這個 IRP 完成了沒,所以 Driver 可以呼叫 IoSetCompletionRoutine() 為這個 IRP 設定你專屬的 CompletionRoutine。 注意,IoSetCompletionRoutine() 會把你的 CompletionRoutine 設定在下個 stack location 內,而非現在的 stack location 內。

而當 physical device driver 最後完成這個 IRP,並呼叫 IoCompleteRequest()。

typedef struct _IRP {
 CSHORT Type;
 USHORT Size;

 // Define the common fields used to control the IRP.

 // Define a pointer to the Memory Descriptor List (MDL) for this I/O
 // request.  This field is only used if the I/O is "direct I/O".

 PMDL MdlAddress;

 // Flags word - used to remember various flags.

 ULONG Flags;

 // The following union is used for one of three purposes:
 //    1. This IRP is an associated IRP.  The field is a pointer to a master IRP.
 //
 //    2. This is the master IRP.  The field is the count of the number of
 //       IRPs which must complete (associated IRPs) before the master can
 //       complete.
 //
 //    3. This operation is being buffered and the field is the address of
 //       the system space buffer.
 union {
  struct _IRP *MasterIrp;
  __volatile LONG IrpCount;
  PVOID SystemBuffer;
 } AssociatedIrp;

 // Thread list entry - allows queueing the IRP to the thread pending I/O
 // request packet list.
 LIST_ENTRY ThreadListEntry;

 // I/O status - final status of operation.
 IO_STATUS_BLOCK IoStatus;

 // Requestor mode - mode of the original requestor of this operation.
 KPROCESSOR_MODE RequestorMode;

 // Pending returned - TRUE if pending was initially returned as the
 // status for this packet.
 BOOLEAN PendingReturned;

 // Stack state information.
 CHAR StackCount;
 CHAR CurrentLocation;

 // Cancel - packet has been canceled.
 BOOLEAN Cancel;

 // Cancel Irql - Irql at which the cancel spinlock was acquired.
 KIRQL CancelIrql;

 // ApcEnvironment - Used to save the APC environment at the time that the
 // packet was initialized.
 CCHAR ApcEnvironment;

 // Allocation control flags.
 UCHAR AllocationFlags;

 // User parameters.
 PIO_STATUS_BLOCK UserIosb;
 PKEVENT UserEvent;

 union {
  struct {

   union {
    PIO_APC_ROUTINE UserApcRoutine;
    PVOID IssuingProcess;
   };

   PVOID UserApcContext;

  } AsynchronousParameters;

  LARGE_INTEGER AllocationSize;
 } Overlay;

 // CancelRoutine - Used to contain the address of a cancel routine supplied
 // by a device driver when the IRP is in a cancelable state.
 __volatile PDRIVER_CANCEL CancelRoutine;

 // Note that the UserBuffer parameter is outside of the stack so that I/O
 // completion can copy data back into the user's address space without
 // having to know exactly which service was being invoked.  The length
 // of the copy is stored in the second half of the I/O status block. If
 // the UserBuffer field is NULL, then no copy is performed.
 PVOID UserBuffer;

 // Kernel structures
 //
 // The following section contains kernel structures which the IRP needs
 // in order to place various work information in kernel controller system
 // queues.  Because the size and alignment cannot be controlled, they are
 // placed here at the end so they just hang off and do not affect the
 // alignment of other fields in the IRP.

 union {
  struct {
   union {

    // DeviceQueueEntry - The device queue entry field is used to
    // queue the IRP to the device driver device queue.
    KDEVICE_QUEUE_ENTRY DeviceQueueEntry;

    struct {
     // The following are available to the driver to use in
     // whatever manner is desired, while the driver owns the
     // packet.
     PVOID DriverContext[4];
    } ;
   } ;

   // Thread - pointer to caller's Thread Control Block.
   PETHREAD Thread;

   // Auxiliary buffer - pointer to any auxiliary buffer that is
   // required to pass information to a driver that is not contained
   // in a normal buffer.
   PCHAR AuxiliaryBuffer;

   // The following unnamed structure must be exactly identical
   // to the unnamed structure used in the minipacket header used
   // for completion queue entries.
   struct {

    // List entry - used to queue the packet to completion queue, among others.
    LIST_ENTRY ListEntry;

    union {

     // Current stack location - contains a pointer to the current
     // IO_STACK_LOCATION structure in the IRP stack.  This field
     // should never be directly accessed by drivers.  They should
     // use the standard functions.
     struct _IO_STACK_LOCATION *CurrentStackLocation;

     // Minipacket type.
     ULONG PacketType;
    };
   };

   // Original file object - pointer to the original file object
   // that was used to open the file.  This field is owned by the
   // I/O system and should not be used by any other drivers.
   PFILE_OBJECT OriginalFileObject;

  } Overlay;

  // APC - This APC control block is used for the special kernel APC as
  // well as for the caller's APC, if one was specified in the original
  // argument list.  If so, then the APC is reused for the normal APC for
  // whatever mode the caller was in and the "special" routine that is
  // invoked before the APC gets control simply deallocates the IRP.
  KAPC Apc;

  // CompletionKey - This is the key that is used to distinguish
  // individual I/O operations initiated on a single file handle.
  PVOID CompletionKey;

 } Tail;

} IRP, *PIRP;

typedef struct _IO_STATUS_BLOCK {
 union {
  NTSTATUS Status;
  PVOID Pointer;
 };

 // A common use of the Information field is to hold the total number of bytes transferred
 // by an operation such as IRP_MJ_READ that transfers data.

 ULONG_PTR Information;
} IO_STATUS_BLOCK, *PIO_STATUS_BLOCK;
typedef struct _IO_STACK_LOCATION {
 UCHAR MajorFunction;
 UCHAR MinorFunction;
 UCHAR Flags;
 UCHAR Control;

 // The following user parameters are based on the service that is being
 // invoked.  Drivers and file systems can determine which set to use based
 // on the above major and minor function codes.

 union {

  // System service parameters for:  NtCreateFile
  struct {
   PIO_SECURITY_CONTEXT SecurityContext;
   ULONG Options;
   USHORT FileAttributes;
   USHORT ShareAccess;
   ULONG EaLength;
  } Create;

  // System service parameters for:  NtReadFile
  struct {
   ULONG Length;
   ULONG Key;
   LARGE_INTEGER ByteOffset;
  } Read;

  // System service parameters for:  NtWriteFile
  struct {
   ULONG Length;
   ULONG Key;
   LARGE_INTEGER ByteOffset;
  } Write;

  // System service parameters for:  NtQueryDirectoryFile
  struct {
   ULONG Length;
   PUNICODE_STRING FileName;
   FILE_INFORMATION_CLASS FileInformationClass;
   ULONG FileIndex;
  } QueryDirectory;

  // System service parameters for:  NtNotifyChangeDirectoryFile
  struct {
   ULONG Length;
   ULONG CompletionFilter;
  } NotifyDirectory;

  // System service parameters for:  NtQueryInformationFile
  struct {
   ULONG Length;
   FILE_INFORMATION_CLASS FileInformationClass;
  } QueryFile;

  // System service parameters for:  NtSetInformationFile
  struct {

   ULONG Length;
   FILE_INFORMATION_CLASS POINTER_ALIGNMENT FileInformationClass;
   PFILE_OBJECT FileObject;

   union {

    struct {
     BOOLEAN ReplaceIfExists;
     BOOLEAN AdvanceOnly;
    };

    ULONG ClusterCount;
    HANDLE DeleteHandle;
   };

  } SetFile;

  // System service parameters for:  NtQueryEaFile
  struct {
   ULONG Length;
   PVOID EaList;
   ULONG EaListLength;
   ULONG EaIndex;
  } QueryEa;

  // System service parameters for:  NtSetEaFile
  struct {
   ULONG Length;
  } SetEa;

  // System service parameters for:  NtQueryVolumeInformationFile
  struct {
   ULONG Length;
   FS_INFORMATION_CLASS FsInformationClass;
  } QueryVolume;

  // System service parameters for:  NtSetVolumeInformationFile
  struct {
   ULONG Length;
   FS_INFORMATION_CLASS FsInformationClass;
  } SetVolume;

  // System service parameters for:  NtFsControlFile
  //
  // Note that the user's output buffer is stored in the UserBuffer field
  // and the user's input buffer is stored in the SystemBuffer field.
  struct {
   ULONG OutputBufferLength;
   ULONG InputBufferLength;
   ULONG FsControlCode;
   PVOID Type3InputBuffer;
  } FileSystemControl;

  // System service parameters for:  NtLockFile/NtUnlockFile
  struct {
   PLARGE_INTEGER Length;
   ULONG POINTER_ALIGNMENT Key;
   LARGE_INTEGER ByteOffset;
  } LockControl;

  // System service parameters for:  NtFlushBuffersFile
  //
  // No extra user-supplied parameters.

  // System service parameters for:  NtCancelIoFile
  // No extra user-supplied parameters.

  // System service parameters for:  NtDeviceIoControlFile
  // Note that the user's output buffer is stored in the UserBuffer field
  // and the user's input buffer is stored in the SystemBuffer field.
  struct {
   ULONG OutputBufferLength;
   ULONG InputBufferLength;
   ULONG IoControlCode;
   PVOID Type3InputBuffer;
  } DeviceIoControl;

  // System service parameters for:  NtQuerySecurityObject
  struct {
   SECURITY_INFORMATION SecurityInformation;
   ULONG Length;
  } QuerySecurity;

  // System service parameters for:  NtSetSecurityObject
  struct {
   SECURITY_INFORMATION SecurityInformation;
   PSECURITY_DESCRIPTOR SecurityDescriptor;
  } SetSecurity;

  // Non-system service parameters.
  //
  // Parameters for MountVolume
  struct {
   PVPB Vpb;
   PDEVICE_OBJECT DeviceObject;
  } MountVolume;

  // Parameters for VerifyVolume
  struct {
   PVPB Vpb;
   PDEVICE_OBJECT DeviceObject;
  } VerifyVolume;

  // Parameters for Scsi with internal device contorl.
  struct {
   struct _SCSI_REQUEST_BLOCK *Srb;
  } Scsi;

  // System service parameters for:  NtQueryQuotaInformationFile
  struct {
   ULONG Length;
   PSID StartSid;
   PFILE_GET_QUOTA_INFORMATION SidList;
   ULONG SidListLength;
  } QueryQuota;

  // System service parameters for:  NtSetQuotaInformationFile
  struct {
   ULONG Length;
  } SetQuota;

  // Parameters for IRP_MN_QUERY_DEVICE_RELATIONS
  struct {
   DEVICE_RELATION_TYPE Type;
  } QueryDeviceRelations;

  // Parameters for IRP_MN_QUERY_INTERFACE
  struct {
   CONST GUID *InterfaceType;
   USHORT Size;
   USHORT Version;
   PINTERFACE Interface;
   PVOID InterfaceSpecificData;
  } QueryInterface;


  // Parameters for IRP_MN_QUERY_CAPABILITIES
  struct {
   PDEVICE_CAPABILITIES Capabilities;
  } DeviceCapabilities;

  // Parameters for IRP_MN_FILTER_RESOURCE_REQUIREMENTS
  struct {
   PIO_RESOURCE_REQUIREMENTS_LIST IoResourceRequirementList;
  } FilterResourceRequirements;

  // Parameters for IRP_MN_READ_CONFIG and IRP_MN_WRITE_CONFIG
  struct {
   ULONG WhichSpace;
   PVOID Buffer;
   ULONG Offset;
   ULONG Length;
  } ReadWriteConfig;

  // Parameters for IRP_MN_SET_LOCK
  struct {
   BOOLEAN Lock;
  } SetLock;

  // Parameters for IRP_MN_QUERY_ID
  struct {
   BUS_QUERY_ID_TYPE IdType;
  } QueryId;

  // Parameters for IRP_MN_QUERY_DEVICE_TEXT
  struct {
   DEVICE_TEXT_TYPE DeviceTextType;
   LCID LocaleId;
  } QueryDeviceText;

  // Parameters for IRP_MN_DEVICE_USAGE_NOTIFICATION
  struct {
   BOOLEAN InPath;
   BOOLEAN Reserved[3];
   DEVICE_USAGE_NOTIFICATION_TYPE Type;
  } UsageNotification;

  // Parameters for IRP_MN_WAIT_WAKE
  struct {
   SYSTEM_POWER_STATE PowerState;
  } WaitWake;

  // Parameter for IRP_MN_POWER_SEQUENCE
  struct {
   PPOWER_SEQUENCE PowerSequence;
  } PowerSequence;

  // Parameters for IRP_MN_SET_POWER and IRP_MN_QUERY_POWER
  struct {
   union {
    ULONG SystemContext;
    SYSTEM_POWER_STATE_CONTEXT SystemPowerStateContext;
   };

   POWER_STATE_TYPE Type;
   POWER_STATE State;
   POWER_ACTION ShutdownType;
  } Power;

  struct {
   ULONG SystemContext;
   POWER_STATE_TYPE Type;
   POWER_STATE State;
   POWER_ACTION ShutdownType;
  } Power;

  // Parameters for StartDevice
  struct {
   PCM_RESOURCE_LIST AllocatedResources;
   PCM_RESOURCE_LIST AllocatedResourcesTranslated;
  } StartDevice;

  // Parameters for Cleanup
  //
  // No extra parameters supplied

  // WMI Irps
  struct {
   ULONG_PTR ProviderId;
   PVOID DataPath;
   ULONG BufferSize;
   PVOID Buffer;
  } WMI;

  // Others - driver-specific
  struct {
   PVOID Argument1;
   PVOID Argument2;
   PVOID Argument3;
   PVOID Argument4;
  } Others;

 } Parameters;

 // Save a pointer to this device driver's device object for this request
 // so it can be passed to the completion routine if needed.
 // IoCallDriver() fills this field.

 PDEVICE_OBJECT DeviceObject;

 // The following location contains a pointer to the file object for this
 // request.

 PFILE_OBJECT FileObject;

 // The following routine is invoked depending on the flags in the above flags field.
 // Use IoSetCompletionRoutine() to set this field.

 PIO_COMPLETION_ROUTINE CompletionRoutine;

 // The following is used to store the address of the context parameter
 // that should be passed to the CompletionRoutine.
 // Use IoSetCompletionRoutine() to set this field.

 PVOID Context;

} IO_STACK_LOCATION, *PIO_STACK_LOCATION;

Windows Driver programming[6]

前面提到,DRIVER_OBJECT 的 MajorFunction function pointer array。這玩意兒要怎麼填?先來看看 IRP_MJ_MAXIMUM_FUNCTION 及其相關的定義吧:

#define IRP_MJ_CREATE                             0x00
#define IRP_MJ_CREATE_NAMED_PIPE                  0x01
#define IRP_MJ_CLOSE                              0x02
#define IRP_MJ_READ                               0x03
#define IRP_MJ_WRITE                              0x04
#define IRP_MJ_QUERY_INFORMATION                  0x05
#define IRP_MJ_SET_INFORMATION                    0x06
#define IRP_MJ_QUERY_EA                           0x07
#define IRP_MJ_SET_EA                             0x08
#define IRP_MJ_FLUSH_BUFFERS                      0x09
#define IRP_MJ_QUERY_VOLUME_INFORMATION           0x0a
#define IRP_MJ_SET_VOLUME_INFORMATION             0x0b
#define IRP_MJ_DIRECTORY_CONTROL                  0x0c
#define IRP_MJ_FILE_SYSTEM_CONTROL                0x0d
#define IRP_MJ_DEVICE_CONTROL                     0x0e
#define IRP_MJ_INTERNAL_DEVICE_CONTROL            0x0f
#define IRP_MJ_SHUTDOWN                           0x10
#define IRP_MJ_LOCK_CONTROL                       0x11
#define IRP_MJ_CLEANUP                            0x12
#define IRP_MJ_CREATE_MAILSLOT                    0x13
#define IRP_MJ_QUERY_SECURITY                     0x14
#define IRP_MJ_SET_SECURITY                       0x15
#define IRP_MJ_POWER                              0x16
#define IRP_MJ_SYSTEM_CONTROL                     0x17
#define IRP_MJ_DEVICE_CHANGE                      0x18
#define IRP_MJ_QUERY_QUOTA                        0x19
#define IRP_MJ_SET_QUOTA                          0x1a
#define IRP_MJ_PNP                                0x1b
#define IRP_MJ_PNP_POWER                          IRP_MJ_PNP      // Obsolete....
#define IRP_MJ_MAXIMUM_FUNCTION                   0x1b


IRP_MJ_MAXIMUM_FUNCTION 的定義會依據平台不同而有所不同。MajorFunction,顧名思義,就是定義 major function,而每個 major function 裡面還有一堆 minor function(IRP_MN_XXX) 可以實做,喔天啊。不過幸好一個 driver 只需要實做它需要的 major function 就好了。比較重要的當然是 IRP_MJ_READ/IRP_MJ_WRITE 啦。IRP_MJ_POWER 牽涉到電源管理,很重要,但是在 driver 開發前中期是不會去管它的。至於這些 major function 的詳細說明,請參考 DDK 文件,非常詳細。

注意一下,MajorFunction 的型別:

typedef NTSTATUS (*PDRIVER_DISPATCH) (
    IN struct _DEVICE_OBJECT *DeviceObject,
    IN struct _IRP *Irp
);


傳入值一定是 DEVICE_OBJECT 跟 IRP。傳入 DEVICE_OBJECT 是為了取得 DeviceExtension,而 DEVICE_OBJECT 是透過 IoAttachDeviceToDeviceStack() 註冊到 IO Manager,環環相扣,搞清楚就不會摸不著頭緒了。 而 IRP 當然就是希望 driver 處理的東西啦。

至於 IRP 怎麼處理嘛,下一節再說吧。Programming the microsoft windows driver model 花了一整章在說明 IRP 的處理方式耶。

Tuesday, September 04, 2007

Windows Driver programming[5]

Windows driver 永遠是被動者。實做 driver 要用哪種語言呢?當然是 C/C++啦。

既然我永遠都是被動者,那我怎麼知道你呼叫我的時候,我的狀態是什麼?舉個例子來說,當 OS 跟我說, 要我 clean up 的時候,我怎麼知道我要 clean up 什麼東西,有哪些東西是需要等待?Global 變數嗎?這是個好主意。 不過這種方式實在太下流了,連 MS 都不屑使用。這時,當然是 Driver 兩大主角要出場啦:Drvier Object 以及 Device Object。
typedef struct _DEVICE_OBJECT {
 CSHORT Type;
 USHORT Size;
 LONG ReferenceCount;
 struct _DRIVER_OBJECT *DriverObject;
 struct _DEVICE_OBJECT *NextDevice;
 struct _DEVICE_OBJECT *AttachedDevice;
 struct _IRP *CurrentIrp;
 PIO_TIMER Timer;
 ULONG Flags; 
 ULONG Characteristics;
 __volatile PVPB Vpb;
 PVOID DeviceExtension;
 DEVICE_TYPE DeviceType;
 CCHAR StackSize;

 union {
     LIST_ENTRY ListEntry;
     WAIT_CONTEXT_BLOCK Wcb;
 } Queue;

 ULONG AlignmentRequirement;

 KDEVICE_QUEUE DeviceQueue;
 KDPC Dpc;
 ULONG ActiveThreadCount;
 PSECURITY_DESCRIPTOR SecurityDescriptor;
 KEVENT DeviceLock;
 USHORT SectorSize;
 USHORT Spare1;
 struct _DEVOBJ_EXTENSION *DeviceObjectExtension;
 PVOID Reserved;
} DEVICE_OBJECT;

typedef struct _DRIVER_OBJECT {
 CSHORT Type;
 CSHORT Size;

 // The following links all of the devices created by a single driver together on a list, and the Flags word provides an extensible flag
 // location for driver objects.

 PDEVICE_OBJECT DeviceObject;
 ULONG Flags;

 // The following section describes where the driver is loaded.  The count
 // field is used to count the number of times the driver has had its
 // registered reinitialization routine invoked.

 PVOID DriverStart;
 ULONG DriverSize;
 PVOID DriverSection;
 PDRIVER_EXTENSION DriverExtension;

 // The driver name field is used by the error log thread
 // determine the name of the driver that an I/O request is/was bound.

 UNICODE_STRING DriverName;

 // The following section is for registry support.  Thise is a pointer
 // to the path to the hardware information in the registry

 PUNICODE_STRING HardwareDatabase;

 // The following section contains the optional pointer to an array of
 // alternate entry points to a driver for "fast I/O" support.  Fast I/O
 // is performed by invokzing the driver routine directly with separate
 // parameters, rather than using the standard IRP call mechanism.  Note
 // that these functions may only be used for synchronous I/O, and when
 // the file is cached.

 PFAST_IO_DISPATCH FastIoDispatch;

 // The following section describes the entry points to this particular
 // driver.  Note that the major function dispatch table must be the last
 // field in the object so that it remains extensible.

 PDRIVER_INITIALIZE DriverInit;
 PDRIVER_STARTIO DriverStartIo;
 PDRIVER_UNLOAD DriverUnload;
 PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];

} DRIVER_OBJECT;

typedef struct _DRIVER_EXTENSION {

 // Back pointer to Driver Object

 struct _DRIVER_OBJECT *DriverObject;

 // The AddDevice entry point is called by the Plug & Play manager
 // to inform the driver when a new device instance arrives that this
 // driver must control.

 PDRIVER_ADD_DEVICE AddDevice;

 // The count field is used to count the number of times the driver has
 // had its registered reinitialization routine invoked.

 ULONG Count;

 // The service name field is used by the pnp manager to determine
 // where the driver related info is stored in the registry.

 UNICODE_STRING ServiceKeyName;

 // Note: any new shared fields get added here.

} DRIVER_EXTENSION, *PDRIVER_EXTENSION;

很複雜對不對?不用擔心啦,先看 DEVICE_OBJECT,有沒有看到 DeviceExtension 啊?這個就是 Windows Driver 的奧妙啦(搞不好大家都是這樣玩)。 它的型別是 PVOID,也就是 void *。為甚麼要宣告成 PVOID,那是因為你的東西都可以放在裡面,而且不管是那一種 Driver,都可以把它自己需要紀錄的東西放到 DeviceExtension 內。 而且 Windows 保證,每當呼叫到你的 Driver 所提供的 function 時,一定會把 DEVICE_OBJECT 放到參數內,所以你可以不必擔心怎樣知道你的 Driver 的狀態啦。

接下來得進入正題,Driver 的 entry point:

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)

DriverEntry 就跟 C 的 main 一樣,只是 C 的 runtime 認 main,而 Windows 認 DriverEntry 罷了。 在 DriverEntry 內要做什麼事呢?記住,IN 表示這個參數是 OS 給的,表示我們要利用這個參數作某些事情,或是把這個參數給設定好。

回頭看 DRIVER_OBJECT,我們要填的東西可多了:有 DriverUnload、AddDevice function、以及 MajorFunction function pointer array。

DriverUnload 很簡單,就是 undo DriverEntry()。而 AddDevice 會等到 OS 真的要把你的 Driver instantiate 的時候,才會呼叫。MajorFunction array 可多了,基本上,OS 會透過這個 function pointer array 來呼叫你填入的 function。

DriverEntry() 呼叫完,OS 會接著呼叫你剛剛在 DRIVER_OBJECT 內填入的 AddDevice():

NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)

DriverObject 就是 DriverEntry() 傳入的 DriverObject。pdo 呢,就是你的下家的 DeviceObject。 要知道,WDM driver stack 是由下往上長的,所以你會有下家幫你處理 IRP,而你的上家也會把 IRP 丟給你處理。 而要把 IRP 丟給下家,得先把這個 pdo 給記起來才行。記到哪裡?當然是萬惡的 DeviceExtension 啦。

問題來啦,DRIVER_OBJECT 是 OS 給的,那 DEVICE_OBJECT 呢?當然是呼叫 IoCreateDevice() 啦:

NTSTATUS IoCreateDevice(
    IN PDRIVER_OBJECT  DriverObject,
    IN ULONG  DeviceExtensionSize,
    IN PUNICODE_STRING  DeviceName  OPTIONAL,
    IN DEVICE_TYPE  DeviceType,
    IN ULONG  DeviceCharacteristics,
    IN BOOLEAN  Exclusive,
    OUT PDEVICE_OBJECT  *DeviceObject
    );

注意一下,DeviceExtensionSize 是指 OS allocate 出來的 DEVICE_OBJECT 所含的 DeviceExtension 大小。 換句話說,DeviceExtension 是 OS 幫你配置出來的。取得 DEVICE_OBJECT 之後,呼叫 IoAttachDeviceToDeviceStack(),把你的 DeviceObject 放到你的下家之上。大功告成。

當然,在 AddDevice() 內還要初始化許多東西,包括 spinlock/event、Dpc for ISR、double-linked list、DeviceExtension 等等的 resource。

Windows Driver programming[4]

IRQ sharing

共享 IRQ 的意思就是共享 IRQ Line。共享 IRQ 的 Driver 在收到 Interrupt 的時候,得檢查這個 Interrupt 是不是自己的 Interrupt, 因為這個 Interrupt source 有可能是來自別的 Device。

Bus Master

一般而言,可以主動存取 Bus 的裝置就稱為 Bus Master。

Scatter-Gather DMA

跟 MDL 類似,當傳輸 DMA 時,可以一次傳輸不連續的 Bus address. 這個要硬體支援才行。基本上,DMA 傳輸都是連續的 Bus address。一次傳輸多筆不連續的 Bus address 資料需要額外的硬體支援。

IRP(I/O Request Packet)

IRP 的概念跟用法在 MSDN 的 Handling IRPs 一節裡有詳盡的描述。不過看得的人不是已經懂了,就是看了也不懂。我儘量寫的清楚一點。

IRP( I/O Request Packet),顧名思義,就是一個要求做 IO 的封包。那為甚麼文件寫的這麼複雜?因為 WDM driver 是屬於疊疊樂型的, 所以會變成你的 Driver 的上方還有許多 Driver,下方也有很多 Driver。

如果你的 Driver 需要對 Device 做 IO 的話,因為 Driver 並沒有直接面對硬體(下面還有許多 Driver!),所以變成你得透過這些 Driver 幫你轉送這些 IRP 才能把你要的轉給 Device。 並且因為 Driver 的種類太多了,MS 無法為每種 Driver 定 IRP 的 routing 架構,所以只好訂出一般化的 IRP routing 方式,而後果就是,很多名詞變得很抽象,入門者光看這些名稱根本無法瞭解文件在寫甚麼。

但是這樣做好處太多了:
  • 你可以在 Driver 上下插入 Driver,監控進出的 IRP。不爽還可以偷偷把 IRP 幹掉。

壞處就是,架構變得很複雜,跟效能的損失。

而處理 IRP 變成 Driver 最最重要的部份,光是需要注意的便有: Synchronous/Asynchronous IRP、IRP 的配置、IRP 的傳送、取消 IRP、釋放 IRP 等工作。

Synchronous and Asynchronous IRP

IRP 只有一種,就是 IRP。會有 Synchronous 跟 Asynchronous 之分,是源自於 IRP 傳送的方式。

如果你寫過 Socket 程式,那你應該會知道 Asynchronous I/O 跟 Synchronous I/O 的概念。簡言之,便是 Synchronous IRP 一傳送,Driver 就得等這個 IRP 完成才能夠繼續執行, 而 Asynchronous IRP 會繼續執行下去。這個考量在 User mode Application 當然沒問題(其實還是會有啦,不然不會有一堆 Asynchronous API 出來), 只要把這些東西放到 Thread 內,然後再用一些同步機制就可以搞定。

但是這一招在 Driver 裡面是不行的,Windows Driver 裡有定義每個函式能夠執行的 IRQL,而跑在 Dispatch Level 之上的 thread,是不能夠使用 Synchronous IRP 的,因為它們不可以睡覺,而 Synchronous IRP 會讓該 thread 去睡覺。

使用 Asynchronous IRP 也有很大的困擾:同步的問題。在 Driver 裡面同步可不比 Application mode,一不小心就 BSOD 了,麻煩。另外,Asynchronous IRP 必須在 Driver 內部配置跟釋放,而 Synchronous IRP 則是 Driver 配置,系統(I/O manager)釋放。

Windows Driver programming[3]

MDL (memory descriptor list)

Programming the microsoft windows driver model 對於 MDL 的描述不算多,它只有在需要的時候才會提出一些操作 MDL 的 function。 相信大多數人在對於 MDL 根本就還沒頭緒的狀況下,應該會很火大吧?以下是 MDL 的資料結構:

typedef struct _MDL {
    struct _MDL *Next;
    CSHORT Size;
    CSHORT MdlFlags;
    struct _EPROCESS *Process;
    PVOID MappedSystemVa;
    PVOID StartVa;
    ULONG ByteCount;
    ULONG ByteOffset;
} MDL, *PMDL;

MDL 只是一個對實體記憶體的描述,但是因為系統跟 Driver 都是使用虛擬記憶體,所以 MDL 就是把虛擬記憶體『映射』到實體記憶體(from DDK)。

這樣講是很模糊的,其實 MDL 的作用很簡單:當 Driver 要存取某個記憶體位置時,確保 MDL 所描述的記憶體位置不會引起 page fault。 為甚麼?因為『虛擬記憶體』的關係。Driver 存取的是虛擬記憶體,所以當系統丟給 Driver 一塊虛擬記憶體位置,而 Driver 如何知道這塊記憶體所代表的資料到底是在 RAM 還是 Disk(被 paged out) 內?

在 NDIS 裡面,Driver 是不需要去關心系統丟給 Driver 的記憶體到底是如何,反正一定是在 RAM 裡面就是了。但是在 I/O Control 內的 Direct I/O method 裡面, 這可不一定,因為這個記憶體位置可能是指到某個應用程式的虛擬記憶體,而應用程式的虛擬記憶體是非常有可能被 paged out。 所以 Driver 得呼叫 MmGetSystemAddressForMdlSafe() 來鎖定這塊記憶體(系統得把把資料由 Disk 載入到 RAM)。 寫個小程式就可以證明這一點:

PVOID    buf = NULL;
PMDL     mdl = NULL;
NDIS_STATUS    ndis_status;

ndis_status = NdisAllocateMemoryWithTag(&buf, 1000, 0x1111L);
if (ndis_status != NDIS_STATUS_SUCCESS){
    DbgPrint("NetVMini : Can not allocate test Memory for MDL.\n");
} else {
    mdl = IoAllocateMdl(buf, 1000, FALSE, FALSE, NULL);
    if (mdl != NULL) {
        MmBuildMdlForNonPagedPool(mdl);
        DbgPrint("buf = 0x%p\n", buf);
        DbgPrint("MDL.Size = 0x%X\n", mdl->Size);
        DbgPrint("MDL.MdlFlags = 0x%X\n", mdl->MdlFlags);
        DbgPrint("MDL.MappedSystemVa = 0x%p\n", mdl->MappedSystemVa);
        DbgPrint("MDL.StartVa = 0x%p\n", mdl->StartVa);
        DbgPrint("MDL.ByteCount = 0x%X\n", mdl->ByteCount);
        DbgPrint("MDL.ByteOffset = 0x%X\n", mdl->ByteOffset);
        IoFreeMdl(mdl);
        mdl = NULL;
    }
    NdisFreeMemory(buf, 1000, 0);
}

這個程式只是配置一塊 non-paged 記憶體,然後用一個 MDL 來描述它。 下面是程式的結果:

    00000008    0.00103058    buf = 0x85322008   
    00000009    0.00104175    MDL.Size = 0x20   
    00000010    0.00104985    MDL.MdlFlags = 0xC   
    00000011    0.00106243    MDL.MappedSystemVa = 0x85322008   
    00000012    0.00107919    MDL.StartVa = 0x85322000   
    00000013    0.00109120    MDL.ByteCount = 0x3E8   
    00000014    0.00110321    MDL.ByteOffset = 0x8    

buf = MappedSystemVa = StartVa + ByteOffset. Eureka!

看起來直接用 NdisAllocateMemoryWithTag() 所得到指標還比較好? MDL 還是有好處的啦,至少它可以串起一堆 MDL,當系統需要給 Driver 一堆記憶體區塊,而這些記憶體區塊全都是不連續的,那 MDL 就會很好用(NDIS_BUFFER 就是如此)。 而且在 NDIS 6.0 裡面,NET_BUFFER 只能使用 MDL。

跟 MDL 相關的函式有:

PMDL IoAllocateMdl(
    IN PVOID VirtualAddress,
    IN ULONG Length,
    IN BOOLEAN SecondaryBuffer,
    IN BOOLEAN ChargeQuota,
    IN OUT PIRP Irp OPTIONAL
    );

VirtualAddress:這個 MDL 所描述的虛擬記憶體位置。
Length:這個 MDL 所描述的記憶體大小。

DDK 裡面特別強調,如果 Driver 希望建立的 MDL 是映射到 Driver 自己配置的 Non-Paged 記憶體的話,Driver 還得呼叫 MmBuildMdlForNonPagedPool()。 這是因為 IoAllocateMdl() 只有配置記憶體,但是並沒有 Build MDL(好模糊的說法)。 NDIS 6.0 有提供 NdisAllocateMdl(),它提供一氣呵成的服務。

VOID MmBuildMdlForNonPagedPool(
    IN OUT PMDL MemoryDescriptorList
    );

The MmBuildMdlForNonPagedPool routine receives an MDL that specifies a virtual memory buffer in nonpaged pool, and updates it to describe the underlying physical pages. The MDL virtual address that is input must be within the nonpaged portion of system space, such as memory allocated by ExAllocatePoolWithTag with PoolType = NonPagedPool.

VOID IoBuildPartialMdl(
    IN PMDL SourceMdl,
    IN OUT PMDL TargetMdl,
    IN PVOID VirtualAddress,
    IN ULONG Length
    );
VOID IoFreeMdl(
    IN PMDL Mdl
    );
ULONG MmGetMdlByteCount(IN PMDL  Mdl){
    return Mdl->ByteCount;
}

回傳這個 MDL 的大小。

ULONG MmGetMdlByteOffset(IN PMDL  Mdl) {
    return Mdl->ByteOffset;
}

回傳這個 MDL 離 StarVa 有多遠。

PVOID MmGetMdlVirtualAddress(IN PMDL  Mdl) {
    return ((PVOID) ((PCHAR) ((Mdl)->StartVa) + (Mdl)->ByteOffset));
}

取得 MDL 的虛擬記憶體位置。DDK 特別講明,Lower-Level Driver 不可以直接把這個 Address 拿來使用,因為這有可能是 user-space 的記憶體位置。因此,Driver 必須呼叫 MmGetSystemAddressForMdlSafe() 來取得並鎖定這個 Address 所對應到的 system-space 的記憶體位置。

PVOID MmGetSystemAddressForMdlSafe(IN PMDL Mdl, IN MM_PAGE_PRIORITY Priority) {
    if (Mdl->MdlFlags & (MDL_MAPPED_TO_SYSTEM_VA | MDL_SOURCE_IS_NONPAGED_POOL)) {
        return Mdl->MappedSystemVa;
    } else {
        return MmMapLockedPagesSpecifyCache(Mdl, KernelMode, MmCached, NULL, FALSE, Priority);
    }
}

這個函式是個 Macro。程式碼很簡單,如果這個 MDL 已經被鎖定並映射,直接回傳 MappedSystemVa,否則就鎖定並映射它。

VOID MmInitializeMdl(IN PMDL  MemoryDescriptorList, IN PVOID  BaseVa, IN SIZE_T  Length){
    MemoryDescriptorList->Next = (PMDL) NULL;
    MemoryDescriptorList->Size = (CSHORT)(sizeof(MDL) + sizeof(PFN_NUMBER) * ADDRESS_AND_SIZE_TO_SPAN_PAGES((BaseVa), (Length))));
    MemoryDescriptorList->MdlFlags = 0;
    MemoryDescriptorList->StartVa = (PVOID) PAGE_ALIGN((BaseVa));
    MemoryDescriptorList->ByteOffset = BYTE_OFFSET((BaseVa));
    MemoryDescriptorList->ByteCount = (ULONG)(Length);
}

這段程式碼已經對 MDL 說明的很清楚了。MDL 的目的是要串起多個分散的記憶體,而 MnInitializeMdl() 只是串起第一個記憶體位置。

VOID MmPrepareMdlForReuse(IN PMDL  MDL) {
    if ((MDL->MdlFlags & MDL_PARTIAL_HAS_BEEN_MAPPED) != 0) {
        MmUnmapLockedPages( MDL->MappedSystemVa, MDL );
    }
}

Very few drivers call this routine (DDK says).

VOID MmProbeAndLockPages(
    IN OUT PMDL  MemoryDescriptorList,
    IN KPROCESSOR_MODE AccessMode,
    IN LOCK_OPERATION Operation
    );

這似乎是用在 Direct IO method 上。它會映射並鎖定 MDL所描述的虛擬記憶體。

ULONG MmSizeOfMdl(
    IN PVOID Base,
    IN SIZE_T Length
    );

The MmSizeOfMdl routine returns the number of bytes to allocate for an MDL describing a given address range.

VOID MmUnmapLockedPages(
    IN PVOID BaseAddress,
    IN PMDL  MemoryDescriptorList
    );

解除 MmProbeAndLockPages() 或 MmMapLockedPagesSpecifyCache() 所造成的影響。

Monday, September 03, 2007

Windows Driver programming[2]

IRQL(Interrupt Request Level)

Microsoft 訂出了 32 個 IRQL,但是呢,基本上 Driver 只會碰到三個 IRQ Level:Passive、Dispatch、Device(DIRQL)。 IRQL 高的 Thread 不會被 IRQL 低的 Thread 中斷。跑在 Passive Level 的 thread,限制最少。 而跑在 Dispath Level 的 Thread,則是不可以 wait event/semaphore/mutex,但是可以 busy waiting。 而跑在 DIRQL 的 Thread,最好連 busy waiting 都不要。

Microsoft 已經訂出所有函式能夠執行的 IRQL,Driver 必須遵照指示。為甚麼會有 IRQL?我相信這是大多數人在看 Programming the microsoft windows driver model 這本書時, 一開始會有的疑問。這牽涉到 OS threading model。要知道,CPU 一次只能執行一個指令,這代表,CPU 一次只能執行一個 thread, 好吧,不要考慮雙 CPU 的情況,畢竟,SMP 的狀況,連 DDK 都講的不清不楚。而執行中的 thread,又有分重要跟不重要(處理 hardware interrupt 的 thread 一定比處理 software interrupt 跟 service call 的 thread 重要吧?)。

當重要的 thread 在處理事情時,一定不會希望被別人搶走 CPU 的執行權吧?而這就是 IRQL 的概念:IRQL 高的 thread 不會被 IRQL 低的 thread 搶走 CPU 使用權。 這其實很簡單,只是這跟 driver 有什麼關係?喔,driver 也者,被動呼叫也,所以它自己是不會去搶 CPU 的,只有某些 OS thread 需要你的 driver 的時候,才會呼叫你。

OS thread 有分重要跟不重要吧?當重要的 thread 呼叫你的時候,結果你卻跑去睡覺,或是不重要的 thread 呼叫你,結果你卻引起 page fault,這都是不被允許的。舉個比較明確的例子(DDK 都是用那些非常抽象的語法), 當 Windows 的某個重要的 thread 在 disable interrupt 之後,呼叫你的 driver,結果你卻在裡面睡大覺,結果就是沒有任何 interrupt 會被處理,OS 就跟當掉沒兩樣,因為它期待你很快就會結束,結果你卻不鳥它。

而另外一個例子,非常容易發生的例子,你一定會遇到的例子,『IRQL less than or equal』,這通常表示,某個不重要的 thread 呼叫你的 driver,可是你的 driver 卻存取一些不在實體記憶體內的資料。

什麼叫做『不在實體記憶體內的資料』?就是你要存取的記憶體位置是非法的,尚未被載入到 ram。有這種事嗎?有的,OS 課本提到的虛擬記憶體。虛擬記憶體的內容有可能是在 disk,或是 ram 內。 而一般而言,driver 直接存取在 disk 內的虛擬記憶體內容,都會產生 page fault,而很不幸的,某些 IRQL 不能允許 page fault 產生,因為它的 IRQL 比處理 page fault 的 thread 還要高,而 IRQL 高的 thread 是不會被 IRQL 低的 thread 搶走 CPU 使用權, 這表示一旦發生 page fault,沒人可以處理這個問題,BSOD 直接產生。所幸,BSOD 跟 kernel dump 會跟我們講問題發生點,所以這個問題很好解決。

基本上,Windows driver 是一堆 function 的集合,等著被呼叫。所以搞清楚每個 function 執行的 IRQL,跟每個 OS service call 的 IRQL 是很重要的。

Windows Driver programming[1]

Windows Driver programming 是我在 rxxltxk 被冷凍時(2006/07~2006/12)無聊研究的題材。 當時剛好有顆 IC 要出來,剛好最會寫 driver 的人走了,剛好被冷凍,只好自己玩玩 Windows Driver。

需要的工具

1. 編譯工具

  • Windows Driver Development Kit(DDK):這個去 Microsoft 官方網站抓,不過要錢。現在加入 Microsoft 的測試計畫可以免費下載。
  • Soft -Ice內建在 DriverStudio 中,去 Compuware 公司抓,這也要錢。 不過當你的 driver 在某個地方一定會讓系統整個當掉的時候,你會發現 Soft-Ice 真的很好用。 Microsoft 有個 kernel mode debugger,不過它只支援 remote debug,這表示你得透過 com port,用另外一台電腦 debug, 雖然它支援 USB remote debug,不過一樣很麻煩,光設定就要人命了。 不過 Soft-Ice也不是萬能的,至少它在執行的時候,會把整個系統停住,這會導致一些很奇怪的現象。 另外,Soft-Ice 跟某些 CPU 會很不合,一開就死。如果你的 CPU 支援 hyper-threading,最好把這個功能關掉。 以上都是鬼扯....SoftIce 的相容性相當不好,導致只好改用 Windows debugging tool,結果發覺意外的好用。當時覺得不好用是因為,公司不給我另外一台機器啊(被冷凍的人還要機器幹嘛?)。windows debugger tool 不用錢喔。而且它還可以分析 kernel dump file,然後跟你的 source code 作連結,幾乎都可以抓出程式死在哪裡。
  • DbgView:當你的 driver 呼叫 DbgPrint()/KdPrint() 時,它會把字串轉到 DbgView 的視窗中,這個在 local debug 的時候很有用。你可以在 http://www.sysinternals.com/Utilities/DebugView.html 下載,不用錢。

2. 書

  • Programming the microsoft windows driver model:這本書一定要看,雖然很難看得懂。你也可以在寫完第一隻 Driver(當然是很爛的 driver)之後再去看,會受益良多。這本書的內容只包含 WDM driver,所以如果你要寫 NDIS driver或 Kernel stream driver,嘿嘿,DDK 繼續 K 吧。
  • Microsoft Windows Internals, Fourth Edition:這算是 OS 參考書,有一些概念是很值得看的。
  • Advanced Windows Debugging:大部分介紹如何使用 Windows Debugging Tools,很實用的一本書。

codeblock