Linux情景分析 读书笔记(二) 基础知识

来源:互联网 发布:淘宝怎么增长微淘粉丝 编辑:程序博客网 时间:2024/06/05 17:32

三种地址:

逻辑地址:我们在写汇编的时候,使用的段+偏移的组合,就是逻辑地址,我们使用CS确定代码段,DS确定数据段,还有SS栈段,然后我们的各种不同的代码就在各自的段内进行操作,我们可以想象成如下的结构:

struct log_addr_t{    struct seg_reg_t seg_reg;    uint32_t seg_off;};

线性地址:CPU将逻辑地址变成统一的单个地址,这是通过分段机制完成的,我们需要做的是提供分段机制必须的段内容,但具体硬件如何转换我们是不用关心的,只需要知道肯定是会按照我们提供的段内容进行正确转换的

物理地址:将线性地址转换为真正内存中的地址,可以放到总线上请求读/写的地址,一般这个地址是通过分页机制完成的,硬件将线性地址进行解释,产生了对应的物理地址,我们需要做的,也是提供分页的页内容,具体的转换过程我们不用关心

线性地址和物理地址一般就是传统的32位无符号整型,至于内部的整合,我们下面介绍各种机制时介绍


可以看到,硬件会为我们做大多数事情,但是他需要我们提供内容,分段的时候需要段的信息,分页的时候需要页的信息,只要我们提供了这些,硬件会完成他的工作


分段机制:

分段机制是逃不开的,一定要记住,就算我们最后使用的分页,物理上也是逻辑地址-->线性地址-->物理地址的转换,虽然有的时候逻辑->线性比较简单.记住,分段在i386是必然的,所有的地址都是要先经过分段机制才能进入分页的程序的.

分段机制包括两部分,一是如何从逻辑地址到线性地址.旧的模型不多言了,新的结构是段寄存器不再是段的起始位置,而是作为一个下标,指向真正段的信息,包括段的起始,长度,状态,权限的,

段寄存器可以如下表示:

struct seg_reg_t{    unsigned int seg_index : 13; // index in reg_table    unsigned int ti        :  1; // which reg_table    unsigned int rpl       :  2; // requested p-level};

如注释中所言,seg_index表示段表中的下标(可以看到,段表最多2^13项),ti表示哪个段表(有两个,下面说),rpl表示这个段需要的特权等级(我们代码的特权等级必须高于这个才能对其操作,当前的CS段)

而段表的结构则是段表相的数组,段表项可以如下表示:

struct seg_table_entry_t{    unsigned int base_24_31 :  8;    unsigned int g          :  1; // seg granularity    unsigned int d_b        :  1; // default bit exp    unsigned int unused     :  1; // must be 0    unsigned int avl        :  1; // user     unsigned int limit_16_19:  4;    unsigned int p          :  1; // present in mem    unsigned int dpl        :  2; // discriptor p-level    unsigned int s          :  1; //    unsigned type           :  4; // used with s    unsigned int base_0_23  : 24;    unsigned int limit_0_16 : 16;};
可以看到,base是32位的,limit则是20位,其他的则是一些选项来表示该段的属性:

g:粒度,0的话limit单位是字节B,1的话单位则成了4K

d_b:运行方式,0表示16位模式,1表示32位(一般都为1)

p:内存中存在,0表示不存在,1表示在(等价于支持段级别的虚存管理)

dpl:同段寄存器中的rpl一致

s & type:二者是结合起来判断的,具体的看书


可以看到,这样的结构就比单纯的段基址+偏移要好得多,多了一层段表的查询,就多了一层控制的余地,另外要注意,base是32位的,不再是原来需要移位的,所以可以直接和off相加得到线性地址了.到时候我们可以这样的假装操作(这是假的,仅仅模仿获取地址):

struct log_addr_t log_addr = { /* seg addr */ };uint32_t lin_addr = seg_get_base(seg_table[log.addr.seg_reg.seg_index]) + log_addr.seg_off

以上只是分段机制的第一部份,第二部份就是怎样提供这些段信息,也就是说我们已经知道了逻辑-->线性是如何实现的,那么硬件提供怎样的接口供我们具体操作呢?上面也提到了,ti表示哪个段表,全局的或者局部的,硬件提供了这两种,一种是GDTR,表示全局的段表,一种是LDTR,表示局部的段表,提供这两个表的初衷估计也是为了更好的隔离各个程序.需要记住的一点是,这两个都是寄存器,有专门的汇编指令来操作(lgdt & lldt),这两个寄存器都保存的是各自段表的起始地址(32位),这个地址应该是线性地址(否则就循环回去了),千万记住,不要等会儿和后面的分页机制弄混了


分页机制:

分页机制构建于分段机制之上,当通过分段机制得到线性地址后,再通过分页机制得到真正的物理地址

分页机制其实就本质而言,类似与分段机制,都是提供一个间接层,透明的完成地址的转移,连提供的内容都是页框起始+页框偏移,二者最大的不同是分段机制的流程是被硬件钉死的,只有两步,查表,相加(当然,中间还有一系列检查过程),只要提供了段信息,硬件自动完成,而分页机制则仅仅提供了一个分页入口(后面说),提供了一个全局下可以获得的寄存器,但至于怎样使用,这就在于我们的实现了

开启了分页机制之后,OS对内存的使用包括调度都是以页为单位了,所以我们经常听说的页对齐,满页索引什么的,就是出自这里.

以32位linux为例,其线性地址被OS解释为如下的结构:

struct lin_addr_t{    unsigned int dir_off : 10; // offset in page_dir    unsigned int tab_ff  : 10; // offset in page_table    unsigned int off     : 12; // offset in page_frame};

而其分页结构,也类似的分成了双层,一层为页目录,一层为页表,其结构为:

struct page_dir_entry_t{    unsigned int ptba : 20; // most significant bits of addr    unsigned int av   :  3; // user    unsigned int g    :  1; // global    unsigned int ps   :  1; // page_size (res in page_table_entry_t)    unsigned int res  :  1; // must be 0 (dirty bit in page_table_entry_t)    unsigned int a    :  1; // accessed    unsigned int pcd  :  1; // cache disable    unsigned int pwd  :  1; // write through    unsigned int u_s  :  1; // p-level    unsigned int r_w  :  1; // read / write    unsigned int p    :  1; // present};
页目录和页表结构类似,都是如上的数组结构,ptba为物理地址的最高20位(看清楚!!是物理地址,不是线性地址,否则有回去了),具体属性有:

g:1表示全局页表,0表示局部页表(暂时不知道具体含义)

ps:0表示4K,1表示4M(在page_table_entry_t中,这项不需要,因为页大小有一个地方定义即可,此时此项保留为0)

res:0,保留(在page_table_entry_t中,这样作为dirty位,表示该页被写过了)

a:0表示没有被访问过,1表示被访问过(可能参与到具体调度算法中)

pcd:0表示使用cache,1表示从内存中取

pwd:0表示直接写到内存里,1表示在cache中,等需要时再写回

u_s:0表示系统权限,1表示用户权限

r_w:0表示只读,1表示可写

p:0表示不在内存中,1表示在内存中


表项大小为32位,一个页表中哟2^10项,正好4K,一个页面就可以装下了,正也方便了OS的调度,可以将表项整体移入移出,而不影响其他表的操作.

具体的算法呢,同分段类似,先通过dir_off找到页目录中的一项,再取出ptba,补全其后12位(都是0,因为表示页起始位置),再以ptba为页表起始,通过table_off找到页表的一项,取出ptba,补全(同样为0),后与off相加,即得到最后的物理地址,大致是如下的过程:

struct lin_addr_t lin_addr = { /* linear addr */ };struct page_dir_engtry_t pd_ent = (struct page_dir_entry_t *)page_dir + lin_addr.dir_off;uint32_t page_table = pd_ent.ptba << 12;struct page_table_entry_t pt_ent = (struct page_table_entry_t *)page_table + lin_addr.tab_off;uint32_t phy_addr = pt_ent.ptba << 12 + lin_addr.off;
至于page_dir,则是有硬件提供的一个寄存器保存(cr3,听着很想分段机制吧),每个进程有每个进程的页表,所以这个是需要保存,而且OS调度时需要进行替换的.分页机制是需要启动的,有一个cr0寄存器的最高位,就是控制是否分页的标识,开启之后cr3包括连带的快表,缺页异常等一系列相关分页机制的东东就启动了.

需要注意的是,我们刚才提醒过了,页目录和页表中的ptba都是物理起始地址!!!真正切切的物理地址,而不再需要什么分段/分页来获取了..记住这一点..别搞乱了


好吧,最混淆的时刻来临了.

我们需要了解,分段机制是任何时候都开启的,也就是说,如果我们没有打开分页机制,分段后的线性地址就直接做了物理地址了,而如果我们开启了分页机制,线性地址就需要通过分页得到物理地址.

如果我们分页了,GDTR和LDTR保存的就是各自段表的线性地址,需要经过分页处理(也就是说我们可以随意映射这两个表到任何地方),段表中的Base也是线性地址(我们也可以随意映射了),而cr3,页目录和页表中的地址则是真正的物理地址(建立页表的时候就确定),是直接拿来可以用的.注意区分,这些慢慢体会就好了,如果体会不到,就看Linux的实现


还有一部分是汇编的内容,我这方面只学了皮毛,给出两个不错的连接,我们一起学习.

伪指令:

http://ted.is-programmer.com/posts/5263.html

嵌入式汇编:

http://hi.baidu.com/linux_lfs/blog/item/e2954d99d1e7e30d6f068cfa.html/cmtid/556e78b13bf4f05f082302c1


以后等钻研深了,我还是会写一篇这个的.