Apple Siliconアセンブリ言語入門ガイド 第2回:制御構造と分岐処理

はじめに

第1回では、Apple Siliconの基礎知識、レジスタ、基本的な命令セットについて学びました。第2回では、プログラミングに欠かせない制御構造について深く掘り下げていきます。

制御構造とは、プログラムの実行フローを制御する仕組みです。条件分岐(if文)、ループ(for、while)、関数呼び出しなど、高級言語で当たり前に使っている機能を、アセンブリ言語レベルでどう実装するのかを学びます。

プロセッサステータスレジスタ(PSTATE)

条件フラグの理解

ARM64プロセッサには、演算結果の状態を記録する条件フラグがあります。これらは PSTATE レジスタに格納され、条件分岐の判定に使用されます。

主な条件フラグ:

  1. N(Negative)フラグ
    • 演算結果が負の場合に1になる
    • 結果の最上位ビットがセットされた場合
  2. Z(Zero)フラグ
    • 演算結果がゼロの場合に1になる
    • 比較命令で2つの値が等しい場合
  3. C(Carry)フラグ
    • 符号なし演算でキャリーまたはボローが発生した場合に1
    • 加算でオーバーフロー、減算でアンダーフロー
  4. V(Overflow)フラグ
    • 符号付き演算でオーバーフローが発生した場合に1
    • 結果が表現可能な範囲を超えた場合

フラグを設定する命令

フラグを更新する命令には、通常命令に「s」サフィックスを付けたものと、専用の比較命令があります:

// フラグを更新する算術命令
adds    x0, x1, x2      // x0 = x1 + x2、フラグ更新
subs    x0, x1, x2      // x0 = x1 - x2、フラグ更新
ands    x0, x1, x2      // x0 = x1 & x2、フラグ更新

// フラグを更新しない通常の命令
add     x0, x1, x2      // x0 = x1 + x2、フラグ変更なし
sub     x0, x1, x2      // x0 = x1 - x2、フラグ変更なし
and     x0, x1, x2      // x0 = x1 & x2、フラグ変更なし

比較命令

比較命令は、演算結果を破棄してフラグのみを更新する特殊な命令です:

// CMP - 比較(減算してフラグのみ更新)
cmp     x0, x1          // x0 - x1 を計算、結果は破棄、フラグ更新
cmp     x0, #42         // x0 - 42 を計算

// CMN - 比較(加算してフラグのみ更新)
cmn     x0, x1          // x0 + x1 を計算、結果は破棄、フラグ更新

// TST - テスト(ANDしてフラグのみ更新)
tst     x0, #0xFF       // x0 & 0xFF を計算、結果は破棄、フラグ更新
tst     x0, x1          // x0 & x1 を計算

使用例:

.global _main
.align 2

_main:
    mov     x0, #10
    mov     x1, #20
    
    // x0 と x1 を比較
    cmp     x0, x1          // 10 - 20 = -10
                            // N=1(負)、Z=0(非ゼロ)、C=0、V=0
    
    // x0 がゼロかテスト
    cmp     x0, #0          // 10 - 0 = 10
                            // N=0(正)、Z=0(非ゼロ)
    
    // x0 の最下位バイトをテスト
    tst     x0, #0xFF       // 10 & 0xFF = 10
                            // N=0、Z=0(非ゼロ)
    
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

条件コードと条件分岐

条件コード一覧

ARM64では、フラグの状態に基づいて様々な条件を判定できます:

条件コード意味フラグ条件用途
EQEqual(等しい)Z == 1a == b
NENot Equal(等しくない)Z == 0a != b
GTGreater Than(より大きい)Z == 0 && N == Va > b(符号付き)
GEGreater or Equal(以上)N == Va >= b(符号付き)
LTLess Than(より小さい)N != Va < b(符号付き)
LELess or Equal(以下)Z == 1 || N != Va <= b(符号付き)
HIHigher(より大きい)C == 1 && Z == 0a > b(符号なし)
HS/CSHigher or Same(以上)C == 1a >= b(符号なし)
LO/CCLower(より小さい)C == 0a < b(符号なし)
LSLower or Same(以下)C == 0 || Z == 1a <= b(符号なし)
MIMinus(負)N == 1a < 0
PLPlus(正またはゼロ)N == 0a >= 0
VSOverflow SetV == 1オーバーフロー発生
VCOverflow ClearV == 0オーバーフローなし
ALAlways(常に真)-無条件

