Apple Siliconアセンブリ言語入門ガイド 第2回:制御構造と分岐処理
はじめに
第1回では、Apple Siliconの基礎知識、レジスタ、基本的な命令セットについて学びました。第2回では、プログラミングに欠かせない制御構造について深く掘り下げていきます。
制御構造とは、プログラムの実行フローを制御する仕組みです。条件分岐(if文)、ループ(for、while)、関数呼び出しなど、高級言語で当たり前に使っている機能を、アセンブリ言語レベルでどう実装するのかを学びます。
プロセッサステータスレジスタ(PSTATE)
条件フラグの理解
ARM64プロセッサには、演算結果の状態を記録する条件フラグがあります。これらは PSTATE レジスタに格納され、条件分岐の判定に使用されます。
主な条件フラグ:
- N(Negative)フラグ
- 演算結果が負の場合に1になる
- 結果の最上位ビットがセットされた場合
- Z(Zero)フラグ
- 演算結果がゼロの場合に1になる
- 比較命令で2つの値が等しい場合
- C(Carry)フラグ
- 符号なし演算でキャリーまたはボローが発生した場合に1
- 加算でオーバーフロー、減算でアンダーフロー
- 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では、フラグの状態に基づいて様々な条件を判定できます:
| 条件コード | 意味 | フラグ条件 | 用途 |
|---|---|---|---|
| EQ | Equal(等しい) | Z == 1 | a == b |
| NE | Not Equal(等しくない) | Z == 0 | a != b |
| GT | Greater Than(より大きい) | Z == 0 && N == V | a > b(符号付き) |
| GE | Greater or Equal(以上) | N == V | a >= b(符号付き) |
| LT | Less Than(より小さい) | N != V | a < b(符号付き) |
| LE | Less or Equal(以下) | Z == 1 || N != V | a <= b(符号付き) |
| HI | Higher(より大きい) | C == 1 && Z == 0 | a > b(符号なし) |
| HS/CS | Higher or Same(以上) | C == 1 | a >= b(符号なし) |
| LO/CC | Lower(より小さい) | C == 0 | a < b(符号なし) |
| LS | Lower or Same(以下) | C == 0 || Z == 1 | a <= b(符号なし) |
| MI | Minus(負) | N == 1 | a < 0 |
| PL | Plus(正またはゼロ) | N == 0 | a >= 0 |
| VS | Overflow Set | V == 1 | オーバーフロー発生 |
| VC | Overflow Clear | V == 0 | オーバーフローなし |
| AL | Always(常に真) | - | 無条件 |
分岐命令
無条件分岐:
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 < b なら label へジャンプ(符号付き)
b.le label // a <= b なら label へジャンプ(符号付き)
b.hi label // a > b なら label へジャンプ(符号なし)
b.lo label // a < 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 <= 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 < 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 < 0 なら x0 = -x0、そうでなければそのまま
ret
ループ処理
カウンタを使ったループ(for文)
C言語のコード:
int sum = 0;
for (int i = 1; i <= 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 <= 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 <= 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 <= 1 ?
b.le fact_end // n <= 1 なら終了
fact_loop:
cmp x2, x0 // i <= 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 <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= 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 <= 1 ?
b.le fib_end // n <= 1 なら n を返す
mov x1, #0 // a = 0
mov x2, #1 // b = 1
mov x3, #2 // i = 2
fib_loop:
cmp x3, x0 // i <= 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 < 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 < 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方式とジャンプテーブル方式)
重要なポイント:
- 比較とフラグ: CMP命令は減算を行いフラグのみを更新する
- 符号の扱い: 符号付き(GT, LT)と符号なし(HI, LO)の条件を使い分ける
- 分岐予測: 可能な限りCSEL等を使って分岐を避けることでパフォーマンス向上
- ゼロ比較の最適化: CBZ/CBNZは頻繁に使われる最適化技法
- ループの効率: カウンタの配置やレジスタの使い方が重要
パフォーマンスの考慮事項
分岐予測とパイプライン
現代のプロセッサは、命令をパイプライン処理で並列実行しています。分岐命令があると、分岐先を予測しながら処理を進めますが、予測が外れると大きなペナルティが発生します。
分岐予測ミスのコスト:
- 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 < 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 < 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 < 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 < 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 <= 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] < 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 <= 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 < 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 // 無限ループ
デバッグのコツ
- レジスタの値を確認: lldbで
register readを使う - ブレークポイント: 重要な箇所に設定
- printfデバッグ: 中間値を出力して確認
- ステップ実行:
stepiで1命令ずつ実行 - メモリダンプ:
memory readでメモリ内容を確認
次回予告
第3回では、関数の作成と呼び出し規約について詳しく学びます:
- スタックフレームの構造
- 関数呼び出し規約(AAPCS64)
- レジスタの保存と復元
- 可変長引数の処理
- 再帰関数の実装
- 関数ポインタの使用
これらをマスターすることで、より複雑で実用的なプログラムを書けるようになります。
第2回で学んだ制御構造は、プログラミングの基礎中の基礎です。練習問題に取り組んで、しっかり身につけましょう!
