第22天 用C語言編寫應用程序

第22天 用C語言編寫應用程序

2020.4.26

1. 保護操作系統(5)(harib19a)

  • 把OS的段地址存入DS和訪問OS管理的內存空間這兩招已經不能用了。

  • 試試在定時器上做手腳:這樣,光標閃爍就會變得異常,任務切換的速度也會變慢。

  • 編寫惡意應用程序crack3.nas:

    [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
    
    • 原本定時器的設定值是11932=0x2e9c:
      io_out8(PIT_CTRL, 0x34);
      io_out8(PIT_CNT0, 0x9c);
      io_out8(PIT_CNT0, 0x2e);
      
    • 現在設定值變成了0xffff=65535,顯然,這樣光標閃爍就會變慢。
  • make後用VMware運行:

    • 顯示了一般保護性異常。
    • 當以應用程序模式運行時,執行IN指令和OUT指令都會產生一般保護性異常。當然,通過修改CPU設置,可以允許應用程序使用IN指令和OUT指令,但這樣會留下隱患。
  • 繼續編寫惡意應用程序crack4.nas:

    [INSTRSET "i486p"]
    [BITS 32]
            CLI
    fin:
            HLT
            JMP		fin
    
    • 先CLI再HLT,這樣電腦就死機了吧?
  • make後用VMware運行:

    • 再次顯示了一般保護性異常。
    • 當以應用程序模式運行時,執行CLI、STI和HLT這些指令都會產生異常。因爲中斷是由OS來管理的,應用程序不可以隨便進行控制。
    • 不能執行HLT,應用程序就無法省電,不過可以通過調用任務休眠的API來實現,而不能由應用程序自己來執行HLT。
    • 此外,在多任務下,調用休眠API還可以讓OS將CPU時間分配給其他任務。
  • OS代碼中有io_cli函數,如果應用程序far-CALL這個函數呢?找到bootpack.map文件中io_cli的地址:

    0x00000AC1 : _io_cli
    
  • 編寫惡意應用程序crack5.nas:

    [INSTRSET "i486p"]
    [BITS 32]
            CALL	2*8:0xac1
            MOV		EDX,4
            INT		0x40
    
  • make後用VMware運行試試:

    • 再次產生一般保護性異常。
    • 如果應用程序可以CALL任何地址的話,那crack5.nas就成功了。因此,CPU規定除了設置好的地址以外,禁止應用程序CALL其他的地址。因此,此OS中應用程序調用OS只能採用INT 40的方法。
  • 既然應用程序只能調用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;
    }
    
  • 編寫惡意應用程序crack6.nas:

    [INSTRSET "i486p"]
    [BITS 32]
            MOV		EDX,123456789
            INT		0x40
            MOV		EDX,4
            INT		0x40
    
  • make後用VMware運行:

    • OS崩潰了。
    • OS之所以崩潰是因爲出現了“內鬼API”——123456789。

