引言
重點講述linux上使用gcc編譯動態庫的一些操作.並且對其深入的案例分析.
最後介紹一下動態庫插件技術, 讓代碼向後兼容.關於linux上使用gcc基礎編譯,
預編譯,編譯,生成機械碼最後鏈接輸出可執行文件流程參照下面.
gcc編譯流程
而本文重點是分析動態庫相關的知識點. 首先看需要用到的測試素材
heoo.h
#ifndef _H_HEOO #define _H_HEOO /* * 測試接口,得到key內容 * : 返回key的字符串 */ extern const char* getkey(void); /* * 測試接口,得到value內容 * arg : 傳入的參數 * : 返回得到的結果 */ extern void* getvalue(void* arg); #endif // !_H_HEOO
heoo-getkey.c
#include "heoo.h" /* * 測試接口,得到key內容 * : 返回key的字符串 */ const char* getkey(void) { return "heoo-getkey.c getkey"; }
heoo-getvalue.c
#include "heoo.h" #include <stdio.h> /* * 測試接口,得到value內容 * arg : 傳入的參數 * : 返回得到的結果 */ const void* getvalue(void* arg) { const char* key = "heoo-getvalue.c getvalue"; printf("%s - %s\n", key, (void*)arg); return key; }
heoo.c
#include "heoo.h" #include <stdio.h> /* * 測試接口,得到key內容 * : 返回key的字符串 */ const char* getkey(void) { return "heoo.c getkey"; } /* * 測試接口,得到value內容 * arg : 傳入的參數 * : 返回得到的結果 */ const void* getvalue(void* arg) { const char* key = "heoo.c getvalue"; printf("%s - %s\n", key, (char*)arg); return key; }
main.c
#include <stdio.h> #include "heoo.h" // 測試邏輯主函數 int main(int argc, char* argv[]) { // 簡單的打印數據 printf("getkey => %s\n", getkey()); getvalue(NULL); return 0; }
到這裡也許感覺有點臃腫, 但是理解為什麼是必要的. 會讓你對於動態庫高度高上0.01毫米的.哈哈.
先讓上面代碼跑起來.
gcc -g -Wall -o main.out main.c heoo.c
測試結果如下
測試完成,那就開始靜態庫到動態庫擴展之旅.
前言
從靜態庫說起來
首先參照下面編譯語句
gcc -c -o heoo-getkey.o heoo-getkey.c gcc -c -o heoo-getvalue.o heoo-getvalue.c
對於靜態庫創建本質就是打包. 所以用linux上一個 ar創建靜態庫壓縮命令.詳細用法可以看
ar詳細用法參照 http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201121093917552/
那麼我們開始制作靜態庫
ar rcs libheoo.a heoo-getvalue.o heoo-getkey.o
那麼我們采用靜態庫執行編譯上面main.c 函數
gcc -g -Wall -o main.out main.c -L. -lheoo
運行的截圖如下
運行一切正常. 對於靜態庫編譯 簡單說明一下. ar 後面的 rcs表示 替換創建和添加索引. 具體的看上面的網址.
後面gcc中 -L表示查找庫的目錄, -l表示搜索的 libheoo庫. 還有其它的-I表示查找頭文件地址, -D表示添加全局宏.......
對於上面靜態庫編譯還有一種方式如下
gcc -g -Wall -o main.out main.c libheoo.a
執行結果也是一樣的.可以將 *.a 理解成多個 *.o合體.
好到這裡前言就說完了.那我們開始說正題動態庫了.
正文
動態庫的構建和使用
動態庫構建命名如下,仍然以heoo.c heoo.h 為例
gcc -shared -fPIC -o libheoo.so heoo.c
開始編譯代碼 先介紹一種最簡單的有點類似上面靜態庫最後一種方式.
gcc -g -Wall -o main.out main.c ./libheoo.so
這裡是顯式編譯. 結果如下
對於 上面編譯 動態庫的時候如果 直接使用 libheoo.so. 例如
gcc -g -Wall -o main.out main.c libheoo.so
如果沒有配置動態庫路徑, 查找動態庫路徑會出問題. 這裡就不復現了(因為我把環境調好了). 會面會給出解決辦法.
下面說libheoo.so 標准的使用方式
gcc -g -Wall -o main.out main.c -L. -lheoo
運行結果如下
上面是個常見錯誤, 系統找不見動態庫在那. 需要配置一下, 再編譯參照如下
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH;./" gcc -g -Wall -o main.out main.c -L. -lheoo
上面第一句話是在當前會話層. 添加庫查找路徑,包含當前文件目錄.這個會話層關閉了就失效了. Linux上shell確實很重要. 現在執行結果
到這裡動態庫的也都完畢了. 一切正常.
一個奇巧淫技
問: gcc -l 鏈接一個庫的時候,但是庫中存在同名的靜態庫和動態庫. 會鏈接到那個庫?
通過上面的那麼多測試應該知道是動態庫吧,因為使用動態庫會報錯.使用靜態庫沒有事.
那麼問題來了, 我想使用靜態庫怎麼辦.
-static
上面gcc 選項可以幫助我們強制鏈接靜態庫!
動態庫的顯示使用
到這裡基本上是重頭戲了. 扯一點,這些知識點在window也一樣知識環境變了,設置變了.鏈接編譯顯式加載都有的. 下面是重新操作的代碼.
heooso.c
#include <stdio.h> #include <dlfcn.h> #define _STR_PATH "./libheoo.so" // 顯示調用動態庫, 需要 -ldl 鏈接程序庫 int main(int argc, char* argv[]) { const char* (*getkey)(void); const void* (*getvalue)(void* arg); /* * 對於dlopen 函數第二個參數 * RTLD_NOW:將共享庫中的所有函數加載到內存 * RTLD_LAZY:會推後共享庫中的函數的加載操作,直到調用dlsym()時方加載某函數 */ void* handle = dlopen(_STR_PATH, RTLD_LAZY); // 下面得到錯誤信息,是一種小心的變成方式,每次都檢測一下錯誤是否存在 const char* err = dlerror(); if(!handle || err) { fprintf(stderr, "dlopen " _STR_PATH " no open! err = %s\n", err); return -1; } getkey = dlsym(handle, "getkey"); if((err = dlerror())){ fprintf(stderr, "getkey err = %s\n", err); dlclose(handle); return -2; } puts(getkey()); //這種顯式調用dll代碼,很不安全代碼注入太簡單了 getvalue = dlsym(handle, "getvalue"); if((err = dlerror())){ fprintf(stderr, "getvalue err = %s\n", err); dlclose(handle); return -3; } puts(getvalue(NULL)); dlclose(handle); return 0; }
編譯代碼
gcc -g -Wall -o heooso.out heooso.c -ldl
測試結果截圖如下
運行一切正常. 功能是實現了.但是大家千萬別這麼用.否則還是比較危險的.也是一種編程思路吧.後面
後記會寫一個向後兼容的插件機制. 大家可以觀摩一下. 方便更深入的了解Linux系統開發.算是一個簡易的
Linux運用插件技術的小項目吧.
後記
錯誤是難免的,歡迎吐槽. 最後獻上一個linux上如何通過動態庫運行時加載插件的案例.麻雀雖小,五髒俱全.
Makefile
CC = gcc DEBUG = -g -Wall LIB = -ldl RUNSO = $(CC) -fPIC -shared -o $@ $^ RUN = $(CC) $(DEBUG) -o $@ $^ #總的任務 all:libheoo.so libheootwo.so libheoothree.so main.out #簡單lib%.so生成 libheoo.so:heoo.c $(RUNSO) libheootwo.so:heootwo.c $(RUNSO) libheoothree.so:heoothree.c $(RUNSO) #生成的主要內容 main.out:main.c $(RUN) $(LIB) # 簡單的清除操作 make clean .PHONY:clean clean: rm -rf *.so *.s *.i *.o *.out *~ ; ls -hl
heoo.h
#ifndef _H_HEOO #define _H_HEOO /* * 測試接口,得到key內容 * : 返回key的字符串 */ extern const char* getkey(void); /* * 測試接口,得到value內容 * arg : 傳入的參數 * : 返回得到的結果 */ extern const void* getvalue(void* arg); #endif // !_H_HEOO
heootwo.c
#include "heoo.h" #include <stdio.h> /* * 測試接口,得到key內容 * : 返回key的字符串 */ const char* getkey(void) { return "heootwo.c getkey"; } /* * 測試接口,得到value內容 * arg : 傳入的參數 * : 返回得到的結果 */ const void* getvalue(void* arg) { const char* key = "heootwo.c getvalue"; printf("%s - %s\n", key, (char*)arg); return key; }
heoothree.c
#include "heoo.h" #include <stdio.h> /* * 測試接口,得到key內容 * : 返回key的字符串 */ const char* getkey(void) { return "heoothree.c getkey"; } /* * 測試接口,得到value內容 * arg : 傳入的參數 * : 返回得到的結果 */ const void* getvalue(void* arg) { const char* key = "heoothree.c getvalue"; printf("%s - %s\n", key, (char*)arg); return key; }
main.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> #include <errno.h> #include <unistd.h> #include <dirent.h> #include <dlfcn.h> //塞入的句柄數 #define _INT_HND (3) // 最多支持108個插件 #define _INT_LEN (108) // 文件路徑最大長度 #define _INT_BUF (512) // 處理dll,並且將返回的數據保存在a[_INT_HND]中, 這個數組長度必須是 bool dll_add(void* a[], const char* dllpath); // 處理指定目錄得到結果塞入a中, nowpath為NULL表示當前目錄 int dll_new(void* a[][_INT_HND], int len, const char* nowpath); // 釋放資源 void dll_del(void* a[][_INT_HND], int len); /* * 動態加載機制 */ int main(int argc, char* argv[]) { int idx, len, i; void* a[_INT_LEN][_INT_HND]; // 當前目錄下,處理結果 len = dll_new(a, _INT_LEN, NULL); if(len == 0){ fprintf(stderr, "感謝使用,沒有發現合法插件內容!\n"); exit(1); } //數據展示 puts("------------------------------ 歡迎使用main插件 ----------------------------------"); for(i=0; i<len; ++i){ const char* (*getkey)(void) = a[i][1]; printf(" %d => %s\n", i, getkey()); } printf(" 請輸入 待執行的 索引[0, %d)\n", len); if(scanf("%d", &idx)!=1 || idx<0 || idx >= len){ puts(" fake 錯誤的命令,程序退出中!"); goto __exit; } puts(" 執行結果如下:"); const void* (*getvalue)(void* arg) = a[idx][2]; puts(getvalue(NULL)); __exit: puts("------------------------------ 謝謝使用main插件 ----------------------------------"); dll_del(a, len); return 0; } // 處理dll,並且將返回的數據保存在a[_INT_HND]中, 這個數組長度必須是 bool dll_add(void* a[], const char* dllpath) { const char* (*getkey)(void); const void* (*getvalue)(void* arg); void* handle = dlopen(dllpath, RTLD_LAZY); // 下面得到錯誤信息,是一種小心的變成方式,每次都檢測一下錯誤是否存在 const char* err = dlerror(); if(!handle || err) return false; getkey = dlsym(handle, "getkey"); if((err = dlerror())){ dlclose(handle); return false; } //這種顯式調用dll代碼,很不安全代碼注入太簡單了 getvalue = dlsym(handle, "getvalue"); if((err = dlerror())){ dlclose(handle); return false; } // 句柄, key, value, 協議訂的 a[0] = handle; a[1] = getkey; a[2] = getvalue; return true; } // 處理指定目錄得到結果塞入a中, nowpath為NULL表示當前目錄 int dll_new(void* a[][_INT_HND], int len, const char* nowpath){ int j = 0, rt; DIR* dir; struct dirent* ptr; char path[_INT_BUF]; // 設置默認目錄 if(!nowpath || !*nowpath) nowpath = "."; // 打開目錄信息 if((dir = opendir(nowpath)) == NULL) { fprintf(stderr, "opendir open %s, error:%s\n", nowpath, strerror(errno)); exit(-1); } //挨個讀取文件 while(j<len && (ptr=readdir(dir))){ //只處理文件,包含未知文件 if(DT_BLK == ptr->d_type || DT_UNKNOWN == ptr->d_type){ rt = snprintf(path, _INT_BUF, "%s/%s", nowpath, ptr->d_name);// 只有確實是 *.so 文件才去出去運行 if(rt>3&&rt < _INT_BUF&&path[rt-1]=='o'&&path[rt-2]=='s'&&path[rt-3]=='.') { // 添加數據 dao數組 a中 if(dll_add(a[j], path)) ++j; } } } closedir(dir); return j; } // 釋放資源 void dll_del(void* a[][_INT_HND], int len) { int i=-1; while(++i < len) dlclose(a[i][0]); }
最後運行截圖
到這裡一個小demo就完工了. 關於Linux gcc上動態庫插件開發,剖析完畢.O(∩_∩)O哈哈~