Linux | c&cpp | Email | github | QQ群:425043908 关注本站

itarticle.cc

您现在的位置是:网站首页 -> 代码相关 文章内容

C/C++ 彻底了解链接器(三)-itarticl.cc-IT技术类文章记录&分享

发布时间: 9年前代码相关 132人已围观返回

Windows DLLs


虽然 Unix 和 Windows 平台的共享库原理大体上一致,但有一些细节如果不注意的话,还是很容易犯错的。

导出符号

两个平台之间最大的区别在于 Windows 的共享库不会自动导出程序中的符号。在 Unix 上,每一个目标文件中所有与共享库关联的符号,对用户而言都是可见的,但在 Windows 上,为了使这些符号可见,程序员必须做一些额外的操作,例如,将其导出

从 Windows DLL 中导出符号信息的方法一共有三种(这三种方法可以同时用于同一个库中)

在源代码中为符号声明关键字declspec(dllexport),例如:

1
2
//C
__declspec(dllexport)intmy_exported_function(intx,doubley);

使用链接器 LINK.EXE 提供的选项: /export:_symbol_to_export

1
2
//C
LINK.EXE /dll /export:my_exported_function

使用链接器的 /DEF:_def_file_ 这一选项,它可用于导入模块定义文件(module definition (.DEF) file)。该文件中有一部分名为 EXPORTS,它包含了你想导出的符号信息。

1
2
3
4
//C
EXPORTS
my_exported_function
my_other_exported_function

对于以上三种方法而言,第一种方法最为简便,因为编译器会自行为你考虑命名改写(name mangling)的问题。

.LIB 以及其它与库相关的文件

Windows 的这一特性(符号不可见)导致了 Windows 库的第二重复杂性:链接器在将各符号链接到一起时所需要的导出符号信息,并不包含在 DLL 文件中,而是包含在与之相对应的 .LIB 文件中。

与某个 DLL 库关联的 .LIB 文件列出了该 DLL 库中(导出的)符号以及符号地址。所有使用这个 DLL 库的程序都必须同时访问它的 .LIB 文件才能保证所有符号正常链接

有件经常把人弄糊涂的事:静态库的扩展名也是 .LIB

事实上,与 Windows 库有关的文件类型简直千姿百态,除了上文件提及的 .LIB 文件和(可选的).DEF 文件外,以下列出了你可能遇到的所有与 Windows 库有关的文件。

链接输出文件:

library.DLL: 库的实现代码,它可实时导入每个使用该库的可执行程序。

library.LIB: “导入库”文件,给定了 DLL 文件中的符号及地址列表。只有当 DLL 导出某些符号时才会产生这个文件,如果没有符号导出,.LIB 文件也就没有存在的必要了。所有使用该库的程序在链接阶段都必需用到该文件。

library.EXP: 这是动态库处在链接期时的一个“导出文件”,当链接中二进制文件出现循环依赖时,该文件就派上用场了。

library.ILK: 如果链接时指定了 /INCREMENTAL 选项这就意味着开启了增量链接功能,该文件保存着增量链接时的相关状态,以供该动态库下次增量链接时使用。

library.PDB: 如果链接时指定了 /DEBUG 选项,将生成程序数据库,包含了整个库的所有调试信息。

library.MAP: 如果链接时指定了 /MAP 选项,将生成描述整个库内部布局信息的文件。

链接输入文件:

library.LIB: “导入库”文件,给定了链接时所需的 DLL 文件中的符号及地址列表。

library.LIB: 这是一个静态库文件,包含了链接时所需的系统目标文件集。请注意:使用 .LIB 文件时,需要区分是静态库还是“导入库”。

library.DEF: 这是一个“模块定义”文件,该文件对链接库的各种细节都给予了控制权,其中包括符号导出([译者注4])。

library.EXP: 这是动态库处于链接期时的一个“导出文件”,它提前运行一个与库文件对应的 LIB.EXE 工具([译者注5]),并提前生成对应的 .LIB 文件。当链接中的二进制文件出现循环依赖时,该文件就派上用场了。

