題圖來自網路tcp/ip 體系中有兩種校驗機制:crc 校驗與校驗和,用於保證訊息的完整性。crc 校驗用於整個乙太網幀的校驗,32 位的校驗碼被新增到乙太網幀的最後四個位元組。更為常用的是校驗和機制,被用於 ip,icmp,tcp,udp 等三四層協議中。
校驗和機制的運算很簡單,計算方法為將校驗範圍內的位元組取反相加,校驗方法為將校驗範圍內的位元組與校驗和相加,如果結果為 1 ,則代表沒有傳輸錯誤發生。
取反相加,相加就相加了,為什麼要取反?取反是因為上文提到的校驗方式:
傳送的訊息欄位為 1011,校驗和取反相加:0100,最終傳送的訊息為 1011 0100可見取反相加是為了能在校驗時,直接將訊息欄位和校驗碼相加,通過結果是否為全 1 來判斷是否有錯誤。利用的是二進位制加法 1+1 = 0 的特性。接收端驗證訊息時,將訊息欄位與校驗和字段通通相加,得到:1011 + 0100 = 1111,全部為1,所以沒有錯誤發生。
作為對比,小明同學提出一種不需要取反的方法,傳送端訊息字段相加得到校驗位。接收端只將訊息字段相加,然後和校驗欄位做比較。相當於傳送端節省了取反的開銷,而接收端的開銷從做一次二進位制加法+與全1做比較變成一次比較(資料字段相加後與校驗和字段比較)。那麼兩種方法孰優孰劣?為什麼 tcp/ip 選擇了前者。作者目前的理解是,當時的設計者基於當時的硬體開銷狀況,選擇了目前使用的這套校驗機制。可能現在的開銷情況會有所不同,需要一些更細緻的硬體實現細節和實驗來了解。
顯然校驗和只能發現奇數字錯誤,曾經學過通訊原理的小明同學表示,為什麼不用漢明碼,最小碼距 3 ,可以檢 2 個錯,還能糾 1 個錯。對於在三層以上使用的校驗和來說,檢錯太多可以算得上是多做無功,因為資料已經通過了二層的檢驗機制,去除了那些受到了嚴重錯誤的資料。過大的校驗開銷並不實用。
兩個數相加,可能是 8 位,也可能是 16 位,亦或者是 32 位,總有可能會溢位,這多了一位如何是好。
簡單 把這溢位的 1 加到最低位去
這裡只需要乙個固定的生成/驗證校驗機制,並不是運算。
那如果又溢位了呢?
那就再加到最低位去。
對每個數先取反再相加似乎很麻煩,把每個數加起來再取反好像容易多了,並且兩者是相同的。
1011 取反-> 0100 0110 取反-> 1001 取反相加 1101在 lwip 實現中,使用將所有校驗區域字段相加再取反的方式,減少了每次取反的操作開銷。直接相加-> 10001 處理溢位 -> 0010 取反 -> 1101 兩者相同
在 ip 資料報首部校驗中,校驗和的寬度為 16bit ,兩個位元組。校驗和本身的長度決定了累加欄位的寬度。ip 首部校驗和計算中,以兩個位元組為單位進行累加。
位元組0 位元組1 + 位元組2 位元組3 + .....+校驗和位元組0 校驗和位元組1.
在校驗和程式中,按照主機位元組序進行計算,即低位址在前,最後將生成的校驗和轉換為網路位元組序。需要轉換因為校驗和的寬度為 16 位,如果校驗只有 1 個位元組,那麼就無所謂位元組序的轉換了。
在將兩個位元組組成 16 位待累加數時,低位址的位元組在高位,這對應於協議中位元組的位置,具體可以參見下方的 lwip 程式實現。
lwip 提供了不只一種校驗的標準實現方式,多種方式可以通過定義在 lwipopts.h 中的巨集定義進行切換。
校驗和相關的函式位於 lwip 根目錄下的 /core/inet_chksum.c 中。inet_chksum 函式為校驗和計算提供統一的入口,接收兩個引數:校驗區域的開始位址以及校驗區域的位元組長度。
inet_chksum 函式中呼叫了真正的校驗和計算函式,我們首先分析 v1.4.1 版本中的校驗和函式:
static u16_t
lwip_standard_chksum(void *dataptr, u16_t len)
if (len > 0)
//完成了所有數的累加後,開始處理進製。
//溢位的機制是,如果發生了溢位,則無視溢位的 1 ,將 1 加到求和結果中
//lwip 這裡的處理是:不關心每次溢位的產生,將所有數完成求和後,統一將高 16 位(溢位數)加到低 16 位(求和數)
//溢位數就相當於溢位的總次數,每次溢位加1,那麼將溢位次數統一加到結果中即可
acc = (acc >> 16) + (acc & 0x0000fffful);
//在完成上一次操作後,可能又發生了溢位,再執行一次同樣的操作,請問是否可能再發生溢位?
if ((acc & 0xffff0000ul) != 0)
//將校驗和轉換為網路位元組序
return htons((u16_t)acc);
}
這裡討論幾個問題,一是 lwip 實現中校驗和的溢位處理。我們之前說到如果發生了溢位,則無視溢位的 1 ,將 1 加到求和結果中。這是從每次計算->處理溢位的思路上來說的。
lwip 實現中,通過宣告乙個 32 位的累加數,先將所有的 16bit數累加,得到溢位的總數(累加數的高 16 位),一起加到校驗和結果(低 16 位)中。
這裡存在兩個問題:16bit 數累加時,如果有 2^16 個數累加,那麼會使 32 位數本身發生溢位,但好在目前人類還沒提出這麼長的協議,所有不用擔心 32 位數的溢位問題。
另乙個問題是,如果將溢位數與結果數累加後,有可能再次溢位 1 ,所以在完成第一次高 16 位與低 16 位的運算後,需要再進行一次該運算,第二次運算不可能產生溢位。(可以用最極端的情況考慮下 16bit 全1 與 16bit 全1 進行運算)
v2.0 版本的預設標準校驗和函式使用一種經過優化的校驗和計算函式,在迴圈中一次計算 8位元組,對開始和結束的非 8 位元組對齊位元組,則另行處理。
u16_t
lwip_standard_chksum(const void *dataptr, int len)
ps = (const u16_t *)(const void*)pb;
if (((mem_ptr_t)ps & 3) && len > 1)
pl = (const u32_t *)(const void*)ps;
while (len > 7)
sum = tmp + *pl++;
if (sum < tmp)
len -= 8;
} sum = fold_u32t(sum);
ps = (const u16_t *)pl;
while (len > 1)
if (len > 0)
sum += t;
sum = fold_u32t(sum); //進行溢位處理操作 作用和之前版本的語句相同
sum = fold_u32t(sum); //直接進行兩次高位和低位相加,而不再對是否溢位進行判斷
if (odd)
return (u16_t)sum;
}
新版本通過一次計算 8 位元組,相比 1.4 版本中一次進行兩位元組運算,加快了校驗和累加的速度。但這樣一來就必須對非對齊的位元組進行處理,因為需要計算的校驗和位元組數不一定是 8 位元組對齊的。程式分別對開始和結束處的單位元組資料和雙位元組資料做了處理。
程式的巧妙之處不少,如果校驗和計算位址從奇位址開始,那麼校驗和的計算順序使用主機位元組序,反之則使用網路位元組序。
另外將高 16 位加到低 16 位的操作會直接執行兩次。先前的版本執行第二次相加操作時,會先進行一次判斷。直接執行加法操作可能要比做判斷快一些。
老實說,作者目前對這個函式的原理還只是略懂,這裡暫時不繼續展開了,歡迎有興趣的讀者一起討論。
校驗和的計算和校驗都使用上述的校驗和生成函式完成。在傳送端生成校驗時,校驗和字段先填充為 0 ,計算完成後將結果填入校驗和字段傳送。接收端則直接對所有字段進行校驗和計算,判斷計算結果是否為全 1 。
在一次自環測試中,使用網路助手傳送某些字串可以接收到同樣的字串,完成自環測試。但是某些字串傳送後卻接收不到 。通過 wireshark 抓包發現,字元所在的包收到了,但是因為 udp 校驗和錯誤沒有被作業系統交付給應用層網路除錯助手顯示。
那麼為什麼有些字串可以通過校驗,有些字串卻會有校驗和錯誤,而且發現他們的校驗和總是比正確的校驗和大 1。
通過檢查發現,網路助手對端協議棧使用和 lwip 1.4 版本類似的方法實現校驗和,使用 32 位變數累加 16 位字段的校驗和,但是沒有處理 32 位變數的進製,導致在產生進製的情況下,校驗和比正確的結果小 1,所以在取反後就比正確的結果大 1 。
校驗和的錯誤實現會造成協議棧的錯誤,更糟糕的是這些錯誤似乎「偶爾」發生。
對於硬體 verilog 來說,假設要校驗的字段以資料流的形式輸入,那麼只要不斷將輸入的資料流相加,最後取反即可,也會在後續的文章中更詳細地討論硬體的實現。
十六進製制數相加校驗和計算程式
function getcheckstr astr string aindex integer string varnewstr1,he,oldstr string tj boolean i integer begin i 1 he tj true oldstr copy astr,aindex,l...
關於ip報文校驗和一些思考
首部檢驗和字段是根據ip首部計算的檢驗和碼。它不對首部後面的資料進行計算 為了計算乙份資料報的ip檢驗和,首先需要把檢驗和字段置為0 對首部中每個16bit進行二進位制反碼求和 整個首部看成是由一串16bit的字組成 收到乙份ip資料報後,同樣對首部中每個16bit進行二進位制反碼求和。由於接收方在...
校驗18位身份證號碼(判斷最後一位校驗位和性別)
公民身份號碼是一系列組合碼,由十七位數字本體碼和一位校驗碼組成。1 將身份證號碼前17位數分別乘以不同的係數。從第一位到第十七位的係數分別為 7 9 10 5 8 4 2 1 6 3 7 9 10 5 8 4 2 2 將這17位數字和係數相乘的結果相加 3 用加出來和除以11,看餘數是多少?4 餘數...