バニキPの気ままに

気ままにやったこととか紹介していきます

C言語で自作shell作ってみた(コマンド実行とリダイレクト)

この記事は,Akatsuki Advent Calendar 2021 の3日目の記事です.

はじめに

11月半ばに急にslackでメンションが飛んできて,アドベントカレンダーの空き枠後少ししかないよ!と. 毎回アドベントカレンダー然り,何かしらのイベント然りで何を喋るかネタに困る. キラーパスがよく飛んできて,毎回なんとかなってるので今回もまあいっかということで引き受けました. 投稿1週間前の土日に頑張るぞと思ってたら,ライブがあってその夢は打ち砕かれました. なので,という訳ではないですが,そこまでやったぜ感はないです.(続報としてもっと良い感じに作って行こうかなとかは思ってます)

経緯

最近,Shell Scriptを読んで割と学ぶ機会がありました. まだまだ,Linux関連知識をもっと深めていった方がいいなと感じる場面がありました. じゃあ,勉強になる何かを作って勉強しつつアドベントカレンダーのネタにしようという作戦で,脳内会議では合意が取れました. 何か良い感じの学習の流れとか書いてないかなと探していた時に,このブログに出会いました. Linuxの基礎を理解する的なやつだったのですが,shellの欄があってbashとかの理解の結果として自作shellを作っていると書いてました. これだと思ってやってみるかということでネタを決めました.

本編

shellって何?

bash」や「zsh」などの種類が存在する,プログラムのことで,入力デバイスから何かしらのコマンドを受け取って,OSに渡して実行し,その結果をユーザに示すユーザインタフェースである. ユーザは,shellを使ってOSを操作したり,他のプログラムを実行したりする.

実装していくにあたって知っておいた方がいいこと

基礎として,以下の3つが挙げられる.

プロセス

Wikipedia先輩によるとプログラムの動作中インスタンスであるらしい. プログラムを実行するとプロセスが起動して,プログラムに従って動作する. かく言うshellもプロセスになる. 注意するべき点は,プロセスには制約があると言う点である. プロセスは,自分の「外部」にアクセスできない. つまり,ファイルの読み書きや自分以外のプロセスの起動などが「外部」に当たる. そうしたできないことは,カーネルに処理を委譲することになる.

カーネル

OSの中核にある特権を持つプログラムのことである. その特権には,ファイルの読み書きやプロセスの起動など,プロセスができないことを可能としている. このカーネルに処理をお願いする場合に使うのがシステムコールである.

システムコール

プログラムがOSの特権機能を呼び出す仕組みのことである. OSによって数が異なり,Linuxだと300個程度で,FreeBSDだと500個程度ある. 主要なシステムコールは,ファイル操作のopen(2),read(2),write(2),close(2),プロセスを起動するfork(2)などがある. プロセスは,システムコールを利用して,OSの特権を行使することで,本来できないことを実現している.

shellはどうやって動作してるの?

先ほどいったようにshellもプロセスの一つである. ということは,プロセスの制約を受けるということになる. shell自身は何かのコマンドを実行した後も他のコマンドを受け付ける必要がある. ということは,コマンド自体はshellと同一のプロセスとは別のプロセスで実行される必要がある. つまり,shellは別プロセスを起動してコマンドを実行するためのプログラムとなる. shellの実行の流れを説明する.

  1. コマンドを受け付ける
  2. fork(2)を実行しOSに別プロセスを起動してもらい,shell自身は新しく起動したプロセスの終了を待つ
  3. 新たに起動したプロセスでは,実際にexec(2)を用いて,コマンドを動作させる
  4. コマンドの結果をユーザに返した後,コマンド実行のために起動したプロセスは終了する
  5. shell自身のプロセスは,終了を確認すると次のコマンドを待つ

基本的には,1~5を繰り返すのがshellの動作となる.

実装してみた

今回は,久々にCを書きたくなったという雑な理由ではありますが,C言語を選択してます. (久々すぎて,色々includeしないと使いたい関数が使えないというのに戸惑いましたw)

簡単な解説

(1)のところで,入力を受け付けてから,入力された文字列をbufferに詰めてます. その後,読み取ったコマンドの列をパースしてtokenに入れます, (2)のところでは,入力されたコマンドを実行するためにforkしてshellとは別のプロセスを起動してます. (3)のところで,入力されたコマンドを実行して,(4)のところで,実行の終了を待機するという一連の流れを実装しました,

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>

#define BUF_SIZE 1024

char** eliminateToken(char **token, int size, int pos1, int pos2);

