30天开发操作系统 第22天 -- 用C语言编写应用程序

news2025/5/24 23:30:27

前言

在昨天的最后我们成功干掉了crack2.hrb, 今天我们要尝试一下更厉害的攻击手段。 所以说, 从现在开始又要打开坏人模式了哟,嘿嘿嘿

虽然把操作系统的段地址存入DS这一招现在已经不能用了,不过我可不会善罢甘休的。我要想个更厉害的招数,把使用的人推进恐怖的深渊,哈哈哈哈!在操作系统管理的内存空间里搞破坏是行不通了,这次算你厉害,不过我还可以在定时器上动动手脚。一定很不爽吧。这样一来,光标闪烁就会变得异常缓慢,任务切换的速度也会变慢。嗯,光想想就觉得很有趣啊,啊哈哈。

一、保护操作系统 4.0

好,完成了!赶紧 – make run 然后输入 crack3,口中念念有词道: 吃我这招!然后按下回车键。

[INSTRSET "i486p"]
[BITS 32]
		MOV		AL,0x34
		OUT		0x43,AL
		MOV		AL,0xff
		OUT		0x40,AL
		MOV		AL,0xff
		OUT		0x40,AL

; 	上述代码的功能与下面代码相当
;	io_out8(PIT_CTRL, 0x34);
;	io_out8(PIT_CNT0, 0xff);
;	io_out8(PIT_CNT0, 0xff);

		MOV		EDX,4
		INT		0x40

在这里插入图片描述
哎呀,竟然有闪,有两下子嘛!可恶!
当以应用程序模式运行时,执行IN指令和OUT指令都会产生一般保护异常。当然,通过修改CPU设置,可以允许应用程序使用IN指令和OUT指令,不过这样大家会担心留下bug而遭到恶意攻击。
我还没输呢,这点挫折我可不会善罢甘休!既然如此,我就给你执行CLI然后再HLT,这样 一来电脑就死机了。由于不再产生定时器中断,任务切换也会停止,键盘和鼠标中断也停止响应, 除了按下机箱上的Reset按钮以外没有别的办法了。我真是个天才,哈哈哈哈!

[INSTRSET "i486p"]
[BITS 32]
		CLI
fin:
		HLT
		JMP		fin

这次一定要成功, make run!
在这里插入图片描述
又产生了异常, 为什么啊!
当以应用程序模式运行时,执行CLI、STI和HLT这些指令都会产生异常。因为中断应该 是由操作系统来管理的,应用程序不可以随便进行控制。不能执行HLT的话,应用程序就没 办法省电了,不过一般情况下,这应该通过调用任务休眠API来实现,而不能由应用程序自 己来执行HLT。此外,在多任务下,调用休眠API还可以让系统将CPU时间分配给其他任务。
连CLI也不让我执行吗?怎么会有这种事!这样的话不就干不成坏事了吗?难道只能缴械投降了?
哦哦,想起来了!操作系统里面不是有一个用来CLI的函数嘛,far-CALL这个函数不就行了吗?这样一来应该就会死机了。应该CALL哪个地址呢?只要有map文件就可以轻松找到了。
要嗯, map文件中有这样一行:
0x00000AC1 : _io_cli
我就来far-CALL这个地址吧, 哈哈!

[INSTRSET "i486p"]
[BITS 32]
		CALL	2*8:0xac1
		MOV		EDX,4
		INT		0x40

嘿嘿, 准备接招吧 make run!
在这里插入图片描述
又产生异常了!到底为啥呀!能不能让我赢一次啊!可恶哦!
如果应用程序可以CALL任意地址的话,像这样的恶作剧就可以成功了,因此CPU规定除了设置好的地址以外,禁止应用程序CALL其他的地址。因此,应用程序要调用操作系统只能采用INT0x40的方法。

于是坏人只好失望地洗洗睡了(笑)。3天后·····
有了!这次应该能行,我怎么早没想到这个办法呢?哈哈, 这次绝对可以成功! 既然应用程序只能调用API,那么把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 TASK *task = task_now();
	struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
	if (edx == 1) {
		cons_putchar(cons, eax & 0xff, 1);
	} else if (edx == 2) {
		cons_putstr0(cons, (char *) ebx + cs_base);
	} else if (edx == 3) {
		cons_putstr1(cons, (char *) ebx + cs_base, ecx);
	} else if (edx == 4) {
		return &(task->tss.esp0);
	} else if (edx == 123456789) {
		*((char *) 0x00102600) = 0;
	}
	return 0;
}

好啦!准备接招吧, make run

没有产生异常!不过到底成功了没有呢? dir一下看看…成功了!这次我赢了, 哈哈!
在这里插入图片描述

