2012年9月6日 星期四

[參考文章]automake and autoconf



Abstract

Unix 底下,automake  autoconf 常被用來管理 C/C++ 的專案。 如果您的專案是根據 GNU 的標準建構出來的,那麼它們能夠幫您節省很多 make 以及 configure 的時間。然而,萬事起頭難。希望這篇指南能夠為剛入門的程式設計師提供一些有用的資訊。
我不是 Unix 以及 autoconf, automake 的專家,所以我歡迎有建設性的批評與指教。

make and configure

這個 make tool 能夠用來管理多個檔案的專案。make 會用到您專案裡的 Makefile 這個檔案,這個檔案列出了各式的編譯以及連結步驟,目的,以及一些相依關係。多檔的專案與 GNU make 工具程式是有很大的關係的。
configure 這個 script 被用來協助處理跨平台的編譯工作。一個合適的 configure script 能夠正確的解譯 Makefile.in 然後產生該平台相依的 Makefile。當然。產生 Makefile 之前必須經過很多測試來決定該平台的特性。
所以我們允許使用者執行 './configure' 然後再執行 'make' 來根據自身的平台而編譯這個專案。

aclocal.m4

很明顯的,大部分寫好的 Makefile configure script 看起來都非常的類似。事實上,GNU 提供了一些關於這些檔案的細部描述。因此,GNU 寫出了 automake 以及 autoconf 來簡化處理這些過程,並且確保 Makefile 以及 configure script 能夠遵守 GNU 標準。
這裡是這些工具的簡單的介紹。我這裡也提供了範例供您測試使用。
注意:這些工具使用 m4 程式語言。 aclocal 增加 aclocal.m4 這個檔案到您的專案目錄,因為包含了一些需要用到的 m4 巨集。

autoconf

autoconf 會尋找叫做 configure.in 這個檔案,然後會根據他找到的巨集而生出 configure script
注意要的是 autoscan 可以用來產生一個可能使用的 configure.in。它會測試你的原始檔案,然後放進一些建議值到一個叫做 configure.scan 的檔案。當然您必須改名成 configure.in
不管什麼時候,您在 configure.in 加入一個巨集時,然後必須執行 aclocal  autoconf,因為 aclocal 會掃描 configure.in 然後找出它必須提供的巨集。
Lines which every configure.in should have
每一個 configure.in 應該有底下這幾行:
AC_INIT(hello.cc)

AM_INIT_AUTOMAKE(hello,0.1)

AC_PROG_CC

AC_PROG_CXX

AC_PROG_INSTALL

AC_OUTPUT(Makefile)
AC_INIT
這個巨集以一個原始檔當作參數。它會檢查該檔是否存在,也會檢查目錄是否存在。
AM_INIT_AUTOMAKE
這行增加了幾個標準的檢查。它是以程式名稱以及版本號碼當作參數。
AC_PROG_CC
指出了該原始檔是用 C 寫成的。假如原始檔是用 C++ 寫成的,那麼應該改成AC_PROG_CXX
AC_PROG_INSTALL
會產生安裝目的地,好讓使用者打 make install 便能安裝這套軟體。
AC_OUTPUT
指出應該產生的 Makefile 檔案名稱。
Using a Config Header
AM_CONFIG_HEADER(config.h) 這行指出了您將會使用 config.h 檔。autoconf 將會需要 config.h.in ,並將之處理成 config.h。這是你的原始檔案中,透過 #define 定義的標頭檔,好提供一個方式讓人們為它們的平台自訂 configurationconfig.h.in 能夠被autoheader 這個工具程式自動產生。
然而,在你的專案中,你需要 stamp-h 這個檔案來確保 automake config.h.in 重新產生 config.h。執行 'touch stamp-h' 讓您的專案新增這個檔案。

automake

automake 會尋找 Makefile.am 這個檔案,然後依據它找到的巨集產生 Makefile.in。當然,這個會在稍後的 configure script 用到。
GNU-style projects, or not
因為預設值 automake 會嘗試產生 GNU-style 的專案,因此如果一些該有的檔案不存在,automake 會向您抱怨。所以您可以用以下指令產生需要的空白文件:
touch NEWS README AUTHORS ChangeLog
如果您不需要 GNU-style 的檔案,您可以在您的 Makefile.am 增加以下這行:
AUTOMAKE_OPTIONS = foreign
Telling automake about your source files
使用下面這兩行來命名您的程式名稱,以及列出它的原始程式:
bin_PROGRAMS = hello

hello_SOURCES = hello.h hello.cc main.cc
注意:第二個變數的前置是依據第一個變數的。

The Whole Process

假設您已經概略的編輯完 Makefile.am configure.in (底下有範例),藉由底下的指令,您應該能夠 build 您專案了:
autoheader

touch NEWS README AUTHORS ChangeLog

touch stamp-h

aclocal

autoconf

automake



./configure

make
當你想完整重新 build 專案,請重複最後五個步驟。
* autoheader - creates config.h.in
* aclocal - adds aclocal.m4 to directory.
* autoconf - creates configure from configure.in
* automake - Creates Makefile.in from Makefile.am
* ./configure - creates Makefile from Makefile.in

Sub Directories

然,專案的檔案應該被組織化的放在子目錄裡。理想上來說,原始檔和 makefileconfigure scripts,以及 README 等等這些檔案,應該都放在專案的 src 這個子資料匣中。多層式的專案我們把它叫做 Deep 專案。我會列出應該處理的步驟,但是你依然可以看看範例程式。
當使用子目錄的時候,你必須坐下的步驟:
1. 增加 SUBDIRS 這一項到最頂層的 Makefile.am。例如:
SUBDIRS = doc intl po src tests
注意這些資料匣的名稱要以空白區分。
2. 在每一個子目錄中都要新增一個 Makefile.am。但是不需要 configure.in。而且要確定最頂層的 configure.in 中的 AC_OUPUT 巨集要加入檔名以產生 Makefile
For sub directories containing additional source code
3. 增加 AC_PROG_RANLIB 巨集到您的 configure.in。這允許您在子目錄中 build code 能夠暫時放到某個暫存目錄,以便之後用來和其他 code 作連結之用。
4. 增加一些巨集到 src 下的任何原始檔目錄的 Makefile.am。這將會 build 一個不會安裝的函式庫。你需要給定以 lib 字母為開頭的函式庫名稱,並且指定該函式庫的原始碼以及標頭檔,例如:
noinst_LIBRARIES = libfoo.a

