这个工程包含三段代码,一是引导代码,负责引导OS,二是OS,负责引导app,三是app。最后,OS会在app返回之后hlt。
下面阐述每段代码的结构。
1. 引导代码:
1) 流程:进入保护模式-->从硬盘中将OS代码读入内存-->为OS将要使用的段"注册"段描述符-->跳转到OS
2) 子程序:
i. read_hard_disk_0:
;从硬盘读取一个逻辑扇区
;EAX=逻辑扇区号
;DS:EBX=目标缓冲区地址
;返回:EBX=EBX+512
ii. make_gdt_descriptor:
;构造描述符
;输入:EAX=线性基地址
; EBX=段界限
; ECX=属性(各属性位都在原始
; 位置,其它没用到的位置0)
;返回:EDX:EAX=完整的描述符
2. OS
1) 头部
操作系统的头部包含三部分信息,一是内核长度,用于引导时确定读取的扇区数量;二是段起始点偏移,用于向GDT中"注册"段描述符前计算段基地址和段界限(具体的说,内核基地址加上这些段起始点偏移就是段基地址,而下一个段的起始点偏移减去本段起始点偏移就能得到本段大小);三是程序入口点,供mbr代码跳转,其中选择子用equ定义好,与start一起在编译阶段作为常量数据写入。
#00 doubleWord core_length ;内核长度
#04 doubleWord sys_routine_seg ;例程段的起始点位移
#08 doubleWord core_data_seg ;核心数据段的起始点位移
#0c doubleWord core_code_seg ;核心代码段起始点位移
#10 doubleWord + word start + core_code_seg_sel ;入口点:4字节偏移+2字节选择子
2) 数据段
3) 代码段
i. 重定位子程序
load_relocate_program:
;加载并重定位用户程序
;输入:ESI=起始逻辑扇区号
;返回:AX=指向用户程序头部的选择子
重定位子程序要做两项工作,一是从硬盘将用户程序读入内存,二是为用户程序的运行准备环境,包括GDT的更新和OS例程入口的映射。该程序的工作流程简述如下。
首先,读取用户程序的头部到内核数据段的预定区域。程序的头部包含两部分信息,一是入口点。二是和段相关的信息,包括各段相对于整个程序的偏移、各段的大小、用户程序希望得到的栈的大小,有了这些信息,OS就可以为程序刷新GDT,申请栈空间,将CPU控制权交给用户程序。三是程序要使用的系统例程。这些例程由内核提供,常驻于内核的例程代码段,可以由OS和用户程序调用(远call)。为了让OS和用户程序更好的隔离,即允许用户程序方便的利用过程名而不是过程入口地址调用过程,OS进行查表映射操作,将用户程序头部的过程名映射为入口地址,回写入用户程序头部。所谓“内核数据段的预定区域”指内核数据段的一块2KB的缓冲区,用以缓存用户程序头部信息。
第二,OS从用户程序头部读取用户程序大小,并且向上扩充为512的倍数。
第三,OS申请足够大的内存用以承载用户程序,让bs指向申请到的内存起始点。
第四,OS根据用户程序大小,计算需要读取的扇区数。
第五,OS循环读取完整的用户程序到内存里,存在之前申请到的内存里。
第六,OS根据头部信息为用户程序安装段描述符,并将段选择子回写到头部信息中。
第七,OS根据头部信息申请栈空间,为栈段注册段描述符,并回写到头部。
第八,OS重定位用户程序要使用的OS例程。用户程序将使用到的系统例程名字登记在头部,OS依据这些信息查找内核数据段中的映射表,将系统例程的名字映射到系统例程的入口地址,并且回写到用户程序的头部,覆盖其内的例程名称。在内核中提供的例程中有一个"TerminateProgram",这个例程的入口指向OS代码中所谓的"返回点",也就是jmp跳转用户程序之后的位置。用户程序可以调用(jmp)TerminateProgram将CPU控制权交还给OS。
经过上述复杂而繁琐的操作之后,重定位子程序load_relocate_program为用户程序的运行准备好了环境,并且让AX填充入一个选择子,指向用户程序头部段(用户程序头部信息自成一段)。
ii. 流程
显示处理器品牌信息-->调用load_relocate_program加载并重定位app-->保护堆栈指针-->跳转到app-->从app返回后修正DS和栈(包括SS和SP)-->hlt。这个地方要留意对栈的保护。当CPU从内核跳转到应用程序后,相应的栈段(ss与sp所决定)应该从内核的栈段(0x7c00开始的4KB,向低地之方向拓展,由mbr代码初始化)转换成应用程序的栈段(由OS动态分配)。相应的ss和sp的更新由应用程序执行,而从应用程序返回后,相应的更新则应由OS尽早完成。在进入应用程序之前,为了完成对ss和sp的保护,OS可以将ss和sp缓存入OS数据段的指定位置,待应用程序返回后重新取出、恢复。
3) 系统公共例程
这是一些子程序,用于被OS和app调用。
i. put_string:
;显示0终止的字符串并移动光标
;输入:DS:EBX=串地址
ii. put_char:
;在当前光标处显示一个字符,并推进
;光标。仅用于段内调用
;输入:CL=字符ASCII码
iii. read_hard_disk_0:
;从硬盘读取一个逻辑扇区
;EAX=逻辑扇区号
;DS:EBX=目标缓冲区地址
;返回:EBX=EBX+512
iv. put_hex_dword:
;在当前光标处以十六进制形式显示
;一个双字并推进光标
;输入:EDX=要转换并显示的数字
;输出:无
v. allocate_memory:
;分配内存
;输入:ECX=希望分配的字节数
;输出:ECX=起始线性地址
vi. set_up_gdt_descriptor:
;在GDT内安装一个新的描述符
;输入:EDX:EAX=描述符
;输出:CX=描述符的选择子
vii. make_seg_descriptor:
;构造存储器和系统的段描述符
;输入:EAX=线性基地址
; EBX=段界限
; ECX=属性。各属性位都在原始
; 位置,无关的位清零
;返回:EDX:EAX=描述符
3.app
1) 头部段
app的头部段非常重要。如果用一句话概括,这个段的作用就是:负责OS和app之间的通信。首先,app在编译阶段将头部信息填充完毕,这些信息将帮助OS给本app准备运行环境。所谓准备运行环境包括代码装载、内存申请、gdt安装、OS例程入口映射。其次,当OS一切就绪,希望将有关该app的运行环境的信息告知该app时,就将信息回写入头部段。回写的信息包括头部段、栈段、代码段、数据段的选择子。头部段包括下面两部分。
i.
#0x00 doubleWord program_length 程序总长度
#0x04 doubleWord head_len 头部长度
#0x08 doubleWord stack_seg 栈段选择子
#0x0c doubleWord stack_len 程序建议的堆栈大小
#0x10 doubleWord prgentry 程序入口
#0x14 doubleWord code_seg 代码段位置
#0x18 doubleWord code_len 代码段长度
#0x1c doubleWord data_seg 数据段位置
#0x20 doubleWord data_len 数据段长度
ii. 符号地址检索表
该表存储app要使用的OS例程的名称,比如@PrintString。OS在跳转入app之前会将这些名称映射成相应的入口地址。
2) 数据段
1024字节的缓冲区 + message。其中1024字节缓冲区用于缓存从硬盘中读取的数据。
3) 代码段
流程:
更新ds,ss,sp --> 显示一些信息 --> 调用TerminateProgram返回OS
下面阐述每段代码的结构。
1. 引导代码:
1) 流程:进入保护模式-->从硬盘中将OS代码读入内存-->为OS将要使用的段"注册"段描述符-->跳转到OS
2) 子程序:
i. read_hard_disk_0:
;从硬盘读取一个逻辑扇区
;EAX=逻辑扇区号
;DS:EBX=目标缓冲区地址
;返回:EBX=EBX+512
ii. make_gdt_descriptor:
;构造描述符
;输入:EAX=线性基地址
; EBX=段界限
; ECX=属性(各属性位都在原始
; 位置,其它没用到的位置0)
;返回:EDX:EAX=完整的描述符
2. OS
1) 头部
操作系统的头部包含三部分信息,一是内核长度,用于引导时确定读取的扇区数量;二是段起始点偏移,用于向GDT中"注册"段描述符前计算段基地址和段界限(具体的说,内核基地址加上这些段起始点偏移就是段基地址,而下一个段的起始点偏移减去本段起始点偏移就能得到本段大小);三是程序入口点,供mbr代码跳转,其中选择子用equ定义好,与start一起在编译阶段作为常量数据写入。
#00 doubleWord core_length ;内核长度
#04 doubleWord sys_routine_seg ;例程段的起始点位移
#08 doubleWord core_data_seg ;核心数据段的起始点位移
#0c doubleWord core_code_seg ;核心代码段起始点位移
#10 doubleWord + word start + core_code_seg_sel ;入口点:4字节偏移+2字节选择子
2) 数据段
3) 代码段
i. 重定位子程序
load_relocate_program:
;加载并重定位用户程序
;输入:ESI=起始逻辑扇区号
;返回:AX=指向用户程序头部的选择子
重定位子程序要做两项工作,一是从硬盘将用户程序读入内存,二是为用户程序的运行准备环境,包括GDT的更新和OS例程入口的映射。该程序的工作流程简述如下。
首先,读取用户程序的头部到内核数据段的预定区域。程序的头部包含两部分信息,一是入口点。二是和段相关的信息,包括各段相对于整个程序的偏移、各段的大小、用户程序希望得到的栈的大小,有了这些信息,OS就可以为程序刷新GDT,申请栈空间,将CPU控制权交给用户程序。三是程序要使用的系统例程。这些例程由内核提供,常驻于内核的例程代码段,可以由OS和用户程序调用(远call)。为了让OS和用户程序更好的隔离,即允许用户程序方便的利用过程名而不是过程入口地址调用过程,OS进行查表映射操作,将用户程序头部的过程名映射为入口地址,回写入用户程序头部。所谓“内核数据段的预定区域”指内核数据段的一块2KB的缓冲区,用以缓存用户程序头部信息。
第二,OS从用户程序头部读取用户程序大小,并且向上扩充为512的倍数。
第三,OS申请足够大的内存用以承载用户程序,让bs指向申请到的内存起始点。
第四,OS根据用户程序大小,计算需要读取的扇区数。
第五,OS循环读取完整的用户程序到内存里,存在之前申请到的内存里。
第六,OS根据头部信息为用户程序安装段描述符,并将段选择子回写到头部信息中。
第七,OS根据头部信息申请栈空间,为栈段注册段描述符,并回写到头部。
第八,OS重定位用户程序要使用的OS例程。用户程序将使用到的系统例程名字登记在头部,OS依据这些信息查找内核数据段中的映射表,将系统例程的名字映射到系统例程的入口地址,并且回写到用户程序的头部,覆盖其内的例程名称。在内核中提供的例程中有一个"TerminateProgram",这个例程的入口指向OS代码中所谓的"返回点",也就是jmp跳转用户程序之后的位置。用户程序可以调用(jmp)TerminateProgram将CPU控制权交还给OS。
经过上述复杂而繁琐的操作之后,重定位子程序load_relocate_program为用户程序的运行准备好了环境,并且让AX填充入一个选择子,指向用户程序头部段(用户程序头部信息自成一段)。
ii. 流程
显示处理器品牌信息-->调用load_relocate_program加载并重定位app-->保护堆栈指针-->跳转到app-->从app返回后修正DS和栈(包括SS和SP)-->hlt。这个地方要留意对栈的保护。当CPU从内核跳转到应用程序后,相应的栈段(ss与sp所决定)应该从内核的栈段(0x7c00开始的4KB,向低地之方向拓展,由mbr代码初始化)转换成应用程序的栈段(由OS动态分配)。相应的ss和sp的更新由应用程序执行,而从应用程序返回后,相应的更新则应由OS尽早完成。在进入应用程序之前,为了完成对ss和sp的保护,OS可以将ss和sp缓存入OS数据段的指定位置,待应用程序返回后重新取出、恢复。
3) 系统公共例程
这是一些子程序,用于被OS和app调用。
i. put_string:
;显示0终止的字符串并移动光标
;输入:DS:EBX=串地址
ii. put_char:
;在当前光标处显示一个字符,并推进
;光标。仅用于段内调用
;输入:CL=字符ASCII码
iii. read_hard_disk_0:
;从硬盘读取一个逻辑扇区
;EAX=逻辑扇区号
;DS:EBX=目标缓冲区地址
;返回:EBX=EBX+512
iv. put_hex_dword:
;在当前光标处以十六进制形式显示
;一个双字并推进光标
;输入:EDX=要转换并显示的数字
;输出:无
v. allocate_memory:
;分配内存
;输入:ECX=希望分配的字节数
;输出:ECX=起始线性地址
vi. set_up_gdt_descriptor:
;在GDT内安装一个新的描述符
;输入:EDX:EAX=描述符
;输出:CX=描述符的选择子
vii. make_seg_descriptor:
;构造存储器和系统的段描述符
;输入:EAX=线性基地址
; EBX=段界限
; ECX=属性。各属性位都在原始
; 位置,无关的位清零
;返回:EDX:EAX=描述符
3.app
1) 头部段
app的头部段非常重要。如果用一句话概括,这个段的作用就是:负责OS和app之间的通信。首先,app在编译阶段将头部信息填充完毕,这些信息将帮助OS给本app准备运行环境。所谓准备运行环境包括代码装载、内存申请、gdt安装、OS例程入口映射。其次,当OS一切就绪,希望将有关该app的运行环境的信息告知该app时,就将信息回写入头部段。回写的信息包括头部段、栈段、代码段、数据段的选择子。头部段包括下面两部分。
i.
#0x00 doubleWord program_length 程序总长度
#0x04 doubleWord head_len 头部长度
#0x08 doubleWord stack_seg 栈段选择子
#0x0c doubleWord stack_len 程序建议的堆栈大小
#0x10 doubleWord prgentry 程序入口
#0x14 doubleWord code_seg 代码段位置
#0x18 doubleWord code_len 代码段长度
#0x1c doubleWord data_seg 数据段位置
#0x20 doubleWord data_len 数据段长度
ii. 符号地址检索表
该表存储app要使用的OS例程的名称,比如@PrintString。OS在跳转入app之前会将这些名称映射成相应的入口地址。
2) 数据段
1024字节的缓冲区 + message。其中1024字节缓冲区用于缓存从硬盘中读取的数据。
3) 代码段
流程:
更新ds,ss,sp --> 显示一些信息 --> 调用TerminateProgram返回OS