操作系统需要考虑采用什么办法加载用户程序(所有的段,在使用前都要以描述符的形式定义在描述符表中),用户程序需要提供一些必要的信息帮助操作系统.
操作系统提供了大量的例程供用户使用,比如显示一个字符串,就不要让用户自己来写代码了,直接调用操作系统的代码即可.但操作系统系统和用户程序应当协商一种机制,让用户程序能够使用这些例程.(API)
内核不能放到主引导扇区(超过512字节),主引导程序加载内核,把控制权移交给它.内核加载用户程序,提供API给用户使用.
学习保护模式下,加载重定位用户程序的一般原理.
内核分四个部分初始化代码(主引导程序是初始化代码的部分),内核代码段,内核数据段,公共例程.
初始化代码 安装最基本的描述符,初始化执行环境,加载内核(从磁盘到内存)
初始化代码 内核的加载(主引导程序)
初始化代码,一开始就想进行保护模式,所以安装好gdt后,就直接进入保护模式了
- 安装描述符
- 进入保护模式
- 加载内核代码
- 动态安装描述符
为啥要动态安装: 因为内核的段的数据,代码,公共例程的长度不确定,起始地址不确定.
//主引导程序代码
call make_gdt_descriptor
mov [esi+0x28],eax
mov [esi+0x2c],edx
//内核代码
;以下常量定义部分。内核的大部分内容都应当固定
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内存的段的选择子
//这些段选择子的顺序是人为规定的,在写内核的时候可以引用这个段选择子.
//比如在内核代码中
mov eax,mem_0_4_gb_seg_sel ;切换DS到0-4GB的段
mov ds,eax
内核代码
以下是系统核心的头部,用于加载核心程序
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
声明常数,内存段的选择子,它们对应的描述符会在内核初始化的时候创建.内核代码知道每个段选择子的具体数值(选择子要和对应的段对应上),对应于引导程序的第4步, 动态安装描述符.
头部 // 核心程序总长度#00
记录整个文件的大小
记录各个段的汇编偏移量(系统公用例程段位置,核心数据段位置,核心代码段位置)
内核入口点(段内汇编偏移量,段选择子[前面的常数])
[bits 32]
SECTION sys_routine vstart=0 ;系统公共例程代码段
SECTION core_data vstart=0 ;系统核心的数据段
SECTION core_code vstart=0
用户程序
用户的程序要有一定的结构才能被内核加载
- 实际上即始是大多数汇编语言也不需要亲自构造头文件,那是链接器的工作.但是链接器是为流行的操作系统服务的,用于构造他们可以识别的可执行文件格式.
- 在流行的操作系统里,内存管理是一项重要又严肃的工作,它要记住所有可以分配的内存,将它们分成块,当要求分配内存时,内存管理程序将查找并分配那些空闲块.当占用这些块的用户终止执行后,还要负责回收它们,以便再用于分配.当内存紧张时,通过磁盘来换出不常用的块.
SECTION header vstart=0
program_length dd program_end ;程序总长度#0x00
head_len dd header_end ;程序头部的长度#0x04
stack_seg dd 0 ;用于接收堆栈段选择子#0x08
stack_len dd 1 ;程序建议的堆栈大小#0x0c
;以4KB为单位
prgentry dd start ;程序入口#0x10
code_seg dd section.code.start ;代码段位置#0x14
code_len dd code_end ;
data_seg dd section.data.start ;数据段位置#0x1c
data_len dd data_end ;数据段长度#0x20
程序长度
程序的头部长度
栈选择子,内核不要求用户程序提供栈空间,而是由内核动态分配,以减轻用户编写的负担.内核空间分配好栈后,会把选择子写在这.
//内核中的代码,把选择子写在这
mov [edi+0x14],cx
//这个用户代码的头部一定要是这样的
jmp far [0x10] ;控制权交给用户程序(入口点)
栈大小1=4kb,2=8kb
程序入口的32位偏移地址(在这个段中)
代码段的位置(在整个汇编中),当内核对用户的程序加载重定位后,把该段的选择子回写到这里(仅占低字节部分),这样一来,它和上面的一起组成6字节程序的入口.内核从这里转移控制权到程序
代码段的长度
数据段同上
符号地址检索表
SECTION data vstart=0
buffer times 1024 db 0 ;缓冲区
message_1 db 0x0d,0x0a,0x0d,0x0a
db '**********User program is runing**********'
db 0x0d,0x0a,0
message_2 db ' Disk data:',0x0d,0x0a,0
data_end:
SECTION code vstart=0
start:
;下面这两句可能理解为规范,用户程序的开头就应该这样写
mov eax,ds ;ds的内核中被指向了用户的头选择子.
mov fs,eax ;保存自己的头选择子.
mov eax,[stack_seg] ;读取自己的段选择子
mov ss,eax
mov esp,0
mov eax,[data_seg] ;读取自己的数据选择子
mov ds,eax
mov ebx,message_1
call far [fs:PrintString]
mov eax,100 ;逻辑扇区号100
mov ebx,buffer ;缓冲区偏移地址
call far [fs:ReadDiskData] ;段间调用,[fs:ReadDiskData] 里面的内容是内核定义的代码的
选择子和偏移量, 可以直接跳到对应位置对执行
mov ebx,message_2
call far [fs:PrintString]
mov ebx,buffer
call far [fs:PrintString] ;too.
jmp far [fs:TerminateProgram] ;将控制权返回到系统
看这章的代码
- 内核和引导程序其实是一起的.它们两被分开是因为bios只读取硬盘的第一个扇区并执行.所以在写的时候,内核代码常常去引用 引导程序定义的选择子.而用户程序又是和引导程序,内核代码完全分开的.用户程序不知道内核.所以用户代码的开头会读取自己的栈选择子,数据选择子.用户程序位于第50扇区这是内核写好的.
- 内核要提供一些例程供用户程序调用.但它们在操作系统内部,对任何人来说都是不可见的,call调用要直接或间接地址.如果地址写死,也会有问题,操作系统升级后地址也会有变化.早期是通过API中断号来公开它们,另一种方式是使用符号名.(方法名),但不会列出段地址和偏移地址在操作系统开发手册中,会列出所有的符号名,符号名在高级语言里就是库函数名.要求用户在0x28的地方构造一个表格,在表格中列出要用到的符号名,256字节,不足用0x00填充.在用户程序加载后,内核会分析这个表格,并将每一个符号名替换成相应的内存地址.这就是重定位过程 (符号地址检索表)
//调用指向这个表对应函数名,从这个地址中读取内核代码提供的函数的 段选择子和偏移量
call far [fs:PrintString]
- 汇编语言写东西太难看懂了
//用户程序一开头就写这个,那么ds是什么? 看要看内核,ds是内核给设置的,用户程序对应的选择子
mov eax,ds
mov fs,eax
//用户内核的部分代码
mov esi,50 ;用户程序位于逻辑50扇区
call load_relocate_program
mov ebx,do_status
call sys_routine_seg_sel:put_string
mov [esp_pointer],esp ;临时保存堆栈指针
mov ds,ax
//看这段代码你需要看 load_relocate_progra的函数说明,但是没有变量名的帮助,真的是记不住
;加载并重定位用户程序
;输入:ESI=起始逻辑扇区号
;返回:AX=指向用户程序头部的选择子
其它内容
//内核定义了符号地址检索表, 名称,偏移地址和段选择子
//实际上是 别名,偏移地址和段选择子
;符号地址检索表
salt:
salt_1 db '@PrintString'
times 256-($-salt_1) db 0
dd put_string
dw sys_routine_seg_sel
//用这个表和用户头部定义的要使用的函数对比,替换成相应的内容.
cld 清空方法正向 cli关闭中断
rep,repz,repe