seo超级外链工具/网站自动化宣传机器(gcc我没有系统学习过编译原理的内容(图))

优采云 发布时间: 2021-12-28 20:13

  seo超级外链工具/网站自动化宣传机器(gcc我没有系统学习过编译原理的内容(图))

  1

  intprintf(constchar*format, ...);

  请自行测试。另外,gcc在调用之前不需要定义或声明函数(MSVC不允许),因为gcc在处理未知类型的函数时,会为它创建一个隐式声明,并假定这个函数的返回值类型是内部 但是gcc此时无法检查传递给函数的实参的类型和个数是否正确,这不利于编译器为我们排除错误(如果函数的返回值不是int,也会犯错误)。因此,建议您在调用之前定义或声明该函数。

  预处理部分完成,我们来看编译和组装。那么什么是编译呢?一句话描述: 编译是对预处理文件进行一系列词法分析、语法分析、语义分析和优化后生成的相应汇编代码文件。这部分我们无法展开。首先,我没有系统地研究过编译原理。我不敢相信。第二,如果把这部分展开说,需要一本很厚很厚的书。详细可以学习《编译原理》。“吧,相关资料自然是经典的龙书、虎书、鲸书。

  gcc 如何查看编译后的汇编代码?命令是gcc -S HelloWorld.c -o HelloWorld.s,所以输出的是汇编代码文件HelloWorld.s。其实输出文件名可以随意,我已经习惯了。顺便说一下,这里生成的程序集是 AT&T 风格的程序集代码。如果你对Intel风格比较熟悉,可以在命令行加上参数-masm=intel,这样gcc就会生成Intel风格的汇编代码(如图,这个很多人不知道)。不过gcc的内联汇编只支持AT&T风格,所以找资料学习一下AT&T风格吧。

  

  接下来是汇编步骤,我们继续用一句话来描述:汇编就是将编译好的汇编代码翻译成机器码,几乎每条汇编指令都对应一个机器码。

  这里其实没什么好说的。命令行 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的定义:链接是将各种代码和数据部分采集

并组合成单个文件的过程,该文件可以加载(或复制)到内存中并执行。

  需要强调的是,链接可以在编译时(compile time)执行,也就是将源代码翻译成机器码的时候;也可以在加载时执行,即程序被加载器加载到内存并执行的时候;甚至在运行时(run time)执行,由应用程序执行。

  说了这么多,理解链接有什么用呢?人生苦短,我们为什么要学习一些我们根本不需要的东西?当然有用。继续引用CSAPP的声明如下:

  了解链接器将帮助您构建大型程序。了解链接器将帮助您避免一些危险的编程错误。理解链接将帮助您了解语言的范围是如何实现的。了解这些链接将帮助您了解其他重要的系统概念。了解链接将使您能够利用共享库。...

  言归正传,让我们开始吧。为了避免我们的描述过于枯燥,我们以C语言为例。想必,通过我们上一篇的描述,你已经知道了C代码编译后的目标文件。最后,目标文件必须与标准库链接以生成最终的可执行文件。那么,标准库和我们生成的目标文件是什么关系呢?

  其实,任何程序的背后,都有庞大的代码支撑,程序才能正常运行。这组代码至少包括入口函数和它所依赖的函数组成的函数集。当然,它还收录