int main(int argc, char **argv) {
  char *buffer = malloc(sizeof(char) * BUF_SIZE);
  char **token = malloc(sizeof(char*) * BUF_SIZE);
  int c, status, position;
  pid_t pid;

  do {
    position = 0;
    printf("$ "); // 入力受付(1)
    while (true) {
      c = getchar();

      if (c == EOF || c == '\n') {
        buffer[position] = '\0';
        break;
      } else {
        buffer[position] = c;
      }
      position++;
    }
    token[0] = strtok(buffer, " ");
    for(position = 1; position < BUF_SIZE; position++) {
      token[position] = strtok(NULL, " ");
      if (token[position] == NULL) { break; }
    }
    token = realloc(token, sizeof(char*) * position);

    pid = fork(); // forkして別プロセスの起動(2)
    if (pid < 0) {
      perror("fork error");
      exit(EXIT_FAILURE);
    } else if (pid == 0) {
      if (execvp(token[0], token) == -1) { // コマンドの実行(3)
        perror("exec error");
        exit(EXIT_FAILURE);
      }
    } else {
      waitpid(pid, &status, WUNTRACED); // コマンドの終了を待機(4)
    }

    puts("");
    token = calloc(position, sizeof(char*) * BUF_SIZE);
  } while (!status);

  free(buffer);
  free(token);
  return EXIT_SUCCESS;
}

困りポイント

最初,実装した時は入力はgetcharではなく,fgetsを使っていたのですが,_や.といった記号のところで入力内容がおかしくなってしまう現象が発生しました. 結局,調査してもすぐに結論はでなさそうだし,時間もないということで一文字ずつbufferに格納する実装で動作させるようにしました. 普段rubyをメインで扱っていることもあり,すごく慣れない時間が長かったです. 学生時代はあんなに書いてたのに...使わないとすぐに錆びつきますね.

機能追加してみた(リダイレクト)

本当は,色々機能追加したかったのですが,ネタを決めるまでに時間がかかりすぎたので,そこまで時間がないから本当はコマンドが実行できたらいいかくらいでいました. しかし,いつものようにネコさんが,課題を設定してくれました. それが,リダイレクトです. 当初の予定では,リダイレクトとパイプくらい実装できればいいかと思ってましたのでいけるだろう.(そんな風に思って時期が自分にもありました.)

簡単な解説

コメントの間が追加した処理になります. 今回の実装で対応したのは,「>」と「>>」だけで,かつ必ず記号の後ろに空白を入れないと動作しません...(そこまで対応が間に合わなかったです...) 動作としては,入力された文字列にリダイレクトのマークがあったら処理をします. 確認方法は,strncmpを使って最初から指定文字が指定のマークか見てます. 「>>」が先なのは,「>」の場合「>>」の可能性もあるため先に確認してます. 「>>」の場合,ファイルを開く時,読み書きモードで開いて(O_RDWR),ファイルが存在していたらファイルに追加するようにAppendモードで開いて(O_APPEND),存在しない場合には新しく作成します(O_CREAT). 「>」の場合には,基本的に動作が同じで,ファイルが存在する場合の動作が異なります. ファイルが存在している場合は,中身を空にしてから書き込めるようにファイルを開きます(O_TRUNC). ファイルが開けたら,dup2を使って標準出力の先を開いたファイルに書き込むように変更します. つまり,ファイルディスクリプタをゴニョゴニョ弄っている訳です.(dup2の引数の2つ目のWRITEは標準出力を示すファイルディスクリプタ1番が入ってます.) 実行前に,eliminateTokenでリダイレクト処理の文字を削った実際に実行する必要のあるコマンドとオプションだけにしてexec(2)することで,リダイレクトを実現します.

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define BUF_SIZE 1024
#define READ     0
#define WRITE    1

char** eliminateToken(char **token, int size, int pos1, int pos2);

