庖丁解牛-----winpcap源码彻底解密(四)
版权申明: 原创文章,转贴请注明出处!!!!!!!!
(1) 如何设置内核缓冲区的大小,前面已经谈过设置内核缓冲区的函数是pcap_setbuff,查看winpcap的开发文档,pcap_setbuff的定义如下:
int pcap_setbuff(pcap_t *p,int dim);
Set the size of the kernel buffer associated with an adapter. dim specifies the size of the buffer in bytes. The return value is 0 when the call succeeds, -1 otherwise. If an old buffer was already created with a previous call to pcap_setbuff(), it is deleted and its content is discarded. pcap_open_live() creates a 1 MByte buffer by default.
下面主要讲解pcap_setbuff是怎样设置内核缓冲区的,在wpcap.dll中的pcap.c文件中定义了pcap_setbuff函数,定义如下:
int pcap_setbuff(pcap_t *p, int dim)
{
return p->setbuff_op(p, dim);
}
其中setbuff_op是一个回调函数,其实调用的是pcap_setbuff_win32,在pcap-win32.c中定义
p->setbuff_op = pcap_setbuff_win32;下面看看这个函数是怎么设置内核缓冲区的。
static int pcap_setbuff_win32(pcap_t *p, int dim)
{
#ifdef HAVE_REMOTE
if (p->rmt_clientside)
{
/* Currently, this is a bug: the capture buffer cannot be set with remote capture */
return 0;
}
#endif /* HAVE_REMOTE */
if(PacketSetBuff(p->adapter,dim)==FALSE)
{
snprintf(p->errbuf, PCAP_ERRBUF_SIZE, "driver error: not enough memory to allocate the kernel buffer");
return -1;
}
return 0;
}
从pcap_setbuff_win32的源码可以出,它是调用PacketSetBuff将应用程序对应的网卡的内核缓冲区的大小dim传递到了parket.dll 下一层。如果要相知道PacketSetBuff怎么讲内核缓冲区的大小传递到驱动程序npf.sys中,就要取跟踪PacketSetBuff的源码了。PacketSetBuff在parket32.c文件中。
//设置内核缓冲区大小
BOOLEAN PacketSetBuff(LPADAPTER AdapterObject,int dim)
{
DWORD BytesReturned;
BOOLEAN Result;
TRACE_ENTER("PacketSetBuff");
#ifdef HAVE_WANPACKET_API
if (AdapterObject->Flags == INFO_FLAG_NDISWAN_ADAPTER)
{
Result = WanPacketSetBufferSize(AdapterObject->pWanAdapter, dim);
TRACE_EXIT("PacketSetBuff");
return Result;
}
#endif
#ifdef HAVE_AIRPCAP_API
if(AdapterObject->Flags == INFO_FLAG_AIRPCAP_CARD)
{
Result = (BOOLEAN)g_PAirpcapSetKernelBuffer(AdapterObject->AirpcapAd, dim);
TRACE_EXIT("PacketSetBuff");
return Result;
}
#endif // HAVE_AIRPCAP_API
#ifdef HAVE_NPFIM_API
if(AdapterObject->Flags == INFO_FLAG_NPFIM_DEVICE)
{
Result = (BOOLEAN)g_NpfImHandlers.NpfImSetCaptureBufferSize(AdapterObject->NpfImHandle, dim);
TRACE_EXIT("PacketSetBuff");
return Result;
}
#endif // HAVE_NPFIM_API
#ifdef HAVE_DAG_API
if(AdapterObject->Flags == INFO_FLAG_DAG_CARD)
{
// We can't change DAG buffers
TRACE_EXIT("PacketSetBuff");
return TRUE;
}
#endif // HAVE_DAG_API
if (AdapterObject->Flags == INFO_FLAG_NDIS_ADAPTER)
{
Result = (BOOLEAN)DeviceIoControl(AdapterObject->hFile,BIOCSETBUFFERSIZE,&dim,sizeof(dim),NULL,0,&BytesReturned,NULL);
}
else
{
TRACE_PRINT1("Request to set buf size on an unknown device type (%u)", AdapterObject->Flags);
Result = FALSE;
}
TRACE_EXIT("PacketSetBuff");
return Result;
}
通过PacketSetBuff 的源码可以看到它是调用DeviceIoControl将设置内核缓冲区的大小的命令发送到npf.sys,上面我们已经多次提到,应用程序和驱动的通信,无论你怎么封装,到了底层都是调用DeviceIoControl,WriteFile,ReadFile函数,一般设置命令使用DeviceIoControl,发送数据包使用WriteFile,但是你要使用DeviceIoControl发送数据包也是可以的,读取驱动中的数据包就使用ReadFile了。设置内核缓冲区的控制码为:BIOCSETBUFFERSIZE,在NTSTATUS NPF_IoControl(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)函数可以看到不同的控制码的处理方式。NPF_IoControl的源码在前面已经说了,这里主要看看内核缓冲区在驱动是怎么设置的。设置内核缓冲区的源码主要如下:
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;
从上面的源码可以看出,winpcap的高明之处在于,它充分的使用了每个cpu,这样的话,你的cpu有几个核,性能就可以明显的体现出来。每个cpu的缓冲区设置为dim/g_NCpu,其中Open是一个全局变量,保存的是一些和绑定网卡相关的信息。CpuData[i].P和CpuData[i].C在读取数据包是非常有用的,他可以用来判断内核缓冲区的数据是否大于pcap_setmintocopy的最小copy的size。这个函数我会在后面的讲道。讲道这里,我们知道pcap_setbuff是怎么设置内核缓冲区的了,主要是调用DeviceIoControl将用户要设置的size传递到内核,而在内核中将它保存一个全局变量中,这样就设置好了内核缓冲区。
(2)如何设置用户缓冲区的大小?下面讲解怎样设置用户缓冲区的大小,linux下面的libcap是没有提供设置用户缓冲区大小(user buffer)的api的,要设置用户缓冲区,必须修改libcap的源码,但是winpcap的高版本是提供了设置用户缓冲区的函数,在wpcap.dll的win32-Extensions.c文件中有一个pcap_setuserbuffer函数,在用户使用时必须添加win32-Extensions.h头文件。pcap_setuserbuffer函数源码如下:
Int pcap_setuserbuffer(pcap_t *p, int size)
{
unsigned char *new_buff;
if (!p->adapter) {
sprintf(p->errbuf,"Impossible to set user buffer while reading from a file or on a TurboCap port");
return -1;
}
if (size<=0) {
/* Bogus parameter */
sprintf(p->errbuf,"Error: invalid size %d",size);
return -1;
}
/* Allocate the buffer */
new_buff=(unsigned char*)malloc(sizeof(char)*size);
if (!new_buff) {
sprintf(p->errbuf,"Error: not enough memory");
return -1;
}
free(p->buffer);
p->buffer=new_buff;
p->bufsize=size;
/* Associate the buffer with the capture packet */
PacketInitPacket(p->Packet,(BYTE*)p->buffer,p->bufsize);
return 0;
}
从上面的源码可以看出,pcap_setuserbuffer调用的是PacketInitPacket函数
VOID PacketInitPacket(LPPACKET lpPacket,PVOID Buffer,UINT Length)
{
TRACE_ENTER("PacketInitPacket");
lpPacket->Buffer = Buffer;
lpPacket->Length = Length;
lpPacket->ulBytesReceived = 0;
lpPacket->bIoComplete = FALSE;
TRACE_EXIT("PacketInitPacket");
}
从PacketInitPacket源码和pcap_setuserbuffer的源码可以看出,设置用户缓冲区相对容易,因为它不涉及到内核,就是对应用程序对应的网卡,pcap_t *p,设置它的用户缓冲区的大小,在设置前清空原来的缓冲区,然后再分配一个size,完成用户缓冲区的设置。
(3)设置内核缓冲区到用户缓冲区最小的copy数据的size,采用pcap_setmintocopy函数进行设置。
Int pcap_setmintocopy(pcap_t *p, int size)
{
return p->setmintocopy_op(p, size);
}
从pcap_setmintocopy的源码可以看出,它和设置内核缓冲区大小有点相似,调用的是setmintocopy_op回调函数。在pcap_win32.c中有:
p->setmintocopy_op = pcap_setmintocopy_win32;
/*set the minimum amount of data that will release a read call*/
static int pcap_setmintocopy_win32(pcap_t *p, int size)
{
if(PacketSetMinToCopy(p->adapter, size)==FALSE)
{
snprintf(p->errbuf, PCAP_ERRBUF_SIZE, "driver error: unable to set the requested mintocopy size");
return -1;
}
return 0;
}
Pcap_setmintocopy_win32调用PacketSetMinToCopy函数设置最小的copy缓冲区:
BOOLEAN PacketSetMinToCopy(LPADAPTER AdapterObject,int nbytes)
{
DWORD BytesReturned;
BOOLEAN Result;
TRACE_ENTER("PacketSetMinToCopy");
#ifdef HAVE_WANPACKET_API
if (AdapterObject->Flags == INFO_FLAG_NDISWAN_ADAPTER)
{
Result = WanPacketSetMinToCopy(AdapterObject->pWanAdapter, nbytes);
TRACE_EXIT("PacketSetMinToCopy");
return Result;
}
#endif //HAVE_WANPACKET_API
#ifdef HAVE_NPFIM_API
if(AdapterObject->Flags == INFO_FLAG_NPFIM_DEVICE)
{
Result = (BOOLEAN)g_NpfImHandlers.NpfImSetMinToCopy(AdapterObject->NpfImHandle, nbytes);
TRACE_EXIT("PacketSetMinToCopy");
return Result;
}
#endif // HAVE_NPFIM_API
#ifdef HAVE_AIRPCAP_API
if(AdapterObject->Flags == INFO_FLAG_AIRPCAP_CARD)
{
Result = (BOOLEAN)g_PAirpcapSetMinToCopy(AdapterObject->AirpcapAd, nbytes);
TRACE_EXIT("PacketSetMinToCopy");
return Result;
}
#endif // HAVE_AIRPCAP_API
#ifdef HAVE_DAG_API
if(AdapterObject->Flags & INFO_FLAG_DAG_CARD)
{
TRACE_EXIT("PacketSetMinToCopy");
// No mintocopy with DAGs
return TRUE;
}
#endif // HAVE_DAG_API
if (AdapterObject->Flags == INFO_FLAG_NDIS_ADAPTER)
{
Result = (BOOLEAN)DeviceIoControl(AdapterObject->hFile,BIOCSMINTOCOPY,&nbytes,4,NULL,0,&BytesReturned,NULL);
}
else
{
TRACE_PRINT1("Request to set mintocopy on an unknown device type (%u)", AdapterObject->Flags);
Result = FALSE;
}
TRACE_EXIT("PacketSetMinToCopy");
return Result;
}
和设置内核缓冲区类似,该函数又是调用的DeviceIoControl函数将nbytes传递到内核npf.sys中,传递码为:BIOCSMINTOCOPY,对应驱动中的源码如下,每个cpu的MintoCopy为(*((PULONG)Irp->AssociatedIrp.SystemBuffer))/g_NCpu; 其中SystemBuffer的大小为应用程序传递过来的缓冲区size。
case BIOCSMINTOCOPY: //set the minimum buffer's size to copy to the application
TRACE_MESSAGE(PACKET_DEBUG_LOUD, "BIOCSMINTOCOPY");
if(IrpSp->Parameters.DeviceIoControl.InputBufferLength < sizeof(ULONG))
{
SET_FAILURE_BUFFER_SMALL();
break;
}
//An hack to make the NCPU-buffers behave like a larger one
Open->MinToCopy = (*((PULONG)Irp->AssociatedIrp.SystemBuffer))/g_NCpu;
SET_RESULT_SUCCESS(0);
break;
其中Open->MinToCopy为打开上下文的全局变量,该变量在读数据包的时候会使用,用来判断内核缓冲区的大小是不是已经满足最小的copy size。如果是就将数据copy到用户缓冲区中,对应的部分源码如下:
for(i=0;i<g_NCpu;i++)
Occupation += (Open->Size - Open->CpuData[i].Free); //计算出已经占用的内核缓冲区
if( Occupation <= Open->MinToCopy*g_NCpu || Open->mode & MODE_DUMP )