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 了。

codeblock