分岐命令

無条件分岐:

b       label           // labelへジャンプ
bl      function        // functionを呼び出し(戻りアドレスをLRに保存)
br      x0              // x0が指すアドレスへジャンプ
blr     x0              // x0が指すアドレスを呼び出し
ret                     // LR(x30)に保存されたアドレスへ戻る

条件付き分岐:

b.eq    label           // Z == 1 なら label へジャンプ
b.ne    label           // Z == 0 なら label へジャンプ
b.gt    label           // a > b なら label へジャンプ(符号付き)
b.ge    label           // a >= b なら label へジャンプ(符号付き)
b.lt    label           // a &lt; b なら label へジャンプ(符号付き)
b.le    label           // a &lt;= b なら label へジャンプ(符号付き)
b.hi    label           // a > b なら label へジャンプ(符号なし)
b.lo    label           // a &lt; b なら label へジャンプ(符号なし)

実践例:if文の実装

C言語のコード:

int x = 10;
int y = 20;
int max;

if (x > y) {
    max = x;
} else {
    max = y;
}

アセンブリ実装:

.global _main
.align 2

_main:
    mov     x0, #10         // x = 10
    mov     x1, #20         // y = 20
    
    // if (x > y)
    cmp     x0, x1          // x と y を比較
    b.le    else_block      // x &lt;= y なら else へ
    
    // then部分: max = x
    mov     x2, x0          // max = x
    b       end_if          // ifの終わりへ
    
else_block:
    // else部分: max = y
    mov     x2, x1          // max = y
    
end_if:
    // x2 にmaxの値が入っている
    
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

実践例:複数条件のif-else if-else

C言語のコード:

int score = 75;
char grade;

if (score >= 90) {
    grade = 'A';
} else if (score >= 80) {
    grade = 'B';
} else if (score >= 70) {
    grade = 'C';
} else {
    grade = 'F';
}

アセンブリ実装:

.global _main
.align 2

_main:
    mov     x0, #75         // score = 75
    
    // if (score >= 90)
    cmp     x0, #90
    b.lt    check_80        // score &lt; 90 なら次の条件へ
    mov     x1, #'A'        // grade = 'A'
    b       end_grade
    
check_80:
    // else if (score >= 80)
    cmp     x0, #80
    b.lt    check_70
    mov     x1, #'B'        // grade = 'B'
    b       end_grade
    
check_70:
    // else if (score >= 70)
    cmp     x0, #70
    b.lt    grade_f
    mov     x1, #'C'        // grade = 'C'
    b       end_grade
    
grade_f:
    // else
    mov     x1, #'F'        // grade = 'F'
    
end_grade:
    // x1 に成績が入っている
    
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

条件付き実行命令

ARM64には、分岐を使わずに条件に応じて値を選択できる便利な命令があります。これらは分岐予測ミスを避けるため、パフォーマンス向上に役立ちます。

CSEL - 条件付き選択

// csel dest, true_val, false_val, condition
// 条件が真なら true_val、偽なら false_val を dest に格納

csel    x0, x1, x2, gt  // x0 = (条件 > なら) ? x1 : x2

実践例:最大値を求める

// int max(int a, int b)
.global _max
.align 2

_max:
    cmp     x0, x1          // a と b を比較
    csel    x0, x0, x1, gt  // a > b なら x0、そうでなければ x1
    ret

分岐版と比較してみましょう:

// 分岐を使った実装(遅い)
_max_branch:
    cmp     x0, x1
    b.le    return_b
    ret                     // a を返す
return_b:
    mov     x0, x1          // b を返す
    ret

// CSEL を使った実装(速い)
_max_csel:
    cmp     x0, x1
    csel    x0, x0, x1, gt  // 分岐なし
    ret

その他の条件付き命令

