3 ポイント 投稿者 GN⁺ 2025-07-14 | 1件のコメント | WhatsAppで共有
  • x86-64アセンブリ 入門のためのシリーズ第1回の紹介
  • 現代の64ビットシステム を前提に、ツールの導入と基本構造を説明
  • Flat Assembler (FASM)WinDbg を主要な開発・デバッグツールとして案内
  • PE形式、DLLインポート、Windowsの呼び出し規約 など実務で必要な中核知識の要約を含む
  • 単純な終了プログラムの作成と デバッグ手順 の実習を中心に説明

紹介と意義

  • x86アセンブリに初めて触れた際、大学では古い環境(16ビット、DOS、セグメントメモリ)に基づく方式で学んだ経験がある
  • 現代では 64ビットプロセッサ が主流であるため、本シリーズでは 実際に使われているx86-64環境 のみを扱い、旧式の要素はすべて排除する
  • 本チュートリアルは Windowsオペレーティングシステム 環境で動作する64ビットプログラム開発に集中する
  • ライブラリを使わず、OSに直接アクセスする最小限のコードから始める
  • この記事はアセンブリを初めて学ぼうとする開発者を対象とし、基本的なC/C++の知識 があることを前提とする

開発ツールの準備

アセンブラ(Assembler)

  • CPUは人間に理解しにくい マシンコード しか解釈できず、それを 人が読めるコード にしたものがアセンブリ言語である
  • アセンブリ言語をマシンコードに変換するプログラムが アセンブラ である
  • x86-64アセンブリ言語には標準が定められておらず、アセンブラごとに文法と動作方式が異なる
  • 本シリーズでは Flat Assembler(FASM) を使用し、小さく扱いやすく、強力なマクロシステムとエディタを備えている

デバッガ(Debugger)

  • 書いたアセンブリコードの分析や実行フローの観察のために デバッガ を必須ツールとして使う
  • WinDbg を推奨し、レジスタ、メモリ、アセンブリコードなどを独立して確認・操作できる
  • Windows 10 SDKからコンポーネントのみを選択してインストールできる
  • デバッガを通じて、プログラム内部の状態、メモリ構造、レジスタの変化を直接観察できる

アセンブリプログラミングの視点

CPU構造と命令セット

  • CPUは特定の 命令セット に従って制限された動作しか行えない
  • 命令 とはCPUが実行できる基本単位の作業である
  • 各命令は引数とともに非常に単純な形で動作し、値の保存や算術演算などを行う
  • 低水準プログラミングやデバッグでは、この構造があらゆる高水準概念の土台であると理解することが重要である

レジスタ(Registers)

  • レジスタ はCPU内部に組み込まれた非常に高速な専用メモリ領域である
  • x86-64には 汎用レジスタ が16個あり、すべて64ビット幅である
  • 各レジスタはバイト、ワード、ダブルワード単位で部分アクセスできる
レジスタ 下位バイト 下位ワード 下位ダブルワード
rax al ax eax
rbx bl bx ebx
rcx cl cx ecx
rdx dl dx edx
rsp spl sp esp
rsi sil si esi
rdi dil di edi
rbp bpl bp ebp
r8~r15 r8b~r15b r8w~r15w r8d~r15d
  • rspスタックポインタrsi/rdi は文字列処理のインデックスとして動作するなど、一部のレジスタには 特殊な用途 が割り当てられている
  • rip命令ポインタrflags は演算結果の状態フラグを保持する特別なレジスタである

メモリとアドレス

  • メモリは0番目のインデックスから連続するバイト配列のように動作する
  • 過去のx86構造ではセグメント・オフセット方式が必須だったが、x86-64ではすべてのメモリを フラット(Flat)アドレス空間 として扱う
  • 実際には オペレーティングシステムとハードウェアが各プロセスごとの仮想アドレス空間を物理メモリに動的にマッピング して提供する
  • つまり、同じ仮想アドレスでも異なるプロセスでは別の物理メモリに対応する
  • 命令とデータは同一メモリ上に存在し(フォン・ノイマン型)、これはArduinoで使われるAVRのようにデータを別に保存するハーバードアーキテクチャと区別される

最初のアセンブリプログラムを書く

  • FASMをインストールした後、以下の簡単なプログラムコードを作成し、ビルドする実習を行う
