システムプログラム(第1回)

情報学群情報科学類
大山 恵弘

目次

  1. 講義「システムプログラム」の目的
  2. ソフトウェアの構成と「システムプログラム」
  3. プログラムの実行環境
  4. プログラムの開発環境
  5. プログラミングとデバッグ
  6. ポインタ(1)
  7. 練習問題

講義「システムプログラム」の目的

「システムプログラム」では,ユーザの立場から計算機システムやオペレーティングシステムをより深く理解し,活用できるようになるためのプログラミングスキルの習得を目的とする.

オペレーティングシステム(OS: Operating System)とは

OS はハードウェアの違いを隠蔽し,ユーザがプログラムを実行するための使いやすい環境を提供する. 例えば,デスクトップ PC,ノート PC,サーバ PC,タブレット,スマートフォンのハードウェア構成(プロセッサの種類や数,メモリの量,入出力デバイスなど)の違いによって,プログラムの変更が必要だとすると,プログラミングが大変である. OS は,ハードウェアを抽象化した概念(アブストラクション)を提供することで,いろいろなコンピュータで共通に使用できる実行環境を提供する.

OSが提供する主な抽象概念としては以下のものがある.

抽象概念ハードウェア機能
プロセスプロセッサ,メモリ
ファイル,ディレクトリストレージ
プロセス間通信コンピュータ間の通信
シグナル割り込み
アクセス制御コンピュータ共有時の保護

基本的な抽象概念の考え方はめったに変わることはない. 実際,上記の抽象概念は30年以上変わっていない.

OS の「内部の仕組み」については秋学期ABの「オペレーティングシステム I」という講義で扱われる. また,秋学期 AB の「分散システム」,秋学期 C の「オペレーティングシステム II」では,より高度な話題が扱われる. 内部の仕組みの前に,外から見た場合の OS の考え方,使い方を理解することが「システムプログラム」の目的である.

API(Application Program Interface)を用いたプログラミング

OS の提供する抽象概念の集合が,ユーザから見た場合のコンピュータになる. その抽象的なコンピュータを操作するためのインタフェースとして API(Application Program Interface)が提供されている. API を用いることにより,抽象化されたコンピュータを操作することができるようになる. API を通して,プログラムという実体の無いものが,OS や OS の管理するデバイスを通して外の世界とつながり,プログラムと人間のインタラクションが可能になる.

API は一般に

により提供される. API を使いこなすことにより,プログラムの生産性,信頼性,安全性,移植性を上げることができる. プログラミング言語を自然言語と対比してみると,プログラミング言語は文法,API は語彙に相当すると考えられる. API を覚えることにより,わかりやすく簡潔な表現が可能になる.

講義・演習で使用するプログラミング言語

この講義では UNIX (macOS) を用いて,POSIX (Portable Operating System Interface for UNIX) API を利用してプログラムを作成する. POSIX は C 言語により規定されており,特にシステムコールを直接呼び出す方法は,C 言語に対してのみ提供されている. 例えば Java が文字出力を行うには,最終的にはシステムコールを呼び出すしかない. システムコールを呼び出すプログラムは,Java VM の中で C 言語を用いて書かれている. 従って,この授業の講義・演習で使用するプログラミング言語は,特に断りのない限り,C 言語とする.

macOS は Apple が開発した OS である. Mach マイクロカーネルに,FreeBSD という UNIX の OS サービスを組み合わせたアーキテクチャとなっている. UNIX 部分は Darwin と呼ばれ,オープンソースであるため https://opensource.apple.com/ から入手可能である.

ソフトウェアの構成とシステムプログラム

プログラムの種類

プログラムの種類

システムプログラムでは,ライブラリやシステムコールを通してUNIXカーネルを直接的に利用する.

UNIX OS の構成要素

OS という言葉は様々な意味で使われる. 様々なプログラムから構成される OS 環境を意味する場合もあるし,OS カーネルを意味する場合もある. 以下の図は一般的な UNIX 環境を構成するプログラムを示している. macOS でも基本的な構成は同じである.

UNIX OSの構成要素

以下,ユーザの目に触れる部分から構成要素について概説する.

シェル

UNIXユーザのユーザインタフェースとして最もよく使われるプログラムがシェル(shell)である. シェルは,ユーザの視点からOSを見た場合,OSを操作するためにOSを取り囲んでいる殻という意味である. 一般的なシェルには,bsh(Bourne Shell),bash(Bourne-Again Shell),csh(C Shell),tcsh(Tenex-like C Shell),dash,zsh などがある.

シェルは CLI(Command Line Interface)を通して,ユーザからのコマンドを受け付け,解釈,実行し,その結果を出力する機能を提供する. シェルは,コマンドの入出力を,ファイルや別のコマンドにするリダイレクションやパイプといった機能を提供する. この機能により,比較的単純な機能を持つ複数のコマンドを組み合わせて複雑な機能を実現することが可能になる. シェルは,実行するコマンドを記述したプログラムであるシェルスクリプトを実行することもでき,プログラミングの素養を持つユーザには非常に強力なインタフェースを提供している.

情報システム実験 A の「システムプログラム」では,シェルを実際に作成してみる. シェルは UNIX の基本となるプログラムであり,UNIX の基本的な抽象概念の操作が必要であるため,システムプログラムの基本を学習するための題材として最適である.

X ウィンドウシステムと GUI(Graphical User Interface)

現在のユーザが使用するコンピュータのほとんどは GUI を提供している. macOS や Windows では GUI は完全に OS の一部となってしまっている.

他の一般的な UNIX では GUI は X ウィンドウシステムとウィンドウマネージャという独立したプログラムとして提供されている. X ウィンドウシステムは,ビットマップディスプレイ上にウィンドウを表示するための基本的な機能を提供するだけである. ユーザがウィンドウを操作するための GUI は,GNOME や KDE といったプログラム群(デスクトップ環境とも呼ばれる)により提供される.

コマンド,アプリケーション

コマンドとは,ユーザが(シェルを通して)コンピュータに与える命令である. UNIX は,シェルから使用することのできる非常に多くのコマンドを提供している. コマンドはシェルとは別のプログラムである場合(外部コマンドである場合)もあるし,シェルに組み込まれている場合(組み込みコマンドである場合)もある. cd, set, umask, exit といったシェル自身の状態を変更するコマンドや,history などのシェルが持つ情報を表示するコマンドは組み込みコマンドである. そうでないコマンドは,通常,外部コマンドである.

プログラムをコマンドと呼ぶ場合とアプリケーションと呼ぶ場合があるが,それらに明確な違いがあるわけではない. Office やビデオ再生プログラムのような,それだけで必要な機能を提供する自己完結的なプログラムをアプリケーションと呼ぶ場合が多い.

サーバ,デーモン

UNIX では,バックグラウンドで動作し様々なサービスを提供する裏方で働くプログラムのことをデーモン(daemon)と呼んでいたが,最近ではサーバと呼ぶことも多い. デーモンには,メイルの配信をするプログラム,プリンタへの出力要求を仲介するプログラム,リモートログインやリモートファイルコピーなどのネットワーク機能を提供するプログラムなどがある.

システムコール,ライブラリ,ミドルウェア

