26、用户程序编程接口及其实现


上一节: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指令执行时要从栈中弹出返回地址到CSEIP。但是当前正在使用的是用户程序的栈,而不是内核的栈,这些返回地址是保存在内核的栈中的。

修改程序,在进入用户程序之后使用用户程序自己的栈,在返回内核时再将栈修改为内核的栈:

。。。
。。。
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减4ESP是先保存再压入。
在这里插入图片描述
对应的popad指令:
在这里插入图片描述
其中xalt指令:
在这里插入图片描述
运行过程:将EDX循环左移4位,在备份到EAX中,EAX除了低4位其他零,之后就取得AL的值了。
在这里插入图片描述
上一节:25、保护模式程序的动态加载和执行
下一节:27、任务和任务的创建

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZhangDaniel_ZD

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值