llvm-sysクレートを使うときにリンクに失敗した

事の始まり

LLVMには、フロントエンド開発の事始めとしてKaleidoscopeという独自言語を開発するチュートリアルがある。 このチュートリアルC++で実装していく流れなのだが、グローバル変数だらけでLLVMどの関数/変数が何に依存していて何の機能に重要なのかがいまいち汲み取るのがしんどかった。 そこで、グローバル変数を取り除いていきモダンなコードで書けるようにしたいと思いたち、どうせならRustで書いていくかと始めたのが事のはじまりであった。 このチュートリアルの1、2章は字句解析器と構文解析器の実装なので特に問題は起こらなかった。

このブログは3章に突入したときの話である。 C++APIになるべく近い形でLLVMAPIを叩きたかったので、llvm-sysクレートを使うことに決めた。

llvm-sysクレート

ここで少しllvm-sysクレートの紹介をする。 Rustの*-sysクレートの命名の慣習をご存知の方はすぐに予想ができるであろうが、このクレートはllvmC/C++APIをRust側から呼べるように単純にラップしたものだ。 crates.ioでは既に使われていないAppveyorのバッジが残っており、build failingになってるのが気にはなるが、 開発は継続されてなされており最新のLLVM9にも対応しているようなので、信頼できそうだ。

本題

さて話を戻そう。dependenciesにllvm-sysを追加してcargo buildをしたところ、以下のようなリンクエラーが起きた(一部抜粋)。

  = note: /usr/lib/gcc/x86_64-pc-linux-gnu/9.2.0/../../../../x86_64-pc-linux-gnu/bin/ld: /home/ben1jake/wrk/develop/kaleidoscope/target/debug/deps/kaleidoscope-7e8763b44e7ac4a4.3otu9pud9f3w0saw.rcgu.o: in function `kaleidoscope::ir::LLVMContext::new':
          /home/ben1jake/wrk/develop/kaleidoscope/src/ir.rs:14: undefined reference to `LLVMContextCreate'

どうやらLLVMContextCreateが見つからないようだ。 しかし、このチュートリアルC++で書いたときはちゃんとリンクができており、 LLVMのライブラリをリンクするときに必要な情報はllvm-configコマンドで取得していた。 この情報はllvm-sysクレートが取得してくれているはずである。 Rustのプロジェクトではリンクなどのrustcに渡される情報はbuild.rsによって生成でき、この情報はtarget/debug/buildなどの下に出力される。 今回はllvm-sysの結果を見たかったので、target/debug/build/llvm-sys-*/outputを見にいった(以下一部抜粋)。

cargo:rustc-link-lib=static=targetwrappers
cargo:rustc-link-search=native=/home/ben1jake/wrk/develop/kaleidoscope/target/debug/build/llvm-sys-ada015182d93d582/out
cargo:config_path=llvm-config
cargo:libdir=/usr/lib64/llvm/9/lib64

cargo:rustc-link-search=native=/usr/lib64/llvm/9/lib64

cargo:rustc-link-lib=dylib=stdc++

rustcにリンクすべきライブラリを指定するのはcargo:rustc-link-lib=であるが、 ここではtargetwrappersstdc++だけが指定されており、LLVMのライブラリが指定されていない (targetwrappersllvm-sysが生成するライブラリであり、これだけでは足りない)。

少なくともcargo:libdir=は正しく出力されているようなのでllvm-configは使われているようだ。 どうしようもないので、llvm-configのbuild.rsを覗きに行ったら原因が発覚した。

gitlab.com

上記のURLにあるとおり、llvm-sysではllvm-configにオプション--link-staticを渡しており、 LLVMを静的リンクするようになっていたようだ。そこで自分の環境を見てみるとLLVMの動的ライブラリしかなかったのである! しかたがないので、LLVMを自前ビルドし、PATHを通してから再度ビルドを行ったら無事にリンクも成功した。

