コンテンツにスキップ

AMD64アセンブラ

出典: フリー教科書『ウィキブックス(Wikibooks)』

はじめに

[編集]

本書はFreeBSD環境におけるAMD64(x86-64)アセンブラプログラミングを解説する教科書です。FreeBSDのベースシステムに含まれるClang/LLVMツールチェインを使用し、x86-64-2をターゲットとしています。AT&T構文を採用しています。

AT&T構文の特徴

[編集]

Intel構文・AT&T構文・Plan9構文の主な違いは以下の通りです:

項目 AT&T構文 Intel構文 Plan9表記
オペランドの順序 ソース, デスティネーション デスティネーション, ソース デスティネーション, ソース
即値の表記 $42 42 $42
レジスタの表記 %rax rax AX
メモリ参照 (%rax) [rax] (AX)
サイズ指定 movq, movl, movw, movb mov MOVQ, MOVL, MOVW, MOVB
オフセット付きメモリ参照 8(%rax) [rax+8] 8(AX)
ベース+インデックス (%rax,%rbx) [rax+rbx] (AX)(BX*1)
スケールファクタ付き (%rax,%rbx,4) [rax+rbx*4] (AX)(BX*4)

各アセンブラ表記の特徴を詳しく説明させていただきます。

AT&T構文は、Unix系システムで広く採用されている表記法です。ソースオペランドを先に記述し、デスティネーションを後に配置する「左から右への」データの流れを表現します。レジスタ名の前には%を付け、即値の前には$を付けることで、オペランドの種類を明示的に識別できます。また、命令のサフィックスによってオペランドのサイズを明確に指定する(例:movq は64ビット転送)という特徴があり、これによってコードの意図が分かりやすくなっています。

Intel構文は、x86プロセッサの公式マニュアルで使用され、Windows環境での開発でも一般的な表記法です。デスティネーションを先に記述し、ソースを後に配置する形式を採用しており、これは多くのプログラミング言語での代入文の記法に近い形となっています。レジスタや即値に特別な記号を付けず、メモリ参照は角括弧[]で囲むシンプルな記法を採用しています。オペランドのサイズは命令自体には明示されず、必要に応じてPTR指定子(BYTE PTR、WORD PTR等)を用います。

Plan9表記は、Bell LabsのPlan 9オペレーティングシステムで導入され、Goの開発でも採用されている表記法です。Intel構文と同様にデスティネーションを先に記述しますが、独自の特徴として全ての命令とレジスタ名を大文字で表記します。メモリ参照の形式はAT&T構文に近いものの、より数学的な表記法を採用しており、特にスケールファクタを使用したメモリアドレッシングでは(AX)(BX*4)のような直感的な記法を用います。即値には$記号を使用しますが、これはAT&T構文とは異なる文脈で使われます。

これら三つの表記法は、それぞれの環境や用途に応じた特徴を持っており、開発者は使用するプラットフォームやツールチェインに応じて適切な表記法を選択する必要があります。特にクロスプラットフォーム開発やシステムレベルのデバッグを行う際には、これらの表記法を相互に理解し、必要に応じて変換できることが重要となります。

AMD64アーキテクチャの基礎

[編集]

x86-64アーキテクチャの歴史と概要

[編集]

x86-64アーキテクチャは、AMDによって開発された64ビットプロセッサアーキテクチャです。2003年に最初のAMD64プロセッサがリリースされて以来、デスクトップからサーバーまで幅広い用途で使用されています。

主な特徴

[編集]
  • 64ビットの一般目的レジスタ
  • 拡張されたアドレス空間(最大で48ビットの物理アドレス空間)
  • 後方互換性(32ビットx86命令セットのサポート)
  • 拡張されたSIMD命令セット

レジスタセット

[編集]

汎用レジスタ

[編集]

AMD64アーキテクチャでは、以下の16個の64ビット汎用レジスタが利用可能です:

レジスタ名 用途 呼び出し規約での役割
%rax アキュムレータ 関数の戻り値
%rbx ベースレジスタ 呼び出し先保存
%rcx カウンタ 第4引数
%rdx データ 第3引数
%rsi ソースインデックス 第2引数
%rdi デスティネーションインデックス 第1引数
%rbp ベースポインタ フレームポインタ
%rsp スタックポインタ スタックポインタ
%r8-r15 拡張レジスタ %r8-%r9は引数、他は汎用

レジスタのサイズ指定:

  • %rax (64ビット) → %eax (32ビット) → %ax (16ビット) → %ah/%al (8ビット)
  • 同様のパターンが%rbx, %rcx, %rdxにも適用

セグメントレジスタ

[編集]

セグメントレジスタは64ビットモードでは限定的な役割を持ちます:

レジスタ 64ビットモードでの用途
%cs コードセグメント
%ds データセグメント(ほとんど使用されない)
%es エクストラセグメント(ほとんど使用されない)
%ss スタックセグメント
%fs スレッドローカルストレージ
%gs スレッドローカルストレージ(カーネル用)

フラグレジスタ

[編集]

%rflagsレジスタには、演算結果やプロセッサの状態が格納されます:

フラグ ビット 説明
CF 0 キャリーフラグ
PF 2 パリティフラグ
AF 4 補助キャリーフラグ
ZF 6 ゼロフラグ
SF 7 符号フラグ
OF 11 オーバーフローフラグ

メモリモデルとアドレッシングモード

[編集]

メモリモデル

[編集]

AMD64は、以下の特徴を持つメモリモデルを採用しています:

  • フラットメモリモデル
  • 48ビット仮想アドレス空間(256TBまで)
  • 4レベルページテーブル
  • 4KBから1GBまでの可変ページサイズ

アドレッシングモード

[編集]

AT&T構文での基本的なアドレッシングモード:

即値アドレッシング
movq $42, %rax          # 即値42をraxに格納
レジスタアドレッシング
movq %rbx, %rax        # rbxの値をraxに格納
直接メモリアドレッシング
movq 0x400000, %rax    # アドレス0x400000の内容をraxに格納
間接メモリアドレッシング
movq (%rbx), %rax      # rbxが指すメモリの内容をraxに格納
ベース + インデックス + スケール + 変位
movq 0x10(%rbx,%rcx,8), %rax  # 複合アドレッシング
%rip相対アドレッシング
movq message(%rip), %rax       # 現在の命令ポインタからの相対アドレスでアクセス
leaq function(%rip), %rax      # 関数アドレスを%ripからの相対で計算
movq var@GOTPCREL(%rip), %rax # GOT経由でグローバル変数にアクセス
PIC (Position Independent Code)と%rip相対アドレッシング
%rip相対アドレッシングは、AMD64アーキテクチャで導入された重要なアドレッシングモードです。このモードでは、現在の命令ポインタ(%rip)からの相対オフセットでデータやコードのアドレスを参照します。基本的な形式は次のようになります:
# AT&T構文での%rip相対アドレッシングの例
    movq    message(%rip), %rax    # messageラベルのアドレスを%ripからの相対で計算
    leaq    function(%rip), %rax   # 関数アドレスを%ripからの相対で計算

.data
message:
    .asciz  "Hello, World\n"

PICは、実行時のベースアドレスに依存せずに正しく動作するコードを生成する手法です。主な利点は以下の通りです:

  1. 共有ライブラリの効率的な実装が可能
  2. Address Space Layout Randomization (ASLR)との親和性が高い
  3. テキストセグメントの共有が容易

具体的な実装例を示します:

    .text
    .globl  pic_example
pic_example:
    pushq   %rbp
    movq    %rsp, %rbp

    # データセクションの値を%rip相対で参照
    movq    static_var(%rip), %rax
    
    # 外部関数の呼び出し(PLT経由)
    callq   external_func@PLT
    
    # グローバル変数へのアクセス(GOT経由)
    movq    global_var@GOTPCREL(%rip), %rax
    movq    (%rax), %rax
    
    popq    %rbp
    ret

    .data
static_var:
    .quad   42

このコードでは、以下のPICの主要な要素が使用されています:

  1. %rip相対アドレッシング:
    • 静的データへの直接アクセス
    • GOTエントリの位置の計算
    • PLTエントリの位置の計算
  2. Global Offset Table (GOT):
    • グローバル変数のアドレスを保持
    • 実行時に動的リンカがアドレスを解決
  3. Procedure Linkage Table (PLT):
    • 外部関数呼び出しの間接ジャンプテーブル
    • 遅延バインディングをサポート

FreeBSDでのPICの特徴:

  1. デフォルトでPICを使用
    # PICの確認
    readelf -d yourlibrary.so | grep TEXTREL
    # 出力がない場合、完全なPICが実現できている
    
  2. コンパイラフラグ:
    clang -fPIC -shared source.c -o library.so
    

注意点:

  1. パフォーマンスへの影響:
    • GOT/PLT経由のアクセスは直接アドレッシングより若干遅い
    • しかし現代のプロセッサではその影響は最小限
  2. デバッグ時の考慮事項:
    • アドレスが実行時に決定されるため、静的解析が複雑になる
    • デバッガでのブレークポイント設定時に注意が必要
  3. セキュリティ上の利点:
    • ASLRとの組み合わせで攻撃の難度が上がる
    • リターン指向プログラミング(ROP)攻撃の防止に貢献