libfoo_a_SOURCES = foo.h foo.cc

INCLUDES = -I@top_srcdir@/src/includes
注意 SOURCES 巨集會使用底線 '_' 來代替函式庫的名稱的點 '.'。而且要使用 top_srcdir 這個變數來參考到專案的最頂端。
5. 在上一層目錄使用 Makefile.am  LDADD 這個巨集,來連結任何可能用到這個暫時函式庫的 Code。例如:
LDADD = foofiles/libfoo.a
For sub directories containing non-source files
3. 一個子目錄下的 Makefile.am 應該包含像底下的這行:
EXTRA_DIST = somefile.txt someotherfile.html
這會告訴 automake 您要發布這些檔案,但是它們不需要編譯。

Example Files

底下就是範例檔了。它是一個使用一個標準函式庫的 C++ 專案。
看看 autoconf automake 的手冊來參閱使用到的一些巨集以及變數。我不要這些範例因為註解而變的難以閱讀。
這個 Deep 專案的架構如下:
helloworld_cc
configure.in
Makefile.am
src
Makefile.am
helloworld.h
helloworld.cc
main.cc
foofiles
Makefile.am
foo.h
foo.cc
configure.in
AC_INIT(src/hello.cc)

AM_INIT_AUTOMAKE(hello,0.1)

AM_CONFIG_HEADER(config.h)

AC_PROG_CC

AC_PROG_CXX

AC_PROG_INSTALL

AC_PROG_RANLIB

AC_OUTPUT(Makefile src/Makefile src/foofiles/Makefile)
Makefile.am
SUBDIRS = src
Makefile.am for the src directory
bin_PROGRAMS = hello

INCLUDES = foofiles

hello_SOURCES = hello.h hello.cc main.cc

SUBDIRS = foofiles

LDADD = foofiles/libfoo.a
Makefile.am for foofiles directory under src
noinst_LIBRARIES = libfoo.a

libfoo_a_SOURCES = foo.h foo.cc

INCLUDES = -I@top_srcdir@/

圖解


1.autoscan (autoconf): 掃描源代碼以搜尋普通的可攜性問題,比如檢查編譯器,庫,頭文件等,生成檔configure.scan,它是configure.ac的一個雛形。
2.aclocal (automake):根據已經安裝的巨集,用戶定義巨集和acinclude.m4檔中的宏將configure.ac檔所需要的巨集集中定義到檔 aclocal.m4中。aclocal是一個perl 腳本程式,它的定義是:“aclocal - create aclocal.m4 by scanning configure.ac”

user input files   optional input     process          output files
================   ==============     =======          ============

                    acinclude.m4 - - - - -.
                                          V
                                      .-------,
configure.ac ------------------------>|aclocal|
                 {user macro files} ->|       |------> aclocal.m4
                                      `-------'
3.autoheader(autoconf): 根據configure.ac中的某些巨集,比如cpp巨集定義,運行m4,聲稱config.h.in

user input files    optional input     process          output files
================    ==============     =======          ============

                    aclocal.m4 - - - - - - - .
                                             |
                                             V
                                     .----------,
configure.ac ----------------------->|autoheader|----> autoconfig.h.in
                                     `----------'
4.automake: automake將Makefile.am中定義的結構建立Makefile.in,然後configure腳本將生成的Makefile.in檔轉換為Makefile。如果在configure.ac中定義了一些特殊的宏,比如AC_PROG_LIBTOOL,它會調用libtoolize,否則它會自己產生config.guess和config.sub

user input files   optional input   processes          output files
================   ==============   =========          ============

                                     .--------,
                                     |        | - - -> COPYING
                                     |        | - - -> INSTALL
                                     |        |------> install-sh
                                     |        |------> missing
                                     |automake|------> mkinstalldirs
configure.ac ----------------------->|        |
Makefile.am  ----------------------->|        |------> Makefile.in
                                     |        |------> stamp-h.in
                                 .---+        | - - -> config.guess
                                 |   |        | - - -> config.sub
                                 |   `------+-'
                                 |          | - - - -> config.guess
                                 |libtoolize| - - - -> config.sub
                                 |          |--------> ltmain.sh
                                 |          |--------> ltconfig
                                 `----------'
5.autoconf:將configure.ac中的宏展開,生成configure腳本。這個過程可能要用到aclocal.m4中定義的宏。

user input files   optional input   processes          output files
================   ==============   =========          ============

                   aclocal.m4 - - - - - -.
                                         V
                                     .--------,
configure.ac ----------------------->|autoconf|------> configure ----->autoconfig.h,Makefile


[參考文章]Linux下.o .so .a .la


linux下檔的類型是不依賴於其尾碼名的,但一般來講:
.o,是目標檔,相當於windows中的.obj文件
.so 為共用庫,shared object,用於動態連接的,dll差不多
.a為靜態庫,是好多個.o合在一起,用於靜態連接
.lalibtool自動生成的一些共用庫,vi編輯查看,主要記錄了一些設置資訊。能用如下命令查看*.la檔的格式   $file *.la
      *.la: ASCII English text
所以能用vi來查看其內容。
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
創建.a庫文件和.o庫文件:
[yufei@localhost perl_c2]$ pwd
/home/yufei/perl_c2
[yufei@localhost perl_c2]$ cat mylib.c
#include 
#include 
void hello(){
        printf("success call from perl to c libraryn");
}
[yufei@localhost perl_c2]$ cat mylib.h
extern void hello();
[yufei@localhost perl_c2]$ gcc -c mylib.c
[yufei@localhost perl_c2]$ dir
mylib.c  mylib.h  mylib.o
[yufei@localhost perl_c2]$ ar -r mylib.a mylib.o
ar: 正在創建 mylib.a
[yufei@localhost perl_c2]$ dir
mylib.a  mylib.c  mylib.h  mylib.o
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
動態連結程式庫*.so的編譯和使用- -
                                      
