MacOS环境-手写操作系统-46,47-C语言开发应用程序

系统与应用内存交叉

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把八个通用寄存器的值压入堆栈

一定要注意 此时这些数据都存储在应用程序的内存里 内核是无法直接访问到的

此时内存的情景如下

应用程序堆栈 内核堆栈
edi _
esi _
ebp _
esp _
ebx _
edx _
ecx _
eax _
es _
ds _

相关寄存器的信息都被压入到应用程序的堆栈上

内核堆栈上还是空 要想让内核获得这些数据

就必须把这些数据从应用程序的堆栈搬运到内核堆栈上

接下来的代码做的就是这些苦力活

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

这些语句执行完后 应用程序和内核堆栈的情况如下

应用程序堆栈 内核堆栈
edi edi
esi esi
ebp ebp
esp esp
ebx ebx
edx edx
ecx ecx
eax eax
es _
ds _

也就是处于应用程序堆栈上前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提供的专门指令来实现这些功能

当上面的代码编译执行后 结果如下

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/w776341482/article/details/128676949