PICと%rip相対アドレッシングは、現代のx86-64システムプログラミングにおいて不可欠な要素となっています。特に共有ライブラリの開発やセキュアなアプリケーションの構築において、これらの理解と適切な使用が重要です。

基本的な命令例

[編集]

データ移動命令

[編集]
命令 説明
movq 64ビット転送 movq $42, %rax
movl 32ビット転送 movl $42, %eax
movw 16ビット転送 movw $42, %ax
movb 8ビット転送 movb $42, %al

算術演算命令

[編集]
命令 説明
addq 64ビット加算 addq $1, %rax
subq 64ビット減算 subq %rbx, %rax
imulq 64ビット符号付き乗算 imulq $2, %rax
idivq 64ビット符号付き除算 idivq %rbx

章末問題

[編集]
  1. 以下のIntel構文のコードをAT&T構文に変換してください:
    mov rax, 42
    add rbx, rax
    shl rbx, 3
    add rax, rbx
    
  2. AT&T構文における各種アドレッシングモードを使用して、メモリアドレス0x1000の内容を%raxに読み込む方法を3通り示してください。
  3. 以下のAT&T構文のコードを実行した後の各レジスタの値を追跡してください:
    movq $100, %rax
    addq $50, %rax
    movq %rax, %rbx
    shrq $2, %rbx
    addq %rbx, %rax
    

FreeBSDにおけるアセンブリ開発環境

[編集]

Clang/LLVM ツールチェイン概要

[編集]

ツールチェインの構成要素

[編集]
コンポーネント コマンド 主な役割
Clangドライバ clang コンパイル処理の統括
Clangでアセンブル clang -nostdlib -x assembler-with-cpp 内部アセンブラでアセンブル
LLVMリンカ ld.lld オブジェクトファイルのリンク
LLDB lldb デバッグ作業

基本的な開発フロー

[編集]
  1. アセンブリソースコード (.s) の作成
  2. アセンブル: clang -c source.s -o object.o
  3. リンク: clang -nostdlib object.o -o program
    • -nostdlibを指定し、標準Cライブラリとスタートアップコードのリンクを抑止
  4. デバッグ(必要な場合): lldb ./program

アセンブラの基本的な使い方

[編集]

ソースファイルの構造

[編集]
    .section .text
    .global _start      # エントリーポイントの定義

_start:
    movq $1, %rax      # システムコール番号 (write)
    movq $1, %rdi      # ファイルディスクリプタ (stdout)
    leaq message(%rip), %rsi # メッセージのアドレス
    movq $13, %rdx     # メッセージの長さ
    syscall            # システムコール呼び出し

    xorq %rdi, %rdi      # 終了コード
    movq $60, %rax     # システムコール番号 (exit)
    syscall

    .section .data
message:
    .ascii "Hello, World\n"

主要なアセンブラディレクティブ

[編集]
ディレクティブ 説明
.section セクションの定義 .section .text
.global グローバルシンボルの定義 .global _start
.align アライメントの設定 .align 8
.ascii ASCII文字列の定義 .ascii "Hello"
.byte バイトデータの定義 .byte 0x42
.long 32ビット整数の定義 .long 42
.quad 64ビット整数の定義 .quad 0x123456789

アセンブラのオプション

[編集]

よく使用するClangオプション

[編集]
オプション 説明
-c オブジェクトファイルの生成
-g デバッグ情報の付加
-O<数字> 最適化レベルの指定
-nostdlib 標準Cライブラリとスタートアップコードのリンクを抑止
-target ターゲットアーキテクチャの指定
-v 詳細な出力の表示

アセンブル時の注意点

[編集]
  • 適切なセクション配置
  • シンボルの可視性管理
  • アライメント要件の遵守
  • 適切なレジスタ使用規約の順守
  • PICコードへの配慮

リンカの使用方法

[編集]

基本的なリンク操作

[編集]
コマンド 説明
clang -o program object.o 単一オブジェクトファイルのリンク
clang -o program obj1.o obj2.o 複数オブジェクトファイルのリンク
clang -o program object.o -lc 標準Cライブラリとのリンク

リンカスクリプト

[編集]
SECTIONS
{
    . = 0x400000;
    .text : { *(.text) }
    .data : { *(.data) }
    .bss  : { *(.bss) }
}

LLDBデバッガの活用

[編集]

基本的なデバッグコマンド

[編集]
コマンド 説明
breakpoint set ブレークポイントの設定 b _start
register read レジスタの内容表示 register read rax
memory read メモリの内容表示 memory read --size 8 --format x --count 4 0x400000
step 1命令実行 s
continue 実行継続 c

デバッグ情報の解析

[編集]
  • バックトレースの表示
  • レジスタ状態の監視
  • メモリ内容の検証
  • 条件付きブレークポイント

実践演習

[編集]

基本的なプログラムの作成

[編集]

以下のプログラムを作成し、ビルドしてみましょう:

    .section .text
    .global main

main:
    pushq %rbp
    movq %rsp, %rbp

    # 数値を加算
    movq $10, %rax
    addq $20, %rax

    # 結果を返す
    movq %rbp, %rsp
    popq %rbp
    ret

    .section .data
    # データセクションは空

デバッグ演習

[編集]
  1. プログラムにブレークポイントを設定
  2. レジスタの値を確認
  3. メモリの内容を表示
  4. ステップ実行で動作を確認

章末問題

[編集]
  1. 以下のプログラムをアセンブル・リンクし、実行してください:
        .section .text
        .global main
    
    main:
        # ここにコードを書いてください
        # 2つの数値を加算し、結果を返すプログラム
    
  2. LLDBを使用して以下の操作を行ってください:
    • mainラベルにブレークポイントを設定
    • ステップ実行でレジスタの変化を確認
    • スタックの内容を表示
  3. 次のエラーメッセージの原因と解決方法を説明してください:
    • "undefined reference to main"
    • "can't resolve symbol"
    • "segmentation fault"
C/C++/Rustなどの高級言語があるのにFreeBSD/AMD64のアセンブリレベルのプログラミングを学ぶ意味と意義
FreeBSD/AMD64のアセンブリレベルのプログラミングを学ぶ意味と意義は、以下の点にあります:
コンピュータアーキテクチャの理解を深める
アセンブリ言語を学ぶことは、コンピュータの動作原理を深く理解する助けになります。高級言語で書かれたコードがどのようにCPUに変換され、実行されるのかを理解することで、システムの性能や動作をより効率的に最適化する能力が身につきます。特に、AMD64のような64ビットアーキテクチャにおけるレジスタやメモリ管理の仕組みを知ることは、ハードウェアに密接に関連するシステム開発や最適化に役立ちます。
パフォーマンス向上
アセンブリ言語は、特定のハードウェアやアーキテクチャに最適化されたコードを書くことが可能です。高級言語では抽象化されている多くの処理が、アセンブリで書くことで直接的に制御でき、特にパフォーマンスが求められる場面(リアルタイムシステムや組み込みシステムなど)で重要になります。例えば、ハードウェア制御や高効率なアルゴリズムをアセンブリで実装することで、パフォーマンスの向上が図れます。
OSやドライバの開発
FreeBSDのようなOSの開発やドライバ開発では、ハードウェアとの直接的なやり取りが求められる場面があります。アセンブリを理解していないと、これらの開発において重要な細かい部分にアクセスすることができません。例えば、カーネルの一部や低レベルのシステムコール、割り込み処理、メモリ管理の最適化を行う際には、アセンブリ言語が不可欠です。
ハードウェアとの直接的なインタラクション
アセンブリ言語を用いることで、ハードウェアの動作を直接的に制御できます。特定の命令セットに精通することは、システム全体を理解する上で非常に有用です。ハードウェアの動作を直接理解し、必要に応じて最適化やデバッグを行うことができるため、システム開発者としてのスキルを高めることができます。
低レベルプログラミングのスキルを磨く
高級言語(C/C++/Rust)を使っていると、メモリ管理やプロセッサの動作に関して多くの抽象化がなされます。しかし、アセンブリを使うことで、メモリの配置、レジスタ操作、スタックの管理、CPU命令の最適化など、コンピュータの低レベルでの動作に対する理解が深まります。この知識は、どのように高級言語が実行されるのか、またどのように最適化できるのかを学ぶ上で非常に有益です。
デバッグやトラブルシューティング能力の向上
アセンブリ言語の知識があれば、バイナリコードや低レベルのデバッグ時に役立ちます。プログラムが期待通りに動作しない場合でも、アセンブリのコードを理解していれば、問題の原因を特定しやすくなります。デバッグツール(gdbなど)を使用した際に、アセンブリコードレベルでの確認ができることで、より深い理解と問題解決能力が養われます。
歴史的な知識としての価値
アセンブリ言語は、コンピュータサイエンスの発展における基盤的な知識です。特に、古いシステムやレガシーなハードウェアとの互換性を維持するためには、アセンブリの知識が必要です。また、現在のコンピュータの設計やプログラミングの手法は、過去のアセンブリ言語を基に進化してきたため、その歴史を理解することは、今後の技術の発展を理解するうえでも重要です。
高級言語が進化し、システム開発が抽象化されてきている現代においても、FreeBSD/AMD64のアセンブリレベルのプログラミングを学ぶことには深い意味があります。システム全体の理解やパフォーマンス最適化、低レベルプログラミングのスキル向上に繋がり、システムアーキテクチャやコンピュータサイエンスの基礎的な部分を強化する助けになります。

