现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

Linux下glibc和Windows下MSVC CRT对比

2020-01-08 08:22 工业·编程 ⁄ 共 5407字 ⁄ 字号 暂无评论

任何一个C程序要想要得到实现,都离不开背后的一套庞大的代码来进行支持(至少包括入口函数,以及其所依赖的函数所构成的函数集合等),这样一套背后代码被称为运行库,C语言的运行库叫做CRT(Runtime Library)。

C语言的运行库某种程度上就是C语言的程序和不同操作系统平台之间的抽象层。它将不同的操作系统API抽象成相同的库函数(银弹理论的又一次胜利)。比如可以在不同OS上使用fread来读取文件,而事实上,fread在不同的操作系统平台下的实现是不同的,但作为运行库的使用者我们不需要关心这一点。

一个典型的程序运行步骤大致如下:

1. 操作系统在创建进程后,把控制权交到程序的入口,这个入口往往是运行库中的某个入口函数;

2. 入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造等;

3. 入口函数在完成初始化后,调用main函数,正式开始执行程序主体部分;

4. main函数执行完毕以后,返回入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。

根据上述的描述,可以理出来一个C语言运行库至少包含的功能有:

1. 启动与退出:包括入口函数及入口函数所依赖的其他函数等;

2. 标准函数:由C语言标准规定的C语言标准库所拥有的函数实现;

3. I/O:I/O功能的封装和实现(主要是文件句柄的管理);

4. 堆:堆的封装和实现;

5. 语言实现:语言中一些特殊功能的实现

6. 调试:实现调试功能的代码

虽然各个平台下的C语言运行库提供了很多功能,但很多时候它们毕竟有限,比如用户的权限控制、操作系统线程创建等都不是属于标准的C语言运行库,于是我们不得不通过其他的办法,诸如绕过C语言运行直接调用操作系统API或使用其他的库。

值得注意的是,像线程操作这样的功能并不是标准的C语言运行库的一部分,但是glibc和MSVCRT都包含了线程操作的库函数。比如glibc有一个可选的pthread库中pthread_create()函数可以用来创建线程;而MSVCRT中可以使用_beginthread()函数来创建线程。所以glibc和MSVCRT都是标准C语言运行库的超集,它们各自对C标准库进行了一些扩展。

1. glibc运行库

将目标文件.o转换成可执行文件.out。一个简单地示例如下:

$ld –static crt1.o crti.o crtbeginT.o hello.o -start -group –lgcc –lgcc_eh -lc -end –group crtend.o crtn.o

这样一大堆链接才能得到hello.c 最终的可执行文件a.out

glibc启动文件

crt1.o:包含入口函数_start -> 调用__libc_start_main(初始化libc并调用main函数进入真正的程序主体, 实际上最初这个.o并不叫做crt1.o,而叫做crt.o,但是连接器有时候在链接时对于目标文件和库的链接顺序有依赖性要求,crt.o必须是所有输入文件的第一个,所以为了强调这一点,改名成了crt0.o,后来为了支持下面将介绍的.init和.finit启动代码,crt0.o升级为crt1.o,这一点可以从crt1.o的反汇编中看出,它向__libc_start_main()传递了两个函数指针__libc_csu_init和__libc_csu_fini,这两个函数指针将负责调用_init()和_finit(),分别对应在main函数运行之前的初始化函数集合和main主程序结束后的收尾函数集合。)

crti.o & crtn.o:由于C++的出现,对于全局/静态对象必须在main()函数主体执行之前便要创建,于是运行库在每个目标文件中引入两个和初始化相关的段.init和.finit。运行库会在链接阶段,将所有目标文件中的.init和.finit段收集起来合成最终可执行文件的.init和.finit段,用来在main()函数之前批量执行初始化操作和完成之后的析构操作。但是收集.init和.finit段需要额外的辅助(比如计算GOT表),于是crti.o和crt.o文件便是用来辅助完成.init和.finit段的收集以实现初始化工作。