UNIX でプログラミングをする場合,システムコール,ライブラリ,ミドルウェアを使用してプログラムを作成する. システムコールは,OS カーネルの機能を直接呼び出すためのインタフェースである. UNIX のシステムコールは,できるだけシンプルになるように設計されている. ライブラリとミドルウェアはプログラムの部品となる関数の集合である. ライブラリとミドルウェアの違いは微妙であるが,ライブラリは様々な目的のプログラムに共通の機能を提供するものであるのに対し,ミドルウェアはより特定されたプログラム(例えば GUI)の共通部品となるものであるという見方がある. これらをうまく使うことで,開発効率,信頼性,安全性,移植性が上がり,またでき上がったプログラムも読みやすくなり,実行効率(または見栄え)も良くなる.

システムコールもライブラリも,C プログラムから呼び出す場合はどちらも関数呼び出しの形態で使用できるため,同じに見える. UNIX のマニュアルでは,システムコールは2章,ライブラリは3章に分類されている.ヘッダ部分に「read(2)」のように「(2)」と付いているものは2章に分類されるシステムコールであり,「fread(3)」のように「(3)」と付いているものは3章に分類されるライブラリ関数である.

プログラムとの関係については,プログラム,ライブラリ,システムコールの関係でより詳しく述べる.

UNIX カーネル

UNIX カーネルは,プロセッサの特権モードというハードウェアの全てを制御することのできる動作モードで動作し,直接ハードウェアを制御するプログラムである. UNIX カーネルは,プロセッサの機能を使うことで,複数プログラムの同時実行を可能にし,それぞれのプログラム(またはユーザ)がコンピュータを占有しているかのような幻想を与える. UNIX 環境で特権モードで動作するプログラムはカーネルだけである. その他のプログラムは,ユーザモードというハードウェアへのアクセスは制御された環境で動作する.

UNIX カーネルは,大まかに言ってプロセス管理,ファイルシステム,メモリ管理,ネットワーク,プロセッサ依存部,デバイスドライバからなる. ファイルシステム,ネットワークは比較的部品化されているが,全体的にお互いが関係しあって動作する大きなプログラムである.

プログラム,ライブラリ関数,システムコールの関係

ライブラリ関数とシステムコールは,入出力機能やその他にもいろいろ便利な機能を提供してくれるという点で,プログラマから見ると似ているところがある. しかし,ライブラリ関数とシステムコールには,いくつか大きな違いがある.

プログラム,ライブラリ,システムコールの関係

上の例では,read はシステムコールなので直接カーネルを呼び出している. strcmp はライブラリだけで機能が実現されているライブラリ関数である. printf はライブラリ関数であるが,標準出力に文字出力を行うために,write システムコールを使用している.

プログラムの実行環境

プログラムとプロセスの関係

一言で言えば,プログラムを実行中のものがプロセスである. 別の言い方をすると,プログラムはプロセスにより実行される. プログラムとそれを実行中のプロセスは別のものである. 従って,1つのプログラムを複数実行する(同じプログラムを実行しているプロセスを複数作る)ことができる.

プログラムは,CPUが実行できる機械語命令とそれにより処理されるデータの集合(実行形式,ロードモジュール)がファイルに格納されたものである. つまり,プログラムには何かをするためにCPUで処理を開始するための情報が入っている.
一方,プロセスには実行中の情報が入っている. 実行が進めばデータは書き換えられたり,追加されたりする(機械語命令は通常変わらない). プログラムには含まれていない,実行中の履歴を格納するためのデータ(スタック)も必要である.

プログラムの実行は通常シェルから行う. シェルのプロンプトにプログラムのファイル名を打ち込むと,そのプログラムを実行するためのプロセスが作られ,実行される. プロセスは誰か(そのプロセス自身でもよい)が終了させないと,いつまでも動いている. 終了させるためには,そのための手順を踏む必要がある(終了のためのシステムコールを呼ぶか,強制的に終了させる).

プログラムとプロセスの関係

上の例は,HDD (Hard Disk Drive) に格納されている2つのプログラムemacsとfirefoxを異なる二人のユーザが実行している例である. emacs,firefoxのプログラムはそれぞれ1つであるが,同じプログラムを実行して複数のプロセスを作ることができるため,ユーザ毎に別々のプロセスになっている.

プロセスの観察

UNIX にはプロセスを観察するためのコマンドがいくつか用意されている. ps コマンドはプロセスの状態を得るための最も標準的なコマンドであり,全ての UNIX で使用できる. pstree,top コマンドは,用意されていない UNIX システムもあるかもしれない.

ps コマンドはオプションにより様々な情報を表示することができる.

$ ps[←]
  PID TTY          TIME CMD
 7287 pts/0    00:00:00 bash
 7316 pts/0    00:00:43 emacs
17106 pts/0    00:00:27 firefox
17162 pts/0    00:00:00 dbus-launch
17263 pts/0    00:00:01 gnome-terminal
17265 pts/0    00:00:00 gnome-pty-helpe
17298 pts/0    00:00:00 ps
$ ps ux[←]
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
oyama     7286  0.0  0.0 129600  2588 ?        S    08:28   0:26 sshd: oyama@pts/0
oyama     7287  0.0  0.0 125256  2152 pts/0    Ss   08:28   0:00 -bash
oyama     7316  0.1  0.1 314212 33276 pts/0    S    08:28   0:43 emacs
oyama     7425  0.0  0.0 120672  1564 pts/2    Ss+  08:35   0:00 /bin/bash --noediting -i
oyama    17106 21.8  0.6 1094868 208512 pts/0  Sl   20:03   0:27 /usr/lib64/firefox/firefox
oyama    17162  0.0  0.0  30336   788 pts/0    S    20:04   0:00 dbus-launch --autolaunch bd35...
oyama    17163  0.0  0.0  52140  1136 ?        Ssl  20:04   0:00 /bin/dbus-daemon --fork --pri...
oyama    17166  0.1  0.0 142776  4352 ?        S    20:04   0:00 /usr/libexec/gconfd-2
oyama    17263  2.4  0.0 303520 13428 pts/0    Sl   20:05   0:01 gnome-terminal
oyama    17265  0.0  0.0  22748   852 pts/0    S    20:05   0:00 gnome-pty-helper
oyama    17266  0.0  0.0 120804  1844 pts/3    Ss+  20:05   0:00 bash
oyama    17299  1.0  0.0 122696  1280 pts/0    R+   20:06   0:00 ps ux
$ 

top はたくさん CPU 時間を使用しているプロセスの状態を一定時間おきに表示する. CPU使用率やメモリ使用量などのシステムの統計情報も表示する.

$ top[←]
Processes: 292 total, 2 running, 2 stuck, 288 sleeping, 827 threads                                                         20:05:58
Load Avg: 1.55, 1.31, 1.23  CPU usage: 0.0% user, 19.4% sys, 80.95% idle  SharedLibs: 12M resident, 15M data, 0B linkedit.
MemRegions: 28202 total, 3551M resident, 112M private, 500M shared. PhysMem: 7522M used (1495M wired), 8622M unused.
VM: 723G vsize, 1066M framework vsize, 0(0) swapins, 0(0) swapouts. Networks: packets: 398003520/414G in, 293755742/45G out.
Disks: 21610705/228G read, 15917948/520G written.

