やさしいアセンブラ入門(第1回)~アセンブラとは・環境準備・最初のプログラム~【GitHub対応】

コンピュータのしくみ

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が使える場合は、この手順は読み飛ばしてください。

  1. スタートメニューから「PowerShell」を検索し、管理者として実行する。
  2. 次のコマンドを実行する。
wsl --install -d Ubuntu
  1. 再起動を求められたら、パソコンを再起動する。
  2. Ubuntuのウィンドウが開いたら、ユーザー名とパスワードを入力して登録する。
    ウィンドウが開かない場合は、スタートメニューから「Ubuntu」を起動する。

パスワードは、入力しても画面には何も表示されないが、入力は受け付けられている。
なお、インストール直後の動きは、パソコンの状態によって異なる。

PowerShellで次のコマンドを実行し、VERSION が 2 になっていることを確認しよう。

wsl -l -v

実行例

  NAME            STATE           VERSION
*
Ubuntu-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は、キー入力で「入力するモード」と「操作するモード」が切り替わるのが特徴である。
慣れないうちは戸惑うかもしれないが、この連載で使う操作は、限られている。
なお、ほかのエディタに慣れている方は、お好きなエディタで同じ内容のファイルを作ってみよう。

  1. ファイルを開く:次のコマンドを実行する。このファイルが未作成の場合は、新しく作られる。
   vi exit42.asm
  1. 入力モードにする:キーボードの i を押す。
    画面の左下に -- INSERT -- と表示されたら、文字を入力できる状態である。
  2. コードを貼り付ける:ブラウザで、上のコードエリア内をクリックして、コードを範囲選択(Ctrl + A)して、コピー(Ctrl + C)する。
    次に、WSL(Ubuntu)のウィンドウを表示して、右クリック(または Ctrl + Shift + V)で貼り付ける。もちろん直接入力してもよいが、貼り付けのほうが入力ミスを防げる。
  3. 入力モードを終わる:Esc キーを押す。左下の -- INSERT -- の表示が消える。
  4. 保存して終了する:「: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つの段階がある。

  1. アセンブル:nasm でソースをオブジェクトファイル(.o)に変換する
  2. リンク: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 ./exit42

gdb のプロンプト「(gdb)」が表示されたら、次のコマンドを順に入力しよう。

(gdb) break _start
(gdb) run
(gdb) stepi
(gdb) stepi
(gdb) info registers rax rdi
  • break _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                42

0x3c は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!” を表示するところまで進める予定である。

タイトルとURLをコピーしました