2025-09-23 15:31:46
諏訪子
lowlevel
assembly

【プログラミング】C言語で使ってアセンブリ言語の勉強する方法

アセンブリ言語を学ぶ方法は沢山ありますが、少なくとも1つのアセンブリ方言を学ぶ事をお勧めします。
1つの方言を習得すると、他のアセンブリ言語も学び易くなり、コンピュータがどの様に動作するかを深く理解出来ます。
アセンブリを読めないと、コンピュータの仕組みを本当に理解するのは難しいです。
通常は、MIPSやRISC-Vアセンブリから始める事をお勧めします。
これらは非常にシンプルだからです。
然し、今回は貴方がIntel又はAMDプロセッサを使用していると仮定して、x64アセンブリを見ていきます。

C言語からアセンブリを学ぶワークフロー

  • 簡単なCプログラムを書く。
  • cc -o program program.c でコンパイルする。
  • objdump -d -Mintel program | less を使ってバイナリを逆アセンブルする。
  • main 関数を探し、アセンブリ命令を分析する。
  • 各アセンブリ命令を対応するCコードにマッピングする。
  • 最適化なし(デフォルトで -O0 )で cc main.c -o ftoc とコンパイルします。
    -O2 の様な最適化フラグを使用すると、異なるアセンブリが生成される可能性がある為、学習には -O0 を使用して下さい。

    Cからアセンブリへの変換

    アセンブリを学ぶ最も簡単な方法は、先ず簡単なCプログラムを作成し、それをコンパイルし、objdump を使ってバイナリを逆アセンブルする事です。
    この記事ではGhostBSDを使用しますが、Linux、Illumos、FreeBSD、OpenBSD、NetBSD、又はその他のUnix系OSを使用していても問題ありません。
    この記事で使用する全てのツールは、既に貴方のコンピュータにインストールされています。
    アセンブリにはIntel構文を使用します(objdump-Mintel を指定)。
    これにより、命令はAT&Tの movl %ebx, %eax ではなく、mov eax, ebx の様に記述されます。

    以下は、B.W.カーニハンとD.M.リッチーによる『プログラミング言語C第2版』から直接引用したCコードです。
    これは全てのCプログラマーにとって金字塔ともいえる書籍です。

    #include <stdio.h>
    
    int main() {
      int fahr, celsius;
      int lower, upper, step;
    
      lower = 0;      /* 温度表の下限 */
      upper = 300;    /* 上限 */
      step = 20;      /* きざみ */
    
      fahr = lower;
      while (fahr <= upper) {
        celsius = 5 * (fahr-32) / 9;
        printf("%d\t%d\n", fahr, celsius);
        fahr = fahr + step;
      }
    }

    このCプログラムは、華氏(Fahrenheit)から摂氏(Celsius)への変換を行い、0°Fから300°Fまで20°F刻みで表を出力します。
    使用されている計算式は celsius = 5 * (fahr - 32) / 9 です。
    次に、cc main.c -o ftoc でコンパイルします。
    そして、 objdump -d -Mintel ./ftoc | less コマンドを実行します。
    / キーを押して main と入力します。

    見える内容は、使用しているCPUアーキテクチャやOSによって若干異なります。
    あたしの場合、以下の様な結果が表示されます:

    00000000002016a0 <main>:
      2016a0: 55                            push    rbp
      2016a1: 48 89 e5                      mov     rbp, rsp
      2016a4: 48 83 ec 20                   sub     rsp, 0x20
      2016a8: c7 45 fc 00 00 00 00          mov     dword ptr [rbp - 0x4], 0x0
      2016af: c7 45 f0 00 00 00 00          mov     dword ptr [rbp - 0x10], 0x0
      2016b6: c7 45 ec 2c 01 00 00          mov     dword ptr [rbp - 0x14], 0x12c
      2016bd: c7 45 e8 14 00 00 00          mov     dword ptr [rbp - 0x18], 0x14
      2016c4: 8b 45 f0                      mov     eax, dword ptr [rbp - 0x10]
      2016c7: 89 45 f8                      mov     dword ptr [rbp - 0x8], eax
      2016ca: 8b 45 f8                      mov     eax, dword ptr [rbp - 0x8]
      2016cd: 3b 45 ec                      cmp     eax, dword ptr [rbp - 0x14]
      2016d0: 7f 36                         jg      0x201708 <main+0x68>
      2016d2: 8b 45 f8                      mov     eax, dword ptr [rbp - 0x8]
      2016d5: 83 e8 20                      sub     eax, 0x20
      2016d8: 6b c0 05                      imul    eax, eax, 0x5
      2016db: b9 09 00 00 00                mov     ecx, 0x9
      2016e0: 99                            cdq
      2016e1: f7 f9                         idiv    ecx
      2016e3: 89 45 f4                      mov     dword ptr [rbp - 0xc], eax
      2016e6: 8b 75 f8                      mov     esi, dword ptr [rbp - 0x8]
      2016e9: 8b 55 f4                      mov     edx, dword ptr [rbp - 0xc]
      2016ec: 48 bf d8 04 20 00 00 00 00 00 movabs  rdi, 0x2004d8
      2016f6: b0 00                         mov     al, 0x0
      2016f8: e8 c3 00 00 00                call    0x2017c0 <printf@plt>
      2016fd: 8b 45 f8                      mov     eax, dword ptr [rbp - 0x8]
      201700: 03 45 e8                      add     eax, dword ptr [rbp - 0x18]
      201703: 89 45 f8                      mov     dword ptr [rbp - 0x8], eax
      201706: eb c2                         jmp     0x2016ca <main+0x2a>
      201708: 8b 45 fc                      mov     eax, dword ptr [rbp - 0x4]
      20170b: 48 83 c4 20                   add     rsp, 0x20
      20170f: 5d                            pop     rbp
      201710: c3                            ret
      201711: cc                            int3
      201712: cc                            int3
      201713: cc                            int3
      201714: cc                            int3
      201715: cc                            int3
      201716: cc                            int3
      201717: cc                            int3
      201718: cc                            int3
      201719: cc                            int3
      20171a: cc                            int3
      20171b: cc                            int3
      20171c: cc                            int3
      20171d: cc                            int3
      20171e: cc                            int3
      20171f: cc                            int3

    末尾の int3 命令(オペコード cc)は、コンパイラがアライメントやデバッグ用のトラップ、或いは無効な制御フローの為に追加したパディングです。
    これらはプログラムのロジックには影響しない為、今回は無視出来ます。

    全ての数値が16進数で表現されている事に気付くでしょう。
    見逃した人の為に、ビット演算について詳しく説明した記事をこちらに書いています

    アセンブリの基礎

    アセンブリ言語は、マシン命令に直接対応する低レベル言語です。
    主な概念は以下の通りです:

  • レジスタ: CPU内の小さくて高速なストレージ(例:raxrbprsp)。
  • スタック: ローカル変数や関数の状態を管理するメモリ領域で、スタックポインタ(rsp)とベースポインタ(rbp)で管理されます。
  • 命令: mov(データ移動)、cmp(比較)、jmp(アドレスへのジャンプ)等のコマンド。
  • 簡単な部分

    整数の定義

    最初に気付くのは以下の部分かもしん:

      2016af: c7 45 f0 00 00 00 00          mov     dword ptr [rbp - 0x10], 0x0
      2016b6: c7 45 ec 2c 01 00 00          mov     dword ptr [rbp - 0x14], 0x12c
      2016bd: c7 45 e8 14 00 00 00          mov     dword ptr [rbp - 0x18], 0x14
      2016c4: 8b 45 f0                      mov     eax, dword ptr [rbp - 0x10]
      2016c7: 89 45 f8                      mov     dword ptr [rbp - 0x8], eax

    これは以下のCコードに対応します:

    lower = 0;
    upper = 300;
    step = 20;
    fahr = lower;

    rbp - 0x10lower を、 rbp - 0x14upper を、 rbp - 0x18step を、 rbp - 0x8fahrを格納しています。
    命令 mov eax, dword ptr [rbp - 0x10]lower (0) を eaxrax レジスタのバイト0-3)にロードします。
    rax は関数の戻り値を格納する為に使用されます。
    mov dword ptr [rbp - 0x8], eaxlowerfahr に代入します。

    rbp は現在のスタックフレームで、 rsp は現在のスタックポインタです。
    プログラムの冒頭では、以下のようなコードが見られます:

      2016a0: 55                            push    rbp
      2016a1: 48 89 e5                      mov     rbp, rsp
      2016a4: 48 83 ec 20                   sub     rsp, 0x20

    これらの命令は、 main 関数のスタックフレームを設定します。

    push rbpmov rbp, rsp、及び sub rsp, 0x20 は、x64関数の標準的なプロローグを形成します。
    push rbp は呼び出し元のベースポインタを保存し、mov rbp, rsp は現在のスタックポインタを main のスタックフレームの新しいベースポインタとして設定し、sub rsp, 0x20 はローカル変数とアライメントの為に32バイトを割り当てます。
    この部分は全てのコードで見られ、Cコードには直接対応しません。
    これはマシンがバックグラウンドで実行する処理だからです。

    又、[rbp - 0x10] の様な記述も見られます。
    これは、rbp レジスタが現在のスタックフレームのベースポインタであり、16進数のオフセット 0x10 (16) を引く事で、そのアドレスに格納されたローカル変数(この場合は lower)をロードするからです。

    printf関数

    次に、簡単に見つけられるのは以下の部分です:

      2016e6: 8b 75 f8                      mov     esi, dword ptr [rbp - 0x8]
      2016e9: 8b 55 f4                      mov     edx, dword ptr [rbp - 0xc]
      2016ec: 48 bf d8 04 20 00 00 00 00 00 movabs  rdi, 0x2004d8
      2016f6: b0 00                         mov     al, 0x0
      2016f8: e8 c3 00 00 00                call    0x2017c0 <printf@plt>

    これはループ内の printf() 関数に対応します。
    mov esi, dword ptr [rbp - 0x8]fahresiprintf の2番目の引数)にロードします。
    mov edx, dword ptr [rbp - 0xc]celsiusedxprintf の3番目の引数)にロードします。
    movabs rdi, 0x2004d8 はフォーマット文字列 "%d\t%d\n" のアドレスを rdiprintf の1番目の引数)にロードします。
    mov al, 0x0al を0に設定し、printf に浮動小数点引数が渡されていない事を示します。
    これはx64プロセッサでの要件です。

    次に、call 0x2017c0 <printf@plt> が見られます。
    call 0x2017c0 <printf@plt> は、動的リンクされたバイナリで使用されるプロシージャリンケージテーブル(PLT)を介して printf を呼び出し、実行時に libc.so 内の printf のアドレスを解決します。
    静的リンクされたバイナリでは、後で説明する様に、完全な printf 実装が含まれます。

    では、アドレス 0x2004d8 を確認しましょう。
    先ず、q を押して現在の objdump インスタンスを終了します。
    次に、objdump -d -Mintel -s -j .rodata ./ftoc を実行します。

    あたしの場合、出力は以下の様になります:

    ./ftoc:  file format elf64-x86-64
    Contents of section .rodata:
     2004d8 25640925 640a0000                    %d.%d...
    
    Disassembly of section .rodata:
    
    00000000002004d8 <.rodata>:
      2004d8: 25 64 09 25 64                and     eax, 0x64250964
      2004dd: 0a 00                         or      al, byte ptr [rax]
      2004df: 00                            <unknown>

    特に 2004d8: 25 64 09 25 64 and eax, 0x64250964 を見てみましょう。
    % = 0x25
    d = 0x64
    x64はリトルエンディアンアーキテクチャなので、バイトシーケンス 25 64%d を表します。
    \t = 0x09
    その後、25 64 が繰り返され、これは %d を2回使用している為納得できます。

    次の行では、2004dd: 0a 00 or al, byte ptr [rax] があります。
    \n = 0x0a
    最後の 0x00 は、コンパイラが挿入するヌルターミネータです。

    これが printf の定義場所です。
    詳細には触れませんが、アドレス 2017c0 を検索すると、あたしの場合、バイナリの最後にあります。
    以下の様な内容が見えます。

    00000000002017c0 <printf@plt>:
      2017c0: ff 25 aa 21 00 00             jmp     qword ptr [rip + 0x21aa] # 0x203970 <printf+0x203970>
      2017c6: 68 02 00 00 00                push    0x2
      2017cb: e9 c0 ff ff ff                jmp     0x201790 <.plt>

    宿題として、これが何を意味するのか理解して下さい。

    但し、これは動的リンクされたバイナリの printf です。
    代わりに静的リンクされたバイナリを見ると、出力は以下の様になります:

    0000000000227320 <printf>:
      227320: 55                            push    rbp
      227321: 48 89 e5                      mov     rbp, rsp
      227324: 48 81 ec d0 00 00 00          sub     rsp, 0xd0
      22732b: 49 89 fa                      mov     r10, rdi
      22732e: 84 c0                         test    al, al
      227330: 74 26                         je      0x227358 <printf+0x38>
      227332: 0f 29 85 60 ff ff ff          movaps  xmmword ptr [rbp - 0xa0], xmm0
      227339: 0f 29 8d 70 ff ff ff          movaps  xmmword ptr [rbp - 0x90], xmm1
      227340: 0f 29 55 80                   movaps  xmmword ptr [rbp - 0x80], xmm2
      227344: 0f 29 5d 90                   movaps  xmmword ptr [rbp - 0x70], xmm3
      227348: 0f 29 65 a0                   movaps  xmmword ptr [rbp - 0x60], xmm4
      22734c: 0f 29 6d b0                   movaps  xmmword ptr [rbp - 0x50], xmm5
      227350: 0f 29 75 c0                   movaps  xmmword ptr [rbp - 0x40], xmm6
      227354: 0f 29 7d d0                   movaps  xmmword ptr [rbp - 0x30], xmm7
      227358: 48 89 b5 38 ff ff ff          mov     qword ptr [rbp - 0xc8], rsi
      22735f: 48 89 95 40 ff ff ff          mov     qword ptr [rbp - 0xc0], rdx
      227366: 48 89 8d 48 ff ff ff          mov     qword ptr [rbp - 0xb8], rcx
      22736d: 4c 89 85 50 ff ff ff          mov     qword ptr [rbp - 0xb0], r8
      227374: 4c 89 8d 58 ff ff ff          mov     qword ptr [rbp - 0xa8], r9
      22737b: 48 8b 05 ae 3c 07 00          mov     rax, qword ptr [rip + 0x73cae] # 0x29b030 <__stack_chk_guard>
      227382: 48 89 45 f8                   mov     qword ptr [rbp - 0x8], rax
      227386: 48 8d 85 30 ff ff ff          lea     rax, [rbp - 0xd0]
      22738d: 48 89 45 f0                   mov     qword ptr [rbp - 0x10], rax
      227391: 48 8d 45 10                   lea     rax, [rbp + 0x10]
      227395: 48 89 45 e8                   mov     qword ptr [rbp - 0x18], rax
      227399: 48 b8 08 00 00 00 30 00 00 00 movabs  rax, 0x3000000008
      2273a3: 48 89 45 e0                   mov     qword ptr [rbp - 0x20], rax
      2273a7: 48 8b 3d 22 ea 06 00          mov     rdi, qword ptr [rip + 0x6ea22] # 0x295dd0 <__stdoutp>
      2273ae: 48 8d 55 e0                   lea     rdx, [rbp - 0x20]
      2273b2: 4c 89 d6                      mov     rsi, r10
      2273b5: e8 46 40 00 00                call    0x22b400 <vfprintf>
      2273ba: 48 8b 0d 6f 3c 07 00          mov     rcx, qword ptr [rip + 0x73c6f] # 0x29b030 <__stack_chk_guard>
      2273c1: 48 3b 4d f8                   cmp     rcx, qword ptr [rbp - 0x8]
      2273c5: 75 09                         jne     0x2273d0 <printf+0xb0>
      2273c7: 48 81 c4 d0 00 00 00          add     rsp, 0xd0
      2273ce: 5d                            pop     rbp
      2273cf: c3                            ret
      2273d0: e8 1b cc 00 00                call    0x233ff0 <__stack_chk_fail_local>
      2273d5: 66 66 2e 0f 1f 84 00 00 00 00 00      nop     word ptr cs:[rax + rax]

    又、宿題として、printf@plt(動的)と printf(静的)の関数を調べてみて下さい。

    静的リンクでは、printf 関数とそれが必要とする全ての関数が含まれますが、動的リンクでは、システム上の何処かにある libc.so ファイル内のアドレスを指すだけです。

    その場合、objdump -d -Mintel /lib/libc.so.7 | less(OSによってファイル名やパスが異なる場合があります)で確認出来ます。
    ここでは以下の様な内容が見られます:

    00000000001cd370 <printf@plt>:
      1cd370: ff 25 82 1d 01 00             jmp     qword ptr [rip + 0x11d82] # 0x1df0f8
      1cd376: 68 93 01 00 00                push    0x193
      1cd37b: e9 b0 e6 ff ff                jmp     0x1cba30 <.plt>

    残念ながら、このオブジェクトダンプ内では完全な定義を見つける事は出来ません。
    但し、ライブラリ全体をダンプする事で確認出来ます:objdump -d /lib/libc.so.7 > libc_disasm.txt
    次に、less libc_disasm.txt を実行し、/ を押して <printf> を検索します。
    すると、完全な定義が以下の様に見えます(AT&T構文で):

    000000000011b220 <printf>:
      11b220: 55                            pushq   %rbp
      11b221: 48 89 e5                      movq    %rsp, %rbp
      11b224: 53                            pushq   %rbx
      11b225: 48 81 ec d8 00 00 00          subq    $0xd8, %rsp
      11b22c: 49 89 fa                      movq    %rdi, %r10
      11b22f: 84 c0                         testb   %al, %al
      11b231: 74 29                         je      0x11b25c <printf+0x3c>
      11b233: 0f 29 85 50 ff ff ff          movaps  %xmm0, -0xb0(%rbp)
      11b23a: 0f 29 8d 60 ff ff ff          movaps  %xmm1, -0xa0(%rbp)
      11b241: 0f 29 95 70 ff ff ff          movaps  %xmm2, -0x90(%rbp)
      11b248: 0f 29 5d 80                   movaps  %xmm3, -0x80(%rbp)
      11b24c: 0f 29 65 90                   movaps  %xmm4, -0x70(%rbp)
      11b250: 0f 29 6d a0                   movaps  %xmm5, -0x60(%rbp)
      11b254: 0f 29 75 b0                   movaps  %xmm6, -0x50(%rbp)
      11b258: 0f 29 7d c0                   movaps  %xmm7, -0x40(%rbp)
      11b25c: 48 89 b5 28 ff ff ff          movq    %rsi, -0xd8(%rbp)
      11b263: 48 89 95 30 ff ff ff          movq    %rdx, -0xd0(%rbp)
      11b26a: 48 89 8d 38 ff ff ff          movq    %rcx, -0xc8(%rbp)
      11b271: 4c 89 85 40 ff ff ff          movq    %r8, -0xc0(%rbp)
      11b278: 4c 89 8d 48 ff ff ff          movq    %r9, -0xb8(%rbp)
      11b27f: 48 8b 1d 3a dc 0b 00          movq    0xbdc3a(%rip), %rbx     # 0x1d8ec0
      11b286: 48 8b 03                      movq    (%rbx), %rax
      11b289: 48 89 45 f0                   movq    %rax, -0x10(%rbp)
      11b28d: 48 8d 85 20 ff ff ff          leaq    -0xe0(%rbp), %rax
      11b294: 48 89 45 e0                   movq    %rax, -0x20(%rbp)
      11b298: 48 8d 45 10                   leaq    0x10(%rbp), %rax
      11b29c: 48 89 45 d8                   movq    %rax, -0x28(%rbp)
      11b2a0: 48 b8 08 00 00 00 30 00 00 00 movabsq $0x3000000008, %rax     # imm = 0x3000000008
      11b2aa: 48 89 45 d0                   movq    %rax, -0x30(%rbp)
      11b2ae: 48 8b 05 1b dd 0b 00          movq    0xbdd1b(%rip), %rax     # 0x1d8fd0
      11b2b5: 48 8b 38                      movq    (%rax), %rdi
      11b2b8: 48 8d 55 d0                   leaq    -0x30(%rbp), %rdx
      11b2bc: 4c 89 d6                      movq    %r10, %rsi
      11b2bf: e8 cc 11 0b 00                callq   0x1cc490 <vfprintf@plt>
      11b2c4: 48 8b 0b                      movq    (%rbx), %rcx
      11b2c7: 48 3b 4d f0                   cmpq    -0x10(%rbp), %rcx
      11b2cb: 75 0a                         jne     0x11b2d7 <printf+0xb7>
      11b2cd: 48 81 c4 d8 00 00 00          addq    $0xd8, %rsp
      11b2d4: 5b                            popq    %rbx
      11b2d5: 5d                            popq    %rbp
      11b2d6: c3                            retq
      11b2d7: e8 a4 07 0b 00                callq   0x1cba80 <__stack_chk_fail@plt>
      11b2dc: 0f 1f 40 00                   nopl    (%rax)

    簡単ですよね?
    次に、もっと分かり憎い部分に進みます!

    分かり憎い部分

    whileループの条件は以下の様に構築されます:

      2016ca: 8b 45 f8                      mov     eax, dword ptr [rbp - 0x8]
      2016cd: 3b 45 ec                      cmp     eax, dword ptr [rbp - 0x14]
      2016d0: 7f 36                         jg      0x201708 <main+0x68>

    これは、whileループの条件、つまり fahrupper 以下かどうかをチェックします。
    mov eax, dword ptr [rbp - 0x8]fahreax にロードします。
    cmp eax, dword ptr [rbp - 0x14]fahrupperrbp - 0x14 に格納)と比較します。
    この2つで fahr <= upper を表現します。
    jg 0x201708 は、fahrupper より大きい場合にアドレス 201708(ループの終わり)にジャンプします。
    これは fahr <= upper の否定であり、fahrupper 以下の場合にループが継続します。

    ループ内では以下のようなコードが見られます:

      2016d2: 8b 45 f8                      mov     eax, dword ptr [rbp - 0x8]
      2016d5: 83 e8 20                      sub     eax, 0x20
      2016d8: 6b c0 05                      imul    eax, eax, 0x5
      2016db: b9 09 00 00 00                mov     ecx, 0x9
      2016e0: 99                            cdq
      2016e1: f7 f9                         idiv    ecx
      2016e3: 89 45 f4                      mov     dword ptr [rbp - 0xc], eax

    これは celsius = 5 * (fahr - 32) / 9 を計算します。
    mov eax, dword ptr [rbp - 0x8]fahreax にロードします。
    sub eax, 0x20eax から32(0x20)を引き、Cコードの fahr - 32 に相当します。
    imul eax, eax, 0x5eax を5倍し、Cコードの 5 * (fahr - 32) に相当します。
    mov ecx, 0x9 は9を ecx(除数)にロードします。
    cdqeax を符号拡張して edx:eax にし、idiv 命令に備えます。
    これは edx:eax の64ビット値を符号付き除算する為に使用され、5 * (fahr - 32) / 9 の負の数を正しく処理します。
    idiv ecxedx:eaxecx(9)で割り、商を eax に格納します。
    mov dword ptr [rbp - 0xc], eax は結果を celsiusrbp - 0xc)に格納します。
    詰り、Cコードの1行に対してアセンブリ命令が7行必要です。

    次に、以下の計算が行われます:

      2016fd: 8b 45 f8                      mov     eax, dword ptr [rbp - 0x8]
      201700: 03 45 e8                      add     eax, dword ptr [rbp - 0x18]
      201703: 89 45 f8                      mov     dword ptr [rbp - 0x8], eax

    これまでに多くの説明を受けたので、これは fahr = fahr + step; を意味する事が分かるはずです。

    その後、以下が見られます:

      201706: eb c2                         jmp     0x2016ca <main+0x2a>

    これは、whileループの先頭(アドレス 2016ca)に戻り、fahr が未だ upper 以下かどうかをチェックする事を意味します。
    答えが「false」の場合、ループは終了し、「true」の場合は続行します。

    最後に、以下があります:

      201708: 8b 45 fc                      mov     eax, dword ptr [rbp - 0x4]
      20170b: 48 83 c4 20                   add     rsp, 0x20
      20170f: 5d                            pop     rbp
      201710: c3                            ret

    これはスタックフレームをクリーンアップし、main から戻ります。
    mov eax, dword ptr [rbp - 0x4] は、rbp - 0x4 の値を eax にロードします。
    これは main の戻り値で、アドレス 2016a8 で暗黙的に初期化されています(mov dword ptr [rbp - 0x4], 0x0)。

    Cでは、main が明示的に戻り値を指定しない場合、C標準により0が返されます。
    アセンブリでは、mov dword ptr [rbp - 0x4], 0x0(アドレス 2016a8)は fahr を初期化しており、戻り値ではありません。
    実際の戻り値は、最後に mov eax, dword ptr [rbp - 0x4] で設定されます。
    add rsp, 0x20 は32バイトのスタックスペースを解放します。
    pop rbp は呼び出し元のベースポインタを復元します。
    ret は呼び出し元に戻ります。

    これでも未だ簡単ですよね?

    明示的 vs 暗黙的戻り値

    今回の例では、return 0; を省略しましたが、C及びC++では、int main() の場合にのみこれが許されます。
    然し、return 0; をCコードに追加すると、アセンブリ出力が少し変わります。

    00000000002016a0 <main>:
      2016a0: 55                            push    rbp
      2016a1: 48 89 e5                      mov     rbp, rsp
      2016a4: 48 83 ec 20                   sub     rsp, 0x20
      2016a8: c7 45 fc 00 00 00 00          mov     dword ptr [rbp - 0x4], 0x0
      2016af: c7 45 f0 00 00 00 00          mov     dword ptr [rbp - 0x10], 0x0
      2016b6: c7 45 ec 2c 01 00 00          mov     dword ptr [rbp - 0x14], 0x12c
      2016bd: c7 45 e8 14 00 00 00          mov     dword ptr [rbp - 0x18], 0x14
      2016c4: 8b 45 f0                      mov     eax, dword ptr [rbp - 0x10]
      2016c7: 89 45 f8                      mov     dword ptr [rbp - 0x8], eax
      2016ca: 8b 45 f8                      mov     eax, dword ptr [rbp - 0x8]
      2016cd: 3b 45 ec                      cmp     eax, dword ptr [rbp - 0x14]
      2016d0: 7f 36                         jg      0x201708 <main+0x68>
      2016d2: 8b 45 f8                      mov     eax, dword ptr [rbp - 0x8]
      2016d5: 83 e8 20                      sub     eax, 0x20
      2016d8: 6b c0 05                      imul    eax, eax, 0x5
      2016db: b9 09 00 00 00                mov     ecx, 0x9
      2016e0: 99                            cdq
      2016e1: f7 f9                         idiv    ecx
      2016e3: 89 45 f4                      mov     dword ptr [rbp - 0xc], eax
      2016e6: 8b 75 f8                      mov     esi, dword ptr [rbp - 0x8]
      2016e9: 8b 55 f4                      mov     edx, dword ptr [rbp - 0xc]
      2016ec: 48 bf d8 04 20 00 00 00 00 00 movabs  rdi, 0x2004d8
      2016f6: b0 00                         mov     al, 0x0
      2016f8: e8 b3 00 00 00                call    0x2017b0 <printf@plt>
      2016fd: 8b 45 f8                      mov     eax, dword ptr [rbp - 0x8]
      201700: 03 45 e8                      add     eax, dword ptr [rbp - 0x18]
      201703: 89 45 f8                      mov     dword ptr [rbp - 0x8], eax
      201706: eb c2                         jmp     0x2016ca <main+0x2a>
      201708: 31 c0                         xor     eax, eax
      20170a: 48 83 c4 20                   add     rsp, 0x20
      20170e: 5d                            pop     rbp
      20170f: c3                            ret

    先ず、デバッグ用のパディングがなくなっています。
    変更された関連する行は以下の通りです:

    201708: 8b 45 fc                      mov     eax, dword ptr [rbp - 0x4]

    代わりに、以下が表示されます:

    201708: 31 c0                         xor     eax, eax

    詰り、戻り値を検索する代わりに、バイナリは直接0の戻り値にアクセスします。
    これにより、処理が高速化され、数バイト節約出来ます。

    結論

    全体として、それ程難しくありません。
    一見すると威圧的に見えるかもしんが、何が起こっているかを理解すれば、アセンブリを学ぶのは直ぐに簡単になります。
    アセンブリを理解すれば、全てのソフトウェアがオープンソースになります。

    以上