RPATH和RUN-PATH
背景
需有簡單的linux編程知識,瞭解動態庫是什麼。瞭解LD_LIBRARY_PATH的作用。
RPATH是什麼?
什麼是運行時(run-time)?運行時就是程序運行的時候(一句廢話)。我們知道,在程序運行的時候,會依賴一些動態庫,只有所依賴的庫文件在運行的機器上存在,才能運行程序。問題是如何找到這些庫?這些庫可能在不同的目錄中,每個人的操作系統中的目錄結構可能都不一樣。運行時搜索路徑即提供了一些路徑,程序運行的時候去這些路徑下搜索程序所依賴的庫。這些路徑被稱為rpath(run-time search path,即運行時搜索路徑)。
運行時動態庫搜索路徑有哪些?
一般情況下,我們可以知道的路徑有如下幾個:
- 環境變量LD_LIBRARY_PATH指定的目錄列表。
- /etc/ld.so.cache 緩存文件,通常包含 /etc/ld.so.conf 文件編譯出的二進制列表(比如 CentOS 上,該文件會使用 include 從而使用 ld.so.conf.d 目錄下面所有的 *.conf 文件,這些都會緩存在 ld.so.cache 中)
-
系統的默認路徑。如/lib,/usr/lib等。
是否還可以設置別的路徑呢?當然可以,而且可以在編譯的時候就可以指定。
什麼情況下需要在編譯時就指定呢?一種場景是:我們在自己的設備上編譯好一個程序及其所有依賴的庫,如果另一個人(假設和我們的系統相同)想使用我們的程序,直接拷貝我們的程序到對方電腦上就可以運行。這個時候,如果我們依賴的庫都是我們自己編寫的,那麼除非我們顯式指定這些so的路徑(或者拷貝到系統的默認路徑中),否則就無法運行。是否可以讓用户直接拷貝程序到自己系統上,而不用修改別的內容呢?這個時候就可以通過在編譯時指定rpath路徑來實現。這種程序一般稱為便攜式軟件(portable software)。RPATH的設置
RPATH有兩個比較相近的名稱:rpath和runpath。這兩個往往容易讓人搞混。在最初的時候,ELF文件只有一個DT_RPATH標籤用來表示rpath列表,後來ELF規範棄用了DT_RPATH,並引入了一個新的標籤--DT_RUNPATH用於rpath列表。這兩個標籤的區別是它們相對於LD_LIBRARY_PATH環境變量的相對優先級。當鏈接器在程序運行時搜索動態庫時,DT_RPATH有較高的優先級,DT_RUNPATH有較低的優先級。即搜索順序如下:
- DT_RPATH指定的目錄列表。
- 環境變量LD_LIBRARY_PATH指定的目錄列表。
- DT_RUNPATH指定的目錄列表
- /etc/ld.so.cache 緩存文件,通常包含 /etc/ld.so.conf 文件編譯出的二進制列表(比如 CentOS 上,該文件會使用 include 從而使用 ld.so.conf.d 目錄下面所有的 *.conf 文件,這些都會緩存在 ld.so.cache 中)
- 系統的默認路徑。如/lib,/usr/lib等。
查看RPATH
對於任意的ELF文件(如可執行程序和so),可以使用readelf -d xxx | grep PATH 來查看,如果有RUNPATH或者RPATH,則表示ELF文件中設置了RPATH路徑。如
readelf -d libtest.so
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN:/home/test]
如何設置RPATH
gcc編譯器有-Wl,-rpath選項可以設置RPATH,如下所示:
gcc -Wl,-rpath,dir1 test.c
如想設置多個目錄,則每個目錄之間用分號分割即可,如下所示:
gcc -Wl,-rpath,dir1:dir2:...:dirN test.c
例子如下:
[zy@fedora rpath]$ gcc --version
gcc (GCC) 13.2.1 20231205 (Red Hat 13.2.1-6)
Copyright © 2023 Free Software Foundation, Inc.
[zy@fedora rpath]$ tree .
.
├── liba.c
├── liba.h
├── libb.c
├── libb.h
└── main.c
[zy@fedora rpath]$ cat libb.c
#include <stdio.h>
#include <stdlib.h>
int libb_func(int a, int b)
{
printf("this is libb_func, sum is %d.\n", a + b);
return 0;
}
[zy@fedora rpath]$ cat libb.h
#ifndef __LIBB_FUNC__
#define __LIBB_FUNC__
int libb_func(int a, int b);
#endif
[zy@fedora rpath]$ cat liba.c
#include <stdio.h>
#include <stdlib.h>
#include "libb.h"
int liba_func(int a, int b)
{
int sum = 0;
sum = a + b;
printf("%d + %d = %d.\n", a, b, sum);
a = a * a;
b = b * 2;
sum = a * b;
printf("sum = %d.\n", sum);
libb_func(6, 6);
return 0;
}
[zy@fedora rpath]$ cat liba.h
#ifndef __LIBA_FUNC__
#define __LIBA_FUNC__
int liba_func(int a, int b);
#endif
[zy@fedora rpath]$ cat main.c
#include <stdio.h>
#include <stdlib.h>
#include "liba.h"
int main()
{
int main_a = 3;
int main_b = 5;
int main_sum = 0;
main_a = main_a * main_a;
main_b = main_b * 2;
main_sum = main_a * main_b;
printf("main_sum = %d.\n", main_sum);
liba_func(1, 2);
return 0;
}
[zy@fedora rpath]$ gcc main.c -L. -I. -la -g -Wl,-rpath,/home/zy/rpath:/home/zy -o test
[zy@fedora rpath]$ readelf -d test |grep -i path
0x000000000000001d (RUNPATH) Library runpath: [/home/zy/rpath:/home/zy]
可以看到,使用gcc編譯後,生成的二進制文件中添加了RUNPATH。上文説到其實還有一個是RPATH,為什麼傳給鏈接器的是-rpath選項,而編譯出來的卻是RUNPATH呢?這個和鏈接器有關係,編譯的時候添加了-rpath選項後,鏈接器默認使用runpath,而不是rpath。編譯的時候,可以添加--disable-new-dtags選項給鏈接器,表示使用rpath。如下所示:
[zy@fedora rpath]$ gcc main.c -L. -I. -la -g -Wl,-rpath,/home/zy/rpath:/home/zy -Wl,--disable-new-dtags -o test
[zy@fedora rpath]$ readelf -d test |grep -i path
0x000000000000000f (RPATH) Library rpath: [/home/zy/rpath:/home/zy]
如果有些鏈接器默認使用rpath,而不是runpath,想強制使用runpath的話,可以添加--enable-new-dtags選項。如下所示:
[zy@fedora rpath]$ gcc main.c -L. -I. -la -g -Wl,-rpath,/home/zy/rpath:/home/zy -Wl,--enable-new-dtags -o test
[zy@fedora rpath]$ readelf -d test |grep -i path
0x000000000000001d (RUNPATH) Library runpath: [/home/zy/rpath:/home/zy]
$ORIGIN問題
使用rpath或者runpath指定一個路徑後,會存在一個問題。比如我們編譯的時候設定了一個/home/test路徑,但我們將程序打包給其他人用的時候,其他人的環境不一定將包放到這個目錄,那麼依然會報找不到庫。為了解決這個問題,編譯器提供了一個特殊的目錄,$ORIGIN,它在動態鏈接時表示文件所在的當前路徑。這是一個非常有用的功能,尤其是當我們需要創建一個可移植的應用程序或庫時。需要注意的是,$ORIGIN需要加引號,如果不加引號,會當成普通的字符串。編譯時傳遞的flags為-Wl,-rpath,'$ORIGIN'或'-Wl,-rpath,$ORIGIN',如下所示:
[zy@fedora rpath]$ gcc main.c -L. -I. -la -g -Wl,-rpath,'$ORIGIN' -o test
[zy@fedora rpath]$ readelf -d test |grep -i path
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN]
[zy@fedora rpath]$ gcc main.c -L. -I. -la -g '-Wl,-rpath,$ORIGIN' -o test
[zy@fedora rpath]$ readelf -d test |grep -i path
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN]
rpath和runpath其它方面的不同
上面説了rpath和runpath在加載程序時,搜索路徑的優先級是不同的,除了這個區別外,還有以下區別:
- 如果同時有rpath和runpath,那麼rpath是失效的,只有runpath有效。
-
rpath和runpath對間接依賴庫的作用。
在搜索程序或庫的間接依賴時,rpath和runpath是不同的,rpath設置的路徑對間接庫的搜索也生效,即搜索間接庫時,也會優先從rpath指定的路徑中搜索。而runpath設置的路徑在對間接庫搜索時是不起作用的。所謂的間接庫是指一個庫所依賴的庫中又依賴的別的庫,如test程序依賴liba.so,而liba.so又依賴libb.so,那麼對於test程序來説,libb.so就是一個間接依賴庫。那麼加載器在加載test程序時,尋找libb.so的時候,rpath和runpath的作用是不同的,如下所示:[zy@fedora rpath]$ gcc main.c -L. -I. -la -g -Wl,-rpath,'$ORIGIN' -o test [zy@fedora rpath]$ [zy@fedora rpath]$ ldd test linux-vdso.so.1 (0x00007fff3b538000) liba.so => /home/zy/rpath/./liba.so (0x00007f09a9203000) libc.so.6 => /lib64/libc.so.6 (0x00007f09a900a000) /lib64/ld-linux-x86-64.so.2 (0x00007f09a920a000) libb.so => not found [zy@fedora rpath]$ readelf -d test |grep -i path 0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN] [zy@fedora rpath]$ gcc main.c -L. -I. -la -g -Wl,-rpath,'$ORIGIN' -Wl,--disable-new-dtags -o test [zy@fedora rpath]$ readelf -d test |grep -i path 0x000000000000000f (RPATH) Library rpath: [$ORIGIN] [zy@fedora rpath]$ ldd test linux-vdso.so.1 (0x00007fffd1272000) liba.so => /home/zy/rpath/./liba.so (0x00007f6fcac3b000) libc.so.6 => /lib64/libc.so.6 (0x00007f6fcaa42000) libb.so => /home/zy/rpath/./libb.so (0x00007f6fcaa3d000) /lib64/ld-linux-x86-64.so.2 (0x00007f6fcac42000)從上面可以看出,當使用runpath時,libb.so是無法找到的。
查看rpath
readelf -d xxx
可以看到類似這一行:
0x000000000000001d (RUNPATH) Library runpath: [$ORIGIN:/home/test]
該用哪一個?
如果必須要使用rpath或runpath,建議還是使用runpath。因為最開始是隻有rpath的,為什麼後來又增加了runpath呢?而且runpath和rpath同時存在的時候,只有runpath生效。因為只有rpath的情況下,一旦設置了rpath,那麼在運行時,其優先級是最高的,且我們無法通過其它手段(如通過設置LD_LIBRARY_PATH等)覆蓋默認的庫路徑,我們必須重新編譯程序才能加載其它路徑下的庫,這對於某些情況下是很不方便的。而使用runpath的時候,我們可以很方便的通過LD_LIBRARY_PATH變量去覆蓋默認的路徑。
注意事項
runpath和rpath的行為,和linux發行版中鏈接器的實現有關,不同的發行版,可能實現並不相同。
參考資料
The Linux Programming Interface --41.9(upgrading shared libraries) 41.11 finding shared libraries at run time
C_C++ 庫的動態鏈接,深入理解動態鏈接器:RPATH, RUNPATH與$ORIGIN