PLTとGOTってなんだっけ

May 6, 2020 (Updated on: August 15, 2023)
by Keichi Takahashi

PLTとGOTが何だったか今までに何回も検索したので, 忘れないよう自分なりにまとめておくことにしました.

TL;DR PLTとGOTは共有ライブラリ関数のリロケーションを呼び出し時まで遅延するための仕組み

  • PLT (Procedure Linkage Table): 実行ファイルから直接呼ばれる. GOTから対応する共有ライブラリ関数のアドレスを取得し,間接ジャンプする.
  • GOT (Global Offsets Table): 共有ライブラリ関数のアドレス一覧. 初回に関数が呼ばれた際にアドレスが設定される.

概要

ELFバイナリにおいて共有ライブラリ関数の呼び出しは,まず実行ファイルから PLTにジャンプした後,PLTから共有ライブラリにジャンプするという2段構えになっています. なぜこのような回りくどい仕組みを採用しているかというと,大きな実行ファイル では共有ライブラリ関数の呼び出しが多数存在し,実行ファイルの起動時に 全ての呼び出しのリロケーションを実行すると,起動に時間がかかってしまうからです.

そこで,GOT/PLTはリロケーションをライブラリ関数の最初の呼び出しまで遅延する ことで,実行ファイルの起動時のオーバヘッドを削減します. 具体的には,プログラムがある共有ライブラリ関数を呼び出すと, その際に動的リンカが共有ライブラリから関数のアドレスを探し出し,GOTに設定します. PLTはGOTに設定されているアドレスを参照し,共有ライブラリへジャンプします.

もう少し細かい流れは下記の通りです:

初回の呼び出し

  1. 実行ファイルがPLT内のエントリを呼ぶ.
  2. PLTはGOTの対応するエントリが示すアドレスへジャンプする.初回の呼び出し時は, PLTにあるリロケーション処理のアドレスが設定されている.
  3. PLTがリロケーションのための準備を行い,動的リンカにジャンプする.
  4. 動的リンカがライブラリ関数のアドレスを解決し,GOTのエントリにライブラリ関数 のアドレスを上書きする.
  5. ライブラリ関数へジャンプする.

2回目以降の呼び出し

  1. 実行ファイルがPLT内のエントリを呼ぶ.
  2. PLTはGOTが示す共有ライブラリの関数へジャンプする.

PLTとGOTの動作を確かめてみる

理解を深めるため,実際に手を動かして,PLTとGOTが動く仕組みを確かめてみました.

下準備

まず,gcc -no-pie -o hello hello.cで次のソースコードをコンパイルします. -no-pieフラグでPIE (と後述するRELRO) を切ります.

#include <stdio.h>

int main()
{
    puts("hello, world");
    puts("hello, again");
}

生成された実行可能ファイルをlddで調べると,libcと動的リンカ (ld) に依存してい ることがわかります.

$ ldd hello
	linux-vdso.so.1 (0x00007fff5efe1000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb18c594000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fb18c795000)

readelf -a helloすると,関係ありそうなセクションが見えます.

...
  [13] .plt              PROGBITS         0000000000401020  00001020
       0000000000000020  0000000000000010  AX       0     0     16
  [14] .plt.sec          PROGBITS         0000000000401040  00001040
       0000000000000010  0000000000000010  AX       0     0     16
...
  [23] .got              PROGBITS         0000000000403ff0  00002ff0
       0000000000000010  0000000000000008  WA       0     0     8
  [24] .got.plt          PROGBITS         0000000000404000  00003000
       0000000000000020  0000000000000008  WA       0     0     8
...

初回の呼び出し

gdbでhelloを起動し,mainをディスアセンブルすると,下記のようになります. puts@pltというアドレスを2回呼んでいることがわかります.

(gdb) disas
Dump of assembler code for function main:
=> 0x0000000000401136 <+0>:	endbr64
   0x000000000040113a <+4>:	push   %rbp
   0x000000000040113b <+5>:	mov    %rsp,%rbp
   0x000000000040113e <+8>:	lea    0xebf(%rip),%rdi        # 0x402004
   0x0000000000401145 <+15>:	callq  0x401040 <puts@plt>
   0x000000000040114a <+20>:	lea    0xec1(%rip),%rdi        # 0x402012
   0x0000000000401151 <+27>:	callq  0x401040 <puts@plt>
   0x0000000000401156 <+32>:	mov    $0x0,%eax
   0x000000000040115b <+37>:	pop    %rbp
   0x000000000040115c <+38>:	retq
End of assembler dump.

puts@pltは名前の通りPLTのエントリです. puts@pltをディスアセンブルすると,puts@got.pltが指すアドレスへジャンプしていま す.

(gdb) disas 'puts@plt'
Dump of assembler code for function puts@plt:
   0x0000000000401040 <+0>:	endbr64
   0x0000000000401044 <+4>:	bnd jmpq *0x2fcd(%rip)        # 0x404018 <puts@got.plt>
   0x000000000040104b <+11>:	nopl   0x0(%rax,%rax,1)
End of assembler dump.

puts@got.pltは,同じく名前の通りGOTのエントリです. この時点では,0x401030という.plt内のアドレスになっています.

(gdb) x/a 0x404018
0x404018 <puts@got.plt>:	0x401030

ジャンプ先の.plt内の処理では,GOTの先頭アドレスと,GOTにおけるputsのエントリの インデックスをスタックにプッシュし,動的リンカ (ld) へジャンプしています. ldはglibcからputsのアドレスを解決した後,そのアドレスをputs@got.pltに書き込みま す.その後,glibcのputs本体にジャンプします.

2回目以降の呼び出し

2回目のputsの呼び出しでputs@got.pltの中身を調べると,下記の通り, puts本体のアドレスが設定されていることがわかります.

(gdb) x/a 0x404018
0x404018 <puts@got.plt>:	0x7ffff7e555a0 <__GI__IO_puts>

メモ

  • 何らかの方法で攻撃者がGOTへ値を書き込めてしまうと,任意コード実行が成立して してしまいます.そのため,プログラム起動時にGOTのエントリを全て埋めた後, GOTをread-onlyに設定する,RELRO (Relocation Read-Only) という機能があります.
  • 共有ライブラリ関数の呼び出しをトレースするltraceというツールがありますが, ltraceはPLTにブレークポイントを書き込むことによってトレースを実現しています.

参考URL