1. ホーム
  2. Qt

GNU bashの実装機構とソースコード解析

2022-02-27 12:21:04


GNU bashの実装機構とソースコードのスケッチ


目次

1. 概要
1.1. bash 1.2. 環境とツール
2. プログラム構造解析
2.1. システム・アーキテクチャ 2.2. 主要なデータ構造
2.2.1 WORD_DESC と WORD_LIST 2.2.2. COMMAND 2.2.3. REDIRECT および REDIRECTEE 2.2.4. VAR_CONTEXT と SHELL_VAR
3. 主文書分析
3.1. ルートディレクトリ 3.2. その他のディレクトリー
4. メインプロセス分析
4.1 コマンドの構文解析と実行 4.2. リダイレクトの実装 4.3 内部コマンド(ビルトイン)構築 4.4 環境変数とコンテキスト 4.5 sshdからのbashの起動 4.6. サブシェル
5. 雑記
5.1. bashのプログラミングスタイル
A. 学習上の注意点(Q&A) B. 参考文献 <スパン C. 著者情報
林 建

概要

この記事は、シェルの実装の仕組みを学びながら、GNU bashのソースコードを解析していく中で、私がまとめたメモ書きである。bashのソースコードを解析することで、その主要な機能モジュールがどのように構成され、実装されているかを、いくつかの具体的なワークフローを示しながら解説しています。

第1章 概要

目次

1.1. bash 1.2. 環境とツール

1.1. bash

GNU bash はあらゆる種類の UNIX システム、特に Linux のための古典的なシェルです。コマンドラインインタプリタとして、オペレーティングシステムの機能への優れたインタフェースをユーザに提供する、強力なプログラム可能な機能を提供します。GNU bash はコマンドラインインタプリタとして、強力なプログラム機能を提供し、ユーザにオペレーティングシステムの機能の優れたインタフェースを提供します。古典的なオープンソースプロジェクトとして、比較的明確なソースコード構造を持ち、信頼性、性能、使いやすさがテストされています。

この記事ではbashバージョン3.2.0(1)を解析していますが、ソースコードの一部はconfigure時にヘルパーツールによって生成されているため、configure後のバージョンとなります。builtinsディレクトリにある*.cファイルはmake後のバージョンです(builtins/Makefileの*.cファイルをコメントアウトしてbuilds/Makefileの記述を削除する必要があります)。これは、このソースコードを生成するヘルパーは、make処理中に生成する必要があるためです。

1.2. 環境とツール

プロジェクトのconfigureおよびmake環境は、Ubuntu 7.10 (Linux 2.6.22), Intel Pentium IIIです。

ソースコード解析ツールはSource Insight 3.5 for Windowsで、Source Insightはクロスリファレンスとコールチェーン解析により、複雑な関数呼び出しや依存関係を整理することができます。

第2章 プログラム構造解析

目次

<スパン 2.1. システム・アーキテクチャ 2.2. 主なデータ構造
2.2.1. WORD_DESC と WORD_LIST 2.2.2. コマンド 2.2.3. REDIRECT および REDIRECTEE 2.2.4. VAR_CONTEXTとSHELL_VAR

2.1. システム構成

一般にシェルの機能は,ターミナルなどからコマンドラインを受け取り,それを操作命令に解析し,システムカーネルや適切な外部プログラムを呼び出して実行し,その結果をターミナルなどの出力に返すことである.このように、簡単なシェルを実装することは容易である。しかし、bashはこれに限らず、パイプやリダイレクトによるコマンドの同時実行をサポートし、強力なスクリプト機能を提供し、ジョブ管理機能も備えている。一般的なLinuxディストリビューションでは、bashの実行ファイルは/binにある数少ない大きなユーティリティであることが多いが、これはその複雑さを客観的に反映している。

bashの基本アーキテクチャは、そのソースコードのファイル構成と関数呼び出しの関係から分析することができる。

図2.1. bashの基本アーキテクチャ図

<イグ

bash は GNU Readline ライブラリを使用してユーザコマンドの入力を処理します。Readlineはviやemacsに似た行編集機能を提供します。

bashランタイムのスケジューリングの中心はマスターループである。マスターループは比較的単純な機能で、ユーザー(またはスクリプト)入力をループして構文パーサーに渡し、低レベルの再帰処理で返されたエラーを処理します。

構文解析は、まずテキスト入力に対してワイルドカード、エイリアス、算術、変数展開を行い、次にコマンド生成器に渡して、コマンド実行器で実行するために特別なリダイレクト処理機構によってリダイレクトセマンティクスが充填された正規のコマンド構造を取得します。コマンドエグゼキュータは、コマンドの種類に応じて、内部コマンド関数、外部プログラム、ファイルシステムコールを実行する。コマンドの実行中、エグゼキュータはシステム・シグナルを捕捉し処理する。

ジョブ管理をサポートするオペレーティング・システムでは、コマンド・エグゼキュータがジョブ制御機構にプロセス情報を追加し、ユーザーが内部コマンドやキーボード信号を使ってジョブを開始および停止できるようにします。ジョブ管理をサポートしていないOSでbashをコンパイルした場合、プロセス情報の簡単なメンテナンスには同じ機構を持つ別のインタフェースを使用することになります。

