zebian.log

技術系備忘録とか

セキュリティ・キャンプ2022 Y1参加記

8/8 ~ 8/12の間に行われたセキュリティ・キャンプ全国大会2022オンライン Y1 OS自作ゼミに参加したので、応募から終了までの様子を記録がてら振り返ってみようと思います。私が送った応募課題も晒しておきますので、これからY1ゼミの応募課題に取り組むぞ!という方に少しでも参考になればいいなと思っています。

セキュリティ・キャンプとは

IPAが開催している情報セキュリティの人材育成プログラムです。普段は4泊5日の合宿形式で行われていたのですが、今年は新型コロナウイルスの影響でオンライン形式でした。オンライン・オフライン共に参加費や教材費など全て無料で受けることができます。また、ゼミには専門コースと開発コースがあり、私が参加したY1ゼミは開発コースでした。(どのゼミも講師陣がとても豪華…!) www.ipa.go.jp

参加しようと思ったきっかけ

学校で先生から「こういうイベントあるよ〜」と紹介されたのがセキュリティ・キャンプでした。IPAの紹介ページをざっと眺めて開発コースにOS自作ゼミがあることを知り、前々から「30日でできる!OS自作入門」などでOSを自作することに興味があったということだったことが参加の決め手でした。また、学校の先生によると「過去うちの学校でセキュリティ・キャンプに参加できた生徒は誰もいなかった」ということで、「(うちの学校で)史上初の参加者になれば多少は有名になれるんじゃね?」という浮かれた気持ちもありました。

応募課題晒し

セキュリティ・キャンプに参加するには応募課題での選考を通過する必要がありました。以下が私の取り組んだY1の応募課題です。課題プログラムの実装が雑だったりと、なぜこの回答で選考を通過することができたのかは謎です。

共通課題A

OS自作ゼミで挑戦してみたいことを教えてください。また、それを実現するために必要となる実装や調査はどのようなものになるか、あなたの予想を教えてください。間違っていても大丈夫です。トピック例: コンテキストスイッチやページング、ネットワーク通信の実装など。もちろんこれ以外のトピックも歓迎します。講師によって得意分野が違いますので、それも踏まえて回答していただけると助かります。

