# TCP校验和的原理和实现

38 篇文章 6 订阅
84 篇文章 33 订阅
84 篇文章 255 订阅

### 概述

TCP校验和是一个端到端的校验和，由发送端计算，然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到

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

TCP的校验和是必需的，而UDP的校验和是可选的。

TCP和UDP计算校验和时，都要加上一个12字节的伪首部。

Author : zhangskd @ csdn blog

### 定义

(1) 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报头中的校验和字段置为0（否则就陷入鸡生蛋还是蛋生鸡的问题）。

(2) RFC 1071的IP校验和定义

1. Adjacent octets to be checksummed are paired to form 16-bit integers, and the 1's complement sum of these

16-bit integers is formed.

2. To generate a checksum, the checksum field itself is cleared, the 16-bit 1's complement sum is computed over

the octets concerned, and the 1's complement of this sum is placed in the checksum field.

3. To check a checksum, the 1's complement sum is computed over the same set of octets, including the checksum

field. If the result is all 1 bits (-0 in 1's complement arithmetic), the check succeeds.

### 实现

csum_tcpudp_nofold()按4字节累加伪首部到sum中。

static inline unsigned long csum_tcpudp_nofold (unsigned long saddr, unsigned long daddr,
unsigned short len, unsigned short proto,
unsigned int sum)
{
"adcl %3, %0\n"    /* 累加len(2字节), proto, 0*/
"adcl $0, %0\n" /*加上进位 */ : "=r" (sum) : "g" (daddr), "g" (saddr), "g" ((ntohs(len) << 16) + proto*256), "0" (sum)); return sum; }  csum_tcpudp_magic()产生最终的校验和。 首先，按4字节累加伪首部到sum中。 其次，累加sum的低16位、sum的高16位，并且对累加的结果取反。 最后，截取sum的高16位，作为校验和。 static inline unsigned short int csum_tcpudp_magic(unsigned long saddr, unsigned long daddr, unsigned short len, unsigned short proto, unsigned int sum) { return csum_fold(csum_tcpudp_nofold(saddr, daddr, len, proto, sum)); } static inline unsigned int csum_fold(unsigned int sum) { __asm__( "addl %1, %0\n" "adcl 0xffff, %0" : "=r" (sum) : "r" (sum << 16), "0" (sum & 0xffff0000) /* 将sum的低16位，作为寄存器1的高16位，寄存器1的低16位补0。 * 将sum的高16位，作为寄存器0的高16位，寄存器0的低16位补0。 * 这样，addl %1, %0就累加了sum的高16位和低16位。 * * 还要考虑进位。如果有进位，adcl 0xfff, %0为：0x1 + 0xffff + %0，寄存器0的高16位加1。 * 如果没有进位，adcl 0xffff, %0为：0xffff + %0，对寄存器0的高16位无影响。 */ ); return (~sum) >> 16; /* 对sum取反，返回它的高16位，作为最终的校验和 */ } ### 发送校验 #define CHECKSUM_NONE 0 /* 需要由传输层自己计算校验和 */ #define CHECKSUM_HW 1 /* 由硬件计算报头和首部的校验和 */ #define CHECKSUM_UNNECESSARY 2 /* 表示不需要校验，或者已经成功校验了 */ #define CHECKSUM_PARTIAL CHECKSUM_HW #define CHECKSUM_COMPLETE CHECKSUM_HW  @tcp_transmit_skb() icsk->icsk_af_ops->send_check(sk, skb->len, skb); /* 计算校验和 */ void tcp_v4_send_check(struct sock *sk, int len, struct sk_buff *skb) { struct inet_sock *inet = inet_sk(sk); struct tcphdr *th = skb->h.th; if (skb->ip_summed == CHECKSUM_HW) { /* 只计算伪首部，TCP报头和TCP数据的累加由硬件完成 */ th->check = ~tcp_v4_check(th, len, inet->saddr, inet->daddr, 0); skb->csum = offsetof(struct tcphdr, check); /* 校验和值在TCP首部的偏移 */ } else { /* tcp_v4_check累加伪首部，获取最终的校验和。 * csum_partial累加TCP报头。 * 那么skb->csum应该是TCP数据部分的累加，这是在从用户空间复制时顺便累加的。 */ th->check = tcp_v4_check(th, len, inet->saddr, inet->daddr, csum_partial((char *)th, th->doff << 2, skb->csum)); } } unsigned csum_partial(const unsigned char *buff, unsigned len, unsigned sum) { return add32_with_carry(do_csum(buff, len), sum); } static inline unsigned add32_with_carry(unsigned a, unsigned b) { asm("addl %2, %0\n\t" "adcl$0, %0"
: "=r" (a)
: "0" (a), "r" (b));
return a;
}


do_csum()用于计算一段内存的校验和，这里用于累加TCP报头。

1. 反码累加时，按16位、32位、64位来累加的效果是一样的。

2. 使用内存对齐，减少内存操作的次数。

