TCP校验和计算原理与实现
阅读原文时间:2021年04月25日阅读:1

1. 概述

TCP首部校验和计算三部分:TCP首部+TCP数据+TCP伪首部。

TCP校验和覆盖TCP首部和TCP数据,而IP首部中的校验和只覆盖IP的首部,不覆盖IP数据报中的任何数据。

伪首部是为了增加TCP校验和的检错能力:如检查TCP报文是否收错了(目的IP地址)、传输层协议是否选对了(传输层协议号)等。伪首部来自IP首部。

RFC 793的TCP校验和定义

The checksum field is the 16 bit one's complement of the one's complement sum of all 16-bit words in the header and text.

If a segment contains an odd number of header and text octets to be checksummed, the last octet is padded on the right

with zeros to form a 16-bit word for checksum purposes. The pad is not transmitted as part of the segment. While computing

the checksum, the checksum field itself is replaced with zeros.

上述的定义说得很明确:

首先,把伪首部、TCP报头、TCP数据分为16位的字,如果总长度为奇数个字节,则在最后增添一个位都为0的字节。

把TCP报头中的校验和字段置为0。

其次,用反码相加法累加所有的16位字( 进位也要累加)。

最后,将上述结果作为TCP的校验和。

验证示例:

校验和  反码求和过程

以4bit 为例

发送端计算:

数据:   1000  0100   校验和  0000

则反码:0111  1011               1111

叠加:   0111+1011+1111 = 0010 0001   高于4bit的, 叠加到低4位      0001 + 0010 = 0011 即为校验和

接收端计算:

数据:  1000   0100   检验和  0011

反码:  0111   1011                1100

叠加:  0111 + 1011 +1100 = 0001 1110  叠加为4bit为1111.   全为1,则正确

2.  校验和反码求和  的实现

发送方:原码相加 ,并将高位叠加到低位,取反 ,得到反码求和结果,放入校验和

接收方:将所有原码 相加,高位叠加, 如全为1,则正确  

代码如下:

  ` USHORT checksum (USHORT *buffer,int size)`




  ` {`




  ` Unsigned long cksum=0;`




  ` While (size>1)`




  ` {`




  ` Cksum +=*buffer++;`




  ` size -=sizeof(USHORT);`




  ` }`




  ` If (size)`




  ` {`




  ` Cksum +=*(UCHAR *) buffer;`




  ` }`




  ` //将32位转换为16位`




  ` While (cksum>>16)`




  ` Cksum = (cksum>>16) + (cksum & 0xffff);`




  ` return (USHORT) (~cksum);`




  ` }`

    buffer是指向需要校验数据缓冲区的指针,size是需要检验数据的总长度(字节为单位)。

    4-8行代码是对数据按16bit累加求和,由于最高位的进位需要加在最低位上,所以cksum必须是32位的unsigned long型,高16bit用于保存累加过程中的进位;

    9-11行是对size为奇数情况的处理。

    14~15行代码的作用是将cksum高16bit的值加到低16bit上,即把累加中最高位的进位加到最低位上。这里使用了 while循环,判断cksum高16bit是否非零,因为第16行代码执行的时候,还是可能向cksum的高16bit进位。

    有些地方是通过下面两条代码实现的:

    Cksum = (cksum >> 16) + (cksum & 0xffff);

    Cksum = (cksum >> 16);

    这里只进行了两次相加,即可保证相加后cksum的高16位为0,两种方式的效果是一样,事实上,上面的循环也最多执行两次!

    16行代码即对16bit数据累加的结果取反,得到二进制反码求和的结果,然后函数返回该值。

3. Linux kernel中实现:

csum为32bit ,存储原码相加的结果, 将原码高16bit叠加到低16bit,然后求反,即可得到 反码求和的结果

 `static inline __sum16 csum_fold(__wsum csum)`




 `{`




 ` u32 sum = (__force u32)csum;`




 ` sum = (sum & 0xffff) + (sum >> 16); //将高16 叠加到低16`




 ` sum = (sum & 0xffff) + (sum >> 16); //将产生的进位   叠加到 低16`




 ` return (__force __sum16)~sum; //求反,   得到二进制反码求和的结果`




 `}`

其中上述csum的计算如下:

skb_add_data -> csum_partial     计算  添加数据段的校验和 ,将其放置在 skb->csum 中,完成tcp数据部分计算。

tcp_transmit_skb(传输层统一出口函数) 为 skb添加 tcp首部,并在其中调用  tcp_v4_send_check 来完成 tcp首部的最终校验和计算( tcp首部+tcp数据+tcp伪首部 )。

源码分析:

tcp_transmit_skb

{

    icsk->icsk_af_ops->send_check(sk, skb->len, skb); // 完成 tcp首部校验和的计算

    …

    …

    err = icsk->icsk_af_ops->queue_xmit(skb, 0);  // 将其发往 IP层     ip_queue_xmit

}

其中  tcp_transmit_skb--> icsk->icsk_af_ops->send_check( tcp_v4_send_check)

tcp_v4_send_check

{

    …..    

    th->check = tcp_v4_check(len, inet->inet_saddr, inet->inet_daddr,

                     csum_partial(th, th->doff << 2, skb->csum));  // 将 th首部 校验和 和 tcp数据段校验和相加

    ….

}

//NETIF_F_ALL_CSUM

static inline __sum16 tcp_v4_check(int len, __be32 saddr,

                   __be32 daddr, __wsum base)

{

    return csum_tcpudp_magic(saddr,daddr,len,IPPROTO_TCP,base);// 将tcp首部+tcp数据+tcp伪首部 三部分校验和相加,并将 32bit的 csum (skb->csum),折叠计算为16bit的 check,存放在 th->check (tcp首部校验和字段)中

即产生最终校验和

}

// 对伪头 计算校验和

static inline __sum16 csum_tcpudp_magic(__be32 saddr, __be32 daddr,  unsigned short len,

                           unsigned short proto,  __wsum sum)

{

    return csum_fold(csum_tcpudp_nofold(saddr,daddr,len,proto,sum));

}

//将32位值的高16位折叠到低16位中,然后取反输出值,即为发送端应填入的校验和

static inline __sum16 csum_fold(__wsum csum)

{

u32 sum = (__force u32)csum;

sum = (sum & 0xffff) + (sum >> 16);      //将高16 叠加到低16

sum = (sum & 0xffff) + (sum >> 16);      //将产生的进位   叠加到 低16

return (__force __sum16)~sum;               //求反,    得到二进制反码求和的结果

}