参考教程:

概述

通常来说,学习任何新的东西,都会围绕这三个问题来由浅入深地展开攻势:

  • (What):这是什么东西?

  • (Why):为什么要学习它?

  • (How):如何学习它?

下面通过回答这三个问题,来对操作系统进行一场美丽的邂逅!

什么是操作系统?

先来看一下《Operating Systems: Three Easy Pieces》对**操作系统(Operating systems)**的定义:

Operating System: A body of software, in fact, that is responsible for making it easy to run programs (even allowing you to seemingly run many at the same time), allowing programs to share memory, enabling programs to interact with devices, and other fun stuff like that.

翻译:

操作系统:事实上,软件的主体,负责使程序易于运行(甚至允许您看似同时运行多个程序),允许程序共享内存,使程序能够与设备交互,以及其他诸如此类的有趣的事情。

这段话看似说了很多,但是又好像什么都没说对吧?因为这段话基本上将操作系统相关的所有内容全部涵盖了,压缩成了及其简短的一句概括,所以理解起来较为抽象,但是没有关系,当你熟知了操作系统相关的知识之后,回过头来再看这句话,一切都清晰明了了~


下面是基于我个人对操作系统的理解和定义:

如果以一个最简单的定义来说,操作系统就是一个程序(Program)

So?仅此而已吗?

当然不只是如此,操作系统是一个较为特别的程序,它和我们编写的Hello world有那么一丢丢不同,一丢丢是多少?

操作系统本质上来说就是一个由C语言编写的程序。不同的是它的体系非常庞大,并且更加接近计算机底层硬件,可以对硬件进行控制和调度。我们编写的程序都是运行在操作系统之上的,操作系统管理所有的应用程序,调度硬件为程序提供资源

可以划分为三个角色:

  1. 硬件:计算机各个器件。

  2. 软件: 程序。

  3. 操作系统: 管理硬件软件的 ”软件“。

所以,操作系统是软件和硬件之间的桥梁,是介于这两者之间的中间层。它完成了对计算机硬件系统的抽象,为应用程序提供运行所需要的资源。操作系统是一个非常庞大的体系,所以从不同的视角来看,可以观察出它不同的一面:

  • 从应用程序的视角来看,操作系统定义了一系列对象(进程/线程、地址空间、文件系统、设备…)和操纵它们的API(系统调用)。这组强大的API把一台计算机的硬件资源共享给操作系统中的所有应用程序。我们看到的一切程序,不限于浏览器、游戏、容器虚拟机、调试器以及游戏外挂等程序,都是在系统调用上实现的。
  • 从硬件的视角来看,操作系统是一个拥有可以访问全部硬件能力的程序(操作系统就是个C程序)。硬件会帮助操作系统完成最初的初始化和加载,之后操作系统加载完第一个程序后,此后为作为**“中断处理程序”**在后台管理整个计算机系统。
  • 从数学的视角来看,一切计算机系统都是如同 “1 + 1 = 2” 一样 “well-defined” 的数学对象,这包括机器、程序,当然也包括操作系统。计算机系统的行为是可以用数学函数 (当然,也可以用代码) 表示的。

为什么学习操作系统?

操作系统(Operating system)是计算机体系的命脉之一。作为一名程序开发者而言,我们所编写的所有源程序最终都是要运行在操作系统之上,想要将自己所写的程序每一条脉络都掌控自如,那么必须要对操作系统有很深的了解和掌握。


操作系统发展历史

如果想要对一个事物展开深入的学习和理解,那么它从无到有的发展历程将是你必经之路。随着科技的发展,所有的新事物绝不是凭空而来:一个事物的产生,一定源自于一个目的,而目的则是为了解决各种各样的问题


计算机前生今世

我们把时间拨回到19世纪中后期,在工业化淘金热的推动下,美国称为了移民者向往的目的地,使得美国当时的人口急剧增长。到了1890年,根据美国当时宪法的规定,每隔10年要进行一次人口大普查,来记录和分析美国各地的人口数据。但是,先前1880年的人口普查用了将近7年的时间,但是面临着爆炸式增长人口的趋势,如果纯靠人力来统计的话,预计这次的普查需要13年。纳尼?每隔10年统计一次,每次统计要花10年时间以上…

