爱收集资源网

GCC编译后的汇编代码如何轻松查看?

网络整理 2023-10-01 01:06

#8:a:e:c:b:1:1:6:1:2:f:0:a:5:6:e:f:4:9:2:0:3:e:3:1:2:b:8:1:3:6:2#

#d:e:b:d:7:6:4:5:6:f:f:3:f:9:5:3:9:e:e:2:3:a:2:6:6:7:c:a:5:f:c:0#

这个你们就自行测试吧。另外再补充一点,gcc似乎并不要求函数一定要在被调用之前定义或则申明(MSVC不容许),因为gcc在处理到某个未知类型的函数时,会为其创建一个隐式申明,并假定该函数返回值类型为int。但gcc此时难以检测传递给该函数的形参类型和个数是否正确,不利于编译器为我们排除错误(而且假如该函数的返回值不是int的话也会出错)。所以还是建议你们在函数调用前,先对其定义或申明。

预处理部份说完了,我们接着看编译和汇编。那么哪些是编译?一句话描述:编译就是把预处理以后的文件进行一系列词法分析、语法剖析、语义剖析以及优化后生成的相应汇编代码文件。这一部分我们不能展开说了,一来我没有系统学习过编译原理的内容不敢信口开河,二来这部份要是展开去说须要很厚很厚的一本书了,细节你们就自己学习《编译原理》吧,相关的资料自然就是精典的龙书、虎书和鲸书了。

gcc如何查看编译后的汇编代码呢?命令是 gcc -S HelloWorld.c -o HelloWorld.s,这样输出了汇编代码文件HelloWorld.s,其实输出的文件名可以随便,我是习惯作祟。顺便说一句,这里生成的汇编是沃达丰风格的汇编代码,如果你们更熟悉Intel风格,可以在命令行加上参数 -masm=intel ,这样gcc才会生成Intel风格的汇编代码了(如图,这个好多人不知道哦)。不过gcc的内联汇编只支持沃达丰风格,大家还是找找资料学学沃达丰风格吧。

#a:c:0:9:f:c:8:6:6:3:3:7:f:8:2:e:b:8:b:9:6:d:9:e:6:2:c:c:6:9:9:7#

再出来是汇编步骤,我们继续用一句话来描述:汇编就是将编译后的汇编代码翻译为机器码,几乎每一条汇编指令对应一句机器码。

这里似乎也没有哪些好说的了,命令行 gcc -c HelloWorld.c 可以让编译器只进行到生成目标文件这一步,这样我们才能在目录下见到HelloWorld.o文件了。

Linux下的可执行文件以及目标文件的格式叫作ELF(Executable Linkable Format)。其实Windows下的PE(Portable Executable)也好,ELF也罢,都是COFF(Common file format)格式的一种变种,甚至Windows下的目标文件就是以COFF格式去储存的。不同的操作系统之间的可执行文件的格式一般是不一样的,所以导致了编译好的HelloWorld没有办法直接复制执行,而须要在相关平台上重新编译。当然了,不能运行的诱因自然不是这一点点,不同的操作系统插口(windows API和Linux的System Call)以及相关的泛型不同也是诱因之一。

由于本文的读者定位,我们不能详尽展开说了,有相关需求的朋友可以去看《Windows PE权威指南》和《程序员的自我修养》去详尽了解。

我们接下来看最后的链接过程。这一步是将汇编形成的目标文件和所使用的库函数的目标文件链接生成一个可执行文件的过程。我想在这儿稍稍的扩充一下篇幅,稍微详尽的说一说链接,一来这儿导致的错误一般难以理解和处理,二来使用第三方库在开发中越来越常见了,想着大伙可能更需要稍为了解一些细节了。

我们先介绍gnu binutils工具包,这是一整套的二进制剖析处理工具包。详细介绍请你们参考喂猪百科:

我的fedora早已自带了这套工具包,如果你的发行版没有,请自行搜索安装方式。

