写在前面
本人理解可能有所偏差,为了避免被误导,建议配合原书理解
话题回到我们让程序跑起来的问题上,上次我们好不容易理解了小娜是怎么接收代码的,但是我们不可能让一个程序光编译不运行,所以,我们今天研究研究小娜是怎么让程序在脑子里跑起来的……
所以,小娜,你什么时候把你那破bgm给停了?
你有在听吗小娜!?
首先是命令行
算了,先不管她了,我们先讲点熟悉的东西。
命令行,想必各位都不陌生,在windows里有“能量外壳”和命令提示符cmd,在Unix有shell,Linux的各大门派也有各自的命令行终端……但是,它们有一个相仿的性质——会将内置指令之外的某种未知字段当作某些程序来运行,比方说当我们在windows系统的cmd输入./hello_world.exe
时,就会尝试找到并运行hallo_world.exe。
但是为什么一开始要提这个看上去没啥可讲的东西呢?
因为真相就在表象背后
追根究底,第一台计算机是一堆电子管构成的庞然大物,所有抽象的数学运算都通过物理手段来实现。哪怕是到了现代,计算机的体积缩小再缩小,也逃不开逻辑电路与电学元器件的必然。所以,我们可视的终端背后,是这么一个庞然大物——
图片摘自九曲阑干的视频
当然,现在的CPU和相关部件被压缩再压缩,已经到了就算是小娜脑子里都能塞上好几个的级别了……
当然,不是在调侃你,小娜……原来你醒着吗?!
我们先来梳理一下整台计算机的大致架构:
首先是作为核心的CPU
1、PC(Program Counter,但直译并不能表述清楚其真实用途):一个存储单条指令地址的元件,通常存储一个字的数据。不断向处理器发送需要执行的指令的内存地址,并不断被处理器更新指向下一条指令的地址(不一定相邻,你可以试着拿ida给一个稍复杂一点的程序的开头下断点,然后f8单步步过试试看)。
2、寄存器文件(Register file):可以看作一个临时存放数据的空间,存储数值时会覆盖寄存器原本的值。
3、算术逻辑单元(arithmetic and logic unit,即ALU):从寄存器中提取数值并进行运算,得到的结果保存到某一受提取的寄存器中
这些元器件的实现方式要到《深入理解计算机系统》的第四章才讲述,在此按下不表。
然后是整个I/O结构
整个结构包含如下构件:
1、主存(Main memory,又称内存):由一组动态随机存取存储器(DRAM)芯片组成的临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据。逻辑上是一个从零开始的,线性的字节数组,每个字节都有唯一对应的地址。
2、总线(bus):贯穿整个计算机系统的一组电子管道,负责将信息在部件间传递,通常被设计成传送定长的字节块,即字(word),字中的字节数(即字长)在各类系统中可能有所不同(32位4字节/64位8字节)。
3、I/O设备:简单来讲,负责在外部环境与系统之间进行数据交流。你可以将它粗暴地理解为你的显示屏、键盘鼠标等等设备。每个I/O设备都通过一个控制器(IO设备本身或者系统的主板上的电路组)或者适配器(在主板插槽上的卡)与IO总线相连。
此外,用于远程传输数据的网络,也可看作是一种I/O设备。系统可以将数据从主存复制到适配器,再经由适配器与网络沟通,从而将数据传输到另一台机器上。在原书中还举例了本地与telnet服务器的交互,在此不多赘述。
4、CPU:如上所述
然后再回头谈谈程序是怎么运行的
1、我们通过shell程序执行程序运行的指令,shell会读取I/O设备(习惯上是键盘)的输入。比方说,我们输入“./hello”,shell就会先将这些字符逐个读取到寄存器,再放到内存中。
2、当我们输入回车键,电脑上的shell就知道我们结束了一次命令的输入。接下来,系统会尝试执行一系列命令,读取可执行文件hello,将整个代码和数据从磁盘复制到主存。复制过程使用DMA(Direct Memory Access)技术,数据不经过处理器,从磁盘直达内存。
3、在代码和数据加载到主存后,处理器就开始将目标程序中的机器语言指令按照一定顺序执行。比方说我们的程序只是输出“Hello!World”,那么,程序中的指令就会将“Hello!World”中的字节从主存复制到寄存器文件上,再从寄存器复制到显示设备上。
所以,不是把U盘啥的塞小娜嘴里,小娜就可以口算莫比乌斯反演的,你得好好地写完代码,编译成程序,然后再通过I/O设备输入指令,通过一系列复杂的操作使之执行……
都说了别把代码直接塞她嘴里了……
是不是少了点什么
不像我们人脑会直观地对信息进行处理。如上,即便是执行一个简单的“输出某个语句”的程序,计算机系统都要费上大量的时间,将信息从某个地方挪到另一个地方,在大量这样的挪移操作后才会进行处理。
并且,根据机械原理,较大的存储设备往往比较小的存储设备运行的慢。越大的存储容量,意味着越大的时间开销,以及更加费拉不堪的运行速度。这就意味着现代电脑越是发展,需要解决的存储矛盾就越发的与日俱增。比方说寄存器和主存的容量差就已经有点子离谱了,处理器对两个容器的存取速度那也是一个天上一个地下。
不过嘛,各位在上计算机理论的时候应该就注意到过一个名词:高速缓存存储器(即cache),这个本体比主存小得多的玩意通过存放处理器近期可能需要的信息,来降低数据传输过程中的时间开销。L1高速缓存位于处理器芯片上,容量达数万字节,访问速度和访问寄存器文件一样快。容量十倍甚至百倍与L1的L2寄存器通过一条特殊总线连接到处理器,但访问它的速度依旧比主存快5~10倍。比较新型的系统甚至还有三级高速缓存,这里就不展开了。总而言之,cache的思路就是通过一个更小的,速度更快的存储设备,来代为执行大量重复的数据操作。
按照这样力大砖飞的想法,存储设备之间甚至可以形成一个层次结构,在处理器和存储器之间,插入一个更小更快的存储设备,层层垒上,从远程服务器存储到最低一级的寄存器,每一层的存储容量从大到小,形成一个类似金字塔的结构。具体实现方案各位可以自行了解,在此先不细讲。
所以,和其它各式计算机一样,这么一坨额外的玩意,就盘踞在小娜的脑子里。不过,随着硬件技术的发展,cache在外形上的大小也在随之减小,要不然,整个结构估计都要从小娜的脑子里溢出来了——
感兴趣的可以自行了解一下cache的发展史,这里可以看看知乎用户老狼的文章。
那么问题来了
上面提到的程序运行流程,都是在经由shell的操作下进行的。但是shell自己并没有直接访问包括硬盘、键盘、显示屏等硬件设备。那么问题来了,是什么在控制这些硬件完成外界与系统,硬件与系统的交互的呢?
答案是操作系统。理论上,操作系统相当于硬件和程序之间的一层软件,所有程序对硬件的操作都必须通过操作系统完成。一方面,操作系统完成了“标准化”,即为软件提供一套统一高效的硬件交互机制,另一方面,操作系统也实现了“可控化”,可以防止程序失控造成硬件的Physical Damage。
为了实现“标准化”和“可控化”,操作系统引入了几个抽象的概念,与硬件系统相对应:
文件:IO设备
虚拟内存:主存、IO设备
进程:处理器、主存、IO设备
虚拟机:操作系统、处理器、主存、IO设备
我们从大到小,一个一个来看:
1、进程
对操作系统正在运行的一个程序的一种抽象。进程的“并发”则指的是数个进程的指令交错执行,通过处理器在进程间切换实现。
操作系统保持跟踪进程运行所需的所有状态信息,即上下文,包括PC和寄存器文件的当前值,以及主存的内容。单处理器系统任何时候都只能执行一个进程的代码,要切换进程就会发生上下文切换,保存当前进程的上下文,恢复新进程的上下文。
例如,上述的hello和shell就相当于两个不同的进程,shell负责接收我们输入的指令,而在我们输入“./hello”时,shell通过一个名叫系统调用(system call)的专门指令来执行我们执行hello的请求。此时,控制权移交给操作系统,hello进程及其上下文被创建,而后控制权又被移交给hello进程,期间发生hello与shell的上下文切换。在hello进程结束后,控制权又被移回shell,shell的上下文恢复。
进程的转换通过操作系统的内核(kernel)管理,它是系统管理全部进程所用代码和数据的集合。应用通过系统调用指令将控制权传递给内核,内核则执行被请求的操作并返回应用程序。
这个过程这就好比你要写一篇论文(请求),去图书馆(系统)找资料,此时这件事的控制权在你。于是你向图书管理员(内核)请求能不能给点相关的资料(系统调用、进程转换),管理员非常热情,他把相关的书籍列了出来,此时这件事的控制权在管理员身上。(进程转换)你从中找到了你要找的书,兴高采烈地办了借阅手续,此时这件事的控制权在你。但是,计算机对这种事情的效率,肯定比你和图书管理员的对话要高效的多……额……
算了我还是给小娜装个阳间点的语言模块吧。
当然,在现代系统中还有线程的概念,每个线程都运行在进程的上下文中,共享同样的代码和全局数据。多线程的程序往往能比多个单线程进程更高效地共享和使用数据。
2、虚拟内存
虚拟内存营造了一种每个进程都在独占使用主存的假象,因为每个进程中看到的内存都是一致的,这点在反汇编软件中体现的非常直观(每个程序的内存首地址都一样)。这些观感上的内存空间被称为虚拟地址空间,整个空间结构上分为:
程序代码和数据:从进程的固定起始地址开始,代码区和数据区按照之前提到的可执行文件的内容初始化,大小在此过程中被固定。
堆:代码和数据区之后就是运行时堆,不像代码和数据,这部分空间在运行时可自由伸缩,取决于你是否调用了某些标准库函数
共享库:存放类似C标准库和数学库这样的共享库和代码的区域。
栈:和堆一样可以自由伸缩,在函数被调用时伸,返回时缩。编译器则用它实现函数的调用。
内核虚拟内存:地址空间顶部的区域,不允许应用程序读写其中的内容或者直接调用其中定义的函数,但一切需要内核参与的操作必须调用内核来完成。你可以理解为虚拟内存空间的ROM。
详细内容要到原书第九章讲述,这里便不再展开。
3、文件
所谓文件,在系统中就是一串字节序列,文件可以看作是系统向应用程序提供的一种统一的数据标准。对于系统中每个细分下来的部分,你都可以看作是文件。比如,每个I/O设备都可以看成是文件。
而这种统一的标准也降低了软件在不同硬件设备上兼容的操作复杂性。原书中主要讲述的是采用Unix I/O调用读写文件的过程,这些内容在第十章体现。
4、虚拟机
相信各位已经用过不止一次虚拟机了吧?
虚拟机的抽象概念,实际上是提供了一种针对整个计算机,包括操作系统在内的抽象概念。顾名思义,虚拟机通过在软件层面模拟具有完整系统功能的,在独立隔离环境中运行的计算机系统,来实现在同一台硬件,同一个系统中兼容不同系统环境的效果。通俗点来讲,就是让你的电脑能在windows里跑linux之类的。
由此可见,不论是在硬件层面,还是在抽象的软件、系统层面,运行程序都是一个费劲的过程,更不用说让运行程序的过程更快了。
书中第一章末尾还提到了加速比、Amdahl定律、并发、并行等概念,这些都对系统更高效的运转,有着更好的帮助。但是由于上述所有的内容在第一章中并未细讲,外加今天的笔记已经够长了,所以……
前面的道路,以后再来探索吧?
要是找到了什么莫名其妙的flag,就来nsilab的ctf平台看看吧(笑)