※この記事は、前回の記事「<実開発で頻出!>ポインタのキャスト①【C言語】」の続きとなります。
前回のまとめとして、ポインタのキャストを行うと、
- アドレスが示す先を参照したときの値の扱い方が、キャストした型のものになる
- ポインタを増減するときの増減の単位が、キャストした型のサイズになる
ということが分かった。
今回は、これが実際の開発でどのように使われることがあるのか、一例を実戦形式で解説していこう。
例えばC言語で低レイヤの開発の一つとして、ネットワークから受信したメッセージを解析する処理の実装が挙げられる。
イーサネットフレーム、IPパケット、TCPパケット等々、コンピュータのアプリケーションが外部からデータを受信するためには、アプリケーションデータのみを抽出するために、まずは外側のデータ全体を解析しなければならない。
そういった低レイヤの処理を担う機能(OS・ドライバなど)の実装に、C言語はよく使われるのだ。
ということで今回は、「IPヘッダの解析処理」を一例として用いてやってみよう。
やりたいこと
今、上図のようにIPヘッダの先頭を指すポインタp_packetが引数に渡されてきたとする。
今回やりたいことは、これを使って ①IPヘッダを解析できる準備(IPヘッダの各要素を取り出す) を行い、解析処理(ここは今回はコメントアウトで省略)を行った後、②データ処理関数に「データ」の先頭アドレスを指すポインタを渡す という内容である。
大枠だけソースコード書くならこんな感じだろうか。
#include <stdio.h>
int analyze_data(unsigned char* data);
int analyze_ip_header(unsigned char* p_packet) {
// ①ヘッダ解析準備(各パラメータ取り出し)
// ヘッダ解析処理(ここは省略)
// ②データ解析関数「analyze_data」にデータの先頭アドレスを指すポインタを渡す処理
// analyze_data(※ここにポインタ);
return 0;
}
// データ解析関数
int analyze_data(unsigned char* data) {
// データ解析処理
return 0;
}
①IPヘッダの各要素の取り出し (構造体ポインタへのキャスト)
まず重要なのがIPヘッダの構成で、以下のようになる。
パラメータ名 | サイズ |
---|---|
Version | 4ビット |
Header Length | 4ビット |
Type of service | 8ビット |
Total length | 16ビット |
Identification | 16ビット |
Flag | 3ビット |
Flagment Offset | 13ビット |
Time to Live | 8ビット |
Protocol | 8ビット |
Checksum | 16ビット |
Source IP | 32ビット |
Destination IP | 32ビット |
Options | 可変 |
Padding | 可変 |
もうちょっと知っておきたいという方は、以下のサイトが簡潔で分かりやすいと思う。
まずは、この構成の通りに構造体を作ってみよう。
typedef struct ip_hedear {
unsigned char version : 4;
unsigned char header_length : 4;
unsigned char type_of_service;
unsigned short total_length;
unsigned short identification;
unsigned short flag : 3;
unsigned short flagment_offset : 13;
unsigned char time_to_live;
unsigned char protocol;
unsigned short checksum;
unsigned int source_ip;
unsigned int destination_ip;
} IP_HEADER;
さて、本関数はまだIPヘッダの先頭ポインタしか知らない状態であるため、その中身にアクセスすることは不可能であった。(ポインタで強引にアクセスするのは大変)
しかし先ほど準備した構造体をキャストしてやることによって、それぞれのパラメータにアクセスすることができるようになる。
// ①ヘッダ解析準備(各パラメータ取り出し)
IP_HEADER* ip_header = (IP_HEADER*)p_packet; // ここでキャスト
printf("例えばヘッダ長は%d, 例えば合計長は%d\n", ip_header->header_length, ip_header->total_length);
これは前回、アドレスが示す先を参照したときの値の扱い方が、キャストした型のものになる、と説明した特徴を活かしている。
今回はIP_HEADER*型になったことによって、もともとchar*型で1バイトずつでしか扱えなかったIPヘッダを、本来のIPヘッダの構成通りに扱えるようになった。
プログラムをまとめると、以下のようになる。
#include <stdio.h>
int analyze_data(unsigned char* data);
typedef struct _ip_hedear {
unsigned char version : 4;
unsigned char header_length : 4;
unsigned char type_of_service;
unsigned short total_length;
unsigned short identification;
unsigned short flag : 3;
unsigned short flagment_offset : 13;
unsigned char time_to_live;
unsigned char protocol;
unsigned short checksum;
unsigned int source_ip;
unsigned int destination_ip;
} IP_HEADER;
int analyze_ip_header(unsigned char* p_packet) {
// ①ヘッダ解析準備(各パラメータ取り出し)
IP_HEADER* ip_header = (IP_HEADER*)p_packet; // ここでキャスト
printf("例えばヘッダ長は%d, 例えば合計長は%d\n", ip_header->header_length, ip_header->total_length);
// ヘッダ解析処理(ここは省略)
// ②データ解析関数「analyze_data」にデータの先頭アドレスを指すポインタを渡す処理
// analyze_data(※ここにポインタ);
return 0;
}
// データ解析関数
int analyze_data(unsigned char* data) {
// データ解析処理
return 0;
}
②データ処理関数へのポインタ渡し(ポインタの加算)
さて、この関数の仕事は「IPヘッダの解析」のみである。
IPヘッダの解析が終了すると、「データ処理関数」に処理を委譲してやらなければならない。
データ部の解析はそいつの仕事だ。
そしてその関数に委譲する際、ポインタを「データ」の先頭まで進ませて渡してあげなければならない。
データ部の解析をするのにポインタがそれより前のヘッダの位置にあったらたまったもんじゃない。
さて、ご丁寧にIPヘッダには「Header length」というパラメータがある。
これはIPヘッダの長さ(4Byte単位)を表しているので、ここにある値に4を掛けたものをポインタに加算してやればよい。
この長さは○バイトという単位であるので、ポインタの増減が1バイト単位の増減となっているのが望ましい。
気にするところは前回の、ポインタを増減するときの増減の単位が、キャストした型のサイズになるというところだ。
今回は、1バイト単位なのでchar*型でよい。
つまり引数のp_packetそのままの型で計算すればよい。
そしてそのポインタを渡してやればよい。
// ②データ解析関数「analyze_data」にデータの先頭アドレスを指すポインタを渡す処理
p_packet = p_packet + ip_header->header_length *4;
analyze_data(p_packet);
ここで理解を深めるために疑問を持ってほしい。
ip_header++;
ではダメなのか?
IP_HEDEAR*型であるip_headerの増減の単位は、その構造体の大きさとなる。
となると、1回の加算だけでそのままIPヘッダ分ポインタを進めることができそうだが、これはIPヘッダの最後の2つのパラメータ「Opttions」と「Padding」が可変長であるために不可能である。
構造体における可変長データについては、ポインタとして先頭アドレスを持つことはできても、実行時のサイズを理解する術がない。
逆にすべてが固定長であれば、先述したやり方でも問題ない。
まとめると以下のようになる。
#include <stdio.h>
int analyze_data(unsigned char* data);
typedef struct _ip_hedear {
unsigned char version : 4;
unsigned char header_length : 4;
unsigned char type_of_service;
unsigned short total_length;
unsigned short identification;
unsigned short flag : 3;
unsigned short flagment_offset : 13;
unsigned char time_to_live;
unsigned char protocol;
unsigned short checksum;
unsigned int source_ip;
unsigned int destination_ip;
} IP_HEADER;
int analyze_ip_header(unsigned char* p_packet) {
// ①ヘッダ解析準備(各パラメータ取り出し)
IP_HEADER* ip_header = (IP_HEADER*)p_packet; // ここでキャスト
printf("例えばヘッダ長は%d, 例えば合計長は%d\n", ip_header->header_length, ip_header->total_length);
// ヘッダ解析処理(ここは省略)
// ②データ解析関数「analyze_data」にデータの先頭アドレスを指すポインタを渡す処理
p_packet = p_packet + ip_header->header_length *4;
analyze_data(p_packet);
return 0;
}
// データ解析関数
int analyze_data(unsigned char* data) {
// 試しに、データの上位4バイトを表示
printf("%08x\n", *((int*)data));
// データ解析処理
return 0;
}
テスト
正常性を確かめるために、以下のコードをmainとしてpaiza.ioでテストしてみよう。
#include <stdio.h>
int analyze_ip_header(unsigned char* p_packet);
typedef struct _ip_hedear {
unsigned char version : 4;
unsigned char header_length : 4;
unsigned char type_of_service;
unsigned short total_length;
unsigned short identification;
unsigned short flag : 3;
unsigned short flagment_offset : 13;
unsigned char time_to_live;
unsigned char protocol;
unsigned short checksum;
unsigned int source_ip;
unsigned int destination_ip;
} IP_HEADER;
typedef struct _ip_packet {
IP_HEADER ip_header;
unsigned int data;
} IP_PACKET;
int main(void){
IP_HEADER ip_header = {
0, 5, 0, 456, 0, 0, 0, 0, 0, 0, 0, 0
};
IP_PACKET ip_packet = {
ip_header,
252645135 // 16進数で「0f0f0f0f」
};
analyze_ip_header((unsigned char*)&ip_packet);
return 0;
}
以下のようになればOKである。
例えばヘッダ長は5, 例えば合計長は456
0f0f0f0f
Main.cでもキャストをしたり、構造体を作ったりしているので、意味を確認してみると面白いかもしれない。
最後に
ポインタのキャストについては以上である。
実務でC言語を使ううえでは大変重要なポイントであるが、そこらへんの教科書やサイト等ではなかなか深掘りしてくれないポイントでもある。
プログラミング学習では、体系的に理解することの他、実用的なポイントを押さえ理解することも大切になる。
普段の学習にもう一歩踏み込んで、実際の使われた方を模索してみたり、paiza.ioで実際の使用を想定して試してみたりしてみよう!
コメント