莱昂氏unix源代码分析导读笔记.docx

上传人:b****6 文档编号:4695952 上传时间:2022-12-07 格式:DOCX 页数:51 大小:113.31KB
下载 相关 举报
莱昂氏unix源代码分析导读笔记.docx_第1页
第1页 / 共51页
莱昂氏unix源代码分析导读笔记.docx_第2页
第2页 / 共51页
莱昂氏unix源代码分析导读笔记.docx_第3页
第3页 / 共51页
莱昂氏unix源代码分析导读笔记.docx_第4页
第4页 / 共51页
莱昂氏unix源代码分析导读笔记.docx_第5页
第5页 / 共51页
点击查看更多>>
下载资源
资源描述

莱昂氏unix源代码分析导读笔记.docx

《莱昂氏unix源代码分析导读笔记.docx》由会员分享,可在线阅读,更多相关《莱昂氏unix源代码分析导读笔记.docx(51页珍藏版)》请在冰豆网上搜索。

莱昂氏unix源代码分析导读笔记.docx

莱昂氏unix源代码分析导读笔记

(莱昂氏unix源代码分析导读-1)引子

一直以来,操作系统都是程序员心中的圣殿,每个程序员都想登堂入室,掌握其中的奥秘。

但是,在上世纪六七十年代,对普通程序员来说,这只能是个梦想。

因为在那时,操作系统不啻为一个蛮荒巨兽,它往往由数十万乃至数百万行的汇编语句组成,对普通程序员而言,根本无法对其进行分析和理解。

Unix的出现改变了这一切。

肯·汤姆森和丹尼斯·里奇在1974年7月号上的《ACM通信》上的一篇论文“TheUNIXTimeSharingSystem”正式将unix操作系统介绍给世人,立即引起了学术界的广泛兴趣。

人们惊奇的发现,同传统的操作系统相比,unix有两个显著的特点:

(1)代码量要少1~2个数量级,但却实现了在当时看来十分强大的功能;

(2)其内核代码多由c语言写就,只有极少量的代码由汇编完成。

例如,作为莱昂模型的unixv6,其内核仅有9000多行代码,而汇编代码仅不到1000行。

更重要的是,unix采用源代码方式进行发行,只要购买了unix许可证就可以获得整个源代码。

其后不久,一本神秘的小册子开始流行,并引发了大规模的违法复印活动。

这本小册子就是《莱昂氏unix源代码分析》。

毫不夸张的说,这本书的流行极大促进了unix的发展,并哺育了一代unix精英。

因此,此书在unix界具有极高的声誉。

时过境迁,我们现在可以轻易的找到各种开源操作系统的源码和分析,此书的重要性已大大降低。

但是,它仍具有巨大的价值,它以区区9000多行代码实现了一个操作系统的基本功能,其设计之巧妙、代码质量之高,令人叹为观止。

从教育的角度看,它仍然是最好的学习操作系统的教材之一。

对我来说,阅读此书是个快乐的过程,同时也是个痛苦的过程。

因此,我决定写下自己的一点心得,希望能够为大家的读码过程减轻一点痛苦吧。

(莱昂氏unix源代码分析导读-2)PDP11/40系统简介(上)

1.地址管理

PDP11最大的特点是其UNIBUS系统,CPU对所有设备的访问都通过单一总线来完成。

因此,所有设备都具有统一的编址规则。

也就是说,所有设备,包括Memory、Register、外设、DeviceRegister等都具有统一的地址。

外设、DeviceRegister等编址在高地址空间上。

PDP-11总线为16位,故其虚存空间为64K。

但是,PDP-11可配置256K物理内存。

为有效使用物理内存空间,使用“Page”将虚存空间映射到物理空间。

每个page8K字节,每个程序拥有8个virtualPage,分别映射到8个物理Page上。

具体来说,PDP11有两种内存管理模式:

(1)“存管系统关闭”

系统初启时,处于此种状态。

此时,系统自动将virtual地址空间的前7个page映射到物理空间的前7个page,但virtual地址空间的第8个page会被映射到物理地址的最后一个page——以访问外设。

(2)“存管系统开启”