2. 幫助發現bug(harib19b)

  • CPU的異常處理功能,除了可以保護OS免遭應用程序的破壞,還可以幫助我們在編寫應用程序時及早發現bug。

  • 編寫有bug的應用程序bug1.c:

    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,即超出數組邊界。
  • make後用VMware運行試試:

    • 這應該是產生了沒有設置過的異常所導致的。
  • 順便提一下,保護OS暫時結束了,因而把“內鬼API”刪掉,也把crack[1-6].[nas|c]刪除

  • 由於a數組是保存在棧中的,因此數組越界產生了棧異常

    • 棧異常的中斷號是0x0c
    • 根據CPU的說明書,從0x00到0x1f都是異常所使用的中斷,所以,IRQ的中斷號都是從0x20開始的。
    • 其他比較有用的異常有:
      • 0x00號:除零異常,當試圖除以0時產生。
      • 0x06號:非法指令異常,當試圖執行CPU無法理解的機器語言指令時產生,比如當試圖執行一段數據時可能產生。
  • 編寫asm_inthandler0c函數:

    _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號中斷時,必須強制結束應用程序。
  • 編寫inthandler0c函數(console.c文件中,inthandler0c和inthandler0d都在console.c中):

    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);	/* 強制結束應用程序 */
    }
    
    • inthandler0c和inthandler0d函數的區別只是顯示信息不同。
  • 在IDT中註冊:

    void init_gdtidt(void)
    {
        ……
        /* IDT設置 */
        set_gatedesc(idt + 0x0c, (int) asm_inthandler0c, 2 * 8, AR_INTGATE32); /*註冊0x0c號中斷*/
        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);
    
        return;
    }
    
  • make後在VMware上運行:

    • 顯示出“AB”以後才產生異常,也就是寫入的“C”被判定爲異常,而“B”並沒有被判定成異常。
    • 因此,異常並不能發現所有的bug。不過相比於一個bug都發現不了,發現一個bug也是從無到有的。
  • 修改inthandler0c和inthandler0d,讓它們幫忙輸出引發異常的指令的地址

    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)顯示出來。
    • 想要得到異常時其他寄存器的值,只需要參照下圖即可:
  • make後在VMware上運行:

    • 顯示EIP=0x42。
    • 查看bug1.map的內容:
      ……
      0x00000024 : _HariMain
      0x00000052 : _api_putchar
      ……
      
      • 顯然,0x42位於HariMain和api_putchar之間。那麼0x42位於HariMain中。
    • 查看bug1.lst:
      1 00000000                                 [FORMAT "WCOFF"]
      2 00000000                                 [INSTRSET "i486p"]
      3 00000000                                 [OPTIMIZE 1]
      4 00000000                                 [OPTION 1]
      5 00000000                                 [BITS 32]
      6 00000000                                 	EXTERN	_api_putchar
      7 00000000                                 	EXTERN	_api_end
      8 00000000                                 [FILE "bug1.c"]
      9                                          [SECTION .text]
      10 00000000                                 	GLOBAL	_HariMain
      11 00000000                                 _HariMain:
      12 00000000 55                              	PUSH	EBP
      13 00000001 89 E5                           	MOV	EBP,ESP
      14 00000003 83 EC 70                        	SUB	ESP,112
      15 00000006 6A 41                           	PUSH	65
      16 00000008 C6 45 9A 41                     	MOV	BYTE [-102+EBP],65
      17 0000000C E8 [00000000]                   	CALL	_api_putchar
      18 00000011 6A 42                           	PUSH	66
      19 00000013 C6 45 F6 42                     	MOV	BYTE [-10+EBP],66
      20 00000017 E8 [00000000]                   	CALL	_api_putchar
      21 0000001C 6A 43                           	PUSH	67
      22 0000001E C6 45 0B 43                     	MOV	BYTE [11+EBP],67
      23 00000022 E8 [00000000]                   	CALL	_api_putchar
      24 00000027 E8 [00000000]                   	CALL	_api_end
      25 0000002C C9                              	LEAVE
      26 0000002D C3                              	RET
      
      • 11 00000000 _HariMain:一行可以看出,在bug1.lst中,HariMain的地址暫時被當做是0(臨時地址),而實際的地址要等到連接之後才能決定,因此nask是不知道的,只好先用.obj文件中的臨時地址來生成.lst文件。連接之後的HariMain實際地址記載於.map中,是0x24。
    • 那麼,0x42地址到底位於.lst文件的哪裏呢?通過對比.map文件,我們發現.lst文件中的0x1e就相當於EIP=0x42所指向的地址(0x24+0x1e=0x42)。
    • 注意bug1.lst:
      22 0000001E C6 45 0B 43                     	MOV	BYTE [11+EBP],67
      
    • 67=0x43,即爲字符C。這裏正好是將a[123]賦值字符C的地方。看來CPU真的是對這個bug做出了響應。

