动手写操作系统5----C语言实现绘制系统界面

本节实现C语言与汇编互相调用,利用C语言实现系统界面绘制功能。

之前显示字符串等功能都是使用显卡的字符界面模式,接下来需要打开显卡的图形模式,打开显卡图形模式需要使用BIOS INT 0x10中断

    ;设置屏幕色彩模式
    mov al, 0x13
    mov ah, 0
    INT 0x10

其中al 的值决定了要设置显卡的色彩模式,下面是一些常用的模式设置:

  1. 0x03, 16色字符模式
  2. 0x12, VGA图形模式, 640 * 480 * 4位彩色模式,独特的4面存储模式
  3. 0x13, VGA图形模式, 320 * 200 * 8位彩色模式,调色板模式
  4. 0x6a, 扩展VGA图形模式, 800 * 600 * 4彩色模式

这里采用0x13模式,其中320*200*8 中,最后的数值8表示的是色彩值的位数,也就是我们可以用8位数值表示色彩,总共可以显示256种色彩。系统显存的地址是0x000a0000,往显存地址写入数据,那么屏幕就会出现相应的变化。

具体实现过程如下:

1. 准备C语言绘制系统界面函数

write_vram.c

//extern void io_hlt();

void showChar() {
    int i;
    char *p = (char *)0xa0000;
    for(i=0; i<=0xFFFF; i++){
        *p = i & 0x0F;
        p++;
    }
    for(;;){
        io_hlt();
    }
}

将指针P指向vga显存地址0xa0000, vga显存地址从0xa0000开始,直到0xaffff结束,总共64k.
*p = i & 0x0f 将一个数值写入显存,这个值可以是0-256中任意一个数值,这里将i的最后4位作为像素颜色写入显存

2.将C语言函数编译为二进制文件

gcc -m32 -c -fno-asynchronous-unwind-tables -fno-pic write_vram.c -o write_vram.o

这里我使用的gcc版本为gcc (Ubuntu 8.3.0-6ubuntu1) 8.3.0,不同的gcc编译遇到的问题可能不一样

这里在编译时注意增加编译条件,由于编译之后的汇编文件需要反汇编,关联到kernel.asm中,因此这里要添加编译条件

-m32:编译出来的是32位程序,既可以在32位操作系统运行,又可以在64位操作系统运行。

对于C语言编译不太熟悉,感觉版本依赖太强,后面慢慢研究吧。

3.安装objconv 反汇编二进制文件得到汇编文件

反汇编工具objconv安装 https://github.com/vertis/objconv.git
下载后进入objconv目录,编译该工具,运行下面的命令:
 