此种状态下,针对Kernel和User模式,各有8个Pageaddressregister来进行映射。

每个AddressRegister对应一个virtualPage,记录的是该page对应于物理地址空间的首地址(以block——64字节为单位)。

具体方法如下:

(1)根据virtual地址计算出逻辑页和页内偏移;

(2)物理地址=该页的AddressRegister的数值*64+页内偏移

【注】:

需要注意的是,很多情况下,源代码中使用Segment来表示Page。

但是,segment有时又用作程序的分段,如text、data、bss。

所以,这里使用page的话,更清晰,更容易理解。

【思考题】:

我们想把虚存地址中前6个Page映射到物理内存的前6个Page。

请问:

前6个AddressRegister的值应该设置为多少呢?

2.指令寻址

莱昂氏书中对各种编址方式进行了详细的讲解,唯一需要提一下的是:

(1)自动减一(加一)方式,如:

mov-(r1),r2

(2)变址方式,如:

clr2(r0)

它们“变化”的单位是不同的。

(1),它减去的一是一个Word;

【注】:

PDP-11还有一套基于Byte的指令,其特点是在正常的指令名后加一个“b”用来结尾,如movb等等。

这套指令在源码中使用较少。

(2)中的2,是2个Bytes。

(莱昂氏unix源代码分析导读-3)PDP11/40系统简介(中)

1。

通用寄存器。

共有8个通用寄存器,r0~r7

R5:

环境寄存器。

R6:

又名sp,即栈寄存器——kernel/usr各有一个

R7:

又名PC,即指令指针寄存器

2PSRegister

即processorstatusregister,该寄存器的使用非常频繁,其结构如下所示:

(1)coditioncode部分——用于记录指令执行后结果状态

共有4个标志:

N:

negative

Z:

zero

V:

overflow

C:

carriage

(2)T:

Trap(陷入位)

置位将发生陷入,中断矢量为014。

(3)Priority:

CPU的Priority

取值范围:

0~7,从低----高。

设置为7时,外部设备无法interruptCPU的执行。

(4)Mode位

CurrentMode:

当前的模式(kernelorUser)

PreviousMode:

“前”模式(kernelorUser)

需要注意的是,PS的PreviousMode部分是可以自行设置的,所以,其PreviousMode并不一定是其字面上的意思

(并不一定是“上”一次的Mode)。

这样设计可以提供一种同时访问Kernel/User地址的方法,有两个指令用于此用途:

i.mfpi:

将PreviousMode的地址里的内容,压入CurrentMode的栈;

ii.mtpi:

将CurrentMode的栈内内容,弹出到PreviousMode的地址空间内。

这两个指令使用相当频繁,我会在后面结合代码进行讲解。

PS寄存器是最常用的寄存器之一,源码中有大量对PS的操作。

下面就介绍一下对PS的常见操作。

首先介绍三个位运算指令,他们经常用于设置和CheckPS状态:

(1)BIT——logicalAND,但不改变source和destinationoperand。

会设置PS的ConditionCode。

用于checkDestinationoperand的bit设置情况;

(2)BIC——bitclear。

根据sourceoperand的bitset,cleardestinationoperand的相应bit;

(3)BIS——bitset。

“或”操作,修改destinationoperand。

对PS常见操作举例:

(1).Mov

679mov$30340,PS0011000011100000

700mov$30340,PS30340

该条语句对PS寄存器进行了如下设置:

(1)CurrentMode:

Kernal

(2)PreviousMode:

User

(3)Priority:

7

(2).Bis

0726bis$340,PS

0735~

0741~

将Priority设置为7。

(3).Bic

0748bic$340,PS

将Priority设置为0。

(莱昂氏unix源代码分析导读-4)PDP11/40系统简介(下)

1PageRegister

分为AddressRegister和DescriptionRegister,在“存管系统”起效后使用。

Kernel和User态各有8组Register,分别完成8页的“物理---逻辑”地址映射。

比如,代码中对AddressRegister有如下的声明:

KernelUser

1447KISA0=1723401451UISA0=177640

1448KISA6=1723541452UISA1=177642

其实,这些AddressRegister是连续编址的,比如,以Kernel为例:

KISA0=172340KISA1=172342KISA2=172344

KISA3=172346KISA4=172350KISA5=172352

KISA6=172354KISA7=172356

由于KISA0和KISA6使用较多,故进行了定义。

而其他的AddressRegister也可以通过这KISA0、KISA6进行访问。

PageAddressRegister低12个bits(称为PAF)有效,记录的是以block(32Words、64bytes)为单位指定地址,

即该逻辑页页首映射到的物理地址。

【思考题】:

PDP11-40的存管系统最大支持多大的物理内存?

PageDescriptionRegister:

(1)最常用的是ACF(1~2bit),如:

00不可访问

01Read

02Write

11ReadandWrite

mov$6,…ReadandWrite

(2)PLF(8~14bit)该物理page的大小(以block为单位)

多数情况下为8K,为:

mov$774064kwords,ReadandWrite

现在可以对PDP11的物理page有更进一步的了解了。

PDP11的物理内存其实是连续的地址空间,所谓的物理page

在某种程度上是一个逻辑上的概念,是通过刚才的PageRegister指定的起始地址和长度来指定的。

换言之,我们

可以(但最好不要这么做)指定两个overlap的物理page。

进一步,我们可以认为物理内存是由许多blocks组成的,

而这些block从0开始连续进行编号,每个Block直接对应一段物理内存。

因此,通过block号我们可以直接操作

物理内存。

2.Page状态Register——SSR0SSR2

SSR0和SSR2(SSR1在PDP11/40中没有使用)主要用于Page硬件中断处理。

SSR0

Bits15~13用来说明中断原因,如:

(1)违反存取限制;

(2)Page越界;

(3)写保护

Bit8——当Bit0和Bit8置位时,表示进入到“Pagecontrolinformation”状态,此时,SSR0其他Bit均无效。

该种状态用于

进行Page状态的监控。

Bit5~6用于说明中断发生时的CPUmode(00:

kernel;11:

user)。

Bit1~3:

pageNumber(0~)

Bit0:

“存管系统”启动indicator

SSR2为内部使用的只读Register,记录每条存取指令的virtualaddress,我们一般不会直接操作它。

(莱昂氏unix源代码分析导读-5)中断与陷入初探

对于中断,大家应该都比较熟悉。

莱昂氏书中的第9章对这部分也有精彩的讲解,一定要多读多看,确保深入理解。

我在这里还是啰嗦几句。

(1)中断由其他硬件(非CPU)产生的,带有优先级,如CPU当前优先级高于中断优先级,该中断不会被马上处理。

硬件产生的最高优先级中断为6。

因此,将CPUpriority设置为7时,可屏蔽硬件中断。

代码中有大量这样的设置,

refertoPSregister的讲解部分。

(2)陷入为CPU内部事件,无优先级,必须马上处理;

(3)中断或陷入处理流程

(1)将当前PC、PS暂存;

(2)通过中断矢量表重装PC、PS(一定为核心态);

(3)将原先的PC、PS压入当前栈;

(4)执行(即中断矢量表中指向的地址)

(5)执行中断例程后,根据压入的PC,返回原来的程序地址,并恢复PS。

PDP11提供了rtt指令来完成这一操作。

PDP11提供了trap指令,可以“引发”一个“陷入”,如图所示:

Trap指令会引发如下事件:

(1)PS<----(36)(切换到核心态)

(2)PS压入核心栈;

(3)pc压核心栈;

(4)pc<----中断矢量(34)处执行。

需要注意的有两点:

(1)trap指令一般用于系统调用(systementry)的实现,而“中断模拟”指令为“emt”,该指令与“trap”十分相似,

唯一的不同是其中断矢量地址为(30);

(2)trap指令code横跨104400~104777

其中后8个bit可被看着systementryno,也就是说最多有256(0~255)种系统调用。

【注】:

本版本的unix使用了后6位,也就是说支持64种systemcall。

最后,让我们检查一下中断矢量表的源码:

502br4=200

503br5=240

504br6=300

505br7=340

506

507.=0^.当前地址=0octal

508br1f地址0~1

5094地址2~3(指令IOT。

执行20octal的中断)

510