最终输出的可执行文件中的.init段和.finit段中实际上包含了的是_init()和_finit()函数,即对应__libc_csu_init和__libc_csu_fini两个传递给__libc_start_main()的函数指针,而crti.o和crtn.o世纪上包含的便是_init()和_finit()函数的开始和结尾部分

wps5

wps6

首尾结合,中间按照目标文件的顺序依次收拢它们的.init段和.finit段(分别对应各目标文件的对象初始化函数和析构函数),这也是上述的链接指令例子必须按照如下方式组合

$ld crt1.o crti.o  [user_obj] [ system_libs]  crtn.o

除了用来完成在main函数之前/之后的对象构造和析构任务之外,因为.init和.finit段的特殊性,故而可以将一些监控程序性能、调试工具插入在这两个段中,前面说过在gcc下通过如下指令可以显式将某个符号添加到指定段

__attribute__( (section(".init")) )  int foorbar(...) {...}

但是需要注意的是,正常的int foorbar(...) {...}源码在编译后会产生ret等返回指令,这会破坏了正常.init段节奏,所以int foorbar(...) {...}处代码只能采用汇编手动写出来,手动去掉ret等返回指令。

crtbeginT.o & crtend.o:因为glibc只是C语言的库,并不了解C++的机理。真正的全局对象的构造和析构函数的遍历执行,是在crtbeginT.o和crtend.o两个文件中实现的。这两个文件是由GCC实现,配合glibc来完成C++文件编译的。

//crtbegin.cpp在Linux下的简单实现#ifndef WIN32  typedef void(*ctor_func) (void);

ctor_func ctors_begin[1] __attribute__ ((section(".ctors"))) =

{

    (ctor_func) -1 //作为所有ctors段的开始,crtbegin.o的.ctor段里面存储的是一个4字节的-1(0xFFFF FFFF)

//由链接器负责将这个数字改成全局构造函数的数量

};

void run_hooks()

{

const ctor_func* list = ctors_begin;

while((int) *++list != -1)

        (**list)();

}#endif

//crtend.cpp在Linux下的简单实现#ifndef WIN32

typedef void (*ctor_func)(void);

ctor_func crt_end[1] __attribute__((section(".ctors"))) =

{

    (ctor_func) -1 //crtend.o的.ctor段将将该文件的初始化函数指针置为-1

};#endif

libgcc_eh.a & libgcc.a:GCC支持多平台,要处理不同平台之间的差异是比较复杂的一件事,比如在32位系统进行64位大数运算,则编译器其实并不能直接产生正确的CPU指令的,需要libgcc.a提供正确的函数实现。libgcc_eh.a包含了支持C++的异常处理的平台相关函数

2. MSVC CRT运行库

相比于一直较为自由分散的glibc,一直伴随着不同版本的Visual C++发布的MSVC CR(Microsoft Visual C++ C Runtime)倒是看着更有序一点。

同一个版本的MSVC CRT根据不同的属性提供了多种子版本,供不同需求的开发者使用。

1.按照静态/动态链接,可以分为静态版和动态版;

2.按照单线程/多线程,可以分为单线程版和多线程版;

3.按照调试/发布,可以分为调试版和发布版;

4.按照是否支持C++,可以分为纯C运行库版本和支持C++版;

5.按照是否支持托管代码,可以分为支持本地代码/托管代码和纯托管代码版。

2.1 托管代码

/clr托管代码是指代码被编译成MSIL(机器中间型语言,编译原理后套,方便多种语言在同一平台运行)后在.net的Framework下运行,同操作系统底层的交互都交给framework去做,托管代码应用程序可以获得公共语言运行库服务,例如自动垃圾回收、运行库类型检查和安全支持等,这些服务独立于平台和语言的、统一的托管代码应用程序的行为。所谓非托管代码就是脱离framework的管制,直接通过底层API打交道,自己去管理自己的堆内存和安全机制等东西,可以不通过访问公共语言运行库Runtime就可以直接访问操作系统 的程序。