2.2. 主なデータ構造

2.2.1. WORD_DESCとWORD_LIST

/* A structure which represents a word. */
typedef struct word_desc {
  char *word; /* Zero terminated string. */
  int flags; /* Flags associated with this word. */
} WORD_DESC;

/* A linked list of words. */
typedef struct word_list {
  struct word_list *next;
  WORD_DESC *word;
} WORD_LIST;

bashはWORD_DESCやWORD_LIST構造体を使って、さらにセマンティックな処理が必要な文字列をカプセル化することができる。

WORD_DESC 構造体は、文字ポインタをラップする層であり、文字列記述子と呼ばれることがある。WORD_DESC 構造体は、フラグビットのコレクションを文字ポインタに付加します。フラグビットには、W_HASDOLLAR、W_QUOTED、W_DQUOTE、W_GLOBEXPなどがあり、それぞれ文字列に"$"、引用符、ダブルクォート、ワイルドカードなどが含まれることを示し、変数置換やワイルドカード展開などの際に該当する文字列を扱いやすくするために用意されているものです。

WORD_LIST 構造体は、WORD_DESC オブジェクトの連鎖である。

2.2.2. コマンド

/* What a command looks like.
typedef struct command {
  enum command_type type; /* FOR CASE WHILE IF CONNECTION or SIMPLE. */
  int flags; /* Flags controlling execution environment. */
  int line; /* line number the command starts on */
  REDIRECT *redirects; /* Special redirects for FOR CASE, etc. */
  union {
    struct for_com *For;
    struct case_com *Case;
    struct while_com *While;
    struct if_com *If;
    struct connection *Connection;
    struct simple_com *Simple;
    struct function_def *Function_def;
    struct group_com *Group;
#if defined (SELECT_COMMAND)
    struct select_com *Select;
#endif
#if defined (DPAREN_ARITHMETIC)
    struct arith_com *Arith;
#endif
#if defined (COND_COMMAND)
    struct cond_com *Cond;
#endif
#if defined (ARITH_FOR_COMMAND)
    struct arith_for_com *ArithFor;
#endif
    struct subshell_com *Subshell;
  } value;
} COMMAND;

COMMAND 構成は、bash コマンドを記述します。quot;command" の概念は、構文パーサーが区切り文字、パイプ、制御文を通して解析する比較的独立した実行単位を指し、内部または外部のコマンド、関数、制御構造、算術式などが含まれます。

列挙型パラメータは、コマンドオブジェクトが表現するコマンドが上記のどのタイプに属するかを示す。整数変数flagsは、以下のような実行環境に関するフラグビットの集合を記録する。

/* Possible values for command->flags. */
#define CMD_WANT_SUBSHELL 0x01 /* User wants a subshell: ( command ) */
#define CMD_FORCE_SUBSHELL 0x02 /* Shell needs to force a subshell. */
#define CMD_INVERT_RETURN 0x04 /* Invert the exit value. */
#define CMD_IGNORE_RETURN 0x08 /* Ignore the exit value. for set -e. */
#define CMD_NO_FUNCTIONS 0x10 /* Ignore functions during command lookup. */
#define CMD_INHIBIT_EXPANSION 0x20 /* Do not expand the command words. */
#define CMD_NO_FORK 0x40 /* Don't fork; just call execve */
#define CMD_TIME_PIPELINE 0x80 /* Time a pipeline */
#define CMD_TIME_POSIX 0x100 /* time -p; use POSIX.2 time output spec.
#define CMD_AMPERSAND 0x200 /* command & */
#define CMD_STDIN_REDIR 0x400 /* async command needs implicit </dev/null */
#define CMD_COMMAND_BUILTIN 0x0800 /* command executed by `command' builtin */

整数変数 line は、このコマンドがスクリプトの中で何行目にあるかを示す。ポインタ redirects は、このコマンドのリダイレクト情報を記述した REDIRECT 構造体オブジェクトを指します。最後に、異なるコマンドタイプのために、COMMAND構造体は、異なるコマンドタイプのための特定の内部構造体のユニオンを含んでいます。

内部コマンド、外部コマンドには、主にコマンド名とコマンドラインパラメータを記録する、ユニオンのsimple_com構造体を使用します。これらは、WORD_LIST チェーンテーブル構造体に格納され、テーブル要素ノードは、WORD_DESC 構造体である。

分岐やループなどの制御構造については、その内部構造には主に当該制御フローに対応するコマンドへのポインタが含まれる。例えば、while_com構造体はテスト条件とループ本体に対応するコマンドへのポインタを含み、if_com構造体はテスト条件、真値実行体、偽値実行体に対応するコマンドへのポインタを含む。

関数定義の場合、function_def 構造体は、関数名を表す WORD_DESC 構造体へのポインタ、関数に対応するコマンドへのポインタ、関数が指定されているファイル名へのポインタからなる。

2.2.3. REDIRECTとREDIRECTEE

/* Structure describing a redirection.
   (or translator in redir.c) encountered an out-of-range file descriptor. */