参考文献

[編集]
  • LLVM Documentation
  • FreeBSD Assembly Language Programming
  • LLDB Command Reference
  • System V AMD64 ABI

基本的な命令セット

[編集]

データ転送命令

[編集]

基本的な転送命令

[編集]
命令 説明 動作
movq 64ビット転送 movq $42, %rax %rax ← 42
movl 32ビット転送 movl $42, %eax %eax ← 42, 上位32ビットをクリア
movw 16ビット転送 movw $42, %ax %ax ← 42
movb 8ビット転送 movb $42, %al %al ← 42
movabsq 64ビット即値転送 movabsq $0x1234567890ABCDEF, %rax 64ビット即値を転送

スタック操作命令

[編集]
命令 説明
pushq スタックへの格納 pushq %rax
popq スタックからの復帰 popq %rax
pushfq フラグのプッシュ pushfq
popfq フラグのポップ popfq

転送命令の使用例

[編集]
    # レジスタ間転送
    movq %rax, %rbx      # %rax → %rbx
    
    # メモリからレジスタへの転送
    movq (%rax), %rbx    # [%rax] → %rbx
    
    # レジスタからメモリへの転送
    movq %rax, (%rbx)    # %rax → [%rbx]
    
    # 即値からレジスタへの転送
    movq $42, %rax       # 42 → %rax
    
    # スケールドインデックス付き転送
    movq 8(%rax,%rcx,4), %rdx  # [%rax + %rcx*4 + 8] → %rdx

算術演算命令

[編集]

基本的な算術命令

[編集]
命令 説明 フラグ影響
addq 加算 addq $1, %rax OF, SF, ZF, CF, PF
subq 減算 subq %rbx, %rax OF, SF, ZF, CF, PF
imulq 符号付き乗算 imulq $2, %rax OF, CF
idivq 符号付き除算 idivq %rbx なし
incq インクリメント incq %rax OF, SF, ZF, PF
decq デクリメント decq %rax OF, SF, ZF, PF
negq 2の補数否定 negq %rax OF, SF, ZF, CF, PF

算術演算の例

[編集]
    # 基本的な加算
    movq $10, %rax      # %rax ← 10
    addq $5, %rax       # %rax ← %rax + 5
    
    # 乗算の例(128ビット結果)
    movq $1000, %rax    # 被乗数を%raxに
    imulq $50           # %rdx:%rax ← %rax * 50
    
    # 除算の例(%rdx:%rax を %rbxで除算)
    movq $0, %rdx       # 上位64ビットをクリア
    movq $100, %rax     # 被除数を設定
    movq $3, %rbx       # 除数を設定
    idivq %rbx          # %rax ← 商, %rdx ← 余り

論理演算命令

[編集]

基本的な論理命令

[編集]
命令 説明 フラグ影響
andq 論理積 andq $0xF, %rax OF←0, SF, ZF, PF, CF←0
orq 論理和 orq $0xF0, %rax OF←0, SF, ZF, PF, CF←0
xorq 排他的論理和 xorq %rax, %rax OF←0, SF, ZF, PF, CF←0
notq ビット反転 notq %rax なし

シフト・ローテート命令

[編集]
命令 説明 フラグ影響
shlq 左シフト shlq $1, %rax OF, SF, ZF, PF, CF
shrq 右シフト(論理) shrq $1, %rax OF, SF, ZF, PF, CF
sarq 右シフト(算術) sarq $1, %rax OF, SF, ZF, PF, CF
rolq 左ローテート rolq $1, %rax OF, CF
rorq 右ローテート rorq $1, %rax OF, CF

比較・分岐命令

[編集]

比較命令

[編集]
命令 説明 フラグ影響
cmpq 比較 cmpq $42, %rax OF, SF, ZF, CF, PF
testq ビットテスト testq $1, %rax OF←0, SF, ZF, PF, CF←0

条件分岐命令

[編集]
命令 条件 フラグ条件
je/jz 等しい ZF=1
jne/jnz 等しくない ZF=0
jl/jnge より小さい(符号付き) SF≠OF
jle/jng 以下(符号付き) ZF=1 or SF≠OF
jg/jnle より大きい(符号付き) ZF=0 and SF=OF
jge/jnl 以上(符号付き) SF=OF
jb/jnae より小さい(符号なし) CF=1
jbe/jna 以下(符号なし) CF=1 or ZF=1

分岐命令の使用例

[編集]
    # 数値の比較
    movq $10, %rax
    cmpq $5, %rax       # %rax - 5 を計算してフラグを設定
    jg greater_than     # %rax > 5 なら分岐
    
    # ゼロチェック
    testq %rax, %rax    # %rax AND %rax
    jz is_zero         # %rax = 0 なら分岐

greater_than:
    # %rax > 5 の場合の処理

is_zero:
    # %rax = 0 の場合の処理

章末問題

[編集]
  1. 以下のコードの実行後の%raxの値を求めてください:
        movq $100, %rax
        addq $50, %rax
        shlq $2, %rax
        subq $25, %rax
    
  2. 次のコードを完成させ、2つの数の最大値を求めてください:
        movq $42, %rax
        movq $67, %rbx
        # ここにコードを追加
    
  3. 以下の条件分岐をAT&T構文で実装してください:
       if (x > 0) { x++; } else { x--; }
    
    ただし、xの値は%raxに格納されているものとします。

参考文献

[編集]
  • AMD64 Architecture Programmer's Manual Volume 3: General-Purpose and System Instructions
  • System V AMD64 ABI
  • Intel® 64 and IA-32 Architectures Software Developer's Manual

関数呼び出しと制御フロー

[編集]

System V AMD64 ABI

[編集]

AMD64アーキテクチャにおけるSystem V ABIは、Unix系システムでの標準的な呼び出し規約です。

レジスタの使用規約

[編集]
分類 レジスタ 保存責任 用途と注意点
引数渡し %rdi, %rsi, %rdx, %rcx, %r8, %r9 呼び出し側 整数およびポインタ引数用。順序は固定
浮動小数点引数 %xmm0-%xmm7 呼び出し側 浮動小数点数の引数渡しに使用
戻り値 %rax, %rdx 呼び出し側 %raxは整数/ポインタ、%rdxは必要な場合に使用
一時レジスタ %r10, %r11 呼び出し側 システムコールで%rcxの代わりに%r10を使用
保存レジスタ %rbx, %rbp, %r12-%r15 呼び出される側 関数内で使用する場合は必ず保存と復元が必要
スタックポインタ %rsp 特別 16バイトアラインメントを常に維持する必要がある

データ型のサイズと整列

[編集]
データ型 サイズ(バイト) 整列要件
char 1 1
short 2 2
int 4 4
long 8 8
long long 8 8
pointer 8 8
float 4 4
double 8 8
long double 16 16

引数の受け渡し規則

[編集]
整数/ポインタ引数の処理:
example_function:
    # %rdi: 第1引数
    # %rsi: 第2引数
    # %rdx: 第3引数
    # %rcx: 第4引数
    # %r8:  第5引数
    # %r9:  第6引数
    
    # 7番目以降の引数はスタックから取得
    movq 8(%rbp), %rax   # 第7引数
    movq 16(%rbp), %rax  # 第8引数
浮動小数点引数の処理:
float_function:
    # %xmm0: 第1引数(float/double)
    # %xmm1: 第2引数
    # %xmm2: 第3引数
    # %xmm3: 第4引数
    # %xmm4: 第5引数
    # %xmm5: 第6引数
    # %xmm6: 第7引数
    # %xmm7: 第8引数
    
    # 9番目以降の浮動小数点引数はスタックから取得

関数呼び出し時のスタックアライメント

[編集]
16バイトアライメント要件
function_setup:
    pushq %rbp              # スタックは8バイト減少
    movq %rsp, %rbp
    subq $16, %rsp         # ローカル変数用に16バイト確保
    andq $-16, %rsp        # 16バイトアライメントの強制
    
    # この時点でスタックは16バイトアラインされている
SIMD命令のためのアライメント
simd_function:
    pushq %rbp
    movq %rsp, %rbp
    subq $32, %rsp         # SSE/AVX命令用の32バイト確保
    andq $-32, %rsp        # 32バイトアライメントの強制
    
    # SIMD操作
    movaps %xmm0, (%rsp)   # アライメント済みメモリアクセス

スタックフレームの動的管理

[編集]

可変長配列のための動的スタック確保

[編集]
dynamic_array:
    pushq %rbp
    movq %rsp, %rbp
    
    # 配列サイズを計算(例:第1引数 * 8バイト)
    movq %rdi, %rax
    shlq $3, %rax          # サイズ * 8
    
    # スタックサイズの調整
    subq %rax, %rsp
    andq $-16, %rsp        # 16バイトアライメント
    
    # これ以降、-8(%rbp, %rdi, 8)でアクセス可能

