関数 (2)

サンプルコード
以下は、C 標準ライブラリの printf を使って、文字列を表示するサンプルです。
C ソースは test2.c を使います。

default rel

extern printf   ; 外部関数を宣言
global testfunc

section .text

testfunc:
    push rbp ; RSP を 16byte 境界に合わせておく

    lea rdi, [message] ; 文字列のポインタ
    mov esi, 'a'       ; %c で表示する文字
    mov edx, -100      ; %d で表示する整数
    mov rax, __?float64?__(1.234) ; RAX に倍精度値をセット
    movq xmm0, rax     ; %f で表示する浮動小数点数 (RAX -> XMM0)
    mov al, 1          ; XMM レジスタの使用数 (1)

    call printf wrt ..plt

    pop rbp
    ret

message:
    db `test %c, %d, %.4f\n`,0

まず、printf のアドレスを参照できるようにするため、extern で、外部参照であることを宣言しておきます。
スタックの境界合わせ
最初に PUSH RBP を行っていますが、これは、現在のスタック位置 (RSP) を 16 byte 境界にしておくためです。
Sytem V AMD64 ABI に準拠した関数を呼び出す場合は、CALL 命令を呼び出す直前で、RSP の値が 16 の倍数になっている必要があります。

testfunc 関数が呼び出された時、関数開始直後は、CALL 命令によって RIP がプッシュされているため、16 byte 境界になっていません。

上記のコードの場合は、RBP の値を変更していないため、わざわざ RBP をプッシュする必要はありませんが、ここでは、スタックの境界位置を揃える目的で使用しています。

試しに、PUSH RBP と POP RBP を消して実行してみると、Segmentation fault になります。
(呼び出す関数によっては、強制終了にならない場合もあります)
引数
printf の引数として、RDI, ESI, EDX, XMM0 レジスタに、「文字列のポインタ」「%c の値」「%d の値」「%.4f の値」を格納します。

レジスタ、スタックのどちらに値を格納するにしても、常に 8 byte 単位になるため、引数に値を格納する際は、厳密なサイズを気にする必要はありません。
%c に対応する引数に対して、char, short, int, long 型、いずれのサイズの数値を格納しても良いということです。

「mov esi, 'a'」ではなく、「mov sil, 'a'」としても、結果は変わりません。

なお、XMM レジスタには、即値を代入することができないので、汎用レジスタに値を入れてから、MOVQ 命令を使ってコピーしています。

最後の AL は、可変数引数の隠し要素である、XMM レジスタの使用数です。ここでは 1 となります。
CALL の呼び出し方
CALL 命令を使って、共有ライブラリの関数を呼び出す場合は、関数のシンボル名を直接指定するだけでは、PIE (位置独立実行形式) に対応できません。

オブジェクトファイルや静的ライブラリ内の関数の場合は、リンク時に、.text セクションに実際のコードが配置されるため、通常の CALL 命令で、相対位置による移動先の指定ができます。

しかし、共有ライブラリの場合、実際のコードは、別の場所にロードされているため、相対位置で直接アドレス指定をすることができません。

共有ライブラリの関数を、PIC (位置独立コード) に対応する形で呼び出したい場合は、「call printf wrt ..plt」というように、関数シンボル名の後に「wrt ..plt」を付けます。
WRT
NASM の WRT (With Reference To) は、左側のシンボルを参照する時に、どのような形で扱うかを指定することができます。
空白を開けて、右側に、種類を示す特殊シンボルを指定します。

WRT は、共有ライブラリを作る時や、共有ライブラリ内の関数を呼び出す時などに使います。

..plt は、CALL 命令または JMP 命令で、共有ライブラリの関数を参照したい時に使います。

この場合、移動先のアドレスは、.plt セクション内のコード位置になり、ここから、いくつかの処理を経た上で、実際の関数に飛ぶ形になっています。
逆アセンブル
サンプルの実行ファイルを、逆アセンブル&情報を表示してみると、以下のようになっています。

# .rela.plt セクション

  オフセット      情報           型             シンボル値    シンボル名 + 加数
000000004000  000300000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0

# .got.plt セクション

0x00003fe8 e03d0000 00000000 00000000 00000000 .=..............
                             ↑0x3FF0
0x00003ff8 00000000 00000000 36100000 00000000 ........6.......
           ↑3FF8            ↑0x4000

# .plt セクション

0000000000001020 <printf@plt-0x10>:
    1020:    ff 35 ca 2f 00 00        push   QWORD PTR [rip+0x2fca]   # 3ff0 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:    ff 25 cc 2f 00 00        jmp    QWORD PTR [rip+0x2fcc]   # 3ff8 <_GLOBAL_OFFSET_TABLE_+0x10>
    102c:    0f 1f 40 00              nop    DWORD PTR [rax+0x0]

0000000000001030 <printf@plt>:
    1030:    ff 25 ca 2f 00 00        jmp    QWORD PTR [rip+0x2fca]   # 4000 <printf@GLIBC_2.2.5>
    1036:    68 00 00 00 00           push   0x0
    103b:    e9 e0 ff ff ff           jmp    1020 <_init+0x20>  #-> printf@plt-0x10