typedef struct redirect {
  struct redirect *next; /* Next element, or NULL. */
  int redirector; /* Descriptor to be redirected. */
  int flags; /* Flag value for `open'. */
  enum r_instruction instruction; /* What to do with the information. */
  REDIRECTEE redirectee; /* File descriptor or filename */
  char *here_doc_eof; /* The word that appeared in <<foo. */
} REDIRECT;

REDIRECT構造体は、コマンドの入出力リダイレクトを定義するものです。1つのコマンドに対して複数の(入力、出力、エラー)リダイレクトを設定できるので、REDIRECT構造体自体に次のREDIRECTオブジェクトへのポインタがあり、コマンドに対してリダイレクトの連鎖を形成することができます。

整数のパラメータ redirector はリダイレクト元のファイルディスクリプタであり、フラグビットのセット flags はターゲットファイルのオープン方法を定義する。命令列挙はリダイレクトの具体的なタイプを定義し、それらは以下のものを含む。

/* Instructions describing what kind of thing to do for a redirection. */
enum r_instruction {
  r_output_direction, r_input_direction, r_inputa_direction,
  r_appending_to, r_reading_until, r_reading_string,
  r_duplicating_input, r_duplicating_output, r_deblank_reading_until,
  r_close_this, r_err_and_out, r_input_output, r_output_force,
  r_duplicating_input_word, r_duplicating_output_word,
  r_move_input, r_move_output, r_move_input_word, r_move_output_word
};

リダイレクトの対象はREDIRECTEEユニオンに記録され、ファイル名やファイルディスクリプタにすることができる。定義は以下の通りである。

/* What a redirection descriptor looks like. If the redirection instruction
   If the redirection instruction is ri_duplicating_input or ri_duplicating_output, use DEST, otherwise use the file in FILENAME.
   Out-of-range descriptors are identified by a
   negative DEST. */

typedef union {
  int dest; /* Place to redirect REDIRECTOR to, or ... */
  WORD_DESC *filename; /* filename to redirect to. */
} REDIRECTEE;

また、Here Document 型リダイレクションの場合、REDIRECT 構造体の here_doc_eof ポインタは Here Document を指す。

2.2.4. VAR_CONTEXTとSHELL_VAR

bash自体のシェル変数と、その中で実行される関数のローカル変数のコンテキストは、VAR_CONTEXT構造体に格納されています。

/* A variable context. */
typedef struct var_context {
  char *name; /* empty or NULL means global context */
  int scope; /* 0 means global context */
  int flags;
  struct var_context *up; /* previous function calls */
  struct var_context *down; /* down towards global context */
  HASH_TABLE *table; /* variables at this scope */
} VAR_CONTEXT;

VAR_CONTEXT の文字ポインタ name は,空の場合は bash のグローバルコンテキストを格納することを示し,それ以外の場合は関数のローカルコンテキストを示し,name はその関数名を指す.整数変数 scope は,このコンテキストのスタック上の階層数であり,0 はグローバルコンテキストを意味し,関数呼び出しのスコープが深くなるごとに 1 ずつ増加し,コンテキストのスコープが反映される.フラグのセットは,コンテキストがローカルか,関数に属しているか,内部コマンドに属しているか,一時的に作成されていないかを記録します.上下のポインタは,関数呼び出しスタック内の前と次のローカルコンテキストを指します.ハッシュテーブルの内容は、そのコンテキストの変数名と値の組である。

bashの変数は型を強調しないので、文字列と考えることができます。その格納構造は次の通りである。

typedef struct variable {
  char *name; /* Symbol that the user types. */
  char *value; /* Value that is returned. */
  char *exportstr; /* String for the environment. */
  sh_var_value_func_t *dynamic_value; /* Function called to return a `dynamic'
				   value for a variable, like $SECONDS
				   or $RANDOM. */
  sh_var_assign_func_t *assign_func; /* Function called when this `special
				   variable' is assigned a value in
				   bind_variable. */
  int attributes; /* export, readonly, array, invisible... */ */
  int context; /* Which context this variable belongs to. */
} SHELL_VAR;

文字ポインタの name と value は、それぞれコンテキスト変数の名前と値の文字列を指します。export環境変数の場合、exportstrは"name=value"のような形の文字列を指します。RANDOMのように動的に変化する値を返す変数については、関数ポインタ dynamic_value がその値を生成した関数を指す。特定の変数については、assign_func へのポインタにより、値を代入する際のコールバック関数を設定することができる。整数型変数属性は、コンテキスト変数がエクスポートされているか、読み取り専用か、非表示かなどのアクセシビリティを記録する。integer variable context は、コンテキスト変数がアクセス可能なスコープ内のローカル変数スタックのどのレベルに属しているかを記録します。

第3章 主要文書の分析

目次

3.1. ルートディレクトリ 3.2. その他のディレクトリ