動態庫*.solinux下用cc++編程時經常會碰到,最近在網站找了幾篇文章介紹動態庫的編譯和鏈結,總算搞懂了這個之前一直不太瞭解得東東,這裏做個筆記,也為其他正為動態庫程式庫而苦惱的兄弟們提供一點幫助。
1、動態庫的編譯
下面通過一個例子來介紹怎麼生成一個動態庫。這裏有一個頭檔:so_test.h,三個.c文件:test_a.ctest_b.ctest_c.c,我們將這幾個檔編譯成一個動態庫:libtest.so
so_test.h
#include 
#include 
void test_a();
void test_b();
void test_c();
test_a.c
#include "so_test.h"
void test_a()
{
    printf("this is in test_a...n");
}
test_b.c
#include "so_test.h"
void test_b()
{
    printf("this is in test_b...n");
}
test_c.c
#include "so_test.h"
void test_c()
{
    printf("this is in test_c...n");
}
將這幾個檔編譯成一個動態庫:libtest.so
$ gcc test_a.c test_b.c test_c.c -fPIC -shared -o libtest.so
2、動態庫的鏈結
1、中,我們已成功生成了一個自己的動態連結程式庫libtest.so,下面我們通過一個程式來調用這個庫裏的函數。程式的原始檔案為:test.c
test.c
#include "so_test.h"
int main()
{
    test_a();
    test_b();
    test_c();
    return 0;
}
l         test.c和動態庫libtest.so鏈結生成執行檔test
$ gcc test.c -L. -ltest -o test
l         測試是否動態連接,如果列出libtest.so,那麼應該是連接正常了
$ ldd test
l         執行test,能看到他是怎麼調用動態庫中的函數的。
3、編譯參數解析
最主要的是GCC命令行的一個選項:
          -shared 該選項指定生成動態連接庫(讓連接器生成T類型的導出符號表,有時候也生成弱連接W類型的導出符號),不用該標誌外部程式無法連接。相當於一個可執行檔
l         -fPIC:表示編譯為位置獨立的代碼,不用此選項的話編譯後的代碼是位置相關的所以動態載入時是通過代碼拷貝的方式來滿足不同進程的需要,而不能達到真正代碼段共用的目的。
l         -L.:表示要連接的庫在當前目錄中
l         -ltest:編譯器查找動態連接庫時有隱含的命名規則,即在給出的名字前面加上lib,後面加上.so來確定庫的名稱
l         LD_LIBRARY_PATH:這個環境變數指示動態連接器能裝載動態庫的路徑。
l         當然如果有root許可權的話,能修改/etc/ld.so.conf檔,然後調用 /sbin/ldconfig來達到同樣的目的,不過如果沒有root許可權,那麼只能採用輸出LD_LIBRARY_PATH的方法了。
4、注意

     調用動態庫的時候有幾個問題會經常碰到,有時,明明已將庫的頭檔所在目錄 通過 “-I” include進來了,庫所在檔通過
“-L”參數引導,並指定了“-l”的庫名,但通過ldd命令察看時,就是死活未找到你指定鏈結的so檔,這時你要作的就是通過修改
LD_LIBRARY_PATH/etc/ld.so.conf檔來指定動態庫的目錄。通常這樣做就能解決庫無法鏈結的問題了。
makefile裏面怎麼正確的編譯和連接生成.so庫檔,然後又是在其他程式的makefile裏面怎麼編譯和連接才能調用這個庫檔的函數????
:
       你需要告訴動態鏈結器、載入器ld.so在哪里才能找到這個共用庫,能設置環境變數把庫的路徑添加到庫目錄/lib/usr/libLD_LIBRARY_PATH=$(pwd),這種方法採用命令行方法不太方便,一種替代方法
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^注釋^^^^^^^^^^^^^^^^^^^^^^^^^^^^
LD_LIBRARY_PATH
能在/etc/profile還是 ~/.profile還是 ./bash_profile裏設置,或.bashrc裏,
改完後運行source /etc/profile . /etc/profile
更好的辦法是添入/etc/ld.so.conf, 然後執行 /sbin/ldconfig
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^注釋^^^^^^^^^^^^^^^^^^^^^^^^^^^^
是把庫路徑添加到/etc/ld.so.conf,然後以root身份運行ldconfig
      也能在連接的時候指定檔路徑和名稱 -I  -L.
      GCC=gcc
CFLAGS=-Wall -ggdb -fPIC
#CFLAGS=
all: libfunc test
libfunc:func.o func1.o
        $(GCC) -shared -Wl,-soname,libfunc.so.1 -o libfunc.so.1.1 $
編譯目標檔時使用gcc-fPIC選項,產生和位置無關的代碼並能被載入到所有地址:
gcc -fPIC -g -c liberr.c -o liberr.o

使用gcc-shared-soname選項;
使用gcc-Wl選項把參數傳遞給連接器ld
使用gcc-l選項顯示的連接C庫,以確保能得到所需的啟動(startup)代碼,從而避免程式在使用不同的,可能不相容版本的C庫的系統上不能啟動執行。
gcc -g -shared -Wl,-soname,liberr.so -o liberr.so.1.0.0 liberr.o -lc

建立相應的符號連接:
ln -s liberr.so.1.0.0 liberr.so.1;
ln -s liberr.so.1.0.0 liberr.so;
MAKEFILE中:
$@
    表示規則中的目標檔集。在模式規則中,如果有多個目標,那麼,"$@"就是匹配於目標中模式定義的集合。
$%
   
僅當目標是函數庫檔中,表示規則中的目標成員名。例如,如果一個目標是"foo.a(bar.o)",那麼,"$%"就是"bar.o""$@"就是
"foo.a"。如果目標不是函數庫檔(Unix下是[.a],視窗系統下是[.lib]),那麼,其值為空。
$
*********************************************注釋***********************************************************************
test: test.o libfunc
        $(GCC) -o test test.o -L. -lfunc
