Day21

今天补完了昨天的中断,以及开始了对操作系统的保护。

一些感想

API(续)

昨天貌似有个程序并不能正确打印字符串。原因是打印单个字符的时候,可以通过[CS:ECX]来指定代码段,但是显示字符串的时候不能这样,于是就会误认为是DS(数据段)的地址,达不到预期的结果。
所以作者采用的方法是调用中断时,预先在某个商量好的位置指定代码段的基址,这样就能保证找到想要的了(一种类似全局变量的写法)。

然后补全一下应用程序调用API,昨天主要讲的是从汇编往下调用,现在补一下C语言调用相应的汇编。

_api_putchar:    ; void api_putchar(int c);
        MOV        EDX,1
        MOV        AL,[ESP+4]        ; c
        INT        0x40
        RET

执行C语言应用程序

这应该是要定位入口的意思

[BITS 32]
    CALL 0x1b
    RETF

0x1b是生成的可执行文件中HariMain的地址,RETF则是在程序执行完毕后返回命令行。
但其实到了后面,应用程序的结束是产生一个中断,这样来返回操作系统。

保护操作系统

保护操作系统有以下的一些方式

  1. 设置应用程序专用的内存空间
    把一块专属于应用程序的内存分给它。可以把这块内存注册到GDT中。
  2. 禁止应用程序修改某些寄存器
    把操作系统的栈、寄存器保存起来,应用程序使用应用程序需要用的。
  3. 硬件实现
    x86提供了一些异常处理的方法,甚至包括保存恢复各种栈和寄存器,可以说很方便了。

处理异常

既然有了保护机制,当出现异常情况的时候就需要处理。硬件层面上,已经有了相应的中断机制,因此只要在0x0d上注册一个中断服务程序即可。
我们需要把访问段的权限+0x60,这样变为应用程序可用的段。
与此同时,操作系统调用应用程序的时候,需要RETF,应用程序返回操作系统的时候,通过API来返回

攻克难题——字符串显示API

harib18a

既然是没有指定正确的段,那么要做的就是把正确的段存到某个地方,供API使用。

if(finfo != 0)
{
    // 找到文件的情况
    p = (char *)memman_alloc_4k(memman, finfo->size);
    *((int *)0xfe8) = (int)p;
    file_loadfile(finfo->clustno, finfo->size, p, fat, (char *)(ADR_DISKIMG + 0x003e00));
    set_segmdesc(gdt + 1003, finfo->size - 1, (int)p, AR_CODE32_ER);
    farcall(0, 1003 * 8);
    memman_free_4k(memman, (int)p, finfo->size);
    cons_newline(cons);
    return 1;
}

在0xfe8这个地方存入了我们需要的基址
之后,调整我们的API

void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
    int cs_base = *((int *)0xfe8);
    struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
    switch(edx)
    {
    case 1:
        cons_putchar(cons, eax & 0xff, 1);
        break;
    case 2:
        cons_putstr0(cons, (char *)ebx + cs_base);
        break;
    case 3:
        cons_putstr1(cons, (char *)ebx + cs_base, ecx);
        break;
    default:
        break;
    }
    return;
}

加上这个地址之后,就能正确显示字符串了。

用C语言编写应用程序

harib18b

就按目前的版本写好了。
首先是一个简单的应用程序

void api_putchar(int c);

void HariMain(void)
{
    api_putchar('A');
    return;
}

但是光这样写还不够,我们需要指定函数的入口地址(根据猜测的话,这个代码并不是在文件开始的位置存放的),简单来说就是把开始的6个字节进行替换(替换成之前说的汇编指令)。
为了省事起见,在我们执行应用程序的时候帮它改一下。

if(finfo->size >= 8 && strncmp(p + 4, "Hari", 4) == 0)
{
    p[0] = 0xe8;
    p[1] = 0x16;
    p[2] = 0x00;
    p[3] = 0x00;
    p[4] = 0x00;
    p[5] = 0xcb;
}

这样就能正确打开应用程序了。

保护操作系统(1)

harib18c

目前的操作系统还没有添加任何保护功能,所以像这样的代码

void HariMain(void)
{
    *((char *)0x00102600) = 0;
    return;
}

就可以搞崩操作系统。。

保护操作系统(2)

harib18d

首先,分配一些空间给应用程序

