第28天 文件操作与文字显示
2020.5.8
1. alloca(1)(harib25a)
-
编写一个求素数的应用程序sosu.c:
#include <stdio.h> #include "apilib.h" #define MAX 1000 void HariMain(void) { char flag[MAX], s[8]; int i, j; for (i = 0; i < MAX; i++) { flag[i] = 0; } for (i = 2; i < MAX; i++) { if (flag[i] == 0) { /* 没有标记的为素数 */ sprintf(s, "%d ", i); api_putstr0(s); for (j = i * 2; j < MAX; j += i) { flag[j] = 1; /* 给它的倍数做上标记 */ } } } api_end(); }
- 这个应用程序求1000以内的素数。
-
在suso目录下生成一个精简的OS磁盘映像并用VMware运行:
-
修改一下MAX的宏定义,改成求10000以内的素数,并另存为新的应用程序sosu2.c。
- sosu2.c需要在栈中保存很多变量,光flag[10000]就大概需要10KB的空间,因此在Makefile中指定栈大小改成了11k。
-
make run
后,出现了一条警告“Warning: can’t link __alloca”。不管它,运行sosu2.hrb试试:
- 产生了一般保护性中断!
-
警告在提示:缺少一个叫__alloca的函数。
- 使用的C语言编译器规定:如果栈中的变量超过4KB,需要调用__alloca这个函数。这个函数的功能是根据OS的规格来获取栈的空间。在Windows或者Liunx中,如果不调用这个函数,而是仅对ESP进行减法运算的话,貌似无法成功获得内存空间。小于4KB时,只要对ESP进行减法运算即可。
- 不过在此OS中,对栈的管理并没有什么特殊的设计,因此也用不着去调用__alloca函数,可C语言编译器并不是此OS专用的,于是又会擅自去调用这个函数了。
-
为了解决上述问题,需要编写一个__alloca函数,只对ESP进行减法运算,而不做其他多余的操作。
-
其实,不用__alloca函数也可以运行,因为可以调用OS的9号API,使用api_malloc函数申请内存空间(笑)。编写应用程序sosu3.c:
#include <stdio.h> #include "apilib.h" #define MAX 10000 void HariMain(void) { char *flag, s[8]; int i, j; api_initmalloc(); flag = api_malloc(MAX); /*申请10000字节的内存空间*/ for (i = 0; i < MAX; i++) { flag[i] = 0; } for (i = 2; i < MAX; i++) { if (flag[i] == 0) { sprintf(s, "%d ", i); api_putstr0(s); for (j = i * 2; j < MAX; j += i) { flag[j] = 1; } } } api_end(); }
-
make
后用VMware运行:
2. alloca(2)(harib25b)
-
思前想后,虽然能够使用api_malloc申请内存空间,但是__alloca函数还是要写。编写__alloca函数(apilib目录下的alloca.nass):
[FORMAT "WCOFF"] [INSTRSET "i486p"] [BITS 32] [FILE "alloca.nas"] GLOBAL __alloca [SECTION .text] __alloca: ADD EAX,-4 SUB ESP,EAX JMP DWORD [ESP+EAX] ; 代替RET
- alloca.nas的归类有点难分,所以暂时现将它放在apilib目录下,虽然它不是个API。
-
详细讲解__alloca函数:
- __alloca函数会在下述情况下被C语言的程序调用(采用near-CALL的方式):
- 要执行的操作:从栈中分配EAX个字节的内存空间(ESP-=EAX)。
- 要遵守的规则:不能改变ECX、EDX、EBX、EBP、ESI、EDI的值(可以临时改变,但是要使用PUSH/POP来复原)
- 根据上述描述,于是编写出了第一版错误的alloca:
SUB ESP,EAX RET
- 这个程序是无法运行的,因为RET返回的地址保存在ESP中,而ESP的值在这里被改变了,于是读取了错误的返回地址。注意:RET相当于
POP EIP
。
- 这个程序是无法运行的,因为RET返回的地址保存在ESP中,而ESP的值在这里被改变了,于是读取了错误的返回地址。注意:RET相当于
- 接着又编写了第二版错误的alloca:
SUB ESP,EAX JMP DWORD [ESP+EAX] ;代替RET
- JMP的目标地址从[ESP]变成了[ESP+EAX],ESP+EAX的值刚好是减法运算之前的ESP值,也就是正确的地址。
RET
指令相当于POP EIP
,而POP EIP
又相当于下面两条指令:MOV EIP,[ESP] ;没有这个指令,用JMP [ESP]代替。 ADD ESP,4
- 也就是说刚刚忘记给ESP+4了。
- 编写第三版错误的alloca:
SUB ESP,EAX JMP DWORD [ESP+EAX] ;代替RET ADD ESP,4
- 第三版错误的原因是ADD指令的位置,将ADD指令放在了JMP指令的后面,所以ADD指令不会被执行。
- 编写第四版正确的alloca:
SUB ESP,EAX ADD ESP,4 JMP DWORD [ESP+EAX-4] ;代替RET
- 用这个程序直接作为alloca.nas是完全没有问题的。
- 编写第五版正确的alloca:
ADD EAX,-4 SUB ESP,EAX JMP DWORD [ESP+EAX] ; 代替RET
- 和第四版大同小异,不多精简了一点儿。
- __alloca函数会在下述情况下被C语言的程序调用(采用near-CALL的方式):
-
make
后用VMware重新运行sosu2.hrb,这次成功输出,没有产生一般保护性中断。 -
这样的话sosu2.hrb和sosu3.hrb在运行结果上没有任何区别,看一下文件大小:
- sosu2.hrb:1484字节
- sosu3.hrb:1524字节
- 虽然差别不大,但是还是sosu2.hrb小一点。既然小一点,那么把winhelo也从栈中分配空间吧,不再用malloc了。
- 修改winhelo.c:
#include "apilib.h" void HariMain(void) { int win; char buf[150 * 50]; win = api_openwin(buf, 150, 50, -1, "hello"); for (;;) { if (api_getkey(1) == 0x0a) { break; } } api_end(); }
- 在Makefile中设定
STACK = 8k
,因为buf大概需要7.5KB的空间。 make
后可以成功运行应用程序winhelo,要知道修改前有7664KB。之所以对文件大小这样苛刻,是因为担心磁盘空间不够(后面还要支持汉字字库),因此应用程序能小就小。
- 在Makefile中设定
-
顺便把winhelo2.c也改了:
#include "apilib.h" void HariMain(void) { int win; char buf[150 * 50]; 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"); for (;;) { if (api_getkey(1) == 0x0a) { break; } } api_end(); }
- 把Makefile中的STACK设置为8K。
-
比较一下winhelo[23]?前后的大小:
winhelo(buf[]) winhelo2(buf[]) winhelo3(malloc方式) 改良前 7664b 7808b 359b(未改良) 改良后 174b 315b 359b(未改良)
3. 文件操作API(harib25c)
-
所谓文件操作API,就是可以指定文件,并能够自由读写文件内容的API。现在的OS还不能对磁盘进行写入操作,因此只要能够读取文件内容就可以了。
-
一般的OS中,输入输出文件的API基本上都有如下的功能:
- 打开:open
- 打开和关闭API用来对要读写的文件进行打开和关闭的操作。一个文件必须先打开才能进行读写操作,因为在打开时,OS需要对读写的文件进行准备工作,关闭时也需要进行一些善后处理。
- 打开文件时需要指定文件名,如果打开成功,OS返回文件句柄。在随后的操作中,只要提供这个文件的句柄就可以进行读写操作了,操作结束后将文件关闭。
- 定位:seek
- 定位API的功能是指定下次读取、写入命令需要操作的目标位于文件中的位置。
- 读取:read
- 读取和写入API需要指定需要读取(写入)的数据长度以及内存地址,文件的内容会被传送至内存。(写入操作时是由内存传至文件)
- 写入:write
- 同读取
- 关闭:close
- 同打开
- 打开:open
-
设计API:
- 打开文件
- EDX = 21
- EBX = 文件名
- 返回值EAX = 文件句柄(当OS返回0时,代表文件打开失败)
- 关闭文件
- EDX = 22
- EAX = 文件句柄
- 文件定位
- EDX = 23
- EAX = 文件句柄
- ECX = 定位模式
- 0:定位起点为文件开头
- 1:定位起点为当前访问位置
- 2:定位起点为文件末尾
- EBX = 定位偏移量
- 获取文件大小
- EDX = 24
- EAX = 文件句柄
- ECX = 文件大小获取模式
- 0:普通文件大小
- 1:当前读取位置从文件开头算起的偏移量
- 2:当前读取位置从文件末尾算起的偏移量
- 返回值EAX = 文件大小
- 文件读取
- EDX = 25
- EAX = 文件句柄
- EBX = 缓冲区地址
- ECX = 最大读取字节数
- 返回值EAX = 本次读取到的字节数
- 打开文件
-
修改bootpack.h:
struct TASK { int sel, flags; int level, priority; struct FIFO32 fifo; struct TSS32 tss; struct SEGMENT_DESCRIPTOR ldt[2]; struct CONSOLE *cons; int ds_base, cons_stack; struct FILEHANDLE *fhandle; int *fat; }; struct FILEHANDLE { char *buf; int size; int pos; };
- 结构体TASK增加了成员fhandle和fat,是为了让hrb_api和cmd_app能够使用在console_task中声明的变量。
- fhandle是一个指向FILEHANDLE的指针,用于存放应用程序打开文件的信息(应用程序可能打开不止一个文件)。
- fat是指向文件配置表的指针。
- 结构体FILEHANDLE,文件句柄:
- 结构体TASK增加了成员fhandle和fat,是为了让hrb_api和cmd_app能够使用在console_task中声明的变量。