所以,为了提高普查效率,美国人口普查局开始向全社会招标:寻求一种发明,能够解决这个问题。恰逢此时,发明了打孔卡制表机赫尔曼·赫尔茨带着他的一身专利从这些挑选的方案中脱颖而出。他借鉴了巴贝奇的发明,设计了一种使用穿孔卡片算出数据的机器,仅仅使用了6周的时间,就得出了准确的数据。他的这项发明为计算机发展史留下了浓墨重彩的一笔,同时也会现代计算机的诞生奠定了基础。

1946年,世界上第一台现代电子计算机 **ENIAC(Electronic Numerical Integrator And Computer)**诞生在美国宾夕法尼亚大学。

所以在当时最早期的程序员,编写"程序"时,需要手工将要运算的信息打孔在卡纸上,再将打好的卡纸放入卡纸阅读机中。非但如此,在当时计算机是非常稀有的东西,普及程度低,只有在军事或学术研究方面的领域才能接触得到。


操作系统的诞生

正如上所说,在那个连编程语言都未诞生的年代,程序员们都是通过对纸带打孔的方式来表示出要计算的数据,对于当时的计算机而言,这一张张被打好孔的纸带就是一条条指令,而这些指令还需要人工投放到阅读机中才行。

就算是当时的顶流大学也可能只有那么一台计算机,那么问题就来了:数学系要用,物理系和化学系也要用,所以只能按部就班的排队,然后等待运算结果。而且每次计算完后,是依靠人工操作来继续投放其他纸带,所以整体的运算速度受限于人工操作,机器的利用率并不充分。

后来,随着制造工艺以及电子技术的发展,计算机开始由机械向电子方向转变。由于是借助于电子硬件进行工作,因此计算机主体需要通过成千上万根电缆接到插线板上连上电路来控制机器的基本功能。在这一阶段的程序员需要人工扳动多个开关,将运算的数据输入到计算机中,整个过程既繁琐又麻烦。

时间来到1950年,随着晶体管和集成电路时代的到来,计算机的体积开始缩小,功能也逐渐完善,存储能力越来越强,其运算速度也越来越快。人工操作与计算机之间的速度差距开始慢慢扩大,如果继续依赖于人工操作计算机的运算调度,那么无法充分发挥计算机的功能,唯一的解决办法就是取代人工操作,让计算机自动实现相关的任务的自动处理。

在此期间,出生于匈牙利的美国籍犹太数学家、理论计算机科学与博弈论奠基者约翰·冯诺依曼提出了计算机的基本原理:存储程序程序控制。并将计算机从逻辑结构划分为:运算器控制器存储器输入设备以及输出设备五大部件。这一理论为计算机科学的发展奠定了坚实的基础,也为后续的操作系统的萌芽打上了灵魂的烙印。

为了能让计算机能够自动成批地处理一个或多个用户的作业,人们便想到了是不是可以在计算机中加载一个系统监督程序,在它的控制下让计算机能够自动成批的处理一个或多个用户的作业,因此单道批处理系统应运而生。操作系统的概念也由此萌芽。

随着单道批处理系统的广泛应用,人们逐渐发现了新的问题:在单道批处理系统中,用户需要将预处理的数据信息通过卡纸输入机存储到磁带,然后由控制主机读取磁带,进行运算,运算完毕后由输出机将结果保存到磁带并进行输出。然后继续处理下一批作业数据。然而在这个过程中,输入输出(IO)操作需要花费大量的时间,而主机在这段时间处于空闲状态,会造成资源的浪费。所以为了提升主机在空闲状态下的利用率,系统监督程序得到了进一步的进化,开始允许多批数据同时进入主机中运算,当计算机在进行输入输出操作导致主机处于空闲状态时,监督程序会调度另一个批数据开始运行,从而形成了多道批处理系统


混沌初开-Unix

1960年,麻省理工学院开发了兼容分时系统,这种系统可以让多个终端机连接到一台大型主机中进行联机运算。这些终端机仅仅具备输入输出功能,而具体的运算能力则集中在主机上,计算完的结果会从主机响应并传输到请求的终端机上。这样一来,一台主机就可以同时为多个用户同时使用。然而在当时的技术条件下,较为先进的主机也只能带动约30个终端机。

1965年,为了进一步提升大型主机能够带动更多的终端机,由贝尔实验室麻省理工学院以及通用电气公司共同发起了 Multics计划:旨在创建一个高效、可靠、易于扩展的多用户分时系统。但是该项目在1969年由于资金短缺而宣告失败。