q = (char *)memman_alloc_4k(memman, 64 * 1024);
set_segmdesc(gdt + 1004, 64 * 1024 - 1, (int)q, AR_DATA32_RW);
start_app(0, 1003 * 8, 64 * 1024, 1004 * 8);
memman_free_4k(memman, (int)q, 64 * 1024);

然后是软件实现的启动到用户态。(因为最后搞成硬件实现了,这段就相当于是补充了)

_start_app:        ; void start_app(int eip, int cs, int esp, int ds);
        PUSHAD        ; 将32位寄存器的值全部保存起来
        MOV        EAX,[ESP+36]    ; 应用程序用EIP
        MOV        ECX,[ESP+40]    ; 应用程序用CS
        MOV        EDX,[ESP+44]    ; 应用程序用ESP
        MOV        EBX,[ESP+48]    ; 应用程序DS/SS
        MOV        [0xfe4],ESP        ; 操作系统用ESP
        CLI            ; 在切换过程中禁止中断请求
        MOV        ES,BX
        MOV        SS,BX
        MOV        DS,BX
        MOV        FS,BX
        MOV        GS,BX
        MOV        ESP,EDX
        STI            ; 切换完成后恢复中断请求
        PUSH    ECX                ; 用于far-CALL的PUSH(cs)
        PUSH    EAX                ; 用于far-CALL的PUSH(eip)
        CALL    FAR [ESP]        ; 调用应用程序

;    应用程序结束后返回此处

        MOV        EAX,1*8            ; 操作系统用DS/SS
        CLI            ; 再次进行切换,禁止中断请求
        MOV        ES,AX
        MOV        SS,AX
        MOV        DS,AX
        MOV        FS,AX
        MOV        GS,AX
        MOV        ESP,[0xfe4]
        STI            ; 切换完成后恢复中断请求
        POPAD    ; 恢复之前保存的寄存器值
        RET

简而言之,我们一开始保存了操作系统的各种寄存器状态,转到应用程序所在的内存;当运行完毕之后,再恢复操作系统的各种寄存器状态,调转到操作系统。
之后还要修改API,因为API是应用程序调用的,需要切换到操作系统状态

_asm_hrb_api:
        ; 为方便起见从开头就禁止中断请求
        PUSH    DS
        PUSH    ES
        PUSHAD        ; 用于保存的PUSH
        MOV        EAX,1*8
        MOV        DS,AX            ; 先仅将DS设定为操作系统用
        MOV        ECX,[0xfe4]        ; 操作系统的ESP
        ADD        ECX,-40
        MOV        [ECX+32],ESP    ; 保存应用程序的ESP
        MOV        [ECX+36],SS        ; 保存应用程序的SS

; 将PUSHAD后的值复制到系统栈
        MOV        EDX,[ESP   ]
        MOV        EBX,[ESP+ 4]
        MOV        [ECX   ],EDX    ; 复制传递给hrb_api
        MOV        [ECX+ 4],EBX    ; 复制传递给hrb_api
        MOV        EDX,[ESP+ 8]
        MOV        EBX,[ESP+12]
        MOV        [ECX+ 8],EDX    ; 复制传递给hrb_api
        MOV        [ECX+12],EBX    ; 复制传递给hrb_api
        MOV        EDX,[ESP+16]
        MOV        EBX,[ESP+20]
        MOV        [ECX+16],EDX    ; 复制传递给hrb_api
        MOV        [ECX+20],EBX    ; 复制传递给hrb_api
        MOV        EDX,[ESP+24]
        MOV        EBX,[ESP+28]
        MOV        [ECX+24],EDX    ; 复制传递给hrb_api
        MOV        [ECX+28],EBX    ; 复制传递给hrb_api

        MOV        ES,AX            ; 将剩余的段寄存器也设为操作系统用
        MOV        SS,AX
        MOV        ESP,ECX
        STI            ; 恢复中断请求

        CALL    _hrb_api

        MOV        ECX,[ESP+32]    ; 取出应用程序的ESP
        MOV        EAX,[ESP+36]    ; 取出应用程序的SS
        CLI
        MOV        SS,AX
        MOV        ESP,ECX
        POPAD
        POP        ES
        POP        DS
        IRETD        ; 这个命令会自动执行STI

确实有点看不懂。。大概的意思就是存储好应用程序的寄存器值之后,切换到操作系统状态。
接下来是中断的部分,如果应用程序产生了中断请求,同样也需要切换到内核态。