例外処理のためのスタック管理

[編集]
try_block:
    pushq %rbp
    movq %rsp, %rbp
    subq $32, %rsp         # 例外ハンドリング情報用
    
    # 例外ハンドラ情報の設定
    leaq exception_handler(%rip), %rax
    movq %rax, 8(%rsp)
    
    # 保護された処理
    # ...
    
exception_handler:
    # 例外処理
    # ...

システムコール規約

[編集]

システムコール番号とレジスタ割り当て

[編集]
レジスタ システムコール時の用途
%rax システムコール番号、戻り値
%rdi 第1引数
%rsi 第2引数
%rdx 第3引数
%r10 第4引数(%rcxの代わり)
%r8 第5引数
%r9 第6引数

システムコール例

[編集]
    # write(1, "Hello\n", 6) システムコール
    movq $1, %rax          # システムコール番号(write)
    movq $1, %rdi          # ファイルディスクリプタ(標準出力)
    leaq message(%rip), %rsi # バッファアドレス
    movq $6, %rdx          # バッファ長
    syscall               # システムコール呼び出し
    
    # エラーチェック
    cmpq $0, %rax
    jl error_handler      # 負の値はエラー
    
.data
message:
    .ascii "Hello\n"

例外処理の基礎

[編集]

基本的な例外処理メカニズム

[編集]
命令 説明
ud2 無効オペコード例外の発生
int3 ブレークポイント例外の発生
int $N ソフトウェア割り込みの発生

try-catchの実装例

[編集]
    .section .data
exception_handler:
    .quad 0          # 例外ハンドラのアドレス

    .section .text
    # 例外ハンドラの設定
    leaq handler, %rax
    movq %rax, exception_handler(%rip)
    
    # try ブロック
    # ... 通常のコード ...
    
    jmp end_try     # 正常終了時
    
handler:
    # 例外処理コード
    # ... 
    
end_try:
    # 続行

章末問題

[編集]
  1. 以下の再帰関数をアセンブリで実装してください:
    int fib(int n) {
        if (n <= 1) return n;
        return fib(n-1) + fib(n-2);
    }
    
  2. 次のC関数をSystem V AMD64 ABIに従ってアセンブリに変換してください:
    int max_of_three(int a, int b, int c) {
        if (a > b) {
            return (a > c) ? a : c;
        } else {
            return (b > c) ? b : c;
        }
    }
    
  3. スタックフレームのトレース機能を実装してください。各関数呼び出しで以下の情報を表示します:
    • 現在の関数名
    • 呼び出し元のアドレス
    • 引数の値
    • ローカル変数の数

参考文献

[編集]
  • System V AMD64 ABI Reference
  • AMD64 Architecture Programmer's Manual Volume 2: System Programming
  • FreeBSD Developer's Handbook - Chapter 8: X86 Assembly Language Programming
  • DWARF Debugging Information Format Version 5

SIMD命令とベクトル処理

[編集]

SIMD(Single Instruction, Multiple Data)命令ベクトル処理は、並列計算を実現するための技術です。これらは、複数のデータ要素を一度に同時に処理する方法を提供し、特に大規模なデータセットを扱うアプリケーションにおいて大きな性能向上をもたらします。

SSE/SSE2命令セット

[編集]

SSE(Streaming SIMD Extensions)およびSSE2(Streaming SIMD Extensions 2)は、Intelによって導入されたSIMD(Single Instruction, Multiple Data)命令セットで、CPUの並列処理能力を活かして高速なデータ処理を実現するためのものです。これらは、特にメディア処理や数値計算、データ解析などで大きなパフォーマンス向上を提供します。

XMMレジスタの基礎

[編集]

XMMレジスタ(%xmm0〜%xmm15)は128ビット幅のSIMDレジスタです。これらは以下のデータ型を扱えます:

  • 4つの単精度浮動小数点数(32ビット×4)
  • 2つの倍精度浮動小数点数(64ビット×2)
  • 16個の8ビット整数
  • 8個の16ビット整数
  • 4個の32ビット整数
  • 2個の64ビット整数

基本的なSSE命令

[編集]
単精度浮動小数点演算
# 4つの単精度浮動小数点数の加算
    movaps  (%rdi), %xmm0   # メモリから4つの値を読み込み
    addps   (%rsi), %xmm0   # 4つの値を同時に加算
    movaps  %xmm0, (%rdx)   # 結果をメモリに書き込み
整数演算
# 8個の16ビット整数の同時加算
    movdqu  (%rdi), %xmm0   # アラインメントされていないメモリからロード
    paddw   (%rsi), %xmm0   # パックド加算(16ビット×8)
    movdqu  %xmm0, (%rdx)   # 結果を保存

AVX/AVX2命令セット

[編集]

YMMレジスタの拡張

[編集]

AVXで導入されたYMMレジスタ(%ymm0〜%ymm15)は256ビット幅を持ち、SSEの機能を2倍に拡張します:

  • 8つの単精度浮動小数点数
  • 4つの倍精度浮動小数点数
  • 32個の8ビット整数
  • 16個の16ビット整数
  • 8個の32ビット整数
  • 4個の64ビット整数

VEX命令プレフィックス

[編集]

AVX命令は3オペランド形式をサポートし、デスティネーションレジスタを保持したまま演算が可能です:

# 3オペランド形式の浮動小数点乗算
    vfmadd231ps (%rdi), %ymm1, %ymm0  # %ymm0 = %ymm0 + (%ymm1 * (%rdi))

SIMD命令の最適化テクニック

[編集]

データアライメント

[編集]
  • メモリアクセスは16バイト(SSE)または32バイト(AVX)境界にアラインすることで最適なパフォーマンスが得られます
  • アラインメントの確認と強制:
.section .data
.align 32
vector_data: .zero 32  # 32バイト境界にアライン

SIMDベクトル化のパターン

[編集]
ループのベクトル化
/* float a[1024], b[1024], c[1024];
   for(int i==0; i<1024; i++) c[i] == a[i] + b[i]; */

    movq    $1024, %rcx
    xorq    %rax, %rax
.loop:
    vmovups (%rdi,%rax), %ymm0      # a[i]からロード
    vaddps  (%rsi,%rax), %ymm0, %ymm0   # b[i]を加算
    vmovups %ymm0, (%rdx,%rax)      # c[i]に保存
    addq    $32, %rax               # 8要素ずつ処理
    subq    $8, %rcx
    jnz     .loop

SSE/SSE2命令セットの実践

[編集]

XMMレジスタを使用したSIMD処理では、データ型に応じて異なるアプローチが必要です。例えば、画像処理で良く使用される8ビット整数の並列処理を見てみましょう:

# RGBデータの輝度調整(すべての色成分を1.5倍)
    # 定数の準備(1.5をパックド形式で用意)
    movaps xmm1, [scale_factor]  # scale_factor: {1.5, 1.5, 1.5, 1.5}
    
    # 16バイトのRGBデータを処理
.loop:
    movdqu xmm0, [rdi]          # RGBデータをロード(アラインメント不要)
    punpcklbw xmm2, xmm0        # 下位8バイトを16ビットに展開
    punpckhbw xmm3, xmm0        # 上位8バイトを16ビットに展開
    
    # 浮動小数点に変換して乗算
    cvtdq2ps xmm2, xmm2
    cvtdq2ps xmm3, xmm3
    mulps xmm2, xmm1
    mulps xmm3, xmm1
    
    # 整数に戻してパック
    cvtps2dq xmm2, xmm2
    cvtps2dq xmm3, xmm3
    packuswb xmm2, xmm3         # 結果を8ビットにパック
    
    movdqu [rsi], xmm2          # 結果を保存
    add rdi, 16                 # 次の16バイト
    add rsi, 16
    dec rcx                     # カウンタを減算
    jnz .loop

このコードでは、RGBデータの各色成分を1.5倍に調整しています。8ビットデータを16ビットに展開し、浮動小数点演算を行った後、再度8ビットにパックする一連の処理を示しています。

次に、科学技術計算でよく使用する行列の乗算処理の例を見てみましょう:

# 4x4行列の乗算(単精度浮動小数点)
matrix_multiply:
    # rdi: 行列A、rsi: 行列B、rdx: 結果行列C
    xor ecx, ecx            # 行カウンタ
.row_loop:
    xor ebx, ebx           # 列カウンタ
.col_loop:
    vxorps ymm0, ymm0      # 結果の累積用
    xor eax, eax           # 要素カウンタ
.elem_loop:
    # 1行の要素と1列の要素の積の累積を計算
    vmovups ymm1, [rdi + rax * 4]    # 行列Aの1行をロード
    vbroadcastss ymm2, [rsi + rbx]   # 行列Bの要素をブロードキャスト
    vfmadd231ps ymm0, ymm1, ymm2     # 積を累積
    
    add rax, 8                        # 次の8要素
    cmp rax, 32                       # 32要素処理したか?
    jl .elem_loop
    
    vmovups [rdx + rbx], ymm0        # 結果を保存
    add rbx, 32                       # 次の列へ
    cmp rbx, 128                      # 4列処理したか?
    jl .col_loop
    
    add rdi, 128                      # 次の行へ
    add rcx, 1
    cmp rcx, 4                        # 4行処理したか?
    jl .row_loop
    ret