3.1. ルート

  • shell.c/shell.h

    shell.c には main() 関数があり、シェルの起動時および実行時の状態をある程度定義し、さまざまな起動パラメータや環境変数などに基づいてシェルの作業状態(制限モードなどを含む)を初期化し、eval.c の対話型 reader_loop() 関数に入り、終了するまでコマンドをパースしています。

    初期化関数 shell_initialize() は variables.c の initialize_shell_variables() や set.c の initialize_shell_options() など一連のサブモジュール初期化関数を呼び出します。新しい機能モジュールを追加したい場合は、ここにその初期化呼び出しを記述してください。

    run_startup_files() 関数は ~/.profile, ~/.bash_profile, ~/.bash_login 設定ファイルを実行し、bash が sshd と rshd のどちらで起動されたかを判断します。ログインシェルでは ~/.bashrc は実行されません。非ログイン対話型シェル、または sshd や rshd によって起動されたシェルでは ~/.bashrc が実行されます。

    run_one_command() 関数は、-c 引数を指定してコマンドを実行するモードを処理します。

    open_shell_script()関数は、スクリプトファイルを実行するモードを扱います。

    終了関数exit_shell()は、ジョブのハングアップや履歴の保存などのアフターケアに使用します。

  • eval.c

    シェルコマンドの実行を読み取り、解釈する。メインループは reader_loop() 関数で、 read_command() が構文解析器 y.tab.c 内の parse_command() を呼び、 parse_command() が yyparse() を呼び出してコマンドを取得した後、 reader_loop() が execute_cmd.c の execute_command() を呼び出してコマンドの実行を行います。

    注:トークン検索の優先順位:alias> keyword> function> internal command> script or executable.

  • execute_cmd.c/execute_cmd.h

    コマンドを実行する(COMMAND構造体)。外部呼び出しインターフェースはexecute_command()で、内部ではexecute_command_internal()でコマンドが実行される。execute_command_internal()にはオプションでパイプラインリダイレクトとバックグラウンド動作用のパラメータが含まれる。

    異なるタイプのコマンド(制御構造、関数、演算など)に対して、execute_command_internal()は適切な機能を実現するために異なる関数を呼び出す。execute_builtin() は内部コマンドを実行し、 execute_disk_command() は外部ファイルを実行します。 execute_disk_command() は jobs.c または nojobs.c で make_child() を呼び出して新しいプロセスをフォークします。

    ファイルディスクリプタのビットマップは、このファイルで管理されます。

  • make_cmd.c/make_cmd.h

    様々なコマンド、リダイレクト、その他の構文構造の例を構築するために必要な関数です。シンタックスパーサー、redir.c などから呼び出されます。

    make_redirection()は、コマンド構造体のリダイレクトパラメータを記入するところ。

  • copy_command.c

    様々なCOMMAND構造体を再帰的にコピーするために使用される一連の関数。

    作者注:これは主に関数定義を作るために必要ですが、他の人が必要とすることはないでしょう。

  • dispose_command.c/dispose_command.h

    COMMAND 構造体によって占有されているリソースをクリーンアップし、 dispose_redirects() によってリダイレクト文をクリーンアップしています。

  • print_cmd.c

    コマンド構造体を印刷可能な文字列に変換します。execute_cmd.c の execute_command_internal() で呼び出される。

  • redir.c/redir.h

    入出力リダイレクトの実装。コマンド構造体の redirects パラメータは、実行前に make_cmd.c によって埋められ、主に execute_cmd.c によって外部から呼び出されてリダイレクション処理が実行される。

    外部インタフェースは do_redirections() で、コマンド構造体を解析してリダイレクトパラメータを求め、内部的には do_redirection_internal() に委ねる。そして、リダイレクトの種類に応じて、redir_open() が通常の open(), redir_special_open(), noclobber_open() 関数を用いてリダイレクト先のファイル記述子をそれぞれオープンする。新しいリダイレクト方法 (例: FTP へのリダイレクト) を追加するには、ここにコードを追加することを検討してください。

    リダイレクトの原則については 参照 Unix/Linuxプログラミングチュートリアル実践編」の10.3項をご覧ください。

  • paser.y

    yaccの構文定義ファイルです。

  • y.tab.c/y.tab.h

    yaccが生成する構文解析器。

    トークンを解析し、make_cmd.c の関数を呼び出し、 execute_cmd.c の関数の実行を容易にするためのコマンド構造を生成する。

    これには、コマンド構造体のリダイレクトパラメータを記入するための make_redirection() の呼び出しが含まれる。

  • alias.c/alias.h

    追加、削除、確認、変更など、エイリアス操作関連関数。内部コマンドのエイリアスは、このファイルにある関数を呼び出すことで実装されます。

  • array.c/array.h/arrayfunc.c/arrayfunc.c

    文字列配列の定義と、配列に対するいくつかの高度な操作を実装した関連関数です。bashプログラムの文字列配列の中には、ここで定義されたARRAY構造体を使用するものがあります。

  • bashansi.h

    コンパイラによって異なるシステムヘッダーファイルのインクルード関係を処理します。

  • bashhist.c/bashhist.h

    コマンドヒストリー機能に関する機能(ヒストリーの開始、停止、インクリメント、チェックなど)です。

  • bashintl.h

    locate.h、gettext.hなどの国際化対応を導入する。

  • bashjmp.h

    setjmp.hをラップして、longjmp()の状態パラメータをいくつか定義しています。

  • bashline.c/bashline.h

    コマンドの自動補完、emacsやviのような行編集機能などに対応するためのreadlineライブラリとのインターフェイスです。

  • bashtypes.h

    単語の種類を定義する。

  • bracecomp.c/braces.c

    braces ワイルドカードファイル名機能を使用する関数です。

  • buildins.h

    内部コマンド構造体の基本構造を定義しています。

  • command.h

    各種コマンドの構造体定義(制御構造、関数、演算処理、リダイレクトなど)。

  • config.h/config-top.h/config-bot.h

    config.h は configure によって生成され、どの機能を bash にコンパイルするかを決定します。新しい機能を追加したい場合は、 "switch" マクロ定義を追加します。

  • conftypes.h

    ホストアーキテクチャとオペレーティングシステムのタイプの名前を定義します。

  • error.c/error.h

    エラー処理とレポート機能。

  • expr.c

    算術式を処理する。外部呼び出しインタフェースはevalexp()です。

  • externs.h

    独自のヘッダーファイルで宣言されていないいくつかの関数をソースファイル内で宣言します。

  • findcmd.c/findcmd.h

    コマンド名で検索します。主にPATH変数の位置から外部の実行プログラムを探す。

  • flags.c/flags.h

    標準のShフラグや非標準のフラグなど、個々のランタイムフラグを格納し、処理する。

  • general.c/general.h

    多くのファイルに共通するような、基本的で不便なカテゴリの関数をいくつか紹介します。

  • hashcmd.c/hashcmd.h

    ハッシュテーブルを管理します。主にコマンド名とフルパスの対応付けに使用します。

  • hashlib.c/hashlib.h

    ハッシュテーブルのデータ構造です。

  • input.c/input.h

    入力ストリームのバッファリングを処理する。

  • jobs.c/jobs.h

    ジョブの制御です。主なエントリポイントはmake_child()で、プロセスの作成と実行に使用されます。

    jobs、fg、bg、killなどのコマンドの内部実装はこちらです。

    ジョブ管理の詳細なプロセスについては、まだ解析されていません。

  • nojobs.c

    ジョブ制御を実装していないOS上で、jobs.cの代わりにコンパイルされます。

  • リスト.c

    リンクされたテーブルのデータ構造。

  • ロケール.c

    環境変数 "LC_" ファミリーの操作を含む、国際化に関連する関数です。

  • lsignames.h/signames.h

    signames.h は、コンパイル時のヘルパーである mksignames によって生成され、そのソースコードは support サブディレクトリにあります。

  • mailcheck.c/mailcheck.h

    アカウントのメールボックスをチェックする関数です。

  • mksyntax.c

    mksyntax は字句解析ファイル syntax.c の生成に使用されます。

  • syntax.c/syntax.h

    syntax.cはmksyntaxが生成する字句解析ファイル、syntax.hは字句解析作業に必要なマクロやフラグビットなどを定義しています。

  • パーサー h

    parse.y と bashhist.c で必要となるデリミタスタック構造体 (struct dstack) の定義です。

  • パッチレベル.h

    bashの修正バージョン番号を記録する。

  • pathexp.c/pathexp.h

    ワイルドカード(グロビング)ライブラリへのインターフェイス。

  • pathnames.h

    いくつかのオペレーティングシステム設定ファイルのパスを記録します。

  • pcomplete.c/pcomplete.h/pcomplib.c

    プログラマブルコマンド補完機能。

  • quit.h

    SIGINT信号に対する応答である汎用例外終了マクロを定義しています。

  • sig.c/sig.h/siglist.c/siglist.h

    信号処理関連の関数です。

  • stringlib.c

    文字列処理関連関数。文字列-整数キー-値ペア構造(ALIST)からデータ項目を検索する機能などが含まれます。

  • サブスト.c/サブスト.h

    パラメータ、コマンドの置換・展開、演算、パス展開、引用符などを担当します。

  • test.c/test.h

    GNUテストプログラム、様々な条件比較項目、一般的にシェルスクリプトで使用されます。

  • トラップ.c

    trapコマンドを操作するために必要ないくつかのオブジェクトの関数です。

  • unwind_prot.c/unwind_prot.h

    汎用的な関数実行の保護と終了処理機構。

  • 変数.c/変数.h

    シェル変数を扱う。シェル変数と関数はそれぞれライフサイクルの異なるハッシュテーブルを使用して格納します。

    変数のリストは現在の環境によって初期化されます。bashはmain()のchar **env引数で渡された環境から開始します。

    関数の場合、スタックはローカル変数のコンテキストの保持と切り替えに使用されます。

  • version.c/version.h

    bashのバージョン番号を表示します。

  • xmalloc.c/xmalloc.h

    セキュア版 malloc ラッパー。