int main(int argc, char **argv) {
  char *buffer = malloc(sizeof(char) * BUF_SIZE);
  char **token = malloc(sizeof(char*) * BUF_SIZE);
  int c, status, position, fd;
  pid_t pid;

  do {
    position = 0;
    printf("$ ");
    while (true) {
      c = getchar();

      if (c == EOF || c == '\n') {
        buffer[position] = '\0';
        break;
      } else {
        buffer[position] = c;
      }
      position++;
    }
    token[0] = strtok(buffer, " ");
    for(position = 1; position < BUF_SIZE; position++) {
      token[position] = strtok(NULL, " ");
      if (token[position] == NULL) { break; }
    }
    token = realloc(token, sizeof(char*) * position);

    pid = fork();
    if (pid < 0) {
      perror("fork error");
      exit(EXIT_FAILURE);
    } else if (pid == 0) {
      // ここからしたがリダイレクトの処理
      for (int i = 0; i < position; i++) {
        if (!strncmp(token[i], ">>", 2)) {
          if ((fd = open(token[i+1], O_RDWR | O_APPEND | O_CREAT, 0666)) < 0) {
            perror("open error");
            close(fd);
            exit(EXIT_FAILURE);
          }
          dup2(fd, WRITE);
          token = eliminateToken(token, position, i, i+1);
          break;
        } else if (!strncmp(token[i], ">", 1)) {
          if ((fd = open(token[i+1], O_RDWR | O_TRUNC | O_CREAT, 0666)) < 0) {
            perror("open error");
            close(fd);
            exit(EXIT_FAILURE);
          }
          dup2(fd, WRITE);
          token = eliminateToken(token, position, i, i+1);
          break;
        }
      }
      // ここまでがリダイレクトの処理
      if (execvp(token[0], token) == -1) {
        perror("exec error");
        exit(EXIT_FAILURE);
      }
      close(fd);
    } else {
      waitpid(pid, &status, WUNTRACED);
    }

    puts("");
    token = calloc(position, sizeof(char*) * BUF_SIZE);
  } while (!status);

  free(buffer);
  free(token);
  return EXIT_SUCCESS;
}

char** eliminateToken(char **token, int size, int pos1, int pos2) {
  char **eliminate_buffer = malloc(sizeof(char*) * BUF_SIZE);
  int position = 0;

  for (int i = 0; i < size; i++) {
    if (i != pos1 && i != pos2) {
      eliminate_buffer[position] = token[i];
      position++;
    }
  }

  return eliminate_buffer;
  }

実際に動かしてみた

まずは,lsやcatなどの基本的なコマンド実行しました. f:id:shivashin495:20211203163956p:plain ちゃんと,動作して必要な情報を吐き出してくれてますし,オプションを指定しても動作してくれてます.

次にリダイレクトを実行してみます. まずは,「>」から実行しました. f:id:shivashin495:20211203164335p:plain

きちんとlsの結果がhogeというファイルに書き込まれています. また,リダイレクトの位置は真ん中にあっても良いのですが,それにも対応しています.

続いて,「>>」を実行してみます. f:id:shivashin495:20211203164742p:plain

もちろん元々書き込まれている内容に追記もできてますし,リダイレクトの位置が真ん中にあっても動作しています.

感想

今回は,勉強の一環も兼ねてC言語でshellを作ってみました. 正直,セキュリティ的にまずいところもあるだろうし,まだまだ効率的でなかったり,ほとんど関数に分割してないので可読性が低いと思います. しかし,勉強という点ではかなり良かったかなと思います. 身近に使っているbashzshというのがかなり高性能でよくできていると感じられますし,どんな動作をしているのかの理解度が上がりました. ただ,C言語で書いたのですか,パースする部分だったり,書くのが大変だったので,まだまだ勉強はしたほうが,今後のためにもなると思いました. まだshell作ったことがない方は,このブログが参考になるかはわかりませんが,誰でも調べたりすればできるので,勉強や趣味がてらやってみてはいかがでしょうか.

bpftraceで2048作ってみた

はじめに

去年のアドベントカレンダーでeBPFについていろいろ調べた. その時に,ネコの人に「SystemTapで昔ゲームを作ったことがあるから,BPFでも作れるはずだから作ってみたら」と. ちょうどLT会のネタに困っていたこともあり,作るかとなったので,bpftraceで動作する2048を作りました.(SystenTapで動作する2048もあります)

動作環境

bpftraceの環境構築は公式に書いてあるinstallの手順に従って導入してください. Linux5.3以上である事が必須となっています. Linux5.2以前では,命令数制限でverifierでカーネルにロードされずエラーとなってしまいます. また,プログラム内でwhileを使用しており,whileもLinux5.3以上でないとサポートされてないため(確かこのバージョンで入ったはず...)動作しません. このあたりの環境構築をするのが面倒な人のためにアドベントカレンダーの時にAnsibleを書いてますので良かったら利用してもらえる楽になるはず...

動かし方

1.下記からソースをcloneなりして取得してください. https://github.com/shivashin/bpftrace-2048

2.実行

sudo bpftrace 2048.bt 