PID    COMMAND      %CPU TIME     #TH  #WQ  #PORT #MREG MEM    RPRVT  PURG   CMPRS  VPRVT  VSIZE  PGRP  PPID  STATE    UID
91440  CloudKeychai 0.0  00:00.02 2    0    47+   42+   908K+  560K+  0B     0B     53M+   2411M+ 91440 91146 sleeping 6240
91310  com.apple.BK 0.0  00:00.15 2    1    52+   63+   2896K+ 2000K+ 0B     0B     53M+   2435M+ 91310 1     sleeping 6240
91286  secd         0.0  00:01.41 2    1    58+   51+   2660K+ 2200K+ 0B     0B     53M+   2435M+ 91286 91146 sleeping 6240
91270  IMDPersisten 0.0  00:00.17 3    0    58+   72+   1964K+ 1280K+ 0B     0B     53M+   2435M+ 91270 1     sleeping 6240
91241  com.apple.Ic 0.0  00:02.37 2    1    53+   175+  111M+  110M+  0B     0B     159M+  2519M+ 91241 1     sleeping 6240
91213  tccd         0.0  00:01.51 2    1    47+   65+   3768K+ 3176K+ 0B     0B     53M+   2437M+ 91213 91146 sleeping 6240
91209  xpcd         0.0  00:00.59 2    1    45+   63+   4656K+ 4008K+ 0B     0B     54M+   2436M+ 91209 1     sleeping 6240
91200  cfprefsd     0.0  00:02.87 2    1    43+   81+   4724K+ 4388K+ 0B     0B     56M+   2415M+ 91200 91146 sleeping 6240
91197  distnoted    0.0  00:04.12 2    0    55+   39+   4108K+ 3864K+ 0B     0B     53M+   2433M+ 91197 91146 sleeping 6240
91146  launchd      0.0  00:04.07 2    0    77+   40+   2480K+ 2356K+ 0B     0B     55M+   2414M+ 91146 1     sleeping 6240
80061  com.apple.BK 0.0  00:00.35 2    1    51+   64+   4900K+ 3992K+ 0B     276K+  53M+   2435M+ 80061 1     sleeping 5820
78599  syspolicyd   0.0  00:03.53 3    2    37+   63+   4900K+ 4432K+ 0B     28K+   54M+   2436M+ 78599 1     sleeping 0
78264  com.apple.BK 0.0  00:00.35 2    1    51+   63+   4968K+ 4048K+ 0B     256K+  53M+   2435M+ 78264 1     sleeping 6253
77718  secd         0.0  00:00.33 2    1    47+   48+   4624K+ 4244K+ 0B     276K+  53M+   2412M+ 77718 77556 sleeping 6253
77664  com.apple.In 0.0  00:01.72 2    0    57+   57+   3000K+ 2372K+ 0B     16K+   53M+   2434M+ 77664 1     sleeping 6253
77650  IMDPersisten 0.0  00:00.42 3    0    58+   72+   1936K+ 1248K+ 0B     0B     53M+   2435M+ 77650 1     sleeping 6253
77645  distnoted    0.0  00:06.95 2    0    40+   36+   1328K+ 1084K+ 0B     0B     53M+   2433M+ 77645 77641 sleeping 92
77642  com.apple.Ic 0.0  00:07.95 2    1    54+   314+  148M+  146M+  0B     80K+   191M+  2552M+ 77642 1     sleeping 6253
77641  launchd      0.0  00:00.96 2    0    62+   39+   552K+  428K+  0B     0B     55M+   2413M+ 77641 1     sleeping 92
77640  com.apple.Ic 0.0  00:00.32 2    1    47+   50+   4616K+ 3864K+ 0B     276K+  53M+   2414M+ 77640 1     sleeping 92
77638  xpcd         0.0  00:00.02 2    0    32+   43+   2108K+ 1976K+ 0B     4096B+ 55M+   2413M+ 77638 1     sleeping 92

プログラムの開発環境

マニュアルの読み方

$ man システムコール名[←]
$ man ライブラリ関数名[←]
$ man コマンド名[←]
$ man -k キーワード[←]

マニュアルの章立ては以下のようになっている.

1章コマンド
2章システムコール
3章ライブラリ関数
4章デバイスファイル
5章ファイル形式
6章ゲーム
7章その他
8章管理用コマンド

man コマンドの引数に指定された名前は,1章から順番に検索される. 従って,man printf を実行すると1章の printf コマンドのマニュアルが表示される. ライブラリ関数の printf について知りたいときには,3章であることを指定するために次のように章を指定する.

$ man 3 printf[←]

各章の説明用に intro というエントリが用意されている. 2章(システムコール)について知りたいときには,次のようにする.

$ man 2 intro[←]

プログラム作成から実行までの流れ

プログラムの作成では,Emacs などのエディタでプログラムを記述し,それをCコンパイラ(cc)でコンパイルし,実行を繰り返すことになる. コンパイルでエラーになったら,エディタに戻りプログラムを変更の後,またコンパイル,実行する. 実行時に間違いが見つかれば,エディタに戻りプログラムを変更の後,またコンパイル,実行する. プログラムは,いきなり全部を作ろうとするのではなく,部品となる部分を少しずつ動作を確かめながら作るのが良い.

プログラム作成から実行まで

インデンテーション

プログラム作成時に気をつけてほしいことの一つにインデンテーションがある.

エディタとして Emacs を使用している場合,デフォルトのインデンテーション(字下げ,段付け)は2文字字下げである. 一方,K&R と呼ばれる有名な C 言語の書籍のインデンテーションは4文字字下げであり,Linux カーネルで採用されているインデンテーションは8文字字下げである. 字下げ幅に限らず,空白の有無やカッコの配置も,インデンテーションによって多様である. この授業では Linux カーネルで採用されている8文字字下げを用いることにする. Emacs でそのインデンテーションを用いる場合には,ホームディレクトリにある .emacs というファイルに以下の行を追加する. 追加したら,変更を有効にするために,Emacs を一旦終了して再び起動する.

(setq c-default-style "linux")

Emacs 上で TAB キーを叩くと,設定された値だけ右にインデント(字下げ)される. TAB キーを叩くのは行頭である必要はない.

"linux" 以外には例えば以下の設定が可能である.
(setq c-default-style "gnu")
(setq c-default-style "k&r")
(setq c-default-style "bsd")
(setq c-default-style "stroustrup")

例えば以下のプログラムのインデンテーションを変更して,

     1  void sort(int data[], int count){
     2    int i, sw=0, last=1, n=count-1;
     3    while(sw!=n){
     4      sw = n;
     5      for(i=n; i>=last; i--)
     6        if(data[i]<data[i-1]){
     7          swap_array(data, i, i-1);
     8          sw=i;
     9        }
    10      last=sw+1;
    11    }
    12  }

以下のプログラムに変えることができる. 字下げ幅の変更のみならず,変数名や予約語,演算子の前後にスペースを入れる変更を施した. さらに,変数宣言部分と実行部分の間に空行を入れ,それらの部分を見やすく分離した. 空白や空行を使うことで,プログラムの構造はわかりやすくなる.

     1  void sort(int data[], int count)
     3  {
     4          int i;
     5          int sw = 0;
     6          int last = 1;
     7          int n = count - 1;
     8  
     9          while (sw != n) {
    10                  sw = n;
    11                  for (i = n; i >= last; i--)
    12                          if (data[i] < data[i - 1]) {
    13                                  swap_array(data, i, i - 1);
    14                                  sw = i;
    15                          }
    16                  last = sw + 1;
    17          }
    18  }