这套工具包含了足够多的工具,我们甚至可以拿来研究ELF文件的格式等内容。不过本文只是抛砖引玉,更多的使用方式和方法还是须要你们自己去学习和研究。

由于时间关系,上篇到此就告一段落了,我们的问题2和3还没有给出完整的答案,而且链接还没有详尽去解释和说明。这些内容我们将在上篇中解决,当然,大家也可以先行研究,到时候我们互相学习补充。

上回书我们说到了链接曾经,今天我们来研究最后的链接问题。

链接这个话题延展以后完全可以挪到九霄云外去,为了防止本文牵涉到过多的话题造成言之泛泛,我们先设定本文涉及的范围。我们明天讨论只链接进行的大致步骤及其规则、静态链接库与动态链接库的创建和使用这两大块的问题。至于可执行文件的加载、可执行文件的运行时储存器映像之类的内容我们暂时不讨论。

首先,什么是链接?我们引用CSAPP的定义:链接(linking)是将各类代码和数据部份搜集上去并组合成为一个单一文件的过程,这个文件可被加载(或被拷贝)到存储器并执行。

需要指出的是,链接可以执行于编译时(compile time),也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器(loader)加载到存储器并执行时;甚至执行于运行时(run time),由应用程序来执行。

说了这么多,了解链接有什么用呢?生命如此短暂,我们干嘛要去学习一些根本用不到的东西。当然有用了,继续引用CSAPP的说法,如下:

理解链接器将帮助你构造小型程序。

理解链接器将帮助你防止一些危险的编程错误。

理解链接将帮助你理解语言的作用域是怎样实现的。

理解链接将帮助你理解其他重要的系统概念。

理解链接将使你还能借助共享库。

……

言归正传,我们开始吧。为了防止我们的描述过分沉闷,我们还是以C语言为例吧。想必你们通过我们在下篇中的描述,已经晓得C代码编译后的目标文件了吧。目标文件最终要和标准库进行链接生成最后的可执行文件。那么,标准库和我们生成的目标文件是哪些关系呢?

其实,任何一个程序,它的背后都有一套庞大的代码在支撑着它,以促使该程序才能正常运行。这套代码起码包括入口函数、以及其所依赖的函数构成的函数集合。当然,它还包含了各类标准库函数的实现。

这个“支撑模块”就称作运行时库(Runtime Library)。而C语言的运行库,即被称为C运行时库(CRT)。

CRT大致包括:启动与退出相关的代码(包括入口函数及入口函数所依赖的其他函数)、标准库函数(ANSI C标准规定的函数实现)、I/O相关、堆的封装实现、语言特殊功能的实现以及调试相关。其中标准库函数的实现抢占了主要地位。标准库函数你们想必很熟悉了,而我们平常常用的printf,scanf函数就是标准库函数的成员。C语言标准库在不同的平台上实现了不同的版本,我们只要依赖其插口定义,就能保证程序在不同平台上的一致行为。C语言标准库有24个,囊括标准输入输出、文件操作、字符串操作、数学函数以及日期等等内容。大家有兴趣的可以自行搜索。

既然C语言提供了标准库函数供我们使用,那么以哪些方式提供呢?源代码吗?当然不是了。下面我们引入静态链接库的概念。我们几乎每一次写程序都难免去使用库函数,那么每一次去编译岂不是太麻烦了。干嘛不把标准库函数提早编译好,需要的时侯直接链接呢?我很负责任的说,我们就是如此做的。

那么,标准库以哪些方式存在呢?一个目标文件?我们晓得,链接的最小单位就是一个个目标文件,如果我们只用到一个printf函数,就须要和整个库链接的话岂不是太浪费资源了么?但是,如果把库函数分别定义在彼此独立的代码文件里,这样编译下来的而且一大堆目标文件,有点混乱吧?所以,编辑器系统提供了一种机制,将所有的编译下来的目标文件打包成一个单独的文件,叫做静态库(static library)。当链接器和静态库链接的时侯,链接器会从这个打包的文件中“解压缩”出须要的部份目标文件进行链接。这样就解决了资源浪费的问题。

