x86-64アセンブリを学ぶ
(gpfault.net)- 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ビット GUIentry 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件のコメント
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 アクセスにした
フィボナッチプログラムには多くのバイトコードは必要なかった
さらに改善できる点について本当にアドバイスを聞きたい
文脈はよく分からないが、差はディスパッチ機構(命令フェッチ方式)ではなく、実際の命令実装の違いによる可能性もあると思う
最適化案として、エミュレートするレジスタを 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 方式のアセンブリ文法(構文)でよかったと思う
アセンブリで何かやってみたい気持ちはあるが、特にアイデアが思い浮かばない
一種の疑似アセンブリでパズルを解くゲームである
こういうゲームがアセンブリへの渇きを満たしてくれるかもしれない