511/TrapVector

512trap;br7+0;/buserror

/地址4~7,中断矢量地址为4

/PC指向Trap例程,PS设置为340(优先级7)

513trap;br7+1;/illegalinstruction(地址10~13,中断矢量地址为10)

514trap;br7+2;/bpttracetrap(地址14~17,中断矢量地址为14)

515trap;br7+3;/iottrap(地址20~23,中断矢量地址为20)

516trap;br7+4;/powerfail(地址24~27,中断矢量地址为27)

517trap;br7+5;/emulatortrap(地址30~33,中断矢量地址为30)

518trap;br7+6;/systementry(地址34~37,中断矢量地址为34——就是trap指令的中断矢量地址啦)

519

……

525.=60^.当前地址设置为60octal

526klin;br4/(地址60~63,中断矢量地址为60)

527klou;br4/(地址64~67,中断矢量地址为64)

……

540.=200^.当前地址设置为200octal

541lpou;br4;/(地址200~203,中断矢量地址为200)

……

【注】:

代码中的“trap”并非是trap指令,而是定义于第755行的“subroutine”。

0755:

trap:

unxi源码中有大量这样的“重名”现象,容易给人带来混乱,需要格外注意。

不妨跟书中的两个列表比较一下:

矢量单元

陷入类型

处理机优先权

010

非法指令

7

014

BPT跟踪

7

020

Iot

7

030

仿真陷入指令

7

034.

Trap

7

注:

仿真陷入指令,即emt指令

矢量单元

外部设备中断

优先级

处理机优先权

060

电传打字机输入

4

4

064

电传打字机输出

4

4

200

行式打印机

4

4

(莱昂氏unix源代码分析导读-6)stack使用和进程的分段

1.Stack使用:

PDP11使用倒置的栈,即栈底在高地址,栈向低地址生长,如下图所示:

(1)压栈示例:

MOVR1,–(SP)

先移动SP,再放置R1

(2)POP示例:

MOV(SP)+,R1

先取栈内值,再移动栈指针释放该地址。

参数压栈规则:

(1)参数逆序压栈;

(2)最后放置ReturnAddress

如:

rpta(aa,bb)

栈的示意图

【代码示例1】:

1889savu(u.rsav);savu是一个单参数的Subroutine

725_savu:

726bis$340,PS设置CPUPriorityto7

727mov(sp)+,r1“ReturnAddress”tor1

728mov(sp),r0参数(u.rsav)tor0

729movsp,r0+sp--->u.rsav[0]

730movr5,r0+r5--->u.rsav[1]

731bic$340设置CPUPriorityto0

732jmp(r1)return

【代码示例2】:

696_copyses:

697movPS,-(sp)在栈中保存PS

698movUISA0,-(sp)保存UISA0

699movUISA1,-(-sp)保存UISA1

700mov$30340,PS设置PS

701mov10(sp),UISA0参数1--->UISA0

702mov12(sp),UISA1参数2--->UISA1

703movUISD0,-(sp)保存UISD0

704movUISD1,-(sp)保存UISD1

705mov$6,UISD06--->UISD0

706mov$6,UISD16--->UISD1

707……

这里的陷阱是:

(1)对栈使用了两种寻址:

“-sp”、“10(sp)”的单位是不同的,-sp会移动一个word(2bytes);而10(sp)中的10是以byte为单位的。

(2)10(sp)、12(sp)中的10、12为octal,即8dec、10dec。

PDP11在硬件上对这种栈结构的形成提供了便利——它提供了jsr和rts指令,以完成子函数的调用和返回。

这两个指令比较复杂,简单的用法:

(1)jsrpc,gword——当前pc压栈,然后将pc设置为gword(即跳到gword处)

(2)rtspc——从栈顶弹回pc

更复杂一些的,如:

0558:

klin:

jsrr0,call;_klrint

(1)r0压栈;

(2)r0-<--updatedpc(即(r0)==_klrint)

(3)pc<----call

这样r0的内容可以作为参数带入。

【思考题】:

ReturnAddress处于栈顶,“被调用程序”可直接使用rts指令Return回调用程序。

那末,栈由谁负责清理呢?

