C言語のコードを書いていると、「このコードは、コンピュータの中で実際には何になっているのだろう」と気になることがあるかもしれない。
その答えに最も近いのがアセンブラ(アセンブリ言語)であろう。
この連載では、C言語の知識をもとに、アセンブラを全6回で体験していく。
第1回は、アセンブラとは何かを確認してうえで、自分のパソコンに環境を作り、最初のプログラムを動かすところまでを扱う。
なお、動作環境を統一するため、WindowsパソコンにWSL2を導入して、Ubuntuを使用する。
記事の手順に従って、プログラムを動かして試してみよう。
1. アセンブラとは何か
機械語とアセンブリ言語
CPUが直接実行できるのは機械語と呼ばれる、0と1の並びだけである。だからといって、人が機械語を直接書くのは現実的ではない。そこで、機械語の1命令ずつに、人が読める短い名前(ニーモニック)を対応させたものがアセンブリ言語である。
たとえば「レジスタに値を入れる」という命令は、アセンブリ言語では mov と書く。この名前を機械語に変換するのがアセンブラというソフトウェアである。なお、アセンブリ言語そのものを「アセンブラ」と呼ぶことも多く、この連載でも題名には「アセンブラ」を使うことにする。
Cとの関係
C言語で書いたプログラムは、コンパイラによって機械語に変換される。その過程でアセンブリ言語が使われることもある。アセンブリ言語を読めると、Cのコードが機械の中でどう動いているのかを理解する手がかりになる。
| 言語 | 1行がやること | 読みやすさ |
|---|---|---|
| C言語 | 複数の命令に展開される | 読みやすい |
| アセンブリ言語 | ほぼCPUの1命令に対応する | 慣れが必要 |
| 機械語 | CPUが実行する命令そのもの | 人には読めない |
この連載で扱うアセンブリ
アセンブリ言語はCPUの種類ごとに異なる。この連載では、一般的なWindowsパソコンで使われているx86-64(64ビットのIntel/AMD系CPU)を対象とし、WSL上で動かす。アセンブラにはNASMを使い、書き方はIntel構文で統一する。
ARM版のWindowsパソコンを使っている場合
CPUの種類が異なるため、この連載のコードはそのままでは動かない。
Linuxでuname -mを実行し、x86_64と表示される環境で試してほしい。
2. 環境を準備する
WSL2のUbuntuを準備する
Windows11パソコンでは、WSL2(Windows Subsystem for Linux)を導入して、Ubuntuを動かすのが手軽である。
Ubuntuは20.04 LTS以降であれば、この連載の内容はそのまま動作する。
すでにWSL2のUbuntuが使える場合は、この手順は読み飛ばしてください。
- スタートメニューから「PowerShell」を検索し、管理者として実行する。
- 次のコマンドを実行する。
wsl --install -d Ubuntu- 再起動を求められたら、パソコンを再起動する。
- Ubuntuのウィンドウが開いたら、ユーザー名とパスワードを入力して登録する。
ウィンドウが開かない場合は、スタートメニューから「Ubuntu」を起動する。
パスワードは、入力しても画面には何も表示されないが、入力は受け付けられている。
なお、インストール直後の動きは、パソコンの状態によって異なる。
PowerShellで次のコマンドを実行し、VERSION が 2 になっていることを確認しよう。
wsl -l -v実行例
NAME STATE VERSIONUbuntu-20.04
*Running 2
ツールをインストールする
PowerShellで「wsl」を入力してWSLを起動しよう。
Ubuntuのターミナルで、次の順にコマンドを実行しよう。(最初のコマンドを入力するとパスワードが求められる)
sudo apt-get update
sudo apt-get upgrade -y
sudo apt-get install -y nasm gcc gdb binutilsインストールするツールの役割は次のとおりである。
| ツール | 役割 |
|---|---|
nasm | アセンブリ言語を機械語(オブジェクトファイル)に変換するアセンブラ |
ld(binutils) | オブジェクトファイルを実行ファイルにまとめるリンカ |
gdb | プログラムを1命令ずつ実行して中身を観察するデバッガ |
objdump(binutils) | 実行ファイルを逆アセンブルして表示する |
gcc | 第6回でCと連携するときに使うCコンパイラ |
正しくインストールできたか、バージョンを表示して確認しておこう。
nasm -v
ld -v
gdb --versionそれぞれバージョン情報が表示されれば準備完了である。
3. 最初のプログラム:終了コードを返す
最初のプログラムは、画面には何も表示せず、終了コード42を返して終わるものである。
まずは「アセンブラで書いたプログラムが動いた」という体験が目的である。
作業用のディレクトリを作り、exit42.asm というファイルを作成する。
ファイルの作り方は、このあとの「viでファイルを作って保存する」で説明する。
mkdir -p ~/asm/01
cd ~/asm/01ファイルの内容は次のとおりである。
global _start
section .text
_start:
mov rax, 60
mov rdi, 42
syscall
viでファイルを作って保存する
ファイルを作るには、Ubuntuに最初から入っているテキストエディタのvi(ヴィアイ)を使う。
viは、キー入力で「入力するモード」と「操作するモード」が切り替わるのが特徴である。
慣れないうちは戸惑うかもしれないが、この連載で使う操作は、限られている。
なお、ほかのエディタに慣れている方は、お好きなエディタで同じ内容のファイルを作ってみよう。
- ファイルを開く:次のコマンドを実行する。このファイルが未作成の場合は、新しく作られる。
vi exit42.asm
- 入力モードにする:キーボードの
iを押す。
画面の左下に-- INSERT --と表示されたら、文字を入力できる状態である。 - コードを貼り付ける:ブラウザで、上のコードエリア内をクリックして、コードを範囲選択(
Ctrl+ A)して、コピー(Ctrl+C)する。
次に、WSL(Ubuntu)のウィンドウを表示して、右クリック(またはCtrl+Shift+V)で貼り付ける。もちろん直接入力してもよいが、貼り付けのほうが入力ミスを防げる。 - 入力モードを終わる:
Escキーを押す。左下の-- INSERT --の表示が消える。 - 保存して終了する:「
:wq」と入力して、Enterキーを押す。:(コロン)も含めて入力しよう。wは保存(write)、qは終了(quit)の意味である。
シェルの画面($ が表示された状態)に戻れば、保存は完了である。
念のため次のコマンドで、ファイルの内容を表示して確認しておこう。
cat exit42.asm貼り付けたコードが表示されればよい。
なお、viについては、次のキーを覚えておくと便利である。
| やりたいこと | 操作 |
|---|---|
| 入力モードにする | i |
| 入力モードを終わる | Esc |
| 保存して終了する | :wq と入力して Enter |
| 保存せずに終了する | :q! と入力して Enter |
| 保存だけする(終了しない) | :w と入力して Enter |
| 直前の操作を取り消す | u(Esc のあとで押す) |
| カーソルを動かす | 矢印キー |
Viのトラブルシューティング
- 文字を入力しても入らず、画面が勝手に動く:入力モードになっていない。
Escを押してからiを押す - 貼り付けたコードの字下げが階段状にずれた:アセンブラの動作には影響しないが、見づらい。
Escを押して:q!で保存せずに終了し、もう一度vi exit42.asmで開く。続けて:set pasteと入力してEnterを押し、そのあとでiを押して貼り付け直す - 終了できなくなった:
Escを何回か押してから、:q!と入力してEnterを押す。そのあと、最初からやり直そう
コードの意味
3つの命令だけの短いプログラムだが、意味を順に確認しよう。
global _start:_startというラベルを、リンカから見えるようにする。_startはプログラムの実行開始位置である。section .text:ここから先が、実行されるプログラム本体(命令)であることを示す。_start::実行開始位置の名前(ラベル)である。mov rax, 60:raxというレジスタに60を入れる。mov rdi, 42:rdiというレジスタに42を入れる。syscall:OS(Linuxカーネル)に処理を依頼する。
レジスタとは、CPUの内部にある小さな記憶領域で、計算や受け渡しの作業台として使われる。詳しくは第2回で扱うので、ここでは「値を一時的に置く場所」と理解しておけばよい。
syscall で依頼する処理は、rax に入れた番号で指定する。x86-64のLinuxでは、60番がプログラムの終了(exit)を表す。そして、終了コードは1つ目の引数として rdi に入れて渡す。つまり、このプログラムは「終了コード42でプログラムを終了せよ」とOSに依頼しているわけである。
C言語で次のように書けば、コードとほぼ同じである。
#include <stdlib.h>
int main(void) {
exit(42);
}4. 組み立てて実行する
アセンブリ言語のソースが実行できるようになるまでには、2つの段階がある。
- アセンブル:
nasmでソースをオブジェクトファイル(.o)に変換する - リンク:
ldでオブジェクトファイルを実行ファイルにする
nasm -f elf64 exit42.asm -o exit42.o
ld exit42.o -o exit42-f elf64 は、Linuxの64ビット実行ファイル形式(ELF)で出力する指定である。エラーが出なければ、実行ファイル exit42 ができている。実行して、終了コードを確認する。
./exit42
echo $?echo $? は、直前に実行したコマンドの終了コードを表示するコマンドである。
次のように表示されれば成功である。
42
このように最初は画面には何も表示されないが、アセンブラで書いたプログラムが動き、OSに終了コードを返したことが確認できた。
5. gdbで1命令ずつ動かしてみる
アセンブラの学習では、デバッガで1命令ずつ実行しながらレジスタを観察することが、理解の近道になる。gdb を使って、先ほどのプログラムの動きを追ってみよう。
gdb ./exit42gdb のプロンプト「(gdb)」が表示されたら、次のコマンドを順に入力しよう。
(gdb) break _start
(gdb) run
(gdb) stepi
(gdb) stepi
(gdb) info registers rax rdibreak _start:_startの位置で一時停止するよう設定する。run:プログラムを開始する。(_startで停止する)stepi:機械語の命令を1つだけ実行する。今回は2回実行する。info registers rax rdi:raxとrdiの中身を表示する。
stepi を2回実行したので、mov rax, 60 と mov rdi, 42 が実行済みである。
次のように、16進数と10進数で値が表示される。
rax 0x3c 60
rdi 0x2a 420x3c は16進数で、10進数の60である。0x2a も同様に、10進数の42である。コードに書いた値が、実際にレジスタに入っていることが確認できた。
確認が終わったら、以下の手順で gdb を終了しよう。
gdbを終了する
確認が終わったら、gdb を終了する。次のコマンドを入力して、Enter キーを押す。
(gdb) quitなお、プログラムが途中で止まっている状態で終了しようとすると、次のように確認が表示される。
A debugging session is active.
Inferior 1 [process 1234] will be killed.
Quit anyway? (y or n)
「実行中のプログラムを終了させてよいか」という確認なので、y と入力して Enter キーを押す。$ が表示されたシェルの画面に戻れば、終了である(process のあとの番号は、実行のたびに異なる)。なお、quit は、短く q と入力してもよい。
機械語も見てみる
最後に、objdump で実行ファイルを逆アセンブルし、機械語とアセンブリ言語の対応を見ておく。
objdump -d -M intel exit42実行例
exit42: ファイル形式 elf64-x86-64
セクション .text の逆アセンブル:
0000000000401000 <_start>:
401000: b8 3c 00 00 00 mov eax,0x3c
401005: bf 2a 00 00 00 mov edi,0x2a
40100a: 0f 05 syscall
左側に機械語(16進数の並び)、右側にアセンブリ言語の命令が並んで表示される。
たとえば syscall は、機械語では 0f 05 というたった2バイトである。アセンブラは、このような対応表にしたがってニーモニックを機械語に置き換えているわけである。
(参考)サンプルコード
参考として、記事のサンプルコードは筆者のGitHubリポジトリに置いてある。必要に応じて利用してほしい。第1回のコードは 01/exit42.asm である。
6. 今回のまとめ
- アセンブリ言語は、機械語の命令に人間が読める名前をつけたものである。
- アセンブラ(NASM)でオブジェクトファイルにし、リンカ(ld)で実行ファイルにする。
movでレジスタに値を入れ、syscallでOSに処理を依頼する。- x86-64のLinuxでは、
raxに60、rdiに終了コードを入れてsyscallすると、プログラムを終了できる。 gdbのstepiとinfo registersを使うと、レジスタの変化を目で確認できる。
次回は、レジスタと mov・算術命令を取り上げる。足し算や引き算をアセンブラで書き、システムコールを使って画面に “Hello, World!” を表示するところまで進める予定である。


