文章目录
上一节:25、保护模式程序的动态加载和执行
下一节:27、任务和任务的创建
01、内核与用户程序之间的栈切换问题
从内核进入用户程序时,使用的是call far
指令,这是一个过程调用指令,是需要使用retf
指令返回的。过程调用需要隐式的栈操作,使用栈来保存返回地址,因此当call far
指令执行时需要将下一条指令的地址压入当前正在使用的栈中保存,就是内核的栈中。
用户程序:
。。。
start:
mov eax,ds ;此时DS指向用户程序头部段
mov fs,eax ;但是我们不能失去对DS的追踪,所以使用其他FS段寄存器重新设置DS
mov eax,[stack_seg]
mov ss,eax ;mov ss, [fs:stack_seg]
mov esp,stack_end
;mov eax,[data_seg]
;mov ds,eax
;用户程序要做的事情(省略)
retf
一旦将栈切换到用户程序自己的栈中,将无法返回内核,因为retf
指令执行时要从栈中弹出返回地址到CS
和EIP
。但是当前正在使用的是用户程序的栈,而不是内核的栈,这些返回地址是保存在内核的栈中的。
修改程序,在进入用户程序之后使用用户程序自己的栈,在返回内核时再将栈修改为内核的栈:
。。。
。。。
SECTION data vstart=0
os_ss dw 0 ;用于保存内核栈的状态
os_esp dd 0
message_1 db 0x0d,0x0a,0x0d,0x0a
db '**********User program is runing**********'
db 0x0d,0x0a,0
data_end:
。。。
。。。
start:
mov eax,ds ;此时DS指向用户程序头部段
mov fs,eax ;但是我们不能失去对DS的追踪,所以使用其他FS段寄存器重新设置DS
mov ds,[fs:data_seg] ;切换到用户程序自己的数据段
mov [os_ss],ss ;将ss的当前值保存到os_ss和os_esp
mov [os_esp],esp
mov ss,[fs:stack_seg] ;将栈切换到用户程序自己的栈
mov esp,stack_end
mov ss,[os_ss] ;恢复内核栈的状态
mov esp,[os_esp]
;用户程序要做的事情(省略)
retf ;将控制权返回到系统
。。。
。。。
在内核中,进入用户程序之前保存栈指针的指令可以去掉、返回内核之后恢复栈和栈指针的指令也可去掉
start:
。。。
。。。
;mov [esp_pointer],esp ;临时保存堆栈指针
mov ds,ax
call far [0x08] ;控制权交给用户程序(入口点)
;堆栈可能切换
return_point: ;用户程序返回点
mov eax,core_data_seg_sel ;使ds指向核心数据段
mov ds,eax
;mov eax,core_stack_seg_sel ;切换回内核自己的堆栈
;mov ss,eax
;mov esp,[esp_pointer]
mov ebx,message_6
call sys_routine_seg_sel:put_string
;这里可以放置清除用户程序各种描述符的指令
;也可以加载并启动其它程序
hlt
虚拟机验证:修改是可行的。
虽然可以这样做,但是需要禁止这样的做法。
- 首先因为用户程序不需要为内核提供服务,如为内核保存栈状态等等。
- 其次内核必须是稳定的、不能依赖用户程序,否则用户程序出错,内核也将崩溃。
02、内核中为用户程序提供编程支持
在用户程序中暂时无法访问显存,在进入内核之前创建过一个显存的描述符,由内核使用。因此知道显存的段选择子,原则上就可以在用户程序中访问显存显示文本。也可以使用内核中的put_string
例程显示字符串。代码如下:
。。。
。。。
SECTION data vstart=0
os_ss dw 0 ;用于保存内核栈的状态
os_esp dd 0
message_1 db 0x0d,0x0a,0x0d,0x0a
db '**********User program is runing**********'
db 0x0d,0x0a,0
data_end:
。。。
。。。
start:
mov eax,ds ;此时DS指向用户程序头部段
mov fs,eax ;但是我们不能失去对DS的追踪,所以使用其他FS段寄存器重新设置DS
mov ds,[fs:data_seg] ;切换到用户程序自己的数据段
mov [os_ss],ss ;将ss的当前值保存到os_ss和os_esp
mov [os_esp],esp
mov ss,[fs:stack_seg] ;将栈切换到用户程序自己的栈
mov esp,stack_end
mov ebx,message_1
call 0x28:0 ;0x28是内核公共历程段的选择子
;0是put_string例程在内核公共历程段内的偏移
mov ss,[os_ss] ;恢复内核栈的状态
mov esp,[os_esp]
;用户程序要做的事情(省略)
retf ;将控制权返回到系统
。。。
。。。
虚拟机调试:
通常用户程序不可能知道内核的段选择子
和内核中的例程
,所以上述代码一般不会出现。但是用户程序还可以自己安装一些描述符在GDT
中,如显存的段描述符,一般不能这样做,GDT
只能给内核使用,可能会造成内核的崩溃。
所以处理器有特权级DPL
的概念,限制用户程序的权限。但是内核会给用户程序提供一些例程,供其使用,也叫用户程序接口(API:Applicatio Programming Interface
)。
03、用户程序中的符号地址检索表
本节使用c13_mbr0.asm
作为引导程序:首先创建一些初始的段描述符、进入保护模式、加载内核、对内核进行设置和重定位、最后将控制权交给内核跳转执行内核程序。
内核发布时需要向程序员发布一个内核编程手册,对于我们这个c13_core1.asm
内核也有一个编程手册:
符号地址检索表位于用户程序头部,当内核加载用户程序时需要对这个表进行处理,会将每一个字符串的名字替换成其在内核中的地址,包括段选择子和段内偏移量。如此一来标号处存储的就是对应例程在内核中的地址。在用户程序头部定义符号地址检索表SALT(Symbol Address Lookup Table
)如下:
;文件名:c13_app1.asm
;文件说明:用户程序
;===============================================================================
SECTION header vstart=0
program_length dd program_end ;程序总长度#0x00
head_len dd header_end ;程序头部的长度#0x04
prgentry dd start ;程序入口#0x08
code_seg dd section.code.start ;代码段位置#0x0c
code_len dd code_end ;代码段长度#0x10
data_seg dd section.data.start ;数据段位置#0x14
data_len dd data_end ;数据段长度#0x18
stack_seg dd section.stack.start ;栈段位置#0x1c
stack_len dd stack_end ;栈段长度#0x20
;-------------------------------------------------------------------------------
;符号地址检索表
salt_items dd (header_end-salt)/256 ;#0x24
salt: ;#0x28
PrintString db '@PrintString'
times 256-($-PrintString) db 0
TerminateProgram db '@TerminateProgram'
times 256-($-TerminateProgram) db 0
ReadDiskData db '@ReadDiskData'
times 256-($-ReadDiskData) db 0
header_end:
。。。
。。。
04、内核程序中的符号地址检索表
本节主要讲了内核中的符号地址检索表SALT
的内容,在c13_core1.asm
中定义如下:
。。。
。。。
SECTION core_data vstart=0 ;系统核心的数据段
;-------------------------------------------------------------------------------
pgdt dw 0 ;用于设置和修改GDT
dd 0
ram_alloc dd 0x00100000 ;下次分配内存时的起始地址
;符号地址检索表
salt:
salt_1 db '@PrintString'
times 256-($-salt_1) db 0
dd put_string
dw sys_routine_seg_sel
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 return_point
dw core_code_seg_sel
salt_item_len equ $-salt_4
salt_items equ ($-salt)/salt_item_len
。。。
。。。
其中涉及的例程具体看代码。
05、串比较指令CMPS
内核启动流程:
- 从主引导程序进入内核
- 显示一些文本信息、显示处理器品牌信息
- 调用
load_relocate_program
加载和重定位用户程序 - 显示文本信息
- 调转执行用户程序
其中load_relocate_program
中添加了对符号地址检索表的处理:
- 读硬盘扇区,将用户程序读入内存,为每个段创建描述符;
这段指令执行完段寄存器DS
指向4G字节内存段,通用寄存器EDI
是程序加载的起始线性地址,被当成4G字节段内偏移量来用。 - 处理符号地址检索表,即将符号处的字符串替换成其对应例程在内存中的地址,替换成例程在内存中的
段选择子
和段内偏移量
。如何知道每个字符串所代表的例程在内存中的地址呢?
将用户程序中的符号取出,依次和内核中的符号地址检索表中的内容进行比较,相同的话就是用内核中对应6字节的段选择子
和段内偏移量
覆盖用户程序原先的符号,覆盖之后用户程序中存放的就是符号对应例程的段选择子
和段内偏移量
。 - 处理完
SALT
之后返回
其中,使用字符串比较指令cmpsb、cmpsw、smpsd、cmpsq
进行字符串比较:
06、串比较的方向和重复前缀
接上一节。
cmpsb
等指令只能执行一次,需要使用重复前缀rep
。
rep
的重复次数:指令每执行一次,CX/ECX/RCX
的值减一。
如果总字节数除不尽的话,应该使用cmpsb
,而不是cmpsw
等指令,CX等
设置的总字节数是字符串的长度,所以两个字符串长度要相等。
重复前缀rep
并不会在比较字符串时做出判断,相等还是不相等,这个问题使用如下方法解决:
例如指令,repe cmpsw
,执行过程是:以字节为单位进行字符串比较,若相等则设置零标志位ZF=1
,也将CX/ECX/RCX
减一,若CX
等不为0、且ZF=1
表示相等再次执行指令进行比较。若在CX
等不为0之前某次执行之后ZF=0
表示当前串不相等,比较过程提前终止,两个串不相等。
比较的方向:
字符串比较的完整过程:
07、使用外循环依次取得用户SALT表中的每个条目
接05节。
在load_relocate_program
中,添加了符号地址检索表SALT
的重定位过程:
load_relocate_program: ;加载并重定位用户程序
;输入:ESI=起始逻辑扇区号
;返回:AX=指向用户程序头部的选择子
。。。
。。。
;重定位SALT
mov eax,[edi+0x04] ;取出用户程序头部段的选择子
mov es,eax ;es -> 用户程序头部段
mov eax,core_data_seg_sel ;内核数据段的选择子
mov ds,eax ;ds -> 内核程序数据段
;用DS和ESI指向内核程序的符号地址检索表SALT
;用ES和EDi指向用户程序的符号地址检索表SALT
cld ;符号地址检索表本质上就是字符串比较过程
;使用串比较指令,CLD清方向标志DF为0
;意味着从前向后(低地址向高地址方向)进行比较
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]
pop es ;恢复到调用此过程前的es段
pop ds ;恢复到调用此过程前的ds段
pop edi
pop esi
pop edx
pop ecx
pop ebx
ret
。。。
。。。
其中:在用户程序中,偏移为4
的地方之前是用户程序头部段的长度,现在被修改为用户程序头部段的选择子。
其中:比较过程使用循环进行,外循环遍历用户程序、内循环遍历内核程序:
外循环:每执行一次都会使得EDI
指向下一个代表例程名的字符串。
08、使用内循环依次取得内核SALT表中的每个条目并进行比较
接上一节。
在上一节中:EDI
增量为256字节、ESI
增量为262字节。
内循环:
其中,字符串比较:
执行比较之前:
- 现在段寄存器
ES
指向用户程序数据段; EDI
指向用户程序符号地址检索表的某个条目;- 段寄存器
DS
指向内核数据段; ESI
指向内核符号地址检索表的某个条目。
在检索表中,每个字符串的长度是256字节=64个双字
。
如果两字符串相同,比较操作要进行64次,结束后零标志位ZF=1;
如果两字符串不同,比较操作中途停止。此时零标志位ZF=0。
若比较结束之后ZF=1
,表示字符串相同,执行重定位操作:
具体过程看下列代码注释:
。。。
。。。
mov ecx,64 ;检索表中,每条目的比较次数
repe cmpsd ;每次比较4字节
jnz .b4
mov eax,[esi] ;若匹配,esi恰好指向其后的地址数据
;将此条目所对应 段内偏移量 传送给eax
mov [es:edi-256],eax ;将字符串改写成偏移地址
;将对应的 段内偏移量 覆盖 用户程序 原位置完成重定位
mov ax,[esi+4]
mov [es:edi-252],ax ;以及段选择子
.b4:
。。。
。。。
09、用户程序中使用内核编程接口读硬盘和显示文本
其中在内核跳转执行用户程序时,使用jmp far [0x08]
指令,不需要在栈中压入返回地址;之前使用call
指令需要在栈中压入返回地址,所以有栈的切换问题。
进入用户程序执行,使用内核例程。
。。。
。。。
SECTION code vstart=0
start:
mov eax,ds
mov fs,eax
mov ss,[fs:stack_seg] ;ss指向用户程序自己的栈段
mov esp,stack_end
mov ds,[fs:data_seg] ;ds指向用户程序自己的数据段
mov ebx,message_1 ;ebx指向偏移地址
call far [fs:PrintString] ;调用内核put_string例程
mov eax,100 ;逻辑扇区号100
mov ebx,buffer ;缓冲区偏移地址
call far [fs:ReadDiskData] ;段间调用
mov ebx,message_2
call far [fs:PrintString]
mov ebx,buffer
call far [fs:PrintString] ;too.
jmp far [fs:TerminateProgram] ;将控制权返回到系统
code_end:
。。。
。。。
10、虚拟机验证程序执行
Virtual Box
虚拟机:还有文本写入100扇区号中。
11、16进制显示双字、PUSHAD、POPAD和XLAT
在内核中实现了put_hex_dword
例程:
。。。
。。。
;-------------------------------------------------------------------------------
;汇编语言程序是极难一次成功,而且调试非常困难。这个例程可以提供帮助
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
。。。
。。。
使用上述例程显示一个双字的16进制形式。
在内核数据段中定义了一个字符擦串:
每个字符在字符串中的偏移代表了其16进制数字。
我i们将32位二进制数分成8个4位,用每个4位二进制数据的值作为偏移,到这个字符串中取出对应的字符显示出来即可。
其中pushad
:每次压栈先将ESP减4
,ESP
是先保存再压入。
对应的popad
指令:
其中xalt
指令:
运行过程:将EDX
循环左移4位,在备份到EAX
中,EAX
除了低4位其他零,之后就取得AL
的值了。
上一节:25、保护模式程序的动态加载和执行
下一节:27、任务和任务的创建