Linux/Unix系统下ANSI C的库名称作libc.a,另外物理函数单独在libm.a库里。静态库采用一种称为存档(archive)的特殊文件格式来保存。其实就是一个目标文件的集合,文件头描述了每位成员目标文件的位置和大小。

光说不练是假把式,我们自己做个静态库试试。为了简单起见我们就做一个只有两个函数的私有库吧。

我们在swap.c里定义一个swap函数,在add.c里定义了一个add函数。最后还有富含它们申明的calc.h头文件。

#b:6:a:8:b:5:c:5:8:5:f:3:4:a:a:7:a:1:0:a:a:f:8:0:d:0:6:5:c:0:6:e#

#4:b:6:6:a:b:0:b:c:9:e:4:e:0:2:a:d:e:f:c:d:c:9:3:3:1:3:3:c:4:6:e#

#8:0:7:c:6:5:0:0:a:5:d:a:b:3:f:3:6:9:8:a:0:0:a:7:3:0:3:0:5:9:5:f#

#1:d:8:9:0:e:e:2:3:0:3:4:a:4:1:3:6:7:3:6:5:4:9:5:5:8:e:f:7:b:e:5#

#a:e:c:3:d:f:3:1:c:1:f:9:0:7:0:b:3:0:7:8:e:9:8:2:b:6:a:8:6:3:7:f#

#9:6:d:4:a:2:1:5:3:d:3:b:0:7:f:c:8:c:3:d:7:2:b:2:5:c:d:0:b:3:5:1#

我们分别编译它们得到了swap.o和add.o这两个目标文件,最后使用ar命令将其打包为一个静态库。

#9:c:f:2:a:1:8:f:f:6:7:f:7:1:6:9:d:f:b:5:d:1:3:8:e:2:2:6:7:d:7:b#

现在我们如何使用这个静态库呢?我们写一个test.c使用这个库中的swap函数吧。代码如下:

#f:4:0:2:9:9:6:d:0:9:6:f:4:8:9:2:b:0:9:3:1:0:3:0:a:7:8:0:f:8:2:2#

#e:e:4:8:d:1:8:a:2:b:2:2:1:3:b:3:6:3:7:0:c:e:3:c:4:8:3:f:2:9:d:b#

下来是编译执行,命令行执行gcc test.c ./libcalc.a -o test编译,执行。如图,我们输出了预期的结果。

#1:d:2:d:5:d:6:c:f:1:8:f:0:a:3:8:7:8:8:8:0:3:c:4:c:c:8:f:b:3:5:a#

可能你会问,我们使用C语言标准库的时侯,编译并不需要加哪些库名啊。是的,我们不需要。因为标准库早已是标准了,所以会被默认链接。不过由于数学函数库libm.a没有默认链接,所以我们使用了物理函数的代码在编译时须要在命令行指定 -lm 链接(-l是制订链接库,m是去除lib以后的库名),不过现今很多gcc都默认链接libm.c库了,比如我机子上的gcc 4.6.3会默认链接的。

正如我们所见到的,静态链接库解决了一些问题,但是它同时带来了另一些问题。比如说每一个使用了相同的C标准函数的程序都须要和相关目标文件进行链接,浪费c盘空间;当一个程序有多个副本执行时,相同的库代码部份被载入显存,浪费显存;当库代码更新以后,使用这种库的函数必须全部重新编译……

有更好的办法吗?当然有。我们接下来引入动态链接库/共享库(shared library)。

动态链接库/共享库是一个目标模块,在运行时可以加载到任意的存储器地址,并和一个正在运行的程序链接上去。这个过程就是动态链接(dynamic linking),是由一个称作动态链接器(dynamic linker)的程序完成的。

Unix/Linux中共享库的后缀名一般是.so(微软那种恐怕你们很熟悉,就是DLL文件)。怎么完善一个动态链接库呢?

