程序员的自我修养--链接、装载与库笔记:总结

《程序员的自我修养----链接、装载与库》这本书是2009年出版的,书中有些内容的介绍可能已经过时,已不再适用于现在的C/C++开发,而且书中展示的结果均是在32位机上进行的操作,这里全部是在64位进行的操作。

这里是基于之前所有笔记的简单总结,笔记列表如下:

编译和链接:https://blog.csdn.net/fengbingchun/article/details/88699951

目标文件里面有什么:https://blog.csdn.net/fengbingchun/article/details/88932028

静态链接:https://blog.csdn.net/fengbingchun/article/details/89297427

Windows PE/COFF:https://blog.csdn.net/fengbingchun/article/details/89388105

可执行文件的装载与进程:https://blog.csdn.net/fengbingchun/article/details/100803751

动态链接:https://blog.csdn.net/fengbingchun/article/details/101120761

Linux共享库的组织:https://blog.csdn.net/fengbingchun/article/details/101610029

Windows下动态链接:https://blog.csdn.net/fengbingchun/article/details/101719347

内存:https://blog.csdn.net/fengbingchun/article/details/101780432

运行库:https://blog.csdn.net/fengbingchun/article/details/102142691

系统调用与API:https://blog.csdn.net/fengbingchun/article/details/102166511

下面是对每章中关键语句的摘记:

编译和链接:从最直观的角度来讲,编译器就是将高级语言翻译成机器语言的一个工具。编译过程一般可分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。编译器所能分析的语义是静态语义(Static Semantic),所谓静态语义是指在编译期可以确定的语义,与之对应的动态语义(Dynamic Semantic)就是只有在运行期才能确定的语义。

编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。人们把每个源代码模块独立地编译,然后按照须要将它们”组装”起来,这个组装模块的过程就是链接(Linking)。链接过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等这些步骤。

目标文件里面有什么:现在PC平台流行的可执行文件格式(Executable)主要是Windows下的PE(Portable Executable)和Linux的ELF(Executable Linkable Format),它们都是COFF(Common file format)格式的变种。目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的.obj和Linux的.o),它跟可执行文件的内容与结构很相似。

静态链接库稍有不同,它是把很多目标文件捆绑在一起形成一个文件,再加上一些索引,你可以简单地把它理解为一个包含很多目标文件的文件包。

目标文件中的内容至少有编译后的机器指令代码、数据,还包括了链接时所须要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以”节”(Section)的形式存储,有时候也叫”段”(Segment),在一般情况下,它们都表示一个一定长度的区域。

ELF目标文件格式的最前部是ELF文件头(ELF Header),它包含了描述整个文件的基本属性,比如ELF文件版本、目标机器型号、程序入口地址等。紧接着是ELF文件各个段。其中ELF文件中与段有关的重要结构就是段表(Section Header Table),该表描述了ELF文件包含的所有段的信息,比如每个段的段名、段的长度、在文件中的偏移、读写权限及段的其它属性。ELF的文件头中定义了ELF魔数、文件机器字节长度、数据存储方式、版本、运行平台、ABI版本、ELF重定位类型、硬件平台、硬件平台版本、入口地址、程序头入口和长度、段表的位置和长度及段的数量等。编译器、链接器和装载器都是依靠段表来定位和访问各个段的属性的。

链接过程的本质就是要把多个不同的目标文件之间相互”粘”到一起,或者说像玩具积木一样,可以拼装形成一个整体。在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。在链接中,我们将函数和变量统称为符号(Symbol),函数名或变量名就是符号名(Symbol Name)。每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对应变量和函数来说,符号值就是它们的地址。

函数签名(Function  Signature):包含了一个函数的信息,包括函数名、它的参数类型、它所在的类和名称空间及其它信息。编译器在将C++源代码编译成目标文件时,会将函数和变量的名字进行修饰,形成符号名,也就是说,C++的源代码编译后的目标文件中所使用的符号名是相应的函数和变量的修饰后名称。C++编译器和链接器都使用符号来识别和处理函数和变量,所以对于不同函数签名的函数,即使函数名相同,编译器和链接器都认为它们是不同的函数。

GCC的基本C++名称修饰方法如下:所有的符号都以”_Z”开头,对于嵌套的名字(在名称空间或在类里面的),后面紧跟”N”,然后是各个名称空间和类的名字,每个名字前是名字字符串长度,再以”E”结尾。对于一个函数来说,它的参数列表紧跟在”E”后面,对于int类型来说,就是字母”i”。binutils里面提供了一个叫”c++filt”的工具可以用来解析被修饰过的名称。

