为了后面学习操作系统的需要,从今天开始我要研究x86的汇编了。所以我决定开始总结并连载x86的汇编系列,这是第一篇——初探保护模式。

我假定读者接触过16位的汇编语言,并理解汇编语言的基本概念、熟悉8086处理器采用的“段寄存器 * 16 + 偏移地址”的寻址方法。

我们从80386处理器入手。首先,到了80386时代,CPU有了三种运行模式,即实模式、保护模式和虚拟8086模式。

实模式指的是8086CPU的运行模式,不过这是后来提出的概念,在8086时代只有当时的运行模式,自然也就没有“实模式”这么个提法。如果世界上只有一种性别的人,也就没有男人,女人这种名称了。8086的汇编中,我们对于实模式的各种机制应该算是比较了解了,其大致包括实模式1MB的线性地址空间、内存寻址方法、寄存器、端口读写以及中断处理方法等内容。

不过到了80386时代,引进了一种沿用至今的CPU运行机制——保护模式(Protected Mode)。保护模式有一些新的特色,用来增强多工和系统稳定度,比如内存保护,分页系统,以及硬件支持的虚拟内存等。大部分现今基于 x86的操作系统都在保护模式下运行,包括Linux、FreeBSD、以及 微软 Windows 2.0 和之后版本 [都指32位操作系统] 。

虚拟8086模式用于在保护模式下运行原来实模式下的16位程序,我们不关心。

事实上,现在的64位处理器,拥有三种基本模式(保护模式、实模式、系统管理模式)和一种扩展模式(IA-32e模式(又分兼容模式和64位模式)) 详见这里

我们先来研究保护模式,学校目前基本还处于只讲8086实模式的时代。至于现代CPU的模式……我们有精力再来研究吧。声明下,我不是在吐槽我们的大学教育,真的。

Read More

以前写过一篇《进程眼中的线性地址空间》,这是她的姊妹篇线程篇。而且和以前一样我们只谈32位Linux下的实现。另外读者可能还需要之前的一篇文章《Linux线程的前世今生》作为前期的辅助资料。

如果读者已经看过这两篇文章,那么我们就可以继续往下说了。

我简单列出上述文章中的几个要点:

  1. 32位操作系统下的每个进程拥有4GB的线性地址空间。

  2. 从Linux内核的角度来说,它并没有线程这个概念。在内核中,线程看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,比如地址空间)。

暂时有这两点就可以了。我们直接就能从第二点中看出来,一个进程创建的所有线程实际上是都是在它的线性地址空间里运行的。也就是说,一个进程所创建的所有线程没有创建新的地址空间,而是共享着进程所拥有的4G的线性空间罢了。除了地址空间还共享什么呢?大致还有文件系统资源、文件描述符、信号处理程序以及被阻断的信号等内容。不过即便是共享地址空间,但是每个线程还是有自己的私有数据的,比如线程的运行时栈。

Read More

最近在重新翻阅《Unix环境高级编程》的时候,被书上的一段例程所困扰,那段代码是分别在主线程和子线程中使用 getpid() 函数打印进程标识符PID,书上告诉我们是不同的值,但是测试结果是主线程和子线程中打印出了相同的值。

在我的印象中《Linux内核设计与实现》这本书曾经谈到线程时如是说:从内核的角度来说,它并没有线程这个概念。Linux内核把所有的线程都当成进程来实现……在内核中,线程看起来就像是一个普通的进程(只是线程和其他一些进程共享某些资源,比如地址空间)。

《Unix环境高级编程》第二版著书时的测试内核是2.4.22,而《Linux内核设计与实现》这本书是针对2.6.34内核而言的(兼顾2.6.32),而我的内核是3.9.11,难道是内核发展过程中线程的实现发生了较大的变化?百度一番之后发现资料乱七八糟不成系统,索性翻阅诸多文档和网页,整理如下。如有偏差,烦请大家指正。

在 Linux 创建的初期,内核一直就没有实现“线程”这个东西。后来因为实际的需求,便逐步产生了LinuxThreads 这个项目,其主要的贡献者是Xavier Leroy。LinuxThreads项目使用了 clone() 这个系统调用对线程进行了模拟,按照《Linux内核设计与实现》的说法,调用 clone() 函数参数是clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0),即创建一个新的进程,同时让父子进程共享地址空间、文件系统资源、文件描述符、信号处理程序以及被阻断的信号等内容。也就是说,此时的所谓“线程”模型符合以上两本经典巨著的描述,即在内核看来,没有所谓的“线程”,我们所谓的“线程”其实在内核看来不过是和其他进程共享了一些资源的进程罢了。

通过以上的描述,我们可以得到以下结论:

  1. 此时的内核确实不区分进程与线程,内核没有“线程”这个意识。
  2. 在不同的“线程”内调用 getpid() 函数,打印的肯定是不同的值,因为它们在内核的进程链表中有不同的 task_struct 结构体来表示,有各自不同的进程标识符PID。

Read More

从文章的题目我们就知道今天是以一个进程的角度来看待自身的运行环境。我们先提出第一个问题,什么是进程?对于这个问题,各种参考资料上给出的定义都显得过于抽象而难以理解,下面是我自己的定义:

进程是一个动态的概念,它是静态的可执行文件执行过程的描述,其包含了一个静态程序运行时的状态和其所占据的系统资源的总和。

还是很抽象吗?那么,我们可以这样比喻,如果说菜谱是程序代码,厨具是硬件的话,那么炒菜的整个过程就是一个进程。这下理解了吧?那我们继续。

每个程序在启动之后都会拥有自己的虚拟地址空间(Virtual Address Space),这个虚拟地址空间的大小由计算机平台决定,具体一点说由操作系统的位数和CPU的地址总线宽度所决定,其中CPU的地址总线宽度决定了地址空间的理论上限(先不考虑主板…)。

比如32位的硬件平台可编址范围就是0x00000000~0xFFFFFFFF,即就是4GB。而64位的硬件平台达到了理论上0x0000000000000000~0xFFFFFFFFFFFFFFFF的寻址空间,即就是17179869184GB的大小(事实上我自己的64位 Intel Core i3 处理器也仅有36位地址总线而已,因为暂时用不到那么大的物理地址范围)。

为了行文的简单,我就以32位硬件平台来描述吧(事实上我对64位所知甚少,不敢信口开河…),同时指定环境为32位的Linux操作系统。

可能看到这里你反而更迷惑了,我一直在说一个进程拥有4GB的线性地址空间(以下只讨论32位),可是操作系统上同时在运行着N个进程,难不成每个都有4GB的线性地址空间不成?没错,每个都有。我们一直在使用术语“线性地址空间”而非“主存储器(内存)”,因为线性地址空间并非和主存等价。我们平时只要一提到“地址”这个概念,想必大家自然而然的就想到了主存储器。但事实上并非线性地址就一定指向主存储器的物理地址,如果你对“线性地址空间”不理解的话,我建议你先去看看我的另一篇博文《基于Intel 80×86 CPU的IBM PC及其兼容计算机的启动流程》。

Read More