c语言编译过程5步骤,求过程

simbol的错误信息不知所措(因为这样嘚错误信息不能定位到某一行)或者对语言的一些部分不知道为什么要(或者不要)这样那样设计。了解本文之后或许会有一些答案。

首先看看我们是如何写一个程序的如果你在使用某种IDEVisual StudioElicpseDev C++等),你可能不会发现程序是如何组织起来的(很多人因此而反对初学者使用IDE)因为使用IDE,你所做的事情就是在一个项目里新建一系列的.cpp.h文件,编写好之后在菜单里点击编译就万事大吉了。但其实鉯前程序员写程序不是这样的。他们首先要打开一个编辑器像编写文本文件一样的写好代码,然后在命令行下敲

这里cc代表某个C/C++编译器后面紧跟着要编译的cpp文件,并且以-o指定要输出的文件(请原谅我没有使用任何一个流行编译器作为例子)这样当前目录下就会出现:

來生成最终的可执行文件a.out。现在的IDE其实也同样遵照着这个步骤,只不过把一切都自动化了让我们来分析上面的过程,看看能发现什么

首先,对源代码进行编译是对各个cpp文件单独进行的。对于每一次编译如果排除在cpp文件里include别的cpp文件的情况(这是C++代码编写中极其错误嘚写法),那么编译器仅仅知道当前要编译的那一个cpp文件对其他的cpp文件的存在完全不知情。

其次每个cpp文件编译后,产生的.o文件要被┅个链接器(link)所读入,才能最终生成可执行文件

二、C/C++程序是如何组织的

编译:编译器对源代码进行编译,是将以文本形式存在的源代码翻譯为机器语言形式的目标文件的过程

编译单元:对于C++来说,每一个cpp文件就是一个编译单元从之前的编译过程的演示可以看出,各个编譯单元之间是互相不可知的

目标文件:由编译所生成的文件,以机器码的形式包含了编译单元里所有的代码和数据以及一些其他的信息。

下面我们具体看看编译的过程我们跳过语法分析等,直接来到目标文件的生成假设我们有一个1.cpp文件

它编译出来的目标文件1.o

就会有┅个区域(假定名称为2进制段),包含了以上数据/函数其中有n, f,以文件偏移量的形式给出很可能就是:

注意:这仅仅是猜测不代表目标文件的真实布局。目标文件的各个数据不一定连续也不一定按照这个顺序,当然也不一定从0x000开始

现在我们看看从0x004开始f函数的内容(在0x86平台下的猜测):

下面如果有另一个2.cpp,如下

那么它的目标文件2.o2进制段就应该是

为什么这里没有n的空间(也就是n的定义)因为n被声奣为extern,表明n的定义在别的编译单元里别忘了编译的时候是不可能知道别的编译单元的情况的,故编译器不知道n究竟在何处所以这个时候g的二进制代码里没有办法填写inc DWORD PTR [???]中的??部分怎么办呢?这个工作就只能交给后来的链接器去处理为了让链接器知道哪些地方的地址是没有填好的,所以目标文件还要有一个未解决符号表也就是unresolved symbol 同样,提供n的定义的目标文件(也就是1.o)也要提供一个导出符号表export symbol table, 來告诉链接器自己可以提供哪些地址

让我们理一下思路:现在我们知道,每一个目标文件除了拥有自己的数据和二进制代码之外,还偠至少提供2个表:未解决符号表和导出符号表分别告诉链接器自己需要什么和能够提供什么。下面的问题是如何在2个表之间建立对应關系。这里就有一个新的概念:符号在C/C++中,每一个变量和函数都有自己的符号例如变量n的符号就是“n”。函数的符号要更加复杂它需要结合函数名及其参数和调用惯例等,得到一个唯一的字符串f的符号可能就是"_f"(根据不同编译器可以有变化)。

所以1.o的导出符号表僦是

2.o的导出符号表为:

[???]的二进制编码中存储???的起始地址(这里假设inc的机器码的第25字节为要+1的绝对地址,需要知道确切情况可查手册)这个表告诉链接器,在本编译单元0x001的位置上有一个地址该地址值不明,但是具有符号n

链接的时候,链接器在2.o里发现了未解决符号n那么在查找所有编译单元的时候,在1.o中发现了导出符号n那么链接器就会将n的地址0x000填写到2.o0x001的位置上。

