编程语言
首页 > 编程语言> > X86汇编语言从实模式到保护模式14:用户程序编程接口及其实现

X86汇编语言从实模式到保护模式14:用户程序编程接口及其实现

作者:互联网

目录

1. 内核与用户程序之间的栈切换问题

1.1 当前内核与用户程序切换方式

1.2 栈切换问题

1.3 栈切换问题的不完美解决方案

1.3.1 解决方案说明

1.3.2 解决方案问题

2. 在内核中为用户程序提供编程支持

2.1 内核例程提供机制概述

2.2 基于符号-地址检索表的例程提供机制

2.2.1 定义内核提供的例程

2.2.2 内核中的符号-地址检索表

2.2.3 用户程序中的符号-地址检索表

2.2.4 符号-地址检索表解析机制

2.3 地址检索表解析过程

2.3.1 串比较指令

2.3.2 外层循环分析

2.3.3 内层循环分析

2.3.4 字符串比较分析

2.4 地址检索表使用示例

2.5 内核与用户程序切换方式

2.6 put_hex_dword例程分析

2.6.1 例程功能概述

2.6.2 例程功能实现


1. 内核与用户程序之间的栈切换问题

1.1 当前内核与用户程序切换方式

我们首先来回顾一下上一章节中内核与用户程序的切换方式,

① 从内核进入用户程序

在内核中使用call far远调用指令,跳转到用户程序执行

② 从用户程序返回内核

与call far指令对应,使用retf指令从用户程序返回内核

1.2 栈切换问题

① call far和retf指令均包含隐式的栈操作,

a. call far指令将CS : EIP压栈保存,供后续返回时使用

b. retf指令将当前栈顶的2个元素出栈到CS : EIP,实现远调用返回

② 由于内核在call far压栈时使用的是内核栈,在进入用户程序后,则不能进行栈切换,即不能切换到用户程序自己的栈(就是上图中注释掉的部分),否则retf将无法返回到内核中

1.3 栈切换问题的不完美解决方案

1.3.1 解决方案说明

一种解决方案是在用户程序中保存内核栈的信息,首先在用户程序数据段中定义保存内核栈信息的变量

之后在进入用户程序后,先保存内核栈的信息,之后就可以切换到用户程序自己的栈

最后,在用户程序返回之前,恢复内核栈的状态

1.3.2 解决方案问题

这种解决方案在现实中是必须禁止的,原因如下,

① 内核与用户程序是由不同的人在不同的时间和地点开发的,用户程序没有责任为内核提供支持和服务

② 内核必须是稳定和独立的,不能依赖用户程序,更不能让用户程序保存栈的状态,否则用户程序便可以篡改内核栈中的数据

③ 在实际的操作系统中,内核栈段DPL为0,用户栈段DPL为3,用户程序没有权限将SS寄存器再设置为内核栈段选择子

说明:本章将介绍一种新的方式,用于实现内核与用户程序的切换

2. 在内核中为用户程序提供编程支持

2.1 内核例程提供机制概述

假设我们要在用户程序中显示字符串,我们将面临如下问题,

① 用户程序只能访问自己的数据段、代码段和栈段等,不能访问系统段。因此用户程序不能获取显存栈段,也就不能向显存写入数据

② 在内核的sys_routine段已经实现了一个显示字符串的例程,没有必要在用户程序中再实现一遍

因此,问题就转换为需要提供一种机制,使得用户程序可以调用内核提供的显示字符串例程

这种机制本质上是需要向用户程序提供内核例程的段选择子和段内偏移,然后在用户程序中通过call far进行远调用

2.2 基于符号-地址检索表的例程提供机制

2.2.1 定义内核提供的例程

假设我们的内核提供如下编程接口,供内核与用户程序调用

在内核的sys_routine段按编程接口的要求实现了相应的过程

2.2.2 内核中的符号-地址检索表

内核的符号-地址检索表(Symbol Address Lookup Table,SALT)在内核数据段中,列出了所有可以提供给用户程序的例程

