第584回ではカーネルデバッグツールのひとつ
droppedになったフレームのMACアドレスを調べる
Linuxは予期しないEthernetフレームを受け取ったとき、
そこでこのdroppedカウンターが上昇した理由を調べるために、
予期しないEthernetフレームを送る仕組みを作る
まずはテスト用に予期しないEthernetフレームを送る仕組みを作ります。今回はテスト用なのでloインターフェースを対象にします。
droppedカウンターはip -s link show
」
$ ip -s link show lo 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 RX: bytes packets errors dropped overrun mcast 25919416 262042 0 0 0 0 TX: bytes packets errors dropped carrier collsns 25919416 262042 0 0 0 0
上記の
「予期しないEthernetフレーム」
任意のバイト列のEthernetフレームを送信する方法はいろいろ存在しますが、
#!/usr/bin/env python3
from socket import *
dst = b'\x00\x00\x00\x00\x00\x00'
src = b'\x00\x00\x5E\x00\x53\x00'
etype = b'\x43\x21'
payload = bytes(46)
interface = 'lo'
sock = socket(AF_PACKET, SOCK_RAW)
sock.bind((interface, 0))
sock.send(dst + src + etype + payload)
コード自体は特に難しいものではないと思います。送信元のMACアドレスsrc
)dst
も送信元インターフェースであるinterface
もそれに合わせています。payload
には最小フレームサイズを満たすよう0で埋めたバイト列を作っているだけです。
後半はSOCK_
なsocket()
を作成して、bind()
し、send()
で生のEthernetフレームを送っています。送信元と送信先が一緒になっているため多少わかりにくいかもしれません。実際にdrooppedが起きるのは実デバイスに紐付いたインターフェースのはずなので、src
やdst
、interface
はそれらに合わせて適宜変更してください。
実際にこのコードを実行してから、
$ chmod +x errframe.py $ sudo ./errframe.py $ ip -s link show lo 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 RX: bytes packets errors dropped overrun mcast 25959740 262444 0 1 0 0 TX: bytes packets errors dropped carrier collsns 25959740 262444 0 0 0 0
無事にdroppedが1増えました。
droppedカウンターが更新されたことを表示する
この受信用のdroppedカウンターはいくつかの箇所で更新されうるのですが、
net/__
では、rx_
を更新します。
4523 | drop:
4524 | if (!deliver_exact)
4525 | atomic_long_inc(&skb->dev->rx_dropped);
4526 | else
4527 | atomic_long_inc(&skb->dev->rx_nohandler);
4528 | kfree_skb(skb);
4529 | /* Jamal, now you will not able to escape explaining
4530 | * me how you were going to use this. :-)
4531 | */
4532 | ret = NET_RX_DROP;
atomic_
がまさにその場所です。コードを見るとわかるように、rx_
ではなくrx_
が上がることもあるようです。というわけでこのラベルにジャンプするかどうかを確認してみましょう。幸い、.label("FOO")
」
#!/usr/bin/env stap
probe kernel.function("__netif_receive_skb_core").label("drop")
{
printf("pakcet dropped\n");
}
早速、
$ sudo stap drop.stp pakcet dropped
裏でerrframe.
rx_dropped
の値を表示する
次にrx_
の内容を表示してみましょう。
SystemTapではいくつかのローカル変数にアクセスできます。たとえばdropラベル付近の変数であれば、
$ stap -L 'kernel.function("__netif_receive_skb_core").label("drop")' kernel.function("__netif_receive_skb_core@/build/linux-hwVdeu/linux-4.15.0/net/core/dev.c:4523").label("drop") $skb:struct sk_buff* $pfmemalloc:bool $orig_dev:struct net_device* $ret:int $type:__be16
上記の結果だと、$skb
」struct sk_
」
さらに@cast()
を使うことで、
#!/usr/bin/env stap
probe kernel.function("__netif_receive_skb_core").label("drop")
{
net_device = @cast($skb, "struct sk_buff", "kernel<linux/skbuff.h>")->dev
dropped = atomic_long_read(&@cast(net_device, "struct net_device", "kernel<linux/netdevice.h>")->rx_dropped)
printf("dropped1 = %lu\n", dropped)
}
上記では$skb
」linux/
」struct sk_
」dev
メンバーにアクセスしています。さらにdev
はstruct net_
であり、rx_
メンバーの値をatomic_
」dropped
変数に保存、
実際に実行してみると次のような結果になります。
$ sudo stap drop.stp dropped1 = 2
どうやら値が読めているようです。
ちなみにip -s link show
の結果と比べてみると、dropped1
の値は1つ少ないことがわかります。これはrx_
カウンターの内容を見てしまっているためです。コードをよく見るとわかりますが、rx_
はdropラベルにジャンプしたあとに更新されるので、
ラベルのある箇所と違って、stap -L
」kfree_
が実行される4528行目
probe kernel.statement("__netif_receive_skb_core@/build/linux-hwVdeu/linux-4.15.0/net/core/dev.c:4532").nearest
{
net_device = @cast($skb, "struct sk_buff", "kernel<linux/skbuff.h>")->dev
dropped = atomic_long_read(&@cast(net_device, "struct net_device", "kernel<linux/netdevice.h>")->rx_dropped)
printf("dropped2 = %lu\n", dropped)
}
「statement("関数名@ファイル名:行番号").nearest
」
$ sudo stap drop.stp dropped1 = 3 dropped2 = 4
dropped2のほうは、rx_
が表示されましたね。
関数を定義する
SystemTapではfunction
を指定することで自前の関数を定義します。さきほどの例ではrx_
の増加前後で同じようなコードをコピーしていたので、
#!/usr/bin/env stap
function print_dropped(skb, label)
{
net_device = @cast(skb, "struct sk_buff", "kernel<linux/skbuff.h>")->dev
dropped = atomic_long_read(&@cast(net_device, "struct net_device", "kernel<linux/netdevice.h>")->rx_dropped)
printf("%s = %lu\n", label, dropped)
}
probe kernel.function("__netif_receive_skb_core").label("drop")
{
print_dropped($skb, "dropped1")
}
probe kernel.statement("__netif_receive_skb_core@/build/linux-hwVdeu/linux-4.15.0/net/core/dev.c:4528").nearest
{
print_dropped($skb, "dropped2")
}
これだけです。説明するまでもないぐらいわかりやすいですね。
MACアドレスを表示する
最後にMACアドレスを表示してみましょう。
skbからMACアドレスを取り出すには、skb_
でEthernetフレームのヘッダーを示すポインタを取り出します。そのメンバーであるh_
とh_
が送信先・
実はSystemTapのスクリプトの中でC言語の関数を呼び出すことも可能です。先に完成形のスクリプトを提示しておきましょう。
#!/usr/bin/env stap
%{
#include <linux/skbuff.h>
#include <uapi/linux/if_ether.h>
%}
function get_ethhdr:string(skb:long)
%{
struct sk_buff *skb;
struct ethhdr *ehdr;
skb = (struct sk_buff *)(long)STAP_ARG_skb;
ehdr = (struct ethhdr *)skb_mac_header(skb);
snprintf(STAP_RETVALUE, MAXSTRINGLEN, " src=%pM\n dst=%pM\n proto=%#x\n",
ehdr->h_source, ehdr->h_dest, ntohs(ehdr->h_proto));
CATCH_DEREF_FAULT();
%}
function print_dropped(skb, label)
{
net_device = @cast(skb, "struct sk_buff", "kernel<linux/skbuff.h>")->dev
dropped = atomic_long_read(&@cast(net_device, "struct net_device", "kernel<linux/netdevice.h>")->rx_dropped)
printf("%s = %lu\n", label, dropped)
}
probe kernel.function("__netif_receive_skb_core").label("drop")
{
print_dropped($skb, "dropped1")
}
probe kernel.statement("__netif_receive_skb_core@/build/linux-hwVdeu/linux-4.15.0/net/core/dev.c:4528").nearest
{
print_dropped($skb, "dropped2")
printf("%s", get_ethhdr($skb))
}
最初のポイントはC言語の部分は%{...%}
」
get_
では、STAP_
はSTPスクリプト上の変数をC言語で解釈するためのマクロであり、STAP_
は関数から値を返すためのマクロです。
カーネルのprintf関連のフォーマットには便利なフォーマット変換子%pM
など)MAXSTRINGLEN
」
あとは作成した文字列をそのまま出力しているだけです。実際の出力結果は次のようになります。
$ sudo stap -g drop.stp dropped1 = 4 dropped2 = 5 src=00:00:5e:00:53:00 dst=00:00:00:00:00:00 proto=0x4321
stapコマンドは-g
」-g
」
errframe.