%.o:%.c
        $(GCC) -c $(CFLAGS) -o $@ $ref:http://niefei.blog.ccidnet.com/blog/ccid/do_showone/tid_42855.html
1. 介紹
 
 使用GNU的工具我們怎麼在Linux下創建自己的程式函數庫?一個程式 函數庫簡單的說就是個檔包含了一些編譯好的代碼和資料,這些編
譯好的代碼和資料能在事後供其他的程式使用。程式函數庫能使整個程式更加模組化,更容易重新編譯,而且更方便升級。程式函數庫可分為3種類型:靜態函
數庫(static libraries)、共用函數庫(shared libraries)和動態載入函數庫(dynamically loaded
libraries)。
  靜態函數庫是在程式執行前就加入到目標程式中去了;而共用函數庫則是在程式啟動的時候載入到程式中,他能被
不同的程式共用;動態載入函數庫則能在程式運行的所有時候動態的載入。實際上,動態函數庫並非另外一種庫函數格式,差別是動態載入函數庫是怎麼被程式員
使用的。後面我們將舉例說明。
  本檔主要參考Program Library HOWTO,作者是luster
hwang@ustc.edu
),所有非商業目的的再次發行本檔都是允許的,不過請保留作者資訊和本版權聲明。本檔首先在
www.linuxaid.com.cn
發佈。
  2. 靜態函數庫
 
 靜態函數庫實際上就是簡單的一個普通的目標檔的集合,一般來說習慣用“.a”作為檔的尾碼。能用ar這個程式來產生靜態函數庫檔。Ar
archiver的縮寫。靜態函數庫目前已不在像以前用得那麼多了,主要是共用函數庫和之相比較有非常多的優勢的原因。慢慢地,大家都喜歡使用共用函數
庫了。不過,在一些場所靜態函數庫仍然在使用,一來是保持一些和以前某些程式的相容,二來他描述起來也比較簡單。
  靜態庫函數允許程式
員把程式link起來而不用重新編譯代碼,節省了重新編譯代碼的時間。不過,在今天這麼快速的電腦面前,一般的程式的重新編譯也花費不了多少時間,所以
這個優勢已不是像他以前那麼明顯了。靜態函數庫對研發者來說還是非常有用的,例如你想把自己提供的函數給別人使用,不過又想對函數的原始碼進行保密,你就
能給別人提供一個靜態函數庫檔。理論上說,使用ELF格式的靜態庫函數生成的代碼能比使用共用函數庫(或動態函數
庫)的程式運行速度上快一些,大概15%。
  創建一個靜態函數庫檔,或往一個已存在地靜態函數庫檔添加新的目標代碼,能用下面的命令:
ar rcs my_library.a file1.o file2.o
 
 這個例子中是把目標代碼file1.ofile2.o加入到my_library.a這個函數庫檔中,如果my_library.a不存在
則創建一個新的檔。在用ar命令創建靜態庫函數的時候,更有其他一些能選擇的參數,能參加ar的使用幫助。這裏不再贅述。
  一旦
你創建了一個靜態函數庫,你能使用他了。你能把他作為你編譯和連接過程中的一部分用來生成你的可執行代碼。如果你用gcc來編譯產生可
執行代碼的話,你能用“-l”參數來指定這個庫函數。你也能用ld來做,使用他的“-l”“-L”參數選項。具體用法,能參考info:gcc
 3. 共用函數庫
  共用函數庫中的函數是在當一個可執行程式在啟動的時候被載入。如果一個共用函數庫正常安裝,所有的程式在重新運行的時候都能自動載入最新的函數庫中的函數。對於Linux系統更有更多的能實現的功能:
o 升級了函數庫不過仍然允許程式使用老版本的函數庫。 o 當執行某個特定程式的時候能覆蓋某個特定的庫或庫中指定的函數。 o 能在庫函數被使用的過程中修改這些函數庫。
  3.1. 一些約定
 
 如果你要編寫的共用函數庫支援所有有用的特性,你在編寫的過程中必須遵循一系列約定。你必須理解庫的不同的名字間的差別,例如他的
“soname”“real
name”之間的差別和他們是怎麼相互作用的。你同樣還要知道你應該把這些庫函數放在你檔系統的什麼位置等等。下面我們具體看看這些問題。
  3.1.1. 共用庫的命名
  每個共用函數庫都有個特別的名字,稱作“soname”Soname名字命名必須以“lib”作為首碼,然後是函數庫的名字,然後是“.so”,最後是版本號資訊。不過有個特例,就是非常底層的C庫函數都不是以lib開頭這樣命名的。
  每個共用函數庫都有一個真正的名字(“real name”),他是包含真正庫函數代碼的檔。真名有一個主版本號,和一個發行版本號。最後一個發行版本號是可選的,能沒有。主版本號和發行版本號使你能知道你到底是安裝了什麼版本的庫函數。
另外,更有一個名字是編譯器編譯的時候需要的函數庫的名字,這個名字就是簡單的soname名字,而不包含所有版本號資訊。
 
 管理共用函數庫的關鍵是區分好這些名字。當可執行程式需要在自己的程式中列出這些他們需要的共用庫函數的時候,他只要用soname就能了;
反過來,當你要創建一個新的共用函數庫的時候,你要指定一個特定的檔案名,其中包含非常細節的版本資訊。當你安裝一個新版本的函數庫的時候,你只要先將這些
函數庫檔拷貝到一些特定的目錄中,運行ldconfig這個實用就能。Ldconfig檢查已存在的庫檔,然後創建soname的符號鏈結到真正
的函數庫,同時設置/etc/ld.so.cache這個緩衝檔。這個我們稍後再討論。
  Ldconfig並不設置鏈結的名字,通常
的做法是在安裝過程中完成這個鏈結名字的建立,一般來說這個符號鏈結就簡單的指向最新的soname
或最新版本的函數庫檔。最佳把這個符號鏈結指向soname,因為通常當你升級你的庫函數的後,你就能自動使用新版本的函數庫勒。
  我們來舉例看看:
 
 /usr/lib/libreadline.so.3
