mizuiro_rivi’s diary

日々の生活を連ねるブログ

動的ライブラリのリンク先を変更させる

tags: libc patchelf linux

この記事は備忘録であり、自分自身の実験に基づくものです。 正確性を保証出来ません。(自分の環境では動きました。) (もし、間違っている点があれば指摘していただけると幸いです。)

やり方だけ参照するならこのリンクを踏んでください

前提知識

ライブラリとは

ライブラリとなんでしょうか? 普段からプログラミングをしている方なら、外部からファイルを取り込むことは誰しもがやったことがあると思います。 これから、低レイヤのライブラリの取り扱いについて語っていこうと思います。 まずライブラリは2つの種類があります。 * 静的ライブラリ * 動的ライブラリ(共有ライブラリ)

Linux のバイナリは、コンパイルの時に ld に対して -static オプションが指定されていない限り、動的リンク (実行時リンク) が標準になります。

それぞれの長所と短所

静的ライブラリとリンクには、動的なライブラリとリンクと比較した場合、主に 3 つの問題に注意が必要です。 * 静的ライブラリは自己依存性 (独立性) に優れていますが、適用性に劣る。 実行可能ファイルを静的にリンクすると、必要なライブラリルーチンは実行可能バイナリファイルの一部となります。 しかし、静的にリンクすると、静的ライブラリルーチンを更新する必要が出てきた場合 リンクし、生成し直さなければ、更新されたライブラリを利用することができません。

動的ライブラリを使用すれば、ライブラリは a.out ファイルの一部とはならず、リンクは実行時に行われます。更新された動的ライブラリを利用するために必要なことは、新しいライブラリをシステムにインストールするだけです。 * 動的ライブラリは必要ない機能を実行時削げる為、最適化しやすい。 動的リンカにはLazy Bindingという仕組みがあります。 実際に関数が呼ばれるまで、テーブルにはありますが、 アドレス解決は行われません。つまり、ロードされないのです。 こういった最適化は静的ライブラリではやりずらいことが多いです。 * 静的ライブラリのリンクでは、リンクの順序が重要です。 リンカーは、コマンド行に現れる順番、すなわち左から右に入力ファイルを処理します。リンカーがライブラリの要素を読み込むべきかどうかは、すでに処理されたライブラリの要素によって決定されます。この順番は、要素がライブラリファイル中で現れる順番に依存するだけでなく、コンパイルコマンド行上で指定されたライブラリの順番にも依存します。

ライブラリのロード、呼び出し法について

まず、実行ファイルというのはそれ単体であることが少ないです。 以下のよくあるc言語チュートリアルを見てもらうとよく分かるのですが、なにかしら、ライブラリを含むことが多いです。 ここでいうとstdio.hですね。

#include<stdio.h>
int main(){
    printf("Hello World\n");
}

これは、なにもinclude文がなかったら、ライブラリを含まないわけではありません。

int main(){}

上の何もしないソースコードにはライブラリはロードされるでしょうか?

実はされるんです

lddとはコマンドラインで指定された各プログラムまたは共有ライブラリに必要な共有ライブラリを出力するコマンドです。 lddを使って実際にどんなライブラリがロードされているか見てみましょう。

mizuiro@mizuiro-arch ~/PROJECTS> gcc test.c -o test
mizuiro@mizuiro-arch ~/PROJECTS> ldd test
    linux-vdso.so.1 (0x00007fffb94b4000)
    libc.so.6 => /usr/lib/libc.so.6 (0x00007fbf51b21000)
    /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007fbf51d63000)

はい、見に覚えのないライブラリがロードされていることが確認できます。おかしいですね。何もしないプラグラムなのに...

mizuiro@mizuiro-arch ~/PROJECTS> gcc test.c -o test
mizuiro@mizuiro-arch ~/PROJECTS> ./test
mizuiro@mizuiro-arch ~/PROJECTS> 

これは、実際にmain関数を呼ぶ場合はいくつかの処理を終えてから呼ばれるからです。

main関数というのはlibc_start_main関数の中で呼ばれます。(古いバージョン) このlibc_start_mainは動的ライブラリ(共有ライブラリ)で定義されています。

ldについて

さて、ここで話が変わりますが、ldとはどういったものなのか詳しく説明していこうと思います。

ld.so と ld-linux.so* はプログラムに必要な共有ライブラリを見つけてロードし、 プログラムの実行を準備してから起動させるものです。これがないと、実行させる際に動的にライブラリをロードさせてあげることが出来なくなるのです。

やり方

動的ライブラリのリンク先を変更させる方法はいくつかあるようですが、ここではpatchelfを使った方法を使っていきます。 まずは、patchをあてたい実行ファイルと動的ライブラリを用意します。今回はlibc2.27をあてることにします。 次に対応するldを用意します。 そうしたら、libcとldをルート直下のmylibにでも置いておきましょうか。

root@03a37eb13ba2:/mylib# ls -al
total 2160
drwxr-xr-x 2 root root    4096 Mar 22 22:03 .
drwxr-xr-x 1 root root    4096 Mar 22 22:00 ..
-rwxr-xr-x 1 root root  170960 Mar 22 22:03 ld-2.27.so
-rwxr-xr-x 1 root root 2030544 Mar 22 22:03 libc-2.27.so

次に、実行ファイルを用意します。