3.2. その他のディレクトリ

  • ビルトイン

    このディレクトリには、内部コマンドのソースコードが含まれています。

    各内部コマンドはdefファイルであり、MakefileのDEFSRCはすべての内部コマンドのdefファイルを宣言しています。

    mkbuiltins は *.def ファイルを処理してコマンドの *.c ソースプログラムを生成し、 builins.c, builtext.h を生成する。builins.c と builtext.h はそれぞれの内部コマンドのインデックスと等価である。

    最後にすべてのファイルをコンパイルして、libbuiltins.aを得る。

    この例 テストは内部コマンドを追加します。

  • クロスビルド

    このディレクトリのファイルは、他のシステムでのクロスビルドのためにキャッシュされる設定結果です。

  • CWRU

    CWRUのものと思われる未解析の雑多なファイル。

  • ドクター

    テ・ドックス

  • スクリプトの例(拡張bashの検証に使用できる)。

  • インクルード/ライブラリ

    bashに必要なヘッダ、ライブラリファイル(ソースコード)です。

  • 国際化のための言語定義ファイル。

  • サポート

    コンパイル時に必要なサポートツールとそのソースコードです。

  • テスト

    makeテストに使用するテストスクリプトで、拡張bashの検証に使用することができます。

第4章 主要工程分析

