第20天 API

第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_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的地址,然後再寫入到應用程序的代碼中。
  • 注意: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):

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
    
    • 添加了兩行代碼:PUSHADPOPAD
    • 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代替。
  • 修改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頁了!
  • 夜深了,明天繼續。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章