AVX命令を使用したこの行列乗算では、vfmadd231ps命令を使用して積和演算を効率的に行っています。vbroadcastss命令により、行列Bの要素を効率的に複製して並列計算を実現しています。

データのアライメントが重要な場合、以下のようにアライメントを確認して処理を分岐させることができます:

# アライメントを考慮したメモリコピー
aligned_copy:
    mov rax, rdi                  # ソースアドレス
    and rax, 0x1F                # 32バイトアライメントをチェック
    jz .aligned_loop             # アラインされていれば高速ループへ
    
    # アラインされていない部分を1バイトずつコピー
.unaligned_loop:
    mov al, [rdi]
    mov [rsi], al
    inc rdi
    inc rsi
    dec rax                      # アライメントまでの残りバイト数
    jnz .unaligned_loop
    
.aligned_loop:
    vmovaps ymm0, [rdi]         # アラインされたロード
    vmovaps [rsi], ymm0         # アラインされた保存
    add rdi, 32
    add rsi, 32
    sub rcx, 32
    jnz .aligned_loop
    ret

このように、実際のコードでは、アライメントの処理、データ型の変換、効率的なループ構造の実装など、多くの要素を考慮する必要があります。特にSIMD命令を使用する際は、データの配置とアクセスパターンが性能に大きく影響します。

AVX/AVX2での拡張された機能を活用する例として、複数のデータ型を同時に処理する場合を見てみましょう:

# 整数と浮動小数点の混在処理
mixed_calculation:
    # 整数データの処理
    vpmulld ymm0, ymm1, ymm2    # 8個の32ビット整数の乗算
    
    # 整数から浮動小数点への変換
    vcvtdq2ps ymm3, ymm0
    
    # 浮動小数点演算
    vaddps ymm3, ymm3, [rdi]    # メモリからロードして加算
    
    # 結果の保存(アラインメント要)
    vmovaps [rsi], ymm3

これらの例は、SIMD命令を使用した実際の処理の流れを示しています。命令の選択、データの配置、処理の順序など、多くの要素を適切に組み合わせることで、効率的なベクトル処理を実現できます。

システムプログラミング

[編集]

FreeBSDシステムコール

[編集]

システムコールの基本

[編集]

FreeBSD/AMD64ではシステムコールはsyscall命令を使用して呼び出します:

システムコールは単純なファイル操作から複雑なプロセス管理まで、さまざまな用途に使用されます。以下に代表的な例を示します。

ファイル操作の例
[編集]
# ファイルを開き、データを書き込み、閉じる例
.section .data
    filename: .string "output.txt"
    content:  .string "Hello, FreeBSD!\n"
    content_len = . - content
    
.section .text
.global _start
_start:
    # open(filename, O_WRONLY | O_CREAT, 0644)
    movq    $5, %rax             # open syscall
    leaq    filename(%rip), %rdi # ファイル名
    movq    $0x601, %rsi         # O_WRONLY | O_CREAT
    movq    $0644, %rdx          # パーミッション
    syscall
    
    # エラーチェック
    testq   %rax, %rax
    js      error_exit
    
    # ファイルディスクリプタを保存
    movq    %rax, %r12
    
    # write(fd, content, content_len)
    movq    $4, %rax            # write syscall
    movq    %r12, %rdi          # fd
    leaq    content(%rip), %rsi # バッファ
    movq    $content_len, %rdx  # 長さ
    syscall
    
    # close(fd)
    movq    $6, %rax            # close syscall
    movq    %r12, %rdi          # fd
    syscall
    
    # 正常終了
    movq    $1, %rax            # exit syscall
    xorq    %rdi, %rdi          # status = 0
    syscall

error_exit:
    movq    $1, %rax            # exit syscall
    movq    $1, %rdi            # status = 1
    syscall
高度なメモリ管理の例
[編集]

メモリマッピングを使用してファイルを効率的に読み込む例を示します:

# ファイルをメモリにマップして処理する例
.section .data
    filename: .string "data.bin"
    
.section .text
.global _start
_start:
    # まずファイルを開く
    movq    $5, %rax            # open syscall
    leaq    filename(%rip), %rdi
    movq    $0, %rsi            # O_RDONLY
    syscall
    
    testq   %rax, %rax
    js      error_exit
    movq    %rax, %r12          # FDを保存
    
    # ファイルサイズを取得
    movq    $497, %rax          # fstat syscall
    movq    %r12, %rdi
    subq    $144, %rsp          # struct stat用のスペース
    movq    %rsp, %rsi
    syscall
    
    # ファイルサイズを取得(st_size)
    movq    48(%rsp), %r13      # サイズを保存
    
    # mmapでマッピング
    movq    $477, %rax          # mmap syscall
    xorq    %rdi, %rdi          # NULL
    movq    %r13, %rsi          # length
    movq    $1, %rdx            # PROT_READ
    movq    $2, %r10            # MAP_PRIVATE
    movq    %r12, %r8           # fd
    xorq    %r9, %r9            # offset
    syscall
    
    # マップアドレスを保存
    movq    %rax, %r14
    
    # ここでメモリマップされたデータを処理
    # ...
    
    # 後片付け
    movq    $73, %rax           # munmap syscall
    movq    %r14, %rdi          # addr
    movq    %r13, %rsi          # length
    syscall
    
    movq    $6, %rax            # close syscall
    movq    %r12, %rdi
    syscall
プロセス管理の実践例
[編集]

fork, execを使用したプロセス生成例:

# 新しいプロセスを生成してコマンドを実行
.section .data
    command:    .string "/bin/ls"
    arg1:       .string "-l"
    args:       .quad command, arg1, 0
    env:        .quad 0          # 環境変数なし
    
.section .text
.global _start
_start:
    # fork()を実行
    movq    $2, %rax            # fork syscall
    syscall
    
    testq   %rax, %rax
    js      error_exit          # エラー時
    jz      child_proc          # 子プロセス時
    
parent_proc:
    # 親プロセスは子の終了を待つ
    movq    $7, %rax            # wait4 syscall
    movq    $-1, %rdi           # any child
    xorq    %rsi, %rsi          # status = NULL
    xorq    %rdx, %rdx          # options = 0
    xorq    %r10, %r10          # rusage = NULL
    syscall
    jmp     exit_success
    
child_proc:
    # 子プロセスは新しいプログラムを実行
    movq    $59, %rax           # execve syscall
    leaq    command(%rip), %rdi # pathname
    leaq    args(%rip), %rsi    # argv
    leaq    env(%rip), %rdx     # envp
    syscall
    
    # execveが返ってきた場合はエラー
    jmp     error_exit
シグナルハンドリングの実装例
[編集]

より詳細なシグナルハンドラの実装:

.section .data
    sigaction:
        .quad   signal_handler  # sa_handler
        .quad   0x04000000     # SA_RESTART
        .zero   128            # sa_mask (シグナルマスク)
        
    msg:    .string "Signal caught!\n"
    msg_len = . - msg
    
.section .text
.global _start
_start:
    # シグナルハンドラを設定
    movq    $416, %rax          # sigaction syscall
    movq    $2, %rdi            # SIGINT
    leaq    sigaction(%rip), %rsi
    xorq    %rdx, %rdx
    syscall
    
    # メインループ
main_loop:
    # なにか処理
    jmp     main_loop
    
signal_handler:
    # シグナルハンドラのコンテキストを保存
    pushq   %rax
    pushq   %rdi
    pushq   %rsi
    pushq   %rdx
    
    # write(1, msg, msg_len)
    movq    $4, %rax
    movq    $1, %rdi
    leaq    msg(%rip), %rsi
    movq    $msg_len, %rdx
    syscall
    
    # コンテキストを復元
    popq    %rdx
    popq    %rsi
    popq    %rdi
    popq    %rax
    ret

割り込みハンドリング

[編集]

割り込みハンドラの設定

[編集]
sigaction構造体の設定
.section .data
sigaction:
    .quad   handler         # sa_handler
    .quad   0              # sa_flags
    .quad   0              # sa_mask
    
.text
    movq    $416, %rax      # sigaction syscall番号
    movq    $2, %rdi        # SIGINT
    leaq    sigaction(%rip), %rsi # 新しいハンドラ
    xorq    %rdx, %rdx      # 古いハンドラ(不要)
    syscall
