《程序员的自我修养-链接、装载与库》读书笔记
1.写在最开始
学习C语言的时候,最经典的入门便是Hello Word,然后使用一系列IDE运行便可打印Hello World,但是其中的运行原理我到现在都不是清除,即便现在大四已经是找到了不错的工作,而《程序员的自我修养-链接、装载与库》这本书便打开了我对其中底层原理探索的大门
#include<stdio.h>
int main(){
printf("Hello World\n");
return 0;
}
书中第一部分便提出下面一些问题,可以说我大部分都是一知半解,
-
程序为什么要被编译器编译了之后才可以运行?
-
编译器在把C语言程序转换成可以执行的机器码的过程中做了什么,怎么做的?
-
最后编译出来的可执行文件里面是什么?除了机器码还有什么?它们怎么存放的,怎么组织的?
-
include <stdio.h>是什么意思?把stdio.h包含进来意味着什么?C语言库又是什么?它怎么实现的?
-
不同的编译器(Microsoft vc.GCC)和不同的硬件平台(x86、SPARC、MIPS、ARM),以及不同的操作系统( Windows、Linux、UNIX、Solaris),最终编译出来的结果一样吗?为什么?
-
Hello World程序是怎么运行起来的?操作系统是怎么装载它的?它从哪儿开始执行,到哪儿结束?main函数之前发生了什么?main函数结束以后又发生了什么?
-
如果没有操作系统,Hello World可以运行吗?如果要在一台没有操作系统的机器上运行Hello World需要什么?应该怎么实现?
-
printf是怎么实现的?它为什么可以有不定数量的参数?为什么它能够在终端上输出字符串?
Hello World程序在运行时,它在内存中是什么样子的?
从这些问题中可以看出,这本书是非常值得我们学习的,即使我目前是一名Java开发程序员,但是了解底层程序的执行原理便于很好的提高我们的内功
写于2021年11月3号
2.到底是怎么编译的?
此前,我们运行一段C/C++程序都是gcc xxx.c,gcc编译器便会帮助我们生成一个a.out的可执行文件(Linux平台下)然后执行这个可执行文件我们的程序说是执行成功了,但是gcc xxx.c到底做了多少工作?实际上,上述过程可以分为四个过程,预处理(Prepressing)
,编译(Compliation)
,汇编(Assembly)
,链接(Linking)
2.1 预编译
预编译过程是源文件hello.c和相关的头文件被预编译cpp预编译成一个.i文件,可以使用如下命令
gcc -E hello.c -o hello.i
预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。比如“#include"、“define”等,主要处理规则如下:
- 将所有的“#define”删除,并且展开所有的宏定义
- 处理所有条件预编译指令,比如“#if”、“#ifdef”、“#elif”、“#else”、“#endif”。
- 处理“#include”预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
- 删除所有的注释“!”和“/**/”。
- 添加行号和文件名标识,比如#2“hello.c”2,以便于编译时编译器产生调试用的行号信息及用于
- 编译时产生编译错误或警告时能够显示行号。
- 保留所有的#pragma编译器指令,因为编译器须要使用它们
经过预编译后的.i文件不包含任何宏定义,因为所有的宏都已经被展开,所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以预编译之后来确定问题
2.2 编译
编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析以及优化后生产响应的汇编代码,使用下面的命令
gcc -S hello.i -o hello.s
2.2.1 词法分析
源代码程序被输入到扫描器,运用有限状态机的算法可以很轻松的将源代码分割成一些列的记号,例如上面的程序将会产生16个记号
2.2.2 语法分析
接下来语法分析器将对扫描器产生的记号进行语法分析,从而产生语法树,语法树就是以表达式为节点的树,例如上面程序将会解析为下面的语法树
2.2.3 语义分析
接下来语义分析器将会进行语义分析,语法分析只是完成对表达式的语法层面的分析,但是并不了解这个语句是否真正具有意义,例如c语言里面两个指针做乘法运算在语法上是合法的,但是不具备任何意义,编译器能分析的语义是静态语义,是在在编译期间能确定过的语义
静态语义经常包括声明和类型的匹配,比如类型向下转型语义分析要完成这个步骤,如果类型不匹配将会报出编译错误的信息,而对应的动态语义则是运行期间才能确定的语义例如0作为除数。经过语义分析之后整个语法树的表达式都被标识了类型
2.2.4 编译器优化
gcc编译器会有很多层级的优化,这里不做非常详细的论述,对于上述的代码,可以很明确的一个优化点是2+6可以直接算出
2.3 汇编
汇编器是将汇编代码转变为机器可以执行的一个指令,每一条汇编语句几乎都对应一条机器指令,所以汇编只是根据汇编指令和机器指令一一对应便可以,输出一个目标文件
as hello.s -o hello.o
gcc -c hello.s -o hello.o
2.4 链接
思考一个问题,在汇编的时候已经将汇编代码转为机器可以执行的代码,那为什么还要有链接这个过程呢?回头看我们的程序中调用了print函数,那么我们怎么确定print函数的位置呢?
我们都已经知道一个好的项目代码应该是分模块的,各个模块之间相互独立可以运行调试,例如我们在main.c中需要调用fun.c中的函数fun,在main.c模块中的每一处调用函数fun都需要foo这个函数的地址,但是由于每个模块是单独编译的,所以在编译的时候暂时把调用foo函数的指令的目标地址搁置,等待最后连接的时候由链接器去将这些指令进行修正,而链接的过程便是做这些工作,试想一下如果没有链接器,需要手动的将每个调用foo的指令进行修正,当fun.c模块被重新编译foo的地址发生变化那么这将会是巨大的灾难
gcc -o hello hello.o
做这些工作,试想一下如果没有链接器,需要手动的将每个调用foo的指令进行修正,当fun.c模块被重新编译foo的地址发生变化那么这将会是巨大的灾难
gcc -o hello hello.o