初老のボケ防止日記

おっさんのひとりごとだから気にしないようにな。

続・ご無沙汰のC

JavaやPython等の最近の言語を使っていると、C言語でよくあるパターンの罠にはまりがち。そう俺はハマったのだよ。だから次にまた嵌らないようにメモしておく。

IPv4アドレスを文字列で表示する

ネットワークプログラミングを書いた人なら通信相手のIPアドレスをログ表示とかするよね? え? tcpdumpかwireshark使うって?まぁそれもいいけどさ、中年はプリント文が好きなわけよ。でもって自分で頑張って出力してもいいけど、標準関数があるのでそれを使う方が楽だよね。

inet_ntoa()

  char *inet_ntoa(struct in_addr in);

で、C言語から暫く離れていたらこの関数がスレッドセーフじゃないということをすっかり忘れておりました。この関数のインタフェースを見てお分かりの通り、引数にin_addr構造体を渡すと文字列のポインタを返してくれる。manを見る限り、呼んだら解放しろと明示されてないので、動的にメモリ確保してくれてるわけでもない。じゃあどこにその領域をもってるんだ?という話になる。で、「man inet_ntoa」すればちゃんと説明にあって、

The string is returned in a statically allocated buffer, which subsequent calls will overwrite.

静的領域のバッファに設定した文字列を返すからな? その後の呼び出しで上書きすっからよ。

って丁寧に書いてある。え?よくわからない?じゃあ例題を書いてみよう。

inet_ntoa()で問題がない場合

  • ip1.c
#include <string.h>
#include <netinet/ip.h>
#include <arpa/inet.h>

int main(){

  struct iphdr iphdr;
  memset( &iphdr, 0x00, sizeof(struct iphdr));
  inet_aton("192.168.1.10",(struct in_addr *)&iphdr.saddr);
  inet_aton("255.255.255.255",(struct in_addr *)&iphdr.daddr);

  char *p1;
  char *p2;

  printf("---- test1\n");
  p1 = inet_ntoa(*(struct in_addr *)&iphdr.saddr);
  printf("saddr([0x%x]%s)\n", p1,p1);
  p2 = inet_ntoa(*(struct in_addr *)&iphdr.daddr);
  printf("daddr([0x%x]%s)\n", p2,p2);
}

これをコンパイルして実行するとこうなる。

  • 「make ip1」で実行した結果
---- test1
saddr([0xd698f718]192.168.1.10)
daddr([0xd698f718]255.255.255.255)

一応説明すると、「struct iphdr」というのはIPv4ヘッダの構造体。saddrは送信元IPアドレス、daddrは送信先IPアドレスが入っている。この例は送信先と送信元のIPアドレスを表示しているサンプル。IPアドレスの前に表示されているのは文字列が設定されているポインタのアドレス。両方とも同じアドレスなのでinet_ntoa()が静的バッファを使っていることがわかる。

inet_ntoa()で問題がある場合

  • ip2.c
#include <string.h>
#include <netinet/ip.h>
#include <arpa/inet.h>

int main(){

  struct iphdr iphdr;
  memset( &iphdr, 0x00, sizeof(struct iphdr));
  inet_aton("192.168.1.10",(struct in_addr *)&iphdr.saddr);
  inet_aton("255.255.255.255",(struct in_addr *)&iphdr.daddr);

  char *p1;
  char *p2;

  printf("---- test2\n");
  p1 = inet_ntoa(*(struct in_addr *)&iphdr.saddr);
  p2 = inet_ntoa(*(struct in_addr *)&iphdr.daddr);
  printf("saddr([0x%x]%s) daddr([0x%x]%s)\n",
    p1,p1,
    p2,p2
  );
}

これをコンパイルして実行するとこうなる。

  • 「make ip2」で実行した結果
---- test2
saddr([0x92166718]255.255.255.255) daddr([0x92166718]255.255.255.255)

送信元IPアドレスと送信先IPアドレスが一緒になっている。なんでかというと、2回目めの呼び出しで静的バッファの内容が上書きされているから。それをわかりやすく表現するためにサンプルではinet_ntoa()の戻り値をわざわざポインタに入れているんだけど、実際はこういう呼び出しをしても同じ。

  printf("saddr(%s) daddr(%s)\n",
    inet_ntoa(*(struct in_addr *)&iphdr.saddr),
    inet_ntoa(*(struct in_addr *)&iphdr.daddr),
  );
}

