程序员的自我修养——链接、装载与库 笔记(一)

程序员的自我修养

  这篇文章可能需要对计算机有过系统的学习,不然看着可能一脸懵。如果有疑问的话,欢迎大家评论区留言指教!此笔记只是刚刚开始,后续我会接着写后面的笔记。


  《程序员的自我修养——链接、装载与库》,初次听到这本书是因为学长学姐的推荐,其实在刚接触 C 语言的时候,就很好奇一个程序最终是怎么跑起来的,一个简单的输出 “Hello Word!” 中间到底经历了什么样的过程?

  奈何那时对计算机的了解刚刚入门,计算机的体系架构混乱,实在无法理解书中的精华。大三第一学期结束,已经学完操作系统和计算机组成原理这两门课,有了基础后,准备详细的阅读这本书,我会陆陆续续的把我的笔记和所感记录在这。

//hello.c文件
#include <stdio.h>

 int main(void)
 {
 	printf("hello world!\n");
	return 0;
 }

1. 被隐藏了的过程

在这里插入图片描述

预编译(也可以叫预处理) 展开所有的宏定义(就是#define)、处理所有的预编译指令(如:“#if”、“#ifdef”、“#elif”、“#else”、“#endif”等)、递归包含头文件并将其中逻辑插入需要的地方、删除所有注释、添加行号和文件名标识(便于编译器产生调试用的行号信息)、保留所有 #pragma 指令

gcc -E hello.c -o hello.i

输出的 hello.i 文件中存放着 hello.c 经预处理之后的代码

编译:编译过程就是把预处理完的文件进行一系列的词法分析、语法分析、语义分析及优化后生成相应的汇编文件。可以使编程者不必过多考虑与机器有关的细节。

gcc -S hello.i -o hello.s

可以得到汇编的输出文件 hello.s
PS:现在版本的 gcc 通常把预编译和编译两个步骤合成一个步骤:

gcc -S hello.c -o hello.s

汇编:将汇编代码转换成机器可以执行的命令(一连串的二进制数,你看不懂,但机器看得懂)。hello.o 是目标文件。 调用汇编器 as 来完成

as hello.s -o hello.o

得到可以执行的机器指令目标文件——hello.o

链接:把一大堆用到的文件拼接到一起,通过符号表解析和重定位等最终输出可加载、可执行的目标文件。

gcc hello.o -o hello

得到可执行程序 hello.exe,在命令行窗口运行 hello.exe 即可出现 “hello world!”

2. 编译器扮演了一个什么样的角色?

  提到编译器,我就想到了编辑器和 IDE,三者的区别是很大的,不了解的人经常闹出笑话,在这里做一点普及。

编辑器,提供非常方便易用的开发环境,提供很好的界面。你可以用它们来编写代码,查看源文件和文档等,简化你的工作。例如VS code、Nodepad++、Atom等。

编译器,大多数情况下,编译是将更高级的语言(C、C++等)编译成低级语言(汇编语言、机器语言)。比如 gcc。

集成开发环境(IDE,Integrated Development Environment )是用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面工具。集成了代码编写功能、分析功能、编译功能、调试功能等一体化的开发软件服务套。常见的有Dev-c、VS、Eclipse 等 IDE。

  有一句话很经典: “计算机科学领域内的任何问题都可以通过增加一个间接的中间层来解决”,这句话会贯穿你学习计算机的始终,很有意义,值得慢慢品味。

   编译器简单来说就是将高级语言翻译成机器语言的一个工具。使用机器指令或者汇编语言写的程序依赖于特定机器,一个为某种CPU编写的程序在另一块 CPU 下完全无法运行,需重新编写,这是令人无法接受的。所以人们期望能够采用类似于自然语言来描述一个程序,但自然语言不够精确,所以类似于数学定义的编程语言诞生了:C、C++、JAVA、Python、PHP等等。

2.1 编译的过程

在这里插入图片描述
编译过程一般可以分为五步:

  • 词法分析:源代码程序被输入扫描器,扫描器运用一种类似于有限状态机的算法可以很轻松的将源代码的字符分割成一系列记号。产生的记号一般可以分为:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(如加号、等号)等等。
    eg: array[index]=(index+4)*(2+6)
记号类型
array标识符
[左方括号
index标识符
]右方括号
=赋值
左圆括号
+加号
4数字
右圆括号
*乘号
  • 语法分析:语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树。简单的讲,由语法分析器生成的语法树就是以表达式为节点的树。在语法分析的同时,很多运算符号的优先级和函数也被确定下来。比如乘法表达式的优先级比加法高,而圆括号的优先级比乘法高,等等。另外有些符号具有多重含义,比如 ′ ∗ ′ '*' 既有乘号的意思,也有指针的意思,所以语法分析阶段必须对这些内容进行区分。如果出现表达式不合法,比如各种括号不匹配、表达式中缺少操作符等,编译器就会报告语法分析阶段的错误。(这就是让人难过的语法报错,与我们几乎天天见面。只要你经历过编程,你一定见过他~~,虽然我极其极其讨厌他,可他对我却不离不弃,生死相依/++/)
    eg:id1:=id2+id3*10 生成语法树:在这里插入图片描述

  • 语义分析:由语义分析器来完成。语法分析仅仅是完成了对表达式的语法层面的分析,但他并不了解这个语句是否有真正意义。比如C语言里两个指针相乘是没有意义的,这一个语句在语法上是合法的。又比如说特别让人头疼的内存泄漏、指针泄露、野指针等等。编译器所能分析的语义是静态语义,所谓静态语义是指在编译期可以确定地语义,与之对应的是动态语义,就是只有在运行期才能确定的语义。经过语义分析阶段之后,整个语法树的表达式都被标识了类型,如果有些类型需要隐式转换,语义分析程序会在语法树中插入相应的转换节点。(如果说上边那玩意语法分析报错还可以很容易找到哪出错了,语义分析这如果警告,那你自求多福吧,慢慢找~~)

  • 中间语言的生成:现代编译器有着很多层次的优化,往往在源代码级别会有一个优化过程。我们这里描述的源码级优化器在不同编译器中可能会有不同定义。例如语句:array[index]=(index+4)*(2+6),2+6将被优化成8。源代码优化器往往将整个语法树转换成中间代码,他是语法树的顺序表示,其实他已经非常接近目标代码了。但它一般跟目标机器和运行环境是无关的,比如不包含数据尺寸、变量地址、和寄存器名字等。中间代码有很多种类型,常见的有三地址码和P-代码。比如,上式的三地址中间代码是:

t1=index+4
t1=t1*8
array[index]=t1

  中间代码使得编译器可以被分为前端和后端。编译器前端负责产生于机器无关的中间代码,编译器后端将中间代码转换为目标机器代码。这样对于一个跨平台的编译器而言,他们可以针对不同的平台使用同一个前端和针对不同机器平台的数个后端。

  • 目标代码生成与优化:源代码优化器产生中间代码标志着下面这些过程都属于编译器后端。编译器后端主要包括代码生成器目标代码优化器。代码生成器将中间代码转换成目标机器代码,这个过程十分依赖于目标机器,因为不同的机器有不同的字长、寄存器、整数数据类型等。
movl index, %ecx;
addl $4, %ecx;
mull $8, %ecx;
movl index, %ecx;
movl %ecx, array(,eax,4);

最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等。

movl     index, %edc
leal     32(,%edx,8),%eax
movl     %eax,array(,%edx,4)

 现代编译器有着异常复杂的结构,这是因为现代高级编程语言本身也很复杂,比如 C++ 语言的定义就极为复杂。另外现代计算机的CPU 也相当复杂,为了支持 CPU 的特性,编译器的机器指令也变得十分复杂。比如著名的 GCC 编译器就支持几乎所有的 CPU 平台,这也导致了编译器的指令生成过程十分复杂。

2.2 链接器

 经过上述步骤,源代码终于被编译成目标代码。但还有一个问题,那就是 index 和 array 的地址问题。index 和 array 的地址从哪得到呢?如果 index 和 array 定义在跟上边源代码同一个编译单元里,那么编译器可以为他们分配空间。那如果是定义在其他程序模块呢?这里引出一个问题,很重要!目标代码中有变量定义在其他模块该怎么办?事实上,定义在其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链接的时候才能确定。所以,现代编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些个目标文件链接起来形成可执行文件。 事实上,链接器的年龄比编译器年龄要长。

  • 程序并不是一成不变的,修改后的地址变化问题非常繁琐。为了简化编程,人们开始使用符号来代指位置,其地址在使用过程中动态插入到需要的位置。重新计算各个目标位置的过程即为重定位
  • 运行一个程序所需要的代码量可能非常庞大,而且很大一部分代码可重用性很高,而且模块之间耦合度很低。于是人们便将代码分割成了很多部分,使用时再将各个部分拼接起来,这个过程就是链接,这些部分在不同的语言中有不同的形式,比如引用、包、或者库。
  • 可想而知,链接过程中必定存在很多内部或外部的函数或变量,所以这个过程包括了很多诸如地址空间分配、符号决议、重定位等步骤。
  • 链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确的衔接。从原理上讲,他的工作无非就是把一些指令对其他符号地址的引用加以修正。每个模块的源代码文件(.c)文件经过编译器编译成目标文件(.o或.obj),目标文件和库一起链接形成最终可执行文件。

3.小结

  首先回顾了从源程序代码到最终可执行文件的4个步骤:预编译、编译、汇编、链接。IDE 集成开发环境和编译器默认命令通常将这些步骤合成一步,使得我们通常很少关注这些步骤。
  还回顾了4个步骤中的主要步骤,编译步骤。介绍了编译器将 C 程序源代码转变成汇编代码的若干个步骤。最后介绍了链接的历史及一些基本概念:重定位、符号、目标文件、库、运行库的概念。

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值