Assembler
アセンブリ言語 包括的技術ガイド
目次
- はじめに
- CPU アーキテクチャの基礎
- x86/x86-64 アーキテクチャ
- ARM/AArch64 アーキテクチャ
- RISC-V アーキテクチャ
- アセンブリ言語の基本構文
- データ型とメモリアドレッシングモード
- 算術・論理演算命令
- 制御フロー
- スタック操作と呼び出し規約
- システムコールと OS インターフェース
- SIMD 命令
- インラインアセンブリ
- アセンブラツールの設定と使い方
- リンカとオブジェクトファイルフォーマット
- デバッグ手法
- 最適化テクニック
- セキュリティ
- 実践例
- 現代の開発におけるアセンブリの役割
1. はじめに
1.1 アセンブリ言語とは
アセンブリ言語(Assembly Language)は、CPU が直接実行する機械語(マシンコード)と 1 対 1 に近い対応関係を持つ低水準プログラミング言語である。人間が読みやすいニーモニック(mnemonic)を使って機械語命令を記述し、アセンブラ(assembler)と呼ばれるツールがこれを実際の機械語バイナリに変換する。
高水準言語(C、Python、Java など)がハードウェアの詳細を抽象化するのに対し、アセンブリ言語はプロセッサのレジスタ、メモリアドレス、命令パイプラインといったハードウェアの振る舞いを直接的に制御できる。これにより、実行速度・メモリ効率・ハードウェア制御において最高レベルの最適化が可能となる。
高水準言語 (C/Python)
↓ コンパイラ / インタプリタ
アセンブリ言語
↓ アセンブラ
機械語 (バイナリ)
↓ CPU
実行
1.2 歴史と背景
アセンブリ言語の歴史は、コンピュータそのものの歴史と密接に結びついている。
| 年代 | 出来事 | 意義 |
|---|---|---|
| 1940年代 | ENIAC の直接配線プログラミング | プログラムをハードウェアで構成 |
| 1949年 | EDSAC で最初のアセンブラ開発 | ニーモニックによる命令記述の誕生 |
| 1950年代 | IBM 704 用 SAP (Symbolic Assembly Program) | マクロアセンブラの概念登場 |
| 1960年代 | IBM System/360 のアセンブラ | 統一アーキテクチャの汎用アセンブラ |
| 1970年代 | C 言語の登場(1972年) | 高水準言語への移行が加速 |
| 1978年 | Intel 8086 リリース | x86 アーキテクチャの始まり |
| 1985年 | Intel 80386 リリース | 32ビット x86(IA-32)の登場 |
| 1989年 | NASM(Netwide Assembler)開発開始 | オープンソースアセンブラの普及 |
| 2003年 | AMD64(x86-64)リリース | 64ビット拡張の標準化 |
| 2010年 | ARM Cortex シリーズの普及 | モバイル・組み込み分野での ARM 拡大 |
| 2011年 | RISC-V プロジェクト開始 | オープンソース ISA の登場 |
| 2020年 | Apple M1 (ARM ベース) リリース | デスクトップ/ラップトップでの ARM 本格化 |
| 2024年 | Apple M4 リリース | ARM ベース高性能プロセッサの成熟 |
1.3 なぜアセンブリ言語を学ぶ必要があるのか
現代の開発において、アセンブリ言語を日常的に書く機会は少ない。しかし、以下の理由から学習する価値は極めて高い。
1. コンピュータの動作原理の深い理解
アセンブリ言語を学ぶことで、CPU がどのように命令を実行し、メモリをどのように管理するかという根本的な仕組みを理解できる。高水準言語が「なぜそう動くのか」を理解するための基盤となる。
2. パフォーマンスの最適化
コンパイラが生成するアセンブリコードを読み解くことで、パフォーマンスボトルネックの原因を特定できる。また、SIMD 命令を使ったベクトル処理など、コンパイラが自動最適化しきれない領域での手動最適化が可能になる。
3. セキュリティの理解
バッファオーバーフロー、Return-Oriented Programming(ROP)、シェルコードなどのセキュリティ攻撃手法は、すべてアセンブリレベルの理解が前提となる。脆弱性分析やエクスプロイト開発にはアセンブリの知識が不可欠である。
4. リバースエンジニアリング
マルウェア解析、プロプライエタリソフトウェアの動作解析、レガシーシステムの保守などでは、逆アセンブルされたコードを読む能力が必要となる。
5. 組み込みシステム開発
マイクロコントローラやリアルタイムシステムの開発では、ブートローダー、割り込みハンドラ、デバイスドライバなどの低水準コードをアセンブリで記述する場面がある。
6. OS / カーネル開発
オペレーティングシステムのカーネルには、コンテキストスイッチ、システムコール遷移、割り込み処理など、必然的にアセンブリで記述する部分が存在する。
1.4 本記事の対象読者と前提知識
本記事は、C/C++ など高水準言語の基本的な知識を持ち、コンピュータアーキテクチャやアセンブリ言語をより深く理解したいソフトウェアエンジニアを対象としている。以下の知識があることを前提とする。
- C 言語の基本構文(ポインタ、配列、構造体、関数呼び出し)
- 二進数・十六進数の基礎
- コマンドラインツールの基本操作(Linux/macOS)
- コンパイルとリンクの基本概念
2. CPU アーキテクチャの基礎
2.1 CPU の基本構成
CPU(Central Processing Unit)は以下の主要コンポーネントから構成される。
┌─────────────────────────────────────────────────────────────┐
│ CPU │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ 制御ユニット │ │ ALU │ │ レジスタファイル │ │
│ │ (Control │ │ (Arithmetic │ │ (Register File) │ │
│ │ Unit) │ │ Logic │ │ │ │
│ │ │ │ Unit) │ │ 汎用レジスタ │ │
│ │ 命令デコーダ │ │ │ │ 特殊レジスタ │ │
│ │ 命令ポインタ │ │ 整数演算 │ │ フラグレジスタ │ │
│ │ マイクロコード │ │ 浮動小数点 │ │ │ │
│ └──────────────┘ │ SIMD │ └──────────────────┘ │
│ └──────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ キャッシュ階層 │ │
│ │ L1 命令キャッシュ | L1 データキャッシュ | L2 キャッシュ │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
↕ バス
┌─────────────────────────────────────────────────────────────┐
│ メインメモリ (RAM) │
└─────────────────────────────────────────────────────────────┘
2.2 レジスタ
レジスタは CPU 内部にある最も高速な記憶装置である。メインメモリ(RAM)へのアクセスが数十~数百サイクルかかるのに対し、レジスタへのアクセスは 1 サイクルで完了する。
レジスタの種類
| 種類 | 説明 | 例 |
|---|---|---|
| 汎用レジスタ (GPR) | 演算やデータ保持に使用 | RAX, RBX (x86-64), X0-X30 (AArch64) |
| プログラムカウンタ (PC) | 次に実行する命令のアドレス | RIP (x86-64), PC (ARM) |
| スタックポインタ (SP) | 現在のスタックトップのアドレス | RSP (x86-64), SP (ARM) |
| フラグ/状態レジスタ | 演算結果の条件フラグ | RFLAGS (x86-64), NZCV (ARM) |
| セグメントレジスタ | メモリセグメンテーション | CS, DS, SS (x86-64) |
| 浮動小数点レジスタ | 浮動小数点演算用 | XMM0-15, YMM0-15 (x86-64) |
| ベクトルレジスタ | SIMD 演算用 | ZMM0-31 (AVX-512), V0-V31 (ARM NEON) |
2.3 メモリモデル
フォン・ノイマン型 vs ハーバード型
フォン・ノイマン型: ハーバード型:
┌─────┐ ┌──────────┐ ┌─────┐ ┌──────────────┐
│ CPU │◄──►│命令+データ │ │ CPU │◄──►│ 命令メモリ │
└─────┘ │ メモリ │ │ │◄──►│ データメモリ │
└──────────┘ └─────┘ └──────────────┘
現代の CPU は修正ハーバード型アーキテクチャを採用している。L1 キャッシュは命令用(I-cache)とデータ用(D-cache)に分離しているが、メインメモリは統一されている。
メモリ階層
レジスタ : ~1 サイクル, KB 規模
L1 キャッシュ : ~4 サイクル, 32-64 KB
L2 キャッシュ : ~12 サイクル, 256 KB - 1 MB
L3 キャッシュ : ~40 サイクル, 4-32 MB
メインメモリ : ~100-300 サイクル, GB 規模
SSD : ~10,000 サイクル, TB 規模
HDD : ~10,000,000 サイクル, TB 規模
エンディアン
データのバイト順序はアーキテクチャによって異なる。
値 0x12345678 のメモリ上の配置:
リトルエンディアン (x86, ARM default):
アドレス: 0x00 0x01 0x02 0x03
値: 0x78 0x56 0x34 0x12 ← 下位バイトが低いアドレス
ビッグエンディアン (ネットワークバイトオーダー):
アドレス: 0x00 0x01 0x02 0x03
値: 0x12 0x34 0x56 0x78 ← 上位バイトが低いアドレス
x86/x86-64 はリトルエンディアン固定。ARM は設定可能だが、通常リトルエンディアンで使用される(AArch64 はデフォルトでリトルエンディアン)。
2.4 命令セットアーキテクチャ (ISA)
ISA は CPU とソフトウェアの間のインターフェースを定義する。
CISC vs RISC
| 特性 | CISC (x86) | RISC (ARM, RISC-V) |
|---|---|---|
| 命令長 | 可変長(1-15バイト) | 固定長(4バイト) |
| 命令の複雑さ | 複雑な命令が多い | 単純な命令に限定 |
| メモリアクセス | 演算命令でメモリ直接参照可能 | Load/Store アーキテクチャ |
| レジスタ数 | 比較的少ない(16個) | 多い(31-32個) |
| デコード | 複雑なデコーダが必要 | シンプルなデコーダ |
| パイプライン | 複雑 | シンプルで効率的 |
| 消費電力 | 高い傾向 | 低い傾向 |
命令の基本形式
; x86-64 (CISC): 操作コード + オペランド(可変長)
mov rax, [rbx + rcx*8 + 16] ; メモリから直接ロード
add [rsp + 8], 42 ; メモリ上の値に直接加算
; ARM64 (RISC): 固定32ビット命令
ldr x0, [x1, x2, lsl #3] ; まずメモリからロード
add x0, x0, #42 ; レジスタ上で加算
str x0, [x1, x2, lsl #3] ; メモリへストア
2.5 命令実行のパイプライン
現代の CPU は命令をパイプライン処理して並列実行する。
基本的な5段パイプライン:
時間 → T1 T2 T3 T4 T5 T6 T7
命令1: IF ID EX MEM WB
命令2: IF ID EX MEM WB
命令3: IF ID EX MEM WB
IF = Instruction Fetch(命令フェッチ)
ID = Instruction Decode(命令デコード)
EX = Execute(実行)
MEM = Memory Access(メモリアクセス)
WB = Write Back(書き戻し)
現代の高性能 CPU(Intel Core、Apple M4 など)はさらに高度なパイプラインを持つ。
- スーパースカラ: 複数の命令を同時にフェッチ・デコード・実行
- アウトオブオーダー実行: データ依存がない命令を順序を入れ替えて実行
- 投機的実行: 分岐結果を予測して先に実行を開始
- レジスタリネーミング: WAR/WAW ハザードを解消
3. x86/x86-64 アーキテクチャ
3.1 x86 アーキテクチャの歴史的経緯
x86 アーキテクチャは Intel 8086(1978年)に始まり、40年以上にわたる後方互換性を維持しながら拡張されてきた。
8086 (16bit, 1978)
→ 80286 (保護モード, 1982)
→ 80386 (32bit, 1985)
→ 80486 (統合FPU, 1989)
→ Pentium (スーパースカラ, 1993)
→ Pentium Pro (アウトオブオーダー, 1995)
→ AMD64/x86-64 (64bit, 2003)
→ 現代の Intel Core / AMD Ryzen
3.2 x86-64 汎用レジスタ
x86-64 は 16 個の 64 ビット汎用レジスタを持つ。各レジスタは 8/16/32/64 ビット幅でアクセスできる。
64-bit 32-bit 16-bit 8-bit(H) 8-bit(L)
┌─────────────────────────────────────────────────┐
│ RAX │ EAX │ AX │ AH │ AL │ アキュムレータ
│ RBX │ EBX │ BX │ BH │ BL │ ベースレジスタ
│ RCX │ ECX │ CX │ CH │ CL │ カウンタ
│ RDX │ EDX │ DX │ DH │ DL │ データレジスタ
│ RSI │ ESI │ SI │ ─ │ SIL │ ソースインデックス
│ RDI │ EDI │ DI │ ─ │ DIL │ デスティネーションインデックス
│ RBP │ EBP │ BP │ ─ │ BPL │ ベースポインタ
│ RSP │ ESP │ SP │ ─ │ SPL │ スタックポインタ
│ R8 │ R8D │ R8W │ ─ │ R8B │ 汎用 (x86-64 で追加)
│ R9 │ R9D │ R9W │ ─ │ R9B │
│ R10 │ R10D │ R10W │ ─ │ R10B │
│ R11 │ R11D │ R11W │ ─ │ R11B │
│ R12 │ R12D │ R12W │ ─ │ R12B │
│ R13 │ R13D │ R13W │ ─ │ R13B │
│ R14 │ R14D │ R14W │ ─ │ R14B │
│ R15 │ R15D │ R15W │ ─ │ R15B │
└─────────────────────────────────────────────────┘
重要な注意点: 32 ビットレジスタ(EAX など)に書き込むと、上位 32 ビットは自動的にゼロクリアされる。一方、8/16 ビットレジスタへの書き込みでは上位ビットは変更されない。
; x86-64 NASM
mov eax, 0xFFFFFFFF ; RAX = 0x00000000FFFFFFFF (上位32bitがゼロクリア)
mov ax, 0x1234 ; RAX = 0x00000000FFFF1234 (上位48bitは変更されない)
mov al, 0x56 ; RAX = 0x00000000FFFF1256 (上位56bitは変更されない)
3.3 特殊レジスタ
命令ポインタ (RIP)
; RIP は直接読み書きできないが、RIP 相対アドレッシングで使用可能
lea rax, [rip + offset] ; RIP 相対アドレッシング(x86-64 の標準)
; PIC(Position-Independent Code)の基本
フラグレジスタ (RFLAGS)
RFLAGS レジスタのビット構成:
ビット 名前 説明
─────────────────────────────────────────
0 CF キャリーフラグ(符号なし演算のオーバーフロー)
2 PF パリティフラグ(結果の下位8ビットの1の数が偶数)
4 AF 補助キャリーフラグ(BCD演算用)
6 ZF ゼロフラグ(結果がゼロ)
7 SF サインフラグ(結果が負)
8 TF トラップフラグ(シングルステップデバッグ)
9 IF 割り込み許可フラグ
10 DF 方向フラグ(文字列操作の方向)
11 OF オーバーフローフラグ(符号付き演算のオーバーフロー)
12-13 IOPL I/O特権レベル
14 NT ネストタスクフラグ
21 ID CPUID命令サポートフラグ
; フラグの使用例
section .text
global _start
_start:
mov rax, 0x7FFFFFFFFFFFFFFF ; 符号付き最大値
add rax, 1 ; オーバーフロー発生
; OF=1 (符号付きオーバーフロー), SF=1 (結果が負), CF=0
mov rbx, 0xFFFFFFFFFFFFFFFF ; -1 (符号付き) / 最大値 (符号なし)
add rbx, 1 ; CF=1 (符号なしオーバーフロー), ZF=1, OF=0
; 条件分岐でフラグを使用
cmp rax, rbx
je equal ; ZF=1 なら分岐
jg greater ; SF=OF かつ ZF=0 なら分岐
jl less ; SF≠OF なら分岐
セグメントレジスタ
CS - コードセグメント(現在実行中のコードのセグメント)
DS - データセグメント(デフォルトのデータアクセス)
SS - スタックセグメント(スタック操作)
ES - エクストラセグメント(文字列操作の送り先)
FS - 追加セグメント(Linux: スレッドローカルストレージ)
GS - 追加セグメント(macOS: スレッドローカルストレージ、Linux カーネル: per-CPU データ)
x86-64 のロングモードでは、セグメンテーションは事実上フラットモデル(ベース=0)として動作する。ただし、FS と GS はスレッドローカルストレージ(TLS)のために使用される。
3.4 浮動小数点・SIMD レジスタ
x87 FPU スタック: ST(0) - ST(7) 80bit 拡張精度
SSE: XMM0 - XMM15 128bit
AVX: YMM0 - YMM15 256bit (XMM の拡張)
AVX-512: ZMM0 - ZMM31 512bit (YMM の拡張)
k0 - k7 マスクレジスタ(64bit)
レジスタの関係:
┌────────────────────────────────────────────────────────────────┐
│ ZMM0 (512-bit) │
│ ┌───────────────────────────────────────────┤
│ │ YMM0 (256-bit) │
│ │ ┌────────────────────────────────┤
│ │ │ XMM0 (128-bit) │
└────────────────────┴──────────┴────────────────────────────────┘
3.5 メモリモデルとページング
x86-64 では 4 レベルのページテーブル(PML4)を使用し、48 ビット仮想アドレス空間(256 TB)をサポートする。
x86-64 仮想アドレスの構造 (4KB ページ):
63 48 47 39 38 30 29 21 20 12 11 0
┌─────────┬────────┬────────┬────────┬────────┬──────────┐
│ Sign Ext│ PML4 │ PDPT │ PD │ PT │ Offset │
│ (16bit) │ (9bit) │ (9bit) │ (9bit) │ (9bit) │ (12bit) │
└─────────┴────────┴────────┴────────┴────────┴──────────┘
5レベルページング(LA57、Intel Ice Lake 以降)では
57ビット仮想アドレス空間(128 PB)をサポート
3.6 動作モード
x86-64 プロセッサは複数の動作モードを持つ。
| モード | ビット幅 | 説明 |
|---|---|---|
| Real Mode | 16bit | 電源投入時のモード。1MB メモリ空間 |
| Protected Mode | 16/32bit | セグメンテーション、ページング、特権レベル |
| Long Mode (64-bit Mode) | 64bit | x86-64 の完全な 64 ビットモード |
| Compatibility Mode | 32bit | Long Mode 内で 32 ビットコードを実行 |
| System Management Mode | - | ファームウェア用の特殊モード |
4. ARM/AArch64 アーキテクチャ
4.1 ARM アーキテクチャの概要
ARM(Advanced RISC Machine)は、ARM Holdings(現 Arm Ltd.)が設計する RISC ベースの命令セットアーキテクチャ(ISA)である。自社でプロセッサを製造するのではなく、IP ライセンスモデルにより Apple、Qualcomm、Samsung 等の半導体メーカーに設計をライセンスしている。
ARM アーキテクチャは、ARMv7(32ビット、AArch32)と ARMv8 以降(64ビット、AArch64)に大別される。本記事では主に AArch64 を扱う。
4.2 AArch64 レジスタセット
AArch64 は 31 個の 64 ビット汎用レジスタを持つ(x86-64 の 16 個と比較して約 2 倍)。
汎用レジスタ:
┌──────────────────────────────────────────────┐
│ 64-bit │ 32-bit │ 用途 │
├───────────┼───────────┼───────────────────────┤
│ X0 - X7 │ W0 - W7 │ 引数 / 戻り値 │
│ X8 │ W8 │ 間接結果レジスタ │
│ X9 - X15 │ W9 - W15 │ 一時レジスタ(caller-saved)│
│ X16 (IP0) │ W16 │ イントラプロシージャコール │
│ X17 (IP1) │ W17 │ イントラプロシージャコール │
│ X18 (PR) │ W18 │ プラットフォーム予約 │
│ X19 - X28 │ W19 - W28 │ callee-saved レジスタ │
│ X29 (FP) │ W29 │ フレームポインタ │
│ X30 (LR) │ W30 │ リンクレジスタ │
├───────────┼───────────┼───────────────────────┤
│ SP │ WSP │ スタックポインタ │
│ PC │ ─ │ プログラムカウンタ │
│ XZR │ WZR │ ゼロレジスタ(常に0) │
└──────────────────────────────────────────────┘
重要な相違点(x86-64 との比較):
- ゼロレジスタ(XZR/WZR): 常にゼロを返す特殊レジスタ。ゼロ初期化やカウント比較で効率的
- リンクレジスタ(X30/LR): BL 命令で戻りアドレスが自動的に格納される(x86 はスタックに push)
- X18 はプラットフォーム予約。macOS/iOS ではシステムが使用するため、ユーザーコードでは使用禁止
4.3 条件フラグとシステムレジスタ
NZCV レジスタ (条件フラグ):
N - Negative(結果が負)
Z - Zero(結果がゼロ)
C - Carry(キャリー/ボロー)
V - Overflow(符号付きオーバーフロー)
FPCR - 浮動小数点制御レジスタ
FPSR - 浮動小数点ステータスレジスタ
4.4 AArch64 命令の特徴
ARM64 の命令はすべて固定 32 ビット長であり、命令デコーダが大幅に簡素化される。
// AArch64 アセンブリの基本命令
// レジスタ間の移動
mov x0, x1 // x0 = x1
mov x0, #42 // x0 = 42 (即値)
movz x0, #0x1234 // x0 = 0x1234
movk x0, #0x5678, lsl #16 // x0 の 16-31 ビットに 0x5678 をセット
// 算術演算
add x0, x1, x2 // x0 = x1 + x2
add x0, x1, #100 // x0 = x1 + 100
adds x0, x1, x2 // x0 = x1 + x2 (フラグ更新)
sub x0, x1, x2 // x0 = x1 - x2
mul x0, x1, x2 // x0 = x1 * x2
madd x0, x1, x2, x3 // x0 = x1 * x2 + x3 (積和演算)
// シフトとビット操作
lsl x0, x1, #4 // x0 = x1 << 4 (論理左シフト)
lsr x0, x1, #4 // x0 = x1 >> 4 (論理右シフト)
asr x0, x1, #4 // x0 = x1 >> 4 (算術右シフト)
and x0, x1, x2 // x0 = x1 & x2
orr x0, x1, x2 // x0 = x1 | x2
eor x0, x1, x2 // x0 = x1 ^ x2
// メモリアクセス (Load/Store)
ldr x0, [x1] // x0 = *x1 (64bit ロード)
ldr w0, [x1] // w0 = *(uint32_t*)x1 (32bit ロード)
ldrb w0, [x1] // w0 = *(uint8_t*)x1 (バイトロード)
str x0, [x1] // *x1 = x0 (64bit ストア)
ldp x0, x1, [x2] // ペアロード: x0=*x2, x1=*(x2+8)
stp x0, x1, [sp, #-16]! // プリデクリメント付きペアストア
4.5 条件実行と条件選択
AArch64 では ARM32 の条件付き実行(条件プレフィックス)は廃止され、代わりに条件選択命令が導入された。
// 条件分岐
cmp x0, x1
b.eq label_equal // x0 == x1 なら分岐
b.ne label_not_equal // x0 != x1 なら分岐
b.lt label_less // x0 < x1 (符号付き)
b.gt label_greater // x0 > x1 (符号付き)
b.lo label_lower // x0 < x1 (符号なし)
b.hi label_higher // x0 > x1 (符号なし)
// 条件選択命令 (分岐を回避してパイプライン効率を向上)
cmp x0, x1
csel x2, x3, x4, eq // x2 = (x0 == x1) ? x3 : x4
csinc x2, x3, x4, lt // x2 = (x0 < x1) ? x3 : x4 + 1
cset x2, eq // x2 = (x0 == x1) ? 1 : 0
// abs(x0) の実装例
cmp x0, #0
csneg x0, x0, x0, ge // x0 = (x0 >= 0) ? x0 : -x0
4.6 Apple Silicon (M1/M2/M3/M4) の特徴
Apple Silicon は ARMv8.5-A 以降をベースとした独自設計の SoC である。
Apple M4 の主な仕様:
─────────────────────────────────
ISA: ARMv9.2-A
高性能コア: 4x (P-core)
高効率コア: 6x (E-core)
L1 I-cache: 192 KB (P-core), 128 KB (E-core)
L1 D-cache: 128 KB (P-core), 64 KB (E-core)
L2 キャッシュ: 16 MB (P-core), 4 MB (E-core)
SLC: 16 MB (System Level Cache)
SIMD: 128-bit NEON + SME (Scalable Matrix Extension)
メモリ: 統合メモリアーキテクチャ (LPDDR5X)
Apple Silicon 固有の機能:
// ポインタ認証 (PAC - Pointer Authentication Code)
// ARMv8.3-A で導入、Apple が積極的に活用
paciasp // SP をキーにして LR に署名
autiasp // LR の署名を検証
retab // 認証付きリターン
// メモリタグ拡張 (MTE - Memory Tagging Extension)
// ARMv8.5-A で導入
irg x0, sp // ランダムタグを生成
stg x0, [x0] // メモリにタグを設定
ldg x0, [x1] // メモリからタグを読み取り
// Apple 固有の AMX (Apple Matrix Extensions) - 非公開命令セット
// 機械学習の行列演算を高速化(Neural Engine と連携)
// 公式ドキュメントは未公開だが、Accelerate フレームワーク経由で利用可能
4.7 macOS/iOS での AArch64 アセンブリ
macOS (Apple Silicon) でのアセンブリプログラミングには、いくつかの注意点がある。
// macOS AArch64 の Hello World
// ファイル: hello_mac.s
.global _main
.align 4
_main:
// macOS のシステムコール番号は Unix 番号 + 0x2000000
// write(1, message, 13)
mov x0, #1 // fd = stdout
adrp x1, message@PAGE
add x1, x1, message@PAGEOFF // message のアドレス
mov x2, #13 // length
mov x16, #4 // syscall: write (macOS: 0x2000004)
svc #0x80 // システムコール呼び出し
// exit(0)
mov x0, #0 // exit code
mov x16, #1 // syscall: exit (macOS: 0x2000001)
svc #0x80
.data
message:
.ascii "Hello, ARM64\n"
# macOS でのビルドと実行
as -o hello_mac.o hello_mac.s
ld -o hello_mac hello_mac.o -lSystem -syslibroot $(xcrun --show-sdk-path) -e _main -arch arm64
./hello_mac
5. RISC-V アーキテクチャ
5.1 RISC-V の概要
RISC-V(リスクファイブ)は、カリフォルニア大学バークレー校で 2010 年に開発が始まったオープンソースの ISA(命令セットアーキテクチャ)である。BSD ライセンスで公開されており、誰でも自由に RISC-V プロセッサを設計・製造・販売できる。
ARM が IP ライセンス料を必要とするのに対し、RISC-V はライセンスフリーであるため、特にカスタム SoC、学術研究、IoT デバイスの分野で急速に普及している。
5.2 RISC-V のモジュラー ISA
RISC-V の最大の特徴は、モジュラー(モジュール式)な ISA 設計である。基本整数命令セット(I)に対して、必要な拡張を選択的に追加できる。
基本命令セット:
RV32I - 32ビット基本整数命令(47命令)
RV64I - 64ビット基本整数命令
RV128I - 128ビット基本整数命令(仕様策定中)
標準拡張:
M - 整数乗除算 (Multiply/Divide)
A - アトミック操作 (Atomic)
F - 単精度浮動小数点 (Single-precision Float)
D - 倍精度浮動小数点 (Double-precision Float)
Q - 四倍精度浮動小数点 (Quad-precision Float)
C - 圧縮命令 (Compressed: 16ビット命令)
V - ベクトル拡張 (Vector)
B - ビット操作 (Bit manipulation)
よく使われる組み合わせ:
RV32IMAC - 組み込み向け(32ビット、乗除算、アトミック、圧縮命令)
RV64GC - 汎用(G = IMAFD, C = 圧縮命令)
RV64IMAFDC - RV64GC と同じ(Linux 向け標準)
5.3 RISC-V レジスタセット
RV64I 汎用レジスタ (32個 × 64ビット):
┌─────────┬──────────┬──────────────────────────────┐
│ レジスタ │ ABI 名 │ 用途 │
├─────────┼──────────┼──────────────────────────────┤
│ x0 │ zero │ ハードワイヤードゼロ(常に0) │
│ x1 │ ra │ リターンアドレス │
│ x2 │ sp │ スタックポインタ │
│ x3 │ gp │ グローバルポインタ │
│ x4 │ tp │ スレッドポインタ │
│ x5-x7 │ t0-t2 │ 一時レジスタ │
│ x8 │ s0/fp │ saved レジスタ / フレームポインタ │
│ x9 │ s1 │ saved レジスタ │
│ x10-x11 │ a0-a1 │ 関数引数 / 戻り値 │
│ x12-x17 │ a2-a7 │ 関数引数 │
│ x18-x27 │ s2-s11 │ saved レジスタ │
│ x28-x31 │ t3-t6 │ 一時レジスタ │
├─────────┼──────────┼──────────────────────────────┤
│ pc │ ─ │ プログラムカウンタ │
└─────────┴──────────┴──────────────────────────────┘
5.4 RISC-V 命令フォーマット
RISC-V の命令フォーマットは 6 種類に分類される。すべて 32 ビット固定長(C 拡張使用時は 16 ビットも可)。
R-Type (レジスタ間演算):
31 25 24 20 19 15 14 12 11 7 6 0
┌──────────┬───────┬───────┬──────┬───────┬────────┐
│ funct7 │ rs2 │ rs1 │funct3│ rd │ opcode │
└──────────┴───────┴───────┴──────┴───────┴────────┘
I-Type (即値演算・ロード):
31 20 19 15 14 12 11 7 6 0
┌─────────────────────┬───────┬──────┬───────┬────────┐
│ imm[11:0] │ rs1 │funct3│ rd │ opcode │
└─────────────────────┴───────┴──────┴───────┴────────┘
S-Type (ストア):
31 25 24 20 19 15 14 12 11 7 6 0
┌──────────┬───────┬───────┬──────┬───────┬────────┐
│ imm[11:5]│ rs2 │ rs1 │funct3│imm[4:0]│ opcode │
└──────────┴───────┴───────┴──────┴───────┴────────┘
B-Type (条件分岐):
31 30 25 24 20 19 15 14 12 11 8 7 6 0
┌───┬─────────┬───────┬───────┬──────┬──────┬───┬────────┐
│[12]│imm[10:5]│ rs2 │ rs1 │funct3│[4:1] │[11]│opcode │
└───┴─────────┴───────┴───────┴──────┴──────┴───┴────────┘
5.5 RISC-V アセンブリの基本
# RISC-V アセンブリ (RV64I)
# GAS (GNU Assembler) 構文
# レジスタ操作
li a0, 42 # a0 = 42 (疑似命令: addi a0, zero, 42)
mv a1, a0 # a1 = a0 (疑似命令: addi a1, a0, 0)
add a0, a1, a2 # a0 = a1 + a2
addi a0, a1, 10 # a0 = a1 + 10
sub a0, a1, a2 # a0 = a1 - a2
mul a0, a1, a2 # a0 = a1 * a2 (M 拡張)
div a0, a1, a2 # a0 = a1 / a2 (M 拡張)
# 論理演算
and a0, a1, a2 # a0 = a1 & a2
or a0, a1, a2 # a0 = a1 | a2
xor a0, a1, a2 # a0 = a1 ^ a2
sll a0, a1, a2 # a0 = a1 << a2 (論理左シフト)
srl a0, a1, a2 # a0 = a1 >> a2 (論理右シフト)
sra a0, a1, a2 # a0 = a1 >> a2 (算術右シフト)
# メモリアクセス
ld a0, 0(sp) # a0 = *(int64_t*)(sp + 0)
lw a0, 4(sp) # a0 = *(int32_t*)(sp + 4)
lh a0, 8(sp) # a0 = *(int16_t*)(sp + 8)
lb a0, 12(sp) # a0 = *(int8_t*)(sp + 12)
sd a0, 0(sp) # *(int64_t*)(sp + 0) = a0
sw a0, 4(sp) # *(int32_t*)(sp + 4) = a0
# 分岐
beq a0, a1, label # a0 == a1 なら分岐
bne a0, a1, label # a0 != a1 なら分岐
blt a0, a1, label # a0 < a1 (符号付き) なら分岐
bge a0, a1, label # a0 >= a1 (符号付き) なら分岐
bltu a0, a1, label # a0 < a1 (符号なし) なら分岐
# 関数呼び出し
jal ra, function # function を呼び出し (ra に戻りアドレスを保存)
jalr ra, 0(a0) # a0 が指すアドレスを呼び出し
ret # 疑似命令: jalr zero, 0(ra)
5.6 RISC-V Hello World (Linux)
# hello_riscv.s - RISC-V 64-bit Linux
.global _start
.section .text
_start:
# write(1, message, 14)
li a7, 64 # syscall: write (RISC-V Linux = 64)
li a0, 1 # fd = stdout
la a1, message # buffer address
li a2, 14 # length
ecall # システムコール呼び出し
# exit(0)
li a7, 93 # syscall: exit (RISC-V Linux = 93)
li a0, 0 # exit code
ecall
.section .rodata
message:
.ascii "Hello, RISC-V\n"
# クロスコンパイルとエミュレーション
riscv64-linux-gnu-as -o hello_riscv.o hello_riscv.s
riscv64-linux-gnu-ld -o hello_riscv hello_riscv.o
qemu-riscv64 ./hello_riscv
5.7 三大アーキテクチャの比較
| 特性 | x86-64 | AArch64 | RISC-V (RV64GC) |
|---|---|---|---|
| 設計思想 | CISC | RISC | RISC |
| 命令長 | 1-15 バイト | 4 バイト固定 | 4 バイト (C拡張: 2バイト) |
| 汎用レジスタ数 | 16 | 31 | 31 |
| ゼロレジスタ | なし | XZR/WZR | x0 (zero) |
| 条件実行 | フラグベース分岐 | 条件選択命令 | 比較分岐命令 |
| ライセンス | Intel/AMD 独占 | Arm のライセンス | オープンソース (BSD) |
| 主な用途 | デスクトップ、サーバー | モバイル、サーバー、Mac | 組み込み、学術、新興 |
| エンディアン | リトルエンディアン | バイエンディアン(通常LE) | リトルエンディアン |
| メモリモデル | 強順序 (TSO) | 弱順序 | 弱順序 (RVWMO) |
6. アセンブリ言語の基本構文
6.1 二大構文体系: Intel 構文と AT&T 構文
x86/x86-64 のアセンブリ言語には、2 つの主要な構文体系が存在する。
| 特性 | Intel 構文 (NASM) | AT&T 構文 (GAS) |
|---|---|---|
| オペランド順序 | dest, src | src, dest |
| レジスタ接頭辞 | なし | % |
| 即値接頭辞 | なし | $ |
| メモリ参照 | [base + index*scale + disp] | disp(%base, %index, scale) |
| サイズ指定 | BYTE/WORD/DWORD/QWORD | b/w/l/q サフィックス |
| 主なアセンブラ | NASM, YASM, MASM | GAS (GNU Assembler) |
| 主な使用場面 | Windows, 独立プログラム | Linux, GCC 出力, macOS |
; === Intel 構文 (NASM) ===
section .text
global _start
_start:
mov rax, 1 ; RAX に 1 を代入
mov rdi, 1 ; 第1引数: stdout
lea rsi, [rel message] ; 第2引数: 文字列アドレス
mov rdx, 13 ; 第3引数: 長さ
syscall ; write(1, message, 13)
mov rax, 60 ; exit syscall
xor rdi, rdi ; exit code 0
syscall
section .rodata
message:
db "Hello, World!", 10 ; 10 = '\n'
# === AT&T 構文 (GAS) ===
.section .text
.global _start
_start:
movq $1, %rax # RAX に 1 を代入
movq $1, %rdi # 第1引数: stdout
leaq message(%rip), %rsi # 第2引数: 文字列アドレス (RIP相対)
movq $13, %rdx # 第3引数: 長さ
syscall # write(1, message, 13)
movq $60, %rax # exit syscall
xorq %rdi, %rdi # exit code 0
syscall
.section .rodata
message:
.ascii "Hello, World!\n"
6.2 NASM の基本構文
NASM(Netwide Assembler)は最も広く使われるオープンソースの x86/x86-64 アセンブラである。
; NASM の基本構造
; ==============================
; 1. セクション定義
section .data ; 初期化済みデータ
myvar dd 42 ; 32ビット整数
mystr db "Hello", 0 ; NULL 終端文字列
myarr times 100 db 0 ; 100バイトのゼロ配列
float1 dd 3.14 ; 単精度浮動小数点
double1 dq 2.71828 ; 倍精度浮動小数点
section .bss ; 未初期化データ
buffer resb 1024 ; 1024バイト予約
counter resd 1 ; 32ビット整数予約
bigbuf resq 256 ; 256個の64ビット値予約
section .rodata ; 読み取り専用データ
fmtstr db "Value: %d", 10, 0
section .text ; コード
global main
extern printf
; 2. データ定義疑似命令
; db - Define Byte (1バイト)
; dw - Define Word (2バイト)
; dd - Define Doubleword (4バイト)
; dq - Define Quadword (8バイト)
; dt - Define Ten-byte (10バイト, x87 拡張精度)
; 3. サイズ指定
; BYTE - 1バイト
; WORD - 2バイト
; DWORD - 4バイト
; QWORD - 8バイト
; 4. マクロ定義
%macro pushall 0
push rax
push rbx
push rcx
push rdx
%endmacro
%macro popall 0
pop rdx
pop rcx
pop rbx
pop rax
%endmacro
; 5. 条件付きアセンブル
%ifdef DEBUG
; デバッグ用コード
%endif
%if __BITS__ == 64
; 64ビット用コード
%else
; 32ビット用コード
%endif
; 6. 定数定義
%define BUFFER_SIZE 4096
%define SYS_WRITE 1
%define SYS_EXIT 60
%define STDOUT 1
main:
push rbp
mov rbp, rsp
; printf の呼び出し
lea rdi, [rel fmtstr]
mov esi, [rel myvar]
xor eax, eax ; 浮動小数点引数の数 = 0
call printf wrt ..plt
xor eax, eax ; return 0
leave
ret
6.3 GAS (GNU Assembler) の基本構文
# GAS の基本構造 (AT&T 構文)
# ==============================
# 1. ディレクティブ
.section .data
myvar: .long 42 # 32ビット整数
mystr: .asciz "Hello" # NULL 終端文字列
myarr: .fill 100, 1, 0 # 100バイトのゼロ
float1: .float 3.14 # 単精度浮動小数点
double1: .double 2.71828 # 倍精度浮動小数点
mywords: .word 1, 2, 3, 4 # 16ビット整数の配列
.section .bss
.lcomm buffer, 1024 # 1024バイト予約
.lcomm counter, 4 # 4バイト予約
.section .rodata
fmtstr: .string "Value: %d\n"
.section .text
.global main
.type main, @function
# 2. アラインメント
.align 16 # 16バイト境界にアラインメント
.p2align 4 # 2^4 = 16バイト境界
# 3. マクロ定義
.macro push_callee_saved
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
.endm
.macro pop_callee_saved
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
.endm
# 4. 条件付きアセンブル
.ifdef DEBUG
# デバッグ用コード
.endif
# 5. 定数定義
.equ BUFFER_SIZE, 4096
.equ SYS_WRITE, 1
.equ SYS_EXIT, 60
.equ STDOUT, 1
main:
pushq %rbp
movq %rsp, %rbp
# printf の呼び出し
leaq fmtstr(%rip), %rdi
movl myvar(%rip), %esi
xorl %eax, %eax
call printf@PLT
xorl %eax, %eax
leave
ret
.size main, .-main
6.4 GAS の Intel 構文モード
GAS は .intel_syntax ディレクティブで Intel 構文モードに切り替えることもできる。
# GAS で Intel 構文を使用
.intel_syntax noprefix
.section .text
.global main
main:
push rbp
mov rbp, rsp
mov eax, 42
add eax, 8
leave
ret
.att_syntax # AT&T 構文に戻す
6.5 ARM64 (AArch64) アセンブリ構文
ARM64 は統一アセンブリ言語(UAL)構文を使用する。
// AArch64 アセンブリの基本構造
// GAS 形式 (clang/LLVM-MC も同様)
.global _main
.align 4
// データセクション
.section __DATA,__data
myvar: .word 42
mystr: .asciz "Hello, ARM64"
// テキストセクション
.section __TEXT,__text
_main:
// スタックフレームの設定
stp x29, x30, [sp, #-16]! // FP, LR を保存
mov x29, sp // フレームポインタ設定
// 処理
adrp x0, myvar@PAGE
add x0, x0, myvar@PAGEOFF
ldr w1, [x0] // myvar の値をロード
add w1, w1, #10 // 10 を加算
// スタックフレームの復帰
ldp x29, x30, [sp], #16 // FP, LR を復帰
ret // リターン
7. データ型とメモリアドレッシングモード
7.1 データ型
アセンブリ言語にはC言語のような型システムは存在しないが、データのサイズを明示的に指定する必要がある。
データサイズの対応表:
┌───────────────┬──────────┬───────────┬───────────┬──────────┐
│ サイズ │ x86 NASM │ x86 GAS │ AArch64 │ C/C++ │
├───────────────┼──────────┼───────────┼───────────┼──────────┤
│ 1 byte (8b) │ BYTE/db │ .byte/b │ B suffix │ char │
│ 2 bytes (16b) │ WORD/dw │ .word/w │ H suffix │ short │
│ 4 bytes (32b) │ DWORD/dd │ .long/l │ W reg │ int │
│ 8 bytes (64b) │ QWORD/dq │ .quad/q │ X reg │ long │
│ 16 bytes │ OWORD │ .octa │ Q reg │ __int128 │
│ 4 bytes (fp) │ dd │ .float │ S reg │ float │
│ 8 bytes (fp) │ dq │ .double │ D reg │ double │
│ 10 bytes (fp) │ dt │ ─ │ ─ │ long dbl │
└───────────────┴──────────┴───────────┴───────────┴──────────┘
7.2 x86-64 アドレッシングモード
x86-64 は非常に柔軟なアドレッシングモードを提供する。
; ============ x86-64 アドレッシングモード ============
; 1. 即値 (Immediate)
mov rax, 42 ; rax = 42
mov rax, 0xFF ; rax = 255
mov rax, 'A' ; rax = 65
; 2. レジスタ直接 (Register Direct)
mov rax, rbx ; rax = rbx
; 3. メモリ直接 (Direct/Absolute)
mov rax, [0x400000] ; rax = *(uint64_t*)0x400000
; ※ x86-64 では通常 RIP 相対を使用
; 4. RIP 相対 (RIP-Relative) — x86-64 の標準
mov rax, [rel myvar] ; rax = *(myvar) (NASM)
; GAS: movq myvar(%rip), %rax
; 5. レジスタ間接 (Register Indirect)
mov rax, [rbx] ; rax = *(uint64_t*)rbx
; 6. ベース + ディスプレースメント (Base + Displacement)
mov rax, [rbx + 8] ; rax = *(uint64_t*)(rbx + 8)
mov rax, [rbp - 16] ; ローカル変数アクセス(スタックフレーム内)
; 7. ベース + インデックス (Base + Index)
mov rax, [rbx + rcx] ; rax = *(uint64_t*)(rbx + rcx)
; 8. ベース + インデックス * スケール (Base + Index * Scale)
mov rax, [rbx + rcx*8] ; rax = *(uint64_t*)(rbx + rcx*8)
; スケール: 1, 2, 4, 8 のいずれか
; 配列アクセスに最適: array[i] で sizeof(element) が 1/2/4/8
; 9. 完全形式 (Base + Index * Scale + Displacement)
mov rax, [rbx + rcx*8 + 16]
; C 言語の struct_array[i].field に相当
; struct myStruct { long pad[2]; long value; };
; value = array[i].value → [base + i*sizeof(struct) + offsetof(value)]
; ============ AT&T 構文での等価表現 ============
; Intel: [base + index*scale + disp]
; AT&T: disp(%base, %index, scale)
# movq (%rbx), %rax # [rbx]
# movq 8(%rbx), %rax # [rbx + 8]
# movq (%rbx,%rcx), %rax # [rbx + rcx]
# movq (%rbx,%rcx,8), %rax # [rbx + rcx*8]
# movq 16(%rbx,%rcx,8), %rax # [rbx + rcx*8 + 16]
7.3 AArch64 アドレッシングモード
AArch64 は Load/Store アーキテクチャであり、メモリアクセスは専用の LDR/STR 命令でのみ行う。
// ============ AArch64 アドレッシングモード ============
// 1. 即値オフセット (Immediate Offset)
ldr x0, [x1] // x0 = *x1
ldr x0, [x1, #8] // x0 = *(x1 + 8)
ldr x0, [x1, #-8] // x0 = *(x1 - 8)
// 2. プリインデックス (Pre-Index) — ベースレジスタを先に更新
ldr x0, [x1, #8]! // x1 += 8; x0 = *x1
// C 言語: x0 = *(++ptr)
// 3. ポストインデックス (Post-Index) — アクセス後にベースレジスタを更新
ldr x0, [x1], #8 // x0 = *x1; x1 += 8
// C 言語: x0 = *(ptr++)
// 4. レジスタオフセット (Register Offset)
ldr x0, [x1, x2] // x0 = *(x1 + x2)
// 5. シフト付きレジスタオフセット
ldr x0, [x1, x2, lsl #3] // x0 = *(x1 + x2*8)
// 配列アクセス: uint64_t array[]; val = array[i]
// 6. 拡張付きレジスタオフセット
ldr x0, [x1, w2, sxtw #3] // w2 を符号拡張して x1 + (sext(w2) * 8)
// 符号付きインデックスでの配列アクセス
// 7. PC 相対アドレッシング
adrp x0, myvar@PAGE // myvar のページアドレス(4KB境界)
add x0, x0, myvar@PAGEOFF // ページ内オフセットを加算
ldr x1, [x0] // myvar の値をロード
// 8. リテラルロード (PC 相対)
ldr x0, =0x123456789ABCDEF // リテラルプールからの即値ロード
// 9. ペアロード/ストア
ldp x0, x1, [x2] // x0 = *x2, x1 = *(x2+8)
stp x0, x1, [sp, #-16]! // sp -= 16; *sp = x0; *(sp+8) = x1
7.4 RISC-V アドレッシングモード
RISC-V は最もシンプルなアドレッシングモードを持つ。
# ============ RISC-V アドレッシングモード ============
# RISC-V は基本的に「ベース + 12ビット符号付きオフセット」のみ
# 1. ベース + オフセット
ld a0, 0(sp) # a0 = *(sp + 0)
ld a0, 8(sp) # a0 = *(sp + 8)
ld a0, -8(s0) # a0 = *(s0 - 8)
# 2. グローバル変数アクセス (2命令必要)
lui a1, %hi(myvar) # 上位20ビットをロード
ld a0, %lo(myvar)(a1) # 下位12ビットオフセットでロード
# または疑似命令を使用
la a1, myvar # myvar のアドレスをロード (疑似命令)
ld a0, 0(a1) # 値をロード
# 3. PC 相対 (auipc + addi)
auipc a0, %pcrel_hi(myvar) # PC + 上位20ビット
addi a0, a0, %pcrel_lo(myvar) # + 下位12ビット
# RISC-V は複雑なアドレッシングモードを持たないため、
# 配列アクセスにはインデックス計算を明示的に行う必要がある
# array[i] のアクセス:
slli t0, a1, 3 # t0 = i * 8 (int64_t の場合)
add t0, a0, t0 # t0 = base + i * 8
ld a2, 0(t0) # a2 = array[i]
7.5 LEA 命令 (x86-64) の活用
LEA (Load Effective Address) は x86-64 で特に強力な命令で、アドレス計算の結果をレジスタに格納する(メモリアクセスは行わない)。
; LEA の一般的な使用法
; 1. アドレス計算
lea rax, [rbx + rcx*8 + 16] ; rax = rbx + rcx*8 + 16 (メモリアクセスなし)
; 2. 高速な算術演算として使用
lea rax, [rbx + rbx*2] ; rax = rbx * 3
lea rax, [rbx + rbx*4] ; rax = rbx * 5
lea rax, [rbx*8 + rbx] ; rax = rbx * 9
lea rax, [rbx + rbx*2 + 1] ; rax = rbx * 3 + 1
; 3. 複数の演算を1命令で実行
; C: result = base + index * 4 + offset
lea rax, [rdi + rsi*4 + 100]
; 4. RIP 相対アドレスの取得
lea rax, [rip + label] ; label のアドレスを取得
8. 算術・論理演算命令
8.1 x86-64 の算術演算
; ============ 加算・減算 ============
add rax, rbx ; rax += rbx (フラグ更新)
add rax, 42 ; rax += 42
adc rax, rbx ; rax += rbx + CF (キャリー付き加算 → 多倍長演算)
sub rax, rbx ; rax -= rbx
sbb rax, rbx ; rax -= rbx - CF (ボロー付き減算)
inc rax ; rax++ (CF は変更しない)
dec rax ; rax-- (CF は変更しない)
neg rax ; rax = -rax (2の補数)
; ============ 乗算 ============
imul rax, rbx ; rax *= rbx (符号付き、下位64bit)
imul rax, rbx, 10 ; rax = rbx * 10 (3オペランド形式)
mul rbx ; RDX:RAX = RAX * RBX (符号なし、128bit結果)
imul rbx ; RDX:RAX = RAX * RBX (符号付き、128bit結果)
; ============ 除算 ============
; 除算は RDX:RAX を被除数として使用
xor rdx, rdx ; 符号なし除算の前に上位をゼロクリア
div rbx ; RAX = RDX:RAX / RBX, RDX = RDX:RAX % RBX (符号なし)
cqo ; RAX を RDX:RAX に符号拡張
idiv rbx ; RAX = 商, RDX = 余り (符号付き)
; ============ 128ビット加算の例 ============
; [r9:r8] = [rdx:rax] + [rcx:rbx]
add rax, rbx ; 下位64ビットを加算
adc rdx, rcx ; 上位64ビットをキャリー付き加算
mov r8, rax
mov r9, rdx
8.2 x86-64 の論理演算とビット操作
; ============ 論理演算 ============
and rax, rbx ; rax &= rbx
or rax, rbx ; rax |= rbx
xor rax, rbx ; rax ^= rbx
not rax ; rax = ~rax (ビット反転)
test rax, rbx ; rax & rbx (結果を捨て、フラグのみ更新)
; ============ シフト演算 ============
shl rax, 4 ; rax <<= 4 (論理左シフト = 2^4 倍)
shr rax, 4 ; rax >>= 4 (論理右シフト、符号なし)
sar rax, 4 ; rax >>= 4 (算術右シフト、符号保持)
sal rax, 4 ; shl と同じ
; CL レジスタによるシフト
mov cl, 5
shl rax, cl ; rax <<= cl
; ============ ローテーション ============
rol rax, 4 ; 左ローテーション
ror rax, 4 ; 右ローテーション
rcl rax, 1 ; CF を含む左ローテーション
rcr rax, 1 ; CF を含む右ローテーション
; ============ ビット走査 ============
bsf rax, rbx ; Bit Scan Forward: 最下位の1ビットの位置
bsr rax, rbx ; Bit Scan Reverse: 最上位の1ビットの位置
popcnt rax, rbx ; ポピュレーションカウント(1ビットの数)
lzcnt rax, rbx ; リーディングゼロカウント
tzcnt rax, rbx ; トレーリングゼロカウント
; ============ BMI (Bit Manipulation Instructions) ============
; BMI1
andn rax, rbx, rcx ; rax = ~rbx & rcx
bextr rax, rbx, rcx ; ビットフィールド抽出
blsi rax, rbx ; 最下位セットビットを分離: rax = rbx & (-rbx)
blsmsk rax, rbx ; 最下位セットビットまでのマスク
blsr rax, rbx ; 最下位セットビットをリセット
; BMI2
pdep rax, rbx, rcx ; パラレルビットデポジット
pext rax, rbx, rcx ; パラレルビットエクストラクト
bzhi rax, rbx, rcx ; 指定ビット以上をゼロクリア
8.3 x86-64 の実用的なイディオム
; ============ よく使われるイディオム ============
; レジスタのゼロクリア(最も効率的な方法)
xor eax, eax ; RAX = 0 (2バイト、依存関係破壊)
; mov rax, 0 は 7バイト → xor のほうが短くて速い
; 値が0かどうかの判定
test rax, rax ; ZF = (rax == 0)
jz is_zero
; 値の符号判定
test rax, rax
js is_negative ; SF = 1 なら負
; 偶数/奇数の判定
test rax, 1
jz is_even ; 最下位ビットが0なら偶数
; 2のべき乗かどうかの判定
; n が 2のべき乗 ⟺ n > 0 && (n & (n-1)) == 0
mov rbx, rax
dec rbx
test rax, rbx
jz is_power_of_two
; 絶対値
mov rbx, rax
sar rbx, 63 ; rbx = (rax < 0) ? -1 : 0
xor rax, rbx
sub rax, rbx ; rax = abs(rax)
; min(rax, rbx) - 分岐なし
cmp rax, rbx
cmovg rax, rbx ; rax > rbx なら rax = rbx
; max(rax, rbx) - 分岐なし
cmp rax, rbx
cmovl rax, rbx ; rax < rbx なら rax = rbx
; swap(rax, rbx) - 一時変数なし
xchg rax, rbx ; ただしメモリオペランドだと暗黙の LOCK
; 代替: 3つの xor
; xor rax, rbx
; xor rbx, rax
; xor rax, rbx
8.4 AArch64 の算術・論理演算
// ============ AArch64 算術演算 ============
add x0, x1, x2 // x0 = x1 + x2
adds x0, x1, x2 // x0 = x1 + x2 (フラグ更新)
adc x0, x1, x2 // x0 = x1 + x2 + C
sub x0, x1, x2 // x0 = x1 - x2
subs x0, x1, x2 // x0 = x1 - x2 (フラグ更新)
neg x0, x1 // x0 = -x1
// シフト付き演算 (ARM の特徴)
add x0, x1, x2, lsl #3 // x0 = x1 + (x2 << 3) = x1 + x2*8
sub x0, x1, x2, asr #2 // x0 = x1 - (x2 >> 2) (算術)
// 乗算と積和演算
mul x0, x1, x2 // x0 = x1 * x2
madd x0, x1, x2, x3 // x0 = x1 * x2 + x3
msub x0, x1, x2, x3 // x0 = x3 - x1 * x2
smulh x0, x1, x2 // x0 = (x1 * x2) >> 64 (符号付き上位)
umulh x0, x1, x2 // x0 = (x1 * x2) >> 64 (符号なし上位)
smaddl x0, w1, w2, x3 // x0 = (int64_t)w1 * w2 + x3
// 除算
sdiv x0, x1, x2 // x0 = x1 / x2 (符号付き)
udiv x0, x1, x2 // x0 = x1 / x2 (符号なし)
// 余りは直接の命令がない → msub を使用
// remainder = dividend - (quotient * divisor)
sdiv x0, x1, x2
msub x3, x0, x2, x1 // x3 = x1 - x0 * x2 = x1 % x2
// ============ AArch64 論理演算 ============
and x0, x1, x2
orr x0, x1, x2
eor x0, x1, x2
bic x0, x1, x2 // x0 = x1 & ~x2 (bit clear)
orn x0, x1, x2 // x0 = x1 | ~x2
eon x0, x1, x2 // x0 = x1 ^ ~x2
// ビットフィールド操作
ubfx x0, x1, #4, #8 // 符号なしビットフィールド抽出 (bit4から8ビット)
sbfx x0, x1, #4, #8 // 符号付きビットフィールド抽出
bfi x0, x1, #4, #8 // ビットフィールド挿入
bfxil x0, x1, #4, #8 // 低位ビットフィールド挿入
// ビットカウント
cls x0, x1 // Count Leading Signs
clz x0, x1 // Count Leading Zeros
rbit x0, x1 // ビットリバース
rev x0, x1 // バイトリバース (エンディアン変換)
8.5 RISC-V の算術・論理演算
# ============ RISC-V 算術演算 ============
add a0, a1, a2 # a0 = a1 + a2
addi a0, a1, 100 # a0 = a1 + 100 (即値は12ビット符号付き)
sub a0, a1, a2 # a0 = a1 - a2
# subi は存在しない → addi a0, a1, -100
# M 拡張 (乗除算)
mul a0, a1, a2 # a0 = (a1 * a2)[63:0] (下位64ビット)
mulh a0, a1, a2 # a0 = (a1 * a2)[127:64] (符号付き上位)
mulhu a0, a1, a2 # a0 = (a1 * a2)[127:64] (符号なし上位)
div a0, a1, a2 # a0 = a1 / a2 (符号付き)
divu a0, a1, a2 # a0 = a1 / a2 (符号なし)
rem a0, a1, a2 # a0 = a1 % a2 (符号付き)
remu a0, a1, a2 # a0 = a1 % a2 (符号なし)
# ============ RISC-V 論理演算 ============
and a0, a1, a2
andi a0, a1, 0xFF
or a0, a1, a2
ori a0, a1, 0xFF
xor a0, a1, a2
xori a0, a1, 0xFF
# シフト
sll a0, a1, a2 # 論理左シフト
slli a0, a1, 4 # 即値論理左シフト
srl a0, a1, a2 # 論理右シフト
srli a0, a1, 4
sra a0, a1, a2 # 算術右シフト
srai a0, a1, 4
# 比較 (Set Less Than)
slt a0, a1, a2 # a0 = (a1 < a2) ? 1 : 0 (符号付き)
sltu a0, a1, a2 # a0 = (a1 < a2) ? 1 : 0 (符号なし)
slti a0, a1, 100 # 即値比較
# RISC-V にはフラグレジスタがない
# 条件はすべて比較命令 (slt) または分岐命令 (beq/bne/blt/bge) で処理
9. 制御フロー
9.1 x86-64 の分岐命令
; ============ 無条件ジャンプ ============
jmp label ; 直接ジャンプ
jmp rax ; レジスタ間接ジャンプ
jmp [rax] ; メモリ間接ジャンプ
jmp [rip + jumptable + rax*8] ; ジャンプテーブル
; ============ 条件ジャンプ ============
; CMP/TEST の後に使用
cmp rax, rbx
; 符号なし比較
ja label ; Above (CF=0 かつ ZF=0)
jae label ; Above or Equal (CF=0)
jb label ; Below (CF=1)
jbe label ; Below or Equal (CF=1 または ZF=1)
; 符号付き比較
jg label ; Greater (ZF=0 かつ SF=OF)
jge label ; Greater or Equal (SF=OF)
jl label ; Less (SF≠OF)
jle label ; Less or Equal (ZF=1 または SF≠OF)
; 等価
je label ; Equal (ZF=1) (jz と同義)
jne label ; Not Equal (ZF=0) (jnz と同義)
; フラグ個別チェック
jz label ; Zero (ZF=1)
jnz label ; Not Zero (ZF=0)
js label ; Sign (SF=1、結果が負)
jns label ; Not Sign (SF=0)
jo label ; Overflow (OF=1)
jno label ; Not Overflow (OF=0)
jc label ; Carry (CF=1)
jnc label ; Not Carry (CF=0)
9.2 if-else の実装
; ============ C: if (x > 0) { a = 1; } else { a = -1; } ============
; 方法1: 分岐を使用
cmp rdi, 0
jle .else
mov eax, 1
jmp .endif
.else:
mov eax, -1
.endif:
; 方法2: CMOV を使用(分岐なし — パイプライン効率向上)
mov eax, 1
mov ecx, -1
cmp rdi, 0
cmovle eax, ecx ; rdi <= 0 なら eax = ecx
; 方法3: SETcc を使用
xor eax, eax
cmp rdi, 0
setg al ; rdi > 0 なら al = 1, そうでなければ al = 0
lea eax, [rax*2 - 1] ; 0 → -1, 1 → 1
9.3 ループの実装
; ============ for ループ: for (int i = 0; i < n; i++) sum += array[i] ============
; rdi = array pointer, rsi = n
; 戻り値: rax = sum
sum_array:
xor eax, eax ; sum = 0
xor ecx, ecx ; i = 0
test rsi, rsi
jle .done ; n <= 0 ならスキップ
.loop:
add rax, [rdi + rcx*8] ; sum += array[i]
inc rcx ; i++
cmp rcx, rsi ; i < n ?
jl .loop
.done:
ret
; ============ while ループ: while (*str != '\0') count++ ============
; rdi = str pointer
; 戻り値: rax = count (strlen の実装)
my_strlen:
xor eax, eax ; count = 0
test rdi, rdi
jz .done
.loop:
cmp byte [rdi + rax], 0
je .done
inc rax
jmp .loop
.done:
ret
; ============ do-while ループ ============
; 最低1回は実行する
mov ecx, 10 ; counter = 10
.do_loop:
; ループ本体
dec ecx
jnz .do_loop ; counter != 0 なら繰り返し
; ============ LOOP 命令 (レガシー、非推奨) ============
mov ecx, 100 ; ループカウンタ
.legacy_loop:
; ループ本体
loop .legacy_loop ; ecx--; ecx != 0 なら分岐
; ※ loop 命令は遅い。dec + jnz のほうが高速
9.4 switch-case の実装(ジャンプテーブル)
; ============ switch (value) { case 0: ... case 1: ... case 2: ... } ============
; rdi = value
switch_example:
cmp rdi, 2
ja .default ; value > 2 なら default
; ジャンプテーブルを使用
lea rax, [rel .jumptable]
movsxd rcx, dword [rax + rdi*4] ; テーブルから相対オフセット取得
add rax, rcx
jmp rax
.jumptable:
dd .case0 - .jumptable
dd .case1 - .jumptable
dd .case2 - .jumptable
.case0:
mov eax, 100
ret
.case1:
mov eax, 200
ret
.case2:
mov eax, 300
ret
.default:
mov eax, -1
ret
9.5 AArch64 の制御フロー
// ============ AArch64 分岐命令 ============
// 無条件分岐
b label // 直接分岐 (±128MB 範囲)
br x0 // レジスタ間接分岐
bl function // 分岐とリンク (x30 に戻りアドレス保存)
blr x0 // レジスタ間接分岐とリンク
ret // x30 へリターン
// 条件分岐
cmp x0, x1
b.eq label // Equal
b.ne label // Not Equal
b.lt label // Less Than (符号付き)
b.le label // Less or Equal
b.gt label // Greater Than
b.ge label // Greater or Equal
b.lo label // Lower (符号なし)
b.hi label // Higher (符号なし)
b.mi label // Minus (負)
b.pl label // Plus (正または0)
b.vs label // Overflow
b.vc label // No Overflow
// ============ Compare and Branch (ゼロ比較の分岐) ============
cbz x0, label // x0 == 0 なら分岐
cbnz x0, label // x0 != 0 なら分岐
// ============ Test and Branch (ビットテスト分岐) ============
tbz x0, #31, label // x0 のビット31が0なら分岐 (正の値)
tbnz x0, #0, label // x0 のビット0が1なら分岐 (奇数)
// ============ for ループの例 ============
// for (int i = 0; i < n; i++) sum += array[i]
// x0 = array, x1 = n
sum_array_arm:
mov x2, #0 // sum = 0
mov x3, #0 // i = 0
cmp x1, #0
b.le .Ldone
.Lloop:
ldr x4, [x0, x3, lsl #3] // x4 = array[i]
add x2, x2, x4 // sum += array[i]
add x3, x3, #1 // i++
cmp x3, x1 // i < n ?
b.lt .Lloop
.Ldone:
mov x0, x2 // return sum
ret
9.6 RISC-V の制御フロー
# ============ RISC-V 分岐命令 ============
# RISC-V は比較と分岐が一体化した命令を持つ(フラグレジスタなし)
# 条件分岐 (B-Type)
beq a0, a1, label # a0 == a1 なら分岐
bne a0, a1, label # a0 != a1 なら分岐
blt a0, a1, label # a0 < a1 (符号付き) なら分岐
bge a0, a1, label # a0 >= a1 (符号付き) なら分岐
bltu a0, a1, label # a0 < a1 (符号なし) なら分岐
bgeu a0, a1, label # a0 >= a1 (符号なし) なら分岐
# ゼロ比較 (疑似命令)
beqz a0, label # beq a0, zero, label
bnez a0, label # bne a0, zero, label
# 無条件ジャンプ
jal ra, function # function 呼び出し (ra に戻りアドレス)
jalr ra, 0(a0) # レジスタ間接呼び出し
j label # 疑似命令: jal zero, label
ret # 疑似命令: jalr zero, 0(ra)
# ============ RISC-V for ループの例 ============
# for (int i = 0; i < n; i++) sum += array[i]
# a0 = array, a1 = n
sum_array_rv:
li a2, 0 # sum = 0
li a3, 0 # i = 0
ble a1, zero, .Ldone # n <= 0 ならスキップ
.Lloop:
slli t0, a3, 3 # t0 = i * 8
add t0, a0, t0 # t0 = &array[i]
ld t1, 0(t0) # t1 = array[i]
add a2, a2, t1 # sum += array[i]
addi a3, a3, 1 # i++
blt a3, a1, .Lloop # i < n なら繰り返し
.Ldone:
mv a0, a2 # return sum
ret
10. スタック操作と呼び出し規約
10.1 スタックの基本
スタックは後入れ先出し(LIFO)のデータ構造であり、関数呼び出し、ローカル変数の格納、レジスタの退避に使用される。x86-64 および AArch64 では、スタックは高アドレスから低アドレスに向かって成長する。
高アドレス
┌──────────────────┐
│ 前のフレーム │
├──────────────────┤ ← 関数呼び出し前の RSP/SP
│ 戻りアドレス │ (x86: CALL で自動 push)
├──────────────────┤
│ 保存された RBP │ ← RBP (フレームポインタ)
├──────────────────┤
│ ローカル変数1 │
│ ローカル変数2 │
│ ... │
├──────────────────┤ ← RSP/SP (スタックポインタ)
│ (未使用領域) │
└──────────────────┘
低アドレス
10.2 x86-64 System V ABI (Linux/macOS/FreeBSD)
System V AMD64 ABI は、Linux、macOS、FreeBSD などの Unix 系 OS で使用される呼び出し規約である。
整数/ポインタ引数: RDI, RSI, RDX, RCX, R8, R9 (6個まで)
浮動小数点引数: XMM0-XMM7 (8個まで)
戻り値: RAX (整数), RAX:RDX (128ビット), XMM0 (浮動小数点)
スタックアラインメント: 16バイト (CALL 命令実行時)
Caller-saved (揮発性): RAX, RCX, RDX, RSI, RDI, R8-R11, XMM0-XMM15
Callee-saved (不揮発性): RBX, RBP, R12-R15
スタックポインタ: RSP (callee-saved)
; ============ System V ABI の関数呼び出し例 ============
; C: long result = my_function(1, 2, 3, 4, 5, 6, 7, 8);
; 第7, 8引数はスタック経由 (逆順で push)
push 8 ; 第8引数
push 7 ; 第7引数
mov r9d, 6 ; 第6引数
mov r8d, 5 ; 第5引数
mov ecx, 4 ; 第4引数
mov edx, 3 ; 第3引数
mov esi, 2 ; 第2引数
mov edi, 1 ; 第1引数
call my_function
add rsp, 16 ; スタック引数のクリーンアップ (8*2)
; 戻り値は RAX に入っている
; ============ 関数のプロローグとエピローグ ============
my_function:
; プロローグ
push rbp ; 旧フレームポインタを保存
mov rbp, rsp ; 新しいフレームポインタを設定
sub rsp, 32 ; ローカル変数用に 32 バイト確保
push rbx ; callee-saved レジスタの保存
push r12
; ローカル変数へのアクセス
mov dword [rbp - 4], edi ; 第1引数をローカル変数に保存
mov dword [rbp - 8], esi ; 第2引数をローカル変数に保存
; 関数本体
; ...
; エピローグ
pop r12 ; callee-saved レジスタの復帰
pop rbx
leave ; mov rsp, rbp; pop rbp
ret ; pop rip (戻りアドレスへジャンプ)
10.3 Windows x64 呼び出し規約
整数/ポインタ引数: RCX, RDX, R8, R9 (4個まで)
浮動小数点引数: XMM0-XMM3 (4個まで)
戻り値: RAX
スタック: 32バイトのシャドウスペース必須
スタックアラインメント: 16バイト
Caller-saved: RAX, RCX, RDX, R8-R11, XMM0-XMM5
Callee-saved: RBX, RBP, RDI, RSI, R12-R15, XMM6-XMM15
; ============ Windows x64 の関数呼び出し例 ============
; C: long result = my_function(1, 2, 3, 4, 5);
sub rsp, 48 ; シャドウスペース(32) + 第5引数(8) + アラインメント
mov dword [rsp + 32], 5 ; 第5引数 (スタック)
mov r9d, 4 ; 第4引数
mov r8d, 3 ; 第3引数
mov edx, 2 ; 第2引数
mov ecx, 1 ; 第1引数
call my_function
add rsp, 48
10.4 AArch64 AAPCS64 呼び出し規約
整数/ポインタ引数: X0-X7 (8個まで)
浮動小数点引数: D0-D7 / V0-V7 (8個まで)
戻り値: X0 (整数), X0:X1 (128ビット), D0 (浮動小数点)
フレームポインタ: X29 (FP)
リンクレジスタ: X30 (LR)
スタックアラインメント: 16バイト
Caller-saved: X0-X18, D0-D7, D16-D31
Callee-saved: X19-X28, X29(FP), X30(LR), D8-D15
// ============ AArch64 関数の例 ============
// int64_t add_numbers(int64_t a, int64_t b, int64_t c)
// a=x0, b=x1, c=x2
.global _add_numbers
.align 4
_add_numbers:
// プロローグ
stp x29, x30, [sp, #-16]! // FP, LR を保存 (sp -= 16)
mov x29, sp // フレームポインタ設定
// 関数本体
add x0, x0, x1 // a + b
add x0, x0, x2 // + c
// 戻り値は x0
// エピローグ
ldp x29, x30, [sp], #16 // FP, LR を復帰 (sp += 16)
ret
// ============ callee-saved レジスタの保存例 ============
complex_function:
// callee-saved レジスタの保存 (ペアで保存)
stp x29, x30, [sp, #-64]!
mov x29, sp
stp x19, x20, [sp, #16]
stp x21, x22, [sp, #32]
stp x23, x24, [sp, #48]
// x19-x24 を自由に使用可能
mov x19, x0
mov x20, x1
// ...
// 復帰
ldp x23, x24, [sp, #48]
ldp x21, x22, [sp, #32]
ldp x19, x20, [sp, #16]
ldp x29, x30, [sp], #64
ret
10.5 RISC-V 呼び出し規約
整数/ポインタ引数: a0-a7 (8個まで)
浮動小数点引数: fa0-fa7 (8個まで)
戻り値: a0 (整数), a0:a1 (128ビット), fa0 (浮動小数点)
リターンアドレス: ra (x1)
スタックポインタ: sp (x2)
フレームポインタ: s0/fp (x8)
Caller-saved: ra, t0-t6, a0-a7, ft0-ft11, fa0-fa7
Callee-saved: sp, s0-s11, fs0-fs11
# ============ RISC-V 関数の例 ============
# int64_t factorial(int64_t n) -- 再帰版
.global factorial
factorial:
# プロローグ
addi sp, sp, -16 # スタックフレーム確保
sd ra, 8(sp) # リターンアドレス保存
sd s0, 0(sp) # s0 (callee-saved) 保存
# ベースケース: n <= 1
li t0, 1
ble a0, t0, .Lbase
# 再帰ケース
mv s0, a0 # n を保存 (callee-saved)
addi a0, a0, -1 # a0 = n - 1
call factorial # factorial(n - 1)
mul a0, s0, a0 # a0 = n * factorial(n - 1)
j .Lepilogue
.Lbase:
li a0, 1 # return 1
.Lepilogue:
# エピローグ
ld ra, 8(sp)
ld s0, 0(sp)
addi sp, sp, 16
ret
10.6 Red Zone
System V AMD64 ABI では、RSP より下の 128 バイトが「Red Zone」として予約されている。リーフ関数(他の関数を呼び出さない関数)は、スタックポインタを調整せずにこの領域を使用できる。
; Red Zone を利用するリーフ関数(スタック調整不要)
leaf_function:
; sub rsp, ... は不要
mov [rsp - 8], rdi ; Red Zone 内のローカル変数
mov [rsp - 16], rsi
; ...
ret
; ※ 割り込みハンドラや シグナルハンドラでは Red Zone は使用できない
; ※ Windows x64 ABI には Red Zone は存在しない
; ※ Linux カーネルコードでは -mno-red-zone を指定する
11. システムコールと OS インターフェース
11.1 システムコールの仕組み
システムコール(syscall)は、ユーザー空間のプログラムがカーネルの機能を呼び出すためのインターフェースである。
ユーザー空間 カーネル空間
┌──────────────┐ ┌──────────────┐
│ アプリケーション │ syscall │ カーネル │
│ │ ──────────→ │ │
│ write(fd, │ │ sys_write() │
│ buf, len) │ ←────────── │ │
│ │ return │ │
└──────────────┘ └──────────────┘
Ring 3 Ring 0
(特権レベル3) (特権レベル0)
11.2 Linux x86-64 システムコール
; Linux x86-64 のシステムコール規約:
; RAX = システムコール番号
; 引数: RDI, RSI, RDX, R10, R8, R9 (最大6個)
; 戻り値: RAX (-errno は負の値で返る)
; 破壊されるレジスタ: RCX, R11 (syscall 命令が使用)
; 主要なシステムコール番号 (Linux x86-64):
; 0: read 1: write 2: open 3: close
; 9: mmap 11: munmap 12: brk 57: fork
; 59: execve 60: exit 62: kill
; 231: exit_group
; ============ write システムコール ============
section .data
msg db "Hello, Linux syscall!", 10
msg_len equ $ - msg
section .text
global _start
_start:
; write(STDOUT_FILENO, msg, msg_len)
mov rax, 1 ; syscall: write
mov rdi, 1 ; fd: stdout
lea rsi, [rel msg] ; buffer
mov rdx, msg_len ; count
syscall ; カーネル呼び出し
; 戻り値: rax = 書き込みバイト数 (エラー時は負の errno)
; exit(0)
mov rax, 60 ; syscall: exit
xor rdi, rdi ; status: 0
syscall
; ============ ファイル操作の例 ============
; int fd = open("test.txt", O_RDWR | O_CREAT, 0644);
open_file:
mov rax, 2 ; syscall: open
lea rdi, [rel filename]
mov rsi, 0o102 ; O_RDWR(2) | O_CREAT(0100)
mov rdx, 0o644 ; パーミッション
syscall
; rax = ファイルディスクリプタ (エラー時は負の値)
ret
; ============ mmap の例 ============
; void *ptr = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
; MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
mmap_example:
mov rax, 9 ; syscall: mmap
xor rdi, rdi ; addr: NULL (カーネルに選択させる)
mov rsi, 4096 ; length: 4096 bytes
mov rdx, 3 ; prot: PROT_READ(1) | PROT_WRITE(2)
mov r10, 0x22 ; flags: MAP_PRIVATE(2) | MAP_ANONYMOUS(0x20)
mov r8, -1 ; fd: -1 (anonymous)
xor r9, r9 ; offset: 0
syscall
; rax = マップされたアドレス
ret
11.3 macOS (Darwin) x86-64 システムコール
; macOS のシステムコール番号は Unix 番号 + 0x2000000
; (Mach trap は 0x1000000 ベース)
; それ以外は Linux と同じレジスタ規約を使用
; macOS x86-64 のシステムコール
SYS_EXIT equ 0x2000001
SYS_FORK equ 0x2000002
SYS_READ equ 0x2000003
SYS_WRITE equ 0x2000004
SYS_OPEN equ 0x2000005
SYS_CLOSE equ 0x2000006
section .data
msg db "Hello, macOS!", 10
msg_len equ $ - msg
section .text
global _main
_main:
; write(1, msg, msg_len)
mov rax, SYS_WRITE
mov rdi, 1
lea rsi, [rel msg]
mov rdx, msg_len
syscall
; exit(0)
mov rax, SYS_EXIT
xor rdi, rdi
syscall
11.4 macOS AArch64 システムコール
// macOS AArch64 のシステムコール規約:
// X16 = システムコール番号 (Unix: 番号そのまま + SVC #0x80)
// 引数: X0-X5 (最大6個)
// 戻り値: X0
// エラー: Carry Flag がセットされる
.global _main
.align 4
.equ SYS_EXIT, 1
.equ SYS_WRITE, 4
_main:
// write(1, message, 14)
mov x0, #1 // fd = stdout
adrp x1, message@PAGE
add x1, x1, message@PAGEOFF // buffer address
mov x2, #14 // count
mov x16, #SYS_WRITE
svc #0x80 // supervisor call
// exit(0)
mov x0, #0
mov x16, #SYS_EXIT
svc #0x80
.data
message:
.ascii "Hello, macOS!\n"
11.5 Linux AArch64 システムコール
// Linux AArch64 のシステムコール規約:
// X8 = システムコール番号
// 引数: X0-X5
// 戻り値: X0
// 命令: SVC #0
.global _start
.align 4
_start:
// write(1, message, 14)
mov x0, #1 // fd = stdout
ldr x1, =message // buffer
mov x2, #14 // count
mov x8, #64 // syscall: write (Linux AArch64 = 64)
svc #0
// exit(0)
mov x0, #0
mov x8, #93 // syscall: exit (Linux AArch64 = 93)
svc #0
.data
message:
.ascii "Hello, Linux!\n"
11.6 C ライブラリ関数との連携
実際のプログラムでは、直接システムコールを発行するよりも、C ライブラリ(libc)の関数を呼び出すほうが一般的である。
; Linux x86-64: C ライブラリ関数を使用
section .data
fmt db "Result: %ld", 10, 0 ; printf のフォーマット文字列
section .text
global main
extern printf
extern malloc
extern free
extern strlen
main:
push rbp
mov rbp, rsp
; printf("Result: %ld\n", 42)
lea rdi, [rel fmt] ; フォーマット文字列
mov rsi, 42 ; 引数
xor eax, eax ; 浮動小数点引数の数 = 0
call printf wrt ..plt ; PLT 経由で呼び出し
; char *buf = malloc(1024)
mov edi, 1024
call malloc wrt ..plt
; rax = ポインタ
; free(buf)
mov rdi, rax
call free wrt ..plt
xor eax, eax ; return 0
leave
ret
12. SIMD 命令
12.1 SIMD の概要
SIMD(Single Instruction, Multiple Data)は、1 つの命令で複数のデータ要素に対して同じ演算を並列に実行する技術である。画像処理、音声処理、科学計算、機械学習などの並列性が高い処理で大幅な高速化が可能となる。
スカラー演算: SIMD 演算 (128bit SSE):
A1 + B1 = C1 [A1, A2, A3, A4]
A2 + B2 = C2 + [B1, B2, B3, B4]
A3 + B3 = C3 = [C1, C2, C3, C4]
A4 + B4 = C4 → 1命令で4つの演算を実行
(4命令必要)
12.2 x86-64 SSE/AVX
拡張セットの進化:
MMX (1997): 64-bit, MM0-MM7, 整数のみ
SSE (1999): 128-bit, XMM0-XMM7, 単精度浮動小数点
SSE2 (2001): 128-bit, XMM0-XMM15, 倍精度浮動小数点 + 整数
SSE3 (2004): 水平演算の追加
SSE4.1 (2006): blend, round, insert/extract
SSE4.2 (2008): 文字列処理、CRC32、POPCNT
AVX (2011): 256-bit, YMM0-YMM15
AVX2 (2013): 256-bit 整数演算
AVX-512(2016): 512-bit, ZMM0-ZMM31, マスクレジスタ
; ============ SSE: 4つの float の加算 ============
section .data
align 16
vec_a: dd 1.0, 2.0, 3.0, 4.0 ; 4 x float
vec_b: dd 5.0, 6.0, 7.0, 8.0
section .bss
align 16
vec_c: resd 4
section .text
global sse_add_example
sse_add_example:
movaps xmm0, [rel vec_a] ; xmm0 = [1.0, 2.0, 3.0, 4.0]
movaps xmm1, [rel vec_b] ; xmm1 = [5.0, 6.0, 7.0, 8.0]
addps xmm0, xmm1 ; xmm0 = [6.0, 8.0, 10.0, 12.0]
movaps [rel vec_c], xmm0 ; 結果を保存
ret
; ps = Packed Single-precision (4 x float)
; pd = Packed Double-precision (2 x double)
; ss = Scalar Single-precision (1 x float)
; sd = Scalar Double-precision (1 x double)
; ============ AVX: 8つの float の乗算 ============
avx_mul_example:
vmovaps ymm0, [rdi] ; ymm0 = 8 floats from array a
vmovaps ymm1, [rsi] ; ymm1 = 8 floats from array b
vmulps ymm2, ymm0, ymm1 ; ymm2 = ymm0 * ymm1 (要素ごと)
vmovaps [rdx], ymm2 ; 結果を格納
vzeroupper ; AVX→SSE 遷移ペナルティ回避
ret
; ============ AVX2: 整数ベクトル演算 ============
; 32バイト(8 x int32)の加算
avx2_int_add:
vmovdqa ymm0, [rdi] ; 8 x int32 をロード
vmovdqa ymm1, [rsi]
vpaddd ymm2, ymm0, ymm1 ; 要素ごとに32ビット整数加算
vmovdqa [rdx], ymm2
vzeroupper
ret
12.3 SSE を使った実用例: ドット積
; float dot_product(const float *a, const float *b, int n)
; rdi = a, rsi = b, edx = n
global dot_product
dot_product:
xorps xmm0, xmm0 ; sum = 0.0 (4 x float)
mov ecx, edx
shr ecx, 2 ; n / 4 (4要素ずつ処理)
jz .remainder
.loop4:
movups xmm1, [rdi] ; a[i..i+3] をロード (アラインメント不要)
movups xmm2, [rsi] ; b[i..i+3] をロード
mulps xmm1, xmm2 ; 要素ごとの乗算
addps xmm0, xmm1 ; 部分和に加算
add rdi, 16
add rsi, 16
dec ecx
jnz .loop4
; 水平加算: xmm0 = [s0, s1, s2, s3]
haddps xmm0, xmm0 ; xmm0 = [s0+s1, s2+s3, s0+s1, s2+s3]
haddps xmm0, xmm0 ; xmm0 = [s0+s1+s2+s3, ...]
.remainder:
; 残り要素の処理 (n % 4)
and edx, 3
jz .done
.loop1:
movss xmm1, [rdi]
mulss xmm1, [rsi]
addss xmm0, xmm1
add rdi, 4
add rsi, 4
dec edx
jnz .loop1
.done:
ret ; 戻り値は xmm0 (float)
12.4 ARM NEON
ARM NEON は AArch64 の標準 SIMD 拡張であり、128 ビットベクトルレジスタ(V0-V31)を使用する。
// ============ NEON の基本 ============
// V0-V31: 128ビットベクトルレジスタ
// アクセスパターン:
// Vn.16B - 16 x byte
// Vn.8H - 8 x half-word (16-bit)
// Vn.4S - 4 x single-word (32-bit)
// Vn.2D - 2 x double-word (64-bit)
// Bn, Hn, Sn, Dn - スカラーアクセス
// ============ 4つの float の加算 ============
neon_add_example:
ld1 {v0.4s}, [x0] // v0 = 4 floats from array a
ld1 {v1.4s}, [x1] // v1 = 4 floats from array b
fadd v2.4s, v0.4s, v1.4s // v2 = v0 + v1 (要素ごと)
st1 {v2.4s}, [x2] // 結果を格納
ret
// ============ NEON ドット積 ============
// float neon_dot_product(const float *a, const float *b, int n)
// x0 = a, x1 = b, w2 = n
neon_dot_product:
movi v0.4s, #0 // sum = 0 (4 x float)
lsr w3, w2, #2 // n / 4
cbz w3, .Lremainder
.Lloop4:
ld1 {v1.4s}, [x0], #16 // a[i..i+3] をロード、x0 += 16
ld1 {v2.4s}, [x1], #16 // b[i..i+3] をロード、x1 += 16
fmla v0.4s, v1.4s, v2.4s // v0 += v1 * v2 (積和演算)
subs w3, w3, #1
b.ne .Lloop4
// 水平加算
faddp v0.4s, v0.4s, v0.4s // ペアワイズ加算 [s0+s1, s2+s3, ...]
faddp s0, v0.2s // 最終加算 → s0 に合計
.Lremainder:
and w2, w2, #3
cbz w2, .Ldone
.Lloop1:
ldr s1, [x0], #4
ldr s2, [x1], #4
fmadd s0, s1, s2, s0 // s0 += s1 * s2
subs w2, w2, #1
b.ne .Lloop1
.Ldone:
ret // 戻り値は s0 (float)
// ============ NEON 画像処理: RGBA→グレースケール変換 ============
// 各ピクセル: gray = 0.299*R + 0.587*G + 0.114*B
neon_rgba_to_gray:
// 係数をセットアップ
movi v16.8b, #77 // 0.299 * 256 ≈ 77
movi v17.8b, #150 // 0.587 * 256 ≈ 150
movi v18.8b, #29 // 0.114 * 256 ≈ 29
.Lpixel_loop:
ld4 {v0.8b, v1.8b, v2.8b, v3.8b}, [x0], #32
// v0=R, v1=G, v2=B, v3=A (8ピクセル分をデインターリーブ)
umull v4.8h, v0.8b, v16.8b // R * 77
umlal v4.8h, v1.8b, v17.8b // + G * 150
umlal v4.8h, v2.8b, v18.8b // + B * 29
shrn v5.8b, v4.8h, #8 // >> 8 (256 で割る)
st1 {v5.8b}, [x1], #8 // グレースケール結果を保存
subs w2, w2, #8
b.gt .Lpixel_loop
ret
12.5 RISC-V ベクトル拡張 (RVV)
# RISC-V ベクトル拡張 (RVV 1.0)
# 特徴: ベクトル長非依存 (VLA) プログラミングモデル
# ハードウェアが実際のベクトル長 (VLEN) を決定
# ベクトル加算: void vadd(float *a, float *b, float *c, int n)
# a0 = a, a1 = b, a2 = c, a3 = n
.global vadd_rvv
vadd_rvv:
vsetvli t0, a3, e32, m1 # ベクトル長を設定
# e32=32bit要素, m1=LMUL=1
.Lloop:
vle32.v v0, (a0) # a[] からベクトルロード
vle32.v v1, (a1) # b[] からベクトルロード
vfadd.vv v2, v0, v1 # v2 = v0 + v1
vse32.v v2, (a2) # c[] へベクトルストア
# ポインタとカウンタの更新
slli t1, t0, 2 # t1 = vl * 4 (sizeof(float))
add a0, a0, t1
add a1, a1, t1
add a2, a2, t1
sub a3, a3, t0 # n -= vl
bnez a3, .Lloop
ret
13. インラインアセンブリ
13.1 GCC/Clang のインラインアセンブリ
インラインアセンブリは、C/C++ のソースコード内にアセンブリ命令を埋め込む機能である。コンパイラの最適化を活用しつつ、特定の部分だけをアセンブリで記述できる。
// ============ 基本構文 (GCC Extended Asm) ============
// asm volatile (
// "アセンブリテンプレート"
// : 出力オペランド
// : 入力オペランド
// : クロバーリスト (破壊されるレジスタ)
// );
// 制約文字:
// "r" = 汎用レジスタ
// "a" = RAX, "b" = RBX, "c" = RCX, "d" = RDX
// "S" = RSI, "D" = RDI
// "m" = メモリオペランド
// "i" = 即値
// "=" = 出力専用, "+" = 入出力, "&" = 早期クロバー
// "x" = SSE レジスタ
#include <stdint.h>
// ============ 例1: RDTSC (タイムスタンプカウンタ読み取り) ============
static inline uint64_t rdtsc(void) {
uint32_t lo, hi;
asm volatile (
"rdtsc"
: "=a" (lo), "=d" (hi) // 出力: EAX → lo, EDX → hi
: // 入力: なし
: // クロバー: なし
);
return ((uint64_t)hi << 32) | lo;
}
// ============ 例2: CPUID ============
static inline void cpuid(uint32_t leaf, uint32_t *eax, uint32_t *ebx,
uint32_t *ecx, uint32_t *edx) {
asm volatile (
"cpuid"
: "=a" (*eax), "=b" (*ebx), "=c" (*ecx), "=d" (*edx)
: "a" (leaf), "c" (0)
:
);
}
// ============ 例3: アトミック CAS (Compare And Swap) ============
static inline int atomic_cas(volatile int64_t *ptr, int64_t expected,
int64_t desired) {
int64_t old_val;
int success;
asm volatile (
"lock cmpxchgq %[desired], %[ptr]"
: "=a" (old_val), // RAX に古い値
[ptr] "+m" (*ptr), // メモリオペランド (入出力)
"=@ccz" (success) // ZF フラグ → success
: "a" (expected), // RAX に期待値をセット
[desired] "r" (desired) // レジスタに新しい値
: "memory", "cc" // メモリとフラグを破壊
);
return success;
}
// ============ 例4: ビット操作 ============
static inline int count_leading_zeros(uint64_t x) {
uint64_t result;
asm (
"lzcntq %1, %0"
: "=r" (result)
: "r" (x)
: "cc"
);
return (int)result;
}
// ============ 例5: メモリバリア ============
static inline void memory_fence(void) {
asm volatile ("mfence" ::: "memory");
}
static inline void store_fence(void) {
asm volatile ("sfence" ::: "memory");
}
static inline void load_fence(void) {
asm volatile ("lfence" ::: "memory");
}
13.2 AArch64 のインラインアセンブリ
// ============ AArch64 インラインアセンブリ ============
// レジスタ制約:
// "r" = 汎用レジスタ (Xn/Wn)
// "w" = 浮動小数点/SIMD レジスタ (Vn/Dn/Sn)
// "m" = メモリ
// "i" = 即値
// 例1: サイクルカウンタの読み取り
static inline uint64_t read_cycle_counter(void) {
uint64_t val;
asm volatile (
"mrs %0, cntvct_el0" // Virtual Timer Count register
: "=r" (val)
);
return val;
}
// 例2: キャッシュラインのフラッシュ
static inline void cache_flush(void *addr) {
asm volatile (
"dc civac, %0\n\t" // Clean & Invalidate by VA to PoC
"dsb sy\n\t" // Data Synchronization Barrier
"isb" // Instruction Synchronization Barrier
:
: "r" (addr)
: "memory"
);
}
// 例3: CAS (Compare And Swap) - ARMv8.1 LSE 拡張
static inline int64_t atomic_cas_arm(volatile int64_t *ptr,
int64_t expected, int64_t desired) {
int64_t old_val = expected;
asm volatile (
"casal %0, %2, [%1]" // Compare And Swap with Acquire-Release
: "+r" (old_val) // 入出力: 古い値 / 期待値
: "r" (ptr), "r" (desired)
: "memory"
);
return old_val;
}
// 例4: NEON を使ったベクトル演算
#include <arm_neon.h>
float neon_horizontal_sum(float32x4_t v) {
float result;
asm (
"faddp %0.4s, %1.4s, %1.4s\n\t"
"faddp %s0, %0.2s"
: "=w" (result) // "w" = SIMD レジスタ
: "w" (v)
);
return result;
}
13.3 コンパイラ組み込み関数 (Intrinsics)
インラインアセンブリの代替として、コンパイラが提供する組み込み関数(intrinsics)を使う方法がある。コンパイラが最適化できるため、一般的にはこちらが推奨される。
// ============ x86-64 SSE/AVX Intrinsics ============
#include <immintrin.h>
// SSE: 4つの float のドット積
float dot_product_sse(const float *a, const float *b, int n) {
__m128 sum = _mm_setzero_ps(); // sum = [0, 0, 0, 0]
int i;
for (i = 0; i + 4 <= n; i += 4) {
__m128 va = _mm_loadu_ps(a + i); // a[i..i+3] をロード
__m128 vb = _mm_loadu_ps(b + i); // b[i..i+3] をロード
__m128 prod = _mm_mul_ps(va, vb); // 要素ごとの乗算
sum = _mm_add_ps(sum, prod); // 部分和に加算
}
// 水平加算
sum = _mm_hadd_ps(sum, sum);
sum = _mm_hadd_ps(sum, sum);
float result;
_mm_store_ss(&result, sum);
// 残り要素
for (; i < n; i++) {
result += a[i] * b[i];
}
return result;
}
// AVX2: 8つの int32 の加算
#include <immintrin.h>
void add_arrays_avx2(const int *a, const int *b, int *c, int n) {
int i;
for (i = 0; i + 8 <= n; i += 8) {
__m256i va = _mm256_loadu_si256((__m256i*)(a + i));
__m256i vb = _mm256_loadu_si256((__m256i*)(b + i));
__m256i vc = _mm256_add_epi32(va, vb);
_mm256_storeu_si256((__m256i*)(c + i), vc);
}
for (; i < n; i++) {
c[i] = a[i] + b[i];
}
}
// ============ ARM NEON Intrinsics ============
#include <arm_neon.h>
float dot_product_neon(const float *a, const float *b, int n) {
float32x4_t sum = vdupq_n_f32(0.0f);
int i;
for (i = 0; i + 4 <= n; i += 4) {
float32x4_t va = vld1q_f32(a + i);
float32x4_t vb = vld1q_f32(b + i);
sum = vmlaq_f32(sum, va, vb); // Multiply-Accumulate
}
// 水平加算
float result = vaddvq_f32(sum);
for (; i < n; i++) {
result += a[i] * b[i];
}
return result;
}
13.4 コンパイラ出力の確認
# GCC/Clang でアセンブリ出力を生成
gcc -S -O2 -o output.s input.c # AT&T 構文
gcc -S -O2 -masm=intel -o output.s input.c # Intel 構文
clang -S -O2 -o output.s input.c
# 特定の ISA 拡張を有効化
gcc -S -O2 -mavx2 -o output.s input.c
gcc -S -O2 -march=native -o output.s input.c # 現在のCPU向け
# objdump で逆アセンブル
objdump -d -M intel binary # Intel 構文
objdump -d binary # AT&T 構文
# LLVM の llvm-objdump
llvm-objdump -d --x86-asm-syntax=intel binary
# Compiler Explorer (Godbolt) をローカルで使う代わりに
# https://godbolt.org/ でリアルタイムにコンパイラ出力を確認可能
14. アセンブラツールの設定と使い方
14.1 NASM (Netwide Assembler)
# インストール
# Ubuntu/Debian
sudo apt install nasm
# macOS
brew install nasm
# バージョン確認
nasm --version
# NASM version 2.16.01
# 基本的な使い方
nasm -f elf64 -o hello.o hello.asm # Linux x86-64 (ELF)
nasm -f macho64 -o hello.o hello.asm # macOS x86-64 (Mach-O)
nasm -f win64 -o hello.obj hello.asm # Windows x86-64 (PE/COFF)
# デバッグ情報付き
nasm -f elf64 -g -F dwarf -o hello.o hello.asm
# プリプロセッサの使用
nasm -f elf64 -DDEBUG -o hello.o hello.asm # マクロ定義
nasm -f elf64 -I./include/ -o hello.o hello.asm # インクルードパス
# リスティング出力
nasm -f elf64 -l listing.lst -o hello.o hello.asm
; NASM 固有のディレクティブ
[BITS 64] ; 64ビットモード
[DEFAULT REL] ; デフォルトで RIP 相対アドレッシング
[SECTION .text] ; セクション指定
; NASM のマクロシステム
%macro function_prologue 0
push rbp
mov rbp, rsp
%endmacro
%macro function_epilogue 0
leave
ret
%endmacro
; 条件付きアセンブル
%ifdef __MACHO__ ; macOS Mach-O
%define SYS_WRITE 0x2000004
%elifdef __ELF__ ; Linux ELF
%define SYS_WRITE 1
%endif
14.2 GAS (GNU Assembler)
# GAS は binutils に含まれる(通常プリインストール済み)
# 基本的な使い方
as -o hello.o hello.s # デフォルト
as --64 -o hello.o hello.s # 64ビットモード明示
as -g -o hello.o hello.s # デバッグ情報付き
# クロスアセンブル
aarch64-linux-gnu-as -o hello.o hello.s # AArch64 Linux
riscv64-linux-gnu-as -o hello.o hello.s # RISC-V 64
# macOS では clang 内蔵のアセンブラを使用
clang -c hello.s -o hello.o
# または
as -arch arm64 -o hello.o hello.s # macOS ARM64
14.3 LLVM-MC (LLVM Machine Code)
# LLVM-MC は LLVM プロジェクトのアセンブラ/逆アセンブラ
# アセンブル
llvm-mc -filetype=obj -triple=x86_64-linux-gnu -o hello.o hello.s
llvm-mc -filetype=obj -triple=aarch64-linux-gnu -o hello.o hello.s
# 逆アセンブル
llvm-mc -disassemble -triple=x86_64 <<< "0x48 0x89 0xc1"
# 出力: movq %rax, %rcx
# エンコーディング確認
llvm-mc -show-encoding -triple=x86_64 <<< "mov rax, rbx"
# 出力: movq %rbx, %rax # encoding: [0x48,0x89,0xd8]
# macOS での使用 (Xcode に含まれる)
xcrun llvm-mc -filetype=obj -triple=arm64-apple-macos -o hello.o hello.s
14.4 リンク
# ============ Linux x86-64 ============
# 静的リンク (libc なし)
ld -o hello hello.o
# 動的リンク (libc 使用)
ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 \
-o hello hello.o -lc
# GCC 経由でリンク (推奨)
gcc -no-pie -o hello hello.o # 非PIE
gcc -o hello hello.o # PIE (デフォルト)
# ============ macOS x86-64 ============
ld -o hello hello.o -lSystem \
-syslibroot $(xcrun --show-sdk-path) \
-e _main -arch x86_64
# ============ macOS ARM64 ============
ld -o hello hello.o -lSystem \
-syslibroot $(xcrun --show-sdk-path) \
-e _main -arch arm64
# clang 経由 (推奨)
clang -o hello hello.o
# ============ RISC-V クロスコンパイル ============
riscv64-linux-gnu-ld -o hello hello.o
riscv64-linux-gnu-gcc -o hello hello.o
14.5 Makefile の例
# ============ Linux x86-64 (NASM) ============
AS = nasm
ASFLAGS = -f elf64 -g -F dwarf
LD = ld
LDFLAGS =
CC = gcc
SRCS = $(wildcard *.asm)
OBJS = $(SRCS:.asm=.o)
TARGET = program
.PHONY: all clean
all: $(TARGET)
$(TARGET): $(OBJS)
$(LD) $(LDFLAGS) -o $@ $^
%.o: %.asm
$(AS) $(ASFLAGS) -o $@ $<
clean:
rm -f $(OBJS) $(TARGET)
# ============ macOS ARM64 (GAS) ============
# Makefile.arm64
AS = as
ASFLAGS = -arch arm64
LD = ld
LDFLAGS = -lSystem -syslibroot $(shell xcrun --show-sdk-path) -arch arm64
CC = clang
SRCS = $(wildcard *.s)
OBJS = $(SRCS:.s=.o)
TARGET = program
all: $(TARGET)
$(TARGET): $(OBJS)
$(LD) $(LDFLAGS) -e _main -o $@ $^
%.o: %.s
$(AS) $(ASFLAGS) -o $@ $<
clean:
rm -f $(OBJS) $(TARGET)
# ============ C とアセンブリの混合プロジェクト ============
# Makefile.mixed
CC = gcc
AS = nasm
CFLAGS = -O2 -Wall -g
ASFLAGS = -f elf64 -g -F dwarf
C_SRCS = main.c utils.c
S_SRCS = fast_math.asm simd_ops.asm
C_OBJS = $(C_SRCS:.c=.o)
S_OBJS = $(S_SRCS:.asm=.o)
TARGET = mixed_program
all: $(TARGET)
$(TARGET): $(C_OBJS) $(S_OBJS)
$(CC) $(CFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
%.o: %.asm
$(AS) $(ASFLAGS) -o $@ $<
clean:
rm -f $(C_OBJS) $(S_OBJS) $(TARGET)
14.6 CMake での設定
# CMakeLists.txt - アセンブリとCの混合プロジェクト
cmake_minimum_required(VERSION 3.20)
project(AsmProject LANGUAGES C ASM_NASM)
# NASM の設定
set(CMAKE_ASM_NASM_OBJECT_FORMAT elf64)
set(CMAKE_ASM_NASM_FLAGS "-g -F dwarf")
# ソースファイル
set(C_SOURCES
src/main.c
src/utils.c
)
set(ASM_SOURCES
src/fast_math.asm
src/simd_ops.asm
)
# ターゲット
add_executable(myprogram ${C_SOURCES} ${ASM_SOURCES})
target_include_directories(myprogram PRIVATE include)
target_compile_options(myprogram PRIVATE
$<$<COMPILE_LANGUAGE:C>:-O2 -Wall -mavx2>
)
15. リンカとオブジェクトファイルフォーマット
15.1 オブジェクトファイルフォーマットの概要
| フォーマット | プラットフォーム | 拡張子 | ツール |
|---|---|---|---|
| ELF | Linux, FreeBSD, Solaris | .o, .so, (なし) | readelf, objdump |
| Mach-O | macOS, iOS | .o, .dylib | otool, nm, objdump |
| PE/COFF | Windows | .obj, .exe, .dll | dumpbin, objdump |
15.2 ELF (Executable and Linkable Format)
ELF ファイル構造:
┌──────────────────────┐
│ ELF ヘッダー │ マジックナンバー, アーキテクチャ, エントリポイント
├──────────────────────┤
│ プログラムヘッダー │ 実行時のセグメント情報 (実行ファイル/共有ライブラリ)
│ テーブル │
├──────────────────────┤
│ .text │ 実行可能コード
├──────────────────────┤
│ .rodata │ 読み取り専用データ (定数文字列など)
├──────────────────────┤
│ .data │ 初期化済みグローバル/静的変数
├──────────────────────┤
│ .bss │ 未初期化データ (ゼロ初期化)
├──────────────────────┤
│ .symtab │ シンボルテーブル
├──────────────────────┤
│ .strtab │ 文字列テーブル
├──────────────────────┤
│ .rel.text │ 再配置情報
├──────────────────────┤
│ セクションヘッダー │ 各セクションのメタデータ
│ テーブル │
└──────────────────────┘
# ELF ファイルの解析
readelf -h hello.o # ELF ヘッダー
readelf -S hello.o # セクション一覧
readelf -s hello.o # シンボルテーブル
readelf -r hello.o # 再配置テーブル
readelf -l hello # プログラムヘッダー (実行ファイル)
# objdump で逆アセンブル
objdump -d -M intel hello # コードの逆アセンブル
objdump -t hello # シンボルテーブル
objdump -x hello # 全ヘッダー情報
# nm でシンボル一覧
nm hello.o
# T = テキスト(コード)セクションの定義シンボル
# D = データセクションの定義シンボル
# U = 未定義シンボル(外部参照)
# B = BSS セクション
15.3 Mach-O (macOS)
Mach-O ファイル構造:
┌──────────────────────┐
│ Mach-O ヘッダー │ マジックナンバー, CPU タイプ, ファイルタイプ
├──────────────────────┤
│ ロードコマンド │ セグメント定義, シンボル情報, 動的リンク情報
├──────────────────────┤
│ __TEXT セグメント │
│ __text │ 実行可能コード
│ __stubs │ PLT スタブ
│ __stub_helper │ 遅延バインディングヘルパー
│ __cstring │ C 文字列定数
├──────────────────────┤
│ __DATA セグメント │
│ __data │ 初期化済みデータ
│ __bss │ 未初期化データ
│ __la_symbol_ptr │ 遅延バインディングポインタ
│ __got │ Global Offset Table
├──────────────────────┤
│ __LINKEDIT セグメント │ シンボル, 文字列, 再配置情報
└──────────────────────┘
# macOS での解析ツール
otool -h hello.o # Mach-O ヘッダー
otool -l hello # ロードコマンド
otool -tv hello # テキストセクションの逆アセンブル
otool -L hello # 動的ライブラリの依存関係
# nm (macOS 版)
nm hello.o
nm -m hello # Mach-O 形式で表示
# LLVM ツール
llvm-objdump -d hello # 逆アセンブル
llvm-nm hello.o # シンボル一覧
llvm-readobj --headers hello # ヘッダー情報
15.4 リンクの仕組み
コンパイル・リンクの流れ:
main.c ──→ [コンパイラ] ──→ main.o ─┐
│
utils.c ──→ [コンパイラ] ──→ utils.o ┼──→ [リンカ] ──→ program (実行ファイル)
│
math.asm ──→ [アセンブラ] ──→ math.o ┘
↑
libc.so (共有ライブラリ)
シンボル解決
; file1.asm
section .text
global my_function ; 外部に公開するシンボル
extern printf ; 外部シンボルの参照
my_function:
; printf を呼び出す
lea rdi, [rel fmt]
xor eax, eax
call printf wrt ..plt ; PLT 経由の呼び出し
ret
section .rodata
fmt: db "Hello from asm", 10, 0
// file2.c
extern void my_function(void); // アセンブリで定義された関数
int main(void) {
my_function(); // リンカがシンボルを解決
return 0;
}
# コンパイルとリンク
nasm -f elf64 -o file1.o file1.asm
gcc -c -o file2.o file2.c
gcc -o program file1.o file2.o # リンカが my_function と printf を解決
15.5 位置独立コード (PIC/PIE)
; 位置独立コード (Position-Independent Code)
; ASLR (Address Space Layout Randomization) に必要
; PIC でのグローバル変数アクセス
section .text
global get_global_var
get_global_var:
; RIP 相対アドレッシング (x86-64 のPICの基本)
mov eax, [rel my_global] ; NASM: rel キーワード
ret
; PIC での外部関数呼び出し
call_external:
; PLT (Procedure Linkage Table) 経由
call printf wrt ..plt ; NASM
; GAS: call printf@PLT
; GOT (Global Offset Table) 経由のアクセス
mov rax, [rel my_extern wrt ..got] ; GOT エントリのアドレス
mov rax, [rax] ; 実際のアドレスをロード
# PIE (Position-Independent Executable) のビルド
gcc -pie -o program main.o asm_module.o # PIE (デフォルト)
gcc -no-pie -o program main.o asm_module.o # 非PIE
# 共有ライブラリの作成
nasm -f elf64 -o mylib.o mylib.asm
gcc -shared -o libmylib.so mylib.o
# 使用
gcc -o program main.c -L. -lmylib
LD_LIBRARY_PATH=. ./program
16. デバッグ手法
16.1 GDB でのアセンブリデバッグ
# GDB の起動
gdb ./program
# Intel 構文に設定(推奨)
(gdb) set disassembly-flavor intel
# .gdbinit に追加しておくと便利:
# echo "set disassembly-flavor intel" >> ~/.gdbinit
# ============ GDB 基本コマンド ============
# ブレークポイント
(gdb) break main # 関数にブレークポイント
(gdb) break *0x401000 # アドレスにブレークポイント
(gdb) break *main+20 # main+20バイトの位置
(gdb) info breakpoints # ブレークポイント一覧
# 実行制御
(gdb) run # プログラム開始
(gdb) run arg1 arg2 # 引数付きで開始
(gdb) continue # 続行
(gdb) stepi (si) # 1命令ステップ実行(関数に入る)
(gdb) nexti (ni) # 1命令ステップ実行(関数をスキップ)
(gdb) finish # 現在の関数を最後まで実行
(gdb) until *0x401050 # 指定アドレスまで実行
# 逆アセンブル
(gdb) disassemble # 現在の関数
(gdb) disassemble main # main 関数
(gdb) disassemble /r main # バイトコード付き
(gdb) disassemble 0x401000,0x401050 # アドレス範囲
(gdb) x/10i $rip # 現在のRIPから10命令表示
# レジスタ表示
(gdb) info registers # 全レジスタ
(gdb) info registers rax rbx # 特定レジスタ
(gdb) print $rax # RAX の値
(gdb) print/x $rax # 16進数で表示
(gdb) print/t $rax # 2進数で表示
(gdb) info registers xmm0 # SIMD レジスタ
# フラグレジスタ
(gdb) print $eflags # フラグレジスタ
# 出力例: [ CF PF ZF SF IF ]
# メモリ表示 (x コマンド)
(gdb) x/4xg $rsp # スタックトップから 4 x 8バイト (16進数)
(gdb) x/16xb $rsp # 16バイト表示
(gdb) x/s 0x402000 # 文字列として表示
(gdb) x/10i $rip # 命令として表示
(gdb) x/4xw $rsp # 4 x 4バイト (ワード)
# x コマンドのフォーマット: x/[数][フォーマット][サイズ]
# フォーマット: x(16進), d(10進), u(符号なし), o(8進), t(2進), s(文字列), i(命令)
# サイズ: b(byte), h(halfword), w(word=4byte), g(giant=8byte)
# メモリ書き換え
(gdb) set *0x7fffffffde00 = 42 # メモリに値を書き込み
(gdb) set $rax = 0xFF # レジスタに値をセット
# スタックの確認
(gdb) backtrace (bt) # コールスタック
(gdb) info frame # 現在のスタックフレーム
(gdb) info locals # ローカル変数
# ウォッチポイント
(gdb) watch *0x404000 # メモリアドレスの変更を監視
(gdb) rwatch *0x404000 # 読み取りを監視
(gdb) awatch *0x404000 # 読み書き両方を監視
16.2 GDB TUI モード
# TUI (Text User Interface) モード
(gdb) layout asm # アセンブリビュー
(gdb) layout regs # レジスタ + アセンブリ
(gdb) layout split # ソース + アセンブリ
(gdb) tui reg general # 汎用レジスタウィンドウ
(gdb) tui reg float # 浮動小数点レジスタ
(gdb) focus asm # アセンブリウィンドウにフォーカス
# GDB ダッシュボード (gdb-dashboard) もおすすめ
# https://github.com/cyrus-and/gdb-dashboard
16.3 LLDB でのアセンブリデバッグ (macOS)
# LLDB の起動
lldb ./program
# ============ LLDB コマンド ============
# ブレークポイント
(lldb) breakpoint set --name main # 関数名
(lldb) b main # 短縮形
(lldb) breakpoint set --address 0x100003f50 # アドレス
(lldb) b -a 0x100003f50
# 実行制御
(lldb) run
(lldb) process launch -- arg1 arg2
(lldb) continue (c)
(lldb) thread step-inst (si) # 1命令ステップ
(lldb) thread step-inst-over (ni) # 1命令ステップオーバー
# 逆アセンブル
(lldb) disassemble --frame # 現在のフレーム
(lldb) di -f # 短縮形
(lldb) disassemble --name main # 関数指定
(lldb) disassemble --start-address 0x100003f50 --count 20
(lldb) di -s 0x100003f50 -c 20
# レジスタ
(lldb) register read # 全レジスタ
(lldb) register read rax rbx # 特定レジスタ
(lldb) register read --all # SIMD レジスタ含む全て
(lldb) register write rax 0x42 # レジスタに書き込み
# メモリ
(lldb) memory read $rsp # スタックの内容
(lldb) memory read --size 8 --format x --count 4 $rsp
(lldb) x -s8 -fx -c4 $rsp # 短縮形
(lldb) memory read --format s 0x100004000 # 文字列
# AArch64 固有
(lldb) register read x0 x1 x29 x30 sp pc
(lldb) register read cpsr # 条件フラグ
(lldb) register read v0 v1 # NEON レジスタ
16.4 デバッグの実践テクニック
# ============ コアダンプの解析 ============
# コアダンプを有効化
ulimit -c unlimited
# クラッシュしたプログラムのコアダンプを GDB で解析
gdb ./program core
(gdb) bt # クラッシュ時のスタックトレース
(gdb) info registers # クラッシュ時のレジスタ
(gdb) x/10i $rip-20 # クラッシュ前後のコード
# ============ strace/dtrace との併用 ============
# システムコールのトレース (Linux)
strace ./program
strace -e trace=write,read ./program # 特定のシステムコールのみ
# macOS の場合
dtruss ./program
# ============ Valgrind でのメモリエラー検出 ============
valgrind --tool=memcheck ./program
valgrind --tool=callgrind ./program # プロファイリング
# ============ perf でのパフォーマンス分析 (Linux) ============
perf stat ./program # 基本統計
perf record -g ./program # サンプリング
perf report # レポート表示
perf annotate --stdio -s function_name # 関数のアセンブリレベル分析
17. 最適化テクニック
17.1 パイプライン最適化
; ============ 命令レベル並列性 (ILP) の向上 ============
; 悪い例: データ依存のチェーン (各命令が前の結果に依存)
add rax, rbx ; rax = rax + rbx
add rax, rcx ; rax に依存 → パイプラインストール
add rax, rdx ; rax に依存 → パイプラインストール
; レイテンシ: 3サイクル (直列実行)
; 良い例: 依存関係を分散
add rax, rbx ; 独立した加算
add rcx, rdx ; rax に依存しない → 並列実行可能
add rax, rcx ; 最後に統合
; レイテンシ: 2サイクル (add rax,rbx と add rcx,rdx が並列実行)
; ============ ループ展開 (Loop Unrolling) ============
; 展開前
.loop:
add rax, [rdi]
add rdi, 8
dec ecx
jnz .loop
; 4倍展開
shr ecx, 2 ; n / 4
.loop4:
add rax, [rdi]
add rax, [rdi + 8]
add rax, [rdi + 16]
add rax, [rdi + 24]
add rdi, 32
dec ecx
jnz .loop4
; 利点: 分岐予測ミスの頻度が1/4に、ループオーバーヘッド削減
; さらなる最適化: アキュムレータを分散して依存チェーンを分割
xor eax, eax ; sum0 = 0
xor r8d, r8d ; sum1 = 0
xor r9d, r9d ; sum2 = 0
xor r10d, r10d ; sum3 = 0
shr ecx, 2
.loop4_parallel:
add rax, [rdi]
add r8, [rdi + 8]
add r9, [rdi + 16]
add r10, [rdi + 24]
add rdi, 32
dec ecx
jnz .loop4_parallel
add rax, r8
add r9, r10
add rax, r9 ; total = sum0 + sum1 + sum2 + sum3
17.2 キャッシュ最適化
; ============ キャッシュラインサイズの考慮 ============
; 典型的なキャッシュラインサイズ: 64バイト
; ソフトウェアプリフェッチ
prefetcht0 [rdi + 256] ; L1 キャッシュにプリフェッチ
prefetcht1 [rdi + 512] ; L2 キャッシュにプリフェッチ
prefetchnta [rdi + 256] ; Non-Temporal Access (キャッシュ汚染を回避)
; ストリーミングストア (キャッシュをバイパス)
; 大量データの書き込み時にキャッシュ汚染を回避
vmovntps [rdi], ymm0 ; Non-Temporal Store (AVX)
vmovntdq [rdi], ymm0 ; Non-Temporal Store (整数)
; ※ ストア後に sfence が必要
sfence
; キャッシュラインアラインメント
section .data
align 64 ; 64バイトアラインメント (キャッシュライン境界)
my_array: times 1024 dq 0
// C でのキャッシュフレンドリーなアクセスパターン
// 悪い例: 列優先アクセス (キャッシュミス多発)
for (int j = 0; j < N; j++)
for (int i = 0; i < N; i++)
sum += matrix[i][j]; // 行をまたいでアクセス → キャッシュミス
// 良い例: 行優先アクセス (キャッシュフレンドリー)
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
sum += matrix[i][j]; // 連続メモリアクセス → キャッシュヒット
17.3 分岐予測の最適化
; ============ 分岐予測ミスの回避 ============
; 方法1: CMOV を使った分岐レス化
; if (a > b) max = a; else max = b;
cmp rdi, rsi
mov rax, rsi ; max = b
cmovg rax, rdi ; a > b なら max = a
; 分岐なし → 分岐予測ミスペナルティなし
; 方法2: SETcc + 算術
; result = (x == 0) ? a : b
test rdi, rdi
setz al ; al = (x == 0) ? 1 : 0
movzx eax, al
; eax を使って条件に応じた値を計算
; 方法3: ビット演算による条件選択
; mask = (a > b) ? -1 : 0
cmp rdi, rsi
sbb rax, rax ; CF=1 なら rax=-1, CF=0 なら rax=0
; max = (a & ~mask) | (b & mask) のように使用
; ============ 分岐のヒント (Likely/Unlikely) ============
; GCC __builtin_expect に相当するアセンブリヒント
; x86: Intel Pentium 4 以降、静的分岐予測ヒント (0x2E: not taken, 0x3E: taken)
; ただし現代のCPUでは通常無視される。コンパイラに任せるのが最善。
17.4 命令選択の最適化
; ============ 効率的な命令の選択 ============
; ゼロクリア
xor eax, eax ; 2バイト、依存関係破壊 (最速)
; mov rax, 0 は 7バイト。sub rax, rax は依存関係あり
; 2のべき乗による乗算
shl rax, 3 ; rax *= 8
lea rax, [rax + rax*2] ; rax *= 3
lea rax, [rax*4 + rax] ; rax *= 5
; 2のべき乗による除算 (符号なし)
shr rax, 3 ; rax /= 8
; 2のべき乗による除算 (符号付き、丸め補正付き)
; C: int result = x / 8; (x が負の場合の切り捨て方向が異なる)
mov rcx, rax
sar rcx, 63 ; rcx = (x < 0) ? -1 : 0
shr rcx, 61 ; rcx = (x < 0) ? 7 : 0
add rax, rcx ; 負の値に対する丸め補正
sar rax, 3 ; /8
; 定数による乗算の最適化 (コンパイラが自動生成する例)
; x * 10 = x * 8 + x * 2 = (x << 3) + (x << 1)
lea rax, [rdi + rdi*4] ; rax = rdi * 5
shl rax, 1 ; rax = rdi * 10
; x * 7 = x * 8 - x
lea rax, [rdi*8]
sub rax, rdi ; rax = rdi * 7
; ============ アラインメント ============
; ループ先頭を16バイト境界にアラインメント
align 16
.hot_loop:
; 頻繁に実行される命令
jnz .hot_loop
17.5 メモリアクセスの最適化
; ============ アラインメントの重要性 ============
; アラインされたアクセスは高速
movaps xmm0, [rdi] ; 16バイトアラインメント必須 (高速)
movups xmm0, [rdi] ; アラインメント不要 (やや遅い可能性)
; 現代のCPUではキャッシュライン境界をまたがなければ差は小さい
; ============ メモリコピーの最適化 ============
; rep movsb (Enhanced REP MOVSB, ERMS 対応CPUで高速)
my_memcpy:
mov rcx, rdx ; 長さ
; rdi = dest (呼び出し規約で設定済み)
; rsi = src (呼び出し規約で設定済み)
rep movsb ; バイト単位コピー (ERMS対応なら高速)
ret
; ============ メモリゼロクリアの最適化 ============
my_memset_zero:
mov rcx, rsi ; 長さ
xor eax, eax ; 0
rep stosb ; ゼロフィル
ret
18. セキュリティ
18.1 バッファオーバーフロー
バッファオーバーフローは、確保されたバッファの境界を超えてデータを書き込む脆弱性であり、アセンブリレベルの理解が不可欠である。
// 脆弱なコード例
#include <string.h>
void vulnerable_function(char *input) {
char buffer[64];
strcpy(buffer, input); // 境界チェックなし!
}
正常時のスタックレイアウト:
高アドレス
┌──────────────────┐
│ 戻りアドレス │ ← これを上書きされると任意コード実行
├──────────────────┤
│ 保存された RBP │
├──────────────────┤
│ buffer[56..63] │
│ buffer[48..55] │
│ ... │
│ buffer[0..7] │ ← バッファの先頭
├──────────────────┤ ← RSP
低アドレス
オーバーフロー時:
高アドレス
┌──────────────────┐
│ 攻撃者の値!!! │ ← 戻りアドレスを上書き → 任意コード実行
├──────────────────┤
│ AAAAAAAAAAAAA │ ← RBP を上書き
├──────────────────┤
│ AAAAAAAAAAAAA │
│ AAAAAAAAAAAAA │ ← オーバーフローしたデータ
│ ... │
│ 入力データの先頭 │
├──────────────────┤
低アドレス
18.2 防御メカニズム
スタックカナリア (Stack Canary / Stack Protector)
; GCC -fstack-protector-strong が生成するコード
my_function:
push rbp
mov rbp, rsp
sub rsp, 80
; カナリア値をスタックに配置
mov rax, qword [fs:0x28] ; スレッドローカルストレージからカナリア取得
mov qword [rbp - 8], rax ; スタックに保存
; ... 関数本体 ...
; カナリアの検証
mov rax, qword [rbp - 8] ; スタックからカナリア読み取り
xor rax, qword [fs:0x28] ; 元の値と比較
jne .stack_smash_detected ; 不一致ならスタック破壊を検出
leave
ret
.stack_smash_detected:
call __stack_chk_fail ; abort() を呼び出して終了
ASLR (Address Space Layout Randomization)
# ASLR の確認と設定 (Linux)
cat /proc/sys/kernel/randomize_va_space
# 0 = 無効, 1 = スタック+ライブラリ, 2 = スタック+ライブラリ+ヒープ
# PIE (Position Independent Executable) が ASLR の完全な効果に必要
gcc -pie -o program program.c
NX ビット (No-Execute / DEP)
仮想メモリのページ属性:
┌──────────────────┬──────┬──────┬──────┐
│ セグメント │ 読み │ 書き │ 実行 │
├──────────────────┼──────┼──────┼──────┤
│ .text │ ✓ │ ✗ │ ✓ │ コード: 読み取り+実行可能
│ .rodata │ ✓ │ ✗ │ ✗ │ 定数: 読み取り専用
│ .data / .bss │ ✓ │ ✓ │ ✗ │ データ: 読み書き、実行不可
│ スタック │ ✓ │ ✓ │ ✗ │ NX: スタック上のコード実行を防止
│ ヒープ │ ✓ │ ✓ │ ✗ │ NX: ヒープ上のコード実行を防止
└──────────────────┴──────┴──────┴──────┘
18.3 Return-Oriented Programming (ROP)
NX ビットによりスタック上のシェルコード実行が防止されたため、既存のコード断片(ガジェット)を連鎖させる攻撃手法が開発された。
ROP ガジェット: ret で終わるコード断片
ガジェット1: pop rdi; ret (引数をレジスタにセット)
ガジェット2: pop rsi; ret
ガジェット3: syscall; ret (システムコール実行)
攻撃者が構築するスタック:
┌──────────────────────┐
│ ガジェット1のアドレス │ ← 最初の戻りアドレスを上書き
├──────────────────────┤
│ "/bin/sh" のアドレス │ ← pop rdi で RDI にロードされる
├──────────────────────┤
│ ガジェット2のアドレス │ ← ガジェット1の ret で飛ぶ
├──────────────────────┤
│ 0 │ ← pop rsi で RSI にロードされる
├──────────────────────┤
│ ガジェット3のアドレス │
├──────────────────────┤
│ 59 (execve番号) │ ← RAX にセットするガジェットが必要
└──────────────────────┘
# ROP ガジェットの検索ツール
# ROPgadget
ROPgadget --binary ./program
ROPgadget --binary ./program --only "pop|ret"
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6
# ropper
ropper --file ./program --search "pop rdi; ret"
18.4 ROP に対する防御
// ============ AArch64 ポインタ認証 (PAC) ============
// Apple Silicon で積極的に活用されている
_my_function:
// 関数エントリで LR に署名
paciasp // LR を SP をキーにして署名
stp x29, x30, [sp, #-16]!
mov x29, sp
// ... 関数本体 ...
ldp x29, x30, [sp], #16
// リターン前に LR を検証
autiasp // LR の署名を検証
ret // 検証失敗時はフォールト
// PAC によって ROP 攻撃は困難になる:
// 戻りアドレスを改ざんすると署名の検証に失敗する
; ============ x86-64 Control Flow Integrity (CET) ============
; Intel CET: Shadow Stack + Indirect Branch Tracking
; Shadow Stack: CALL/RET の戻りアドレスを別のスタックにも保存
; RET 時に通常スタックとシャドウスタックの値を比較
; 不一致 → #CP (Control Protection) 例外
; Indirect Branch Tracking (IBT)
; 間接分岐の着地点に ENDBR64 命令が必要
endbr64 ; 間接分岐の有効な着地点をマーク
; ENDBR64 がない場所への間接ジャンプ → #CP 例外
18.5 セキュアコーディングのベストプラクティス
; ============ センシティブデータのクリア ============
; 使用後に秘密鍵やパスワードをメモリから確実に消去
secure_clear:
; コンパイラの最適化で消去が削除されないよう volatile アクセスが必要
; アセンブリでは最適化の心配なし
mov rcx, rsi ; 長さ
xor eax, eax
rep stosb ; メモリをゼロクリア
; さらにキャッシュからもフラッシュ
; clflush [rdi] (必要に応じて)
ret
; ============ 定数時間比較 (タイミング攻撃対策) ============
; 暗号処理で使用: 入力の値によらず一定時間で実行
; int constant_time_compare(const uint8_t *a, const uint8_t *b, size_t len)
constant_time_compare:
; rdi = a, rsi = b, rdx = len
xor eax, eax ; result = 0
xor ecx, ecx ; i = 0
.loop:
cmp rcx, rdx
jge .done
movzx r8d, byte [rdi + rcx]
xor r8b, byte [rsi + rcx] ; 差異を検出
or al, r8b ; 差異を蓄積 (短絡評価しない)
inc rcx
jmp .loop
.done:
; al != 0 なら不一致、al == 0 なら一致
test al, al
setnz al
movzx eax, al
ret
; 重要: 不一致を検出しても即座に return しない
; 常に全バイトを比較することで、タイミングから情報が漏れない
19. 実践例
19.1 Hello World プログラム(3アーキテクチャ比較)
x86-64 Linux (NASM)
; hello_x86.asm
section .data
msg db "Hello, World!", 10
msg_len equ $ - msg
section .text
global _start
_start:
mov rax, 1 ; sys_write
mov rdi, 1 ; stdout
lea rsi, [rel msg]
mov rdx, msg_len
syscall
mov rax, 60 ; sys_exit
xor rdi, rdi
syscall
nasm -f elf64 -o hello_x86.o hello_x86.asm
ld -o hello_x86 hello_x86.o
./hello_x86
AArch64 macOS
// hello_arm.s
.global _main
.align 4
_main:
mov x0, #1
adrp x1, msg@PAGE
add x1, x1, msg@PAGEOFF
mov x2, #14
mov x16, #4
svc #0x80
mov x0, #0
mov x16, #1
svc #0x80
.data
msg: .ascii "Hello, World!\n"
as -arch arm64 -o hello_arm.o hello_arm.s
ld -o hello_arm hello_arm.o -lSystem -syslibroot $(xcrun --show-sdk-path) -e _main -arch arm64
./hello_arm
RISC-V Linux
# hello_rv.s
.global _start
.text
_start:
li a7, 64
li a0, 1
la a1, msg
li a2, 14
ecall
li a7, 93
li a0, 0
ecall
.rodata
msg: .ascii "Hello, World!\n"
riscv64-linux-gnu-as -o hello_rv.o hello_rv.s
riscv64-linux-gnu-ld -o hello_rv hello_rv.o
qemu-riscv64 ./hello_rv
19.2 フィボナッチ数列(反復版)
; fibonacci.asm - x86-64 Linux
; uint64_t fibonacci(uint64_t n)
; 引数: rdi = n
; 戻り値: rax = fib(n)
section .text
global fibonacci
fibonacci:
cmp rdi, 1
jbe .base_case ; n <= 1 なら n を返す
xor eax, eax ; a = 0 (fib(0))
mov rcx, 1 ; b = 1 (fib(1))
mov rdx, 1 ; i = 1
.loop:
mov r8, rcx ; temp = b
add rcx, rax ; b = a + b
mov rax, r8 ; a = temp
inc rdx ; i++
cmp rdx, rdi
jb .loop ; i < n なら継続
mov rax, rcx ; return b
ret
.base_case:
mov rax, rdi ; return n (0 or 1)
ret
// main.c - テスト用
#include <stdio.h>
#include <stdint.h>
extern uint64_t fibonacci(uint64_t n);
int main(void) {
for (uint64_t i = 0; i <= 20; i++) {
printf("fib(%lu) = %lu\n", i, fibonacci(i));
}
return 0;
}
nasm -f elf64 -o fibonacci.o fibonacci.asm
gcc -o fib_test main.c fibonacci.o
./fib_test
19.3 文字列操作: strlen と memcpy
; string_ops.asm - x86-64
section .text
; size_t my_strlen(const char *s)
global my_strlen
my_strlen:
mov rax, rdi ; ポインタを保存
.scan:
cmp byte [rdi], 0 ; NULL 文字チェック
je .found
inc rdi
jmp .scan
.found:
sub rdi, rax ; 長さ = 現在位置 - 開始位置
mov rax, rdi
ret
; SIMD を使った高速版 strlen
global my_strlen_sse
my_strlen_sse:
mov rax, rdi
pxor xmm0, xmm0 ; xmm0 = all zeros
; 16バイトアラインメントまで処理
mov rcx, rdi
and rcx, 15 ; アラインメントオフセット
jz .aligned
; 非アライン部分をバイト単位で処理
.byte_scan:
cmp byte [rdi], 0
je .done
inc rdi
test rdi, 15
jnz .byte_scan
.aligned:
; 16バイトずつ NULL バイトを検索
movdqa xmm1, [rdi] ; 16バイトロード
pcmpeqb xmm1, xmm0 ; 各バイトを0と比較
pmovmskb ecx, xmm1 ; 結果のマスクビット
test ecx, ecx
jnz .found_in_chunk
add rdi, 16
jmp .aligned
.found_in_chunk:
bsf ecx, ecx ; 最初のNULLバイトの位置
add rdi, rcx
.done:
sub rdi, rax
mov rax, rdi
ret
; void *my_memcpy(void *dest, const void *src, size_t n)
global my_memcpy
my_memcpy:
mov rax, rdi ; 戻り値 = dest
mov rcx, rdx ; n
rep movsb ; RDI←RSI をRCXバイトコピー
ret
19.4 クイックソートの実装
; quicksort.asm - x86-64 System V ABI
; void quicksort(int64_t *arr, int64_t lo, int64_t hi)
; rdi = arr, rsi = lo, rdx = hi
section .text
global quicksort
quicksort:
cmp rsi, rdx
jge .done ; lo >= hi なら終了
; スタックフレームと callee-saved レジスタの保存
push rbp
mov rbp, rsp
push rbx
push r12
push r13
push r14
mov r12, rdi ; arr
mov r13, rsi ; lo
mov r14, rdx ; hi
; partition
mov rax, [r12 + r14*8] ; pivot = arr[hi]
mov rcx, r13 ; i = lo
mov rbx, r13 ; j = lo
.partition_loop:
cmp rbx, r14
jge .partition_done
cmp qword [r12 + rbx*8], rax ; arr[j] <= pivot?
jg .skip_swap
; swap(arr[i], arr[j])
mov r8, [r12 + rcx*8]
mov r9, [r12 + rbx*8]
mov [r12 + rcx*8], r9
mov [r12 + rbx*8], r8
inc rcx ; i++
.skip_swap:
inc rbx ; j++
jmp .partition_loop
.partition_done:
; swap(arr[i], arr[hi])
mov r8, [r12 + rcx*8]
mov r9, [r12 + r14*8]
mov [r12 + rcx*8], r9
mov [r12 + r14*8], r8
; rcx = pivot index
; quicksort(arr, lo, pivot - 1)
mov rdi, r12
mov rsi, r13
lea rdx, [rcx - 1]
push rcx ; pivot index を保存
call quicksort
; quicksort(arr, pivot + 1, hi)
pop rcx
mov rdi, r12
lea rsi, [rcx + 1]
mov rdx, r14
call quicksort
pop r14
pop r13
pop r12
pop rbx
pop rbp
.done:
ret
19.5 逆アセンブルとリバースエンジニアリング
# ============ バイナリ解析の基本ワークフロー ============
# 1. ファイル種別の確認
file ./target_binary
# 出力例: ELF 64-bit LSB executable, x86-64, dynamically linked
# 2. シンボル情報の確認
nm ./target_binary | head -20
nm -D ./target_binary # 動的シンボルのみ
strings ./target_binary | grep -i password # 文字列検索
# 3. セクション情報
readelf -S ./target_binary
# 4. 逆アセンブル
objdump -d -M intel ./target_binary | less
# 特定の関数だけ逆アセンブル
objdump -d -M intel ./target_binary | grep -A 50 '<main>:'
# 5. 動的解析 (GDB)
gdb ./target_binary
(gdb) break main
(gdb) run
(gdb) disassemble
(gdb) info registers
(gdb) x/20i $rip
# 6. システムコールのトレース
strace ./target_binary 2>&1 | head -50
ltrace ./target_binary 2>&1 | head -50 # ライブラリ関数トレース
19.6 C コンパイラ出力の読解
// example.c
int sum_of_squares(int *arr, int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i] * arr[i];
}
return sum;
}
gcc -O2 -S -masm=intel -o example.s example.c
; GCC -O2 が生成するコード (概要)
sum_of_squares:
test esi, esi ; n == 0?
jle .L4 ; n <= 0 なら 0 を返す
lea eax, [rsi-1] ; eax = n - 1
xor edx, edx ; sum = 0
lea rcx, [rdi+rax*4+4] ; 配列の終端アドレス
.L3:
mov eax, [rdi] ; arr[i]
imul eax, eax ; arr[i] * arr[i]
add rdi, 4 ; ポインタを進める
add edx, eax ; sum += arr[i]^2
cmp rdi, rcx ; 終端に達したか?
jne .L3
mov eax, edx ; return sum
ret
.L4:
xor edx, edx ; sum = 0
mov eax, edx
ret
20. 現代の開発におけるアセンブリの役割
20.1 コンパイラ出力の読解とパフォーマンスチューニング
現代のソフトウェア開発において、アセンブリを「書く」よりも「読む」能力のほうがはるかに重要である。
# Compiler Explorer (Godbolt) の活用
# https://godbolt.org/
# リアルタイムでコンパイラの出力を確認できる Web ツール
# 対応コンパイラ: GCC, Clang, MSVC, ICC
# 対応アーキテクチャ: x86-64, ARM64, RISC-V, etc.
# ローカルでのコンパイラ出力確認
gcc -O2 -S -fverbose-asm -o output.s input.c
# -fverbose-asm: C のソースコードをコメントとして付加
# 特定の最適化の確認
gcc -O2 -ftree-vectorize -fopt-info-vec -S input.c
# ベクトル化の成功/失敗を報告
# clang の最適化レポート
clang -O2 -Rpass=loop-vectorize -Rpass-missed=loop-vectorize -S input.c
20.2 パフォーマンスプロファイリング
# ============ Linux perf ============
# ハードウェアパフォーマンスカウンタを使用した精密な分析
# 基本統計
perf stat ./program
# 出力例:
# 1,234,567,890 cycles
# 2,345,678,901 instructions # IPC = 1.90
# 12,345 cache-misses
# 1,234 branch-misses
# サンプリングプロファイル
perf record -g ./program
perf report # インタラクティブレポート
# アセンブリレベルのアノテーション
perf annotate -s hot_function
# 各命令のサンプル数(実行頻度)が表示される
# ============ Apple Instruments (macOS) ============
# Time Profiler で CPU ホットスポットを特定
# Counters テンプレートでハードウェアカウンタを収集
xcrun xctrace record --template 'Time Profiler' --launch ./program
20.3 アセンブリが依然として必要な領域
カーネル / OS 開発
// Linux カーネルのコンテキストスイッチ (arch/x86/entry/entry_64.S より概念)
// ユーザーモード→カーネルモードの遷移は必然的にアセンブリ
// カーネルのスタック切り替え (概念的なコード)
// swapgs ; GS ベースをカーネル用に切り替え
// mov rsp, [gs:cpu_tss + TSS_sp0] ; カーネルスタックに切り替え
暗号ライブラリ
// OpenSSL, BoringSSL などの暗号ライブラリは
// パフォーマンスとタイミング攻撃耐性のためにアセンブリで実装
// 例: AES-NI を使った AES 暗号化
// 例: SHA-256 の SIMD 実装
ブートローダー
; x86 ブートセクタ (MBR) の例 (概念)
; BIOS は最初のセクタ (512 バイト) を 0x7C00 にロードして実行
[BITS 16]
[ORG 0x7C00]
boot:
cli ; 割り込み禁止
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
; メッセージ表示
mov si, msg
.print:
lodsb
test al, al
jz .halt
mov ah, 0x0E ; BIOS テレタイプ出力
int 0x10
jmp .print
.halt:
hlt
jmp .halt
msg: db "Boot!", 0
times 510-($-$$) db 0 ; 512バイトまでパディング
dw 0xAA55 ; ブートシグネチャ
20.4 WASM (WebAssembly) とアセンブリ
WebAssembly は仮想的な命令セットアーキテクチャであり、スタックマシンベースのバイナリフォーマットである。
;; WebAssembly テキスト形式 (WAT)
;; アセンブリ言語に似た低水準な記述が可能
(module
;; 関数定義: fibonacci(n) -> result
(func $fibonacci (param $n i32) (result i32)
(local $a i32)
(local $b i32)
(local $i i32)
;; if n <= 1 return n
(if (i32.le_s (local.get $n) (i32.const 1))
(then (return (local.get $n)))
)
(local.set $a (i32.const 0))
(local.set $b (i32.const 1))
(local.set $i (i32.const 1))
(block $break
(loop $loop
(local.set $a
(i32.add (local.get $a) (local.get $b)))
;; swap a, b
(local.set $b
(i32.sub (local.get $a) (local.get $b)))
(local.set $a
(i32.sub (local.get $a) (local.get $b)))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br_if $loop (i32.lt_s (local.get $i) (local.get $n)))
)
)
(local.get $b)
)
(export "fibonacci" (func $fibonacci))
)
20.5 学習リソースとツール
| リソース | 種類 | URL / 説明 |
|---|---|---|
| Compiler Explorer | Web ツール | https://godbolt.org/ |
| Intel SDM | 公式マニュアル | Intel 64 and IA-32 Software Developer's Manual |
| ARM ARM | 公式マニュアル | ARM Architecture Reference Manual |
| RISC-V Spec | 公式仕様 | https://riscv.org/specifications/ |
| x86 and amd64 instruction reference | リファレンス | https://www.felixcloutier.com/x86/ |
| Programming from the Ground Up | 書籍 | Jonathan Bartlett |
| Computer Systems: A Programmer's Perspective | 書籍 | Bryant & O'Hallaron (CS:APP) |
| Hacker's Delight | 書籍 | Henry S. Warren Jr. (ビット演算テクニック) |
20.6 まとめ
アセンブリ言語は、以下のような場面で今なお重要な役割を果たしている。
-
パフォーマンスクリティカルなコードの最適化: コンパイラが生成するコードの品質を評価し、必要に応じて手動最適化を行う。SIMD 命令を活用した高速な数値計算、暗号処理、コーデック処理など。
-
セキュリティ分析: 脆弱性の解析、マルウェアのリバースエンジニアリング、エクスプロイト開発と防御技術の理解。
-
システムプログラミング: OS カーネル、ブートローダー、デバイスドライバ、割り込みハンドラなど、高水準言語では記述できない低水準処理。
-
コンピュータサイエンスの基礎理解: CPU の動作原理、メモリ階層、パイプライン、分岐予測など、コンピュータアーキテクチャの深い理解。
-
デバッグとトラブルシューティング: コアダンプの解析、コンパイラのバグの特定、最適化の問題の調査。
現代のソフトウェアエンジニアにとって、アセンブリ言語を日常的に書く必要はないが、読み理解する能力は、高品質なソフトウェアの開発、パフォーマンス問題の解決、セキュリティの強化において不可欠なスキルである。アセンブリ言語の理解は、ソフトウェアとハードウェアの境界を超えた包括的な技術力の基盤となる。
本記事で使用した主な環境:
- x86-64: NASM 2.16+, GAS (GNU Binutils), GCC 13+, Clang 17+
- AArch64: macOS Sonoma/Sequoia (Apple Silicon M1-M4), Clang/LLVM
- RISC-V: GCC RISC-V Cross-Compiler, QEMU User Mode Emulation
- デバッガ: GDB 14+, LLDB (Xcode 15+)