// CSINC - 条件付き選択して増分
csinc   x0, x1, x2, eq  // x0 = (eq) ? x1 : (x2 + 1)

// CSINV - 条件付き選択して反転
csinv   x0, x1, x2, ne  // x0 = (ne) ? x1 : ~x2

// CSNEG - 条件付き選択して符号反転
csneg   x0, x1, x2, gt  // x0 = (gt) ? x1 : -x2

// CINC - 条件付き増分
cinc    x0, x1, eq      // x0 = (eq) ? (x1 + 1) : x1

// CINV - 条件付き反転
cinv    x0, x1, eq      // x0 = (eq) ? ~x1 : x1

// CNEG - 条件付き符号反転
cneg    x0, x1, lt      // x0 = (lt) ? -x1 : x1

実践例:絶対値を求める

// int abs(int x)
.global _abs
.align 2

_abs:
    cmp     x0, #0          // x と 0 を比較
    cneg    x0, x0, lt      // x &lt; 0 なら x0 = -x0、そうでなければそのまま
    ret

ループ処理

カウンタを使ったループ(for文)

C言語のコード:

int sum = 0;
for (int i = 1; i &lt;= 10; i++) {
    sum += i;
}

アセンブリ実装:

.global _main
.align 2

_main:
    mov     x0, #0          // sum = 0
    mov     x1, #1          // i = 1
    
loop_start:
    cmp     x1, #10         // i と 10 を比較
    b.gt    loop_end        // i > 10 なら終了
    
    add     x0, x0, x1      // sum += i
    add     x1, x1, #1      // i++
    b       loop_start      // ループ先頭へ
    
loop_end:
    // x0 に 1+2+...+10 = 55 が入っている
    
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

条件判定ループ(while文)

C言語のコード:

int n = 100;
int count = 0;

while (n > 0) {
    n = n / 2;
    count++;
}

アセンブリ実装:

.global _main
.align 2

_main:
    mov     x0, #100        // n = 100
    mov     x1, #0          // count = 0
    
while_loop:
    cmp     x0, #0          // n > 0 ?
    b.le    while_end       // n &lt;= 0 なら終了
    
    lsr     x0, x0, #1      // n = n / 2(右シフト)
    add     x1, x1, #1      // count++
    b       while_loop
    
while_end:
    // x1 に count が入っている
    
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

do-while文の実装

C言語のコード:

int n = 5;
int result = 1;

do {
    result *= 2;
    n--;
} while (n > 0);

アセンブリ実装:

.global _main
.align 2

_main:
    mov     x0, #5          // n = 5
    mov     x1, #1          // result = 1
    
do_loop:
    lsl     x1, x1, #1      // result *= 2(左シフト)
    sub     x0, x0, #1      // n--
    
    cmp     x0, #0          // n > 0 ?
    b.gt    do_loop         // n > 0 なら繰り返す
    
    // x1 に result = 32 が入っている
    
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

CBZとCBNZ - ゼロ比較分岐

頻繁に使われるゼロとの比較には、専用の命令があります:

// CBZ - Compare and Branch if Zero
cbz     x0, label       // x0 == 0 なら label へジャンプ

// CBNZ - Compare and Branch if Not Zero
cbnz    x0, label       // x0 != 0 なら label へジャンプ

ループの最適化例:

// 改善前
_loop_old:
    cmp     x0, #0
    b.eq    end
    // 処理
    sub     x0, x0, #1
    b       _loop_old
end:

// 改善後
_loop_new:
    cbz     x0, end
    // 処理
    sub     x0, x0, #1
    b       _loop_new
end:

実践例:アルゴリズムの実装

例1:階乗の計算

C言語のコード:

int factorial(int n) {
    int result = 1;
    for (int i = 2; i &lt;= n; i++) {
        result *= i;
    }
    return result;
}

アセンブリ実装:

// int factorial(int n)
// x0: 引数 n
// 戻り値: x0
.global _factorial
.align 2

_factorial:
    mov     x1, #1          // result = 1
    mov     x2, #2          // i = 2
    
    cmp     x0, #1          // n &lt;= 1 ?
    b.le    fact_end        // n &lt;= 1 なら終了
    