签名和名称修饰机制不光被使用到函数上,C++中的全局变量和静态变量也有同样的机制。对于全局变量来说,它跟函数一样都是一个全局可见的名称,它也遵循上面的名称修饰机制,比如一个名称空间foo中的全局变量bar,它修饰后的名字为:_ZN3foo3barE。值得注意的是,变量的类型并没有被加入到修饰后名称中,所以不论这个变量是整型还是浮点型甚至是一个全局对象,它的名称都是一样的。不同的编译器厂商的名称修饰方法可能不同,所以不同的编译器对于同一个函数签名可能对应不同的修饰后名称。

C++编译器会将在extern “C”的大括号内部的代码当作C语言代码处理。C++的宏”__cpluplus”,C++编译器会在编译C++的程序时默认定义这个宏,我们可以使用条件宏来判断当前编译单元是不是C++代码。

对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号(Strong Symbol),未初始化的全局变量为弱符号(Weak Symbol)。我们也可以通过GCC的”__attribute__((weak))”来定义任何一个强符号为弱符号。注意:强符号和弱符号都是针对定义来说的,不是针对符号的引用。

静态链接:对于链接器来说,整个链接过程中,它就是将几个输入目标文件加工后合并成一个输出文件。

现代的链接机制在处理弱符号的时候,采用的就是与COMMON块一样的机制,当不同的目标文件需要的COMMON块空间大小不一致时,以最大的那块为准。未初始化的全局变量就是典型的弱符号。

由于现在的程序和库通常来讲都非常庞大,一个目标文件可能包含成千上百个函数或变量。当我们需要用到某个目标文件中的任意一个函数或变量时,就需要把它整个地链接起来,也就是说那些没有用到的函数也被一起链接了起来。这样的后果是链接输出文件会变得很大,所有用到的没用到的变量和函数都一起塞到了输出文件中。VISUAL C++编译器提供了一个编译选项叫函数级别链接(Functional-Level Linking, /Gy),这个选项的作用就是让所有的函数都向前面模板函数一样,单独保存到一个段里面。当链接器需要用到某个函数时,它就将它合并到输出文件中,对于那些没有用的函数则将它们抛弃。这种做法可以很大程度上减少输出文件的长度,减少空间浪费。但是这个优化选项会减慢编译和链接过程。GCC编译器也提供了类似的机制,它有两个选择分别是”-ffunction-sections”和”-fdata-sections”,这两个选项的作用就是将每个函数或变量分别保持到独立的段中。

一般的一个C/C++程序时从main开始执行的,随着main函数的结束而结束。然而,其实在main函数被调用之前,为了程序能够顺利执行,要先初始化进程执行环境,比如堆分配初始化(malloc, free)、线程子系统等。C++的全局对象构造函数也是在这一时期被执行的,C++的全局对象的构造函数在main之前被执行,C++全局对象的析构函数在main之后被执行。

Linux系统下一般程序的入口是”_start”,这个函数是Linux系统库(Glibc)的一部分。当我们的程序与Glibc库链接在一起形成最终可执行文件以后,这个函数就是程序的初始化部分的入口,程序初始化部分完成一系列初始化过程之后,会调用main函数来执行程序的主体。

API往往是指源代码级别的接口;而ABI是指二进制层面的接口。

其实一个静态库可以简单地看成一组目标文件的集合,即很多目标文件经过压缩打包后形成的一个文件。

可以通过GCC的”--verbose”参数将整个编译链接过程的中间步骤打印出来。

现在GCC、链接器ld、调试器GDB及binutils的其它工具都通过BFD库来处理目标文件,而不是直接操作目标文件。

Windows PE/COFF:微软引入了一种叫PE(Portable Executable)的可执行格式。在Windows平台,VISUAL C++编译器产生的目标文件仍然使用COFF格式,而可执行文件为PE格式。与ELF文件相同,PE/COFF格式也是采用了那种基于段的格式。

“cl.exe”是VISUAL C++的编译器,即”Compiler”的缩写。”/c”参数表示只编译,不链接,即将.c文件编译成.obj文件,而不调用链接器生成.exe文件。

跟GNU的工具链中的”objdump”一样,Visual C++也提供了一个用于查看目标文件和可执行文件的工具,就是”dumpbin.exe”。

