- 7.2 打开与关闭适配器的幕后
- defne PCAP_OPENFLAG_PROMISCUOUS 1
- defne PCAP_OPENFLAG_DATATX_UDP 2
- defne PCAP_OPENFLAG_NOCAPTURE_RPCAP 4
- defne PCAP_OPENFLAG_NOCAPTURE_LOCAL 8
- defne PCAP_OPENFLAG_MAX_RESPONSIVENESS 16
- ifdef WIN32
- endif//WIN32
- ifdef WIN32
- endif
- defne DEVICE_PREFIX"\Device\"
- defne NPF_DRIVER_NAME"NPF"
- defne NPF_SERVICE_DESC"WinPcap Packet Driver("NPF_DRIVER_NAME")"
- defne NPF_DRIVER_COMPLETE_PATH
- ifdefX86
- endif//X86
- ifdef HAVE_BUGGY_TME_SUPPORT
- endif//HAVE_BUGGY_TME_SUPPORT
- ifdef HAVE_BUGGY_TME_SUPPORT
- endif//HAVE_BUGGY_TME_SUPPORT
- ifdef HAVE_BUGGY_TME_SUPPORT
- endif//HAVE_BUGGY_TME_SUPPORT
7.2 打开与关闭适配器的幕后
图7-3所示为与打开及关闭本地主机适配器相关的重要函数。后续各节将会依据从用户空间到内核空间的顺序,来详细描述各重要函数的具体实现。
图 7-3 重要函数的调用关系图
7.2.1 打开适配器的实现
WinPcap打开适配器的实现主要依赖于wpcap.dll库中的pcap_activate_win32函数,而该函数主要依赖于Packet.dll库所提供的PacketOpenAdapter函数,不过,最终是在内核空间的NPF_Open函数中调用NDIS库NdisOpenAdapter函数来完成的。
打开适配器的主要函数调用关系图,如图7-4所示。下面将详细分析WinPcap中相应函数的具体设计与实现。
图 7-4 打开适配器的主要函数调用关系图
1.wpcap.dll库中打开适配器的实现
下面对wpcap.dll库中各相关函数的具体实现进行详细分析。
(1)pcap_open函数
pcap_open函数会打开一个通用的源,用于捕获操作。该函数的原型如下:
pcap_tpcap_open(const charsource,int snaplen,int fags,
int read_timeout,struct pcap_rmtauthauth,charerrbuf);
上述函数中,参数source是以“\0”结尾的字符串,包含所需打开的捕获源的名称。而该名称必须包含格式化的前缀,并要求符合新“源语法规范”(参见附录A)的规定,也可以为NULL。
为了更容易使用源的语法,应记住下列提示:
❑pcap_findalldevs_ex函数返回的适配器能够直接被pcap_open函数使用。
❑如果使用者想给pcap_open函数传递自己的源字符串,则应调用pcap_createsrcstr函数创建正确的源标识。
参数snaplen表示必须保留数据包的长度。对于过滤器所接收的每个数据包,只有开始部分的snaplen长度字节的内容会被存储到缓冲区中,并被传递给用户层的应用程序中。比如,snaplen等于100,也就意味着每个数据包只有开始的100字节会被缓存。
参数flags用来保存几个捕获数据包所需的标识,它在remote-ext.h文件中定义,如下所示:
defne PCAP_OPENFLAG_PROMISCUOUS 1
defne PCAP_OPENFLAG_DATATX_UDP 2
defne PCAP_OPENFLAG_NOCAPTURE_RPCAP 4
defne PCAP_OPENFLAG_NOCAPTURE_LOCAL 8
defne PCAP_OPENFLAG_MAX_RESPONSIVENESS 16
参数read_timeout是以毫秒为单位的读取超时时间,它用来处理捕获一个数据包后,读操作并不需要立即返回的情况。有时候可能需要等待一些时间以允许捕获更多的数据包,这样用户层的一次读操作就可从操作系统的内核中读取更多的数据包了。注意并不是所有的平台都支持读取超时,如果平台不支持,将忽略读取超时值。
参数auth是一个指向pcap_rmtauth结构体的指针,该结构体保存远程主机上用户所需的认证信息。如果这不是一个远程捕获,该指针可以被设置为NULL。
参数errbuf指向为用户分配的一个缓冲区,它用来存储该函数的错误信息。
pcap_open函数会返回一个pcap_t结构体类型指针,作为后续调用的参数(如pcap_sendpacket函数等),其描述了一个已打开适配器的实例。如果出现问题,函数返回NULL,并通过errbuf变量保存错误信息。
pcap_open函数的主要实现代码如下:
pcap_tpcap_open(const charsource,int snaplen,int fags,int read_timeout,
struct pcap_rmtauthauth,charerrbuf)
{
char host[PCAP_BUF_SIZE],port[PCAP_BUF_SIZE],name[PCAP_BUF_SIZE];
int type;
……
/分析源的种类,是文件、本地主机适配器还是远程主机适配器类型/
if(pcap_parsesrcstr(source,&type,host,port,name,errbuf)==-1)
return NULL;
/依据不同的源类型做不同的处理/
switch(type)
{
//文件类型,直接调用pcap_open_offline函数处理
case PCAP_SRC_FILE:
fp=pcap_open_offine(name,errbuf);
break;
//远程主机适配器,打开远程主机的适配器,设置几个重要标识的值
case PCAP_SRC_IFREMOTE:
……
break;
//本地主机适配器,直接调用pcap_open_live函数,
//然后给NPF驱动设置几个标识
case PCAP_SRC_IFLOCAL:
fp=pcap_open_live(name,snaplen,(fags&
PCAP_OPENFLAG_PROMISCUOUS),read_timeout,
errbuf);
ifdef WIN32
//这些标识仅被Windows支持
if(fp!=NULL&&fp->adapter!=NULL)
{
//如果需要,禁止回环数据包捕获
if(fags&PCAP_OPENFLAG_NOCAPTURE_LOCAL)
{
if(!PacketSetLoopbackBehavior(fp->adapter,
NPF_DISABLE_LOOPBACK))
{//设置失败,函数返回
……
}
}
//如果需要,设置一次读操作所需获得的最小数据长度
//(单位为字节)
if(fags&PCAP_OPENFLAG_MAX_RESPONSIVENESS)
{
if(!PacketSetMinToCopy(fp->adapter,0))
{//设置失败,函数返回
……
}
}
}
endif//WIN32
break;
default:
//不支持的源类型
strcpy(errbuf,"Source type not supported");
return NULL;
}
return fp;
}
函数首先调用pcap_parsesrcstr函数分析源的种类是文件、本地主机适配器还是远程主机适配器,并依据不同的源类型做不同的处理。
如果是文件类型,直接调用pcap_open_offline函数处理,函数的实现参见第12章。
如果是本地主机适配器类型,直接调用pcap_open_live函数,然后给NPF驱动设置几个标识。根据需要,可设置禁止环回数据包捕获,还可设置一次读操作所需获得的最小数据长度(单位为字节)。
最后函数返回一个pcap_t结构体指针,供后续调用使用。
pcap_parsesrcstr函数通过解析一个源字符串,来分析源的类型,并返回分析出来的信息(type,host,port,name)。pcap_parsesrcstr函数的原型如下:
int pcap_parsesrcstr(const charsource,inttype,
charhost,charport,charname,charerrbuf)
上述函数中,参数source为要解析的源名称;参数host、port与name分别返回源的主机、端口与名称,针对不同的源类型,该三个参数可能为空值;参数errbuf保存函数的错误信息。
pacp_parsesrcstr函数中还有一个参数type,用于返回源的类型。WinPcap内部用来表示源类型的各标识如下所示:
❑PCAP_SRC_FILE指明为一个文件,表示用户希望从一个本地文件获得数据包。
❑PCAP_SRC_IFLOCAL指明为一个本机适配器,表示用户希望从一个本地主机适配器上获得数据包,它不采用RPCAP协议。
❑PCAP_SRC_IFREMOTE指明为一个远程配器,表示用户希望从一个远程主机配器获得数据包,需要采用RPCAP协议。
(2)pcap_open_live函数
pcap_open_live函数用来打开一个本地主机网络适配器,进行网络数据包的捕获,其原型如下:
pcap_tpcap_open_live(const chardevice,int snaplen,
int promisc,int to_ms,char*ebuf)
上述函数中,参数device是描述所打开网络适配器的字符串。
参数snaplen描述所能捕获数据包的最大字节长度。如果该值小于所捕获数据包的长度,只有开始部分的snaplen个长度字节的内容会被捕获,并且作为数据包的数据。将该参数设为大于网络MTU的值,比如65535,在大多数网络情况下,即可捕获数据包可用的所有数据。
参数promisc描述该适配器是否被设置为混杂模式。
参数to_ms为读超时时间。在支持超时操作的平台上,如果将to_ms设置为0,表示没有超时,也就是说,如果没有数据包到达,读操作将永远不会返回。如果设置成-1,则恰好相反,无论有没有数据包到达,读操作都会立即返回。
参数errbuf指向用户所分配的一个缓冲区,它用来存储该函数的警告与错误信息。
函数返回一个指向pcap_t结构体的指针,它被作为后续调用的参数。
pcap_open_live函数的重要代码如下:
pcap_tpcap_open_live(const charsource,int snaplen,
int promisc,int to_ms,char*errbuf)
{
pcap_t*p;
int status;
/创建pcap_t结构体/
p=pcap_create(source,errbuf);
if(p==NULL)
return(NULL);
/设置捕获数据包的长度/
status=pcap_set_snaplen(p,snaplen);
if(status<0)
goto fail;
/设置混杂模式/
status=pcap_set_promisc(p,promisc);
if(status<0)
goto fail;
/设置读取超时时间/
status=pcap_set_timeout(p,to_ms);
if(status<0)
goto fail;
/如果使用pcap_open_live函数,就设置为1/
p->oldstyle=1;
/激活已打开适配器实例p/
status=pcap_activate(p);
if(status<0)
goto fail;
return(p);
fail:
//出错处理
……
}
该函数首先会创建pcap_t结构体,设置捕获数据包的长度,设置网络适配器为混杂模式,设置读取超时时间,最后则会激活已打开的适配器实例。
pcap_create函数调用pcap_create_common函数来创建一个pcap_t结构体,并设置该结构体相应各方法的具体执行函数。此函数的重要代码如下:
pcap_tpcap_create(const chardevice,char*ebuf)
{
pcap_t*p;
/*检查device是否为Unicode字符串,如果是,则转换为ASCII字符串,并把转换结果传递给
pcap_create_common函数*/
if(strlen(device)==1)
{
size_t length;
char*deviceAscii;
length=wcslen((wchar_t*)device);
deviceAscii=(char*)malloc(length+1);
……
snprintf(deviceAscii,length+1,"%ws",(wchar_t*)device);
p=pcap_create_common(deviceAscii,ebuf);
free(deviceAscii);
}
else
{
p=pcap_create_common(device,ebuf);
}
……
/设置p->activate_op为pcap_activate_win32/
p->activate_op=pcap_activate_win32;
……
return(p);
}
此函数首先检查参数device是否为Unicode字符串,如果是,则转换为ASCII字符串,然后设置p->activate_op函数指针指向pcap_activate_win32函数。
pcap_Create函数代码中涉及的pcap_create_common函数会创建一个pcap_t结构体,并对结构体中的一些字段进行初始化,如果成功将返回指向pcap_t结构体内存的指针。参数source是所要打开的源名称,参数ebuf则存储函数的错误信息。
pcap_create_common函数的重要代码如下:
pcap_tpcap_create_common(const charsource,char*ebuf)
{
pcap_t*p;
/分配pcap_t的内存空间,并初始化清零/
p=malloc(sizeof(*p));
if(p==NULL){
//分配失败,函数退出
}
memset(p,0,sizeof(*p));
/给p->opt.source复制源字符串/
p->opt.source=strdup(source);
if(p->opt.source==NULL){
//失败,函数退出
}
/*
*默认为“不能设置rfmon模式”
*如果被平台支持,可以设置该操作句柄,使用所设置的例程检测设备是否支持
*/
p->can_set_rfmon_op=pcap_cant_set_rfmon;
/*
*一些操作只有在网络适配器实例被激活的状态下才能执行
*在此,把这些操作设为“不支持”的操作,直到该网络适配器实例被激活
*/
p->read_op=(read_op_t)pcap_not_initialized;
p->inject_op=(inject_op_t)pcap_not_initialized;
p->setflter_op=(setflter_op_t)pcap_not_initialized;
p->setdirection_op=(setdirection_op_t)pcap_not_initialized;
p->set_datalink_op=(set_datalink_op_t)pcap_not_initialized;
p->getnonblock_op=(getnonblock_op_t)pcap_not_initialized;
p->setnonblock_op=(setnonblock_op_t)pcap_not_initialized;
p->stats_op=(stats_op_t)pcap_not_initialized;
ifdef WIN32
p->setbuff_op=(setbuff_op_t)pcap_not_initialized;
p->setmode_op=(setmode_op_t)pcap_not_initialized;
p->setmintocopy_op=(setmintocopy_op_t)pcap_not_initialized;
endif
p->cleanup_op=pcap_cleanup_live_common;
/把一些值设为默认的值/
pcap_set_timeout(p,0);
pcap_set_snaplen(p,65535);
p->opt.promisc=0;
p->opt.buffer_size=0;
return(p);
}
pcap_create_common函数首先会分配一个pcap_t结构体的内存空间,并对其清零;然后给p->opt.source复制源字符串,设置p->can_set_rfmon_op操作句柄,并使用所设置的例程pcap_cant_set_rfmon检测设备是否支持rfmon模式;接着它把一些只有在网络适配器实例激活时才能执行的操作函数设置为“不支持”的pcap_not_initialized函数,并把p->cleanup_op清除操作设置为pcap_cleanup_live_common函数,使得与pcap_open_live函数对应的pcap_close函数可被调用,从而正确释放各种资源;最后把读取超时时间设为0,设置捕获的数据包的长度为65535字节,并把混杂模式p->opt.promisc设置为0,把缓冲区大小p->opt.buffer_size设置为0。
pcap_create_common函数代码中所涉及的pcap_cant_set_rfmon函数的实现代码如下:
static int pcapcant_set_rfmon(pcap_t*p_U)
{
return(0);
}
由上可见,系统不支持rfmon模式。
pcap_not_initialized函数的实现代码如下:
int pcap_not_initialized(pcap_t*pcap)
{
/意味着“不初始化”/
return PCAP_ERROR_NOT_ACTIVATED;
}
(3)pcap_activate函数
pcap_open_live函数在最后会调用pcap_activate函数,来激活已打开的适配器实例。pcap_activate函数的实现代码如下:
int pcap_activate(pcap_t*p)
{
int status;
status=p->activate_op(p);
if(status>=0)
p->activated=1;//激活
return(status);
}
上述函数中,p->activate_op(p)会调用pcap_activate_win32函数来为激活网络适配器实例p做各种准备工作,最后设置p->activated=1;正式激活实例p。
现有如下实例代码:
pcap_tpcap_open_live(const charsource,int snaplen,
int promisc,int to_ms,char*errbuf)
{
……
/设置捕获数据包的长度/
status=pcap_set_snaplen(p,snaplen);
if(status<0)
goto fail;
……
/激活已打开的适配器实例p/
status=pcap_activate(p);
……
}
int pcap_set_snaplen(pcap_t*p,int snaplen)
{
if(pcap_check_activated(p))//检测实例p是否激活
{//已激活,不能设置参数p->snapshot
return PCAP_ERROR_ACTIVATED;
}
//未激活,可设置参数p->snapshot
p->snapshot=snaplen;
return 0;
}
int pcap_check_activated(pcap_t*p)
{
if(p->activated){//1表示已激活,0表示未激活
snprintf(p->errbuf,PCAP_ERRBUF_SIZE,"can't perform"
"operation on activated capture");
return-1;
}
return 0;
}
从上面的代码中可见,如果网络适配器实例p已被激活,就不能再设置某些参数了,例如函数pcap_set_snaplen的操作将失败,最终也会导致pcap_open_live函数失败。可以这样认为,WinPcap通过p->activated这个标识来防止对一个已被激活的实例进行参数修改,同时也防止对一个已被激活的实例进行再次打开操作。
(4)pcap_activate_win32函数
pcap_activate_win32函数为激活网络适配器实例做了各种准备工作,它的处理过程如图7-5所示。
图 7-5 pcap_activate_win32函数的处理过程
pcap_activate_win32函数首先会调用wsockinit函数初始化WinSock;然后会调用PacketOpenAdapter函数打开适配器;接着调用PacketGetNetType函数获取网络类型,并根据网络类型设置链路类型(注意,此处我们只分析常规的以太网类型);接下来调用PacketSetHwFilter函数设置网络适配器为混杂模式;然后设置用户层与内核捕获数据的缓冲区大小分别为WIN32_DEFAULT_USER_BUFFER_SIZE(256KB)与WIN32_DEFAULT_KERNEL_BUFFER_SIZE(1MB),并设置内核缓冲区,分配用户缓冲区,进行初始化;最后返回调用。
设置驱动程序只有在存储了至少16KB的数据后,才能复制数据到用户空间,并设置读取超时时间。
设置NPF进行读取的操作函数为pcap_read_win32_npf函数,设置过滤的操作函数为pcap_setfilter_win32_npf函数,设置发送单个数据包的操作函数为pcap_inject_win32函数,同时还设置其他的操作函数。
pcap_activate_win32函数的主要实现代码如下:
static int pcap_activate_win32(pcap_t*p)
{
NetType type;
/初始化WinSock/
wsockinit();
/调用Packet.dll库的PacketOpenAdapter函数打开适配器/
p->adapter=PacketOpenAdapter(p->opt.source);
if(p->adapter==NULL)
{//错误,函数返回
……
}
/调用Packet.dll库的PacketGetNetType函数获取网络类型/
if(PacketGetNetType(p->adapter,&type)==FALSE)
{
//错误,函数返回
……
}
/设置数据链路层类型/
switch(type.LinkType)
{
case NdisMediumWan:
p->linktype=DLT_EN10MB;
break;
case NdisMedium802_3:
p->linktype=DLT_EN10MB;
/*
*这大概是一个真正的以太网捕获;将其数据链路层类型链表设置为
*DLT_EN10MB与DLT_DOCSIS,
*因此一个应用程序可以让你选择它的类型,
*以防万一,正在捕获的DOCSIS网络数据包是由一个Cisco网线调制器终端
*系统发送到以太网上的(它不发送一个以太网协议头到线上,
*而是发送原始的DOCSIS数据帧到线上)
*/
p->dlt_list=(u_int)malloc(sizeof(u_int)2);
/如果分配失败,仅留下空链表/
if(p->dlt_list!=NULL){
p->dlt_list[0]=DLT_EN10MB;
p->dlt_list[1]=DLT_DOCSIS;
p->dlt_count=2;
}
break;
……
default://一个未知适配器假定为以太网适配器
p->linktype=DLT_EN10MB;
break;
}
/设置网络适配器的过滤模式为混杂模式/
if(p->opt.promisc)
{//混杂模式
if(PacketSetHwFilter(p->adapter,
NDIS_PACKET_TYPE_PROMISCUOUS)==FALSE)
{
//错误,函数返回
……
}
}
else
{//非混杂模式
if(PacketSetHwFilter(p->adapter,
NDIS_PACKET_TYPE_ALL_LOCAL)==FALSE)
{
//错误,函数返回
……
}
}
/设置pcap_t结构体中的缓冲区大小/
p->bufsize=WIN32_DEFAULT_USER_BUFFER_SIZE;
/分配在捕获过程中使用的数据包结构体/
if((p->Packet=PacketAllocatePacket())==NULL)
{
//错误,函数返回
……
}
/传统的适配器/
if(!(p->adapter->Flags&INFO_FLAG_DAG_CARD))
{
//如果缓冲区的大小不是显式的设定,那么
//默认为WIN32_DEFAULT_USER_BUFFER_SIZE
if(p->opt.buffer_size==0)
p->opt.buffer_size=
WIN32_DEFAULT_KERNEL_BUFFER_SIZE;
//设置内核NPF的缓冲区
if(PacketSetBuff(p->adapter,p->opt.buffer_size)==FALSE)
{//错误,函数返回
……
}
p->buffer=(u_char*)malloc(p->bufsize);
if(p->buffer==NULL)
{//错误,函数返回
……
}
//在初始化捕获过程中使用的数据包结构体
PacketInitPacket(p->Packet,(BYTE*)p->buffer,
p->bufsize);
//告诉驱动程序只有D存储了至少16KB的数据后,才能复制数据
if(PacketSetMinToCopy(p->adapter,16000)==FALSE)
{
//错误,函数返回
……
}
}
/设置读取超时时间/
PacketSetReadTimeout(p->adapter,p->md.timeout);
/设置各函数指针的实际操作函数/
p->read_op=pcap_read_win32_npf;//设置读取操作函数
p->setfilter_op=pcap_setflter_win32_npf;//设置过滤操作函数
p->setdirection_op=NULL;//没有实现
p->inject_op=pcap_inject_win32;//设置发送单个数据包的操作函数
p->set_datalink_op=NULL;//不能改变数据链路层的类型
p->getnonblock_op=pcap_getnonblock_win32;//获得读操作阻塞状态的函数
p->setnonblock_op=pcap_setnonblock_win32;//设置读操作阻塞状态的函数
p->stats_op=pcap_stats_win32;//获得统计信息的函数
p->setbuff_op=pcap_setbuff_win32;//设置内核空间捕获缓存大小的函数
p->setmode_op=pcap_setmode_win32;//设置驱动的工作模式的函数
p->setmintocopy_op=pcap_setmintocopy_win32;//设置一次读操的最小数量
p->cleanup_op=pcap_cleanup_win32;//关闭适配器,释放资源
return(0);
bad:
pcap_cleanup_win32(p);
return(PCAP_ERROR);
}
上述代码中,wsockinit函数主要调用WSAStartup系统函数实现Winsocket的初始化。只有把p->cleanup_op清除操作设置为pcap_cleanup_win32函数,才能使与pcap_open_live函数对应的pcap_close函数调用该函数正确释放各种资源。
2.Packet.dll库中打开适配器的实现
下面将对Packet.dll库中的各相关函数的具体实现进行详细分析。
(1)关键结构体
❑结构体_ADAPTER:描述一个已打开的适配器。该结构体对Packet.dll库中的函数非常重要,不过对用户而言其中的大部分成员都可不用关心,该库原本就是为了避免用户处理底层的参数而设计的。其具体定义如下:
typedef struct_ADAPTER
{
//指向一个NPF驱动程序实例的句柄
HANDLE hFile;
//保存当前打开网络适配器的名称
CHAR SymbolicLink[MAX_LINK_NAME_LENGTH];
//每个数据包重复发送的次数
int NumWrites;
//与适配器读调用关联的通知事件
HANDLE ReadEvent;
//读超时时间
UINT ReadTimeOut;
CHAR Name[ADAPTER_NAME_LENGTH];
PWAN_ADAPTER pWanAdapter;
//适配器标识
UINT Flags;
}ADAPTER,*LPADAPTER;
❑结构体_PACKET:包含了从驱动程序处得到的一组数据包。该结构体定义了与每个递送给应用程序的数据包相关联的头信息。其具体定义如下:
typedef struct_PACKET
{
//存储数据包的缓冲区
//想了解缓冲区数据的组织形式,可参见PacketReceivePacket
PVOID Buffer;
//缓冲区的长度
UINT Length;
//缓冲区中的有效字节数,
//如最后调用PacketReceivePacket所接收的数据量
DWORD ulBytesReceived;
}PACKET,*LPPACKET;
❑结构体_PACKET_OID_DATA:包含了一个OID请求。它被PacketRequest函数使用,用于发送一个OID请求给网络接口卡(NIC)驱动程序。它也能被用来获取诸如适配器的错误计数、MAC地址、多播列表等信息。其具体定义如下:
struct_PACKET_OID_DATA
{
//OID码。参见Microsoft DDK文档或ntddndis.h文件
//获得一个有效编码的完整列表
ULONG Oid;
//Data成员的长度
ULONG Length;
//变长成员,包含传递到适配器或从适配器接收的信息
UCHAR Data[1];
};
typedef struct_PACKET_OID_DATA PACKET_OID_DATA,*PPACKET_OID_DATA;
(2)PacketOpenAdapter函数
在wpcap.dll库中pcap_activate_win32函数调用PacketOpenAdapter函数来打开适配器。所以PacketOpenAdapter函数的作用就是打开一个适配器,其原型如下:
LPADAPTER PacketOpenAdapter(PCHAR AdapterNameWA);
参数AdapterNameWA为一个字符串,其中包含待打开设备的名称。
PacketOpenAdapter函数如果执行成功,则返回一个已被正确初始化的ADAPTER对象指针,否则返回NULL。
该函数的主要实现代码如下:
LPADAPTER PacketOpenAdapter(PCHAR AdapterNameWA)
{
……
/*
*检查适配器的名称是否为ASCII码字符串,
*如果不是(为Unicode码字符串),转换为ASCII码字符串
*/
……
/获得g_AdaptersInfoMutex互斥信号/
WaitForSingleObject(g_AdaptersInfoMutex,INFINITE);
/查找适配器,并打开它/
do
{
//第一次查找给定适配器的PADAPTER_INFO结构体
TAdInfo=PacketFindAdInfo(AdapterNameA);
if(TAdInfo==NULL)
{//没有找到,更新链表,然后执行第二次查找
PacketUpdateAdInfo(AdapterNameA);
TAdInfo=PacketFindAdInfo(AdapterNameA);
}
if(TAdInfo==NULL)
{//出错处理,跳出do……while循环
……
break;
}
……
//找到适配器,检查适配器的类型,查看它是否被正确支持,
//此处只考虑正常的NPF适配器
if(TAdInfo->Flags==INFO_FLAG_DONT_EXPORT)
{//该适配器不允许被导出,跳出循环
……
break;
}
if(TAdInfo->Flags!=INFO_FLAG_NDIS_ADAPTER)
{//打开不明标识的适配器,跳出循环
……
break;
}
//正常的NPF适配器,试图打开它
lpAdapter=PacketOpenAdapterNPF(AdapterNameA);
if(lpAdapter==NULL)
{//打开失败
dwLastError=GetLastError();
}
}while(FALSE);
/释放g_AdaptersInfoMutex互斥信号/
ReleaseMutex(g_AdaptersInfoMutex);
/如果执行了Unicode转换,则释放所用的内存/
……
/函数返回/
if(dwLastError!=ERROR_SUCCESS)
{//函数失败,返回NULL
SetLastError(dwLastError);
return NULL;
}
else
{//函数成功,返回与所打开适配器相关联的结构体指针
return lpAdapter;
}
}
此函数首先会检查适配器的名称是否为ASCII码字符串,如果不是,则执行Unicode码到ASCII码的转换。
然后获得g_AdaptersInfoMutex互斥信号,调用PacketFindAdInfo函数在g_AdaptersInfoList链表中查找适配器。如果第一次没有查到,则调用PacketUpdateAdInfo函数更新该链表,执行第二次查找。如果找到适配器,就检查适配器的类型,查看它是否被正确支持,此处我们只考虑正常使用NPF驱动的适配器。如果是正常的NPF适配器,调用PacketOpenAdapterNPF函数打开它,然后释放g_AdaptersInfoMutex互斥信号,如果执行了Unicode转换,则释放所用的内存。
最后,如果函数执行成功,则返回与所打开适配器相关联的结构体指针lpAdapter,否则返回NULL。
下面,重点介绍PacketOpenAdapterNPF函数。PacketOpenAdapterNPF函数的作用是打开一个使用NPF驱动的适配器,该函数被PacketOpenAdapter与AddAdapter函数作为内部函数调用。此函数的原型如下:
LPADAPTER PacketOpenAdapterNPF(PCHAR AdapterNameA);
上述函数中,参数AdapterNameA为一个字符串,其中包含待打开适配器的名称。
如果PacketOpenAdapterNPF函数执行成功,则返回一个已被正确初始化的ADAPTER对象指针,否则返回NULL。
PacketOpenAdapterNPF函数的处理过程如图7-6所示。
图 7-6 PacketOpenAdapterNPF函数的处理过程
PacketOpenAdapterNPF函数首先会调用OpenSCManager系统函数连接到服务控制管理器(Service Control Manager,SCM),以检查NPF驱动程序是否已经存在,服务是否正在运行。如果NPF驱动程序不存在,则调用PacketInstallDriver函数安装驱动程序。如果服务没有运行,则调用StartService系统函数启动服务。
接下来可以分配并初始化ADAPTER对象了。设置单个数据包发送的次数,NPF驱动程序会依据该值决定一个数据包重复发送的次数,此处设为1次。
接着根据原始的设备名创建NPF设备的名称,并尝试是否能直接打开该适配器。如果打开成功,则分配与该捕获实例相关联的读事件信号,为驱动程序NPF_tap函数设置最大可能的前视缓冲区,然后指明这是一个由NPF.sys管理的设备。
最后返回一个已经正确初始化的ADAPTER对象指针。
PacketOpenAdapterNPF函数的主要实现代码如下:
LPADAPTER PacketOpenAdapterNPF(PCHAR AdapterNameA)
{
LPADAPTER lpAdapter;
……
CHAR SymbolicLinkA[MAX_PATH];
//NPF_DRIVER_NAME定义为"NPF"
CHAR NpfDriverName[MAX_WINPCAP_KEY_CHARS]=NPF_DRIVER_NAME;
CHAR NpfServiceLocation[MAX_WINPCAP_KEY_CHARS];
/连接到服务控制管理器/
scmHandle=OpenSCManager(NULL,NULL,GENERIC_READ);
if(scmHandle==NULL)
{//连接到服务控制管理器失败
error=GetLastError();
}
else
{
//检查NPF服务是否已经存在,如果存在则接下来可以分配并初始化ADAPTER对象
//设置NPF服务的注册表位置
StringCchPrintfA(
NpfServiceLocation,
sizeof(NpfServiceLocation),
"SYSTEM\CurrentControlSet\Services\%s",
NpfDriverName);
//检查NPF注册表的键值是否已存在,如果已经存在,则表示驱动已经安装,
//我们就不需要调用PacketInstallDriver函数安装驱动程序了
KeyRes=RegOpenKeyExA(HKEY_LOCAL_MACHINE,
NpfServiceLocation,
0,
KEY_READ,
&PathKey);
if(KeyRes!=ERROR_SUCCESS)
{//NPF注册表的键值不存在,调用PacketInstallDriver函数
Result=PacketInstallDriver();
}
else
{//NPF注册表的键值已经存在,驱动已经安装
Result=TRUE;
RegCloseKey(PathKey);
}
if(Result)
{//驱动已经存在,检查NPF服务是否正在运行
//打开NPF服务
svcHandle=OpenServiceA(scmHandle,NpfDriverName,
SERVICE_START|SERVICE_QUERY_STATUS);
if(svcHandle!=NULL)
{//获得服务状态
QuerySStat=QueryServiceStatus(svcHandle,
&SStat);
if(!QuerySStat||SStat.dwCurrentState
!=SERVICE_RUNNING)
{//获得服务状态失败或驱动NPF没有运行,启动NPF服务
if(StartService(svcHandle,0,NULL)==0)
{//如果不是服务正在运行或
//服务已经存在的状态,
//就处理错误,函数返回NULL
……
return NULL;
}
}
//驱动NPF已正常运行,关闭服务控制管理器的句柄
CloseServiceHandle(svcHandle);
svcHandle=NULL;
}
else
{//打开NPF服务失败
error=GetLastError();
SetLastError(error);
}
}
else
{
if(KeyRes!=ERROR_SUCCESS)
{//第一次安装驱动程序失败,并且NPF注册表的键值
//不存在,再次安装驱动
Result=PacketInstallDriver();
}
else
Result=TRUE;
if(Result){//NPF的驱动程序已存在
//打开NPF服务
svcHandle=OpenServiceA(scmHandle,
NpfDriverName,SERVICE_START);
if(svcHandle!=NULL)
{//打开NPF服务成功,获取NPF服务的状态
QuerySStat=QueryServiceStatus(svcHandle,&SStat);
if(!QuerySStat||SStat.dwCurrentState!=SERVICE_RUNNING)
{//获得服务状态失败或驱动NPF没有运行,启动NPF服务
if(StartService(svcHandle,0,NULL)==0)
{//如果不是服务正在运行或服务已经存在的状态,
//就处理错误,函数返回NULL
……
return NULL;
}
}
//驱动NPF已正常运行,关闭服务控制管理器的句柄
CloseServiceHandle(svcHandle);
svcHandle=NULL;
}
else{
//打开NPF服务失败,设置错误状态
error=GetLastError();
SetLastError(error);
}
}
}
}
/关闭服务控制管理器的句柄/
if(scmHandle!=NULL)
CloseServiceHandle(scmHandle);
/分配ADAPTER结构体的内存空间/
lpAdapter=(LPADAPTER)GlobalAllocPtr(
GMEM_MOVEABLE|GMEM_ZEROINIT,sizeof(ADAPTER));
if(lpAdapter==NULL)
{//分配失败,函数返回NULL;
……
return NULL;
}
/设置单个数据包发送的次数为1次/
lpAdapter->NumWrites=1;
/根据原始的设备名创建NPF设备的名称/
defne DEVICE_PREFIX"\Device\"
if(LOWORD(GetVersion())==4)
{//操作系统为Windows NT 4、Windows 95、Windows 98、
//或Windows ME的处理
……
}
else
{
if(strlen(AdapterNameA)>strlen(DEVICE_PREFIX))
{
StringCchPrintfA(SymbolicLinkA,MAX_PATH,
"\\.\Global\%s",
AdapterNameA+strlen(DEVICE_PREFIX));
}
else
{
ZeroMemory(SymbolicLinkA,sizeof(SymbolicLinkA));
}
}
/内存清零/
ZeroMemory(lpAdapter->SymbolicLink,sizeof(lpAdapter->SymbolicLink));
/看可否直接打开适配器/
lpAdapter->hFile=CreateFileA(SymbolicLinkA,
GENERIC_WRITE|GENERIC_READ,0,NULL,OPEN_EXISTING,0,0);
if(lpAdapter->hFile!=INVALID_HANDLE_VALUE)
{//打开成功
//分配与该捕获实例相关联的读事件信号,并把它传递给内核驱动,
//并存储在该_ADAPTER结构体中(lpAdapter所指的结构体)
if(PacketSetReadEvt(lpAdapter)==FALSE){
//失败,函数返回NULL
……
return NULL;
}
//为驱动程序Packet_tap(NPF中为NPF_tap)函数
//设置最大可能的前视缓冲区
PacketSetMaxLookaheadsize(lpAdapter);
//指明这是一个由NPF.sys管理的设备
lpAdapter->Flags=INFO_FLAG_NDIS_ADAPTER;
StringCchCopyA(lpAdapter->Name,ADAPTER_NAME_LENGTH,AdapterNameA);
//成功打开适配器,返回一个已经正确初始化的ADAPTER对象的指针
return lpAdapter;
}
/设置错误信息/
……
return NULL;
}
注意 前视缓冲区(lookahead buffer)是tap函数能从NIC驱动程序中直接访问的一部分内存,所以不用执行一次内存复制。
1)PacketInstallDriver函数
PacketInstallDriver函数用于把驱动程序Windows\system32\drivers\npf.sys安装到操作系统中。如果函数PacketInstallDriver执行成功则返回非0值。该函数的主要代码实现如下:
BOOLEAN PacketInstallDriver()
{
……
CHAR driverName[MAX_WINPCAP_KEY_CHARS]=NPF_DRIVER_NAME;
CHAR driverDesc[MAX_WINPCAP_KEY_CHARS]=NPF_SERVICE_DESC;
CHAR driverLocation[MAX_WINPCAP_KEY_CHARS]=
NPF_DRIVER_COMPLETE_PATH;
/连接到服务控制管理器/
scmHandle=
OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS);
if(scmHandle==NULL)
return FALSE;
/给操作系统安装驱动程序的服务/
svcHandle=CreateServiceA(scmHandle,
driverName,
driverDesc,
SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START,
SERVICE_ERROR_NORMAL,
driverLocation,
NULL,NULL,NULL,NULL,NULL);
if(svcHandle==NULL)
{
err=GetLastError();
if(err==ERROR_SERVICE_EXISTS)
{
//npf.sys的服务已经存在
err=0;
result=TRUE;
}
}
else
{
//给npf.sys成功创建了服务
result=TRUE;
}
/安装完毕,释放资源/
if(svcHandle!=NULL)
CloseServiceHandle(svcHandle);
……
CloseServiceHandle(scmHandle);
SetLastError(err);
return result;
}
上述代码中,NPF_DRIVER_NAME、NPF_SERVICE_DESC与NPF_DRIVER_COMPLETE_PATH的定义如下:
defne NPF_DRIVER_NAME"NPF"
defne NPF_SERVICE_DESC"WinPcap Packet Driver("NPF_DRIVER_NAME")"
defne NPF_DRIVER_COMPLETE_PATH
"system32\drivers\"NPF_DRIVER_NAME".sys"
PacketInstallDriver函数使用CreateServiceA系统函数给操作系统安装驱动程序的服务。CreateServiceA函数会创建一个服务对象,它在注册表HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services下创建与服务同名的键值,并把此服务安装到服务控制管理器(SCM)的数据库中。
CreateServiceA函数的原型如下:
__checkReturn
WINADVAPI
SC_HANDLE
WINAPI
CreateServiceA(
__in SC_HANDLE hSCManager,//指向SCM的句柄
__in LPCSTR lpServiceName,//开始的服务名称
__in_opt LPCSTR lpDisplayName,//显示名称
__in DWORD dwDesiredAccess,//服务访问类型
__in DWORD dwServiceType,//服务类型
__in DWORD dwStartType,//什么时候开始服务
__in DWORD dwErrorControl,//服务失败的严重程度
__in_opt LPCSTR lpBinaryPathName,//二进制文件名
__in_opt LPCSTR lpLoadOrderGroup,//服务群的加载顺序
__out_opt LPDWORD lpdwTagId,//lpLoadOrderGroup中的标签标识
__in_opt LPCSTR lpDependencies,//启动服务所依赖的其他服务的名称
__in_opt LPCSTR lpServiceStartName,//账户名
__in_opt LPCSTR lpPassword//账户密码
);
CreateServiceA函数如果执行成功,则会返回指向该服务的句柄,可通过调用CloseServiceHandle函数关闭该句柄。
2)PacketSetReadEvt函数
PacketSetReadEvt函数分配并捕获与实例相关联的读事件信号,同时把它传递给内核驱动程序,存储在一个_ADAPTER结构体中,其原型如下:
BOOLEAN PacketSetReadEvt(LPADAPTER AdapterObject);
上述函数中,参数AdapterObject指向一个适配器。
如果此函数执行成功,则返回非0值。
PacketSetReadEvt函数的主要实现代码如下:
BOOLEAN PacketSetReadEvt(LPADAPTER AdapterObject)
{
DWORD BytesReturned;
HANDLE hEvent;
if(AdapterObject->ReadEvent!=NULL)
{
SetLastError(ERROR_INVALID_FUNCTION);
return FALSE;
}
/创建读事件信号/
hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
if(hEvent==NULL)
{
//创建失败,函数返回
return FALSE;
}
/传递给内核驱动/
if(DeviceIoControl(AdapterObject->hFile,
BIOCSETEVENTHANDLE,
&hEvent,
sizeof(hEvent),
NULL,
0,
&BytesReturned,
NULL)==FALSE)
{//失败,函数返回
……
return FALSE;
}
/赋给AdapterObject->ReadEvent/
AdapterObject->ReadEvent=hEvent;
/把timeout设置为0,无超时/
AdapterObject->ReadTimeOut=0;
return TRUE;
}
PacketOpenAdapter函数调用PacketSetReadEvt函数来分配读事件信号,并通过IOCTL系统调用把它传递给驱动程序,同时赋给AdapterObject所指的_ADAPTER结构。此函数最后由NPF驱动程序的NPF_IoControl函数的case BIOCSETEVENTHANDLE:分支语句在底层实现。
3)PacketSetMaxLookaheadsize函数
PacketSetMaxLookaheadsize函数给驱动程序的Packet_tap(NPF的为NPF_tap)函数设置尽可能大的前视缓冲区。参数AdapterObject是指向ADAPTER对象的一个指针。如果函数执行成功则返回非0值。
PacketSetMaxLookaheadsize函数的主要实现代码如下:
BOOLEAN PacketSetMaxLookaheadsize(LPADAPTER AdapterObject)
{
BOOLEAN Status;
ULONG
IoCtlBufferLength=(sizeof(PACKET_OID_DATA)+sizeof(ULONG)-1);
PPACKET_OID_DATA OidData;
OidData=GlobalAllocPtr(GMEM_MOVEABLE|GMEM_ZEROINIT,
IoCtlBufferLength);
if(OidData==NULL)
{
//内存分配失败
Status=FALSE;
}
else
{
//设置前视缓冲区大小为NIC驱动程序能提供的最大可用值
OidData->Oid=OID_GEN_MAXIMUM_LOOKAHEAD;
OidData->Length=sizeof(ULONG);
Status=PacketRequest(AdapterObject,FALSE,OidData);
//查看当前视缓冲区的大小
OidData->Oid=OID_GEN_CURRENT_LOOKAHEAD;
Status=PacketRequest(AdapterObject,TRUE,OidData);
GlobalFreePtr(OidData);
}
return Status;
}
该函数主要调用PacketRequest函数实现参数的查询或设置功能。
(3)PacketGetNetType函数
PacketGetNetType函数返回一个适配器网络数据链路层的类型信息,其原型如下:
BOOLEAN PacketGetNetType(LPADAPTER AdapterObject,NetType*type);
上述函数中,参数AdapterObject为需要获取信息的适配器;参数type指向一个NetType结构体,它将被PacketGetNetType函数填充。
如果PacketGetNetType函数执行成功,则返回非0值,否则返回0值。
PacketGetNetType函数的主要代码如下:
BOOLEAN PacketGetNetType(LPADAPTER AdapterObject,
NetType*type)
{
PADAPTER_INFO TAdInfo;
BOOLEAN ret;
WaitForSingleObject(g_AdaptersInfoMutex,INFINITE);
/查找该适配器的PADAPTER_INFO结构体/
TAdInfo=PacketFindAdInfo(AdapterObject->Name);
if(TAdInfo!=NULL)
{//找到该适配器,获得type值
memcpy(type,&(TAdInfo->LinkLayer),
sizeof(struct NetType));
ret=TRUE;
}
else
{//没找到适配器
ret=FALSE;
}
ReleaseMutex(g_AdaptersInfoMutex);
return ret;
}
该函数会返回一个已打开适配器数据链路层的类型与速度(以bps为单位)。参数type的LinkType成员可能为下列值之一:
❑NdisMedium802_3:Ethernet(802.3)
❑NdisMediumWan:WAN
❑NdisMedium802_5:Token Ring(802.5)
❑NdisMediumFddi:FDDI
❑NdisMediumAtm:ATM
❑NdisMediumArcnet878_2:ARCNET(878.2)
(4)PacketSetHwFilter函数
PacketSetHwFilter函数会对到来的数据包设置一个硬件过滤器。其原型如下:
BOOLEAN PacketSetHwFilter(LPADAPTER AdapterObject,ULONG Filter);
上述函数中,参数AdapterObject指向一个_ADAPTER结构体;参数Filter是过滤器的类型。该函数如果执行成功,则返回非0值。
PacketSetHwFilter函数的主要实现代码如下:
BOOLEAN PacketSetHwFilter(LPADAPTER AdapterObject,ULONG Filter)
{
BOOLEAN Status;
ULONG IoCtlBufferLength
=(sizeof(PACKET_OID_DATA)+sizeof(ULONG)-1);
PPACKET_OID_DATA OidData;
……
if(AdapterObject->Flags==INFO_FLAG_NDIS_ADAPTER)
{
OidData=GlobalAllocPtr(GMEM_MOVEABLE|GMEM_ZEROINIT,
IoCtlBufferLength);
if(OidData==NULL){
//分配失败,函数返回
}
OidData->Oid=OID_GEN_CURRENT_PACKET_FILTER;
OidData->Length=sizeof(ULONG);
*((PULONG)OidData->Data)=Filter;
Status=PacketRequest(AdapterObject,TRUE,OidData);
GlobalFreePtr(OidData);
}
else
{//该类型的适配器不支持所设置的硬件过滤器类型
Status=FALSE;
}
return Status;
}
PacketSetHwFilter函数主要依靠PacketRequest函数设置过滤器,实际上该过滤器的过滤由网络接口卡(NIC)执行。
下列是一些常用的硬件过滤器类型,完整的列表参见ntddndis.h文件。
❑NDIS_PACKET_TYPE_PROMISCUOUS:设置为混杂模式,网卡接收到来的每个数据包。
❑NDIS_PACKET_TYPE_DIRECTED:网卡只接收到本机的数据包。
❑NDIS_PACKET_TYPE_BROADCAST:网卡只接收广播数据包。
❑NDIS_PACKET_TYPE_MULTICAST:网卡只接收所属多播组的多播数据包。
❑NDIS_PACKET_TYPE_ALL_MULTICAST:网卡接收所有的多播数据包。
❑NDIS_PACKET_TYPE_ALL_LOCAL:网卡接收所有发送给本机的数据包,也就是NDIS_PACKET_TYPE_DIRECTED+NDIS_PACKET_TYPE_BROADCAST+NDIS_PACKET_TYPE_MULTICAST。
(5)PacketAllocatePacket与PacketFreePacket函数
PacketAllocatePacket函数用于分配一个_ADAPTER结构体内存空间,其原型如下:
LPPACKET PacketAllocatePacket(void)
此函数主要由下面的代码实现:
lpPacket=(LPPACKET)GlobalAllocPtr(
GMEM_MOVEABLE|GMEM_ZEROINIT,sizeof(PACKET));
如果该函数执行成功,则返回所分配的_ADAPTER结构体指针。
返回的结构体传递给PacketReceivePacket函数,用于存储从驱动程序处接收的数据包。
注意 _PACKET结构体的Buffer成员不是由函数PacketAllocatePacket设置的,该缓冲区必须由应用程序分配,并调用PacketInitPacket函数把该缓冲区与该_PACKET结构体相关联。
PacketFreePacket函数释放一个_ADAPTER结构体内存空间,其原型如下:
VOID PacketFreePacket(LPPACKET lpPacket);
上述函数中,参数lpPacket为所要释放的结构体指针。
PacketFreePacket函数主要由下面的代码实现:
GlobalFreePtr(lpPacket);
注意 用户层所分配的与该_PACKET结构体相关联的缓冲区Buffer成员并不在该函数中释放,该缓冲区必须显式地由应用程序释放。
下面介绍在wpcap.dll库中对这些函数的实际使用情况:
//pcap_open最终会调用该函数
static int pcap_activate_win32(pcap_t*p)
{
……
/设置缓冲区大小/
p->bufsize=WIN32_DEFAULT_USER_BUFFER_SIZE;
/分配在捕获过程中使用的数据包结构体/
if((p->Packet=PacketAllocatePacket())==NULL)
{
}
/分配在捕获过程中使用的缓冲区/
p->buffer=(u_char*)malloc(p->bufsize);
/初始化捕获过程中使用的数据包结构体,并把缓冲区与该_PACKET结构体相关联/
PacketInitPacket(p->Packet,(BYTE*)p->buffer,p->bufsize);
……
}
//pcap_close最终会调用该函数
static void pcap_cleanup_win32(pcap_t*p)
{
……
/释放捕获过程中使用的数据包结构体/
if(p->Packet){
PacketFreePacket(p->Packet);
p->Packet=NULL;
}
pcap_cleanup_live_common(p);
}
void pcap_cleanup_live_common(pcap_t*p)
{
/释放在捕获过程中使用的缓冲区/
if(p->buffer!=NULL){
free(p->buffer);
}
……
}
(6)PacketSetBuff函数
PacketSetBuff函数设置一个与捕获实例相关联的内核缓冲区大小,其原型如下:
BOOLEAN PacketSetBuff(LPADAPTER AdapterObject,int dim);
上述函数中,参数AdapterObject指向一个_ADAPTER结构体;参数dim是缓冲区新的大小,以千字节(KB)为单位。
如果PacketSetBuff函数执行成功,则返回TRUE,如果没有足够的内存分配,则此函数执行失败,返回FALSE。
PacketSetBuff函数的主要实现代码如下:
BOOLEAN PacketSetBuff(LPADAPTER AdapterObject,int dim)
{
……
if(AdapterObject->Flags==INFO_FLAG_NDIS_ADAPTER)
{
Result=(BOOLEAN)DeviceIoControl(AdapterObject->hFile,
BIOCSETBUFFERSIZE,&dim,sizeof(dim),
NULL,0,&BytesReturned,NULL);
}
else
{//一个未知类型的适配器请求设置缓冲区的大小
Result=FALSE;
}
return Result;
}
函数最后由NPF驱动程序的NPF_IoControl函数的case BIOCSETBUFFERSIZE:分支语句在底层实现。
当设置一个新的大小时,旧缓冲区内的数据会被丢掉,同时其中存储的数据包也会被丢弃。
注意 内核缓冲区的大小严重影响了捕获过程的性能。当应用程序忙时,如果驱动程序中有一个足够大的缓冲区能够保存数据包,那就可弥补应用程序的延时,避免在网路高峰期时丢失数据包。当一个驱动程序实例被打开时,缓冲区的大小设置为0,因此程序员应该记得为它重新设置一个合适的值。比如当wpcap.dll库开始捕获时设置该缓冲区的大小为1MB。
(7)PacketInitPacket函数
PacketInitPacket函数主要用于初始化一个_PACKET结构体,其原型如下:
VOID PacketInitPacket(LPPACKET lpPacket,PVOID Buffer,UINT Length);
上述函数中,参数lpPacket指向需要初始化的结构体;参数Buffer指向用户所分配的缓冲区,它会被用来缓冲所捕获的数据包;参数Length为该缓冲区的大小,这也是从驱动程序到应用程序的一次读操作所能传递最大的数据大小。
PacketInitPacket函数的主要实现代码如下:
VOID PacketInitPacket(LPPACKET lpPacket,PVOID Buffer,UINT Length)
{
lpPacket->Buffer=Buffer;
lpPacket->Length=Length;
lpPacket->ulBytesReceived=0;
lpPacket->bIoComplete=FALSE;
}
注意 与_PACKET结构体相关联的缓冲区用来存储从驱动程序处所接收的数据包,其大小能够明显影响捕获过程的性能。驱动程序能够只进行一次系统调用(参见PacketReceivePacket函数)就返回几个数据包。传递给PacketReceivePacket函数的缓冲区大小,限制了一次调用中传递给应用程序的数据包的数目。因此使用PacketInitPacket函数设置一个大的缓冲区,可以显著降低系统调用的次数,降低捕获过程给处理器带来的压力。
(8)PacketSetMinToCopy函数
PacketSetMinToCopy函数用于定义最少字节数,对适配器执行一次读操作时,要求内核缓冲区中的数据达到该字节数后才可以返回,其原型如下:
BOOLEAN PacketSetMinToCopy(LPADAPTER AdapterObject,int nbytes);
上述函数中,参数AdapterObject是一个指向_ADAPTER结构体的指针;参数nbytes是对该适配器执行一次读操作时,内核缓冲区中数据要求达到的最少字节数,只有达到此字节数时,读操作才可返回。
如果PacketSetMinToCopy函数执行成功,则返回非0值,否则返回0值。
如果nbytes是一个很大的值,内核则要接收几个数据包后才能把数据复制到用户层中。这可保证少量的系统调用,也就是可保证低的处理器占用与更好的性能,该方式对于网络分析器来说是一个不错的设置。相反,一个小的值意味着内核只要一接收到数据包,就需要尽可能快地把数据包复制给准备接收它的应用程序。建议需要内核具有更好响应的实时应用程序(比如一个网桥)采用该方式。
PacketSetMinToCopy函数主要调用下面的语句:
Result=(BOOLEAN)DeviceIoControl(AdapterObject->hFile,
BIOCSMINTOCOPY,&nbytes,4,NULL,0,&BytesReturned,NULL);
最终在NPF驱动程序的NPF_IoControl函数的case BIOCSMINTOCOPY:分支语句下实现该功能。
(9)PacketSetReadTimeout函数
PacketSetReadTimeout函数用于设置一个适配器上读操作的超时时间,其原型如下:
BOOLEAN PacketSetReadTimeout(LPADAPTER AdapterObject,int timeout);
上述函数中,参数AdapterObject指向一个_ADAPTER结构体;参数timeout为超时时间,以毫秒(ms)为单位。
如果PacketSetReadTimeout函数执行成功,则返回非0值,否则返回0值。
在AdapterObject适配器上调用PacketReceivePacket函数后,如果在timeout时间内没有数据包到达,将放弃该调用。把timeout设置为0表示无超时,也就是说如果没有数据包到达,PacketReceivePacket函数就不返回。如果把timeout设为-1,PacketReceivePacket函数便总是立即返回。
注意 如果适配器工作于统计模式(参见第11章)下,该函数可用来设置两个统计报告之间的时间间隔。
PacketSetReadTimeout函数的主要代码实现如下:
BOOLEAN PacketSetReadTimeout(LPADAPTER AdapterObject,int timeout)
{
BOOLEAN Result;
/设置超时间/
AdapterObject->ReadTimeOut=timeout;
……
if(AdapterObject->Flags==INFO_FLAG_NDIS_ADAPTER)
{
Result=TRUE;
}
else
{//一个未知类型的适配器请求设置超时时间
Result=FALSE;
}
return Result;
}
(10)PacketSetLoopbackBehavior函数
PacketSetLoopbackBehavior函数用于设置NPF驱动程序对环回数据的处理方式:捕获还是丢弃,其原型如下:
BOOLEAN PacketSetLoopbackBehavior(LPADAPTER AdapterObject,
UINT LoopbackBehavior)
上述函数中,参数AdapterObject是一个指向一个_ADAPTER结构体的指针;参数LoopbackBehavior可以为NPF_ENABLE_LOOPBACK或NPF_DISABLE_LOOPBACK,它们分别表示激活或禁止环回数据捕获。注意,适配器刚打开时,环回数据包捕获是激活了的。
如果PacketSetLoopbackBehavior函数执行成功,则返回非0值。
PacketSetLoopbackBehavior函数主要调用下列语句:
result=(BOOLEAN)DeviceIoControl(
AdapterObject->hFile,
BIOCISETLOBBEH,
&LoopbackBehavior,
sizeof(UINT),
NULL,
0,
&BytesReturned,
NULL);
最终在NPF驱动程序NPF_IoControl函数的case BIOCISETLOBBEH:分支语句下实现该功能。
3.内核空间中打开适配器的实现
下面对驱动程序中与打开、关闭网络适配器相关的函数的具体实现进行详细分析。
(1)数据结构体_OPEN_INSTANCE
结构体_OPEN_INSTANCE描述了NPF驱动程序的运行实例状态。这是NPF最重要的结构体,它几乎被所有驱动程序的函数使用。一个_OPEN_INSTANCE结构体与每个用户层会话关联,并允许对该驱动程序进行并发访问。其具体定义如下:
typedef struct_OPEN_INSTANCE
{
//指向该实例所绑定设备的_DEVICE_EXTENSION结构体的指针
PDEVICE_EXTENSION DeviceExtension;
//该实例使用适配器的NDIS标识符
NDIS_HANDLE AdapterHandle;
//底层NDIS驱动使用的物理介质类型
//可通过微软WDK的NdisOpenAdapter函数文档了解详情
UINT Medium;
//NDIS_PACKET结构体的存储池,用来与NIC驱动程序传输数据包
NDIS_HANDLE PacketPool;
//用来同步OID请求的自旋锁
KSPIN_LOCK RequestSpinLock;
//挂起的OID请求列表
LIST_ENTRY RequestList;
//挂起的适配器复位请求列表
LIST_ENTRY ResetIrpList;
//封装每单个OID请求的结构体数组
INTERNAL_REQUEST Requests[MAX_REQUESTS];
//指向一个内存描述列表(MDL)的指针,MDL映射了环形缓冲区的内存
PMDL BufferMdl;
//指向该实例读调用事件的指针
PKEVENT ReadEvent;
//指向与当前实例相关的过滤伪代码
//该代码只能在特定情景中使用(例如,当从NIC驱动程序中接收的数据包存储
//在两个非连续的缓冲区中时)
//正常情况下,使用JIT编译器创建并被下一个成员Filter指向的过滤程序
//参见NPF,了解过滤过程的细节
PUCHAR bpfprogram;
ifdefX86
//指向由jitter所创建的本机过滤函数的指针
//参见BPF_jitter函数,了解细节
JIT_BPF_Filter*Filter;
endif//X86
//环形缓冲区中没被锁定的最小可读的数据数量,
//通过IOCTL的BIOCSMINTOCOPY命令码设置
UINT MinToCopy;
//读操作超时时间
LARGE_INTEGER TimeOut;
//驱动程序的工作模式,参见PacketSetMode函数,了解细节
int mode;
//当该实例在统计模式下时,记录被过滤器接收的字节数量
LARGE_INTEGER Nbytes;
//当该实例在统计模式下时,记录被过滤器接收的数据包数量
LARGE_INTEGER Npackets;
//保护统计模式计数器的自旋锁
NDIS_SPIN_LOCK CountersLock;
//一个数据包需要被重复发送的次数
UINT Nwrites;
//对一个发送操作已做的重复发送次数进行计数
ULONG Multiple_Write_Counter;
//同步多个写进程的事件
NDIS_EVENT WriteEvent;
//标识写过程是否正在进行,如正在写,则为TRUE,
//NPF当前允许在同一个Open实例上执行一个单独的写操作
BOOLEAN WriteInProgress;
//保护WriteInProgress变量的自旋锁
NDIS_SPIN_LOCK WriteLock;
//用来同步NDIS的回调结构体与I/O请求的事件
NDIS_EVENT NdisRequestEvent;
//如果该实例不应该捕获它自己所传输的数据包,则为TRUE
BOOLEAN SkipSentPackets;
//维护OID请求调用的状态,这将传递给应用程序
NDIS_STATUS IOStatus;
//在转储模式下使用的文件句柄
HANDLE DumpFileHandle;
//指向转储模式下使用的文件对象的指针
PFILE_OBJECT DumpFileObject;
//指向转储模式下使用的线程对象的指针
PKTHREAD DumpThreadObject;
//转储模式下创建的线程句柄,为了异步地把数据从缓冲区搬移到磁盘上
HANDLE DumpThreadHandle;
//转储模式下,用来同步转储线程与探针(tap函数)读取的事件
NDIS_EVENT DumpEvent;
//转储文件当前的偏移
LARGE_INTEGER DumpOffset;
//转储文件的文件名
UNICODE_STRING DumpFileName;
//转储文件能存储的最大字节数
//如果转储文件达到该大小限定,它将被关闭,0值表示无大小限制
UINT MaxDumpBytes;
//所能存储到转储文件中的最大的数据包包数,
//如果转储文件达到该限定,它将被关闭,0值表示无此限制
UINT MaxDumpPacks;
//如果达到了转储文件的最大值(MaxDumpBytes或MaxDumpPacks),
//该值为TRUE
BOOLEAN DumpLimitReached;
ifdef HAVE_BUGGY_TME_SUPPORT
//被TME虚拟协处理器使用的内存
MEM_TYPE mem_ex;
//包含TME协处理器虚拟机的数据结构体
TME_CORE tme;
endif//HAVE_BUGGY_TME_SUPPORT
//保护BPF过滤器与TME引擎的自旋锁
NDIS_SPIN_LOCK MachineLock;
//底层MAC所能接收的最大帧的大小
//用来检测通过NPF_Write函数或NPF_BufferedWrite函数发送的帧大小
UINT MaxFrameSize;
//内核缓冲区结构体的内存缓冲池,每个CPU一个
//其中KAFFINITY是一个位掩码,用于实现内存缓冲池在系统中的亲缘性
//在每个被支持的操作系统上,该掩码的位数对于系统上所有的CPU来说都足够大
//(如x86上为32位)
//我们使用它的大小计算CPU的最大数目
CpuPrivateData CpuData[sizeof(KAFFINITY)*8];
//下一个,将从内核缓冲池中读取的数据包的序号
ULONG ReaderSN;
//下一个,将写入内核缓冲池中的数据包的序号
//这两个序号对于每个捕获实例来说都是唯一的
ULONG WriterSN;
//包含在CpuData字段中的每个内核缓冲区的大小
ULONG Size;
ULONG AdapterHandleUsageCounter;
NDIS_SPIN_LOCK AdapterHandleLock;
//描述NPF是否仍然被该实例使用的适配器所绑定
ULONG AdapterBindingStatus;
NDIS_EVENT NdisOpenCloseCompleteEvent;
//事件,当所有数据包被NdisSend成功发送时产生该事件通知
//并且对应的sendComplete函数会被调用
NDIS_EVENT NdisWriteCompleteEvent;
NTSTATUS OpenCloseStatus;
//说明正被挂起的待传输数据包的数目,
//比如,已被提交给NdisSendXXX,但是SendComplete仍没被调用
ULONG TransmitPendingPackets;
//通过引用计数记录在该运行实例上挂起的IRP,
//当IRP_MJ_CLEANUP IRP被调用时,可以等待这些挂起的IRP执行完成
ULONG NumPendingIrps;
BOOLEAN ClosePending;
NDIS_SPIN_LOCKOpenInUseLock;
}OPEN_INSTANCE,*POPEN_INSTANCE;
上述代码中,Requests[MAX_REQUESTS]的类型为INTERNAL_REQUEST结构体,对NDIS_REQUEST结构体进行封装。该结构体用于存储一个OID请求。驱动程序则使用该结构体,在底层NIC驱动程序上执行OID请求或设置操作。通常仅网络驱动程序执行OID操作,但NPF会通过IOCTL接口(设置OID的命令字BIOCSETOID,查询OID的命令字BIOCQUERYOID)导出该机制给用户空间应用程序,用户空间应用程序可使用PacketRequest函数执行OID操作。
结构体INTERNAL_REQUEST的具体定义如下:
typedef struct_INTERNAL_REQUEST{
//用来操控请求链表的句柄
LIST_ENTRY ListElement;
//请求完成事件
NDIS_EVENT InternalRequestCompletedEvent;
//带有实际请求的结构体,将由NdisRequest调用传递
NDIS_REQUEST Request;
NDIS_STATUS RequestStatus;
}INTERNAL_REQUEST,*PINTERNAL_REQUEST;
(2)相关的IRP
NPF驱动程序入口DriverEntry函数的下列代码指定了与IRP_MJ_CREATE、IRP_MJ_CLOSE、IRP_MJ_CLEANUP、IRP_MJ_DEVICE_CONTROL的IRP对应的处理函数。
DriverObject->MajorFunction[IRP_MJ_CREATE]=NPF_Open;
DriverObject->MajorFunction[IRP_MJ_CLOSE]=NPF_Close;
DriverObject->MajorFunction[IRP_MJ_CLEANUP]=NPF_Cleanup;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]=NPF_IoControl;
PacketOpenAdapterNPF函数的CreateFileA系统调用使NPF_Open函数被调用,代码如下。
lpAdapter->hFile=CreateFileA(SymbolicLinkA,
GENERIC_WRITE|GENERIC_READ,0,NULL,OPEN_EXISTING,0,0);
PacketCloseAdapter函数的CloseHandle系统调用则使NPF_Cleanup函数被调用,代码如下。
CloseHandle(lpAdapter->hFile);
PacketSetBuff函数中的DeviceIoControl系统调用则使NPF_IoControl函数被调用,代码如下。
Result=(BOOLEAN)DeviceIoControl(AdapterObject->hFile,
BIOCSETBUFFERSIZE,&dim,sizeof(dim),NULL,0,&BytesReturned,NULL);
(3)NPF_Open函数
NPF_Open函数用于打开驱动的一个新实例。其原型如下:
NTSTATUS NPF_Open(IN PDEVICE_OBJECT
DeviceObject,IN PIRP Irp);
上述函数中,参数DeviceObject指向用户所使用的设备对象,参数Irp指向用户所请求的IRP。
函数返回操作的状态,可通过WDK的ntstatus.h文件了解状态值的详细定义。
当用户的应用程序在NPF所创建的一个设备上执行一个CreateFile系统调用时,NPF_Open函数会被操作系统调用。NPF_Open函数用于分配与初始化一个新实例所需的变量、对象与缓冲区,填充与该实例关联的OPEN_INSTANCE结构体,并调用NdisOpenAdapter系统函数打开适配器。
NPF_Open函数的主要实现步骤如图7-7所示。
图 7-7 NPF_Open函数的处理过程
NPF_Open函数首先会获得_DEVICE_EXTENSION结构体指针(此指针在后面会赋给Open->DeviceExtension),接着会获得IRP中调用者栈的位置。然后它会分配内存空间给Open结构体,对所分配的内存清零,并为数据包的接收与发送分配一个数据包内存缓冲池。
接下来初始化Open结构体的各种事件与各种自旋锁,设置Open实例的其他成员。
然后试图打开网络设备,在调用的协议与一个特定底层NIC驱动程序或NDIS中间层驱动程序之间建立绑定。检查NdisOpenAdapter函数返回的状态是否为挂起状态,如果是,则等待打开完成事件。如果绑定成功,则给全局记录打开实例的计数器原子操作加1,然后调用NPF_GetDeviceMTU函数获取设备的最大MTU。如果该调用成功,则把Open实例保存到IrpSp->FileObject->FsContext中,以便其他函数使用。比如在NPF_Cleanup函数中使用Open=IrpSp->FileObject->FsContext;语句获得Open实例。
NPF_Open函数的主要实现代码如下:
NTSTATUS NPF_Open(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)
{
PDEVICE_EXTENSION DeviceExtension;
POPEN_INSTANCE Open;
PIO_STACK_LOCATION IrpSp;
NDIS_STATUS Status;
NDIS_STATUS ErrorStatus;
UINT i;
PUCHAR tpointer;
PLIST_ENTRY PacketListEntry;
NTSTATUS returnStatus;
/获得_DEVICE_EXTENSION结构体指针,在后面赋给Open->DeviceExtension/
DeviceExtension=DeviceObject->DeviceExtension;
/获得IRP中的调用者栈的位置/
IrpSp=IoGetCurrentIrpStackLocation(Irp);
/分配内存空间给Open结构体,并对所分配的内存清零/
Open=ExAllocatePoolWithTag(NonPagedPool,
sizeof(OPEN_INSTANCE),'0OWA');
if(Open==NULL)
{
//分配内存失败,函数返回
Irp->IoStatus.Status=STATUS_INSUFFICIENT_RESOURCES;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_INSUFFICIENT_RESOURCES;
}
RtlZeroMemory(Open,sizeof(OPEN_INSTANCE));
Open->DeviceExtension=DeviceExtension;
/为数据包的接收与发送分配一个数据包内存缓冲池/
NdisAllocatePacketPool(&Status,&Open->PacketPool,TRANSMIT_PACKETS,
sizeof(PACKET_RESERVED));
if(Status!=NDIS_STATUS_SUCCESS)
{
//分配数据包缓冲池失败,函数返回
ExFreePool(Open);
Irp->IoStatus.Status=STATUS_INSUFFICIENT_RESOURCES;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_INSUFFICIENT_RESOURCES;
}
/初始化Open结构体/
//初始化Open结构体的各种事件
NdisInitializeEvent(&Open->WriteEvent);
NdisInitializeEvent(&Open->NdisRequestEvent);
NdisInitializeEvent(&Open->NdisWriteCompleteEvent);
NdisInitializeEvent(&Open->DumpEvent);
//初始化Open结构体的各种自旋锁
NdisAllocateSpinLock(&Open->MachineLock);
NdisAllocateSpinLock(&Open->WriteLock);
//设置没有执行写操作的状态
Open->WriteInProgress=FALSE;
//初始化Open结构体的各种自旋锁
for(i=0;i<g_NCpu;i++)
{
NdisAllocateSpinLock(&Open->CpuData[i].BufferLock);
}
NdisInitializeEvent(&Open->NdisOpenCloseCompleteEvent);
//初始化存储适配器复位IRP请求的链表ResetIrpList
InitializeListHead(&Open->ResetIrpList);
//初始化请求链表RequestList
KeInitializeSpinLock(&Open->RequestSpinLock);
InitializeListHead(&Open->RequestList);
ifdef HAVE_BUGGY_TME_SUPPORT
//初始化NPF虚拟机的扩展内存
Open->mem_ex.buffer=ExAllocatePoolWithTag(NonPagedPool,
EFAULT_MEM_EX_SIZE,'2OWA');
if((Open->mem_ex.buffer)==NULL)
{
//分配内存空间失败,函数返回
ExFreePool(Open);
Irp->IoStatus.Status=STATUS_INSUFFICIENT_RESOURCES;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_INSUFFICIENT_RESOURCES;
}
Open->mem_ex.size=DEFAULT_MEM_EX_SIZE;
RtlZeroMemory(Open->mem_ex.buffer,DEFAULT_MEM_EX_SIZE);
endif//HAVE_BUGGY_TME_SUPPORT
//初始化Open实例的一些成员
Open->bpfprogram=NULL;//复位过滤器
Open->mode=MODE_CAPT;//设置为捕获工作模式
Open->Nbytes.QuadPart=0;//把过滤器所接收字节数置0
Open->Npackets.QuadPart=0;//把过滤器所接收数据包数置0
Open->Nwrites=1;//设置一个数据包需要被重复发送的次数为1次
Open->Multiple_Write_Counter=0;//写操作已做的重复次数清零
Open->MinToCopy=0;//设置缓冲区中没被锁定的最小可读
//的数据数量
Open->TimeOut.QuadPart=(LONGLONG)1;//设置读超时值
Open->DumpFileName.Buffer=NULL;//初始化文件转储的各参数
Open->DumpFileHandle=NULL;
ifdef HAVE_BUGGY_TME_SUPPORT
Open->tme.active=TME_NONE_ACTIVE;
endif//HAVE_BUGGY_TME_SUPPORT
Open->DumpLimitReached=FALSE;
Open->MaxFrameSize=0;//设置底层MAC所能接收的最大帧的大小
Open->WriterSN=0;//下一个将写入内核缓冲池中的数据包的序号
Open->ReaderSN=0;//下一个将从内核缓冲池中读取的数据包的序号
Open->Size=0;//包含在CpuData字段中的每个内核缓冲区的大小
Open->SkipSentPackets=FALSE;//设置实例捕获自己所传输的数据包
Open->ReadEvent=NULL;//初始化事件,对实例的读调用必须等待
//保存挂起IRPs的计数,当IRP_MJ_CLEANUP调用时,能够等待这些IRPs完成
Open->NumPendingIrps=0;
Open->ClosePending=FALSE;
NdisAllocateSpinLock(&Open->OpenInUseLock);
//初始化统计计数器的自旋锁
NdisAllocateSpinLock(&Open->CountersLock);
//连接Open的请求链表
for(i=0;i<MAX_REQUESTS;i++)
{
NdisInitializeEvent(
&Open->Requests[i].InternalRequestCompletedEvent);
//在双向链表的尾部插入一个元素,该插入为原子操作
ExInterlockedInsertTailList(
&Open->RequestList,
&Open->Requests[i].ListElement,
&Open->RequestSpinLock);
}
NdisResetEvent(&Open->NdisOpenCloseCompleteEvent);
/在打开MAC前,设置正确的绑定标识/
Open->AdapterBindingStatus=ADAPTER_BOUND;
Open->AdapterHandleUsageCounter=0;
NdisAllocateSpinLock(&Open->AdapterHandleLock);
/*
*试图打开网络设备,在调用的协议与
*一个特定底层NIC驱动程序或NDIS中间层驱动程序之间建立绑定
*/
returnStatus=STATUS_SUCCESS;
NdisOpenAdapter(
&Status,
&ErrorStatus,
&Open->AdapterHandle,
&Open->Medium,
MediumArray,
NUM_NDIS_MEDIA,
g_NdisProtocolHandle,
Open,
&DeviceExtension->AdapterName,
0,
NULL);
if(Status==NDIS_STATUS_PENDING)
{//打开网络设备返回挂起状态,等待打开完成事件
NdisWaitEvent(&Open->NdisOpenCloseCompleteEvent,0);
if(!NT_SUCCESS(Open->OpenCloseStatus))
{//失败
returnStatus=Open->OpenCloseStatus;
}
else
{
returnStatus=STATUS_SUCCESS;
}
}
else
{
//请求没被挂起,我们已经知道结果,就不调用OpenComplete了
if(Status==NDIS_STATUS_SUCCESS)
{
returnStatus=STATUS_SUCCESS;
}
else
{
//完成不正确,我们把一个NDIS_STATUS转换为NTSTATUS
returnStatus=Status;
}
}
/如果成功,则给全局记录打开实例的计数器原子操作加1,然后获取设备的最大MTU/
if(returnStatus==STATUS_SUCCESS)
{//完成打开网络设备
ULONG localNumOpenedInstances;
//给全局记录打开实例的计数器原子操作加1
localNumOpenedInstances=InterlockedIncrement(&g_NumOpenedInstances);
//获得系统启动以来的时间绝对值,用于时间转换
TIME_SYNCHRONIZE(&G_Start_Time);
//获取设备的最大MTU
returnStatus=NPF_GetDeviceMTU(Open,Irp,
&Open->MaxFrameSize);
if(!NT_SUCCESS(returnStatus))
{//获取失败,关闭绑定
NPF_CloseBinding(Open);
}
}
/如果成功,则把Open实例保存到IrpSp->FileObject->FsContext中/
if(!NT_SUCCESS(returnStatus))
{
NPF_ReleaseOpenInstanceResources(Open);
ExFreePool(Open);
}
else
{//此处保存Open实例
IrpSp->FileObject->FsContext=Open;
}
Irp->IoStatus.Status=returnStatus;
Irp->IoStatus.Information=0;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return returnStatus;
}
1)NPF_OpenAdapterComplete函数
NPF_OpenAdapterComplete函数用于结束一个适配器的打开操作,它是NDIS函数NdisOpenAdapter所关联的回调函数。当NIC驱动程序完成一个打开操作(在NPF_Open中调用NdisOpenAdapter启动打开操作)时它被NDIS调用。该函数原型如下:
VOID NPF_OpenAdapterComplete(
IN NDIS_HANDLE ProtocolBindingContext,
IN NDIS_STATUS Status,
IN NDIS_STATUS OpenErrorStatus
);
上述函数中,参数ProtocolBindingContext指向一个上下文区块,它包含一个指向与当前实例关联的OPEN_INSTANCE结构体指针;参数Status描述NIC驱动程序打开操作的最终状态;参数OpenErrorStatus没有被NPF使用。
NPF_OpenAdapterComplete函数的主要实现代码如下:
VOID NPF_OpenAdapterComplete(
IN NDIS_HANDLE ProtocolBindingContext,
IN NDIS_STATUS Status,
IN NDIS_STATUS OpenErrorStatus)
{
POPEN_INSTANCE Open;
PLIST_ENTRY RequestListEntry;
PINTERNAL_REQUEST MaxSizeReq;
NDIS_STATUS ReqStatus;
/获得Open实例/
Open=(POPEN_INSTANCE)ProtocolBindingContext;
ASSERT(Open!=NULL);
if(Status!=NDIS_STATUS_SUCCESS)
{
//打开操作没有正确完成,我们把一个NDIS_STATUS转换为NTSTATUS
Open->OpenCloseStatus=Status;
}
else
{//打开成功
Open->OpenCloseStatus=STATUS_SUCCESS;
}
/唤醒NdisOpenAdapter的调用者,也就是NPF_Open函数/
NdisSetEvent(&Open->NdisOpenCloseCompleteEvent);
}
此函数会检测适配器的打开是否成功,并先设置Open->OpenCloseStatus。如果打开成功,则唤醒NPF_Open函数中的等待事件Open->NdisOpenCloseCompleteEvent,以使NPF_Open函数继续执行。
2)NPF_GetDeviceMTU函数
NPF_GetDeviceMTU函数用于获得网络的最大传输单元(MTU),它主要调用NdisRequest函数实现此功能。对于函数执行过程中遇到的各种错误情况,均假定其为以太网类型的MTU,并将其设置为1514字节长度。
NPF_GetDeviceMTU函数的主要实现代码如下:
NTSTATUS NPF_GetDeviceMTU(
IN POPEN_INSTANCE pOpen,
IN PIRP pIrp,
OUT PUINT pMtu)
{
PLIST_ENTRY RequestListEntry;
PINTERNAL_REQUEST MaxSizeReq;
NDIS_STATUS ReqStatus;
ASSERT(pOpen!=NULL);
ASSERT(pIrp!=NULL);
ASSERT(pMtu!=NULL);
/从双向链表中移出第一个元素/
RequestListEntry=ExInterlockedRemoveHeadList(
&pOpen->RequestList,&pOpen->RequestSpinLock);
if(RequestListEntry==NULL)
{
//错误,假设为以太网,设为1514字节长度,函数返回
*pMtu=1514;
return STATUS_SUCCESS;
}
MaxSizeReq=CONTAINING_RECORD(RequestListEntry,
INTERNAL_REQUEST,ListElement);
/设置Request成员的各参数/
MaxSizeReq->Request.RequestType=
NdisRequestQueryInformation;
MaxSizeReq->Request.DATA.QUERY_INFORMATION.Oid=
OID_GEN_MAXIMUM_TOTAL_SIZE;
MaxSizeReq->Request.DATA.QUERY_INFORMATION.InformationBuffer=pMtu;
MaxSizeReq->Request.DATA.QUERY_INFORMATION.InformationBufferLength=
sizeof(*pMtu);
NdisResetEvent(&MaxSizeReq->InternalRequestCompletedEvent);
/提交请求/
NdisRequest(&ReqStatus,pOpen->AdapterHandle,
&MaxSizeReq->Request);
if(ReqStatus==NDIS_STATUS_PENDING)
{//挂起,等待请求完成
NdisWaitEvent(
&MaxSizeReq->InternalRequestCompletedEvent,0);
ReqStatus=MaxSizeReq->RequestStatus;
}
/在双向链表尾部原子插入一个元素,因为前面移出了该请求/
ExInterlockedInsertTailList(&pOpen->RequestList,
&MaxSizeReq->ListElement,&pOpen->RequestSpinLock);
if(ReqStatus==NDIS_STATUS_SUCCESS)
{//成功,返回
return STATUS_SUCCESS;
}
else
{
//错误,假设为以太网,设为1514字节长度,函数返回
*pMtu=1514;
return STATUS_SUCCESS;
}
}
上述代码中,宏CONTAINING_RECORD的意义较难理解,其原型如下:
PCHAR CONTAINING_RECORD(IN PCHAR Address,IN TYPE Type,IN PCHAR Field);
上述代码中,参数Address是指向Type类型的结构体实例中的一个成员指针;参数Type是需要返回结构体基地址的类型名;参数Field是Address所指向的成员名,该成员包含在Type类型的结构体中。
宏CONTAINING_RECORD返回一个包含Field成员的结构体实例的基地址。
可根据下面的代码来进一步理解该宏:
PLIST_ENTRY RequestListEntry;
PINTERNAL_REQUEST MaxSizeReq;
/从双向链表中移出第一个元素/
RequestListEntry=ExInterlockedRemoveHeadList(
&pOpen->RequestList,&pOpen->RequestSpinLock);
MaxSizeReq=CONTAINING_RECORD(
RequestListEntry,INTERNAL_REQUEST,ListElement);
typedef struct_INTERNAL_REQUEST{
LIST_ENTRY ListElement;
NDIS_EVENT InternalRequestCompletedEvent;
NDIS_REQUEST Request;
NDIS_STATUS RequestStatus;
}INTERNAL_REQUEST,*PINTERNAL_REQUEST;
也就是说如果需要从结构体INTERNAL_REQUEST实例中的一个成员ListElement的地址RequestListEntry中,获得包含该成员实例的INTERNAL_REQUEST结构体实例的地址MaxSizeReq,就可以通过CONTAINING_RECORD宏实现。
3)NPF_RequestComplete函数
NPF_RequestComplete函数用于结束一个OID请求,它是NDIS库函数NdisRequest所关联的回调函数。当NIC驱动程序完成一个OID请求操作(如在NPF_GetDeviceMTU或NPF_IoControl中调用NdisRequest开始请求操作)时它会被NDIS调用。该函数原型如下:
VOID NPF_RequestComplete(
IN NDIS_HANDLE ProtocolBindingContext,
IN PNDIS_REQUEST NdisRequest,
IN NDIS_STATUS Status
)
上述函数中,参数ProtocolBindingContext指向一个上下文区块,它包含一个指向与当前实例相关联的OPEN_INSTANCE结构体的指针;参数NdisRequest为指向已完成OID请求的指针;参数Status为请求操作的状态。
该函数的主要实现代码如下:
VOID NPF_RequestComplete(
IN NDIS_HANDLE ProtocolBindingContext,
IN PNDIS_REQUEST NdisRequest,
IN NDIS_STATUS Status
)
{
PINTERNAL_REQUEST pRequest;
pRequest=CONTAINING_RECORD(
NdisRequest,INTERNAL_REQUEST,Request);
//设置请求结果
pRequest->RequestStatus=Status;
//唤醒调用者
NdisSetEvent(&pRequest->InternalRequestCompletedEvent);
return;
}
(4)NPF_IoControl函数
NPF_IoControl函数用于执行IOCTL调用。当用户对一个正在运行的驱动程序实例调用DeviceIoControl系统函数时,该函数会被调用(响应IRP_MJ_DEVICE_CONTROL),其原型如下:
NTSTATUS NPF_IoControl(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
);
上述函数中,参数DeviceObject指向用户所使用的设备驱动对象;参数Irp指向包含用户请求的IRP。
NPF_IoControl函数返回操作的状态。
一旦打开数据包捕获驱动程序,用户层应用程序就能执行DeviceIoControl系统调用,并通过IOCTL命令来进行配置或查询。IOCTL命令码BIOCSMINTOCOPY用来设置内核缓冲区中最小的数据量大小,即用该命令来设置OPEN_INSTANCE的MinToCopy成员值。如果接收的数据量大于该值,就解除一个读调用的锁定。NPF_IoControl函数中与打开、关闭适配器相关的主要代码如下:
/#defne BIOCSMINTOCOPY 7414/
case BIOCSMINTOCOPY:
/验证输入参数的合法性/
if(IrpSp->Parameters.DeviceIoControl.InputBufferLength<sizeof(ULONG))
{
SET_FAILURE_BUFFER_SMALL();
break;
}
Open->MinToCopy=(*((PULONG)Irp->AssociatedIrp.SystemBuffer))/g_NCpu;
SET_RESULT_SUCCESS(0);
break;
IOCTL命令码BIOCISETLOBBEH用来设置环回的行为,处理接收自己发送数据包的方式:捕获或丢弃。其实现代码如下:
/#defne BIOCISETLOBBEH 7410/
case BIOCISETLOBBEH:
/验证输入参数的合法性/
if(IrpSp->Parameters.DeviceIoControl.InputBufferLength<sizeof(INT))
{
SET_FAILURE_BUFFER_SMALL();
break;
}
if(*(PINT)Irp->AssociatedIrp.SystemBuffer==NPF_DISABLE_LOOPBACK)
{//禁止接收环回数据
Open->SkipSentPackets=TRUE;
//复位捕获缓冲区,因为可能含有环回数据包
NPF_ResetBufferContents(Open);
SET_RESULT_SUCCESS(0);
break;
}
else if(*(PINT)Irp->AssociatedIrp.SystemBuffer==NPF_ENABLE_LOOPBACK)
{//接收环回数据
Open->SkipSentPackets=FALSE;
SET_RESULT_SUCCESS(0);
break;
}
else
{//不可知的操作
SET_FAILURE_INVALID_REQUEST();
break;
}
break;
IOCTL命令码BIOCSETBUFFERSIZE用来设置内核缓冲区大小。在设置了一个NPF实例环形缓冲区的大小后,如果接收到一个BIOCSETBUFFERSIZE命令,驱动程序就会释放掉老的缓冲区,再分配一个新的缓冲区,并在OPEN_INSTANCE结构体中复位与该缓冲区相关的所有参数,而且当前缓冲的所有数据包都会被丢弃。其实现代码如下:
/#defne BIOCSETBUFFERSIZE 9592/
case BIOCSETBUFFERSIZE:
/验证输入参数的合法性/
if(IrpSp->Parameters.DeviceIoControl.InputBufferLength<sizeof(ULONG))
{
SET_FAILURE_BUFFER_SMALL();
break;
}
/获得所需分配缓冲区的字节数/
dim=*((PULONG)Irp->AssociatedIrp.SystemBuffer);
if(dim/g_NCpu<sizeof(struct PacketHeader))
{
dim=0;
}
else
{
tpointer=ExAllocatePoolWithTag(NonPagedPool,dim,'6PWA');
if(tpointer==NULL)
{//没有内存
SET_FAILURE_NOMEM();
break;
}
}
/获得所有缓冲区的自旋锁/
for(i=0;i<g_NCpu;i++)
{
NdisAcquireSpinLock(&Open->CpuData[i].BufferLock);
}
/如果有旧的缓冲区,就释放掉/
if(Open->CpuData[0].Buffer!=NULL)
{
ExFreePool(Open->CpuData[0].Buffer);
}
/在OPEN_INSTANCE结构体中复位相关的所有参数/
for(i=0;i<g_NCpu;i++)
{
if(dim>0)
Open->CpuData[i].Buffer=(PUCHAR)tpointer+(dim/g_NCpu)*i;
else
Open->CpuData[i].Buffer=NULL;
Open->CpuData[i].Free=dim/g_NCpu;
Open->CpuData[i].P=0;
Open->CpuData[i].C=0;
Open->CpuData[i].Accepted=0;
Open->CpuData[i].Dropped=0;
Open->CpuData[i].Received=0;
}
Open->ReaderSN=0;
Open->WriterSN=0;
Open->Size=dim/g_NCpu;
/释放所有缓冲区的自旋锁/
i=g_NCpu;
do
{
i—;
NdisReleaseSpinLock(&Open->CpuData[i].BufferLock);
}while(i!=0);
SET_RESULT_SUCCESS(0);
break;
IOCTL命令码BIOCSETEVENTHANDLE用来把用户(Packet.dll)所分配的读事件句柄传递给内核空间。其中,参数HANDLE为事件句柄,参数size为句柄的大小sizeof(HANDLE)。其实现代码如下:
/#defne BIOCSETEVENTHANDLE 7920/
case BIOCSETEVENTHANDLE:
/验证输入参数的合法性/
if(IrpSp->Parameters.DeviceIoControl.InputBufferLength
!=sizeof(hUserEvent))
{
SET_FAILURE_INVALID_REQUEST();
break;
}
hUserEvent=*(PHANDLE)Irp->AssociatedIrp.SystemBuffer;
/*
*对hUserEvent进行访问授权确认,
*如果访问被授权,就返回该对象体对应的指针pKernelEvent
*/
Status=ObReferenceObjectByHandle(hUserEvent,
EVENT_MODIFY_STATE,*ExEventObjectType,
Irp->RequestorMode,
PVOID*)&pKernelEvent,NULL);
if(!NT_SUCCESS(Status))
{
Information=0;
break;
}
/*
*如果&Open->ReadEvent等于NULL,
*就把Open->ReadEvent赋为pKernelEvent,
*否则Open->ReadEvent值不改变
*/
if(InterlockedCompareExchangePointer(
&Open->ReadEvent,pKernelEvent,NULL)!=NULL)
{//释放pKernelEvent
ObDereferenceObject(pKernelEvent);
SET_FAILURE_INVALID_REQUEST();
break;
}
/把ReadEvent复位为非激活状态/
KeResetEvent(Open->ReadEvent);
SET_RESULT_SUCCESS(0);
break;