今Rustでやっている自作のx86 OS(リポジトリhttps://github.com/Zakki0925224/myos)にFAT32ファイルシステムを実装することに挑戦してみたいです。ファイルシステムを実装し、ファイルの読み書きができるようになれば、カーネルの開発だけではなく、内蔵ソフトウェアの開発もできたりと、OS開発の幅が広がり、とても楽しそうだなと思ったからです。

ファイルシステムを実装するにあたり、ハードディスクを制御するための実装、PCIに繋がっているハードディスクを認識するための実装が必要になります。当日までにそれらの実装を完成させ、そこからはファイルシステムの実装に専念できればなと思っています。それに伴い、PCI、HDD、FAT32など、様々な規格の仕様を調査する必要があります。OSDev.org(https://wiki.osdev.org/Main_Page)では、自作OSの設計の方法やハードウェアの制御方法など、様々な情報を閲覧することができます。また、GitHub上でも様々な人が作った自作OSのリポジトリがあるので、それらが参考になるかと思います。

共通課題B

テキストエディタでコードを書いているとして、キーボードのキーを押してから画面に文字が表示される間の処理の流れを説明してください。また、そのなかで、あなたが大事だと思うOSの処理をいくつか取り上げて説明してください。

(USBキーボードについてはまだ触れたことがなく、仕様がわからないので、ここではPS/2キーボードについて説明します)

キーボードのキーを押してから画面に文字が表示される間に、キーボード入力によってPICが割り込みが発生させます。この割り込みで実行されるのが割り込みハンドラーです。割り込みハンドラーでは、キーボードから受け取ったデータ(それぞれのキーに割り当てられている、押した時と離した時で送信される内容が異なるスキャンコード)をバッファに保存します。それらを順次解析することで、どのキーを押したのか、離したのかを判定することができます。判定し、それぞれのキーの押した時に実行される処理によって、文字が表示されます。

そこで私が大事だと思ったのはバッファに保存されたデータを解析する処理です。キーによっては、複数のスキャンコードで構成されているものもあるので、割り込みハンドラー内ではなく、カーネルのメイン関数内ループで解析する必要があります。キーボードにはキー配列があり、同じスキャンコードでもキー配列が変われば役割も変わるキーがあります。そのため、あらかじめそれぞれのキー配列に合ったスキャンコード表を定義しておき、現在設定されているキー配列のスキャンコード表を参照して解析しなければいけません。

参考文献:30日でできる!OS自作入門

参考サイト:https://wiki.osdev.org/PS2_Keyboard

book.mynavi.jp

共通課題C

C言語で整数を要素に持つ固定長のリングバッファを実装してください。

以下の関数が最低限必要です。

  • リングバッファを新規に作成する関数 ring_new(size)
  • 要素を追加する関数 ring_push(ring, value)
  • 要素を取得する関数 ring_pop(ring)

実装の制約:

  • C言語の標準ライブラリのみを利用して実装してください。
  • プログラムとテストケースのひな形ファイルring.cをダウンロードして使ってください。
  • 関数やテストケースを追加するのは構いませんが、最初に定義してあるテストケースだけは変更せず、それらがパス(成功)するようにプログラムを作ってください。
  • 応募課題を提出する際は、みなさんが実装を追加したring.cをフォームの添付ファイルとして追加して提出してください。
  • 提出されたring.cの内容をそのままコンパイル&実行すると、すべてのテストケースにパスする状態で提出してください。テストケースにパスすると、"PASSED: All tests finished succesfully"と表示されます。初期状態では"FAILED: 2/2 tests failed" と表示されますので、これを修正するのが課題です。
  • 提供したリングバッファのひな形の関数定義には、改善できる点がいくつかあります。余裕があればそれについても指摘し、修正して説明してください。
#include <stdio.h>
#include <stdlib.h>

#define RING_SIZE 4

// リングバッファを表す構造体
struct RingBuffer {
  int index;
  int buffer[RING_SIZE];
};

struct RingBuffer ring_buffer;

// リングバッファを生成して返す
struct RingBuffer *ring_new() {
  int i;
  ring_buffer.index = 0;

  for (i = 0; i < RING_SIZE; i++) {
    ring_buffer.buffer[i] = 0;
  }

  return &ring_buffer;
}

// リングバッファの中身をすべて表示(Rust風)
void ring_print(struct RingBuffer *ring) {
  printf("RingBuffer: { ");
  printf("index: %d, ", ring->index);
  printf("buffer: [");
  int i;
  for (i = 0; i < RING_SIZE; i++) {
    printf(" %d,", ring->buffer[i]);
  }
  printf(" ], }\n");
}

// リングバッファ ring の末尾に要素 value を追加
void ring_push(struct RingBuffer *ring, int value) {
  ring->buffer[ring->index] = value;
  ring->index++;

  // インデックスの範囲が超えていたら0にリセット
  if (ring->index == RING_SIZE) {
    ring->index = 0;
  }

  printf("pushed: %d\n", value);
  ring_print(ring);
}

// リングバッファ ring の先頭要素を取り除いて返す
int ring_pop(struct RingBuffer *ring) {
  int i;
  int value = ring->buffer[0];

  // 先頭要素を取り除き、すべての要素を複製してずらす
  for (i = 1; i < RING_SIZE; i++) {
    ring->buffer[i - 1] = ring->buffer[i];
  }

  // 末尾要素の初期化
  if (ring->buffer[RING_SIZE - 1] != 0) {
    ring->buffer[RING_SIZE - 1] = 0;
  }

  // 現在のインデックスを-1(すでに0だった場合、リングの後ろに)
  if (ring->index == 0) {
    ring->index = RING_SIZE - 1;
  }
  else {
    ring->index--;
  }

  printf("poped: %d\n", value);
  ring_print(ring);

  return value;
}

// 指定したオフセットで中身のデータを取得
int get_ring_value_from_offset(struct RingBuffer *ring, int offset) {
  if (offset < 0) {
    offset *= -1;
  }

  if (offset >= RING_SIZE) {
    offset %= RING_SIZE;
  }

  return ring->buffer[offset];
}

// テスト記述のための便利関数
int fail, success;
void assert_equal(int want, int got) {
  if (want == got) {
    printf("OK: want %d, got %d\n", want, got);
    success++;
  } else {
    printf("NG: want %d, got %d\n", want, got);
    fail++;
  }
}

// テストケース群
void test_push_pop() {
  struct RingBuffer *r = ring_new();

  ring_push(r, 2);
  ring_push(r, 3);

  assert_equal(2, ring_pop(r));
  assert_equal(3, ring_pop(r));
}

void test_100push_100pop() {
  int i;
  struct RingBuffer *r = ring_new();

  for (i = 1; i <= 100; i++) {
    ring_push(r, i);
    assert_equal(i, ring_pop(r));
  }
}

void test_push_zero() {
  int i;
  struct RingBuffer *r = ring_new();

  for (i = 0; i < 10; i++) {
    ring_push(r, 0);
  }

  assert_equal(0, ring_pop(r));
}

void test_push_null() {
  struct RingBuffer *r = ring_new();
  ring_push(r, NULL);
  assert_equal(NULL, ring_pop(r));
}

void test() {
  struct RingBuffer *r = ring_new();
  ring_push(r, 1);
  ring_push(r, 2);
  ring_push(r, 3);
  ring_push(r, 4);
  ring_push(r, 5);

  ring_push(r, ring_pop(r));
  ring_push(r, ring_pop(r));
  ring_push(r, ring_pop(r));
  ring_push(r, ring_pop(r));
  ring_push(r, ring_pop(r));
}

void test_offset() {
  struct RingBuffer *r = ring_new();
  ring_push(r, 1);
  ring_push(r, 2);
  ring_push(r, 3);
  ring_push(r, 4);
  ring_push(r, 5);

  assert_equal(5, get_ring_value_from_offset(r, 4));
  assert_equal(5, get_ring_value_from_offset(r, 8));

  assert_equal(5, get_ring_value_from_offset(r, -4));
  assert_equal(5, get_ring_value_from_offset(r, -40));

  assert_equal(5, get_ring_value_from_offset(r, 392876));
}

int main() {
  test_push_pop();
  // 他のテストケース呼び出しはここに追記
  test_100push_100pop();
  test_push_zero();
  test_push_null();
  test();
  test_offset();

  if (fail != 0) {
    printf("FAILED: %d/%d tests failed\n", fail, fail + success);
    return EXIT_FAILURE;
  }
  printf("PASSED: All tests finished succesfully\n");
  return EXIT_SUCCESS;
}

選択課題D

(hikalium*1に担当してもらいたい方は回答してください)

なぜコンピューターにはOSが必要とされていると思いますか?現代の、もしくは将来のOSが果たすべき役割はどのようなものだと考えますか?この設問には正解はありませんので、あなたの考えを自由に記述してください。

OSが必要な理由は、ユーザに専門知識がなくてもコンピュータを簡単に扱えるようにするためだと思います。昔はコンピュータで計算するごとにハードウェア構成を変える必要がありました。技術が発展し、ハードウェア構成を変えなくてもソフトウェア側でハードウェアを制御できるようになり、現代のOSはそれらをすべて自動で行ってくれます。

現代のOSが果たすべき役割は「ハードウェアの違いを吸収すること」、将来のOSが果たすべき役割は「OSの違いを吸収すること」だと私は思います。

現在世界に出回っているデバイスはCPU、メモリ、ハードディスクなど、それぞれハードウェアの構成が様々です。WindowsMacOSLinuxなどの現代のOSは様々なハードウェア構成上で動かすことができ、ユーザはハードウェアの違いを意識せずにコンピュータを使うことができます。

現在アプリケーション開発で、クロスプラットフォームという複数のOSでも同じアプリケーションが動くという技術がありますが、アプリケーション側が複数のOSに合わせる形になっており、いちいちそれぞれに対応させなければいけません。大型OS市場で、派生ではなく完全独自OSが少ない原因にこれも含まれているのではないかと私は考えています。そこで、OS依存であるシステムコールなどに、OS側で共通基盤を用意すれば、OSの違いを意識せずにアプリケーションを開発することができるのではないかと思いました。私はそのあたりの知識がなく、これが実現・普及できるかどうかはわかりませんが、クロスプラットフォーム開発はかなり容易になるし、OS市場もより活発になると思います。

選択課題E

(内田*2に担当してもらいたい方は回答してください)

x86系CPUのメモリ管理機構が持つ「ページング」と呼ばれる仕組みを用いて実現されるOSの機能を1つ挙げ、その機能の概要と詳しい仕組みを説明してください。取り上げるOSの機能は既存の機能でも、独創的な機能でも良いです。

選択課題Dに回答したので未回答

任意課題F

その他、何かあれば自由に書いてください。これまでにあなたが行ってきた自作OSやシステムプログラミングに関連するリポジトリや、関連する活動へのリンク、OS自作へかける熱い想いなど、なんでもOKです。

私が自作OSに出会ったのは高校2年のときです。当時は自作OSを作りたくても、作り方など全く知らず、DebianLinuxのLiveCD上で、OS名やテーマ、プリインストールソフトなどを変更してもう一度LiveCDを作るというカスタムLinux作りをやっていました。しかし、これではプログラミングを一切していないので自作OSとは言えないのではないか、と考えていた矢先、偶然書店で見つけたのが「30日でできる!OS自作入門」でした。この本と出会い、OSの仕組みや制御の方法を知り、プログラミングに対する考え方が変わりました。

しかし自分でも手を動かしながら本を読み進めていく中で、どうしても上手くコンパイルできない部分があり、原因が全くわからず、入門の3割も満たないうちに挫折してしまいました。それから1年経ち、「Writing an OS in Rust」(https://os.phil-opp.com/)と出会い、初めてRustを使ってOSを作りました。このサイトの手順に従って最後まで進めることはできたのですが、入門手順に従っているだけではつまらないなと思い、今年に入ってからブートローダーにGRUBを使った自作x86 OSをRustで始めました。今まで書いてきた自作OSのコードや公開されいているGitHubリポジトリ、いろいろな解説サイトなどを見ながら、今回はコピペではなくできるだけ自分で考えて実装する、という目標を持ってやっています。今現在は仕組みを学びながらページングを実装している途中です。

今回のOS自作ゼミでは、講師の皆さんから奥義を教えて頂いたり、モチベーションになればいいなと思っています。私はまだまだRust初心者なので、特にhikaliumさんにはRustの便利な使い方なども伺えたらいいなと思っていますので、よろしくおねがいします。

全期間を通して

全5日間のセキュリティ・キャンプで、ゼミ講義の時間はそのうちの3日間でした。他のゼミはどうなのか分かりませんが、Y1では講義というよりかは受講生それぞれが自分の自作OSの開発をするという、開発メインのゼミでした。講師の方にはデバッグを手伝って頂いたり、わからないところを講義して頂きました。

事前学習期間

開発はセキュリティ・キャンプ1ヶ月前の事前学習期間中から始まりました。そこで設けられた週に一度のゼミのミーティング(自由参加)で開発方針を相談したり、進捗を話したりしました。私の開発目標は、応募課題にも書いたように「自作OSにFAT32ファイルシステムを実装する」にしました。事前学習期間中にFAT32イメージをどこに置いてどうやって読み取るかで右往左往していたのをミーティングで相談し、無事に方針を決めることができました。それからセキュリティ・キャンプ当日まで、設定した開発目標が達成できるように少しづつ開発を進めました。

また、セキュリティ・キャンプ3週間前から毎週金曜の夜に共通講義がありました。セキュリティ・キャンプの受講生全員がZoomに参加し講義を受けるといった感じで、どの講義もとても深く面白い内容で、勉強になりました。

それ以外にも「これ本当に無料でいいの?」と思うほどの技術書や協賛企業のノベルティが届いて驚きました。ゼミ講師の内田さんが書かれた「ゼロからのOS自作入門」も頂きました!

book.mynavi.jp

1日目

この日はLT発表、共通講義、グループワークが主な活動でした。

LT発表はつよつよな人たちばかりで、眺めてるだけでも面白かったです(理解できたとは言ってない)。微生物に論理回路を組む研究、デバッガ自作、CTF作問…etc

グループワークでは「セキュリティ・キャンプ」終了後に継続してグループで取り組む内容を決めました。私のグループでは、CTFを通じてセキュリティにまつわる技術を学ぶことになりました。私自身そもそも「CTFってなんぞや」状態だったので、とりあえずpicoCTFの一番簡単な問題をやってみることにしました。

2日目

朝から晩まで丸一日ゼミ講義=ひたすら開発しました。定期的に進捗確認したり休憩を挟んだりするだけでも結構効率よく開発を進めることができたような気がしました。しかしその日の夕方頃、文字列が思い通りに出力されないことから、アロケータにバグ(alloc先のメモリアドレスが一切変わってなかった)があることがわかりました。講師に相談するまでは何が原因なのか分からず、ひたすらデバッグで時間を溶かしました。結構な時間を溶かしてしまったので、期間内に目標が達成できないのではないかという危機感を抱き、その日はゼミ講義の時間が終わった後も深夜まで開発を続けていました。

3日目

この日も途中で企業イベントを挟んだ以外はひたすら開発でした。2日目まででディレクトリエントリが解釈できるようになったので、ファイル操作系のコマンド「ls」「cd」「cat」を実装しました。自作OS上で自作コマンドが動くようになると結構楽しくなってきます(小並感)。「cd」でちゃんとカレントディレクトリを移動できる様子は、GUIを実装していない私の自作OSの中では初めての目に見えるギミックで、達成感というか、なにか感慨深いものがありました。

企業イベントでは予め決めた2社の企業説明を聞きました。セキュリティ・キャンプということで、企業の方もセキュリティのスペシャリストが沢山いました。私はセキュリティ関係はからっきしなので、実際のインシデント対応などの現場の話を企業の方から聞けたのはかなり新鮮でした。

4日目

夕方までひたすら開発、その日の終わりにゼミ内成果発表会が行われました。開発も終盤に差し掛かり、報告会まで残り1時間を切ったところでまたもやアロケータにバグが見つかりました。今回のバグの原因は2日目に作ったメモリアドレスを求める計算式で使っている括弧の位置が間違っていたからでした。何故こんな簡単なことに気づかなかったのかは自分でもよくわかってません。多分あの時は疲労が凄くて頭が回っていなかったような気がします。

そしてついに成果報告。個人的にはファイルの書き込みも実装してみたかったのですが、時間的に実装には至りませんでした。他の受講生の成果発表を聞くと、どれも自分よりもレベルが高い内容で凄いなと思いました。周りと比べると、どうしても自分はあまり成果を上げることができなかったのかなと感じてしまいますが、自分の中では満足な結果だったかなと思いました。

  • 成果発表で見せた動画

5日目

長かったようで短かった5日間の最終日。グループワークで決めたことの発表と、全ゼミの代表者成果発表が行われました。どのゼミもめちゃめちゃ高度なことやってるなあ、とまだまだな私には小並感な感想しか思い浮かびませんでしたが、特にジュニア開発ゼミの技術力には驚かされました。最近の中学生は強すぎる。

感想

私にとってこの5日間(+事前学習期間)は全てが学びでした。CTFなど、セキュリティ・キャンプに参加していなければ知ることのなかったであろうたくさんの事を知るきっかけになりました。また、プロの講師たちに質問できるという貴重な期間でしたが、周りの受講生の技術力に圧倒されてなかなか質問しづらく、もっと積極的に質問すればよかったなと思いました。自分の中で無意識に周りとの技術力ギャップを感じてしまっていたところがありましたが、むしろそれをバネにもっと自分の技術力を高めようとモチベーションに変える姿勢が大切なんだなと痛感しました。

事前学習期間のワクワク感、開発中の楽しさや思うように進まない焦燥感など、今振り返ってみればあっという間でしたが、多くのことを経験し、手に入れることができたと思っています。講師、チューター、受講生の皆さんには感謝の気持ちでいっぱいです。貴重な経験をありがとうございました!