サンプルコード
以下は、C 標準ライブラリの printf を使って、文字列を表示するサンプルです。
C ソースは test2.c を使います。
まず、printf のアドレスを参照できるようにするため、extern で、外部参照であることを宣言しておきます。
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 になります。
(呼び出す関数によっては、強制終了にならない場合もあります)
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 となります。
レジスタ、スタックのどちらに値を格納するにしても、常に 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」を付けます。
オブジェクトファイルや静的ライブラリ内の関数の場合は、リンク時に、.text セクションに実際のコードが配置されるため、通常の CALL 命令で、相対位置による移動先の指定ができます。
しかし、共有ライブラリの場合、実際のコードは、別の場所にロードされているため、相対位置で直接アドレス指定をすることができません。
共有ライブラリの関数を、PIC (位置独立コード) に対応する形で呼び出したい場合は、「call printf wrt ..plt」というように、関数シンボル名の後に「wrt ..plt」を付けます。
WRT
NASM の WRT (With Reference To) は、左側のシンボルを参照する時に、どのような形で扱うかを指定することができます。
空白を開けて、右側に、種類を示す特殊シンボルを指定します。
WRT は、共有ライブラリを作る時や、共有ライブラリ内の関数を呼び出す時などに使います。
..plt は、CALL 命令または JMP 命令で、共有ライブラリの関数を参照したい時に使います。
この場合、移動先のアドレスは、.plt セクション内のコード位置になり、ここから、いくつかの処理を経た上で、実際の関数に飛ぶ形になっています。
空白を開けて、右側に、種類を示す特殊シンボルを指定します。
WRT は、共有ライブラリを作る時や、共有ライブラリ内の関数を呼び出す時などに使います。
..plt は、CALL 命令または JMP 命令で、共有ライブラリの関数を参照したい時に使います。
この場合、移動先のアドレスは、.plt セクション内のコード位置になり、ここから、いくつかの処理を経た上で、実際の関数に飛ぶ形になっています。
逆アセンブル
サンプルの実行ファイルを、逆アセンブル&情報を表示してみると、以下のようになっています。
※movabs は、64bit 即値の MOV であることをわかりやすく表示するために置き換えているだけで、実際は MOV 命令です。
testfunc 内の CALL 命令は、「call 1030」となっていますが、実際に相対オフセット値としてエンコードされている値は、0xFFFFFEA5 (-347) です。
CALL の次の位置 0x118B から -347 の相対位置なので、4144 (0x1030) となり、この実行ファイル内のアドレスとしては、0x1030 になります。
0x1030 のアドレス位置を確認すると、.plt セクション内の位置 (printf@plt) となっています。
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 にジャンプしています。
ここで、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 内の最初のジャンプで、実際の関数位置に飛ぶことになります。
# .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 内の最初のジャンプで、実際の関数位置に飛ぶことになります。