Apple Siliconアセンブリ言語入門ガイド 第1回:基礎知識と環境構築

はじめに

Apple SiliconはAppleが独自に設計したARMアーキテクチャベースのプロセッサです。2020年のM1チップ以降、MacはIntelプロセッサからApple Siliconへと完全に移行しました。このガイドでは、Apple Silicon(ARM64アーキテクチャ)でのアセンブリ言語プログラミングを基礎から丁寧に学んでいきます。

アセンブリ言語は、プロセッサが直接理解できる機械語に最も近い低レベル言語です。学習することで、コンピュータの動作原理を深く理解でき、パフォーマンスが重要な場面や、OSの内部動作を知りたい場合に役立ちます。

Apple Siliconの特徴とアーキテクチャ

ARM64アーキテクチャとは

Apple SiliconはARM64(AArch64とも呼ばれる)命令セットアーキテクチャを採用しています。これは従来のIntel x86-64とは全く異なる設計思想に基づいています。

主な特徴:

  1. RISC(Reduced Instruction Set Computer)設計
    • 命令セットがシンプルで規則性が高い
    • 1つの命令で1つの処理を行う
    • 命令の実行速度が均一で予測しやすい
  2. 豊富なレジスタ
    • 31個の汎用64ビットレジスタ(X0-X30)
    • 各レジスタの下位32ビットにW0-W30としてアクセス可能
    • x86-64の16個と比べて作業領域が広い
  3. 固定長命令
    • すべての命令が32ビット(4バイト)
    • 命令のデコードが高速
    • 分岐予測が効率的
  4. ロード/ストアアーキテクチャ
    • 演算はレジスタ間でのみ実行
    • メモリアクセスは専用のロード/ストア命令で行う
    • x86のようにメモリを直接演算に使えない

Intel x86-64との違い

従来のIntel Macを使っていた方のために、主な違いを整理します:

特徴ARM64 (Apple Silicon)x86-64 (Intel)
設計思想RISCCISC
命令長固定(32ビット)可変(1-15バイト)
レジスタ数31個16個
メモリアクセスロード/ストア専用命令多くの命令で直接可能
呼び出し規約最大8個をレジスタ渡し最大6個をレジスタ渡し

開発環境の準備

必要なツール

Apple Siliconでアセンブリプログラミングを始めるには、以下のツールが必要です:

  1. Xcode Command Line Tools - アセンブラ、リンカなどが含まれる
  2. テキストエディタ - VSCode、Vim、Emacsなど任意
  3. ターミナル - コマンド実行用

インストール手順

ターミナルを開いて以下のコマンドを実行します:

# Xcodeコマンドラインツールのインストール
xcode-select --install

ダイアログが表示されるので、指示に従ってインストールを完了させます。

インストール確認:

# アセンブラの確認
as --version
# 出力例: Apple clang version 15.0.0

# リンカの確認
ld -v
# 出力例: @(#)PROGRAM:ld PROJECT:ld-1053.12

# コンパイラの確認(C言語と混在させる場合に使用)
clang --version

開発ワークフローの基本

アセンブリプログラムの開発は以下の流れで行います:

  1. エディタでソースコード作成 - .s または .asm 拡張子で保存
  2. アセンブル - ソースコードをオブジェクトファイルに変換
  3. リンク - オブジェクトファイルを実行可能ファイルに変換
  4. 実行 - プログラムを動かして動作確認
  5. デバッグ - 必要に応じてlldbでデバッグ

レジスタの詳細

汎用レジスタ(64ビット:X0-X30)

ARM64には31個の汎用レジスタがあり、それぞれ特定の用途が推奨されています:

引数と戻り値用レジスタ

  • X0-X7: 関数の引数(第1引数から第8引数)
    • X0: 第1引数、かつ関数の戻り値
    • X1: 第2引数
    • X2-X7: 第3-8引数
  • X8: 間接的な戻り値用(構造体を返す場合など)

一時レジスタ(Caller-saved)

  • X9-X15: 関数呼び出しで保存不要
    • 関数内で自由に使える
    • 関数を呼ぶ側が保存する責任がある

プラットフォームレジスタ

  • X16, X17: IPレジスタ(Intra-Procedure-call registers)
    • システムコール番号の格納(X16)
    • ダイナミックリンカが使用
  • X18: プラットフォーム予約レジスタ
    • 使用しないこと

保存レジスタ(Callee-saved)

  • X19-X28: 関数呼び出し時に保存が必要
    • 関数内で使う場合は、最初に保存して最後に復元
    • 呼ばれる側が保存する責任がある

特殊用途レジスタ

  • X29: FP(Frame Pointer)
    • スタックフレームの基底アドレス
    • デバッグに便利
  • X30: LR(Link Register)
    • 関数の戻りアドレスを保存
    • BL命令実行時に自動設定される
  • SP: スタックポインタ
    • スタックの先頭を指す
    • 独立したレジスタ(X31ではない)