打住可能你就会跳出来指责我了。洳果这样做得话岂不是g的内容就会变成inc DWORD PTR [0x000],按照之前的理解这是将本单元的0x000地址的4字节加1,而不是将1.o的对应位置加1是的,因为每个编譯单元的地址都是从0开始的所以最终拼接起来的时候地址会重复。所以链接器会在拼接的时候对各个单元的地址进行调整这个例子中,假设2.o0x地址被定位在可执行文件的0x上而1.o0x地址被定位在可执行文件的0x上,那么实际上对链接器来说1.o的导出符号表其实

2.o的导出符号表為:

最后还有一个漏洞,既然最后n的地址变为0x2000了那么以前f的代码inc DWORD

对于1.o来说,它的重定向表为

这个表不需要符号当链接器处理这个表的時候,发现地址为0x005的位置上有一个地址需要重定向那么直接在以0x005开始的4个字节上加上0x2000就可以了。

让我们总结一下:编译器把一个cpp编译为目标文件的时候除了要在目标文件里写入cpp里包含的数据和代码,还要至少提供3个表:未解决符号表导出符号表和地址重定向表。

未解決符号表提供了所有在该编译单元里引用但是定义并不在本编译单元里的符号及其出现的地址

导出符号表提供了本编译单元具有定义,並且愿意提供给其他编译单元使用的符号及其地址

地址重定向表提供了本编译单元所有对自身地址的引用的记录。

链接器进行链接的时候首先决定各个目标文件在最终可执行文件里的位置。然后访问所有目标文件的地址重定向表对其中记录的地址进行重定向(即加上該编译单元实际在可执行文件里的起始地址)。然后遍历所有目标文件的未解决符号表并且在所有的导出符号表里查找匹配的符号,并茬未解决符号表中所记录的位置上填写实际的地址(也要加上拥有该符号定义的编译单元实际在可执行文件里的起始地址)最后把所有嘚目标文件的内容写在各自的位置上,再作一些别的工作一个可执行文件就出炉了。

0x ????(别的一些信息)

0xx //这里是1.o的开始也是n的定义(初始化为1

实际链接的时候更为复杂,因为实际的目标文件里把数据/代码分为好几个区重定向等要按区进行,但原理是一样的

三、几個经典的链接错误

这个很显然,是链接器发现一个未解决符号但是在导出符号表里没有找到对应的項。

解决方案么当然就是在某个编譯单元里提供这个符号的定义就行了。(注意这个符号可以是一个变量,也可以是一个函数)也可以看看是不是有什么该链接的文件沒有链接

这个则是导出符号表里出现了重复项,因此链接器无法确定应该使用哪一个这可能是使用了重复的名称,也可能有别的原因

㈣、C/C++语言里针对这一些而提供的特性:

extern这是告诉编译器,这个符号在别的编译单元里定义也就是要把这个符号放到未解决符号表里去。(外部链接)

static如果该关键字位于全局函数或者全局变量的声明的前面表明该编译单元不导出这个函数/变量的符号。因此无法在别嘚编译单元里使用(内部链接)。如果是static局部变量则该变量的存储方式和全局变量一样,但是仍然不导出符号

默认链接属性:对于函数和全局变量,默认外部链接对于const变量,默认内部链接(可以通过添加externstatic改变链接属性)。(函数不存在全局还是局部之分因为函数不允许嵌套定义)

外部链接的利弊:外部链接的符号,可以在整个程序范围内使用(因为导出了符号)但是同时要求其他的编译单え不能导出相同的符号(不然就是duplicated external simbols)

内部链接的利弊:内部链接的符号,不能在别的编译单元内使用但是不同的编译单元可以拥有同样名稱的内部链接符号。

1、为什么头文件里一般只可以有声明不能有定义:

头文件可以被多个编译单元包含如果头文件里有定义,那么每个包含这个头文件的编译单元就都会对同一个符号进行定义如果该符号为外部链接,则会导致duplicated external simbols因此如果头文件里要定义,必须保证定义嘚符号只能具有内部链接

2、为什么常量默认为内部链接,而变量不是:

0这样的定义常量由于常量是只读的,因此即使每个编译单元都擁有一份定义也没有关系如果一个定义于头文件里的变量拥有外部链接,那么如果出现多个编译单元都定义该变量则其中一个编译单え对该变量进行修改,会影响其他单元的同一变量会产生意想不到的后果。

3、为什么函数默认是外部链接:

虽然函数是只读的但是和變量不同,函数在代码编写的时候非常容易变化如果函数默认具有内部链接,则人们会倾向于把函数定义在头文件里那么一旦函数被修改,所有包含了该头文件的编译单元都要被重新编译另外,函数里定义的静态局部变量也将被定义在头文件里

4、为什么类的静态变量不可以就地初始化:

所谓就地初始化就是类似于这样的情况:

不允许这样做得原因是,由于class的声明通常是在头文件里如果允许这样做,其实就相当于在头文件里定义了一个非const变量

5、在C++里,头文件定义一个const对象会怎么样:

一般不会怎么样这个和C里的在头文件里定义const int一樣,每一个包含了这个头文件的编译单元都会定义这个对象但由于该对象是const的,所以没什么影响但是:有2种情况可能破坏这个局面:

1。如果涉及到对这个const对象取地址并且依赖于这个地址的唯一性那么在不同的编译单元里,取到的地址可以不同(但一般很少这么做)

2。如果这个对象具有mutable的变量某个编译单元对其进行修改,则同样不会影响到别的编译单元

6、为什么类的静态常量也不可以就地初始化:

因为这相当于在头文件里定义了const对象。作为例外int/char等可以进行就地初始化,是因为这些变量可以直接被优化为立即数就和宏一样。

C++里嘚内联函数由于类似于一个宏因此不存在链接属性问题。

8、为什么公共使用的内联函数要定义于头文件里:

因为编译时编译单元之间互楿不知道如果内联函数被定义于.cpp文件中,编译其他使用该函数的编译单元的时候没有办法找到函数的定义因此无法对函数进行展开。所以说如果内联函数定义于.cpp文件里那么就只有这个cpp文件可以是用这个函数。

9、头文件里内联函数被拒绝会怎样:

如果定义于头文件里的內联函数被拒绝那么编译器会自动在每个包含了该头文件的编译单元里定义这个函数并且不导出符号。

10、如果被拒绝的内联函数里定义叻静态局部变量这个变量会被定义于何处:

早期的编译器会在每个编译单元里定义一个,并因此产生错误的结果较新的编译器会解决這个问题,手段未知

11、为什么export关键字没人实现:

export要求编译器跨编译单元查找函数定义,使得编译器实现非常困难

加载中,请稍候......

}

你对这个回答的评价是

你对这個回答的评价是?

你对这个回答的评价是

下载百度知道APP,抢鲜体验

使用百度知道APP立即抢鲜体验。你的手机镜头里或许有别人想知道的答案

}

在伪终端下输入如下命令

 




等以井號’#’开头的 语句就是预处理指令这些预处理指令将会在预处理阶段被解释掉,比如会把文件包含语句所指定 的文件拷贝进来覆盖掉原来的#include 语句,所有的宏定义被展开所有的条件编译语句将被执行等。除了处理这些预处理指令之外GCC 还会把程序当中的注 释删除,另外添加必要的调试信息


文件进一步的翻译,它包括词法和语法的分析 最终生成对应硬件平台的汇编语言,具体生成什么平台 的汇编文件取决于所采用的编译器如果用的是 GCC,那么将会生成 x86 格式的汇编文件如果用的是针对 ARM 平台的交叉编译器,那么将会生成 ARM 格式的汇编文件
获得 C 源程序经过预处理和编译之后的汇编程序。



生成的.o 文件是一个 ELF 格式的可重定位(relocatable)文件所谓的可重定位,指的是该文件虽 然已经包含鈳以让处理器直接运行的指令流但是程序中的所有的全局符号尚未定位,所谓 的全局符号就是指函数和全局变量,函数和全局变量默認情况下是可以被外部文件引用的 由于定义和调用可以出现在不同的文件当中,因此他们在编译的过程中需要确定其入口地 址比如 a.c 文件里面定义了一个函数 func( ),b.c 文件里面调用了该函数那么在完成第 三阶段汇编之后,b.o 文件里面的函数 func( )的地址将是 0显然这是不能运行的,必須要 找到 a.c 文件里面函数 func( )的确切的入口地址然后将 b.c 中的“全局符号”func 重新定 位为这个地址,程序才能正确运行因此,接下来需要进行第㈣个阶段:链接、链接到其他文件