FreeBSDでアセンブリ言語を学ぶ理由について
なぜGNU/LinuxなどではなくFreeBSDを選ぶ意義を改めて説明します。以下に、FreeBSDがアセンブリ学習において優れている点をまとめます。
シンプルで一貫したツールチェイン
FreeBSDは、非常に統一された開発環境を提供しています。ベースシステムにClang/LLVMコンパイラが含まれており、これを使用してソースコードをコンパイルするプロセスは非常にスムーズで、一貫性があります。これに対し、Linuxは多くのディストリビューションがあり、それぞれ異なるツールチェインを使用することがあり、環境構築に差異が生じる可能性があります。FreeBSDではツールチェインが標準化されているため、特に低レベルなプログラミングやアセンブリ学習において、セットアップが簡単で安定しています。
システムの簡潔さと安定性
FreeBSDは非常にシンプルで直感的なシステム設計を採用しています。ファイルシステムやカーネルの設計が洗練されており、Linuxに比べて学習の障害となる複雑な要素が少ないです。これにより、アセンブリやシステムプログラミングに集中できる環境が整っています。Linuxでは、カーネルやシステム全体が多様であるため、初心者がどこから手をつけていいのか迷うことがあるかもしれません。
シンプルなカーネルとモジュール管理
FreeBSDはシンプルなカーネル構造を持ち、カーネルモジュールやシステムの設定がLinuxよりも直感的に管理できます。特にカーネルやシステムの挙動を理解するために、アセンブリやCで直接操作する際に、システム全体の仕組みが分かりやすくなります。Linuxではカーネルやデバイスドライバなどの部分での複雑性が高く、これが学習の障害になることがあります。
パフォーマンスの最適化
FreeBSDは、高いパフォーマンスが要求される環境向けに最適化されており、特にネットワークやI/O処理において優れた性能を発揮します。このパフォーマンスの背後にあるのが、システム全体の低レベルな最適化です。アセンブリを学ぶことによって、この最適化がどのように行われているのかを深く理解できます。FreeBSDのシステム設計に従って学習することで、パフォーマンスや効率の向上に関する知識を得ることができます。
標準的なBSDライセンス
FreeBSDはBSDライセンスを採用しており、ソースコードの自由な利用と改変が許可されています。このライセンスの自由度により、学習者は自由にシステムを変更し、アセンブリコードを試すことができます。Linuxの多くはGPLライセンスに基づいており、商用利用やコードの再利用に制約があるため、学習目的でも若干の制限を感じることがあります。
完全にオープンなドキュメンテーションとコミュニティ
FreeBSDのドキュメントは非常に整備されており、アセンブリやシステムプログラミングに関するリソースも充実しています。FreeBSDは特に開発者のための資料が多く、ユーザーと開発者の間で密接なコミュニケーションが行われています。この点で、アセンブリ学習者にとっても、質問やトラブルシューティングが非常に効率的に行えます。Linuxも同様に大きなコミュニティがありますが、フレームワークやディストリビューションごとの差異が大きいため、環境に依存した問題に直面することがあります。
低レベルプログラミングの理解
FreeBSDの設計自体が非常に低レベルであり、システムの挙動を理解するためにアセンブリやCが不可欠です。これにより、学習者はアセンブリ言語を用いて、システムがどのように動作しているのか、どうやってメモリを管理し、CPUを制御しているのかについて深い理解を得ることができます。Linuxでも低レベルプログラミングは可能ですが、FreeBSDの方がよりシンプルでストレートに学べます。
FreeBSDでアセンブリ言語を学ぶことは、シンプルで統一された環境を提供し、パフォーマンス最適化やシステム設計を深く理解するための有益な選択です。特に、システム全体の設計がシンプルで直感的なため、低レベルプログラミングの学習に集中しやすいというメリットがあります。Linuxも強力なプラットフォームですが、FreeBSDの環境で学ぶことで、アセンブリ言語を用いたシステムの理解がより深まります。

最適化とパフォーマンスチューニング

[編集]

パイプラインと命令レイテンシ

[編集]

命令スケジューリング

[編集]
パイプライン最適化の例
# 不適切な例(依存関係による停止)
    movq    (%rdi), %rax
    addq    $1, %rax
    movq    %rax, (%rdi)
    movq    (%rdi), %rbx      # %raxへの依存で待機

# 最適化例(命令の並べ替え)
    movq    (%rdi), %rax
    movq    (%rsi), %rbx      # 依存のない処理を挟む
    addq    $1, %rax
    movq    %rax, (%rdi)

命令スケジューリングとは、命令が依存関係に従って実行される順序を最適化することです。上記の最適化例では、依存関係のない命令(movq (%rsi), %rbx)を加えることで、処理が並列に実行され、パイプラインの停止を避けることができます。

メモリアクセスの最適化

[編集]
アライメントとキャッシュライン
# 効率の悪いアクセスパターン
    movl    (%rdi), %eax
    movl    4096(%rdi), %ebx  # 新しいキャッシュライン

# 効率的なアクセスパターン
    movl    (%rdi), %eax
    movl    4(%rdi), %ebx     # 同一キャッシュライン内

メモリアクセスの最適化では、データのアライメントやキャッシュラインの効率的な利用が重要です。新しいキャッシュラインへのアクセスを最小化し、同じキャッシュライン内でデータをアクセスすることで、キャッシュミスを減らし、メモリアクセスのパフォーマンスを向上させます。

SIMD演算の最適化

[編集]

ベクトル化の例

[編集]
配列の要素ごとの乗算
# float c[8] = a[8] * b[8]の計算
    vmovups (%rdi), %ymm0          # a[0:7]をロード
    vmulps  (%rsi), %ymm0, %ymm0   # b[0:7]との乗算
    vmovups %ymm0, (%rdx)          # 結果を保存

ベクトル化は、複数のデータ要素に対して同時に演算を行う技術で、SIMD命令を使用してデータ並列性を最大化します。上記の例では、vmovupsvmulps を使用して、ab の配列を並列に乗算し、結果をcに保存しています。このようにして、演算を効率的に並列化することができます。

データパッキング

[編集]
16ビット整数の圧縮
    vpackssdw %ymm1, %ymm0, %ymm2  # 32ビット→16ビット
    vpacksswb %ymm3, %ymm2, %ymm4  # 16ビット→8ビット

データパッキングは、複数のデータを密に詰め込むことでメモリ効率を高め、演算を最適化する技術です。上記の例では、vpackssdwvpacksswb を使用して、32ビットと16ビットの整数をそれぞれ16ビットと8ビットに圧縮しています。このようにして、メモリ帯域を節約し、演算のパフォーマンスを向上させることができます。

実践的なプログラミング例

[編集]

文字列処理

[編集]

基本的な文字列操作

[編集]

FreeBSDのAMD64環境における文字列処理の基本を解説します。

文字列の長さを求める例
    .text
    .globl strlen
strlen:
    pushq   %rbp
    movq    %rsp, %rbp
    xorq    %rax, %rax    # カウンタを0で初期化
    
.loop:
    cmpb    $0, (%rdi)    # null終端をチェック
    je      .done
    incq    %rax          # カウンタをインクリメント
    incq    %rdi          # 次の文字へ
    jmp     .loop
    
.done:
    popq    %rbp
    ret

SSE/AVX命令を使用した高速化

[編集]

SSE2のPCMPISTRI命令を使用した高速な文字列検索の実装例を示します。

    .text
    .globl strstr_sse2
strstr_sse2:
    # ... SSE2実装コード ...

暗号化アルゴリズムの実装

[編集]

AES-NIの活用

[編集]

Intel AES New Instructions (AES-NI)を使用した効率的な実装:

    .text
    .globl aes_encrypt_block
aes_encrypt_block:
    # AESキーのロード
    movdqu  (%rsi), %xmm0
    # データブロックのロード
    movdqu  (%rdi), %xmm1
    # AES暗号化
    aesenc  %xmm0, %xmm1
    # ... 続くラウンド処理 ...

メディア処理

[編集]

画像処理

[編集]

YUV→RGB変換などの基本的な画像処理操作:

    .text
    .globl yuv_to_rgb
yuv_to_rgb:
    # YUV→RGB変換係数をSIMDレジスタにロード
    vmovaps yuv_coefficients(%rip), %ymm0
    # ... 変換処理 ...

音声処理

[編集]

オーディオサンプルの処理例:

    .text
    .globl audio_mix
audio_mix:
    # ... オーディオミキシング処理 ...

数値計算

[編集]

線形代数演算

[編集]

SIMD命令を活用した行列乗算の実装:

    .text
    .globl matrix_multiply
matrix_multiply:
    # 行列乗算の実装
    vmovaps (%rdi), %ymm0
    # ... 行列演算処理 ...

浮動小数点演算

[編集]
行列乗算(2x2)
# void matrix2x2_multiply(double *c, double *a, double *b)
.globl matrix2x2_multiply
matrix2x2_multiply:
    vmovsd  (%rdi), %xmm0           # a[0][0]
    vmovsd  8(%rdi), %xmm1          # a[0][1]
    vmovsd  16(%rdi), %xmm2         # a[1][0]
    vmovsd  24(%rdi), %xmm3         # a[1][1]

    vmulsd  (%rsi), %xmm0, %xmm4    # a[0][0] * b[0][0]
    vmulsd  16(%rsi), %xmm1, %xmm5  # a[0][1] * b[1][0]
    vaddsd  %xmm5, %xmm4, %xmm4
    vmovsd  %xmm4, (%rdx)           # c[0][0]

    vmulsd  8(%rsi), %xmm0, %xmm4   # a[0][0] * b[0][1]
    vmulsd  24(%rsi), %xmm1, %xmm5  # a[0][1] * b[1][1]
    vaddsd  %xmm5, %xmm4, %xmm4
    vmovsd  %xmm4, 8(%rdx)          # c[0][1]

    vmulsd  (%rsi), %xmm2, %xmm4    # a[1][0] * b[0][0]
    vmulsd  16(%rsi), %xmm3, %xmm5  # a[1][1] * b[1][0]
    vaddsd  %xmm5, %xmm4, %xmm4
    vmovsd  %xmm4, 16(%rdx)         # c[1][0]

    vmulsd  8(%rsi), %xmm2, %xmm4   # a[1][0] * b[0][1]
    vmulsd  24(%rsi), %xmm3, %xmm5  # a[1][1] * b[1][1]
    vaddsd  %xmm5, %xmm4, %xmm4
    vmovsd  %xmm4, 24(%rdx)         # c[1][1]
    ret