_asm_inthandler20:
        PUSH    ES
        PUSH    DS
        PUSHAD
        MOV        AX,SS
        CMP        AX,1*8
        JNE        .from_app
;    当操作系统活动时产生的中断情况和之前差不多
        MOV        EAX,ESP
        PUSH    SS                ; 保存中断时的SS
        PUSH    EAX                ; 保存中断时的ESP
        MOV        AX,SS
        MOV        DS,AX
        MOV        ES,AX
        CALL    _inthandler20
        ADD        ESP,8
        POPAD
        POP        DS
        POP        ES
        IRETD
.from_app:
;    当应用程序活动时发生中断
        MOV        EAX,1*8
        MOV        DS,AX            ; 先仅将DS设定为操作系统用
        MOV        ECX,[0xfe4]        ; 操作系统的ESP
        ADD        ECX,-8
        MOV        [ECX+4],SS        ; 保存中断时的SS
        MOV        [ECX  ],ESP        ; 保存中断时的ESP
        MOV        SS,AX
        MOV        ES,AX
        MOV        ESP,ECX
        CALL    _inthandler20
        POP        ECX
        POP        EAX
        MOV        SS,AX            ; 将SS设回应用程序用
        MOV        ESP,ECX            ; 将ESP设回应用程序用
        POPAD
        POP        DS
        POP        ES
        IRETD

其实差不多,都是一个寄存器的切换。
虽然这些代码很快就被废弃掉了,但是给我们提供了一个软件实现保护的视角)

对异常的支持

harib18e

硬件层面上,0x0d是异常中断

_asm_inthandler0d:
        STI
        PUSH    ES
        PUSH    DS
        PUSHAD
        MOV        AX,SS
        CMP        AX,1*8
        JNE        .from_app
;    当操作系统活动时产生中断的情况和之前差不多
        MOV        EAX,ESP
        PUSH    SS                ; 保存中断时的ES
        PUSH    EAX                ; 保存中断时的ESP
        MOV        AX,SS
        MOV        DS,AX
        MOV        ES,AX
        CALL    _inthandler0d
        ADD        ESP,8
        POPAD
        POP        DS
        POP        ES
        ADD        ESP,4            ; 在INT 0x0d中需要这句
        IRETD
.from_app:
;    当应用程序活动时产生中断
        CLI
        MOV        EAX,1*8
        MOV        DS,AX            ; 先仅将DS设定为操作系统用
        MOV        ECX,[0xfe4]        ; 操作系统的ESP
        ADD        ECX,-8
        MOV        [ECX+4],SS        ; 保存产生中断时的SS
        MOV        [ECX  ],ESP        ; 保存产生中断时的ESP
        MOV        SS,AX
        MOV        ES,AX
        MOV        ESP,ECX
        STI
        CALL    _inthandler0d
        CLI
        CMP        EAX,0
        JNE        .kill
        POP        ECX
        POP        EAX
        MOV        SS,AX            ; 将SS恢复为应用程序用
        MOV        ESP,ECX            ; 将ESP恢复为应用程序用
        POPAD
        POP        DS
        POP        ES
        ADD        ESP,4            ; INT 0x0d需要这句
        IRETD
.kill:
;    将应用程序强制结束
        MOV        EAX,1*8            ; 操作系统用的DS/SS
        MOV        ES,AX
        MOV        SS,AX
        MOV        DS,AX
        MOV        FS,AX
        MOV        GS,AX
        MOV        ESP,[0xfe4]        ; 强制返回start_app时的ESP
        STI            ; 切换完成后恢复中断请求
        POPAD    ; 恢复事先保存的寄存器值
        RET

这个中断可以做到杀掉进程,之后强制返回到cmd_app

int inthandler0d(int *esp)
{
    struct CONSOLE *cons = (struct CONSOLE *)*((int *)0x0fec);
    cons_putstr0(cons, "\nINT 0D :\n General Protected Exception.\n");
    return 1; // 强制结束程序
}

然后注册到IDT里

set_gatedesc(idt + 0x0d, (int) asm_inthandler0d, 2 * 8, AR_INTGATE32);

方便我们之后继续保护操作系统

保护操作系统(3)

harib18f

这次的目的是禁止汇编层面上随便指定段号

保护操作系统(4)

harib18g

在硬件层面上,如果把访问权限加上0x60,就代表这个段为应用程序用。如果段内的代码想要存入操作系统的段地址,就会产生异常。