以下のような点にも注意して,他の人も読みやすく見た目にも綺麗なプログラムを書くことを心がけると良い.

以下は,Linuxカーネルのコーディング規約である. 少なくとも第3章くらいまでは読むと良いでしょう.
https://linuxjf.osdn.jp/JFdocs/kernel-docs-2.6/CodingStyle.html

他にも以下のようなコーディング規約がある.

コーディングスタイルに関するWebサイトには例えば以下のようなものがある.

※注意※

エディタとして Emacs を使用しているか否かに関わらず,前半(第1〜5週)のレポート課題では,インデンテーションに加えて上記の注意点にも気をつけてプログラムを作成し,提出すること. 1つのレポートの中でインデンテーションなどの書式に一貫性がない場合は,再提出にすることがあります. また,書式に一貫性があっても,プログラムがあまりにも読みにくい場合は,再提出にします.

コンパイルとリンク

コンパイル・実行・修正

以下は,1から10までの総和を求める簡単なプログラムである.

sum_warning.c
     1  #define MAX 10
     2
     3  int main(void)
     4  {
     5          int i, total;
     6
     7          total = 0;
     8          for (i = 1; i <= MAX; i++) {
     9                  total += i;
    10          }
    11
    12          printf("total = %d\n", total);
    13
    14          return 0;
    15  }

コンパイル,実行してみる.

$ cc sum_warning.c[←]
sum_warning.c:12:9: warning: implicitly declaring library function 'printf' with type 'int (const char *, ...)' [-Wimplicit-function-declaration]
        printf("total = %d\n", total);
        ^
sum_warning.c:12:9: note: include the header  or explicitly provide a declaration for 'printf'
1 warning generated.
$ ./a.out[←]
total = 55
$ 

cc が warning(警告)のメッセージを出したが,コンパイルは正常に終了している. 警告メッセージは,sum_warning.c ファイルの12行目の9文字目からの部分で,ライブラリ関数の printf を int (const char *, ...) という型で暗黙に宣言した,という意味である. warning が出てもコンパイルは正常に終了するが,取り除くべきである.

warning の原因は,printf が宣言されていないのに printf を使用していることである. 従って,warning を取り除くには,printf の宣言を追加すればよい. ライブラリ関数については,その宣言が入っているヘッダファイルが用意されているのが普通である. そのヘッダファイル名は man を見るとわかる.

$ man 3 printf[←]
PRINTF(3)                BSD Library Functions Manual                PRINTF(3)

NAME
     printf, fprintf, sprintf, snprintf, asprintf, dprintf, vprintf, vfprintf,
     vsprintf, vsnprintf, vasprintf, vdprintf -- formatted output conversion

LIBRARY
     Standard C Library (libc, -lc)

SYNOPSIS
     #include <stdio.h>

     int
     printf(const char * restrict format, ...);

printf の宣言は stdio.h に含まれるので,以下のように #include <stdio.h> を追加する修正をすればよい.

sum.c
     1  #include <stdio.h>
     2
     3  #define MAX 10
     4
     5  int main(void)
     6  {
     7          int i, total;
     8
     9          total = 0;
    10          for (i = 1; i <= MAX; i++) {
    11                  total += i;
    12          }
    13
    14          printf("total = %d\n", total);
    15
    16          return 0;
    17  }

cc により起動されるプログラム

cc コマンド自体は実はコンパイルの処理をしない. コンパイルに必要な処理をしてくれるコマンドを呼び出すだけである. cc に -v オプションを追加すると,cc により起動されるプログラムの様子がわかる. さらに,それらのプログラムに与えられる引数も全て表示される. 以下の出力では,cc が clang や ld というプログラムを起動していることがわかる.

$ cc -v sum.c[←]
Apple LLVM version 9.0.0 (clang-900.0.39.2)
Target: x86_64-apple-darwin17.5.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang" -cc1 -triple x86_64-apple-macosx10.13.0 -Wdeprecated-objc-isa-usage -Werror=deprecated-objc-isa-usage -emit-obj -mrelax-all -disable-free -disable-llvm-verifier -discard-value-names -main-file-name sum.c -mrelocation-model pic -pic-level 2 -mthread-model posix -mdisable-fp-elim -fno-strict-return -masm-verbose -munwind-tables -target-cpu penryn -target-linker-version 305 -v -dwarf-column-info -debugger-tuning=lldb -resource-dir /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/9.0.0 -fdebug-compilation-dir /home/prof/oyama/edu/syspro2018 -ferror-limit 19 -fmessage-length 107 -stack-protector 1 -fblocks -fobjc-runtime=macosx-10.13.0 -fencode-extended-block-signature -fmax-type-align=16 -fdiagnostics-show-option -fcolor-diagnostics -o /var/folders/zz/zyxvpxvq6csfxvn_n00004rh000164/T/sum-6ba2fd.o -x c sum.c
clang -cc1 version 9.0.0 (clang-900.0.39.2) default target x86_64-apple-darwin17.5.0
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/include
 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/9.0.0/include
 /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include
 /usr/include
 /System/Library/Frameworks (framework directory)
 /Library/Frameworks (framework directory)
End of search list.
 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld" -demangle -lto_library /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libLTO.dylib -no_deduplicate -dynamic -arch x86_64 -macosx_version_min 10.13.0 -o a.out /var/folders/zz/zyxvpxvq6csfxvn_n00004rh000164/T/sum-6ba2fd.o -lSystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/9.0.0/lib/darwin/libclang_rt.osx.a
$ 

cc -E

cc に -E オプションを追加すると,コンパイルの前処理を実行するプログラム(プリプロセッサ)だけが起動され,結果は標準出力(stdout)に出される. マクロがうまく展開されない時や,typedef がうまく処理されない時など,ヘッダファイルに関係するエラーが起きたときに,プリプロセッサを通した結果を見ると原因がわかることがある.

$ cc -E sum.c | cat -n[←]
     1  # 1 "sum.c"
     2  # 1 "" 1
     3  # 1 "" 3
     4  # 331 "" 3
     5  # 1 "" 1
     6  # 1 "" 2
     7  # 1 "sum.c" 2
     8  # 1 "/usr/include/stdio.h" 1 3 4
     9  # 64 "/usr/include/stdio.h" 3 4
    10  # 1 "/usr/include/_stdio.h" 1 3 4
    11  # 68 "/usr/include/_stdio.h" 3 4
    12  # 1 "/usr/include/sys/cdefs.h" 1 3 4
    13  # 587 "/usr/include/sys/cdefs.h" 3 4
    14  # 1 "/usr/include/sys/_symbol_aliasing.h" 1 3 4
    15  # 588 "/usr/include/sys/cdefs.h" 2 3 4
    16  # 653 "/usr/include/sys/cdefs.h" 3 4
    17  # 1 "/usr/include/sys/_posix_availability.h" 1 3 4
    18  # 654 "/usr/include/sys/cdefs.h" 2 3 4
    19  # 69 "/usr/include/_stdio.h" 2 3 4
    20  # 1 "/usr/include/Availability.h" 1 3 4