おそらく、今回の例のように静的ライブラリが存在しないというのは稀なケースだと思われるが、 パッケージマネージャーでLLVMをインストールする際に静的ライブラリが含まれないような環境の人には、是非ともLLVMを自前ビルドしてもらいたい。 余談ではあるが、LLVMを自前ビルドする際に並列ビルドをしたらリンク時にメモリをすべて喰われてリンクに失敗したので、並列ビルドのご利用は計画的に。

fortranのmodファイルのタイムスタンプについて

TL;DR

  • gfortranでモジュールファイルをコンパイルする時、インターフェースが変わらなければmodファイルのタイムスタンプは更新されない。
  • *.modファイルは*.oファイルと*.f90ファイルに依存する形でMakefileなどに書き込もう。
  • *.modファイルを更新すべきときは何も実行しない。

まえおき

自分の研究室には、Fortranで書かれている自家製プログラムがあるんですよ。 今どきFortranかよって思いますけど、シミュレーション界隈ではまだまだFortranが現役で使われてるところが結構あるらしいので、まぁうちの研究室もご多分に漏れずって感じなんですかね。

このプログラムはお世辞にも綺麗に書けてるとは言えず、色々文句を言いたい点があります。 そもそもFortranで書かれているっていうのは百歩譲って許しますが、Makefileの書き方が雑なんですよ。

なにが雑って、Fortranコンパイラを切り替えるためにMakefileを直接編集しなきゃいけないところとか(autotools使えよ)、 ソースコードの依存関係をちゃんと解決できていないことが原因で、コードを編集した後、再度コンパイルしようとしたらコンパイルエラーになるとか、色々です。

そこで、後者の依存関係を解決するために書き直そうとしたところ、ちょっと躓いたっていうのが主題です。

サンプルプログラム

研究室のプログラム全体をここに示すのは冗長すぎるので、サンプルプログラムを書きました。 Makefileにおけるモジュールの依存関係を書いてないところ以外は普通だと思います。

! math_const.f90
module math_const
  implicit none

  real(4) :: pi = 3.14159265359
end module
! main.f90
program main
  use math_const
  implicit none

  write(*,*) 'pi = ', pi

end program
# Makefile
FC = gfortran

.PHONY: all
all: main

main: main.o math_const.o
        $(FC) -o $@ $^

main.o: main.f90
        $(FC) -c -o $@ $<

math_const.o: math_const.f90
        $(FC) -c -o $@ $<

.PHONY: clean
clean:
        rm -f main *.o *.mod

これと同じディレクトリでmakeすると、math_const.f90よりも先にmain.f90コンパイルしようとするので、コンパイルエラーになります。

gfortran -c -o main.o main.f90
main.f90:2:6:

   use math_const
      1
Fatal Error: Can't open module file ‘math_const.mod’ for reading at (1): No such file or directory
compilation terminated.
make: *** [Makefile:10: main.o] Error 1

これを回避するには先にmath_const.f90コンパイルしてmath_const.modを作成しておかないといけません。 この例で手っ取り早いのはMakefile中のmain.omath_const.oに関する項目の順番を入れ替えることですが、 大きなプロジェクトになるといちいちMakefile内の順番を気にしてなんていられません。 それに依存関係が関連付けられるわけでもないので、依存してるファイルが再コンパイルされない問題も起きます。

ですので、依存関係を記さなければいけません。

ダメなケース1

main.f90コンパイルしてmain.oを作成する前に、math_const.modを作成しなくてはいけないので、 main.oの依存ファイルにmath_const.modを追加しなければいけません。 ですが、math_const.modに関する項目はMakefileには無いので、サボってmath_const.oを追加してしまいましょう。

コンパイルしてmath_const.oが作成されるときには同時にmath_const.modが作成されるのでコンパイルできると思います。

# Makefile
...
main.o: main.f90 math_const.o
...

こうすると、コンパイルは通るようになります。

$ make
gfortran -c -o math_const.o math_const.f90
gfortran -c -o main.o main.f90
gfortran -o main main.o math_const.o

ですが、math_const.f90が更新された時にすこし不具合が生じます。

