昨天教程中,我们用汇编写了5个应用程序APP。
写这5个APP的同时,也写了3个API,这3个API分别是cons_putchar,cons_putchar0,cons_putchar1.
大家平时在不同的操作系统上做开发,都要使用相应的API.
那么我们自制的操作系统,也有了初步的API。
昨天的教程可以看这里:30天自制操作系统day20:API
那么我们的APP在操作系统上运行的整体流程是怎样的?
用一个例子来说明,就用第5个APP吧。
今天内容的pipeline:
- 编写APP
- 在操作系统的命令行任务里运行APP的可执行文件hello.hrb
- 用C写一个窗口
编写APP
APP的源码是hello2.nas
[INSTRSET "i486p"]
[BITS 32]
MOV EDX,2 ; 把EDX的值设置为2,选择使用putchar0函数
MOV EBX,msg ;把字符串的地址给EDS:EBX
INT 0x40 ;调用中断函数,中断函数会调用EDX值所指定的putchar0函数,对EBX处的字符串进行输出
RETF
msg:
DB "hello",0
我们通过编译命令:
TOOLPATH = ../../../tolset/z_tools/
NASK = $(TOOLPATH)nask*ex.e**
hello2.hrb : hello2.nas Makefile
$(NASK) hello2.nas hello2.hrb hello2.lst
使用NASK把hello.nas编译成了hello.hrb。
这个 hello.hrb就是自制操作系统上的APP了
在操作系统的命令行任务里运行hello.hrb
第一步:把hell0.hrb打包到操作系统的镜像里
haribote.img : ipl10.bin haribote.sys hello.hrb hello2.hrb Makefile
edimg*ex.e** imgin:../../../tolset/z_tools/fdimg0at.tek \
wbinimg src:ipl10.bin len:512 from:0 to:0 \
copy from:haribote.sys to:@: \
copy from:ipl10.nas to:@: \
copy from:make.bat to:@: \
copy from:hello.hrb to:@: \
copy from:hello2.hrb to:@: \
imgout:haribote.img
第二步:使用打包好的操作系统镜像haribote.img 来启动电脑
可以真机,也可以虚拟机。
在虚拟机的话,就是
@set SDL_VIDEODRIVER=windib
@set QEMU_AUDIO_DRV=none
@set QEMU_AUDIO_LOG_TO_MONITOR=0
qemu*ex.e** -L . -m 32 -localtime -std-vga -fda haribote.img
第三步:操作系统成功启动,桌面上有个命令行窗口。使用tab键切换到命令行窗口上,在操作系统的命令行里运行dir命令:

dir命令的输出如上图所示,5个文件,分别是haribote.sys,ipl10.nas,make.bat,hello.hrb,hello2.hrb。
这5个文件正是第一步的操作,在第一步我们不仅仅打包了hello2.hrb到镜像文件里,还打包了其他4个文件。这4个文件中的第一个文件就是操作系统的代码。现在的桌面之所以能够运行起来,就是因为电脑在执行haribote.sys里的代码。
后面的ipl10.nas,make.bat都是为了测试dir命令二打包的文件。
再后面的hello.hrb,hello2.hrb就是APP了
第四步:在命令行里执行hello2,可以看到运行效果:

执行hello2后成功打印出了hello字符串,完成了hello2.nas源码中指定的功能。
那么运行hello2命令时,操作系统内部是如何执行hello2.nas中的代码的?
第五步:操作系统中的console_task函数总,把hello2.nas的内容读取到内存里
//从空闲内存里拿到大小为hello2.nas文件大小finfo->size的内存p
p = (char *) memman_alloc_4k(memman, finfo->size);
*((int *) 0xfe8) = (int) p;
// 把文件内容读取到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);
第六步:设置p放到GDT中的1003个位置,然后farcall跳转到1003个位置处所指定的地址处执行,这里其实是跳转到p处开始执行,也就是执行的代码就是hello2.nas中的代码
为什么要执行p中的代码,不直接执行?而是先把p放到GDT表中然后再跳转过去?
其实是为了对内存中所存在的任务代码统一管理。操作系统是一个多任务的系统。每个任务对应一段代码。任务对应的代码可以存放在内存的任意位置,不受限制,但是代码所在的内存位置必须登记到GDT中,因为只有这样,CPU才能知道整个内存里,哪里是代码,哪里不是代码。从而CPU可以对这些代码做一些权限管理:比如操作系统的代码可以访问内存的任意位置。但是APP的代码就只能访问某些内存位置。如果没有这个表,CPU根本不知道代码的位置,更不知道如何对代码段的权限进行设置。set_segmdesc函数的最后一个参数AR_CODE32_ER就设置额了1003号位置登记的代码的权限。
第七步:执行hello2.nas中的代码
[INSTRSET "i486p"]
[BITS 32]
MOV EDX,2 ; 把EDX的值设置为2,选择使用putchar0函数
MOV EBX,msg ;把字符串的地址给EDS:EBX
INT 0x40 ;调用中断函数,中断函数会调用EDX值所指定的putchar0函数,对EBX处的字符串进行输出
RETF
msg:
DB "hello",0
把edx的值设置为2,把ebx的值设置为msg,"hell0"这个字符串相对于hello2.nas文件头的偏移地址,然后调用操作系统的0x40号中断
第八步:去执行0x40号中断对应的函数asm_hrb_api.
set_gatedesc(idt + 0x40, (int) asm_hrb_api, 2 * 8, AR_INTGATE32);
在操作系统代码中,通过上面的代码将函数asm_hrb_api设置为0x40号中断的中断函数。操作系统在GDT的第2*8个位置所指向的地址所指向的代码段中,就可以找到这个函数。
这个函数是个汇编函数:
_asm_hrb_api:
STI ;开启中断
PUSHAD ;第一次备份各种通用寄存器
PUSHAD ;第二次备份各种通用寄存器,此次备份的通用寄存器的值就是_hrb_api函数的输入参数
CALL _hrb_api ;调用用c写的函数 hrb_api函数,
ADD ESP,32 ; 栈地址+32,跳过第二次备份的8个通用寄存器
POPAD ; 恢复处第一次备份的通用寄存器
IRETD ;返回
仔细读每一句代码,发现中断函数_asm_hrb_api函数其实只是调用了_hrb_api函数,调用这个参数的时候,还把通用寄存器EDI,ESI,EBP,ESP,EBX,EDX,ECX,EAX的值作为参数传到_hrb_api函数里了。
在第七步中,BX=字符串的偏移地址,DX=2,其他寄存器没有设置。
第九步:执行hrb_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);// 获取p的地址
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
if (edx == 1) {
cons_putchar(cons, eax & 0xff, 1);
} else if (edx == 2) {//当edx=2时
// 在命令行cons上绘制ebx+cs_base处的字符串
cons_putstr0(cons, (char *) ebx + cs_base);
} else if (edx == 3) {
cons_putstr1(cons, (char *) ebx + cs_base, ecx);
}
return;
}
第十步:执行cons_putsrt0函数
void cons_putstr0(struct CONSOLE *cons, char *s)
{
for (; *s != 0; s++) {
cons_putchar(cons, *s, 1);
}
return;
}
我们就不再往cons_putchar函数里看了。
到这里,我们很清晰地看到了一个代码为hello2.nas的源代码,是如何编译成可执行文件hello2.hrb的,hello2.hrb是如何放到操作系统镜像里的,hello2.hrb的内容又是如何从镜像文件里被读取到内存里,又是如何在内存里被cpu执行的,以及cpu执行hello2.hrb文件内的代码时,是如何一行行的执行,设置EDX,EBX,然后调用0x40号中断函数,调用汇编函数asm_hrb_api,调用C语言函数hrb_api,调用C语言函数cons_putstr0,实现hello2.nas中定义的字符串在命令行窗口的显示。
这里面的关键就是x40号中断,利用这个中断,我们只用设置一下 edx的值,就可以调用不同的系统函数cons_putstr,cons_putstr0,cons_putstr1了。
其实各操作系统都在用中断实现APP去操作系统本身的的函数。
比如Linux系统中,就是用的0x80号中断把系统函数开放给APP去调用的。
比如各种设备的驱动程序,就是通过中断实现设备与操作系统通信的。
比如咱们的显示程序:

就是调用的0x10号中断对应的中断函数,实现用AL,AH中的值取设置显卡的。这个0x10号中断对应的中断函数是BIOS里自带的,不用我们写了。
遇到一些新设备,我们就可以这样操作。
好了,写APP时,使用中断函数调用系统函数是个一般的方法。
用C写APP
昨天写的几个APP都是用汇编写的。
显然,对于写APP的程序员来说,拿汇编和C相比的话,C还是受众更广泛一些,也更接近人的思维。
这里有一个程序a.c
void api_putchar(int c);
void HariMain(void)
{
api_putchar('A');
return;
}
功能是打印出一个字符"A"
还记得咱们的操作系统是怎么用C写的么?其实就是用C去调用汇编。
我们这里也可以这么操作。
那么api_putchar(int c)对应的汇编是a_nask.nas:
[FORMAT "WCOFF"]
[INSTRSET "i486p"] ; 486
[BITS 32] ; 32位
[FILE "a_nask.nas"]
GLOBAL _api_putchar ;声明这个函数可以导出
[SECTION .text]
_api_putchar: ; void api_putchar(int c);
MOV EDX,1 ; 设置EDX为1
MOV AL,[ESP+4] ; [ESP+4]地址处对应函数的第一个参数
INT 0x40 ; 调用系统中断函数0x40
RET
然后我们编译这两个文件,得到APP的可运行程序a.hrb
//将a.c编译成a.gas
a.gas : a.c bootpack.h Makefile
cc1*ex.e** -I$(INCPATH) -Os -Wall -quiet -o a.gas a.c
//将a.gas编译成a.nas
a.nas : a.gas Makefile
gas2nask*ex.e** -a a.gas a.nas
//将a.nas编译成a.obj
a.obj : a.nas Makefile
nask*ex.e** a.nas a.obj a.lst
//将a_nask.nas编译成a_nask.obj
a_nask.obj : a_nask.nas Makefile
nask*ex.e** a_nask.nas a_nask.obj a_nask.lst
//将a.obj,a_nask.obj编译为a.bin
a.bim : a.obj a_nask.obj Makefile
obj2bim*ex.e** @$(RULEFILE) out:a.bim map:a.map a.obj a_nask.obj
//将a.bim编译为a.hrb
a.hrb : a.bim Makefile
bim2hrb*ex.e** a.bim a.hrb 0
编译好之后,我们还需要把a.hrb的前6个字符改写为:
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);
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;
}
farcall(0, 1003 * 8);
memman_free_4k(memman, (int) p, finfo->size);
cons_newline(cons);
在第五步,把a.hrb读取到p里之后就按上面的代码改。
这六个字符对应的汇编代码为:
[BITS 32]
CALL 0x1b
RET
意思是去执行0x1b处的代码,然后返回。
为什么要这样改?因为用C编译之后得到的hrb在结构上与汇编不同,通常把文件的前几个字符作为文件的一些辨别符,所以,文件的前几个字符,并不是代码。
所以,要跳转到真正的代码0x1b处执行,执行完后,要返回到命令行的函数中,继续执行后面的memman_free_4k(memman,(int) p,fifnfo->size);
那么c语言编译得到的hrb文件的文件结构是怎样的?我们随后去分析。
总是,这里按照这样改之后,C语言编写的APP的可执行文件就可以运行了。
视频演示:
视频加载中...
用C写一个窗口
在第九步的中断hrb_api函数里添加一个可以绘制窗口的函数:
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
int ds_base = *((int *) 0xfe8);
struct TASK *task = task_now();
struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
struct SHTCTL *shtctl = (struct SHTCTL *) *((int *) 0x0fe4);
struct SHEET *sht;
int *reg = &eax + 1;
if (edx == 1) {
cons_putchar(cons, eax & 0xff, 1);
} else if (edx == 2) {
cons_putstr0(cons, (char *) ebx + ds_base);
} else if (edx == 3) {
cons_putstr1(cons, (char *) ebx + ds_base, ecx);
} else if (edx == 4) { //一种执行完APP返回的新方法
return &(task->tss.esp0);
} else if (edx == 5) {// 当edx=5时,调用系统的图像控制器shtctl,产生新的图层
sht = sheet_alloc(shtctl);
//绘制字符
sheet_setbuf(sht, (char *) ebx + ds_base, esi, edi, eax);
// 绘制窗口
make_window8((char *) ebx + ds_base, esi, edi, (char *) ecx + ds_base, 0);
// 将窗口移动100,50
sheet_slide(sht, 100, 50);
// 将层号设置为3,3目前是最上层
sheet_updown(sht, 3);
reg[7] = (int) sht;
}
return 0;
}
然后再提供一个汇编函数,来调用这个中断函数内,当edx=5时候的代码:
这个函数还是写在a_nask.asm文件中,这个文件专门写给C调用的汇编函数:
_api_openwin: ; int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
PUSH EDI
PUSH ESI
PUSH EBX
MOV EDX,5 ; 给edx设置为5,调用绘制窗口的函数
MOV EBX,[ESP+16] ; buf 窗口的缓冲区
MOV ESI,[ESP+20] ; xsiz 位置
MOV EDI,[ESP+24] ; ysiz 位置
MOV EAX,[ESP+28] ; col_inv 颜色
MOV ECX,[ESP+32] ; title 标题字符
INT 0x40 ;调用中断函数,这样就会调用到hrb_api函数了
POP EBX
POP ESI
POP EDI
RET
_api_end: ; void api_end(void);//执行完APP就返回的方法
MOV EDX,4
INT 0x40
在a_nask中准备好了api_openwin函数,我们就可以写APP的C源码了winhelo.c了
//从汇编 a_nask.asm中引入两个函数
int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
void api_end(void);
//申请窗口需要的缓冲区
char buf[150 * 50];
void HariMain(void)
{
int win;
// 调用api_openwin建立一个150x50的没有激活的标题为hello的窗口,
win = api_openwin(buf, 150, 50, -1, "hello");
api_end();// 返回
}
至此,我们就用完成了用C语言写一个建立窗口的APP了。
咱们演示一下:
视频加载中...