了各种标准库函数的实现。

  这个“支持模块”称为运行库。C 语言的运行库称为 C 运行库 (CRT)。

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

  既然C语言提供了标准库函数供我们使用,那么以什么形式呢?源代码?当然不是。下面我们介绍一下静态链接库的概念。几乎每次写程序,都难免会用到库函数,所以每次都编译太麻烦。为什么不提前编译标准库函数,需要的时候直接链接呢?我很负责任,说这就是我们所做的。

  那么,标准库以什么形式存在呢?目标文件?我们知道链接的最小单位是每个目标文件。如果只使用一个printf函数,需要链接整个库,岂不是浪费资源?但是,如果将库函数定义在单独的代码文件中,那么大量的目标文件会被这样编译,有点混乱,对吧?因此,编辑器系统提供了一种机制,可以将所有已编译的目标文件打包到一个称为静态库的文件中。当链接器和静态库链接时,链接器会从打包文件中“解压”一些需要的目标文件以进行链接。这样就解决了资源浪费的问题。

  Linux/Unix系统下的ANSI C库名为libc.a,数学函数单独在libm.a库中。静态库以称为存档的特殊文件格式保存。其实就是目标文件的集合,文件头描述了每个成员的目标文件的位置和大小。

  只说不练都是假的,我们自己试试静态库吧。为了简单起见,让我们制作一个只有两个功能的私有库。

  我们在 swap.c 中定义了一个交换函数,在 add.c 中定义了一个 add 函数。最后,还有收录

