RasPiでアセンブリ言語を学ぼう その1
ARM64 アセンブリ言語学習ガイド
目次
アセンブリ言語とは
アセンブリ言語は、コンピュータのCPUが直接理解できる機械語に最も近い低レベルプログラミング言語です。
特徴
- 1命令 = 1行: 各行がCPUの1つの動作に対応
- 高速実行: CPUが直接実行するため、最も高速
- ハードウェア制御: レジスタやメモリを直接操作
- 難易度: 人間にとって理解しにくいが、CPUにとって最も効率的
比較
| 言語レベル | 例 | 特徴 |
|---|---|---|
| 高級言語 | Python, Java, C | 人間が読みやすい、書きやすい |
| 低級言語 | アセンブリ | CPUに近い、高速、直接制御 |
| 機械語 | 0101101... | CPUが直接実行する2進数 |
レジスタとは
レジスタは、CPUの内部にある超高速な記憶場所です。
レジスタの重要性
速度比較(アクセス時間):
レジスタ: 0.3 ナノ秒 ⚡
L1キャッシュ: 1 ナノ秒
RAM: 100 ナノ秒 🐌
→ レジスタはRAMの約300倍高速!
レジスタの役割
- 計算の作業スペース: 足し算、引き算などの計算に使用
- 一時データ保管: 計算中の値を一時的に保存
- 関数間のデータ受け渡し: 引数と戻り値の格納
- プログラム制御: 次に実行する命令の位置を記憶
今回のRaspberry Piのレジスタ
今回のRaspberry Piは ARM64 (AArch64) アーキテクチャで、以下のレジスタを持っています。
1. 汎用レジスタ(31個)
x0 - x30 : 64ビット汎用レジスタ(31個)
w0 - w30 : 上記の下位32ビット版
レジスタの用途規約
| レジスタ | 別名 | 用途 |
|---|---|---|
| x0-x7 | - | 関数の引数と戻り値 |
| x8 | - | 間接結果レジスタ |
| x9-x15 | - | 一時レジスタ(自由に使用可) |
| x16-x17 | IP0, IP1 | プロシージャ内一時レジスタ |
| x18 | - | プラットフォームレジスタ |
| x19-x28 | - | 保存レジスタ(関数呼び出し後も保持) |
| x29 | FP | フレームポインタ |
| x30 | LR | リンクレジスタ(戻りアドレス) |
2. 特殊レジスタ(3個)
| レジスタ | 名称 | 用途 |
|---|---|---|
| SP | スタックポインタ | スタックメモリの現在位置 |
| PC | プログラムカウンタ | 次に実行する命令のアドレス |
| XZR | ゼロレジスタ | 常に0を返す特殊レジスタ |
合計レジスタ数
31個(汎用) + 3個(特殊) = 34個のレジスタ
基本命令セット
データ転送命令
mov x0, #5 // x0 に即値 5 を代入
mov x1, x0 // x1 に x0 の値をコピー
算術命令
add x0, x1, x2 // x0 = x1 + x2
sub x0, x1, x2 // x0 = x1 - x2
mul x0, x1, x2 // x0 = x1 × x2
比較命令
cmp x0, x1 // x0 と x1 を比較(結果はフラグに保存)
分岐命令
b label // 無条件でlabelへジャンプ
beq label // 等しければジャンプ (equal)
bne label // 等しくなければジャンプ (not equal)
blt label // 小さければジャンプ (less than)
ble label // 以下ならジャンプ (less or equal)
bgt label // 大きければジャンプ (greater than)
bge label // 以上ならジャンプ (greater or equal)
bl function // 関数呼び出し(戻りアドレスをLRに保存)
ret // 関数から戻る(LRの示すアドレスへジャンプ)
システムコール
svc #0 // システムコール実行
よく使うシステムコール番号
| 番号 | 名前 | 機能 | x0 | x1 | x2 |
|---|---|---|---|---|---|
| 64 | write | 画面出力 | ファイル番号(1=stdout) | メッセージアドレス | 文字数 |
| 93 | exit | プログラム終了 | 終了コード | - | - |
実践プログラミング例
1. Hello World(画面出力)
ファイル名: hello.s
.section .data
msg:
.ascii "Hello, ARM Assembly!\n"
len = . - msg
.section .text
.global _start
_start:
// write(1, msg, len)
mov x0, #1 // ファイル番号 1 (stdout)
ldr x1, =msg // メッセージのアドレス
mov x2, #len // メッセージの長さ
mov x8, #64 // システムコール番号 64 (write)
svc #0 // システムコール実行
// exit(0)
mov x0, #0 // 終了コード 0
mov x8, #93 // システムコール番号 93 (exit)
svc #0 // プログラム終了
説明:
-
.section .data: データセクション(定数を定義) -
.ascii: 文字列定義 -
ldr x1, =msg: メッセージのアドレスをx1に読み込む -
svc #0: システムコールを実行
2. 足し算(5 + 3)
ファイル名: add.s
.global _start
_start:
mov x0, #5 // x0 に 5 を代入
mov x1, #3 // x1 に 3 を代入
add x0, x0, x1 // x0 = x0 + x1 (5 + 3 = 8)
// exit(8)
mov x8, #93 // システムコール番号 93 (exit)
svc #0 // プログラム終了(終了コードは x0 の値)
実行後の確認:
echo $? # 8 が表示される
3. 引き算(10 - 3)
ファイル名: subtract.s
.global _start
_start:
mov x0, #10 // x0 に 10 を代入
mov x1, #3 // x1 に 3 を代入
sub x0, x0, x1 // x0 = x0 - x1 (10 - 3 = 7)
mov x8, #93 // exit システムコール
svc #0 // 終了コード: 7
4. 掛け算(6 × 7)
ファイル名: multiply.s
.global _start
_start:
mov x0, #6 // x0 に 6 を代入
mov x1, #7 // x1 に 7 を代入
mul x0, x0, x1 // x0 = x0 × x1 (6 × 7 = 42)
mov x8, #93 // exit システムコール
svc #0 // 終了コード: 42
5. ループ(1 + 2 + ... + 10)
ファイル名: sum.s
.global _start
_start:
mov x0, #0 // x0 = 0(合計を保存)
mov x1, #1 // x1 = 1(カウンタ)
loop:
add x0, x0, x1 // x0 = x0 + x1(合計に加算)
add x1, x1, #1 // x1 = x1 + 1(カウンタを1増やす)
cmp x1, #11 // x1 と 11 を比較
ble loop // x1 <= 10 なら loop へジャンプ
mov x8, #93 // exit システムコール
svc #0 // 終了コード: 55
処理の流れ:
x0 = 0, x1 = 1 → x0 = 1
x0 = 1, x1 = 2 → x0 = 3
x0 = 3, x1 = 3 → x0 = 6
...
x0 = 45, x1 = 10 → x0 = 55
6. 関数呼び出し
ファイル名: function.s
.global _start
_start:
mov x0, #10 // 引数: 10
bl double // double関数を呼び出す
// x0 には戻り値が入る (20)
mov x8, #93 // exit システムコール
svc #0 // 終了コード: 20
double:
add x0, x0, x0 // x0 = x0 + x0(2倍にする)
ret // 呼び出し元に戻る
説明:
-
bl double:double関数を呼び出し、戻りアドレスをx30(LR)に保存 -
ret: x30(LR)に保存されたアドレスに戻る
7. 条件分岐(大きい方を選ぶ)
ファイル名: compare.s
.global _start
_start:
mov x0, #15 // x0 = 15
mov x1, #20 // x1 = 20
cmp x0, x1 // x0 と x1 を比較
bge skip // x0 >= x1 なら skip へ
mov x0, x1 // x0 = x1 (x1の方が大きい)
skip:
mov x8, #93 // exit システムコール
svc #0 // 終了コード: 20(大きい方)
8. 複雑な計算((5 + 3) × (10 - 2))
ファイル名: complex.s
.global _start
_start:
// 第1の計算: 5 + 3
mov x0, #5
mov x1, #3
add x2, x0, x1 // x2 = 8
// 第2の計算: 10 - 2
mov x0, #10
mov x1, #2
sub x3, x0, x1 // x3 = 8
// 最終計算: x2 × x3
mul x0, x2, x3 // x0 = 8 × 8 = 64
mov x8, #93 // exit システムコール
svc #0 // 終了コード: 64
コンパイルと実行
方法1: gcc を使う(推奨)
gcc -nostdlib -static -o program program.s
./program
echo $? # 終了コードを確認
方法2: as と ld を使う
as -o program.o program.s
ld -o program program.o
./program
echo $?
方法3: aarc64用アセンブラを使う ※私はこの方法を用いました
sudo apt install binutils-aarch64-linux-gnu
aarch64-linux-gnu-as -o program.o program.s
aarch64-linux-gnu-as -o program program.o
./program
echo $?
デバッグ
# オブジェクトファイルの内容を確認
objdump -d program.o
# 実行ファイルの内容を確認
objdump -d program
# レジスタの状態を確認(gdb使用)
gdb ./program
(gdb) break _start
(gdb) run
(gdb) info registers
(gdb) step
補足:低レベル言語を学ぶ意味
「高級言語があるのに、なぜわざわざアセンブリのような低レベル言語を学ぶのか?」という疑問は自然です。ここでは実用的な理由を説明します。
1. コンピュータの動作原理を理解できる
高級言語では隠されている部分が見える:
- メモリがどう使われているか
- CPUがどう計算しているか
- プログラムがどう実行されているか
例: Pythonの x = 5 + 3 の裏側
mov x0, #5 // レジスタに5を読み込む
mov x1, #3 // レジスタに3を読み込む
add x2, x0, x1 // CPUが実際に足し算を実行
2. パフォーマンス最適化ができる
どこが遅いか理解できる:
# Python(遅い理由がわかりにくい)
for i in range(1000000):
result = data[i] * 2
アセンブリで考えると:
- ループごとにメモリアクセス(遅い)
- 型チェックのオーバーヘッド
- インタプリタの実行コスト
→ C/C++でリファクタリングする判断ができる
3. 組み込みシステム・IoT開発で必須
実例: 今回のRaspberry Pi + Sense HAT
- センサーは直接メモリアドレスで制御
- リアルタイム性が重要(OSのオーバーヘッド削減)
- メモリが限られている(数KB〜数MB)
具体的な場面:
- 産業用ロボット制御
- 自動車のECU(エンジン制御ユニット)
- 家電製品のファームウェア
4. セキュリティ・リバースエンジニアリング
バイナリ解析の実例:
# 実行ファイルの中身を見る
objdump -d ./program
できること:
- マルウェア解析
- 脆弱性診断(バッファオーバーフロー等)
- ソフトウェアのライセンス検証
5. デバッグ能力が飛躍的に向上
高級言語でバグが出たとき:
Segmentation fault (core dumped)
アセンブリがわかると:
gdb ./program
(gdb) disassemble
# どの命令でクラッシュしたか特定できる
6. 他の言語の理解が深まる
例1: なぜCの関数は引数を6個までしか高速に渡せないか?
→ ARM64のレジスタx0〜x7が引数用だから
例2: なぜPythonのリストは配列より遅いか?
→ ポインタの間接参照が発生するから
例3: なぜGo言語はgoroutineが軽量か?
→ スタック管理を工夫しているから
7. キャリアの選択肢が広がる
アセンブリが必要な職種:
| 分野 | 具体例 |
|---|---|
| 組み込み | 家電、自動車、医療機器 |
| ゲーム開発 | コンソール最適化、エンジン開発 |
| セキュリティ | マルウェア解析、脆弱性診断 |
| OS開発 | カーネル、デバイスドライバ |
| コンパイラ開発 | 最適化エンジニア |
8. 実際の開発での使い所
完全にアセンブリで書くことは稀ですが:
パターン1: ホットスポット最適化
// 99%の時間を使う関数だけアセンブリで書く
void critical_loop() {
asm volatile (
"mov x0, #0\n"
"loop:\n"
"add x0, x0, #1\n"
"cmp x0, #1000000\n"
"blt loop\n"
);
}
パターン2: ハードウェア制御
// GPIOを直接制御(Raspberry Pi)
void set_gpio(int pin) {
volatile uint32_t *gpio = (uint32_t*)0x3F200000;
*gpio = (1 << pin); // ← メモリアドレスに直接書き込み
}
パターン3: SIMD命令の活用
// 4つの値を同時に計算(高速化)
ld1 {v0.4s}, [x0] // 128ビット一気に読む
add v1.4s, v0.4s, v0.4s // 4つ同時に2倍
st1 {v1.4s}, [x1] // 128ビット一気に書く
9. 学習コストは実は低い
誤解: アセンブリは難しすぎる
現実: 基本命令は30個程度、1週間で基礎習得可能
| 言語 | 基本構文数 | 習得目安 |
|---|---|---|
| Python | 100+ | 1〜2ヶ月 |
| C言語 | 50+ | 1ヶ月 |
| アセンブリ | 30程度 | 1週間 |
参考資料
- ARM Architecture Reference Manual ARMv8
- GNU Assembler (GAS) Documentation
- Linux System Call Table (ARM64)
作成日: 2025年10月17日
対象: Raspberry Pi (ARM64/AArch64)
環境: Linux/GNU Assembler
Discussion