struct TASK *task = task_now();

set_segmdesc(gdt + 1003, finfo->size - 1, (int)p, AR_CODE32_ER + 0x60);
set_segmdesc(gdt + 1004, 64 * 1024 - 1, (int)q, AR_DATA32_RW + 0x60);
start_app(0, 1003 * 8, 64 * 1024, 1004 * 8, &(task->tss.esp0));

但是这样的话,x86规定既不能操作系统CALL应用程序,也不能far-JMP到应用程序,可做的就只有RETF了。

_start_app:        ; void start_app(int eip, int cs, int esp, int ds, int *tss_esp0);
        PUSHAD        ; 将32位寄存器的值全部保存下来
        MOV        EAX,[ESP+36]    ; 应用程序用EIP
        MOV        ECX,[ESP+40]    ; 应用程序用CS
        MOV        EDX,[ESP+44]    ; 应用程序用ESP
        MOV        EBX,[ESP+48]    ; 应用程序用DS/SS
        MOV        EBP,[ESP+52]    ; tss.esp0的地址
        MOV        [EBP  ],ESP        ; 保存操作系统用ESP
        MOV        [EBP+4],SS        ; 保存操作系统用SS
        MOV        ES,BX
        MOV        DS,BX
        MOV        FS,BX
        MOV        GS,BX
;    下面调整栈,以免用RETF跳转到应用程序
        OR        ECX,3            ; 将应用程序用段号和3进行或运算(据作者说是为了调用RETF而用的小技巧)
        OR        EBX,3            ; 将应用程序用段号和3进行或运算
        PUSH    EBX                ; 应用程序的SS
        PUSH    EDX                ; 应用程序的ESP
        PUSH    ECX                ; 应用程序的CS
        PUSH    EAX                ; 应用程序的EIP
        RETF
;    应用程序结束后不会回到这里

之后也要修改API

_asm_hrb_api:
        STI
        PUSH    DS
        PUSH    ES
        PUSHAD        ; 用于保存的PSUH
        PUSHAD        ; 用于向hrb_api传值的PUSH
        MOV        AX,SS
        MOV        DS,AX        ; 将操作系统用段地址存入DS和ES
        MOV        ES,AX
        CALL    _hrb_api
        CMP        EAX,0        ; 当EAX不为0时程序结束
        JNE        end_app
        ADD        ESP,32
        POPAD
        POP        ES
        POP        DS
        IRETD
end_app:
;    EAX为tss.esp0的地址
        MOV        ESP,[EAX]
        POPAD
        RET                    ; 返回cmd_app

硬件帮我们实现了各种栈操作,因此就比较简短了

做一个用于结束的API

int* hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
    int cs_base = *((int *)0xfe8);
    struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
    struct TASK *task = task_now();
    switch(edx)
    {
    case 1:
        cons_putchar(cons, eax & 0xff, 1);
        break;
    case 2:
        cons_putstr0(cons, (char *)ebx + cs_base);
        break;
    case 3:
        cons_putstr1(cons, (char *)ebx + cs_base, ecx);
        break;
    case 4:
        return &(task->tss.esp0);
        break;
    default:
        break;
    }
    return 0;
}

然后修改0x0d的中断服务程序

int *inthandler0d(int *esp)
{
    struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
    struct TASK *task = task_now();
    cons_putstr0(cons, "\nINT 0D :\n General Protected Exception.\n");
    return &(task->tss.esp0);    // 让程序强制结束
}

之后是把各种中断处理改回之前的版本,因为CPU帮我们切换了栈

_asm_inthandler0d:
        STI
        PUSH    ES
        PUSH    DS
        PUSHAD
        MOV        EAX,ESP
        PUSH    EAX
        MOV        AX,SS
        MOV        DS,AX
        MOV        ES,AX
        CALL    _inthandler0d
        CMP        EAX,0        ; 只有这里不同
        JNE        end_app        ; 只有这里不同
        POP        EAX
        POPAD
        POP        DS
        POP        ES
        ADD        ESP,4            ; 在INT 0x0d中需要这句
        IRETD

最后,仅将API中断开放给用户使用

set_gatedesc(idt + 0x40, (int) asm_hrb_api,      2 * 8, AR_INTGATE32 + 0x60);

当各种应用程序结束的时候,调用

MOV EDX, 4
INT 0x40

即可。

今天的汇编比较多。。看着比较麻烦)。。