Upgrade to Pro — share decks privately, control downloads, hide ads and more …

x86-64 Assembly Essentials

Sponsored · SiteGround - Reliable hosting with speed, security, and support you can count on. →
Avatar for Latte72 Latte72
March 07, 2025

x86-64 Assembly Essentials

x86-64 アセンブリ⾔語の基礎的な⽂法の解説スライドです.
このスライドの内容は間違いを含む可能性があります.ご了承ください.

Avatar for Latte72

Latte72

March 07, 2025

More Decks by Latte72

Other Decks in Technology

Transcript

  1. x86-64 Assembly Essentials 自己紹介 名前: Latte72 よく使う言語: C, Python, JavaScript,

    etc... など HP:https://latte72.net/ X:@Latte72R SNS by Latte72
  2. x86-64 Assembly Essentials 目次 はじめに 2. 基礎知識(レジスタ,スタックなど) 3. 命令の解説(データ転送,算術演算など) 4.

    条件分岐・ループの実装方法 5. 関数呼び出し規約 6. セクション管理 7. まとめと参考資料 1. by Latte72
  3. x86-64 Assembly Essentials お約束 このスライドで注意書きなしにアセンブリ言語と書いた場合は x86-64 の アセンブリを指すこととします このスライドでは GNU

    アセンブリの Intel 記法 を用います アセンブリの1行目に .intel_syntax noprefix と記述してください このスライドではSyntax highlightingの関係で ; をコメント用記号として用います が,GNU アセンブリではコメント用記号として # を使うことに注意してください LinuxやmacOSで利用されている System V ABI を前提とします Windowsや他のOSでは異なる部分があるので注意してください by Latte72
  4. x86-64 Assembly Essentials アセンブリ言語とは 言語やPythonなどと比べ,ハードウェアに近い低レベルのプログラミング言語 プログラムの各命令がCPUの動作に直結しているため,詳細な制御が可能 コンピュータの種類によって x86-64, Arm64, RISC-V

    などの多くの種類がある C x86-64アーキテクチャの概要 は,64bit CPU向けの命令セットアーキテクチャでIntelとAMDによって 採用されている(大半のWindows PCと2020年以前のMacintosh) x86-64は,x86アーキテクチャの拡張であり,32bitから64bitへの移行を 可能にするための命令セットを提供する x86-64 by Latte72
  5. x86-64 Assembly Essentials 命令 とは,CPUに具体的な動作を指示するためのものです (個々の命令の説明は後で行います) 命令 命令の種類(一部) 算術演算命令 データ転送命令

    スタック操作命令 仕様により,基本的にメモリからメモリへの直接の演算は許されていません 両方のオペランドがメモリの場合はエラーになるので注意してください by Latte72
  6. x86-64 Assembly Essentials レジスタ は,データの一時保存や演算結果の保持,関数呼び出し時の管理などに 使われます (個々のレジスタの説明は後で行います) レジスタ レジスタの種類(一部) 汎用レジスタ

    演算やデータ操作に用いる スタック関連レジスタ スタックの管理に用いる 特殊レジスタ 特定の目的で使用されるレジスタ(例:フラグレジスタ) by Latte72
  7. x86-64 Assembly Essentials Caller-saved レジスタ レジスタは,一時的な値を保持するためのもので, 関数呼び出し後も値が必要なら呼び出し側が保存する必要があるレジスタです 具体的には,以下のレジスタが Caller-saved レジスタです

    Caller-saved rax , rcx , rdx , rsi , rdi , r8 , r9 , r10 , r11 Callee-saved レジスタ レジスタは,呼び出し先の関数が値を変更する場合に, 変更前の値を保存し,関数から戻る前に元の値へ復元する必要があるレジスタです 具体的には,以下のレジスタが Callee-saved レジスタです Callee-saved rbx , rbp , r12 , r13 , r14 , r15 by Latte72
  8. x86-64 Assembly Essentials 汎用レジスタ 主に演算結果や関数の戻り値を保持します rax , rbx , rcx

    , rdx , rsi , rdi , r8 , r9 , r10 , r11 などがあり,計算やデータ操作 に用います これらのレジスタは基本的に意味のないものとして扱われますが,処理によっては 特別な意味を持っていることもあります 例) rax は関数の戻り値を表すために用いられます by Latte72
  9. x86-64 Assembly Essentials 汎用レジスタの構造 は 64 bit あり,その下位 32 bit

    が eax レジスタ, eax レジスタの下位 16 bit が ax レジスタ, ax レジスタの上位 8 bit が ah レジスタ,下位 8 bit が al レジスタとなっています rbx , rcx , rdx など他のレジスタも同じ構造です rax 出典:https://qiita.com/kaito_tateyama/items/89272098f4b286b64115 by Latte72
  10. x86-64 Assembly Essentials スタック関連レジスタ スタックを管理するために使います rsp Stack Pointer, rbp Base

    Pointer, by Latte72 スタックの先頭位置を示します 関数内でローカル変数の位置の基準として利用します
  11. x86-64 Assembly Essentials 特殊レジスタ プログラムカウンタ rip (Instruction Pointer), フラグレジスタ は,演算や比較の結果を示すビットの集合です

    ZF (Zero Flag): 演算結果がゼロの場合にセットされます SF (Sign Flag): 演算結果が負の場合にセットされます OF (Overflow Flag): 演算結果がオーバーフローした場合にセットされます フラグレジスタ by Latte72 次に実行する命令のアドレスを記録します
  12. x86-64 Assembly Essentials スタック操作命令 push データをスタック( rsp の位置)に積みます rsp は自動的にデータのサイズ分移動します

    pop スタック( rsp の位置)からデータを取り出して指定したレジスタに格納します rsp は自動的にデータのサイズ分移動します by Latte72
  13. x86-64 Assembly Essentials mov 値をレジスタやアドレスに格納します mov A, B で B

    を A に格納します 例) mov eax, 7 mov rax, rcx mov rax, qword ptr [rbp - 8] mov dword ptr [rbp - 8], eax eax, ax, ah, al に格納するときの rax の上位ビットの残り部分 に格納するときには上位32bitは 0 で初期化されます しかし, ax , ah , al に格納するときには,上位ビットは変更されません eax by Latte72
  14. x86-64 Assembly Essentials movsx 小さいサイズの値を符号拡張して大きなレジスタに格納する命令 movsx A, B で B

    を A に格納します 例) movsx eax, cx movsx rax, word ptr [rbp - 6] movsxd の値を符号拡張して 64 bit のレジスタに格納する命令 movsxd A, B で B を A に格納します 32 bit 例) movsxd rax, ebx by Latte72
  15. x86-64 Assembly Essentials movzx 小さいサイズの値をゼロ拡張して大きなレジスタに格納する命令 movzx A, B で B

    を A に格納します 例) movzx rax, cx lea メモリの内容ではなく計算したアドレス自体をレジスタに格納します lea A, [B] で B の計算結果を A に格納します 例) lea rax, [rbp - 16] ※ lea はアドレス計算に特化しており,値の読み出しや格納は行いません by Latte72
  16. x86-64 Assembly Essentials アドレスの読み出しと格納 アドレスからの読み出し 例えば, メモリの rbp - 16

    のアドレスに格納されている 32 bit の値を A に 格納するときには, mov A, dword ptr [rbp - 16] と記述します アドレスへの格納 例えば, A に格納されている値を rbp - 16 から 32 bit 分の領域に格納するときには, mov dword ptr [rbp - 16], A と記述します ※ 格納先や格納元の A のサイズが決まっている場合はもう片方のサイズは 省略可能ですが,ミスを減らすために記述したほうがいいです by Latte72
  17. x86-64 Assembly Essentials 符号拡張とゼロ拡張 符号拡張 符号拡張は,負の数を正しく表現するために,上位ビットを元の値の符号に合わせて 拡張します 例えば,8 bit の値

    0b11111111 (-1)を 16 bit に符号拡張すると 0b1111111111111111 になります ゼロ拡張 ゼロ拡張は,値を大きなサイズに変換する際に,上位ビットを常に0で埋めます 例えば,8 bit の値 0b11111111 (255)を 16 bit にゼロ拡張すると 0b0000000011111111 になります by Latte72
  18. x86-64 Assembly Essentials 算術演算命令 アセンブリ言語には以下のような算術演算命令があります add は加算命令で add A, B

    で A = A + B を意味します sub は減算命令で sub A, B で A = A - B を意味します imul は乗算命令で imul A, B で A = A * B を意味します idiv は除算命令ですが,少し特殊なので次のページで解説します なお, imul と idiv の i は整数(integer)を表しています by Latte72
  19. x86-64 Assembly Essentials idiv の使い方 割られる値が rdx:rax という形で格納され,指定した除数で割ると,商が rax に,

    余りが rdx に格納されます 通常, rax に被除数の下位部分をセットし,その符号を拡張するために cqo 命令 (convert quadword to octaword) で rdx に符号拡張された値を設定します 例)100 / 7 を計算 mov rax, 100 mov rcx, 7 cqo idiv rcx by Latte72
  20. x86-64 Assembly Essentials ここまでのおさらい(1) スライドの最初で紹介した 8 * 9 の計算です 理解できるようになったでしょうか?

    ( .text と .globl main の解説がまだ終わっていませんが,一旦無視してください) .intel_syntax noprefix .text .globl main main: mov rax, 8 mov rcx, 9 imul rax, rcx ret by Latte72
  21. x86-64 Assembly Essentials 関数 アセンブリでは高水準言語のような「関数」という概念は存在しませんが, call 命令 と ret 命令を用いて関数呼び出しを実現します

    call 命令を用いて関数を呼び出し, ret 命令で呼び出し元の関数に戻ります 関数内では rbp を利用してスタックフレームを設定し,ローカル変数や引数を管理し ます ※ 今までのスライドで用いたアセンブリでは操作が簡単だったため, スタックフレームの設定や管理については省略していました 次ページからの rbp を使う方法は一般的ですが,必須ではありません by Latte72
  22. x86-64 Assembly Essentials 関数の開始時の処理 プログラム全体のスタック管理を安全に行うために,関数呼び出しごとに ローカル変数のためのメモリ領域を確保し,終了時に元の状態に戻します rbp は Callee-saved レジスタなので,

    関数呼び出し前の値をスタックに保存しておきます 関数の開始時には以下の処理を記述します なお,A には関数内で使用する一時データや変数のサイズを書きます push rbp mov rbp, rsp sub rsp, A by Latte72
  23. x86-64 Assembly Essentials push rbp 関数呼び出し前の rbp の値をスタックに退避します mov rbp,

    rsp の値を rbp にコピーして,現在のスタックフレームの基準点を設定し, 関数内でローカル変数や引数へのアクセスが容易にします rsp sub rsp, A スタックポインタを A バイト分下げます この領域は関数内で使用する一時データや変数の保存に利用されます by Latte72
  24. x86-64 Assembly Essentials 関数の終了時の処理 関数の終了時には以下の処理を記述します mov rsp, rbp pop rbp

    ret それぞれ関数の開始時の処理に対応しています, ret は call に対応しています mov rsp, rbp mov rbp, rsp pop rbp をする前の rsp の値に戻します 関数呼び出し前の rbp の値に戻します by Latte72
  25. x86-64 Assembly Essentials 関数呼び出し規約 関数の引数用レジスタ 関数の呼び出し時に引数を渡すために使用されます 第1引数から順に rdi , rsi

    , rdx , rcx , r8 , r9 を利用します 関数の戻り値用レジスタ 整数やポインタを戻す関数の戻り値は rax に格納します 関数の呼び出し時の rax 可変引数の関数を呼び出す場合には,引数渡しに使ったベクトルレジスタ数を rax の下位 8 bit( al )に設定する必要があります by Latte72
  26. x86-64 Assembly Essentials スタックアラインメント 関数呼び出し前にスタックを 16バイト単位 でアラインメントします なぜアラインメントが必要なのか 浮動小数点演算や拡張命令を正しく行うために,呼び出し先の関数では 「関数呼び出し直前にスタックが16バイト境界に揃っている」という前提に

    基づいて,ローカル変数や一時領域を配置するからです 呼び出し側でのスタック調整の例 もし関数を呼び出す際に rsp % 16 == 8 ならば,以下の調整を行います sub rsp, 8 call function add rsp, 8 by Latte72 ; スタックを16バイト境界に合わせるために8バイトだけ下げる ; 呼び出しが終わったら戻す
  27. x86-64 Assembly Essentials .globl を用いて,他のファイルから参照可能なシンボルを宣言します これによりリンカがそのシンボルを認識し,他のオブジェクトファイルから 参照できるようになります ※ .globl を用いて宣言しないと,そのシンボルはローカル扱いとなり,

    他のファイルから参照しようとすると 「未定義シンボル」 エラーになります 例えば, main を参照できるようにするには .globl main と宣言する必要があります .globl .globl main main: ; main 関数の実装 by Latte72
  28. x86-64 Assembly Essentials ここまでのおさらい(2) 変数を使ったプログラムをアセンブルしたものです .intel_syntax noprefix .text .globl main

    main: push rbp mov rbp, rsp sub rsp, 8 mov dword ptr [rbp - 4], 8 mov dword ptr [rbp - 8], 9 mov eax, dword ptr [rbp - 4] mov ecx, dword ptr [rbp - 8] imul eax, ecx mov rsp, rbp pop rbp ret by Latte72
  29. x86-64 Assembly Essentials 比較演算命令 アセンブリで値を比較するときは cmp 命令を使います これは実際に引き算を行ってフラグレジスタを更新する命令です フラグレジスタ フラグレジスタには,演算や比較の結果を表す

    ビット がいくつも存在します 代表的なものには次のようなものがあります ZF (Zero Flag): cmp rax, 0 で rax が 0 のときにセットされる SF (Sign Flag):演算結果が負の数になったときにセットされる OF (Overflow Flag):演算でオーバーフローが発生したときにセットされる by Latte72
  30. x86-64 Assembly Essentials set 命令の一覧 (代表的なもの) :ZF が 1 なら

    1 をセット (==) setne al:ZF が 0 なら 1 をセット (!=) setl al:SF != OF なら 1 をセット (<) setle al:(SF != OF) || ZF なら 1 (<=) sete al 実際の比較方法 例として rax = rdx < rcx という比較演算を考えます cmp rdx, rcx setl al movzx rax, al by Latte72
  31. x86-64 Assembly Essentials 論理演算子 || と && の実装 C言語の論理演算子 :

    両方が真なら真 || : いずれかが真なら真 アセンブリでは,フラグと条件付き命令を用いてこれらを実現します && test 命令 命令は,2つのオペランドの論理積を計算し,CPUのフラグを更新します 主に,ゼロかどうかや符号などの条件を調べるために利用されます ※ このスライドでは短絡評価を用いない単純な実装を解説していますが, 実際のコンパイラでは短絡評価が用いられます test by Latte72
  32. x86-64 Assembly Essentials 論理AND (&&) and 命令 命令は,2つのオペランドのビットごとの論理積(AND)を計算し,その結果を 最初のオペランドに格納します and

    論理AND (&&) の実装 レジスタ rax と rdi の両方が非ゼロ(真)の場合に 1 を戻す例です test rax, rax setne al test rdi, rdi setne dl and al, dl movzx rax, al by Latte72 ; raxが0かどうかをチェック ; rax != 0なら AL に 1 をセット,そうでなければ 0 ; rdiが0かどうかをチェック ; rdi != 0なら DL に 1 をセット,そうでなければ 0 ; AL と DL の論理積(AND)を計算 ; (rax != 0) && (rdi != 0) を表す
  33. x86-64 Assembly Essentials 論理OR (||) or 命令 命令は,2つのオペランドのビットごとの論理和(OR)を計算し,その結果を最 初のオペランドに格納します or

    論理OR (||) の実装 レジスタ rax または rdi のいずれかが非ゼロ(真)であれば 1 を戻す例です test rax, rax setne al test rdi, rdi setne dl or al, dl movzx rax, al by Latte72 ; raxが0かどうかをチェック ; rax != 0なら AL に 1 をセット,そうでなければ 0 ; rdiが0かどうかをチェック ; rdi != 0なら DL に 1 をセット,そうでなければ 0 ; AL と DL の論理和(OR)を計算 ; (rax != 0) || (rdi != 0) を表す
  34. x86-64 Assembly Essentials 条件分岐命令 高水準言語での if や while , for

    は,アセンブリでは条件付きジャンプ命令と 無条件ジャンプ命令で実現します 条件付きジャンプ命令 命令で値を比較してから,フラグレジスタの内容に応じてジャンプします je (jump if equal) は,比較した 2 つの値が等しいときにジャンプする命令です 他にも jne , jg , jge , jl , jle などがあります cmp 無条件ジャンプ命令 無条件ジャンプ命令には jmp を用います by Latte72
  35. x86-64 Assembly Essentials if 文の実装(else がないとき) 高水準言語での if 文は,条件が成立した場合にのみ処理を実行する構造ですが, アセンブリではまず値を

    cmp 命令で比較し,その結果に基づき条件付きジャンプ命令 で分岐します ; rax に条件の真偽を入れておく(偽ならば rax = 0 ) cmp rax, 0 je .LendXXX ; ここに条件が真だったときの実行内容を書く .LendXXX: by Latte72
  36. x86-64 Assembly Essentials if 文の実装(else があるとき) else がある場合には少し構造が複雑になりますが基本は同じです ; rax

    に条件の真偽を入れておく(偽ならば rax = 0 ) cmp rax, 0 je .LelseXXX ; ここに条件が真だったときの実行内容を書く jmp .LendXXX .LelseXXX: ; ここに条件が偽だったときの実行内容を書く .LendXXX: なお, else if が使われている場合は else 節の中に if を埋め込んでください by Latte72
  37. x86-64 Assembly Essentials if 文のイメージ 条件が真のときの処理 No rax の値が 0

    か? .LendXXX Yes by Latte72 .LelseXXX ラベル else の処理 ラベル
  38. x86-64 Assembly Essentials while ループの実装 ループは,条件が成立している間,ループ内の処理を繰り返します do while 構文はここでは無視します while

    .LbeginXXX: ; rax に条件の真偽を入れておく(偽ならば rax = 0 ) cmp rax, 0 je .LendXXX ; ここに条件が真だったときの実行内容を書く .LstepXXX: jmp .LbeginXXX .LendXXX: by Latte72
  39. x86-64 Assembly Essentials while ループのイメージ ラベル (ループ終了) .LendXXX Yes rax

    .LbeginXXX ラベル の値が 0 か? No 条件が真のときの処理 ※ break と continue の実装は後で説明します by Latte72 break continue .LstepXXX ラベル
  40. x86-64 Assembly Essentials for ループの実装 for す ループは,初期化,条件判定,ループ本体,更新処理の一連の流れで構成されま ; ここに初期化処理を書く

    .LbeginXXX: ; rax に条件の真偽を入れておく(偽ならば rax = 0 ) cmp rax, 0 je .LendXXX ; ここに条件が真だったときの実行内容を書く .LstepXXX: ; ここに更新処理の実行内容を書く jmp .LbeginXXX .LendXXX: by Latte72
  41. x86-64 Assembly Essentials for ループのイメージ ラベル (ループ終了) .LendXXX Yes rax

    初期化処理 .LbeginXXX ラベル の値が 0 か? No 条件が真のときの処理 break .LstepXXX continue ※ break と continue の実装は後で説明します by Latte72 ラベル 更新処理
  42. x86-64 Assembly Essentials break と continue の実装 break の実装 break

    はループを終了するために用います jmp .LendXXX ; ループを終了するためのジャンプ continue の実装 continue はループの次の反復に移るために用います jmp .LstepXXX by Latte72 ; ループの次の反復に移るためのジャンプ
  43. x86-64 Assembly Essentials セクション管理 アセンブリプログラムでは,以下のようなセクションを用いてデータを管理します .text セクション 実際の命令コードを配置する領域です .data セクション

    文字列リテラルやグローバル変数など初期化済みのデータを配置する領域です .text main: ; main 関数で実行する命令 .data msg: .string "Hello, World!\n" by Latte72 ; 初期化済みの文字列データ
  44. x86-64 Assembly Essentials .rodata セクション 実行中に変更しない読み取り専用データを配置する領域です .section .rodata value: .long

    123456 ; 4バイト整数定数 .bss セクション グローバル変数や静的変数で未初期化のデータを配置する領域です .bss arr: .skip 64 by Latte72 ; 64バイトの未初期化領域を確保
  45. x86-64 Assembly Essentials ここまでのおさらい(3) "Hello"[2] にアクセスするプログラムです .intel_syntax noprefix .text .globl

    main main: push rbp mov rbp, rsp lea rax, [rip+.L0] movzx rax, byte ptr [rax+2] mov rsp, rbp pop rbp ret .data .L0: .string "Hello" by Latte72
  46. x86-64 Assembly Essentials ここまでのおさらい(3)の解説 このプログラムは,文字列 "Hello" の 3 文字目( 'l'

    )にアクセスして, その値を rax に格納します lea 命令を用いて文字列のアドレスを取得し, movzx 命令で 1 バイトの値を rax にゼロ拡張して格納しています int main() { char *str = "Hello"; // 文字列リテラルをポインタに格納 char c = str[2]; // 3 文字目にアクセス return c; // 'l' の ASCII コードを戻す } by Latte72
  47. x86-64 Assembly Essentials ここまでのおさらい(4) .intel_syntax noprefix .data .L0: .string "Hello

    World\n" .text .globl main main: push rbp mov rbp, rsp lea rdi, [rip+.L0] mov rax, 0 call printf mov rax, 0 mov rsp, rbp pop rbp ret by Latte72
  48. x86-64 Assembly Essentials ここまでのおさらい(4)の解説 このプログラムは,文字列 "Hello World\n" を printf 関数を使って出力します

    lea 命令を用いて文字列のアドレスを rdi レジスタに格納し, call printf で printf 関数を呼び出しています printf は可変引数を取る関数なので, rax レジスタに浮動小数点引数の 個数 0 を設定しています #include <stdio.h> int main() { char *str = "Hello World\n"; printf("%s", str); return 0; } by Latte72