プログラムのバージョン管理とオープンソース
プログラムを複数人で開発する場合のバージョン管理と、オープンソースプログラムを使う場合の注意を説明する。
バージョン管理システム
プログラムを学校や自宅のパソコンで開発する場合、そのソースコードはどのように持ち運び管理修正すべきだろうか?
最も原始的な方法は、常に全部を持ち歩く方法かもしれない。しかし、プログラムが巨大になってくるとコピーに時間がかかる。またコピーを取る時に、どれが最新なのか正しく把握する必要がある。
- 同期方式 – 2つのディレクトリのファイルの古い日付のファイルを、新しい日付のファイルで上書きするようなディレクトリ同期ソフトを使って管理
- 圧縮保管 – ファイル全体だと容量も多いため、複数のファイルを1つのファイルにまとめて圧縮を行う tar コマンドを使うことも多い。(tar ball管理)
diffとpatch
プログラムの修正を記録し、必要最小限で修正箇所の情報を共有する方式に patch がある。これには、2つのファイルの差異を表示する diff コマンドの出力結果(通称patch)を用る。diff コマンドでは、変更のある場所の前後数行の差異を !(入替) +(追加) -(削除) の目印をつけて出力する。patch コマンドに diff の出力を与えると、!,+,- の情報を元に修正を加えることができる。(通称「patchをあてる」)
((( helloworld-old.c ))) #include <stdio.h> void main() { printf( "Hello World\n" ) ; } ((( helloworld.c ))) #include <stdio.h> int main( void ) { printf( "Hello World\n" ) ; return 0 ; } ((( diff の実行 ))) $ diff -c helloworld-old.c helloworld.c ((( 生成された patch 情報 ))) *** helloworld-old.c 2022-07-25 10:09:10.694442400 +0900 --- helloworld.c 2022-07-25 10:09:26.136433100 +0900 *************** *** 1,5 **** #include <stdio.h> ! void main() { printf( "Hello World\n" ) ; } --- 1,6 ---- #include <stdio.h> ! int main( void ) { printf( "Hello World\n" ) ; + return 0 ; }
インターネットの初期の頃には、他の人のプログラムに対して間違いを見つけると、作者に対してこのpatch(diff出力)をメールなどで送付し、プログラムの修正が行われた。
広く世界で使われている Web サーバ apache は、オープンソースで開発されてきた。当初はプログラム公開後に間違いや機能追加の情報(patch)が世界中のボランティア開発者から送られてきながら改良が加えられていった。このため、”a too many patches”「つぎはぎだらけ」という自虐的皮肉を込めて apache と名付けられたと言われている。
初期のバージョン管理システム
バージョン管理システムは、複数人で少しづつテキストファイルに修正を加えながら改良を行うような際に、誰がどのような修正を行ったかという修正履歴を管理するためのツール。unix などのプログラム管理では rcs (revision control system) が使われていたが、その改良版として cvs (concurrent version system) が使われるようになっていった。(現在は後に紹介する Git などが主流)
- ci コマンド(check in) – ファイルをバージョン管理の対象として登録する。
- co コマンド(check out) – ファイルを編集対象とする(必要に応じて書き込みロックなども可能)。co されたファイルは、編集した人が ci して戻すまで ci することができない。
- 修正結果を ci する際には、新しい編集のバージョン番号などをつけて保存される。
- co コマンドでは、バージョン番号を指定してファイルを取り出すことも可能。
[Bさんの修正] /check out \check in ファイルver1.0-----→ver1.1------→ver1.2 \check out /check in [Aさんの修正]
集中管理型バージョン管理システム
rcs,cvs では、ファイルのバージョンは各ファイルを対象としているため、ファイルやディレクトリの移動や削除は管理が困難であった。これらの問題を解決するために、集中管理を行うサーバを基点として、対象ファイルのディレクトリ全体(ソースツリー)に対してバージョン番号を振って管理を行う。subversion はサーバに ssh などのネットワークコマンドを介して、保存・改変を行うことができる。
しかし、複数の人の修正のマージ作業の処理効率が悪く、処理速度が遅いため使われなくなっていった。同様のバージョン管理システムが企業により有償開発されていた(BitKeeperなど)が製品のライセンス問題が発生し、業を煮やした Linux 開発の Linus が Git のベースを開発・公開している。
分散型バージョン管理システム
Gitは、プログラムのソースコードなどの変更履歴を記録・追跡するための分散型バージョン管理システムである。Linus によって開発され、ほかの多くのプロジェクトで採用されている。(以下wikipedia記事を抜粋加筆)
Gitは分散型のソースコード管理システムであるため、リモートサーバ等にある中心リポジトリの完全なコピーを手元(ローカル環境)に作成して、そのローカルリポジトリを使って作業を行う。
一般的な開発スタイルでは、大雑把に言えば、以下のようなステップの繰り返しで作業が行なわれる:
- git clone – リモートサーバ等にある中心リポジトリをローカルに複製する。
- git commit – ローカルでコンテンツの修正・追加・削除を行い、ローカルリポジトリに変更履歴を記録する。
- 必要に応じて過去の状態の閲覧や復元などを行う。場合によってはこのステップを何度か繰り返す。
- git push – ローカルの変更内容を中心リポジトリに反映させる。
- git merge – git push の段階で、作業者ごとの変更内容が衝突することもある。Gitが自動で解決できる場合もあれば、手動での解決する。
- git pull – 更新された中心リポジトリ(他者の作業内容も統合されている)をローカルの複製にも反映する。これによりローカル環境のコードも最新の内容になるので、改めてステップ2の作業を行う。
ローカルリポジトリ(Aさん) ver1.0a1 ver1.0a2 ver1.1a1 修正--(git commit)--修正--(git commit) 修正--(git commit) /git clone \git push /git pull Bさんの修正 中心リポジトリver1.0-----------------ver1.1 も含まれる \git clone /git push 修正--(git commit)--修正--(git commit) 編集の衝突が発生すると ver1.0b1 ver1.0b2 git merge が必要かも ローカルリポジトリ(Bさん)
GitHub
Git での中心リポジトリを保存・管理(ホスティング)するためのソフトウェア開発のプラットフォーム。コードの管理には Git を利用し GitHub 社によって保守されている。2018年よりマイクロソフトの傘下企業となっている。
GitHub では単なるホスティングだけでなく、プルリクエストやWiki機能(ドキュメントの編集・閲覧機能)といった、開発をスムーズに行うための機能も豊富である。(個人的な例:github.com/tohrusaitoh/)
GitHub で管理されているリポジトリには、公開リポジトリと非公開リポジトリがあり、非公開リポジトリはその管理者からの招待をうけないとリポジトリ改変に参加できない。
企業でのプログラム開発で GitHub を内々で使っている事例なども多いが、間違って公開リポジトリと設定されていて企業の開発中のプログラムが漏洩してしまった…との事例もあるので、企業での利用では注意が必要。
オープンソースとライセンス
オープンソースプログラムは、プログラムのソースコードをインターネットで公開されたものである。しかし、元となったプログラムの開発者がその利用に対していくつかの制約を決めていることが多い。これらのオープンソースプログラムでのソフトウェア開発手法の概念として「伽藍とバザール」を紹介する。
伽藍とバザール
伽藍(がらん)とは、優美で壮大な寺院のことであり、その設計・開発は、優れた設計・優れた技術者により作られた完璧な実装を意味している。バザールは有象無象の人の集まりの中で作られていくものを意味している。
たとえば、伽藍方式の代表格である Microsoft の製品は、優秀なプロダクトだが、中身の設計情報などを普通の人は見ることはできない。このため潜在的なバグが見つかりにくいと言われている。
これに対しバザール方式では明確な方針が決められないまま、インターネットで公開されているプログラムをボランティアを中心とした開発者を中心に開発していく手法である。
代表格の Linux は、インターネット上にソースコードが公開され、誰もがソースコードに触れプログラムを改良してもいい(オープンソース)。その中で、新しい便利な機能を追加しインターネットに公開されれば、良いコードは生き残り、悪いコードは自然淘汰されていく。このオープンソースを支えているツールとしては、前に述べた git が有名。
オープンソース・ライセンス
ソースコードを公開している開発者の多くは、ソフトウェア開発が公開することで発展することを期待する一方で、乱用をふせぐために何らかの制約をつけていることが多い。最初の頃は、開発者に敬意を示す意味で、プログラムのソースコードに開発者の名前を残すこと、プログラムを起動した時に開発者の名前が参照できること…といった条件の場合もあったが、最近ではソフトウェアが広く普及・発展することを願って条件をつけることも多い。
こういったオープンライセンスの元となったのは、Emacs(エディタ),gcc(コンパイラ)の開発者のストールマンであり、「ユーザーが自由にソフトウェアを実行し、(コピーや配布により)共有し、研究し、そして修正するための権利に基づいたソフトウェアを開発し提供することにより、ユーザーにそのような自由な権利を与えた上でコンピュータやコンピューティングデバイスの制御をユーザーに与えること」を目標に掲げた GNU プロジェクトがある。linux を触る際のコマンドで、g で始まるプログラムの多くは GNU プロジェクトのソフトウェア。
GNU プロジェクトが掲げる GNU ライセンス(GPL)では、GPLが適用されていれば、改良したソフトウェアはインターネットに公開する義務を引き継ぐ。オープンソースライセンスとして公開の義務の範囲の違いにより、BSD ライセンス、Apacheライセンスなどがある。
コピーレフト型 | GNU ライセンス(GPL) | 改変したソースコードは公開義務, 組み合わせて利用では対応箇所の開示が必要。 |
準コピーレフト型 | LGPL, Mozilla Public License | 改変したソースコードは公開義務。 |
非コピーレフト型 | BSDライセンス Apacheライセンス |
ソースコードを改変しても公開しなくてもいい。 |
GPLライセンス違反
GPLライセンスのソフトウェアを組み込んで製品を開発した場合に、ソースコード開示を行わないとGPL違反となる。大企業でこういったGPL違反が発生すると、大きな風評被害による損害をもたらす場合がある。
- SwitchBot 社製品のGPL違反の注意喚起 – といっても2年間放置されてたの?
- SwitchBot 社が、この2023年7月に、GPL違反の注意喚起を受け、ようやく対応したようだ
最近のライセンスが関連する話題を1つ紹介:GitHub を使った AI プログラミング機能「Copilot」というサービスが提供されている。Copilot のプラグインをインストールした vscode(エディタ) では、編集している関数名や変数名などの情報と GitHub で公開されているプログラムの 学習結果を使って、関数名を数文字タイプするだけで関数名・引数・処理内容などの候補を表示してくれる。しかし、Copilot を使うと非オープンライセンスで開発していたプログラムに、オープンソースのプログラムが紛れ込む可能性があり、非オープンソースプロジェクトが GPL で訴えられる可能性を心配し「Copilot は使うべきでない」という意見の開発者も出ている。Copilot だけでなく、生成系 AI によるプログラムでも、同様の問題が指摘されている。
理解度確認
ライブラリと分割コンパイル
巨大なプログラムを作ってくると、プログラムのコンパイルに時間がかかる。こういった場合には、共有できる処理であればライブラリにまとめたり、分割コンパイルといった方法をとる。
C言語とライブラリ
C言語でプログラムを作っている時、printf() や scanf() といった関数を使うが、こういった組み込み関数のプログラムはどこに書かれているのだろうか?
ソースプログラムがコンパイルする際には、コンパイラ(compiler)によるコンパイル処理(compiler)、リンカ(linker or linkage editor)によるリンク処理(link)が行われる。この時に、printf()やscanf() の機械語(組み込み関数などのライブラリの内容)が実行プログラム a.out の中に埋め込まれる。通常は、コンパイルとリンク処理は一連の処理として実行される。
helloworld.c ソースプログラム ↓ compiler $ gcc -c helloworld.c (コンパイルだけ行う) ↓ ただしC言語では、コンパイルの前にプリプロセッサ処理 #include,#define の処理が行われる。 helloworld.o オブジェクトファイル(中間コード) ↓ linker $ gcc helloworld.o (リンク処理を行う) (+) ← libgcc.a ライブラリ(printf, scanf....) ↓ $ ./a.out a.out 実行プログラム
静的リンクライブラリと動的リンクライブラリ
しかし、printf() や scanf() のような組み込み関数の機械語が実行プログラムの中に単純に埋め込まれると、
- よく使われるprintf()やscanf()の処理は、沢山の実行プログラムの中に埋め込まれる。
そして、組み込み関数を使ったプログラムが複数実行されると、実行中のメモリに複数の組み込み関数の処理が配置されてメモリの無駄が発生する。 - その組み込み関数に間違いがあった場合、その組み込み関数を使った実行プログラムをすべて再コンパイルしないといけなくなる。
リンクされたプログラムの機械語が実行プログラムに埋め込まれる方式は、静的リンクライブラリと呼ぶ。
しかし、静的リンクライブラリの方式は、実行時の命令の領域のムダや、ライブラリに間違いがあった場合の再コンパイルの手間があるため、動的リンクライブラリ方式(共有ライブラリ方式)がとられる。
動的リンクライブラリでは、プログラム内には動的リンクを参照するための必要最小限の命令が埋め込まれ、命令の実体は OS が動的リンクライブラリとして管理する。
Linux では、静的リンクライブラリのファイルは、lib~.a といった名前で保存され、動的リンクライブラリは、lib~.so という名前で保存されている。 Windows であれば、拡張子が ~.DLL のファイルが動的リンクライブラリである。
OS にとって必須の動的リンクライブラリは /lib 配下に保存されるが、ユーザが独自にインストールするパッケージの場合 /lib のアクセス権限の都合で別の場所に保存されるかもしれない。この場合、その独自パッケージを起動する時に、動的リンクライブラリの保存場所を見つけ出す必要がある。Linux では 環境変数 LD_LIBRARY_PATH に自分が利用する動的リンクライブラリの保存場所を記載すれば、OS がプログラム起動時に動的リンクライブラリを見つけてくれる。
Javaのコンパイル と JIT と CLASSPATH
Java のプログラムを実行する場合は、以下のような流れで実行される。
Main.java ソースプログラム ↓ コンパイラ $ javac Main.java Main.class Javaバイトコード ↓ JVM実行 $ java Main (Main.classを実行) JIT (Just In Time コンパイラ) ↓ バイトコードを機械語に変換 機械語を実行
この場合、複数のファイルに分割された Java のプログラムであれば、以下のように分割、実行となる。
((( ListNode.java ))) import java.util.* ; class ListNode { int data ; ListNode next ; ListNode( int d , ListNode n ) { this.data = d ; this.next = n ; } static void print( ListNode p ) { for( ; p != null ; p = p.next ) System.out.print( p.data + " " ) ; System.out.println() ; } } ; ((( Main.java ))) import java.util.* ; public class Main { public static void main(String[] args) throws Exception { ListNode a = new ListNode( 11 , new ListNode( 22 , new ListNode( 33 , null ) ) ) ; ListNode.print( a ) ; } } ((( コンパイルと実行 ))) $ javac ListNode.java $ javac Main.java $ java Main
この時、ListNode.java , ListNode.class を別フォルダ(例えば module/ListNode.java に保存する場合、java Main にてプログラムを実行する際に、ListNode.class が見つからないといったエラーが出てくる。こういった場合には、javac –classpath module Main.java とか、java –classpath module Main といったように実行するか、環境変数 CLASSPATH に、export CLASSPATH=module:その他… といったように実行に必要な class ファイルが保存されている場所を登録しておく必要がある。
分割コンパイル
複数人でプログラムを開発する場合、1つのファイルを全員で編集するのは混乱してしまう。例えば、ちょうど情報構造論で説明している、リスト処理のようなプログラムであれば、List 構造の構造体、cons(),print() といったList 構造を操作する関数を作る人と、そのそれらの関数を利用するプログラムを書く人に分かれてプログラム開発をすれば混乱も減らせる。そして、それぞれ別のファイルになっている方が開発しやすい。
- list.h : ヘッダファイル – 構造体の宣言や関数のプロトタイプ宣言や変数のextern宣言などを記載
- list.c : リスト処理の cons,print などの処理内容を記載
- main.c : cons,print を使った処理を記載
#include “ヘッダファイル”
自作のヘッダファイルを読み込む場合は、#include “list.h“ のように記載する。
#include で、ヘッダファイルを < > で囲むと、/usr/include フォルダから探してくれる。” “ で囲むと、ソースプログラムと同じフォルダの中からヘッダファイルを探す。
プロトタイプ宣言と extern 宣言
ヘッダファイルには、list.c と main.c の両方で使われるデータ構造、関数、変数の宣言を記載する。関数は、引数の型や返り値の型を記載した struct List* cons( int , struct List*) ; といったプロトタイプ宣言を記載する。変数については、変数の型だけを宣言する extern struct List* stack ; といった extern 宣言を記載する。
// list.h ----------------------------- // リスト構造の宣言 struct List { int data ; struct List* next ; } ; // リスト操作の関数のプロトタイプ宣言 extern struct List* cons( int , struct List* ) ; extern void print( struct List* ) ; // stack の extern 宣言 extern struct List* stack ; // スタック操作関数のプロトタイプ宣言 extern void push( int ) ; extern int pop()
// list.c ----------------------------- #include <stdio.h> #include <stdlib.h> #include "list.hxx" // リストの要素を作る struct List* cons( int x , struct List* n ) { struct List* ans = (struct List*)malloc( sizeof( struct List ) ) ; if ( ans != NULL ) { ans->data = x ; ans->next = n ; } return ans ; } // 全要素の出力 void print( struct List* p ) { for( ; p != NULL ; p = p->next ) printf( "%d " , p->data ) ; printf( "\n" ) ; } // stack の実体 struct List* stack = NULL ; // スタックに x を保存 void push( int x ) { stack = cons( x , stack ) ; } // スタックの先頭を取り出す int pop() { int ans = stack->data ; struct List* d = stack ; stack = stack->next ; free( d ) ; return ans ; }
// main.c ----------------------------- #include <stdio.h> #include "list.hxx" int main() { struct List* top = cons( 1 , cons( 2 , cons( 3 , NULL ) ) ) ; print( top ) ; push( 11 ) ; push( 22 ) ; push( 33 ) ; printf( "%d\n" , pop() ) ; printf( "%d\n" , pop() ) ; printf( "%d\n" , pop() ) ; return 0 ; }
分割コンパイルの作業を確認するために、以下のコマンドを実行してみよう。
((( 一度にコンパイルする方法 ))) guest00@nitfcei:~$ cp /home0/Challenge/seg-compile/* . guest00@nitfcei:~$ gcc list.c main.c guest00@nitfcei:~$ ./a.out # 正しく実行できる。 ((( 失敗するコンパイル ))) guest00@nitfcei:~$ gcc list.c /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o: in function `_start': (.text+0x24): undefined reference to `main' collect2: error: ld returned 1 exit status # list.c の中に main() が無いからエラー guest00@nitfcei:~$ gcc main.c /usr/bin/ld: /tmp/ccxr4Fif.o: in function `main': main.c:(.text+0x17): undefined reference to `cons' /usr/bin/ld: main.c:(.text+0x24): undefined reference to `cons' /usr/bin/ld: main.c:(.text+0x41): undefined reference to `print' : collect2: error: ld returned 1 exit status # main.c の中に、cons(),print(),push(),pop() が無いからエラー ((( プログラムをひとつづつコンパイル ))) guest00@nitfcei:~$ gcc -c list.c # list.o を作る guest00@nitfcei:~$ gcc -c main.c # main.o を作る guest00@nitfcei:~$ gcc list.o main.o # list.o と main.o から a.out を作る guest00@nitfcei:~$ ./a.out # 正しく実行できる。
make と Makefile
上記のように分割コンパイルのためにファイルを分割すると、実行プログラムを生成するには以下のコマンドを実行する必要がある。
- gcc -c list.c (list.o を作る)
- gcc -c main.c (main.o を作る)
- gcc list.o main.o (list.oとmain.oを使って a.out を作る)
また、プログラムのデバッグ作業中ですでに list.o , main.o , a.out が生成されている状態で、main.c の中に間違いを見つけて修正した場合、list.o を作るための 手順1 は不要となる。例えば list.c が巨大なプログラムであれば、手順1を省略できれば、コンパイル時間も短くできる。一方で、どのファイルを編集したから、どの手順は不要…といった判断をプログラマーが考えるのは面倒だったりする。
こういった一部の修正の場合に、必要最小限の手順で目的の実行プログラムを生成するためのツールが make であり、どのファイルを利用してどのファイルが作られるのかといった依存関係と、どういった手順を実行するのかといったことを記述するファイルが Makefile である。
### Makefile ### # a.out を作るには list.o , main.o が必要 a.out: list.o main.o # 最終的に生成する a.out の依存関係を最初に書く gcc list.o main.o # list.o は list.c , list.h に依存 list.o: list.c list.h gcc -c list.c # main.o は main.c , list.h に依存 main.o: main.c list.h gcc -c main.c clean:; rm *.o a.out # 仮想ターゲット: make clean と打つと、rm *.o a.out を実行してくれる。
Makefile では、依存関係と処理を以下の様に記載する。make コマンドは、ディレクトリ内の Makefile を読み込み、ターゲットファイルのタイムスタンプと依存ファイルのタイムスタンプを比較し、依存ファイルの方が新しい場合(もしくはターゲットファイルが無い場合)、ターゲットを生成するための処理が行われる。
ターゲットファイル: 依存ファイル ... ターゲットファイルを生成する処理 # 行の先頭の空白は"タブ"を使うこと
理解確認
シェルスクリプトの演習
今回は、前回までのシェルの機能を使って演習を行う。
プログラムの編集について
演習用のサーバに接続して、シェルスクリプトなどのプログラムを作成する際のプログラムの編集方法にはいくつかの方式がある。
- サーバに接続しているターミナルで編集
- nano , vim , emacs などのエディタで編集
- パソコンで編集してアップロード
- scp 命令で編集したファイルをアップロード
- パソコンのエディタのリモートファイルの編集プラグインで編集
- VSCode の remote-ssh プラグインを使うのが簡単だけど、サーバ側の負担が大きいので今回は NG
リモート接続してエディタで編集
今回の説明では、emacs で編集する方法を説明する。
((( Emacs を起動 ))) guest00@nitfcei.mydns.jp:~$ emacs helloworld.sh
エディタが起動すると、以下のような画面となる。
- 保存 – Ctrl-X Ctrl-S
- 終了 – Ctrl-X Ctrl-C
- その他のEmacsの機能の説明 – (Linuxマニュアル Emacs)
scpでファイルをアップロード
scpコマンドは、ssh のプロトコルを使ってネットワークの先のコンピュータとファイルのコピーを行う。前述の emacs などのエディタが使いにくいのなら scp を使えばいい。
((( scp 命令の使い方 ))) $ scp ユーザ名@ホスト名:ファイルの場所 ((( サーバの helloworld.sh をダウンロード ))) C:\Users\t-saitoh> scp -P 443 guest00@nitfcei.mydns.jp:helloworld.sh . C:\Users\t-saitoh> scp -P 443 guest00@nitfcei.mydns.jp:/home0/Challenge/3-shellscript/helloworld.sh . ((( パソコンの hoge.sh をアップロード ))) C:\Users\t-saitoh> scp -P 443 hoge.sh guest00@nitfcei.mydns.jp: ((( パソコンの hoge.html を public_html にアップロード ))) C:\Users\t-saitoh> scp -P 443 hoge.html guest00@nitfcei.mydns.jp:public_html
シェルスクリプトの命令
条件式の書き方
シェルには、test コマンド( [ コマンド ) で条件判定を行う。動作の例として、テストコマンドの結果を コマンドの成功/失敗 を表す $? を使って例示する。
guest00@nitfcei:~$ [ -f helloworld.sh ] ; echo $? # [ -f ファイル名 ] 0 # ファイルがあれば0/なければ1 guest00@nitfcei:~$ [ -x /bin/bash ]; echo $? # [ -x ファイル名 ] 0 # ファイルが存在して実行可能なら0/だめなら1 guest00@nitfcei:~$ [ -d /opt/local/bin ] ; echo $? # [ -d ディレクトリ名 ] 1 # ディレクトリがあれば0/なければ1 guest00@nitfcei:~$ [ "$PATH" = "/bin:/usr/bin" ] ; echo $? # [ "$変数" = "文字列" ] 1 # $変数が"文字列"と同じなら0/違えば1
シェルの制御構文
((( シェルの if 文 ))) if [ -f helloworld.sh ]; then echo "exist - helloworld.sh" elif [ -f average.c ]; then echo "exist - average.c" else echo "みつからない" fi
((( シェルの for 文 ))) for user in /home0/guests/* # ワイルドカード文字 * があるので、/home0/guests/ のファイル一覧 do # が取り出されて、その1つづつが、$user に代入されながら繰り返し。 echo $user done --- 結果: /home0/guests/guest00, /home0/guests/guest01 ...
((( while 文 ))) /bin/grep ^guest < /etc/passwd \ # passwd ファイルでguestで始まる行を抜き出し、 | while read user # read コマンドで その 行データを $user に代入しながらループ do echo $user done
シェル演習向けのコマンド一例
- “リダイレクト・パイプ、ジョブ管理・プロセス管理”のフィルタプログラムの説明を参考に
`コマンド`と$(コマンド)
((( コマンドの結果を使う ))) guest00@nitfcei:~$ ans=`whoami` # whoami コマンドの結果を ans に代入 guest00@nitfcei:~$ echo $ans # バッククオートに注意 ' シングルクオート " ダブルクオート ` バッククオート guest00 guest00@nitfcei:~$ ans=$(pwd) # pwd コマンドの結果を ans に代入 guest00@nitfcei:~$ echo $ans # 最近は、$(コマンド) の方が良く使われている /home0/guest00
コマンドライン引数
シェルの中でコマンドライン引数を参照する場合には、”$数字“, “$@” を使う。$1 , $2 で最初のコマンドライン引数, 2番目のコマンドライン引数を参照できる。すべてのコマンドライン引数を参照する場合には、$@ を使う。
((( argv.sh : コマンドライン引数を表示 ))) #!/bin/bash echo "$@" for argv in "$@" do echo "$argv" done ((( argv.sh を実行 ))) guest00@nitfcei:~$ chmod 755 argv.sh guest00@nitfcei:~$ ./argv.sh abc 111 def abc 111 def # echo "$@" の結果 abc # for argv ... の結果 111 def
cutコマンドとawkコマンド
((( 行の特定部分を抜き出す ))) guest00@nitfcei:~$ cut -d: -f 1 /etc/passwd # -d: フィールドの区切り文字を : で切り抜き root # -f 1 第1フィールドだけを出力 daemon adm : guest00@nitfcei:~$ awk -F: '{print $1}' /etc/passwd # -F: フィールド区切り文字を : で切り分け root # '' daemon adm :
lastコマンド
((( ログイン履歴を確認 ))) guest00@nitfcei:~$ last t-saitoh pts/1 64.33.3.150 Thu Jul 7 12:32 still logged in 最近のログインした名前とIPアドレスの一覧 : ((( guest* がログインした履歴 ))) guest00@nitfcei:~$ last | grep guest guest15 pts/11 192.156.145.1 Tue Jul 5 16:00 - 16:21 (00:21) : ((( 7/5にログインしたguestで、名前だけを取り出し、並び替えて、重複削除 ))) guest00@nitfcei:~$ last | grep guest | grep "Jul 5" | awk '{print $1}' | sort | uniq 7/5("Jul 5")の授業で演習に参加していた学生さんの一覧が取り出せる。 ### あれ、かなりの抜けがあるな!?!? ###
whoisコマンド
((( IPアドレスなどの情報を調べる ))) guest00@nitfcei:~$ whois 192.156.145.1 : inetnum: 192.156.145.0 - 192.156.148.255 netname: FUKUI-NCT country: JP : guest00@nitfcei:~$ whois 192.156.145.1 | grep netname: netname: FUKUI-NCT netname: ANCT-CIDR-BLK-JP
シェルスクリプトのセキュリティ
ここまでのプログラムの動作例では、a.out などのプログラムを実行する際には、先頭に “./” をつけて起動(./a.out)している。これは「このフォルダ(“./“)にある a.out を実行せよ」との意味となる。
いちいち、カレントフォルダ(“./”)を先頭に付けるのが面倒であっても、環境変数 PATH を “export PATH=.:/bin:/usr/bin” などと設定してはいけない。こういった PATH にすれば、”a.out” と打つだけでプログラムを実行できる。しかし、”ls” といったファイル名のプログラムを保存しておき、そのフォルダの内容を確認しようとした他の人が “ls” と打つと、そのフォルダの中身を実行してしまう。
guest00@nitfcei:~$ export PATH=".:/bin:/bin/bash" guest00@nitfcei:~$ cat /home0/Challenge/1-CTF.d/Task5/Bomb/ls #!/bin/bash killall -KILL bash guest00@nitfcei:~$ cd /home0/Challenge/1-CTF.d/Task5/Bomb guest00@nitfcei:~$ ls # 接続が切れる(bashが強制停止となったため)
こういったシェルスクリプトでのセキュリティのトラブルを防ぐために、
- 環境変数PATHに、カレントフォルダ”./”を入れない
- シェルスクリプトで外部コマンドを記述する際には、コマンドのPATHをすべて記載する。
コマンドのPATHは、which コマンドで確認できる。echo とか [ といったコマンドは、bash の組み込み機能なので、コマンドのPATHは書かなくていい。
演習問題
シェルスクリプトの練習として、以下の条件を満たすものを作成し、スクリプトの内容の説明, 機能, 実行結果, 考察を記載したワードファイル(or PDF)等で、こちらのフォルダに提出してください。
- スクリプトとして起動して結果が表示されること。(シバン,実行権限)
- コマンドライン引数を使っていること。
- 入出力リダイレクトやパイプなどを使っていること。
- 以下の例を参考に。
((( 第1コマンドライン引数指定したユーザが、福井高専からアクセスした履歴を出力する。))) #!/bin/bash if [ -x /usr/bin/last -a -x /bin/grep ]; then # [ ... -a ... ] は、複数条件のAND /usr/bin/last "$1" | /bin/grep 192.156.14 fi ------------------------------------------------------------------------- ((( guest グループで、$HOME/helloworld.sh のファイルの有無をチェック ))) #!/bin/bash for dir in /home0/guests/* do if [ -f "$dir/helloworld.sh" ]; then # PATHの最後の部分を取り出す echo "$(/usr/bin/basename $dir)" # $ basename /home0/guests/guest00 fi # guest00 ~~~~~~~basename done
プロセス管理とシェルスクリプト
ジョブ管理
プログラムを実行している時、それがすごくメモリも使い計算時間もかかる処理の場合、条件を変化させながら結果が欲しい時、どのように実行すべきだろうか?1つの処理が1時間かかるとして、画面を見ながら1時間後に処理が終わったことを確認してプログラムを実行するのか?
簡単な方法としては、1つ目の処理(仮にプログラムAとする)を実行させたままで、新しくウィンドウを開いてそこで新しい条件でプログラムを並行処理すればいい(プログラムBとする)と考えるかもしれない。しかし、メモリを大量に使用する処理をいくつも並行処理させると、仮想メモリが使われるようになる。結果的にスワッピングが発生する分、プログラムAを実行させた後にプログラムBを実行するための時間以上に、時間がかかることになる。
ここで、プログラムを並行処理させるか、逐次処理させるといった、JOB(ジョブ)管理について説明を行う。
以下の説明で、複雑で時間のかかる処理を実行するとサーバの負担が高くなるので指定時間の処理待ちを行うための sleep 命令を使う。
逐次実行と並行実行
プログラムを連続して実行(処理Aの後に処理Bを実行する)場合には、セミコロン”;” で区切って A ; B のように処理を起動する。
guest00@nitfcei:~$ echo A A guest00@nitfcei:~$ echo A ; echo B A B
プログラムを並行して実行(処理Aと処理Bを並行処理)する場合には、アンド”&”で区切って A & B のように処理を起動する。
guest00@nitfcei:~$ sleep 5 & [1] 55 guest00@nitfcei:~$ echo A A [1]+ 終了 sleep 5 guest00@nitfcei:~$ sleep 2 & sleep 3 [1] 56 [1]+ 終了 sleep 2 guest00@nitfcei:~$ time ( sleep 1 ; sleep 1 ) # time コマンドは、コマンドの実行時間を測ってくれる。 real 0m2.007s user 0m0.005s sys 0m0.002s guest00@nitfcei:~$ time ( sleep 1 & sleep 1 ) real 0m1.002s user 0m0.003s sys 0m0.000s
fg, bg, jobs コマンド
プログラムを実行中に、処理(ジョブ)を一時停止したり、一時停止している処理を復帰させたりするときには、fg, bg, jobs コマンドを使う。
- 処理をしている時に、Ctrl-C を入力すると前面処理のプログラムは強制停止となる。
- 処理をしている時に、Ctrl-Z を入力すると前面処理のプログラムは一時停止状態になる。
- fg (フォアグラウンド) は、指定した処理を前面処理(キー入力を受け付ける処理)に変更する。
- bg (バックグラウンド) は、指定した処理を後面処理(キー入力が必要になったら待たされる処理)に変更する。
- jobs (ジョブ一覧) は、実行中や一時停止している処理(ジョブ)の一覧を表示する。
guest00@nitfcei:~$ sleep 10 # 途中で Ctrl-Z を入力する ^Z [1]+ 停止 sleep 10 guest00@nitfcei:~$ fg sleep 10 # 一時停止していた sleep 10 を実行再開 guest00@nitfcei:~$ sleep 3 ^Z [1]+ 停止 sleep 3 guest00@nitfcei:~$ sleep 4 ^Z [2]+ 停止 sleep 4 guest00@nitfcei:~$ jobs [1]- 停止 sleep 3 # [1],[2]というのはjob番号 [2]+ 停止 sleep 4 guest00@nitfcei:~$ fg %1 # ジョブ番号1 を前面処理にする sleep 3 guest00@nitfcei:~$ fg %2 # ジョブ番号2 を前面処理にする sleep 4
ps, kill コマンド
OS では、プログラムの処理単位は プロセス(process) と呼ぶ。OS はプロセスごとにメモリの実行範囲などの管理を行う。一連のプロセスを組み合わせて実行する単位を ジョブ(job) と呼ぶ。
複数のプロセスは間違ったメモリアクセスで他のプロセスが誤動作するようでは、安心して処理が実行できない。そこで、OS は、プロセスが他のプロセスのメモリをアクセスすると強制停止させるなどの保護をしてくれる。しかし、プロセスと他のプロセスが協調して処理を行うための情報交換のためにメモリを使うことは困難である。プロセス間で情報交換が必要であれば、パイプ機能やプロセス間共有メモリ機能を使う必要がある。
最近のOSでは、共通のメモリ空間で動き 並行動作する個々の処理は スレッド(thread) と呼び、その複数のスレッドをまとめたものがプロセスとなる。OS では、プロセスごとに番号が割り振られ、その番号を プロセスID(PID) と呼ぶ。実行中のプロセスを表示するには、ps コマンドを使う。
実行中のプロセスを停止する場合には、kill コマンドを用いる。停止するプログラムは、ジョブ番号(%1など) か プロセスID を指定する。
guest00@nitfcei:~$ sleep 3 ^Z [1]+ 停止 sleep 3 guest00@nitfcei:~$ sleep 4 ^Z [2]+ 停止 sleep 4 guest00@nitfcei:~$ jobs [1]- 停止 sleep 3 # [1],[2]というのはjob番号 [2]+ 停止 sleep 4 guest00@nitfcei:~$ ps w # プロセスの一覧(wを付けるとコマンドの引数も確認できる) PID TTY STAT TIME CMD 13 pts/0 Ss 00:00:00 -bash 84 pts/0 T 00:00:00 sleep 3 85 pts/0 T 00:00:00 sleep 4 86 pts/0 R 00:00:00 ps w guest00@nitfcei:~$ kill %1 [1]- Terminated sleep 3 guest00@nitfcei:~$ kill -KILL 85 [2]+ 強制終了 sleep 4 guest00@nitfcei:~$ ps ax # 他人を含めた全プロセスの一覧表示 PID TTY STAT TIME COMMAND 1 ? Ss 0:52 /sbin/init 2 ? S 0:00 [kthreadd] 3 ? I< 0:00 [rcu_gp] :
ここまでの授業では、OSでのリダイレクト・パイプの概念とプロセスの概念について説明を行ってきた。これによりプログラムの実行結果を他のプログラムに渡すことができる。これらの機能を使うと、いくつかのプログラムを次々と実行させるなどの自動化をしたくなってくる。そこで、これ以降では、OSとプログラムの間の情報を伝え合う基本機能の説明や、プログラムの起動をスクリプトとしてプログラム化するためのシェルスクリプト(shell script)について説明する。
環境変数
OSを利用していると、その利用者に応じた設定などを行いたい場合が多い。このような情報を管理する場合には、環境変数が使われる。環境変数はプロセス毎に管理され、プロセスが新しく子供のプロセス(子プロセス)を生成すると、環境変数は子プロセスに自動的に引き渡される。代表的な環境変数を以下に示す。
- HOME – ユーザがログインした際の起点となるディレクトリであり、/home/ユーザ名 となっているのが一般的。
シェルの中では”~” で代用できる。( “cd ~” で、最初のディレクトリに戻る ) - LC_ALL, LANG – ユーザが使う言語。OSからのメッセージなどを日本語で表示したい場合には、ja_JP.UTF-8 などを指定。
- TZ – ユーザの時差の情報(Time Zone) 日本であれば、”JST-9″ を設定するのが一般的。
日本標準時 “JST” で、グリニッジ標準時(GMT)との時差を表す “-9” の組み合わせ。 - PATH – ユーザがよく使うコマンドの保存されているディレクトリの一覧。/bin:/usr/bin の様にディレクトリ名を”:”区切りで書き並べる。
- LD_LIBRARY_PATH – 共有ライブラリの保存されているディレクトリの一覧。
環境変数と同じように、シェルの中で使われるものはシェル変数と呼ぶ。この変数は、子プロセスに引き渡されない。
環境変数を表示するには、env コマンド(環境変数を表示)や、set コマンド(環境変数やシェル変数を表示)を用いる。シェルの中で特定の環境変数を参照する場合には、$変数名 とする。echo コマンドで PATH を表示するなら、”echo $PATH” とすればいい。
guest00@nitfcei:~$ env SHELL=/bin/bash : guest00@nitfcei:~$ echo $PATH /bin:/usr/bin:/usr/local/bin
変数に値を設定する場合には、“変数名=値” の様に設定する。この変数を環境変数に反映させるには、export コマンドを用いるか、“export 変数名=値” を用いる。
環境変数の中で PATH は、コマンドを実行する際にコマンドの保存先を探すための変数であり、例えば PATH=/bin:/usr/bin:/usr/local/bin であったばあい、shell は、最初に /bin の中からコマンドを探し、次に /usr/bin を探し、さらに /usr/local/bin の中からコマンドを探す。PATH の設定の注意点
((( 環境変数の設定 ))) guest00@nitfcei:~$ PATH=/bin:/usr/bin guest00@nitfcei:~$ echo $PATH guest00@nitfcei:~$ export PATH guest00@nitfcei:~$ export PATH=/bin:/usr/bin:/usr/local/bin ((( PATHの確認 ))) guest00@nitfcei:~$ which zsh # which はコマンドの場所を探してくれる /bin/zsh guest00@nitfcei:~$ export PATH=/usr/local/bin:/usr/bin:/bin guest00@nitfcei:~$ which zsh /usr/bin/zsh ((( LC_ALL,LANG の確認 ))) guest00@nitfcei:~$ export LC_ALL=C guest00@nitfcei:~$ man man (英語でマニュアルが表示される) guest00@nitfcei:~$ export LC_ALL=ja_JP.UTF-8 guest00@nitfcei:~$ man man (日本語でマニュアルが表示される) ((( TZタイムゾーンの確認 ))) guest00@nitfcei:~$ export TZ=GMT-0 guest00@nitfcei:~$ date 2022年 7月 4日 月曜日 05:23:23 GMT # イギリスの時間(GMT=グリニッジ標準時間)が表示された guest00@nitfcei:~$ export TZ=JST-9 guest00@nitfcei:~$ date # 日本時間(JST=日本標準時間)で表示された 2022年 7月 4日 月曜日 14:23:32 JST guest00@nitfcei:~$ TZ=GMT-0 date ; date # 環境変数を一時的に変更して date を実行 2022年 7月 4日 月曜日 05:23:23 GMT 2022年 7月 4日 月曜日 14:23:32 JST
環境変数 PATH の考え方は、Windows でも同じように存在し、PATH を変更する場合には、「設定 – システムのプロパティ – 詳細設定 – 環境変数」により編集可能となる。
プログラムとコマンドライン引数と環境変数
この後に説明するシェルスクリプトなどの機能を用いる場合は、自分のプログラムとのデータのやり取りにコマンドライン引数と環境変数を使う。また、プログラムの実行に失敗した時に別の処理を実行するためには、main関数の返り値を使うことができる。
コマンドライン引数
コマンドライン引数は、プログラムを起動する時の引数として書かれている情報であり、C言語でこの情報を用いる時には、main関数の引数”int main( int argc , char** argv ) …” により値をもらうことができ、以下のようなプログラムを記述することで受け取ることができる。
# 参考として Java の場合のコマンドライン引数の取得方法も示す。
((( argv.c ))) #include <stdio.h> int main( int argc , char** argv ) { for( int i = 0 ; i < argc ; i++ ) { printf( "argv[%d] = %s\n" , i , argv[ i ] ) ; } return 0 ; } ((( argv.c を実行してみる ))) guest00@nitfcei:~$ cp /home0/Challenge/3-shellscript/argv.c . guest00@nitfcei:~$ gcc argv.c guest00@nitfcei:~$ ./a.out 111 aaa 234 bcdef argv[0] = ./a.out argv[1] = 111 argv[2] = aaa argv[3] = 234 argv[4] = bcdef ((( Argv.java ))) import java.util.* ; public class Argv { public static void main( String[] args ) throws Exception { for( int i = 0 ; i < args.length ; i++ ) System.out.println( "args["+i+"] = "+args[i] ) ; } } ((( Argv.java を実行してみる ))) guest00@nitfcei:~$ cp /home0/Challenge/3-shellscript/Argv.java . guest00@nitfcei:~$ javac Argv.java guest00@nitfcei:~$ java Argv 111 aaa 234 bcdef args[0] = 111 # Java では コマンド名argv[0]は引数に含まれない args[1] = aaa args[2] = 234 args[3] = bcdef
注意点:コマンドライン引数の0番目には、プロセスを起動した時のプロセス名が入る。
環境変数の参照
C言語のmain関数は、コマンドライン引数のほかに環境変数も参照することができる。envpの情報は、getenv関数でも参照できる。
((( argvenvp.c ))) #include <stdio.h> int main( int argc , char** argv , char** envp ) { // コマンドライン引数argc,argvの処理 for( int i = 0 ; i < argc ; i++ ) { printf( "argv[%d] = %s\n" , i , argv[ i ] ) ; } // 環境変数envpの処理 for( int i = 0 ; envp[i] != NULL ; i++ ) { printf( "envp[%d] = %s\n" , i , envp[ i ] ) ; } return 0 ; } ((( argvenvp.c を実行してみる ))) guest00@nitfcei:~$ cp /home0/Challenge/3-shellscript/argvenvp.c . guest00@nitfcei:~$ gcc argvenvp.c guest00@nitfcei:~$ ./a.out argv[0] = ./a.out envp[0] = SHELL=/bin/bash :
プロセスの返す値
プログラムによっては、処理が上手くいかなかったことを検知して、別の処理を実行したいかもしれない。
こういう場合には、C言語であれば main の返り値に 0 以外の値で return させる。( exit関数を使ってもいい )
以下の例では、入力値の平均を出力するが、データ件数が0件であれば平均値を出力できない。こういう時に、”return 1 ;” のように値を返せば、シェル変数 $? (直前のコマンドの返り値) に return で返された値を参照できる。
((( average.c ))) #include <stdio.h> int main() { int count = 0 ; int sum = 0 ; char buff[ 1024 ] ; while( fgets( buff , sizeof( buff ) , stdin ) != NULL ) { int value ; if ( sscanf( buff , "%d" , &value ) == 1 ) { sum += value ; count++ ; } } if ( count == 0 ) { // データ件数が0の場合は平均が計算できない。 fprintf( stderr , "No data\n" ) ; // プログラムが失敗したことを返すには 0 以外の値を return する。 return 1 ; // exit( 1 ) ; } else { printf( "%lf\n" , (double)sum / (double)count ) ; } return 0 ; } ((( average.c を動かしてみる ))) guest00@nitfcei:~$ gcc average.c guest00@nitfcei:~$ ./a.out 12 14 ^D # Ctrl-D で入力を終わらせる 13.00000 guest00@nitfcei:~$ echo $? # プロセスの実行結果の値を参照するためのシェル変数 $? 0 guest00@nitfcei:~$ ./a.out ^D # データを入力せずにすぐに終了させる。 No data guest00@nitfcei:~$ echo $? 1
シェルスクリプト
今まで、コマンドラインで命令の入力をしてきたが、こういったキーボードと対話的処理を行うプログラムは shell (シェル) と呼ばれ、今回の演習では、/bin/bash を用いている。 shell は、キーボードとの対話的処理だけでなく、shell で入力するような処理をファイルに記録しておき、そのファイルに記載されている順に処理を実行することができる。
guest00@nitfcei:~$ cp /home0/Challenge/3-shellscript/helloworld.sh . guest00@nitfcei:~$ cat helloworld.sh #!/bin/bash echo "Hello World" message="こんにちは" # シェル変数への代入 echo "Hello World = $message" # シェル変数の参照 guest00@nitfcei:~$ bash helloworld.sh # bash で helloworld.sh を実行する Hello World Hello World = こんにちは
シェルスクリプトの基本は、キー入力で実行するようなコマンドを書き並べればいい。
しかし、プログラムを実行する度に、bash ファイル名 と入力するのは面倒。こういう時には以下の2つの設定を行う。
- シェルスクリプトの先頭行に 実行させる shell の名前の前に “#!” をつける。
この行は、通称”シバン shebang (シェバン)“と呼ばれ、bashで実行させたいのなら”#!/bin/bash“、プログラミング言語 Perl で実行させたいのなら “#!/usr/bin/perl” とか、Python で実行させたいのなら、”#!/usr/bin/python” のようにすればいい。(今回のサンプルはすでに記入済み) - 保存したスクリプトに対して、実行権限を与える。
“ls -al “で “rw-r–r–” のようなファイルの書き込みパーミッションが表示されるが、通常ファイルの場合は、“x”の表示があると、プログラムとして実行可能となる。(フォルダであれば、rwxr-xr-x のように”x”の表示があると、フォルダの中に入ることができる)
((( 実効権限の設定 ))) guest00@nitfcei:~$ chmod 755 helloworld.sh guest00@nitfcei:~$ ./helloworld.sh Hello World Hello World = こんにちは
$HOME/.bashrc
シェルスクリプトは、Linux の環境設定を行うためのプログラム言語として使われている。
例えば、ユーザがログインする際には、そのユーザがどういった言語を使うのか(LC_LANG,LANG)や、どういったプログラムをよく使うのか(PATH,LD_LIBRARY_PATH)などは、そのユーザの好みの設定を行いたい。こういう時に、shell に bash を使っているのであれば、$HOME/.bashrc に、shell を使う際の自分好みの設定を記載すればいい。
((( $HOME/.bashrc の例 ))) #!/bin/bash # PATHの設定 export PATH=/usr/local/bin:/usr/bin:/bin # MacOS でインストールされているソフトで PATH を切り替える if [ -d /opt/homebrew/bin ]; then # /opt/homebrew/bin のディレクトリがあるならば... # HomeBrew export PATH=/opt/homebrew/bin:$PATH elif [ -d /opt/local/bin ]; then # /opt/local/bin のディレクトリがあるならば... # MacPorts export PATH="/opt/local/bin:$PATH" fi
ユーザ固有の設定以外にも、OSが起動する時に、起動しておくべきプログラムの初期化作業などにもシェルスクリプトが使われている。
例えば、/etc/init.d/ フォルダには、Webサーバ(apache2)やsshサーバ(ssh) といったサーバを起動や停止をするための処理が、シェルスクリプトで記載してあり、OS 起動時に必要に応じてこれらのシェルスクリプトを使ってサーバソフトを起動する。(ただし最近は systemd が使われるようになってきた)
理解度確認
フィルタとプロセス管理
フィルタプログラム
パイプを使うと、標準入力からデータをもらい・標準出力に結果を出力するような簡単なプログラムを組み合わせて、様々な処理が簡単にできる。こういったプログラムは、フィルタと呼ぶ。
簡単な例として、入力をすべて大文字に変換するプログラム(toupper)、入力文字をすべて小文字に変換するプログラム(tolower)が、下記の例のように保存してあるので動作を確かめよ。
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/toupper.c . guest00@nitfcei:~$ gcc -o toupper toupper.c . guest00@nitfcei:~$ cat toupper.c | ./toupper #INCLUDE <STDIO.H> #INCLUDE <CTYPE.H> INT MAIN() { INT C ; WHILE( (C = GETCHAR()) != EOF ) PUTCHAR( TOUPPER( C ) ) ; RETURN 0 ; } guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/tolower.c . guest00@nitfcei:~$ gcc -o tolower tolower.c guest00@nitfcei:~$ cat tolower.c | ./tolower (((何が出力されるか答えよ)))
よく使われるフィルタのまとめ
文字パターンを含む行だけ出力 | grep 文字パターン |
文字パターンを含まない行を出力 文字パターンを正規表現でマッチングし該当行を出力 大文字小文字を区別しない |
grep -v 文字パターン grep -e 正規表現 grep -i 文字パターン |
入力文字数・単語数・行数をカウント(word counter) | wc |
入力行数をカウント | wc -l |
データを昇順に並べる | sort |
データを降順に並べる 先頭を数字と見なしてソート |
sort -r sort -g |
同じ行データが連続したら1つにまとめる | uniq |
同じ行が連続したら1つにまとめ、連続した数を出力 | uniq -c |
空白区切りで指定した場所(1番目)を抽出 | awk ‘{print$1;}’ |
入力の先頭複数行を表示(10行) | head |
入力の末尾複数行を表示(10行) | tail |
指定した行数だけ、先頭/末尾を表示 | head -行数 tail -行数 |
入力したデータを1画面分表示した所で一時停止する(ページャ) more は、最も単純なページャで、SPACE で1画面送り、ENTER で1行送り、”q”で終了。 lv は前後に移動できるページャで、カーソルキー↑(b)↓(f) で行を前後に移動できる。 |
more lv |
LOG解析
Linux は利用者に様々なサービスを提供するサーバで広く利用されている。しかし、幅広いサービス提供となると、中にはウィルス拡散や個人情報収集のための悪意のあるアクセスも増えてくる。
このためサーバでは、アクセスを受けた時の状況を記録し保存する。このような情報はアクセス履歴、ログと呼ぶ。
ログの中には、以下のような情報が混在することになるが、大量の 1. や 2. 目的のアクセスの中に、3. や 4. といったアクセスが混ざることになるが、これを見逃すとシステムに不正侵入を受ける可能性もある。
- 本来の利用者からのアクセス
- 検索システムの情報収集(クローラーからのアクセス)
- 不正な情報収集のためのアクセス
- システムの不備を探して不正侵入などを試みるアクセス
今回の演習では、電子情報の web サーバのとある1日のアクセス履歴ファイルを用い、パイプ機能を使い様々なフィルタを使い LOG解析の練習を行う。
アクセス履歴の解析
Webサーバのアクセス履歴が、/home0/Challenge/2.2-LOG.d/access.log に置いてある。このファイルで簡単な確認をしてみよう。
(( ファイルの場所に移動 )) $ cd /home0/Challenge/2.2-LOG.d/ (( .asp という文字を含む行を表示 )) $ grep .asp access.log
電子情報のWebサーバには、.asp (WindowsのWebサーバで動かすプログラムの拡張子) など存在しない。明らかに設定不備を探すための攻撃である。
これを見ると、grep で .asp を含む行が抜粋され、.asp の部分が強調されていることで、攻撃を簡単に確認できる。しかしこれは画面行数で10件程度が確認できるが、本当は何回攻撃を受けたのだろうか?この場合は、行数をカウントする”wc -l” を使えばいい。
(( アクセス回数を数える )) $ grep .asp access.log | wc -l 37
access.log の各項目の意味
電子情報のWebサーバの access.log に記録されている各項目の意味は以下の通り。
項目 | log項目 | 内容 |
---|---|---|
1 | %h | リモートホスト。WebサーバにアクセスしてきたクライアントのIPアドレス |
2 | %l | リモートログ名。説明略。通常は “-“ |
3 | %u | ログインして操作するページでのユーザ名。通常は “-“ |
4 | %t | アクセスを受けた時刻 |
5 | %r | 読み込むページ。アクセス方法(GET/POSTなど)と、アクセスした場所。 |
6 | %>s | ステータスコード。(200成功,403閲覧禁止,404Not Found) |
7 | %b | 通信したデータのバイト数。 |
8 | %{Referer}i | Referer どのページからアクセスが発生したのか |
9 | %{User-Agent}i | User-Agent ブラウザ種別(どういったブラウザからアクセスされたのか) |
以下に、フィルタプログラムを活用して、色々な情報を探す例を示す。実際にコマンドを打って何が表示されるか確認しながら、フィルタプログラムの意味を調べながら、何をしているか考えよう。
.asp を使った攻撃を探す
(( .asp を試す最初の履歴を探す )) $ grep "\.asp" access.log | head -1 49.89.249.9 - - [20/Dec/2019:09:19:06 +0900] "POST /Include/md5.asp HTTP/1.1" 404 64344 "https://www.ei.fukui-nct.ac.jp/Include/md5.asp" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)" (( 49.89.249.9 がどんなアクセスを試みているのか探す )) $ grep ^49.89.249.9 access.log | head 49.89.249.9 - - [20/Dec/2019:09:19:06 +0900] "POST /Include/md5.asp HTTP/1.1" 404 64344 "https://www.ei.fukui-nct.ac.jp/Include/md5.asp" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)" 49.89.249.9 - - [20/Dec/2019:09:19:06 +0900] "POST /inc/md5.asp HTTP/1.1" 404 61056 "https://www.ei.fukui-nct.ac.jp/inc/md5.asp" "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)"
- ステータスコードが404は”Not Found”なので、読み出しに失敗している。
- IPアドレス検索で、49.89.249.9 がどこのコンピュータか調べよう。
攻撃の時間を確認
(( 49.89.249.9 がどんな時間にアクセスを試みているのか探す )) $ grep ^49.89.249.9 access.log | awk '{print $4;}' [20/Dec/2019:09:19:06 [20/Dec/2019:09:19:06 [20/Dec/2019:09:19:07 :
- 不正アクセスを試みている時間を調べると、そのアクセス元の09:00~17:00に攻撃していることがわかる場合がある。どういうこと?
ページの閲覧頻度を確認
(( /~t-saitoh/ 見たIPアドレスと頻度 )) $ grep "/~t-saitoh/" access.log | awk '{print $1;}' | sort | uniq -c | sort -g -r | head 38 151.80.39.78 35 151.80.39.209 32 203.104.143.206 31 5.196.87.138 :
- grep – “/~t-saitoh/”のページをアクセスしているデータを抽出
- awk – 項目の先頭(IPアドレス)だけ抽出
- sort – IPアドレス順に並べる(同じIPアドレスならその数だけ重複した行になる)
- uniq – 重複している行数を数える
- sort -g -r – 先頭の重複数で大きい順にソート
- head – 先頭10行だけ抽出
(( /~t-saitoh/ 見たIPアドレスと頻度 )) (( t-saitoh のテスト問題のページを誰が見ているのか? )) $ grep "/~t-saitoh/exam/" access.log 5.196.87.156 - - [20/Dec/2019:06:36:02 +0900] "GET /~t-saitoh/exam/db2009/ex2009-5-1.pdf HTTP/1.1" 200 20152 "-" "Mozilla/5.0 (compatible; AhrefsBot/6.1; +http://ahrefs.com/robot/)" : (( クローラーのアクセスが多くてよくわからないので bot を含まない行を抽出 )) $ grep "/~t-saitoh/exam/" access.log | grep -v -i bot | lv 213.242.6.61 - - [20/Dec/2019:06:33:12 +0900] "GET /%7Et-saitoh/exam/ HTTP/1.0" 200 19117 "http://www.ei.fukui-nct.ac.jp/%7Et-saitoh/exam/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36 OPR/54.0.2952.64 (Edition Yx)" 188.163.109.153 - - [20/Dec/2019:06:43:04 +0900] "GET /%7Et-saitoh/exam/ HTTP/1.0" 200 19117 "http://www.ei.fukui-nct.ac.jp/%7Et-saitoh/exam/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.170 Safari/537.36 OPR/53.0.2907.99" 188.163.109.153 - - [20/Dec/2019:06:43:05 +0900] "POST /cgi-bin/movabletype/mt-comments.cgi HTTP/1.0" 404 432 "http://www.ei.fukui-nct.ac.jp/%7Et-saitoh/exam/" "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.170 Safari/537.36 OPR/53.0.2907.99" 45.32.193.50 - - [20/Dec/2019:07:06:15 +0900] "GET /~t-saitoh/exam/apply-prog.html HTTP/1.0" 200 5317 "http://www.ei.fukui-nct.ac.jp/" "Mozilla/5.0 (Windows NT 5.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"
- この結果を見ると mt-comments.cgi というアクセスが見つかる。どうも コメントスパム(ブログのコメント欄に広告を勝手に書き込む迷惑行為)をしようとしている。
ネットワーク攻撃への対処
今回の access.log のアクセス履歴解析は、Webサーバへのアクセスへの基本的な対処となる。しかし、もっと違うネットワーク接続ではどのような対処を行うべきであろうか?
一般的には、
- サーバにネットワークアクセスの記録ソフトを使う(例ネットワークプロトコルアナライザーWireShark)
- ファイアウォールのアクセス履歴を解析
授業内レポート
- ここまでのLOG解析の例の1つについて、どういう考え方でフィルタを使っているのか、自分の言葉で説明せよ。
- LOG 解析のためのコマンドを考え、その実行結果を示し、それから何が判るか説明せよ。
(例) コメントスパムを何度も試す危ないアクセス元は〇〇である。
ジョブ管理
プログラムを実行している時、それがすごくメモリも使い計算時間もかかる処理の場合、条件を変化させながら結果が欲しい時、どのように実行すべきだろうか?1つの処理が1時間かかるとして、画面を見ながら1時間後に処理が終わったことを確認してプログラムを実行するのか?
簡単な方法としては、1つ目の処理(仮にプログラムAとする)を実行させたままで、新しくウィンドウを開いてそこで新しい条件でプログラムを並行処理すればいい(プログラムBとする)と考えるかもしれない。しかし、メモリを大量に使用する処理をいくつも並行処理させると、仮想メモリが使われるようになる。結果的にスワッピングが発生する分、プログラムAを実行させた後にプログラムBを実行するための時間以上に、時間がかかることになる。
ここで、プログラムを並行処理させるか、逐次処理させるといった、JOB(ジョブ)管理について説明を行う。
以下の説明で、複雑で時間のかかる処理を実行するとサーバの負担が高くなるので指定時間の処理待ちを行うための sleep 命令を使う。
逐次実行と並行実行
プログラムを連続して実行(処理Aの後に処理Bを実行する)場合には、セミコロン”;” で区切って A ; B のように処理を起動する。
guest00@nitfcei:~$ echo A A guest00@nitfcei:~$ echo A ; echo B A B
プログラムを並行して実行(処理Aと処理Bを並行処理)する場合には、アンド”&”で区切って A & B のように処理を起動する。
guest00@nitfcei:~$ sleep 5 & [1] 55 guest00@nitfcei:~$ echo A A [1]+ 終了 sleep 5 guest00@nitfcei:~$ sleep 2 & sleep 3 [1] 56 [1]+ 終了 sleep 2 guest00@nitfcei:~$ time ( sleep 1 ; sleep 1 ) # time コマンドは、コマンドの実行時間を測ってくれる。 real 0m2.007s user 0m0.005s sys 0m0.002s guest00@nitfcei:~$ time ( sleep 1 & sleep 1 ) real 0m1.002s user 0m0.003s sys 0m0.000s
fg, bg, jobs コマンド
プログラムを実行中に、処理(ジョブ)を一時停止したり、一時停止している処理を復帰させたりするときには、fg, bg, jobs コマンドを使う。
- 処理をしている時に、Ctrl-C を入力すると前面処理のプログラムは強制停止となる。
- 処理をしている時に、Ctrl-Z を入力すると前面処理のプログラムは一時停止状態になる。
- fg (フォアグラウンド) は、指定した処理を前面処理(キー入力を受け付ける処理)に変更する。
- bg (バックグラウンド) は、指定した処理を後面処理(キー入力が必要になったら待たされる処理)に変更する。
- jobs (ジョブ一覧) は、実行中や一時停止している処理(ジョブ)の一覧を表示する。
guest00@nitfcei:~$ sleep 10 # 途中で Ctrl-Z を入力する ^Z [1]+ 停止 sleep 10 guest00@nitfcei:~$ fg sleep 10 # 一時停止していた sleep 10 を実行再開 guest00@nitfcei:~$ sleep 3 ^Z [1]+ 停止 sleep 3 guest00@nitfcei:~$ sleep 4 ^Z [2]+ 停止 sleep 4 guest00@nitfcei:~$ jobs [1]- 停止 sleep 3 # [1],[2]というのはjob番号 [2]+ 停止 sleep 4 guest00@nitfcei:~$ fg %1 # ジョブ番号1 を前面処理にする sleep 3 guest00@nitfcei:~$ fg %2 # ジョブ番号2 を前面処理にする sleep 4
ps, kill コマンド
OS では、プログラムの処理単位は プロセス(process) と呼ぶ。OS はプロセスごとにメモリの実行範囲などの管理を行う。一連のプロセスを組み合わせて実行する単位を ジョブ(job) と呼ぶ。
複数のプロセスは間違ったメモリアクセスで他のプロセスが誤動作するようでは、安心して処理が実行できない。そこで、OS は、プロセスが他のプロセスのメモリをアクセスすると強制停止させるなどの保護をしてくれる。しかし、プロセスと他のプロセスが協調して処理を行うための情報交換のためにメモリを使うことは困難である。プロセス間で情報交換が必要であれば、パイプ機能やプロセス間共有メモリ機能を使う必要がある。
最近のOSでは、共通のメモリ空間で動き 並行動作する個々の処理は スレッド(thread) と呼び、その複数のスレッドをまとめたものがプロセスとなる。OS では、プロセスごとに番号が割り振られ、その番号を プロセスID(PID) と呼ぶ。実行中のプロセスを表示するには、ps コマンドを使う。
実行中のプロセスを停止する場合には、kill コマンドを用いる。停止するプログラムは、ジョブ番号(%1など) か プロセスID を指定する。
guest00@nitfcei:~$ sleep 3 ^Z [1]+ 停止 sleep 3 guest00@nitfcei:~$ sleep 4 ^Z [2]+ 停止 sleep 4 guest00@nitfcei:~$ jobs [1]- 停止 sleep 3 # [1],[2]というのはjob番号 [2]+ 停止 sleep 4 guest00@nitfcei:~$ ps w # プロセスの一覧(wを付けるとコマンドの引数も確認できる) PID TTY STAT TIME CMD 13 pts/0 Ss 00:00:00 -bash 84 pts/0 T 00:00:00 sleep 3 85 pts/0 T 00:00:00 sleep 4 86 pts/0 R 00:00:00 ps w guest00@nitfcei:~$ kill %1 [1]- Terminated sleep 3 guest00@nitfcei:~$ kill -KILL 85 [2]+ 強制終了 sleep 4 guest00@nitfcei:~$ ps ax # 他人を含めた全プロセスの一覧表示 PID TTY STAT TIME COMMAND 1 ? Ss 0:52 /sbin/init 2 ? S 0:00 [kthreadd] 3 ? I< 0:00 [rcu_gp] :
理解度確認
Unix演習で ./a.out の理由
今日の演習の後で “コマンドを実行する時に ./a.out とか先頭に ./ をつけるのはなぜ?” との質問があった。
環境変数 PATH とは
通常、フォルダ一覧を表示するために “ls” とか入力しているけど、ls という命令はどこにあるのだろうか?
実は、unix や Windows では「よく使うコマンドの保存場所」を 環境変数 PATH にて管理している。環境変数は “echo $PATH” といった命令で確認ができる。unix では PATH は “:” 区切りで「よく使うコマンドの場所(ディレクトリ)」が列記してある。通常は /usr/local/bin:/usr/bin:/bin といった値になっているはず。
コマンドを実行する時にディレクトリが明記されていない場合は、PATH のディレクトリから探して実行することになっている。
(Windows の PATH は “;” 区切りなので要注意。cmd.exe を起動し echo %PATH% を実行すれば PATHが確認できる)
$ ls helloworld.c $ which ls /usr/bin/ls $ echo $PATH /usr/local/bin:/usr/bin/:/bin
このため、a.out のプログラムを実行する時には、”a.out” とだけ入力しても PATH に記載がないため「どこにある a.out を実行するの?」という状態になる。このため、カレントディレクトリにある a.out を実行する時には、”./” をつけて ./a.out と明示が必要となっている。
カレントディレクトリを PATH に加えればいいじゃん
コマンド実行で、いちいち“./”をつけるのはめんどくさい…と思う人もいるだろう。であれば、PATH を変更すればいい。
$ echo $PATH /usr/local/bin:/usr/bin/:/bin $ a.out a.out: コマンドが見つかりません。 $ PATH=.:/usr/local/bin:/usr/bin/:/bin $ a.out HelloWorld
しかし、この設定はセキュリティ的にも危険なのでやってはいけない設定の代表例です。
# Windows は、カレントディレクトリのプログラム実行で PATH 指定が不要なので要注意。
PATH=.:/usr/bin:/bin が危険な理由
もし、誰にでも書き込みができるフォルダがあって、そのフォルダに “ls” という名前のファイルを置き逃げした人がいたとしよう。
別の人はそのフォルダに入って、どんなファイルがあるのかな?ということで “ls” とタイプするかもしれない。そうすると何が起こるだろうか?
どういったことが発生するか、体験するためのフォルダが作ってあるので何が起こるか試してみよう。
$ cat /home0/Challenge/1-CTF.d/Task5/Bomb/ls #!/bin/bash killall -KILL bash 2> /dev/null # ← bash プロセスを殺すshell script(結果として強制ログアウトされる) $ PATH=.:/usr/bin:/bin # 危険なPATHの指定 $ cd /home0/Challenge/1-CTF.d/Task5/Bomb $ ls # ← どんなファイルがあるかな? ls Connection to nitfcei.mydns.jp closed.
この例では、強制ログアウトする命令となっているが、ls の処理として「login: … password: …」といった入力を行うようなプログラムが置いてあったら、「ls ってタイプしたらなぜかログイン画面にもどっちゃった。よくわからんけど再ログインするか…」と、ID とパスワードを入力する人もいるかもしれない。でも、この ID と パスワードを特定の人にメールするようにしてあれば、アカウント乗っ取りが可能となる。
リダイレクトとパイプ
Linuxを使う上で、キーボードでコマンドを入力しているが、こういうコマンドの管理を行うプログラムを shell と呼ぶ。shell には、色々なものがある(sh, csh, bash, zsh)が、広く使われている bash( born-again shell )について説明する。最初に、コマンドの入出力を組み合わせるために重要となるリダイレクトとパイプについて説明し、次にコマンドなどの処理単位となるジョブやプロセスの考え方について説明を行う。
標準入出力とリダイレクト
出力リダイレクト
C言語のプログラミングで、プログラムの実行結果をレポートに張り付ける時はどのように行っているだろうか?多くの人は、実行画面を PrintScreen でキャプチャした画像を張り付けているかもしれない。しかし、数十行にわたる結果であれば何度もキャプチャが必要となる。
そこで、今日の最初はリダイレクト機能について説明する。
“gcc ファイル.c” は、C言語のプログラムをコンパイルし、a.out という実行ファイルを生成する。”./a.out” にてプログラムを実行する。実行する命令に、“> ファイル名” と書くと、通常の出力画面(標準出力) をファイル名に記録してくれる。これを出力リダイレクトと呼ぶ。また、“>> ファイル名” と書くと、既存ファイルの後ろに追記してくれる。
guest00@nitfcei:~$ cat helloworld.c #include <stdio.h> int main() { printf( "Hello World\n" ) ; return 0 ; } guest00@nitfcei:~$ gcc helloworld.c guest00@nitfcei:~$ ./a.out Hello World guest00@nitfcei:~$ ./a.out > helloworld.txt guest00@nitfcei:~$ cat helloworld.txt Hello World guest00@nitfcei:~$ ./a.out >> helloworld.txt guest00@nitfcei:~$ cat helloworld.txt Hello World Hello World
入力リダイレクト
次に、1行に名前と3教科の点数が書いてある複数行に渡るデータの各人の平均点を求めるプログラムを考える。
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-each-low.c . guest00@nitfcei:~$ cat avg-each-low.c #include <stdio.h> // ((input)) ((output)) // saitoh 43 54 82 saitoh 59.67 // tomoko 89 100 32 tomoko 73.67 // mitsuki 79 68 93 mitsuki 80.00 int main() { char name[ 100 ] ; int point[ 3 ] ; while( scanf( "%s%d%d%d" , name , &point[0] , &point[1] , &point[2] ) == 4 ) { double sum = 0.0 ; for( int i = 0 ; i < 3 ; i++ ) sum += point[i] ; printf( "%s %6.2f\n" , name , sum / 3.0 ) ; } return 0 ; } guest00@nitfcei:~$ gcc avg-each-low.c guest00@nitfcei:~$ ./a.out saitoh 43 54 82 入力 saitoh 59.67 出力 tomoko 89 100 32 入力 tomoko 73.67 出力 ^D ← Ctrl-D を押すとファイル入力を終了
しかし、プログラムの書き方を間違えてプログラムを修正していると、動作確認のたびに何度も同じデータを入力するかもしれないが、面倒ではないだろうか?
プログラムを実行する時に、“< ファイル名” をつけると、通常はキーボードから入力する所を、ファイルからの入力に切り替えて実行することができる。このようなscanf()を使う時のようなプログラムの入力を標準入力といい、それをファイルに切り替えることを入力リダイレクトと呼ぶ。
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/name-point3.txt . guest00@nitfcei:~$ cat name-point3.txt saitoh 43 54 82 tomoko 89 100 32 mitsuki 79 68 93 guest00@nitfcei:~$ ./a.out < name-point3.txt saitoh 59.67 tomoko 73.67 mitsuki 80.00
この入力リダイレクトと出力リダイレクトを合わせて使うこともできる。
guest00@nitfcei:~$ ./a.out < name-point3.txt > name-avg.txt guest00@nitfcei:~$ cat name-avg.txt saitoh 59.67 tomoko 73.67 mitsuki 80.00
パイプ
先の名前と3教科のプログラムの結果から、全員の平均点をも計算したい場合、どのようなプログラムを作るだろうか?C言語だけの知識なら、各人の行のデータを計算するループの中に、全員の合計と人数を求めるプログラムを書いて、最後に平均点を出力するだろう。
一方で、複数人の名前と平均点のデータから平均点を求めるプログラムを書いて、前述のプログラムの実行結果を使う人もいるだろう。
以下の例では、“gcc -o avg-each-row avg-each-row.c” で、avg-each-row という実行ファイル、“gcc -o avg-all avg-all.c” で、avg-all という実行ファイルを生成し、avg-each-row で入力リダイレクト・出力リダイレクトを使って、name-avg.txt を作り、avg-all を入力リダイレクトで、最終結果を表示している。
guest00@nitfcei:~$ cp /home0/Challenge/2.1-RedirectPipe.d/avg-all.c . guest00@nitfcei:~$ cat avg-all.c #include <stdio.h> // ((input)) ((output)) // saitoh 59.67 73.11 // tomoko 73.67 // mitsuki 80.00 int main() { char name[ 100 ] ; double point ; double sum = 0 ; int count = 0 ; while( scanf( "%s%lf" , name , &point ) == 2 ) { sum += point ; count++ ; } printf( "%6.2f\n" , sum / (double)count ) ; return 0 ; } guest00@nitfcei:~$ gcc -o avg-each-low avg-each-low.c guest00@nitfcei:~$ gcc -o avg-all avg-all.c guest00@nitfcei:~$ ./avg-each-low < name-point3.txt > name-avg.txt guest00@nitfcei:~$ ./avg-all < name-avg.txt 71.11
しかし、いちいち入出力の結果を name-avg.txt を作るのは面倒である。であれば、以下の様なイメージで処理をすれば答えが求まる。
name-point3.txt→(avg-each-row)→name-avg.txt→(avg-all)→結果
これは、パイプ機能を使って以下の様に動かすことができる。
guest00@nitfcei:~$ ./avg-each-low < name-point3.txt | ./avg-all 71.11 guest00@nitfcei:~$ cat name-point3.txt | ./avg-each-low | ./avg-all 71.11
プログラムを実行する時に、“A | B” ように書くと、プログラムA の標準出力結果を、プログラムB の標準入力に接続させて、2つのプログラムを実行できる。このような機能を、パイプと呼ぶ。上記例の2つめ “cat… | ./avg-each-low | ./avg-all” では3つのプログラムをパイプでつないでいる。
リダイレクトのまとめ
入力リダイレクト(標準入力) | 実行コマンド < 入力ファイル |
出力リダイレクト(標準出力) | 実行コマンド > 出力ファイル |
出力リダイレクト(標準出力の追記) | 実行コマンド >> 出力ファイル |
標準エラー出力のリダイレクト | 実行コマンド 2> 出力ファイル |
パイプ コマンドAの標準出力をコマンドBの標準入力に接続 |
コマンドA | コマンドB |
標準エラー出力,/dev/null, /dev/zero デバイス
パイプやリダイレクトを使っていると、出力をファイルに保存する場合、途中で異常を伝える目的で出力メッセージを出力するものが見逃すかもしれない。こういった際に、計算結果などを出力する標準出力(stdout)とは別に標準エラー出力(stderr)がある。ファイルを使う際には、デバイスを区別するためのデバイス番号(ファイルディスクリプタ)を使う。この際に、標準入力(stdin) = 0, 標準出力(stdout) = 1, 標準エラー出力(stderr) = 2 という番号を使うことになっている。
#include <stdio.h> int main() { int x , y ; scanf( "%d%d" , &x , &y ) ; if ( y == 0 ) fprintf( stderr , "zero divide\n" ) ; else printf( "%f\n" , (double)x / (double)y ) ; }
Unix のシステムでは、特殊なデバイスとして、/dev/null, /dev/zero というのがある。
/dev/null は、入力リダイレクトに使うと ファイルサイズ 0 byte のデータが得られるし、出力リダイレクトに使うと 書き込んだデータは捨てられる。 /dev/zero は、入力リダイレクトに使うと、’\0′ のデータを延々と取り出すことができ、ファイルをゼロで埋めるなどの用途で使う。
/dev/null の使い方の例としては、例えば標準エラー出力が不要なときは、コマンドラインで以下のようにすれば、標準エラー出力(デバイス番号2番)の内容を捨てることができる。
$ command 2> /dev/null
C言語のコンパイルまとめ
C言語のコンパイル(実行ファイルはa.out) | gcc ソースファイル |
実行ファイル名を指定してコンパイル | gcc -o 実行ファイル ソースファイル |
理解度確認
Windows における PRN デバイス, AUX デバイス
/dev/null といった特殊デバイスの一例として、昔の Windows では PRN デバイス、AUX デバイスというのがあった。
C:< command > PRN昔の文字印刷しかできないプリンタが普通だったころは、PRN という特殊デバイスがあって、このデバイスにリダイレクトをすれば、出力をプリンタに出力することができた。同じように AUX というデバイスは、通信ポートにつながっていて ” command > AUX ” と入力すれば、通信先にデータを送ることができた。最近のプリンタや通信デバイスはもっと複雑になっているためにこういった機能は使えなくなっているが、このデバイス名の名残りで、Windows では PRN とか AUX という名前のファイルを作ることはできない。
ネットワークとセキュリティ
前回の授業では、バックエンドのプログラムの問題によるセキュリティ問題を説明した。今回は、ネットワークの物理的な接続方法などの話を中心にセキュリティについて説明を行う。
ネットワークからの攻撃とFireWall
脆弱性とバッファオーバーフロー
プログラムには、何らかのバグが潜んでいる可能性があり、悪用すると悪意のプログラムの実行や、情報の漏えい、システム異常を発生させサービスができなくするなどの脆弱性があって、悪意のある利用者から攻撃をうける可能性がある。
例えば、下記のようなC言語のプログラムは、配列をはみ出るようなデータを与えることで、関数の戻り番地を破壊させ、はみ出た部分に送り込んだ悪意のプログラムを実行させることが可能となる。このような入力用のデータ領域(バッファ)をはみ出させる攻撃はバッファオーバーフローと呼ばれる。
なお、最近のC言語のライブラリでは、上記のようなバッファオーバーフロー攻撃が一般的であることから、ASLR(Address Space Layout Randomization)により、スタック領域などの位置をランダム化することで、バッファオーバーフローによる攻撃が失敗するようになっている。
ルータとFireWall
外部にサービスを提供するようなシステムで、何らかの脆弱性のあるプログラムが使われていると、外部からのネットワーク接続で悪意のあるプログラム(マルウェア)を実行させられてしまうかもしれない。
このため、コンピュータでは不必要なプログラム(ネットワークサービス)は、起動されないようにする必要がある。もしくは、そのサービスは外部から利用できないように、途中のルータで FireWall(防火壁) を設置する。
FireWall では、(1)攻撃の可能性のあるIPアドレスからの接続を拒否、(2)外部に公開していないネットワークサービスのポート番号の接続を拒否といった方法をとる(拒否リスト方式)。もっと厳しい対策であれば、(3)特定のIPアドレスの機器からのみ接続を許可、(4)許可されているネットワークサービスのポート番号だけからだけ許可する方式をとる(許可リスト方式)
外部に公開する必要のないサービスがFireWallなどで正しく保護されていないと、攻撃をうける可能性がある。
ネットワーク接続のための装置
ルータやFireWallなどの仕組みをもう少し理解するために、組織内でネットワークを接続するための機器とその機能について改めて確認する。
ルータとは
元々の有線LANでは、1本のケーブルを時分割多重で共有しながら通信を行う。このため、瞬間的にはとある機器がネットワークを使用している時は、他の機器はデータ通信ができなくなる。この1本の線を大量の機器で使おうとすると、機器が使えるタイミングは減ってしまう。そこで、1本の線に直接接続する機器を分割したサブネットに分けて、必要な時だけ隣接するサブネットにパケットを中継するルータ or ブリッジが重要となる。
ルータは、隣接するサブネットのネットワーク番号(IPアドレスとサブネットマスク)を確認し、パケットを流す先を決定する。このネットワーク番号(IPアドレスとサブネットマスクの論理積)と中継を依頼するゲートウェイ(転送先)の一覧をルーティングテーブルと呼ぶ。
組織内のルータであれば、ネットワークの構造に合わせてあらかじめルーティングテーブルを定義しておく(静的ルーティング)。組織と組織を接続するようなルータは、自分に送ってほしいネットワーク番号の情報を相互に交換している(動的ルーティング)。
ブリッジとHUB
スイッチングHUB
機器を接続するための古いHUB(ダムHUB)では、通信中は他の機器の通信ができず効率が悪い。最近のHUBでは、通信する相手に応じて、内部のネットワークケーブルをスイッチのように接続・分離することができるスイッチングHUBを用いる。通信相手の識別には、一般的にMACアドレスが用いられる。(レイヤ2でのスイッチングHUB)
家庭用のスイッチングHUBは、特に細かい設定などは不要で管理機能が無いものは、アン マネージド スイッチングHUBと呼ばれる。
L2スイッチとL3スイッチ
サブネットに分割し、それぞれに異なるネットワーク番号を割り振り、中継するルータで FireWall を機能させることで、セキュリティを高めることが行われる。しかし、性能の高いスイッチングHUBは高価でもあり、1つのHUBに異なるネットワークを接続する必要がでてくることもある。この場合、IPアドレスを異なるネットワークの番号を偽装されると、データが盗み見られるかもしれない。
こういった相互に分離すべきネットワークであっても、柔軟なネットワーク構成とするためには、VLAN機能を持った L2スイッチ(レイヤ2スイッチングHUB) が使われる。タグVLAN機能付きのL2スイッチでは、特定のポートにVLANのタグ番号を割り当て、ポートに入る時にパケットに VLAN のタグ情報を付加し、そのパケットは同じ VLAN のタグをもつポートからしかデータを取り出せない。
L2スイッチ(レイヤ2スイッチ)は、機器のMACアドレスやパケットに付けられたVLANのタグ番号の情報(レイヤ2=データリンク層)でパケットの流れを制御している(下記OSI参照モデルの表を参照)。最近では、許可されていない機器がネットワークに侵入する不正侵入を防ぐために、登録されていないMACアドレスのパケットを通さないといった機能がある。
OSI参照モデルとレイヤ 第7層 アプリケーション層 アプリケーションの種類の規定 第6層 プレゼンテーション層 データフォーマットの交換 第5層 セッション層 コネクションの確立や切断などの管理 第4層 トランスポート層 パケットの分割合成や再送といった管理(TCP) 第3層 ネットワーク層 隣接するネットワーク間の通信(IPアドレス) 第2層 データリンク層 直接接続された機器間の通信(MACアドレス) 第1層 物理層 物理的な接続方法(コネクタや電圧など)
スイッチングHUBの中には、レイヤ3(IPアドレス)の情報でパケットの流れを制御するものもある。こういったスイッチは、L3スイッチ(レイヤ3スイッチ)と呼ばれるが、機能的にはルータと同じである。
一般的には、LANとWANを接続するための機器はルータ、LAN内部のネットワークを分離するためのものはL3スイッチと呼ぶ。
インターネットと接続するルータの機能
ネットワーク通信のIPアドレスとポート番号
クライアントの機器と通信相手との通信では、通信相手のIPアドレスとポート番号を指定してパケットを送出するが、処理結果を送信元に送り返すために、送信元のIPアドレスとポート番号が付加されている。送信元ではポート番号は、通信でよく使われる0~1023までのポート番号(ウェルノウンポート)以外で、1024~65535のポート番号(エフェメラルポート)の中から使われていないものをランダムに選んで使う。
送信相手に届いたパケットの返信データには、送信元と送信相手のIPアドレスとポート番号を入れ替えたものを割り当てることで、送信元にパケットが戻ってくる。
- DIP = 送信先IPアドレス、DP = 送信先ポート番号
- SIP = 送信元IPアドレス、SP = 送信元ポート番号
NAT(Network Address Translation)
現在広く使われているIPv4アドレス(32bit)では、40億台の機器間の通信しかできない。このため、組織内だけで使われるIPアドレス(プライベートIPアドレス)を使い、インターネットではグローバルIPアドレスを使う。
プライベートIPアドレス クラスA/8 10.0.0.0~10.255.255.255 大規模組織向け クラスB/12 172.16.0.0~172.31.255.255 中規模組織向け クラスC/16 192.168.0.0~192.168.255.255 家庭用ルータ向け
組織内のLANからインターネット(WAN)に接続する際には、プライベートアドレスをグローバルアドレスに変換するNAT(Network Address Translation)の機能が使われる。
NATの問題点
しかし、インターネットの内側で異なる機器で同じポート番号が割り振られると、戻ってきたパケットをどちらの機器に送ればいいのか区別できなくなる。
NAPT(Netowrk Address and Port Translation)
そこで、最近のNATでは、IPアドレスの変換だけでなくポート番号の付け替えも行われる。この機能は正式には NAPT(Network Address and Port Translation) と呼ぶが、単に NAT と呼ぶことも多い。Linuxでは、NAPTの実装をIPマスカレードと呼ぶ。
FireWall と DMZ
組織内で外部に公開しているサーバがある場合は、以下の図のような構成にするかもしれない。しかし、このようなネットワーク構成では、FireWallの内側の公開サーバが攻撃されて、踏み台となった場合、組織内のPCが簡単に攻撃をうけてしまう。
そこで、外部からの接続を行う DMZ(De-Militarized Zone 非武装地帯) を設け、外部公開用の公開サーバは DMZ 内に設置する。外部とつながる FireWall では、外部からのパケットは DMZ に流れるように構成する。DMZ 内のサーバが踏み台になった場合を想定し、組織内のルータでは DMZ のサーバと組織内PCは通信できないように FireWall を2重に設置する。
Webプログラミングとセキュリティ
ここまでの授業では、Webを使った情報公開で使われる、HTML , JavaScirpt , PHP , SQL などの解説を行ってきたが、これらを組み合わせたシステムを構築する場合には、セキュリティについても配慮が必要である。
今回は、初心者向けの情報セキュリティの講習で使われるCTFという競技の練習問題をつかって、ここまで説明してきた Web の仕組みを使ったセキュリティの問題について解説を行う。