... 中略 ...

   536  # 2 "sum.c" 2
   537
   538
   539
   540  int main(void)
   541  {
   542          int i, total;
   543
   544          total = 0;
   545          for (i = 1; i <= 10; i++) {
   546                  total += i;
   547          }
   548
   549          printf("total = %d\n", total);
   550
   551          return 0;
   552  }
$ 

リンク

cc に -c オプションを追加すると,アセンブラまでが実行され,機械語コードなどが入ったオブジェクトプログラムが作られる. オブジェクトプログラムのファイル名は,元の C プログラムのファイル名のサフィックス(接尾辞)を o に変えたものになる. すなわち,C プログラムが sum.c というファイル名の場合は sum.o というファイル名になる.

オブジェクトプログラムを cc の引数に指定すると,cc はサフィックスから引数のファイルがオブジェクトプログラムであると判定し,リンカを起動する. リンカは,オブジェクトプログラムとライブラリを結合させて実行形式のファイルを作ってくれる. リンカは,個々のオブジェクトプログラムだけでは解決できなかったシンボルの参照関係の解決も行う.

$ cc -c sum.c[←]
$ ls sum.o[←]
sum.o
$ cc -v sum.o[←]
Apple LLVM version 9.0.0 (clang-900.0.39.2)
Target: x86_64-apple-darwin17.5.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld" -demangle -lto_library /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libLTO.dylib -dynamic -arch x86_64 -macosx_version_min 10.13.0 -o a.out sum.o -lSystem /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/9.0.0/lib/darwin/libclang_rt.osx.a
$ 

上の例では,ld というリンカが起動されている.ld の引数に与えられている libclang_rt.osx.a はライブラリのファイルである.

動的リンクと静的リンク

リンクの方法には,動的(ダイナミック)リンクと静的(スタティック)リンクがある. 動的リンクとはライブラリを実行時にリンクする方法である. 一方,静的リンクでは,リンク時に全てのライブラリをリンクした実行形式のファイルを作成する. 動的リンクを用いた実行形式のファイルは,(実行時にリンクされる)ライブラリを含まないため,静的リンクを用いた実行形式のファイルよりも小さくなる. また,動的リンクされたライブラリが載せられるメモリ領域は,プロセス間で共有することができるため,動的リンクを用いるとメモリ使用量が減ることが多い. しかし,動的リンクを用いると,実行時にリンクが行われるので,わずかに実行が遅くなることがある.

動的リンクされた実行形式のファイルが使用するライブラリは otool コマンドに -L オプションを付けて実行することで知ることができる(Linuxでは ldd コマンドを用いる).

$ otool -L a.out[←]
a.out:
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4)
$ 

make コマンド

C プログラムはファイル単位でのシンボルのスコープを持つため,また大きなソースプログラムファイルはコンパイルに時間がかかるため,プログラムは適度に複数ファイルに分割して作成することが望ましい. C プログラムを複数ファイルに分割した場合,まずそれぞれを cc -c によりオブジェクトプログラムにコンパイルする. そうすると,Cプログラムファイルごとに(拡張子が o の)オブジェクトプログラムのファイルができる. これらのオブジェクトプログラムのファイルをリンクして実行形式のファイルを作る.

makeの例

このようなコンパイル手順を自動化してくれるのが make コマンドである. make コマンドは,カレントディレクトリにある makefile または Makefile を読み込み,そこに書かれているルールに従ってコンパイルを行う(ルールに従って処理をするだけで,処理がコンパイルである必要性は全くない). カレントディレクトリに makefile と Makefile の両方があった場合は,makefile が優先される. 以下は,上の図のコンパイル手順を Makefile として記述したものである.

a.out: file-1.o file-2.o
        cc file-1.o file-2.o

file-1.o: file-1.c
        cc -c file-1.c

file-2.o: file-2.c
        cc -c file-2.c

ルールの書き方の基本的なところは単純で,作成したいターゲットの作成方法を以下のように記述する. 「ターゲットが依存するファイル」がまた別のファイルに依存している場合(例えば file1.o が file1.c から作成される場合)には,その依存関係をルールとして書く. make はターゲット作成のために必要な処理を,依存関係を辿りながら順番に実行してくれる. ターゲット作成においては,ターゲットが依存するファイルを全て作成した後,そのターゲットの作成のための処理が上から順に実行される.

ターゲット: ターゲットが依存するファイル
<TAB>ターゲット作成のための処理1
<TAB>ターゲット作成のための処理2
...
<TAB>ターゲット作成のための処理n

実際に make コマンドを実行してみると,以下のようになる.変更されたファイルだけが再コンパイルされる.

$ make[←]
cc -c file-1.c
cc -c file-2.c
cc file-1.o file-2.o
$ 
file-2.cを変更
$ make[←]
cc -c file-2.c
cc file-1.o file-2.o
$ 

Makefile が複雑になると,Makefile を見ただけでは実際にどのように処理が進むのかすぐにはわからないこともある. そのような場合,make -n を実行すると,make はコマンドを実際に起動しないが,起動する(はずだった)コマンドを処理の流れとともに表示してくれる.

デバッガ

注意: 2022年4月1日現在,情報科学類計算機室の macOS 環境では,gdb が動作しない(ことがある)という不具合が観測されている. gdb ではなく lldb というデバッガは使えるようである. また,/usr/local3/coins/macosx/bin/coins-gdb に,上記の不具合が発生しないようにした gdb をインストールしたので,それを使うこともできる. 以下の gdb の実行例は,2年前の情報科学類計算機室の Mac OS X 環境でのものである.

デバッガとして gdb が利用できる. 日本語マニュアルは以下に用意されている.

http://www.coins.tsukuba.ac.jp/~syspro/gdb-5.0-doc/

ソースコードを参照しながらデバッガを用いたい場合は cc に -g オプションを付けてコンパイルする必要がある. -g オプションを付けていない場合でもデバッガを用いることはでき,バックトレース(関数の呼び出し履歴)の表示や,アセンブリコードのどの命令の実行でどんな問題が発生したかの表示などはできる. -g オプションを付けた場合には,ソースコードの何行目で問題が発生したのかがわかり,また大域変数,ローカル変数,関数などの名前を指定してその値やアドレスを調べることもできる.

良くあるバグにセグメンテーションフォルト(segmentation fault)やバスエラー(bus error)がある. これらは,アクセスが許可されていない番地へのアクセスなどを行うと発生する. 典型的な例として NULL ポインタの先へのアクセスがある. NULL ポインタが指すメモリ領域には通常アクセスが許可されていないため,NULL ポインタを辿るとセグメンテーションフォルトやバスエラーが発生する. 以下は NULL ポインタアクセスによりセグメンテーションフォルトを起こすプログラムを,デバッガから実行した例である.