可执行文件的装载与进程:可执行文件只有装载到内存以后才能被CPU执行。每个程序被运行起来以后,它将拥有自己独立的虚拟地址空间(Virtual Address Space),这个虚拟地址空间的大小由计算机的硬件平台决定,具体地说是由CPU的位数决定的。

从程序的角度看,我们可以通过判断C语言程序中的指针所占的空间来计算虚拟地址空间的大小。一般来说,C语言指针大小的位数与虚拟地址空间的位数相同,如32位平台下的指针为32位,即4字节;64位平台下的指针为64位,即8字节。

将程序运行所需要的指令和数据全都装入内存中,这样程序就可以顺利运行,这就是最简单的静态装入的方法。动态装入的思想就是程序用到哪个模块,就将哪个模块装入内存,如果不用就暂时不装入,存放在磁盘中。

页映射也不是一下子就把程序的所有数据和指令都装入内存,而是将内存和所有磁盘中的数据和指令按照”页(Page)”为单位划分成若干个页,以后所有的装载和操作的单位都是页。硬件规定页的大小有4096字节、8192字节、2MB、4MB等。

一个虚拟空间由一组页映射函数将虚拟空间的各个页映射至相应的物理空间,那么创建一个虚拟空间实际上并不是创建空间而是创建映射函数所需要的相应的数据结构。由于可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件很多时候又被叫做映像文件(Image)。

可执行文件最终是要被操作系统装载运行的,这个装载的过程一般是通过虚拟内存的页映射机制完成的。

每种可执行文件的格式的开头几个字节都是很特殊的,特别是开头4个字节,常常被称做魔数(Magic Number),通过对魔数的判断可以确定文件的格式和类型。

动态链接:把链接这个过程推迟到了运行时再进行,这就是动态链接(Dynamic Linking)的基本思想。程序在运行时可以动态地选择加载各种程序模块,这个优点就是后来被人们用来制作程序的插件(Plug-in)。

在Linux系统中,ELF动态链接文件被称为动态共享对象(DSO, Dynamic Shared Objects),简称共享对象,它们一般都是以”.so”为扩展名的一些文件;而在Windows系统中,动态链接文件被称为动态链接库(Dynamical Linking Library),它们通常就是很常见的以”.dll”为扩展名的文件。

在静态链接时,整个程序最终只有一个可执行文件,它是一个不可以分割的整体;但是在动态链接下,一个程序被分成了若干个文件,有程序的主要部分,即可执行文件(Program1)和程序所依赖的共享对象(Lib.so),很多时候我们也把这些部分称为模块,即动态链接下可执行文件和共享对象都可以看作是程序的一个模块。

共享对象的最终装载地址在编译时是不确定的,而是在装载时,装载器根据当前地址空间的空闲情况,动态分配一块足够大小的虚拟地址空间给相应的共享对象。

在静态链接时提到过重定位,那时的重定位叫做链接时重定位(Link Time Relocation),而现在这种情况经常被称为装载时重定位(Load Time Relocation)。Linux和GCC支持这种装载时重定位的方法,我们前面在产生共享对象时,使用了两个GCC参数”-shared”和”-fPIC”,如果只使用”-shared”,那么输出的共享对象就是使用装载时重定位的方法。

希望程序模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以实现的基本想法就是把指令中那些需要被修改的部分分离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本。这种方案就是目前被称为地址无关代码(PIC, Position-independent Code)的技术。

使用GCC产生地址无关代码很简单,我们只需要使用”-fPIC”参数接口。”-fPIC”产生的代码要大,而”-fpic”产生的代码相对较小,而且较快。地址无关代码都是跟硬件平台相关的,不同的平台有着不同的实现,”-fpic”在某些平台上会有一些限制。

Linux还提供了一个ldd命令用来查看程序主模块或一个共享库依赖于哪些共享库。

对于Program1来说,我们往往称Program1导入(Import)了foobar函数,foobar是Program1的导入函数(Import Function);而站在Lib.so的角度来看,它实际上定义了foobar()函数,并且提供给其它模块使用,我们往往称Lib.so导出(Export)了foobar()函数,foobar是Lib.so的导出函数(Export Function)。动态链接符号表的结构与静态链接的符号表几乎一样,我们可以简单地将导入函数看作是对其它目标文件中函数的引用;把导出函数看作是在本目标文件定义的函数就可以了。

