Chinaunix首页 | 论坛 | 博客
  • 博客访问: 111063
  • 博文数量: 24
  • 博客积分: 1475
  • 博客等级: 上尉
  • 技术积分: 291
  • 用 户 组: 普通用户
  • 注册时间: 2006-04-04 14:14
个人简介

交互设计在未来很有前途,不要再说是做界面的了。

文章分类

全部博文(24)

文章存档

2013年(2)

2012年(2)

2010年(4)

2009年(2)

2007年(11)

2006年(3)

我的朋友

分类: C/C++

2006-04-04 14:17:51

1.      动态库、静态库、可执行程序简介
程序的最终运行实际就是一个地址操作的过程,所以从我们写的源代码到程序运行,实
际就是如何将我们用高级语言编写的源代码转化成机器可以识别的地址码,并为之分配
资源使之运行的过程。
动态库历史
A.单模块(无库文件,不需要链接)->多模块(静态链接库)-》多模块(动态链
接库)
B.所有实现在一起-》将不同模块实现相分离,但可执行程序仍包含其实现代码-》
将不同模块实现分离,可执行程序只记录模块信息,不包含其实现代码
严格意义上讲,对于动态链接,unix下和windows下采用不同的链接库形式,unix下
为共享库形式,windows下才为动态库形式,两者在装入内存时存在一些区别,但这里
我们关注的重点是它们的编码实现,所以在本文中将其统称为动态库。
从源代码到程序运行的三个阶段:
编译:在一个编译单元(可以理解为一个源文件)内部完成符号名到地址的转换工作
链接:符号解析和重定位
符号解析:当一个编译单元使用了在该单元中没有定义过的函数或全局变量时,编译器生成的符号表会标记出所有这样的函数或全局变量,而链接器的责任就是要到别的编译单元或模块中去查找它们的定义,如果没有找到合适的定义或者找到的定义不唯一,符号解析都无法正常完成。
重定位:编译器在编译生成目标文件时,通常都使用从零开始的相对地址。然而,在链接过程中,链接器将从一个指定的地址开始,根据输入的目标文件的顺序以段为单位将它们一个接一个的拼装起来。除了目标文件的拼装之外,在重定位的过程中还完成了两个任务:一是生成最终的符号表;二是对代码段中的某些位置进行修改,所有需要修改的位置都由编译器生成的重定位表指出
装入:分析可执行文件的内容,为其分配运行所需要的各种资源,装入的过程可以认为
主要由操作系统来完成,在这里暂不进行讨论。
两种链接方式:
程序的链接方式分为两种:静态链接和动态链接,这两种方式分别对应于静态库文件
和动态库文件。
程序的不同模块可以并行开发,然后独立编译为相应的目标文件。在得到了所有的目标
文件后,我们可以根据需要把某个模块的所有目标文件做成静态库文件或者动态库文件,然后在需要他们的地方通过链接进行使用。
静态库只在程序链接时起作用,最终的执行程序脱离静态库运行[2]。我们可以简单的把静态库理解成把所有的目标文件打了一个包,程序链接静态库跟直接去链接这些目标文件效果是相同的,只是在编写makefile时方便一些。
在使用动态链接时,需要在程序映象中每个调用库函数的地方打一个桩(stub)。stub是一小段代码,用于定位已装入内存的相应的库;如果所需的库还不在内存中,stub将指出如何将该函数所在的库装入内存。
静态库同时被链接到程序代码,被主程序调用的函数目标文件连同主程序组合成单一的
可执行程序,而动态库在可执行程序中只是在调用库函数的地方打一个桩。所以,用动
态库实现的可执行程序要比静态库实现的可执行程序占用硬盘空间要小。
2.      完成动态库需要的几个要素
确定要输出的符号(函数名,变量名,类名等等)
Unix下输出符号的确定方法
库文件:默认情况下,所有的全局变量和函数。但可以通过编译器选项(例如Tru64 unix下的ld命令有hidden ,non_hidden,hidden_symbol symbol等选项供设置)
调用者:extern
Windows下输出符号的确定方法
对于变量:
关键字方式:
库文件:__declspec(dllexport)    调用者:__declspec(dllimport)
模块定义文件方式
库文件:*.def                      调用者:__declspec(dllimport)
对于函数:
   关键字方式:
库文件:__declspec(dllexport)          调用者:__declspec(dllimport)或extern  
模块定义文件方式:
库文件:*.def                          调用者:__declspec(dllimport)或extern
库文件的格式
libxxx.So文件:unix下的动态库文件
libxxx.a文件:unix下的静态库文件
xxx.dll文件:windows下的动态库文件
xxx.lib文件:1.windows下的静态库文件2.windows下动态库文件的符号文件
说明:1.为什么windows下需要一个动态库文件的符号文件,而unix下不需要呢。
参考so文件和dll文件的格式,可以发现在unix的so文件中,在代码段中额外增加了一部分内容,用来存放输入输出符号和重定位的一些信息,所以就不再单独需要文件对要输出的符号进行说明了。而在windows下则是引入库文件(.lib文件)包含被DLL导出的函数的名称和位置,DLL包含实际的函数和数据.
    2.虽然windows下的调用者可以使用__declspec(dllimport)或extern来声明导入的函数,但如果使用extern,会导致动态库中函数与调用者所使用函数地址不一致,如果用到函数地址比较就会出现问题,所以还是采用__declspec(dllimport)的方式进行声明为好。
Unix下的生成命令举例
 Tru64 unix 下的ld命令:
ld –shared –o libxxx.so yyy.o –lc
         Solaris下使用gcc编译器
            gcc –G –o libxxx.so yyy.o –lc
Windows下的生成方式说明:
需要对工程属性进行一些设置,在一些介绍vc的书上或者网络上都可以查得到。
在此不再赘述。
使用方法:
静态加载(隐式链接)
所谓静态加载,就是指由编译系统完成对DLL的加载和应用程序结束时DLL卸载的编码,在unix下一般采用-lxxx选项在生成可执行程序时使用动态库,在windows下通常采用的调用方式是把产生动态连接库时产生的.LIB文件加入到应用程序的工程中。
动态加载(显式链接)
在程序中,通过操作系统或编译环境提供的函数来实现对动态库的加载。在unix
平台下,主要函数有dlopen(),dlsym(),dlclose(),dlerror()等。在windows平台下,主
要有LoadLibray(),GetProcAddress(),FreeLibrary()等函数。这些函数具体的功能和使
用方法可以参考各自的手册或帮助文档。
    注:我们可以利用动态加载的方法来进行插件开发,使代码实现二进制层面的复用。
3.      如何实现跨平台的动态库
a.      不使用头文件的unix和windows动态库文件
unix我们以tru64 unix为例,编译器和链接器为cc和ld.windows我们使用windows server 2003,编译器和链接器使用vc7
动态库源代码
文件1.dll_file1.c
 unix
windows
#include
#include
extern int abc;
void f()
{
abc=100;
printf(function f called abc = %d\n,abc);
}
#include
#include
extern int abc;
__declspec(dllexport) void f()
{
abc=100;
printf(function f called abc = %d\n,abc);
}
文件2.dll_file2.c
 unix
windows
int abc = 0;
__declspec(dllexport) int abc = 0;
调用者源代码 dll_test.c
 unix
windows
#include
#include
extern void f();
extern int abc;
main()
{
printf(abc === %d\n,abc);
f();
}
#include
#include
__declspec(dllimport) int abc;
__declspec(dllimport) void f;
main()
{
printf(abc === %d\n,abc);
f();
}
 
说明:
  同一个变量,在windows下的不同的文件中的声明方式是不一样的。
输出结果为:
       abc === 0
       function f called abc = 100
b.     引入头文件后的代码
引入头文件后,该头文件将为动态库和调用者源文件共同使用。我们需要把用到的函数和全局变量的声明放到头文件中,而在实际编码中,为了保证代码的可读性,我们把变量的定义也放到了头文件中.这在一般的介绍c/c++编程的文章中是禁止的,但这里我们可以利用严格的宏定义来使得这种定义方式对我们的代码影响降至最低.
文件1.头文件dll_head.h
 unix
Windows
#ifdef __MY_DLL_H
#define __MY_DLL_H
#include
#include
#if defined(DLLSRC_COMPILED) #define EXPORT 
#else
#define EXPORT extern
#endif
EXPORT int abc;
EXPORT void f();
#endif
#ifdef __MY_DLL_H
#define __MY_DLL_H
#include
#include
#if defined(TEST_DLL_COMPILED)
#if defined(DLLSRC_COMPILED)
#define EXPORT __declspec(dllexport)
#else
#define EXPORT extern
#endif
#else
#define EXPORT __declspec(dllimport)
#endif
EXPORT int abc ;
EXPORT void f();
#endif
文件2.dll_file1.c
 unix
windows
#include
#include
#include dll_head.h
void f()
{
abc=100;
printf(function f called abc = %d\n,abc);
}
#include
#include
#include dll_head.h
__declspec(dllexport) void f()
{
abc=100;
printf(function f called abc = %d\n,abc);
}
文件3.dll_file2.c
 Unix