如果操作系统内部存在这种笨到作茧自缚的API,那么再优秀的CPU也对此无能为力, 操作系统只能束手就擒。即使操作系统原本没有这样的API,如果像这次一样被篡改的话, 也有可能被植入后门。 “不安装不可靠的操作系统”了。如果大家都能遵 要防止这种问题的发生,我们只能 守这条原则,就不会因为随意下载应用程序而弄坏电脑了——当然,如果操作系统本身就破 绽百出的话就另当别论了。
这次的crack6.hrb其实只能在使用咱们操作系统的人身上发挥效果。如果对方不安装我们的操作系统的话,即便运行了这个应用程序也不会发生任何问题。因此,就目前而言,这个应用程序的受害者就只有这个坏人自己而已,从这个角度来说,他“赢”得还真是空虚啊。
现在坏人已经走了,接下来我们继续做系统吧。

二、帮助发现bug

CPU的异常处理功能,除了可以保护操作系统免遭应用程序的破坏,还可以帮助我们在编写应用程序时及早发现bug。 我们来举个例子:

void api_putchar(int c);
void api_end(void);

void HariMain(void)
{
	char a[100];
	a[10] = 'A';		/* 这句当然没有问题 */
	api_putchar(a[10]);
	a[102] = 'B';		/* 这句就有问题了 */
	api_putchar(a[102]);
	a[123] = 'C';		/* 这句就有问题了 */
	api_putchar(a[123]);
	api_end();
}

这明显是个有bug的程序,因为a是一个100字节的数组,“A” 的赋值显然没有问题,肯定会显示出“A”这个字符,但“B"的赋值就不行, 因为它已经超出数组范围了;“C”的赋值当然也是不行的。

把这个程序 make run 一下, 结果如下
在这里插入图片描述

本来我们以为会产生异常, 结果却没有出现。我们在真机环境下试试看。 在真机环境下运行了一下,结果电脑自动重启了。嗯,这可不妙啊,电脑自动重启应该是产生了没有设置过的异常所导致的。
哦对了, 坏人刚刚擅自加上去的API已经删掉了哦,crack应用程序也已经玩腻了, 所以一起都删除了。

由于a这个数组是保存在栈中的,因此这次可能产生了栈异常。 我们需要一个函数来处理栈异常, 栈异常的中断号为 – 0x0c

_asm_inthandler0c:
		STI
		PUSH	ES
		PUSH	DS
		PUSHAD
		MOV		EAX,ESP
		PUSH	EAX
		MOV		AX,SS
		MOV		DS,AX
		MOV		ES,AX
		CALL	_inthandler0c
		CMP		EAX,0
		JNE		end_app
		POP		EAX
		POPAD
		POP		DS
		POP		ES
		ADD		ESP,4			; 在INT 0x0c中也需要这句
		IRETD

栈异常的中断号为0x0c:可能大家会问,除此之外还有什么异常呢?我们在这里补充讲解一下吧。根据CPU说明书,从0X00到0x1f都是异常所使用的中断,因此,IRQ的中断号都是从0x20之后开始的。其他一些比较有用的异常有0x00号除零异常(当试图除以0时产生)和0x06号非法指令异常(当试图执行CPU无法理解的机器语言指令, 例如当试图执行一段数据时,有可能会产生)等。

然后,我们编写inthandler0c函数,只是将inthandler0d中的出错信息改了一下而已。

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

在IDT中也需要登记一下:

set_gatedesc(idt + 0x0c, (int) asm_inthandler0c, 2 * 8, AR_INTGATE32);
	set_gatedesc(idt + 0x0d, (int) asm_inthandler0d, 2 * 8, AR_INTGATE32);
	set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32);
	set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
	set_gatedesc(idt + 0x27, (int) asm_inthandler27, 2 * 8, AR_INTGATE32);
	set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
	set_gatedesc(idt + 0x40, (int) asm_hrb_api,      2 * 8, AR_INTGATE32 + 0x60);

试试看。啊,果然QEMU对异常的模拟有问题, 因此程序还是可以 顺利运行的,看来只能在真机环境下测试了。真机环境下成功产生了异常。
在真机环境下, “AB”之后才产生异常, 也就是说,写入的“C”被判定为异常,而显示出 “B”却被放过去了。从这个例子可以看出,异常并不能发现所有的bug。不过,比起一个bug都发现不了来说,哪怕能发现一个bug也是非常有帮助的,请大家一定要好好利用哦。
可能有人会问,为什么“C” 会被判定为异常而“B”就可以被放过去呢?下面我们就来简单讲一讲。
a[102]虽然超出了数组的边界,但却没有超出为应用程序分配的数据段的边界,因此虽 然这是个bug, CPU也不会产生异常。另一方面,a[123]所在的地址已经超出了数据段的边界, 因此CPU马上就发现并产生了异常。
其实,CPU产生异常的目的并不是去发现bug,而是为了保护操作系统,它的思路是: “这个程序试图访问自身所在数据段以外的内存地址,一定是想擅自改写操作系统或者其他 应用程序所管理的内存空间,这种行为岂能放任不管?”因此,即便CPU不能帮我们发现所有的bug,也不可以责怪它哦。
要想让它帮忙发现bug的话,最好是能知道引发异常的指令的地址。这个功能很简单, 我们来加上去。

int *inthandler0c(int *esp)
{
	struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
	struct TASK *task = task_now();
	char s[30];
	cons_putstr0(cons, "\nINT 0C :\n Stack Exception.\n");
	sprintf(s, "EIP = %08X\n", esp[11]);	/*这里!*/
	cons_putstr0(cons, s);	/*这里!*/
	return &(task->tss.esp0);	/* 强制结束程序 */
}
int *inthandler0d(int *esp)
{
	struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
	struct TASK *task = task_now();
	char s[30];
	cons_putstr0(cons, "\nINT 0D :\n General Protected Exception.\n");
	sprintf(s, "EIP = %08X\n", esp[11]);	/*这里!*/
	cons_putstr0(cons, s);	/*这里!*/
	return &(task->tss.esp0);	/* 强制结束程序 */
}

上面代码的功能是,将esp (即栈)的11号元素(即EIP)显示出来。
如果想要得到产生异常时其他寄存器的值,只要按照下表显示相应的元素即可。
esp[0~7]为_asm_inthandler中POSHAD的结果
esp[ 0]: EDI
esp[ 1] : ESI
esp[ 2]: EBP
esp[ 4]: EBX
esp[ 5]: EDX
esp[ 6] : ECX
esp[ 7] : EAX
epp[8~9]为_as_inthandler中PUSH的结果
esp[ 8] : DS
esp[ 9] : ES
esp[10] : 错误编号(基本上是0, 显示出来也没什么意思)
esp[11] : EIP
esp[10~15]为异市产生时CPU自动PUBH的结采
esp[12]: CS
esp[13]: EFLAGS
epp[14]: ESP (应用程序用ESP)
esp[15]: SS (应用程序用SS)

三、强制结束应用程序

现在我们的系统已经可以对付大部分恶意破坏和bug,变得越来越优秀了,不过,我们还需要一些别的功能,比如强制结束应用程序。

void HariMain(void)
{
	for (;;) { }
}

如果运行这样一个程序,将永远循环下去而无法结束。中断并没有被禁用,因此其他的任务还可以照常工作,不过这个任务总归要消耗一定的CPU运行时间,系统整体的速度就会变慢,还会白白浪费电。如果操作系统没有强制结束应用程序的功能,那么bug2.hrb也可以算是一个不错 的恶意破坏程序了。
怎样实现强制结束功能呢?将某一个按键设定为强制结束键,按一下就可以结束程序,这样看起来不错。本来想在console.c的console_task中编写当按下强制结束键时结束应用程序的处理,但是命令行窗口任务在应用程序运行的时候不会去读取FIFO缓冲区,强制结束键也就不管用 了,因此我们还是换个方式吧。
于是,我们只好把强制结束处理写在其他的任务中,而bootpack.c看起来很适合。强制结束键我们就定义为 “Shif+F1"吧, 当然,用其他的组合键也完全没问题,大家请按照自己的喜好修改吧。

void HariMain(void)
{
	...
	for (;;) {
		...
		}else{
			...
			if (256 <= i && i <= 511) {
				...
				if (i == 256 + 0x3b && key_shift != 0 && task_cons->tss.ss0 != 0) {	/* Shift+F1 */
					cons = (struct CONSOLE *) *((int *) 0x0fec);
					cons_putstr0(cons, "\nBreak(key) :\n");
					io_cli();	/* 不能在改变寄存器值时切换到其他任务 */
					task_cons->tss.eax = (int) &(task_cons->tss.esp0);
					task_cons->tss.eip = (int) asm_end_app;
					io_sti();
				}
				...
		}
	}
}

asm_ app_end是将naskfunc.nas中的end_ app改名之后得来的函数。
上述程序的工作原理是,当按下强制结束键时,改写命令行窗口任务的的寄存器值,并goto到asm_end_app,仅此而已。
这样一来程序会被强制结束,但也有个问题,那就是当应用程序没有在运行的时候,按下强制结束键会发生误操作。这样可不行,必须要确认task_cons->tss.ss0不为0时才能继续进行处理。
为此,我们还得进行一些修改,使得当应用程序运行时, 该值一定不为0;而当应用程序没有运行时,该值一定为0。
harib19c

// naskfunc.nas
_asm_end_app:
;	EAX为tss.esp0的地址
		MOV		ESP,[EAX]
		MOV		DWORD [EAX+4],0 ;这里!
		POPAD
		RET					; 返回cmd_app

// mtask.c
struct TASK *task_alloc(void)
{
	int i;
	struct TASK *task;
	for (i = 0; i < MAX_TASKS; i++) {
		if (taskctl->tasks0[i].flags == 0) {
			task = &taskctl->tasks0[i];
			task->flags = 1; /* 正在使用的标志 */
			task->tss.eflags = 0x00000202; /* IF = 1; */
			task->tss.eax = 0; /* 将其置为0*/
			...
			task->tss.ss0 = 0;
			return task;
		}
	}
	return 0; /* 已经全部正在使用 */
}

我们来 make run,按下“Shift+F1” 就可以轻松结束应用程序了。
在这里插入图片描述

我们再来创建个bug3.hrb, 该程序负责不断显示字符 a

void api_putchar(int c);
void api_end(void);

void HariMain(void)
{
	for (;;) {
		api_putchar('a');
	}
}

make run 按下强制结束键就可以顺利停止了。
在这里插入图片描述

也许在这个阶段就准备强制结束和异常处理还有点为时过早,因为我们还有很多功能想尽快实现。不过早点做好这些基础工作,在后面制作示例程序时就会轻松很多(更容易发现bug), 所以我们就把这部分内容放在今天做了。

四、用C语言显示字符串

1.0

我们已经做好了用来显示字符串的API,却没做可供C语言调用该API的函数。不过这个很容易,我们现在就来做做看。

_api_putstr0:	; void api_putstr0(char *s);
		PUSH	EBX
		MOV		EDX,2
		MOV		EBX,[ESP+8]		; s
		INT		0x40
		POP		EBX
		RET

利用上面的函数我们来写-个hello4.hrb:

void api_putstr0(char *s);
void api_end(void);

void HariMain(void)
{
	api_putstr0("hello, world\n");
	api_end();
}

make run – 什么都没显示出来,太奇怪了。
在这里插入图片描述
运行没成功感觉很不爽,不过在读程序排查原因思考对策的时候, 想到了一件与此无关的事:那时候对开头6个字节的改写,既然已经不能用RETF指令来结束程序了,那么“Hari" 不到了吧。
去掉6个字节的改写之后,程序就不再JMP到0x1b了,因此start_app的地址也需要修改一下。

int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
	...
	if (finfo != 0) {
		...
		if (finfo->size >= 8 && strncmp(p + 4, "Hari", 4) == 0) {
			start_app(0x1b, 1003 * 8, 64 * 1024, 1004 * 8, &(task->tss.esp0));
		} else {
			start_app(0, 1003 * 8, 64 * 1024, 1004 * 8, &(task->tss.esp0));
		}
	}
}

这样改过以后,hello3.hrb还能不能正常运行呢?我们来试验一下。哦哦, 不错不错,运行正常,太完美了…不过hello4.hrb还是不行。

2.0

为什么字符申显示API会失败呢?怎么想都不应该是a nask,nas的问题,难道这次又是内存段 的问题吗?于是我们对操作系统进行一点修改,使其在字符申显示API被调用的时候,显示EBX 寄存器的值。

int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
	char s[12];
	int ds_base = *((int *) 0xfe8);
	struct TASK *task = task_now();
	struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
	if (edx == 1) {
		cons_putchar(cons, eax & 0xff, 1);
	} else if (edx == 2) {
		cons_putstr0(cons,(char*)ebx + cs_base);	/*从此开始*/
		cons_putstr0(cons, (char *) ebx + ds_base);
		cons_putstr0(cons,s);	/*到此结束*/
	} else if (edx == 3) {
		cons_putstr1(cons, (char *) ebx + ds_base, ecx);
	} else if (edx == 4) {
		return &(task->tss.esp0);
	}
	return 0;
}

make run一下,然后运行hello4.hrb,屏幕上显示出00000400。这到底是怎么回事呢?hello4.hrb的文件大小只有114个字节,这样根本不可能显示出“hello, world"。
为什么EBX里面会被写入这样一个匪夷所思的值呢?其实是因为连接了.obj文件的bim2hrb认为“hello, world”这个字符申就应该存放在0x400这个地址中。
由bim2hrb生成的.hrb文件其实是由两个部分构成的:
代码部分与数据部分

虽然有两个部分,不过之前我们一直都是不考虑数据部分的。当程序中没有使用字符申和外 部变量(即在函数外面所定义的变量)时, 就会生成不包含数据部分的.hrb文件,因此之前的程序都没有任何问题。
由bim2hrb生成的.hrb文件,开头的36个字节不是程序,而是存放了下列这些信息:
0x0000 (DWORD) ------ 请求操作系统为应用程序准备的数据段的大小
0x0004 (DWORD) ------ “Hari" (.hrb文件的标记)
0x0008 (DWORD) ------ 数据段内预备空间的大小
0x000c (DWORD) ------ ESP初始值与数据部分传送目的地址
0x0010 (DWORD) ------ hrb文件内数据部分的大小
0x0014 (DWORD) ------ hb文件内数据部分从哪里开始
0x0018 (DWORD) ------ 0xe9000000
0x001c (DWORD) ------ 应用程序运行入口地址-0x20
0x0020 (DWORD) ------ malloc空间的起始地址

0x0000中存放的是数据段的大小。现在在“纸娃娃系统”中,应用程序用的数据段大小固定 为64KB,但根据应用程序的内容,可能会需要更 更多的内存空间。那么把数据段都改成1MB不就 好了吗?但这样一来,明明不需要那么多内存就可以运行的程序,也会被分配很大的内存空间, 内存很快就会不够用了。因此,我们就在应用程序中先写好需要多大的内存空间。
只是操作系统用来判断 0x0004中存放的是“Hari”这4个字节。这几个字符本来没什么用, 这是不是一个应用程序文件的标记,在文件中写入这样的标记,说不定在某些情况下就会派上用场。也许在这个世界上,除了我们的系统以外,还会有其他的软件也使用.hrb这个扩展名,那样的话,光凭扩展名来判断文件的格式就有点危险了。因此,我们在文件中加上一个标记,并在操作系统中添加相应的判断功能,如果没有找到这个标记,则停止运行该文件。
如果我们不去确认“Hari” 这个标记,而错误地运行了一个数据文件的话,这就和去运行一个JPEG文件差不多,会造成很严重的后果。不过现在我们使用了异常处理功能来保护操作系统,像磁盘数据被清除以及损坏电脑这种情况,已经完全可以避免了,而且操作系统也不会发生宕机。 能做到这些,都是异常处理的功劳。
0x0008中存放的内容为 “数据段内预备空间的大小”,不过这个值目前还没什么用(说不定以后也不会有什么用),大家不用管它就是了。 在hello4.hrb中,这个值并没有被设置,所以为0。
0x000c中存放的是应用程序启动时ESP寄存器的初始值,也就是说在这个地址之前的部分会 被作为栈来使用,而这个地址将被用于存放字符串等数据。 在hello4.hrb中,这个值为0x400·· 也就是说ESP寄存器的初始值为0x400,并且分配了1KB的栈空间。1KB这个数是从哪里来的呢? 其实是在生成hello4.bim的时候,在Makefile中设置的(注意看“stack:1k”这里!)。

hello4.bim : hello4.obj a_nask.obj Makefile
	$(OBJ2BIM) @$(RULEFILE) out:hello4.bim stack:1k map:hello4.map \
		hello4.obj a_nask.obj

0x0010中存放的是需要向数据段传送的部分的字节数。
0x0014中存放的是需要向数据段传 送的部分在.hrb文件中的起始地址。

0x0018中存放的是Oxe9000000这个数值,这个 数在内存中存放的时候形式为“000000E9”。 前面几个00的部分没什么用,后面的E9才是关键。其实E9是JMP指令的机器语言编码, 和后面4个字节合起来的话,就表示JMP到应用程序运行的入口地址。
0x001c中存放的是应用程序运行入口地址减去0x20后的值。为什么不直接写上入口地址而是 要减掉一个数呢?因为我们在0x0018(其实是0x :001b)写了一个JMP指令,这样可以通过JMP指 令跳转到应用程序的运行入口地址。通过这样的处理,只要先JMP到0x001b这个地址,程序就可以开始运行了。
0x0020中存放的是将来编写应用程序用malloc函数时要使用的地址,因此现在先不用管它。malloc这个函数和memman_ alloc函数十分相似。
根据上面的讲解,我们来修改console.c:

int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
{
	int i, segsiz, datsiz, esp, dathrb;
	...
	if (finfo != 0) {
		/* 找到文件的情况 */
		p = (char *) memman_alloc_4k(memman, finfo->size);
		file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
		if (finfo->size >= 36 && strncmp(p + 4, "Hari", 4) == 0 && *p == 0x00) {
			segsiz = *((int *) (p + 0x0000));
			esp    = *((int *) (p + 0x000c));
			datsiz = *((int *) (p + 0x0010));
			dathrb = *((int *) (p + 0x0014));
			q = (char *) memman_alloc_4k(memman, segsiz);
			*((int *) 0xfe8) = (int) q;
			set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);
			set_segmdesc(gdt + 1004, segsiz - 1,      (int) q, AR_DATA32_RW + 0x60);
			for (i = 0; i < datsiz; i++) {
				q[esp + i] = p[dathrb + i];
			}
			start_app(0x1b, 1003 * 8, esp, 1004 * 8, &(task->tss.esp0));
			memman_free_4k(memman, (int) q, segsiz);
		} else {
			cons_putstr0(cons, ".hrb file format error.\n");
		}
		memman_free_4k(memman, (int) p, finfo->size);
		cons_newline(cons);
		return 1;
	}
	/* 没有找到文件的情况 */
	return 0;
}

本次修改的要点如下:
1.文件中找不到“Hari”标志则报错。
2.数据段的大小根据.hrb文件中指定的值进行分配。
3.将.hrb文件中的数据部分先复制到数据段后再启动程序。
hello4.hrb运行成功了,但不是由bim2hrb生成的hello.hrb等程序就会出错。在以后的内容中, 即便使用汇编语言编写应用程序,我们也需要先生成.obj文件,然后再生成.bim并转换成.hrb。这样一来即便将文件扩展名误写为.hrb,也不会发生运行不该运行的文件的风险了。
在这里插入图片描述

下面我们用一个子来看看只用汇编语言编写应用程序的情形,我们写一段和hello4.c功能相同的程序。

[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "hello5.nas"]

		GLOBAL	_HariMain

[SECTION .text]

_HariMain:
		MOV		EDX,2
		MOV		EBX,msg
		INT		0x40
		MOV		EDX,4
		INT		0x40

[SECTION .data]

msg:
		DB	"hello, world", 0x0a, 0

将上面的程序make一下,得到78个字节的hello5.hrb, 而同样内容的hello4.hrb却需要114个字节,果然还是汇编语言比较节省呢(哈哈)。
在WCOFF模式下的nask中必须要使用SECTION命令, 这个命令是用来下达“将程序的这个部分放在代码段,将那个部分放在数据段”之类的指示(不过在.obj文件中不用“段”[segment] 这个词,而是用“区”[section],比如代码段在这里要被称为文本区[text section]。为什么呢?我也不知道,从一开始就是这样叫的,如果大家有意见的话…不知道该去找谁投诉了)。
如果大家明白了,.hrb文件中所包含的信息,那么对于asmhead.nas启动bootpack.hrb的部分,应该也会理解得更透彻了。

5、显示窗口

应用程序显示字符已经玩腻了,这次我们来挑战让应用程序显示窗口吧。这要如何实现呢?我们只要编写一个用来显示窗口的API就可以了,听起来很简单吧。
这个API应该写成什么样呢?考虑了一番之后,我们决定这样设计。
EDX=5
EBX=窗口缓冲区
ESI=窗口在x轴方向上的大小(即窗口宽度)
EDI=窗口在y轴方向上的大小(即窗口高度)
EAX=透明色
ECX=窗口名称
调用后,返回值如下:
EAX=用于操作窗口的句柄(用于刷新窗口等操作)
确定思路之后,新的问题又来了:我们没有考虑如何在调用API之后将值存入寄存器并返回给应用程序。
不过说起来,在asm_hrb_api中我们执行了两次PUSHAD,第一次是为了保存寄存器的值,第二次是为了向hrb_api传递值。因此如果我们查出被传递的变量的地址,在那个地址的后面应该正好存放着相同的寄存器的值。然后只要修改那个值,就可以由POPAD获取修改后的值,实现将 值返回给应用程序的功能。
我们来按这种思路编写程序。

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;	/* eax后面的地址 */
		/* 强行改写通过PUSHAD保存的值 */
		/* reg[0] : EDI,   reg[1] : ESI,   reg[2] : EBP,   reg[3] : ESP */
		/* reg[4] : EBX,   reg[5] : EDX,   reg[6] : ECX,   reg[7] : EAX */

	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) {
		return &(task->tss.esp0);
	} else if (edx == 5) {	/* 从此开始 */
		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);
		sheet_slide(sht, 100, 50);
		sheet_updown(sht, 3);	/* 背景层高度3位于task_a之上 */
		reg[7] = (int) sht;
	}	/* 到此结束 */
	return 0;
}

shtctl的值是bootpack.c的HariMain中的变量, 因此我们可以从0x0fe4地址获得。reg就是我们为了向应用程序返回值所动的手脚。
窗口我们就暂且显示在(100,50)这个位置上,背景层高度3。
bootpack.c中也添加了1行。

void HariMain(void)
{
	...
	*((int *) 0x0fe4) = (int) shtctl;
	...
}

我们编写这样一个应用程序来测试:

// a_nask.nas
_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
		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
		POP		EBX
		POP		ESI
		POP		EDI
		RET
// winhelo.c
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;
	win = api_openwin(buf, 150, 50, -1, "hello");
	api_end();
}

大家应该能理解吧?
make run” 快出现吧, …出来了!
在这里插入图片描述

6、在窗口中描绘字符和方块

虽然时间已经很晚了,大家也很困了,不过看到成功显示出窗口,我们的精神又振奋了起来,所以我们再来试一下在窗口上显示字符和方块吧。这两个功能都是现成的,只要加在API上面就可以了。
在窗口上显示字符的API如下:
EDX=6
EBX=窗口句柄
ESI=显示位置的x坐标
EDI=显示位置的)坐标
EAX=色号
ECX=字符串长度
EBP=字符串

描绘方块的API如下:

EDX=7
EBX=窗口句柄
EAX=x0
ECX=y0
ESI=x1
EDI =y1
EBP=色号

如果再多一个参数寄存器就要不够用了。 哎哟, 真悬!

接下来就是写程序了,这个简单。

// console.c
int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
{
	...
	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) {
		return &(task->tss.esp0);
	} else if (edx == 5) {
		...
	} else if (edx == 6) {	/*从此开始*/
		sht = (struct SHEET *) ebx;
		putfonts8_asc(sht->buf, sht->bxsize, esi, edi, eax, (char *) ebp + ds_base);
		sheet_refresh(sht, esi, edi, esi + ecx * 8, edi + 16);
	} else if (edx == 7) {
		sht = (struct SHEET *) ebx;
		boxfill8(sht->buf, sht->bxsize, ebp, eax, ecx, esi, edi);
		sheet_refresh(sht, eax, ecx, esi + 1, edi + 1);	/*到此结束*/
	}
	return 0;
}
```c
// a_nask.nas
_api_putstrwin:	; void api_putstrwin(int win, int x, int y, int col, int len, char *str);
		PUSH	EDI
		PUSH	ESI
		PUSH	EBP
		PUSH	EBX
		MOV		EDX,6
		MOV		EBX,[ESP+20]	; win
		MOV		ESI,[ESP+24]	; x
		MOV		EDI,[ESP+28]	; y
		MOV		EAX,[ESP+32]	; col
		MOV		ECX,[ESP+36]	; len
		MOV		EBP,[ESP+40]	; str
		INT		0x40
		POP		EBX
		POP		EBP
		POP		ESI
		POP		EDI
		RET
_api_boxfilwin:	; void api_boxfilwin(int win, int x0, int y0, int x1, int y1, int col);
		PUSH	EDI
		PUSH	ESI
		PUSH	EBP
		PUSH	EBX
		MOV		EDX,7
		MOV		EBX,[ESP+20]	; win
		MOV		EAX,[ESP+24]	; x0
		MOV		ECX,[ESP+28]	; y0
		MOV		ESI,[ESP+32]	; x1
		MOV		EDI,[ESP+36]	; y1
		MOV		EBP,[ESP+40]	; col
		INT		0x40
		POP		EBX
		POP		EBP
		POP		ESI
		POP		EDI
		RET

// winhelo2.c
int api_openwin(char *buf, int xsiz, int ysiz, int col_inv, char *title);
void api_putstrwin(int win, int x, int y, int col, int len, char *str);
void api_boxfilwin(int win, int x0, int y0, int x1, int y1, int col);
void api_end(void);

char buf[150 * 50];

void HariMain(void)
{
	int win;
	win = api_openwin(buf, 150, 50, -1, "hello");
	api_boxfilwin(win,  8, 36, 141, 43, 3 /* 黄色 */);
	api_putstrwin(win, 28, 28, 0 /* 黑色 */, 12, "hello, world");
	api_end();
}

大功告成, 之后结果如下。对了, 刚刚忘记说了, bug1.hrb已经没有用了, 所以把它删掉了哦。
在这里插入图片描述

总结

明天见哦。 运行得很顺利,心里相当满意呀。那么今天就到这里吧, 大家晚安!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2301773.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

后端开发:开启技术世界的新大门

在互联网的广阔天地中&#xff0c;后端开发宛如一座大厦的基石&#xff0c;虽不直接与用户 “面对面” 交流&#xff0c;却默默地支撑着整个互联网产品的稳定运行。它是服务器端编程的核心领域&#xff0c;负责处理数据、执行业务逻辑以及与数据库和其他后端服务进行交互。在当…

20250220解决使用top指令查看荣品PRO-RK3566开发板的CPU占用率为400%的问题

20250220解决使用top指令查看荣品PRO-RK3566开发板的CPU占用率为400%的问题 2025/2/20 19:14 缘起&#xff0c;使用荣品PRO-RK3566开发板配套的百度网盘中的SDK&#xff1a;Android13编译之后&#xff0c;查看RK3566的CPU占用率为400%。 开机就是400%&#xff0c;什么时候都是4…

win32汇编环境,窗口程序中使用月历控件示例二

;运行效果 ;win32汇编环境,窗口程序中使用月历控件示例二 ;以下示例有2个操作,即将每周的开始日进行改变,将默认的周日开始改为周一开始,同时实现点击哪个日期,则设定为哪个日期 ;直接抄进RadAsm可编译运行。重要部分加备注。 ;下面为asm文件 ;>>>>>>>…

java毕业设计之医院门诊挂号系统(源码+文档)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于ssm的医院门诊挂号系统。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 医院门诊挂号系统的主要使用者…

Linux 实操篇 组管理和权限管理、定时任务调度、Linux磁盘分区和挂载

一、组管理和权限管理 &#xff08;1&#xff09;Linux组基本介绍 在linux中的每个用户必须属于一个组&#xff0c;不能独立于组外 在linux中每个文件有所有者、所在组、其他组的概念 &#xff08;2&#xff09;文件/目录 所有者 一般为文件的创建者&#xff0c;谁创建了该…

MySql中的事务、MySql事务详解、MySql隔离级别

文章目录 一、什么是事务&#xff1f;二、事务四大特性ACID 2.1、原子性&#xff08;Atomicity&#xff09;2.2、一致性&#xff08;Consistency&#xff09;2.3、隔离性&#xff08;Isolation&#xff09;2.4、持久性&#xff08;Durability&#xff09; 三、事务操作/事务的…

10、k8s对外服务之ingress

service和ingress的作用 service的作用 NodePort&#xff1a;会在每个节点开放一个端口&#xff0c;端口号30000-32767。 也是只能用于内网访问&#xff0c;四层转发。实现负载均衡。不能基于域名进行访问。 clusterip&#xff1a;service的默认类型&#xff0c;只能在集群…

【STM32】舵机SG90

1.舵机原理 舵机内部有一个电位器&#xff0c;当转轴随电机旋转&#xff0c;电位器的电压会发生改变&#xff0c;电压会带动转一定的角度&#xff0c;舵机中的控制板就会电位器输出的电压所代表的角度&#xff0c;与输入的PWM所代表的角度进行比较&#xff0c;从而得出一个旋转…

个人简历html网页模板,科技感炫酷html简历模板

炫酷动效登录页 引言 在网页设计中,按钮是用户交互的重要元素之一。这样一款黑色个人简历html网页模板,科技感炫酷html简历模板,设计效果类似科技看板图,可帮您展示技能、任职经历、作品等,喜欢这种风格的小伙伴不要犹豫哦。该素材呈现了数据符号排版显示出人形的动画效…

<2.20>Leetcode哈希、双指针

还可以用双指针的做法 我们要找等于9 排序后从两边开始左右指针 2 3 7 9 如果29>9那么9肯定不能要 去掉 左边也一样 2 3 5 6 26小于9 那么2肯定不能要 去掉 package Leetcode; import java.util.*;public class 两数之和 {public int[] twoSum(int[] nums,int target…

vxe-table 如何实现跟 Excel 一样的数值或金额的负数自动显示红色字体

vxe-table 如何实现跟 Excel 一样的数值或金额的负数自动显示红色字体&#xff0c;当输入的值为负数时&#xff0c;会自动显示红色字体&#xff0c;对于数值或者金额输入时该功能就非常有用了。 查看官网&#xff1a;https://vxetable.cn gitbub&#xff1a;https://github.co…

【Word转PDF】在线Doc/Docx转换为PDF格式 免费在线转换 功能强大好用

在日常办公和学习中&#xff0c;将Word文档转换为PDF格式的需求非常普遍。无论是制作简历、撰写报告还是分享文件&#xff0c;都需要确保文档格式在不同设备上保持一致。而小白工具的“Word转PDF”功能正是为此需求量身打造的一款高效解决方案。 【Word转PDF】在线Doc/Docx转换…

陶瓷膜分离技术保障食品工业原料用水‌安全

陶瓷膜分离技术在食品工业中应用广泛&#xff0c;尤其是在保障原料用水的安全性方面发挥着重要作用。下面将从几个方面介绍陶瓷膜分离技术如何保障食品工业原料用水的安全&#xff1a; 高效过滤杂质&#xff1a;陶瓷膜具有非常细小的孔径(通常在纳米级别)&#xff0c;能够有效去…

蓝桥杯 2.基础算法

蓝桥杯 2.基础算法 文章目录 蓝桥杯 2.基础算法基础算法时空复杂度枚举模拟编程11-16递归编程17进制转换编程18-19前缀和编程20-22差分编程23-27离散化贪心编程28-37二分双指针编程38-45构造编程46-49位运算编程50-55 排序冒泡排序选择排序插入排序快速排序归并排序编程56-65 基…

Linux中的Ctrl+C与Ctrl+Z

CtrlC与CtrlZ的区别 在Linux中&#xff0c;当我们在执行一个命令运行代码时&#xff0c;由于运行时间过长或中途出现报错&#xff0c;此时&#xff0c;我们可能需要终止该操作&#xff0c;这时候&#xff0c;该使用CtrlC还是CtrlZ呢&#xff1f; 1、CtrlC CtrlC&#xff1a;终…

【深度学习】手写数字识别任务

数字识别是计算机从纸质文档、照片或其他来源接收、理解并识别可读的数字的能力&#xff0c;目前比较受关注的是手写数字识别。手写数字识别是一个典型的图像分类问题&#xff0c;已经被广泛应用于汇款单号识别、手写邮政编码识别等领域&#xff0c;大大缩短了业务处理时间&…

Linux-GlusterFS操作子卷

文章目录 分布式卷添加卷分布式卷删除子卷删除总卷 &#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;Linux专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2025年02月20日19点30分 分布式卷添加卷 Node1上进行操作 扩容 #服务器端 gluster volu…

修改阿里云服务器内网ip

运维同事问能不能改我自己的服务内网ip&#xff0c; 买了一台服99元服务器&#xff0c;以为不能结果&#xff0c;结果还真改成功了&#xff0c; 分享一下经验。 首先最后关闭服务器-关机&#xff0c;必须要关闭服务 访问vpc控制台&#xff0c;就是要新建立一个网络 https://…

用DeepSeek零基础预测《哪吒之魔童闹海》票房——从数据爬取到模型实战

系列文章目录 1.元件基础 2.电路设计 3.PCB设计 4.元件焊接 5.板子调试 6.程序设计 7.算法学习 8.编写exe 9.检测标准 10.项目举例 11.职业规划 文章目录 **一、为什么要预测票房&#xff1f;****二、准备工作****三、实战步骤详解****Step 1&#xff1a;数据爬取与清洗&am…

医院管理系统方案-基于蓝牙室内定位技术的院内智能导航系统:技术详解、功能设计及核心优势

文面向IT技术员、医院信息化负责人及物联网应用开发者&#xff0c;本文介绍了一款基于蓝牙室内定位技术的智能导航系统。该系统通过高精度定位与智能路径规划&#xff0c;极大提升了患者就医体验与医院运营效率。 如需获取院内智能导航系统技术文档可前往文章最下方获取&#x…