共享对象需要重定位的主要原因是导入符号的存在。动态链接下,无论是可执行文件或共享对象,一旦它依赖于其它共享对象,也就是说有导入的符号时,那么它的代码或数据中就会有对于导入符号的引用。在编译时这些导入符号的地址未知,在静态链接中,这些未知的地址引用在最终链接时被修正。但是在动态链接中,导入符号的地址在运行时才确定,所以需要在运行时将这些导入符号的引用修正,即需要重定位。

动态链接的步骤基本上分为3步:先是启动动态链接器本身,然后装载所有需要的共享对象,最后是重定位和初始化。

装载共享对象:完成基本自举以后,动态链接器将可执行文件和链接器本身的符号表都合并到一个符号表当中,我们可以称它为全局符号表(Global Symbol Table)。然后链接器开始寻找可执行文件所依赖的共享对象,”.dynamic”段中,有一种类型的入口是DT_NEEDED,它所指出的是该可执行文件(或共享对象)所依赖的共享对象。由此,链接器可以列出可执行文件所需要的所有共享对象,并将这些共享对象的名字放入到一个装载集合中。然后链接器开始从集合里取一个所需要的共享对象的名字,找到相应的文件后打开该文件,读取相应的ELF文件头和”.dynamic”段,然后将它相应的代码段和数据段映射到进程空间中。如果这个ELF共享对象还依赖于其它共享对象,那么将所依赖的共享对象的名字放到装载集合中。如此循环直到所有依赖的共享对象都被装载进来为止。当一个符号需要被加入全局符号表时,如果相同的符号名已经存在,则后加入的符号被忽略。

动态链接器是个非常特殊的共享对象,它不仅是个共享对象,还是个可执行的程序,可以直接在命令行下面运行。共享库和可执行文件实际上没什么区别,除了文件头的标志位和扩展名有所不同之外,其它都是一样的。

支持动态链接的系统往往都支持一种更加灵活的模块加载方式,叫做显式运行时链接(Explicit Run-time Linking),有时候也叫做运行时加载。也就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。这种运行时加载使得程序的模块组织变得很灵活,可以用来实现一些诸如插件、驱动等功能。当程序需要用到某个插件或者驱动的时候,才将相应的模块加载进来,而不需要从一开始就将它们全部加载进来,从而减少了程序启动时间和内存使用。

动态链接实现时,共享模块中符号名冲突时,先装入的符号优先,我们把这种优先级方式称为装载序列(Load Ordering)。

Linux共享库的组织:二进制接口,即ABI(Application Binary Interface)。共享库的ABI跟程序语言有着很大的关系,不同的语言对于接口的兼容性要求不同。ABI对于不同的语言来说,主要包括一些诸如函数调用的堆栈结构、符号命名、参数规则、数据结构的内存分布等方面的规则。很多因素会导致ABI的不兼容,比如不同版本的编译器、操作系统和硬件平台等,使得ABI兼容尤为困难。

Linux有一套规则来命名系统中的每一个共享库,它规定共享库的文件名规则必须如下:libname.so.x.y.z。最前面使用前缀”lib”、中间是库的名字和后缀”.so”,最后面跟着的是三个数字组成的版本号。”x”表示主版本号(Major Version Number),”y”表示次版本号(Minor Version Number),”z”表示发布版本号(Release Version Number)。

每个共享库都有一个对应的”SO-NAME”,这个SO-NAME即共享库的文件名去掉次版本号和发布版本号,保留主版本号。很明显,”SO-NAME”规定了共享库的接口。建立以SO-NAME为名字的软链接目的是,使得所有依赖某个共享库的模块,在编译、链接和运行时,都使用共享库的SO-NAME,而不使用详细的版本号。

Linux中提供了一个工具叫做”ldconfig”,当系统中安装或更新一个共享库时,就需要运行这个工具,它会遍历所有的默认共享库目录,比如/lib、/usr/lib等,然后更新所有的软链接,使它们指向最新版的共享库;如果安装了新的共享库,那么ldconfig会为其创建相应的软链接。

当我们在编译器里面使用共享库的时候(比如使用GCC的”-l”参数链接某个共享库),我们使用了更为简洁的方式,比如需要链接一个libXXX.so.2.6.1的共享库,只需要在编译器命令行里面指定-lXXX即可,可省略所有其它部分。编译器会根据当前环境,在系统中的相关路径(往往由-L参数指定)查找最新版本的”XXX”库。这个”XXX”又被称为共享库的链接名(Link Name)。

在Linux系统中,动态链接器是/lib/ld-linux.so.X(X是版本号),程序所依赖的共享对象全部由动态链接器负责装载和初始化。

