forked from StevenBaby/onix
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0210774
commit 0409c4f
Showing
1 changed file
with
106 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# IP 校验和 | ||
|
||
IP 校验和定义在 RFC1071 [^rfc1071]: | ||
|
||
设需要计算一列字节为 A, B, C, D, ..., Y, Z 的校验和,使用 [a, b] 标识十六位整型 a * 256 + b 或者说 a << 8 + b,十六位反码和可以表示成如下两种形式: | ||
|
||
- [A,B] +' [C,D] +' ... +' [Y,Z] [1] | ||
|
||
- [A,B] +' [C,D] +' ... +' [Z,0] [2] | ||
|
||
其中 +' 表示反码加法,两式分别表示字节数量为偶数和奇数的情况,奇数需要在最后补零,**反码求和** 的结果 **再按位取反** 就是校验和。 | ||
|
||
## 反码 (1's complement) 加法 | ||
|
||
补码 (2's complement) | ||
|
||
首先需要解释一下定义中的反码加法,反码的表示方式为: | ||
|
||
正数就是自身,负数是其正数按位取反,下面是一些例子(以四位为例): | ||
|
||
| 正数 | 正二进制 | 负数 | 负二进制 | | ||
| ---- | -------- | ---- | -------- | | ||
| 0 | 0000 | -0 | 1111 | | ||
| 1 | 0001 | -1 | 1110 | | ||
| 2 | 0010 | -2 | 1101 | | ||
| 3 | 0011 | -3 | 1100 | | ||
| 4 | 0100 | -4 | 1011 | | ||
| 5 | 0101 | -5 | 1010 | | ||
| 6 | 0110 | -6 | 1001 | | ||
| 7 | 0111 | -7 | 1000 | | ||
|
||
反码的加法需要 **循环进位(End Around Carry)**,下面是一个例子,没有进位就结束了,有进位需要将进位加到最低有效位(Least Significant Bit): | ||
|
||
``` | ||
7 + (-3) = 4 | ||
0111 -> 7 | ||
1100 -> -3 | ||
----- | ||
10011 -> carry | ||
0011 | ||
1 | ||
----- | ||
0100 -> 4 | ||
``` | ||
|
||
对于反码机器来说这个进位的过程是自动的,但一般 Intel 的 CPU 是小端补码加法,这个进位的过程需要手动计算; | ||
|
||
于是对于补码机器来说 [A,B] +' [C,D] 的运算过程为: | ||
|
||
[A,B] +' [C,D] = [A,B] + [C,D] + carry | ||
|
||
其中 + 为十六位补码加法,carry 为进位; | ||
|
||
## 交换律和结合律 | ||
|
||
由于程序使用加法,所以很显然具有交换律和结合律,但是,RFC1071 给出了一个拆分的例子,[1] 式可以表示成: | ||
|
||
([A,B] +' [C,D] +' ... +' [J,0] ) +' ([0,K] +' ... +' [Y,Z]) [3] | ||
|
||
## 字节序无关 | ||
|
||
校验和的计算与字节序无关,所以一下方式计算结果与 [1] 式相同: | ||
|
||
[B,A] +' [D,C] +' ... +' [Z,Y] [4] | ||
|
||
对于奇数个字节的情况,需要单独处理最后一个字节,交换过程后就变成了: | ||
|
||
[B,A] +' [D,C] +' ... +' [0,Z] [5] | ||
|
||
所以 RFC1071 后面的 C 代码 **只适用于小端字节序**; | ||
|
||
## 并行求和 | ||
|
||
当机器字长大于 16 bit 是,可以并行计算校验和,这个比较好理解;比如 32 位机器上一次可以计算两个 16bit 的和,然后再拆分成 16bit,需要注意的是进位操作可能需要操作状态字; | ||
|
||
## 延迟进位 | ||
|
||
对于补码机器,每次计算都可能产生进位需要 **循环进位**,实际上可以在溢出之前再做进位,这样可以减少每次进位的判断,或者少做很多次进位加法; | ||
|
||
## 增量更新 | ||
|
||
增量更新在 RFC1141 [^rfc1141],RFC1624 [^rfc1624] 中有更详细的说明。 | ||
|
||
有时可以避免在更新一个报头字段时,重新计算整个校验和。最著名的例子是网关更改 IP 报头中的 TTL 字段,但还有其他例子(例如,更新源路由时)。在这些情况下,可以在不扫描消息或数据报的情况下更新校验和。 | ||
|
||
要更新校验和,只需将已更改的 16 位整数的差相加即可。要了解其工作原理,请观察每个 16 位整数都有一个可加的逆,并且加法具有结合律和交换律。由此可以得出,给定原值 m,新值 m' 和旧校验和 C,新校验和 C'为: | ||
|
||
C' = C + (-m) + m' = C + (m' - m) | ||
|
||
## 循环展开 | ||
|
||
为了减少循环开销,可以展开内部求和循环,在一次循环中执行多次加法,通常可以提供性能,但是会使程序逻辑变得复杂。 | ||
|
||
## 校验和结合数据获取 | ||
|
||
将数据从一个内存位置复制到另一个内存位置涉及到每字节的开销。在这两种情况下,瓶颈本质上是内存总线,也就是说,获取数据的速度有多快。 | ||
|
||
在某些机器上(特别是相对缓慢和简单的微型计算机),通过将内存到内存复制和校验和结合起来,两者只获取一次数据,可以显著减少开销。 | ||
|
||
## 参考 | ||
|
||
[^rfc1071]: <https://datatracker.ietf.org/doc/html/rfc1071> | ||
[^rfc1141]: <https://datatracker.ietf.org/doc/html/rfc1141> | ||
[^rfc1624]: <https://datatracker.ietf.org/doc/html/rfc1624> |