三种地址:
逻辑地址:我们在写汇编的时候,使用的段+偏移的组合,就是逻辑地址,我们使用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
以后等钻研深了,我还是会写一篇这个的.