在原本参与 Multics计划的人员中,贝尔实验室的一位研究员肯·汤普逊希望开发一个小小的操作系统,满足自己可以重新运行原本编写在 Multics 上的星际旅行游戏的需求。于是在1969年8月份,趁着自己妻子休假回娘家探亲时,在一个月黑风高的夜晚,他在实验室内找到一台主机,在一个月内采用汇编语言写出了一个简化版的 Multics,并且在上面成功运行了他的星际旅行游戏,不仅如此,他在上面还编写了一些核心工具,例如进程设备文件命令行解释器文件系统等。

后来,由于汤普逊写的这个系统非常好用,开始在贝尔实验室内广为流传,由于是以 Multics 为基础将其简化的,所以他的同事们称这个系统为 Unics,这就是Unix的原型。

1970年,汤普逊越来越嫌弃汇编语言与硬件平台的关联紧密的特性,每次硬件变更都要重新修改代码,无法满足他的跨平台特性的需求。于是他以BCPL语言为基础,设计相对简单的 B 语言,并且用 B 语言编写出了第一个Unix系统

补充: 肯·汤普逊(Ken Thompson)不仅是Unix系统和B语言的创始人,同时还是UTF8、正则表达式、Go语言的设计者之一

1973年,由于 B 语言依然存在系统移植与性能原因,还是没有办法兼容不同架构设计的处理器。汤普逊的同事丹尼斯·里奇开始将 B 语言进行改造,以 B 语言为基础重新设计了一门语言,并且在命名时为了致敬先行者,又是以 B 语言为基础,所以取了 BCPL 的下一个字母,于是大名鼎鼎的 C 语言就此诞生。

后来两人用C语言重新编写了 Unix 系统并发行了 Unix 第一个正式版本。由于这一重要贡献,两人于1983年获得图灵奖。当时的贝尔实验室隶属于 AT&T(美国电话电报公司) ,而当时的 AT&T 签订了不进入计算机行业的的协议,所以对此时的Unix采取较为开放的态度。而且此时的 Unix 是完全开源的,以及C语言本身是具有可移植性的,也就是说只要取得了 Unix 的源码,就可以移植到任何一台主机上。

后来,AT&T又意识到了 Unix 的商业价值,于是又收回了 Unix 的版权,这一举措也引爆了很多商业纠纷。到了1984年,AT&T的限制法令被解除,并且察觉到了 操作系统的商机,于是将 Unix 从研究性质的项目转变为了商业项目。


开天辟地-Linux

林纳斯·托瓦兹(Linus Torvalds) 的外祖父是赫尔辛基大学的统计学家,为了让自己的小孙子能够多学点东西,从小就将托瓦兹带到身边来管理一些微计算机,在这个时期,他开始接触汇编语言。到了1988年,托瓦兹顺利进入到了赫尔辛基大学,并且选读计算机专业。在就学期间,他接触了 Unix 操作系统,当时赫尔辛基大学只有一台主机安装了 Unix 系统,而且仅提供16个终端机供用户使用。再后来 Unix 又因版权问题陷入风波,随后他又发现了一个 简化版的 Unix 系统,也就是 Minix 系统,但是这个系统仅允许用于教学使用,而不允许用商业且后续不再更新。

于是他贷款买了一部英特尔386计算机, 并且参考 Minix 设计理念和书上的程序代码,使用 GUN计划提供的 bash工作环境以及GCC编译器等自由软件,编写出了一个核心程序,这个程序与386计算机紧紧结合在一起,也就是**Linux kernel(Linux 内核)**程序。他希望这个程序可以获得一些修改建议,于是他便将这个程序放在网络上,供大家下载查看。

这则消息一经发出,便很快引起了当时计算机爱好者的注意,纷纷取托瓦兹提供的网站上下载这个程序。由于托瓦兹放置这个程序的FTP网站的目录名叫做 Linux,所以大家自此之后便称这个核心为 Linux

万物衍生- DOS


时间的齿轮再次转动,公元1971年,此时小型计算机刚刚开始流行。英特尔公司发布了世界上第一款商用微处理器 - 英特尔4004,它的发布催生了微型计算机的诞生。