是個完全的完整的sonameldconfig能設置一個符號鏈結到其他某個真正的函數庫檔,例如是
/usr/lib/libreadline.so.3.0。同時還必須有一個鏈結名字,例如/usr/lib/libreadline.so
就是個符號鏈結指向/usr/lib/libreadline.so.3
3.1.2. 檔系統中函數庫檔的位置
 
 共用函數庫檔必須放在一些特定的目錄裏,這樣通過系統的環境變數設置,應用程式才能正確的使用這些函數庫。大部分的源碼研發的程式都遵循
GNU的一些標準,我們能看info幫助檔獲得相信的說明,info資訊的位置是:info:
standards#Directory_VariablesGNU標準建議所有的函數庫檔都放在/usr/local/lib目錄下,而且建議命令
可執行程式都放在/usr/local/bin目錄下。這都是一些習慣問題,能改動的。
  檔系統層次化標準FHSFilesystem Hierarchy Standard)(
http://www.pathname.com/fhs
)規定了在一個發行包中大部分的函數庫檔應該安裝到/usr/lib目錄 下,不過如果某些庫是在系統啟動的時候要載入的,則放到/lib目錄下,而那些不是系統本身一部分的庫則放到/usr/local/lib下面。
  上面兩個路徑的不同並沒有本質的衝突。GNU提出的標準主要對於研發者研發源碼的,而FHS的建議則是針對發行版本的路徑的。具體的位置資訊能看/etc/ld.so.conf裏面的設置資訊。
  3.2. 這些函數庫怎麼使用
 
 在基於GNU glibc的系統裏,包括所有的linux系統,啟動一個ELF格式的二進位可執行檔會自動啟動和運行一個program
loader。對於Linux系統,這個loader的名字是/lib/ld-linux.so.XX是版本號)。這個loader啟動後,反過來就會
load所有的其他本程式要使用的共用函數庫。
  到底在哪些目錄裏查找共用函數庫呢?這些定義缺省的是放在
/etc/ld.so.conf檔裏面,我們能修改這個檔,加入我們自己的一些
特別的路徑需求。大多數RedHat系列的發行包的/etc/ld.so.conf檔裏面不包括/usr/local/lib這個目錄,如果沒有這個目
錄的話,我們能修改/etc/ld.so.conf,自己手動加上這個條目。
  如果你想覆蓋某個庫中的一些函數,用自己的函數替換他們,同時保留該庫中其他的函數的話,你能在/etc/ld.so.preload中加入你想要替換的庫(.o結尾的檔),這些preloading的庫函數將有優先載入的權利。
 
 當程式啟動的時候搜索所有的目錄顯然會效率非常低,於是Linux系統實際上用的是個高速緩衝的做法。Ldconfig缺省情況下讀出
/etc/ld.so.conf相關資訊,然後設置適當地符號鏈結,然後寫一個cache/etc/ld.so.cache這個檔中,而這個
/etc/ld.so.cache則能被其他程式有效的使用了。這樣的做法能大大提高訪問函數庫的速度。這就需求每次新增加一個動態載入的函數庫的時
候,就要運行ldconfig來更新這個cache,如果要刪除某個函數庫,或某個函數庫的路徑修改了,都要重新運行ldconfig來更新這個
cache。通常的一些包管理器在安裝一個新的函數庫的時候就要運行ldconfig
  另外,FreeBSD使用cache的檔不相同。FreeBSDELF cache/var/run/ld-elf.so.hints,而a.outcache責是/var/run/ld.so.hints。他們同樣是通過ldconfig來更新。
  3.3. 環境變數
 
 各種各樣的環境變數控制著一些關鍵的過程。例如你能臨時為你特定的程式的一次執行指定一個不同的函數庫。Linux系統中,通常變數
LD_LIBRARY_PATH就是能用來指定函數庫查找路徑的,而且這個路徑通常是在查找標準的路徑之前查找。這個是非常有用的,特別是在調試一個新的
函數庫的時候,或在特別的場合使用一個肥標準的函數庫的時候。環境變數LD_PRELOAD列出了所有共用函數庫中需要優先載入的庫檔,功能和
/etc/ld.so.preload類似。這些都是有/lib/ld-linux.so這個loader來實現的。值得一提的是,
LD_LIBRARY_PATH能在大部分的UNIX-linke系統下正常起作用,不過並非所有的系統下都能使用,例如HPUX系統下,就是用
SHLIB_PATH這個變數,而在AIX下則使用LIBPATH這個變數。
  LD_LIBRARY_PATH在研發和調試過程中經常大量使用,不過不應該被一個普通用戶在安裝過程中被安裝程式修改,大家能去參考 
http://www.visi.com/~barr/ldpath.html
,這裏有一個檔專門介紹為什麼不使用LD_LIBRARY_PATH這個 變數。
  事實上更有更多的環境變數影響著程式的調入過程,他們的名字通常就是以LD_RTLD_打頭。大部分這些環境變數的使用的檔都是不全,通常搞得人頭昏眼花的,如果要真正弄清晰他們的用法,最佳去讀loader的源碼(也就是gcc的一部分)。
 
 允許用戶控制動態鏈結函數庫將涉及到setuid/setgid這個函數如果特別的功能需要的話。因此,GNU
loader通常限制或忽略用戶對這些變數使用setuidsetgid。如果loader通過判斷程式的相關環境變數判斷程式的是否使用了
setuidsetgid,如果uideuid不同,或gidegid部相同,那麼loader就假定程式已使用了setuid
setgid,然後就大大的限制器控制這個老鏈結的許可權。如果閱讀GNU
glibc的庫函數源碼,就能清晰地看到這一點,特別的我們能看elf/rtld.csysdeps/generic/dl-sysdep.c這兩
個文件。這就意味著如果你使得uidgideuidegid分別相等,然後調用一個程式,那麼這些變數就能完全起效。
3.4. 創建一個共用函數庫
 
 目前我們開始學習怎麼創建一個共用函數庫。其實創建一個共用函數庫非常容易。首先創建object檔,這個檔將加入通過gcc -fPIC
