加载动态链接库——dlopen dlsym dlclose
NAME
dlclose, dlopen, dlmopen - 打开/关闭共享对象
SYNOPSIS
#include
void *dlopen(const char *filename, int flags);
int dlclose(void *handle);
#define _GNU_SOURCE
#include
void *dlmopen (Lmid_t lmid, const char *filename, int flags);
DESCRIPTION
dlopen()
这个函数加载由以null结尾的字符串文件名命名的动态共享对象(共享库)文件,并为加载的对象返回不透明的“句柄”。此句柄与 dlopen API 中的其他函数一起使用,例如dlsym(),dladdr(),dlinfo()和dlclose()。
如果 filename 为 NULL,则返回的句柄用于主程序。如果 filename 包含斜杠(“/”),则它被解释为(相对或绝对)路径名。否则,动态链接器将按如下方式搜索对象(有关详细信息,请参阅ld.so(8)):
(仅限ELF)如果调用程序的可执行文件包含 DT_RPATH 标记,并且不包含 DT_RUNPATH 标记,则会搜索 DT_RPATH 标记中列出的目录。
如果在程序启动时,环境变量 LD_LIBRARY_PATH 被定义为包含以冒号分隔的目录列表,则会搜索这些目录。 (作为安全措施,set-user-ID 和 set-group-ID程序将忽略此变量。)
(仅限ELF)如果调用程序的可执行文件包含 DT_RUNPATH 标记,则搜索该标记中列出的目录。
检查缓存文件/etc/ld.so.cache(由ldconfig(8)维护)以查看它是否包含filename的条目。
搜索目录 /lib和 /usr/lib(按此顺序)。
如果 filename 指定的对象依赖于其他共享对象,则动态链接器也会使用相同的规则自动加载这些对象。 (如果这些对象依次具有依赖性,则此过程可以递归地发生)
flags 参数必须包括以下两个值中的一个:
RTLD_LAZY
执行延迟绑定。仅在执行引用它们的代码时解析符号。如果从未引用该符号,则永远不会解析它(只对函数引用执行延迟绑定;在加载共享对象时,对变量的引用总是立即绑定)。自 glibc 2.1.1,此标志被LD_BIND_NOW环境变量的效果覆盖。
RTLD_NOW
如果指定了此值,或者环境变量LD_BIND_NOW设置为非空字符串,则在dlopen()返回之前,将解析共享对象中的所有未定义符号。如果无法执行此操作,则会返回错误。
flags 也可以通过以下零或多个值进行或运算设置:
RTLD_GLOBAL
此共享对象定义的符号将可用于后续加载的共享对象的符号解析。
RTLD_LOCAL
这与RTLD_GLOBAL相反,如果未指定任何标志,则为默认值。此共享对象中定义的符号不可用于解析后续加载的共享对象中的引用。
RTLD_NODELETE (since glibc 2.2)
在dlclose()期间不要卸载共享对象。因此,如果稍后使用dlopen()重新加载对象,则不会重新初始化对象的静态变量。
RTLD_NOLOAD (since glibc 2.2)
不要加载共享对象。这可用于测试对象是否已经驻留(如果不是,则dlopen()返回 NULL,如果是驻留则返回对象的句柄)。此标志还可用于提升已加载的共享对象上的标志。例如,以前使用RTLD_LOCAL加载的共享对象可以使用RTLD_NOLOAD | RTLD_GLOBAL重新打开。
RTLD_DEEPBIND (since glibc 2.3.4)
将符号的查找范围放在此共享对象的全局范围之前。这意味着自包含对象将优先使用自己的符号,而不是全局符号,这些符号包含在已加载的对象中。
dlmopen()
这个函数除了以下几点与dlopen()有所不同外,都执行同样的任务。
dlmopen()与dlopen()的主要不同之处主要在于它接受另一个参数 lmid,它指定应该被加载的共享对象的链接映射列表(也称为命名空间)。对于命名空间,Lmid_t 是个不透明的句柄。
lmid 参数要么是已经存在的命名空间的ID(这个命名空间可以通过dlinfo RTLD_DI_LMID请求获得)或者是以下几个特殊值中的其中一个:
LM_ID_BASE
在初始命名空间中加载共享对象(即应用程序的命名空间)。
LM_ID_NEWLM
创建新的命名空间并在该命名空间中加载共享对象。该对象必须已正确链接到引用 所有其他需要的共享对象,因为新的命名空间最初为空。
如果 filename 是 NULL,那么 lmid 的值只能是LM_ID_BASE。
dlclose()
dlclose()减少指定句柄 handle 引用的动态加载共享对象的引用计数。如果引用计数减少为0,那么这个动态加载共享对象将被真正卸载。所有在dlopen()被调用的时候自动加载的共享对象将以相同的方式递归关闭。
dlclose()成功返回并不保证与句柄相关的符号将从调用方的地址空间中删除。除了显式通过dlopen()调用产生的引用之外,一些共享对象作为依赖项可能已被隐式加载(和引用计数)。只有当所有引用都已被释放才可以从地址空间中删除共享对象。
RETURN VALUE
执行成功时,dlopen()和dlmopen()返回一个非空句柄。
执行失败时(文件找不到、不可读、错误的格式或者在加载的时候出现错误),dlopen()和dlmopen()返回 NULL。
对于dlclose()成功执行,将返回0值,失败时,返回一个非0值。
以上这些函数产生的错误,其错误信息都可以通过dlerror()获知。
NOTES
dlmopen() 与 命名空间
链接映射列表定义了通过动态链接器解析的符号的孤立命名空间。在命名空间内,被依赖的共享对象根据通常的规则被隐式加载,符号引用同样以通常的规则被解析。但是这种方案受限于已经被(显式和隐式)加载进命名空间的对象的定义。
dlmopen()函数允许对象隔离加载——在新的命名空间中加载共享对象而不暴露其余的应用于新对象提供的符号。注意使用RTLD_LOCAL标志不足以达到此目的,因为它防止一个共享对象的符号对任何其他共享对象可用。在某些情况下,我们可能想使得由一些动态加载共享对象提供的符号对于其他共享对象可用,而不将这些符号暴露给整个应用。这可以通过使用单独的命名空间和RTLD_GLOBAL标志来实现。
dlmopen()函数可以提供比RTLD_LOCAL标志更好的隔离效果。特别是,当共享对象是通过RTLD_LOCAL标志加载的,并且其依赖的共享对象是通过RTLD_GLOBAL加载的,那么有可能升级为RTLD_GLOBAL。因此,明确控制了所有共享对象的依赖的这种情况外,RTLD_LOCAL是不足以隔离加载的共享对象,。
dlmopen()函数的一种用法是多次加载同样的对象。不使用dlmopen()函数来实现这个功能的话,需要创建共享对象的一个副本。而如果使用dlmopen()函数来实现的话,可以通过将相同的共享对象文件加载到不同的命名空间来实现。 glibc实现最多支持16个命名空间。
初始化和终结功能
共享对象可以使用attribute ((constructor))和attribute ((destructor))函数属性。构造函数在dlopen()返回之前执行,而析构函数在dlclose()返回之前执行。共享对象可以导出多个构造函数和析构函数并且优先顺序可以和每个函数相关联来决定它们的执行顺序。
DLSYM
NAME
dlsym, dlvsym - 获取共享对象或可执行文件中符号的地址
SYNOPSIS
#include
void *dlsym(void *handle, const char *symbol);
#define _GNU_SOURCE
#include
void *dlvsym(void *handle, char *symbol, char *version);
DESCRIPTION
dlsym()接受由dlopen()返回的动态加载的共享对象的“句柄”,并返回该符号加载到内存中的地址。如果未找到符号,则在加载该对象时,在指定对象或dlopen()自动加载的任何共享对象中,dlsym()将返回NULL。(dlsym()通过这些共享对象的依赖关系树进行宽度优先搜索。)
因为符号本身可能是 NULL(所以dlsym()返回 NULL 并不意味着错误),因此判断是否错误的正确做法是调用dlerror()清除任何旧的错误条件,然后调用dlsym(),并且再次调用dlerror(),保存其返回值,判断这个保存的值是否是 NULL。
可以在句柄中指定两个特殊的伪句柄:
RTLD_DEFAULT
使用默认共享对象搜索顺序查找所需符号的第一个匹配项。搜索将包括可执行文件中的全局符号及其依赖项,以及使用RTLD_GLOBAL 标志动态加载的共享对象中的符号。
RTLD_NEXT
在当前对象之后的搜索顺序中查找下一个所需符号。这允许人们在另一个共享对象中提供一个函数的包装器,因此,例如,预加载的共享对象中的函数定义(参见ld.so(8)中的LD_PRELOAD)可以找到并调用在另一个共享对象中提供的“真实”函数(或者就此而言,在存在多个预加载层的情况下,函数的“下一个”定义)。
dlvsym()除了比dlsym()多提供了一个额外的参数外,其余与dlsym()相同。
RETURN VALUE
执行成功,这些函数将会返回 symbol 关联的地址。执行失败,它们将返回 NULL。错误的原因可以通过dlerror()进行诊断。
EXAMPLE
#include
#include
#include
#include <gnu/lib-names.h> /* Defines LIBM_SO (which will be a string such as "libm.so.6") */
int
main(void)
{
void *handle;
double (*cosine)(double);
char *error;
handle = dlopen(LIBM_SO, RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
exit(EXIT_FAILURE);
}
dlerror(); /* Clear any existing error */
cosine = (double (*)(double)) dlsym(handle, "cos");
/* According to the ISO C standard, casting between function
pointers and 'void *', as done above, produces undefined results.
POSIX.1-2003 and POSIX.1-2008 accepted this state of affairs and
proposed the following workaround:
*(void **) (&cosine) = dlsym(handle, "cos");
This (clumsy) cast conforms with the ISO C standard and will
avoid any compiler warnings.
The 2013 Technical Corrigendum to POSIX.1-2008 (a.k.a.
POSIX.1-2013) improved matters by requiring that conforming
implementations support casting 'void *' to a function pointer.
Nevertheless, some compilers (e.g., gcc with the '-pedantic'
option) may complain about the cast used in this program. */
error = dlerror();
if (error != NULL) {
fprintf(stderr, "%s\n", error);
exit(EXIT_FAILURE);
}
printf("%f\n", (*cosine)(2.0));
dlclose(handle);
exit(EXIT_SUCCESS); }
cgo 使得在 Golang 中可以使用 C 代码。
Hello World
为了有一个较为直观的了解,我们来看一个简单的例子,创建文件 main.go:
package main
/*
#include
void sayHi() {
printf(“Hi”);
}
*/
import “C”
func main() {
C.sayHi()
}
执行程序:
go run main.go
程序执行并输出 hi(更多的范例可以见 $GOROOT/misc/cgo)。我们可以使用命令:
go build -x main.go
来显示构建时的命令。
Windows 下的准备工作
如果想要在 Windows 上使用 cgo,那么需要安装 gcc 编译器,这里我使用 mingw-w64,可以在这里、这里下载,另外你也可以使用 TDM-GCC。
设置编译和链接标志
我们使用 import “C” 导入的是一个伪包(pseudo-package),我们通过其来使用 C 代码。在 import “C” 之前,紧跟着 import “C” 的注释可以包括:
编译器和链接器标志
C 代码
我们可以通过 #cgo 指令来设置编译器和链接器标志,例如:
// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo amd64 386 CFLAGS: -DX86=1
// #cgo LDFLAGS: -lpng
// #include
import "C"
附带提及一点的是,这些指令中可以包含构建约束(build constraint),详细内容见:http://golang.org/pkg/go/build/#hdr-Build_Constraints。
常用的 #cgo 指令有:
CPPFLAGS、CFLAGS 指令被用于编译当前包中的 C 文件(任何的 .c、.s、.S 文件)
CPPFLAGS、CXXFLAGS 指令被用于编译当前包中的 C++ 文件(任何的 .cpp、.cc、.cxx 文件)
LDFLAGS 指令用于指定链接器标志
pkg-config 指令用于通过 pkg-config 工具获取编译器和链接器标志(例如:#cgo pkg-config: png cairo)
在 Golang 的工具发现存在使用 import “C” 的 Golang 源文件时,它将会查找当前目录下的非 Golang 源文件并编译它们作为此 Golang 包的一部分:
文件 test.c
#include
void sayHi() {
printf(“Hi”);
}
文件 test.go
package main
/*
extern void sayHi();
*/
import “C”
func main() {
C.sayHi()
}
进行构建工作:
go build
需要注意的是,使用 go build test.go 将不会编译 test.c 文件。
Golang 引用 C
结构体上需要注意的点:
C 结构体的域名称如果为 Golang 的关键字时,访问时需要在域名称前面加上 _。比如说,C 中有一个结构体变量 x,此变量对应的结构体中有一个域 type,那么在 Golang 中需要通过 x._type 来访问 type 域
结构体的位域、非对齐数据等无法在 Golang 中表示时会被忽略
Golang 结构体中不能使用 C 类型的域
标准的 C 数值类型对应:
C.char
C.schar(signed char)
C.uchar(unsigned char)
C.short
C.ushort(unsigned short)
C.int
C.uint(unsigned int)
C.long
C.ulong(unsigned long)
C.longlong(long long)
C.ulonglong(unsigned long long)
C.float
C.double
任何的 C 函数(包括 void 函数)都可以返回一个返回值和 C 的 errno 变量(作为错误):
n, err := C.sqrt(-1)
_, err := C.voidFunc()
直接调用 C 函数指针目前还无法支持。
有一些特殊的函数可以用于 C 类型和 Golang 类型之间转换(通过数据拷贝的方式),伪定义如下:
// Golang 的字符串转为 C 字符串
// C 的字符串是使用 malloc 分配的,因此,此函数的调用者
// 需要调用 C.free 来释放内存
func C.CString(string) *C.char
// 转换 C 字符串到 Golang 字符串
func C.GoString(*C.char) string
// 转换一定长度的 C 字符串到 Golang 字符串
func C.GoStringN(*C.char, C.int) string
// 转换一块 C 内存区域到 Golang 的字节数组中去
func C.GoBytes(unsafe.Pointer, C.int) []byte
其他需要注意的点:
C 语言中的 void* 对应 unsafe.Pointer
C 语言中的结构、联合、枚举类型(而非变量),在 Golang 中需要加上 struct_、union_、enum_ 前缀访问。由于 Golang 中没有联合这种数据类型,因此 C 的联合在 Golang 中被表示为字节数组
和 C 语言等价的那些类型是不可以导出的
C 引用 Golang
Golang 的函数可以导出给 C 使用:
//export MyFunction
func MyFunction(arg1, arg2 int, arg3 string) int64 {…}
//export MyFunction2
func MyFunction2(arg1, arg2 int, arg3 string) (int64, *C.char) {…}
对应的 C 代码为(被生成在 _cgo_export.h 中):
extern int64 MyFunction(int arg1, int arg2, GoString arg3);
extern struct MyFunction2_return MyFunction2(int arg1, int arg2, GoString arg3);
有几点需要注意:
Golang 函数的多个返回值会被映射为一个 C 结构体
使用 //export 来使得 Golang 函数可以在 C 中引用。在使用了 //export 之后,此 Golang 源文件中就不能包含 C 的定义(但可以存在声明)