1973年,英特尔发布了第二款微处理器 - 英特尔8008,和第三款微处理器 - 8080。当时担任英特尔顾问的美国海军计算机教师,计算机博士加里·基尔代尔开发出了世界上第一个面向微处理器的高级编程语言:PL/M。随后他又创作出了世界上第一个微型计算机操作系统:CP/M。但是当时英特尔只想推广PL/M编程语言,并不看好CP/M操作系统。因为那时Unix系统已经是程序员眼中的无冕之王。

1980年,IBM公司敏锐的嗅到了个人计算机的商业发展前景,于是由唐·埃斯特里奇负责领导建立了以国际象棋为代号的项目,目标是开发一款面向个人微型计算机的操作系统。于是他们找到了当时担任全美联合劝募协会执行理事会主席玛丽·麦克斯韦尔·盖茨儿子比尔·盖茨的公司:微软(Microsoft),因为微软此前推出过一款硬件产品,名为Z-80 SoftCard,它可以让CP/M操作系统在 apple ll上运行。但是当时的比尔盖茨并不看好操作系统,他认为编程语言才是未来的摇钱树,所以他向IBM公司提出建议,希望能够使用他们开发的Basic语言来编写程序。加上当时 CP/M 系统已经称为了市场的标准,于是他便向IBM推荐了基尔代尔。但是最终还是未能达成合作。

没有办法的IBM只能再次回头找上微软,这次比尔盖茨找到了一家叫做西雅图计算机的公司。当时这家公司购买了 CP/M 操作系统,但是因为软件交货延期影响销售。于是这家公司有一个叫做蒂姆·帕特森的24岁程序员,他花了4个月的时间自己开发出了QDOS(Quick and Dirty Operating System)操作系统,后来改名为86-DOS(磁盘操作系统)。但其本质还是抄袭CP/M操作系统。于是比尔盖茨买下了这款操作系统的授权,甚至将帕特森一并挖到微软,直接照单全收。随后微软对86-DOS系统一顿魔改后,将其改名为IBM PC DOS交给了IBM。由于在当时软件版权的法律并不健全,基尔代尔眼睁睁看着自己的成果被窃取,气的直骂街,但却无可奈何。最终IBM选择了英特尔的微处理器,微软的 IBM PC DOS 解决方案,产品一经面世后在短时间内迅速占领了市场。

由于 IBM PC DOS 是以买断形式一次性卖给 IBM 的,于是微软为了兼容市场将 IBM PC DOS 进行了删改,于1981年重新命名为 MS-DOS(Microsoft Disk Operating System)微软磁盘操作系统 发布。随后不断更新迭代,推出了 MSDOS2.0/3.0/4.0/… 一直到千禧年的 Ms DOS8.0 版本。

上帝之眼-Windows

时间跳动到1968年,道格拉斯·恩格尔巴特在国际斯坦福研究所开发了OLS在线系统。并在 ACM/IEEE 会议上对整个计算机软硬件系统进行了展示,其中展示了包括:窗口超文本图形导航命令窗口视频会议协同办公版本控制等功能,并首次演示了滑动鼠标作为交互界面的媒介。这一演示也被后人们称为所有演示之母。

美国施乐公司创建于1906年,发明了复印技术,是一家具有悠久历史的公司。施乐公司在1970年成立了为数字领域创想提供成长环境的帕洛阿尔托研究中心。1973年,施乐的工程师们基于 Unix 系统发明了世界上第一台可以运行图形化界面的操作系统的微型电脑和可以点击操作的鼠标,这台电脑的名字叫做阿尔托。发明这台电脑的目的是将桌面图形的概念应用到屏幕上,可以通过屏幕以及鼠标点击来操作系统中的文件,从而替代掉那些复杂的命令行和DOS提示符。但是当时施乐公司的高层又担心这个系统太过于简单好用,一旦售卖出去,那谁还来买他们的打印机?(你可真是个小机灵鬼啊)… 于是最终还是决定弃之不用。不过为了向世人展示他们不是做不了而是不想做,他们将这项技术进行了演示。

在1979年12月,史蒂夫·乔布斯参观了施乐PARC的技术成果:图形界面和位图显示屏幕。当时的乔布斯脑闻着味就发现了商机,这可是我自己凭本事看到的,我是无辜的!这件事后来被称为工业史最严重的抢劫行为之一。后来苹果工程师参考施乐的设计灵感,开发出了体验感更好的界面系统,该系统可以任意拖拽窗口和文件、调整窗口大小、添加漂亮的图标、双击鼠标可以打开文件夹或文件等功能,将桌面的虚拟概念转化为现实。

