GCC製作動態庫匯出符號表
GCC製作動態連結庫時預設會將所有的函式及變數都匯出到符號表,這裡的函式及變數指的是沒有使用static修飾的,使用static修飾的函式及變數不會匯出。正常情況下所有符號均匯出是不會有問題的,但是有時會有問題,在下邊的例子中會說明。為了避免這種情況,就需要定製符號表,即僅僅將需要提供給其他模組使用的介面或變數匯出到符號表,本文就是介紹製作這樣的動態庫的方法。
一 問題示例
介紹製作方法前先舉例說明其必要性,有兩個動態庫libhello.so及libhello1.so,對應的原始檔是hello.c及hello1.c,其原始碼如下:
/*
*hello.c原始碼
*/
int hello()
{
return 1;
}
/*
*hello1.c原始碼
*/
int hello()
{
return 10;
}
/*
*hello.h原始碼
*/
int hello();
將hello.c和hello1.c分別編譯成動態庫:
gcc -shared -o libhello.so hello.c
gcc -shared -o libhello1.so hello1.c
測試檔案原始碼:
#include <stdio.h>
#include "hello.h"
int main()
{
printf(">>>%d\n", hello());
return 0;
}
現在編譯測試檔案,並且同時連線libhello.so和libhello1.so兩個檔案:
gcc main.c -L. -lhello -lhello1
執行結果:
>>>1
結果如下圖所示:
由上圖ldd檢視結果可知,實際上只連線了libhello.so庫,現在交換編譯測試檔案時連線庫的順序並執行:
gcc main.c -L. –lhello1 -lhello
執行結果:
>>>10
結果如下圖所示:
由上圖ldd檢視結果可知,實際上只連線了libhello1.so庫。
由此可知有相同函式時,呼叫先找到的庫中的函式實體。假設專案中有兩個模組以庫的方式提供分別為A、B庫,而兩個庫中均有函式D並且函式格式一樣但功能不同,其中A庫中的D是模組內部使用的,但是沒有用static修飾,這樣兩個庫的符號表中均有D函式符號,上層模組呼叫了D函式介面,編譯時同時連線了A、B庫,則實際呼叫的D函式就有編譯連結時A在前還是B在前,如果B在前則不會有問題,但是如果A在前就會呼叫A中的D函式,功能就會出錯。
這種問題遇到了就很不好查詢與排除,因為並非程式碼問題,修改程式碼解決不了問題,所以才有了本文描述的製作自己需要的庫,該庫僅僅暴露提供給外部模組使用的介面符號表。
二 定製方法
2.1 前期準備
前期準備只是熟悉檢視庫的符號表方法及相關介紹。檢視符號表用的命令是readelf,選項是-s,如readelf –s libhello.so,檢視的結果如圖所示:
(注:圖比較長,並未截完整,剩下的都是.symtab的內容)
從上圖可知,符號表有兩部分:.dynsyn和.symtab,這裡只做簡單介紹,具體資訊可自行查詢。
.symtab和.dynsym兩個不同的symbol table,.dynsym用來儲存與動態連結相關的匯入匯出符號,而 .symtab 則儲存所有符號,包括 .dynsym 中的符號,.dynsym是.symtab的一個子集。
ELF檔案包含一些sections(如code和data)是在執行時需要的, 這些sections被稱為allocable;而其他一些sections僅僅是linker、debugger等工具需要,在執行時並不需要,這些sections被稱為non-allocable。當linker構建ELF檔案時,它把allocable的資料放到一個地方,將non-allocable的資料放到其他地方,當OS載入ELF檔案時,僅僅allocable的資料被對映到記憶體,non-allocable的資料仍靜靜地呆在檔案裡不被處理。strip就是用來移除某些non-allocable sections的,移除後不影響使用。
2.2 製作動態庫
本文製作的動態庫的目的是控制暴露在符號表中的符號,使用的是gcc的visibility屬性,網上介紹還有其他方法,本文不做介紹。
製作動態庫的原始碼hello.c如下:
/*
*hello.c原始碼
*/
int call_count = 0;
int hello_init()
{
call_count = 0;
return 0;
}
int hello_call_count_add()
{
return ++call_count;
}
int hello_handle()
{
return hello_call_count_add();
}
void hello_exit()
{
call_count = 0;
}
/*
*hello.h原始碼
*/
#ifndef __HELLO_H
#define __HELLO_H
int hello_init();
int hello_handle();
void hello_exit();
#endif
由hello.h標頭檔案可知hello_init()、hello_handle()和hello_exit()是對外介面,而hello_call_count_add()不是。先用常規方法制作libhello.so,檢視符號表,此處僅檢視.dynsyn部分,.symtab部分用strip命令處理掉。
gcc -shared -o libhello.so hello.c
strip libhello.so
readelf -s libhello.so
檢視結果:
由圖可知,不僅所有的函式在符號表中,未用static修飾的變數call_count也在符號表中,這沒有達到目的,需要將hello_call_count_add()函式和變數call_count去掉。
要達到目的,需要用gcc的visibility屬性,將需要匯出的符號用__attribute__ ((visibility (“default”)))修飾,並且gcc編譯時需要加入-fvisibility=hidden選項。
使用visibility屬性後的原始碼:
#define DLL_PUBLIC __attribute__ ((visibility ("default")))
int call_count = 0;
DLL_PUBLIC int hello_init()
{
call_count = 0;
return 0;
}
int hello_call_count_add()
{
return ++call_count;
}
DLL_PUBLIC int hello_handle()
{
return hello_call_count_add();
}
DLL_PUBLIC void hello_exit()
{
call_count = 0;
}
編譯並用strip命令處理掉.symtab部分:
gcc -shared -fvisibility=hidden -o libhello.so hello.c
strip libhello.so
readelf -s libhello.so
結果如下:
從結果可知達到了目的,經驗證製作的庫可以正常使用。
2.3 整理
gcc在連結時設定-fvisibility=hidden,則不加 visibility宣告的都預設為hidden,即都是隱藏的, gcc預設設定-fvisibility=default,即全部可見。