定义:托管代码是可以使用20多种支持Microsoft .NET Framework的高级语言编写的代码,它们包括:C#, J#, Microsoft Visual Basic .NET, Microsoft JScript .NET, 以及C++。所有的语言共享统一的类库集合,并能被编码成为中间语言(IL)。运行库编译器(runtime-aware compiler)在托管执行环境下编译中间语言IL使之成为本地可执行的代码,并使用数组边界和索引检查,异常处理,垃圾回收等手段确保类型的安全。

优点:在托管执行环境中使用托管代码及其编译,可以避免许多典型的导致安全漏洞和不稳定程序的编程错误。同样,许多不可靠的设计也自动的被增强了安全性,例如 类型安全检查,内存管理和释放无效对象。程序员可以花更多的精力关注程序的应用逻辑设计并可以减少代码的编写量。这就意味着更短的开发时间和更健壮的程序。

简单点说,托管代码是Microsoft的中间语言,他主要的作用是在.NET FRAMEWORK的CLR执行代码前去编译源代码,也就是说托管代码充当着翻译的作用,源代码在运行时分为两个阶段:

1.源代码编译为托管代码;(所以源代码可以有很多种,如VB,C#,J#)

2.托管代码编译为microsoft系统的.net平台专用文件(如类库、可执行文件等)。

2.2 MSVC CRT运行库子版本

特性组合:有静态单线程纯C版本地代码调试版 static single-thread pure-C unmanaged debug

有动态多线程纯C版本本地发布版dynamic multiple-thread pure-C managed release

无效的:但是动态链接和单线程是不能组合的dynamic XX single-thread

正是这些不同的特性组合可以让CRT出现很多子版本,于是为微软提供一套运行库的命名方法。

静态版CRT位于MSVC安装目录VC9.0\VC\lib,命名规则为:

libc [p][mt][d].lib

[p]表示C++,即C++标准库;[mt]表示Multi-Thread,即表示支持多线程;d表示Debug,表示调试版本。比如静态的非C++的多线程版CRT的文件名为libcmtd.lib

动态版的CRT每个版本有两个对应 文件,一个用于连接的.lib文件,一个用于运行时用的.dll动态链接库。动态版CRT命名规则:

msvc [p] [u|m] [rt] [d]

[p]表示C++标准库,[u|m]中u表示纯托管代码、m表示托管/本地混合代码,[rt]多线程,[d]表示

wps7

默认情况下,如果在编译链接时不指定选择哪个CRT,编译器会默认选择LIBCMT.lib。

如果要运行C++程序,则在C标准库的基础上提供C++的额外标准库( 只负责实现C++的功能,故而还是要和C标准库共同使用)

wps8

C语言的.drectve段中保存在标准库的链接信息,比如下面这样在编译时明文指定采用多线程,动态链接,调试方式cl /c /MDd target.cpp,然后通过dumpbin工具dumpbin /DIRECTIVES target.obj

wps9

可以看到编译C++程序并指定采用/MDd选项时,将导致最后的可执行文件声明依赖msvprtd.lib、msvcrtd.lib和oldnames.lib。

2.3 MSVC CRT运行库多版本混合使用冲突问题

如果一个程序依赖的DLL文件使用了不同的CRT版本,系统将会提出warning信息,甚至有可能导致程序无法运行。并且一个程序运行了多个不同的CRT版本,将会导致运行期间存在多个不同的CRT副本,这会导致不同CRT管辖的DLL文件之间无法共享一些资源,比如DLL A和DLL B分别使用了静态CRT和动态CRT,这样变回导致在A中打开的文件句柄不能再B中使用,因为两者的文件操作部分并不一致;A中动态申请的内存空间不能在B中释放,因为不同CRT会创建自己专属的堆空间。

所以比较好的方式是保证一个工程中所有的目标文件和DLL文件使用同一个版本的CRT运行库。但是如果用到了第三方DLL文件时,这种情况还是无法避免的。所以这也是采用manifest机制的关键所在,在程序依赖清单中明确给出依赖的DLL文件的版本信息,还有一种方法便是游戏安装包常见的,将所有所需的DLL文件和程序文件打包在一起发布给用户。

作者:墨篙和小奶猫

给我留言

留言无头像?