static __force_inline unsigned do_csum(const unsigned char *buff, unsigned len)
{
unsigned odd, count;
unsigned long result = 0;

if (unlikely(len == 0))
return result;

/* 使起始地址为XXX0，接下来可按2字节对齐 */
odd = 1 & (unsigned long) buff;
if (unlikely(odd)) {
result = *buff << 8; /* 因为机器是小端的 */
len--;
buff++;
}
count = len >> 1; /* nr of 16-bit words，这里可能余下1字节未算，最后会处理*/

if (count) {
/* 使起始地址为XX00，接下来可按4字节对齐 */
if (2 & (unsigned long) buff) {
result += *(unsigned short *)buff;
count--;
len -= 2;
buff += 2;
}
count >>= 1; /* nr of 32-bit words，这里可能余下2字节未算，最后会处理 */

if (count) {
unsigned long zero;
unsigned count64;
/* 使起始地址为X000，接下来可按8字节对齐 */
if (4 & (unsigned long)buff) {
result += *(unsigned int *)buff;
count--;
len -= 4;
buff += 4;
}
count >>= 1; /* nr of 64-bit words，这里可能余下4字节未算，最后会处理*/

/* main loop using 64byte blocks */
zero = 0;
count64 = count >> 3; /* 64字节的块数，这里可能余下56字节未算，最后会处理 */
while (count64) { /* 反码累加所有的64字节块 */
asm ("addq 0*8(%[src]), %[res]\n\t"    /* b、w、l、q分别对应8、16、32、64位操作 */
"addq 1*8(%[src]), %[res]\n\t"    /* [src]为指定寄存器的别名，效果应该等同于0、1等 */
: [res] "=r" (result)
: [src] "r" (buff), [zero] "r" (zero), "[res]" (result));
buff += 64;
count64--;
}

/* 从这里开始，反序处理之前可能漏算的字节 */

/* last upto 7 8byte blocks，前面按8个8字节做计算单位，所以最多可能剩下7个8字节 */
count %= 8;
while (count) {
: "=r" (result)
: "m" (*(unsigned long *)buff), "r" (zero), "0" (result));
--count;
buff += 8;
}

/* 带进位累加result的高32位和低32位 */

/* 之前始按8字节对齐，可能有4字节剩下 */
if (len & 4) {
result += *(unsigned int *) buff;
buff += 4;
}
}

/* 更早前按4字节对齐，可能有2字节剩下 */
if (len & 2) {
result += *(unsigned short *) buff;
buff += 2;
}
}

/* 最早之前按2字节对齐，可能有1字节剩下 */
if (len & 1)
result += *buff;

/* 再次带进位累加result的高32位和低32位 */
result = add32_with_carry(result>>32, result & 0xffffffff);

/* 这里涉及到一个技巧，用于处理初始地址为奇数的情况 */
if (unlikely(odd)) {
result = from32to16(result); /* 累加到result的低16位 */
/* result为：0 0 a b
* 然后交换a和b，result变为：0 0 b a
*/
result = ((result >> 8) & 0xff) | ((result & oxff) << 8);
}

return result; /* 返回result的低32位 */
}

static inline unsigned short from32to16(unsigned a)
{
unsigned short b = a >> 16;
: "=r" (b)
: "0" (b), "r" (a));
return b;
}

csum_partial_copy_from_user()用于拷贝用户空间数据到内核空间，同时计算用户数据的校验和，

/**
* csum_partial_copy_from_user - Copy and checksum from user space.
* @src: source address (user space)
* @len: number of bytes to be copied.
* @isum: initial sum that is added into the result (32bit unfolded)
*
* Returns an 32bit unfolded checksum of the buffer.
* src and dst are best aligned to 64bits.
*/

unsigned int csum_partial_copy_from_user(const unsigned char __user *src,
unsigned char *dst, int len, unsigned int isum, int *errp)
{
might_sleep();
*errp = 0;

/* Why 6, not 7? To handle odd addresses aligned we would need to do considerable
* complications to fix the checksum which is defined as an 16bit accumulator. The fix
* alignment code is primarily for performance compatibility with 32bit and that will handle
* 处理X010、X100、X110的起始地址。不处理X001，因为这会使复杂度大增加。
*/
if (unlikely((unsigned long)src & 6)) {
while (((unsigned long)src & 6) && len >= 2) {
__u16 val16;
*errp = __get_user(val16, (__u16 __user *)src);
if (*errp)
return isum;
*(__u16 *)dst = val16;
src += 2;
dst += 2;
len -= 2;
}
}

/* 计算函数是用纯汇编实现的，应该是因为效率吧 */
isum = csum_parial_copy_generic((__force void *)src, dst, len, isum, errp, NULL);

if (likely(*errp == 0))
return isum; /* 成功 */
}

*errp = -EFAULT;
memset(dst, 0, len);
return isum;
}


unsigned int csum_partial_copy_from_user(const unsigned char *src,
unsigned char *dst, int len, int sum,
int *err_ptr)
{
if (copy_from_user(dst, src, len)) { /* 拷贝用户空间数据到内核空间 */
return (-1);
}

return csum_partial(dst, len, sum); /* 计算用户数据的校验和，会存到skb->csum中 */
}


