[ Checksum Field (TCP/IP) ] データの損失があるかどうかを判断するフィールドである。 ヘッダフォーマットをみれば分かるがチェックサムフィールドは 16bits(2bytes) だ。 どうやって、たった 16bits の領域でデータの損失があるかどうかを判断してるのか? チェックサムフィールドにあるデータとは チェックサムフィールドを 0x00 とした IPヘッダ を 16bits(2bytes) ずつに分割して 「1の補数和」をとり、その計算結果の「1の補数」である。 例えばIPヘッダを例にとると「1の補数和」とは? 0x45 0x00 0x00 0x28 0xFE 0x2D 0x40 0x00 0x14 0x06 0x00 0x00 0xFF 0xFF 0xFF 0xFF 0x7F 0x00 0x00 0x01 という20bytes のデータ(仮にIPヘッダとする)があった場合に これらの「16bits ごとの1の補数和」とは、 0x4500 + 0x0028 + 0xFE2D + 0x4000 + 0x1406 + 0x0000 + 0xFFFF + 0xFFFF + 0x7F00 + 0x0001 = 0x25E14 0x25E14 の後4桁 0x5E14 に桁上がりの数字(この例では 2 )を足す。0x5E14 + 2 = 0x5E16 この 0x5E16 が「16bits ごとの1の補数和」である。 さらに この数字( 0x5E16 )の「1の補数」とは 0x5E16 を bit反転 した値である。つまり 0x5E16 ( 0100 1110 0111 0110 ) 0xA1E9 ( 1011 0001 1000 1001 ) これはつまり 0xFFFF - 0x5E16 = 0xA1E9 ということである。 この値( 0xA1E9 )がチェックサムフィールドに書き込まれる。 ではパケットを受け取ったサーバはどうするのかというと、 まずチェックサムフィールドの値(仮に x とする)を読み込み、チェックサムフィールドを 0x00 にする。 そしてパケットの 16bits ごとの1の補数和(仮に y とする)をとる。 するとデータが壊れてなければ x + y = 0xFFFF となるはずだ。 上の例でいうと 0xA1E9 + 0x5E16 = 0xFFFF となるということだ。 つまり もし 0xFFFF でなければどこかでデータの損失が起こっていることになる。 これにより損失の有無を判断しているのだ。 ここまで分かれば チェックサムを求める関数 を作るのは簡単だ。 16bits ごとの1の補数和 をとるために引数に unsigned short *data を設定し 戻り値を チェックサムフィールド に書き込まれるべく値とする。 よってプログラムはこうだ。 ------------------------------------------------------------------------------ unsigned short in_cksum(unsigned short *data, int size) { unsigned long sum = 0; while(size > 1){ sum += *(data++); size -= 2; } if(size > 0) sum += (*data) & 0xff00; sum = (sum & 0xffff) + (sum >> 16); return ((~(unsigned short)((sum >> 16) + (sum & 0xffff)))); } ------------------------------------------------------------------------------ 基本的にはこれで計算できます。 しかし、これではちょっとした問題が起こります。 試しにこの関数を使ったプログラムを書いてみると.... checksum.c ------------------------------------------------------------------------------ unsigned short in_cksum(unsigned short *data, int size) { unsigned long sum = 0; while(size > 1){ sum += *(data++); size -= 2; } if(size > 0) sum += (*data) & 0xff00; sum = (sum & 0xffff) + (sum >> 16); return ((~(unsigned short)((sum >> 16) + (sum & 0xffff)))); } int main(void) { unsigned short num; unsigned char data[] = { 0x00,0x05,0x00,0x01 }; num = in_cksum((unsigned short *)data, 4); printf("0x%x\n", num); num = ~(0x0005 + 0x0001); printf("0x%x\n", num); return 0; } ------------------------------------------------------------------------------ cksum]$ gcc checksum.c cksum]$ ./a.out 0xf9ff 0xfff9 cksum]$ ん?答えが違うぞ。 0x0005 + 0x0001 = 0x0006 桁上がりは無いのでそのまま bit反転します。 0xFFFF - 0x0006 = 0xFFF9 つまり 0xFFF9 が正しい答えなのだが、 チェックサム関数の戻り値は 0xF9FF を返している。どういうことか? これは上位 1byte と下位 1byte がひっくり返っているのだ。 例えば Intel の x86 (Pentiumも含む)を使っている場合などに起こる。 この問題は バイトオーダー(byte order) の違いから起こるものであり x86系プロセッサ は リトルエンディアン(little endian)であるためである。 このへんの詳しい説明はしないが(検索すれば分かるだろう) 要は環境の違いによって上位 1byte と下位 1byte がひっくり返る場合があるということだ。 ではどうすればいいのか? 答えは簡単である。 htons関数を使う。 つまり num = in_cksum((unsigned short *)data, 4); を num = htons(in_cksum((unsigned short *)data, 4)); とすればいいだけだ。 checksum2.c ------------------------------------------------------------------------------ unsigned short in_cksum(unsigned short *data, int size) { unsigned long sum = 0; while(size > 1){ sum += *(data++); size -= 2; } if(size > 0) sum += (*data) & 0xff00; sum = (sum & 0xffff) + (sum >> 16); return ((~(unsigned short)((sum >> 16) + (sum & 0xffff)))); } int main(void) { unsigned short num; unsigned char data[] = { 0x00,0x05,0x00,0x01 }; /* ここにhtons関数を追加! */ num = htons(in_cksum((unsigned short *)data, 4)); printf("0x%x\n", num); num = ~(0x0005 + 0x0001); printf("0x%x\n", num); return 0; } ------------------------------------------------------------------------------ cksum]$ gcc checksum2.c cksum]$ ./a.out 0xfff9 0xfff9 cksum]$ こんどはちゃんとしたデータが出力された! つまりは、環境によって htons を使用するかどうかを決めればいいということだ。 最初は htons を使用せずにパケットを送信してみて、 もしうまく送信されなければ htons を記述してやってみればいい。 チェックサムに関するコードは /usr/src/sys/netinet/in_cksum.c にあるらしいが 私のは無かったので in_cksum で検索して見 つけました。 興味があれば詳しく調べてみてください。 チェックサム1つとってもなかなか興味深くかつ難しいことが分かるでしょう(^^; End. written by kenji aiko 2003/09/11 Copyright (C) 2003 kenji aiko All Rights Reserved