デバイスドライバの基礎

[編集]

I/Oポートの操作

[編集]

FreeBSDでのデバイスI/O操作:

    .text
    .globl port_read
port_read:
    # 特権レベルのチェック
    # ポートからの読み込み
    inl     %dx, %eax
    ret

CPU脆弱性とアセンブリレベルの対策

[編集]

Meltdownの理解と対策

[編集]

脆弱性の基本的なメカニズム

[編集]

Meltdownは、投機的実行中にキャッシュに残されたデータを読み取る脆弱性です。以下のコードで具体例を示します:

# Meltdownの典型的なパターン
.section .text
dangerous_code:
    # カーネル領域へのアクセスを試みる命令(通常は例外が発生)
    movq    kernel_addr(%rip), %rax
    
    # 投機的実行中に以下が実行される可能性がある
    movq    %rax, %rbx
    andq    $0xff, %rbx        # 最下位バイトを抽出
    shlq    $12, %rbx          # インデックスとしてシフト
    movq    probe_array(%rbx), %rcx  # キャッシュにデータを残す

KAISER/KPTIによる対策

[編集]

カーネルページテーブル分離を実装する例:

# ユーザー空間用とカーネル空間用の別々のページテーブル
.section .data
user_cr3:   .quad 0    # ユーザー空間ページテーブルのベースアドレス
kernel_cr3: .quad 0    # カーネル空間ページテーブルのベースアドレス

.section .text
switch_to_user_space:
    # CR3レジスタを切り替えてページテーブルを分離
    movq    user_cr3(%rip), %rax
    movq    %rax, %cr3
    
    # ユーザー空間コードの実行
    # ...

switch_to_kernel_space:
    # システムコール時などにカーネル空間に切り替え
    movq    kernel_cr3(%rip), %rax
    movq    %rax, %cr3
    
    # カーネル処理
    # ...

メモリアクセスの安全な実装

[編集]

投機的実行を考慮した安全なメモリアクセス:

# 境界チェック付きの安全なメモリアクセス
.section .text
safe_array_access:
    # 配列の境界チェック
    cmpq    %rsi, array_size(%rip)
    jae     out_of_bounds      # 境界外アクセスを防止
    
    # CPUフェンスを挿入して投機的実行を制限
    lfence
    
    # 安全なアクセス
    movq    array_base(%rip, %rsi, 8), %rax
    ret

out_of_bounds:
    xorq    %rax, %rax
    ret

Spectre対策

[編集]

Variant 1(分岐予測の悪用)対策

[編集]

分岐予測を利用した攻撃を防ぐコード例:

# Spectre Variant 1対策の実装例
.section .text
safe_bounds_check:
    # インデックスの境界チェック
    movq    array_size(%rip), %rcx
    cmpq    %rcx, %rdi         # インデックスと配列サイズを比較
    
    # 投機的実行を防ぐフェンス
    lfence
    
    # マスクを生成(境界内なら0xFF...FF、境界外なら0)
    cmovae  zero_mask(%rip), %rdi
    
    # 安全なアクセス(境界外の場合は0にマスクされる)
    andq    %rdi, %rsi         # アドレスをマスク
    movq    array_base(%rsi), %rax
    
    ret

.section .data
zero_mask: .quad 0

Variant 2(間接分岐の予測)対策

[編集]

リターンスタックバッファ(RSB)の保護例:

# RSBスタッフィングの実装
.section .text
rsb_stuffing:
    # RSBをスタッフィング
    pushq   safe_target(%rip)
    pushq   safe_target(%rip)
    pushq   safe_target(%rip)
    pushq   safe_target(%rip)
    
    # 各エントリをポップ
    ret
    ret
    ret
    ret

safe_target:
    # 安全な実行パス
    lfence
    # ...

間接呼び出しの保護

[編集]
# 間接呼び出しの安全な実装
.section .data
    # 有効な関数ポインタテーブル
    func_table:
        .quad valid_func1
        .quad valid_func2
        .quad valid_func3
    func_table_size: .quad 3

.section .text
safe_indirect_call:
    # インデックスの検証
    cmpq    func_table_size(%rip), %rdi
    jae     invalid_func
    
    # 投機的実行の制限
    lfence
    
    # 関数ポインタの取得と呼び出し
    leaq    func_table(%rip), %rax
    movq    (%rax, %rdi, 8), %rax
    
    # リターンアドレススタックの保護
    pushq   return_point(%rip)
    jmp     *%rax

return_point:
    ret

invalid_func:
    ud2     # 無効な関数呼び出しを検出

パフォーマンスと安全性のバランス

[編集]

条件付き実行の最適化

[編集]

セキュリティと性能のバランスを取った実装:

# 条件付き実行の安全な最適化
.section .text
optimized_conditional:
    # クリティカルな操作の判定
    testq   %rdi, %rdi
    jz      non_critical_path
    
    # クリティカルパス(完全な保護)
    lfence
    movq    sensitive_data(%rip), %rax
    retq
    
non_critical_path:
    # 非クリティカルパス(通常の実行)
    movq    public_data(%rip), %rax
    retq

キャッシュの制御

[編集]

キャッシュフラッシュを使用したデータ保護:

# キャッシュ制御を使用したデータアクセス
.section .text
cache_controlled_access:
    # キャッシュをフラッシュ
    clflush sensitive_data(%rip)
    mfence
    
    # データアクセス
    movq    sensitive_data(%rip), %rax
    
    # アクセス後の保護
    lfence
    retq

これらの対策は、以下の点に注意して実装する必要があります:

  1. メモリアクセスの順序の保証
  2. 投機的実行の適切な制御
  3. キャッシュサイドチャネルの防止
  4. 分岐予測の制御
  5. パフォーマンスへの影響の最小化

特に重要なのは、セキュリティ対策がパフォーマンスに与える影響を考慮しながら、必要な箇所にのみ適切な保護を実装することです。すべての箇所に最大限の保護を実装すると、システム全体の性能が著しく低下する可能性があります。

附録A AMD64命令セットリファレンス

[編集]

A.1 アドレッシングモード

[編集]

AMD64アーキテクチャでは、以下の主要なアドレッシングモードをサポートしています:

即値アドレッシング
movq $42, %rax          # 即値42をraxに格納
movq $-1, %rbx         # 負の即値
movabsq $0x1234567890ABCDEF, %rax  # 64ビット即値
レジスタアドレッシング
movq %rbx, %rax        # 64ビットレジスタ間転送
movl %ebx, %eax        # 32ビットレジスタ間転送
movw %bx, %ax         # 16ビットレジスタ間転送
movb %bl, %al         # 8ビットレジスタ間転送
直接メモリアドレッシング
movq 0x400000, %rax    # 絶対アドレスからの読み込み
movq %rax, 0x400000    # 絶対アドレスへの書き込み
間接メモリアドレッシング
movq (%rbx), %rax      # ポインタ間接参照
movq (%rsp), %rax      # スタックトップの参照
複合アドレッシング(ベース + インデックス + スケール + 変位)
movq (%rbx,%rcx), %rax           # ベース + インデックス
movq 0x10(%rbx), %rax            # ベース + 変位
movq (%rbx,%rcx,8), %rax         # ベース + インデックス*スケール
movq 0x10(%rbx,%rcx,8), %rax     # すべての要素を使用
%rip相対アドレッシング
movq message(%rip), %rax          # データ参照
leaq function(%rip), %rax         # 関数アドレス計算
movq var@GOTPCREL(%rip), %rax    # GOT経由のグローバル変数アクセス
callq func@PLT                    # PLT経由の関数呼び出し

A.2 データ転送命令

[編集]

A.2.1 基本データ転送

[編集]
MOV - 汎用データ転送
movq %rax, %rbx        # 64ビット転送
movl %eax, %ebx        # 32ビット転送
movw %ax, %bx         # 16ビット転送
movb %al, %bl         # 8ビット転送
MOVZ/MOVS - ゼロ拡張/符号拡張転送
movzbq %al, %rax       # 8→64ビットゼロ拡張
movzwq %ax, %rax       # 16→64ビットゼロ拡張
movsbq %al, %rax       # 8→64ビット符号拡張
movswq %ax, %rax       # 16→64ビット符号拡張

A.2.2 特殊データ転送