### 接收校验

@tcp_v4_rcv

/* 检查校验和 */

if (skb->ip_summed != CHECKSUM_UNNECESSARY && tcp_v4_checksum_init(skb))

static int tcp_v4_checksum_init(struct sk_buff *skb)
{
/* 如果TCP报头、TCP数据的反码累加已经由硬件完成 */
if (skb->ip_summed == CHECKSUM_HW) {

/* 现在只需要再累加上伪首部，取反获取最终的校验和。
* 校验和为0时，表示TCP数据报正确。
*/
skb->ip_summed = CHECKSUM_UNNECESSARY;
return 0; /* 校验成功 */

} /* 没有else失败退出吗？*/
}

/* 对伪首部进行反码累加，主要用于软件方法 */

/* 对于长度小于76字节的小包，接着累加TCP报头和报文，完成校验；否则，以后再完成检验。*/
if (skb->len <= 76) {
return __skb_checksum_complete(skb);
}
}


tcp_v4_rcv、tcp_v4_do_rcv()

| --> tcp_checksum_complete()

| --> __tcp_checksum_complete()

| --> __skb_checksum_complete()

tcp_rcv_established()

| --> tcp_checksum_complete_user()

| --> __tcp_checksum_complete_user()

| --> __tcp_checksum_complete()

| --> __skb_checksum_complete()

unsigned int __skb_checksum_complete(struct sk_buff *skb)
{
unsigned int sum;

sum = (u16) csum_fold(skb_checksum(skb, 0, skb->len, skb->csum));

if (likely(!sum)) { /* sum为0表示成功了 */
/* 硬件检测失败，软件检测成功了，说明硬件检测有误 */
if (unlikely(skb->ip_summed == CHECKSUM_HW))
netdev_rx_csum_fault(skb->dev);
skb->ip_summed = CHECKSUM_UNNECESSARY;
}
return sum;
}


/* Checksum skb data. */
unsigned int skb_checksum(const struct sk_buff *skb, int offset, int len, unsigned int csum)
{
int start = skb_headlen(skb); /* 线性区域长度 */
/* copy > 0，说明offset在线性区域中。
* copy < 0，说明offset在此skb的分页数据中，或者在其它分段skb中。
*/
int i, copy = start - offset;
int pos = 0; /* 表示校验了多少数据 */

if (copy > 0) { /* 说明offset在本skb的线性区域中 */
if (copy > len)
copy = len; /* 不能超过指定的校验长度 */

/* 累加copy长度的线性区校验 */
csum = csum_partial(skb->data + offset, copy, csum);

if ((len -= copy) == 0)
return csum;

offset += copy; /* 接下来从这里继续处理 */
pos = copy; /* 已处理数据长 */
}

/* 累加本skb分页数据的校验和 */
for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {
int end;
BUG_TRAP(start <= offset + len);

end = start + skb_shinfo(skb)->frags[i].size;

if ((copy = end - offset) > 0) { /* 如果offset位于本页中，或者线性区中 */
unsigned int csum2;
skb_frag_t *frag = &skb_shinfo(skb)->frags[i];

if (copy > len)
copy = len;

vaddr = kmap_skb_frag(frag); /* 把物理页映射到内核空间 */
csum2 = csum_partial(vaddr + frag->page_offset + offset - start, copy, 0);

/* 如果pos为奇数，需要对csum2进行处理。
* csum2：a, b, c, d => b, a, d, c
*/

if (! (len -= copy))
return csum;

offset += copy;
pos += copy;
}
start = end; /* 接下来从这里处理 */
}

/* 如果此skb是个大包，还有其它分段 */
if (skb_shinfo(skb)->frag_list) {
struct sk_buff *list = skb_shinfo(skb)->frag_list;

for (; list; list = list->next) {
int end;
BUG_TRAP(start <= offset + len);

end = start + list->len;

if ((copy = end - offset) > 0) { /* 如果offset位于此skb分段中，或者分页，或者线性区 */
unsigned int csum2;
if (copy > len)
copy = len;

csum2 = skb_checksum(list, offset - start, copy, 0); /* 递归调用 */
if ((len -= copy) == 0)
return csum;

offset += copy;
pos += copy;
}
start = end;
}
}

BUG_ON(len);
return csum;
}

• 32
点赞
• 85
收藏
觉得还不错? 一键收藏
• 打赏
• 7
评论
12-27
07-25 324
01-27 3万+
03-19 2721
02-27 4万+
03-22 4122
04-24 1万+
09-30 8272
06-30 2630
02-25 1176
08-21 1162

### “相关推荐”对你有帮助么？

• 非常没帮助
• 没帮助
• 一般
• 有帮助
• 非常有帮助

zhangskd

¥1 ¥2 ¥4 ¥6 ¥10 ¥20

1.余额是钱包充值的虚拟货币，按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载，可以购买VIP、付费专栏及课程。