參數命令加入到共用函數庫裏面。PIC的意思是位置無關代碼Position Independent Code)。下面是個標準的格式:
gcc -shared -Wl,-soname,your_soname -o library_name file_list library_list
  下面再給一個例子,他創建兩個object檔(a.ob.o),然後創建一個包含a.ob.o的共用函數庫。例子中”-g”Wall”參數不是必須的。
gcc -fPIC -g -c -Wall a.cgcc -fPIC -g -c -Wall b.cgcc -shared -Wl,
-soname,liblusterstuff.so.1 -o liblusterstuff.so.1.0.1 a.o b.o -lc
  下面是一些需要注意的地方:
?
不用使用-fomit-frame-pointer這個編譯參數除非你不得不這樣。雖然使用了這個參數獲得的函數庫仍然能使用,不過這使得調試程式幾乎
沒有用,無法跟蹤調試。 ? 使用-fPIC來產生代碼,而不是-fpic ? 某些情況下,使用gcc
來生成object檔,需要使用“-Wl,-export-dynamic”這個選項參數。通常,動態函數庫的符號表裏面包含了這些動態的物件的符號。
這個選項在創建ELF格式的檔時候,會將所有的符號加入到動態符號表中。能參考ld的幫助獲得更周詳的說明。
  3.5. 安裝和使用共用函數庫
  一旦你了一個共用函數庫,你還需要安裝他。其實簡單的方法就是拷貝你的庫檔到指定的標準的目錄(例如/usr/lib),然後運行ldconfig
 
 如果你沒有許可權去做這件事情,例如你不能修改/usr/lib目錄,那麼你就只好通過修改你的環境變數來實現這些函數庫的使用了。首先,你需要
創建這些共用函數庫;然後,設置一些必須得符號鏈結,特別是從soname到真正的函數庫檔的符號鏈結,簡單的方法就是運行ldconfig
ldconfig -n directory_with_shared_libraries
  然後你就能設置你的LD_LIBRARY_PATH這個環境變數,他是個以逗號分隔的路徑的集合,這個能用來指明共用函數庫的搜索路徑。例如,使用bash,就能這樣來啟動一個程式my_program:
LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH my_program
 
 如果你需要的是重載部分函數,則你就需要創建一個包含需要重載的函數的object檔,然後設置LD_PRELOAD環境變數。通常你能非常
方便的升級你的函數庫,如果某個API改動了,創建庫的程式會改動soname。然而,如果一個函數升級了某個函數庫而保持了原來的soname,你能
強行將老版本的函數庫拷貝到某個位置,然後重新命名這個檔(例如使用原來的名字,然後後面加.orig尾碼),然後創建一個小的“wrapper”腳本
來設置這個庫函數和相關的東西。例如下面的例子:
#!/bin/sh export LD_LIBRARY_PATH=/usr/local/my_lib:$LD_LIBRARY_PATH exec
/usr/bin/my_program.orig $*
  我們能通過運行ldd來看某個程式使用的共用函數庫。例如你能看ls這個實用工具使用的函數庫:
ldd /bin/ls
    libtermcap.so.2 => /lib/libtermcap.so.2 (0x4001c000)
    libc.so.6 => /lib/libc.so.6 (0x40020000)
    /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
  通常我麼能看到一個soname的列表,包括路徑。在所有的情況下,你都至少能看到兩個庫:
? /lib/ld-linux.so.NN1或更大,一般至少2)。
這是這個用力載入其他所有的共用庫的庫。
? libc.so.N(N應該大於或等於6)。這是C語言函數庫。
 
 值得一提的是,不要在對你不信任的程式運行ldd命令。在lddmanual裏面寫得非常清晰,ldd是通過設置某些特別的環境變數(例如,對
ELF物件,設置LD_TRACE_LOADED_OBJECTS),然後運行這個程式。這樣就有可能使得某地程式可能使得ldd來執行某些意想不到的
代碼,而產生不安全的隱患。
3.6. 不相容的函數庫
  如果一個新版的函數庫要和老版本的二進位的庫不相容,則soname需要改動。對於C語言,一共有4個基本的理由使得他們在二進位碼上非常難相容:
  o. 一個函數的行文改動了,這樣他就可能和最開始的定義不相符合。
  o. 輸出的資料項目改動了。
  o. 某些輸出的函數刪除了。
  o. 某些輸出函數的介面改動了。
  如果你能避免這些地方,你就能保持你的函數庫在二進位碼上的相容,或說,你能使得你的程式的應用二進位介面(ABIApplication Binary Interface)上相容。
  4. 動態載入的函數庫Dynamically Loaded (DL) Libraries
 
 動態載入的函數庫Dynamically loaded (DL)
libraries是一類函數庫,他能在程式運行過程中的所有時間載入。他們特別適合在函數中載入一些模組和plugin擴展模組的場合,因為他能在
當程式需要某個plugin模組時才動態的載入。例如,Pluggable Authentication
Modules(PAM)系統就是用動態載入函數庫來使得管理員能設置和重新設置身份驗證資訊。
  Linux系統下,DL函數庫和其
他函數庫在格式上沒有特別的差別,我們前面提到過,他們創建的時候是標準的object格式。主要的差別就是
這些函數庫不是在程式鏈結的時候或啟動的時候載入,而是通過一個API來打開一個函數庫,尋找符號表,處理錯誤和關閉函數庫。通常C語言環境下,需要包
含這個頭文件。
  Linux中使用的函數和Solaris中相同,都是dlpoen()
API。當時不是所有的平臺都使用同樣的介面,例如HP-UX使用shl_load()機制,而視窗系統平臺用另外的其他的調用介面。如果你的目的
是使得你的代碼有非常強的移植性,你應該使用一些wrapping函數庫,這樣的wrapping函數庫隱藏不同的平臺的介面差別。一種方法是使用
glibc函數庫中的對動態載入模組的支援,他使用一些潛在的動態載入函數庫介面使得他們能誇平臺使用。具體能參考http:
//developer.gnome.org/doc/API/glib/glib-dynamic-loading-of-modules.html.
另外一個方法是使用libltdl,是GNU libtool的一部分,能進一步參考CORBA相關資料。
  4.1. dlopen()
  dlopen函數打開一個函數庫然後為後面的使用做準備。C語言原形是:
void * dlopen(const char *filename, int flag);
  如果檔案名filename是以“/”開頭,也就是使用絕對路徑,那麼dlopne就直接使用他,而不去查找某些環境變數或系統設置的函數庫所在的目錄了。否則dlopen()
  就會按照下面的次序查找函數庫檔:
1. 環境變數LD_LIBRARY指明的路徑。 2. /etc/ld.so.cache中的函數庫列表。 3. /lib目錄,然後/usr/lib。不過一些非常老的a.outloader則是採用相反的次序,也就是先查/usr/lib,然後是/lib
 
 Dlopen()函數中,參數flag的值必須是RTLD_LAZYRTLD_NOWRTLD_LAZY的意思是resolve
undefined symbols as code from the dynamic library is
executed,而RTLD_NOW的含義是resolve all undefined symbols before dlopen()
returns and fail if this cannot be done’
  如果有好幾個函數庫,他們之間有一些依賴關係的話,例如X依賴Y,那麼你就要先載入那些被依賴的函數。例如先載入Y,然後載入X
  dlopen()函數的返回值是個控制碼,然後後面的函數就通過使用這個控制碼來做進一步的操作。如果打開失敗dlopen()就返回一個NULL。如果一個函數庫被多次打開,他會返回同樣的控制碼。
  如果一個函數庫裏面有一個輸出的函數名字為_init,那麼_init就會在dlopen()這個函數返回前被執行。我們能利用這個函數在我的函數庫裏面做一些初始化的工作。我們後面會繼續討論這個問題的。
  4.2. dlerror()
  通過調用dlerror()函數,我們能獲得最後一次調用dlopen()dlsym(),或dlclose()的錯誤資訊。
4.3. dlsym()
  如果你載入了一個DL函數庫而不去使用當然是不可能的了,使用一個DL函數庫的最主要的一個函數就是dlsym(),這個函數在一個已打開的函數庫裏面查找給定的符號。這個函數如下定義:
void * dlsym(void *handle, char *symbol);
  函數中的參數handle就是由dlopen打開後返回的控制碼,symbol是個以NIL結尾的字串。
 
 如果dlsym()函數沒有找到需要查找的symbol,則返回NULL。如果你知道某個symbol的值不可能是NULL0,那麼就非常
好,你就能根據這個返回結果判斷查找的symbol是否存在了;不過,如果某個symbol的值就是NULL,那麼這個判斷就有問題了。標準的判斷方法
是先調用dlerror(),清除以前可能存在的錯誤,然後調用dlsym()來訪問一個symbol,然後再調用dlerror()來判斷是否出現了錯
誤。一個典型的過程如下:
dlerror(); /* clear error code */
s = (actual_type) dlsym(handle, symbol_being_searched_for);
if ((err = dlerror()) != NULL)
{
/* handle error, the symbol wasn’t found */
}
else
{
/* symbol found, its value is in s */
}
  4.4. dlclose()
 
 dlopen()函數的反過程就是dlclose()函數,dlclose()函數用力關閉一個DL函數庫。Dl函數庫維持一個資源利用的計數
器,當調用dlclose的時候,就把這個計數器的計數減一,如果計數器為0,則真正的釋放掉。真正釋放的時候,如果函數庫裏面有_fini()這個函
數,則自動調用_fini()這個函數,做一些必要的處理。Dlclose()返回0表示成功,其他非0值表示錯誤。
  4.5. DL Library Example
  下面是個例子。例子中調入math函數庫,然後列印2.0的余弦函數值。例子中每次都檢查是否出錯。應該是個不錯的範例:
  #include
  #include
  #include
  int main(int argc, char **argv)
  {
    void *handle;
    double (*cosine)(double);
    char *error;
    handle = dlopen ("/lib/libm.so.6", RTLD_LAZY);
    if (!handle) {
        fputs (dlerror(), stderr);
        exit(1);
    }
    cosine = dlsym(handle, "cos");
    if ((error = dlerror()) != NULL)
  {
        fputs(error, stderr);
        exit(1);
    }
    printf ("%f ", (*cosine)(2.0));
    dlclose(handle);
}
  如果這個程式名字叫foo.c,那麼用下面的命令來編譯:
  gcc -o foo foo.c -ldl
5. 其他
  5.1. nm命令
 
 nm命令能列出一個函數庫檔中的符號表。他對於靜態的函數庫和共用的函數庫都起作用。對於一個給定的函數庫,nm命令能列出函數庫中定義
的所有符號,包括每個符號的值和類型。還能給出在原程式中這個函數(符號)是在多少行定義的,不過這必須需求編譯該函數庫的時候加“-l”選項。
 
 關於符號的類型,這裏我們再多討論一下。符號的類型是以一個字母的形式顯示的,小寫字母表示這個符號是本地(local)的,而大寫字母則表示
這個符號是全局的(global,externel)。一般來說,類型有一下幾種:TDBUW。各自的含義如下:T表示在代碼段中定義的一般變數
符號;D表示時初始化過的資料段;B表示初始化的資料段;U表示沒有定義的,在這個庫裏面使用了,不過在其他庫中定義的符號;Wweak的縮寫,表示如
果其他函數庫中也有對這個符號的定義,則其他符號的定義能覆蓋這個定義。
  如果你知道一個函數的名字,不過你不知道這個函數在什麼庫中定義的,那麼能用mn“-o”選項和grep命令來查找庫的名字。-o選項使得顯示的每一行都有這個函數庫檔案名。例如,你要查找“cos”這個是在什麼地方定義的,大致能用下面的命令:
nm -o /lib/* /usr/lib/* /usr/lib/*/* /usr/local/lib/* 2> /dev/null
| grep ’cos$’
  關於nm的更周詳的用法我們能參考info檔,位置是info:binutils#nm
  5.2. 特別函數_init_fini
 
 函數庫裏面有兩個特別的函數,_init_fini,這個我們在前面已說過了。主要是分別用來初始化函數庫和關閉的時候做一些必要的處理,
我們能把自己認為需要的代碼放到這兩個函數裏面,他們分別在函數庫被載入和釋放的時候被執行。具體說,如果一個函數庫裏面有一個名字為“_init”
函數輸出,那麼在第一次通過dlopen()函數打開這個函數庫,或只是簡單的作為共用函數庫被打開的時候,_init函數被自動調用執行。和之相對應
的就是_fini函數,當一個程式調用dlclose()去釋放對這個函數庫的引用的時候,如果該函數庫的被引用計數器為0了,或這個函數庫是作為一般
的共用函數庫被使用而使用他的程式正常退出的時候,_fini就會被調用執行。C語言定義他們的原型如下:
void _init(void); void _fini(void);
  當用gcc編譯源程式為“.o”檔的時候,需要加一個“-nostartfiles”選項。這個選項使得C編譯器不鏈結系統的啟動函數庫裏面的啟動函數。否則,就會得到一個“multiple-definition”的錯誤。
  5.3. 共用函數庫也能使腳本(Scripts
  GNUloader允許使用特別格式的腳本語言來寫一個函數庫。這對於那些需要間接包含其他函數庫的情況還是有用的。例如,下麵是個/usr/lib/libc.so的例子:
/* GNU ld script. Use the shared library, but some functions are only in
the static library, so try that secondarily. */GROUP ( /lib/libc.so.6
/usr/lib/libc_nonshared.a )
  更多的資訊能參考texinfo檔中關於ld鏈結的腳本部分。一般的資訊還能參考: info:ld#Options info:ld#Commands,也能參考info:ld#Option Commands
  5.4. GNU libtool
 
 如果你正在編譯的系統相非常方便的移植到其他作業系統下,你能使用GNU libtool來創建和安裝這個函數庫。GNU
libtool是個函數庫支援的典型的腳本。Libtool隱藏了使用一個可移植的函數庫的負責性。Libtool提供了一個能移植的介面來創建
object檔,鏈結函數庫(靜態或共用的),並且安裝這些庫。他還包含了libltdl,一個可移植的動態函數庫調入程式的wrapper。更多的
周詳討論,能在
http://www.gnu.org/software/libtool/manual.html
看到。
  5.5. 刪除一些符號
  在一個生產的檔中非常多符號都是為了debug而包含的,佔用了不少空間。如果空間不夠,而且這些符號也許不再需要,就能將其中一些刪除。
 
 最佳的方法就是先正常的生成你需要的object檔,然後debug和測試你需要的一些東西。一旦你完全測試完畢了,就能用strip去刪
除一些不必的符號了。Strip命令能使你非常方便的控制刪除什麼符號,而保留什麼符號。Strip的具體用法能參考其幫助文件。
  另外的方法就是使用GNU ld的選項“-S”“-s”;“-S”會刪除一些debugger的符號,而“-s”則是將所有的符號資訊都刪除。通常我們能在gcc中加這樣的參數“-Wl,-S”“-Wl,-s”來達到這個目的。
摘要

面是一些例子,例子中我們會使用三種函數庫(靜態的、共用的和動態載入的函數庫)。文件libhello.c是個函數庫,libhello.h
是他的頭文件;demo_use.c則是個使用了libhello函數庫的。Script_staticscript_dynamic分別演示怎麼以
靜態和共用方式使用函數庫,而後面的demo_dynamic.cscript_dynamic則表示演示怎麼以動態載入函數庫的方式來使用他。
(2002-08-25 17:38:37)
By Wing
  6. 更多的例子
 
 下面是一些例子,例子中我們會使用三種函數庫(靜態的、共用的和動態載入的函數庫)。文件libhello.c是個函數庫,
libhello.h是他的頭文件;demo_use.c則是個使用了libhello函數庫的。Script_static
script_dynamic分別演示怎麼以靜態和共用方式使用函數庫,而後面的demo_dynamic.cscript_dynamic則表示演示
怎麼以動態載入函數庫的方式來使用他。
  6.1. File libhello.c
/* libhello.c - demonstrate library use. */
#include
void hello(void)
{
printf("Hello, library world.
");
}
  6.2. File libhello.h
/* libhello.h - demonstrate library use. */
void hello(void);
  6.3. File demo_use.c
/* demo_use.c -- demonstrate direct use of the "hello" routine */
#include "libhello.h"
int main(void)
{
hello();
return 0;
}
  6.4. File script_static
#!/bin/sh
# Static library demo
# Create static library’s object file, libhello-static.o.
# I’m using the name libhello-static to clearly
# differentiate the static library from the
# dynamic library examples, but you don’t need to use
# "-static" in the names of your
# object files or static libraries.gcc -Wall -g -c -o libhello-static.o
libhello.c
# Create static library.ar rcs libhello-static.a libhello-static.o
# At this point we could just copy libhello-static.a
# somewhere else to use it.
# For demo purposes, we’ll just keep the library
# in the current directory.
# Compile demo_use program file.gcc -Wall -g -c demo_use.c -o demo_use.o
# Create demo_use program; -L. causes "." to be searched during
# creation of the program. Note that this command causes
# the relevant object file in libhello-static.a to be
# incorporated into file demo_use_static.gcc -g -o demo_use_static
demo_use.o -L. -lhello-static
# Execute the program../demo_use_static
  6.5. File script_shared
#!/bin/sh
# Shared library demo
# Create shared library’s object file, libhello.o.gcc -fPIC -Wall
-g -c libhello.c
# Create shared library.
# Use -lc to link it against C library, since libhello
# depends on the C library.gcc -g -shared -Wl,-soname,libhello.so.0 -o
libhello.so.0.0 libhello.o -lc# At this point we could just copy
libhello.so.0.0 into
# some directory, say /usr/local/lib.
# Now we need to call ldconfig to fix up the symbolic links.
# Set up the soname. We could just execute:
# ln -sf libhello.so.0.0 libhello.so.0
# but let’s let ldconfig figure it out./sbin/ldconfig -n .
# Set up the linker name.
# In a more sophisticated setting, we’d need to make
# sure that if there was an existing lin

文章分類