root@03a37eb13ba2:/PROJECTS# ls
test  test.c
root@03a37eb13ba2:/PROJECTS# cat test.c
#include<stdio.h>
int main(){
    printf("Hello World\n");
    return 0;
}
root@03a37eb13ba2:/PROJECTS# ldd test
    linux-vdso.so.1 (0x00007ffd6fba1000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f422b1c0000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f422b3bd000)

こいつのlibcのバージョンはいくつでしょうか。確認してみます。

oot@03a37eb13ba2:/PROJECTS# /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.7) stable release version 2.31.
...

2.31だそうですね。それではこれが2.27になるようにしていきます。

まずは、ライブラリの名前を合わせるために libc.so.6mylibにつくります。 シンボリックリンクを貼ってあげましょう。

root@03a37eb13ba2:/mylib# ln -s ./libc-2.27.so ./libc.so.6
root@03a37eb13ba2:/mylib# ls
ld-2.27.so  libc-2.27.so  libc.so.6

これで用意を終わったのでコマンドを実行します。 --set-rpathはライブラリのディレクトリを指します。 --set-interpreterでldを指定してあげて、最後に実行ファイルを指定してあげれば良いです。

root@03a37eb13ba2:/PROJECTS# patchelf --set-rpath /mylib/ --set-interpreter /mylib/ld-2.27.so ./test        
root@03a37eb13ba2:/PROJECTS# ldd test
    linux-vdso.so.1 (0x00007ffe5dfe8000)
    libc.so.6 => /mylib/libc.so.6 (0x00007f0caf2ed000)
    /mylib/ld-2.27.so => /lib64/ld-linux-x86-64.so.2 (0x00007f0caf6e6000)
root@03a37eb13ba2:/PROJECTS# ./test
Hello World

どうやら、ちゃんとパッチを当てることができたようですね。 一見落着(?)

libc-2.35での振る舞い(付録的なあれ)

ちなみに、現在のmainは__libc_start_call_mainで呼ばれます。(2.35)

mizuiro@mizuiro-arch ~/PROJECTS [1]> ldd test
    linux-vdso.so.1 (0x00007ffe1b1eb000)
    libc.so.6 => /usr/lib/libc.so.6 (0x00007f5a1eaa1000)
    /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f5a1ece3000)
mizuiro@mizuiro-arch ~/PROJECTS> /usr/lib/libc.so.6
GNU C Library (GNU libc) stable release version 2.35.
...
0x00007fffffffcc88│+0x0000: 0x00007ffff7db0310  →  <__libc_start_call_main+128> mov edi, eax    ← $rsp
0x00007fffffffcc90│+0x0008: 0x0000000000000000
0x00007fffffffcc98│+0x0010: 0x0000555555555119  →  <main+0> push rbp
0x00007fffffffcca0│+0x0018: 0x00000001ffffcd80
0x00007fffffffcca8│+0x0020: 0x00007fffffffcd98  →  0x00007fffffffd17a  →  "/mnt/nvme0n1p7/test"
0x00007fffffffccb0│+0x0028: 0x0000000000000000
0x00007fffffffccb8│+0x0030: 0xd1bd3d699ca29e5e
0x00007fffffffccc0│+0x0038: 0x00007fffffffcd98  →  0x00007fffffffd17a  →  "/mnt/nvme0n1p7/test"
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x55555555511a <main+1>         mov    rbp, rsp
   0x55555555511d <main+4>         mov    eax, 0x0
   0x555555555122 <main+9>         pop    rbp
 → 0x555555555123 <main+10>        ret    
   ↳  0x7ffff7db0310 <__libc_start_call_main+128> mov    edi, eax
      0x7ffff7db0312 <__libc_start_call_main+130> call   0x7ffff7dc7d60 <exit>
      0x7ffff7db0317 <__libc_start_call_main+135> call   0x7ffff7e0d660 <__nptl_deallocate_tsd>
      0x7ffff7db031c <__libc_start_call_main+140> lock   dec DWORD PTR [rip+0x1ccda5]        # 0x7ffff7f7d0c8 <__nptl_nthreads>
      0x7ffff7db0323 <__libc_start_call_main+147> sete   al
      0x7ffff7db0326 <__libc_start_call_main+150> test   al, al

<__libc_start_call_main+128> mov edi, eaxmainのリターンアドレスとしてあるので、 その前の文が呼び出している部分でしょうか?

mizuiro@mizuiro-arch ~/PROJECTS> objdump -M intel -d /usr/lib/libc.so.6 | sed -n '/<__libc_start_call_main>:/,/^$/p'
000000000002d290 <__libc_start_call_main>:
   ...
   2d30e:   ff d0                   call   rax
   2d310:   89 c7                   mov    edi,eax
   2d312:   e8 49 7a 01 00          call   44d60 <exit>
   2d317:   e8 44 d3 05 00          call   8a660 <__GI___nptl_deallocate_tsd>
   ...

__libc_start_call_main+128なので2d310の部分ですね。 その直前が呼び出している部分です。call raxと書いてありますが、慣例でcall rax of call eaxとなるようです。もし、どこの部分でmainを呼んでいるのか確認したければ、 これを目印にするとよさそうです。

動かない時,

ldのパーミッション(755)は適正ですか? ldのグループと所有者は適正ですか?(どちらもroot)

連絡先

@mizuiro_rivi もし困ったときに、相談してくだされば、力になれるかもしれません。 ぜひ連絡ください。 また、間違った記述等あれば、こちらにお願いします。

Ref