🤖

RasPiでアセンブリ言語を学ぼう その2

に公開

目次

  1. メモリアドレッシングモード
  2. 条件分岐の応用
  3. ビット演算
  4. メモリアクセス入門
  5. スタックの使い方
  6. 実践:配列の合計値計算
  7. トラブルシューティング

メモリアドレッシングモード

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

対処:

  • 実行環境と一致するツールチェーンでビルドし直す

参考資料


練習問題

以下のプログラムを自分で作成してみましょう。

問題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