$ touch math_const.f90
$ make
gfortran -c -o math_const.o math_const.f90
gfortran -c -o main.o main.f90
gfortran -o main main.o math_const.o

一見すると、問題ないように見えますが、touchしただけではmath_constモジュールのインターフェースには変化がないのでmain.f90を再コンパイルする必要はないはずです。

ダメなケース2

では、諦めてmath_const.modの項目をMakefileに追加したいところですが、Makefileには一つの処理に複数の出力ファイルがある場合の対処方法が一般にはありません。 ですが、ここで思い出してください、当初の目的はmain.f90よりも先にmath_const.f90コンパイルするでした。 これを実現するだけなら、math_const.oの項目をmath_const.modに変えるだけでできますが、mainの依存ファイルにconst_math.oがあるので、 この項目を消すと厄介なことになるかも知れません。

そこで、この項目をコピペしちゃいましょう。

# Makefile
...
main.o: main.f90 math_const.mod
        $(FC) -c -o $@ $<

math_const.o: math_const.f90
        $(FC) -c -o $@ $<

math_const.mod: math_const.f90
        $(FC) -c -o math_const.o $<
...

ここでmake clean && makeするとちゃんとコンパイルできます。

$ make clean && make
rm -f main *.o *.mod
gfortran -c -o math_const.o math_const.f90
gfortran -c -o main.o main.f90
gfortran -o main main.o math_const.o

math_const.f90を更新してみます。

$ touch math_const.f90
$ make
gfortran -c -o math_const.o math_const.f90
gfortran -o main main.o math_const.o

main.f90が再コンパイルされていません。うまくいったように見えます。

が、実は問題があります。もう一度makeすると、

$ make
gfortran -c -o math_const.o math_const.f90
gfortran -o main main.o math_const.o

再度、math_const.f90コンパイルが走ってしまいます。この後何度繰り返しても再コンパイルし続けます。

modファイルのタイムスタンプ

これが何故起きるのかと言うと、gfortranでモジュール定義を含むファイルをコンパイルした時、 モジュールのインターフェースが更新されなかったら、modファイルも更新されません。 つまり、modファイルのタイムスタンプが更新されないのです。

makeなどの主要なビルドシステムはファイルのタイムスタンプで更新するべきかどうかを判断しています。 例えば、math_const.oのタイムスタンプよりもmath_const.f90のタイムスタンプの方が新しい場合、ソースコードが更新されたから再コンパイルするべきだと判断されます。

# math_const.f90の方が新しい場合
$ make math_const.o
gfortran -c -o math_const.o math_const.f90
# math_const.oの方が新しい場合
$ make math_const.o
make: 'math_const.o' is up to date.

ここで、math_const.modの例に戻ると、math_const.f90は更新されましたが、モジュールのインターフェースが変わっていません。 ですので再コンパイル時にmath_const.modのタイムスタンプは更新されず、math_const.f90のタイムスタンプよりも古いままになっています。

何度makeしてもこのタイムスタンプの新旧は変化しないため、毎度再コンパイルされる羽目になっているのです。

最終的なMakefile

そこで、この無駄な再コンパイルを阻止するためには、以下のようにします。 このようなMakefileの書き方は検索すれば出てくるやり方と大体同じです。

# Makefile
FC = gfortran

.PHONY: all
all: main

main: main.o math_const.o
        $(FC) -o $@ $^

main.o: main.f90 math_const.mod
        $(FC) -c -o $@ $<

math_const.o: math_const.f90
        $(FC) -c -o $@ $<

math_const.mod: math_const.o

.PHONY: clean
clean:
        rm -f main *.o *.mod

math_const.modの項目は依存関係だけを記して、特に何もしません。 math_const.oを作成する時にコンパイルが走るので、そちらに依存するだけです。 こうすることで、math_const.modmath_const.oとのタイムスタンプの比較になるのですが(math_const.f90との比較では無いですが問題ないです)、 もしmath_const.modのタイムスタンプの方が古くても、ここでは何もせず、math_const.oが更新すべきかどうかの判定に移ります。 math_const.oコンパイルされるごとにタイムスタンプが更新されるので、一度コンパイルされればmath_const.f90が更新されない限り、 再コンパイルされることはなくなります。

