🤖
RasPiでアセンブリ言語を学ぼう その2
目次
メモリアドレッシングモード
ARM64では様々なメモリアクセス方法(アドレッシングモード)があります。
基本的なアドレッシングモード
1. ベースレジスタ
ldr x0, [x1] // x1 が指すアドレスから読み込む
str x0, [x1] // x1 が指すアドレスへ書き込む
2. オフセット付き
ldr x0, [x1, #8] // x1 + 8 のアドレスから読み込む
str x0, [x1, #16] // x1 + 16 のアドレスへ書き込む
3. プリインデックス(Pre-index)
ldr x0, [x1, #8]! // x1 = x1 + 8 してから、そのアドレスから読み込む
str x0, [x1, #-16]! // x1 = x1 - 16 してから、そのアドレスへ書き込む
4. ポストインデックス(Post-index)
ldr x0, [x1], #8 // x1 から読み込んでから、x1 = x1 + 8
str x0, [x1], #16 // x1 へ書き込んでから、x1 = x1 + 16
5. レジスタオフセット
ldr x0, [x1, x2] // x1 + x2 のアドレスから読み込む
ldr x0, [x1, x2, lsl #3] // x1 + (x2 * 8) のアドレスから読み込む
実践例:配列を逆順に読む
ファイル名: reverse_read.s
.section .data
array:
.quad 10, 20, 30, 40, 50
.section .text
.global _start
_start:
// 配列の最後のアドレスを計算
ldr x1, =array
add x1, x1, #32 // 配列の最後(8バイト × 4 = 32)
// 逆順に読み込む(ポストインデックス使用)
ldr x0, [x1], #-8 // 50 を読み込み、x1 -= 8
ldr x2, [x1], #-8 // 40 を読み込み、x1 -= 8
ldr x3, [x1], #-8 // 30 を読み込み、x1 -= 8
// x0 + x2 + x3 = 50 + 40 + 30 = 120
add x0, x0, x2
add x0, x0, x3
mov x8, #93
svc #0
条件分岐の応用
条件フラグ
比較命令(cmp)は以下のフラグを設定します:
| フラグ | 名称 | 意味 |
|---|---|---|
| Z | Zero | 結果が0 |
| N | Negative | 結果が負 |
| C | Carry | キャリー発生 |
| V | Overflow | オーバーフロー発生 |
条件コード一覧
| 条件 | 意味 | フラグ条件 |
|---|---|---|
| EQ | Equal(等しい) | Z == 1 |
| NE | Not Equal(等しくない) | Z == 0 |
| GT | Greater Than(より大きい) | Z == 0 && N == V |
| GE | Greater or Equal(以上) | N == V |
| LT | Less Than(より小さい) | N != V |
| LE | Less or Equal(以下) | Z == 1 |
| HI | Higher(符号なし大) | C == 1 && Z == 0 |
| LS | Lower or Same(符号なし以下) | C == 0 |
実践例1:絶対値を求める
ファイル名: absolute.s
.global _start
_start:
mov x0, #-25 // x0 = -25
// x0 が負なら正に変換
cmp x0, #0 // x0 と 0 を比較
bge done // x0 >= 0 なら done へ
neg x0, x0 // x0 = -x0(負数を正数に)
done:
mov x8, #93
svc #0 // 終了コード: 25
実践例2:3つの数の最大値
ファイル名: max3.s
.global _start
_start:
mov x0, #42 // 第1の値
mov x1, #67 // 第2の値
mov x2, #25 // 第3の値
// x0 と x1 を比較
cmp x0, x1
bge check_x2 // x0 >= x1 なら x0 と x2 を比較
mov x0, x1 // x0 = x1 (x1 の方が大きい)
check_x2:
// x0 と x2 を比較
cmp x0, x2
bge done // x0 >= x2 なら終了
mov x0, x2 // x0 = x2 (x2 の方が大きい)
done:
mov x8, #93
svc #0 // 終了コード: 67
実践例3:if-else文の実装
ファイル名: if_else.s
.global _start
_start:
mov x0, #15 // テストする値
mov x1, #0 // 結果
// if (x0 > 10)
cmp x0, #10
ble else_branch // x0 <= 10 なら else へ
// then: x1 = x0 * 2
add x1, x0, x0
b endif
else_branch:
// else: x1 = x0 + 5
add x1, x0, #5
endif:
mov x0, x1 // 結果を x0 に移動
mov x8, #93
svc #0 // 終了コード: 30
ビット演算
基本的なビット演算命令
| 命令 | 意味 | 例 |
|---|---|---|
| and | ビット論理積(AND) | and x0, x1, x2 |
| orr | ビット論理和(OR) | orr x0, x1, x2 |
| eor | ビット排他的論理和(XOR) | eor x0, x1, x2 |
| bic | ビットクリア | bic x0, x1, x2 |
| mvn | ビット反転(NOT) | mvn x0, x1 |
| lsl | 論理左シフト | lsl x0, x1, #3 |
| lsr | 論理右シフト | lsr x0, x1, #2 |
| asr | 算術右シフト | asr x0, x1, #1 |
実践例1:偶数・奇数判定
ファイル名: even_odd.s
.global _start
_start:
mov x0, #17 // テストする値
// 最下位ビットをチェック(AND演算)
and x1, x0, #1 // x1 = x0 & 1
// x1 == 0 なら偶数、x1 == 1 なら奇数
cmp x1, #0
beq is_even
// 奇数の場合
mov x0, #1
b done
is_even:
// 偶数の場合
mov x0, #0
done:
mov x8, #93
svc #0 // 終了コード: 1(奇数)
実践例2:ビットフラグの操作
ファイル名: bitflags.s
.global _start
_start:
mov x0, #0 // フラグを初期化
// ビット2をセット(OR演算)
orr x0, x0, #4 // x0 |= (1 << 2) = 0b00000100
// ビット1をセット
orr x0, x0, #2 // x0 |= (1 << 1) = 0b00000110
// ビット2をクリア(BIC演算)
bic x0, x0, #4 // x0 &= ~(1 << 2) = 0b00000010
// ビット1をトグル(XOR演算)
eor x0, x0, #2 // x0 ^= (1 << 1) = 0b00000000
mov x8, #93
svc #0 // 終了コード: 0
実践例3:2の累乗判定
ファイル名: power_of_two.s
.global _start
_start:
mov x0, #16 // テストする値
// 2の累乗は (n & (n-1)) == 0 という性質を利用
sub x1, x0, #1 // x1 = x0 - 1
and x2, x0, x1 // x2 = x0 & (x0 - 1)
// x2 == 0 かつ x0 != 0 なら2の累乗
cmp x2, #0
bne not_power
cmp x0, #0
beq not_power
// 2の累乗
mov x0, #1
b done
not_power:
// 2の累乗ではない
mov x0, #0
done:
mov x8, #93
svc #0 // 終了コード: 1(2の累乗)
実践例4:ビットカウント(1の個数)
ファイル名: bitcount.s
.global _start
_start:
mov x0, #0b10110101 // テストする値(2進数表記)
mov x1, #0 // カウンタ
mov x2, x0 // 作業用コピー
loop:
cbz x2, done // x2 == 0 なら終了
// 最下位ビットをチェック
and x3, x2, #1
add x1, x1, x3 // ビットが1ならカウント増
// 右シフト
lsr x2, x2, #1
b loop
done:
mov x0, x1 // カウント結果を x0 へ
mov x8, #93
svc #0 // 終了コード: 5(1のビット数)
メモリアクセス入門
ldr/str 命令(Load/Store)
ARM64 はロード/ストア・アーキテクチャです。メモリアクセスは専用命令で行います。
基本構文
ldr x0, [x1] // x1 が指すアドレスからデータを x0 に読み込む
str x0, [x1] // x0 の値を x1 が指すアドレスに書き込む
ldr x0, =label // label のアドレスを x0 に読み込む
実践例1: 変数の読み書き
ファイル名: memory.s
.section .data
value:
.quad 42 // 64ビット整数 42 を定義
.section .text
.global _start
_start:
// value のアドレスを x1 に読み込む
ldr x1, =value
// value の内容(42)を x0 に読み込む
ldr x0, [x1]
// x0 を 2倍にする
add x0, x0, x0 // x0 = 84
// 新しい値を value に書き戻す
str x0, [x1]
// exit(84)
mov x8, #93
svc #0
実行:
gcc -nostdlib -static -o memory memory.s
./memory
echo $? # 84 が表示される
実践例2: 配列アクセス
ファイル名: array.s
.section .data
array:
.quad 10, 20, 30, 40, 50 // 5要素の配列
.section .text
.global _start
_start:
// 配列のアドレスを x1 に読み込む
ldr x1, =array
// array[0] を x0 に読み込む
ldr x0, [x1] // x0 = 10
// array[1] を x2 に読み込む(8バイトオフセット)
ldr x2, [x1, #8] // x2 = 20
// x0 + x2
add x0, x0, x2 // x0 = 30
// exit(30)
mov x8, #93
svc #0
説明:
-
.quadは 64ビット(8バイト)整数 -
[x1, #8]は x1 + 8 のアドレスにアクセス
スタックの使い方
スタックとは
- 関数呼び出し時にレジスタを一時保存する領域
- LIFO(Last In, First Out)構造
- SP(スタックポインタ)が現在位置を指す
スタック操作
// レジスタをスタックに保存(push相当)
stp x19, x20, [sp, #-16]! // x19, x20 をスタックに保存
// スタックからレジスタに復元(pop相当)
ldp x19, x20, [sp], #16 // x19, x20 をスタックから復元
説明:
-
stp: Store Pair(2つのレジスタを保存) -
ldp: Load Pair(2つのレジスタを復元) -
[sp, #-16]!: SP を 16バイト減らして書き込む(pre-index) -
[sp], #16: 読み込んでから SP を 16バイト増やす(post-index)
実践例: 関数でレジスタを保存
ファイル名: stack.s
.global _start
_start:
mov x19, #100 // x19 に 100 を保存(保護したい値)
mov x0, #5 // 引数: 5
bl triple // triple 関数呼び出し
// x0 = 15 になる
add x0, x0, x19 // x0 = 15 + 100 = 115
mov x8, #93
svc #0
triple:
// x19 をスタックに退避(関数内で使うため)
stp x19, x30, [sp, #-16]!
mov x19, x0 // 引数を x19 に保存
add x0, x19, x19 // x0 = x19 * 2
add x0, x0, x19 // x0 = x19 * 3
// x19, x30 を復元
ldp x19, x30, [sp], #16
ret
実行:
gcc -nostdlib -static -o stack stack.s
./stack
echo $? # 115 が表示される
実践:配列の合計値計算
配列の全要素を合計するプログラムです。
ファイル名: array_sum.s
.section .data
array:
.quad 5, 10, 15, 20, 25 // 配列(合計 75)
array_size:
.quad 5 // 要素数
.section .text
.global _start
_start:
// 初期化
ldr x1, =array // x1 = 配列のアドレス
ldr x2, =array_size
ldr x2, [x2] // x2 = 要素数(5)
mov x0, #0 // x0 = 合計値(初期値 0)
mov x3, #0 // x3 = カウンタ
loop:
// 終了条件チェック
cmp x3, x2 // カウンタ == 要素数?
beq done // 等しければ done へ
// 配列要素を読み込んで加算
ldr x4, [x1, x3, lsl #3] // x4 = array[x3]
// lsl #3 は x3 を 8倍(8バイトオフセット)
add x0, x0, x4 // x0 += x4
// カウンタを増やす
add x3, x3, #1
b loop
done:
// exit(75)
mov x8, #93
svc #0
説明:
-
lsl #3: Left Shift Logical(論理左シフト)で 2^3 = 8 倍 -
[x1, x3, lsl #3]はx1 + (x3 * 8)のアドレスにアクセス
実行:
gcc -nostdlib -static -o array_sum array_sum.s
./array_sum
echo $? # 75 が表示される
トラブルシューティング
問題1: Segmentation fault
症状:
$ ./program
Segmentation fault
原因と対処:
- 不正なメモリアクセス → アドレスを確認
- スタックポインタの破壊 → stp/ldp の対応を確認
- 初期化忘れ → レジスタを使う前に初期化
デバッグ方法:
gdb ./program
(gdb) run
# Segmentation fault 発生箇所が表示される
(gdb) backtrace
(gdb) info registers
問題2: 終了コードが期待と異なる
原因:
- 計算ミス → gdb でステップ実行してレジスタを確認
- システムコール番号の間違い → x8 の値を確認
問題3: リンクエラー
症状:
ld: warning: cannot find entry symbol _start
対処:
-
.global _startを追加 -
_start:ラベルが正しく定義されているか確認
問題4: コンパイルは成功するが実行できない
症状:
$ ./program
bash: ./program: cannot execute binary file: Exec format error
原因:
- 異なるアーキテクチャ用にコンパイルされている
確認:
file ./program
# 出力例: ELF 64-bit LSB executable, ARM aarch64
対処:
- 実行環境と一致するツールチェーンでビルドし直す
参考資料
- ARM Developer Documentation
- GNU Assembler Manual
- Linux System Call Reference (ARM64)
- GDB User Manual
練習問題
以下のプログラムを自分で作成してみましょう。
問題1: 最大値を求める
配列 [15, 42, 8, 23, 67, 12] の中から最大値を見つけて終了コードとして返す。
問題2: 階乗計算
5の階乗(5! = 5 × 4 × 3 × 2 × 1 = 120)を計算する。
問題3: 文字列の長さ
null終端文字列の長さを数える(.asciz "Hello" を使用)。
作成日: 2025年10月22日
対象: Raspberry Pi (ARM64/AArch64)
環境: Linux/GNU Assembler
Discussion