它们的声明的 calc.h 头文件。

  1

  2

  3

  4

  5

  6

  7

  // 交换.c

  voidswap(int*num1, int*num2)

  {

  inttmp = *num1;

  *num1 = *num2;

  *num2 = tmp;

  }

  1

  2

  3

  4

  5

  // 添加.c

  intadd(INTA,INTB)

  {

  返回a + b;

  }

  1

  2

  3

  4

  5

  6

  7

  8

  9

  10

  11

  12

  13

  14

  15

  16

  17

  // 计算.h

  #ifndefCALC_H_

  #defineCALC_H_

  #ifdef_cplusplus

  外部“C”

  {

  #万一

  voidswap(int*, int*);

  intadd(int, int);

  #ifdef_cplusplus

  }

  #万一

  #endif// CALC_H_

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

  

  我们现在如何使用这个静态库?让我们写一个 test.c 并使用这个库中的交换函数。代码显示如下:

  1

  2

  3

  4

  5

  6

  7

  8

  9

  10

  11

  12

  13

  14

  15

  #包括

  #包括

  #include"calc.h"

  intmain(intargc, char*argv[])

  {

  输入 = 1, b = 2;

  交换(&a, &b);

  printf("%d %dn", a, b);

  returnEXIT_SUCCESS;

  }

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

  

  你可能会问,当我们使用C语言标准库时,编译时不需要添加任何库名。是的,我们不需要它。因为标准库已经是标准了,所以默认会被链接。但是由于数学函数库libm.a没有默认链接,我们在编译使用数学函数的代码时需要在命令行指定-lm链接(-l是链接库,m是库删除 lib 后的名称),但现在许多 gcc 默认链接 libm.c 库。比如我机器上的gcc 4.6.3 默认会链接。

  正如我们所见,静态链接库解决了一些问题,但也带来了其他问题。例如,每个使用相同C标准函数的程序都需要链接相关的目标文件,浪费磁盘空间;当一个程序有多个执行副本时,将相同的库代码部分加载到内存中,浪费内存;库代码更新后,所有使用这些库的函数都必须重新编译...

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

  动态链接库/共享库是一个对象模块,可以在运行时加载到任何内存地址并与正在运行的程序链接。这个过程就是动态链接,是由一个叫做动态链接器的程序完成的。

  Unix/Linux中共享库的后缀通常是.so(微软估计大家都很熟悉,也就是DLL文件)。如何构建动态链接库?

  我们以上面的代码为例,我们先把之前的静态库和目标文件删除。首先是建立一个动态链接库,我们执行gcc swap.c add.c -shared -o libcalc.so,就这么简单(微软的不一样,我们这里只是为了说明概念,有兴趣的同学请自行搜索)。

  顺便说一句,最好在gcc命令行中加一句-fPIC来生成位置无关代码(PIC)。具体原因不在本文讨论范围内,不再赘述。

  

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

  

  这看起来没什么不同。其实不然,我们使用ldd命令(ldd是上一篇一)推荐的GNU binutils工具包的组合)来检查测试文件的依赖。

  

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

  好处在哪里?首先,库更新后,只需要替换动态库文件即可,无需编译所有依赖库的可执行文件。其次,当执行多份程序时,内存中只需要一份库代码,节省空间。

  想一想,C语言标准库中的很多程序都在使用,但是内存中只有一个代码,节省了很多空间,如果发现库代码有bug,只需要更新libc .so,所有程序都可以使用。新代码很酷。

  好吧,我们就图书馆的事情到此为止,我们不能再结束它了。

  下面我们来看看链接过程中具体做了哪些事情。链接的步骤大致包括地址和存储分配、符号解析和重定位的主要步骤。

  首先是地址和空间分配。我们之前提到的目标文件实际上叫做可重定位的目标文件(这只是一个翻译,叫了很多……)。目标文件的格式无限接近于可执行文件。Unix/Linux下目标文件的格式称为ELF(Executable and Linkable Format)。对可执行文件格式的详细讨论超出了本文的范围。我们只需要知道可执行文件的代码、数据、符号等内容是存放在不同的段中的。这也与保护模式下的内存分段相同。有一定的关系,不过这还差得很远,我就不详细讨论了……

  地址和空间的分配和重定位将简单描述,但在这里我想扩展符号解析。

  什么是符号?简单的说,我们在代码中定义的函数和变量可以统称为符号。符号名称是函数和变量的名称。

  目标文件的合并实际上是对目标文件之间相互符号引用的修正。我们知道一个C语言代码文件只要声明了所有的符号就可以编译,但是一个符号的引用怎么知道位置呢?比如我们调用printf函数,编译时留下函数的地址要填,那么printf函数的实际地址在哪里呢?这个空缺什么时候改正?当然,链接的时候,重定位步骤就是这样做的。但是在修改地址之前,需要进行符号解析,那么什么是符号解析呢?前面说过,编译时有很多符号需要重定位,所以在目标文件中会有一个区域来保存符号表。链接器如何知道具体位置?实际上,

  这时候可以隆重介绍一个几乎每个人在编译程序时都会遇到的问题——符号搜索的问题。这通常有两种形式的错误,即找不到符号或符号重新定义。

  首先是找不到符号。比如我们声明了一个swap函数但是没有定义它,调用这个函数的代码是可以编译的,但是在链接的时候会遇到错误。看起来像“test.c:(.text+0x29): undefined reference to 'swap'”。特别是MSVC编译器报错说找不到符号_swap。咦?哪里来的下划线从何而来?这得从C语言的诞生说起,C语言刚发布的时候,已经有很多用汇编语言写的库了,因为链接器唯一的符号规则,如果库中存在main函数,我们不能在C代码中出现main函数,因为会遇到符号重定义错误,如果放弃这些库是一个很大的损失。因此,那个时候的编译器会对代码中的符号进行修饰(名称修饰),C语言代码会在符号前加一个下划线。Fortran语言在符号前后加了下划线,这样每个目标文件就不会同名,解决了符号冲突的问题。随着时间的推移,操作系统和编译器都被重写了很多次,目前的问题可以忽略不计。所以新版gcc一般不加下划线作为符号修饰(也可以在编译后的命令行加-fleading-underscore/-fno-fleading-underscore打开/关闭是否加下划线)。MSVC 仍然保留了这个传统,所以我们可以看到类似 _swap 的修改。而C语言代码会在符号前加一个下划线。Fortran语言在符号前后加了下划线,这样每个目标文件就不会同名,解决了符号冲突的问题。随着时间的推移,操作系统和编译器都被重写了很多次,目前的问题可以忽略不计。所以新版gcc一般不加下划线作为符号修饰(也可以在编译后的命令行加-fleading-underscore/-fno-fleading-underscore打开/关闭是否加下划线)。MSVC 仍然保留了这个传统,所以我们可以看到类似 _swap 的修改。而C语言代码会在符号前加一个下划线。Fortran语言在符号前后加了下划线,这样每个目标文件就不会同名,解决了符号冲突的问题。随着时间的推移,操作系统和编译器都被重写了很多次,目前的问题可以忽略不计。所以新版gcc一般不加下划线作为符号修饰(也可以在编译后的命令行加-fleading-underscore/-fno-fleading-underscore打开/关闭是否加下划线)。MSVC 仍然保留了这个传统,所以我们可以看到类似 _swap 的修改。这就解决了符号冲突的问题。随着时间的推移,操作系统和编译器都被重写了很多次,目前的问题可以忽略不计。所以新版gcc一般不加下划线作为符号修饰(也可以在编译后的命令行加-fleading-underscore/-fno-fleading-underscore打开/关闭是否加下划线)。MSVC 仍然保留了这个传统,所以我们可以看到类似 _swap 的修改。这就解决了符号冲突的问题。随着时间的推移,操作系统和编译器都被重写了很多次,目前的问题可以忽略不计。所以新版gcc一般不加下划线作为符号修饰(也可以在编译后的命令行加-fleading-underscore/-fno-fleading-underscore打开/关闭是否加下划线)。MSVC 仍然保留了这个传统,所以我们可以看到类似 _swap 的修改。所以新版gcc一般不加下划线作为符号修饰(也可以在编译后的命令行加-fleading-underscore/-fno-fleading-underscore打开/关闭是否加下划线)。MSVC 仍然保留了这个传统,所以我们可以看到类似 _swap 的修改。所以新版gcc一般不加下划线作为符号修饰(也可以在编译后的命令行加-fleading-underscore/-fno-fleading-underscore打开/关闭是否加下划线)。MSVC 仍然保留了这个传统,所以我们可以看到类似 _swap 的修改。

  顺便说一下,符号冲突很常见,尤其是在大型项目的开发中,所以我们需要一个完善的命名约定。C++ 还引入了命名空间来帮助我们解决这些问题。因为C++中存在函数重载,C++的符号修饰比较复杂,难以理解(Linux下的c++filt命令帮助我们翻译了一个被C++编译器修改的Symbol)。

  话虽如此,现在是我们成为需要关注的大问题的时候了。也就是说,当存在同名符号时链接器会做什么。你刚才不是说会报同名错误吗?你为什么要再研究这个?可惜,事情不止这么简单。编译时,编译器会将每个全局符号输出给汇编器,分为强符号和弱符号。汇编器在可重定位目标文件的符号表中隐式编码此信息。其中,函数和已初始化的全局变量为强符号,未初始化的全局变量为弱符号。根据强弱符号的定义,GNU链接器采用的规则如下:

  不允许使用多个强符号。如果有一个强符号和一个或多个弱符号,则选择强符号。如果有多个弱符号,则随机选择一个

  好吧,只有三个,第一个会报同名符号错误,后两个默认甚至不会有警告。重点在这里,即使默认情况下也没有警告。

  我们在实验中说一下:

  1

  2

  3

  4

  5

  6

  7

  8

  9

  10

  11

  // 链接1.c

  #包括

  内部结构;

  intmain(intargc, char*argv[])

  {

  printf("这是%dn", n);

  返回0;

  }

  1

  2

  // 链接2.c

  整数 = 5;

  这两个文件的输出将编译和运行什么?聪明的你应该已经知道了吧?没错,是5。

  初始化后的 n 是一个强符号,首先被选中。但是在非常复杂的项目代码中,这样的错误是很难发现的,尤其是多线程的……但是当我们怀疑代码中的bug可能是这个原因导致的时候,我们可以在gcc命令行中添加-fno -common 参数,这样当链接器遇到多个定义的符号时,无论强弱符号都会给出警告信息。如图所示:

  好了,到这里我们下一篇文章就结束了,但是编译链接其实远比这复杂的多。

0 个评论

要回复文章请先登录注册


官方客服QQ群

微信人工客服

QQ人工客服


线