アセンブリ言語の概要について以下の記事で解説しましたが、アセンブリ言語の使い方の詳細は解説できていませんでした。
今回は初心者向けにアセンブリ言語の使い方のイメージが分かるように、書き方を1つずつ説明しながら、“Hello, world!”を出力する簡単なコードを作成・実行していきたいと思います。
本記事では、x86またはx86-64アーキテクチャ向けのNASM(Netwide Assembler)向けのアセンブリ言語の書き方で、Linux環境で動作するコードの作成を行います。 ※本記事では、Linuxディストリビューションとして「Ubuntu 22.04 LTS」を使用します。
必要パッケージのインストール
今回使用するアセンブラ(NASM)をインストールします。
sudo apt-get install nasm
コード作成
NASMでアセンブルするコードを作成していきます。
コードの作成については、以下の項目に分けて解説します。
- データセクションの定義
- テキストセクション開始の宣言とエントリポイントの指定
- テキストセクションの定義
1. データセクションの定義
まず最初に、データセクションの定義を行います。データセクションとは、プログラムで使用する定数や変数を格納するための場所です。今回の場合は”Hello, world!”の文字列がデータにあたります。
以下のように定義します。
section .data
msg db 'Hello, world!',0xA
len equ $ - msg
section .data
「section .data」でデータセクションの開始を宣言します。
msg db ‘Hello, world!’,0xA
「msg」はラベルで、’Hello, world!’という文字列と改行文字(0xAは改行文字のASCIIコード1)をメモリに格納します。
「db」(Define Byte)は指定された文字列をバイトとして格納することを指示します。
アセンブリ言語でのラベルとは、メモリアドレスに名前を付けたものです。高水準言語における変数名に近いです。 また、「db」はデータの型を示すもので、1バイトのデータを表します。他にも「dw」(2バイト)や「dd」(4バイト)があります。
len equ $ – msg
「len」はメッセージの長さを計算して格納するためのラベルです。
「$」は現在のアドレス(この場合は文字列の末尾)を表します。上記のコンテキストでは、msgラベルが定義された後のメモリアドレスを指します。
「msg」は文字列の開始アドレスです。「$ – msg」で文字列の長さを計算できます。
「equ」は”equate”(同等にする) の略で、指定されたシンボル2(この場合はlen)に定数値を割り当てるアセンブリのディレクティブです。C言語の定数定義(const)と近い意味を持ちます。
2. テキストセクション開始の宣言とエントリポイントの指定
次に、テキストセクション開始の宣言とエントリポイントの指定を行います。以下のように定義します。
section .text
global _start
section .text
「section .text」でテキストセクション(コードセクション)の開始の宣言を行います。
テキストセクションには、プログラムの実行可能な命令が含まれます。これは、プログラムがCPUによって実行される実際のコードです。
global _start
「global」ディレクティブは、指定されたシンボル(_start)を他のファイルやモジュールから参照可能にします。
「_start」はプログラムのエントリポイント(プログラムが実行されるときに最初に実行される部分)を指すラベルです。
3. テキストセクションの定義
最後にテキストセクションの処理内容を定義します。具体的には、エントリポイントである「_start」の処理内容のことです。
以下のように定義します。
_start:
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, len
int 0x80
mov eax, 1
xor ebx, ebx
int 0x80
内容についてそれぞれ説明します。
_start:
プログラムのエントリポイントを示します。これ以降に具体的な処理を記述していきます。
mov eax, 4 ~ int 0x80 までの処理
「mov eax, 4」 ~ 「int 0x80」 までの処理は、文字列の標準出力を行う処理です。
各命令について1つずつ説明します。
mov eax, 4
eaxレジスタに「4」を移動します。
「mov」命令は値をレジスタに移動することを意味しています。 (「mov レジスタ名, 値」のような使い方となります。) ここでの「4」はLinuxにおけるシステムコール3番号を表しており、sys_write(書き込み)を意味します。
mov ebx, 1
ebxレジスタに「1」を移動します。
ここでの「1」はsys_writeにおける書き込み先を表しており、標準出力を意味します。
mov ecx, msg
ecxレジスタにmsgラベルのアドレス(文字列’Hello, world!’の先頭アドレス)を移動します。
mov edx, len
edxレジスタに「len」(メッセージの長さ)を移動します。
int 0x80
システムコールを実行します。
「int」は「interrupt」(割り込み)の略で、割り込みをトリガーするために使用されます。「int」の後に割り込みベクタの番号を指定します。
「0x80」は割り込みベクタの番号であり、Linuxではシステムコールに割り当てられています。
割り込みベクタとは割り込み処理のアドレスを格納するために使用されるデータ構造のことを指します。番号と割り込み処理のアドレスの対応表のことを割り込みベクタテーブルと呼びます。 x86アーキテクチャのLinuxの割り込みベクタテーブルでは、0x80番にシステムコールのアドレスが割り当てられています。
「int 0x80」でシステムコールを実行するとき、これまでに値を設定したeax, ebx, ecx, edxのレジスタをそれぞれ参照します。
eaxがシステムコール番号を表し、ebx, ecx, edxはeaxに対する引数で、システムコール番号によって意味が異なります。
今回のコードでは、sys_write(書き込み)を行う「4」がeaxに格納されます。
sys_writeにおいて、ebx, ecx, edxはそれぞれ以下のような意味を持ちます。
- ebx:書き込み先を表す。
- ecx:書き込みを行うデータのアドレスを表す。
- edx:書き込みを行うデータの長さを表す。
「int」命令は、ISAではなくOSの命令です。アセンブリ言語の一部の命令(システムコールなど)はOS依存のものとなっている場合があります。
mov eax, 1 ~ int 0x80 までの処理
「mov eax, 1」 ~ 「int 0x80」 までの処理は、プログラムを正常に終了させるための処理です。これがないと「Segmentation fault」が生じ、プログラムが正常に終了しません。
各命令について1つずつ説明します。
mov eax, 1
eaxレジスタに「1」を移動します。
ここでの「1」はLinuxにおけるシステムコール番号を表しており、sys_exit(プログラムの終了)を意味します。
xor ebx, ebx
ebxレジスタの値同士の排他的論理和を行い、ebxレジスタに格納します。同じレジスタの値同士で排他的論理和を行うと、結果は常に0になり、これによりebxレジスタがゼロクリアされます。
ここでの「0」はsys_exitにおける終了コード(正常終了)を意味します。 今回のプログラムは常に正常終了となります。 アセンブリ言語では、レジスタをゼロクリアするためにレジスタ同士の排他的論理和の結果を格納するのが慣習となっています。
int 0x80
システムコールを実行します。
eaxレジスタに「1」が設定されているため、sys_exit(プログラムの終了)を実行します。
また、ebxレジスタに設定された値(0)を終了コードとして返します。
完成コード
完成したコードは以下のようになります。以下のコードをhello.asmに保存します。
section .data
msg db 'Hello, world!',0xA
len equ $ - msg
section .text
global _start
_start:
mov eax, 4
mov ebx, 1
mov ecx, msg
mov edx, len
int 0x80
mov eax, 1
xor ebx, ebx
int 0x80
アセンブル&実行
コード作成の章で作成したコード(hello.asm)をアセンブルして実行していきたいと思います。
まず、nasmコマンドでアセンブルを行い、オブジェクトファイル(hello.o)を作成します。
nasm -f elf64 hello.asm -o hello.o
「-f elf64」オプションを付けることで、NASMが64ビットのELF形式4でコードをアセンブルするよう指示します。32ビットシステムの場合は「-f elf32」を付けます。
次に、ldコマンドでオブジェクトファイルを実行可能ファイル(hello)にリンクします。
ld hello.o -o hello
作成された実行可能ファイル(hello)を実行します。
./hello
Hello, world!
アセンブル&実行は以上となります。
まとめ
高水準言語では1行で終わる”Hello, world!”の出力ですが、アセンブリ言語ではレジスタに1つずつ値を設定するという面倒さがありますね。
ただ、1つ1つルールを理解していけば、アセンブリ言語を理解するのはそこまで時間がかからないと思います。
色んなコードの読み書きを行って慣れていくことが、言語習得のために最も重要だと思います。