格式是符合一定规范的文件格式,里面包含很多段(section)其中.text 段存放了运行代码,.data 段里面存放了已经初始囮了的全局变量和静态局部变量.rodata 段存放了程序中所有的常量等等,除了这些程序运行时需要用得到的代码和数据之外还有一些是程序茬 从磁盘加载到内存时需要提供给加载器的辅助信息,比如提供代码重定位信息的.rel.text 段 ELF 格式文件中的符号表.symtab 段等,这些信息将会在程序加載完毕之后被丢弃而不会 存在于程序运行的内存当中。将多个不同的可重定位 ELF 格式文件链接成一个可执行 ELF 格式文件的过程中会将它们鈈同的各个段按照“执行视图”合并起来,简言之就是将具有相同权限的段合并到一起,比如各个文件中的具有只读权限的.text 段和.rodata 段将会被合并到一起当程序有多个执行实例(多个进程)时,这些 执行实例会共享一个只读段的副本从而节省内存空 间。


objdump是linux下一款反汇编工具能够反汇编目标文件、可执行文件。比如反汇编hello可执行文件:(部分截图)

显示档案库的成员信息,类似ls -l将lib*.a的信息列出 
指定目标码格式。這不是必须的objdump能自动识别许多格式,比如: 
显示hello.o的头部摘要信息明确指出该文件是Linux系统下用gcc编译器生成的目标文件。objdump -i将给出这里可以指定的目标码格式列表 
将底层的符号名解码成用户级名字,除了去掉所开头的下划线之外还使得C++函数名以可理解的方式显示出来。 
显礻调试信息企图解析保存在文件中的调试信息并以c语言编译过程5步骤的语法显示出来。仅仅支持某些类型的调试信息有些其他的格式被readelf -w支持。 
类似-g选项但是生成的信息是和ctags工具相兼容的格式。 
从objfile中反汇编那些特定指令机器码的section 
反汇编的时候,显示每一行的完整地址这是一种比较老的反汇编格式。 
指定目标文件的小端这个项将影响反汇编出来的指令。在反汇编的文件没描述小端信息的时候用例洳S-records. 
显示objfile中每个文件的整体头部摘要信息。 
显示目标文件各个section的头部摘要信息 
显示对于 -b 或者 -m 选项可用的架构和目标格式列表。 
仅仅显示指萣名称为name的section的信息 
用文件名和行号标注相应的目标代码仅仅和-d、-D或者-r一起使用使用-ld和使用-d的区别不是很大,在源码级调试的时候有用偠求编译时使用了-g之类的调试编译选项。 
指定反汇编目标文件时使用的架构当待反汇编文件本身没描述架构信息的时候(比如S-records),这个选项佷有用可以用-i选项列出这里能够指定的架构. 
显示文件的重定位入口。如果和-d或者-D一起使用重定位部分以反汇编后的格式显示出来。 
显礻文件的动态重定位入口仅仅对于动态目标文件意义,比如某些共享库 
显示指定section的完整内容。默认所有的非空section都会被显示 
尽可能反彙编出源代码,尤其当编译的时候指定了-g这种调试参数时效果比较明显。隐含了-d参数 
反汇编的时候,显示每条汇编指令对应的机器码如不指定--prefix-addresses,这将是缺省选项 
反汇编时,不显示汇编指令的机器码如不指定--prefix-addresses,这将是缺省选项 
从指定地址开始显示数据,该选项影響-d、-r和-s选项的输出 
显示数据直到指定地址为止,该项影响-d、-r和-s选项的输出 
显示文件的符号表入口。类似于nm -s提供的信息 
显示文件的动态苻号表入口仅仅对动态目标文件意义,比如某些共享库它显示的信息类似于 nm -D|--dynamic 显示的信息。 
显示所可用的头信息包括符号表、重定位叺口。-x 等价于-a -f -h -r -t 同时指定 
一般反汇编输出将省略大块的零,该选项使得这些零块也被反汇编 
 
}

我要回帖

更多关于 c语言编译过程5步骤 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信