library.ILK: 增量链接状态文件,详见上文。

library.RES: 资源文件,包含了执行过程中所需的各种GUI部件信息,这些信息都将包含在最终的二进制文件中。

这与Unix正好相反,Unix中这些外部库所需的大部分信息一般情况下全都包含在库文件里了。

导入符号

正如上文所提,Windows 要求 DLL 显示地声明需要导出的符号,同样,使用动态库文件的程序必须显示地声明它们想导入的符号。这是一个可选功能,但对于16位 Windows 里的一些古老功能来说,这个选项可以实现运行速度的优化。

我们所要做的是在源代码里加上这么一句话:declare the symbol as __declspec(dllimport) ,看上去就像这样:

1
2
3
C
__declspec(dllimport)intfunction_from_some_dll(intx,doubley);
__declspec(dllimport)externintglobal_var_from_some_dll;

这一方法看似稀松平常,但由于 C 语言里所有函数以及全局变量都在且仅在头文件中声明一次,这会让我们陷入一个两难的境地:DLL 中包含了函数和变量的定义的代码需要进行符号导出,但 DLL 以外的代码需进行符号导入。

一般采取的回避方式是在头文件中加上一个预处理宏(preprocessor macro):

1
2
3
4
5
6
7
8
9
//C
#ifdef EXPORTING_XYZ_DLL_SYMS
#define XYZ_LINKAGE __declspec(dllexport)
#else
#define XYZ_LINKAGE __declspec(dllimport)
#endif
XYZ_LINKAGEintxyz_exported_function(intx);
XYZ_LINKAGEexternintxyz_exported_variable;