この時,sudoをつけて実行してください.(命令数制限が緩和されたのは特権ユーザのみであるため)

3.ゲームを楽しむ

f:id:shivashin495:20210325233237p:plain
bpftrace-2048

※実行時引数に「emacs」や「wasd」を指定すると操作キーが変わります

操作の方向は盤面の下に記載されているキーを押すと事で遊ぶ事ができます.

どうやって動いてるの?(工夫とか紹介)

操作の入力

キーボードドライバのイベント関数(kbd_event)にフックして入力を取得しようとしましたが,これだと本体に接続されたPS/2やUSBキーボードでないと動作しない. 仮想環境で作業していたこともあり,ssh環境でも動作するようkprobesを使ってpty_write関数にフックして入力を取得するようにしました.

盤面の表示

操作されるたびに盤面が更新されるので,perfのCPU clockのsoftware eventにプログラムをアタッチして,一定間隔で再描画しています.

盤面の値

値を常に保存しておく必要があるため,BPF mapに盤面の値を保存していて,再描画される時にBPFプログラム側からperf_event_outputでメッセージを出力して,メッセージを受け取ったbpftraceプログラムがANSIエスケープシーケンスを利用して描画してます.

最後に

今回はカーネルで動作する2048を作ってみました. 関数に分割するのができないため,辛いところでしたね...(何度も分割したいと思った) ループの書き方もフラグとかでループを抜け出すみたいな処理を書くとVerifierに弾かれて実行できなかったり,LOGサイズの上限に達してしまったりと多少の苦労はありました. これを,LT会で発表する時はこの使い方が本来の使い方でないことを説明しないと変な勘違いしそうだから,せめて言ってあげてと言われて,本来の使い方をしてないことをそこで思い出したw. 有名な人もWifi強度で音の変わる楽器のプログラムを書いたり,キーボードを叩くとタイプライターの音がするプログラムを書いたりしてるので,あながち違ってないのもかもしれないですねw. 良かったら,みなさんもbpftraceでゲームを作ってみてはいかがでしょうか?

参考にしたやつ

neocat.hatenablog.com

eBPFについて知ってみよう

この記事は,Akatsuki Advent Calendar 202014日目の記事です.

はじめに

後輩が研究で使っている?もしくは,それに関連しているとかでBPFという技術があるということを知りました. 後輩の研究に対して何かしらのコメントとか指摘とかしたくて軽く調べたことがきっかけで興味を持ちました. このコロナ下の恩恵で,eBPF Summitが開催されたので,英語はそこまで聞き取れなかったけれども,何となくこんなことが出来るのか...と理解した. そのことを軽く話していると,ネコさんが「SystemTapで昔ゲームを作ったことがあるから,BPFでも作れるはずだから作ってみたら」と. 調べてみるとBPFでテトリスを動かしている人がいた. これは,何か簡単なゲームなら作れるかもしれないと思い,Advent Calendarに参加した.(出来るとは言ってない) まずは,BPFについてあれこれ調べようということで調査してみた.

BPFとは何か

BPFとは,Berkeley Packet Filterの略で,1992年にUNIX上でパケットキャプチャ.フィルタリングを効率的にするために開発されたソフトウェアである. 特定のパケットのみをアプリケーション側でキャプチャする時は,NICが受診したパケットを全てキャプチャプログラムへ渡してフィルタリングする方法がある. しかし,カーネル空間とユーザ空間で大量のシステムコールとデータコピーが発生してしまう. その問題解消解消するために,BPFが提唱されている. BPFは,カーネル内でパケットフィルタリングを行うことで,データコピーの削減やカーネル空間とユーザ空間の切り替えのオーバーヘッド削減を可能とした. 1997年に,Linuxカーネルに移植され,LinuxでBPFがパケットフィルタリング以外で利用されるseccomp(システムコール制限機構)が生まれた. BPFは,カーネル内で動作する独自のレジスタマシンと命令セットを持った仮想マシンの1つで,カーネルモジュールと違い,使える機能やプログラムの文法に制限がある. 加えて,ユーザ空間で作ったプログラムがカーネルとクラッシュさせたり,破壊したりしないように検証する機構を持っている. 2014年にBPFをより汎用的な仮想マシンにするために拡張され,拡張されたBPFをeBPF(extended BPF)と呼び,従来のBPFをcBPF(classic BPF)と呼ぶ. この拡張によって,パケットフィルタリング以外の用途で使われるようになった. 具体的には,以下のような場面にBPFが利用可能になっている.

  • ネットワーキング
  • トレーシング
    • カーネルトレーシング
    • ユーザプログラムトレーシング
    • パーフォマンスカウンタモニタリング
  • セキュリティ