32ビットレジスタ(W0-W30)

各64ビットレジスタの下位32ビットには、W0-W30としてアクセスできます:

mov     x0, #0x123456789ABCDEF0  // 64ビット値を設定
mov     w0, #42                   // 下位32ビットのみ設定(上位32ビットはゼロクリア)

重要な注意点:

  • Wレジスタへの書き込みは、対応するXレジスタの上位32ビットをゼロクリアします
  • これは意図しないバグの原因になることがあるので注意

その他の重要なレジスタ

ゼロレジスタ

  • XZR/WZR: 読み出すと常に0、書き込みは無視される
    • 比較やゼロ初期化に便利

プログラムカウンタ

  • PC: 現在実行中の命令のアドレス
    • 直接アクセスできないが、相対アドレッシングで使用

ステータスレジスタ

  • PSTATE: プロセッサの状態を保持
    • 条件フラグ(N, Z, C, V)を含む

基本的な命令セット

データ移動命令

MOV命令 - レジスタへの値の設定

mov     x0, #42              // x0 = 42(即値)
mov     x1, x0               // x1 = x0(レジスタコピー)
mov     w2, #100             // w2 = 100(32ビット)

MOV命令には制限があり、大きな即値は直接設定できません。16ビットの即値のみ使用可能です。

MOVZ, MOVK, MOVN - 大きな値の設定

// 64ビットの大きな値を設定する方法
movz    x0, #0x1234, lsl #48  // x0 = 0x1234000000000000
movk    x0, #0x5678, lsl #32  // x0 = 0x1234567800000000
movk    x0, #0x9ABC, lsl #16  // x0 = 0x123456789ABC0000
movk    x0, #0xDEF0           // x0 = 0x123456789ABCDEF0

// または
movz    x1, #0xFFFF           // x1 = 0x000000000000FFFF
movn    x2, #0                // x2 = 0xFFFFFFFFFFFFFFFF(-1)

算術演算命令

加算と減算

add     x0, x1, x2           // x0 = x1 + x2
add     x0, x1, #100         // x0 = x1 + 100
sub     x0, x1, x2           // x0 = x1 - x2
sub     x0, x1, #50          // x0 = x1 - 50

// フラグを更新する加算・減算
adds    x0, x1, x2           // x0 = x1 + x2、フラグ更新
subs    x0, x1, x2           // x0 = x1 - x2、フラグ更新

乗算と除算

mul     x0, x1, x2           // x0 = x1 * x2
sdiv    x0, x1, x2           // x0 = x1 / x2(符号付き)
udiv    x0, x1, x2           // x0 = x1 / x2(符号なし)

// 余りを求める場合は、除算後に計算が必要
udiv    x3, x1, x2           // x3 = x1 / x2
msub    x4, x3, x2, x1       // x4 = x1 - (x3 * x2) = 余り

論理演算命令

and     x0, x1, x2           // x0 = x1 & x2(AND)
orr     x0, x1, x2           // x0 = x1 | x2(OR)
eor     x0, x1, x2           // x0 = x1 ^ x2(XOR)
mvn     x0, x1               // x0 = ~x1(NOT)

// 即値との論理演算
and     x0, x1, #0xFF        // x0 = x1 & 0xFF
orr     x0, x1, #0xF0        // x0 = x1 | 0xF0

シフト命令

lsl     x0, x1, #4           // x0 = x1 << 4(論理左シフト)
lsr     x0, x1, #4           // x0 = x1 >> 4(論理右シフト)
asr     x0, x1, #4           // x0 = x1 >> 4(算術右シフト、符号保持)
ror     x0, x1, #4           // x0 = x1を4ビット右回転

メモリアクセス命令

ロード命令(メモリからレジスタへ)

ldr     x0, [x1]             // x0 = *x1(64ビット)
ldr     w0, [x1]             // w0 = *x1(32ビット)
ldrh    w0, [x1]             // w0 = *x1(16ビット、符号なし拡張)
ldrb    w0, [x1]             // w0 = *x1(8ビット、符号なし拡張)
ldrsb   x0, [x1]             // x0 = *x1(8ビット、符号拡張)

ストア命令(レジスタからメモリへ)

str     x0, [x1]             // *x1 = x0(64ビット)
str     w0, [x1]             // *x1 = w0(32ビット)
strh    w0, [x1]             // *x1 = w0(16ビット)
strb    w0, [x1]             // *x1 = w0(8ビット)

オフセット付きアクセス

