ホーム » スタッフ » 斉藤徹 » 講義録 » 情報メディア工学

情報メディア工学」カテゴリーアーカイブ

2025年4月
 12345
6789101112
13141516171819
20212223242526
27282930  

検索・リンク

JavaScriptによるフロントエンドとPHPバックエンド入門

前回の講義では、インターネットの仕組みを復習し、そこで使われるプログラミング言語などを紹介した。
今回の授業では、インターネットのブラウザ側(フロントエンド)で使われるプログラム言語である JavaScript の基本について整理しなおし、簡単な穴埋め問題による演習を行う。

JavaScriptによるフロントエンドプログラミング

インターネットとWebの仕組み

前回の講義では、最初のガイダンスとしてOSの仕組みについて説明をおこなった。

2回目の授業では、インターネットのWebページを作るために使われているHTMLやCSSやプログラム言語について解説を行う。

Webページの生成とプログラム言語

理解確認

情報メディア工学・ガイダンス/2025

情報メディア工学では、前期では情報を扱うためのOSの仕組みなどを、実践を交えながら演習を中心に行う。後期は5年の人工知能の授業につながる内容として、情報の中のデータをどう処理するのかを議論する。

OSの役割と仕組み

組込み系システム

組込み系のシステムで、OSが無い場合(例えば Arduino でデバイスを制御する場合)には、ユーザプログラムはデバイスを操作するライブラリやI/Oポートを直接制御しながら、ハードウェアを制御する。ユーザプログラムは、デバイスを操作するライブラリを含むため、異なるシステムでは機械語をそのまま使うことはできない。(共通化が不十分)

組込み系システムでは、ハードウェアを操作する命令をすべてユーザプログラムが面倒を見る必要があるため、システムが複雑化するとプログラム開発が大変になってくる。また、ユーザプログラムが間違った制御方法を取れば、ハードウェアを壊すような処理を実行してしまうかもしれない。(資源保護ができない)

オペレーティングシステム経由でハード操作

コンピュータのハードウェアの違いは OS がすべて包み隠し、OSが管理する。OSは 特権モード で動作し、ハードウェアを直接制御する。ユーザプログラムはユーザモードで動作し、OSの機能を呼び出すシステムコールを経由し、デバイス毎のデバイスドライバを経由して、ハードウェアを操作する。ユーザモードのプログラムは、ハードウェアを直接操作するような命令を実行しようとすると、OSがユーザの命令を強制停止させる。(資源保護)

ユーザプログラムには、ハードウェアを直接操作する機械語が含まれていないので、ユーザプログラムの機械語を同じOSが動く他のコンピュータにコピーして動かすことができる。(資源の扱いを共通化)

(例) helloworld のプログラムがコンソールに出力

簡単な例として、helloworld.c のような簡単なコンソール出力プログラムが動いて、画面に文字が表示されるのは以下の図のようにOSを経由して文字を表示している。

古いコンピュータで、プログラムが動作するだけならば、仕組みはすごく簡単にみえる。ユーザプログラムはすべて特権モードで動くOS(狭義のOSとかカーネルと呼ぶことが多い)を経由してハードウェアを操作する。

GUI が使えるグラフィカルな OS の場合

GUI が使えるグラフィカルなOSの場合、GUI の操作を支援するプログラム(ウィンドウマネージャ)などを利用しながら、ユーザはOSを操作する。コンピュータを操作する場合は、こういうウィンドウマネージャなどがないと不便であり、カーネルとユーザ支援のウィンドウマネージャなどをまとめて広義のOSと呼ぶ場合も多い。

ユーザプログラムは、GUIを操作するためのライブラリを経由し、さらにカーネルを経由してディスプレィに結果が表示される。

ユーザモードのプログラムの実行単位プロセスでは、処理を実行するためのメモリなどは他の処理と分離されており、他のプロセスのメモリ領域などを間違ってアクセスすると「メモリエラー」といった例外などが発生し、処理が強制的に停止させられる。このように、プロセスが他に悪影響を及ぼさないように、OS はメモリを管理する。(OSの保護機能)