2.进程的分段:

(1)Text

正文段,可执行语句都在此段中。

写保护,可重用。

即一个程序启动两个进行,则这两个进程可重用Text段。

(2)Data

数据段,存放初始化的数据。

Data段紧跟在Text段之后。

但由于Text段一般为写保护,故如其未满一page,该page也会被Text段保留。

所以,Date的起始地址为Text段后的第一个8K边界。

(3)Bss

存放程序中未初始化的全局变量的一块内存区域,一般在初始化时将会清零。

紧跟在Data段之后。

Data段往往是(可执行)文件的一部分,当可执行文件Load入系统后,data段就由文件内的内容进行了初始化。

bss段不同,在文件中没有这部分的内容,因此,Load入系统时,这部分的内容未经初始化。

以上并非进程的全部空间,例如,还有stacksegment、PPDA(进程数据区)等等,我们将来还会谈到。

unixv6定义了“_edata”和“_end”两个伪变量,分别表示“内核启动进程”的数据段以及数据段加上bss段的长度,如图所示:

最后,顺便谈一下“.”和“^”的用法。

我们已经遇到过这个两个符号了:

(1)“.”表示当前地址(位置);

(2)“^”用于给当前地址赋值。

代码示例:

1463.bssFollowingisthecontentofbsssegment

1464/*-------------------------------*/

1465.globlnofault,ssr,badtrap声明三个符号为全局符号

1466nofault:

.=.+2当前地址=当前地址+2,即保留2个Byte,初始化为0

1467ssr:

.=.+6当前地址=当前地址+6,即保留2个Byte,初始化为0

1468badtrap:

.=.+2……

507.=0^.当前地址=0octal

br1f

4指令IOT。

执行20octal的中断。

.=40^.当前地址设置为40octal

(莱昂氏unix源代码分析导读-7)c语言若干问题

Unix的开创者之一丹尼斯-里奇,同时也是C语言的老祖宗。

他把c语言引入了unix,从而开辟了一个崭新的时代。

C语言和unix的关系,真真称的上是鱼水情深,密不可分。

毫不夸张的说,是c语言成就了unix,而同时也是

unix成就了c语言。

大家对这门语言自然是十分熟悉,但在这里我还是要多说几句。

首先,我们所分析的代码写于70年代初中期,其时c语言也刚刚成型,其语法与现在的c语言稍有不同,比如:

(1)+=、-=、/=等运算符的写法不同,记为=+、=-、=/等等;

(2)看起来当时还没有long这一数据类型。

因此,在记录一些可能会超出1个word(16bits)的数据时,

往往会使用2个变量来进行存放。

比如,对文件size,在structinode中使用了两个变量:

5611:

chari_size0;//文件size高8bits

5612:

char*i_size1;//文件size低16bits

还有一种常见的情形是使用两个word来模拟一个32bits数。

unix甚至提供了一套函数来完成这类数

的加、减等算术操作。

(3)现代c语言是个强类型语言,而当时的c语言类型没有现在这么强。

对指针而言,尤其如此。

而对指针

类型放松类型校验的结果是使指针的使用极其灵活高效,进行低层操作时显得得心应手。

如下面的例子:

0164:

#definePS0177776//0177776即为PS寄存器的逻辑地址

//源码中有大量此类deviceregister的定义,其特点是

//都位于第8个逻辑page(会被映射到高地址空间)

0175:

struct{intinteg;};

2070:

s=PS->integ;

PS仅是一个常数地址,无需定义就可以按指针方式进行使用,而通过一个无名strut,PS获得了

指针类型——指向integer的指针。

最后来看一下函数的call和Return时通用寄存器的保存和恢复,这部分内容在书中的10.6小节,有简洁的说明,

请先翻到该小节,看看莱昂的描述。

下面我举例说明一下函数调用期间栈的变化情况。

为简单起见,我们使

用一个没有参数的函数来进行说明。

假设有一个C语句定义的函数fun(),在调用此函数时:

(1)jsrpc,func

(2)func函数开头会调用jsrr5,csv

(3)csv代码如下:

1420:

csv:

1421:

movr5,r0

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 高中教育 > 理化生

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1