3. 強制結束應用程序(harib19c)

  • 現在的OS已經能夠對付大部分惡意破壞和bug。但是使用快捷鍵結束應用程序是必要的。

  • 編寫應用程序bug2.c:

    void HariMain(void)
    {
        for (;;) { }
    }
    
    • 如果運行應用程序bug2,那麼將永遠循環下去而無法結束。中斷沒有禁用,因此其他任務還可以照常運行。
    • 但這個程序始終在運行,就會佔用CPU的運行時間,因此,系統整體就會變慢。
    • 因此,使用快捷鍵結束應用程序是必要的。
  • 由於命令行窗口任務在應用程序運行的時候不會去讀取FIFO緩衝區,因此快捷鍵不能卸載console.c中。所以,在bootpack.c中寫快捷鍵結束應用程序。

  • 修改bootpack.c中的HariMain:

    void HariMain(void)
    {
        ……
        struct CONSOLE *cons;
        ……
        for (;;) {
            ……
            if (fifo32_status(&fifo) == 0) {
                ……
            } 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();
                    }
                    ……
                } else if (512 <= i && i <= 767) { /* 鼠標數據 */
                    ……
                } else if (i <= 1) { /* 光標定時器 */
                    ……
                }
            }
        }
    }
    
    • asm_app_end是將naskfunc.nas中end_app改名之後的函數。
    • 上述代碼的功能:當按下Shift+F1時,改寫命令行窗口的寄存器值,並跳轉到asm_end_app中。tss.eax設置爲非零,才能返回非0值,asm_end_app才能結束應用程序。
    • 只有噹噹前有應用程序運行時按下Shift+F1纔會結束應用程序。當沒有應用程序運行時,按下Shift+F1可能會產生意想不到的後果。
    • 因此,只有當task_cons->tss.ss0不爲0時才能結束應用程序。
      • 當有應用程序運行時,該值一定不爲0。
      • 當沒有應用程序運行時,該值一定爲0。
  • 修改asm_end_app:

    _asm_end_app:
    ;	EAX爲tss.esp0的地址
            MOV		ESP,[EAX]
            MOV		DWORD [EAX+4],0     ; 添加了這句
            POPAD
            RET					; cmd_app傊婣傞
    
    • 代碼MOV DWORD [EAX+4],0的含義是:將tss.ss0賦值0(當前沒有應用程序運行)。
      • tss的結構:
        struct TSS32 {
            int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
            int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
            int es, cs, ss, ds, fs, gs;
            int ldtr, iomap;
        };
        
      • [EAX]是tss.esp0,那麼[EAX+4]就是ss0, 因爲是int,所以是DWORD。
  • 修改task_alloc(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; 
                ……
                task->tss.ldtr = 0;
                task->tss.iomap = 0x40000000;
                task->tss.ss0 = 0; /*ss0=0*/
                return task;
            }
        }
        return 0; 
    }
    
  • make後用VMware運行:

    • 執行應用程序bug2:
      • 使用Tab能夠切換到task_a,鍵盤輸入可以,鼠標能動。但是切換回console後,鍵盤輸入不了,光標不閃爍。(看來bug2挺厲害啊)
    • 使用快捷鍵Shift+F1結束bug2:
      • 光標閃爍了,且剛剛輸入的字符都顯示出來了。console又正常了。
  • 編寫bug3.c,該應用程序不斷地顯示字符a:

    void api_putchar(int c);
    void api_end(void);
    
    void HariMain(void)
    {
        for (;;) {
            api_putchar('a');
        }
    }
    
  • make後用VMware運行:

    • 由於處理器處理速度飛快,因此顯示a的速度很快。使用Shift+F1可以結束顯示。

