x86架构:分页机制和原理
分页是现在CPU核心的管理内存方式,网上介绍材料很多,这里不赘述,简单介绍一下分页的背景和原理
1、先说说为什么要分段
- 实模式下程序之间不隔离,互相能直接读写对方内存,或跳转到其他进程的代码运行,导致泄密、出错,通过分段隔离不同程序代码对不同内存单元的读写权限;
- 用户程序在内存种加载的地址不确定,通过分段对程序的数据、代码重定位,才能在运行时正确寻址(如果没有特殊声明,编译器编译后生成文件的代码和数据都是相对文件头开始计算偏移的)
2、再说说为什么要分页?
物理内存是有限的,主流普通PC机内存也就8G~16G,除了运行os,还要尽可能多地运行用户程序。但现代大型的用户程序动则大几百M,甚至几个G,要想“同时”把这么多的用户程序加载到内存运行该怎么办了?
- CPU的分页机制把物理内存分割成4K大小的空间,称为“页”。
- 32位的windows操作系统针对每个进程,虚拟出了4GB的进程空间。对于进程来说,低2G的空间随便用,无需任何顾忌。那么问题又来了,不同进程很有可能用到了同样的地址,怎么防止冲突?
- os会根据实际情况,把虚拟地址”挂载“到适合的物理页。对于不同的进程,代码种即使用了同样的地址,os也会挂载到不同的物理内存,这些对于进程来说都是透明不可见的,也不需要关心;
- 内存的空间是有限的,为了尽量多”并发“运行进程,os会酌情把物理页的数据存储到磁盘的pagefile.sys文件。当进程执行需要用到时,发现物理内存没有,此时产生缺页异常,os负责从磁盘取回这些数据放回内存,让进程继续执行;
- 分页可以让段基址和limit变平坦(64位已经这样了),段仅用来鉴权,或在32位和64位之间来回切换(利用这个特性可以让64位的os兼容32位的应用程序,也可以将32位程序的某些重要数据,比如key、密钥、密码之类的放在64位模式下,达到在3环下反调试、反逆向的目的,详细的过程见这里:https://www.bilibili.com/video/BV1SJ411K7LR)
- windwos会对页赋予各种属性,比如可执行,可读写。可人为将页属性更改,比如代码所在的页改为不可执行、不可读,进程运行到这种页时产生缺页异常。此时如果hook pagefault函数,根据异常原因分别处理:如果是执行,那么把页属性改成可执行,替换成自己想要执行的代码;如果是读取异常,那么给该线性地址挂载原物理页。这种hook能达到隐藏钩子的目的,能在VT下过PG保护,这就是著名的shaodw walker,详细过程可以参考这里:https://www.bilibili.com/video/BV1Hb411n7Mw
3、核心代码解读
(1)准备PDT
- 页目录物理地址0x20000开始,后续会把这个地址赋值给CR3;
- PDE也是32位=4字节,那么PDE大小=1024*4=4096字节,刚好是一个页,那么PDT结尾就是0x20000+0x1000=0x21000;
;创建系统内核的页目录表PDT ;页目录表清零 mov ecx,1024 ;1024个目录项PDE mov ebx,0x00020000 ;页目录的物理地址 xor esi,esi .b1: mov dword [es:ebx+esi],0x00000000 ;页目录表项清零 add esi,4 loop .b1 ;在页目录内创建指向页目录自己的目录项,最后一项指向自己,那么线性地址高20位是0xFFFFF的时候,转成物理地址就是页目录自己 mov dword [es:ebx+4092],0x00020003 ;在页目录内创建与线性地址0x00000000对应的目录项 mov dword [es:ebx+0],0x00021003 ;写入目录项(页表的物理地址和属性)
- 以上代码执行完毕后,内存图如下:分别在PDT的首位写入两个地址,其他的都清零,那么问题来了,为啥要分别写这两个数,而不是其他的数?
- 先解释一下PDT的第一项为什么会是0x00021003
一旦开启分页,所有地址都会被认为是线性地址,都会经过转换才能获取物理地址,这是CPU的硬件机制决定的,操作系统都要遵守,无法例外。既然0x20000~0x21000这段地址已经被用于存放PDT,那么就不应该再被写入,避免PDT被破坏,导致线性地址映射到物理地址出错,所以物理地址必须从0x21000开始;这里把0x21000开始的地方用来存放页表;
- 再解释一下最后一个PDE为什么是0x20003
由于业务变化多端,无法在开启分页前全部确定最终地址,导致很多PDT要开启分页后再填;那么问题又来了,一旦开启分页,任何线性地址都要转换才能得到物理地址,PDT也不例外,怎么让线性地址转换后落入0x20000~0x21000这个物理区间了?
来分析一种特殊的地址,前20位都是1,比如0xFFFFF200. 按照10-10-12拆分,3个偏移分别0x3ff, 0x3ff乘以4后分别是 0xffc,0xffc;
第一次转换:0x20000+0xffc=0x20ffc,得到0x20003;后3byte是属性,基址就是0x20000;
第二次转换:0x20000+0xffc=0x20ffc,得到0x20003;后3byte是属性,基址还是0x20000;
最后一次转换:0x20000 + 0x200= 0x20200,地址还是落在0x20000~0x21000区间;所以结论就是:线性地址前20位都是1,转成物理地址会落在PDT内部,线性地址最后12位就是PDT内的偏移;通过一些巧妙的数字设置,这里把页目录当成页表在用了;
最后12位是属性位:
(2)正式开始分页前最后的准备工作:初始化PET页表,让其映射最低端0~1MB的物理地址;实模式下低端1MB物理地址都有用了,所以必须先把这部分地址映射,防止分页开启后找不到;下面有第(3)点有PDT和PET的内存表,方便理解
;创建与上面那个目录项相对应的页表,初始化页表项 mov ebx,0x00021000 ;页表的物理地址 xor eax,eax ;起始页的物理地址 xor esi,esi ;esi=0 .b2: mov edx,eax ;edx=eax; eax = 0x1000*n or edx,0x00000003 ;edx=0x1000*n+3;u/s=1,不允许3环程序访问;P=1,页在内存种;RW=1,页可读可写; mov [es:ebx+esi*4],edx ;登记页的物理地址; 0x21000~0x21400都是PTE,隐射从0~1MB(256*4096=1Mb)的物理地址; add eax,0x1000 ;下一个相邻页的物理地址 inc esi cmp esi,256 ;仅低端1MB内存对应的页才是有效的 jl .b2 .b3: ;其余的页表项置为无效 mov dword [es:ebx+esi*4],0x00000000 ;0x21400~(0x21400+(1024-256)*4=0x22000)清零; inc esi cmp esi,1024 jl .b3
(3)这里 es:ebx+esi = 0xFFFFF800, 开启分页机制后,会映射到0x20800,同样也赋值0x21003,指向页表第一个位置;
;在页目录内创建与线性地址0x80000000对应的目录项 mov ebx,0xfffff000 ;页目录自己的线性地址;高5字节都是F,低3字节就是PDT内的偏移 mov esi,0x80000000 ;映射的起始地址 shr esi,22 ;取线性地址高10位(目录索引),esi=0x200 shl esi,2 ;索引乘以4得到偏移 mov dword [es:ebx+esi],0x00021003 ;写入目录项(页表的物理地址和属性)es:ebx+esi = 0xFFFFF800
虽说这两个PDE都指向同一个页表,但各自的线性地址确不同:第一个线性地址范围0x00000000~0x000FFFFF(PDT的索引是0), 第二个线性地址的范围是0x80000000~0x800FFFFF(PDT的索引是800);为什么要让两个不同的线性地址段指向同一个PTE,进而共享同一块物理内存了? 站在应用开发角度,已经习惯了将0x80000000作为内核地址,并且各个用户程序共享。但此时GDT已加载到0x0~0xFFFFF的低1MB空间,后续内核代码、内核数据段、API也会加载到这1MB空间,为了兼容现有的用户习惯,需要将0x80000000也映射到这里的物理地址;所以这里的结论:线性地址0x80000000~0x800FFFFF映射的物理地址:0x00000~0xFFFFF;
物理地址内容如下,这里设计就很巧妙了:
- 比如未分页的时候物理地址0x00007e10, or 0x80000000后变成0x80007e10,经过下面PDE和PTE的转换,线性地址0x80007e10又变回了物理地址0x00007e10,分页开启在在物理地址保存的各个GDT or 0x80000000 就行,其他没任何影响,照常使用;
- 原0x00000000~0x000FFFFF 低1MB的物理空间,分页开启后转成的物理地址没变。比如0x00007e10,当成线性地址转换成物理地址后还是0x00007e10;
- 巧妙之处:(1)高10位是0x000或0x800的线性地址,在PDT表中查找到0x00021003,这是PTE的起始地址; (2)中间10位是PTD的偏移,每个偏移都乘以0x1000,比如上面的0x007,得到0x7000;(3)最后3字节是页内偏移,所以得到的结果还是以前的物理地址0x00007e10;
(4)此时已开启了分页模式,所有地址都会被认为是线性地址,为了正常找到在实模式下已经存好的描述符,这里对每个描述符最高位置1,原因上面已经解释过:这么做能让新的线性地址经过PDE和PTE的转换后还能变回以前的物理地址,比如线性地址0x80007e10又变回了物理地址0x00007e10;
这里把内核各个核心段的描述符最高位都置1,构建内核区域的线性地址:
;将GDT中的段描述符映射到线性地址0x80000000 sgdt [pgdt] mov ebx,[pgdt+2] ;ebx存放GDT的base or dword [es:ebx+0x10+4],0x80000000 ; or dword [es:ebx+0x18+4],0x80000000 ;内核堆栈段 or dword [es:ebx+0x20+4],0x80000000 ;视频显示缓冲区 or dword [es:ebx+0x28+4],0x80000000 ;API段 or dword [es:ebx+0x30+4],0x80000000 ;内核数据段 or dword [es:ebx+0x38+4],0x80000000 ;内核代码段 add dword [pgdt+2],0x80000000 ;GDTR也用的是线性地址 lgdt [pgdt]
此刻问题又来了:这个时候不是已经开启分页了么?es:ebx+0x18+4 = 0x7e00+0x18+0x4= 0x7e1c,这个地址会被当成线性地址看待;如果按照10-10-12分页,0x7e1c转成物理地址后还是0x7e1c,描述符的最高位成功置1; 更改后的描述符 0x80cf9600`0x7c00fffe ,段基址0x80007c00,转成物理地址后还是0x7c00;
(5)API段一共提供了4个函数,在内核数据段对这4个函数都有登记,每个函数的格式:函数名(不超过256字节,不够的填0补充)、API段内偏移、API段选择子,这个类似于导出表;这里构造每个API函数调用们(权限控制在3环的程序访问),然后将selector写回原选择子处;
其实在API(原作者称为sys_routine段),出了这4个,还有其他函数,比如make_gate_descriptor、set_up_gdt_descriptor、alloc_inst_a_page等,只不过这两个函数并未在导出表列举,一般情况下用户程序是不知道其地址的;同时也是内核0环权限,普通3环程序也无权访问,但还是有办法调用,比如在windows下,做逆向时需要调用很多内核未导出函数,在驱动中完全可以根据特征码查找这些函数的偏移地址,然后call调用,详细可参考之前的文章:https://www.cnblogs.com/theseventhson/p/13024325.html
;以下开始安装为整个系统服务的调用门。特权级之间的控制转移必须使用门 mov edi,salt ;C-SALT表的起始位置,内核API函数导出表,有函数名称、函数在API段内的偏移、API段的选择子 mov ecx,salt_items ;C-SALT表的条目数量,ecx=4 .b4: push ecx mov eax,[edi+256] ;该条目入口点的32位偏移地址;API函数的段内偏移地址 mov bx,[edi+260] ;该条目入口点的段选择子 ;API函数所在段的选择子 mov cx,1_11_0_1100_000_00000B ;特权级3的调用门(3以上的特权级才 ;允许访问),0个参数(因为用寄存器 ;传递参数,而没有用栈) call sys_routine_seg_sel:make_gate_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [edi+260],cx ;将返回的门描述符选择子回填 add edi,salt_item_len ;指向下一个C-SALT条目 pop ecx loop .b4
(6)分配物理页:为了简单,这里只使用2M内存,可用512个页;512个页用512位保存状态,0表示空闲,1表示使用,这512位存放在page_bit_map中;分配内存时先逐个遍历,是0的话就占用;同时把索引号乘以0x1000就是物理地址了;
allocate_a_4k_page: ;分配一个4KB的页 ;输入:无 ;输出:EAX=页的物理地址 push ebx push ecx push edx push ds mov eax,core_data_seg_sel mov ds,eax xor eax,eax .b1: ;遍历page_bit_map,找到第一个标识是0的位,说明该页还未使用 bts [page_bit_map],eax ;[page_bit_map]第eax的位复制给CF,同时置1 jnc .b2 ;CF=0,说明找到了空闲的物理页;物理页索引存放在eax inc eax ;没有找到,eax+1继续找 cmp eax,page_map_len*8 ;遍历到page_bit_map末尾了吗? jl .b1 ;没有就从头继续找 mov ebx,message_3 call sys_routine_seg_sel:put_string hlt ;没有可以分配的页,停机 .b2: shl eax,12 ;eax存放了空闲的物理页索引,乘以4096(0x1000)就是地址 pop ds pop edx pop ecx pop ebx ret
(7)给指定的线性地址挂载物理页
- 线性地址也要求0x1000对齐
- 这里构造新的线性地址:(1)原线性地址高10位放在新地址中间13~22位;原线性地址中间10位(13~22)放新地址低3~12位;新地址高10位置1,这样一来,原地址高10位会作为页目录表的偏移,原地址中间10位作为页表内偏移;mov [esi],eax 会把找好的物理页地址放入合适的页表项,最终完成线性地址到物理地址的映射;
alloc_inst_a_page: ;给指定的线性地址挂载物理页 ;层级分页结构中 ;输入:EBX=页的线性地址,比如0x80104000 push eax push ebx push esi push ds mov eax,mem_0_4_gb_seg_sel mov ds,eax ;检查该线性地址所对应的页表是否存在;把ebx高10位作为PDT的索引查找PTE; mov esi,ebx ;esi=0x80104000 and esi,0xffc00000 ;只保留最高的10位,低22位清零,得到PDT的索引,esi=0x80000000 shr esi,20 ;高12位移到低12位:得到页目录索引,并乘以4,得到PTE在PDE内的偏移地址;esi=0x00000800 or esi,0xfffff000 ;页目录自身的线性地址+表内偏移;最高20位置1的线性地址,转换成物理地址=PDT基址(这里是0x20000)+esi,相当于最低3字节就是PDT内的偏移,高20位置1确保物理地址还是落在PDT内;esi=0xfffff800 test dword [esi],0x00000001 ;P位是否为“1”.如果PDT某项有PTE,结尾不会是0;如果是0,说明还未挂载物理页;[esi]=0x00000003,最后4位是0011; jnz .b1 ;否已经有对应的页表 ;创建该线性地址所对应的页表 call allocate_a_4k_page ;分配一个页做为页表 or eax,0x00000007 ;该页的属性:U/S=1,允许3环访问;RW=1,可读可写;P=1,表明有物理页了 mov [esi],eax ;在页目录中登记该物理地址 .b1: ;不论是否执行JNZ .b1,代码最终会走到这里来 ;开始访问该线性地址所对应的页表 mov esi,ebx ;esi=0x80104000 shr esi,10 ;高22位移到低22位,esi=0x00200410 and esi,0x003ff000 ;只保留原线性地址高10位,也就是PDT的偏移;esi=0x00200000 or esi,0xffc00000 ;原线性地址最高10位保存在esi的中间10位,即11-20位;高10位置1,这样在PDT内查的时候能得到0x21003,也就是页表的基址; ;得到该线性地址在页表内的对应条目(页表项) and ebx,0x003ff000 ;ebx=0x00104000,保留原线性地址中间10位 shr ebx,10 ;相当于右移12位,再乘以4;原线性地址中间10位右移到低2~11位,得到页表内的偏移;ebx=0x410 or esi,ebx ;页表项的线性地址;原线性地址的高10位、中间10位依次右移,现在是从2~20位,高11位置1;原线性地址高10位用来作为页表的偏移,中间10位用来做页表的偏移; esi=0xFFF00410 call allocate_a_4k_page ;分配一个页,这才是要安装的页 or eax,0x00000007 mov [esi],eax pop ds pop esi pop ebx pop eax retf
第一次传入的线性地址是0x80101000,还查不到对应的物理页:
执行完mov [esi],eax后,0x8010100的线性地址被映射到了0x2b000的物理地址:
(8) 在当前PDT,ebx低3字节就是页目录内的偏移;把底2G的页目录清空,根据实际情况填上用户程序的页目录,再复制到其他地方,这样不用切换CR3(一旦切换,需要新的页目录和页表,但还未建设好了,CPU会抛异常的),可以利用现有的地址转换体系;后续每创建新任务,这部分的页目录表都要清零;从0x20800开始的页目录都是映射0x80000000的线性地址,这部分属于各个任务共享的内核;
;清空当前页目录的前半部分(对应低2GB的局部地址空间) mov ebx,0xfffff000 xor esi,esi .b1: mov dword [es:ebx+esi*4],0x00000000 inc esi cmp esi,512 jl .b1
运行完后,内存变成这样:
(9)所谓 “每个用户程序都拥有4GB的虚拟空间” ,核心原理体现在这里了: 每个用户程序都单独定制一个页目录表和页表。每个用户程序页目录表的第1项到512项都映射自己的物理地址,尽管不同用户程序同样用低2G的线性地址,但映射的物理地址却可以不同;
mov [0xfffffff8],ebx: 这里把存放用户程序页目录表的物理地址放在内核地址页目录表的倒数第二项;如果有第二个用户程序,可以放在倒数第三项,即mov [0xfffffff4],ebx 以此类推;
create_copy_cur_pdir: ;创建新页目录,并复制当前页目录内容 ;输入:无 ;输出:EAX=新页目录的物理地址 push ds push es push esi push edi push ebx push ecx mov ebx,mem_0_4_gb_seg_sel mov ds,ebx mov es,ebx call allocate_a_4k_page mov ebx,eax or ebx,0x00000007 ;用户程序的页目录和页表,当然是3环能访问的,所以U/S=1;RW=1可读可写;P=1表明已经有物理页 mov [0xfffffff8],ebx ;页目录表倒数第二项(最后一项已经是0x20003了) mov esi,0xfffff000 ;ESI->当前页目录的线性地址 mov edi,0xffffe000 ;EDI->新页目录的线性地址,刚好指向页目录表的倒数第二项,存放了刚才申请的物理地址 mov ecx,1024 ;ECX=要复制的目录项数 cld repe movsd pop ecx pop ebx pop edi pop esi pop es pop ds retf
(10) API段的描述符和选择子都重置并写回,3环的用户程序才能调用
push edi push esi push ecx mov ecx,64 ;检索表中,每条目的比较次数 repe cmpsd ;每次比较4字节 jnz .b6 mov eax,[esi] ;若匹配,则esi恰好指向其后的地址 mov [es:edi-256],eax ;将字符串改写成偏移地址 mov ax,[esi+4] or ax,0000000000000011B ;以用户程序自己的特权级使用调用门 ;故RPL=3 mov [es:edi-252],ax ;回填调用门选择子
(11)把用户程序导入表需要的函数和内核API段的函数根据名称一一对比,发现名称一样的说明匹配上了,把这些内核API的物理地址、选择子等回填到用户程序的导入表,当用户程序调用API时,才能跳转到正确的地方执行:
;重定位SALT mov eax,mem_0_4_gb_seg_sel ;访问任务的4GB虚拟地址空间时用 mov es,eax mov eax,core_data_seg_sel mov ds,eax cld mov ecx,[es:0x0c] ;U-SALT条目数;位于用户程序程序0x0C处 mov edi,[es:0x08] ;U-SALT在4GB空间内的偏移;位于用户程序0x08偏移处 .b4: push ecx push edi mov ecx,salt_items mov esi,salt .b5: push edi push esi push ecx mov ecx,64 ;检索表中,每条目的比较次数 repe cmpsd ;每次比较4字节 jnz .b6 mov eax,[esi] ;esi是内核API地址 mov [es:edi-256],eax ;edi是用户程序导入表的API地址,这里把内核API地址写入用户程序导入表,用户程序调用时直接跳转到内核API处执行 mov ax,[esi+4] ; or ax,0000000000000011B ;以用户程序自己的特权级使用调用门 ;故RPL=3 mov [es:edi-252],ax ;回填调用门选择子到用户程序的导入表
把内核API的偏移和选择子回填到用户程序导入表关键代码:
其他代码都是利用TSS、TR、任务门切换任务相关的。在32位下,利用TSS切换任务效率较低,需要数百个时钟周期,所以windwos和linux并未采用该方式;64位下连intel自己都废弃这种方式,感兴趣的读者可自行分析剩余代码;
4、分页机制要点
- 为了最大程度利用内存,物理页都是挨着连续分配的,第一个页0x00001000,第二个页0x00002000,直到最后一个页0xFFFFF000;不难发现物理页地址必须以000结尾(或则说除以0x1000余数为0);
- 一旦分页开启,所有地址都会被CPU当成线性地址处理,需要先转成物理地址,这是硬件机制决定的,os也不例外,所以最初构造页目录表的时候有一定的技巧,比如页目录表最后一项指向开始,中间0x20800也指向页表第一基址、低512个页目录给用户程序使用、每个用户程序各自赋值一份页目录表和页表;
- 有了分页,分段就不再那么重要了(64位windows段都平坦了)。通过对页目录表和页表的控制,同样可以达到控制程序对物理内存的使用;
5、为方便理解,这里梳理了一下核心的步骤和流程:
MBR引导代码
core_base_address equ 0x00040000 ;常数,内核加载的起始内存地址 core_start_sector equ 0x00000001 ;常数,内核的起始逻辑扇区号 mov ax,cs mov ss,ax mov sp,0x7c00 ;计算GDT所在的逻辑段地址 mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位物理地址 xor edx,edx mov ebx,16 div ebx ;分解成16位逻辑地址 mov ds,eax ;令DS指向该段以进行操作;ds=0x7e0 mov ebx,edx ;段内起始偏移地址,ebx =0x00 ;跳过0#号描述符的槽位 ;创建1#描述符,这是一个数据段,对应0~4GB的线性地址空间 mov dword [ebx+0x08],0x0000ffff ;基地址为0,段界限为0xFFFFF mov dword [ebx+0x0c],0x00cf9200 ;粒度为4KB,存储器段描述符 ;创建保护模式下初始代码段描述符 mov dword [ebx+0x10],0x7c0001ff ;基地址为0x00007c00,界限0x1FF mov dword [ebx+0x14],0x00409800 ;粒度为1个字节,代码段描述符 ;建立保护模式下的堆栈段描述符 ;基地址为0x00007C00,界限0xFFFFE mov dword [ebx+0x18],0x7c00fffe ;粒度为4KB mov dword [ebx+0x1c],0x00cf9600 ;建立保护模式下的显示缓冲区描述符 mov dword [ebx+0x20],0x80007fff ;基地址为0x000B8000,界限0x07FFF mov dword [ebx+0x24],0x0040920b ;粒度为字节 ;初始化描述符表寄存器GDTR mov word [cs: pgdt+0x7c00],39 ;描述符表的界限 lgdt [cs: pgdt+0x7c00] in al,0x92 ;南桥芯片内的端口 or al,0000_0010B out 0x92,al ;打开A20 cli ;中断机制尚未工作 mov eax,cr0 or eax,1 mov cr0,eax ;设置PE位 ;以下进入保护模式... ... jmp dword 0x0010:flush ;16位的描述符选择子:32位偏移 ;清流水线并串行化处理器 [bits 32] flush: mov eax,0x0008 ;以前是实模式的段基址,现在重新加载保护模式的数据段(0..4GB)选择子 mov ds,eax mov eax,0x0018 ;加载堆栈段选择子 mov ss,eax xor esp,esp ;堆栈指针 <- 0 ;以下加载系统核心程序 mov edi,core_base_address mov eax,core_start_sector mov ebx,edi ;起始地址 call read_hard_disk_0 ;以下读取程序的起始部分(一个扇区) ;以下判断整个程序有多大 mov eax,[edi] ;核心程序尺寸 xor edx,edx mov ecx,512 ;512字节每扇区 div ecx or edx,edx jnz @1 ;未除尽,因此结果比实际扇区数少1 dec eax ;已经读了一个扇区,扇区总数减1 @1: or eax,eax ;考虑实际长度≤512个字节的情况 jz setup ;EAX=0 ? ;读取剩余的扇区 mov ecx,eax ;32位模式下的LOOP使用ECX mov eax,core_start_sector inc eax ;从下一个逻辑扇区接着读 @2: call read_hard_disk_0 inc eax loop @2 ;循环读,直到读完整个内核 setup: ;系统各个段在0x00040000内存中重定位 mov esi,[0x7c00+pgdt+0x02] ;不可以在代码段内寻址pgdt,但可以 ;通过4GB的段来访问, esi=0x7e00 ;建立公用例程段描述符 mov eax,[edi+0x04] ;公用例程sys_routine代码段起始汇编地址=0x18;edi=0x00040000 mov ebx,[edi+0x08] ;核心数据段core_data汇编地址=0x01e4 sub ebx,eax ;core_data紧跟着sys_routine,core_data-sys_routine得到sys_routine长度 dec ebx ;core_data的前面,也就是公用例程段sys_routine界限 add eax,edi ;公用例程段基地址:sys_routine=0x18,加上0x00040000得到sys_routine在内存的地址; mov ecx,0x00409800 ;字节粒度的代码段描述符 call make_gdt_descriptor mov [esi+0x28],eax ;描述符低32位eax=0x001801cb,存入0x7e00+0x28处 mov [esi+0x2c],edx ;描述符高32位edx=0x00409804,存入0x7e00+0x2c处 ;00409804`001801cb: 段基址00040018,limit=0x01cb;4:G=0,D/B=1,L=0,AVL=0;9:p=1,DPL=00,s=1;TYPE=8是代码段; ;在0x7e00处原描述符的末尾追加新描述符,原有描述符不变 ;建立核心数据段描述符 mov eax,[edi+0x08] ;核心数据段起始汇编地址 mov ebx,[edi+0x0c] ;核心代码段汇编地址 sub ebx,eax dec ebx ;核心数据段界限 add eax,edi ;核心数据段基地址 mov ecx,0x00409200 ;字节粒度的数据段描述符 call make_gdt_descriptor mov [esi+0x30],eax mov [esi+0x34],edx ;建立核心代码段描述符 mov eax,[edi+0x0c] ;核心代码段core_code起始汇编地址 mov ebx,[edi+0x00] ;程序总长度 sub ebx,eax dec ebx ;核心代码段界限 add eax,edi ;核心代码段基地址 mov ecx,0x00409800 ;字节粒度的代码段描述符 call make_gdt_descriptor mov [esi+0x38],eax mov [esi+0x3c],edx mov word [0x7c00+pgdt],63 ;描述符表的界限; 0x3f,0x7e00:高4byte是GDT基址,低2byte是limit lgdt [0x7c00+pgdt] ;保护模式新增3个段,分别对应内核3个段 jmp far [edi+0x10] ;edi=0x00040000,edi+0x10=core_code ;------------------------------------------------------------------------------- read_hard_disk_0: ;从硬盘读取一个逻辑扇区 ;EAX=逻辑扇区号 ;DS:EBX=目标缓冲区地址 ;返回:EBX=EBX+512 push eax push ecx push edx push eax mov dx,0x1f2 mov al,1 out dx,al ;读取的扇区数 inc dx ;0x1f3 pop eax out dx,al ;LBA地址7~0 inc dx ;0x1f4 mov cl,8 shr eax,cl out dx,al ;LBA地址15~8 inc dx ;0x1f5 shr eax,cl out dx,al ;LBA地址23~16 inc dx ;0x1f6 shr eax,cl or al,0xe0 ;第一硬盘 LBA地址27~24 out dx,al inc dx ;0x1f7 mov al,0x20 ;读命令 out dx,al .waits: in al,dx and al,0x88 cmp al,0x08 jnz .waits ;不忙,且硬盘已准备好数据传输 mov ecx,256 ;总共要读取的字数 mov dx,0x1f0 .readw: in ax,dx mov [ebx],ax add ebx,2 loop .readw pop edx pop ecx pop eax ret ;------------------------------------------------------------------------------- make_gdt_descriptor: ;构造描述符 ;输入:EAX=线性基地址,比如sys_routine=0x00040018; ; EBX=段界限,比如sys_routine=0x1e4-0x18-1=0x1cb ; ECX=属性(各属性位都在原始 比如sys_routine=0x00409800 ; 位置,其它没用到的位置0) ;返回:EDX:EAX=完整的描述符 mov edx,eax shl eax,16 ;eax从0x00040018变为0x00180000; or ax,bx ;描述符前32位(EAX)构造完毕,eax=0x001801cb; and edx,0xffff0000 ;清除基地址中无关的位 edx=0x00040000 rol edx,8 ;edx = 0x04000000 bswap edx ;装配基址的31~24和23~16 (80486+); edx = 0x00000004; 31-24于0-7交换,23-16与8-15交换 xor bx,bx ;ebx=0x00000000 or edx,ebx ;装配段界限的高4位,edx=0x00000004 or edx,ecx ;装配属性 edx=0x00409804 ret ;------------------------------------------------------------------------------- pgdt dw 0 dd 0x00007e00 ;GDT的物理地址 ;------------------------------------------------------------------------------- times 510-($-$$) db 0 db 0x55,0xaa
内核代码:
;以下常量定义部分。内核的大部分内容都应当固定 core_code_seg_sel equ 0x38 ;内核代码段选择子 core_data_seg_sel equ 0x30 ;内核数据段选择子 sys_routine_seg_sel equ 0x28 ;系统公共例程代码段的选择子 video_ram_seg_sel equ 0x20 ;视频显示缓冲区的段选择子 core_stack_seg_sel equ 0x18 ;内核堆栈段选择子 mem_0_4_gb_seg_sel equ 0x08 ;整个0-4GB内存的段的选择子 ;------------------------------------------------------------------------------- ;以下是系统核心的头部,用于加载核心程序 core_length dd core_end ;核心程序总长度#00 sys_routine_seg dd section.sys_routine.start ;系统公用例程段位置#04 core_data_seg dd section.core_data.start ;核心数据段位置#08 core_code_seg dd section.core_code.start ;核心代码段位置#0c core_entry dd start ;核心代码段入口点#10 dw core_code_seg_sel ;=============================================================================== [bits 32] ;=============================================================================== SECTION sys_routine vstart=0 ;系统公共例程代码段 ;------------------------------------------------------------------------------- ;字符串显示例程 put_string: ;显示0终止的字符串并移动光标 ;输入:DS:EBX=串地址 push ecx .getc: mov cl,[ebx] or cl,cl jz .exit call put_char inc ebx jmp .getc .exit: pop ecx retf ;段间返回 ;------------------------------------------------------------------------------- put_char: ;在当前光标处显示一个字符,并推进 ;光标。仅用于段内调用 ;输入:CL=字符ASCII码 pushad ;以下取当前光标位置 mov dx,0x3d4 mov al,0x0e out dx,al inc dx ;0x3d5 in al,dx ;高字 mov ah,al dec dx ;0x3d4 mov al,0x0f out dx,al inc dx ;0x3d5 in al,dx ;低字 mov bx,ax ;BX=代表光标位置的16位数 cmp cl,0x0d ;回车符? jnz .put_0a mov ax,bx mov bl,80 div bl mul bl mov bx,ax jmp .set_cursor .put_0a: cmp cl,0x0a ;换行符? jnz .put_other add bx,80 jmp .roll_screen .put_other: ;正常显示字符 push es mov eax,video_ram_seg_sel ;0x800b8000段的选择子 mov es,eax shl bx,1 mov [es:bx],cl pop es ;以下将光标位置推进一个字符 shr bx,1 inc bx .roll_screen: cmp bx,2000 ;光标超出屏幕?滚屏 jl .set_cursor push ds push es mov eax,video_ram_seg_sel mov ds,eax mov es,eax cld mov esi,0xa0 ;小心!32位模式下movsb/w/d mov edi,0x00 ;使用的是esi/edi/ecx mov ecx,1920 rep movsd mov bx,3840 ;清除屏幕最底一行 mov ecx,80 ;32位程序应该使用ECX .cls: mov word[es:bx],0x0720 add bx,2 loop .cls pop es pop ds mov bx,1920 .set_cursor: mov dx,0x3d4 mov al,0x0e out dx,al inc dx ;0x3d5 mov al,bh out dx,al dec dx ;0x3d4 mov al,0x0f out dx,al inc dx ;0x3d5 mov al,bl out dx,al popad ret ;------------------------------------------------------------------------------- read_hard_disk_0: ;从硬盘读取一个逻辑扇区,也就是每次读512字节;1个页需要读8次 ;EAX=逻辑扇区号 ;DS:EBX=目标缓冲区地址 ;返回:EBX=EBX+512 push eax push ecx push edx push eax mov dx,0x1f2 mov al,1 out dx,al ;读取的扇区数 inc dx ;0x1f3 pop eax out dx,al ;LBA地址7~0 inc dx ;0x1f4 mov cl,8 shr eax,cl out dx,al ;LBA地址15~8 inc dx ;0x1f5 shr eax,cl out dx,al ;LBA地址23~16 inc dx ;0x1f6 shr eax,cl or al,0xe0 ;第一硬盘 LBA地址27~24 out dx,al inc dx ;0x1f7 mov al,0x20 ;读命令 out dx,al .waits: in al,dx and al,0x88 cmp al,0x08 jnz .waits ;不忙,且硬盘已准备好数据传输 mov ecx,256 ;总共要读取的字数 mov dx,0x1f0 .readw: in ax,dx mov [ebx],ax add ebx,2 loop .readw pop edx pop ecx pop eax retf ;段间返回 ;------------------------------------------------------------------------------- put_hex_dword: ;在当前光标处以十六进制形式显示 ;一个双字并推进光标 ;输入:EDX=要转换并显示的数字 ;输出:无 pushad push ds mov ax,core_data_seg_sel ;切换到核心数据段 mov ds,ax mov ebx,bin_hex ;指向核心数据段内的转换表 mov ecx,8 .xlt: rol edx,4 mov eax,edx and eax,0x0000000f xlat push ecx mov cl,al call put_char pop ecx loop .xlt pop ds popad retf ;------------------------------------------------------------------------------- set_up_gdt_descriptor: ;在GDT内安装一个新的描述符,还是在0x7e00的地方 ;输入:EDX:EAX=描述符 ;输出:CX=描述符的选择子 push eax push ebx push edx push ds push es mov ebx,core_data_seg_sel ;切换到核心数据段 mov ds,ebx sgdt [pgdt] ;以便开始处理GDT mov ebx,mem_0_4_gb_seg_sel mov es,ebx movzx ebx,word [pgdt] ;GDT界限 inc bx ;GDT总字节数,也是下一个描述符偏移 add ebx,[pgdt+2] ;下一个描述符的线性地址 mov [es:ebx],eax ; mov [es:ebx+4],edx ; add word [pgdt],8 ;增加一个描述符的大小 lgdt [pgdt] ;对GDT的更改生效 mov ax,[pgdt] ;得到GDT界限值 xor dx,dx mov bx,8 div bx ;除以8,去掉余数 mov cx,ax shl cx,3 ;将索引号移到正确位置 pop es pop ds pop edx pop ebx pop eax retf ;------------------------------------------------------------------------------- make_seg_descriptor: ;构造存储器和系统的段描述符 ;输入:EAX=线性基地址 ; EBX=段界限 ; ECX=属性。各属性位都在原始 ; 位置,无关的位清零 ;返回:EDX:EAX=描述符 mov edx,eax shl eax,16 or ax,bx ;描述符前32位(EAX)构造完毕 and edx,0xffff0000 ;清除基地址中无关的位 rol edx,8 bswap edx ;装配基址的31~24和23~16 (80486+) xor bx,bx or edx,ebx ;装配段界限的高4位 or edx,ecx ;装配属性 retf ;------------------------------------------------------------------------------- make_gate_descriptor: ;构造门的描述符(调用门等) ;输入:EAX=门代码在段内偏移地址 ; BX=门代码所在段的选择子 ; CX=段类型及属性等(各属 ; 性位都在原始位置) ;返回:EDX:EAX=完整的描述符 push ebx push ecx mov edx,eax and edx,0xffff0000 ;得到偏移地址高16位 or dx,cx ;组装属性部分到EDX and eax,0x0000ffff ;得到偏移地址低16位 shl ebx,16 or eax,ebx ;组装段选择子部分 pop ecx pop ebx retf ;------------------------------------------------------------------------------- allocate_a_4k_page: ;分配一个4KB的页 ;输入:无 ;输出:EAX=页的物理地址 push ebx push ecx push edx push ds mov eax,core_data_seg_sel mov ds,eax xor eax,eax .b1: ;遍历page_bit_map,找到第一个标识是0的位,说明该页还未使用 bts [page_bit_map],eax ;[page_bit_map]第eax的位复制给CF,同时置1 jnc .b2 ;CF=0,说明找到了空闲的物理页;物理页索引存放在eax inc eax ;没有找到,eax+1继续找 cmp eax,page_map_len*8 ;遍历到page_bit_map末尾了吗? jl .b1 ;没有就从头继续找 mov ebx,message_3 call sys_routine_seg_sel:put_string hlt ;没有可以分配的页,停机 .b2: shl eax,12 ;eax存放了空闲的物理页索引,乘以4096(0x1000)就是地址 pop ds pop edx pop ecx pop ebx ret ;------------------------------------------------------------------------------- alloc_inst_a_page: ;给指定的线性地址挂载物理页 ;层级分页结构中 ;输入:EBX=页的线性地址,比如0x80104000 push eax push ebx push esi push ds mov eax,mem_0_4_gb_seg_sel mov ds,eax ;检查该线性地址所对应的页表是否存在;把ebx高10位作为PDT的索引查找PTE; mov esi,ebx ;esi=0x80104000 and esi,0xffc00000 ;只保留最高的10位,低22位清零,得到PDT的索引,esi=0x80000000 shr esi,20 ;高12位移到低12位:得到页目录索引,并乘以4,得到PTE在PDE内的偏移地址;esi=0x00000800 or esi,0xfffff000 ;页目录自身的线性地址+表内偏移;最高20位置1的线性地址,转换成物理地址=PDT基址(这里是0x20000)+esi,相当于最低3字节就是PDT内的偏移,高20位置1确保物理地址还是落在PDT内;esi=0xfffff800 test dword [esi],0x00000001 ;P位是否为“1”.如果PDT某项有PTE,结尾不会是0;如果是0,说明还未挂载物理页;[esi]=0x00000003,最后4位是0011; jnz .b1 ;否已经有对应的页表 ;创建该线性地址所对应的页表 call allocate_a_4k_page ;分配一个页做为页表 or eax,0x00000007 ;该页的属性:U/S=1,允许3环访问;RW=1,可读可写;P=1,表明有物理页了 mov [esi],eax ;在页目录中登记该物理地址 .b1: ;不论是否执行JNZ .b1,代码最终会走到这里来 ;开始访问该线性地址所对应的页表 mov esi,ebx ;esi=0x80104000 shr esi,10 ;高22位移到低22位,esi=0x00200410 and esi,0x003ff000 ;只保留原线性地址高10位,也就是PDT的偏移;esi=0x00200000 or esi,0xffc00000 ;原线性地址最高10位保存在esi的中间10位,即11-20位;高10位置1,这样在PDT内查的时候能得到0x21003,也就是页表的基址; ;得到该线性地址在页表内的对应条目(页表项) and ebx,0x003ff000 ;ebx=0x00104000,保留原线性地址中间10位 shr ebx,10 ;相当于右移12位,再乘以4;原线性地址中间10位右移到低2~11位,得到页表内的偏移;ebx=0x410 or esi,ebx ;页表项的线性地址;原线性地址的高10位、中间10位依次右移,现在是从2~20位,高11位置1;原线性地址高10位用来作为页表的偏移,中间10位用来做页表的偏移; esi=0xFFF00410 call allocate_a_4k_page ;分配一个页,这才是要安装的页 or eax,0x00000007 mov [esi],eax pop ds pop esi pop ebx pop eax retf ;------------------------------------------------------------------------------- create_copy_cur_pdir: ;创建新页目录,并复制当前页目录内容 ;输入:无 ;输出:EAX=新页目录的物理地址 push ds push es push esi push edi push ebx push ecx mov ebx,mem_0_4_gb_seg_sel mov ds,ebx mov es,ebx call allocate_a_4k_page mov ebx,eax or ebx,0x00000007 ;用户程序的页目录和页表,当然是3环能访问的,所以U/S=1;RW=1可读可写;P=1表明已经有物理页 mov [0xfffffff8],ebx ;页目录表倒数第二项(最后一项已经是0x20003了) mov esi,0xfffff000 ;ESI->当前页目录的线性地址 mov edi,0xffffe000 ;EDI->新页目录的线性地址,刚好指向页目录表的倒数第二项,存放了刚才申请的物理地址 mov ecx,1024 ;ECX=要复制的目录项数 cld repe movsd pop ecx pop ebx pop edi pop esi pop es pop ds retf ;------------------------------------------------------------------------------- terminate_current_task: ;终止当前任务 ;注意,执行此例程时,当前任务仍在 ;运行中。此例程其实也是当前任务的 ;一部分 mov eax,core_data_seg_sel mov ds,eax pushfd pop edx test dx,0100_0000_0000_0000B ;测试NT位 jnz .b1 ;当前任务是嵌套的,到.b1执行iretd jmp far [program_man_tss] ;程序管理器任务 .b1: iretd sys_routine_end: ;=============================================================================== SECTION core_data vstart=0 ;系统核心的数据段 ;------------------------------------------------------------------------------- pgdt dw 0 ;用于设置和修改GDT dd 0 ;为了简化,这里只用2M内存,有512个物理页;已经占用的置1,没用的置0 page_bit_map db 0xff,0xff,0xff,0xff,0xff,0x55,0x55,0xff ;低地址基本都用光了,高地址还空着 db 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff db 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff db 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff db 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55 db 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 db 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 db 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 page_map_len equ $-page_bit_map ;符号地址检索表,类似于导出表,详细记录了可供第三方调用的函数名、函数地址 salt: salt_1 db '@PrintString' ;从@PrintString开始,长度是12字节 times 256-($-salt_1) db 0 ;剩余256-12=244字节填0;API函数名最长不超过256字节 dd put_string ;函数在API段内的偏移 dw sys_routine_seg_sel ;API段的选择子,根据后面这6字节可以直接调用API函数 salt_2 db '@ReadDiskData' times 256-($-salt_2) db 0 dd read_hard_disk_0 dw sys_routine_seg_sel salt_3 db '@PrintDwordAsHexString' times 256-($-salt_3) db 0 dd put_hex_dword dw sys_routine_seg_sel salt_4 db '@TerminateProgram' times 256-($-salt_4) db 0 dd terminate_current_task dw sys_routine_seg_sel salt_item_len equ $-salt_4 salt_items equ ($-salt)/salt_item_len message_0 db ' Working in system core,protect mode.' db 0x0d,0x0a,0 message_1 db ' Paging is enabled.System core is mapped to' db ' address 0x80000000.',0x0d,0x0a,0 message_2 db 0x0d,0x0a db ' System wide CALL-GATE mounted.',0x0d,0x0a,0 message_3 db '********No more pages********',0 message_4 db 0x0d,0x0a,' Task switching...@_@',0x0d,0x0a,0 message_5 db 0x0d,0x0a,' Processor HALT.',0 bin_hex db '0123456789ABCDEF' ;put_hex_dword子过程用的查找表 core_buf times 512 db 0 ;内核用的缓冲区 cpu_brnd0 db 0x0d,0x0a,' ',0 cpu_brand times 52 db 0 cpu_brnd1 db 0x0d,0x0a,0x0d,0x0a,0 ;任务控制块链 tcb_chain dd 0 ;内核信息 core_next_laddr dd 0x80100000 ;内核空间中下一个可分配的线性地址;每次在线性地址分配一块内存,该值就会增加; program_man_tss dd 0 ;程序管理器的TSS描述符选择子 dw 0 core_data_end: ;=============================================================================== SECTION core_code vstart=0 ;------------------------------------------------------------------------------- fill_descriptor_in_ldt: ;在LDT内安装一个新的描述符 ;输入:EDX:EAX=描述符 ; EBX=TCB基地址 ;输出:CX=描述符的选择子 push eax push edx push edi push ds mov ecx,mem_0_4_gb_seg_sel mov ds,ecx mov edi,[ebx+0x0c] ;获得LDT基地址 xor ecx,ecx mov cx,[ebx+0x0a] ;获得LDT界限 inc cx ;LDT的总字节数,即新描述符偏移地址 mov [edi+ecx+0x00],eax mov [edi+ecx+0x04],edx ;安装描述符 add cx,8 dec cx ;得到新的LDT界限值 mov [ebx+0x0a],cx ;更新LDT界限值到TCB mov ax,cx xor dx,dx mov cx,8 div cx mov cx,ax shl cx,3 ;左移3位,并且 or cx,0000_0000_0000_0100B ;使TI位=1,指向LDT,最后使RPL=00 pop ds pop edi pop edx pop eax ret ;------------------------------------------------------------------------------- load_relocate_program: ;加载并重定位用户程序 ;输入: PUSH 逻辑扇区号 ; PUSH 任务控制块基地址 ;输出:无 pushad push ds push es mov ebp,esp ;为访问通过堆栈传递的参数做准备 mov ecx,mem_0_4_gb_seg_sel mov es,ecx ;清空当前页目录的前半部分(对应低2GB的局部地址空间) mov ebx,0xfffff000 xor esi,esi .b1: mov dword [es:ebx+esi*4],0x00000000 inc esi cmp esi,512 jl .b1 ;以下开始分配内存并加载用户程序 mov eax,core_data_seg_sel mov ds,eax ;切换DS到内核数据段 mov eax,[ebp+12*4] ;从堆栈中取出用户程序起始扇区号 mov ebx,core_buf ;读取程序头部数据 call sys_routine_seg_sel:read_hard_disk_0 ;以下判断整个程序有多大 mov eax,[core_buf] ;程序尺寸 mov ebx,eax and ebx,0xfffff000 ;使之4KB对齐 add ebx,0x1000 test eax,0x00000fff ;程序的大小正好是4KB的倍数吗? cmovnz eax,ebx ;不是。使用凑整的结果 mov ecx,eax shr ecx,12 ;程序占用的总4KB页数,即用户程序需要几个页加载 mov eax,mem_0_4_gb_seg_sel ;切换DS到0-4GB的段 mov ds,eax mov eax,[ebp+12*4] ;起始扇区号 mov esi,[ebp+11*4] ;从堆栈中取得TCB的基地址 .b2: mov ebx,[es:esi+0x06] ;取得可用的线性地址 add dword [es:esi+0x06],0x1000 ;线性地址分配后加0x1000,下次从这里继续申请新内存 call sys_routine_seg_sel:alloc_inst_a_page push ecx mov ecx,8 .b3: call sys_routine_seg_sel:read_hard_disk_0 inc eax loop .b3 pop ecx loop .b2 ;在内核地址空间内创建用户任务的TSS mov eax,core_data_seg_sel ;切换DS到内核数据段 mov ds,eax mov ebx,[core_next_laddr] ;用户任务的TSS必须在全局空间上分配 call sys_routine_seg_sel:alloc_inst_a_page add dword [core_next_laddr],4096 mov [es:esi+0x14],ebx ;在TCB中填写TSS的线性地址 mov word [es:esi+0x12],103 ;在TCB中填写TSS的界限值 ;在用户任务的局部地址空间内创建LDT mov ebx,[es:esi+0x06] ;从TCB中取得可用的线性地址 add dword [es:esi+0x06],0x1000 call sys_routine_seg_sel:alloc_inst_a_page mov [es:esi+0x0c],ebx ;填写LDT线性地址到TCB中 ;建立程序代码段描述符 mov eax,0x00000000 mov ebx,0x000fffff mov ecx,0x00c0f800 ;4KB粒度的代码段描述符,特权级3 call sys_routine_seg_sel:make_seg_descriptor mov ebx,esi ;TCB的基地址 call fill_descriptor_in_ldt or cx,0000_0000_0000_0011B ;设置选择子的特权级为3 mov ebx,[es:esi+0x14] ;从TCB中获取TSS的线性地址 mov [es:ebx+76],cx ;填写TSS的CS域 ;建立程序数据段描述符 mov eax,0x00000000 mov ebx,0x000fffff mov ecx,0x00c0f200 ;4KB粒度的数据段描述符,特权级3 call sys_routine_seg_sel:make_seg_descriptor mov ebx,esi ;TCB的基地址 call fill_descriptor_in_ldt or cx,0000_0000_0000_0011B ;设置选择子的特权级为3 mov ebx,[es:esi+0x14] ;从TCB中获取TSS的线性地址 mov [es:ebx+84],cx ;填写TSS的DS域 mov [es:ebx+72],cx ;填写TSS的ES域 mov [es:ebx+88],cx ;填写TSS的FS域 mov [es:ebx+92],cx ;填写TSS的GS域 ;将数据段作为用户任务的3特权级固有堆栈 mov ebx,[es:esi+0x06] ;从TCB中取得可用的线性地址 add dword [es:esi+0x06],0x1000 call sys_routine_seg_sel:alloc_inst_a_page mov ebx,[es:esi+0x14] ;从TCB中获取TSS的线性地址 mov [es:ebx+80],cx ;填写TSS的SS域 mov edx,[es:esi+0x06] ;堆栈的高端线性地址 mov [es:ebx+56],edx ;填写TSS的ESP域 ;在用户任务的局部地址空间内创建0特权级堆栈 mov ebx,[es:esi+0x06] ;从TCB中取得可用的线性地址 add dword [es:esi+0x06],0x1000 call sys_routine_seg_sel:alloc_inst_a_page mov eax,0x00000000 mov ebx,0x000fffff mov ecx,0x00c09200 ;4KB粒度的堆栈段描述符,特权级0 call sys_routine_seg_sel:make_seg_descriptor mov ebx,esi ;TCB的基地址 call fill_descriptor_in_ldt or cx,0000_0000_0000_0000B ;设置选择子的特权级为0 mov ebx,[es:esi+0x14] ;从TCB中获取TSS的线性地址 mov [es:ebx+8],cx ;填写TSS的SS0域 mov edx,[es:esi+0x06] ;堆栈的高端线性地址 mov [es:ebx+4],edx ;填写TSS的ESP0域 ;在用户任务的局部地址空间内创建1特权级堆栈 mov ebx,[es:esi+0x06] ;从TCB中取得可用的线性地址 add dword [es:esi+0x06],0x1000 call sys_routine_seg_sel:alloc_inst_a_page mov eax,0x00000000 mov ebx,0x000fffff mov ecx,0x00c0b200 ;4KB粒度的堆栈段描述符,特权级1 call sys_routine_seg_sel:make_seg_descriptor mov ebx,esi ;TCB的基地址 call fill_descriptor_in_ldt or cx,0000_0000_0000_0001B ;设置选择子的特权级为1 mov ebx,[es:esi+0x14] ;从TCB中获取TSS的线性地址 mov [es:ebx+16],cx ;填写TSS的SS1域 mov edx,[es:esi+0x06] ;堆栈的高端线性地址 mov [es:ebx+12],edx ;填写TSS的ESP1域 ;在用户任务的局部地址空间内创建2特权级堆栈 mov ebx,[es:esi+0x06] ;从TCB中取得可用的线性地址 add dword [es:esi+0x06],0x1000 call sys_routine_seg_sel:alloc_inst_a_page mov eax,0x00000000 mov ebx,0x000fffff mov ecx,0x00c0d200 ;4KB粒度的堆栈段描述符,特权级2 call sys_routine_seg_sel:make_seg_descriptor mov ebx,esi ;TCB的基地址 call fill_descriptor_in_ldt or cx,0000_0000_0000_0010B ;设置选择子的特权级为2 mov ebx,[es:esi+0x14] ;从TCB中获取TSS的线性地址 mov [es:ebx+24],cx ;填写TSS的SS2域 mov edx,[es:esi+0x06] ;堆栈的高端线性地址 mov [es:ebx+20],edx ;填写TSS的ESP2域 ;重定位SALT mov eax,mem_0_4_gb_seg_sel ;访问任务的4GB虚拟地址空间时用 mov es,eax mov eax,core_data_seg_sel mov ds,eax cld mov ecx,[es:0x0c] ;U-SALT条目数 mov edi,[es:0x08] ;U-SALT在4GB空间内的偏移 .b4: push ecx push edi mov ecx,salt_items mov esi,salt .b5: push edi push esi push ecx mov ecx,64 ;检索表中,每条目的比较次数 repe cmpsd ;每次比较4字节 jnz .b6 mov eax,[esi] ;若匹配,则esi恰好指向其后的地址 mov [es:edi-256],eax ;将字符串改写成偏移地址 mov ax,[esi+4] or ax,0000000000000011B ;以用户程序自己的特权级使用调用门 ;故RPL=3 mov [es:edi-252],ax ;回填调用门选择子 .b6: pop ecx pop esi add esi,salt_item_len pop edi ;从头比较 loop .b5 pop edi add edi,256 pop ecx loop .b4 ;在GDT中登记LDT描述符 mov esi,[ebp+11*4] ;从堆栈中取得TCB的基地址 mov eax,[es:esi+0x0c] ;LDT的起始线性地址 movzx ebx,word [es:esi+0x0a] ;LDT段界限 mov ecx,0x00408200 ;LDT描述符,特权级0 call sys_routine_seg_sel:make_seg_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [es:esi+0x10],cx ;登记LDT选择子到TCB中 mov ebx,[es:esi+0x14] ;从TCB中获取TSS的线性地址 mov [es:ebx+96],cx ;填写TSS的LDT域 mov word [es:ebx+0],0 ;反向链=0 mov dx,[es:esi+0x12] ;段长度(界限) mov [es:ebx+102],dx ;填写TSS的I/O位图偏移域 mov word [es:ebx+100],0 ;T=0 mov eax,[es:0x04] ;从任务的4GB地址空间获取入口点 mov [es:ebx+32],eax ;填写TSS的EIP域 pushfd pop edx mov [es:ebx+36],edx ;填写TSS的EFLAGS域 ;在GDT中登记TSS描述符 mov eax,[es:esi+0x14] ;从TCB中获取TSS的起始线性地址 movzx ebx,word [es:esi+0x12] ;段长度(界限) mov ecx,0x00408900 ;TSS描述符,特权级0 call sys_routine_seg_sel:make_seg_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [es:esi+0x18],cx ;登记TSS选择子到TCB ;创建用户任务的页目录 ;注意!页的分配和使用是由页位图决定的,可以不占用线性地址空间 call sys_routine_seg_sel:create_copy_cur_pdir mov ebx,[es:esi+0x14] ;从TCB中获取TSS的线性地址 mov dword [es:ebx+28],eax ;填写TSS的CR3(PDBR)域 pop es ;恢复到调用此过程前的es段 pop ds ;恢复到调用此过程前的ds段 popad ret 8 ;丢弃调用本过程前压入的参数 ;------------------------------------------------------------------------------- append_to_tcb_link: ;在TCB链上追加任务控制块 ;输入:ECX=TCB线性基地址 push eax push edx push ds push es mov eax,core_data_seg_sel ;令DS指向内核数据段 mov ds,eax mov eax,mem_0_4_gb_seg_sel ;令ES指向0..4GB段 mov es,eax mov dword [es: ecx+0x00],0 ;当前TCB指针域清零,以指示这是最 ;后一个TCB mov eax,[tcb_chain] ;TCB表头指针 or eax,eax ;链表为空? jz .notcb .searc: mov edx,eax mov eax,[es: edx+0x00] or eax,eax jnz .searc mov [es: edx+0x00],ecx jmp .retpc .notcb: mov [tcb_chain],ecx ;若为空表,直接令表头指针指向TCB .retpc: pop es pop ds pop edx pop eax ret ;------------------------------------------------------------------------------- start: mov ecx,core_data_seg_sel ;令DS指向核心数据段 mov ds,ecx mov ecx,mem_0_4_gb_seg_sel ;令ES指向4GB数据段 mov es,ecx mov ebx,message_0 call sys_routine_seg_sel:put_string ;显示处理器品牌信息 mov eax,0x80000002 cpuid mov [cpu_brand + 0x00],eax mov [cpu_brand + 0x04],ebx mov [cpu_brand + 0x08],ecx mov [cpu_brand + 0x0c],edx mov eax,0x80000003 cpuid mov [cpu_brand + 0x10],eax mov [cpu_brand + 0x14],ebx mov [cpu_brand + 0x18],ecx mov [cpu_brand + 0x1c],edx mov eax,0x80000004 cpuid mov [cpu_brand + 0x20],eax mov [cpu_brand + 0x24],ebx mov [cpu_brand + 0x28],ecx mov [cpu_brand + 0x2c],edx mov ebx,cpu_brnd0 ;显示处理器品牌信息 call sys_routine_seg_sel:put_string mov ebx,cpu_brand call sys_routine_seg_sel:put_string mov ebx,cpu_brnd1 call sys_routine_seg_sel:put_string ;准备打开分页机制 ;创建系统内核的页目录表PDT ;页目录表清零 mov ecx,1024 ;1024个目录项PDE mov ebx,0x00020000 ;页目录的物理地址 xor esi,esi .b1: mov dword [es:ebx+esi],0x00000000 ;页目录表项清零 add esi,4 loop .b1 ;在页目录内创建指向页目录自己的目录项,最后一项指向自己,那么线性地址高20位是0xFFFFF的时候,转成物理地址就是页目录自己 mov dword [es:ebx+4092],0x00020003 ;页目录的第一项,内核第一个页表的物理地址:0x00021000 mov dword [es:ebx+0],0x00021003 ;写入目录项(页表的物理地址和属性) ;创建与上面那个目录项相对应的页表,初始化页表项 mov ebx,0x00021000 ;页表的物理地址 xor eax,eax ;起始页的物理地址 xor esi,esi ;esi=0 .b2: mov edx,eax ;edx=eax; eax=0x1000*n or edx,0x00000003 ;edx=0x1000*n+3;u/s=1,允许所有特权级别的程序访问; mov [es:ebx+esi*4],edx ;登记页的物理地址; 0x21000~0x21400都是PTE,隐射从0~1MB(256*4096=1Mb)的物理地址; add eax,0x1000 ;下一个相邻页的物理地址 inc esi cmp esi,256 ;仅低端1MB内存对应的页才是有效的 jl .b2 .b3: ;其余的页表项置为无效 mov dword [es:ebx+esi*4],0x00000000 ;0x21400~(0x21400+(1024-256)*4=0x22000)清零; inc esi cmp esi,1024 jl .b3 ;令CR3寄存器指向页目录,并正式开启页功能 mov eax,0x00020000 ;PCD=PWT=0,PDT基址=0x00020000 mov cr3,eax mov eax,cr0 or eax,0x80000000 mov cr0,eax ;开启分页机制 ;在页目录内创建与线性地址0x80000000对应的目录项,有了这个项,0x800000000才会被映射到0x21000的PET; 线性地址0x80000000~0x800FFFFF映射的物理地址:0x00000~0xFFFFF mov ebx,0xfffff000 ;页目录自己的线性地址;高5字节都是F,低3字节就是PDT内的偏移 mov esi,0x80000000 ;映射的起始地址 shr esi,22 ;取线性地址高10位(目录索引),esi=0x200 shl esi,2 ;索引乘以4得到偏移 mov dword [es:ebx+esi],0x00021003 ;写入目录项(页表的物理地址和属性)es:ebx+esi = 0xFFFFF800 ;将GDT中的段描述符映射到线性地址0x80000000 sgdt [pgdt] mov ebx,[pgdt+2] ;ebx存放GDT的base or dword [es:ebx+0x10+4],0x80000000 ; or dword [es:ebx+0x18+4],0x80000000 ;内核堆栈段 or dword [es:ebx+0x20+4],0x80000000 ;视频显示缓冲区 or dword [es:ebx+0x28+4],0x80000000 ;API段 or dword [es:ebx+0x30+4],0x80000000 ;内核数据段 or dword [es:ebx+0x38+4],0x80000000 ;内核代码段 add dword [pgdt+2],0x80000000 ;GDTR也用的是线性地址 lgdt [pgdt] jmp core_code_seg_sel:flush ;刷新段寄存器CS,启用高端线性地址 flush: mov eax,core_stack_seg_sel mov ss,eax mov eax,core_data_seg_sel mov ds,eax mov ebx,message_1 call sys_routine_seg_sel:put_string ;以下开始安装为整个系统服务的调用门。特权级之间的控制转移必须使用门 mov edi,salt ;C-SALT表的起始位置,内核API函数导出表,有函数名称、函数在API段内的偏移、API段的选择子 mov ecx,salt_items ;C-SALT表的条目数量,ecx=4 .b4: push ecx mov eax,[edi+256] ;该条目入口点的32位偏移地址;API函数的段内偏移地址 mov bx,[edi+260] ;该条目入口点的段选择子 ;API函数所在段的选择子 mov cx,1_11_0_1100_000_00000B ;特权级3的调用门(3以上的特权级才 ;允许访问),0个参数(因为用寄存器 ;传递参数,而没有用栈) call sys_routine_seg_sel:make_gate_descriptor ;返回完整的描述符,保存在EDX:EAX; call sys_routine_seg_sel:set_up_gdt_descriptor ;上一步构造好的门描述符写回GDT表 mov [edi+260],cx ;将返回的门描述符选择子回填 add edi,salt_item_len ;指向下一个C-SALT条目 pop ecx loop .b4 ;对门进行测试 mov ebx,message_2 call far [salt_1+256] ;通过门显示信息(偏移量将被忽略);salt_1+256,低4字节是段内偏移,高2字节是选择子 ;为程序管理器的TSS分配内存空间 mov ebx,[core_next_laddr] ;从0x80100000开始分配,查找还没使用的线性地址 call sys_routine_seg_sel:alloc_inst_a_page ;给线性地址挂载物理页 add dword [core_next_laddr],4096 ;线性地址增加0x1000; ;在程序管理器的TSS中设置必要的项目;该线性地址已经挂载物理页,可以正常使用了 mov word [es:ebx+0],0 ;反向链=0 mov eax,cr3 mov dword [es:ebx+28],eax ;登记CR3(PDBR) mov word [es:ebx+96],0 ;没有LDT。处理器允许没有LDT的任务。 mov word [es:ebx+100],0 ;T=0 mov word [es:ebx+102],103 ;没有I/O位图。0特权级事实上不需要。 ;创建程序管理器的TSS描述符,并安装到GDT中 mov eax,ebx ;TSS的起始线性地址 mov ebx,103 ;段长度(界限) mov ecx,0x00408900 ;TSS描述符,特权级0 call sys_routine_seg_sel:make_seg_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [program_man_tss+4],cx ;保存程序管理器的TSS描述符选择子 ;任务寄存器TR中的内容是任务存在的标志,该内容也决定了当前任务是谁。 ;下面的指令为当前正在执行的0特权级任务“程序管理器”后补手续(TSS)。 ltr cx ;现在可认为“程序管理器”任务正执行中 ;创建用户任务的任务控制块,类似windows下的进程控制块PCB mov ebx,[core_next_laddr] ;从0x80100000开始分配 call sys_routine_seg_sel:alloc_inst_a_page add dword [core_next_laddr],4096 mov dword [es:ebx+0x06],0 ;用户任务局部空间的分配从0开始。 mov word [es:ebx+0x0a],0xffff ;登记LDT初始的界限到TCB中 mov ecx,ebx call append_to_tcb_link ;将此TCB添加到TCB链中,类似windows下EPROCESS的链条 push dword 50 ;用户程序位于逻辑50扇区 push ecx ;压入任务控制块起始线性地址 call load_relocate_program mov ebx,message_4 call sys_routine_seg_sel:put_string call far [es:ecx+0x14] ;执行任务切换。 mov ebx,message_5 call sys_routine_seg_sel:put_string hlt core_code_end: ;------------------------------------------------------------------------------- SECTION core_trail ;------------------------------------------------------------------------------- core_end:
用户程序:
program_length dd program_end ;程序总长度#0x00 = 0x1F88E entry_point dd start ;程序入口点#0x04 = 0x1F85B salt_position dd salt_begin ;SALT表起始偏移量#0x08 =0x10 salt_items dd (salt_end-salt_begin)/256 ;SALT条目数#0x0C = 0x1F8 ;------------------------------------------------------------------------------- ;符号地址检索表 salt_begin: PrintString db '@PrintString' ;内核代码会对导入表做重定位,把内核API的实际偏移、选择子写回,覆盖@PrintString前6个字节,下面就可以直接通过call far [PrintString]调用内核API函数了 times 256-($-PrintString) db 0 TerminateProgram db '@TerminateProgram' times 256-($-TerminateProgram) db 0 ;------------------------------------------------------------------------------- reserved times 256*500 db 0 ;保留一个空白区,以演示分页 ;------------------------------------------------------------------------------- ReadDiskData db '@ReadDiskData' times 256-($-ReadDiskData) db 0 PrintDwordAsHex db '@PrintDwordAsHexString' times 256-($-PrintDwordAsHex) db 0 salt_end: message_0 db 0x0d,0x0a, db ' ............User task is running with ' db 'paging enabled!............',0x0d,0x0a,0 space db 0x20,0x20,0 ;------------------------------------------------------------------------------- [bits 32] ;------------------------------------------------------------------------------- start: mov ebx,message_0 call far [PrintString] xor esi,esi mov ecx,88 .b1: mov ebx,space call far [PrintString] mov edx,[esi*4] call far [PrintDwordAsHex] inc esi loop .b1 call far [TerminateProgram] ;退出,并将控制权返回到核心 ;------------------------------------------------------------------------------- program_end:
扩展一下:
这里用PE或ELF文件重定位做个对比:windows下加载dll一般都会重定位函数call、全局变量等,这时就要依赖PE文件头中的重定位表了,和这里的虚拟地址到物理地址的“重定位”异曲同工,都是靠某种表格映射来实现的!