1、浅谈 OS 及 build 过程 by Armand*InstructionContent as below:1、长久以来的一些疑问2、计算机系统层次3、OS 功用(文件系统& 进程调度&进程与线程)4、build 的过程By Armand /12 August 2013*长久以来的一些疑问曾经缠着一个助教问很多问题,当时是因为刚接触 Arm linux,没有了跑裸机时的一眼望到底的快感和掌控感,对很多事情都想搞明白所以然来,你用鼠标 click 一下,到结果呈现在屏幕上,中间到底发生了什么?他就推荐我读一本叫程序员的自我修养 ,现在终于有时间好好阅读一下,顺便记下笔记,以备长久之用。相信它会
2、在操作系统、编译原理方面给我带来长足的进步。慢慢从底层往上走这是趋势,而计算机专业的同学从上层往底层走就痛苦多了,要抓住这次机会,持续更新这一系列文档,直到我的知识结构发生变化,就像下图一样,打通:浅谈 OS 及 build 过程 by Armand一个大牛曾经说过:体系结构、汇编、C 语言、操作系统,永远都是编程大师们的护身法宝,如同少林寺的易筋经 ,学会了之后将无所不能。不知你是否也有下面的疑问?C/C+程序如何被编译成目标文件,程序在目标文件中如何存储?目标文件又如何被 linker 链接在一起形成可执行文件(符号处理、重定位、地址分配)可执行文件如何被装载并执行?可执行文件与进程的虚拟
3、空间之间如何映射?什么是动态链接,为什么需要动态链接?什么是堆、栈?什么是运行库、系统调用?如果上面的知识结构能够打通,那么相信这些问题也就不是问题了,go!计算机系统层次计算机科学领域的所有问题都可以通过增加中间层的方式解决。跟通信协议中的分层相似,下层为上层提供服务,并规定服务的申请规则,即定义接口标准(interface) 。计算机系统层次也不例外如下:其中除了硬件和应用之外的所有层次均可称之为中间层。浅谈 OS 及 build 过程 by Armand应用程序调用系统 API,linux 下的 glibc 库提供 Posix 标准的 API,而 window 下则是win32 标准的
4、API。设备驱动屏蔽了硬件,它可以看作是内核的一部分,设备驱动的机制和接口并不是由硬件厂商规定的,这是 OS 的机制,硬件厂商只负责驱动的实现。glibc 是 gnu 发布的 libc 库 ,即 c 运行库。glibc 是 linux 系统中最底层的 api,几乎其它任何运行库都会依赖于 glibc。glibc 除了封装 linux 操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。由于 glibc 囊括了几乎所有的 UNIX 通行的标准,可以想见其内容包罗万有。而就像其他的 UNIX 系统一样,其内含的档案群分散于系统的树状目录结构中,像一个支架一般撑起整个作业系统。
5、在 GNU/Linux 系统中,其 C 函式库发展史点出了GNU/Linux 演进的几个重要里程碑,用 glibc 作为系统的 C 函式库,是 GNU/Linux 演进的一个重要里程碑。分享函式库群,这是 glibc 的主体,分布 /lib 与 /usr/lib 中,包括 libc 标准 C 函式库、libm 数学函式库、 libcrypt 加密与编码函式库、libdb 资料库函式库、libpthread 行程多执行绪函式库、libnss 网路服务函式库 等等。这些都是可分享函式库,档名都以 .so 做结尾。在 Linux 平台上最广泛使用的 C 函数库是 glibc,其中包括 C 标准库的实
6、现,也包括本书第三部分介绍的所有系统函数。几乎所有 C 程序都要调用 glibc 的库函数,所以 glibc 是 Linux 平台 C 程序运行的基础。glibc 提供一组头文件和一组库文件,最基本、最常用的 C 标准库函数和系统函数在 libc.so 库文件中,几乎所有 C 程序的运行都依赖于 libc.so,有些做数学计算的 C 程序依赖于 libm.so,以后我们还会看到多线程的 C 程序依赖于 libpthread.so。以后我说 libc 时专指 libc.so 这个库文件,而说 glibc 时指的是 glibc 提供的所有库文件。glibc 并不是 Linux 平台唯一的基础 C
7、函数库,也有人在开发别的 C 函数库,比如适用于嵌入式系统的 uClibc。讲到标准 C 库,其实很多 C 语言的书里都用了大篇幅来讲解标准 C 库,我认为这是不对的!这样就降低了大家 C 语言本身特性的关注度,特别是对于做嵌入式的人来说,很多 IDE 里根本就没有完整标准 C 库,一本书读下来,就前几章有用。从网上找了一段话,认为比较有用。C 标准主要由两部分组成,一部分描述 C 的语法,另一部分描述 C 标准库。C 标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义。要在一个平台上支持 C 语言,不仅要实现 C 编译器,还要实现 C 标准库,这样的实现才算符
8、合 C 标准。不符合 C 标准的实现也是存在的,例如很多单片机的 C 语言开发工具中只有 C 编译器而没有完整的C 标准库。浅谈 OS 及 build 过程 by ArmandOS 的功用操作系统有两个大的功能:一是提供抽象的接口(即通过库来提供系统服务) ,使得开发变得高效,二是管理硬件资源,是它们运行得高效。它创建了一套管理制度(算法) ,使系统运行便捷、高效、有序。所以,如果能全部搞明白操作系统的管理思想,你就成为了一个出色的管理者。所以,当嵌入式平台的配置越来越高,自然就转向了操作系统,它比前后台系统拥有无可比拟的优势。当 arm 开发转向了 LINUX,不但嵌入式平台上运行的是 li
9、nux,连我们的开发环境也是在linux 上进行的,主要的原因是在编译内核等等开发步骤上,linux 平台提供了完整的 GUN 工具链,还有一个重要的原因是嵌入式 linux 是经过剪裁的 linux,本质上并没有变化,做 linux 开发连 linux 环境都不熟的话怎么行?你的电脑上的主要部件正好对应 OS 的主要功能硬件是死的,比如,CPU 就只负责执行指令,你的 PC 指针指向哪,在 clock 的驱动下,PC就不断地依次执行哪里的指令。所以要进行进行调度,无论是分时还是基于优先级,压栈、出栈、安排任务都是 OS 的事,所有的应用程序都以进程的方式运行在比操作系统更低的级别,每个进程都
10、有自己独立的地址空间,并相互隔离。CPU 由 OS 统一分配,无论优先级高低都会有机会获得 CPU 的使用权,可以想象,系统从一堆半导体器件到如此有序、高效的运行,OS 肯定做了海量的工作。我们的硬盘的结构层次:硬盘盘面磁道扇区。文件系统保存了这些文件的存储结构(仓库与货物清单)并负责维护这些数据结构,同时保证磁盘中的扇区能有效地组织利用。而操作系统内核需要做的是拥有文件系统的操作方式(相当于驱动) ,这就是我们说的支持的文件系统。这样在某个文件系统挂载之后才能正确的读写。现在有一个 8k bytes 的文件 test.datlinux 的 ext3 文件系统可能是这样存储它的:sector
11、1000-1007 共 4096bytes浅谈 OS 及 build 过程 by Armand2000-2007 共 3094 bytes现在要读它,read 系统调用, (内核)文件系统受到 read 请求后,判断出所在扇区的位置,调用硬盘驱动发出读硬盘的指令(一般是读写 I/O 端口寄存器)内存在计算机中是仅次于 CPU 的第二宝贵的资源,支持多任务之后,如何将有限的物理内存高效地分配给多个程序使用成了 OS 的紧要任务内存管理。假如一台电脑的内存大小为 128M,程序运行所需的内存空间连续。现在有 A 和 B 两个任务,A 任务需要 10MB 的运行内存,B 任务需要 100MB 的运行
12、内存,最简单的分配方式是直接分配给他们共 110M 内存。但是这样的坏处是 地址不隔离,即程序直接访问物理内存,在程序遭到恶意修改后,很容易访问到不属于自己的内存空间,造成系统故障,影响其他程序的运行。 内存的使用效率很低。 程序运行的地址不确定,给编程带来了困难。因为它访问数据和指令跳转时目标地址在链接生成可执行文件的时候就确定了,这就涉及到重定位的问题。所以!增加中间层!把程序给出的地址看做是虚拟的,然后经过分配空间之后形成的映射表变成实际的物理地址。当程序访问的地址空间超过分配的空间时直接看做是异常给拒绝掉。这样地址隔离的问题就解决了。单纯的使用映射机制(分段) ,解决了第一个和第三个问
13、题,但是映射机制以程序(进程)为单位,内存不足的时候,被换入换出的都是整个可执行程序,这样就会进行大量的磁盘访问,影响速度,效率低下。根据程序的局部性原理,在某个时间段,只频繁的引用了一小部分数据,我们使用分页机制来解决这个问题。将地址空间等分成固定大小的页。CPU 硬件本身支持整页读写,多种页大小可选。目前几乎多有的 PC OS 都选择 4KB 大小的页。分页就是说,将程序分成若干页,但不全部调入内存,运行时当所需的页不在内存当中时,会发生页错误,操作系统接管该进程,并把所需的页从磁盘调入内存。如下页图:虚拟存储的实现需要依靠硬件MMU浅谈 OS 及 build 过程 by Armand进程
14、与线程线程(thread)又称为轻量级进程(LWP ,lightweight process) ,是程序执行的最小单元,一个进程由多个线程组成,各线程共享程序的内存空间(代码段、数据段、堆)及一些进程级的资源(打开文件、信号)浅谈 OS 及 build 过程 by Armand简析 Build 的过程在 linux 中,由源码生成可执行文件:预编译 删除所有#define 展开所有宏定义 处理所有的条件预编译指令如:#if, #ifdef, #else,#elif,#endif 等#ifdef 标识符程序段 1#else程序段 2#endif它的作用是:当标识符已经被定义过(一般是用#defi
15、ne 命令定义),则对程序段 1 进行编译,否则编译程序段#if 表达式程序段 1#else程序段 2#endif它的作用是:当指定的表达式值为真(非零)时就编译程序段 1,否则编译程序段 2。可以事先给定一定条件,使程序在不同的条件下执行不同的功能。浅谈 OS 及 build 过程 by Armand 处理所有的#include,将被包含的文件出入到该指令的位置。原来只是插入!我说呢,很多疑问解决了, .c 也可以插入!在使用前用 extern 声明与在头文件中声明是一样的!头文件中 define 也是可用的!条件编译防止重复插入! 删除所有的注释 添加行号和文件名标示 保留所有的#prag
16、ma 编译器指令,因为编译器需要使用它们。在所有的预处理指令中,#Pragma 指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作编译:对预处理的文件进行:词法分析、语法分析、语义分析,优化后生成汇编代码。词法分析是将源代码经过 scanner,分离成一个个 token:关键字、标识符、字面量(数字、字符串)和特殊符号(+ =等) 。有专门的一个程序叫 lex 来完成这项工作,当你创建了一种编程语言,没必要自己写词法分析器,只需要把规则传递给 lex 即可。然后用语法分析器对记号进行语法分析,产生语法树(以表达式为节点的树) 。语法分析也有一个叫 yacc 的程
17、序完成。在完成语法分析之后,编译器也不了解这些语句的含义。在语义分析中,整个语法树被标上类型,如果需要隐式转换,则语义分析程序会在语法树上插入相应的转换节点。然后会经过相当多的优化,源码级的优化,然后生成目标代码(汇编代码) ,目标代码优化。现代编程语言本身的特性就非常复杂,就像 C+语言的定义十分复杂,至今也没有一个编译器能完整的支持它的语言标准规定的所有特性。加上现代 CPU 采用了流水线、多发射、超标量等技术,使优化过程也很复杂。汇编:就是将汇编代码翻译成机器码链接:为什么有链接呢,这是代码工程发展到一定程度的产物,如果一个工程只有一个源码文件是不需要链接。当工程分了很多模块,各自完成相
18、应功能,相互调用(头文件相当于在画大饼,真正到产生可执行文件还需要真实的函数体) ,这是工程性的要求。有了耦合就需要链接。链接其实是地址重新分配和重定位的过程。说一下重定位:“符号”这个概念随着汇编语言的发展迅速被使用。它用来表示一个地址,这个地址可以是一段子程序(后来发展成函数)的起始地址,也可以是一个变量的起始地址。如 jump foo 。汇编器在生成二进制代码时都会重新 foo 的地址,因为代码可能已经经过的增加、删除等,foo 标号的起始地址也许已经发生变化。有了符号的概念之后,软件的规模越来越大。直到分成多个文件,符号地址的修改放在了链接阶段重新修改,这就叫重定位。只有当多个文件放在
19、一起的时候,才能知道那些符号代表的地址。这个需要重定位的符号,对于 C 或C+来说就是变量和函数。关于如何链接的细节,要参照 linker file,这个细节我也没研究过。下图描述链接过程浅谈 OS 及 build 过程 by Armand库就是一组目标文件的包。比如 mqx 的 bsp 库里面有很多的源文件,经过 build 生成了一组.o组成的库文件。其实链接的过程很好理解:Main.c 中使用了另一个模块中的函数 foo ()我们每一次调用均需要知道它的确切地址但是由于模块是单独编译的,compiler 在编译汇编 main.c 的时候并不知道 foo 的地址。所以暂时把调用 foo 的指令的目标地址搁置(用 0 代替) ,等链接的时候,模块组合在一起,foo的地址(无论相对的还是绝对的)确定了,由 ld 去修改 main 中引用 foo 的地址。地址修改的过程就是重定位,每一个需要修改的地方叫一个重定位入口。