ldconfig的程序,这个程序的作用是为共享库目录下的各个共享库创建、删除或更新相应的SO-NAME(即相应的符号链接),这样每个共享库的SO-NAME就能够指向正确的共享库文件。所以理论上讲,如果我们在系统指定的共享库目录下添加、删除或更新任何一个共享库,或者我们更改了/etc/ld.so.conf的配置,都应该运行ldconfig这个程序,以便调整SO-NAME和/etc/ld.so.cache。很多软件包的安装程序在往系统里面安装共享库以后都会调用ldconfig。

改变共享库查找路径最简单的方法是使用LD_LIBRARY_PATH环境变量,这个方法可以临时改变某个应用程序的共享库查找路径,而不会影响系统中的其它程序。在Linux系统中,LD_LIBRARY_PATH是一个由若干个路径组成的环境变量,每个路径之间由冒号分割。默认情况下,LD_LIBRARY_PATH为空。如果我们为某个进程设置了LD_LIBRARY_PATH,那么进程在启动时,动态链接器在查找共享库时,会首先查找由LD_LIBRARY_PATH指定的目录。

创建共享库的过程跟创建一般的共享对象的过程基本一致,最关键的是使用GCC的两个参数,即”-shared”和”-fPIC”。”-shared”表示输出结果是共享库类型的;”-fPIC”表示使用地址无关代码(Position Independent Code)技术来生产输出文件。另外还有一个参数是”-Wl”,这个参数可以将指定的参数传递给链接器。

ld链接器提供了一个”-export-dynamic”的参数,这个参数表示链接器在生产可执行文件时,将所有全局符号导出到动态符号表。我们可以使用一个叫”strip”的工具清除掉共享库或可执行文件的所有符号和调试信息(“strip”是binutils的一部分)。

只要在函数声明时加上”__attribute__((constructor))”的属性,即指定该函数为共享库构造函数,拥有这种属性的函数会在共享库加载时被执行,即在程序的main函数之前执行。我们可以使用在函数声明时加上”__attribute__((destructor))”的属性,这种函数会在main()函数执行完毕之后执行(或者是程序调用exit()时执行)。

Windows下动态链接:DLL即动态链接库(Dynamic-Link Library)的缩写,它相当于Linux下的共享对象。Windows系统中大量采用了这种DLL机制,甚至包括Windows的内核的结构都很大程度依赖于DLL机制。Windows下的DLL文件和EXE文件实际上是一个概念,它们都是有PE格式的二进制文件,稍微有些不同的是PE文件头部中有个符号位表示该文件是EXE或是DLL。DLL的设计目的与共享对象有些出入,DLL更加强调模块化。

ELF的动态链接可以实现运行时加载,使得各种功能模块能以插件的形式存在。在Windows下,也有类似ELF的运行时加载,这种技术在Windows下被应用的更加广泛,比如ActiveX技术就是基于这种运行时加载机制实现的。

Windows支持进程拥有独立的地址空间,一个DLL在不同的进程中拥有不同的私有数据副本,就像ELF共享对象一样。在ELF中,由于代码段是地址无关的,所以它可以实现多个进程之间共享一份代码,但是DLL的代码却并不是地址无关的,所以它只是在某些情况下可以被多个进程间共享。

正常情况下,每个DLL的数据段在各个进程中都是独立的,每个进程都拥有自己的副本。但是Windows允许将DLL的数据段设置成共享的,即任何进程都可以共享该DLL的同一份数据段。

ELF默认导出所有的全局符号。但是在DLL中情况有所不同,我们需要显示地”告诉”编译器我们需要导出某个符号,否则编译器默认所有符号都不导出(Export)。当我们在程序中使用DLL导出的符号时,这个过程被称为导入(Import)。我们可以通过”__declspec”属性关键字来修饰某个函数或者变量,当我们使用”__declspec(dllexport)”时表示该符号是从本DLL导出的符号,”__declspec(dllimport)”表示该符号是从别的DLL导入的符号。

除了使用”__declspec”扩展关键字指定导入导出符号之外,我们也可以使用”.def”文件来声明导入导出符号。

程序使用DLL的过程其实是引用DLL中的导出函数和符号的过程,即导入过程。

在静态链接的时候,”.lib”文件是一组目标文件的集合,在动态链接里面这一点仍然没有错,但是Math.lib中并不真正包含Math.c的代码和数据,它用来描述Math.dll的导出符号。

