实模式到保护模式:第13章读书笔记
这一章的内容是加载内核程序和用户程序的基本流程
关于程序的相关信息一般是放在文件的开头,程序本来就是从最开头的读取,所以放在开头也就符合一般的思维习惯,这本章中,作者创建了一个简单的文件头,用于存储该程序的信息,用于加载程序的使用
在这一章中,作者代码演示的加载内核和用户程序的方式可能与真正的操作系统不同,但是这个过程大体是相似的,只是真实操作系统的头部更为复杂
1. 接下来我来读一下内核加载的程序代码:
;代码清单13-2
;文件名:c13_core.asm
;文件说明:保护模式微型核心程序
;创建日期:2011-10-26 12:11
;以下常量定义部分。内核的大部分内容都应当固定
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] flush: mov eax,0x0008 ;加载数据段(0..4GB)选择子 mov ds,eax mov eax,0x0018 ;加载堆栈段选择子 mov ss,eax xor esp,esp ;堆栈指针 <- 0 mov edi,core_base_address ;读取出来的扇区将存储于何处
初始化ss和ds的值,将其初始化对应的段选择子
mov eax,core_start_sector ;内核的起始扇区号 mov ebx,edi ;起始地址 call read_hard_disk_0 ;以下读取程序的起始部分(一个扇区)
;以下判断整个程序有多大 ;内核程序将它所具有的大小填入到了第一个扇区的第一个字节处,只需要读取出来,将
;其与512相除,即能得到扇区的个数
mov eax,[edi] ;核心程序尺寸
首先,将内核将存入的内存的位置加载到edi寄存器中,将待读取扇区加载eax中去,调用read_hard_disk_0例程读取内核的第一个扇区,读取完毕之后,因为内核的首地址便存储了内核程序的长度,所以通过mov eax,[edi]能够将内核程序的长度存放到eax中
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 ;循环读,直到读完整个内核
div 除数:指令默认被除数为edx:eax,商存储在eax中,edx存储余数
首先将edx的值清零,将512存入ecx中,作为除数,因为一个扇区存储512个字节
上面的代码成功得到总扇区数目,并且成功读取了剩余的扇区
;到此处便将内核程序全部从硬盘中读取出来了,也是位于内存0x40000处的内存开始位置处
setup: mov esi,[0x7c00+pgdt+0x02] ;不可以在代码段内寻址pgdt,但可以 ;通过4GB的段来访问 ;建立公用例程段描述符 mov eax,[edi+0x04] ;公用例程代码段起始汇编地址 mov ebx,[edi+0x08] ;核心数据段汇编地址 sub ebx,eax dec ebx ;公用例程段界限 add eax,edi ;公用例程段基地址,edi是内核加载的位置,而eax是公用例程段定义的基地址 mov ecx,0x00409800 ;字节粒度的代码段描述符 call make_gdt_descriptor mov [esi+0x28],eax mov [esi+0x2c],edx ;建立核心数据段描述符 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] ;核心代码段起始汇编地址 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 ;描述符表的界限 lgdt [0x7c00+pgdt]
上面的代码创建了属于内核的数据段,代码段,公共例程段的值,放入gdt表的位置与内核声明的常数的值一致,在最后将新的gdt表的界限写入对应的内存区域中,lgdt指令将新的gdt表的基地址和表的大小加载到GDT寄存器中去
jmp far [edi+0x10]
可以看一下上面内核头部的定义,内核中偏移量为0x10中存储的值为
core_entry dd start ;核心代码段入口点#10 dw core_code_seg_sel
因此通过远跳转指令最终跳转到内核标号为start的位置继续执行
2. 进入内核程序的start开始运行,接下来进入内核加载用户程序的步骤
mov esi,50 ;用户程序位于逻辑50扇区
call load_relocate_program ;加载用户程序
将用户程序所在扇区放入esi中,开始加载用户程序,接下来调用load_relocate_program来执行加载用户程序
load_relocate_program: push ebx push ecx push edx push esi push edi push ds push es mov eax,core_data_seg_sel mov ds,eax mov eax,esi ;读取程序头部数据 mov ebx,core_buf call sys_routine_seg_sel:read_hard_disk_0 ;读取出来的将会被放到core_buf缓冲区中,读第一个扇区 ;以下判断整个程序有多大 mov eax,[core_buf] ;程序尺寸,用户程序的第一个字节记录了该程序的大小 mov ebx,eax and ebx,0xfffffe00 ;使之512字节对齐(能被512整除的数, add ebx,512 ;低9位都为0 test eax,0x000001ff ;程序的大小正好是512的倍数吗? cmovnz eax,ebx ;不是。使用凑整的结果
首先,ds指向内核数据段的选择子,加载用户程序的第一个扇区到内核数据段中的缓冲区中,通过读取第一个字节,得到该用户程序的长度,其中用到了cmovnz指令,避免了跳转指令
mov ecx,eax ;实际需要申请的内存数量,eax存储了该程序大小,而且该大小是512的整数倍 call sys_routine_seg_sel:allocate_memory ;返回时,ecx返回了分配内存的首地址 mov ebx,ecx ;ebx -> 申请到的内存首地址 push ebx ;保存该首地址,被分配内存的首地址 xor edx,edx mov ecx,512 div ecx mov ecx,eax ;总扇区数 mov eax,mem_0_4_gb_seg_sel ;切换DS到0-4GB的段 mov ds,eax
上面得到的eax是用户程序的长度,并且是512字节的倍数,将其除以512,得到用户程序所占据的扇区数,接下来就要将硬盘中的数据加载到内存中去,而通过allocate_memory分配的内存是线性地址,所以将ds切换到0-4GB段
mov eax,esi ;起始扇区号,那么第一个扇区将会被连续读取两次,eax指向要被读取的扇区号 .b1: call sys_routine_seg_sel:read_hard_disk_0 inc eax loop .b1 ;循环读,直到读完整个用户程序
读取所有的扇区到为它分配的内存处,接下来就可以读取用户程序的头部段,创建相对应的描述符,为用户程序使用的例程进行重定位到正确的位置
;建立程序头部段描述符
pop edi ;恢复程序装载的首地址 mov eax,edi ;程序头部起始线性地址 mov ebx,[edi+0x04] ;段长度 dec ebx ;段界限 mov ecx,0x00409200 ;字节粒度的数据段描述符 call sys_routine_seg_sel:make_seg_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [edi+0x04],cx
;建立程序代码段描述符 mov eax,edi add eax,[edi+0x14] ;代码起始线性地址 mov ebx,[edi+0x18] ;段长度 dec ebx ;段界限 mov ecx,0x00409800 ;字节粒度的代码段描述符 call sys_routine_seg_sel:make_seg_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [edi+0x14],cx ;建立程序数据段描述符 mov eax,edi add eax,[edi+0x1c] ;数据段起始线性地址 mov ebx,[edi+0x20] ;段长度 dec ebx ;段界限 mov ecx,0x00409200 ;字节粒度的数据段描述符 call sys_routine_seg_sel:make_seg_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [edi+0x1c],cx ;建立程序堆栈段描述符 mov eax,edi ;得到程序加载的基地址 add eax,[edi+0x08] ;得到栈段的基地址,可是对于栈是要得到其高端地址作为基地址 ; add eax,[edi+0x0c] ;得到堆栈的高端地址 mov ebx,0xffffffff sub ebx,[edi+0x0c] ;两者相减得出栈的界限值,esp > ebx mov ecx,0x00409600 call sys_routine_seg_sel:make_seg_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [edi+0x08],cx
根据用户程序的头部的内容,为用户程序创建了代码段,数据段,堆栈段,将所创建的描述符选择子,并且将其放回用户程序的头部中去,用户程序可以直接引用这些选择子
;重定位SALT mov eax,[edi+0x04] mov es,eax ;es -> 用户程序头部 mov eax,core_data_seg_sel mov ds,eax cld mov ecx,[es:0x24] ;用户程序的SALT条目数 mov edi,0x28 ;用户程序内的SALT位于头部内0x2c处 .b2: push ecx push edi mov ecx,salt_items mov esi,salt .b3: push edi push esi push ecx mov ecx,64 ;检索表中,每条目的比较次数 repe cmpsd ;每次比较4字节 jnz .b4 mov eax,[esi] ;若匹配,esi恰好指向其后的地址数据 mov [es:edi-256],eax ;将字符串改写成偏移地址 mov ax,[esi+4] mov [es:edi-252],ax ;以及段选择子 .b4: pop ecx pop esi add esi,salt_item_len pop edi ;从头比较 loop .b3 pop edi add edi,256 pop ecx loop .b2 mov ax,[es:0x04] ;将用户头部的选择子放到ax中返回 pop es ;恢复到调用此过程前的es段 pop ds ;恢复到调用此过程前的ds段 pop edi pop esi pop edx pop ecx pop ebx ret
为用户程序使用到的系统调用进行重定位,将例程段在内核中的选择子和实际例程的偏移量放置到用户程序的salt中去
此时已经成功加载完成了用户程序,返回内核程序
总结:这一章通过作者通过一个实例来说明了用户程序加载和内核程序加载的大致流程。