(例) helloworld の結果を端末ソフトで表示

以下のように、コンソールアプリの実行結果を表示するような、cmd.exe は、helloworld.exe と OS を経由しながら連動して動いている。

helloworld.exe の出力は、OS を経由しながら cmd.exe に伝わり、cmd.exe はその表示内容に応じて、テキストの文字やフォントに合わせてグラフィカルな画面に文字を表示しようとする。グラフィカルな出力は GUI のライブラリを経由しながら OS に送られ、グラフィックドライバが画面に文字を表示する。

インターネットとプログラム

次に、インターネットの仕組みを踏まえ、インターネットで使われるプログラム言語やデータについて3~4週をかけて演習を中心にしながら、今まで習ってきたことを総括する。

理解確認

2024年度 情報メディア工学(前期)全講義録

プログラムのバージョン管理とオープンソース

プログラムを複数人で開発する場合のバージョン管理と、オープンソースプログラムを使う場合の注意を説明する。

バージョン管理システム

プログラムを学校や自宅のパソコンで開発する場合、そのソースコードはどのように持ち運び管理修正すべきだろうか?

最も原始的な方法は、常に全部を持ち歩く方法かもしれない。しかし、プログラムが巨大になってくるとコピーに時間がかかる。またコピーを取る時に、どれが最新なのか正しく把握する必要がある。

  • 同期方式 – 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は分散型のソースコード管理システムであるため、リモートサーバ等にある中心リポジトリの完全なコピーを手元(ローカル環境)に作成して、そのローカルリポジトリを使って作業を行う。

一般的な開発スタイルでは、大雑把に言えば、以下のようなステップの繰り返しで作業が行なわれる:

  1. git clone – リモートサーバ等にある中心リポジトリをローカルに複製する。
  2. git commit – ローカルでコンテンツの修正・追加・削除を行い、ローカルリポジトリに変更履歴を記録する。
    • 必要に応じて過去の状態の閲覧や復元などを行う。場合によってはこのステップを何度か繰り返す。
  3. git push – ローカルの変更内容を中心リポジトリに反映させる。
  4. git merge – git push の段階で、作業者ごとの変更内容が衝突することもある。Gitが自動で解決できる場合もあれば、手動での解決する。
  5. git pull – 更新された中心リポジトリ(他者の作業内容も統合されている)をローカルの複製にも反映する。これによりローカル環境のコードも最新の内容になるので、改めてステップ2の作業を行う。
  ローカルリポジトリ(Aさん)
           ver1.0a1      ver1.0a2          ver1.1a1
       修正--(git commit)--修正--(git commit)      修正--(git commit)
      /git clone              \git pushgit 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違反が発生すると、大きな風評被害による損害をもたらす場合がある。

最近のライセンスが関連する話題を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

上記のように分割コンパイルのためにファイルを分割すると、実行プログラムを生成するには以下のコマンドを実行する必要がある。

  1. gcc -c list.c        (list.o を作る)
  2. gcc -c main.c     (main.o を作る)
  3. 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

エディタが起動すると、以下のような画面となる。

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つの設定を行う。

  1. シェルスクリプトの先頭行に 実行させる shell の名前の前に “#!” をつける。
    この行は、通称”シバン shebang (シェバン)“と呼ばれ、bashで実行させたいのなら”#!/bin/bash“、プログラミング言語 Perl で実行させたいのなら “#!/usr/bin/perl” とか、Python で実行させたいのなら、”#!/usr/bin/python” のようにすればいい。(今回のサンプルはすでに記入済み)
  2. 保存したスクリプトに対して、実行権限を与える。
    “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. といったアクセスが混ざることになるが、これを見逃すとシステムに不正侵入を受ける可能性もある。

  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 と パスワードを特定の人にメールするようにしてあれば、アカウント乗っ取りが可能となる。

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー