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则是在程序执行完毕后返回命令行。
但其实到了后面,应用程序的结束是产生一个中断,这样来返回操作系统。
保护操作系统
保护操作系统有以下的一些方式
- 设置应用程序专用的内存空间
把一块专属于应用程序的内存分给它。可以把这块内存注册到GDT中。 - 禁止应用程序修改某些寄存器
把操作系统的栈、寄存器保存起来,应用程序使用应用程序需要用的。 - 硬件实现
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
即可。
今天的汇编比较多。。看着比较麻烦)。。