内核符号-地址检索表的每个表项由如下2部分组成,

① 例程名称字符串

例程名称字符串长度固定为256B,不足256B的,后面用0填充

② 例程入口点

例程入口点包含例程的段内偏移量(低4B)和段选择子(高2B)

说明1:salt_4对应的TerminateProgram其实不是一个例程,而是用户程序返回内核的返回点

说明2:salt_item_len标号为每个表项的长度,为256 + 4 + 2 = 262B

说明3:salt_items标号为表项个数

2.2.3 用户程序中的符号-地址检索表

用户程序的符号-地址检索表在用户程序头部段中,列出了用户程序需要用到的内核例程

用户程序符号-地址检索表的每个表项只有例程名称字符串部分,同样字符串长度固定为256B,不足256B的,后面用0填充

说明:salt_items标号为表项个数

2.2.4 符号-地址检索表解析机制

① 内核在加载用户程序时,会解析用户程序头部段的SALT,并将其中的字符串替换为内核例程的段选择子和段内偏移地址

② 解析的方式就是构造循环,将用户程序SALT中的每个表项与内核SALT中的每个表项进行比较,一旦比较成功,则进行字符串替换操作

③ 在字符串替换完成后,用户程序就可以直接使用标号远调用内核例程,比如使用PrintString标号远调用显示字符串的例程

说明1:实际的操作系统中并不使用类似SALT的机制,而是使用系统调用

说明2:使用类似SALT的机制有一个要求,就是用户程序有权限访问内核中的sys_routine段,目前系统中所有段描述符的DPL均为0,即均在特权级0运行

2.3 地址检索表解析过程

2.3.1 串比较指令

2.3.1.1 串比较指令格式

对SALT的处理,本质上就是字符串比较,因此我们先介绍串比较指令,根据比较的串长度,串比较指令有如下格式,

cmpsb ;compare string byte, 1B
cmpsw ;word, 2B
cmpsd ;double word, 4B
cmpsq ;quad word, 8B

同时,既然是串比较,就要有2个串,并且提供2个串的地址,根据默认操作尺寸的不同,分别使用如下图所示的寄存器组合索引串

2.3.1.2 串比较的方向

串比较的方向由EFLAGS寄存器中的DF标志决定(与movs类指令类似)

2.3.1.3 串比较重复前缀

串比较指令只是比较一次,并不是持续执行的,通常需要重复执行指令前缀,即构成如下形式

rep cmpsb
rep cmpsw
rep cmpsd
rep cmpsq

根据默认操作尺寸的不同,分别使用如下图所示的寄存器保存重复次数

在比较时,就需要根据串的长度来设置CX / ECX / RCX寄存器

rep cmpsb ;将CX / ECX / RCX设置为串的总字节数
rep cmpsw ;将CX / ECX / RCX设置为串的总字节数/2
rep cmpsd ;将CX / ECX / RCX设置为串的总字节数/4
rep cmpsq ;将CX / ECX / RCX设置为串的总字节数/8
;对于除不尽的情况,应该使用cmpsb指令

说明:实际一般使用带条件的rep前缀

我们比较2个串的目的,是比较他们是否相同,因此仅使用rep前缀按串的长度进行比较没有实际意义,应该使用带条件的重复前缀

串比较指令cmpsb / cmpsw / cmpsd / cmpsq会影响标志位,可以使用repe / repz / repne / repnz等带条件重复前缀,根据标志位的状态进行比较

从判断条件可知,

① repe是相等则重复,退出循环时,要么比较完成,要么遇到第1个不相等的比较单元;因此repe用于搜索第一个不匹配的比较单元

② repne是不相等则重复,退出循环时,要么比较完成,要么遇到第1个相等的比较单元;因此repne用于搜索第一个匹配的比较单元

2.3.1.4 使用串比较指令的完整过程

① 用CLD或STD清除或者设置方向标志DF

