预处理编译汇编链接
‘壹’ 简述将源程序编译成可执行程序的过程
一个源程序到一个可执行程序的过程:预编译、编译、汇编、链接。其中,编译是主要部分,其中又分为六个部分:词法分析、语法分析、语义分析、中间代码生成、目标代码生成和优化。
预编译:主要处理源代码文件中的以“#”开头的预编译指令。处理规则如下:
1、删除所有的#define,展开所有的宏定义。
2、处理所有的条件预编译指令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。
3、处理“#include”预编译指令,将文件内容替换到它的位置,这个过程是递归进行的,文件中包含其他文件。
4、删除所有的注释,“//”和“/**/”。
5、保留所有的#pragma 编译器指令,编译器需要用到他们,如:#pragma once 是为了防止有文件被重复引用。
6、添加行号和文件标识,便于编译时编译器产生调试用的行号信息,和编译时产生编译错误或警告是能够显示行号。
(1)预处理编译汇编链接扩展阅读:
编译过程中语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。
其中,静态语义通常包括:声明和类型的匹配,类型的转换,那么语义分析就会对这些方面进行检查,例如将一个int型赋值给int*型时,语义分析程序会发现这个类型不匹配,编译器就会报错。
主要使用gcc命令以及以下几个参数:
-E Preprocess only; do not compile, assemble or link
-S Compile only; do not assemble or link
-c Compile and assemble, but do not link
-o <file> Place the output into <file>
环境配置好以后,让我们开始c语言的编译之旅吧~
编写c语言源代码
很多linux命令都可以新建一个文件,比如
$ touch test.c
建立一个空白的文件
$ vim test.c
使用vim(文本编辑器)编辑test.c,如果test.c不存在,则创建
$ echo "123" > test.c
通过输出重定向新建一个文件
创建完test.c后,将下面这段Hello World代码写到test.c中(很多方法)
#include <stdio.h>
int main(){
printf("Hello world\n");
return 0;
}
写完之后可以使用ls命令来查看当前目录下的文件,检查test.c是否存在
$ ls
使用cat命令查看test.c中的内容,检查是否写入成功
$ cat test.c
ls-cat
展开头文件(预处理)
$ gcc -E test.c -o test_pre.c
这个命令把源代码test.c中的头文件展开,并把结果输出到test_pre.c
(可以使用cat或者vim命令查看test_pre.c文件中的内容)
per
test_pre.c中的内容是这样的,可以发现原本几行的代码变成了几百行,而且已经见不到include关键字了,取而代之的是一些变量定义的代码,这些代码就是stdio.h中的内容,和stdio.h中头文件展开后的内容。
编译
$ gcc -S test_pre.c -o test_asm.s
这一条命令将上一步预处理过后的源代码编译成为汇编代码
asm
现在看到的是test_asm.s里面的汇编代码。
什么是汇编?
汇编语言是汇编指令集、伪指令集和使用它们规则的统称,使用具有一定含义的符号为助忆符,用指令助忆符、符号地址等组成的符号指令称为汇编格式指令。
简单的可以理解为汇编语言是一本词典,01100101011010这样的二进制字符串是单词,汇编指令是单词的含义。计算机能读懂二进制字符串,而人能读懂的是翻译过来的汇编指令。
汇编
$ gcc -c test_asm.s -o test_obj.o
这一步将test_asm.s汇编成为目标文件,目标文件中存储的就是010101010这样的字符串了,可以用cat命令试试去读取test_obj.o
obj
可以发现打印出来许多不可见的字符,原因是目标文件已经是二进制格式的了,不同于源代码(文本格式)
有关文件的格式可以看下这里的介绍:
http://www.cnblogs.com/zhangjiankun/archive/2011/11/27/2265184.html
链接
链接器负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。附加的目标文件包括静态连接库和动态连接库。
这个例子中没有附加的目标文件,所以只需要目标文件做被链接的对象。
有关链接器的详细讲解大家可以看下这里:
https://www.hu.com/question/27386057
$ gcc test_obj.o -o hello
gcc本身可以充当链接器,这里使用gcc命令将目标文件test_obj.o链接成了可执行文件hello
ld
运行程序!
至此,源代码已经经历了预处理、编译、汇编、链接四步成为了可执行文件,现在试着运行一下这个程序吧
$ ./hello
hello
小结
首先我们创建了源文件test.c,然后用gcc -E将源文件中的头文件展开,这一步叫做预处理;
之后通过gcc -S将预处理后的源文件编译了汇编代码,这一步叫做编译;
接着使用gcc -c命令将汇编代码转换成了二进制的目标文件,这一步操作叫做汇编;
目标文件不同于源代码,是二进制格式,是源文件编译过程中产生的中间文件,通过链接器可以将多个目标文件链接成为可执行文件,这一步叫做链接。
源文件->(预处理->编译->汇编->链接)->可执行文件
一般大家所说的c语言编译,其实是上述这四步的简称。
‘叁’ C语言编译原理是什么
编译共分为四个阶段:预处理阶段、编译阶段、汇编阶段、链接阶段。
1、预处理阶段:
主要工作是将头文件插入到所写的代码中,生成扩展名为“.i”的文件替换原来的扩展名为“.c”的文件,但是原来的文件仍然保留,只是执行过程中的实际文件发生了改变。(这里所说的替换并不是指原来的文件被删除)
2、汇编阶段:
插入汇编语言程序,将代码翻译成汇编语言。编译器首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,编译器把代码翻译成汇编语言,同时将扩展名为“.i”的文件翻译成扩展名为“.s”的文件。
3、编译阶段:
将汇编语言翻译成机器语言指令,并将指令打包封存成可重定位目标程序的格式,将扩展名为“.s”的文件翻译成扩展名为“.o”的二进制文件。
4、链接阶段:
在示例代码中,改代码文件调用了标准库中printf函数。而printf函数的实际存储位置是一个单独编译的目标文件(编译的结果也是扩展名为“.o”的文件),所以此时主函数调用的时候,需要将该文件(即printf函数所在的编译文件)与hello world文件整合到一起,此时链接器就可以大显神通了,将两个文件合并后生成一个可执行目标文件。
‘肆’ 从预处理、编译、汇编到链接,编译系统都作了哪些工作使用哪些工具生成了哪些文件
这个问题可烦可简,可深可浅。
对于编译执行语言而言:
我所知的笼统过程有
(1)源代码==》目标代码==》可执行程序
(资源==》目标代码)
(2)源代码==》中间代码==》目标代码==》可执行程序
第(1)种一般的为低级汇编采用的模式,第一个主要步骤统称为Assembly(汇编),由“汇编程序”(或称汇编编译器)完成,其包含预处理操作,生成的主要文件是目标文件,当然在生成目的文件前还有许多辅助文件,一般会被“汇编程序”临时生成,用完即删除,不指定控制选项的话最终用户是看不到这些文件的,有哪些中间临时文件,用处是什么可以查看“汇编编译器”的帮助选项得到。第二个主要步骤就是link(链接),其将目标代码文件,链接库里的目标代码块整合为可执行代码,中间也临时生成一些中间文件,如映射文件等,同样可通过链接器的选项查看。
当然,在一些高级汇编里还会有资源编译器,其将各种资源转为(编译为)目标文件(作为链接器的输入)
第(2)种一般是高级语言采用的模式,但有些比较高级的直接跳过中间代码由源代码生成目标代码,其就跟(1)类似,只是此时第一个主要步骤不叫“汇编”而称compile(编译),低级汇编的步骤一“汇编”也可称”编译“。如果有中间代码生成,这中间代码就是汇编代码,此后续处理就同(1)了,此时的中间代码其实也就是临时文件中的一种。
概述:源代码到目标代码的过程通常称为编译,而目标代码到可执行程序的过程称问链接。
或将两个过程统称为代码的编译(全称应为编译连接),这涉及具体的语境,事实上编译器如VC的cl.exe若没有指定/c(只生产目标代码选项),其就是编译连接的统一过程(cl会调用相应的链接器),若指定,则只有编译过程(只生成目标代码而不链接称可执行程序)
上述编译执行类语言开发平台所开发生成的程序一般称为”非托管类程序“
而对于托管类程序(如.NET平台语言C#,VB.NET,JVM平台的java等)
其虽然也有编译过程,但其直接将源代码转为中间代码而不是目标代码(此时不是汇编代码更不是机器码,而是可被.NET或JVM引擎解释执行的代码)
可参看编译原理等相关教材,阿门。。。
‘伍’ c语言可执行程序文件是通过()和()生成的
源程序文件不是可执行文件。 C源程序文件是一种文本文件,它首先需要编译器去编译成目标文件,在通过链接器链接库代码才能形成可执行的二进制exe文件。每一个C语言程序必须要经过编译和链接才能被计算机执行,编译是将C源码翻译成机器码,链接是将将二进制目标文件装配成一个具有特定格式的二进制可执行文件,比如Windows平台上是PE格式,一般以.exe为扩展名。 一个C语言程序从源码到计算机系统可以执行,更细致的划分为:预处理——编译——汇编——链接。预处理是对C语言源码进行文本处理,编译阶断是将C源码经C编译器生成汇编代码,汇编阶断是将汇编代码经汇编器生成二进制机器码文件。这两个合拢起来,笼统的可以叫做编译阶断。语言是一门计算机语言,有自己一定的语法。但是,C语言并不能直接被对象所理解,需要将C语言转变成可执行代码,即二进制代码。在C语言转变成二进制可执行代码时,是以工程为单位的。而一个工程中往往会包含多个C文件。因此,需要将每个C文件都编译成二进制代码。此时,每个C文件所对应的二进制代码是独立的。由于工程是一个系统,所以需要将所有的C文件二进制代码链接到一起,形成一个工程的可执行文件。 综上,编译和链接就是指的将C文件转变成二进制代码,并将各个独立的C文件二进制代码链接到一起,形成一个可执行文件的过程。
‘陆’ c语言编译器如何运行
编译共分为四个阶段:预处理阶段、编译阶段、汇编阶段、链接阶段。
1、预处理阶段:
主要工作是将头文件插入到所写的代码中,生成扩展名为“.i”的文件替换原来的扩展名为“.c”的文件,但是原来的文件仍然保留,只是执行过程中的实际文件发生了改变。(这里所说的替换并不是指原来的文件被删除)
2、汇编阶段:
插入汇编语言程序,将代码翻译成汇编语言。编译器首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,编译器把代码翻译成汇编语言,同时将扩展名为“.i”的文件翻译成扩展名为“.s”的文件。
3、编译阶段:
将汇编语言翻译成机器语言指令,并将指令打包封存成可重定位目标程序的格式,将扩展名为“.s”的文件翻译成扩展名为“.o”的二进制文件。
4、链接阶段:
在示例代码中,改代码文件调用了标准库中printf函数。而printf函数的实际存储位置是一个单独编译的目标文件(编译的结果也是扩展名为“.o”的文件),所以此时主函数调用的时候,需要将该文件(即printf函数所在的编译文件)与hello world文件整合到一起,此时链接器就可以大显神通了,将两个文件合并后生成一个可执行目标文件。
‘柒’ gcc编译流程
gcc编译分为四部;
第一步,预编译,将程序中的宏定义等预编译;
第二步,编译,将*.h,*.c等文件编译成为*.o文件;
第三步,汇编;
第四步,连接,将*.o文件连接库,生成可执行文件!
‘捌’ C文件如何成为可执行文件(编译、链接、执行)——摘自《程序员的自我修养》
本文算是我阅读《程序员的自我修养》(俞甲子等着)相关章节的笔记,文中直接引用了原书中的叙述,强烈建议大家去看原书,本文只做概要介绍而用。——注:文中有很多引用图的地方,请大家自己去找原书看,支持正版!我遇到一个问题,Linux C编程中的问题:.. char *p; unsigned int i = 0xcccccccc; unsigned int j; p = (char *) &i; printf("%.2x %.2x %.2x %.2x\n", *p, p[1], p[2], p[3]); memcpy(&j, p, sizeof(unsigned int)); printf("%x\n", j); ... Output: ffffffcc ffffffcc ffffffcc ffffffcc 0xcccccccc My questions are: 1. Why it prints "ffffffcc ffffffcc ffffffcc ffffffcc"? (if p is unsigned char* then it will print correctly "cc cc cc cc") 2. Why pointer to char p copied to j correctly, why not every member in p overflow? since it is a signed char. 这是别人在邮件列表中提出的问题,在试图回答这个问题的过程中,突然发现,自己对连接器的工作并不熟悉,因此拿来好书《程序员的自我修养》来看,并做如下汇报,强烈推荐《程序员的自我修养》!!!写好的C语言文件,最终能够执行,大致要经过预处理、编译、汇编、链接、装载五个过程。预编译完成的工作: (1)将所有的"#define"删除,并展开所有的宏定义 (2)处理所有条件预编译指令 (3)处理#include预编译指令,将被包含的文件插入到预编译指令的位置,这个过程是递归进行的。 (4)删除所有的注释 (5)添加行号和文件名标识,以便调试 (6)保留所有的#pragma编译器命令,因为编译器需要使用它们。编译完成的工作: (1)词法分析 扫描源代码序列,并将其分割为一系列的记号(Token)。 (2)语法分析 用语法分析器生成语法树,确定运算符号的优先级和含义、报告语法错误。 (3)语义分析 静态语义分析包括生命和类型的匹配,类型的转换;动态语义分析一般是在运行期出现的与语义相关性的问题,如除0错。 (4)源代码生成 源代码级优化器在源代码级别进行优化:如将如(6+2)之类的表达式,直接优化为(8)等等。将语法书转换为中间代码,如三地址码、P-代码等。 (5)代码生成 将源代码转换为目标代码,依赖于目标机器。 (6)目标代码优化汇编完成的工作: 将汇编代码变成机器可以执行的指令链接完成的工作: 链接完成的工作主要是将各个模块之间相互引用的部分处理好,使得各个模块之间正确衔接。链接过程包括:地址和空间分配、符号决议和重定位。 首先讲静态链接,基本的静态链接如下: 我们可能在main函数中调用到定义在另一个文件中的函数foo(),但是由于每个模块式单独编译的,因此main并不知道foo的地址,所以它暂时把这些调用foo的指令的目标地址搁置,等到最后链接的时候让连接器去修正这些地址(重定位),这就是静态链接最基本的过程和作用;对于定义在其他文件中的变量,也存在相同的问题。具体过程如下: (1)空间和地址分配 1)空间与地址分配:扫描所有输入目标文件,获得各个段的属性、长度和位置,并且将目标文件中的符号表中所有的符号定义和符号引用收集起来,放到一个全局符号表中。 2)符号解析和重定位:使用第一步收集到的信息,读取输入文件中段的数据、重定位信息,并进行符号解析与重定位、调整代码中的地址等。 动态链接的过程更为复杂,但是完成的工作类似。 动态链接的初衷是为了解决空间浪费和更新困难的问题,把链接过程推迟到运行时进行 首先介绍一个重要的概念——地址无关代码。为了解决固定装载地址冲突的问题,我们希望对所有绝对地址的引用不作重定位,而把这一步推迟到装载的时候再完成,一旦模块装载地址确定,即目标地址确定,那么系统对程序中所有的绝对地址引用进行重定位。同时我们希望,模块中共享的指令部分在装载时不需要因为装载地址的改变而改变,所以把指令中那些需要被修改的部分分离出来,跟数据放在一起,这样指令部分就可以保持不变,而数据部分可以在每个进程中拥有一个副本,这种方案目前被称为地址无关代码(PIC,Position-independent Code)。 我们需要解决如下四种引用中的重定位问题: 1)模块内部调用或者跳转:这个可以用相对地址调用或者基于寄存器的相对调用,所以不需要重定位2)模块内部数据的访问:用相对寻址的方法,不过链接器实现得十分巧妙: call494 add$0x188c, %ecx mov$0x1, 0x28(%ecx) //a=1 调用一个叫做__i686.get_pc_thunk.cx的函数,把call的下一条指令的地址放到ecx寄存器中,接着执行一条mov指令和一个add指令3)模块间数据的访问:在数据段里建立一个指向全局变量的指针数组,也成全局便宜表(GOT),当要引用全局变量时,可以通过GOT相对应的项间接引用: GOT是做到指令无关的重要的一环:在编译时可以确定GOT相对于当前指令的偏移,根据变量地址在GOT中的偏移就可以得到变量的地址,当然GOT中哪个每个地址对应于哪个变量是由编译器决定的。4)模块间的调用、跳转:采用上面类似的方法,不同的是GOT中相应的项存储的是目标函数的地址,当模块需要调用目标函数时,可以通过GOT中的项进行间接跳转。 地址无关代码小结: 现在,来看动态链接中的另一个重要问题——延迟绑定(PLT)。当函数第一次被用到时才进行绑定,否则不绑定。PLT为了实现延迟绑定,增加了一层间接跳转。调用函数并不是通过GOT跳转的,而是通过一个叫PLT项的结构进行跳转的,每个外部函数在PLT中都有对应的项,如函数bar,其在PLT对应的项的地址记为bar@plt,实现方式如下: bar@plt: jmp* (bar@GOT) pushn pushmoleID jump_dl_runtime_resolve 链接器的这个实现至为巧妙: 如果在连接器初始化阶段,已经正确的初始化了bar@GOT,那么这个跳转指令的结果正是我们所期望的,但是,为了实现PLT,一般在连接器初始化时,将"pushn"的地址放入到bar@GOT中,这样就直接跳转到第二条指令,相当于没有进行任何操作。第二条指令“pushn”,n是bar这个符号引用在重定位表“.rel.plt”中的下标。接着将模块的ID压栈,跳转到_dl_runtime_resolve完成符号解析和重定位工作,然后将bar的地址填入到bar@GOT中。下次再调用到bar时,则bar@GOT中存储的是一个正确的地址,这样就完成了整个过程。 在链接完成之后,就生成了你要的可执行文件了,如ELF文件,至于这个文件的详细的信息,可以参考相关的文档。 现在,你要运行你的可执行文件,这是如何做到的呢? 我们从操作系统的角度来看可执行文件的装载过程。操作系统主要做如下三件事情:(1)创建一个独立的虚拟地址空间,但由于采用了COW机制,这里只是复制了父进程的页目录和页表,甚至不设置映射关系(参考操作系统相关书籍)。(2)读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系。(3)将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。我们来看一下执行过程中,进程虚拟空间的分布。 首先我们来区分Section和Segment,都可以翻译为“段”,那么有什么不同呢?从链接的角度来讲,elf文件是按照Section存储的,从装载的角度讲,elf文件是按照Segment存储的。”Segment”实际上是从装载的角度重新划分了ELF的各个段,将其中属性相似的Section合并为一个Segment,而系统是按照Segment来映射可执行文件的。
‘玖’ C语言 四个过程:预处理,编译,汇编,链接,分别进行了什么过程别度娘。
预处理:替换代码中的预处理命令(宏定义就是在这里直接替换的)
编译:对代码按执行顺序进行编译成.o或.obj目标文件
汇编:将其他高级语言转换成机器语言
链接:代码中的各种调用关系重定位
‘拾’ c语言:exe(可执行文件)是如何被执行的
.exe--是可在操作系统存储空间中浮动定位的可执行程序
.c文件生成.exe文件的过程,经历了预处理,编译,汇编,链接,这四个过程
1.预处理--主要处理源代码中的预处理指令,引入头文件,去除注释,处理所有的条件编译指令,宏的替换,添加行号,保留所有的编译器指令。(生成.i文件)
2.编译--进行的是对预处理后的文件进行语法分析,词法分析,语义分析,符号汇总,然后生成汇编代码。(生成.s文件)
3.汇编--将汇编代码转成二进制文件,二进制文件就可以让机器来读取。(生成一个重定位目标文件,linux下是.o文件,windows下是.obj文件)
4.链接--合并段表,然后把符号表合并并且对符号表进行重定位。