声明DLL中的某个函数为导出函数的办法有两种,一种就是”__declspec(dllexport)”扩展;另外一种就是采用模块定义(.def)文件声明。

当一个PE需要将一些函数或变量提供给其它PE文件使用时,我们把这种行为叫做符号导出(Symbol Exporting),最典型的情况就是一个DLL将符号导出给EXE文件使用。在Windows PE中,符号导出的概念也是类似,所有导出的符号被集中存放在了被称作导出表(Export Table)的结构中。

link.exe链接器提供了一个”/EXPORT”的参数可以指定导出符号

在创建DLL的同时也会得到一个EXP文件,这个文件实际上是链接器在创建DLL时的临时文件。链接器把这个导出表放到一个临时的目标文件叫做”.edata”的段中,这个目标文件就是EXP文件。

导出重定向(Export Forwarding),就是将某个导出符号重定向到另外一个DLL。

如果我们在某个程序中使用到了来自DLL的函数或者变量,那么我们就把这种行为叫做符号导入(Symbol Importing)。

一个DLL中每一个导出的函数都有一个对应的序号(Ordinal Number)。一个导出函数甚至可以没有函数名,但它必须有一个唯一的序号。

DLL绑定(DLL Binding)方法可以使用editbin.exe工具对EXE或DLL进行绑定。事实上,Windows系统所附带的程序都是与它所在的Windows版本的系统DLL绑定的。

一个清单文件来描述程序集。这个清单文件叫做Manifest文件。Manifest文件描述了程序集的名字、版本号以及程序集的各种资源,同时也描述了该程序集的运行所依赖的资源,包括DLL以及其它资源文件等。Manifest是一个XML的描述文件。

内存:在VC下调试程序的时候,常常看到一些没有初始化的变量或内存区域的值是”烫”。之所以会出现”烫”这么一个奇怪的字,就是因为Debug模式,将所有的分配出来的栈空间的每一个字节都初始化为0xCC。0xCCCC(即两个连续排列的0xCC)的汉字编码就是烫,所以0xCCCC如果被当作文本就是”烫”。将未初始化数据设置为0xCC的理由是这样可以有助于判断一个变量是否没有初始化。如果一个指针变量的值是0xCCCCCCCC,那么我们就可以基本相信这个指针没有经过初始化。当然这个信息仅供参考,编译器查未初始化变量的方法并不能以此为证据。有时编译器还会使用0xCDCDCDCD作为未初始化标记,此时我们就会看到汉字”屯屯”。

如果返回值类型的尺寸太大,C语言在函数返回时会使用一个临时的栈上内存区域作为中转,结果返回值对象会被拷贝两次。因而不到万不得已,不要轻易返回大尺寸的对象。函数传递大尺寸的返回值所使用的方法并不是可移植的,不同的编译器、不同的平台、不同的调用惯例甚至不同的编译参数都有权利采用不同的实现方法。C++程序中都尽量避免返回对象。

堆是一块巨大的内存空间,常常占据整个虚拟空间的绝大部分。在这片空间里,程序可以请求一块连续内存,并自由地使用,这块内存在程序主动放弃之前都会一直保持有效。

Linux下提供了两种堆空间的分配方式,即两个系统调用:一个是brk()系统调用,另外一个是mmap()。

一块连续的虚拟地址空间有可能是若干个不连续的物理页拼凑而成的。

运行库:操作系统装载程序之后,首先运行的代码并不是main的第一行,而是某些别的代码,这些代码负责准备好main函数执行所需要的环境,并且负责调用main函数。

环境变量的格式为key=value的字符串,C语言里可以使用getenv这个函数来获取环境变量信息。

更广义地讲,I/O指代任何操作系统理解为”文件”的事务。许多操作系统,包括Linux和Windows,都将各种具有输入和输出概念的实体----包括设备、磁盘文件、命令行等----统称为文件。在操作系统层面上,文件操作也有类似于FILE的一个概念,在Linux里,这叫做文件描述符(File Descriptor),而在Windows里,叫做句柄(Handle)。

任何一个C程序,它的背后都有一套庞大的代码来进行支撑,以使得该程序能够正常运行。这套代码至少包括入口函数,及其所依赖的函数所构成的函数集合。当然,它还理应包括各种标准库函数的实现。这样的一个代码集合称之为运行时库(Runtime Library)。而C语言的运行库,即被称为C运行库(CRT)。

运行库是平台相关的,因为它与操作系统结合得非常紧密。C语言的运行库从某种程度上来讲是C语言的程序和不同操作系统平台之间的抽象层,它将不同的操作系统API抽象成相同的库函数。

