翻译环境和运行环境
前提解释
电脑是不能直接执行C语言的程序代码的,所依赖的是翻译环境进行一个源代码运行的时候需要经过翻译环境和运行环境的处理,才能得到你需要的可运行程序。
这里是源文件,也就是
.c文件,通过翻译环境得到可执行程序。
那么翻译环境里面都有什么呢,下面会进行讲解。
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
翻译环境
总的流程
在计算机科学中,"翻译环境"(Translation Environment)通常指的是一个用于将源代码转换为机器代码的软件环境。这通常包括编译器、汇编器、链接器等工具,以及用于编写、调试和测试代码的集成开发环境(IDE)。
翻译环境的主要作用是将高级编程语言(如C、C++、Java等)编写的源代码转换为计算机能够理解的机器代码。这个过程通常包括以下几个步骤:
1. **预处理**:处理源代码中的预处理器指令,如包含头文件、宏替换等。
2. **编译**:将源代码编译成汇编代码。
3. **汇编**:将汇编代码转换成机器代码。
4. **链接**:将多个机器代码文件合并成一个可执行文件,并处理外部依赖。
运行环境(Runtime Environment)则是指程序运行时所需的环境,包括操作系统、硬件资源、库文件、配置文件等。它为程序提供了一个执行的环境,使得程序能够正确地运行。
例如,当您运行一个用C语言编写的程序时,翻译环境包括编译器(如GCC)、链接器(如ld),以及可能使用的IDE(如Visual Studio Code、Eclipse等)。运行环境则包括操作系统(如Windows、macOS、Linux)和必要的库文件。
总的来说,翻译环境负责将源代码转换为机器代码,而运行环境负责在实际硬件上运行这个机器代码。
——————————————————————————————————————————
举例详解
exe(可执行程序)依赖翻译环境,生成的可执行程序
翻译环境依赖的也就是我们的编译器
也就是vs,vs也就是我用的编译器,以及你们使用的编译器。这个可以是不一样的。
—————————————————————————————————————————— —————————————————————————————————————————— —————————————————————————————————————————— ——————————————————————————————————————————
翻译环境的组成
翻译环境是由两个程序组成,先编译 再链接、
图片里面的obj也就是称之为目标文件
翻译环境的过程
1,我们写的代码先创建文件test.c的文件,也就是test.c,然后经过编译器的处理,生成目标文件(test.obj)也就是(obj)
2,目标文件和链接库一起经过链接器的处理,生成可执行程序。
3,生成目标文件(obj)的过程称之为编译(也就是生成obj的过程)
4,生成可执行程序的过程称之为链接。
5,最后整个我们写代码的地方也就是集成开发环境
翻译环境的须知
一个编译器里面可以有多个.c文件
——————————————————————————————————————————
代码举例
也就是几个文件相互协同,一个负责实现,一个负责定义,就比如
这里只需要进行声明就可以了
这说明每个源文件都是会通过编译器单独处理,生成目标文件
目标文件和链接库一块处理,通过链接器生成可执行程序
—————————————————————————————————————————— —————————————————————————————————————————— —————————————————————————————————————————— ——————————————————————————————————————————
翻译环境里面的编译分为三个过程:预处理,编译,汇编
c代码->预处理->编译->汇编->链接->可执行程序
翻译流程
1,首先创建一个文件test.c
2.经过预处理(预编译)(-E), 生成后缀为test.i 的文件
3.test.i经过 编译(-O),生成后缀为test.S的文件
4.test.s经过汇编,生成目标文件test.o(obj)的文件。
5.此时的目标文件和链接库一块处理,通过链接器进过链接生成可执行程序。
—————————————————————————————————————————— —————————————————————————————————————————— —————————————————————————————————————————— ——————————————————————————————————————————
预处理阶段(预编译)
预处理的流程
预处理阶段也叫预编译
test.c预处理 -E 处理之后生成test.i
这里是test.c -E(也就预处理之后)
我需要生成一个.i的文件
会发现这里.i里面,已经进行处理了
注释在.c里面有 但是子啊.i里面是没有的
注释是给人看的,计算机是看不懂的 也不会进行读取的
通常 编译器编译的时候,中间文件直接都删掉了
——————————————————————————————————————————
预处理阶段规则:
预处理阶段主要处理那些源⽂件中#开始的预编译指令。⽐如:#include,#define,处理的规则如下:
• 将所有的 #define 删除,并展开所有的宏定义。
• 处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif 。
• 处理#include 预编译指令,将包含的头⽂件的内容插⼊到该预编译指令的位置。这个过程是递归进
⾏的,也就是说被包含的头⽂件也可能包含其他⽂件。
• 删除所有的注释
• 添加⾏号和⽂件名标识,⽅便后续编译器⽣成调试信息等。
• 或保留所有的#pragma的编译器指令,编译器后续会使⽤。
经过预处理后的.i⽂件中不再包含宏定义,因为宏已经被展开。并且包含的头⽂件都被插⼊到.i⽂件
中。所以当我们⽆法知道宏定义或者头⽂件是否包含正确的时候,可以查看预处理后的.i⽂件来确认。
编译器默认的时候,进行编译的时候,编译完毕就会直接删掉了
—————————————————————————————————————————— —————————————————————————————————————————— —————————————————————————————————————————— ——————————————————————————————————————————
编译
编译流程
也就是预处理的下一步
gcc -S test.i -o test.s
也就是原来的是 test.i(预处理之后生成的文件) -o(进行编译)生成test.s文件
编译期间会进行词法分析、语法分析、语义分析及优化
编译过程就是将预处理后的⽂件进⾏⼀系列的:词法分析、语法分析、语义分析及优化,⽣成相应的汇编代码⽂件。
编译这个动作是会生成.s的文件的
这个文件里面放置的是汇编代码
test.S里面放置的是汇编代码
gcc(这里使用的是gcc编译器)-s也就是对预处理之后的test,i文件进行处理,生成test.s文件
——————————————————————————————————————————
词法分析
test.s文件生成之前会进行 词法分析、语法分析、语义分析及优化
拿这个代码举例:array[index] = (index+4)*(2+6);
将源代码程序被输⼊扫描器,扫描器的任务就是简单的进⾏词法分析,把代码中的字符分割成⼀系列的记号(关键字、标识符、字⾯量、特殊字符等)
也就是对代码进行拆分和解析,上⾯程序进⾏词法分析后得到了16个记号
简单的说就是看看文件里面有什么,下一步我得干嘛,顺便看看这个符号都有啥,方便等会转化
——————————————————————————————————————————
语法分析
生成语法树(可以理解为计算逻辑应该是什么,怎么进行计算,是先进行加法还是先减法,罗列好,方便下一步的运行)
这里整体根据表达式 进行语法分析
——————————————————————————————————————————
语义分析
什么是语义分析(简单的说就是二次检查语法错误,看看整形+整数是不是整形,不是的话提前报错)
由语义分析器来完成语义分析,即对表达式的语法层⾯分析。编译器所能做的分析是语义的静态分
析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息。
就比如这个,这里是进行语义分析,分析出来是4 这个是整形+整形 本质是整形
这就是我们说的语义分析
—————————————————————————————————————————— —————————————————————————————————————————— —————————————————————————————————————————— ——————————————————————————————————————————
汇编
汇编的流程
gcc -c(汇编)生成test.o生成汇编文件
汇编也就是把汇编转化成二进制指令
下一步就可以把目标文件通过链接库+链接器生成可执行程序了
——————————————————————————————————————————
目标文件的产生流程
汇编会生成.o 文件,test.o文件里面放置的是二进制,这个文件就是我们的目标文件
此时,这个整个大的过程叫做编译,也就是包括了预处理+编译+汇编
包括了
源文件(test.c)->(-E)->预处理->(处理不必要的东西)->编译(test.i)->(-O(词法分析)(语法分析)(语义分析))-->汇编(test.s)(目标文件)
在.o这个文件里面 已经放置的是二进制文件了
汇编就是把计算机代码转换成计算机指令
—————————————————————————————————————————— —————————————————————————————————————————— —————————————————————————————————————————— ——————————————————————————————————————————
链接
什么是链接
链接是⼀个复杂的过程,链接的时候需要把⼀堆⽂件链接在⼀起才⽣成可执⾏程序。
链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤。
链接解决的是⼀个项⽬中多⽂件、多模块之间互相调⽤的问题
——————————————————————————————————————————
链接的过程
链接的时候是把多个文件放到一起,生成一个可执行程序
符号表也是需要合并的,符号表也是需要合并成一份
这里合并的时候 ,保留有效的,舍弃无效的,对符号表进行合并
合并完成之后,我们通过有效的地址找到文件,从而相互调用函数
——————————————————————————————————————————
错误举例
如果此时你写错变量名字了
也就是Add写成add了 此时报错
报错的就是链接错误
链接错误就是这个链接表对照的时候 找不到地址
这就是连接错误的由来
查找的时候是按照大写去查找的,但是实际找到的时候是按照小写着找到的
最后发现,链接表合并结束,对照不上。
由此而来,链接错误。
——————————————————————————————————————————
二次讲解
我们已经知道,每个源⽂件都是单独经过编译器处理⽣成对应的⽬标⽂件。
test.c 经过编译器处理⽣成 test.o
add.c 经过编译器处理⽣成 add.o
我们在 test.c 的⽂件中使⽤了 add.c ⽂件中的 Add 函数和 g_val 变量。
我们在 test.c ⽂件中每⼀次使⽤ Add 函数和 g_val 的时候必须确切的知道 Add 和 g_val 的地
址,但是由于每个⽂件是单独编译的,在编译器编译 test.c 的时候并不知道 Add 函数和 g_val
变量的地址,所以暂时把调⽤ Add 的指令的⽬标地址和 g_val 的地址搁置。等待最后链接的时候由
链接器根据引⽤的符号 Add 在其他模块中查找 Add 函数的地址,然后将 test.c 中所有引⽤到
Add 的指令重新修正,让他们的⽬标地址为真正的 Add 函数的地址,对于全局变量 g_val 也是类
似的⽅法来修正地址。这个地址修正的过程也被叫做:重定位。
前⾯我们⾮常简洁的讲解了⼀个C的程序是如何编译和链接,到最终⽣成可执⾏程序的过程,其实很多
内部的细节⽆法展开讲解。
⽐如:⽬标⽂件的格式elf,链接底层实现中的空间与地址分配,符号解析
和重定位等,如果你有兴趣,可以看《程序的⾃我修养》⼀书来详细了解
—————————————————————————————————————————— —————————————————————————————————————————— —————————————————————————————————————————— ——————————————————————————————————————————
运行环境
运行环境的理解和分析
1. 程序必须载⼊内存中。在有操作系统的环境中:⼀般这个由操作系统完成。在独⽴的环境中,程序的载⼊必须由⼿⼯安排,也可能是通过可执⾏代码置⼊只读内存来完成。(当我们运行程序的时候,我们是双击运行程序,也就是加载到内存里面,这样的操作有些是操作系统完成,有些是手工安排。)
2. 程序的执⾏便开始。接着便调⽤main函数。(此时放到内存里面之后开始只执行。)
3. 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使⽤静态(static)内存,存储于静态内存中的变量在程序的整个执⾏过程⼀直保留他们的值。(软件运行,程序运行依赖的环境)
4. 终⽌程序。正常终⽌main函数;也有可能是意外终⽌。(软件的关闭)
这里说明一下,这既是一个过程,学会理解就好了,不要较真的不得了。
—————————————————————————————————————————— —————————————————————————————————————————— —————————————————————————————————————————— ——————————————————————————————————————————
总结
翻译流程
1,首先创建一个文件test.c
2.经过预处理(预编译)(-E), 生成后缀为test.i 的文件
3.test.i经过 编译(-O),生成后缀为test.S的文件
4.test.s经过汇编,生成目标文件test.o(obj)的文件。
5.此时的目标文件和链接库一块处理,通过链接器进过链接生成可执行程序。
总的流程
源文件(test.c)->(-E)->预处理->(处理不必要的东西)->编译(test.i)->(-O(词法分析)(语法分析)(语义分析))-->汇编(test.s)(目标文件)
—————————————————————————————————————————— —————————————————————————————————————————— —————————————————————————————————————————— ——————————————————————————————————————————
编辑、预处理、编译、链接之间的区别
预处理只会处理#开头的语句,编译阶段只校验语法,链接时才会去找实体,所以是链接时出错的,故选C。这里附上每个步骤的具体操作方式:
预处理:相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有头文件(都已经被展开了)、宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了),没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
编译:将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。编译是针对单个文件编译的,只校验本文件的语法是否有问题,不负责寻找实体。
链接:通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。 链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。