4. 用C語言顯示字符串(1)(harib19d)

  • 查看harib19c的a_nask.nas:

    [FORMAT "WCOFF"]
    [INSTRSET "i486p"]
    [BITS 32]
    [FILE "a_nask.nas"]
    
            GLOBAL	_api_putchar
            GLOBAL	_api_end
    
    [SECTION .text]
    
    _api_putchar:	; void api_putchar(int c);
            MOV		EDX,1
            MOV		AL,[ESP+4]		; c
            INT		0x40
            RET
    
    _api_end:	; void api_end(void);
            MOV		EDX,4
            INT		0x40
    
    • 也就是說,現在可以用C語言調用的API函數有:api_putchar(顯示單個字符)和api_end(結束應用程序)。
  • 編寫api_putstr0,顯示字符串(a_nask.nas):

    _api_putstr0:	; void api_putstr0(char *s);
    	PUSH	EBX
    	MOV		EDX,2
    	MOV		EBX,[ESP+8]		; s
    	INT		0x40
    	POP		EBX
    	RET
    
    • 因爲要使用EBX寄存器,於是先將EBX壓入棧中。
  • 利用api_putstr0編寫應用程序hello4.c:

    void api_putstr0(char *s);
    void api_end(void);
    
    void HariMain(void)
    {
        api_putstr0("hello, world\n");
        api_end();
    }
    
  • make後用VMware運行試試:

    • 並沒有顯示字符串hello, world
  • 因爲已經不能使用RETF來結束應用程序了,因此在harib18b中對hrb文件前6字節數據的改寫也就沒有必要了。因此,去掉對那6個字節的改寫。修改cmd_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));
            }
            ……
        }
        /* 未找到文件的情況 */
        return 0;
    }
    
    • start_app(0x1b, 1003 * 8, 64 * 1024, 1004 * 8, &(task->tss.esp0))中的0x1b也就解釋通了:
      • 在harib18b中,因爲start_app(0, ……),是從地址0開始的,因此需要執行代碼(前6字節)跳轉到0x1b。
      • 現在,start_app(0x1b, ……)直接從0x1b開始即可。
      • start_app的參數含義:
        void start_app(int eip, int cs, int esp, int ds, int *tss_esp0);
        
    • Hari的應用程序是用C語言寫的,沒有的使用匯編寫的。因此要加以區別。
  • 修改後的hello3.hrb可以順利運行,hello4.hrb還是不能運行:

5. 用C語言顯示字符串(2)(haib19e)

  • 臨時修改hrb_api,當調用2號API時,顯示ebx的值:

    int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
    {
        int ds_base = *((int *) 0xfe8);
        char s[12];
        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 + ds_base);  
            sprintf(s, "%08X\n", ebx);  
            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並用VMware運行一下,結果顯示EBX=0x00000400。也就是說說字符串地址是從內存地址0x400(應用程序ORG 0)開始的。
  • EBX被寫入0x400,是因爲連接了.obj文件的bim2hrb認爲“hello, world”這個字符串就應該在0x400這個地址中。

  • 由bim2hrb生成的.hrb文件由兩部分組成:

    • 代碼部分
    • 數據部分
  • 雖然有兩部分,但是之前一直都是不考慮數據部分的。當程序中沒有使用字符串和外部變量(在函數外部定義的變量)時,就會生成不包含數據部分的.hrb文件,因此之前的程序都沒問題

  • .hrb文件的數據部分應該在應用程序啓動的時候被傳送到應用程序用的數據段中。而.hrb文件的數據部分則存放在代碼部分開頭的一部分中。

  • 詳細分析hrb文件結構:

    • 由bim2hrb生成的.hrb文件,開頭的36字節不是程序,而是如下信息:
    • 對應hello4.hrb:
    • 0x0000存放的是數據段的大小。在此前,我們都是在OS中規定了應用程序用的數據段大小爲64KB。現在,可以根據應用程序的需求來爲其分配大小合適的內存空間
    • 0x0004存放的是Hari這4個字節。這個標記加上文件後綴名.hrb可以判斷一個文件到底是不是此OS可運行的可執行文件。也就是說,當一個文件的後綴名是.hrb時,OS還需要判斷這文件是否有Hari標記,如果沒有該標記,則停止運行該文件。
    • 0x0008存放的是數據段內預備空間的大小。這個值暫時沒有什麼用。一般就是0。
    • 0x000c存放的是應用程序啓動時ESP寄存器的初始值。 也就是說,這個地址之前的部分會被當做棧來使用,而*這個地址將被用於存放字符串等數據
      • 在hello4.hrb中,這個值是0x400,也就是說ESP寄存器的初始值是0x400,並且分配了1KB的棧空間。1KB是在生成hello4.bim時,在Makefile中設置的:
        hello4.bim : hello4.obj a_nask.obj Makefile
            $(OBJ2BIM) @$(RULEFILE) out:hello4.bim stack:1k map:hello4.map \
                hello4.obj a_nask.obj
        
        • 注意代碼stack:1k
    • 0x0010存放的是需要向數據段傳送的數據部分的大小
    • 0x0014存放的是需要向數據段傳送的數據部分在.hrb中的起始地址
    • 0x0018存放的是0xe9000000這個數值。這個數值在內省中存放的形式是00 00 00 E9,前面的幾個0沒啥用,關鍵是E9。E9是JMP指令的機器語言編碼,和緊跟着後面的4個字節結合起來,就表示JMP到應用程序運行的入口地址。
    • 0x001c存放的是應用程序運行入口地址減去0x20之後的值。這樣做的原因是:在0x0018(其實是0x001b)中寫入了一個JMP指令,這樣可以通過JMP跳轉到應用程序的運行入口地址。通過這樣的處理,只要先JMP到0x1b這個地址,程序就可以開始運行了。存疑:這裏-0x20,莫非是把JMP 0x1b當做起始,然後程序運行入口地址-0x20?
    • 0x0020存放的是將來編寫應用程序用的malloc函數時要使用的地址。暫時先不用管它。
  • 根據上述分析,修改cmd_app:

    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)); /*數據部分在hrb文件中的開始地址*/
                q = (char *) memman_alloc_4k(memman, segsiz); /*爲應用程序數據段分配合適的內存空間*/
                *((int *) 0xfe8) = (int) q; /*將應用程序數據段地址寫入內存特定地址(1)*/
                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;
    }
    
    • 以前是將指針p寫入0xfe8,因爲以前沒有使用數據段。現在要使用數據段了,所以將q寫入0xfe8。
    • 修改點主要如下:
      • 文件中找不到Hari標誌則報錯
      • 數據段的大小根據.hrb文件中的指定值分配
      • 將.hrb的數據部分先複製到數據段後再啓動程序
    • 注意:這樣修改代碼以後,不是由bim2hrb生成的.hrb文件就會出錯
      • 比如,hello.nas和hello2.nas。
    • 在以後的內容中,即便用彙編語言編寫應用程序,也需要先生成.obj文件,然後生成.bim文件,再使用bim2hrb將.bim文件轉化成.hrb文件
    • 這樣,即便將文件拓展名誤寫成.hrb也不怕會運行該文件的風險了。
  • 修改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);  
        char s[12];
        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);
        }
    }
    
    • 將cs_base改成了ds_base。因爲不從代碼段取ebx,而是從數據段取ebx。
  • make後用VMware運行:

    • hello4順利運行。
    • hello2.nas不能被識別成應用程序:
      [INSTRSET "i486p"]
      [BITS 32]
              MOV		EDX,2
              MOV		EBX,msg
              INT		0x40
              MOV		EDX,4
              INT		0x40
      msg:
              DB	"hello",0
      
  • 如果hello2還想運行,需要帶成這樣,編寫hello5.nas:

    [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
    
    • SECTION命令:將程序的這個部分放在代碼段還是數據段:text(文本區)代表代碼段。
    • 這是用彙編語言編寫應用程序的框架,不可改變。是根據編譯器來搞的,就跟寫C語言代碼的框架一樣。
  • Makefile中有關hello5.nas的編譯代碼:

    hello5.bim : hello5.obj Makefile
    $(OBJ2BIM) @$(RULEFILE) out:hello5.bim stack:1k map:hello5.map hello5.obj
    
    hello5.hrb : hello5.bim Makefile
        $(BIM2HRB) hello5.bim hello5.hrb 0
    

    而不是直接由nas生成hrb。hello.nas:
    hello.hrb : hello.nas Makefile $(NASK) hello.nas hello.hrb hello.lst

  • make後用VMware運行成功輸出。

  • .hrb的題外話

6. 顯示窗口(harib19f)

  • 編寫一個用來顯示窗口的API。API設計:

    • EDX = 5
    • EBX = 窗口緩衝區
    • ESI = 窗口在X軸方向上的大小(即窗口寬度)
    • EDI = 窗口在Y軸方向上的大小(即窗口高度)
    • EAX = 透明色
    • ECX = 窗口名稱
    • 調用後返回值:
      • EAX = 用於操作窗口的句柄。
  • 修改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;	/* 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;
    }
    
    • 在asm_hrb_api中執行了兩次PUSHAD,第一次是爲了保存寄存器的值,第二次是爲了向hrb_api傳遞值。
    • 圖解:
    • 注意:0xfe4沒有使用,在harib18d中,0xfe4存放着系統棧的ESP,但是不需要我們自己寫棧切換的代碼了,所以0xfe4就空出來了。
    • 因此,現在:
      • 0xfe4存放shtctl指針
      • 0xfe8存放應用程序數據段指針p
      • 0xfec存放cons
  • 修改bootpack.c:

    void HariMain(void){
        ……
        *((int *) 0xfe4) = (int)shtctl;
        ……
    }
    
  • 編寫可供C語言調用的API,api_openwin(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
    
    • 32位彙編語言編程規範中:
      • 在調用子程序之前,調用者應該保存一系列被設計爲調用者保存的寄存器的值。調用者保存寄存器有eax,ecx,edx。由於被調用的子程序會修改這些寄存器,所以爲了在調用子程序完成之後能正確執行,調用者必須在調用子程序之前將這些寄存器的值入棧。
      • 也就是說EAX、ECX和EDX,調用者負責保存,因此被調用者不需要保存,直接使用即可。
  • 編寫應用程序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後用VMware運行:

7. 在窗口中描繪字符和方塊(harib19g)

  • 編寫在窗口上顯示字符的API,API設計如下:

    • EDX = 6
    • EBX = 窗口句柄
    • ESI = 顯示位置的X座標
    • EDI = 顯示位置的Y座標
    • EAX = 色號
    • ECX = 字符串長度
    • EBP = 字符串
  • 編寫繪製方塊的API,API設置如下:

    • EDX = 7
    • EBX = 窗口句柄
    • EAX = x0
    • ECX = y0
    • ESI = x1
    • EDI = y1
    • EBP = 色號
  • 真懸,如果再多一個參數寄存器就不夠用了。

  • 修改hrb_api:

    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;
    }
    
  • 添加函數api_putstrwin(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(a_nask.nas)

    _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();
    }
    
  • make後用VMware運行:

    • 字符是黑色的,矩形區域是黃色的。
  • bug1.hrb沒啥用了,刪掉吧。

8. 寫在今天

  • 前幾天的壞毛病又犯了。
  • 現在是2020.4.28 15:35。
  • 轉眼從2020.3.28重新開始,我已經寫了20篇markdown文檔,平均1.5天一篇。
  • 既定目標是2020.5.7之前寫完30篇markdown文檔。
  • 希望既定目標的flag不要倒。Fighting!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章