ポインタが指している先のメモリがどこにあるかを意識すればわかる話だけど、おっさんすっかり忘れておりました。じゃあ、どうやって回避するのよという話になるけど、Linuxの場合は別の関数を使えば良い。

inet_ntop()を使う

  • ip3.c
#include <string.h>
#include <netinet/ip.h>
#include <arpa/inet.h>

int main(){

  struct iphdr iphdr;
  memset( &iphdr, 0x00, sizeof(struct iphdr));
  inet_aton("192.168.1.10",(struct in_addr *)&iphdr.saddr);
  inet_aton("255.255.255.255",(struct in_addr *)&iphdr.daddr);

  char *p1;
  char *p2;

  char src[32];
  char dst[32];
  memset( src, 0x00, sizeof(src));
  memset( dst, 0x00, sizeof(dst));
  inet_ntop(AF_INET, (struct in_addr *)&iphdr.saddr, src, sizeof(src));
  inet_ntop(AF_INET, (struct in_addr *)&iphdr.daddr, dst, sizeof(dst));

  p1 = src;
  p2 = dst;
  printf("---- test3\n");
  printf("saddr([0x%x]%s) daddr([0x%x]%s)\n",
    p1,p1,
    p2,p2
  );
}

これをコンパイルして実行するとこうなる。

  • 「make ip3」で実行した結果
---- test3
saddr([0x1c997e90]192.168.1.10) daddr([0x1c997eb0]255.255.255.255)

inet_ntop()はIPv6にも対応した関数で、引数に呼び元が用意したバッファを渡す。だから当然出力結果のアドレスも異なる。ということでスレッドセーフの場合はコレを使えば解決なのだ。若かりし頃にC言語でガリガリやってた時にはそんな関数は多分おらんぞ。つかIPv6もだけどな。

MACアドレスを文字列で表示する

IPヘッダじゃなくて、イーサヘッダの出力とかすることもあるよね? え? ないって? しょうがないなブタゴリラさんは。

  • eth.c
#include <string.h>
#include <netinet/ether.h>

int main(){

  struct ether_header ethhdr;
  memset( &ethhdr, 0x00, sizeof(struct ether_header));
  memcpy( &ethhdr.ether_shost, ether_aton("11:22:33:44:55:66"),6);
  memcpy( &ethhdr.ether_dhost, ether_aton("77:88:99:00:AA:BB"),6);

  char *p1;
  char *p2;

  printf("---- test1\n");
  p1 = ether_ntoa((struct ether_addr*)ethhdr.ether_shost);
  printf("shost([0x%x]%s)\n", p1,p1);
  p2 = ether_ntoa((struct ether_addr*)ethhdr.ether_dhost);
  printf("dhost([0x%x]%s)\n", p2,p2);

  printf("---- test2\n");
  p1 = ether_ntoa((struct ether_addr*)ethhdr.ether_shost);
  p2 = ether_ntoa((struct ether_addr*)ethhdr.ether_dhost);
  printf("shost([0x%x]%s) dhost([0x%x]%s)\n",
    p1,p1,
    p2,p2
  );

  printf("---- test3\n");
  char src[32];
  char dst[32];
  memset( src, 0x00, sizeof(src));
  memset( dst, 0x00, sizeof(dst));
  ether_ntoa_r((struct ether_addr*)ethhdr.ether_shost, src);
  ether_ntoa_r((struct ether_addr*)ethhdr.ether_dhost, dst);
  p1 = src;
  p2 = dst;
  printf("shost([0x%x]%s) dhost([0x%x]%s)\n",
    p1,p1,
    p2,p2
  );
}
  • 「make eth」で実行した結果
---- test1
shost([0x5851cec0]11:22:33:44:55:66)
dhost([0x5851cec0]77:88:99:0:aa:bb)
---- test2
shost([0x5851cec0]77:88:99:0:aa:bb) dhost([0x5851cec0]77:88:99:0:aa:bb)
---- test3
shost([0xbbadff80]11:22:33:44:55:66) dhost([0xbbadffa0]77:88:99:0:aa:bb)

こちらも同じでether_ntoa()は静的バッファなので問題がある。でスレッドセーフなものとしてはether_ntoa_r()がある。みんなもいつでもイーサヘッダ見ろって言われるかわからないから覚えとくといいぞ。

ルーター自作でわかるパケットの流れ

ルーター自作でわかるパケットの流れ