莱昂氏unix源代码分析导读笔记Word下载.docx
《莱昂氏unix源代码分析导读笔记Word下载.docx》由会员分享,可在线阅读,更多相关《莱昂氏unix源代码分析导读笔记Word下载.docx(51页珍藏版)》请在冰豆网上搜索。
但是,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
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
非法指令
014
BPT跟踪
020
Iot
030
仿真陷入指令
034.
Trap
注:
仿真陷入指令,即emt指令
外部设备中断
优先级
060
电传打字机输入
4
064
电传打字机输出
200
行式打印机
(莱昂氏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