format PE64 NX GUI 6.0
entry start

section '.text' code readable executable
start:
        int3
        ret

コードの説明

  • format PE64 NX GUI 6.0 : FASMが生成する実行ファイル形式を指定する。ここでは PE(Portable Executable) 64ビット GUI
  • entry start : プログラムが入る エントリポイント を定義し、そのラベル(start)位置から実行を開始する
  • section '.text' code readable executable : PEの コードセクション であることを指定し、実行可能な領域とする
  • start: : 先に指定したエントリポイントに名前を付ける
  • int3 : デバッガ用ブレークポイント で、プログラムを一時停止して状態確認を行うために使う
  • ret : スタック上のアドレスを取り出してその位置へ制御を移す命令で、このプログラムではそのまま終了応答となる

デバッグ実習

  • WinDbgで上記プログラムの実行ファイル(.exe)を開き、逆アセンブルやレジスタなど各種ウィンドウを準備する

  • F5を押してプログラムをブレークポイントまで進め、F8を押すたびに1命令ずつ実行する(ステップ実行)

  • レジスタ(rip など)の変化をリアルタイムで観察できる

  • ret 実行後はオペレーティングシステムへ制御が渡され、その後 RtlExitUserThread が呼び出されてスレッドおよびプロセスの終了へ進む

  • 注意 : ret 命令だけで終了した場合、スレッド以外に追加のバックグラウンド実行があるかどうかによってはプロセスが残ることがあるため、正常終了時には必ず ExitProcess を呼び出すのが望ましい

PE形式とDLLインポート

DLL関数インポート構造の概要

  • ExitProcess のようなWinAPI関数は KERNEL32.DLL にある
  • こうした外部関数を使うには、実行ファイルのインポートテーブル(.idataセクション)を構成する必要がある
  • idataセクションの Import Directory Table(IDT) には、DLL名、関数名、IAT/ILTなどのアドレス(RVA)情報が含まれる
  • IAT(Import Address Table)は、実際の関数アドレスでOSローダによって実行時に上書きされる
  • Hint/Name Tableは各関数の名前とヒント情報で構成される

FASMでの .idata セクション定義例

section '.idata' import readable writeable
idt: 
    dd rva kernel32_iat
    dd 0
    dd 0
    dd rva kernel32_name
    dd rva kernel32_iat
    dd 5 dup(0)
name_table: 
    _ExitProcess_Name dw 0
                      db "ExitProcess", 0, 0
kernel32_name: db "KERNEL32.DLL", 0
kernel32_iat: 
    ExitProcess dq rva _ExitProcess_Name
    dq 0 
  • db/dw/dd/dq : バイト/ワード/ダブルワード/クアッドワード(8バイト)単位で値を挿入
  • rva : シンボルの仮想アドレス(Relative Virtual Address)を計算
  • IATとName Tableを手作業で構成してDLL関数を参照できる

64ビットWindows呼び出し規約(MS x64 Calling Convention)

  • 関数呼び出し時の引数の受け渡し方法とスタック使用方法を定める標準規約
  • 64ビットWindowsでは Microsoft x64 Calling Convention を使用する
  • 主な特徴 :
    • スタックポインタは常に 16バイト境界 に揃っていなければならない
    • 最初の4つの整数/ポインタ引数は rcx, rdx, r8, r9 レジスタ を使う
    • 最初の4つの浮動小数点引数は xmm0~xmm3 に入れる
    • 追加の引数はスタックを使う
    • 引数の個数に関係なく 32バイトのshadow space をスタック上に確保しなければならない
    • スタックの後始末は呼び出し側が担当する

ExitProcess 呼び出し例

format PE64 NX GUI 6.0
entry start

section '.text' code readable executable
start:
    int3
    sub rsp, 8 * 5
    xor rcx, rcx
    call [ExitProcess]

section '.idata' import readable writeable
idt: 
    dd rva kernel32_iat
    dd 0
    dd 0
    dd rva kernel32_name
    dd rva kernel32_iat
    dd 5 dup(0)
name_table: 
    _ExitProcess_Name dw 0
                      db "ExitProcess", 0, 0
kernel32_name db "KERNEL32.DLL", 0
kernel32_iat: 
    ExitProcess dq rva _ExitProcess_Name
    dq 0 