fact_loop:
    cmp     x2, x0          // i &lt;= n ?
    b.gt    fact_end        // i > n なら終了
    
    mul     x1, x1, x2      // result *= i
    add     x2, x2, #1      // i++
    b       fact_loop
    
fact_end:
    mov     x0, x1          // 結果を返す
    ret

.global _main
_main:
    stp     x29, x30, [sp, #-16]!
    mov     x29, sp
    
    mov     x0, #5          // 5! を計算
    bl      _factorial      // 関数呼び出し
    
    // x0 に 120 が入っている
    
    ldp     x29, x30, [sp], #16
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

例2:最大公約数(ユークリッドの互除法)

C言語のコード:

int gcd(int a, int b) {
    while (b != 0) {
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

アセンブリ実装:

// int gcd(int a, int b)
// x0: 引数 a
// x1: 引数 b
// 戻り値: x0
.global _gcd
.align 2

_gcd:
    cbz     x1, gcd_end     // b == 0 なら終了
    
gcd_loop:
    udiv    x2, x0, x1      // x2 = a / b
    msub    x2, x2, x1, x0  // x2 = a - (a/b)*b = a % b
    
    mov     x0, x1          // a = b
    mov     x1, x2          // b = a % b
    
    cbnz    x1, gcd_loop    // b != 0 なら繰り返す
    
gcd_end:
    ret                     // a を返す

.global _main
_main:
    stp     x29, x30, [sp, #-16]!
    mov     x29, sp
    
    mov     x0, #48         // gcd(48, 18) を計算
    mov     x1, #18
    bl      _gcd
    
    // x0 に 6 が入っている
    
    ldp     x29, x30, [sp], #16
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

例3:フィボナッチ数列

C言語のコード:

int fibonacci(int n) {
    if (n &lt;= 1) return n;
    
    int a = 0, b = 1;
    for (int i = 2; i &lt;= n; i++) {
        int temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

アセンブリ実装:

// int fibonacci(int n)
// x0: 引数 n
// 戻り値: x0
.global _fibonacci
.align 2

_fibonacci:
    cmp     x0, #1          // n &lt;= 1 ?
    b.le    fib_end         // n &lt;= 1 なら n を返す
    
    mov     x1, #0          // a = 0
    mov     x2, #1          // b = 1
    mov     x3, #2          // i = 2
    
fib_loop:
    cmp     x3, x0          // i &lt;= n ?
    b.gt    fib_done        // i > n なら終了
    
    add     x4, x1, x2      // temp = a + b
    mov     x1, x2          // a = b
    mov     x2, x4          // b = temp
    add     x3, x3, #1      // i++
    b       fib_loop
    
fib_done:
    mov     x0, x2          // b を返す
    
fib_end:
    ret

.global _main
_main:
    stp     x29, x30, [sp, #-16]!
    mov     x29, sp
    
    mov     x0, #10         // fibonacci(10) を計算
    bl      _fibonacci
    
    // x0 に 55 が入っている
    
    ldp     x29, x30, [sp], #16
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

例4:配列の最大値を見つける

C言語のコード:

int find_max(int arr[], int size) {
    int max = arr[0];
    for (int i = 1; i &lt; size; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}

アセンブリ実装:

// int find_max(int *arr, int size)
// x0: 配列のアドレス
// x1: 配列のサイズ
// 戻り値: x0
.global _find_max
.align 2

_find_max:
    ldr     w2, [x0]        // max = arr[0]
    mov     x3, #1          // i = 1
    
find_loop:
    cmp     x3, x1          // i &lt; size ?
    b.ge    find_end        // i >= size なら終了
    
    ldr     w4, [x0, x3, lsl #2]  // w4 = arr[i] (4バイトずつ)
    cmp     w4, w2          // arr[i] > max ?
    csel    w2, w4, w2, gt  // 大きければ更新
    
    add     x3, x3, #1      // i++
    b       find_loop
    
find_end:
    mov     w0, w2          // max を返す
    ret

.global _main
_main:
    stp     x29, x30, [sp, #-16]!
    mov     x29, sp
    
    adrp    x0, array@PAGE
    add     x0, x0, array@PAGEOFF
    mov     x1, #5          // サイズ = 5
    bl      _find_max
    
    // x0 に最大値 89 が入っている
    
    ldp     x29, x30, [sp], #16
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

.data
array:
    .word   23, 45, 12, 89, 34

switch文の実装

分岐テーブルを使った実装

C言語のコード:

int operation(int op, int a, int b) {
    switch (op) {
        case 0: return a + b;
        case 1: return a - b;
        case 2: return a * b;
        case 3: return a / b;
        default: return 0;
    }
}

アセンブリ実装(if-else方式):

.global _operation
.align 2

_operation:
    cmp     x0, #0
    b.eq    op_add
    cmp     x0, #1
    b.eq    op_sub
    cmp     x0, #2
    b.eq    op_mul
    cmp     x0, #3
    b.eq    op_div
    b       op_default
    
op_add:
    add     x0, x1, x2
    ret
    
op_sub:
    sub     x0, x1, x2
    ret
    
op_mul:
    mul     x0, x1, x2
    ret
    
op_div:
    udiv    x0, x1, x2
    ret
    
op_default:
    mov     x0, #0
    ret

アセンブリ実装(ジャンプテーブル方式):

.global _operation_fast
.align 2

_operation_fast:
    cmp     x0, #3          // 範囲チェック
    b.hi    op_default      // op > 3 なら default
    
    adrp    x3, jump_table@PAGE
    add     x3, x3, jump_table@PAGEOFF
    ldr     x3, [x3, x0, lsl #3]  // ジャンプ先取得
    br      x3              // ジャンプ
    
op_add:
    add     x0, x1, x2
    ret
    
op_sub:
    sub     x0, x1, x2
    ret
    
op_mul:
    mul     x0, x1, x2
    ret
    
op_div:
    udiv    x0, x1, x2
    ret
    
op_default:
    mov     x0, #0
    ret

.data
.align 3
jump_table:
    .quad   op_add
    .quad   op_sub
    .quad   op_mul
    .quad   op_div

デバッグテクニック

条件フラグの確認

lldbでフラグを確認する方法:

(lldb) register read cpsr

CPSRレジスタの各ビットが条件フラグです:

  • ビット31: N(Negative)
  • ビット30: Z(Zero)
  • ビット29: C(Carry)
  • ビット28: V(Overflow)

ループのデバッグ

// デバッグ用のカウンタ表示
.global _main
_main:
    stp     x29, x30, [sp, #-16]!
    mov     x29, sp
    
    mov     x19, #0         // カウンタ
    
loop:
    cmp     x19, #5
    b.ge    end
    
    // カウンタを表示
    mov     x1, x19
    adrp    x0, fmt@PAGE
    add     x0, x0, fmt@PAGEOFF
    bl      _printf
    
    add     x19, x19, #1
    b       loop
    
end:
    ldp     x29, x30, [sp], #16
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

.data
fmt:
    .asciz  "Counter: %ld\n"

まとめ

第2回では、制御構造と分岐処理について学びました:

学んだこと:

  • 条件フラグ(N, Z, C, V)の役割と動作
  • 比較命令(CMP, CMN, TST)の使い方
  • 条件コードの種類と使い分け(符号付き・符号なし)
  • 条件分岐命令(B.条件)を使ったif文の実装
  • 条件付き実行命令(CSEL, CINC, CNEG)による効率的な処理
  • 様々なループ構造の実装(for, while, do-while)
  • CBZ/CBNZ命令を使った最適化
  • 実践的なアルゴリズム(階乗、GCD、フィボナッチ、配列操作)
  • switch文の実装方法(if-else方式とジャンプテーブル方式)

重要なポイント:

  1. 比較とフラグ: CMP命令は減算を行いフラグのみを更新する
  2. 符号の扱い: 符号付き(GT, LT)と符号なし(HI, LO)の条件を使い分ける
  3. 分岐予測: 可能な限りCSEL等を使って分岐を避けることでパフォーマンス向上
  4. ゼロ比較の最適化: CBZ/CBNZは頻繁に使われる最適化技法
  5. ループの効率: カウンタの配置やレジスタの使い方が重要

パフォーマンスの考慮事項

分岐予測とパイプライン

現代のプロセッサは、命令をパイプライン処理で並列実行しています。分岐命令があると、分岐先を予測しながら処理を進めますが、予測が外れると大きなペナルティが発生します。

分岐予測ミスのコスト:

  • Apple Silicon M1では約15-20サイクルのペナルティ
  • 頻繁な分岐はパフォーマンスを大きく低下させる

最適化の例:

// 悪い例:予測しにくい分岐
bad_abs:
    cmp     x0, #0
    b.ge    positive        // 分岐予測が必要
    neg     x0, x0
    ret
positive:
    ret

// 良い例:分岐なし
good_abs:
    cmp     x0, #0
    cneg    x0, x0, lt      // 条件付き実行、分岐なし
    ret

ループアンローリング

ループのオーバーヘッドを減らす技法です。

通常のループ:

// 配列の合計(通常)
normal_sum:
    mov     x2, #0          // sum = 0
    mov     x3, #0          // i = 0
loop:
    cmp     x3, x1          // i &lt; size
    b.ge    end
    ldr     w4, [x0, x3, lsl #2]
    add     w2, w2, w4
    add     x3, x3, #1
    b       loop
end:
    mov     w0, w2
    ret

アンローリング版:

// 配列の合計(4回アンローリング)
unrolled_sum:
    mov     x2, #0          // sum = 0
    mov     x3, #0          // i = 0
    
    // 4要素ずつ処理
loop_unroll:
    add     x4, x3, #4
    cmp     x4, x1
    b.gt    remainder
    
    ldr     w5, [x0, x3, lsl #2]
    add     x3, x3, #1
    ldr     w6, [x0, x3, lsl #2]
    add     x3, x3, #1
    ldr     w7, [x0, x3, lsl #2]
    add     x3, x3, #1
    ldr     w8, [x0, x3, lsl #2]
    add     x3, x3, #1
    
    add     w2, w2, w5
    add     w2, w2, w6
    add     w2, w2, w7
    add     w2, w2, w8
    
    b       loop_unroll
    
remainder:
    // 残りの要素を処理
    cmp     x3, x1
    b.ge    end
    ldr     w4, [x0, x3, lsl #2]
    add     w2, w2, w4
    add     x3, x3, #1
    b       remainder
    
end:
    mov     w0, w2
    ret

条件付き実行のベンチマーク

実際に速度差を測定してみましょう:

.global _main
.align 2

_main:
    stp     x29, x30, [sp, #-16]!
    mov     x29, sp
    
    // 分岐版のテスト
    mov     x19, #0         // カウンタ
    mov     x20, #10000000  // 反復回数
    
branch_test:
    mov     x0, #42
    mov     x1, #17
    
    // max関数(分岐版)を呼び出し
    cmp     x0, x1
    b.le    1f
    b       2f
1:  mov     x0, x1
2:  
    add     x19, x19, #1
    cmp     x19, x20
    b.lt    branch_test
    
    // CSEL版のテスト
    mov     x19, #0
    
csel_test:
    mov     x0, #42
    mov     x1, #17
    
    // max関数(CSEL版)を呼び出し
    cmp     x0, x1
    csel    x0, x0, x1, gt
    
    add     x19, x19, #1
    cmp     x19, x20
    b.lt    csel_test
    
    ldp     x29, x30, [sp], #16
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

一般的に、CSEL版の方が10-30%程度高速です。

実践的なプログラム例

例:バブルソート

配列をソートする古典的なアルゴリズムです。

// void bubble_sort(int *arr, int size)
// x0: 配列のアドレス
// x1: 配列のサイズ
.global _bubble_sort
.align 2

_bubble_sort:
    stp     x29, x30, [sp, #-32]!
    mov     x29, sp
    stp     x19, x20, [sp, #16]
    
    mov     x19, x0         // 配列アドレスを保存
    mov     x20, x1         // サイズを保存
    
    mov     x2, #0          // i = 0
    
outer_loop:
    sub     x3, x20, #1     // size - 1
    cmp     x2, x3          // i &lt; size - 1
    b.ge    sort_done
    
    mov     x4, #0          // j = 0
    sub     x5, x20, x2     // size - i
    sub     x5, x5, #1      // size - i - 1
    
inner_loop:
    cmp     x4, x5          // j &lt; size - i - 1
    b.ge    inner_done
    
    // arr[j] と arr[j+1] を比較
    ldr     w6, [x19, x4, lsl #2]       // arr[j]
    add     x7, x4, #1
    ldr     w8, [x19, x7, lsl #2]       // arr[j+1]
    
    cmp     w6, w8          // arr[j] > arr[j+1] ?
    b.le    no_swap
    
    // 交換
    str     w8, [x19, x4, lsl #2]       // arr[j] = arr[j+1]
    str     w6, [x19, x7, lsl #2]       // arr[j+1] = arr[j]
    
no_swap:
    add     x4, x4, #1      // j++
    b       inner_loop
    
inner_done:
    add     x2, x2, #1      // i++
    b       outer_loop
    
sort_done:
    ldp     x19, x20, [sp, #16]
    ldp     x29, x30, [sp], #32
    ret

.global _main
_main:
    stp     x29, x30, [sp, #-16]!
    mov     x29, sp
    
    adrp    x0, test_array@PAGE
    add     x0, x0, test_array@PAGEOFF
    mov     x1, #8
    bl      _bubble_sort
    
    // ソート結果を表示
    mov     x19, #0
print_loop:
    cmp     x19, #8
    b.ge    print_done
    
    adrp    x20, test_array@PAGE
    add     x20, x20, test_array@PAGEOFF
    ldr     w1, [x20, x19, lsl #2]
    
    adrp    x0, fmt@PAGE
    add     x0, x0, fmt@PAGEOFF
    bl      _printf
    
    add     x19, x19, #1
    b       print_loop
    
print_done:
    ldp     x29, x30, [sp], #16
    mov     x0, #0
    mov     x16, #1
    svc     #0x80

.data
test_array:
    .word   64, 34, 25, 12, 22, 11, 90, 88

fmt:
    .asciz  "%d\n"

例:線形探索

配列から特定の値を探します。

// int linear_search(int *arr, int size, int target)
// x0: 配列アドレス
// x1: サイズ
// x2: 探す値
// 戻り値: 見つかったインデックス(見つからない場合は-1)
.global _linear_search
.align 2

_linear_search:
    mov     x3, #0          // i = 0
    
search_loop:
    cmp     x3, x1          // i &lt; size
    b.ge    not_found
    
    ldr     w4, [x0, x3, lsl #2]  // arr[i]
    cmp     w4, w2          // arr[i] == target
    b.eq    found
    
    add     x3, x3, #1      // i++
    b       search_loop
    
found:
    mov     x0, x3          // インデックスを返す
    ret
    
not_found:
    mov     x0, #-1         // -1を返す
    ret

例:二分探索(ソート済み配列)

// int binary_search(int *arr, int size, int target)
// x0: 配列アドレス
// x1: サイズ
// x2: 探す値
// 戻り値: 見つかったインデックス(見つからない場合は-1)
.global _binary_search
.align 2

_binary_search:
    mov     x3, #0          // left = 0
    sub     x4, x1, #1      // right = size - 1
    
binary_loop:
    cmp     x3, x4          // left &lt;= right
    b.gt    binary_not_found
    
    add     x5, x3, x4      // left + right
    lsr     x5, x5, #1      // mid = (left + right) / 2
    
    ldr     w6, [x0, x5, lsl #2]  // arr[mid]
    
    cmp     w6, w2          // arr[mid] == target
    b.eq    binary_found
    
    cmp     w6, w2          // arr[mid] &lt; target
    b.lt    search_right
    
    // arr[mid] > target
    sub     x4, x5, #1      // right = mid - 1
    b       binary_loop
    
search_right:
    add     x3, x5, #1      // left = mid + 1
    b       binary_loop
    
binary_found:
    mov     x0, x5          // インデックスを返す
    ret
    
binary_not_found:
    mov     x0, #-1
    ret

練習問題

初級問題

問題1: 1から100までの偶数の合計を計算するプログラムを書いてください。

問題2: 配列の中から最小値を見つける関数を実装してください。

問題3: 整数が素数かどうかを判定する関数を書いてください。

中級問題

問題4: 配列を逆順にする関数を実装してください(追加メモリを使わずに)。

問題5: 選択ソートアルゴリズムを実装してください。

問題6: 文字列の長さを計算する関数を実装してください(NULLターミネータまで)。

上級問題

問題7: クイックソートアルゴリズムを実装してください(再帰を使って)。

問題8: パスカルの三角形のn行目を計算するプログラムを書いてください。

問題9: 文字列がパリンドローム(回文)かどうかを判定する関数を実装してください。

解答例(問題1)

// 1から100までの偶数の合計
.global _main
.align 2

_main:
    mov     x0, #0          // sum = 0
    mov     x1, #2          // i = 2(最初の偶数)
    
loop:
    cmp     x1, #100        // i &lt;= 100
    b.gt    done
    
    add     x0, x0, x1      // sum += i
    add     x1, x1, #2      // i += 2(次の偶数)
    b       loop
    
done:
    // x0 に 2550 が入っている
    mov     x16, #1
    svc     #0x80

解答例(問題4)

// void reverse_array(int *arr, int size)
// x0: 配列アドレス
// x1: サイズ
.global _reverse_array
.align 2

_reverse_array:
    mov     x2, #0          // left = 0
    sub     x3, x1, #1      // right = size - 1
    
reverse_loop:
    cmp     x2, x3          // left &lt; right
    b.ge    reverse_done
    
    // arr[left] と arr[right] を交換
    ldr     w4, [x0, x2, lsl #2]  // temp = arr[left]
    ldr     w5, [x0, x3, lsl #2]  // arr[right]
    
    str     w5, [x0, x2, lsl #2]  // arr[left] = arr[right]
    str     w4, [x0, x3, lsl #2]  // arr[right] = temp
    
    add     x2, x2, #1      // left++
    sub     x3, x3, #1      // right--
    b       reverse_loop
    
reverse_done:
    ret

よくある間違いとデバッグのコツ

間違い1: 符号付き・符号なしの混同

// 間違い
mov     x0, #-1
mov     x1, #10
cmp     x0, x1
b.hi    label       // HIは符号なし比較、-1は大きな正数として扱われる

// 正しい
mov     x0, #-1
mov     x1, #10
cmp     x0, x1
b.gt    label       // GTは符号付き比較

間違い2: フラグの上書き

// 間違い
cmp     x0, x1      // フラグ設定
add     x2, x3, x4  // この命令でフラグは変わらない
subs    x5, x6, x7  // この命令でフラグが上書きされる!
b.gt    label       // x0 > x1 ではなく x5 > x7 の結果で分岐

// 正しい
cmp     x0, x1      // フラグ設定
b.gt    label       // すぐに分岐

間違い3: ループカウンタのオーバーフロー

// 危険:カウンタが大きくなりすぎる可能性
mov     x0, #0
loop:
    // 終了条件がない、または間違っている
    add     x0, x0, #1
    b       loop        // 無限ループ

デバッグのコツ

  1. レジスタの値を確認: lldbで register read を使う
  2. ブレークポイント: 重要な箇所に設定
  3. printfデバッグ: 中間値を出力して確認
  4. ステップ実行stepi で1命令ずつ実行
  5. メモリダンプmemory read でメモリ内容を確認

次回予告

第3回では、関数の作成と呼び出し規約について詳しく学びます:

  • スタックフレームの構造
  • 関数呼び出し規約(AAPCS64)
  • レジスタの保存と復元
  • 可変長引数の処理
  • 再帰関数の実装
  • 関数ポインタの使用

これらをマスターすることで、より複雑で実用的なプログラムを書けるようになります。

第2回で学んだ制御構造は、プログラミングの基礎中の基礎です。練習問題に取り組んで、しっかり身につけましょう!

\ 最新情報をチェック /

コメントを残す

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