segfault.c
$ cc segfault.c[←]
$ ./a.out[←]
Segmentation fault: 11
$ cc -g segfault.c[←]
$ gdb a.out[←]
GNU gdb (GDB) 7.12
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin13.4.0".
... 中略 ...
Reading symbols from a.out...Reading symbols from /private/tmp/a.out.dSYM/Contents/Resources/DWARF/a.out...done.
done.
(gdb) run[←]
Starting program: /private/tmp/a.out
... 中略 ...
Program received signal SIGSEGV, Segmentation fault.
0x0000000100000f17 in access_pointer (p=0x0) at segfault.c:5
5               printf("%c", *p);
(gdb) backtrace[←]
#0  0x0000000100000f17 in access_pointer (p=0x0) at segfault.c:5
#1  0x0000000100000f43 in test () at segfault.c:10
#2  0x0000000100000f64 in main () at segfault.c:16
(gdb) list[←]
1       #include <stdio.h>
2
3       void access_pointer(char *p)
4       {
5               printf("%c", *p);
6       }
7
8       void test(void)
9       {
10              access_pointer(NULL);
(gdb)[←]
11      }
12
13
14      int main(void)
15      {
16              test();
17              return 0;
18      }
(gdb) print p[←]
$1 = 0x0
(gdb) quit[←]
A debugging session is active.

        Inferior 1 [process 62703] will be killed.

Quit anyway? (y or n) y[←]
$ 

プログラミングとデバッグ

この回の講義の以降の部分は過去の講義科目ですでに学んだ内容かもしれないが,C言語を忘れている人も多いかもしれないので復習する.

プログラミング

ある程度の大きさのプログラムをいきなり作成するのは難しい. 問題や設定,条件を単純化し,それを解く小さい簡単なプログラムを作成し,ちゃんと動くようにするところから始める.

例えば,バブル整列法のプログラムを書くことを考えてみる. 最初から,任意個のデータが入ったファイルの内容を整列して出力するプログラムを書こうとするのは(それなりの経験がなければ)賢明とは言えない. 例えば,入力部,整列の部分,出力部に分割し,それぞれの部分が正しく動作するようプログラミングおよびデバッグをしてから,結合するようにするとよい.

整列の部分を作成する時には,整列するデータが必要である. 入力部が完成する前ならば,大域変数に初期値としてデータを与えることができる. また,結果を確認するための仮の出力部も必要である. このように,仮の入出力部を用いて作成したバブル整列法のプログラムは以下のようになる(整列の部分は省略).

sort0.c
     1  #include <stdio.h>
     2
     3  #define SAMPLE_COUNT 6
     4  int sample[SAMPLE_COUNT] = {8, 12, 3, 15, 7, 4};
     5
     6  void print_data(int a[], int count)
     7  {
     8          int i;
     9
    10          for (i = 0; i < count; i++) {
    11                  printf("%2d ", a[i]);
    12          }
    13          printf("\n");
    14  }
    15
    16  void swap_array(int a[], int i, int j)
    17  {
    18          int tmp;
    19
    20          tmp = a[i];
    21          a[i] = a[j];
    22          a[j] = tmp;
    23  }
    24
    25  void sort(int data[], int count)
    26  {
    27
    28
    29
    30
    31
    32
    33
    34  }
    35
    36  int main(void)
    37  {
    38          sort(sample, SAMPLE_COUNT);
    39          print_data(sample, SAMPLE_COUNT);
    40          return 0;
    41  }

デバッグ

プログラミングにはデバッグ(バグ取り)が付きものである. 書いたプログラムがそのまま正しく動作することは希であり,何らかの問題により思った通りに正しく動作しないのが普通である. そのため,正しく動作しない原因(バグ)を究明し,取り除く(デバッグする)必要がある.

多くの環境ではデバッガが用意されているが,最も基本的なデバッグ方法は printf を用いる方法である. printf を用いることで

をすることができる. printf デバッグでは,プログラムが通るパスの情報や,プログラムの各場所における変数の値を出力させる. これによりプログラムの実行の経過を把握することができる.

上記のバブル整列法のプログラムに以下の sort 関数を入れる.

sort1.c / sortonly1.c
     1  void sort(int data[], int count)
     2  {
     3          int i, j;
     4
     5          for (i = 0; i < count; i++) {
     6                  for (j = i; j < count; j++) {
     7                          if (data[j] > data[j + 1]) {
     8                                  swap_array(data, j, j + 1);
     9                          }
    10                  }
    11          }
    12  }

コンパイル,実行すると,以下のおかしな結果が出力される(結果の中の数の一部は人や環境によって異なる可能性がある).

$ ./a.out[←]
 8  3  4  0  7 12
$ 

そこで,デバッグ用に途中経過を出力するために,6, 7行目を追加する.

sort2.c / sortonly2.c
     1  void sort(int data[], int count)
     2  {
     3          int i, j;
     4
     5          for (i = 0; i < count; i++) {
     6                  printf("%d: ", i);
     7                  print_data(data, count);
     8
     9                  for (j = i; j < count; j++) {
    10                          if (data[j] > data[j + 1]) {
    11                                  swap_array(data, j, j + 1);
    12                          }
    13                  }
    14          }
    15  }

コンパイル,実行すると,以下のような結果になる.

$ ./a.out[←]
0:  8 12  3 15  7  4
1:  8  3 12  7  4  0
2:  8  3  7  4  0 12
3:  8  3  4  0  7 12
4:  8  3  4  0  7 12
5:  8  3  4  0  7 12
 8  3  4  0  7 12
$ 

情報が足りないので,内側の for 文の中で何が起こっているのか調べることにし,10~12行目,14, 15行目,18, 19行目を追加する.

sort3.c / sortonly3.c
     1  void sort(int data[], int count)
     2  {
     3          int i, j;
     4
     5          for (i = 0; i < count; i++) {
     6                  printf("%d: ", i);
     7                  print_data(data, count);
     8
     9                  for (j = i; j < count; j++) {
    10                          printf("\t[%d]=%d > [%d]=%d",
    11                                 j, data[j], j + 1, data[j + 1]);
    12
    13                          if (data[j] > data[j + 1]) {
    14                                  printf(" ... swap!!");
    15 
    16                                  swap_array(data, j, j + 1);
    17                          }
    18
    19                          printf("\n");
    20                  }
    21          }
    22  }

コンパイル,実行すると,以下のような結果になり,要素数を超えて配列にアクセスしていたことがわかる.

$ ./a.out[←]
0:  8 12  3 15  7  4
        [0]=8 > [1]=12
        [1]=12 > [2]=3 ... swap!!
        [2]=12 > [3]=15
        [3]=15 > [4]=7 ... swap!!
        [4]=15 > [5]=4 ... swap!!
        [5]=15 > [6]=0 ... swap!!
1:  8  3 12  7  4  0
        [1]=3 > [2]=12
        [2]=12 > [3]=7 ... swap!!
        [3]=12 > [4]=4 ... swap!!
        [4]=12 > [5]=0 ... swap!!
        [5]=12 > [6]=15
2:  8  3  7  4  0 12
        [2]=7 > [3]=4 ... swap!!
        [3]=7 > [4]=0 ... swap!!
        [4]=7 > [5]=12
        [5]=12 > [6]=15
3:  8  3  4  0  7 12
        [3]=0 > [4]=7
        [4]=7 > [5]=12
        [5]=12 > [6]=15
4:  8  3  4  0  7 12
        [4]=7 > [5]=12
        [5]=12 > [6]=15
5:  8  3  4  0  7 12
        [5]=12 > [6]=15
 8  3  4  0  7 12
$ 

data[j + 1] までアクセスされるので,for 文の比較判定式では count - 1 した値を使うように変更する.