確認すると、ちゃんと機能していることが分かります。

$ make clean && make
rm -f main *.o *.mod
gfortran -c -o math_const.o math_const.f90
gfortran -c -o main.o main.f90
gfortran -o main main.o math_const.o

$ touch math_const.f90

$ make
gfortran -c -o math_const.o math_const.f90
gfortran -o main main.o math_const.o

$ make
make: Nothing to be done for 'all'.

BashスクリプトのUsageを書くのがめんどくさい。

LinuxUNIXを使っていると、自前のBashスクリプトを書くことが多いと思いますが、そのスクリプトが何の引数を受け取るのか覚えていないことがよくあります。 こんなときに、わざわざスクリプトの中身を見て判断するのはめんどくさいので、使い方を間違っているときにUsageを出力するようにするといいのですが、スクリプトの変更を繰り返した結果、このUsageが正しい引数を提示しているかどうかも怪しいことがあります。

例えば、引数を2つ取るspamham.shという以下のようなスクリプトを作るとします。

#!/usr/bin/env bash

readonly program=$(basename $0)

function print_usage_and_exit() {
  echo >&2 "Usage: ${program} SPAM HAM"
  exit 1
}

if [ $# -ne 2 ]; then
  print_usage_and_exit
fi

readonly spam=$1
readonly ham=$2

echo "SPAM: ${spam}"
echo "HAM:  ${ham}"

このスクリプトは引数を2つしか取らず、それ以外の数の引数を渡すとUsageを吐いて終了します。

$ ./spamham.sh
Usage: spamham.sh SPAM HAM

$ ./spamham.sh hoge
Usage: spamham.sh SPAM HAM

$ ./spamham.sh hoge piyo
SPAM: hoge
HAM:  piyo

$ ./spamham.sh 1 2 FIZZ
Usage: spamham.sh SPAM HAM

このスクリプトのままでは引数についての記述が少し冗長で、引数の数なり順番なりを変更するのがめんどくさくなります。 ですので、(spam ham)のように引数を受け取る変数名を列挙するだけで引数を受け取れるようにしましょう。

過程を省きますが、(僕の中では)最終的にこのような形に落ち着きました。

#!/usr/bin/env bash

readonly program=$(basename $0)
readonly args=(spam ham)

function print_usage_and_exit() {
  echo >&2 "Usage: ${program} $(IFS=' '; echo ${args[*]^^})"
  exit 1
}

if [ $# -ne ${#args[@]} ]; then
  print_usage_and_exit
fi

for arg in ${args[@]}; do
  eval "readonly ${arg}=$1"
  shift
done

echo "SPAM: ${spam}"
echo "HAM:  ${ham}"

引数をargs=(spam ham)として定義して、print_usage_and_exitの中では

IFS=' '; echo ${args[*]^^}

として引数の名前を大文字表記で列挙しています。

また、引数の数は配列argsの要素数なので${#args[@]}で取得できます。

最後に、各引数変数に引数の値を代入するところは、evalshiftを使って、次々に変数に$1を代入してはshiftしてを繰り返して実現しています。

こうすることで、引数を変更するときは、args=()の中身を書き換えるだけで済むようになりました。

パケット解析、入門してみました。〜その2〜

前回の続き。

libpcap(続き)

pcap_close

ハンドルを閉じる関数です。

宣言

void pcap_close(pcap_t *handle);

名前の通り、ハンドルhandleを閉じます。 (前回の記事に書くべきだったかも知れません)

サンプルコード

#include <stdio.h>
#include <pcap.h>

int main(int argc, char **argv) {
    char *dev = "enp3s0";
    char errbuf[PCAP_ERRBUF_SIZE];
    pcap_t *handle;

    handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
    if (handle == NULL) {
        fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
        return 2;
    }
    pcap_close(handle);

    return 0;
}

開いて閉じるだけ。 devに入れるデバイス名は適当に書き換えるか、前回の記事にあるようにデフォルトのデバイス名を取得してください。

pcap_next

パケットを一つ読み込んで返す関数です。

宣言

const u_char *pcap_next(pcap_t *handle, struct pcap_pkthdr *header);

ハンドルhandleからパケットを読み込んで、パケットのヘッダ情報をheaderに書き込み、パケット本体をu_char型のポインタとして返します。

pcap_pkthdr

ついでにpcap_pkthdrの宣言も。

struct pcap_pkthdr {
    struct timeval ts;
    bpf_u_int32 caplen;
    bpf_u_int32 len;
};

tsはタイムスタンプで、lenはパケットの長さ。caplenはパケットが分割されている場合に取得したデータの長さです(たぶん)。

パケットの取得について

実際、パケットを取得するときは連続して取得したいときがほとんどなので、ループでpcap_nextを回すことになります。ですが、毎回、自前でループを書いてpcap_nextを使うのは冗長になるので、専ら次に紹介するpcap_loopという関数を使うようです。 また、pcap_loopを使うとエラー処理を自分で書かなくて済むという利点もあるようです。

pcap_loop

連続してパケットを読み込み、処理をする関数です。

宣言

int pcap_loop(pcap_t *handle, int count, pcap_handler callback, u_char *user);

handleは毎度おなじみ。countは読み込むパケット数で、負の数を入れるとエラーが起きるまで読み込みます。 callbackはコールバック関数で、この型は後述します。usercallbackに渡す第一引数で、特に必要ないときはNULLで結構です。

この関数はhandleからパケットを読み込み、そのデータをcallbackで処理するという流れを指定回数もしくはエラーが起きるまで繰り返します。

pcap_handler

それではcallbackの型pcap_handlerを見てみましょう。以下のように宣言されています。

typedef void (*pcap_handler)(u_char *args,
                             const struct pcap_pkthdr *header,
                             const u_char *packet);

引数としてargsheaderpacketを受け取り、voidを返す関数のポインタです。 argsには先程のpcap_loopに渡したuserが渡されるようです。constがついてないので色々便利に使えるかもしれません(今回は使いませんが)。 使い方は以下のサンプルコードで。

サンプルコード

#include <stdio.h>
#include <pcap.h>

void callback(u_char *args, const struct pcap_pkthdr *header, const u_char *packet) {
    printf("packet length: %d\n", header->len);
}

int main(int argc, char **argv) {
    char *dev, errbuf[PCAP_ERRBUF_SIZE];
    bpf_u_int32 net, mask;
    pcap_t *handle;
    struct bpf_program fp;
    char filter_exp[] = "tcp and host x.x.x.x";
    struct pcap_pkthdr header;
    const u_char *packet;

    dev = pcap_lookupdev(errbuf);
    if (dev == NULL) {
        fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
        return 2;
    }

    if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
        fprintf(stderr, "Can't get netmask for device %s: %s\n", dev, errbuf);
        return 2;
    }

    handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
    if (handle == NULL) {
        fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
        return 2;
    }

    if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
        fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
        return 2;
    }

    if (pcap_setfilter(handle, &fp) == -1) {
        fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
        return 2;
    }

    pcap_loop(handle, 20, callback, NULL);

    pcap_close(handle);

    return 0;
}

これはパケットを20個取得し、それぞれのパケットの長さを出力します(おそらくオクテット単位)。

相変わらずx.x.x.xのところのIPアドレスは適当に補ってください。艦これサーバーのIPアドレス鎮守府によって異なるらしいので。 といっても、まだパケットの解析するわけではないので艦これサーバー以外でも大丈夫です。

パケットデータ

最後に、pcap_nextpcap_handlerで扱うpacketは、データリンク層のデータになってます。 殆どの場合はEthernetパケット1でしょう。 ここの取り扱いはまた次の機会に書くと思います。

パケット解析、入門してみました。〜その1〜

今年度に入って艦これ始めました。楽しいです。

艦これをやってくうちに、2017夏イベントに突入したのもあってログを取りたいと思ったので自前でロガーを作ろうと思い立ったわけです。 知り合いにソフトを教えてもらったりもしたのですが、そのソフトはWindows用のバイナリを配布しているだけで、 主にLinuxを使ってる自分は使えなかったので(mono1とか使えばいけるのかも知れませんが)。

ログを取る手段は色々あります。既存のものではスクリーンショットを取って解析するのが主流なんですかね、あまり知りませんが。 画像解析はノウハウは持ってないし勉強するのもめんどくさそうだったので、自分はパケット解析をする手段を取ろうと思います。 なんせ、艦これの通信はSSL/TLSで暗号化してないので。

パケット解析ツール?

軽く調べてみたら、Linuxでパケット解析する手法としてはtcpdump2Wireshark3を使うのが一番お手頃らしいです。 tcpdumpで生パケットをファイルに保存して、Wiresharkでそのファイルに書き込まれたパケットを解析するという感じらしいです。

しかしこの方法だと一度ファイルに保存する必要があり、リアルタイムで解析するのは難しいように思えたので、個人的には腑に落ちませんでした。 ですので、tcpdumpで使われているlibpcapというライブラリを使って自分でプログラムを書くことにします (tcpdumpで使われているというより、tcpdumpの副産物的なライブラリという方が正しいですかね)。

libpcap

libpcapを使うにあたり、適度に解説してくれてる日本語のページが見つからなかったため公式ホームページにある使い方4を参考にしました。 以下は、ここの内容を要約したものになると思います。

ではlibpcapの関数をいくつか紹介していきましょう。

pcap_lookupdev

デフォルトのネットワークデバイス名を取得する関数です。

宣言

char *pcap_lookupdev(char *errbuf);

デフォルトのデバイス名を返します。 エラーが起きた場合は、errbufにエラー文字列を格納し、NULLを返します。

サンプルコード

#include <stdio.h>
#include <pcap.h>

int main(int argc, char** argv) {
    char *dev, errbuf[PCAP_ERRBUF_SIZE];

    dev = pcap_lookupdev(errbuf);
    if (dev == NULL) {
        fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
        return 2;
    }

    printf("Device: %s\n", dev);

    return 0;
}

このコードをコンパイルして実行すると、デフォルトのネットワークデバイス名を表示してくれます(環境によってはroot権限を要求されるかも)。 コンパイル時に-lpcapでリンクするのをお忘れなく。

pcap_lookupnet

バイスの属するネットワークのIPv4ネットワークアドレスとネットマスクを取得する関数です。 デバイスIPv4アドレスを取得するわけではないので注意してください。

宣言

int
pcap_lookupnet(const char *device,
               bpf_u_int32 *net,
               bpf_u_int32 *mask,
               char *errbuf);

devで渡された名前のデバイスのネットワークアドレスとネットマスクをそれぞれnetmaskに格納します。 エラーが起きた場合は、errbufにエラー文字列を格納し、-1を返します。

サンプルコード

#include <stdio.h>
#include <pcap.h>

int main(int argc, char** argv) {

    /* ここは上と同じ */

    bpf_u_int32 net, mask;
    if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
        fprintf(stderr, "Can't get netmask for device %s: %s\n", dev, errbuf);
        return 2;
    }

    printf("net:  %d.%d.%d.%d\n",
           net&0xff, (net>>8)&0xff, (net>>16)&0xff, (net>>24)&0xff);
    printf("mask: %d.%d.%d.%d\n",
           mask&0xff, (mask>>8)&0xff, (mask>>16)&0xff, (mask>>24)&0xff);

    return 0;
}