# .text セクション

0000000000001160 <testfunc>:
    1160:    55                       push   rbp
    1161:    48 8d 3d 25 00 00 00     lea    rdi,[rip+0x25]        # 118d <message>
    1168:    be 61 00 00 00           mov    esi,0x61
    116d:    ba 9c ff ff ff           mov    edx,0xffffff9c
    1172:    48 b8 58 39 b4 c8 76     movabs rax,0x3ff3be76c8b43958
    1179:    be f3 3f 
    117c:    66 48 0f 6e c0           movq   xmm0,rax
    1181:    b8 01 00 00 00           mov    eax,0x1
    1186:    e8 a5 fe ff ff           call   1030 <printf@plt>
    118b:    5d                       pop    rbp
    118c:    c3                       ret

※movabs は、64bit 即値の MOV であることをわかりやすく表示するために置き換えているだけで、実際は MOV 命令です。

call 1030
0000000000001030 <printf@plt>:
    1030:    ff 25 ca 2f 00 00        jmp    QWORD PTR [rip+0x2fca]   # 4000 <printf@GLIBC_2.2.5>
    1036:    68 00 00 00 00           push   0x0
    103b:    e9 e0 ff ff ff           jmp    1020 <_init+0x20>  #-> printf@plt-0x10

    1186:    e8 a5 fe ff ff           call   1030 <printf@plt>

testfunc 内の CALL 命令は、「call 1030」となっていますが、実際に相対オフセット値としてエンコードされている値は、0xFFFFFEA5 (-347) です。

CALL の次の位置 0x118B から -347 の相対位置なので、4144 (0x1030) となり、この実行ファイル内のアドレスとしては、0x1030 になります。

0x1030 のアドレス位置を確認すると、.plt セクション内の位置 (printf@plt) となっています。

printf@plt
# .got.plt
0x00003fe8 e03d0000 00000000 00000000 00000000 .=..............
                             ↑0x3FF0
0x00003ff8 00000000 00000000 36100000 00000000 ........6.......
           ↑3FF8            ↑0x4000

# .plt
0000000000001030 <printf@plt>:
    1030:    ff 25 ca 2f 00 00        jmp    QWORD PTR [rip+0x2fca]   # 4000 <printf@GLIBC_2.2.5>
    1036:    68 00 00 00 00           push   0x0
    103b:    e9 e0 ff ff ff           jmp    1020 <_init+0x20>  #-> printf@plt-0x10

printf@plt では、jmp QWORD PTR [rip+0x2fca] で、指定メモリ位置から読み込んだ、絶対アドレスにジャンプしています。
この実行ファイル内のアドレスとしては、0x1036 + 0x2FCA となるので、0x4000 です。

0x4000 のアドレスに関しては、.rela.plt セクションに情報があります。
ただ、実際には、.got.plt セクション内のアドレスなので、ダンプして 0x4000 の位置を見てみると、値は 0x1036 (64bit) です。

0x1036 のアドレスは、printf@plt 内の、「push 0」の位置です。
つまり、結果として、すぐ下の位置にジャンプします。

この時の PUSH の値は、呼び出す関数の、.rela.plt におけるインデックス値を示しているようです。

その後、「jmp 1020」で printf@plt-0x10 にジャンプしています。

printf@plt-0x10
# .got.plt
0x00003fe8 e03d0000 00000000 00000000 00000000 .=..............
                             ↑0x3FF0
0x00003ff8 00000000 00000000 36100000 00000000 ........6.......
           ↑3FF8            ↑0x4000

# .plt
0000000000001020 <printf@plt-0x10>:
    1020:    ff 35 ca 2f 00 00        push   QWORD PTR [rip+0x2fca]   # 3ff0 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:    ff 25 cc 2f 00 00        jmp    QWORD PTR [rip+0x2fcc]   # 3ff8 <_GLOBAL_OFFSET_TABLE_+0x10>
    102c:    0f 1f 40 00              nop    DWORD PTR [rax+0x0]

ここで、push QWORD PTR [rip+0x2fca] により、0x1026 + 0x2FCA = 0x3FF0 の位置から 64bit 値が読み込まれて、プッシュされています。
0x3FF0 は .got.plt セクション内の位置で、値は 0 です。
ただし、この値は、共有ライブラリがロードされた時に変更されるので、ここでは、実際の値は確認できません。

そして、jmp QWORD PTR [rip+0x2fcc] で、0x102C + 0x2FCC = 0x3FF8 の位置から、絶対アドレスを読み込んで、ジャンプしています。
0x3FF8 も同様に .got.plt セクション内の位置ですが、共有ライブラリのロード時に変更されるので、ここでは実際の値は確認できません。

実際は、glibc の _dl_runtime_resolve 関数が呼ばれ、これまでにプッシュされた値の情報を元に、関数のアドレスが解決され、.got.plt セクション内の 0x4000 の位置の値が、実際の printf の絶対アドレスに置き換わります。

そうすると、次に printf がコールされる時は、printf@plt 内の最初のジャンプで、実際の関数位置に飛ぶことになります。