DLL 中的包含函数和变量定义的 C 文件可以确保它在引用这个头文件之前就已经定义(#defined)了预处理宏EXPORTING_XYZ_DLL_SYMS,对于符号的导出也是如此。任何引用了该文件的其他代码,都无需定义这一符号也无需指示符号的导入。

循环依赖

动态链接库的终级难题在于 Windows 比 Unix 严厉,它要求每个符号在链接期都必须是“已解决符号”。在 Unix 中,链接一个包含链接器不认识的“未解决符号”的动态库是可行的。在 Windows 中,任何使用引用了共享库的代码都必须提供库中的符号,否则程序将加载失败,Windows 不允许任何形式的松懈。

在大部分系统中,这不算个事儿,可执行程序依赖于高级库,高级库依赖于低级库,所有的一切都通过层层反向链接关联到一起:从低级库开始,再到高级库,最终到依赖它们的可执行文件。

然而,一旦两个二进制文件存在着相互依赖关系,事情就变得诡异起来。如果 X.DLL 使用了 Y.DLL 中的符号,而 Y.DLL 又反过来需要 X.DLL 中的符号,于是就出现了“先有鸡还有先有蛋”的问题:无论先链接哪个库,都无法找到另一个库的符号。

Windows提供了一种绕过这一问题的方法,大致过程如下:

首先,生成一个库 X 的假链接。运行 LIB.EXE(不是 LINK.EXE)来生成 X.LIB 文件,这跟用 LIB.EXE 生成的一模一样。这时不会生成 X.DLL 文件,取而代之的是 X.EXP 文件。

以正常的方式进行库 Y 的链接:使用上一步中生成的X.LIB,导出 Y.DLL 和 Y.LIB。

最后以合适的方式链接库 X,这跟正常的链接方式几乎没什么差别,唯一不同的是额外需要第一步生成的 X.EXP 文件。之后采用正常的方式,导入上一步生成的 Y.LIB,并生成 X.DLL。与正常方式不同之处在于,链接时将不再生成 X.LIB 文件,因为第一步已经生成过了(这在 .EXP 文件中有标记指示)

当然,更好的解决方法是去重构这些库来消除这种循环依赖……。


将 C++ 加入示意图

C++ 在 C 的基础上提供了更多额外的功能,这些功能中有很大一部分需要与链接器的操作进行交互。这并不符合最初的设计——最初 C++ 实现的目的是作为 C 编译器的前端,因此作为后端的链接器并不需要任何改变——但随着 C++ 功能日趋复杂,链接器也不得不加入对这些功能的支持。

函数重载和命名改编

C++ 的第一个改变是允许函数重载,即程序中允许存在多个不同版本的同名函数,当然它们的类型不同(即函数签名不同)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//C
intmax(intx,inty)
{
if(x>y)returnx;
elsereturny;
}
floatmax(floatx,floaty)
{
if(x>y)returnx;
elsereturny;
}
doublemax(doublex,doubley)
{
if(x>y)returnx;
elsereturny;
}

这一做法显然给链接器出了一个难题:当其它代码调用 max 函数时,它到底是想调用哪一个呢?

链接器采用一种称为“命名改写(name mangling)”的方法来解决这一问题,之所以使用“mangling”是因为这个词有损坏、弄糟之意,与函数签名相关的信息都被“损坏”了,变成一种文本形式,成为链接器眼中符号的实际名称。不同的函数签名将被“损坏”成不同的名称,这样就解决了函数名重复的问题。

我不打算深入讲解“命名改写”的具体规则,因为不同编译平台有不同的改编规则,但我们通过查看事例代码所对应的目标文件结构,可以对“命名改写”规则有一个直观的认识(记诠住, nm 命令绝对是您不可或缺的好伙伴!):

1
2
3
4
5
6
7
8
//C
fn_overload.o中的符号:
Name Value Class Type Size Line Section
__gxx_personality_v0| | U | NOTYPE| | |*UND*
_Z3maxii |00000000| T | FUNC|00000021| |.text
_Z3maxff |00000022| T | FUNC|00000029| |.text
_Z3maxdd |0000004c| T | FUNC|00000041| |.text

从上图中,我们可以看出,三个名为 max 的函数,在目标文件中的名称并不相同。聪明的你应该能够猜得出来 max 的后两个字母来自各自的参数类型:i表示int, f表示float,d表示double(如果把类、命名空间、模板,以及操作符重载都加入命名改编,情况将更为复杂)。

需要注意的是,如果你希望能够在链接器可识别的名称(the mangled names)和用户可识别的名称(the demangled names)之间相互转化,则需要另外单独使用别的程序(如 c++filt)或者加入命令行选项(对于 GNU 的 nm 命令,可以加 –demangle 选项),这样你就可以得到如下信息:

1
2
3
4
5
6
7
C
fn_overload.o中的符号:
Name Value Class Type Size Line Section
__gxx_personality_v0| | U | NOTYPE| | |*UND*
max(int,int) |00000000| T | FUNC|00000021| |.text
max(float,float) |00000022| T | FUNC|00000029| |.text
max(double,double) |0000004c| T | FUNC|00000041| |.text

命名改写机制最常见的“坑”就是当 C 和 C++ 代码混在一起写的时候,C++ 编译器生成的符号名称都经过了改编处理,而 C 编译器生成的符号名称就是它在源文件中的名称。为了避免这一问题,C++ 采用 extern “C” 来声明和定义 C 语言函数,其目的在于告诉 C++ 编译器这个函数名不能被改变,既可能因为相关的 C 代码需要调用 C++ 函数的定义,也可能因为相关的 C++ 代码需要调用 C 函数。

回到本文最初的例子,现在我们很容易能看出这很可能是因为某人将 C 和 C++ 链接到一起却忘了加 extern “C” 声明。

1
2
3
4
5
6
//C
g++ -o test1 test1a.o test1b.o
test1a.o(.text+0x18): In function `main':
: undefined reference to `findmax(int,int)'
collect2: ld returned 1 exit status

这条错误信息中最明显的提示点是那个函数签名——它不仅仅是在抱怨你没定义 findmax ,换句话说,C++ 代码实际上想找的是形如 “_Z7findmaxii” 的符号,可只找到 “findmax”,因此链接失败了。

顺便提一句,注意 extern “C” 的链接声明对成员函数无效(见 C++ 标准文档的7.5.4章节)

静态初始化

C++ 比C 多出的另一个大到足以影响链接器行为的功能是对象的构造函数(constructors)。构造函数是用于初始化对象内容的一段代码。就其本身而言,它在概念上等同于一个变量的初始值,但关键的区别在于,它初始化的不是一个变量,而是一整块代码。

让我们回想一下前文所学内容:一个全局变量可以给定一个特殊的初值。在 C 语言中,为全局变量设定一个初始是件轻而易举的事:在程序即将运行之时,将可执行文件中数据段所存的值拷贝至内存对应的地址即可。

在 C++ 中,构造过程所需完成的操作远比“拷贝定值”复杂得多:在程序开始正常运行之前,类层次体系中各种构造函数里的代码都必须提前执行。

为了处理好这一切,编译器在每一个C++文件的目标文件中都保存了一些额外信息,例如,保存了某个文件所需的构造函数列表。在链接阶段,链接器把所有列表合成一张大表,通过一次次扫描该表来调用每个全局对象对应的构造函数。

请注意,所有这些全局对象的构造函数的调用顺序并未定义——因此,这完全取决于链接器的实现。(更多细节可以参看 Scott Meyers 的 Effective C++ 一书,第二版的条款47和href=”http://www.amazon.com/gp/product/0321334876″>第三版的条款4有相应的介绍)

我们同样可以使用 nm 命令来查看这些列表信息。以下面这段 C++ 代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//C
classFred {
private:
intx;
inty;
public:
Fred() : x(1), y(2) {}
Fred(intz) : x(z), y(3) {}
};
Fred theFred;
Fred theOtherFred(55);
这段代码的 nm 输出如下(已经进行了反命名改编处理):
//C
//global_obj.o中的符号:
Name Value Class Type Size Line Section
__gxx_personality_v0| | U | NOTYPE| | |*UND*
__static_initialization_and_destruction_0(int,int)|00000000| t | FUNC|00000039| |.text
Fred::Fred(int) |00000000| W | FUNC|00000017| |.text._ZN4FredC1Ei
Fred::Fred() |00000000| W | FUNC|00000018| |.text._ZN4FredC1Ev
theFred |00000000| B | OBJECT|00000008| |.bss
theOtherFred |00000008| B | OBJECT|00000008| |.bss
global constructors keyed to theFred |0000003a| t | FUNC|0000001a| |.text

这段输出内容给了很多信息,但我们感兴趣的是 Class 列为 W 的那两项(W 在这里表示弱符号 [译者注6]),它们的 Section 列形如”.gnu.linkonce.t.stuff”,这些都是全局对象构造函数的特征,我们可以从 “Name” 这一列看出些端倪——在不同情况下使用两个构造函数中的一个。

模板

上文中,我们给了三个不同 max 函数的例子,在这个例子中,每个 max 函数带有不同的参数,但函数体的代码实际上完全相同,作为程序员,我们得为这种“复制粘贴”完全相同的代码感到可耻。

于是 C++ 引入了模板(templates)这一概念来避免这种情况——只需一份代码来完全所有工作。我们先创建一个只含有一个 max 函数代码的头文件 max_template.h :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//C
template<classT>
T max(T x, T y)
{
if(x>y)returnx;
elsereturny;
}
然后将该头文件应用到 C++ 代码中,并使用这个模板函数:
//C
#include "max_template.h"
intmain()
{
inta=1;
intb=2;
intc;
c = max(a,b);// 编译能自动识别出当前需要调用的是 max(int,int
doublex = 1.1;
floaty = 2.2;
doublez;
z = max<double>(x,y);// 编译器无法识别,强制调用 max(double,double
return0;
}

这个例子中的C++文件调用了两种类型的 max(int,int) 和 max(double,double),而对于另一个 C++ 文件,可能会调用该模板的其他实例化函数:比如max(float,float),甚至还有可能是更复杂的 max(MyFloatingPointClass,MyFloatingPointClass)。

模板的每一个实例化函数执行时使用的都是不同的机器码,因此在程序的链接阶段,编译器和链接器需要确保程序调用的每个模板实例函数都扩展出相应类型的程序代码(但对于未被调用的其他模板实例函数而言,不会有任何多余的代码生成,这样可以避免程序代码过度膨胀)。

那么编译器和链接器是如何做到这一切换呢?一般来说,有两种实现方案:一种是将每个实例函数代码展开,另一种是将实例化操作延迟到链接阶段(我喜欢将这两种方法分别称作“普通方法”(the sane way)和 “Sun方法”(the sane way)(译注:之所以取这个名字,是因为Solaris系统下的编译器采用这样的方法,而Solaris是当年Sun公司旗下最著名的操作系统。))。

对于第一种方法,即将每个实例函数代码展开,每个目标文件中都会包含它所调用的所有模板函数的代码,以上文的 C++ 文件为例,目标文件内容如下:

1
2
3
4
5
6
7
//C
max_template.o的符号:
Name Value Class Type Size Line Section
__gxx_personality_v0 | | U | NOTYPE| | |*UND*
doublemax<double>(double,double) |00000000| W | FUNC|00000041| |.text._Z3maxIdET_S0_S0_
intmax<int>(int,int) |00000000| W | FUNC|00000021| |.text._Z3maxIiET_S0_S0_
main |00000000| T | FUNC|00000073| |.text

我们可以从中看出目标文件中即包含了 max(int,int) 也包含了 max(double,double)。

目标函数将这两个函数的定义标记成“弱符号”(weak symbos),这表示当链接器最终生成可执行程序时,将只留下所有重复定义的其中之一,剩余的定义都将弃之不用(如果设计者愿意,那么可以将链接器设计成检查所有的重复定义,它们含有几乎完全相同的代码)。这种方法最显著的缺点是每个目标文件都将占用更多的磁盘空间

另一种方法通常是 Solaris 系统中的 C++ 编译器所使用的方法,它不会在目标文件中包含任何跟模板相关的代码,只将这些符号标记成“未定义”。等到了链接阶段,链接器将所有模板实例化函数对应的未定义符号收集在一起,然后为它们生成相应的机器码

这种方法可以节省每个目标文件所占的空间大小,但其缺点在于链接器必须跟踪头文件所包含的源代码,还必须在链接阶段调用C++编译器,这会减慢链接速度

动态载入库

接下来我们将讨论本文最后一个 C++ 特性:共享库的动态加载。前文介绍了如何使用共享库,这意味着最终的链接操作可以延迟到程序真正运行的时刻。在现代操作系统中,甚至还可以再往后延迟。

这需要通过一对系统调用来实现,分别是:dlopen 和 dlsym (Windows里大致对应的调用分别是LoadLibrary 和 GetProcAddress)。前者获取共享库的名称,并将其载入运行程序的地址空间。当然,载入的这个共享库本身也可能存在未定义符号,因此,调用 dlopen 很可能同时触发多个其他共享库的载入。

dlopen 为使用者提供了两种选择,一种是一次性解决导入库的所有未定义符号(RTLD_NOW),另一种是按遇到的顺序一个个解决未定义符号(RTLD_LAZY)。第一种方法意味着调用一次dlopen需要等待相当长的时间,而第二种方法则可能需要冒一定的风险,即在程序运行过程中,突然发现某个未定义符号无法解决,将导致程序崩溃终止。

如果你想从动态库中找出符号对应的名字显然不可能。但正如以往的编程问题一样,这很容易通过添加额外的间接寻址方式解决,即使用指针而不是用引用来指向该符号。dlsym 调用时,需要传入一个 string 类型的参数,表示要查找的符号的名称,返回该符号所在地址的指针(如果没找到就返回 NULL)。

动态载入与C++特性的交互

这种动态载入的功能让人觉得眼前一亮,但它是如何与影响链接器行为的各种 C++ 特性进行交互的呢?

首当其冲的棘手问题是修改(mangled)后的变量名。当调用 dlsym 时,它接收一个包含符号名的字符串,这里的符号名必须是链接器可识别的名字,换句话说,即修改后的变量名。

由于命名改编机制随着平台和编译器的变化而变化,这意味着你想进行跨平台动态定位 C++ 符号几乎完全不可能。即使你乐意花大把的时间在某个特定的编译器上,并钻研其内部机制,仍然还有更多的问题在前方等着你——这些问题超出了普通类 C 函数的范围,你还必须要把虚表(vtables)这种类型的问题纳入到你考虑的范畴。

总而言之,一般来说最好的办法是只使用唯一一个常用的入口点 extern “C”,它可以已经调用过dlsym了。这个入口点可以是一个工厂函数,返回一个指向 C++ 对象的指针,它允许访问所有的 C++ 精华

在一个已经调用过 dlopen 的库中,编译器可以为全局目标选出构造函数,因为库中可以定义各种特殊符号,这样链接器无论在加载还是运行时,只要库需要动态地加载或者取消,都可以调用这些符号,因此所有需要用到的构造函数和析构函数都可以放到里面。在 Unix 系统中,将这两种函数称为 _init 和 _fini,而对于使用 GNU 工具链的各种现代操作系统中,则是所有标记为__attribute__((constructor)) 和 __attribute__((destructor)) 的函数。在 Windows 中,相应的函数是带有 reason 或者 DLL_PROCESS_ATTACH,再或者 DLL_PROCESS_DETACH 参数的 DllMain 函数。

最后,动态加载可以很好地例用 “折叠重复”(fold duplicated)的方法来进行模板实例化,但对于“链接时编译模板” (compile templates at link time)这一方法则要棘手得多——因为在这种情况下,“链接期”(link time)发生在程序运行之后(而且很可能不是在当初写源代码的机器上运行)。你需要查看编译器和链接器的手册来避免这一问题。

参考资料

本文有意跳过了许多链接器内部实现机制的细节,因为我认为针对程序员们日常工作时所遇到与链接器有关的问题,本文所介绍的内容已经覆盖了其中的95%。

如果你想进行更多的深入了解,可以参考下列文章:

John Levine,链接器和加载器:本书对链接器和加载器的工作原理给出了非常非常详细的介绍,我所略过的所有细节都包含在本书中。本文有一个在线版本供大家翻看(或者说是出版本前的草稿)

在Max操作系统 OS X 上,关于Mach-O([译者注7])格式的二进制文件有一篇超好的文章 [27-Mar-06 更新]

Peter Van Der Linden, C 专家编程:本书详细介绍了如何将 C 语言代码转换成可执行程序,关于这方面内容的书,我再没看过写得比本书更牛的了。

Scott Meyers, More Effective C++: 本书一共有34条条款,覆盖了用 C 和 C++ 共同写出的程序中的存在的各种陷阱(无论是否与链接器有关)。

Bjarne Stroustrup, C++ 语言的设计和演化: 本书的第11.3节讨论了 C++ 的链接以及链接产生的缘由。

Margaret A. Ellis & Bjarne Stroustrup, 带注释的C++参考手册: 本书的7.2c一节中,介绍了一种命名改编机制。

ELF格式的相关参考文献[PDF版]

特别推荐两篇很有趣有文章,分别是:creating tiny Linux executables和minimal Hello World

“How to Write Shared Libraries” [PDF版]: 由 Ulrich Drepper 所写,本文对ELF和重定位给出了更为详细的介绍。

非常感谢Mike Capp和Ed Wilson为本文提出的宝贵建议。


译者注:

[1]

.bss: BSS全称为Block Started by Symbol(或者block storage segment)。在采用段式内存管理的架构中,BSS 段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS段属于静态内存分配。

.data: 表示数据段(data segment),通常用来存放程序中已初始化的全局变量的一块内存区,也属于静态内存分配

.text: 表示代码段(text segment),通常用来存放程序执行代码的一块内存区,这部分区域的大小在程序运行前就已经确定,并且内存区属于只读,代码段中也可能包含少量的只读常数变量,例如字符串常量等。

COM: 全称common段。在《程序员的自我修养》一书中,指出,如果全局变量初始化的值不为0,则保存在data段,值为0,则保存在bss段,如果没有初始化,则保存在common段。当变量为static,且未初始化时放在bss段,否则放在com段

以上内容参考自: 《.bss .data .text 区别》和 《通过未初始化全局变量,研究BSS段和COMMON段的不同 》

[2] ld.so 是 Unit 系统上的动态链接器,常见的变体有两个:ld.so 针对 a.out 格式的二进制可执行文件,ld-linux.so 针对 ELF 格式的二进制可执行文件。当应用程序需要使用动态链接库里的函数时,由 ld.so 负责加载。搜索动态链接库的顺序依此是:环境变量LD——LD_BRARY_PATH(a.out格式),LD_LIBRARY_PATH(ELF格式);在Linux中,LD_PRELOAD 指定的目录具有最高优先权。 缓存文件 /etc/ld.so.cache。此为上述环境变量指定目录的二进制索引文件。更新缓存的命令是 ldconfig。 默认目录,先在 /lib 中寻找,再到 /usr/lib 中寻找。(以上来自wiki百科)

[3] b.o: 这里原文是b.c,想来是作者的笔误

[4] def文件(module definition file模块定义文件)是用来创建dll和对应的导出库的。来自:http://www.fx114.net/qa-71-109424.aspx

def模块定义文件,用来创建dll和对应的lib def文件中,可以指定dll将会导出哪些符号给用户使用,链接器会根据def文件的说明来生成dll和lib。 在def文件中使用exports语句,可以让dll内部符号可见(默认不可见)

[5] exp:导出文件。当生成了两个dll:a.dll, b.dll,二者需要互相调用对方中的函数(循环依赖),这里存在的问题是:生成a.dll时需要b.lib,生成b.dll需要a.lib,这就变成死锁了,微软的解决办尘埃 是使用exp文件,在两个dll生成之前,使用lib.exe(library manager tool库管理工具)来创建一个DLL对应的.lib和.exp 即先生成a.lib, a.exp,然后利用a.lib去生成b.dll和b.lib,这时再用b.lib来生成a.dll。a.exp文件中缓存了a.dll的导出信息,linker加载a.exp中的信息。

[6] 对于C语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号(C++并没有将未初始化的全局符号视为弱符号)。我们也可以通过GCC的”__attribute((weak))”来定义任何一个强符号为弱符号。注意,强符号和弱符号都是针对定义来说的,不是针对符号的引用。 来自:http://blog.csdn.net/astrotycoon/article/details/8008629

[7] Mach不是Mac,Mac是苹果电脑Macintosh的简称,而Mach则是一种操作系统内核。Mach内核被NeXT公司的NeXTSTEP操作系统使用。在Mach上,一种可执行的文件格是就是Mach-O(Mach Object file format)。1996年,乔布斯将NeXTSTEP带回苹果,成为了OS X的内核基础。所以虽然Mac OS X是Unix的“后代”,但所主要支持的可执行文件格式是Mach-O。来自:http://www.molotang.com/articles/1935.html 和 http://www.amazon.com/gp/product/0321334876

发布时间: 9年前代码相关132人已围观返回回到顶端

很赞哦! (1)

文章评论

  • 请先说点什么
    热门评论
    131人参与,0条评论

站点信息

  • 建站时间:2016-04-01
  • 文章统计:728条
  • 文章评论:82条
  • QQ群二维码:扫描二维码,互相交流