これを実行すると、ネットワークアドレスとネットマスクが表示されます。

pcap_open_live

バイスを扱うハンドルを開放する関数です(こんな日本語で良いのか?)。

宣言

pcap_t *
pcap_open_live(const char *device,
               int snaplen,
               int promisc,
               int to_ms,
               char *errbuf);

引数として、デバイス名(device)、読み込む最大バイト数(snaplen)、プロミスキャス・モードで開放するかどうかのフラグ(promisc)、 タイムアウトの時間[ミリ秒](to_ms)、エラー文字列を格納するポインタ(errbuf)を取ります。

返り値はデバイスのハンドルです。 エラーが起きたときはerrbufにエラー文字列を格納し、NULLを返します。

今後はここで返されるpcap_t*型のハンドルを用いてパケット解析をしていくことになります。

サンプルコード

#include <stdio.h>
#include <pcap.h>

int main(int argc, char** argv) {

    /* ここは上と同じ */

    pcap_t *handle;
    handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
    if (handle == NULL) {
        fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
        return 2;
    }

    return 0;
}

エラーが起きなければ、さっきと出力は同じです。 自分の環境ではroot権限で実行しないとOperation not permittedと怒られました。

pcap_datalink

この関数は渡されたデバイスの(OSI参照モデルでの)データリンク層の種類を返します。