sort4.c / sortonly4.c
     1  void sort(int data[], int count)
     2  {
     3          int i, j;
     4          int n = count - 1;
     5
     6          for (i = 0; i < n; i++) {
     7                  printf("%d: ", i);
     8                  print_data(data, count);
     9
    10                  for (j = i; j < n; j++) {
    11                          printf("\t[%d]=%d > [%d]=%d",
    12                                 j, data[j], j + 1, data[j + 1]);
    13
    14                          if (data[j] > data[j + 1]) {
    15                                  printf(" ... swap!!");
    16
    17                                  swap_array(data, j, j + 1);
    18                          }
    19
    20                          printf("\n");
    21                  }
    22          }
    23  }

コンパイル,実行すると,以下のような結果になる. 配列要素の添字は配列の大きさの範囲内におさまっているが,整列されていない.

$ ./a.out[←]
0:  8 12  3 15  7  4
        [0]=8 > [1]=12
        [1]=12 > [2]=3 ... swap!!
        [2]=12 > [3]=15
        [3]=15 > [4]=7 ... swap!!
        [4]=15 > [5]=4 ... swap!!
1:  8  3 12  7  4 15
        [1]=3 > [2]=12
        [2]=12 > [3]=7 ... swap!!
        [3]=12 > [4]=4 ... swap!!
        [4]=12 > [5]=15
2:  8  3  7  4 12 15
        [2]=7 > [3]=4 ... swap!!
        [3]=7 > [4]=12
        [4]=12 > [5]=15
3:  8  3  4  7 12 15
        [3]=7 > [4]=12
        [4]=12 > [5]=15
4:  8  3  4  7 12 15
        [4]=12 > [5]=15
 8  3  4  7 12 15
$ 

このままでは途中経過が見にくいので,上で追加した内側の for 文の中でのデバッグ出力をコメントアウトし,コンパイル,実行すると,以下のような結果になる.

$ ./a.out[←]
0:  8 12  3 15  7  4
1:  8  3 12  7  4 15
2:  8  3  7  4 12 15
3:  8  3  4  7 12 15
4:  8  3  4  7 12 15
 8  3  4  7 12 15
$ 

一番大きい15は最初の外側のループで最後尾まで移動されているが,先頭の8はそのままであることから,内側のループに問題があると考えられる. 内側のループは,最初は data[0] ~ data[5] を,次は data[1] ~ data[5] を比較交換している. これでは先頭の8は最初の外側のループでしか比較交換されない. また,一番大きい15は最初の外側のループで最後尾まで移動されるので,最初は data[0] ~ data[5] を,その次は data[0] ~ data[4] を比較交換しなければいけなかった. そこで,10行目の内側の for 文を以下のように修正する.

sort5.c / sortonly5.c
     1  void sort(int data[], int count)
     2  {
     3          int i, j;
     4          int n = count - 1;
     5
     6          for (i = 0; i < n; i++) {
     7                  printf("%d: ", i);
     8                  print_data(data, count);
     9
    10                  for (j = 0; j < n - i; j++) {
    11  //                        printf("\t[%d]=%d > [%d]=%d",
    12  //                               j, data[j], j + 1, data[j + 1]);
    13
    14                          if (data[j] > data[j + 1]) {
    15  //                                printf(" ... swap!!");
    16
    17                                  swap_array(data, j, j + 1);
    18                          }
    19
    20  //                        printf("\n");
    21                  }
    22          }
    23  }

コンパイル,実行すると,以下のような結果になり,正しく整列できるようになったことがわかる.

$ ./a.out[←]
0:  8 12  3 15  7  4
1:  8  3 12  7  4 15
2:  3  8  7  4 12 15
3:  3  7  4  8 12 15
4:  3  4  7  8 12 15
 3  4  7  8 12 15
$ 

以上のように,printf デバッグでは,最初は大きなブロックの経過を見るように printf を入れ,どの部分に問題がありそうかを予測する. 次に,問題がありそうな部分により細かく printf を入れていく. 原因がわかり修正できたら,細かい結果は出力しないようにすると,全体での途中経過がわかりやすくなる.

デバッグの基本は,実際に確かめもせずに「ここは大丈夫」と思わないことである. 「思い込み」はデバッグの天敵である.

ポインタ(1)

システムプログラムで用いるライブラリ関数やシステムコールにはポインタが必須である. 関数を呼び出し,まとまったデータをやり取りするためにはポインタを使う必要があるからである.

例えば下図のように,あるプログラムがライブラリまたはカーネルを呼び出し,データを読み込もうとしているとする. プログラムは,データを読み込むデータ領域をあらかじめ確保し,そこにデータを読み込みたい. そのデータ領域の先頭を,ライブラリまたはカーネルに知らせる方法がポインタである. ポインタの実体はアドレス(番地)である. ライブラリまたはカーネルはアドレスを受け取り,そこからデータを書き込み始める.

データコピー先のポインタ

他に文字列操作にもポインタは不可欠であるため,ポインタをよく理解しておく必要がある.

& と * の関係

アドレス演算子 & を変数の前に付けると,その変数の値を格納するために割り当てられた領域のアドレスが得られる. このアドレスのことをポインタ値という.

間接参照演算子 * は,ポインタ値が指す領域に格納されている値を取り出す. 領域に付けられた名前(変数名)ではなく,ポインタ値を用いて参照するので間接参照という.

下図では int 型の変数 i, j と,int へのポインタ型変数 p, q の領域がメモリに割り当てられている. i, j, p, q はそれぞれ別の変数であり,それぞれ別の領域が割り当てられる.

ポインタ

i は1000番地に置かれているものとすると,&i の値は 1000 となり,p に &i を代入すると 1000 が入る. i に 5 が代入されたとすると,*p の値は 5 になる. 図中にはないが,p に &i が代入された状態のまま,*p に 50 を代入すると i の値は 50 になる.

ポインタを使うとできること

ポインタ値はある変数(領域)を参照する値である. 値であるので,別の関数にポインタ値を渡すことができる. 渡された方の関数は,ポインタ値により参照される変数の値を変更することができる.

下図は,関数 func のローカル変数 i を引数として別の関数 func2 を呼び出している. func2 では引数 i に 5 を代入しているが,func の i と func2 の i は別の変数であるので,func2 で代入した値は func の i には反映されない. 従って,func2 呼び出し後に i の値を出力した結果は 0 のままである.

関数呼び出し

一方,下図では,関数 func のローカル変数 i のポインタ値を引数として別の関数 func2 を呼び出している. func2 では引数に間接参照演算子を付けた *i に 5 を代入している. この場合,func2 における *i は func の i を参照しているため,func2 で代入した値は func の i には反映される. 従って,func2 呼び出し後に i の値を出力した結果は 5 になる.

関数呼び出し

ポインタを使う時に気をつけること

初期化