[編集]
XCHG - 値の交換
xchgq %rax, %rbx       # レジスタ間の値を交換
xchgq %rax, (%rbx)     # レジスタとメモリの値を交換
LEA - アドレス計算
leaq (%rax,%rbx,8), %rcx    # アドレス計算結果をrcxに格納
leaq 0x10(%rax), %rbx       # オフセット付きアドレス計算

A.3 算術演算命令

[編集]

A.3.1 基本算術演算

[編集]
加算
addq $1, %rax          # 即値加算
addq %rbx, %rax        # レジスタ加算
addq (%rbx), %rax      # メモリ内容の加算
減算
subq $1, %rax          # 即値減算
subq %rbx, %rax        # レジスタ減算
subq (%rbx), %rax      # メモリ内容の減算
乗算
imulq $2, %rax         # 符号付き即値乗算
imulq %rbx, %rax       # 符号付きレジスタ乗算
mulq %rbx              # 符号なし乗算(結果は%rdx:%rax)
除算
idivq %rbx             # 符号付き除算
divq %rbx              # 符号なし除算
cqto                   # 128ビット除算の前準備

A.3.2 インクリメント/デクリメント

[編集]
incq %rax              # 64ビットインクリメント
decq %rax              # 64ビットデクリメント
incl %eax              # 32ビットインクリメント
decl %eax              # 32ビットデクリメント

A.4 論理演算命令

[編集]
AND/OR/XOR/NOT
andq $0xF, %rax        # ビットマスク
orq $0x1, %rax         # ビットセット
xorq %rax, %rax        # レジスタのクリア
notq %rax              # ビット反転
シフト操作
shlq $1, %rax          # 左シフト
shrq $1, %rax          # 論理右シフト
sarq $1, %rax          # 算術右シフト
rolq $1, %rax          # 左ローテート
rorq $1, %rax          # 右ローテート

A.5 分岐命令

[編集]
無条件分岐
jmp label              # 直接分岐
jmp *%rax              # 間接分岐
jmp *(%rax)            # メモリ間接分岐
条件分岐
je/jz  label           # 等しい場合/ゼロの場合
jne/jnz label          # 等しくない場合/非ゼロの場合
jg/jnle label          # より大きい/以下でない(符号付き)
jge/jnl label          # 以上/未満でない(符号付き)
jl/jnge label          # 未満/以上でない(符号付き)
jle/jng label          # 以下/より大きくない(符号付き)
ja/jnbe label          # より大きい/以下でない(符号なし)
jae/jnb label          # 以上/未満でない(符号なし)
jb/jnae label          # 未満/以上でない(符号なし)
jbe/jna label          # 以下/より大きくない(符号なし)

A.6 スタック操作命令

[編集]
プッシュ/ポップ
pushq %rax             # レジスタをスタックにプッシュ
pushq $0x1234          # 即値をスタックにプッシュ
pushq (%rax)           # メモリ内容をスタックにプッシュ
popq %rax              # スタックからポップしてレジスタへ
popq (%rax)            # スタックからポップしてメモリへ

A.7 システム命令

[編集]
システムコール
syscall                # システムコール呼び出し
sysret                 # システムコールからの復帰
割り込み関連
int $0x80              # ソフトウェア割り込み
cli                    # 割り込み禁止
sti                    # 割り込み許可
iretq                  # 割り込みからの復帰

A.8 SIMD命令(SSE/AVX)

[編集]
データ転送
movaps %xmm0, %xmm1    # アライメント済みパックドSingle転送
movups %xmm0, %xmm1    # アライメントなしパックドSingle転送
movapd %xmm0, %xmm1    # アライメント済みパックドDouble転送
vmovaps %ymm0, %ymm1   # AVX 256ビット転送
算術演算
addps %xmm1, %xmm0     # パックドSingle加算
mulps %xmm1, %xmm0     # パックドSingle乗算
vaddps %ymm2, %ymm1, %ymm0  # AVX 3オペランド加算
vmulps %ymm2, %ymm1, %ymm0  # AVX 3オペランド乗算

A.9 仮想化関連命令

[編集]
VMX操作
vmxon (%rax)           # VMX動作の開始
vmxoff                 # VMX動作の終了
vmlaunch              # 仮想マシンの起動
vmresume              # 仮想マシンの再開

A.10 暗号化命令

[編集]
AES-NI
aesenc %xmm1, %xmm0    # AES暗号化ラウンド
aesenclast %xmm1, %xmm0 # AES暗号化最終ラウンド
aesdec %xmm1, %xmm0    # AES復号ラウンド
aesdeclast %xmm1, %xmm0 # AES復号最終ラウンド

この命令セットリファレンスは基本的な命令から高度な命令まで網羅していますが、各命令の詳細なフラグへの影響やタイミング情報については、Intel/AMDの公式ドキュメントを参照してください。

附録B システムコールリファレンス

[編集]

B.1 プロセス管理

[編集]
  • fork(2)
  • exec(2)
  • exit(2)

B.2 ファイル操作

[編集]
  • open(2)
  • read(2)
  • write(2)

附録C コーディング規約とベストプラクティス

[編集]

C.1 命名規則

[編集]
  • ラベル名は意味のある名前をつける
  • ローカルラベルは.で始める
  • グローバルシンボルは_で始める

C.2 コメント規約

[編集]
  • 各関数の先頭に目的と引数の説明を記述
  • 複雑なアルゴリズムには処理の説明を付加

附録D デバッグテクニック

[編集]

D.1 LLDBの使用

[編集]
  • ブレークポイントの設定
  • レジスタ値の確認
  • メモリダンプの取得

D.2 トレース手法

[編集]
  • シングルステップ実行
  • 条件付きブレークポイント
  • バックトレースの取得

附録E パフォーマンス測定手法

[編集]

E.1 プロファイリング

[編集]
  • PMCの使用方法
  • dtrace によるプロファイリング
  • perf の活用

E.2 最適化技法

[編集]
  • キャッシュラインの考慮
  • 分岐予測の最適化
  • SIMD命令の活用

附録

[編集]

FreeBSDでAMD64向けのアセンブラコードをアセンブルしてリンクする方法について、正確な手順を解説します。ここでは、clangを使用し、標準ライブラリを使わずに、システムコールを利用したプログラムの作成手順を説明します。

アセンブラコードの作成

[編集]

まず、アセンブラコード(.sファイル)を記述します。ここでは、標準出力に「Hello, World!」と表示し、その後終了するプログラムを例として使用します。

hello.sというファイルに以下のコードを記述します:

hello.s
    .section .data
msg:    .asciz "Hello, world!\n"  # メッセージ

    .section .text
    .global _start

_start:
    # sys_write: write(1, msg, 14) -> 標準出力に文字列を書き込む
    movq $4, %rax             # sys_write のシステムコール番号
    movq $1, %rdi             # ファイルディスクリプタ 1 (標準出力)
    leaq msg(%rip), %rsi      # メッセージのアドレスを %rsi に格納
    movq $14, %rdx            # メッセージの長さ (14文字)
    syscall                   # システムコール実行

    # sys_exit: exit(0) -> 正常終了
    movq $1, %rax             # sys_exit のシステムコール番号
    xorq %rdi, %rdi           # 終了ステータス 0 (正常終了)
    syscall                   # システムコール実行

アセンブル(コンパイル)

[編集]

次に、アセンブラコードをコンパイルします。clangを使ってアセンブルしますが、標準ライブラリをリンクしないように-nostdlibオプションを指定します。これにより、Cのランタイムやスタートアップコードを使わず、アセンブラだけでシステムコールを利用できます。

clang -nostdlib -o hello hello.s

このコマンドは、hello.sファイルをアセンブルし、helloという実行可能ファイルを生成します。

実行

[編集]

コンパイル後、生成された実行可能ファイルを実行します。

./hello

これにより、標準出力に「Hello, World!」というメッセージが表示され、その後プログラムが終了します。

リンカの詳細(自動的にリンクされる場合)

[編集]

FreeBSDのclangは、アセンブラからリンクまで一貫して行うことができ、通常はldリンカを自動的に呼び出します。しかし、もし手動でリンカを使いたい場合、以下のコマンドを使って、リンク作業を明示的に行うこともできます:

ld -o hello hello.o

この場合、まずアセンブルして .o(オブジェクト)ファイルを作成し、次にldでリンクしますが、clangを使えば通常この手順を省略できます。

FreeBSD特有の注意点

[編集]

アセンブルの入門書はLinuxを対象としているものがあります。 FreeBSDでは、システムコール番号がLinuxとは異なります。例えば、sys_writesys_exitの番号が異なるため、FreeBSDにおける正しい番号を指定する必要があります。上記のコードでは、FreeBSDのシステムコール番号に基づいて、sys_write(番号4)とsys_exit(番号1)を使用しています。

また、FreeBSDのカーネルやライブラリに依存しない形で直接システムコールを呼び出しているので、Cの標準ライブラリや他のライブラリを一切使わないアセンブラコードです。

まとめ
  1. アセンブラコードの作成:システムコールを使ったプログラムを記述。
  2. アセンブルclang -nostdlibを使用してアセンブル。
  3. 実行:生成された実行ファイルを直接実行。

これで、FreeBSD/AMD64におけるアセンブラコードをコンパイルして実行する基本的な手順は完了です。