新しいコードの分析

  • sub rsp, 8 * 5 : スタックポインタを調整して40バイトを確保し、16バイトアラインメントとshadow spaceの確保を一度に行う

  • xor rcx, rcx : 第1引数であるrcxレジスタに0を設定する(終了コードとして使う)

  • call [ExitProcess] : import tableに実際に記録されたExitProcessの関数アドレスへジャンプする

  • WinDbgでステップ実行すると、スタックポインタ(rsp)rcxレジスタ の変化、そしてプロセス終了の流れを直接確認できる

まとめ

  • 本記事は 基礎ツールのセットアップからPE形式、DLLインポート、x64呼び出し規約、最初のプログラム作成とデバッグ まで、実習中心でx86-64アセンブリの全体像を案内する
  • 次回では、より多様な機能実装と実際のコードを扱う予定

1件のコメント

 
GN⁺ 2025-07-14
Hacker Newsのコメント
  • 数年かけて開発してきたプロジェクトを共有したい
    https://asm-editor.specy.app
    M68K、MIPS、RISC-V、X86 などさまざまなアセンブリ言語をサポートするオンラインのインタラクティブ IDE である
    アセンブリプログラミングを教えるための多様な機能が多数ある
    他のウェブサイトにも埋め込める

  • ポインタインデックスレジスタの低位バイトに直接アクセスする機能(例: 16/32ビットで si/esi に対して sil としてアクセス可能)があることを知らなかった
    ax/eax から al にアクセスするのと似た概念である
    x86_64 で新たに追加されたオペコードが実際に存在するのか気になる
    プラットフォーム仕様をもう一度確認してみるべきだと思った
    純粋な好奇心から聞いている

  • 自分で書いたアセンブリ入門資料を共有する
    https://www.nayuki.io/page/a-fundamental-introduction-to-x86-assembly-programming

  • 自分の CPU エミュレータのディスパッチを C++ より速くできるか気になって、アセンブリで最適化を試してみた
    フィボナッチプログラムを実行してみたが、結果はまったく近づけなかった
    結局はデフォルト無効のオプションとしてマージしただけだった
    それでも、もっと速くできる方法があるはずだと信じている
    https://github.com/libriscv/libriscv/blob/master/lib/libriscv/amd64/inaccurate_dispatch.nasm
    メモリアクセスのやり方を学びながら、性能を少し改善した
    ジャンプテーブルを 64 ビットから 32 ビットに縮小し、.text セクションに入れて RIP-relative アクセスにした
    フィボナッチプログラムには多くのバイトコードは必要なかった
    さらに改善できる点について本当にアドバイスを聞きたい

    • 自分で書いたコードと C++ コンパイラが生成したコードを直接比較してみたのか気になる
      文脈はよく分からないが、差はディスパッチ機構(命令フェッチ方式)ではなく、実際の命令実装の違いによる可能性もあると思う
      最適化案として、エミュレートするレジスタを x86-64 の実レジスタにマッピングし、まったくメモリに退避しない方法がある
      こうすれば add のような演算でメモリから取り出さず、そのまま演算できる
      ただし、この方式はエミュレータを書くのがはるかに面倒になる
  • ブラウザで実習できる x86 アセンブリ入門資料である
    特別なローカルセットアップなしで、サンプルをすぐ実行できる
    https://shikaan.github.io/assembly/x86/guide/2024/09/08/x86-64-introduction-hello.html
    ちなみに自分で書いた資料である

    • 入力値の検証は別途行っているのか気になる
      NASM でそのままアセンブルしてバイナリを実行する方式のようなので、セキュリティが気になった
  • プロフィール写真だけ見て junferno だと思った

  • アセンブリは一度でも触ってみるだけで全体的な理解が深まり、いつも良い経験になる
    大きなプロジェクトを作る必要まではないので、勇気を出して少しでも自分でやってみることを勧める

  • (2020年)当時の HN 議論リンクを共有する
    https://news.ycombinator.com/item?id=24195627

  • Intel 方式のアセンブリ文法(構文)でよかったと思う

    • 他にどんなアセンブリ文法があるのか気になる
  • アセンブリで何かやってみたい気持ちはあるが、特にアイデアが思い浮かばない

    • TIS-100 というゲームを勧める
      一種の疑似アセンブリでパズルを解くゲームである
      こういうゲームがアセンブリへの渇きを満たしてくれるかもしれない