在得知这个消息后,不仅施乐公司人麻了,比尔盖茨人也麻了,有了这样的图形化界面谁tm还买我的 Ms DOS 系统啊?好好好,这么玩是吧?于是比尔盖茨秘密研究了,并且讲究出了新的操作系统,名为Windows

深入理解操作系统

程序眼中的操作系统

操作系统是连接软件硬件的桥梁。因此想要理解操作系统,首先要对操作系统的**服务对象(应用程序)**有更清晰和深刻的理解。

什么是"程序"

**程序(Program)**就是由一系列的指令组成的集合,这些指令会被计算机处理器正常执行处理。程序可以用各种编程语言编写,并且在计算机中运行时,通常由操作系统进行管理和调度。

例如一个简单的Hello world,同样也是一个程序:

// hello.c

int main(){
  printf("Hello world!\n");
}

通过GCC编译器来编译它,并且运行:

$ gcc hello.c
$ ./a.out
Hello world!

这就是一个最简单的程序

如果你想要GCC啰嗦一点,可以通过--verbose参数开启:

$ gcc --verbose hello.c

Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/opt/rh/devtoolset-8/root/usr/libexec/gcc/x86_64-redhat-linux/8/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,fortran,lto --prefix=/opt/rh/devtoolset-8/root/usr --mandir=/opt/rh/devtoolset-8/root/usr/share/man --infodir=/opt/rh/devtoolset-8/root/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-
....
Thread model: posix
gcc version 8.3.1 20190311 (Red Hat 8.3.1-3) (GCC)
....

GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/include-fixed"
ignoring nonexistent directory "/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/../../../../x86_64-redhat-linux/include"
#include "..." search starts here:
#include <...> search starts here:
 /opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/include
 /usr/local/include
 /opt/rh/devtoolset-8/root/usr/include
 /usr/include
End of search list.
GNU C17 (GCC) version 8.3.1 20190311 (Red Hat 8.3.1-3) (x86_64-redhat-linux)
        compiled by GNU C version 8.3.1 20190311 (Red Hat 8.3.1-3), GMP version 6.0.0, MPFR version 3.1.1, MPC version 1.0.1, isl version isl-0.16.1-GMP

GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 44fd30fd0fdcf2e8887a67e69c89caa4
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64'
 /opt/rh/devtoolset-8/root/usr/libexec/gcc/x86_64-redhat-linux/8/as -v --64 -o /tmp/cczL9hPJ.o /tmp/ccDYGX0W.s
GNU assembler version 2.30 (x86_64-redhat-linux) using BFD version version 2.30-55.el7.2
COMPILER_PATH=/opt/rh/devtoolset-8/root/usr/libexec/gcc/x86_64-redhat-linux/8/:/opt/rh/devtoolset-8/root/usr/libexec/gcc/x86_64-redhat-linux/8/:/opt/rh/devtoolset-8/root/usr/libexec/gcc/x86_64-redhat-linux/:/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/:/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/:/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64'
 /opt/rh/devtoolset-8/root/usr/libexec/gcc/x86_64-redhat-linux/8/collect2 -plugin /opt/rh/devtoolset-8/root/usr/libexec/gcc/x86_64-redhat-linux/8/liblto_plugin.so -plugin-opt=/opt/rh/devtoolset-8/root/usr/libexec/gcc/x86_64-redhat-linux/8/lto-wrapper -plugin-opt=-fresolution=/tmp/ccLaBJEw.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --no-add-needed --eh-frame-hdr --hash-style=gnu -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 /lib/../lib64/crt1.o /lib/../lib64/crti.o /opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/crtbegin.o -L/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8 -L/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/../../.. /tmp/cczL9hPJ.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/crtend.o /lib/../lib64/crtn.o
COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64'

编译器输出了非常多的信息,我们可以挑出一些有用的信息来看:

// 编译平台架构
Target: x86_64-redhat-linux 

// 使用 "" 引入的头文件检索目录
#include "..." search starts here:
  
// 使用 <> 引入的头文件检索目录
#include <...> search starts here:
 /opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/include
 /usr/local/include
 /opt/rh/devtoolset-8/root/usr/include
 /usr/include
End of search list.

然后可以通过 file 命令查看生成的a.out文件信息:

$ file a.out

a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=670d3892b76c526ccfe58a9ee971259f353a9abf, not stripped