我们还是以里面的代码为例,我们先删掉之前的静态库和目标文件。首先是构建动态链接库,我们执行gcc swap.c add.c -shared -o libcalc.so 就可以了,就那么简单(微软那种有所区别,我们在这儿只为说明概念,有兴趣的朋友请自行搜索)。

顺便说一下,最好在gcc命令行加上一句-fPIC让其生成与位置无关的代码(PIC),具体缘由超出本文范围,故不予讨论。

#e:e:0:2:8:d:8:c:e:2:8:5:1:7:2:6:1:6:a:a:5:0:d:f:6:b:b:8:9:a:2:8#

如何使用呢?我们继续编译测试代码,执行gcc test.c -o test ./libcalc.so即可。运行后我们仍然得到了预期的结果。

#8:b:d:e:3:b:0:5:f:0:6:e:7:3:1:3:1:f:3:6:6:1:a:0:3:b:c:2:5:f:e:c#

这看起来也没啥不一样的啊。其实不然,我们用ldd命令(ldd是我们在下篇中推荐的GNU binutils工具包的组成之一)检查test文件的依赖。

#5:f:1:8:b:f:6:7:2:4:1:6:1:e:8:a:0:0:2:6:a:3:8:e:e:1:6:5:c:8:9:6#

我们听到这个文件能顺利运行须要依赖libcalc.so这个动态库,我们就能看见C语言的标准库默认也是动态链接的(在gcc编译的命令行加上 -static 可以要求静态链接)。

好处在哪?第一,库更新以后,只须要替换掉动态库文件即可,无需编译所有依赖库的可执行文件。第二,程序有多个副本执行时,内存中只须要一份库代码,节省空间。

大家想想,C语言标准库很多程序都在用,但显存只有一份代码,这样节约的空间很可观吧,而且如果库代码发觉bug,只须要更新libc.so即可,所有程序即可使用新的代码,岂不是很Cool。

好了,关于库我们就说到这儿了,再说下去就没法子结束了。

我们来瞧瞧链接过程中具体做的事情。链接的步骤大致包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等主要步骤。

首先是地址和空间分配,我们之前谈到的目标文件或许全称称作可重定位目标文件(这只是一种翻译,叫法好多…)。目标文件的格式早已无限度接近可执行文件了,Unix/Linux下的目标文件的格式称作ELF(Executable and Linkable Format,可执行联接格式)。详细的讨论可执行文件的格式超出了本文范围,我们只须要晓得可执行文件中代码,数据,符号等内容分别储存在不同的段中就可以了,这也和保护模式下的显存分段是有一定关系的,但是这个又会扯远就不详谈了……

地址和空间分配以及重定位我们简单表述一下就好,但是符号决议这儿我想稍为展开描述一下。

什么是符号(symbol)?简单说我们在代码中定义的函数和变量可以合称为符号。符号名(symbol name)就是函数名和变量名了。

目标文件的拼合虽然也就是对目标文件之间互相的符号引用的一个修正。我们晓得一个C语言代码文件只要所有的符号被申明过就可以通过编译了,可是对某符号的引用如何晓得位置呢?比如我们调用了printf函数,编译时留下了要填入的函数地址,那么printf函数的实际地址在那呢?这个空位什么时候修正呢?当然是链接的时侯,重定位那一步就是做这个的。但是在更改地址之前须要做符号决议,那哪些是符号决议呢?正如前文所说,编译期间留下了好多须要重新定位的符号,所以目标文件中会有一块区域专门保存符号表。那链接器怎么晓得具体位置呢?其实链接器不知道,所以链接器会搜索全部的待链接的目标文件,寻找这个符号的位置,然后修正每一个符号的地址。

这时候我们可以隆重介绍一个几乎所有人在编译程序的时侯会遇到的问题——符号查找问题。这个一般有两种错误方式,即找不到某符号或则符号重定义。