宣言

int pcap_datalink(pcap_t *handle);

引数にハンドルのポインタを受け取り、データリンク層の種類をint型で返します。 データリンク層の種類の一覧はこのページにあります。

サンプルコード

#include <stdio.h>
#include <pcap.h>

int main(int argc, char** argv) {

    /* ここは上と同じ */

    if (pcap_datalink(handle) == DLT_EN10MB) {
        printf("Device %s provides Ethernet headers\n", dev);
    } else {
        printf("Device %s doesn't provide Ethernet headers\n", dev);
    }

    return 0;
}

これを実行すると、デフォルトのデバイスデータリンク層Ethernetかどうかを表示します。

pcap_compile

この関数はハンドルで取得するパケットのフィルターを生成します。

宣言

int
pcap_compile(pcap_t *handle,
             struct bpf_program *fp,
             const char *filter_exp,
             int optimize,
             bpf_u_int32 netmask);

handleで取得するパケットをfilter_expで表されるフィルターをコンパイルし、fpに格納します。 optimizeは最適化するかどうかを示すフラグで、0なら偽、1なら真です。 netmaskはこのフィルターを適用するネットワーク範囲を指定します。

フィルターのコンパイルに失敗した場合は-1を返します。

filter_expで渡す文字列はtcpdumpに渡すexpressionと同じものです。詳しくはman tcpdumpを参照してください。

