第20天 API
2020.4.23
1. 程序整理(harib17a)
-
實現由應用程序對OS功能的調用,即API(系統調用)。
- API,application program interface,應用程序(與系統之間的)接口。
- 由應用程序來調用(操作)系統中的功能來完成某種操作。
-
編寫一個在命令行窗口中顯示字符的API。(BIOS中也有這個功能,但是現在無法使用BIOS哦)
-
現在,console.c的代碼有點混亂,整理一下。
-
bootpack.h中有關console.c中函數和常量的聲明:
/* console.c */ struct CONSOLE { struct SHEET *sht; int cur_x, cur_y, cur_c; }; void console_task(struct SHEET *sheet, unsigned int memtotal); void cons_putchar(struct CONSOLE *cons, int chr, char move); void cons_newline(struct CONSOLE *cons); void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, unsigned int memtotal); void cmd_mem(struct CONSOLE *cons, unsigned int memtotal); void cmd_cls(struct CONSOLE *cons); void cmd_dir(struct CONSOLE *cons); void cmd_type(struct CONSOLE *cons, int *fat, char *cmdline); void cmd_hlt(struct CONSOLE *cons, int *fat);
- 結構體CONSOLE:
- 用於存放命令行窗口常用的變量。
- sht是指向命令行窗口圖層的指針。
- (cur_x,cur_y)代表指向下一個要輸入位置(光標所在處)的左上角座標。
- cur_c代表光標顏色,-1代表不顯示光標。
- 結構體CONSOLE:
-
console_task
void console_task(struct SHEET *sheet, unsigned int memtotal) { struct TIMER *timer; struct TASK *task = task_now(); struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; int i, fifobuf[128], *fat = (int *) memman_alloc_4k(memman, 4 * 2880); struct CONSOLE cons; /*聲明cons,用於存放命令行窗口的各種變量*/ char cmdline[30]; cons.sht = sheet; cons.cur_x = 8; /*初始值不再是16*/ cons.cur_y = 28; cons.cur_c = -1; /*光標默認不顯示*/ fifo32_init(&task->fifo, 128, fifobuf, task); timer = timer_alloc(); timer_init(timer, &task->fifo, 1); timer_settime(timer, 50); file_readfat(fat, (unsigned char *) (ADR_DISKIMG + 0x000200)); /*向內存中寫入解壓縮後的FAT*/ /* 顯示提示符 */ cons_putchar(&cons, '>', 1); for (;;) { io_cli(); if (fifo32_status(&task->fifo) == 0) { task_sleep(task); io_sti(); } else { i = fifo32_get(&task->fifo); io_sti(); if (i <= 1) { /* 光標定時器 */ if (i != 0) { timer_init(timer, &task->fifo, 0); /* 下次置0 */ if (cons.cur_c >= 0) { cons.cur_c = COL8_FFFFFF; } } else { timer_init(timer, &task->fifo, 1); /* 下次置1 */ if (cons.cur_c >= 0) { cons.cur_c = COL8_000000; } } timer_settime(timer, 50); } if (i == 2) { /* 光標開啓 */ cons.cur_c = COL8_FFFFFF; } if (i == 3) { /* 光標關閉 */ boxfill8(sheet->buf, sheet->bxsize, COL8_000000, cons.cur_x, cons.cur_y, cons.cur_x + 7, cons.cur_y + 15); cons.cur_c = -1; } if (256 <= i && i <= 511) { /* 鍵盤數據 */ if (i == 8 + 256) { /* 退格鍵 */ if (cons.cur_x > 16) { /* 用空格擦除光標後將光標前移一位 */ cons_putchar(&cons, ' ', 0); cons.cur_x -= 8; /*cur_x更新*/ } } else if (i == 10 + 256) { /* Enter */ /* 將光標用空格擦除後換行 */ cons_putchar(&cons, ' ', 0); cmdline[cons.cur_x / 8 - 2] = 0; cons_newline(&cons); cons_runcmd(cmdline, &cons, fat, memtotal); /* 運行命令 */ /* 顯示提示符 */ cons_putchar(&cons, '>', 1); } else { /* 一般字符 */ if (cons.cur_x < 240) { /* 顯示一個字符以後,將光標後移一位 */ cmdline[cons.cur_x / 8 - 2] = i - 256; cons_putchar(&cons, i - 256, 1); } } } /* 重新顯示光標 */ if (cons.cur_c >= 0) { boxfill8(sheet->buf, sheet->bxsize, cons.cur_c, cons.cur_x, cons.cur_y, cons.cur_x + 7, cons.cur_y + 15); } sheet_refresh(sheet, cons.cur_x, cons.cur_y, cons.cur_x + 8, cons.cur_y + 16); } } }
-
cons_putchar函數:
void cons_putchar(struct CONSOLE *cons, int chr, char move) { char s[2]; s[0] = chr; s[1] = 0; if (s[0] == 0x09) { /* 製表符 */ for (;;) { putfonts8_asc_sht(cons->sht, cons->cur_x, cons->cur_y, COL8_FFFFFF, COL8_000000, " ", 1); cons->cur_x += 8; if (cons->cur_x == 8 + 240) { cons_newline(cons); } if (((cons->cur_x - 8) & 0x1f) == 0) { break; /* 被32整除則break */ } } } else if (s[0] == 0x0a) { /* 換行 */ cons_newline(cons); } else if (s[0] == 0x0d) { /* 回車 */ /* 暫時不做任何操作 */ } else { /* 一般字符 */ putfonts8_asc_sht(cons->sht, cons->cur_x, cons->cur_y, COL8_FFFFFF, COL8_000000, s, 1); if (move != 0) { /* move不爲0,光標後移 */ cons->cur_x += 8; /*cur_x更新*/ if (cons->cur_x == 8 + 240) { /*到達行尾,換行*/ cons_newline(cons); } } } return; }
- cons_putchar函數的作用是:將特定的字符寫到命令行窗口上,並更新相關變量。
- 傳入參數:
- cons:用於存放命令行窗口的常用變量。
- chr:是字符的ASCII表示。將這個字符寫入到圖層上面。
- move:0代表光標不後移,1代表光標後移(cur_x+=8)。
- 無返回值
-
cons_newline:
void cons_newline(struct CONSOLE *cons) { int x, y; struct SHEET *sheet = cons->sht; if (cons->cur_y < 28 + 112) { cons->cur_y += 16; /* 到下一行 */ } else { /* 滾動 */ for (y = 28; y < 28 + 112; y++) { for (x = 8; x < 8 + 240; x++) { sheet->buf[x + y * sheet->bxsize] = sheet->buf[x + (y + 16) * sheet->bxsize]; } } for (y = 28 + 112; y < 28 + 128; y++) { for (x = 8; x < 8 + 240; x++) { sheet->buf[x + y * sheet->bxsize] = COL8_000000; } } sheet_refresh(sheet, 8, 28, 8 + 240, 28 + 128); } cons->cur_x = 8; /*重置cur_x*/ return; }
- 這是根據新的數據結構CONSOLE而重新寫的換行函數,作用是:將光標換到下一行,如果超出命令行底部,則滾動。
- 傳入參數:
- cons:用於存放命令行窗口的常用變量。
- 返回值:無返回值。
-
cons_runcmd:
void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, unsigned int memtotal) { if (strcmp(cmdline, "mem") == 0) { cmd_mem(cons, memtotal); } else if (strcmp(cmdline, "cls") == 0) { cmd_cls(cons); } else if (strcmp(cmdline, "dir") == 0) { cmd_dir(cons); } else if (strncmp(cmdline, "type ", 5) == 0) { cmd_type(cons, fat, cmdline); } else if (strcmp(cmdline, "hlt") == 0) { cmd_hlt(cons, fat); } else if (cmdline[0] != 0) { putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, "Bad command.", 12); cons_newline(cons); cons_newline(cons); } return; }
- cons_runcmd函數用於執行從命令行窗口輸入進來的指令。
- 傳入參數:
- cmdline:指向從命令行窗口輸入的字符串。
- cons:用於存放命令行窗口的常用變量。
- fat:指向爲FAT開闢的內存空間。
- memtotal:用於mem命令,是總的內存空間。
- 返回值:無返回值。
-
cmd_mem:
void cmd_mem(struct CONSOLE *cons, unsigned int memtotal) { struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; char s[30]; sprintf(s, "total %dMB", memtotal / (1024 * 1024)); putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, s, 30); cons_newline(cons); sprintf(s, "free %dKB", memman_total(memman) / 1024); putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, s, 30); cons_newline(cons); cons_newline(cons); return; }
- cmd_mem函數的作用是:執行mem命令。
- 傳入參數:
- cons:用於存放命令行窗口的常用變量。
- memtotal:總的內存空間。
- 返回值:無。
-
cmd_cls:
void cmd_cls(struct CONSOLE *cons) { int x, y; struct SHEET *sheet = cons->sht; for (y = 28; y < 28 + 128; y++) { for (x = 8; x < 8 + 240; x++) { sheet->buf[x + y * sheet->bxsize] = COL8_000000; } } sheet_refresh(sheet, 8, 28, 8 + 240, 28 + 128); cons->cur_y = 28; /*重置cur_y,因爲輸入cls必會輸入回車,回車會產生新行,新行的cur_x恆爲8*/ return; }
- cmd_cls函數:用於執行cls命令。
- 傳入參數:
- cons:用於存放命令行窗口的常用變量。
- 返回值:無。
-
void cmd_dir(struct CONSOLE *cons) { struct FILEINFO *finfo = (struct FILEINFO *) (ADR_DISKIMG + 0x002600); /*獲取文件名存放地址的開始地址*/ int i, j; char s[30]; for (i = 0; i < 224; i++) { if (finfo[i].name[0] == 0x00) { break; } if (finfo[i].name[0] != 0xe5) { if ((finfo[i].type & 0x18) == 0) { sprintf(s, "filename.ext %7d", finfo[i].size); for (j = 0; j < 8; j++) { s[j] = finfo[i].name[j]; } s[ 9] = finfo[i].ext[0]; s[10] = finfo[i].ext[1]; s[11] = finfo[i].ext[2]; putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, s, 30); cons_newline(cons); } } } cons_newline(cons); return; }
- cmd_dir函數:用於執行dir命令。
- 傳入參數:
- cons:用於存放命令行窗口的常用變量。
- 返回值:無。
-
cmd_type
void cmd_type(struct CONSOLE *cons, int *fat, char *cmdline) { struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; struct FILEINFO *finfo = file_search(cmdline + 5, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224); /*調用file_search函數*/ char *p; int i; if (finfo != 0) { /* 找到文件的情況 */ p = (char *) memman_alloc_4k(memman, finfo->size); file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00)); for (i = 0; i < finfo->size; i++) { cons_putchar(cons, p[i], 1); } memman_free_4k(memman, (int) p, finfo->size); } else { /* 沒找到 */ putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, "File not found.", 15); cons_newline(cons); } cons_newline(cons); return; }
- cmd_type函數:用於執行type命令。
- 傳入參數:
- cons:用於存放命令行窗口的常用變量。
- fat: FAT的開始地址。
- 返回值:無。
-
cmd_hlt:
void cmd_hlt(struct CONSOLE *cons, int *fat) { struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; struct FILEINFO *finfo = file_search("HLT.HRB", (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224); struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT; char *p; if (finfo != 0) { /* 找到了文件 */ p = (char *) memman_alloc_4k(memman, finfo->size); file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00)); set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER); farjmp(0, 1003 * 8); memman_free_4k(memman, (int) p, finfo->size); } else { /* 沒找到文件 */ putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, "File not found.", 15); cons_newline(cons); } cons_newline(cons); return; }
- cmd_hlt函數:執行可執行文件hlt.hrb。
- 傳入參數:
- cons:用於存放命令行窗口的常用變量。
- fat: FAT的開始地址。
- 返回值:無
-
file_search:(file.c中)
struct FILEINFO *file_search(char *name, struct FILEINFO *finfo, int max) { int i, j; char s[12]; for (j = 0; j < 11; j++) { s[j] = ' '; } j = 0; for (i = 0; name[i] != 0; i++) { if (j >= 11) { return 0; /* 沒找到 */ } if (name[i] == '.' && j <= 8) { j = 8; } else { s[j] = name[i]; if ('a' <= s[j] && s[j] <= 'z') { /* 將小寫轉化成大寫 */ s[j] -= 0x20; } j++; } } for (i = 0; i < max; ) { if (finfo[i].name[0] == 0x00) { break; } if ((finfo[i].type & 0x18) == 0) { for (j = 0; j < 11; j++) { if (finfo[i].name[j] != s[j]) { goto next; } } return finfo + i; /* 找到文件 */ } next: i++; } return 0; /* 沒有找到 */ }
- file_search函數:搜索文件是否存在。
- 傳入參數:
- name:指向文件名的指針。
- finfo: 文件名存放的開始地址。
- max:最大的文件數,224。
- 返回值:找到文件了,返回文件名的地址;否則返回0。
-
這樣修改以後,代碼清爽了很多。
-
make
後用VMware順利運行。
2. 顯示單個字符的API(1)(harib17b)
-
讓應用程序能夠顯示單個字符的方法很簡單:只要應用程序能夠調用cons_putchar就行了。
-
首先,做一個用於測試的應用程序。將要顯示的字符編碼存入AL寄存器,然後調用操作系統的函數,字符就顯示出來了。
- hlt.nas設想
[BITS 32] MOV AL,'A' CALL (cons_putchar的地址) fin: HLT JMP fin
- CALL是一個用來調用函數的指令。在C語言中,goto和函數調用的處理方式完全不同。但是在彙編語言中,CALL指令和JMP指令其實差不多。他們的區別只是:在執行CALL指令時,爲了能夠在接下來執行RET指令時能夠正確返回,會先將要返回的目標地址PUSH到棧中。
- 這裏有個問題,cons_putchar函數的地址我們應用程序是不知道的。應用程序編譯時,並不會包含OS的代碼。因此,要想知道cons_putchar的地址,只能編寫好OS後編譯,通過人工查看文件得到cons_putchar的地址,然後再寫入到應用程序的代碼中。
- hlt.nas設想
-
注意:cons_putchar函數是用C語言寫的函數,即便將字符編碼寫入寄存器,函數也無法接收,因此,必須在CALL之前將字符編碼寫入棧中才行。
-
解決思路:添加一箇中間層,應用程序先調用這個中間層,把相應的數據存放到棧中,然後這個中間層調用cons_putchar。
-
中間層:彙編語言編寫的用來將寄存器的值存入棧的函數。 這個中間層是OS的一部分,因此要寫在naskfunc.nas中。 在應用程序hlt.nas中,CALL的不再是cons_putchar的地址,而是這個中間層的地址。
-
修改naskfunc.nas:
_asm_cons_putchar: PUSH 1 AND EAX,0xff ; 將AH和EAX的高位置0,將EAX置爲已存入字符編碼的狀態 PUSH EAX PUSH DWORD [0x0fec] ; 將cons的地址壓棧 CALL _cons_putchar ADD ESP,12 ; 將棧中的數據丟棄 RET
- 詳解:
PUSH 1
:因爲現在是32位模式,所以PUSH一次向棧中壓入32位數據,也就是4字節數據。- cons的地址,應用程序是不知道的。因此,將這個地址事先保存在內存的特定地址上,這裏保存在BOOTINFO之前的0x0fec這個地址。
- 因此,還得修改console_task:
void console_task(struct SHEET *sheet, unsigned int memtotal) { …… cons.sht = sheet; cons.cur_x = 8; cons.cur_y = 28; cons.cur_c = -1; *((int *) 0x0fec) = (int) &cons; /*寫入指定內存*/ …… }
- 詳解:
-
make
將編寫好的OS編譯。注意一個叫bootpack.map的文件,這是一個文本文件,打開,找到_asm_cons_putchar的位置:
- 這個
0x00000BE3
就是asm_cons_putchar的地址了。
- 這個
-
將
0x0be3
寫入hlt.nas:[BITS 32] MOV AL,'A' CALL 0xbe3 fin: HLT JMP fin
-
應用程序中的API是:
MOV AL,'A'
和
CALL 0xbe3
-
重新make,用VMware運行試試:
- 不幸的是,出現BUG了。
- 不要輕易在真機上運行harib17b,因爲無法預料會產生什麼後果!
3. 顯示單個字符的API(2)(harib17c)
-
開發OS真刺激!一不小心就產生嚴重問題!
-
產生上述問題的原因:
- 應用程序對API執行CALL的時候,千萬不能忘記加上段號!!。
- 應用程序所在的段是
1003 * 8
;OS所在的段是2 * 8
。 - 不能使用普通的CALL,應該使用far-CALL指令。
- far-CALL指令和far-JMP一樣,只要同時指定段和偏移量即可。
-
修改hlt.nas:
[BITS 32] MOV AL,'A' CALL 2*8:0xbe3 fin: HLT JMP fin
-
相應地,還需要修改asm_cons_putchar:
_asm_cons_putchar: PUSH 1 AND EAX,0xff ; PUSH EAX PUSH DWORD [0x0fec] ; CALL _cons_putchar ADD ESP,12 ; RETF
- 將最後一句RET修改成RETF。
- RET是用於普通的CALL的。而RETF設計用於far-CALL的。
-
make
後用VMware運行:
- 字符成功顯示!
4. 結束應用程序(harib17d)
-
讓應用程序結束後不執行HLT,而是返回操作系統。
-
只要將應用程序中的HLT改成RET,就可以返回了,因爲需要返回,所以OS調用應用程序的地方也應該由JMP改成CALL。
-
由於調用的應用程序位於不同的段,因此應該調用far-CALL,應用程序那邊也不應該是RET,而是RETF。
-
C語言中沒有用來執行far-CALL的指令,因此在naskfunc.nas中創建farcall函數:
_farcall: ; void farcall(int eip, int cs); CALL FAR [ESP+4] ; eip, cs RET
- 和farjmp類似。
-
修改console.c中的cmd_hlt函數:
void cmd_hlt(struct CONSOLE *cons, int *fat) { …… if (finfo != 0) { /* 找到文件的情況 */ …… farcall(0, 1003 * 8); memman_free_4k(memman, (int) p, finfo->size); } else { …… } …… }
- 就是將farjmp(0, 1003*8)改成了farcall(0, 1003*8)。
-
注意,我們修改了OS的代碼,因此asm_cons_putchar的地址也就變化了,需要重新查看bootpack.map,經過查看:
0x00000BE8 : _asm_cons_putchar
-
修改hlt.nas:
[BITS 32] MOV AL,'A' CALL 2*8:0xbe8 RETF
- 其實就是把HLT改成RETF。
- 把0xbe3改成0xbe8。
-
重新
make
後用VMware運行:
- 成功顯示字符’A’!
-
重新修改hlt.nas,輸入hlt顯示字符串"hello"
[BITS 32] MOV AL,'h' CALL 2*8:0xbe8 MOV AL,'e' CALL 2*8:0xbe8 MOV AL,'l' CALL 2*8:0xbe8 MOV AL,'l' CALL 2*8:0xbe8 MOV AL,'o' CALL 2*8:0xbe8 RETF
-
重新make後用VMware運行:
- 成功顯示字符串“hello”。
5. 不隨操作系統版本而改變的API(harib17e)
-
harib17d暴露了一個問題,每次修改OS的代碼,asm_cons_putchar的地址都會變化,從而導致應用程序的代碼必須改變。這是不友好的。
-
解決上述問題的方式有很多。這裏採用如下這一種:
- CPU中有個專門用來註冊函數的地方,這個地方就是中斷處理程序註冊的地方。
- 當發生IRQ-1的時候調用某個函數f,這個函數f是在IDT中註冊的。
- IRQ只有0~15,而CPU用於通知異常狀態的中斷最多也只有32種(查閱資料)。
- IDT最多可以設置256個函數,因此還有剩下很多位置沒有使用。
- 從IDT中選一個空閒的項0x40號(0x30~0xff都是空閒的),將asm_cons_putchar註冊在這裏。
- 這樣,我們只要調用INT 0x40產生一箇中斷就可以調用asm_cons_putchar函數了。
-
修改init_gdtidt(dsctbl.c):
void init_gdtidt(void) { …… /* IDT的設置 */ 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_cons_putchar, 2 * 8, AR_INTGATE32); /*0x40號註冊asm_cons_putchar*/ return; }
-
修改hlt.nas:
[BITS 32] MOV AL,'h' INT 0x40 MOV AL,'e' INT 0x40 MOV AL,'l' INT 0x40 MOV AL,'l' INT 0x40 MOV AL,'o' INT 0x40 RETF
- 將
CALL 2\*8:0xbe8
改成了INT 0x40
。
- 將
-
修改naskfunc.nas中的asm_cons_putchar:
_asm_cons_putchar: STI PUSH 1 AND EAX,0xff PUSH EAX PUSH DWORD [0x0fec] CALL _cons_putchar ADD ESP,12 IRETD
- 由於INT會產生中斷,因此RETF是無法返回的,需要使用IRETD指令。
- INT調用時,對於CPU來講相當於執行了中斷處理,因此,在調用的同時CPU會自動執行CLI指令來禁止中斷請求。。
- 這裏,我們只是用INT來代替CALL,我們不想看到“API處理過程中鍵盤無法輸入”這種情形,因此在開頭添加了STI指令。
- 還有一種方式:在將函數註冊到CPU的時候修改設置來禁止CPU擅自執行CLI,這裏不再深入。
-
make
後用VMware運行:
- 順利運行。
-
應用程序比之前的還小了:
- harib17d的hlt.hrb:46字節。
- harib17e的hlt.hrb:21字節。
- 這是因爲:far-CALL指令需要7個字節,而INT指令還需要2個字節。
6. 爲應用程序自由命名(harib17f)
-
修改cons_runcmd:
void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, unsigned int memtotal) { if (strcmp(cmdline, "mem") == 0) { cmd_mem(cons, memtotal); } else if (strcmp(cmdline, "cls") == 0) { cmd_cls(cons); } else if (strcmp(cmdline, "dir") == 0) { cmd_dir(cons); } else if (strncmp(cmdline, "type ", 5) == 0) { cmd_type(cons, fat, cmdline); } else if (cmdline[0] != 0) { if (cmd_app(cons, fat, cmdline) == 0) { /* 不是命令,不是應用程序,也不是空行 */ putfonts8_asc_sht(cons->sht, 8, cons->cur_y, COL8_FFFFFF, COL8_000000, "Bad command.", 12); cons_newline(cons); cons_newline(cons); } } return; }
- 去掉了cmd_hlt函數,創建了新的函數cmd_app。
-
cmd_app函數:
int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline) { struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; struct FILEINFO *finfo; struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT; char name[18], *p; int i; /* 根據命令行生成文件名 */ for (i = 0; i < 13; i++) { if (cmdline[i] <= ' ') { break; } name[i] = cmdline[i]; } name[i] = 0; /* 暫且將文件名的後面置爲0 */ /* 尋找文件 */ finfo = file_search(name, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224); if (finfo == 0 && name[i - 1] != '.') { /* 找不到文件,在文件名後面加上.hrb後重新尋找 */ name[i ] = '.'; name[i + 1] = 'H'; name[i + 2] = 'R'; name[i + 3] = 'B'; name[i + 4] = 0; finfo = file_search(name, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224); } if (finfo != 0) { /* 找到文件 */ p = (char *) memman_alloc_4k(memman, finfo->size); 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; } /* 沒有找到文件 */ return 0; }
- cmd_app用來根據命令行的內容判斷文件名,並運行相應的應用程序,如果找到了文件則返回1,如果沒有找到文件則返回0。
- cmd_app支持輸入
可執行文件
和可執行文件.hrb
的形式。(這一點模仿了Windows) - 現在的工作流程是:當輸入的命令不是mem、cls、dir、type其中之一是,則調用cmd_api,如果返回0則作爲錯誤處理。
-
將hlt.nas改成hello.nas。
make
後在VMware中運行(Makefile已修改,將hello.nas添加到磁盤映像文件中了)。- 輸入dir確認磁盤文件情況,輸入hello.hrb:
- 輸入hlt(hlt已經被刪除,因此會提示Bad command):
- 輸入dir確認磁盤文件情況,輸入hello.hrb:
7. 當心寄存器(harib17g)
-
修改hello.hrb:
[INSTRSET "i486p"] [BITS 32] MOV ECX,msg putloop: MOV AL,[CS:ECX] CMP AL,0 JE fin INT 0x40 ADD ECX,1 JMP putloop fin: RETF msg: DB "hello",0
- DB: 僞指令,是定義一字節變量的意思。字符h用0x68表示,0x68一字節。
- msg是一個標籤,標籤是一個數字,這個數字的值就是內存地址。MOV ECX,msg 相當於把一個數字寫入ECX中,相當於把一個內存地址寫入ECX中。
- 雖然ECX是32位寄存器,一個內存地址佔8位,源和目的的位數不同,但是msg是一個標籤,是一個數字,這個數字的含義是一個內存地址。
- 使用循環,這樣在寫入長字符串時有優勢。
-
make
後用VMware運行。結果執行hello時,只輸出了一個字母h。很是奇怪啊。 -
應用程序肯定沒問題,問題出現在OS上,那就是asm_cons_putchar。
-
修改asm_cons_putchar:
_asm_cons_putchar: STI PUSHAD PUSH 1 AND EAX,0xff PUSH EAX PUSH DWORD [0x0fec] CALL _cons_putchar ADD ESP,12 POPAD IRETD
- 添加了兩行代碼:
PUSHAD
和POPAD
。 - INT40之後ECX寄存器的值發生了變化,應該是cons_putchar改動了ECX的值,加上這兩句代碼,將寄存器的值全部還原。
- 添加了兩行代碼:
-
make
後用VMware運行,順利運行(這裏修改了字符串“hello”改成了“Hi,stranger!”)。
8. 用API顯示字符串(harib17h)
-
顯示單個字符的API在實際中比顯示一串字符串的API的情況少得多。
-
參考其他OS顯示字符串的API,一般有兩種方式:
- 顯示一串字符串,遇到字符編碼0結束。
- 指定要顯示字符串的長度,然後顯示。
-
由於兩種方式的實現都不困難,因此,二者均在次OS中實現。
-
修改console.c添加函數cons_putstr0和cons_putstr1:
void cons_putstr0(struct CONSOLE *cons, char *s) { for (; *s != 0; s++) { cons_putchar(cons, *s, 1); } return; } void cons_putstr1(struct CONSOLE *cons, char *s, int l) { int i; for (i = 0; i < l; i++) { cons_putchar(cons, s[i], 1); } return; }
- 有了這兩個函數,便可以簡化cons_runcmd、cons_mem、cons_dir、cons_type這幾個函數。
\n
代表換行符,即0x0a。\t
代表製表符,即0x09。
-
修改cons_runcmd:
void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, unsigned int memtotal) { if (strcmp(cmdline, "mem") == 0) { cmd_mem(cons, memtotal); } else if (strcmp(cmdline, "cls") == 0) { cmd_cls(cons); } else if (strcmp(cmdline, "dir") == 0) { cmd_dir(cons); } else if (strncmp(cmdline, "type ", 5) == 0) { cmd_type(cons, fat, cmdline); } else if (cmdline[0] != 0) { if (cmd_app(cons, fat, cmdline) == 0) { /* 不是命令,不是應用程序,也不是空行 */ cons_putstr0(cons, "Bad command.\n\n"); /*\n代表換行*/ } } return; }
- 這樣,cons_newline(cons)可以使用
\n
代替。
- 這樣,cons_newline(cons)可以使用
-
修改cmd_mem:
void cmd_mem(struct CONSOLE *cons, unsigned int memtotal) { struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; char s[60]; sprintf(s, "total %dMB\nfree %dKB\n\n", memtotal / (1024 * 1024), memman_total(memman) / 1024); cons_putstr0(cons, s); return; }
-
修改cmd_dir:
void cmd_dir(struct CONSOLE *cons) { struct FILEINFO *finfo = (struct FILEINFO *) (ADR_DISKIMG + 0x002600); int i, j; char s[30]; for (i = 0; i < 224; i++) { if (finfo[i].name[0] == 0x00) { break; } if (finfo[i].name[0] != 0xe5) { if ((finfo[i].type & 0x18) == 0) { sprintf(s, "filename.ext %7d\n", finfo[i].size); for (j = 0; j < 8; j++) { s[j] = finfo[i].name[j]; } s[ 9] = finfo[i].ext[0]; s[10] = finfo[i].ext[1]; s[11] = finfo[i].ext[2]; cons_putstr0(cons, s); } } } cons_newline(cons); return; }
-
修改cmd_type:
void cmd_type(struct CONSOLE *cons, int *fat, char *cmdline) { struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR; struct FILEINFO *finfo = file_search(cmdline + 5, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224); char *p; if (finfo != 0) { /* 找到文件 */ p = (char *) memman_alloc_4k(memman, finfo->size); file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00)); cons_putstr1(cons, p, finfo->size); memman_free_4k(memman, (int) p, finfo->size); } else { /* 沒找到文件 */ cons_putstr0(cons, "File not found.\n"); } cons_newline(cons); return; }
-
現在的問題是:如何將cons_putstr0和cons_putstr1變成API。
- 最簡單的方法是:像調用單個字符cons_putchar的API一樣,爲cons_putstr0和cons_putstr1分配INT 41和INT 42來調用這兩個函數。不幸的是,這樣IDT很快就會被用光。
- 借鑑BIOS的調用方式:
- 在寄存器中存入功能號,使得只使用1個INT就可以選擇調用不同的函數。
- 用來存放功能號的寄存器一般是AH(8位寄存器),這樣最多隻能設置256個API函數。
- 這裏,改用寄存器EDX(32位寄存器)來存放功能號,這樣可以設置大約42億API函數(夠用了吧)。
-
功能號暫時按如下劃分(寄存器的用法也是隨意設定的):
- 功能號1:顯示單個字符(AL=字符編碼)
- 功能號2:顯示字符串0(EBX=字符串地址(msg))
- 功能號3:顯示字符串1(EBX=字符串地址(msg),ECX=字符串長度)
-
小知識點:內存地址佔一個字節(8位,內存是一個字節一個字節拼接在一起的);32位OS中,指針佔四個字節(32位)。指針不等於地址,指針有類型,地址沒有類型。指針指向地址。
-
將asm_cons_putchar改成一個新的函數asm_hrb_api:
_asm_hrb_api: STI PUSHAD ; 用於保存寄存器的值 PUSHAD ; 用於向hrb_api傳遞參數 CALL _hrb_api ADD ESP,32 POPAD IRETD
-
C語言編寫的API處理函數hrb_api(console.c中):
void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax) { struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec); if (edx == 1) { cons_putchar(cons, eax & 0xff, 1); /*只取eax的低8位*/ } else if (edx == 2) { cons_putstr0(cons, (char *) ebx); } else if (edx == 3) { cons_putstr1(cons, (char *) ebx, ecx); } return; }
- 其實傳進來的ebx就是一個數字,不過這個數字的含義是一個地址。
-
修改IDT的設置,將asm_hrb_api註冊成INT 40:
void init_gdtidt(void) { …… /* INDT設置 */ 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); /*asm_hrb_api註冊*/ return; }
-
此時,需要修改應用程序hello.nas,需要向寄存器EDX中寫入1,才能調用cons_putchar。
[INSTRSET "i486p"] [BITS 32] MOV ECX,msg MOV EDX,1 ;EDX寫入1,調用1號功能的API。 putloop: MOV AL,[CS:ECX] CMP AL,0 JE fin INT 0x40 ADD ECX,1 JMP putloop fin: RETF msg: DB "hello",0
-
make
後用VMware運行:
- 成功顯示。
-
現在使用功能號2或3來顯示字符串。編寫新的應用程序hello2.nas:
[INSTRSET "i486p"] [BITS 32] MOV EDX,2 MOV EBX,msg INT 0x40 RETF msg: DB "hello",0
-
make
後在VMware上運行:
- 很不幸,出BUG了!(明天再研究吧!)
9. 寫在深夜
- 現在是2020.4.23 23:46。腦子暈暈的。寫到現在,確實累了。
- 這篇文檔代碼有1000+行。又刷新了記錄了啊!
- 明天再發布吧,今晚太趕了。
- 恭喜啊,超過400頁了!
- 夜深了,明天繼續。