1.用以a=2,3,...,15为底,给0到1000的数字起名字,需要多少个不同的数字的名称?哪一种基底要求的数字名称最少?
写了个小程序来测试,答案是4,需要8个数字名称.
#include <stdio.h>
#define START_BASE 2
#define BASE_LIMIT 16
#define NUMBER 1001
int main(void)
{
int min_number_base = START_BASE;
int min_number_count = NUMBER;
int base;
int count;
int product;
for(base=START_BASE; base < BASE_LIMIT; base++)
{
count = base;
product = 1;
while((product *= base) < NUMBER)
{
count++;
}
if (count < min_number_count)
{
min_number_base = base;
min_number_count = count;
}
printf("base:%d\n",base);
}
printf("The min number of base is: %d\n",min_number_base);
printf("The count is: %d\n",min_number_count);
待续.......
进程也许是计算机科学史上最成功的人造概念之一,以前看其它的操作系统书时,也总是浓墨重彩,但遗憾的是,除了留一下点印象,始终无法形成形象思维,究其原因,通常上来就是抽象概念定义,接着是锁,信号量之类的,基本的都没有理解,后面的基本上也是扯蛋了。本书这一章的亮点是不空谈理论,先引导读者稍微思考一下本质问题,再马上进入实战。
我们的程序以文件的形式存储在磁盘上,本质上就是一堆二进制编码,究竞要怎么样才能在计算机上运行?从计算机的角度来说,必须首先把程序加载到内存,并设置好相应的寄存器值,其中包括代码段,数据段,堆栈段。我们怎样知道这一块二进制编码那些是数据那些是代码呢,当然得按一定的格式来,在windows下使用的格式叫PE,在linux下叫做ELF格式。当然事情还远不至这么简单,如果程序跑起来了,如果切换到另一个程序,我们必须完整地保存当前进程的所有状态,并在必要切换回当前进程时还原进程的信息。通常是是有个专用的数据结构来做这个事情,叫做进程控制块。通过以上的简单描述,我们可以发现在一个真实的操作系统中至少需要以下组件分工合作才能使进程工作:
1)进程体 包括代码,数据,堆栈
1)加载器 负责加载程序到内存
3)进程调度
4)其它硬件支持
在本书中,做了相当的简化,直接在内核中以一段简单的代码为例,省去了加载,链接等过程,结合本书来看看让进程起来需要那些步骤.
1.准备进程体, 本书就是一段简单的显示"A"的代码
2.初始化TSS和LDT的描述符(在GDT中),初始化TSS
3.初始化进程表,主要包括各寄存器值(CS:EIP指向先前的进程体),LDT Selector,LDT
4.一段跳转代码,主要作用是把堆栈为当前进程表的地址,从中恢复开始设置各种寄存器值,加载ldt,设置tss,最后调用ireted
指令切换到进程中
进程调度
进程调度主要是利用时钟中断,其简单的实现步骤如下:
1)保存当前进程状态信息至进程表
2)切换至内核栈
3)调用进程调度程序
4)离开内核栈
5)恢复调度的进程信息
6)执行ireted指令,调度的进程开始运行.
系统调用
系统调用的实现跟现在linux的系统调用类似
C code:
#include <stdio.h>
extern add(int,int);
int main(int argc,char **argv)
{
int result = add(4,5);
printf("result:%d\n",result);
}
assembly code:
;nasm -f elf add.s -o add.o
[section .data]
Message db "calcuated result:%d",0xa,0
[section .text]
extern printf ;invoke c library
global add
add:
mov eax,[esp+4];
add eax,[esp+8];
mov ebx,eax;
push eax;
push dword Message;
call printf;
add esp,8;
mov eax,ebx;
ret;
编译命令:
nasm -f elf -o add.o add.s
gcc -c testAdd.c -o testAdd.o
gcc -o test_add testAdd.o add.o
./test_add
这一章的比较简短,没有太多的东东,不过内容感觉稍稍有些货不对版,标题是让操作系统走进保护模式,其实大部分内容是在讲述怎么样加载操作系统的Loader到内存.不过思路还是值得记录一下, 引导扇区只有512byte,如果同时加载内核并初始化环境可能不够用,作者中间增加了一个Loader模块,引导扇区负责把Loader加载进内存并把控制权交给它,Loader负责加载内核初始化保护模式环境,并把控制权交给内核,内核开始启动.Loader是个com文件,同时也便于调试.
背景知识点:
CHS模式和LBA模式的转换
CHS模式 (cylinder,head,sector) 是初始的硬盘寻址方法,你要如果要在硬盘上寻找一个扇区,必需要提供柱面号,磁头号和扇区号,如果你了解硬盘的物理结构,这种寻址方法理解还不是太困难。
LBA模式(Logic Block Address) 是一种逻辑寻址,把所有的扇区在逻辑上看成是一个一维数组,顺序编号,这简单多了,现在的硬盘都支持。
CHS到LBA转换公式
#lba = (#c*H + #h) *S + #s -1
#c 是柱面号
#h 是磁头号
#s 是扇区号
H 是每个柱面的磁头数
S 是每个磁道的扇区数
-1 是因为lba 从0开始
LBA到CHS转换通过纯数学都可以推导出来了,
#c = #lba / (S * H)
#h = (#lba/S)% H
#s = #lba % S + 1
书上计算磁头号是公式是 #h = (#lba /S) & 1,这是因为磁头数已知是2, mod 2的结果只可能是0或1,也是就是取决于数二进制的最后一位是0或1,因此只要&1就可以得到磁头数,这算是一种优化了吧。
参考
http://zh.wikipedia.org/wiki/LBA
学完这一章后,我尝试来好好总结一下,发现在很难做到,因为相关的细节牵扯比较多,一不小心就必须要到具体的实现细节,而要了解这些细节Intel 开发手册已有清晰的描述.下面以问题的方式要来记录一下自已的一些体会.
1. 为什么需要保护模式?
这跟操作系统历史的发展密切相关,在早期,操作系统是单进程,你输入一个命令就执行一个命令,像DOS.完了等待用户续继输入.这挺浪费计算机资源,人们希望同时运行多个任务,即同时运行多个进程.多进程会引起一系列的问题,这里说一下一个基本的问题,怎么样保证一个进程不非法访问其它进程的内存.学过8086CPU的人知道,这种CPU是相当阳春白雪,通过段:偏移的方式就可以访问物理内存,如果我们在这种CPU上实现多进程,(我们假设速度足够快,内存足够大),操作系统能合理分配内存,但无法在软件层面保证一个进程非法访问其它的内存,也就是说需要硬件的支持.因此要稳定可靠的实现多进程,必须对原有的硬件体系进行改造增强, 现在我们给这个增强的硬件体系叫做保护模式.
2.保护模式增加有那些基本的硬件设施?
这里以IA-32为例,
a. 4个控制寄存器(32位)
CR0 CPU工作方式工作方式控制,包含实模式/保护模式,启用与禁用分页机制等.
CR1 保留
CR2和CR3用于分页管理管理机制
b. 系统地址寄存器
GDTR(48位)全局描述符表寄存器, 包含全局描述符表段的基地址和段界限.
LDTR(16位) 局部描述符寄存器 LDTR实际是一个特别的选择子,用来定位每个任务的局部描述符表
IDTR(48位) 中断描述符寄存器 包括IDT的基地址32位和段界限16位
TR (16位) 任务状态段寄存器 包含当前任务的任务状态段的选择子
3.保护模式的一些基本术语的解释?
a.描述符
描述符是一个数据结构,用来说明一段内存的情况,包括段的基地址,段界限,段属性(段类型,段权限等), 或 用来说明一种调用门,包括目标代码段的选择子,入口地址偏移和其它属性。
b.描述符表
实际是一个描述符数组,分为全局描述符表,局部描述符表,中断描述表
全局描述符表 表示系统最顶层的内存分配描述符表集合,通常包含内核所使用代码段,数据段,堆栈段,以及一些特别的段.
局部描述符表 每个任务私有的段描述表
中断描述符表 表示中断和异常处理的各种门的描述符表
c.调用门
正如字面的意思,作用相当于一个"门",其实就是一个目标代码段的入口.包括目标代码的选择子和偏移地址和权限级别等其它属性.通常应用程序需要调用操作系统提供的服务,而应用程序运行在较低的特权级别,无法直接访问操作系统的服务,这就要通过调用门实现系统调用,这其中涉及到权限的检查,堆栈的切换等.
d.选择子
选择子有点类似于一个index,所不同的是还包含一个位指示是全局描述表还是局部描述表,还有2个字节表示RPL(Request Privilege Level),在保护模式下是加载到段寄存器CS,SS,DS,ES,FS,GS.选择子加描述符的设计个人感觉是又增加了一个层次,用描述符来专门记录内存的寻址信息,不再像8086那样寻址受寄存器位的影响,如果以后调整内存的寻址方式,基本上不会对其它硬件造成影响.
e.中断和异常
中断和异常是硬件级实现的事件通知机制。中断概念跟实模式是一样的,只是实现的方式发生了变化。异常是CPU检测到指令的执行先决条件不满足(如除法指令不能除以0)或某些系统规则被违返或不满足(缺页等)发出的事件通知,CPU不是God,异外情况发生了只能向外发出通知请求处理。异常分为Faults,Traps和Aborts. Faults为可称之为“良性”的异常,意为可以correct并重新恢复程序执行。Traps 是一种特别的异常,用于设置断点或调试。Aborts是真的发生了错误,会中断程序的执行。中断和异常虽然概念不一样,但具体的实现的方式是一样的。
f.分段和分页
想像一下一本书,如果你把这本书每一页都撕下来排成一列,从第一页到最后一页整体就叫'段',每一页就叫'页',分段就是先确定一块'大'块的内存,确定的它的基地址和界限,然后再切分成更小的块相等的内存,这就是分页.切分页的方法通过二级目录,这里以4K每页为例,首先把线性地址的高10位作为Index在一级目录中得到二级目录的首地址,再把线性地址的中间10位作为Index在二级目录中得到物理地址高20位,最后和线性地址的低12位相加得到真正的物理地址。
另外,分页的另一个好处可以实现虚拟内存.
4.保护模式是如何工作的?
一旦把CR0的PE位(0)设为1,CPU就进入了保护模式.保护模式最大的改变首先是内存寻址。段寄存器退化成了一个选择子,EIP,ESP还是偏移地址,
寻址变成了首选通过选择子,在全局或局部描述符表中找到相应的描述符,描述符中有段的基地址,基地址加偏移地址得到线性地址,如果没有开启分页,
线性地址就是物理地址.如果开启了分页,就发生上面描述的情形。另外保护模式在这其中会进行大量的安全检查,下面描述。
5.保护模式的有那些"保护"?
段级保护
访问是否超出段的范围,段类型是否正确,如你不能把数据段加载到代码段,段的权限级别检查等。
页级保护
包括权限和读写保护
IOPL(I/O Permssion Level)
提供I/O保护,如果当前的程序没有相应的权限,不能运行一些I/O敏感的指令,如in,out,cli,sti.
I/O许可位图
控制当前的程序那些I/O端口可以访问。
保护模式的细节比较旁杂,要详细了解可以参看Intel开发手册三卷,要真的理解保护模式工作原理还得Review书上的源代码,最好自已动手调试一番。