ldr     x0, [x1, #8]         // x0 = *(x1 + 8)
str     x0, [x1, #16]        // *(x1 + 16) = x0
ldr     x0, [x1, x2]         // x0 = *(x1 + x2)

プログラムの基本構造

アセンブリファイルの構成

Apple Siliconのアセンブリプログラムは以下の要素で構成されます:

// コメントは//または#で開始

// ディレクティブ - アセンブラへの指示
.global _main               // シンボルをグローバルにする
.align 2                    // 4バイト境界にアライン(2^2=4)

// テキストセクション - コードを記述
.text
_main:                      // ラベル定義
    // ここに命令を記述
    mov     x0, #0
    ret

// データセクション - 初期化済みデータ
.data
message:
    .ascii  "Hello\n"       // 文字列データ
counter:
    .quad   0               // 64ビット整数

// BSSセクション - 未初期化データ
.bss
buffer:
    .skip   1024            // 1024バイト確保

エントリポイント

macOSでは、プログラムのエントリポイントは _main という名前である必要があります(アンダースコアに注意):

.global _main
.align 2

_main:
    // プログラムのコード
    mov     x0, #0          // 終了コード
    mov     x16, #1         // exit システムコール
    svc     #0x80           // システムコール実行

システムコールの使い方

macOSでシステムコールを呼び出すには:

  1. X16にシステムコール番号を設定
  2. X0-X7に引数を設定
  3. svc #0x80 を実行

主なシステムコール番号:

  • 1: exit - プログラム終了
  • 3: read - ファイル読み込み
  • 4: write - ファイル書き込み
// write(1, message, length)
mov     x0, #1              // ファイルディスクリプタ(stdout)
adrp    x1, message@PAGE    // メッセージアドレス
add     x1, x1, message@PAGEOFF
mov     x2, #13             // 長さ
mov     x16, #4             // writeシステムコール
svc     #0x80

最初のプログラム:Hello World

それでは、実際に動くHello Worldプログラムを作成してみましょう。

ソースコード作成

hello.s という名前でファイルを作成し、以下のコードを記述します:

// hello.s - Apple Silicon用Hello Worldプログラム
// システムコールを使って"Hello, World!"を出力する

.global _main               // エントリポイントをグローバルに
.align 2                    // 命令は4バイト境界にアライン

_main:
    // write(1, message, 14) システムコール
    mov     x0, #1          // 第1引数: ファイルディスクリプタ(1 = stdout)
    adrp    x1, message@PAGE        // 第2引数: メッセージアドレス(ページ)
    add     x1, x1, message@PAGEOFF // メッセージアドレス(オフセット)
    mov     x2, #14         // 第3引数: 書き込むバイト数
    mov     x16, #4         // システムコール番号(4 = write)
    svc     #0x80           // システムコール実行

    // exit(0) システムコール
    mov     x0, #0          // 終了コード(0 = 正常終了)
    mov     x16, #1         // システムコール番号(1 = exit)
    svc     #0x80           // システムコール実行

// データセクション
.data
message:
    .ascii  "Hello, World!\n"  // 出力する文字列

プログラムの解説

ADRPとADD命令について

Apple Siliconでは、メモリアドレスを取得するのに2段階の処理が必要です:

adrp    x1, message@PAGE        // ページアドレス(4KBアライン)取得
add     x1, x1, message@PAGEOFF // ページ内オフセットを加算

これは、ARM64がPIC(Position Independent Code)を前提としているためです。

ビルド手順

ターミナルで以下のコマンドを実行します:

# 1. アセンブル(.s → .o)
as -o hello.o hello.s

# 2. リンク(.o → 実行ファイル)
ld -o hello hello.o \
   -lSystem \
   -syslibroot `xcrun -sdk macosx --show-sdk-path` \
   -e _main \
   -arch arm64

# 3. 実行
./hello

出力:

Hello, World!

ビルドオプションの説明

  • -o hello.o: 出力ファイル名を指定
  • -lSystem: Systemライブラリをリンク(システムコールに必要)
  • -syslibroot: SDKのパスを指定
  • -e _main: エントリポイントを指定
  • -arch arm64: ターゲットアーキテクチャを指定

Makefileの作成

毎回長いコマンドを打つのは面倒なので、Makefileを作成しましょう:

# Makefile for Apple Silicon Assembly

AS = as
LD = ld
SDK = $(shell xcrun -sdk macosx --show-sdk-path)

ASFLAGS = 
LDFLAGS = -lSystem -syslibroot $(SDK) -e _main -arch arm64

TARGET = hello
SRCS = hello.s
OBJS = $(SRCS:.s=.o)

all: $(TARGET)

$(TARGET): $(OBJS)
	$(LD) -o $@ $^ $(LDFLAGS)

%.o: %.s
	$(AS) $(ASFLAGS) -o $@ $<

clean:
	rm -f $(OBJS) $(TARGET)

run: $(TARGET)
	./$(TARGET)

.PHONY: all clean run

使い方:

make          # ビルド
make run      # ビルドして実行
make clean    # クリーンアップ

実践例:数値の計算

例1:簡単な足し算

// add_numbers.s - 2つの数を足す
.global _main
.align 2

_main:
    mov     x0, #10         // x0 = 10
    mov     x1, #32         // x1 = 32
    add     x2, x0, x1      // x2 = 10 + 32 = 42
    
    // 結果(42)を終了コードとして返す
    mov     x0, x2
    mov     x16, #1
    svc     #0x80

実行後、終了コードを確認:

./add_numbers
echo $?      # 42が表示される

例2:複雑な計算式

計算式:result = (a + b) * c - d / e を実装

// calculate.s - 複雑な計算
.global _main
.align 2

_main:
    // 変数の初期化
    mov     x0, #10         // a = 10
    mov     x1, #5          // b = 5
    mov     x2, #3          // c = 3
    mov     x3, #20         // d = 20
    mov     x4, #4          // e = 4
    
    // (a + b) を計算
    add     x5, x0, x1      // x5 = 10 + 5 = 15
    
    // (a + b) * c を計算
    mul     x6, x5, x2      // x6 = 15 * 3 = 45
    
    // d / e を計算
    udiv    x7, x3, x4      // x7 = 20 / 4 = 5
    
    // 最終結果: (a + b) * c - d / e
    sub     x8, x6, x7      // x8 = 45 - 5 = 40
    
    // 結果を終了コードで返す
    mov     x0, x8
    mov     x16, #1
    svc     #0x80

デバッグ方法

lldbを使ったデバッグ

アセンブリプログラムもlldbでデバッグできます:

# デバッグシンボル付きでビルド
as -g -o hello.o hello.s
ld -o hello hello.o -lSystem -syslibroot `xcrun -sdk macosx --show-sdk-path` -e _main -arch arm64

# lldbでデバッグ開始
lldb hello

lldbの基本コマンド:

(lldb) breakpoint set --name _main    # ブレークポイント設定
(lldb) run                            # 実行開始
(lldb) register read                  # 全レジスタ表示
(lldb) register read x0               # x0レジスタのみ表示
(lldb) memory read $sp                # スタックメモリ表示
(lldb) stepi                          # 1命令実行
(lldb) continue                       # 続行
(lldb) quit                           # 終了

printfを使ったデバッグ

C言語のprintf関数を呼び出すこともできます:

// printf_debug.s - printfでデバッグ
.global _main
.align 2

_main:
    // スタックフレーム設定
    stp     x29, x30, [sp, #-16]!
    mov     x29, sp
    
    mov     x0, #42
    mov     x1, x0              // 表示したい値
    adrp    x0, format@PAGE
    add     x0, x0, format@PAGEOFF
    bl      _printf             // printfを呼び出し
    
    // スタックフレーム復元
    ldp     x29, x30, [sp], #16
    
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

.data
format:
    .asciz  "Value: %ld\n"      // フォーマット文字列

ビルド時にCライブラリをリンク:

clang -o printf_debug printf_debug.s
./printf_debug
# 出力: Value: 42

まとめ

第1回では、Apple Siliconアセンブリ言語プログラミングの基礎を学びました:

学んだこと:

  • ARM64アーキテクチャの特徴(RISC設計、豊富なレジスタ、固定長命令)
  • 開発環境の準備(Xcode Command Line Toolsのインストール)
  • レジスタの種類と役割(X0-X30の用途、W0-W30との関係)
  • 基本的な命令セット(MOV、ADD、SUB、MUL、DIV、論理演算、シフト、メモリアクセス)
  • プログラムの基本構造(ディレクティブ、セクション、エントリポイント)
  • システムコールの使い方(write、exit)
  • Hello Worldプログラムの作成とビルド
  • 実践的な計算プログラム
  • デバッグ方法(lldb、printf)

重要なポイント:

  1. Apple SiliconはARM64アーキテクチャで、Intel x86-64とは大きく異なる
  2. レジスタが豊富で、用途に応じた使い分けが重要
  3. メモリアクセスは専用のロード/ストア命令で行う
  4. システムコールはX16に番号を設定してsvc #0x80で呼び出す
  5. アドレス取得にはADRP+ADDの2段階処理が必要

次回予告: 第2回では、より詳しい命令セット、条件分岐、ループ処理について学びます。また、関数の作成方法や呼び出し規約についても詳しく解説します。

練習問題:

  1. 3つの数(a=15, b=7, c=3)の平均値を計算するプログラムを書いてみましょう
  2. 2つの数を交換するプログラムを書いてみましょう
  3. ビット演算を使って、ある数が2の累乗かどうかを判定するプログラムを書いてみましょう

これらの練習問題に取り組むことで、第1回で学んだ内容をより深く理解できます。次回もお楽しみに!

\ 最新情報をチェック /

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です