② 根据处理器的默认操作尺寸和方向标志,设置DS和SI / ESI / RSI指向源串

③ 根据处理器的默认操作尺寸和方向标志,设置ES和DI / EDI / RDI指向目的串

④ 根据处理器的默认操作尺寸以及选择的串比较指令,将比较的次数传送到CX / ECX / RCX

⑤ 根据实际情况,选择一下串比较指令中的一个:CMPSB / CMPSW / CPMSD / CMPSQ

⑥ 根据实际情况,为上述串比较指令指定一个前缀,可以是REPZ / REPE / REPNZ / REPNE

⑦ 重复比较结束后,根据零标志ZF最后的状态是0还是1,判断两个串是否相同

2.3.2 外层循环分析

外层循环功能:依次取得用户SALT表的每个条目

2.3.3 内层循环分析

内层功能:依次取得内核SALT表的每个条目,并和外层循环取得的用户SALT条目进行比较

说明:内层循环中保存ESI  & EDI寄存器,因为CMPSX字符串比较指令会修改这2个寄存器的值

2.3.4 字符串比较分析

此处将[DS : SI]指向的内核SALT表项和[ES : DI]指向的用户程序SALT表项进行字符串比较,如果相同,则将用户程序SALT表项的字符串修改为对应内核例程的入口点

2.4 地址检索表使用示例

由于用户程序SALT中的表项已经被替换为相应的内核例程入口点,在用户程序中直接使用call far远程调用指令即可实现对内核例程的调用

2.5 内核与用户程序切换方式

如上文所述,在引入SALT表后,使用TerminateProgram表项实现用户程序的返回,具体流程如下

① 定义用户程序返回点

在内核SALT表中定义,返回点为内核代码段的return_point标号

② 内核使用jmp far远转移,跳转到用户程序执行

此时DS指向用户程序头部段

③ 用户程序使用jmp far远转移,返回内核执行

2.6 put_hex_dword例程分析

2.6.1 例程功能概述

① put_hex_dword例程能够以十六进制形式显示一个双字(4B),可用于汇编代码调试(e.g. 打印寄存器内容或内存地址)

② 要显示十六进制数,需要将其先转换为对应的字符,这里我们引入查表法。首先在内核数据段定义bin_hex字符串

在该字符串中,每个字符在字符串中的偏移,正好是他代表的十六进制数字(e.g. 偏移为0的地方是字符'0',偏移为1的地方是字符'1',偏移为F的地方是字符'F ')

③ 因此只需要将要显示的双字分成8个4位,然后每个4位依次查表显示即可

④ 要注意显示的顺序,要从最高的4位开始

2.6.2 例程功能实现

说明1:pushad & popad指令

① pushad(push all double)指令用于将所有双字寄存器压栈,压栈顺序为,

EAX-ECX-EDX-EBX-ESP-EBP-ESI-EDI

可见pushad指令也会将ESP压栈,这里压入的是执行pushad指令之前的ESP值

② popad(pop all double)指令与pushad指令相对应,用于将所有双字寄存器出栈,出栈顺序为,

EDI-ESI-EBP-废弃-EBX-EDX-ECX-EAX

这里要特别注意,在popad时废弃了pushad压入的ESP值,因为一旦出栈到ESP就会影响当前指令的执行

说明2:xlat(Table Loop-up Translation)指令

xlat指令的隐含操作数如下,

① DS指向转换表(即示例中的bin_hex表)所在段的选择子

② 如果默认操作尺寸是16位的,用BX指定转换表的段内偏移

③ 如果默认操作尺寸是32位的,用EBX指定转换表的段内偏移

④ AL指定查表的索引值

在xlat指令执行之后,从转换表中取出的字节保存在AL寄存器中(可见xlat指令每次只能查找一个字节)

xlat指令不影响任何标志位

标签:保护模式,X86,例程,指令,内核,用户程序,SALT,比较,14
来源: https://blog.csdn.net/chenchengwudi/article/details/118416088