cBPFとeBPFの違い

eBPFになってどのような拡張が行われ,どのような違いがあるか示す.(以下に示す以外にも違いは存在する)

数値的な差

項目 cBPF eBPF
レジスタ 2 10
レジスタ幅(bit) 32 64
スタックサイズ(byte) 16 512
スタックアクセスサイズ(byte) 4 1,2,4,8
パケットアクセスサイズ(byte) 1,2,4 1,2,4,8

命令数の増加

拡張によって以下の命令が追加された. また,cBPFでは多くの命令がAレジスタに対する命令であったが,eBPFでは,src_reg(代入するレジスタ,メモリアドレスまたは数値を指定する)やdst_reg(値を受け取る記憶領域を指定する)フィールドにより使用するレジスタを柔軟に選択できるようになっている.

  • 事前に登録済みの外部関数(カーネル内関数)呼び出し命令
  • アトミック加算命令(BPF mapのデータ更新に利用する)
  • エンディアン変換命令
  • 64bit幅命令(64bit幅でのメモリ読み書き及び64bit)

eBPF mapの追加

eBPF mapと呼ばれるデータ構造が利用可能になり,BPFプログラムから外部関数呼び出しを使うもしくは,ユーザ空間からシステムコールを利用して,アクセスすることが出来る. この機能によって,BPFプログラムとのやり取りが可能となった. mapには,BPF_TABLE,BPF_HASH,BPF_ARRAYなど16種類のデータ構造を利用することが可能である.

他のプログラムへのジャンプ

Tail Callと呼ばれるBPFプログラムへの遷移を行う機能で,遷移後に遷移元戻ることはない. 遷移先BPFプログラムとはスタックフレームを共有している. 一定時間で終了を保証するため,最大のTail Call回数は32回に制限されている.

eBPFの全体像

BPFの全体像を,以下に示す.

f:id:shivashin495:20201213235925p:plain

1.まずBPFプログラムをC言語で記述し,LLVM/Clangでコンパイルする.
2.bpf(2)システムコールを利用してカーネルにロードします.
3.カーネル内で検証器にてBPFプログラムの安全性を確認し,BPFプログラムのロードが完了する.
4.BPFプログラムとのデータのやり取りが必要な場合,bpf(2)システムコール経由でBPF mapが作成され,BPFプログラムとmapがやり取りを行う.
4'.ユーザ空間でBPF mapにアクセスすることでBPFプログラムから情報を取得することが出来る.
5.sockets,trace_points,kprobes,uprobesのイベントソースを使ってBPFをプログラムをアタッチする.
アタッチに対応するイベントが発生すると,BPFプログラムが呼び出され,処理が実行される.

補足

trace_pointsは,ソースに埋め込んで,イベントに処理を追加する仕組みのこと.
kprobesは,任意のアドレスに対して実行中のカーネルに処理をはさむための仕組みで,kretprobesという名前のものも存在し,違いとしては,関数の実行後に処理をはさむ点で異なる.
uprobesは,任意のアドレスに対してユーザ空間で動作シリアプリケーションに処理をはさむための仕組みで,uretprobesという名前のものも存在し,違いとしては,関数の実行後に処理をはさむ点で異なる.

安全性の確保

全体像で説明した通り,カーネルはBPFプログラムをアタッチする前に,実行しても安全であるか検証器を用いて検証する. 検証器で確認する前にも,bpf(2)システムコールの段階でBPFプログラムが一定サイズ以下(v5.9.14にて4096)であることを確認している. 検証器でのチェック項目は,以下のようなものがある

  • ループがないこと
  • 未初期化のレジスタを利用しないこと
  • コンテキストの許可範囲のみアクセしていること
  • 境界を超えたメモリアクセスをしないこと
  • メモリアクセスのアラインメントが正しいこと

また,検証器は必要に応じて一部の命令の書き換えを行う. 例えば,ネットワーク関連でinclude/uapi/linux/bpf.hで定義されているstruct __sk_buffを使う場合は,実際のカーネル内のデータ構造で利用されるsk_buffの対応したフィールドに対応するように変換する. オプションでJITコンパイルを行って,ROP攻撃対策やSpectre Variant 2対策を行うことが出来る. 他にも,/proc/sys/kernel/unprivileged_bpf_disabledを1にすることで,CAP_SYS_ADMINを持ってないユーザはbpf(2)システムコールが利用できないようにすることが可能である.

