java开发系统内核:应用程序与内核运行环境的交叉转换

时间:2023-02-21 08:52:41

更详细的讲解和代码调试演示过程,请参看视频
用java开发C语言编译器

更详细的讲解和代码调试演示过程,请参看视频
如何进入google,算法面试技能全面提升指南

如果你对机器学习感兴趣,请参看一下链接:
机器学习:神经网络导论

更详细的讲解和代码调试演示过程,请参看视频
Linux kernel Hacker, 从零构建自己的内核

内核为了避免恶意程序通过污染其内存而入侵自己,在启动应用程序前,会专门给应用程序分配一块与内核完全隔离的内存,作为应用程序运行时的专属内存,这样内核就拥有了比应用程序更高的等级,也就是内核可以访问应用程序的内存,反之则不行。

应用程序在运行时,由于需要调用内核提供的API,因此它的运行过程,是一个与内核不断交互的过程,大体流程如下:

内核启动应用程序 -[DS,ES,SS寄存器指向应用程序专有的内存段描述符]-> 应用程序运行自身代码-[DS,ES,SS寄存器切换到内核对应的内存段描述符]->应用程序调用内核API,CPU指向内核代码-[DS,ES,SS寄存器切换到应用程序专有的内存段描述符]->内核执行完API后,把结果交还给应用程序,应用程序继续运行自己的代码-[DS,ES,SS寄存器切换到内核对应的内存段描述符]->应用程序运行结束,CPU控制器交还给内核。

根据上面流程描述,在整个应用程序的生命周期内,切换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

代码运行时,首先判断中断发生时是内核代码在执行还是应用程序的代码在执行,如果是内核代码,那么就没有必要做内存切换了,直接执行时钟中断的代码,如果中断发送时是应用程序代码在执行,那么需要把内存从应用程序专有内存切换到内核专有内存,当中断执行完后,在把内存从内核专有内存切换回应用程序专有内存。

汇编语言编写的代码不好理解,不理解其实也没关系,后面我们会使用itenl提供的专门指令来实现这些功能,当上面的代码编译执行后,结果如下:
java开发系统内核:应用程序与内核运行环境的交叉转换

结果表明API调用成功,字符’A’成功显示到控制台上。本节运行的结果跟前几节所描述的结果是一样的,表明虽然一样,但底层的逻辑已经发生了重大的改变。更详细的讲解和更明了的调试流程,请参看视频。

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
java开发系统内核:应用程序与内核运行环境的交叉转换