目次

4.1. コマンドの構文解析と実行 4.2. リダイレクトの実装 4.3. 内部コマンド(組み込み)構築 4.4. 環境変数とコンテキスト 4.5. sshdからのbashの起動 4.6. サブシェル

4.1. コマンドの構文解析と実行

コマンドのパースと実行の外部ビューを 参照 の記事で、「Bashのコマンドライン処理を詳しく説明する"」とあります。

bashが起動し初期化されると、eval.cの対話型ループ関数reader_loop()に入り、コマンドのパースを開始します。reader_loop()はEOFに遭遇するまでループを繰り返し、コマンドを実行し続けます。

read_command() は構文解析器 y.tab.c の yyparse() を呼び出し、最終的にコマンドを取り込みます。

read_command()には、シェルのアイドル時間後に自動ログアウトする機能(環境変数TMOUT)が追加されています。

parse_command() の追加作業は、PROMPT_COMMAND で指定されたコマンドを実行し、ここの文書を処理する関数を呼び出すことである。

yyparse()は、parse.yを介してyaccによって生成されます。コマンド構文を解析し、make_cmd.c内の様々な関数を呼び出して実行用の異なるCOMMAND構造体のオブジェクトを生成します。

コマンドを読み込んだ reader_loop() は execute_cmd.c の execute_command() を呼び出してコマンドを実行する。異なるタイプのコマンド(制御構造、関数、演算、リダイレクションなど)に対して、execute_command_internal()は、対応する機能を実現するために異なる関数を呼び出す。execute_builtin() は内部コマンドを実行し、execute_disk_command() は外部ファイルを実行します。 execute_disk_command() は jobs.c または nojobs.c で make_child() を呼び出し、新しいプロセスをフォークします。

make_child() は、入力ストリーム・バッファを同期させた後、新しいプロセスをフォークします。jobs.c バージョンの make_child() では、ジョブの初期化を行い、add_process() 関数で保留中のコマンドをプロセス開始チェーンに追加します。

make_child()から戻った後、execute_disk_command()でpidを判別し、子プロセスであればshell_execve()関数を呼び出し、エラー処理をしつつ、その関数で対象のコマンドを実行(exec)しています。

ソースプログラムでは、execute_disk_command()について、以下のようにコメントしています。

/* Execute a simple command that is hopefully defined in a disk file
   somewhere.
   1) fork ()
   2) connect pipes
   3) look up the command
   4) do redirections
   5) execve ()
   6) If the execve failed, see if the file has executable mode set.
   If so, and it isn't a directory, then execute its contents as
   If so, and it isn't a directory, then execute its contents as a shell script.
   Note that the filename hashing stuff has to take place up here,
   This is probably why the Bourne style shells
   This is probably why the Bourne style shells don't handle it, since that would require them to go through
   This is probably why the Bourne style shells don't handle it, since that would require them to go through this gnarly hair, for no good reason.
   NOTE: callers expect this to fork or exit(). */

4.2. リダイレクトの実装

COMMAND 構造体は、このコマンドのリダイレクト情報を指す REDIRECT (redirects) 型のポインタを持つ。

REDIRECT構造体は、リダイレクト元ディスクリプタとリダイレクト先:redirectee(REDIRECTEE型)を記録し、これはターゲットディスクリプタまたはターゲットファイル名のどちらかを指定できるユニオン型である。REDIRECT自体は、次のREDIRECTオブジェクトへのポインタを含みます。したがって、COMMANDオブジェクトの場合、リダイレクトメッセージの連鎖があり得ます。

構文解析は、リダイレクト構文に出会ったときに make_cmd.c の make_redirection() 関数を呼び出して COMMAND 構造体の REDIRECT パラメータを埋め、リダイレクト方法を示すフラグビットをセットします。

execute_cmd.c 内の関数は、コマンド実行時に redir.c 内の do_redirections() を呼び出してリダイレクションを実装する。リダイレクト情報連鎖の各 REDIRECT オブジェクトは、個別に do_redirection_internal() に引き渡される。

do_redirection_internal() は、リダイレクトメソッドのフラグビットに特定の設定を行った後、 redir_open() を呼び出します。

redir_open() は、リダイレクト対象ごとに異なる関数を呼び出してファイル記述子のオープン操作を完了させます。例えば、フロッピードライブやネットワークデバイスファイルの場合はredir_special_open()が、ノクロバーモード(上書き禁止変数モード)の場合はredir_special_open()が、一般的にはシステムの最も使用しないファイルディスクリプタをリダイレクトで開くために通常のopen()が呼ばれる。

4.3. 内部コマンド(ビルトイン)構築