Linux和Windows平台下的两个主要C语言运行库分别为glibc(GNU C Library)和MSVCRT(Microsoft Visual C Run-time)。glibc和MSVCRT事实上是标准C语言运行库的超集,它们各自对C标准库进行了一些扩展。

当使用CRT时(基本上所有的程序都使用CRT),尽量使用_beginthread()/_beginthreadex()/_endthread()/_endthreadex()这组函数来创建线程。

由于全局对象的构建和析构都是由运行库完成的,于是在程序或共享库中有全局对象时,记得不能使用”-nonstartfiles”或”-nostdlib”选项,否则,构建与析构函数将不能正确执行。

所谓flush一个缓冲,是指对写缓冲而言,将缓冲内的数据全部写入实际的文件,并将缓冲清空,这样可以保证文件处于最新的状态。

系统调用与API:系统调用(System Call)是应用程序(运行库也是应用程序的一部分)与操作系统内核之间的接口,它决定了应用程序是如何与内核打交道的。

运行时库将不同的操作系统的系统调用包装为统一固定的接口,使得同样的代码,在不同的操作系统下都可以直接编译,并产生一致的效果。

操作系统一般是通过中断(Interrupt)来从用户态切换到内核态。中断是一个硬件或软件发出的请求,要求CPU暂停当前的工作转手去处理更加重要的事情。

Windows API是指Windows操作系统提供给应用程序开发者的最底层的、最直接与Windows打交道的接口。在Windows操作系统下,CRT是建立在Windows API之上的。

附录

字节序(Byte Order):在不同的计算机体系结构中,对于数据(比特、字节、字)等的存储和传输机制有所不同,因而引发了计算机领域中一个潜在但是又很重要的问题,即通信双方交流的信息单元应该以什么样的顺序进行传送。如果打不成一致的原则,计算机的通信与存储将会无法进行。目前在各种体系的计算机中通常采用的字节存储机制主要有两种:大端(Big-endian)和小端(Little-endian)

MSB是Most Significant Bit/Byte的首字母缩写,通常译为最重要的位或最重要的字节。它通常用来表明在一个bit序列(如一个byte是8个bit组成的一个序列)或一个byte序列(如word是两个byte组成的一个序列)中对整个序列取值影响最大的那个bit/byte。

LSB是Least Significant Bit/Byte的首字母缩写,通常译为最不重要的位或最不重要的字节。它通常用来表明在一个bit序列(如一个byte是8个bit组成的一个序列)或一个byte序列(如word是两个byte组成的一个序列)中对整个序列取值影响最小的那个bit/byte。

Big-endian和Little-endian的区别就是Big-endian规定MSB在存储时放在低地址,在传输时MSB放在流的开始;LSB存储时放在高地址,在传输时放在流的末尾。Little-endian则相反

Little-endian主要用于我们现在的PC的CPU中,即Intel的x86系列兼容机;Big-endian则主要应用在目前的Mac机器中,一般指PowerPC系列处理器。目前的TCP/IP网络及Java虚拟机的字节序都是Big-endian的。

ELF常见段,如下图所示:

常用开发工具命令行参考

(1). gcc, GCC编译器:

-E:只进行预处理并把预处理结果输出。

-c:只编译不链接。

-o <filename>:指定输出文件名。

-S:输出编译后的汇编代码文件。

-I:指定头文件路径。

-e name:指定name为程序入口地址。

-ffreestanding:编译独立的程序,不会自动链接C运行库、启动文件等。

-finline-functions, -fno-inline-functions:启用/关闭内联函数。

-g:在编译结果中加入调试信息,-ggdb就是加入GDB调试器能够识别的格式。

-L <directory>:指定链接时查找路径,多个路径之间用冒号隔开。

-nostartfiles:不要链接启动文件,比如crtbegin.o、crtend.o。

-nostdlib:不要链接标准库文件,主要是C运行库。

-O0:关闭所有优化选项。

-shared:产生共享对象文件。

-static:使用静态链接。

-Wall:对源代码中的多数编译警告进行启用。

-fPIC:使用地址无关代码模式进行编译。

-fPIE:使用地址无关代码模式编译可执行文件。

-XLinker <option>:把option传递给链接器。

-Wl <option>:把option传递给链接器,与上面的选项类似。

-fomit-frmae-pointer:禁止使用EBP作为函数帧指针。

-fno-builtin:禁止GCC编译器内置函数。