通过输出的信息,我们可以得知这是一个ELF格式的executable(可执行程序)

当一个文件是一个二进制可执行程序时,可以通过objdump命令来查看它对应的汇编代码:

$ objdump -d a.out | less

a.out:     file format elf64-x86-64

Disassembly of section .init:

00000000004003e0 <_init>:
  4003e0:       48 83 ec 08             sub    $0x8,%rsp
  4003e4:       48 8b 05 0d 0c 20 00    mov    0x200c0d(%rip),%rax        # 600ff8 <__gmon_start__>
  4003eb:       48 85 c0                test   %rax,%rax
  4003ee:       74 05                   je     4003f5 <_init+0x15>
  4003f0:       e8 3b 00 00 00          callq  400430 <__gmon_start__@plt>
  4003f5:       48 83 c4 08             add    $0x8,%rsp
  4003f9:       c3                      retq
...
00000000004005a0 <__libc_csu_fini>:
  4005a0:       f3 c3                   repz retq

Disassembly of section .fini:

00000000004005a4 <_fini>:
  4005a4:       48 83 ec 08             sub    $0x8,%rsp
  4005a8:       48 83 c4 08             add    $0x8,%rsp
  4005ac:       c3                      retq

打开一看,两眼一黑….

明明我们只是写了一个最基础的Hello world程序,居然这么编译出了这么多指令,这一点都不Minimal。其实原因也很简单,对C语言有过基础了解的小伙伴都知道:

C语言的编译流程分为四个小阶段:预处理编译汇编以及链接。最终将我们写的C语言代码和系统库的C语言代码链接到一起,形成一个的可执行文件,所以这个文件才会如此庞大。

通过stat命令可以查看到,该文件的大小在8.1kb左右.

$ stat a.out
  File: ‘a.out’
  Size: 8232            Blocks: 24         IO Block: 4096   regular file
Device: fd01h/64769d    Inode: 2106734     Links: 1
Access: (0755/-rwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)

如果想只看我们自己的代码对应的汇编指令,可以只对我们的hello.c文件进行编译汇编

$ gcc -c hello.c

然后会生成一个名为hello.o目标文件,也就是没有链接为可执行文件前的二进制目标文件。同样可以使用objdump命令来查看它的内容:

$ objdump -d hello.o

hello.o:     file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
   0:   55                      push   %rbp
   1:   48 89 e5                mov    %rsp,%rbp
   4:   bf 00 00 00 00          mov    $0x0,%edi
   9:   e8 00 00 00 00          callq  e <main+0xe>
   e:   b8 00 00 00 00          mov    $0x0,%eax
  13:   5d                      pop    %rbp
  14:   c3                      retq

诶,现在你会看到,内容确实少了许多,如果你有一定的汇编基础,那么不难看出,其实这些就是我们在hello.c文件中的内容所对应的汇编指令。

如果现在我们对hello.o文件进行单独链接

$ ld hello.o
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000b0
hello.o: In function `main':
hello.c:(.text+0xa): undefined reference to `puts'

哦豁,链接错误,原因为:找不到一个名为puts的函数,因为在printf()函数中,其实调用的就是puts函数,但由于我们单独对这个目标文件进行链接,那么自然找不到这个函数,这是C语言的基础,所以在这里就不多说了。

那么如何可以使得它可以链接成功呢?我们只需要改动一下hello.c源文件内容:

// hello.c
int main(){
}

我们将输出Hello world!语句移除掉,这样这个程序则不会调用任何标准库函数,因此也不需要和任何文件进行链接,再尝试编译和链接:

$ gcc -c hello.c && ld hello.o
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000b0

虽然输出了一句warnning日志,但是确实编译链接都成功了,那么这个程序是否可以正常执行呢?

$ ./a.out
[1]    20470 segmentation fault  ./a.out

执行失败, SIGSEGV(段错误) 对您发出了亲切的问候,这又是为何呢?

因为程序本质上是不能自己停止的,指令集中没有任何一条指令是用于关闭程序的,它需要借助于操作系统来完成优雅停止。

mov $SYS_exit, %eax  # exit( 
mov $1, %edi 				 # status = 1
syscall							 # );

执行syscall指令,操作系统接管该程序,执行exit系统调用,从而结束程序。

Minimal Program

那么如何构建出一个真正意义上的 Minimal Program呢?我们可以通过汇编来编写一个这样的程序。注意,这里使用的是X86-64汇编指令集,并且使用的是AT&T风格。