ソースコードディレクトリの下のbuiltinsディレクトリ($(srcdir)と表記)には、各内部コマンドのソースコード定義済みファイル(*.def)が格納されています。mkbuiltinsは、同じディレクトリにあるmkbuiltins.cをコンパイルして生成され、*.defファイルを処理する一方で、bashのメインプログラムが内部コマンドを呼び出すためのインタフェースとなるbuiltins.cファイルやbuiltext .hファイル、各内部コマンドのインデックスを生成しています。

新しい内部コマンドを追加するには、以下のように元のコマンドが存在する形式を参照するだけです。

1. 新規に定義済みファイルを作成します。既存のコマンドの定義ファイルをコピーして、$PRODUCES, $BUILTIN, $FUNCTION, $SHORT_DOCなどの定義をコマンド名に合わせて変更することができます。

2. 2. 定義済みファイルにコマンド処理関数を作成する。プロトタイプは既存のコマンド処理関数を参照し、関数名は$FUNCTIONの定義と一致させる。パラメータは、$(srcdir)/command.hで定義されているWORD_LIST *listです。ここでも、引数の扱い方の詳細は、既存のコマンドのハンドラ関数(echoなど)を参照することができます。

3. 3. $(srcdir)/builtins/Makefile.in を修正して、DEFSRC と OFILES にそれぞれ [コマンド名].def と [コマンド名].o の定義を追加し、既存のコマンドを参考に [コマンド名].def や他のヘッダーファイルへの依存性を追加してください。

4. 4. $(srcdir) に戻り、ソースコードを configure して make すると、うまくいけば、出来上がった bash プログラムには、新しく追加された内部コマンドが含まれるようになります。

例 4.1. 新しいquot;linjian"コマンドの作成

この例で追加されたコマンド処理関数は

int linjian_builtin (list)
     WORD_LIST *list;
{
  printf ("This is a built-in for test by Lin Jian.\n");
  if (list)
    printf ("Parameter: %s\n", list->word->word);
  return (EXECUTION_SUCCESS);
}

コンパイルされたテストの結果は以下の通りです。

# working under the original bash:.
lj@lj-laptop:~/bash-3.2$ ps
  PID TTY TIME CMD
 6212 pts/2 00:00:00 bash
 9893 pts/2 00:00:00 ps
lj@lj-laptop:~/bash-3.2$ linjian
-bash: linjian: command not found 
#Go to the modified bash.
lj@lj-laptop:~/bash-3.2$ . /bash
lj@lj-laptop:~/bash-3.2$ ps
  PID TTY TIME CMD
 6212 pts/2 00:00:00 bash
 9904 pts/2 00:00:00 bash
 9922 pts/2 00:00:00 ps
lj@lj-laptop:~/bash-3.2$ linjian hello!
This is a built-in for test by Lin Jian.
Parameter: hello!
lj@lj-laptop:~/bash-3.2$ type linjian
linjian is a shell builtin


4.4. 環境変数とコンテキスト

Linux では、各プロセスは独自の環境 (main 関数の char *env[] 引数が指す) を持っており、これはプロセスが参照する必要のあるコンテキスト情報を保持する変数のセットで構成されています。bash は環境変数のコピーを variables の shell_variables というグローバル VAR_context 構造に格納します。 c. variables.c の CONTEXT 構造体 子プロセスにエクスポートされる変数は、グローバル文字列ポインタ char **export_env によって "name=value" 文字列の配列として記録され、これは export コマンドをタイプしたときに表示されるものである。

bash は起動時に variables.c の initialize_shell_variables() 関数を呼び出して main 関数から env 引数を渡し、 env の環境変数を shell_variables に格納して PATH, IFS, PS1 など bash 自体が使用する環境変数を指定します。envにまだ入っていなければ、この時点で作成されます。その他、bashのバージョン管理、コマンドヒストリー、メールチェック、その他の内部ヘルパーのための環境変数もここで作成されます。

execute_cmd.c 内の各種コマンドを呼び出す関数は、まず variables.c 内の maybe_make_export_env() 関数を呼び出して、子プロセスにエクスポートする環境、すなわち export_env を構築してからコマンドを実行します。 shell_execve() は execve() 関数を用いて外部コマンドを実行するので、bash が起動する子プロセスには export_env を渡せばいいわけですね。

環境変数を追加・変更する必要がある場合は、variables.c の bind_variable() 関数を呼び出して、環境変数を追加・変更します。例えば、PWDはcdコマンドを実行した後にリセットする必要があります。

4.5. sshdからbashを起動する

bash が起動するとき、shell.c の run_startup_files() は変数 run_by_ssh に記録されている SSH_CLIENT と SSH2_CLIENT 環境変数の存在を見て sshd から起動されたかどうかを判断します。また、stdinのファイル記述子がネットワークファイルやソケットにリダイレクトされているかどうかで、bashがrshdによって起動されたかどうかを判断することも可能です。bash が sshd または rshd から起動され、(サブシェルではなく) トップレベルのシェルである場合、~/.bashrc スクリプトが実行されます。

~/.bashrcが複数回実行されるのを防ぐには、bashがトップレベルのシェルのときだけロードすることです。作者は、親シェルが sshd によって起動されたことを子シェルが知っていて ~/.bashrc を実行しないように、 initialize_shell_variables() で SSH_CLIENT, SSH2_CLIENT 環境変数を non-export に設定しようとしましたが、結局この方法は断念しています。

