昨今は「モダンPerl 」に代表される、「 より洗練されたプログラミング」を目指すムーブメントが盛り上がっています。
また、ソフトウェア開発に携わる人々の嗜好も、Web 2.0に代表されるようなサービス指向へと大きく傾いていますので、実装言語の選択においても、サービス実現という問題ドメインに適したものが望まれます。
そんなご時勢におけるアセンブラは、本連載の題名通り「骨董品」( antique)のようなものですから、わざわざ修得しようとするのは余程の物好き と言っても良いでしょう。
好事家や目利きにとっては価値があっても一般にはわかりづらいところとか、注ぎ込む手間暇の割には「役立つものがすぐに実装できる」とか「従来よりも手間暇が軽減する」などの直接的な見返りが少ない、といったところも骨董とそっくりです(骨董ファンに怒られそうですが……) 。
それでは現代においてアセンブラの修得は意味がないのでしょうか?
連載の初回として、今回はアセンブラの概要について説明します。今回をお読みになって少しでも興味を引かれる点があれば、読者にとってアセンブラの修得は決して無駄にはならないでしょう。
本記事が、そんな読者のアセンブラ理解の一助になれば幸いです。
アセンブラの位置づけ
C言語におけるコンパイル処理の過程から、アセンブラの位置づけを見てみましょう
図1 コンパイル過程
一般的なコンパイルの手順では、cc
というコマンド(=広義のコンパイラ)が処理を受け付けて、必要とされる以下の処理を順次起動します。
1.プリプロセッサ(Pre-processor)
C言語のプログラムを入力に、マクロの展開やヘッダファイルの読み込みなどを行います(この段階で処理を止める場合のコンパイラオプションは-E) 。
2.狭義のコンパイラ
プリプロセッサの結果を入力に、稼動対象環境のアーキテクチャ向けのアセンブラプログラムを生成します(この段階で処理を止める場合のコンパイラオプションは-S) 。
3.アセンブラ(Assembler)
アセンブラプログラムを入力に、オブジェクトファイルを生成します(この段階で処理を止める場合のコンパイラオプションは-c) 。
4.リンカ(Linker)
オブジェクトファイルを入力に、稼動対象環境における実行可能形式のファイルを生成します。
アセンブラプログラムは、稼動対象環境のCPU命令と1対1となるような記述となります(擬似命令やマクロなどがあるため、厳密には違うのですが…) 。つまり、アセンブラを理解するということは、CPUに密接したレベルでソフトウェアを理解するということに他なりません。
なお、「 アセンブラ(assembler) 」が「アセンブリ言語(assembly language)で書かれたプログラム」を「アセンブル(assemble) 」する、というのが英語的には正しいのかもしれませんが、日本で「アセンブラ」と言った場合、ツールとしての「アセンブラ」を指すと同時に、「 アセンブリ言語」のことも指しますので注意が必要です(本連載の表題の「アセンブラ」も「アセンブリ言語」の意味で使用しています) 。
「アセンブリ言語で書かれたプログラム」という名称は長いですので、本連載では「アセンブラプログラム」という呼び方をします。
アセンブラを修得すべき人
冒頭では「骨董品」と評しましたが、現代においてもアセンブラの修得が必要な状況があります。
コンパイラを開発する人
先述したように、コンパイラはコンパイル対象言語で書かれたプログラムから、対象アーキテクチャのアセンブラプログラムへの変換処理ですから、アセンブラの修得が必要です。
対象アーキテクチャは、仮想マシンのバイトコード(例:Java)や、中間言語(例:GNU Compiler Collectionの枠組みを使用する場合)だったりと、必ずしも特定の物理的なCPU に結びついたものではない場合もありますが、「 アセンブラ的」な概念を理解する必要上、何らかのアーキテクチャにおけるアセンブラを1つは修得する必要があります。
オペレーティングシステム層で動作するプログラムを開発する人
オペレーティングシステム実装の大部分はC言語で書かれていますが、C言語であってもCPUの機能を100%使用できるわけではありません。
特定のCPU機能を使用するために、各CPUごとのアセンブラで記述される部位が必ず含まれますので、そういった部位の実装に関わるのであれば、アセンブラの修得が必要です。
CPU依存性のない部分(例:ファイルシステムやネットワークプロトコル)の開発をする場合、開発そのものはC言語(+カーネル内部で使用可能なAPI)の知識があれば事足りますが、実装の間違いからカーネルパニックした際にクラッシュダンプ[1] の調査を行う場合などは、往々にしてアセンブラレベルの知識が必要とされます。
ソフトウェアに対する理解を深めたい人
たとえそれがどんなに(一見)複雑そうな機能であっても、プログラムは「CPU命令の集まり」に過ぎません。
つまり、より小さい単位へと機能を分割し続けた場合、最終的にはアセンブラのレベルで底を打つわけです。
筆者は以前、「 ついつい論理ゲート(AND/ORを実現する電子部品)レベルで考えてしまう」というハードウェア畑出身の開発者と会ったことがありますが、ソフトウェアのレベルで考えるなら、流石にそこまで細かく分割して理解する必要は無いでしょう。
アセンブラのレベルでソフトウェアの仕組みを考える視点を身につけることで、機能を利用する側=上位レイヤーからの見方と、機能を実現する側=下位レイヤーからの見方、という2つの方向から理解することができますので、より理解を深めることができます。
アセンブラ修得に向かない人
筆者の個人的な見解ですが、以下のような人はアセンブラ修得には向かないように思われます。
プログラミングが初めての人
1980年あたりよりも前であれば、初めてのプログラミングをアセンブラで、というのもアリ(場合によってはそれしか選択肢が無かった)でしょうが、「 年寄りの体験を忠実になぞらせる」といった酔狂な理由でもなければ、21世紀にもなってソレはナシでしょう。
初心者にとっては、ある程度の抽象度を持ったプログラミング言語で、プログラミングに必要な基本的な概念を修得するのが先決です。
なお、宗教論争に巻き込まれるのを避けるため、初心者にとって最適なプログラミング言語に関しては、ここでは言及しません。
学習が暗記ベースな人
新しい分野について学習する際に、もっぱら「暗記」行為がベースになる人が少なからずいます。
開発上の必要性があるなら兎も角、ソフトウェアに関する理解の詳細化/体系化に意義を見出すのでなければ、このご時勢にアセンブラを修得するメリットは皆無、と言っても過言ではありません。
勿論、「 暗記」が不要なわけでもなければ、「 暗記」そのものを否定しているわけでもありません。
しかし同じ丸暗記するなら、もっと時代に即した役に立つ知識を暗記した方が、コストパフォーマンスが高いと言えます。
環境の準備
次回からは、実際にアセンブラプログラムの実装を行いますが、まずはそのための環境を準備しましょう。
前提条件
プログラムを例示する便宜上、この連載では対象CPUアーキテクチャを32ビットIntel 80x86、記述をAT&Tニーモニックに限定したいと思います。
現時点で一般ユーザの利用が最も容易なCPUアーキテクチャ、という視点で見た場合、32ビットIntel 80x86という選択に異論のある方は居ないでしょう。
また、UNIX系OSやCygwin環境といった広範な環境での可用性(入手容易性含む)を考えて、後述するように本連載ではアセンブラ処理系としてGNU Assemblerを想定しますので、例示するプログラムはIntelニーモニックではなく、GNU Assemblerが受け付けることのできるAT&Tニーモニックで記述します。
なお、宗教論争に巻き込まれるのを避けるため、32ビットIntel 80x86アーキテクチャの優劣や、両ニーモニックの優劣に関しては、ここでは言及しません。
こうやって振り返ってみると、ソフトウェア界隈が如何に宗教論争に満ち溢れているのかがわかります。
もっとも、そういった瑣末事項に関する粘着的な拘りが多少なりとも無ければ、ソフトウェアなどという七面倒臭いものはやっていられない気もしますが…
ちなみに、本連載は「Intel 80x86によるアセンブラの入門」なのですが、「 Intel 80x86のアセンブラ」よりは「アセンブラの入門」の方に軸足を置きますので、他のCPUアーキテクチャの話題にも触れる一方で、Intel 80x86に関する説明の網羅性はあまり重視しない予定です。
利用するツール
本連載では以下のツールが必要となりますので、事前に準備をしてください。
アセンブラ(asコマンド)
リンカ(ldコマンド)
これらのツールは、C/C++コンパイラが利用可能になっている環境なら、改めてインストールする必要は無いはずです。
本連載の実行例でこれらのコマンドに指定するオプションは、GNU AssemblerおよびGNU Linker(これらは共にGNU Binutils の一部です)を想定したものですが、他の実装でも概ね共通なはずです。
動作確認したGNU Binutilsのバージョンは、2.11.90(Linux) 、2.15.92(Linux)および2.18.50(Cygwin)です。
デバッガ
プログラムの動作確認を画面表示やファイル入出力によって行うためには、いわゆるお作法に沿った記述 によるプログラム量の増加が避けられません。
そこで本連載では、プログラムの動作確認をデバッガ経由で行うことで、プログラムの記述量を最小限に抑えることにします。
本連載の実行例では、GDB: GNU Project Debugger (gdbコマンド)を使用します。
動作確認したGDBのバージョンは、5.0(Linux) 、6.4.90(Linux)および6.8.0(Cygwin)です。
環境の確認
ツールが一通り揃ったならば、想定通りに機能することを確認しましょう。
まずはリスト1の内容を、ファイル名0001.s
で保存してください。
リスト1 0001.s
.data
.align 4
.global value
value:
.long 1
.text
.align 4
.global entry_point
entry_point:
int3 # プログラム実行の一時停止
movl $2, value
.global end_of_program
end_of_program:
int3 # プログラム実行の一時停止
nop
コメントに日本語を使用する場合、ファイル保存の際の文字コードにShift-JIS(正式にはWindows-31JないしCP932)を使用すると、2バイト目にバックスラッシュと同じ値を持つ文字が行末に来た場合に、asコマンドが想定外の振る舞いをする可能性があります。
コメントに日本語を使用するのであれば、日本語EUCやUTF-8等の、紛らわしいバイトを含まない文字コードを使用することをお勧めします。
ファイルに保存したなら、まずは図2 の手順で実行可能ファイル(0001.exe
)を生成します。手順上、実際に入力する内容は太字で示した部分です。
図2 実行ファイルの生成
$ as -o 0001.o 0001.s
$ ld -o 0001.exe -e entry_point 0001.o
$
実行可能ファイルが生成できたら、図3 の手順で実際にプログラムを実行してみましょう。
本連載では説明の便宜上、gdbをCUI(character user interface)形式で使用します。gdb起動の際にGUIが立ち上がるのであれば、起動時に-nw
オプションを追加指定してください。
図3 プログラムの実行
$ gdb 0001.exe
GNU gdb ........
(gdb) run
Starting program: ....
....
Program received signal SIGTRAP, Trace/breakpoint trap.
0x00401001 in entry_point ()
(gdb) print value
$1 = 1
(gdb) continue
Continuing.
Program received signal SIGTRAP, Trace/breakpoint trap.
0x0040100c in end_of_program ()
(gdb) print value
$2 = 2
(gdb) quit
The program is running. Exit anyway? (y or n) y
$
確認用プログラムは、初期値1のvalue
領域に2を代入するプログラムですので、プログラムの最後(end_of_program
)に到達した際に、"print value
"の結果出力(コマンド入力次行の右辺)が2となれば、期待通りに動作していることになります。
以上で準備は完了です。
次回以降は、いよいよ本格的にアセンブラプログラムの実装を行います。