(1) 把IP数据报的校验和字段置为0。
(2) 把首部看成以16位为单位的数字组成,依次进行二进制反码求和
(3) 把得到的结果存入校验和字段中。
在接收数据时,计算数据报的校验和相对简单,按如下步骤:
(1)把首部看成以16位为单位的数字组成,依次进行二进制反码求和,包括校验和字段。
(2)检查计算出的校验和的结果是否等于零。
(3)如果等于零,说明被整除,校验是和正确。否则,校验和就是错误的,协议栈要抛弃这个数据包。
Linux 2.6内核中的校验算法,使用汇编语言编写的,显然效率要高些
/usr/src/linux-2.6.23/include/asm-i386/checksum.h
static inline __sum16 ip_fast_csum(const void *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"
: "=r" (sum), "=r" (iph), "=r" (ihl)
: "1" (iph), "2" (ihl)
: "memory");
return (__force __sum16)sum;
}
(1) 将IP头部(包括可选项)以32位为单位进行进位加法运算
(2) 将sum的低16位和高16位相加
(3) 取反
1b -- 1 before
在这个函数中,第一个参数显然就是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头数据,来解cksum的惑
IP头数据:
01000101 /*ver_hlen*/
00000000 /*tos*/
00000000 00000010 /*len*/
00000000 00000000 /*id*/
00000000 00000000 /*offset*/
00000100 /*ttl*/
00010001 /*type*/
00000000 00000000 /*cksum(0)*/
01111111 00000000 00000000 00000001 - /*sip*/
01111111 00000000 00000000 00000001 - /*dip*/
运算过程(注意是大端格式加):
for(sum = 0; nword > 0; nword--)
sum += *buf++;
- 01000101 00000000
- 00000000 00000010
---------------------
- 01000101 00000010
- 00000000 00000000
---------------------
- 01000101 00000010
- 00000000 00000000
---------------------
- 01000101 00000010
- 00000100 00010001
---------------------
- 01001001 00010011
- 00000000 00000000
---------------------
- 01001001 00010011
- 01111111 00000000
---------------------
- 11001000 00010011
- 00000000 00000001
---------------------
- 11001000 00010100
- 01111111 00000000
---------------------
101000111 00010100
- 00000000 00000001
---------------------
101000111 00010101 sum
sum = (sum>>16) + (sum&0xffff);
00000000 00000001 (sum>>16)
01000111 00010101 (sum&0xffff)
---------------------
01000111 00010110
sum += (sum>>16);
01000111 00010110
00000000 00000000 (sum>>16)
---------------------
01000111 00010110 sum
~sum
10111000 11101001 cksum
说白了就是循环加,然后在取反!
对方机器调用checksum()计算校验和,如果校验和为0表明IP包传输正确
-----------------------------------------------------------
01000101 /*ver_hlen*/
00000000 /*tos*/
00000000 00000010 /*len*/
00000000 00000000 /*id*/
00000000 00000000 /*offset*/
00000100 /*ttl*/
00010001 /*type*/
10111000 11101001 /*cksum(0)*/
01111111 00000000 00000000 00000001 /*sip*/
01111111 00000000 00000000 00000001 /*dip*/
-01000101 00000000
- 00000000 00000010
---------------------
- 01000101 00000010
- 00000000 00000000
---------------------
- 01000101 00000010
- 00000000 00000000
---------------------
- 01000101 00000010
- 00000100 00010001
---------------------
- 01001001 00010011
- 10111000 11101001
---------------------
100000001 11111100
- 01111111 00000000
---------------------
110000000 11111100
- 00000000 00000001
---------------------
110000000 11111101
- 01111111 00000000
---------------------
111111111 11111101
- 00000000 00000001
---------------------
111111111 11111110 sum
sum = (sum>>16) + (sum&0xffff);
00000000 00000001 (sum>>16)
11111111 11111110 (sum&0xffff)
----------------------
11111111 11111111
sum += (sum>>16);
11111111 11111111
00000000 00000000 (sum>>16)
----------------------
11111111 11111111
~sum
00000000 00000000
| |||||||||||||
UDP校验和覆盖的内容超出了UDP数据报本身的范围。为了计算校验和,UDP把伪首部(PSEUDO-HEADER)引入数据报中, 在伪首部中有一个值为零的填充八位组用于保证整个数据报的长度为16比特的整数倍。使用伪首部的的目的是检验UDP数据报已达到正确的目的地。
伪首部的源IP地址字段和目的IP地址字段记录了发送UDP报文时使用的源IP地址和目的IP地址。 协议字段指明了所使用的协议类型代码(UDP是17),而UDP长度字段是UDP数据报的长度(伪首部的长度不计算在内)。 UDP计算校验和的方法和计算IP数据报首部校验和的方法相似。 但不同的是:IP数据报的校验和只检验IP数据报的首部,但UDP的校验和是将首部和数据部分一起都检验。 在发送端,首先是将全零放入检验和字段。再将伪首部以及UDP用户数据报看成是由许多16bit的字串接起来。 若UDP用户数据报的数据部分不是偶数个字节,则要填入一个全零字节(即:最后一个基数字节应是16位数的高字节而低字节填0)。 然后按二进制反码计算出这些16bit字的和(两个数进行二进制反码求和的运算的规则是:从低位到高位逐列进行计算。 0和0相加是0,0和1相加是1,1和1相加是0但要产生一个进位1,加到下一列。若最高位相加后产生进位,则最后得到的结果要加1)。 将此和的二进制反码写入校验和字段后,发送此UDP用户数据报。 在接收端,将收到的UDP用户数据报连同伪首部(以及可能的填充全零字节)一起,按二进制反码求这些16bit字的和。 当无差错时其结果应全为1。否则就表明有差错出现, 接收端就应将此UDP用户数据报丢弃(也可以上交给应用层,但附上出现了差错的警告)。 |
转载自 若木的博客
tomxaoying:
TCP校验和
TCP 的校验和计算方法同UDP一样,同样要加上一个伪头部,区别是伪头部的协议码是0x06,长度是整个TCP报文的长度(包含TCP头部)。
ICMP的校验和
ICMP校验和的计算方法一样,只不过只是对ICMP包整个进行校验和,没有伪头部,也不包括IP包头部。
注意到校验和计算的这种特性,当只改变ip包的ip地址而其他的都不改变时,可以基于原来的checksum计算新的checksum的一种简单的方法
假设原来的dst ip为 192.168.0.1,在某个处理模块中被改变成192.168.0.10,因此这里变大了9
令原来的checksum为check_s_1,原来在计算checksum时求和之后没取反的临时变量为check_s_2,则 check_s_2 = ~check_s_1, 令新的同样为 check_t_2 = ~check_t_1
则不考虑特殊情况(最后进位的情况)有,check_t_2 = check_s_2 + 10 - 1 = check_s_2 + 9
因此有关系 check_t_1 = check_s_1 - 9 (不考虑特殊溢出情况,如果考虑最后进位,溢出等情况,自己试一试)
上面太长,我只想说最后的几句
|
上一个程序已经看到,sum是一个32位整形,而我们要的效验和是16位的,所以必须要把前面16位和计算得到
的sum再加工,得到最后的answer,这个加工过程看似简单,但我开始是没搞清楚的,后来经过CUBBS上的
兄弟们帮忙,搞清楚了,所以在这里贴一下~~
1).sum=(sum>>16)+(sum&0xffff);
这句是“把sum的高16位加到低16位”,具体分析:(sum>>16)是将高16位移位到低16位,
(记住此时sum值是没有变的哦~),此时这个括号的值就是sum的高16位的值(但在低16位位置上);
(sum&0xffff)是取出sum的低16位,按位与应该都能理解(此时sum的值依然是没变的哦~),
最终,“+”前的高16位值和“+”后的低16位值相加(虽然一个是高16位值一个是低16位值,
但现在它们的位置都在低16位,所以相加)
得到新的sum.
可以把句子"复杂化":
sum1=sum>>16;
sum2=sum&0xffff;
sum=sum1+sum2;
2).sum+=(sum>>16);
(这一句差点没想出来,嘿嘿)这句的意思是:把sum的进位加到低16位。效验和是要加进位的,在上一步中
sum已经变化了,高低16位都已经加到低16位,但不要忽略了可能的进位,因为是多个16位和,所以进位是
常有的事,这就是这一句的作用.3).answer=~ sum;
answer是2B的(不是骂它哈),所以4B的 sum取反,再赋值给answer,answer取到低 16位。
GAME OVER
说简单不简单,说复杂不复杂,需要 注意几个地方:
1. sum值在计算的第一步中,位移和按位与 sum的值是始终不变的;
2.不能忽略进位;
3.怎样把32位的变量的值弄成2B;