ポインタ変数を初期化せずに,すなわち,適切な値(ポインタ値)を入れずに参照してはいけない.

     1  int func(void)
     2  {
     3          int *p;
     4
     5          *p = 5;

間違ったアドレスにデータを書き込む,または,書き込もうとすると問題が発生する. 書き込めないアドレスに書き込もうとすると,セグメンテーションフォルトが発生し,プログラムは異常終了する. この場合は,異常終了により間違ったことにすぐ気づくので,まだましである.

書き込めるデータ領域の中にあるが意図とは違う間違ったアドレスに書き込むと,書き込み自体は成功してしまい,プログラムの実行が進んでから問題が起こる. すると,どうしてここに変なデータが書き込まれているのかと悩むことになる.

これをうまく使ったのがバッファオーバフローである. 外部からスタック上に確保されているバッファよりも多くのデータ(プログラム)を読み込ませることにより,プログラムの実行を乗っ取ってしまう. バッファオーバフローについては第3回目の講義でもう少し詳しく説明する.

関数間でのポインタ値の受け渡し

関数間でポインタ値を受け渡しをする時には,ポインタ値が参照する変数がどこで宣言(領域確保)されているのか,注意する必要がある. 大域変数やヒープ領域(malloc して確保した領域)は,プログラム中どこでも有効であるので問題はない. しかし,ローカル変数については注意が必要である. ローカル変数は,それを宣言した関数から return する前までの間だけ有効である. そのため,ローカル変数へのポインタ値を戻り値にしてはいけない.

以下のプログラムでは,main 関数は関数 badfunc を呼び出しポインタ値を戻り値として受け取っている. ポインタ値を戻り値として受け取ること自体には全く問題はない. しかし,badfunc で返しているポインタ値の参照先が badfunc のローカル変数なのが大きな問題である.

badfunc.c
     1  #include <stdio.h>
     2
     3  int *badfunc(void)
     4  {
     5          int i = 5;
     6
     7          return &i;
     8  }
     9
    10  int main(void)
    11  {
    12          int *i = badfunc();
    13
    14          printf("1: %d\n", *i);
    15          printf("2: %d\n", *i);
    16
    17          return 0;
    18  }

コンパイル,実行すると,以下のような結果になる. badfunc を呼び出した後の関数呼び出し(上のプログラムでは printf の呼び出し)により,badfunc のローカル変数の値は破壊されてしまっていることがわかる. コンパイラは,ローカル変数に関係付けられた領域のアドレスを戻り値としていることに warning を出している. コンパイラの warning を無視すべきではなく,その原因を究明し,コンパイル時に warning が出ないように修正すべきである.

$ cc badfunc.c[←]
badfunc.c:7:17: warning: address of stack memory associated with local variable 'i' returned [-Wreturn-stack-address]
        return &i;
                ^
1 warning generated.
$ ./a.out[←]
1: 5
2: 0
$ 

例えば以下のプログラムのように malloc を使用してヒープ領域に領域を確保すれば,その領域上のデータが破壊されることはない(malloc についてはこの授業の今後の講義で再び説明する).

goodfunc.c
     1  int *goodfunc(void)
     2  {
     3          int *i = malloc(sizeof(int));
     4
     5          *i = 5;
     6
     7          return i;
     8  }

ヒープ領域に領域を確保する関数(malloc およびその派生関数である calloc, valloc)を使用する場合は,確保した領域が不要になった時に必ず free する必要がある. free しないまま,確保した領域のポインタ値を失ってしまうと,プログラムが実際に使用しているメモリの量を超える量のメモリを確保し続けるという問題(メモリリークの問題)が発生する. メモリリークについては,この授業の今後の講義で再び説明する.

他のよくある間違い

ポインタ型変数を複数宣言するときには,それぞれの変数名の前に * を付ける.

     1  int func(void)
     2  {
     3          int *p, *q;

以下では,ポインタ型変数 p と int 型変数 q を宣言している.

     1  int func(void)
     2  {
     3          int *p, q;

ポインタは非常に便利でありシステムプログラミングには欠かせないが,細心の注意をもって使用しないといけない.

練習問題

練習問題(101)

インデンテーションに記述されている設定を Emacs に対して適用し,TAB キーを叩いた時のインデントの深さが変化することを確認しなさい. また,元の(インデントが浅い)スタイルで書かれたプログラムのファイルを Emacs で開き,各行で TAB キーを押すとその行に新しいインデンテーションスタイルが適用されることを確認しなさい.

さらに,

ことにより,プログラムが見やすくなるかどうかを確認しなさい.

練習問題(102)

NULL ポインタアクセス以外の理由で segmentation fault や bus error 等が起こり異常終了するプログラムを書きなさい. そのプログラムの実行をデバッガ(lldbやcoins-gdb等)で追跡し,スタックのバックトレースや変数の値等を表示させ,プログラムのどの行で,どのような原因で異常が起きたかを調べなさい. そして,プログラムを変更して原因を取り除き,正しく実行できるようになったことを示しなさい. cc や gcc 等のコンパイラのコマンドラインオプションには -g を付けてプログラムをコンパイルすること.

レポートには,異常終了するプログラムとその実行結果,デバッガによる調査手順とその結果,変更後のプログラムとその変更点,最終的な実行結果を含めること.

練習問題(103)

Makefile はコンパイルだけに使用する必要はない. make は処理がコンパイルであるかどうかに関係なく,Makefile に書かれたルールにのっとってコマンドを実行するだけである. そこで,Makefile を用いてよく行われるのが,コンパイルによって作られたファイルの消去である.

Makefile にルールを追加し,以下のようにオブジェクトファイルが消去されるようにしなさい.

$ make clean[←]
/bin/rm -f a.out file-1.o file-2.o
$ 

練習問題(104)

make は便利に使えるように,予め様々な暗黙のルールを持っており,処理の全てを事細かに Makefile に記述しなくても処理を行えるようになっている. 例えば,make はカレントディレクトリの .c という拡張子を持つソースファイルから cc -c コマンドを用いて .o という拡張子を持つオブジェクトファイルを作成するというルールを持っている. つまり,file-1.o は file-1.c に依存し,cc -c コマンドで作成するというルールを,いちいち Makefile に書く必要はない(書いても良い). また,変数を定義する機能もあり,複数のファイル名を値として持つ変数を定義することにより,いちいちファイル名を列挙する必要が無くなる. 例えば,

OBJS = file-1.o file-2.o

と書くと,シェルで変数の値を参照するときのように,$(OBJS) は file-1.o file-2.o を意味するようになる.

変数と暗黙のルールを用いて,練習問題(103)で作成した Makefile をできるだけ短く書き換えなさい.

参考: 暗黙のルールは make -p を使うと表示できる. 暗黙のルールを表示したいが,実際にコマンドを実行したくない場合には,make -n -p を実行するとよい.

練習問題(105)

プログラミングとデバッグではバブル整列法を例にして,制御の流れや変数の値を確認するためのデバッグ用の出力を行う文を入れている. このプログラムを変更して,より効率の良い(即ち効率が O(n2) の単純選択法や単純挿入法以外の)整列法(ヒープソート,クィックソート,ラディックスソートなど)を用いるプログラムを作成しなさい. 制御の流れや変数の値を確認するための出力文を入れることにより,ソートの効率が良くなっていることがわかるようなプログラムを作りなさい. また,プログラムの終了時に,比較回数など計算量の指標となる数値を出力するようにしなさい.

プログラムと実行結果だけではなく考察も書くこと. 全てのデバッグ結果や途中経過をレポートに書くと膨大な長さになるので,出力結果は適宜省略しても構わない.


Yoshihiro Oyama / <oyama@cs.tsukuba.ac.jp>