1.程序的翻译环境和执行环境
在ANSI C的任何一种实现(编译器)中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。
test.c要经过翻译环境变成可执行程序。可执行程序(test.exe)依赖执行环境实现效果。
二进制指令只有机器可以读懂,所以也叫机器指令。
翻译环境就是将C语言的源代码翻译成机器能读懂的二进制指令
翻译环境可以再分为两个过程:编译和链接
C语言源代码经过编译会生成一个目标文件,目标文件经过链接可以得到二进制文件
C语言代码在被编译后按ctrl+f7,在工程名路径底下(存在test.c的)的debug中有一个test.obj文件,在windows底下,以obj为后缀名的,就是目标文件。
一个工程中还可能存在多个如test.c一样的.c源文件,每一个.c文件都会单独经过编译生成一个.obj文件。
如存在test.c,add.c,sub.c源文件(写好的源代码),每个源文件都会单独进行编译器处理生成目标文件test.obj,add.obj,sub.obj
所有目标文件和链接库整体经过链接处理生成test.exe。
编译过程依赖编译器,链接过程依赖链接器。
程序的编译分为4个阶段:预处理,编译,汇编,链接
预处理是进行宏替换,去注释等操作,编译是先进行语法检错,然后把C语言代码编译成汇编代码(即目标文件),汇编就是把汇编代码编译成二进制机器指令。链接是将所有用到的代码打包,生成可执行程序
链接库就是把所用到的库函数从库中拿出来,头文件的作用是在编译时告诉编译器,这个函数是有的,格式如何,编译完成后进行链接时会从库中拿出来,头文件的作用是声明。
printf函数是C语言提供的,printf放在Libraries中,以.LIB为后缀的就是库。
静态库是指在我们的应用中,有一些公共代码是需要反复使用,就把这些代码编译为"库"文件;在链接步骤中,链接器将从库文件取得所需的代码,复制到生成的可执行文件中的这种库。
软件中源代码编译成静态库来完成各种功能。
下面将使用Linux中的gcc编译器演示程序编译和链接的过程。
先了解Linux的基本操作指令:is——列出当前目录中的东西,图二是有东西的情况
vim——编辑器,相当于windows下的记事本,vim test.c相当于创建一个test.c文件(如果test.c已经存在,则是对test.c的修改),回车,进行代码的书写。写完后保存(保存方式不知道,但不妨碍理解编译)
gcc——Linux中的编译器
每个源文件都要经过编译器处理生成目标文件
完成基本的书写后,先进行预编译(也叫预处理)操作
使用预编译功能——因为Add已经声明,所以在预编译和编译阶段都不会报错
gcc -E test.c回车 //-E可以在test.c前,可以在test.c后,对test.c进行预编译操作
得到的结果是预处理产生的结果打印到屏幕上,如果不想打印到屏幕上可以使用如下命令:
gcc -E test.c -o test.i回车 //o是输出,将预处理的结果输出到test.i上
回车后会发现没有输出到屏幕上,使用is指令可以看到:
查看test.i:
发现一个十几行的代码经过预处理后变成一个800+行的代码
仔细看test.i的内容,发现最后十几行代码就是test.c中的内容,只有一个#include不见了,所以多出来的代码是头文件中的内容
可以通过指令:cd /usr/include/回车
查找头文件,其中/usr是路径,然后用vim这个工具打开头文件
然后发现有相同的部分(行数不一样是注释被消除的原因)
所以预编译会使头文件的内容包含进来;以及删除注释。(有时是被空格替换)
对test.c进行修改增加两行代码:
#define max 100
int m = max;
然后预编译,结果放到test.i中,再查看test.i
可以看到,max的部分被替换成100了
由此可知,#define定义符号的替换也是预处理的功能之一。替换后#define及其定义的符号会被删掉
所以预编译阶段进行的事就是
1.头文件的包含
2.删除注释。(有时是被空格替换)
3.#define定义符号的替换
都是文本操作。
预编译阶段完成后对test.i进行-S操作(-S可以放在test.i前,也可以放在test.i后面),-S就是编译操作指令,这个过程生成了test.s文件
vim test.s回车,查看test.s中的内容
这里面都是汇编代码
这里可以笼统的概括编程步骤就是将C语言代码转变为汇编代码
其中包含细节操作有:1.语法分析2.词法分析3.语义分析4.符号汇总——是《编译原理》的内容——相当于介绍编译器怎么造
《编译原理》过于晦涩难懂,如果想要了解这方面的知识,推荐《程序员的自我修养》。
编译完成后进行汇编操作指令:
gcc -c test.s
生成test.o文件,在Linux环境下,.o是目标文件的后缀名,windows环境下是.obj
vim查看test.o:
这里已经是二进制指令了。
汇编操作就是将汇编代码转换为二进制代码。
包含的细节操作有:1.形成符号表
同样:add.c也经历了生成目标文件的操作
gcc -E add.c -o add.i
gcc -S add.i -> add.s
gcc -c add.s -> add.o
add.o和test.o加上链接库经过链接操作生成可执行程序
下面讲一下之前的符号汇总,形成符号表是什么操作
这些代码在进行编译后,其中一些符号会被记录下来比如Add,main,printf(重复的只会记录一次),printf暂不关注,库中的函数有库的实现方式。
test.c中Add,main会被符号汇总
add.c中Add会被符号汇总
符号汇总的是全局类型的符号。各种源文件的符号会分别汇总出来。
符号汇总完成后在下一步汇编会进行形成符号表的操作。形成汇总出的符号对应其地址的表格,称为符号表
test.c的符号表
Add没有一个有效地址(之前的操作是单独对test.c的处理,test.c中只有Add的声明,没有函数的实现,所以不存在有效地址,0x000只是一个填充值),main函数的有效地址也只是假设,实际并不一定是0x400。
add.c的符号表
但符号表真的存在吗?
在Linux环境下,.o文件和可执行程序的文件格式(文件组织形式)是elf
如何读懂elf文件
有一个工具:readelf可以解析elf格式的文件。
这样是不行的,会告诉你要有选项的打开
其中-s选项是打印它的符号表
可以看到在.o文件中确实存在符号表
如果之前按序转变的操作有失误,则进行链接操作时不会生成文件
gcc test.c -c 会直接生成目标文件。选择重新生成一次。
gcc test.o add.o -o test
链接test.o和add.o输出到test文件中。 没有-o test也可以,不过生成的是a.out文件。
a.out和test都是可执行程序,a.out是默认生成的可执行程序。
./test是执行该文件,进行./test和./a.out操作,发现得到的结果是一样的。
链接阶段进行的操作有:
1.合并段表
2.符号表的合并与重定位
合并段表,elf的文件格式会将test.o和add.o文件分成一个个段表,段表每个部分代表某个含义,合并段表就是将代表相同含义部分的合并到一起。
符号表的合并与重定位是指将test.o与add.o的符号表合并为一个符号表
main函数没有争议,还是0x400,add函数有两个位置,这就涉及了符号的重定义,重新找到add函数的实现位置是0x200
所以新的符号表就是
程序的编译和链接结束后,将代码拷贝到vs环境下查看效果,可以正常得出30。
原因在于,当需要使用add函数时,可执行文件中的符号表是有add所在位置的。
如果没有add.c文件,那么在汇编阶段生成的符号表add的位置是0x000,生成可执行文件后从0x000位置不是有效地址。编译器就会报错。同样,如果没有声明外部函数,除了会报一个语法错误,其他的都可以正常实现,因为add的实际地址来源于add.c文件中的add函数。
所有多个目标文件进行链接的时候,会通过符号表查看来自外部的符号是否真实存在。
以上就是多个源文件生成可执行程序的过程。
下面简单介绍一下运行环境:
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack)(就是函数栈帧),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止