環境構築

BPFを利用するには,LinuxUNIX環境でないとカーネルに組み込まれていない. また,いろいろ試すにはいろいろ面倒なことが多かった(体験談). そこで,誰でも同じ環境が構築できるようにという目的と勉強も兼ねて,仮想環境の設定ファイルとAnsibleの設定ファイルを作成した.

必要なツール

以下のツールが必要になる. 確かめてはないが,基本安定最新版をインストールすれば,問題ないと思われる.(多分大丈夫なはず...)

インストール手順

Gitページに拙い英語で手順を書いているが,以下に示す. cloneしたディレクトリに入った後,vagrant upコマンドで仮想環境を立ち上げます. ansible-playbook playbooks/upgrade-kernel.yml(カーネルのバージョンアップ)とvagrant reload(仮想環境の再起動)は,やらないくても動作します. これは,BPFプログラムの命令数上限がLinux5.3で4096命令制限が特権ユーザなら最大100万命令までロードできるようになった(このコミット). 上限を引き上げる必要がある場合に実行することが必要になる. ansible-playbook playbooks/setup.ymlで実際に必要なツールなどをインストールしている.

git clone https://github.com/shivashin/bpf-test-environment.git
cd /path/bpf-test-environment
vagrant up
ansible-playbook playbooks/upgrade-kernel.yml
vagrant reload
ansible-playbook playbooks/setup.yml

インストールしているもの

  • BCC
  • python3-bcc
  • bpftrace
  • サンプルプログラム
  • その他必要なツール群

実際に何か動かしてみよう

先ほどの章で説明した環境構築で,手元に仮想環境は構築されている(はず...). vagrant sshで仮想環境にsshして,実際にeBPFのプログラムを動かしてみようと思う. 今回はpython3-bccの導入をしているので,pythonを用いる. 詳しい構文などについての情報はここにある.

コード

今回実行するコードを以下に示す. progの変数に格納されているコードがBPFのプログラムになる. 今回は,「Hello. World!」を出力するだけの簡単なプログラムである. bpf_trace_printkのように見慣れない関数がいるが,これは,ユーザ空間にメッセージを渡す関数である. BPF(text=prog)の部分でカーネルにロードする. このロード時に検証器にて,安全性の確認が行われるので,途中で説明したようなチェックでエラーになるとロードされない. b.attach_kprobe(event=b.get_syscall_fnname("mmap"), fn_name="hello")では,指定するイベントにアタッチしている. 今回の動作では,cloneシステムコールにアタッチしている. get_syscall_fnnameを使って,カーネル内の関数名を取得して,kprobeを用いて関数にアタッチしている. これ以降のコードは出力に関する設定をしている. このプログラムを実行することで,mmapシステムコールは発行された際に,「Hello, World!」を出力するプログラムが呼び出される.

from bcc import BPF

# define BPF program
prog = """
int hello(void *ctx) {
    bpf_trace_printk("Hello, World!\\n");
    return 0;
}
"""

# load BPF program
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("mmap"), fn_name="hello")

# header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE"))

# format output
while 1:
    try:
        (task, pid, cpu, flags, ts, msg) = b.trace_fields()
    except ValueError:
        continue
    print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))

実行してみた

このプログラムを実行して本当に動作しているか確認する. 確認方法は,lsコマンドを実行して確認する. 用意した環境にtmuxを導入しているので,別セッションを開くことで確認を行う. まず,lsコマンドが何回のmmapシステムコールを発行しているか確認するため,straceコマンドを用いる.

f:id:shivashin495:20201214001157p:plain

この結果より,mmapシステムコールは,17回発行されていることがわかる. eBPFプログラム側で17回分実行を検知しているか確認する.

f:id:shivashin495:20201213235959p:plain

eBPF側で17回lsコマンドでmmapが発行されていることを検知して「Hello, World!」を出力することができた.

さいごに

今回は,eBPFについて知ってみようということでこの記事を書きました. 本当は,簡単なゲームでも作ろうとしてましたが,Ansibleの作成やエラー処理,及び執筆に時間を使ったために実現しませんでした. その分,出来るだけ詳しめにBPF周りの情報をまとめたりしました. eBPFを触るきっかけにでもなればと思います.(出来るだけ調べたつもりですが,何か間違いがあったら申し訳ないです.) P.S. 時間はかかってもゲームは作ってみようと思います.(今年中は厳しいかな...,もう少しプログラムを書いてみないと分からないことが多すぎ.)