-fno-stack-protector:是指关闭堆栈保护功能。

-ffunction-sections:将每个函数编译到独立的代码段。

-fdata-sections:将全局/静态变量编译到独立的数据段。

(2). ld, GNU链接器:

-static:静态链接。

-l<libname>:指定链接某个库。

-e name:指定name为程序入口。

-r:合并目标文件,不进行最终链接。

-L<directory>:指定链接时查找路径,多个路径之间用冒号隔开。

-M:将链接时的符号和地址输出成一个映射文件。

-o:指定输出文件名。

-s:清除输出文件中的符号信息。

-S:清除输出文件中的调试信息。

-T <scriptfile>:指定链接脚本文件。

-version-script <file>:指定符号版本脚本文件。

-soname <name>:指定输出共享库的SONAME。

-export-dynamic:将全局符号全部到出。

-verbose:链接时输出详细信息。

-rpath <path>:指定链接时库查找路径。

(3). objdump, GNU目标文件可执行文件查看器:

-a:列举.a文件中的所有的目标文件。

-b bfdname:指定BFD名。

-C:对于C++符号名进行反修饰(Demangle)。

-g:显示调试信息。

-d:对包含机器指令的段进行反汇编。

-D:对所有的段进行反汇编。

-f:显示目标文件文件头。

-h:显示段表。

-l:显示行号信息。

-p:显示专有头部信息,具体内容取决于文件格式。

-r:显示重定位信息。

-R:显示动态链接重定位信息。

-s:显示文件所有内容。

-S:显示源代码和反汇编代码(包含-d参数)。

-W:显示文件中包含有DWARF调试信息格式的段。

-t:显示文件中的符号表。

-T:显示动态链接符号表。

-x:显示文件的所有文件头。

(4). cl, MSVC编译器:

/c:只编译不链接。

/Za:禁止语言扩展。

/link:链接指定的模块或给链接器传递参数。

/Od:禁止优化。

/O2:以运行速度最快为目标优化。

/O1:以最节省空间为目标优化。

/GR或/GR-:开启或关闭RTTI。

/Gy:开启函数级别链接。

/GS或/GS-:开启或关闭。

/Fa[file]:输出汇编文件。

/E:只进行预处理并且把结果输出。

/I:指定头文件包含目录。

/Zi:启动调试信息。

/LD:编译产生DLL文件。

/LDd:编译产生DLL文件(调试版)。

/MD:与动态多线程版本运行库MSVCRT.LIB链接。

/MDd:与调试版动态多线程版本运行库MSVCRTD.LIB链接。

/MT:与静态多线程版本运行库LIBCMT.LIB链接。

/MTd:与调试版静态多线程版本运行库LIBCMTD.LIB链接。

(5). link, MSVC链接器:

/BASE:address:指定输出文件的基地址。

/DEBUG:输出调试模式版本。

/DEF:filename:指定模块定义文件.DEF。

/DEFAULTLIB:library:指定默认运行库。

/DLL:产生DLL。

/ENTRY:symbol:指定程序入口。

/EXPORT:symbol:指定某个符号为导出符号。

/HEAP:指定默认堆大小。

/LIBPATH:dir:指定链接时库搜素路径。

/MAP[:filename]:产生链接MAP文件。

/NODEFAULTLIB[:library]:禁止默认运行库。

/OUT:filename:指定输出文件名。

/RELEASE:以发布版本产生输出文件。

/STACK:指定默认栈大小。

/SUBSYSTEM:指定子系统。

(6). dumpbin, MSVC的COFF/PE文件查看器:

/ALL:显示所有信息。

/ARCHIVEMEMBERS:显示.LIB文件中所有目标文件列表。

/DEPENDENTS:显示文件的动态链接依赖关系。

/DIRECTIVES:显示链接器指示。

/DISASM:显示反汇编。

/EXPORTS:显示导出函数表。

/HEADERS:显示文件头。

/IMPORTS:显示导入函数表。

/LINENUMERS:显示行号信息。

/RELOCATIONS:显示重定位信息。

/SECTION:name:显示某个段。

/SECTION:显示文件概要信息。

/SYMBOLS:显示文件符号表。

/TLS:显示线程局部存储TLS信息。

GitHubhttps://github.com/fengbingchun/Messy_Test

发布了718 篇原创文章 · 获赞 1131 · 访问量 609万+

猜你喜欢

转载自blog.csdn.net/fengbingchun/article/details/102230252
今日推荐