g++ -o objconv -O2 src/*cpp , -O2中的圆圈是大写字母O

或者

g++ -o objconv -O2 src/*cpp --std=c++98

这里由于C++语言版本的问题,可能在编译objconv会出现类型转换报错

src/coff.cpp:142:1: error: narrowing conversion of ‘2147483648’ from ‘unsigned int’ to ‘int’ inside { } [-Wnarrowing],

这里添加C++版本98即可解决。

编译objconv完成之后,反汇编上一步二进制文件write_vram.o得到汇编文件write_vram.asm

./objconv -fnasm write_vram.o -o write_vram.asm

4.编辑汇编文件write_vram.asm

主要是去掉一些全局声明和段描述信息

; Disassembly of file: write_vram.o
; Sun Feb  9 10:04:31 2020
; Mode: 32 bits
; Syntax: YASM/NASM
; Instruction set: 80386


;global showChar: function

;extern io_hlt                                           ; near


;SECTION .text   align=1 execute                         ; section number 1, code

showChar:; Function begin
        push    ebp                                     ; 0000 _ 55
        mov     ebp, esp                                ; 0001 _ 89. E5
        sub     esp, 24                                 ; 0003 _ 83. EC, 18
        mov     dword [ebp-0CH], 655360                 ; 0006 _ C7. 45, F4, 000A0000
        mov     dword [ebp-10H], 0                      ; 000D _ C7. 45, F0, 00000000
        jmp     ?_002                                   ; 0014 _ EB, 15

?_001:  mov     eax, dword [ebp-10H]                    ; 0016 _ 8B. 45, F0
        and     eax, 0FH                                ; 0019 _ 83. E0, 0F
        mov     edx, eax                                ; 001C _ 89. C2
        mov     eax, dword [ebp-0CH]                    ; 001E _ 8B. 45, F4
        mov     byte [eax], dl                          ; 0021 _ 88. 10
        add     dword [ebp-0CH], 1                      ; 0023 _ 83. 45, F4, 01
        add     dword [ebp-10H], 1                      ; 0027 _ 83. 45, F0, 01
?_002:  cmp     dword [ebp-10H], 65535                  ; 002B _ 81. 7D, F0, 0000FFFF
        jle     ?_001                                   ; 0032 _ 7E, E2
?_003:  call    io_hlt                                  ; 0034 _ E8, FFFFFFFC(rel)
        jmp     ?_003                                   ; 0039 _ EB, F9
; showChar End of function


;SECTION .data   align=1 noexecute                       ; section number 2, data


;SECTION .bss    align=1 noexecute                       ; section number 3, bss


5.编辑kernel.asm

kernel.asm汇编代码如下:

; 全局描述符结构 8字节
; byte7 byte6 byte5 byte4 byte3 byte2 byte1 byte0
; byte6低四位和 byte1 byte0 表示段偏移上限
; byte7 byte4 byte3 byte2 表示段基址

;定义全局描述符数据结构
;3 表示有3个参数分别用 %1、%2、%3引用参数
;%1:段基址     %2:段偏移上限  %3:段属性

%macro GDescriptor 3
    dw %2 & 0xffff                         ;设置段偏移上限0,1字节
    dw %1 & 0xffff                         ;设置段基址2,3字节
    db (%1>>16) & 0xff                     ;设置段基址4字节
    dw ((%2>>8) & 0x0f00) | (%3 & 0xf0ff)  ;设置段偏移上限6字节低四位
    db (%1>>24) & 0xff                     ;设置段基址7字节
%endmacro


DA_32     EQU  0x4000    ;32位段属性
DA_CODE   EQU  0x98      ;执行代码段属性
DA_RW     EQU  0x92      ;读写代码段属性值

org 0x9000       ;内核代码在内存中起始加载处

jmp entry

[SECTION .gdt]
;全局描述符                             段基址    段偏移上线     段属性
LABLE_GDT:              GDescriptor     0,        0,             0
LABLE_DESC_CODE:        GDescriptor     0,        SegCodeLen-1,  DA_CODE + DA_32
LABLE_DESC_VIDEO:       GDescriptor     0xb8000,  0xffff,        DA_RW                ;显存内存地址从0xB8000开始
LABLE_DESC_STACK:       GDescriptor     0,        STACK_TOP-1,   DA_32 + DA_RW        ;函数堆栈
LABLE_DESC_VRAM:        GDescriptor     0,        0xffffffff,    DA_RW                ;显存描述符

;GDT表大小
GdtLen    EQU    $ - LABLE_GDT

;GDT表偏移上限和基地址
GdtPtr dw  GdtLen-1
       dd  0

;cpu寻址方式
;实模式  段寄存器×16+偏移地址
;保护模式下  段寄存器中存储的是GDT描述表各个描述符的偏移


SelectorCode32     EQU      LABLE_DESC_CODE - LABLE_GDT
SelectorVideo      EQU      LABLE_DESC_VIDEO - LABLE_GDT
SelectorStack      EQU      LABLE_DESC_STACK - LABLE_GDT
SelectorVRAM       EQU      LABLE_DESC_VRAM - LABLE_GDT

[SECTION .s16]
[BITS 16]

entry:
    mov ax, cs
    mov ss, ax
    mov ds, ax
    mov es, ax
    mov sp, 0x100

    ;设置屏幕色彩模式
    mov al, 0x13
    mov ah, 0
    INT 0x10

    ;设置LABLE_DESC_CODE描述符段基址
    mov eax, 0
    mov ax, cs
    shl eax, 4
    add eax, SEG_CODE32
    mov word [LABLE_DESC_CODE+2], ax
    shr eax, 16
    mov [LABLE_DESC_CODE+4], al
    mov [LABLE_DESC_CODE+7], ah

    ;设置栈空间
    xor eax, eax
    mov ax, cs
    shl eax, 4
    add eax, LABLE_STACK
    mov word [LABLE_DESC_STACK+2], ax
    shr eax, 16
    mov byte [LABLE_DESC_STACK+4], al
    mov byte [LABLE_DESC_STACK+7], ah

    mov eax, 0
    mov ax, ds
	shl eax, 4
    add eax, LABLE_GDT
    mov dword [GdtPtr+2], eax
    
    ;设置GDTR寄存器
    lgdt [GdtPtr]

    cli ;关中断

    ;打开A20
    in al, 0x92
    or al, 0x02
    out 0x92, al
    
    ;进入保护模式CR0寄存器最低位PE设为1
    mov eax, cr0
    or eax, 1
    mov cr0, eax

    jmp dword SelectorCode32:0

    [SECTION .s32]
    [BITS 32]

SEG_CODE32:
    mov ax, SelectorStack
    mov ss, ax
    mov esp, STACK_TOP

    mov ax, SelectorVRAM
    mov ds, ax

    call showChar  ;调用c语言函数

GLOBAL io_hlt      ;声明函数供c语言调用 void io_hlt();
io_hlt:
    HLT
    RET

;注意汇编文件引入位置 在代码段结束符之上
%include "write_vram.asm"

;32位模式代码长度
SegCodeLen EQU $ - SEG_CODE32

[SECTION .gs]
    ALIGN 32
    [BITS 32]

LABLE_STACK:
    TIMES 512 DB 0

STACK_TOP    EQU   $ - LABLE_STACK

这里主要注意引入write_vram.asm的汇编文件的位置在代码段结束符之上。

添加了对于显存段和栈的描述符定义以及初始化代码,系统启动加载内核之后,内核实现段描述符的初始化,然后进入到保护模式,跳转到代码段处开始执行,在代码段处首先将栈段描述符放入ss寄存器,esp寄存器指向栈顶,将显存段描述符放入ds,然后调用showChar函数。

6.编译boot.asm和kernel.asm

编译kernel.asm   编译之后的kernel字节大于1个扇区,也就是说内核代码应该放在软盘的多个扇区位置,之前默认放在1柱面2扇区

nasm -f bin kernel.asm -o kernel

修改makeFloppy.c  将kernel从1柱面2扇区开始往后写入  默认最大写入16个扇区  当超过16个扇区时,这种方式写入会有问题,以后会修改。

#include <stdio.h>
#include <stdlib.h>
#include "floppy.h"
#include <string.h>

int main(int argc, char *argv[]){

    printf("boot %s \n", argv[1]);
    FILE* boot = fopen(argv[1], "r");

    printf("kernel %s \n", argv[2]);
    FILE* kernel = fopen(argv[2], "r");

    printf("img %s \n", argv[3]);
    FILE* img = initFloppy(argv[3]);

    if(boot == NULL || kernel == NULL){
	printf("The boot or kernel file not found");
	exit(0);
    }
    
    //写引导扇区cylinder0 sector1
    char buf[512];
    memset(buf, 0, 512);
    fread(buf, 512, 1, boot);
    writeFloppy(0, 0, 1, img, buf);

    //写内核 cylinder1 sector2
    for(int i=0; !feof(kernel); i++){
        memset(buf, 0, 512);
        fread(buf, 512, 1, kernel);
        writeFloppy(1, 0, 2+i, img, buf);
        printf("The %d read kernel write img sector %d \n", i+1, 2+i);
    }
    fclose(boot);
    fclose(kernel);
}

修改boot.asm  系统启动后将软盘1柱面2扇区之后的16个扇区全部读入到内核代码起始处

org  0x7c00

LOAD_ADDR EQU 0x9000 ;内核代码起始处

jmp  entry
db   0x90
DB   "OSKERNEL"
DW   512
DB   1
DW   1
DB   2
DW   224
DW   2880
DB   0xf0
DW   9
DW   18
DW   2
DD   0
DD   2880
DB   0,0,0x29
DD   0xFFFFFFF
DB   "MYFIRSTOS  "
DB   "FAT12   "
RESB  18

entry:
    mov  ax, 0
    mov  ss, ax
    mov  ds, ax
    mov  es, ax

readFloppy:
    mov ch, 1 ;磁道号cylinder

    mov dh, 0 ;磁头号head

    mov cl, 2 ;扇区号sector
    
    mov bx, LOAD_ADDR ;数据存储缓冲区即将内核从该位置开始加载到内存中

    mov ah, 0x02 ;读扇区

    mov al, 16 ;连续读取扇区数量 先读取16个扇区数

    mov dl, 0 ;驱动器编号

    INT 0x13 ;调用BIOS中断

    jc fin

    jmp LOAD_ADDR

fin:
    HLT
    jmp fin

TIMES 0x1FE-($-$$) DB 0x00

DB 0x55, 0xAA

执行脚本run.sh

#!/bin/bash
nasm boot.asm
echo "nasm boot.asm"
nasm kernel.asm
echo "nasm kernel.asm"
gcc floppy.c makeFloppy.c -o makeFloppy
echo "gcc floppy.c makeFloppy.c -o makeFloppy"
./makeFloppy boot kernel system.img
echo "./makeFloppy boot kernel system.img"

利用virtualBox 加载system.img最终结果如下:

总结分析

        这里通过编译C语言,然后反汇编,再整合所有汇编文件,编译得到最终结果。其实也可以以C语言为核心,通过内联汇编来开发操作系统。核心点:

1.C语言与汇编语言是如何交互的

2.显存图形功能的使用

3.C语言的执行过程  参数传递   函数栈     

https://blog.csdn.net/u014106644/article/details/97260697

        在kernel.asm中添加了对于显存和函数栈描述符的定义  这里定义显存段描述符为0到4GB,定义函数栈为512B,即C语言函数执行过程中使用栈空间不能超过512B。具体细节以后慢慢研究。

代码位置  https://github.com/ChenWenKaiVN/bb_os_core/tree/develop

参考资料:

https://stackoverflow.com/questions/17676026/converting-c-to-nasm-assembly

https://blog.csdn.net/qq_31567335/article/details/100531788

https://stackoverflow.com/questions/22634337/error-compiling-asm-binary-format-does-not-support-any-special-symbol-types

https://blog.csdn.net/cloudblaze/article/details/78526456

https://gcc.gnu.org/onlinedocs/gcc-8.3.0/gcc.pdf

https://www.jianshu.com/p/b27105b9b07d


版权声明:本文为u014106644原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。