pcap_setfilter

pcap_compileで生成したフィルターをハンドルに設定します。

宣言

int pcap_setfilter(pcap_t *handle, struct bpf_program *fp);

フィルターfpをハンドルhandleに設定します。 失敗したときは-1を返します。

pcap_geterr

ハンドルで起きた直近のエラー文字列を返します(たぶん)。

宣言

char *pcap_geterr(pcap_t *handle);

サンプルコード

#include <stdio.h>
#include <pcap.h>

int main(int argc, char** argv) {
    char *dev, errbuf[PCAP_ERRBUF_SIZE];
    bpf_u_int32 net, mask;
    pcap_t *handle;
    struct bpf_program fp;
    char filter_exp[] = "tcp and host x.x.x.x";

    dev = pcap_lookupdev(errbuf);
    if (dev == NULL) {
        fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
        return 2;
    }

    if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
        fprintf(stderr, "Can't get netmask for device %s: %s\n", dev, errbuf);
        return 2;
    }

    handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
    if (handle == NULL) {
        fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
        return 2;
    }

    if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
        fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
        return 2;
    }

    if (pcap_setfilter(handle, &fp) == -1) {
        fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
        return 2;
    }

    return 0;
}

エラーが起きたときしか出力しませんが、フィルターをハンドルに設定しています。 フィルターを表す文字列"tcp and host x.x.x.x"は適宜変更してください。 "x.x.x.x"の部分を艦これサーバーのIPアドレスにすれば、艦これサーバーとのTCP通信を見ることができます。

つづく

今回はここまでにしたいと思います。 実際にパケットのデータを扱うのは次回。