windows
#define DLLSRC_COMPILED
#include dll_head.h
#define DLLSRC_COMPILED
#include dll_head.h
调用者源代码 dll_test.c
 Unix
Windows
#include
#include
#include dll_head.h
main()
{
printf(abc === %d\n,abc);
f();
}
#include
#include
#include dll_head.h
main()
{
printf(abc === %d\n,abc);
f();
}
说明:
1.windows下在编译动态库时需要添加宏选项TEST_DLL_COMPILED
2.默认情况下,变量或函数说明语句都是声明而非定义(即使没有使用extern关键字).所以函数的声明不存在任何问题.
3.无法对变量赋初始值,而只能由系统赋0初始值.
4.上述方案特意将变量和函数的定义放在两个不同的文件中,而在实际情况中我们更多的是采用在一个头文件中声明的所有变量和函数都在一个源文件中实现的方法,如果头文件中包含了多个文件中定义的变量时会比较麻烦,但那样就与我们头文件的设计初衷相背离了.
c.      合并后的代码
合并unix和windows代码要作的主要工作就是将变量和函数的声明和定义方式进行统一处理.
文件1.头文件dll_head.h
#ifdef __MY_DLL_H
#define __MY_DLL_H
#include
#include
#if defined(DLLSRC_COMPILED)
#ifdef WIN32
#define EXPORT __declspec(dllexport)
#else
#define EXPORT
#endif
#else
#if defined(WIN32) && !defined(TEST_DLL_COPILED)
#define EXPORT __declspec(dllimport)
#else
#define EXPORT extern 
#endif
#endif
EXPORT int abc ;
EXPORT void f();
#endif
文件2.dll_file1.c
#include
#include
#include dll_head.h
#ifdef WIN32
__declspec(dllexport) void f()
#else
void f()
#endif
{
abc=100;
printf(function f called abc = %d\n,abc);
}
文件3.dll_file2.c
#define DLLSRC_COMPILED
#include dll_head.h
调用者源代码 dll_test.c
#include
#include
#include dll_head.h
main()  
{
printf(abc === %d\n,abc);
f();
}
说明:
windows下,如果动态库的生成使用了模块定义文件,就不再需要在定义函数或者变量的时候使用__declspec(dllexport)对模块定义文件中导出的符号进行声明了.但需要在调用方声明为__declspec(dllimport).(对函数也可以声明为extern).这样就不能在头文件中统一进行声明,破坏了程序的可读性.而且,对于c++程序,def文件中还需要注明导出函数的修饰符,这也带来可读性的极大的降低,所以一般我们还是应该使用__declspec(dllexport)来进行声明而不要使用模块定义文件.
4.      常见错误
a.     defined multiple
如果在头文件中没有使用上面我们提供的用宏区分不同的包含文件的类型而又直接在头文件中进行了定义,很容易造成变量mulitple defined的错误。而且关键是在Tru64 unix的编译器默认设置中,这不被看作是一个错误,编译器会自作聪明的选择一个定义。这有时候就会造成我们程序调试上的困难。在gcc和vc环境中,这会被作为一个错误报告出来。
b.windows动态库变量数值错误
如果我们在头文件中只是声明int abc,而没有将其设为导出。在windows下,动态库与调用者会各用各的,导致在动态库中对变量的赋值无法被调用者使用,从而会导致程序运行出错。
5.      小知识点
1.     c++代码中使用c语言生成的动态库文件时对函数类型要使用extern “C”进行说明
2.     windows下要求在动态库中申请的内存要在动态库中释放,而不能由调用者来释放。也可以通过设置工程属性来取消这种限制(将运行时库设置为“多线程调试调试dll)。
3.      尽量不要使用操作系统提供的头文件,避免由于系统差异造成的编译问题。
4.      尽量只在头文件中只进行声明,不进行定义。而且只要是声明就要采用extern标识符,避免编译器进行默认处理。
5.      如果找不到函数声明,默认返回值为int,如果使用其返回值,将会引起错误.
6.      上面例子是建立在c语言基础上的,对于c++而言,windows下的导入导出还涉及到类、模板等高级概念,在此不在进行一一说明。
6.小结
本文主要介绍了如何在跨平台系统中合理利用头文件来进行编码,从而达到一套代码到处编译的目的.同时对程序开发中遇到的一些问题进行了总结.
参考资料:
1.      王勇:程序的链接和装入及Linux下动态链接的实现
2.     
3.     
4.     
5.     
6.     
7.     
8.     
9.      msdn
10.    
阅读(1669) | 评论(0) | 转发(0) |
0

上一篇:没有了

下一篇:unix与nt开发平台比较(六)--字节对齐

给主人留下些什么吧!~~