使用dlopen加載動態庫


概述

通過使用dlopen接口可以實現運行時的動態庫函數調用,需要知道動態庫中的函數原型。

以下實現Linux C/C++使用dlopen的基本示例,並說明鏈接選項-rdynamic的作用,提供動態加載可執行文件的示例。

接口

dlopen(), dlsym(), dlclose(), dlerror() 均為Linux系統實現的動態鏈接接口。

#include <dlfcn.h>

// 以指定模式打開指定的動態連接庫文件,並返回一個句柄給調用進程。
// flag中必須設置以下的mode:
// RTLD_LAZY 暫緩決定,等有需要時再解出符號 
// RTLD_NOW 立即決定,返回前解除所有未決定的符號。
void *dlopen(const char *filename, int flag);

// 當動態鏈接庫操作函數執行失敗時,可以返回出錯信息,返回值為NULL時,表示沒有錯誤信息。
char *dlerror(void);

// handle是由dlopen打開動態鏈接庫後返回的指針,symbol就是要求獲取的函數的名稱,函數返回值是void*,指向函數的地址,供調用使用。
void *dlsym(void *handle, const char *symbol);

// 將該.so的引用計數減一,當引用計數為0時,將它從系統中卸載。
int dlclose(void *handle);

為了使用dlopen接口,需要設置鏈接選項-ldl

C

CMakeLists.txt

# C dlopen test
cmake_minimum_required(VERSION 3.10)
project(dlopen_test C)
set(WORK_DIR "${CMAKE_SOURCE_DIR}")

set(CMAKE_C_FLAGS "-O0 -ggdb")
set(CMAKE_EXE_LINKER_FLAGS "-ldl -rdynamic")

file(GLOB_RECURSE SRC_FILES ${WORK_DIR}/src/main.c)
file(GLOB_RECURSE lib_files ${WORK_DIR}/src/add.c)
add_library(add SHARED ${lib_files})
link_directories(${CMAKE_BINARY_DIR})

add_executable(${PROJECT_NAME} ${SRC_FILES})
target_link_libraries(${PROJECT_NAME} add)

src/main.c

// file : main.c
#include <stdio.h>  
#include <stdlib.h>   // EXIT_FAILURE
#include <dlfcn.h>    // dlopen, dlerror, dlsym, dlclose

typedef int(* FUNC_ADD)(int, int); // define alias of function pointer
const char* dllPath = "./libadd.so";

int main()
{
	void* handle = dlopen( dllPath, RTLD_LAZY );

	if( !handle )
	{
		fprintf( stderr, "[%s](%d) dlopen get error: %s\n", __FILE__, __LINE__, dlerror() );
		exit( EXIT_FAILURE );
	}

	do{ // for resource handle
		FUNC_ADD add_func = (FUNC_ADD)dlsym( handle, "add" );
		printf( "1 add 2 is %d \n", add_func(1,2) );
	}while(0); // for resource handle
    
	dlclose( handle );
    
    return 0;
} 

src/add.c

// file : add.c
int add(int a, int b) { return a+b; };

./dlopen_test

1 add 2 is 3

C++

與C版本的區別在於,由於動態庫函數通過C++編譯器完成編譯,需要注意命名修飾。當main中使用不帶修飾的名稱”add”獲取函數地址時,add()實現需要使用extern "C"進行處理。

CMakeLists.txt

# C++ dlopen test
cmake_minimum_required(VERSION 3.10)
project(dlopen_test_cpp CXX)
set(WORK_DIR "${CMAKE_SOURCE_DIR}")

set(CMAKE_CXX_FLAGS "-O0 -ggdb")
set(CMAKE_EXE_LINKER_FLAGS "-ldl -rdynamic")

file(GLOB_RECURSE SRC_FILES ${WORK_DIR}/src/main.cpp)
file(GLOB_RECURSE lib_files ${WORK_DIR}/src/add.cpp)
add_library(add SHARED ${lib_files})
link_directories(${CMAKE_BINARY_DIR})

add_executable(${PROJECT_NAME} ${SRC_FILES})
target_link_libraries(${PROJECT_NAME} add)

src/main.cpp

// file : main.cpp
#include <stdio.h>  
#include <stdlib.h>   // EXIT_FAILURE
#include <dlfcn.h>    // dlopen, dlerror, dlsym, dlclose