それ以外には、bashはsshについて特別な扱いはしていません。

4.6. サブシェル

シェルはサブシェルと呼ばれるシェルサブプロセスを起動します。ファイル名直下に実行ファイルを実行する場合、bashは実行ファイルをバイナリとして呼んでいるのかスクリプトとして呼んでいるのかわからず、そのままシステムカーネルに任せて実行プロセスで処理されます。シェルスクリプトの場合、通常は "#! [シェル実行ファイル名]" で始まり、 "#!" はマジックナンバーです。カーネルは、マジックナンバーによってスクリプトが実行されていると判断すると、スクリプトを解釈するために指定されたシェルの新しいインスタンスを呼び出します(これが子シェルです)。親シェルが自身の変数を環境にエクスポートしない限り、子シェルは親シェルで定義された変数にアクセスすることはできない。

システムログイン後に起動したbashのSHLVLは1であり、各レベルのSHLVLはシェルによって起動されたサブシェルのSHLVLがその環境で読み込まれたSHLVLに追加されます。

sourceコマンド("."コマンド)を使って、サブシェルを開かずにスクリプトを実行する。bashの内部実装では、スクリプトファイルの内容をバッファに読み込んでから構文解析を行っているので、キーボードから直接スクリプトの内容を入力したのと同じ効果が得られる。

第5章 その他

目次

5.1. bashのプログラミングスタイル

5.1. bashのプログラミングスタイル

bash の C 関数宣言には "__P" マクロが使用されており、K&R 仕様に従って定義されているので、古い非 ANSI コンパイラでもコンパイルできます。bash ソースコードは、異なる CPU アーキテクチャ、異なる OS、異なるコンパイラ間の差異を十分に考慮して、マクロと条件付きコンパイルを使用してその問題を処理し、コードを複雑化せずに移植性を高めています。コンパイル可能なオプション機能については、コンパイラが明示的にソースコードを変更しなくても、マクロをスイッチとして使用し、ユーザパラメータで設定することができます。

bashのコードのインデントスタイルはあまり統一されていません。おそらく、さまざまな貢献者のコードがあるためです。ほとんどのコードは一般的なJavaのスタイルとは大きく異なっており、Source Insightのようなツールがなくても簡単に見つけられるものもあります。bashはgotoを使うことを避けておらず、gotoが使われている場所のいくつかは非常に読みやすくなっています。

ほとんどの関数名は完全な動詞-目的語句なので、すべての関数に注釈があるわけではなく、名前からその機能の大まかなことがわかります。いくつかの複雑な処理については、関数内部にいくつかのコメントがあり、かなりの数が将来の貢献者に啓発を与える議論や提案の性質を持っています。ほとんどの GNU プログラムと同様に、bash にも西洋特有のユーモアを反映した生意気なコメントが多数あります。

付録A. 学習上の注意(Q&A)

  • Q: __Pとは何ですか?

    A: ANSI C以前の古いコンパイラは、関数プロトタイプの定義をサポートしていません。P"__P;マクロを使用することで、ANSIおよび非ANSIコンパイラに移植可能なソリューションが提供されます。P"__P"の実装は、通常、以下のようになります。

      # if defined(__STDC__) || defined(__GNUC__)
      # definec__P(x) x
      # else
      # define __P(x) ()
      # endif
    
    

  • Q: IFSとは何ですか?

    シェル環境変数 IFS (Internal Field Separator) に格納できる値は、スペース、TAB、改行などです。bashのデフォルトは0x0aです。構文解析や変数置換などの際にコマンドを定義するために使用されます。

  • Q:「Here Document」とは何ですか?

    ヒアドキュメントは、特定の目的を持ったコードのスニペットです。これは、I/Oリダイレクションを使用して、一連のコマンドをftp、cat、exテキストエディターなどの対話型プログラムやコマンドに渡します。たとえば、以下のようなものです。

    COMMAND <<InputComesFromHERE
    ...
    InputComesFromHERE
    
    

付録B. 参考文献

  1. メンデル・クーパー アドバンスド・バッシュ・スクリプティング・ガイド, 2006. http://personal.riverusers.com/~thegrendel/abs-guide.pdf

  2. ブルース・モレイ A hands-on tutorial on Unix/Linux programming, Beijing: 清華大学出版、2004年。

  3. エリック・S・レイモンド The Art of Unix Programming, Beijing: 電子工業出版社, 2006.

  4. HOME_KINGさん Bashのコマンドライン処理を詳しく解説, 2005. http://www.linuxsir.org/main/?q=node/134

  5. チェット・レイミー Bash FAQ バージョン 3.36, 2006. ftp://ftp.cwru.edu/pub/bash/FAQ

付録C. 著者情報

Jian Linは、北京理工大学コンピューターサイエンスとテクノロジー学部の学生です。この記事の出典を引用してください。また、本文中に不正確な点や誤りを発見された場合は、お気軽にコメントをお寄せください。

著者のホームページです。 http://www.linjian.cn

AuthorBlogです。 http://blog.linjian.cn