前言 本文是《30天自制操作系统》中的windows翻译为linux上的一些实践,过程中也在逐步迁移linux的源码到里面。本身作为对linux上的汇编、启动、内存管理等的一个学习实践。在30天之前都是将此工程翻译到linux,30天后将变成linux移植过程的笔记
关键词解释 启动区: (bootsector) 软盘第一个的扇区称为启动区。那么什么是扇区呢?计算机读写软盘的时候,并不是一个字节一个字节地读写的,而是以512字节为一个单位进行读写。因此,软盘的512字节就称为一个扇区。一张软盘的空间共有1440KB, 也就是1474560字节,除以512得2880, 这也就是说一张软盘共有2880个扇区。那为什么第一个扇区称为启动区呢?那是因为计算机首先从最初一个扇区开始读软盘,然后去检查这个扇区最后2个字节的内容。 IPL: (initial program loader)启动程序加载器。启动区只有区区512字节,实际的操作系统不像hello-os这么小,根本装不进去。所以几乎所有的操作系统,都是把加载操作系统本身的程序放在启动区里的。有鉴于此,有时也将启动区称为IPL。但hello-os没有加载程序的功能,所以HELLOIPL这个名字不太顺理成章。如果有人正义感特别强,觉得“这是撒谎造假,万万不能容忍!",那也可以改成其他的名字。但是必须起一个8字节的名字,如果名字长度不到8字节的话,常要在最后补上空格。 第1天 从计算机结构到汇编程序入门 环境windows 二进制编辑器 notepad++安装hexeditor插件 汇编编辑器 vscode安装x86 and x86_64 Assembly 编译需要使用光盘中的nask编译器 1. vmware启动img 创建系统选择other/other 创建好需要添加硬件,选择软盘,然后使用文件,选择img即可启动 2. qemu启动img 1 qemu-system-x86_64 -enable-kvm -m 4G -smp 1 -fda common/haribote.img
第2天 汇编语言学习与Makefile入门 1. 标准FAT12软盘格式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ; 以下的记述用于标准FAT12格式的软盘 JMP entry DB 0x90 DB "HELLOIPL" ; 启动区的名称可以是任意的字符串 DW 512 ; 每个扇区(sector)的大小(必须为512字节) DB 1 ; 簇(cluster)的大小(必须为1个扇区) DW 1 ; FAT的起始位置(一般从第一个扇区开始) DB 2 ; FAT的个数(必须为2) DW 224 ; 根目录的大小(一般设成224项) DW 2880 ; 该磁盘的大小(必须是2880扇区) DB 0xf0 ; 磁盘的种类 DW 9 ; FAT的长度(必须是9扇区) DW 18 ; 1个磁道(track)有几个扇区(必须是18) DW 2 ; 磁头数(必须是2) DD 0 ; 不使用分区,必须是0 DD 2880 ; 重写一次磁盘大小 DB 0, 0, 0x29 ; 意义不明,固定 DD 0xffffffff ; (可能是)卷标号码 DB "HELLO-OS " ; 磁盘的名称(11字节) DB "FAT12 " ; 磁盘格式名称(8字节) RESB 18 ; 先空出18字节
2. 汇编代表性寄存器介绍 16位寄存器 1 2 3 4 5 6 7 8 AX ---- accumulator 累加寄存器 CX ---- counter 计数寄存器 DX ---- data 数据寄存器 BX ---- base 基址寄存器 SP ---- stack pointer 栈指针寄存器 BP ---- base pointer 基址指针寄存器 SI ---- source index 源变址寄存器 DI ---- destination index 目的变址寄存器
8位寄存器 8位寄存器为16位寄存器的扩展,AL和AH一起代表AX,并不是单独的寄存器
1 2 3 4 5 6 7 8 AL ---- accumulator 累加寄存器低位 CL ---- counter 计数寄存器低位 DL ---- data 数据寄存器低位 BL ---- base 基址寄存器低位 AH ---- accumulator 累加寄存器高位 CH ---- counter 计数寄存器高位 DH ---- data 数据寄存器高位 BH ---- base 基址寄存器高位
32位寄存器 32位系统中使用的32位寄存器,低16位和上述16位相同,高16位没有寄存器名字 32位寄存器加E代表,如EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI 段寄存器,16位 1 2 3 4 5 6 ES ---- extra segment 附加段寄存器 CS ---- code segment 代码段寄存器 SS ---- stack segment 栈段寄存器 DS ---- data segment 数据段寄存器 FS ---- segment part 2 没有名称 GS ---- segment part 3 没有名称
3. CPU和内存 CPU寄存器很少,32位也只有44个字节的空间,所以需要内存当外部储存器 内存和CPU使用管脚连接,速度虽然光速,但是比起来内部寄存器还是慢很多 程序代码储存在内存中,一条一条读取出来进行运行 0xf0000附近存在bios本身启动区内容的装载地址为0x00007c00 -- 0x00007dff,为IBM和intel规定的。所以ORG指令选择此处为起始地址,也仅有512字节 4. Makefile编写 ipl.S是启动时最先执行的代码,需要编译到软盘镜像的第一个分区1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 all: img boot: nasm -w-zeroing -o bootloader ipl.S img: boot dd if=bootloader of=haribote.img count=1 bs=512 dd if=/dev/zero of=haribote.img bs=512 seek=1 skip=1 count=2879 run : qemu-system-x86_64 -enable-kvm -m 4G -smp 1 -fda haribote.img clean : rm -f bootloader haribote.img
第3天 进入32位模式并导入C语言 1. 软盘构成 一面80个柱面 磁盘有两面 每个柱面18个扇区 一个扇区512字节 一共80 * 2 * 18 * 512 = 1474560 Byte = 1440 KB C0-H0-S1代表柱面0,磁头0,扇区1扇区从1开始计数,柱面从0开始计数 (1) 软盘保存文件 参考 制作FAT12软盘以查看软盘的根目录条目+文件属性+文件内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 img: bootloader kernel dd if=bootloader.bin of=haribote.img count=1 bs=512 &>/dev/null dd if=/dev/zero of=haribote.img bs=512 seek=1 skip=1 count=2879 &>/dev/null mkdir -p ./floppy mount -o loop haribote.img ./floppy -o fat=12 sleep 1 cp kernel ./floppy sleep 1 umount ./floppy rm -rf ./floppy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 => hexdump -C haribote.img 00000000 eb 4e 90 48 45 4c 4c 4f 49 50 4c 00 02 01 01 00 |.N.HELLOIPL.....| 00000010 02 e0 00 40 0b f0 09 00 12 00 02 00 00 00 00 00 |...@............| 00000020 40 0b 00 00 00 00 29 ff ff ff ff 48 41 52 49 42 |@.....)....HARIB| 00000030 4f 54 45 4f 53 20 46 41 54 31 32 20 20 20 00 00 |OTEOS FAT12 ..| 00000040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000050 b8 00 00 8e d0 bc 00 7c 8e d8 b8 20 08 8e c0 b5 |.......|... ....| 00000060 00 b6 00 b1 02 be 00 00 b4 02 b0 01 bb 00 00 b2 |................| 00000070 00 cd 13 73 10 83 c6 01 83 fe 05 73 32 b4 00 b2 |...s.......s2...| 00000080 00 cd 13 eb e3 8c c0 83 c0 20 8e c0 80 c1 01 80 |......... ......| 00000090 f9 12 76 d1 b1 01 80 c6 01 80 fe 02 72 c7 b6 00 |..v.........r...| 000000a0 80 c5 01 80 fd 0a 72 bd 88 2e f0 0f e9 51 45 be |......r......QE.| 000000b0 c7 7c 8a 04 83 c6 01 3c 00 74 09 b4 0e bb 0f 00 |.|.....<.t......| 000000c0 cd 10 eb ee f4 eb fd 0a 0a 4c 6f 61 64 20 65 72 |.........Load er| 000000d0 72 6f 72 0a 00 00 00 00 00 00 00 00 00 00 00 00 |ror.............| 000000e0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 000001f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.| 00000200 00 00 00 00 f0 ff 00 00 00 00 00 00 00 00 00 00 |................| 00000210 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00001400 00 00 00 00 f0 ff 00 00 00 00 00 00 00 00 00 00 |................| 00001410 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00002600 41 6b 00 65 00 72 00 6e 00 65 00 0f 00 0f 6c 00 |Ak.e.r.n.e....l.| 00002610 00 00 ff ff ff ff ff ff ff ff 00 00 ff ff ff ff |................| 00002620 4b 45 52 4e 45 4c 20 20 20 20 20 20 00 16 05 62 |KERNEL ...b| 00002630 23 59 23 59 00 00 05 62 23 59 03 00 39 01 00 00 |#Y#Y...b#Y..9...| 00002640 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00004400 b0 13 b4 00 cd 10 c6 06 f2 0f 08 c7 06 f4 0f 40 |...............@| 00004410 01 c7 06 f6 0f c8 00 66 c7 06 f8 0f 00 00 0a 00 |.......f........| 00004420 b4 02 cd 16 a2 f1 0f be 5a c4 e8 50 00 b0 ff e6 |........Z..P....| 00004430 21 90 e6 21 fa e8 d0 00 b0 d1 e6 64 e8 c9 00 b0 |!..!.......d....| 00004440 df e6 60 e8 c2 00 0f 01 16 30 c5 0f 20 c0 66 83 |..`......0.. .f.| 00004450 c8 01 0f 22 c0 ea 90 c4 10 00 0a 0a 6b 65 72 6e |..."........kern| 00004460 65 6c 20 69 73 20 6c 6f 61 64 69 6e 67 00 0a 0a |el is loading...| 00004470 74 72 79 20 69 74 20 61 67 61 69 6e 00 8a 04 83 |try it again....| 00004480 c6 01 3c 00 74 09 b4 0e bb 0f 00 cd 10 eb ee c3 |..<.t...........| 00004490 66 b8 08 00 8e d8 8e c0 8e e0 8e e8 8e d0 bc ff |f...............| 000044a0 ff 3f 00 be 36 c5 00 00 bf 00 00 28 00 b9 00 00 |.?..6......(....| 000044b0 02 00 e8 41 00 00 00 be 00 7c 00 00 bf 00 00 10 |...A.....|......| 000044c0 00 b9 80 00 00 00 e8 2d 00 00 00 be 00 82 00 00 |.......-........| 000044d0 bf 00 02 10 00 8a 0d f0 0f 00 00 69 c9 00 12 00 |...........i....| 000044e0 00 81 e9 80 00 00 00 e8 0c 00 00 00 ea 00 00 00 |................| 000044f0 00 18 00 eb fe f4 eb fd 8b 06 83 c6 04 89 07 83 |................| 00004500 c7 04 83 e9 01 75 f1 c3 e4 64 a8 02 75 fa c3 90 |.....u...d..u...| 00004510 00 00 00 00 00 00 00 00 ff ff 00 00 00 92 cf 00 |................| 00004520 ff ff 00 00 00 9a 47 00 ff ff 00 00 28 9a 47 00 |......G.....(.G.| 00004530 1f 00 10 c5 00 00 f4 eb fd 00 00 00 00 00 00 00 |................| 00004540 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| * 00168000
文件名写在0x0002600的地方 文件内容写在0x004400的地方,这个和书本上的0x004200不太一样,linux上mount后写入文件就在0x004400位置 编译生成的第三阶段启动程序代码在0x004400位置 (2) 当前编译出来的软盘分布 C0-H0-S1: IPL启动区位于此扇区,操作系统启动时会自动装载到0x7c00-0x7dff,这个是iIBM和intel规定的默认行为booloader.S + header.S编译出来的kernel保存在软盘的0x0044002. 内存寻址 ES : BX代表内存寻址的地址,其中BX为0-3位,ES为4-位。如ES=0x0820,BX=0,代表0x8200地址。总内存为12位,1M左右。内存0x7c00-0x7dff为启动区使用,0x7e00-0x9fbff没有什么用,留给操作系统开发使用 内存寻址需要指定段寄存器DS,不然就会加上其16倍的数据,所以一般DS = 0 3. 汇编和C语言链接 使用汇编可以编译出.obj(.o)文件,这个文件和C文件编译出来的是一个效果 可以使用objdump来查看c语言生成的汇编指令代码 既然都是原生汇编,按照c语言生成的汇编格式来写汇编,同样可以链接到c语言中 1 2 3 4 5 6 7 8 9 10 11 12 .code32 # 定义全局符号,这个类似于C语言的export .global io_hlt # 指定.text段 .section .text # 函数实现 io_hlt: hlt ret
4. BIOS介绍 BIOS是使用16位机器语言,32位模式不能调用BIOS函数 VRAM(video RAM)在当前画面模式下是0xa0000 ~ 0xaffff,这个是在BIOS文档中INT 0x10说明最后写着 1) BIOS函数 (1) 0x10 设置画面模式 设置AH寄存器为00 设置AL寄存器为下面的值(省略部分模式)0x03: 16色字符模式,80 x 25 0x12: VGA图形模式,640 x 480 x 4位彩色模式,独特的4面储存模式 0x13: VGA图形模式,320 x 200 x 8位彩色模式,调色板模式 0x12: VGA图形模式,800 x 600 x 4位彩色模式,独特的4面储存模式(部分显卡不支持此模式) 无返回值 1 2 3 4 ; 设置屏幕模式 MOV AL,0x13 ; VGA图形,320 x 200 x 8位颜色 MOV AH,0x00 INT 0x10
5. cpu介绍 为什么写程序使用i486p,这个是cpu指令集 i486p是给486cpu使用,但是如果只是用16位寄存器,也可以8086用 intel系列cpu家谱 1 8086->80186->286->386->486->Pentium->PentiumPro->PentiumII->PentiumIII->Pentium4->...
6. 内存重新分配 引入C语言后,代码编译出的大小越来越大,之前的空间不足。32位模式下可用内存达到4G,重新将磁盘上的代码拷贝到内存新的地址中。 选择0x00100000为软盘拷贝的首地址 选择0x00280000为c语言代码开始位置 选择0x002fffff-0x003fffff为c语言栈的大小,栈设定为1MB。esp从c语言开始赋值为0x003fffff(栈顶是高地址,栈底为低地址) 参考linux内核代码,将cpu管理的内存也就是DS段放到13号段,c代码所在也就是CS段放到12号段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 # header.S文件节选 #define GDT_ENTRY_KERNEL_CS 12 #define GDT_ENTRY_KERNEL_DS 13 # 栈起始位置,共1MB。0x002fffff-0x003fffff #define STACK 0x003fffff # C语言代码所在内存位置 #define C_CODE 0x00280000 # 软盘数据所在内存位置 #define DISK_ADDR 0x00100000 # 软盘数据所在内存位置(真实模式) #define DISK_ADDR_REAL 0x00008000 ... # Set up the protected-mode data segment registers movw $(GDT_ENTRY_KERNEL_DS*8), %ax # Our data segment selector movw %ax, %ds # -> DS: Data Segment movw %ax, %es # -> ES: Extra Segment movw %ax, %fs # -> FS movw %ax, %gs # -> GS movw %ax, %ss # -> SS: Stack Segment # 设置栈指针到栈顶,调用c函数 movl $STACK,%esp # 拷贝c代码到内存中 movl $start_kernel,%esi # 源地址 movl $C_CODE,%edi # 目的地址 movl $(512*1024/4),%ecx # 拷贝512K,注意后面的C语言编译超过512K就要修改这里 call asmmemcpy # 拷贝软盘数据到内存对应位置 # 拷贝启动扇区 movl $0x00007c00,%esi # 源地址 movl $DISK_ADDR,%edi # 目的地址 movl $(512/4),%ecx # 拷贝512B call asmmemcpy # 拷贝剩余数据 movl $(DISK_ADDR_REAL+512),%esi # 源地址 movl $(DISK_ADDR+512),%edi # 目的地址 movb (CYLS),%cl # 把cyls地址内的值赋值给cl # 将ecx乘上512*8*2/4,cyls是bootloader读取的软盘中柱面数,一个柱面18个磁道,一个磁道2个扇区,一个扇区512B imul $(512*18*2/4),%ecx subl $(512/4),%ecx # 去除启动扇区 call asmmemcpy # 调用c语言第一个指令,位置是CS段的首地址 # 已经使用段管理内存了,跳转32位需要使用ljmp进行段+偏移的方式跳转 ljmp $(GDT_ENTRY_KERNEL_CS*8),$0x0000 ... start_kernel:
由于makefile中是直接将cobjs加到hader后面的,所以在header.S后面的start_kernel就是c语言的起始位置 7. makefile解释 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 TOOLPATH = ../../z_tools KERNEL_FILES = ../../kernel_files INCLUDE = -I. INCLUDE += -I$(KERNEL_FILES) /arch/x86 -I$(KERNEL_FILES) /arch/x86/include / -I$(KERNEL_FILES) /arch/x86/include /uapi/ -I$(KERNEL_FILES) /arch/x86/include /generated/ -I$(KERNEL_FILES) /arch/x86/include /generated/uapi/ INCLUDE += -I$(KERNEL_FILES) /include / -I$(KERNEL_FILES) /include /uapi/ -I. CFLAGS = -Wall -Werror -Wno-int-to-pointer-cast -Wno-unused -Wno-array-bounds -Wno-int-conversion CFLAGS += -MD -Os -fno-pie -nostdinc -nostdlib -fno-builtin -fno-stack-protector -DCONFIG_X86_32 -DBITS_PER_LONG=32 -DBITS_PER_LONG_LONG=64 -D__KERNEL__ -D__LITTLE_ENDIAN -m32 LDFLAGS = -m elf_i386 -no-pie all: img bootloader : bootloader.S nasm -w-zeroing -o $@ .bin $@ .S SRCDIR = ./ $(KERNEL_FILES) /arch/x86/lib/ $(KERNEL_FILES) /lib/ $(KERNEL_FILES) /init/ ./init/ C_SOURCES = $(foreach d,$(SRCDIR) ,$(wildcard $(d) *.c) ) C_OBJS = $(patsubst %.c,%.o,$(C_SOURCES) ) C_DEPS = $(patsubst %.c,%.d,$(C_SOURCES) ) %.o : %.c gcc $(INCLUDE) $(CFLAGS) -c -o $* .o $* .c img: bootloader kernel dd if=bootloader.bin of=haribote.img count=1 bs=512 &>/dev/null dd if=/dev/zero of=haribote.img bs=512 seek=1 skip=1 count=2879 &>/dev/null mkdir -p ./floppy mount -o loop haribote.img ./floppy -o fat=12 sleep 1 cp kernel ./floppy sleep 1 umount ./floppy rm -rf ./floppy kernel: header cobjs cat cobjs >> header cp header kernel header: header.S gcc $(INCLUDE) $(CFLAGS) -c -o $@ .bin $< ld $(LDFLAGS) -N -e bootsect_start -Ttext 0xc400 -o $@ .out $@ .bin objdump -S -D $@ .out > $@ .asm objcopy -S -O binary -j .text $@ .out $@ asmfunc.o: asmfunc.S gcc $(INCLUDE) $(CFLAGS) -c -o $@ $< cobjs: $(C_OBJS) asmfunc.o ld $(LDFLAGS) -N -T cobjs.ld -o $@ .out $^ objdump -S -D $@ .out > $@ .asm objcopy -S -O binary $@ .out $@ run : qemu-system-x86_64 -enable-kvm -m 4G -smp 1 -fda haribote.img clean: rm -f kernel rm -f cobjs cobjs.out cobjs.asm rm -f header header.bin header.out header.asm rm -f bootloader.bin haribote.img rm -f $(C_OBJS) $(C_DEPS) rm -f *.o *.d *.bin *.asm *.out
我们需要让start_kernel在cobjs的.text段第一个,有两个方案将main.o在链接的时候放到第一个位置,需要在main.c中唯一 使用.text.first配合ld链接配置文件实现 这里使用第二个方案 1 2 3 4 5 void HariMain () ;void __attribute__((section (".text.first" ))) start_kernel (void ) { HariMain (); }
1 2 3 4 5 6 7 8 9 ENTRY(start_kernel) SECTIONS { . = 0x00280000; .text : { *(.text.first) *(.text) } }
第4天 C语言与画面显示的练习 1. 图形化界面 参考linux-kernel启动过程
2. 调色板 调色板是显卡的一个模块,由于颜色只有8位,也就是256色,但是正常RGB有24位 所以我们可以给显卡设置256种颜色,0-255分别表示一种颜色 在用的时候直接设置对应内存为一个号码,显卡就会直接将对应位置显示成对应的颜色 但是cpu中断和调色板的io存取需要使用汇编来实现,c语言无法实现 设置调色板
先屏蔽中断 将想要设置的号码(0-255)写入到0x03c8 然后按照RGB的顺序写入0x03c9,想要继续设定,就直接继续写就行了 想要读出来对应号码的RGB,将号码写入到0x03c7,再从0x03c9读取3次。同理继续读就是下一个号码 最后恢复中断位 1 2 3 4 5 6 7 8 9 10 11 12 13 14 void set_palette (int start, int end, unsigned char *rgb) { int i, eflags; eflags = io_load_eflags (); io_cli (); io_out8 (0x03c8 , start); for (i = start; i <= end; i++) { io_out8 (0x03c9 , rgb[0 ] / 4 ); io_out8 (0x03c9 , rgb[1 ] / 4 ); io_out8 (0x03c9 , rgb[2 ] / 4 ); rgb += 3 ; } io_store_eflags (eflags); return ; }
3. 速度问题 运行起来后,要显示有颜色的画面一直是黑屏。等待了几分钟后才显示预期画面,差点以为代码有bug。不过这么慢也是个bug
排查了很久发现,原始makefile编译出来的其实是64位的c程序,运行的时候特别慢,应该是不兼容导致的。修改了makefile的cflags和ldflags后就运行特别快了。之前就算导入了c语言,也只是hlt,看不出效果,所以没有发现
1 2 CFLAGS += -m32 LDFLAGS = -m elf_i386
4. 使用c语言遇到的几个问题 switch不能超过5个case,否则会出问题,不清楚为什么 while中不能使用i–,不清楚为什么 第5天 结构体、文字显示与GDT/IDT初始化 1. 字符点阵 假设一个字符占用像素点为8x16,那么可以使用char[16]表示一个字符的点阵 作者找其他人要了一个hankaku.txt里面包含了char里面所有可见字符的点阵,总共由256个字符,占用4096个字节 在对应的vram位置设置color就可以直接显示字符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 extern char hankaku[4096 ];static void put_font8 (char *vram, int xsize, int x, int y, char color, char c) { int i, j; char *font8 = hankaku + c * 16 ; for (i = 0 ; i < 16 ; ++i) { char *p = vram + (y + i) * xsize + x; char tmp = font8[i]; for (j = 0 ; j < 8 ; ++j) { if ((tmp & (0x80 >> j)) > 0 ) { p[j] = color; } } } }
2. GDT和IDT 2.1. 分段 因为操作系统可以执行多个进程,但是每个进程使用的内存是独立的,需要使用分段让每个进程使用的内存隔开
2.2. GDT: global segment descriptor table 全局段记录表 段寄存器是16位,一位一个字节。一个段描述结构体是8个字节占用3位,所以低三位不能用,只有13位,一共8192个段描述结构体组成表 段寄存器可以指示65536个字节(64KB),cpu没这么大内存存储,所以需要放到内存里面,我们可以任意指定一块内存,将首地址和个数信息放到GDTR寄存器中就好了 段描述结构体见下面定义,来自于cpu手册,一共8个字节 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct desc_struct { u16 limit0; u16 base0; u16 base1 : 8 ; u16 type : 4 ; u16 s : 1 ; u16 dpl : 2 ; u16 p : 1 ; u16 limit1 : 4 ; u16 avl : 1 ; u16 l : 1 ; u16 d : 1 ; u16 g : 1 ; u16 base2 : 8 ; } __attribute__((packed));
随意取内存一段地址 0x00270000 - 0x0027ffff 这一段存储 将所有段初始化成全0 将段号1设置位cpu管理段,在内存的地址为0,大小为4GB,为32位内管理的最大内存,可读可写 段号为3的设置为kernel程序所在的内存段,地址在0x00280000,大小512K,可读可执行 最后通过汇编导出的函数写入GDTR,因为c语言无法设置GDTR 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #define ADR_GDT 0x00270000 #define LIMIT_GDT 0x0000ffff #define ADR_BOTPAK 0x00280000 #define LIMIT_BOTPAK 0x0007ffff #define AR_DATA32_RW 0x4092 #define AR_CODE32_ER 0x409a static void set_segmdesc (struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar) { if (limit > 0xfffff ) { ar |= 0x8000 ; limit /= 0x1000 ; } sd->limit_low = limit & 0xffff ; sd->base_low = base & 0xffff ; sd->base_mid = (base >> 16 ) & 0xff ; sd->access_right = ar & 0xff ; sd->limit_high = ((limit >> 16 ) & 0x0f ) | ((ar >> 8 ) & 0xf0 ); sd->base_high = (base >> 24 ) & 0xff ; return ; } void init_gdtidt (void ) { struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *)ADR_GDT; struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *)ADR_IDT; int i; for (i = 0 ; i < LIMIT_GDT / sizeof (struct SEGMENT_DESCRIPTOR); i++) { set_segmdesc (gdt + i, 0 , 0 , 0 ); } set_segmdesc (gdt + 12 , LIMIT_BOTPAK, ADR_BOTPAK, AR_CODE32_ER); set_segmdesc (gdt + 13 , 0xffffffff , 0x00000000 , AR_DATA32_RW); load_gdtr (LIMIT_GDT, ADR_GDT); ... }
1 2 3 4 5 6 7 8 9 10 # 第0个段是空的,全部是0 # 第1个段是数据段,cpu管理的总内存,从0x00000000开始,0x92代表可读可写,内存上限为0xfffff,g = 1代表4K为单位,4G # 按照结构体和16位低位高位计算 0xffff, 0x0000, 0x9200, 0x00cf # 第3个段是代码段,c代码所在位置,从0x00280000开始,0x9a代表可读可执行,内存上限为0x7ffff,g = 0代表字节为单位,512K # 按照结构体和16位低位高位计算 0xffff, 0x0000, 0x9a28, 0x0047 .p2align 2 # 按照2^2 = 4字节对齐 gdt: .word 0x0000,0x0000,0x0000,0x0000 # null seg .word 0xffff,0x0000,0x9200,0x00cf # 可读写的段,数据段 .word 0xffff,0x0000,0x9a28,0x0047 # 给c代码使用的段
2.3. IDT: interrupt descriptor table 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct idt_bits { u16 ist : 3 ; u16 zero : 5 ; u16 type : 5 ; u16 dpl : 2 ; u16 p : 1 ; } __attribute__((packed)); struct gate_struct { u16 offset_low; u16 segment; struct idt_bits bits; u16 offset_middle; #ifdef CONFIG_X86_64 u32 offset_high; u32 reserved; #endif } __attribute__((packed));
2.4. GDT、IDT、LDT和TSS的关系 GDT,IDT都是全局的。LDT是局部的(在GDT中有它的描述符) GDT用来存储描述符(门或非门);系统中几个CPU,就有几个GDT IDT整个系统只有一个 系统启动时候需要初始化GDT和IDT。LDT和进程相关,并不一定必有 TSS: Task-State Segment,任务状态段,保存任务状态信息的系统段 TSS只能存在于GDT中 Task-Gate Descriptor,任务门描述符,用来保存和恢复任务的上下文信息。可以放到GDT、LDT、IDT中,里面的TSS段选择指向GDT的TSS描述符 下图为32位TSS结构 第6天 中断处理 1. PIC: Programmable interrupt controller 可编程中断控制器 就是一个芯片,将8个中断信号合成一个中断信号输出给cpu 当前电脑上不止8个外部设备,所以使用两个pic合并成15个中断信号(主PIC的IRQ2被从PIC占据) PIC是外部设备,不能直接使用C语言的等于赋值,需要使用io_out8 主从PIC的寄存器赋值需要使用端口进行,具体端口定义如下 1 2 3 4 5 6 7 8 9 10 11 12 13 #define PIC0_ICW1 0x0020 #define PIC0_OCW2 0x0020 #define PIC0_IMR 0x0021 #define PIC0_ICW2 0x0021 #define PIC0_ICW3 0x0021 #define PIC0_ICW4 0x0021 #define PIC1_ICW1 0x00a0 #define PIC1_OCW2 0x00a0 #define PIC1_IMR 0x00a1 #define PIC1_ICW2 0x00a1 #define PIC1_ICW3 0x00a1 #define PIC1_ICW4 0x00a1
0x00-0x1f中断号不能使用,是cpu内部用于产生错误的中断号,所以从0x20开始设置PIC的代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void init_pic () { io_out8 (PIC0_IMR, 0xff ); io_out8 (PIC1_IMR, 0xff ); io_out8 (PIC0_ICW1, 0x11 ); io_out8 (PIC0_ICW2, 0x20 ); io_out8 (PIC0_ICW3, 1 << 2 ); io_out8 (PIC0_ICW4, 0x01 ); io_out8 (PIC1_ICW1, 0x11 ); io_out8 (PIC1_ICW2, 0x28 ); io_out8 (PIC1_ICW3, 1 << 1 ); io_out8 (PIC1_ICW4, 0x01 ); io_out8 (PIC0_IMR, 0xfb ); io_out8 (PIC1_IMR, 0xff ); }
2. 中断号对应的中断类型 硬件中断号 系统中断号 用途 IRQ0 INT20 IRQ1 INT21 PS/2键盘 IRQ2 INT22 PIC1的中断 IRQ3 INT23 IRQ4 INT24 IRQ5 INT25 IRQ6 INT26 IRQ7 INT27 初始化PIC可能引发的中断 IRQ8 INT28 IRQ9 INT29 IRQ10 INT2a IRQ11 INT2b IRQ12 INT2c PS/2鼠标 IRQ13 INT2d IRQ14 INT2e IRQ15 INT2f
3. 注册中断函数 也就是将中断函数地址写入idt中 找到对应中断号对应的idt地址,将函数地址和对应的段号放进去,设置标志位即可 由于中断最终要调用IRETD汇编指令退出,所以使用汇编函数调用c函数的方式来进行,并在里面存储了中断打断的进程的上下文信息 gcc链接的时候,已经指定了地址在0x00280000,所有函数在汇编代码中就是真实地址(0x00280000后面),在汇编启动时拷贝到0x00280000,其他c代码实际使用就是这个地址,但是对于cpu来说,不关注是否拷贝到内存的哪个位置,只关注段内偏移,所以idt中设置的是在段中的偏移,这里需要减去ADR_BOTPAK 1 2 3 4 set_gatedesc (idt + 0x21 , (int )asm_inthandler21-ADR_BOTPAK, 12 * 8 , AR_INTGATE32);set_gatedesc (idt + 0x27 , (int )asm_inthandler27-ADR_BOTPAK, 12 * 8 , AR_INTGATE32);set_gatedesc (idt + 0x2c , (int )asm_inthandler2c-ADR_BOTPAK, 12 * 8 , AR_INTGATE32);
4. 中断处理 中断中尽可能少执行代码,所以将中断数据放到全局变量中,在外部进程上下文中读取变量进行处理 第7天 FIFO与鼠标控制 1. 鼠标键盘数据读取 鼠标键盘都属于键盘控制电路,数据获取都在端口0x0060 只能通过中断号来判断此端口数据是属于鼠标还是键盘 2. fifo 由于中断数据可能很多,所以需要使用fifo进行存取,防止数据丢失 fifo自己参考linux的实现,没有使用书上的实现,具体原理查看 linux源码分析-kfifo 3. 鼠标初始化 由于一开始鼠标并不是必须品,后来鼠标才加入 鼠标加入后,当时使用者并不怎么使用,为了防止频繁中断,将鼠标控制默认关闭了 所以想要使用鼠标,需要先通过键盘控制电路设置模式启用鼠标,再进行特定操作进行激活鼠标 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #define PORT_KEYDAT 0x0060 #define PORT_KEYSTA 0x0064 #define PORT_KEYCMD 0x0064 #define KEYSTA_SEND_NOTREADY 0x02 #define KEYCMD_WRITE_MODE 0x60 #define KBC_MODE 0x47 void wait_KBC_sendready () { for (;;) { if ((io_in8 (PORT_KEYSTA) & KEYSTA_SEND_NOTREADY) == 0 ) { break ; } } } void init_keyboard () { wait_KBC_sendready (); io_out8 (PORT_KEYCMD, KEYCMD_WRITE_MODE); wait_KBC_sendready (); io_out8 (PORT_KEYDAT, KBC_MODE); return ; } #define KEYCMD_SENDTO_MOUSE 0xd4 #define MOUSECMD_ENABLE 0xf4 void enable_mouse () { wait_KBC_sendready (); io_out8 (PORT_KEYCMD, KEYCMD_SENDTO_MOUSE); wait_KBC_sendready (); io_out8 (PORT_KEYDAT, MOUSECMD_ENABLE); }
4. 使用linux的idt和gdt的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <asm/desc.h> #include <asm/desc_defs.h> #include <linux/kernel.h> #include "asmfunc.h" #define ADR_IDT 0x0026f800 #define LIMIT_IDT 0x000007ff #define ADR_GDT 0x00270000 #define LIMIT_GDT 0x0000ffff #define ADR_BOTPAK 0x00280000 #define LIMIT_BOTPAK 0x0000007f static const struct gdt_page gdt_page = {.gdt = {#ifdef CONFIG_X86_64 #else [GDT_ENTRY_KERNEL_CS] = GDT_ENTRY_INIT (0xc09a , ADR_BOTPAK, LIMIT_BOTPAK), [GDT_ENTRY_KERNEL_DS] = GDT_ENTRY_INIT (0xc092 , 0 , 0xfffff ), #endif }}; #define DPL0 0x0 #define DPL3 0x3 #define DEFAULT_STACK 0 #define G(_vector, _addr, _ist, _type, _dpl, _segment) \ { \ .vector = _vector, \ .bits.ist = _ist, \ .bits.type = _type, \ .bits.dpl = _dpl, \ .bits.p = 1, \ .addr = _addr, \ .segment = _segment, \ } #define INTG(_vector, _addr) G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS) #define SYSG(_vector, _addr) G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS) static const struct idt_data idt_table[] = { INTG (0x21 , (int )asm_inthandler21-ADR_BOTPAK), INTG (0x27 , (int )asm_inthandler27-ADR_BOTPAK), INTG (0x2c , (int )asm_inthandler2c-ADR_BOTPAK), }; void init_gdtidt (void ) { int i; struct desc_struct *gdt = (struct desc_struct *)ADR_GDT; write_gdt_entry (gdt, GDT_ENTRY_KERNEL_CS, &gdt_page.gdt[GDT_ENTRY_KERNEL_CS], DESCTYPE_S); write_gdt_entry (gdt, GDT_ENTRY_KERNEL_DS, &gdt_page.gdt[GDT_ENTRY_KERNEL_DS], DESCTYPE_S); load_gdtr (LIMIT_GDT, ADR_GDT); gate_desc *idt = (gate_desc *)ADR_IDT; gate_desc desc = {0 }; for (i = 0 ; i < LIMIT_IDT / sizeof (gate_desc); i++) { write_idt_entry (idt, i, &desc); } const struct idt_data *t = idt_table; for (i = 0 ; i < ARRAY_SIZE (idt_table); ++i, ++t) { idt_init_desc (&desc, t); write_idt_entry (idt, t->vector, &desc); } load_idtr (LIMIT_IDT, ADR_IDT); return ; }
不过发现了一个问题,gcc编译不能使用-O1,只能使用-Os,否则中断会崩溃 第8天 鼠标控制与32位模式切换 1. 鼠标数据解读 鼠标使能后会先发送0xfa数据,然后会连续三个中断发送三个字节数据 2. 当前操作系统内存分布图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 0x00000000 - 0x000fffff: 启动中多次使用。(1MB) - 0x00007c00 - 0x00007dff: 启动区装载地址,默认行为(512B) - 0x00008000 - 0x000081ff: 预留磁盘的C0-H0-S1位置,好计算偏移量(512B) - 0x00008200 - 0x00034fff: 磁盘上C0-H0-S2到C9-H1-S18读取位置(180KB) - 0x0000c400 - : 磁盘的0x4400位置的内容,asmhead.nas - 0x000a0000 - 0x000affff: vram内存地址 - 0x000f0000 附近存在bios本身 0x00100000 - 0x00267fff: 用于保存软盘内容。(1440KB) 0x00268000 - 0x0026f7ff: 空(30KB) 0x0026f800 - 0x0026ffff: IDT(2KB) 0x00270000 - 0x0027ffff: GDT(64KB) 0x00280000 - 0x002fffff: kernel(512KB) 0x00300000 - 0x003fffff: 栈及其他(1MB) 0x00400000 - : 空
第9天 内存管理 1. 禁用高速缓存 486之后的cpu存在高速缓存,如果代码上设置一个内存立刻使用或修改,cpu会先用在缓存中,并不会直接写到内存中 内存检查就是写入内存一个值操作后读出来是否正常,这个时候就需要禁用cpu的高速缓存 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 static inline uint32_t io_load_eflags (void ) { uint32_t eflags; asm volatile ("pushfl; popl %0" : "=r" (eflags)) ; return eflags; } static inline void io_store_eflags (uint32_t eflags) { asm volatile ("pushl %0; popfl" : : "r" (eflags)) ; }static inline uint32_t load_cr0 (void ) { uint32_t cr0; asm volatile ("movl %%cr0, %0" : "=r" (cr0)) ; return cr0; } static inline void store_cr0 (uint32_t cr0) { asm volatile ("movl %0, %%cr0" : : "r" (cr0)) ; }unsigned int memtest (unsigned int start, unsigned int end) { bool before_386; unsigned int eflg, cr0, i; eflg = io_load_eflags (); eflg |= EFLAGS_AC_BIT; io_store_eflags (eflg); eflg = io_load_eflags (); before_386 = (eflg & EFLAGS_AC_BIT) == 0 ; eflg &= ~EFLAGS_AC_BIT; io_store_eflags (eflg); if (!before_386) { cr0 = load_cr0 (); cr0 |= CR0_CACHE_DISABLE; store_cr0 (cr0); } i = memtest_sub (start, end); if (!before_386) { cr0 = load_cr0 (); cr0 &= ~CR0_CACHE_DISABLE; store_cr0 (cr0); } return i; }
2. 内存检查和编译器优化 如果使用c语言实现内存检查
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static unsigned int memtest_sub (unsigned int start, unsigned int end) { unsigned int i, old, pat0 = 0xaa55aa55 , pat1 = 0x55aa55aa ; unsigned int *p; for (i = start; i <= end; i += 0x1000 ) { p = (unsigned int *)(i + 0xffc ); old = *p; *p = pat0; *p ^= 0xffffffff ; if (*p != pat1) { not_memory: *p = old; break ; } *p ^= 0xffffffff ; if (*p != pat0) { goto not_memory; } *p = old; } return i; }
编译器会进行优化,发现第一个if判断没有意义,pat0^0xffffffff本来就是等于pat1啊,就算赋值给了*p不是一样吗
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 static unsigned int memtest_sub (unsigned int start, unsigned int end) { unsigned int i, old, pat0 = 0xaa55aa55 , pat1 = 0x55aa55aa ; unsigned int *p; for (i = start; i <= end; i += 0x1000 ) { p = (unsigned int *)(i + 0xffc ); old = *p; *p = pat0; *p ^= 0xffffffff ; *p ^= 0xffffffff ; if (*p != pat0) { goto not_memory; } *p = old; } return i; }
两次*p^0xffffffff数据保持原样,那就没有必要了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 static unsigned int memtest_sub (unsigned int start, unsigned int end) { unsigned int i, old, pat0 = 0xaa55aa55 , pat1 = 0x55aa55aa ; unsigned int *p; for (i = start; i <= end; i += 0x1000 ) { p = (unsigned int *)(i + 0xffc ); old = *p; *p = pat0; if (*p != pat0) { goto not_memory; } *p = old; } return i; }
*p = pat0后比较两个的值,肯定一样啊
1 2 3 4 5 6 7 8 9 10 static unsigned int memtest_sub (unsigned int start, unsigned int end) { unsigned int i, old, pat0 = 0xaa55aa55 , pat1 = 0x55aa55aa ; unsigned int *p; for (i = start; i <= end; i += 0x1000 ) { p = (unsigned int *)(i + 0xffc ); old = *p; *p = old; } return i; }
复制old后又赋值回去,等于没变
1 2 3 4 5 6 7 8 static unsigned int memtest_sub (unsigned int start, unsigned int end) { unsigned int i, old, pat0 = 0xaa55aa55 , pat1 = 0x55aa55aa ; unsigned int *p; for (i = start; i <= end; i += 0x1000 ) { p = (unsigned int *)(i + 0xffc ); } return i; }
p就赋了一个值,全局都没有使用
1 2 3 4 5 6 static unsigned int memtest_sub (unsigned int start, unsigned int end) { unsigned int i, old, pat0 = 0xaa55aa55 , pat1 = 0x55aa55aa ; unsigned int *p; for (i = start; i <= end; i += 0x1000 ) {} return i; }
整个函数就剩下for循环了,所以一定返回输入什么就怎么样。解决这样的问题只需要在p前面修饰上volatile即可,编译器认为这个p是要实时变化的,就不会进行优化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 static unsigned int memtest_sub (unsigned int start, unsigned int end) { unsigned int i, old, pat0 = 0xaa55aa55 , pat1 = 0x55aa55aa ; volatile unsigned int *p; for (i = start; i <= end; i += 0x1000 ) { p = (unsigned int *)(i + 0xffc ); old = *p; *p = pat0; *p ^= 0xffffffff ; if (*p != pat1) { not_memory: *p = old; break ; } *p ^= 0xffffffff ; if (*p != pat0) { goto not_memory; } *p = old; } return i; }
第10天 - 第11天 窗口处理 主要处理图层、刷新等方式,使用图层添加了一个窗口,鼠标放到最上层。在刷新过程中发现了闪烁的问题,将刷新的方式优化了几板后没有了闪烁。主要结构如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #define SHEET_USE 1 struct SHTCTL ;struct SHEET { struct SHTCTL *ctl; unsigned char *buf; int bxsize; int bysize; int vx0; int vy0; int col_inv; int height; int flags; }; #define MAX_SHEETS 256 struct SHTCTL { unsigned char *vram; unsigned char *map; int xsize, ysize; int top; struct SHEET *sheets[MAX_SHEETS]; struct SHEET sheets0[MAX_SHEETS]; };
刷新只刷新某一个区域,对区域内的点,根据map取哪一层要写入则写入vram。
第12天 定时器(1) PIT(Programmable Interval Timer) 可编程的间隔型定时器 cpu有一个旁路芯片,8254芯片(或替代品),定时向cpu的IRQ0发起中断。如果我们接收此中断,设置中断周期就可以用于做定时器中断。
此芯片频率大概为1.19318MHz,我们设置一个计数来让计数到某个值产生一次中断1:1.19318MHz 1000:1.19318KHz 10000:119.318Hz 11932:100Hz 对应的设置指令为
OUT(0x43, 0x34): 0x43为pit_ctrl寄存器,0x34为mode OUT(0x40, 中断周期低8位): OUT(0x40, 中断周期高8位): 基于linux的实现,定义如下宏进行处理
1 2 3 4 5 6 7 8 9 #define PIT_TICK_RATE 1193182ul # define HZ CONFIG_HZ #define PIT_LATCH ((PIT_TICK_RATE + HZ/2) / HZ)
在makefile中定义-DCONFIG_HZ=1000即可实现,直接使用PIT_LATCH设置到中断周期里面
定时器中断优化 可以一步到位,使用链表管理定时器,设置定时器插入到链表里面按照超时时间从小到大排序。中断中仅关注第一个定时器是否到期即可。这里使用内核的hlist处理定时器,不需要timer_alloc也可以使用定时器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 extern volatile unsigned long jiffies;void init_pit (void ) ;#define MAX_TIMER 500 typedef STRUCT_KFIFO (unsigned char , 8 ) TimerBufType ;struct TIMER { struct hlist_node entry; unsigned long expires; unsigned int flags; TimerBufType *fifo; unsigned char data; }; void timer_free (struct TIMER *timer) ;void timer_init (struct TIMER *timer, TimerBufType *fifo, unsigned char data) ;void timer_settime (struct TIMER *timer, unsigned long timeout) ;#include <linux/list.h> volatile unsigned long jiffies = 0 ;static HLIST_HEAD (s_timer_list) ; void init_pit (void ) { io_out8 (PIT_MODE, 0x34 ); io_out8 (PIT_CH0, PIT_LATCH & 0xff ); io_out8 (PIT_CH0, PIT_LATCH >> 8 ); } void timer_free (struct TIMER *timer) { struct TIMER *node; timer->flags = 0 ; hlist_for_each_entry (node, &s_timer_list, entry) { if (node == timer) { hlist_del_init (&node->entry); break ; } } } void timer_init (struct TIMER *timer, TimerBufType *fifo, unsigned char data) { timer->fifo = fifo; timer->data = data; INIT_HLIST_NODE (&timer->entry); } void timer_settime (struct TIMER *timer, unsigned long timeout) { int e; timer->expires = timeout; e = io_load_eflags (); io_cli (); struct TIMER *node; struct TIMER *prev = NULL ; hlist_for_each_entry (node, &s_timer_list, entry) { if (node->expires > timer->expires) { break ; } prev = node; } if (prev == NULL ) { hlist_add_head (&timer->entry, &s_timer_list); } else { hlist_add_behind (&timer->entry, &prev->entry); } io_store_eflags (e); io_sti (); } void inthandler20 (int *esp) { io_out8 (PIC0_OCW2, 0x60 ); ++jiffies; struct TIMER *node; struct hlist_node *n; hlist_for_each_entry_safe (node, n, &s_timer_list, entry) { if (node->expires <= jiffies) { hlist_del_init (&node->entry); kfifo_put (node->fifo, node->data); } else { break ; } } }
第13天 定时器(2) 串口输出日志 搞了这么久,发现出现一些问题没有办法进行排查,只能通过比较搓的方式直接在屏幕上输出某个字符串代表跑到了某个逻辑。而linux启动整个过程中可以有很多输出到串口或控制台上,参考linux的实现将日志加到操作系统中。
对于串口,直接将字符串写到0x3f8即可输出到串口中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include <linux/jiffies.h> #include <linux/kernel.h> #include <linux/string.h> #include "asmfunc.h" static unsigned long early_serial_base = 0x3f8 ; #define XMTRDY 0x20 #define DLAB 0x80 #define TXR 0 #define RXR 0 #define IER 1 #define IIR 2 #define FCR 2 #define LCR 3 #define MCR 4 #define LSR 5 #define MSR 6 #define DLL 0 #define DLH 1 static inline unsigned int io_serial_in (unsigned long addr, int offset) { return inb (addr + offset); }static inline void io_serial_out (unsigned long addr, int offset, int value) { outb (value, addr + offset); }static inline int serial_putc (unsigned char ch) { unsigned timeout = 0xffff ; while ((io_serial_in (early_serial_base, LSR) & XMTRDY) == 0 ) { --timeout; } io_serial_out (early_serial_base, TXR, ch); return timeout ? 0 : -1 ; } static inline void serial_write (const char *s, unsigned n) { while (*s && n-- > 0 ) { if (*s == '\n' ) serial_putc ('\r' ); serial_putc (*s); s++; } } void serial_printf (const char *fmt, ...) { va_list ap; char buf[512 ]; int n; va_start (ap, fmt); n = vscnprintf (buf, sizeof (buf), fmt, ap); va_end (ap); serial_write (buf, n); } void _print_current_time(void ) { unsigned long msecs = jiffies_to_msecs (jiffies); unsigned long secs = msecs / 1000 ; unsigned long msec = msecs % 1000 ; serial_printf ("[%5lu.%03lu]" , secs, msec); }
1 2 3 4 5 6 7 8 9 10 11 12 13 #ifndef __LOG_H__ #define __LOG_H__ void serial_printf (const char *fmt, ...) ;void _print_current_time(void );#define LOG_INFO(fmt, ...) \ _print_current_time(); \ serial_printf("[%s:%d %s] " fmt "\r\n" , __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__) #endif
makefile中修改一下qemu的参数,添加-serial stdio,然后在控制台就可以看到输出,代码中使用LOG_INFO打印即可。输出效果
1 2 3 4 5 6 7 [ 0.000][bootpack.c:21 HariMain] HariMain start [ 0.000][bootpack.c:30 HariMain] init gdtidt done [ 0.002][bootpack.c:44 HariMain] create timer done [ 0.002][bootpack.c:50 HariMain] init keyboard and mouse done [ 0.004][bootpack.c:54 HariMain] init palette done [ 0.314][bootpack.c:64 HariMain] init memory done, cost 310ms, memory 2048MB, free: 2093688KB [ 0.324][bootpack.c:106 HariMain] init windows done
中断处理优化 其实就是搞了个数组实现链表的方式,将timer管理起来。之前使用了hlist来管理timer,天然就是链表,后面两个优化就不做了。
第14天 提高分辨率及键盘输入 VBE(VESA BIOS Extensions) 参考:https://en.wikipedia.org/wiki/VESA_BIOS_Extensions
最开始电脑规格是IBM公司决定的,也制定了相关的规格,各家显卡公司只能迎合IBM规格制作显卡。一段时间后,显卡公司图像处理技术超过了IBM公司,就出现了各式各样的画面模式,造成了显卡公司竞争和设定方法使用方法各不相同。由于开发人员不想记忆那么多情况,所以很多高性能显卡也只使用最原始的320x200来使用。为了解决这个问题,各个显卡公司经过协商,成立了VESA协会(Video Electronics Standards Association,视频电子标准协会)。协会制定了一个通用设定方法,虽然不能完全兼容,但是可以通用。这个BIOS操作称为“VESA BIOS Extension”简称VBE。
在qemu上测试支持下面几个模式
0x0100: 640x400 0x0101: 640x480 0x0105: 1024x768 0x0107: 1280x1024 0x011c: 1600x1200 不过设置之后的vram的地址不是0xe0000000而是0xfd000000。设置方法为
1 2 3 4 5 6 #define VBEMODE 0x0105 mov $(VBEMODE+0x4000),%bx mov $0x4f02,%ax int $0x10 movb $8,(VMODE)
需要ax设置0x4f02,bx设置vbemode+0x4000,然后调用int 10
VBE检查 部分显卡可能真不支持VBE的情况,我们的代码就有问题了,无法显示屏幕,所以需要做一个自适应检查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 # 确认VBE是否存在 mov $0x9000,%ax mov %ax,%es mov $0,%di mov $0x4f00,%ax int $0x10 cmp $0x004f,%ax jne scrn320 # 检查vbe版本需要是0x200,即VBE 2.0 mov %es:(%di,4),%ax cmp $0x0200,%ax jb scrn320 # 测试画面模式是否能使用,需要ax为0x004f mov $VBEMODE,%cx mov $0x4f01,%ax int $0x10 cmp $0x004f,%ax jne scrn320 # 画面的模式 包含了非常多的重要信息,如模式属性 # WORD [es:di+0x00]: 模式属性,第7位需要是1,这样才能加上0x4000 # WORD [es:di+0x12]: X的分辨率 # WORD [es:di+0x14]: Y的分辨率 # BYTE [es:di+0x19]: 颜色数,必须为8,我们代码用的就是8 # BYTE [es:di+0x1b]: 颜色的指定方法,必须为4,即调色板模式 # DWORD [es:di+0x28]: VRAM的地址 # 确认颜色数 movb %es:0x19(%di),%al cmp $8,%al jne scrn320 # 确认颜色指定方法 movb %es:0x1b(%di),%al cmp $4,%al jne scrn320 # 重新调用设置画面模式,需要加上0x4000 mov $(VBEMODE+0x4000),%bx mov $0x4f02,%ax int $0x10 # 记录对应信息给到c语言 movb $8,(VMODE) mov %es:0x12(%di),%ax mov %ax,(SCRNX) mov %es:0x14(%di),%ax mov %ax,(SCRNY) movl %es:0x28(%di),%eax movl %eax,(VRAM)
第15天 - 第16天 多任务 1. 设置tss结构体 从内核中参考的代码,tss_struct直接使用内核的相关实现。因为参考内核将CS和DS放到12和13,0为null。这里使用1和2的保留段进行实验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void HariMain (void ) { ... struct desc_struct *gdt = (struct desc_struct *)ADR_GDT; struct tss_struct tss_a, tss_b; int task_b_esp = memman_alloc_4k (memman, 64 * 1024 ) + 64 * 1024 ; tss_a.x86_tss.ldt = 0 ; tss_b.x86_tss.ldt = 0 ; tss_b.x86_tss.ip = (int )&task_b_main - ADR_BOTPAK; tss_b.x86_tss.flags = 0x00000202 ; tss_b.x86_tss.ax = 0 ; tss_b.x86_tss.cx = 0 ; tss_b.x86_tss.dx = 0 ; tss_b.x86_tss.bx = 0 ; tss_b.x86_tss.sp = task_b_esp; tss_b.x86_tss.bp = 0 ; tss_b.x86_tss.si = 0 ; tss_b.x86_tss.di = 0 ; tss_b.x86_tss.es = __KERNEL_DS; tss_b.x86_tss.cs = __KERNEL_CS; tss_b.x86_tss.ss = __KERNEL_DS; tss_b.x86_tss.ds = __KERNEL_DS; tss_b.x86_tss.fs = __KERNEL_DS; tss_b.x86_tss.gs = __KERNEL_DS; tss_desc tss_a_desc; tss_desc tss_b_desc; set_tssldt_descriptor (&tss_a_desc, (unsigned long )&tss_a.x86_tss, DESC_TSS, __KERNEL_TSS_LIMIT); set_tssldt_descriptor (&tss_b_desc, (unsigned long )&tss_b.x86_tss, DESC_TSS, __KERNEL_TSS_LIMIT); write_gdt_entry (gdt, 1 , &tss_a_desc, DESC_TSS); write_gdt_entry (gdt, 2 , &tss_b_desc, DESC_TSS); load_tr_desc (1 * 8 ); ... }
这里有个坑,内核里面set_tssldt_descriptor会先memset成0再赋值,在我们小系统里面memset操作栈内地址会造成崩溃,所以将memset注释掉才能跑通。
针对load_tr_desc和taskswitch直接使用内联汇编实现
1 2 3 static inline void load_tr_desc (u32 tr) { asm volatile ("ltr %w0" ::"q" (tr)) ; }void taskswitch (void ) { __asm__ __volatile__("ljmp %0, %1" : : "i" (2 * 8 ), "i" (0x0000 )); }
2. jmp far简化 在at&t汇编中,ljmp就可以作为jmp far使用,所以实现内联汇编代码如下
1 2 3 4 5 6 7 8 static inline void farjmp (u16 seg, u16 offset) { __asm__ __volatile__("ljmp *%0\n" : : "m" ((struct { unsigned int off; unsigned short sel; }){offset, seg})); }
3. 给任务传参 32位汇编中,跳转到另一个函数,栈上先放进去传入的参数,再放入调用者的地址。64位汇编中参数小于7个的时候,参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。这里我们使用32位汇编。所以想给task_b_main传参就是将参数放到esp + 4,esp是return跳回的地址,我们用不到。
1 2 3 task_b_esp -= 8 ; *(int *)(task_b_esp + 4 ) = sht_back;