本节实现C语言与汇编互相调用,利用C语言实现系统界面绘制功能。
之前显示字符串等功能都是使用显卡的字符界面模式,接下来需要打开显卡的图形模式,打开显卡图形模式需要使用BIOS INT 0x10中断
;设置屏幕色彩模式
mov al, 0x13
mov ah, 0
INT 0x10
其中al 的值决定了要设置显卡的色彩模式,下面是一些常用的模式设置:
这里采用0x13模式,其中320*200*8 中,最后的数值8表示的是色彩值的位数,也就是我们可以用8位数值表示色彩,总共可以显示256种色彩。系统显存的地址是0x000a0000,往显存地址写入数据,那么屏幕就会出现相应的变化。
具体实现过程如下:
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位作为像素颜色写入显存
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语言编译不太熟悉,感觉版本依赖太强,后面慢慢研究吧。
反汇编工具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
主要是去掉一些全局声明和段描述信息
; 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
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函数。
编译kernel.asm 编译之后的kernel字节大于1个扇区,也就是说内核代码应该放在软盘的多个扇区位置,之前默认放在1柱面2扇区
nasm -f bin kernel.asm -o kernel
修改makeFloppy.c 将kernel从1柱面2扇区开始往后写入 默认最大写入16个扇区 当超过16个扇区时,这种方式写入会有问题,以后会修改。
#include
#include
#include "floppy.h"
#include
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