typedef int(* FUNC_ADD)(int, int); // define alias of function pointer
const char* dllPath = "./libadd.so";

int main()
{
	void* handle = dlopen( dllPath, RTLD_LAZY );

	if( !handle )
	{
		fprintf( stderr, "[%s](%d) dlopen get error: %s\n", __FILE__, __LINE__, dlerror() );
		exit( EXIT_FAILURE );
	}

	do{ // for resource handle
		FUNC_ADD add_func = (FUNC_ADD)dlsym( handle, "add" );
		printf( "1 add 2 is %d \n", add_func(1,2) );
	}while(0); // for resource handle

	dlclose( handle );

	return 0;
}

src/add.cpp

// file : add.cpp
#ifdef __cplusplus
extern "C" {
#endif

int add(int a, int b) { return a+b; };

#ifdef __cplusplus
}
#endif

./dlopen_test_cpp

1 add 2 is 3

-rdynamic

在CMakeLists.txt文件中設置了以下的鏈接選項,這裡選項的設置參考了網絡文章,為使用dlopen,選項-ldl是必須的。

set(CMAKE_EXE_LINKER_FLAGS "-ldl -rdynamic")

但是當不設置-rdynamic時也可以完成編譯鏈接並正確運行。那麼-rdynamic選項起什麼作用呢。

關於-rdynamic,查看man gcc,有以下說明,

-rdynamic

Pass the flag -export-dynamic to the ELF linker, on targets that support it. This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table. This option is needed for some uses of “dlopen” or to allow obtaining backtraces from within a program.

默認情況下符號只會從共享庫中導出,當鏈接器設置-rdynamic後,將使得ELF可執行程序能夠導出符號。或許在動態加載插件中有一定用途。

關於--export-dynamic,查看man ld,有以下說明,

-E

–export-dynamic

–no-export-dynamic

When creating a dynamically linked executable, using the -E option or the –export-dynamic option causes the linker to add all symbols to the dynamic symbol table. The dynamic symbol table is the set of symbols which are visible from dynamic objects at run time.

If you do not use either of these options (or use the –no-export-dynamic option to restore the default behavior), the dynamic symbol table will normally contain only those symbols which are referenced by some dynamic object mentioned in the link.

If you use "dlopen" to load a dynamic object which needs to refer back to the symbols defined by the program, rather than some other dynamic object, then you will probably need to use this option when linking the program itself.

You can also use the dynamic list to control what symbols should be added to the dynamic symbol table if the output format supports it. See the description of –dynamic-list.

Note that this option is specific to ELF targeted ports. PE targets support a similar function to export all symbols from a DLL or EXE ; see the description of –export-all-symbols below.

在以上的解釋中,指出在使用dlopen加載動態目標時,可能需要引用一個程序自身而非其它動態目標定義的符號,即鏈接這個程序自身。

因此,通過開啟這個選項可以實現動態加載可執行文件。那麼,由於在以上的兩個示例中鏈接目標並非可執行文件,可以不用加入鏈接選項-rdynamic

在gcc中,-rdynamic-Wl,-E-Wl,--export-dynamic的作用等價。

-Wl的作用

If the linker is being invoked indirectly, via a compiler driver (e.g. gcc) then all the linker command line options should be prefixed by -Wl, (or whatever is appropriate for the particular compiler driver) like this:

gcc -Wl,--start-group foo.o bar.o -Wl,--end-group

This is important, because otherwise the compiler driver program may silently drop the linker options, resulting in a bad link.

即通過編譯器調用鏈接器並指定鏈接選項時,需要在前面加上-Wl,避免鏈接選項被編譯器忽略,導致鏈接失敗。


下面提供一個示例,動態加載可執行文件。

需要在編譯時使用-fpie/-fPIE並在鏈接時使用-pie。類似-fpic/-fPIC,區別在於生成的代碼供可執行文件鏈接。

在此示例中,將動態加載可執行文件,-rdynamic為必須使用的鏈接選項。

其中在main.cpp中實現一個進行減法的add()函數,並將加載可執行文件dlopen_test_elf。

CMakeLists.txt

# Executable dlopen test
cmake_minimum_required(VERSION 3.10)
project(dlopen_test_elf CXX)
set(WORK_DIR "${CMAKE_SOURCE_DIR}")

set(CMAKE_CXX_FLAGS "-O0 -ggdb -fpie")
set(CMAKE_EXE_LINKER_FLAGS "-ldl -rdynamic -pie")