# minimal.S

#include <sys/syscall.h>
.globl _start
_start:
        mov $SYS_write, %rax 			# 设置要执行 write 系统调用 
        mov $1, %rdi							# 设置写入的文件描述符
        lea addr1(%rip), %rsi			# 设置要输入的 addr1 字符串起始地址
        mov $addr2 - addr1, %rdx	# 设置 addr 的地址结束位置
        syscall										# 执行系统调用
				
				# 执行 exit 系统调用
        mov $SYS_exit, %rax				# 设置要执行 exit 系统调用
        mov $0, %rdi							# 设置 exit 状态码
        syscall										# 执行
addr1:
        .ascii "\033[01;31mHello, minimal program.\033[0m\n"
addr2:

然后对该源文件进行编译汇编链接以及执行:

$ gcc -g -S minimal.S > minimal.s
$ as minimal.s -o minimal.o
$ ld -o minimal minimal.o

$ ./minimal
Hello, minimal program.
$ stat minimal
  File: ‘minimal’
  Size: 896             Blocks: 8          IO Block: 4096   regular file

可以看到,生成的可执行程序大小仅仅只有800字节左右,比C语言编写的程序要小太多太多了。这样我们就得到了一个真正可以正常运行以及正常结束的 Minimal Program


如果你想在ARM架构下也实现一个 Minimal Program,那么可以使用如下代码:

/* 数据段 */
.data
		msg: .ascii  "\033[01;31mHello, minimal program.\033[0m\n"
		len = . - msg

/* 代码段 */
.text

/* 定义入口*/
.globl _start
_start:
    /* 执行 write 系统调用 */
    /* write(int fd, const void *buf, size_t count) */
    mov     x0, #1      /* fd := STDOUT_FILENO */
    ldr     x1, =msg    /* buf := msg */
    ldr     x2, =len    /* count := len */
    mov     w8, #64     /* write is syscall #64 */
    svc     #0          /* invoke syscall */
		
		/* 执行 exit 系统调用 */
    /* syscall exit(int status) */
    mov     x0, #0      /* status := 0 */
    mov     w8, #93     /* exit is syscall #93 */
    svc     #0          /* invoke syscall */

然后进行汇编链接和运行:

$ as -o minimal.o minimal.S
$ ld -s -o minimal minimal.o
$ ./minimal
Hello, minimal program.

strace

在这里,为了能够更加了解我们所编写好的程序,在运行过程中究竟做了什么事情,我们可以通过strace命令一探究竟。strace是一个在Unix和类Unix系统上常用的命令行工具,用于跟踪和记录进程的系统调用以及接收和发送的信号。它可以用于分析和调试应用程序的行为,特别是当你需要了解应用程序与操作系统之间的交互时。

以上面构成的 Minimal program为例:

$ strace ./minimal
execve("./minimal", ["./minimal"], 0x7ffcfd4199a0 /* 41 vars */) = 0
write(1, "\33[01;31mHello, minimal program.\33"..., 36Hello, minimal program.) = 36
exit(0)                                 = ?

可以看到,我们的程序分别执行了execvewriteexit等系统调用。

通过这个命令可以检测任何程序与操作系统的交互过程,比如通过GCC编译一个C语言源文件的过程:

$strace -f gcc -o hello hello.c
8802  execve("/opt/rh/devtoolset-8/root/usr/bin/gcc", ["gcc", "-o", "hello", "hello.c"], 0x7fff5838c1a0 /* 41 vars */) = 0
8802  brk(NULL)                         = 0xd73000
8802  mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f831c02e000
8802  access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
8802  open("/opt/rh/devtoolset-8/root/usr/lib64/tls/x86_64/libm.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOE
NT (No such file or directory)
8802  stat("/opt/rh/devtoolset-8/root/usr/lib64/tls/x86_64", 0x7ffeccdd3110) = -1 ENOENT (No such fi
le or directory)
8802  open("/opt/rh/devtoolset-8/root/usr/lib64/tls/libm.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No
such file or directory)
....

从这里我们就可以得出一些结论:我们的应用程序无时无刻都在依赖于操作系统。哪怕只是输出一行字符,也会涉及到操作系统的IO处理,如果不对操作系统有一定的了解,其程序的运行效率就没有办法实现到极致。


硬件眼中的操作系统

多处理器编程