系统与应用内存交叉
1.简介
内核为了避免恶意程序通过污染其内存而入侵自己
在启动应用程序前 会专门给应用程序分配一块与内核完全隔离的内存 作为应用程序运行时的专属内存
这样内核就拥有了比应用程序更高的等级 也就是内核可以访问应用程序的内存 反之则不行
内核启动应用程序 -[DS,ES,SS寄存器指向应用程序专有的内存段描述符]
-> 应用程序运行自身代码-[DS,ES,SS寄存器切换到内核对应的内存段描述符]
->应用程序调用内核API CPU指向内核代码-[DS,ES,SS寄存器切换到应用程序专有的内存段描述符]
->内核执行完API后 把结果交还给应用程序
应用程序继续运行自己的代码-[DS,ES,SS寄存器切换到内核对应的内存段描述符]
->应用程序运行结束 CPU控制器交还给内核
2.代码
根据上面流程描述
在整个应用程序的生命周期内
切换DS,ES,SS这三个段寄存器 使他们在不同阶段指向不同的全局描述符
以便在发送数据读写操作时 代码读取的是合适的数据
接下来 我们根据前面理论 修改代码 让应用程序在调用内核API时 实现对应的内存切换
下面是应用程序的C代码
void api_putchar(int c); void main() { api_putchar('A'); return; }
应用程序调用内核API在控制台上输出一个字符A
api_putchar是在api_call.asm中实现的 [SECTION .s32] BITS 32 call main retf api_putchar: mov edx, 1 mov al, [esp + 4] int 02Dh ret %include "app.asm"
调用API时 API的输入参数都会存入八个通用寄存器
也就是eax,ebc,ecx…等这些寄存器
其中寄存器edx存储的是所需内核API的编号
这个值需要传给内核 这样内核才知道该执行哪些服务
上面的代码跟以前介绍过的代码一样 真正变化的
是执行指令int 02Dh 后 被执行的中断代码需要做较大修改
当中断指令执行后 被调用的函数代码如下(kernel.asm)
asm_cons_putchar: AsmConsPutCharHandler equ asm_cons_putchar - $$ push ds push es pushad ;以上代码还运行在应用程序的环境中 ......
上面代码执行时
先把两个段寄存器ds,es压入堆栈 然后再通过指令pushad把八个通用寄存器的值压入堆栈
一定要注意 此时这些数据都存储在应用程序的内存里 内核是无法直接访问到的
此时内存的情景如下
相关寄存器的信息都被压入到应用程序的堆栈上
内核堆栈上还是空 要想让内核获得这些数据
就必须把这些数据从应用程序的堆栈搬运到内核堆栈上
接下来的代码做的就是这些苦力活
asm_cons_putchar: AsmConsPutCharHandler equ asm_cons_putchar - $$ .... ;以上代码还运行在应用程序的环境中 ;把内存段切换到内核 mov ax, SelectorVram mov ds, ax mov es, ax mov ecx, [0xfe4];获取内核堆栈指针 add ecx, -40 mov [ecx+32], esp ;保存应用程序堆栈指针 mov [ecx+36], ss ;将pushad 压入到堆栈的值复制到系统堆栈,也就是应用程序调用API时传入的参数 mov edx, [esp] mov ebx, [esp+4] mov [ecx], edx mov [ecx+4], ebx mov edx, [esp+8] mov ebx, [esp+12] mov [ecx+8], edx mov [ecx+12], ebx mov edx, [esp+16] mov ebx, [esp+20] mov [ecx+16], edx mov [ecx+20], ebx mov edx, [esp+24] mov ebx, [esp+28] mov [ecx+24], edx mov [ecx+28], ebx ;把堆栈段切换到内核 mov ax, SelectorStack mov ss, ax mov esp, ecx sti call kernel_api .....
代码先改变两个段寄存器ds,es的值 让他们指向内核的专用内存描述符
这样读写数据时 数据就会写入内核的专有内存
然后通过读取0xfe4处的内存数据获得内核堆栈指针的值 把该值放入ecx寄存器
于是通过ecx寄存器 代码便可以读写内核堆栈
接着的代码就是搬运数据的过程
从语句mov edx, [esp]到语句mov [ecx+28], ebx
这些语句执行完后 应用程序和内核堆栈的情况如下
也就是处于应用程序堆栈上前32个字节的数据被拷贝到了内核的堆栈上
最后代码把内核的堆栈段拷贝到ss寄存器 同时把内核指针复制给esp指针
到此ds,es,ss等寄存器都指向了内核专有内存块 而且数据也从应用程序的堆栈拷贝到内核的堆栈上
指向指令call kernel_api时 CPU的控制器交给内核代码
内核代码运行时如果要读写数据的话 读写的内容都来自于内核原来的专有内存段
因此内核运行时不必担心读取到恶意数据
当内核执行完API后 需要把CPU控制器交还给应用程序
此时就需要把内存从内核专有内存切换到应用程序专有内存 代码如下
;执行完内核代码后,把内存段和堆栈段重新切回到应用程序 mov ecx, [esp+32];恢复应用程序的堆栈指针esp mov eax, [esp+36];恢复应用程序的堆栈段 cli mov ss, ax mov esp, ecx popad pop es pop ds iretd
上面的代码把当前堆栈从内核堆栈切换回应用程序堆栈
同时指令pop es 和 pop ds 把内存从内核专有内存切换回应用程序的专有内存
别忘了 在这些代码运行前
我们先把es,ds压入了堆栈 这里就是把前面压入的内容重新恢复给es和ds这两个寄存器
大家可以感觉到 这个切换过程没什么技术难度 就是繁琐 很容易出错
其实intel专门提供了指令实现这样的功能 后面我们会使用到
需要执行这种繁琐切换流程的还有一处 就是时钟中断
如果应用程序运行过程中 时钟中断发生了
应用程序的代码会停止执行 而中断代码会被执行
中断代码属于内核代码 因此需要再次进行上面所描述的内存切换
因此时钟中断需要修改的代码如下
_timerHandler: timerHandler equ _timerHandler - $$ push es push ds pushad mov ax, ss cmp ax, SelectorStack jne .from_app ;上面代码判断中断发生时是否处于内核环境 push fs push gs call intHandlerForTimer pop gs pop fs popad pop ds pop es iretd .from_app: ;把内存段切换到内核 mov ax, SelectorVram mov ds, ax mov es, ax mov ecx, [0xfe4];获取内核堆栈指针 add ecx, -8 mov [ecx+4], ss ;保存中断时的堆栈段 mov [ecx], esp ;保存中断时堆栈指针 mov ax, SelectorStack ;切换到内核堆栈段 mov ss, ax mov esp, ecx ;切换内核指针 call intHandlerForTimer pop ecx pop eax mov ss, ax mov esp, ecx popad pop ds pop es iretd
代码运行时
首先判断中断发生时是内核代码在执行还是应用程序的代码在执行
如果是内核代码 那么就没有必要做内存切换了 直接执行时钟中断的代码
如果中断发送时是应用程序代码在执行 那么需要把内存从应用程序专有内存切换到内核专有内存
当中断执行完后 在把内存从内核专有内存切换回应用程序专有内存
3.运行
汇编语言编写的代码不好理解 不理解其实也没关系 后面我们会使用itenl提供的专门指令来实现这些功能
当上面的代码编译执行后 结果如下