IP 首部校验和的计算 zz
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
IP 首部校验和的计算zz
1IP数据包的头信息格式:
+-------------------------------------------------+
| 版本 (4位) |
+-------------------------------------------------+
| 首部长度(4位) |
+-------------------------------------------------+
| 服务类型(TOS)8位 |
+-------------------------------------------------+
| 数据包总长度(16位) |
+-------------------------------------------------+
| 标识ID号(16位) |
+-------------------------------------------------+
| 标志位(3位) |
+-------------------------------------------------+
| 片偏移(13位) |
+-------------------------------------------------+
| 生存时间(TTL)(8位) |
+-------------------------------------------------+
| 协议类型 (8位) |
+-------------------------------------------------+
| 首部校验和(16位) |
+-------------------------------------------------+
| 源IP地址(32位) |
+-------------------------------------------------+
| 目的IP地址 (32位) |
+-------------------------------------------------+
* IP选项(若有) (32位) *
+-------------------------------------------------+
* 数据 *
+-------------------------------------------------+
这里要说的是首部校验和字段。
在发送数据时,为了计算数IP据报的校验和。
应该按如下步骤:
(1)把IP数据报的首部都置为0,包括校验和字段。
(2)把首部看成以16位为单位的数字组成,依次进行二进制反码求和。
(3)把得到的结果存入校验和字段中。
在接收数据时,计算数据报的校验和相对简单,按如下步骤:
(1)把首部看成以16位为单位的数字组成,依次进行二进制反码求和,包括校验和字段。
(2)检查计算出的校验和的结果是否等于零。
(3)如果等于零,说明被整除,校验是和正确。
否则,校验和就是错误的,协议栈要抛弃这个数据包。
首先,查看了Linux 2.6内核中的校验算法,使用汇编语言编写的,显然效率要高些。
代码如下:
unsigned short ip_fast_csum(unsigned char * iph,
unsigned int ihl)
{
unsigned int sum;
__asm__ __volatile__(
"movl (%1), %0 ;\n"
"subl $4, %2 ;\n"
"jbe 2f ;\n"
"addl 4(%1), %0 ;\n"
"adcl 8(%1), %0 ;\n"
"adcl 12(%1), %0 ;\n"
"1: adcl 16(%1), %0 ;\n"
"lea 4(%1), %1 ;\n"
"decl %2 ;\n"
"jne 1b ;\n"
"adcl $0, %0 ;\n"
"movl %0, %2 ;\n"
"shrl $16, %0 ;\n"
"addw %w2, %w0 ;\n"
"adcl $0, %0 ;\n"
"notl %0 ;\n"
"2: ;\n"
/* Since the input registers which are loaded with iph and ihl
are modified, we must also specify them as outputs, or gcc
will assume they contain their original values. */
: "=r" (sum), "=r" (iph), "=r" (ihl)
: "1" (iph), "2" (ihl)
: "memory");
return(sum);
}
在这个函数中,第一个参数显然就是IP数据报的首地址,所有算法几乎一样。
需要注意的是第二个参数,它是直接使用IP数据报头信息中的首部长度字段,不需要进行转换,因此,速度又快了(高手就是考虑的周到)。
使用方法会在下面的例子代码中给出。
第二种算法就非常普通了,是用C语言编写的。
我看了许多实现网络协议栈的代码,这个算法是最常用的了,即使变化,也无非是先取反后取和之类的。
考虑其原因,估计还是C语言的移植性更好吧。
下面是该函数的实现:
unsigned short checksum(unsigned short *buf,int nword)
{
unsigned long sum;
for(sum=0;nword>0;nword--)
sum += *buf++;
sum = (sum>>16) + (sum&0xffff);
sum += (sum>>16);
return ~sum;
}
这里我要把我的惨痛的经历也要说一下,为了研究IP校验和的算法,我根据算法也进行了代码编写,可是结果总是有8位不一样,郁闷了好久,最后还是David L .Stevens给我了答案(不要误会,是在他的书中找到的答案,呵呵)。
那就是现在我们所用的机器设备大多数是使用二进制补码算法进行计算的。
因此,仅仅简单的累加得出的校验和并不是正确的结果。
下面就是IP数据报首部校验和算法的代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <sys/socket.h>
#define ETH_P_LENGTH 65535
#define ETHERNET_MAX_LEN 1500
#define ETHERNET_MIN_LEN 46
unsigned short ip_fast_csum(unsigned char * iph, unsigned int ihl);
unsigned short checksum(unsigned short *buf,int nword);
//--------------------------------------------------------------------
// Main function
//
// Do all if it can do
//
//--------------------------------------------------------------------
int main(int argc,char *argv[])
{
int listenfd;
int nbyte;
char buf[ETH_P_LENGTH];
struct ethhdr *eth = NULL;
struct iphdr *ip = NULL;
short chk;
//
// Print banner
//
printf("\n\tSendArp v1.0 - scan IP and MAC\n");
printf("\tNsfocus - \n");
printf("\tby David Zhou\n");
printf("\tDate : 2006/01/19\n\n");
if ((listenfd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))) < 0) {
printf("Call socket() function error\n");
return 1;
}
for (;;)
{
if ((nbyte = recv(listenfd, buf, ETH_P_LENGTH, 0)) > 0)
{
struct ethhdr *eth = (struct ethhdr *)buf;
if(ntohs(eth->h_proto) == ETH_P_IP)
{ // EtherNet frame
// print ip sum
ip = (struct iphdr *)&buf[14];
printf("IP CheckSum = 0x%04X\n",ntohs(ip->check));
//verify ip checksum
chk = checksum((unsigned short*)ip,10);
printf("Verify CheckSum = 0x%04X\n\n",ntohs(chk));
//
// reset check to calc self
//
ip->check = 0;
// 2.6 kernel
chk = ip_fast_csum((unsigned char *)ip,ip->ihl);
printf("Calc CheckSum = 0x%04X - %d\n",ntohs(chk),ip->ihl);
// coustom calc
chk = checksum((unsigned short*)ip,10);
printf("Calc CheckSum = 0x%04X\n\n",ntohs(chk));
}
}
}
return 0;
}
unsigned short checksum(unsigned short *buf,int nword)
{
unsigned long sum;
for(sum=0;nword>0;nword--)
sum += *buf++;
sum = (sum>>16) + (sum&0xffff);
sum += (sum>>16);
return ~sum;
}
/*
* This is a version of ip_compute_csum() optimized for IP headers, * which always checksum on 4 octet boundaries.
*
* By Jorge Cwik <jorge@>, adapted for linux by
* Arnt Gulbrandsen.
*/
unsigned short ip_fast_csum(unsigned char * iph,
unsigned int ihl)
{
unsigned int sum;
__asm__ __volatile__(
"movl (%1), %0 ;\n"
"subl $4, %2 ;\n"
"jbe 2f ;\n"
"addl 4(%1), %0 ;\n"
"adcl 8(%1), %0 ;\n"
"adcl 12(%1), %0 ;\n"
"1: adcl 16(%1), %0 ;\n"
"lea 4(%1), %1 ;\n"
"decl %2 ;\n"
"jne 1b ;\n"
"adcl $0, %0 ;\n"
"movl %0, %2 ;\n"
"shrl $16, %0 ;\n"
"addw %w2, %w0 ;\n"
"adcl $0, %0 ;\n"
"notl %0 ;\n"
"2: ;\n"
/* Since the input registers which are loaded with iph and ihl
are modified, we must also specify them as outputs, or gcc
will assume they contain their original values. */
: "=r" (sum), "=r" (iph), "=r" (ihl)
: "1" (iph), "2" (ihl)
: "memory");
return(sum);
}
.==============
IP 首部里的校验和只校验首部;ICMP、IGMP、TCP 和 UDP 首部中的校验和校验首部和数据。
2. 校验和的计算方法
以 IP 首部中的校验和为例。
1) 首先把校验和字段清零;
2) 然后对每 16 位(2 字节)进行二进制反码求和;
这里说的反码求和,不是说先对每 16 位求反码然后求和,而是说把每 16 位当做反码求和;
反码求和时,最高位的进位要进到最低位,也就是循环进位。
3) 对得到的结果取反,然后存于校验和字段。
3. 校验原理
同样以 IP 首部中的校验和为例。
接收方进行校验时,也是对每 16 位(2 字节)进行二进制反码求和。
接收方计算校验和时的首部与发送方计算校验和时的首部相比,多了一个发送方计算出来的校验和的反码。
因此,如果首部在传输过程中没有发生差错,那么接收方计算的结果应该为全一。
说明:由于 IP 报文在网络中传输时 TTL 是在变化的(每经过一个路由器减一),因此在路由器中要对 IP 首部重新校验。
这也解释了为什么 IP 首部里的校验和只校验首部而不校验数据,因为如果数据也校验,那将给路由器增加巨大的负担。
因此对数据校验的任务交给上层协议(TCP 或 UDP)。
附:求校验和的 C 代码实现:
short checksum(short *buffer, int size)
{
unsigned long cksum=0;
while(size >1)
{
cksum+=*buffer++;
size -=sizeof(short);
}
if(size )
{
cksum += *(char*)buffer;
}
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >>16);
return (short)(~cksum);
}
在计算ICMP,TCP/UDP报文的校验和时为什么要加上伪首部?
伪头标的作用:因在校验和中加入了伪头标,故ICMP除能防止单纯数据差错之外,对IP分组也具有保护作用。
也就是说,在系统安装的协议中保证接口的正常工作。
例如,IP错误地将UDP报文送到ICMP中,因UDP和ICMP使用了相同的校验和算法,对报文内容校验不能防止此报文的误发送。
伪头标也校验IP下一个头标值,该值对ICMP和UDP是不同的。
结果,利用校验就能保证IP将报文送往预定的上层协议(也就是说,由于UDP和ICMP中伪头标的下个头标值是不同的,即使报文相同,其校验和也不一样,如UDP报文错误地进入ICMP或ICMP报文进入UDP,就会出现校验和差错。
下面以UDP为例叙述伪头标的计算方法:
1 在UDP数据报前加上伪头标。
2 当应用数据为奇数字节时,最后应加上其值为0的字节(在净荷长度和数据长度中不计及此字节)。
3 将校验和域的值置 0。
4 对此数据以16比特为单位求反和做按位异或运算。
5 当求和结果为0时,将校验和置成16进制的FFFF。
除此之外的情况,将求得的值原封不动地作为校验和应用。
另外,若校验的是TCP,因为TCP不允许发送方省略检验和,故不需特别对其值为0的校验和进行处理,即可省略验证的步骤1。
1 当校验和的值为0时,就假定是正确的,可省略其余的步骤 (注 1)
2 在UDP数据报头标前加上伪头标。
3 当应用数据为奇数字节时,最后应加上其值为0的字节(在净荷长度和数据报长度中不计及此字节)。
4 对此数据以16比特为单位求反后做按位异或运算。
5 如果求和结果为16进制的FFFF,校验和正确,否则不正确。