file(GLOB_RECURSE SRC_FILES ${WORK_DIR}/src/main.cpp)

add_executable(${PROJECT_NAME} ${SRC_FILES})

src/main.cpp

// file : main.cpp
#include <stdio.h>  
#include <stdlib.h>   // EXIT_FAILURE
#include <dlfcn.h>    // dlopen, dlerror, dlsym, dlclose

extern "C"{
	int add(int a, int b) { return a-b; };
}

typedef int(* FUNC_ADD)(int, int); // define alias of function pointer
const char* dllPath = "./dlopen_test_elf";

int main()
{
	void* handle = dlopen( dllPath, RTLD_LAZY );

	if( !handle )
	{
		fprintf( stderr, "[%s](%d) dlopen get error: %s\n", __FILE__, __LINE__, dlerror() );
		exit( EXIT_FAILURE );
	}

	do{ // for resource handle
		FUNC_ADD add_func = (FUNC_ADD)dlsym( handle, "add" );
		printf( "1 add 2 is %d \n", add_func(1,2) );
	}while(0); // for resource handle

	dlclose( handle );

	return 0;
}

./dlopen_test_elf

1 add 2 is -1

readelf –dyn-syms

通過使用readelf --dyn-syms也可以觀察使用/不使用連接選項-rdynamic時,生成二進制文件中的符號信息是不同的。

不使用-rdynamic時,

Symbol table '.dynsym' contains 15 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     6: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     7: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     8: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)
     9: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlerror@GLIBC_2.2.5 (3)
    10: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlclose@GLIBC_2.2.5 (3)
    11: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlopen@GLIBC_2.2.5 (3)
    12: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlsym@GLIBC_2.2.5 (3)
    13: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND stderr@GLIBC_2.2.5 (2)
    14: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fprintf@GLIBC_2.2.5 (2)

使用-rdynamic時,

Symbol table '.dynsym' contains 29 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     6: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     7: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     8: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)
     9: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlerror@GLIBC_2.2.5 (3)
    10: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlclose@GLIBC_2.2.5 (3)
    11: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlopen@GLIBC_2.2.5 (3)
    12: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND dlsym@GLIBC_2.2.5 (3)
    13: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  UND stderr@GLIBC_2.2.5 (2)
    14: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND fprintf@GLIBC_2.2.5 (2)
    15: 0000000000202068     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    16: 0000000000202070     8 OBJECT  GLOBAL DEFAULT   25 dllPath
    17: 0000000000202080     0 NOTYPE  GLOBAL DEFAULT   26 _end
    18: 0000000000202078     0 NOTYPE  GLOBAL DEFAULT   25 _edata
    19: 0000000000202068     0 NOTYPE  WEAK   DEFAULT   25 data_start
    20: 0000000000000a40     0 FUNC    GLOBAL DEFAULT   13 _start
    21: 0000000000000ca0     4 OBJECT  GLOBAL DEFAULT   15 _IO_stdin_used
    22: 0000000000000c20   101 FUNC    GLOBAL DEFAULT   13 __libc_csu_init
    23: 0000000000000b55    22 FUNC    GLOBAL DEFAULT   13 add
    24: 0000000000202078     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    25: 0000000000000b6b   179 FUNC    GLOBAL DEFAULT   13 main
    26: 0000000000000968     0 FUNC    GLOBAL DEFAULT   11 _init
    27: 0000000000000c90     2 FUNC    GLOBAL DEFAULT   13 __libc_csu_fini
    28: 0000000000000c94     0 FUNC    GLOBAL DEFAULT   14 _fini

因此,-rdynamic的作用在於導出可執行文件的符號信息。

參考資料

  1. 採用dlopen、dlsym、dlclose加載動態鏈接庫 – 獃獃的張先生 – 簡書
  2. 採用dlopen、dlsym、dlclose加載動態鏈接庫【總結】 – Rabbit_Dale – 博客園
  3. dlopen 與dlsym-好喜兒-ChinaUnix博客
  4. dlopen dlsym dlclose解析 – yujixi123的專欄 – CSDN博客
  5. linux動態庫加載的秘密 – LiuYanYGZ – 博客園
  6. c – What exactly does “-rdynamic” do and when exactly is it needed_ – Stack Overflow
  7. shared libraries – Why do we need -rdynamic option in gcc_ – Stack Overflow