首先是找不到符号,比如,当我们申明了一个swap函数却没有定义它的时侯,我们调用这个函数的代码可以通过编译,但是在链接期间却会碰到错误。形如“test.c:(.text+0x29): undefined reference to ‘swap’”这样,特别的,MSVC编译器报错是找不到符号_swap。咦?那个顿号那里来的?这得从C语言刚诞生说起。当C语言刚面世的时侯,已经存在不少用汇编语言写好的库了,因为链接器的符号惟一规则,假如该库中存在main函数,我们就不能在C代码中出现main函数了,因为会遭到符号重定义错误,倘若舍弃那些库又是一大损失。所以当时的编译器会对代码中的符号进行修饰(name decoration),C语言的代码会在符号前加顿号,fortran语言在符号前后都加顿号,这样各个目标文件就不会同名了,就解决了符号冲突的问题。随着时间的流逝,操作系统和编译器都被重画了好多遍了,当前的这个问题早已可以无视了。所以新版的gcc通常不会再加顿号做符号修饰了(也可以在编译的命令行加上-fleading-underscore/-fno-fleading-underscore开打开/关闭这个是否加顿号)。而MSVC仍然保留了这个传统,所以我们可以看见_swap这样的修饰。

顺便说一下,符号冲突是很常见的事情,特别是在小型项目的开发中,所以我们须要一个约定良好的命名规则。C++也引入了命名空间来帮助我们解决这种问题,因为C++中存在函数重载这种东西,所以C++的符号修饰愈加复杂难懂(Linux下有c++filt命令帮助我们翻译一个被C++编译器修饰过的符号)。

说了这么多,该到了我们弄成中须要注意的一个大问题了。即存在同名符号时链接器怎样处理。不是刚才说了会报告重名错误吗?怎么又要研究这个?很可惜,不仅仅那么简单。在编译时,编译器会向汇编器输出每位全局符号,分为强(strong)符号和弱符号(weak),汇编器把这个信息蕴涵的编码在可重定位目标文件的符号表里。其中函数和已初始化过的全局变量是强符号,未初始化的全局变量是弱符号。根据强弱符号的定义,GNU链接器采用的规则如下:

不容许多个强符号

如果有一个强符号和一个或多个弱符号,则选择强符号

如果有多个弱符号,则随机选择一个

好了,就三条,第一条会报符号重名错误的,而后两条默认情况下甚至连警告都不会有。关键就在这儿,默认甚至连警告都没有。

我们来个实验具体说一下:

#b:8:6:d:f:e:9:6:d:d:d:0:1:2:6:0:d:5:3:6:0:f:4:c:1:e:8:5:8:3:4:4#

#6:8:e:3:d:d:7:1:1:0:7:7:2:3:1:e:a:a:2:c:1:e:1:8:1:3:d:3:1:b:7:9#

#3:7:7:0:e:f:9:5:7:e:5:1:d:5:9:6:1:d:c:4:6:b:d:1:8:9:1:7:1:f:8:a#

#f:4:1:d:5:9:e:0:0:b:2:9:9:3:f:0:8:e:f:8:8:8:2:9:9:c:f:5:7:b:c:3#

这两个文件编译运行会输出哪些呢?聪明的你想必早已晓得了吧?没错,就是5。

#5:6:1:0:6:0:d:5:d:7:2:e:a:b:6:7:0:0:a:0:5:0:9:d:e:e:b:a:b:2:5:5#

初始化过的n是强符号,被优先选择了。但是,在很复杂的项目代码,这样的错误很难发觉,特别是多线程的……不过当我们怀疑代码中的bug可能是因为此缘由造成的时侯,我们可以在gcc命令行加上-fno-common这个参数,这样链接器在遇见多重定义的符号时,都会给出一条警告信息,而无关强弱符号。如图所示:

#8:e:1:5:9:e:b:2:1:1:8:5:b:f:2:7:6:0:8:a:4:0:b:b:8:5:b:0:4:e:f:2#

好了,到这儿我们的下篇到此也该结束了,不过关于编译链接虽然远比这晦涩复杂的多,我权当抛砖引玉,各位看官自可深入研究。

.so文件 隐式链接
相关文章