A 前情提要 在此之前,第一部分我们完成了基础的环境配置,bochs配置 ,以及MBR,loader 的的基础编写,成功的进入了保护模式 并且开启了内存分页功能
这一部分主要开启对内核,中断 进军
ps:如果参考本系列文章来实操,需要结合《操作系统真象还原》一起观看,否则会缺失很多细节
B. 加载内核 在之前的学习中,我们基本都是通过汇编来与及其对话,想想以前的驱动开发都是直接用C语言等语言来编写,然后操作系统来帮我们撑腰,但现在我们要用 C 语言写脱离操作系统的程序,必须要自己准备程序的入口地址 ,同时也不能用C标准库 ,因为系统调用的中断处理程序我们还没有准备
B.1 用C语言写内核 这里我们现在之前的目录基础上创建一个目录kernel(以后与内核相关的文件都放在这个目录下面),然后来实现我们自己的第一个程序来体验编译过程
这里首先介绍gcc编译器的基础使用:
gcc -c -o kernel/main.o kernel/main.c -c表示编译,汇编到目标代码(不链接) -o表是将输出的文件以指定的文件名来存储,有同名文件存在的时候就覆盖掉
首先我们创建第一个内核的文件:main.c
1 2 3 4 5 6 int main () { while (1 ); return 0 ; }
在这个文件路径下执行指令
1 2 3 sudo apt-get install gcc gcc -c -o main.o main.c
然后就可以看到main.o文件,但是目前只是一个目标文件(待重定位文件)重定位指的是文件里面所用的符号还没有安排地址,这些符号的地址 需要将来与其他目标文件“组成”一个可执行文件时再重新定位编排地址
在 Linux 下用于链接的程序是 ld ,链接有一个好处,可以指定最终生成的可执行文件的起始虚拟地址 。它是用-Ttext 参数来指定 的,所以咱们可以执行以下命令完成链接
1 ld main.o -Ttext 0xc0001500 -e main -o kernel.bin
-Ttext用来地址初始虚拟地址是0xc0001500,这个地址是设计好的,在后面的加载内核中会具体了解-e用来指定程序的起始地址(这里不仅仅可以以数字形式的地址,也可以是符号名),比如这里是main,用来知道程序从哪里开始执行(当然,如果主函数改成void __start(void) 那么就可以一气呵成不要这个指令了)-o用来指定输出的文件位置和名称
然后我们的目录下就出现了kernel.bin文件了
使用过gcc的人可能会问:直接使用gcc一口气编译链接生成的文件和我们分两步产生的bin文件有什么区别?
首先如果直接编译链接,生成的文件可能有4000字节+,但是手动的大概有1700字节+,因为编译器在编译过程中为咱们引用了别的代码,这就是 c 运行库的功劳,目的是在调用 main 函数前做初始化环境等工作(比如说__start入口符号等)
ok呀,这里也是大概举个main函数的例子来引入
B.2 elf文件简介 之前我们是BIOS 调用mbr(0x7c00),mbr调用loader(0x900),并且这些地址都是固定的,不灵活,调用方要提前与被调用方约定,那肯定会有更灵活的方法来让加载地址不那么固定吧
兄弟,有的有的,最简单的办法就是在程序文件中专门腾出个空间来写入这些程序的入口地址(主调程序再将信息读出来,然后加载到相应的入口地址,最后跳转即可),当然这里还可以添加分配内存什么的,这也就是文件头 的由来
在原先的纯二进制文件中添加新的文件头,就形成了一个新的文件格式,其中,程序头可以自定义,只要我们按照自己定义的格式去解析就行
在Windows下,一个可执行文件的格式是PE(注意,EXE只是拓展名,是文件名的一部分,并不是格式)
在Linux下,一个可执行文件的格式是ELF,是指通过编译链接的二进制可执行文件,该文件可以直接运行
为了避免混淆,和书作者一样采用与 ELF 规范相同的命名方式,本节中所说的目标文件即指各种类型符合 ELF规范的文件
至于如何弄清文件格式的本质,大家可以具体看看书中的解释,这里同样不过过多说明(因为也不是几句话能说明白的)
ELF 文件格式依然分为文件头和文件体两部分,其中文件头很复杂,具体内容可以参考linux系统下的/usr/include/elf.h定义(最权威的)
这里还是通过书中的实例来学习,这里原作者提供了一个sh脚本来方便使用(利用xxd指令逐字节输出文件的内容),主要是避免每次复杂的键入,方便使用,这里也直接列出(添加了中文注释,方便理解),我们来分析之前写的”内核”,也就是kernel.bin文件
1 2 3 4 5 6 7 8 9 10 11 xxd -u -a -g 1 -s $2 -l $3 $1
然后我们就可以来分析文件了
1 sh xxd.sh kernel/kernel.bin 0 300
结果如下:
00000000: 7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00 .ELF………… 00000010: 02 00 3E 00 01 00 00 00 00 15 00 C0 00 00 00 00 ..>…………. 00000020: 40 00 00 00 00 00 00 00 B0 16 00 00 00 00 00 00 @…………… 00000030: 00 00 00 00 40 00 38 00 02 00 40 00 07 00 04 00 ....@.8...@….. 00000040: 01 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 ……………. 00000050: 00 00 00 C0 00 00 00 00 00 00 00 C0 00 00 00 00 ……………. 00000060: 40 15 00 00 00 00 00 00 40 15 00 00 00 00 00 00 @.......@……. 00000070: 00 00 20 00 00 00 00 00 51 E5 74 64 06 00 00 00 .. …..Q.td…. 00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ……………. 00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ……………. 000000a0: 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00 00 ……………. 000000b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ……………. * 00000120: 00 00 00 00 00 00 00 00 00 00 00 00 …………
其中,加粗的部分是文件头(rlf header部分),第二部分特殊标记的部分是程序头表(program header table),这和书中是不一样的,因为书中的系统是32位系统,文件头通常是52字节,而我的是64位系统,所以通常是64字节,当然是多少位在文件头中也有说明(比如0x04: 02,表示是64位ELF)
下面主要通过表格总结这两部分的具体内容:
偏移
字段
说明
0x00-0x03
7F 45 4C 46
ELF 魔数(\x7FELF)
0x04
02
64 位 ELF(01 是 32 位)
0x05
01
小端序(02 是大端序)
0x10-0x11
02 00
可执行文件(ET_EXEC)
0x12-0x13
3E 00
x86-64 架构(0x3E)
0x18-0x1F
00 15 00 C0…
程序入口地址(0xC0001500)
0x20-0x27
40 00 00 00…
Program Header Table 偏移(0x40)
0x28-0x2F
B0 16 00 00…
Section Header Table 偏移(0x16B0)
0x36-0x37
38 00
Program Header 大小(56 字节)
0x38-0x39
07 00
Program Header 数量(7 个)
偏移
字段
说明
0x40-0x43
01 00 00 00
PT_LOAD(可加载段)
0x44-0x47
05 00 00 00
Flags(R+X,可读可执行)
0x48-0x4F
00 00 00 00…
文件偏移(0x0)
0x50-0x57
00 00 00 C0…
虚拟地址(0xC0000000)
0x58-0x5F
00 00 00 C0…
物理地址(通常同虚拟地址)
0x60-0x67
40 15 00 00…
段大小(0x1540 字节)
0x68-0x6F
40 15 00 00…
内存大小(0x1540 字节)
0x70-0x77
00 00 20 00…
对齐(0x200000)
B.3 将内核载入内存 已经期待了很久将内核载入内存
我们的内核文件是 kernel.bin,这个文件是由 loader 将其从硬盘上读出并加载到内存中的,到此,接力棒传到了最后一个选手的手里,也就是说我们现在需要将kernel.bin文件定入硬盘
先回忆一下布局: 0扇区存放着MBR 1扇区空着的 2扇区写入的loader文件(1.3kb),而我们的一个扇区是512字节,所以这里差不多会占用三个扇区,所以2~4扇区也不能使用了,所以这里我们原则要写入5扇区了,但是为了loader可能会有拓展,以及分开更安心,所以这里选择第9个扇区
开始操作:
1 2 dd if =/home/mouse/OS_mouse/tool/bochs/mouse/kernel/kernel.bin of=/home/mouse/OS_mouse/tool/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
加上之前的编译,链接,可能指令会很长,所以将这个指令联合之前的指令都写进一个脚本,方便后面执行使用(同时,如果后面有文件名修改,也同步修改脚本即可),当然,这个脚本不是那么完善,比如不支持多文件,或错误提示等,后面会再添加更新的
1 2 3 gcc -c -o main.o main.c && ldmain.o -Ttext 0xc0001500 -e main -o kernel.bin && dd if =/home/mouse/OS_mouse/tool/bochs/mouse/kernel/kernel.bin of=/home/mouse/OS_mouse/tool/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
这样我们在kernel文件夹下执行下面的执行便可以一气呵成完成编译链接写入了
主要修改两个地方,
加载内核 :将内核加载到内存缓冲区,这里选择在分页前开始加载
初始化内核 :在分页后,将加载寄哪里的elf内核文件放到对应的虚拟内存地址,然后跳过去执行,至此loader的工作到此结束
首先进行第一步,加载内核,这里就涉及到我应该加载到什么地方?
首先我们的内核很小,在低端1MB中安身即可,这里首先要考虑之前的的其它重要数据来分配,我们最终选择0x70000~0x9fbff的堵住开安身(问就是书作者决定的),差不多有190kb,完全够我们的内核使用
内核被加载到内存后,loader 还要通过分析其 elf 结构将其展开到新的位置,所以说,内核在内存中有两份拷贝,一份是 elf 格式的原文件 kernel.bin,另一份是 loader 解析 elf 格式的 kernel.bin 后在内存中生成的内核映像(也就是将程序中的各种段 segment 复制到内存后的程序体),这个映像才是真正运行的内核
其实既然决定好位置了,我们先在boot.inc文件中添加新的符号
1 2 3 4 5 6 7 8 9 ;/home/mouse/OS_mouse/tool/bochs/mouse/boot.inc ;--------------------- loader & kernel ------------------------- KERNEL_START_SECTOR equ 0x9 ;kernel.bin 所在的扇区号 9 KERNEL_BIN_BASE_ADDR equ 0x70000 ;从磁盘读出后写入的内存地址 KERNEL_ENTRY_POINT equ 0xc0001500 ;虚拟地址入口 ;--------------------- elf ------------------------------------- PT_NULL equ 0 ; ELF 程序头类型:未使用 ;略......
下面给出loader中添加的加载内核的代码,因为之前的代码其实已经有点长了,如果全部直接贴到这里显得浪费长度,也不好理解,所以后面的所有代码都是截取对应的函数来列举,当然,在文章末尾会列出本篇文章中用的所有文件的完整版
1 2 3 4 5 6 7 8 9 10 11 12 mov eax ,KERNEL_START_SECTOR mov ebx ,KERNEL_BIN_BASE_ADDR mov ecx ,200 call rd_disk_m_32 call setup_page
下面是初始化内核的代码:需要注意的是因为我是64系统下的,所以编译的结果elf也是64位,而我们的操作系统需要的是32位,所以这里修改修改我们之前的编译过程,即添加-m32参数
这里重新给出脚本的内容,注意在mbr.S文件里面有一行mov cx,4,是用来选择读入扇区的大小的,这里要修改成7,防止loader文件后续继续变大被截短了
1 2 3 4 5 6 7 8 # /home/mouse/OS_mouse/tool/bochs/mouse/kernel/kernel_start. sh BASE_DIR="/home/mouse/OS_mouse/tool/bochs/mouse/kernel" cd $BASE_DIR #编译 链接 写入32 位版本 gcc -m32 -c -o main. o main. c && ld -m elf_i386 main. o -Ttext 0xc0001500 -e main -o kernel. bin && dd if=$BASE_DIR/kernel. bin of=/home/mouse/OS_mouse/tool/bochs/hd60M. img bs=512 count=200 seek=9 conv=notrunc
同理给出loader文件的脚本吧,否则每次输入很多指令也挺烦的,后期会创建一个boot文件夹(位于/mouse/boot),然后将include文件也移动到这个位置,目前还是直接在mouse文件夹下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 BASE_DIR="/home/mouse/OS_mouse/tool/bochs/mouse" HD_IMG="/home/mouse/OS_mouse/tool/bochs/hd60M.img" cd $BASE_DIR nasm -I include/ -o mbr.bin mbr.S nasm -I include/ -o loader.bin loader.S dd if =$BASE_DIR /mbr.bin of=$HD_IMG bs=512 count=1 seek=0 conv=notruncdd if =$BASE_DIR /loader.bin of=$HD_IMG bs=512 count=7 seek=2 conv=notrunc
同时之前给出的表格elf的内容也是32位的,会与下面写内核的具体内容不同,所以这里也更新一遍表格如下(例子为新编译的文件)
偏移
字段
说明
0x00-0x03
7F 45 4C 46
ELF 魔数(\x7FELF)
0x04
01
32 位 ELF(02 是 64 位)
0x05
01
小端序(02 是大端序)
0x10-0x11
02 00
可执行文件(ET_EXEC)
0x12-0x13
03 00
x86 架构(0x03)
0x18-0x1B
00 15 00 C0
程序入口地址(0xC0001500)
0x1C-0x1F
34 00 00 00
Program Header Table 偏移(0x34)
0x20-0x23
60 06 00 00
Section Header Table 偏移(0x660)
0x2A-0x2B
20 00
Program Header 大小(32 字节)
0x2C-0x2D
07 00
Program Header 数量(7 个)
偏移
字段
说明
0x34-0x37
01 00 00 00
PT_LOAD(可加载段)
0x38-0x3B
00 10 00 C0
虚拟地址(0xC0001000)
0x3C-0x3F
00 10 00 C0
物理地址(通常同虚拟地址)
0x40-0x43
3C 05 00 00
段大小(0x53C 字节)
0x44-0x47
3C 05 00 00
内存大小(0x53C 字节)
0x48-0x4B
05 00 00 00
Flags(R+X,可读可执行)
0x4C-0x4F
00 10 00 00
对齐(0x1000)
回归正题,下面是初始化内核的代码:
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 kernel_init: xor eax , eax xor ebx , ebx xor ecx , ecx xor edx , edx mov dx ,[KERNEL_BIN_BASE_ADDR +42 ] mov ebx ,[KERNEL_BIN_BASE_ADDR +28 ] add ebx ,KERNEL_BIN_BASE_ADDR mov cx ,[KERNEL_BIN_BASE_ADDR +44 ] .each_segment: cmp byte [ebx + 0 ],PT_NULL je .PT_NULL push dword [ebx + 16 ] mov eax , [ebx + 4 ] add eax , KERNEL_BIN_BASE_ADDR push eax push dword [ebx + 8 ] call mem_cpy add esp ,12 .PTNULL: add ebx ,edx loop .each_segment ret mem_cpy: cld push ebp mov ebp ,esp push ecx mov edi , [ebp + 8 ] mov esi , [ebp + 12 ] mov ecx , [ebp + 16 ] rep movsb pop ecx pop ebp ret
函数 kernel_init 的作用是将 kernel.bin 中的段(segment)拷贝到各段自己被编译的虚拟地址处,将这些段单独提取到内存中,这就是平时所说的内存中的程序映像,其它的部分可以具体参考书中内容
最后还有一点点添加的地方,也就是调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 lgdt [gdt_ptr] jmp dword SELECTOR_CODE:enter_kernel enter_kernel: mov ax , SELECTOR_VIDEO mov gs , ax mov byte [gs :160 ], 'M' call kernel_init mov esp ,0xc009f000 jmp KERNEL_ENTRY_POINT
boot.inc的代码仅仅添加了4行这里就不打出来了,main.c也只是一个示例,这里也就不列出来了
下面是loader.S的具体代码 ,有需要可以展开看
loader.S: 点击查看更多
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 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 %include "boot.inc" section loader vstart=LOADER_BASE_ADDRLOADER_STACK_TOP equ LOADER_BASE_ADDR jmp loader_startGDT_BASE: dd 0x00000000 dd 0x00000000 CODE_DESC: dd 0x0000FFFF dd DESC_CODE_HIGH4 DATA_STACK_DESC: dd 0x0000FFFF dd DESC_DATA_HIGH4 VIDEO_DESC: dd 0x80000007 dd DESC_VIDEO_HIGH4 GDT_SIZE equ $ - GDT_BASE GDT_LIMIT equ GDT_SIZE - 1 times 50 dq 0 SELECTOR_CODE equ (0x0001 <<3 ) + TI_GDT + RPL0 SELECTOR_DATA equ (0x0002 <<3 ) + TI_GDT + RPL0 SELECTOR_VIDEO equ (0x0003 <<3 ) + TI_GDT + RPL0 times 0x200 - ($ -$$) db 0 total_mem_bytes dd 0 align 4 gdt_ptr: dw GDT_LIMIT dd GDT_BASE loadermsg db 'Mosue' ards_buf times 244 db 0 ards_nr dw 0 loader_start: xor ebx ,ebx mov edx ,0x534D4150 mov di ,ards_buf .e820_mem_get_loop: mov eax ,0x0000e820 mov ecx ,20 int 0x15 jc .e820_failed_so_try_e801 add di ,cx inc word [ards_nr] cmp ebx ,0 jnz .e820_mem_get_loop mov cx ,[ards_nr] mov ebx ,ards_buf xor edx ,edx .find_max_mem_area: mov eax ,[ebx ] add eax ,[ebx +8 ] add ebx ,20 cmp edx ,eax jge .next_ards mov edx ,eax .next_ards: loop .find_max_mem_area jmp .mem_get_ok .e820_failed_so_try_e801: mov ax ,0xe801 int 0x15 jc .e801_failed_so_try_e88 mov cx ,0x400 mul cx shl edx ,16 and eax ,0x0000FFFF or edx ,eax add edx ,0x100000 mov esi ,edx xor eax ,eax mov ax ,bx mov ecx ,0x10000 mul ecx add esi ,eax mov edx ,esi jmp .mem_get_ok .e801_failed_so_try_e88: mov ah ,0x88 int 0x15 jc .error_hlt and eax ,0x0000FFFF mov cx ,0x400 mul cx shl edx ,16 or edx ,eax add edx ,0x100000 jmp .mem_get_ok .mem_get_ok: mov [total_mem_bytes],edx jmp .print_int .error_hlt: jmp $ .print_int: mov sp , LOADER_BASE_ADDR mov bp , loadermsg mov cx , 5 mov ax , 0x1301 mov bx , 0x001f mov dx , 0x1800 int 0x10 in al ,0x92 or al ,0000_0010B out 0x92 ,al lgdt [gdt_ptr] mov eax , cr0 or eax , 0x00000001 mov cr0 , eax jmp dword SELECTOR_CODE:p_mode_start [bits 32 ] p_mode_start: mov ax , SELECTOR_DATA mov ds , ax mov es , ax mov ss , ax mov esp ,LOADER_STACK_TOP mov eax ,KERNEL_START_SECTOR mov ebx ,KERNEL_BIN_BASE_ADDR mov ecx ,200 call rd_disk_m_32 call setup_page sgdt [gdt_ptr] mov ebx ,[gdt_ptr +2 ] or dword [ebx + 0x18 +4 ],0xc0000000 movzx eax , word [gdt_ptr + 0 ] mov ebx , dword [gdt_ptr + 2 ] add ebx , 0xc0000000 mov [gdt_ptr + 2 ], ebx add esp ,0xc0000000 mov eax , PAGE_DIR_TABLE_POS mov cr3 , eax mov eax , cr0 or eax , 0x80000000 mov cr0 , eax lgdt [gdt_ptr] jmp dword SELECTOR_CODE:enter_kernel enter_kernel: call kernel_init mov esp ,0xc009f000 mov ax , SELECTOR_VIDEO mov gs , ax mov byte [gs :160 ], 'M' jmp KERNEL_ENTRY_POINT setup_page: mov ecx ,4096 mov esi ,0 mov ax , SELECTOR_VIDEO mov gs , ax mov byte [gs :160 ], 'M' .clear_page_dir: mov byte [PAGE_DIR_TABLE_POS + esi ],0 inc esi loop .clear_page_dir .creat_pde: mov eax ,PAGE_DIR_TABLE_POS add eax ,0x1000 mov ebx ,eax or eax ,PG_US_U | PG_RW_W | PG_P mov [PAGE_DIR_TABLE_POS + 0X0 ],eax mov [PAGE_DIR_TABLE_POS + 0xc00 ], eax sub eax ,0x1000 mov [PAGE_DIR_TABLE_POS+4092 ],eax mov ecx ,256 mov esi , 0 mov edx ,PG_US_U | PG_RW_W |PG_P .creat_pte: mov [ebx +esi *4 ],edx add edx ,4096 inc esi loop .creat_pte mov eax , PAGE_DIR_TABLE_POS add eax , 0x2000 or eax , PG_US_U | PG_RW_W | PG_P mov ebx , PAGE_DIR_TABLE_POS mov ecx , 254 mov esi , 769 .create_kernel_pde: mov [ebx +esi *4 ],eax inc esi add eax , 0x1000 loop .create_kernel_pde ret kernel_init: xor eax , eax xor ebx , ebx xor ecx , ecx xor edx , edx mov dx ,[KERNEL_BIN_BASE_ADDR +42 ] mov ebx ,[KERNEL_BIN_BASE_ADDR +28 ] add ebx ,KERNEL_BIN_BASE_ADDR mov cx ,[KERNEL_BIN_BASE_ADDR +44 ] .each_segment: cmp byte [ebx + 0 ],PT_NULL je .PTNULL push dword [ebx + 16 ] mov eax , [ebx + 4 ] add eax , KERNEL_BIN_BASE_ADDR push eax push dword [ebx + 8 ] call mem_cpy add esp ,12 .PTNULL: add ebx ,edx loop .each_segment ret mem_cpy: cld push ebp mov ebp ,esp push ecx mov edi , [ebp + 8 ] mov esi , [ebp + 12 ] mov ecx , [ebp + 16 ] rep movsb pop ecx pop ebp ret rd_disk_m_32: pushad push ecx push eax mov dx , 0x1f2 mov al , cl out dx , al pop eax mov dx , 0x1f3 out dx , al mov cl , 8 shr eax , cl mov dx , 0x1f4 out dx , al shr eax , cl mov dx , 0x1f5 out dx , al shr eax , cl and al , 0x0f or al , 0xe0 mov dx , 0x1f6 out dx , al mov dx , 0x1f7 mov al , 0x20 out dx , al .not_ready: nop in al , dx and al , 0x88 cmp al , 0x08 jnz .not_ready pop ecx mov dx , 0x1f0 .read_sector: push ecx mov ecx , 256 .read_word: in ax , dx mov [ebx ], ax add ebx , 2 loop .read_word pop ecx loop .read_sector popad ret
最后就是验证是否成功执行loader,首先我将之前的字符V更改成了M,大家可以在屏幕上看到,其次是如何观察是否进入了main函数 这里验证方式有很多,比如说往一个特定地址写入一个特殊值,然后我们通过bochs调试读取,或者通过操作显存来显示字符,我这里选择后者,所以稍微修改一下main函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int main () { volatile char *video = (volatile char *)0xB8000 ; char *str = "Mouse OS" ; unsigned char attribute = 0x07 ; int i; for (i = 0 ; str[i] != '\0' ; i++) { video[i * 2 ] = str[i]; video[i * 2 + 1 ] = attribute; } while (1 ); return 0 ; }
然后重新编译写入即可,然后就可以在屏幕的第一行看见字符串”Mouse OS”
1 2 sh loader_mbr_start.sh sh kernel/kernel_start.sh
加载内核就到此结束,下面来看看保护模式下最闪亮的内容—特权
C. 特权级的深入浅出 CPU 既是大脑,又是警察,它负责维护计算机内的安全。它将程序拥有的权利分为 4 个等级,这就是保护模式下特权级的由来。 特权级按照权力从大到小分为 0、1、2、3 级,没错,数字越小,权力越大,0 级特权能力最大,3 级特权能力最小。0 级特权是我们操作系统内核所在的特权级 ,计算机在启动之初就以 0 级特权运行
C.1 TSS 简介 TSS,即 Task State Segment,意为任务状态段,它是处理器在硬件上原生支持多任务的一种实现方式,TSS 是一种数据结构,它用于存储任务的环境
TSS 是硬件支持的系统数据结构,它和 GDT 等一样,由软件填写其内容,由硬件使用。GDT 也要加载到寄存器 GDTR 中才能被处理器找到,TSS 也是一样,它是由 TR(Task Register)寄存器加载的,每次处理器执行不同任务时,将 TR 寄存器加载不同任务的 TSS 就成了。至于怎么加载以及相关工作原理,目前咱们用不到,还是放在后面说比较合适。
正是由于处理器提供了硬件方面的框架,所以很多工作都是“自动”完成的,虽然操作系统看上去是底层的技术,但其实也属于“应用型”开发。
C.2 CPL 和 DPL 简介
前情提要,x86 访问内存的机制是“段基址:偏移地址”,无论是实模式,还是保护模式,都要遵循此方式,在实模式下,段基址直接写在段寄存器中,而在保护模式下,段寄存器中的不再是段基址,而是段选择子,通过该选择子从 GDT 或 LDT 中找到相应的段描述符,从该描述符中获取段的起始地址。
代码段描述符中的 DPL,便是当前 CPU 所处的特权级,这个特权级称为当前特权级,即 CPL(Current Privilege Level),它表示处理器正在执行的代码的特权级别。除一致性代码段外,转移后的目标代码据段的 DPL 是将来处理器的当前特权级 CPL。
总之,代码是资源的请求者,代码段寄存器 CS 所指向的是处理器中当前运行的指令,所以代码段寄存器 CS 中选择子的 RPL 位称为当前特权级 CPL
处理器的当前特权级 CPL 放在 CS.RPL 中
DPL,即 Descriptor Privilege Level,描述符特权级,这下您清楚为什么 DPL 字段在段描述符中占 2位的原因了吧,两位能表示 4 个组合,00b、01b、10b、11b,所有特权级都齐了
/略……
C.3 ps: 略一下….等我具体理解了在补充这一部分 这里书中还介绍了调用门 ,RPL 的前世今生 以及IO 特权级 ,这里还是主要记录与代码强相关的内容,所以暂且少写一点,详情可以翻阅书
D. 完善内核–打印功能 之前终于完成内核,但是似乎还是太简陋了,只是实现了打印字符串,所以这里开始,完善内核
D.1 函数调用约定简介 因为我们需要汇编和C语言结合编程,所以如果互相调用了怎么办,这里主要了解一下函数调用规约
这里还是通过书中的例子来帮助理解:
1 2 3 4 5 6 7 8 9 10 11 12 int fun1 (int a,int b) { return a+b; } int main () { int a =1 ; int b =2 ; int c = fun1(1 ,2 ) return 0 ; }
上面就是一个我们常见的用法,但是计算机有事如何来确认参数1,2是在哪嘞,这是一个参数存储问题
首先,每个进程都有自己的栈,这就是每个内存自己的专用内存空间
其次,保存参数的内存地址不用再花精力维护,已经有栈机制来维护地址变化了,参数在栈中的位置可以通过栈顶的偏移量来得到
参数存储的问题解决了,我们决定在进程自己的栈空间中保存参数,那调用函数后谁负责回收所占用的栈空间 ,同时如果参数很多的时候,我们又该以什么顺序传递
在c语言中,我们不用考虑这个问题,因为编译器默默承受了这些事情,我们只用负责使用
而在汇编中,我们写一个函数,再调用一下,自己写的当然可以知道顺序,比如我们之前的mem_cpy函数,我们只用根据自己设置好的顺序,来按顺序调用即可:
1 2 3 mov edi , [ebp + 8 ] mov esi , [ebp + 12 ] mov ecx , [ebp + 16 ]
但是假设函数并不是你自己写的,那么我们就需要双方提前商量好传入参数的顺序和由谁来负责清理栈空间在高级语言中,这两个问题是通过调用约定来解决的,调用约定就是调用方和被调用方就以上问题达成一致解决方案的约定,双方按照这种约定合作就不会发生问题
因为C 语言遵循的调用约定是 cdecl,所以我们自然要遵守 cdecl 约定 ,下面主要介绍一下cdecl约定:
cdecl 调用约定最大的亮点是它允许函数中参数的数量不固定的 ,我们熟识的 printf 函数,它能够支持变长参数,就是利用此 cdecl 调用约定的性质设计出来的,它的原理是利用字符串参数 format 中的’%’来匹配栈中的参数,以后咱们在动手实现 printf 函数时会体验到这一优势
用之前的int fun1(int a,int b)函数来举个例子
1 2 3 4 5 6 int fun1 (int a,int b) { return a+b; } int c = fun1(1 ,2 )
主调用者
1 2 3 4 5 push 2 push 1 call fun1 add esp ,8
被调用者
1 2 3 4 5 6 7 8 9 10 push ebp mov ebp ,esp mov eax ,[ebp +0x8 ] add eax ,[ebp +0xc ] mov esp ,ebp pop ebp ret
ok呀,大概就这样就实现了双方的约定,下面来看看如何混合编程
D.2 函汇编语言和 C 语言混合编程 开门见山,汇编语言和 C 语言混合编程可分为两大类。
单独的汇编代码文件与单独的 C 语言文件分别编译成目标文件后,一起链接成可执行程序。
在 C 语言中嵌入汇编代码,直接编译生成可执行程序。
我们这里主要使用第一种,分开编程,第二种又称为内联汇编,在一些长汇编确实不适合,这个后面会专门说明
大概了解一下什么是Linux系统调用:
系统调用是 Linux 内核提供的一套子程序,它和 Windows 的动态链接库 dll 文件的功能一样,用来实现一系列在用户态不能或不易实现的功能,比如最常见的读写硬盘文件,只有操作系统有权限去访问硬件,用户程序是没有权限的,用户程序只能向操作系统寻求帮助,故系统调用是供用户程序来使用的,操作系统权利至高无上,不需要使用自己对外发布的功能接口,即系统调用
系统调用的入口只有一个,即第 0x80 号中断 为什么系统调用只有一个入口呢?中断的实现是要用到中断描述符表 ,表中很多中断项(号)是被预留的,不能强占,所以 Linux 就选了一个可用的中断号作为所有系统调用的统一入口,具体的子功能在寄存器 eax 中单独指定
BIOS 中断走的是中断向量表,所以有很多中断号给它用,而系统调用走的是中断描述符表中 的一项而已,所以只用了第 0x80 项中断
这里书中有个系统调用write 的例子,大家如果感兴趣可以去看看,我这里就不写出来了
这里说说C语言和汇编如何互相调用 ,举个例子:
1 2 3 4 5 6 7 8 9 10 extern void asm_print (char *,int ) ;void c_print (char * str) { int len = 0 ; while (str[len]) len++; asm_print(str, len); }
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 section .data str: db "asm_print says hello world!" , 0xa , 0 str_len equ $-str section .text extern c_print global _start _start: push str call c_print add esp ,4 mov eax ,1 int 0x80 global asm_print asm_print: push ebp mov ebp ,esp mov eax ,4 mov ebx , 1 mov ecx , [ebp +8 ] mov edx , [ebp +12 ] int 0x80 pop ebp ret
汇编中的__start执行,然后调用c文件的c_print函数,然后c_print函数再调用汇编的asm_print函数,然后再进行系统调用(0x80中断的4功能),然后输出到屏幕上
在汇编代码中导出符号供外部引用是用的关键字 global,引用外部文件的符号是用的关键字extern
在 C 代码中只要将符号定义为全局便可以被外部引用(一般情况下无需用额外关键字修饰,具体请参考 C 语言手册),引用外部符号时用 extern 声明即可
下面我们编译链接试试,验证一下结果:
1 2 3 4 5 6 7 8 gcc -m32 -c c_with_s_c.c -o c_with_s_c.o nasm -f elf c_with_s_s.S -o c_with_s_s.o ld -m elf_i386 c_with_s_c.o c_with_s_s.o -o final_program_32 chmod 777 final_program_32 ./final_program_32
最后可以看到屏幕输出asm_print says hello world!,证明成功了
D.3实现自己的打印函数
一直以来,我们在往屏幕上输出文本时,要么利用 BIOS 中断,要么利用系统调用,这些都是依赖别人的方法。咱们还用过一个稍微有点独立的方法,就是直接写显存,但这貌似又没什么技术含量。如今我们要写一个打印函数了
所以下面来和显卡打打交道
首先呀,显卡肯定不只能打印文本(mov 一些ASCII码写进去),这是因为我们在默认的文本模式下。显卡当然还有显示各种绚丽的图片,这都是显卡的功能,但目前我们还是在学习步骤,所以还是在80*25的文本模式下操作即可
下面书中介绍了显卡的具体的寄存器表,建议大家去翻书或者网上查看,这里就不列出了
这里还是直接通过例子来操作:
在此之前,为了开发方便,我们要学习linux内核一样定义一些数据类型,首先创建一个目录lib,然后再添加两个目录kernel user分别是内核和用户使用的库文件,然后添加一个stdint.h文件
然后打印函数在/lib/kernel/print.S中实现,其中put_char函数就是我们等会要实现的函数(与汇编打交道当然纯汇编比较方便),当然,为了方便查看文件路径,我在每个示例代码的最上面都会添加文件所在的路径,方便查看
下面是这个打印函数的核心处理流程 :
(1)备份寄存器现场。 (2)获取光标坐标值,光标坐标值是下一个可打印字符的位置。 (3)获取待打印的字符。 (4)判断字符是否为控制字符,若是回车符、换行符、退格符三种控制字符之一,则进入>相应的处理 流程。否则,其余字符都被粗暴地认为是可见字符,进入输出流程处理。 (5)判断是否需要滚屏。 (6)更新光标坐标值,使其指向下一个打印字符的位置。 (7)恢复寄存器现场,退出。
书中将这个实现分成了三部分,因为我都把注释写到代码里了,所以这里代码直接展示完整的吧
备份,然后获取光标位置,获取字符,判断应该怎么处理,然后跳转到相应的函数
完善对应的函数
判断滚屏,更新光标位置,恢复现场并退出
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 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 TI_GDT equ 0 RPL0 equ 0 SELECTOR_VIDEO equ (0X0003 <<3 ) + TI_GDT + RPL0 [bits 32 ] section .textglobal put_char put_char: pushad mov ax ,SELECTOR_VIDEO mov gs ,ax mov dx ,0x03d4 mov al ,0x0e out dx ,al mov dx ,0x03d5 in al ,dx mov ah ,al mov dx , 0x03d4 mov al , 0x0f out dx , al mov dx , 0x03d5 in al , dx mov bx ,ax mov ecx ,[esp +36 ] cmp cl ,0xd jz .is_carriage_return cmp cl , 0xa jz .is_line_feed cmp cl ,0x8 jz .is_backspace jmp .put_other .is_backspace: dec bx shl bx ,1 mov byte [gs :bx ],0x20 inc bx mov byte [gs :bx ], 0x07 shr bx ,1 jmp .set_cursor .put_other: shl bx ,1 mov byte [gs :bx ],cl inc bx mov byte [gs :bx ], 0x07 shr bx ,1 inc bx cmp bx , 2000 jl .set_cursor .is_line_feed: .is_carriage_return: xor dx ,dx mov ax ,bx mov si ,80 div si sub bx ,dx .is_carriage_return_end: add bx ,80 cmp bx ,2000 .is_line_feed_end: jl .set_cursor .roll_screen: cld mov ecx ,960 mov esi ,0xc00b80a0 mov edi ,0xc00b8000 rep movsd mov ebx , 3840 mov ecx , 80 .cls: mov word [gs :ebx ],0x0720 add ebx ,2 loop .cls mov bx ,1920 .set_cursor: mov dx , 0x03d4 mov al , 0x0e out dx , al mov dx , 0x03d5 mov al , bh out dx , al mov dx , 0x03d4 mov al , 0x0f out dx , al mov dx , 0x03d5 mov al , bl out dx , al .put_char_done: popad ret
然后是外部调用肯定是引用头文件更加方便,所以同样创建一个头文件
1 2 3 4 5 6 7 8 9 #ifndef __LIB_KERNEL_PRINT_H #define __LIB_KERNEL_PRINT_H #include "stdint.h" void put_char (uint8_t char_asci) ; #endif
然后就是主函数了,这里选择打印一个长字符串,注意要一个字符一个字符打印,因为我们还没实现字符串的功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include "print.h" int main () { put_char('M' ); put_char('o' ); put_char('u' ); put_char('s' ); put_char('e' ); put_char(' ' ); put_char('p' ); put_char('u' ); put_char('t' ); put_char('c' ); put_char('h' ); put_char('a' ); put_char('r' ); put_char('\r' ); put_char('\n' ); while (1 ); return 0 ; }
最后编译链接启动,这里我修改一下脚本,方便一键启动,当然后面肯定会用makefile,因为shell脚本还是不够方便
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 BASE_DIR="/home/mouse/OS_mouse/tool/bochs/mouse/kernel" LIB_DIR="/home/mouse/OS_mouse/tool/bochs/mouse/lib/kernel" cd $BASE_DIR gcc -m32 -I$LIB_DIR -c -o main.o main.c nasm -f elf -o print.o $LIB_DIR /print.S ld -m elf_i386 main.o print.o -Ttext 0xc0001500 -e main -o kernel.bin dd if =$BASE_DIR /kernel.bin of=/home/mouse/OS_mouse/tool/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
在虚拟机运行bochs之后就可以看到屏幕上多了一个字符串 Mosue putchar,那么证明我们写的代码没有问题,并且发现光标在这个字符串的后两行(因为我们输入了\r\n,所以跳转了两次回车换行),也符合我们的代码
那么下面就来实现打印字符串吧,这里直接将put_str函数添加到put_char函数之前即可,因为前面有,就不贴完整代码了:
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 [bits 32 ] section .textglobal put_str put_str: push ebx push ecx xor ecx ,ecx mov ebx ,[esp +12 ] .goon: mov cl ,[ebx ] cmp cl ,0 jz .str_over push ecx call put_char add esp ,4 inc ebx jmp .goon .str_over: pop ebx pop ecx ret
然后就是添加头文件:
1 2 3 4 void put_str (char * essage) ;
最后修改一下main.c,然后就可以执行了:
1 2 3 4 5 6 7 8 #include "print.h" int main () { put_str("Mouse_put_str\r\b" ); while (1 ); return 0 ; }
1 2 3 4 mouse@ubuntu:~/OS_mouse/tool/bochs/mouse/kernel$ sh kernel_start.sh
然后运行之后,如果再屏幕上看见字符串Mouse_put_str,并且光标在字符串这一行的末尾则现象正确(先换行,然后又退了一格)
最后一步,实现打印整数(pot_int),为最后的pirntf实现打基础,同时这里结束后也要学习什么是内联汇编(C和汇编联合编译)
下面还是给出需要添加的代码:
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 [bits 32 ] section .textput_int_buffer dq 0 global put_int put_int: pushad mov ebp , esp mov eax , [ebp +4 * 9 ] mov edx , eax mov edi , 7 mov ecx , 8 mov ebx , put_int_buffer .16based_4bits: and edx , 0x0000000F cmp edx , 9 jg .is_A2F add edx , '0' jmp .store .is_A2F: sub edx , 10 add edx , 'A' .store: mov [ebx +edi ], dl dec edi shr eax , 4 mov edx , eax loop .16based_4bits .ready_to_print: inc edi .skip_prefix_0: cmp edi , 8 je .full0 .go_on_skip: mov cl , [put_int_buffer+edi ] inc edi cmp cl , '0' je .skip_prefix_0 dec edi jmp .put_each_num .full0: mov cl , '0' .put_each_num: push ecx call put_char add esp , 4 inc edi mov cl , [put_int_buffer+edi ] cmp edi , 8 jl .put_each_num popad ret
1 2 3 4 5 6 7 8 9 10 11 #ifndef __LIB_KERNEL_PRINT_H #define __LIB_KERNEL_PRINT_H #include "stdint.h" void put_char (uint8_t char_asci) ; void put_str (char * essage) ; void put_int (uint32_t num) ; #endif
1 2 3 4 5 6 7 8 9 10 11 12 #include "print.h" int main () { put_str("Mouse_put_str\r\b" ); put_int(1 ); put_str(" " ); put_int(12 ); put_int(0x0000f ); while (1 ); return 0 ; }
然后还是执行sh脚本,屏幕上应该会有1 CF显示,那么说明整数打印也没问题啦
D.4 内联汇编简介 之前介绍过了一种汇编方法,就是 C 代码和汇编代码分别编译,最后通过链接的方式结合在 一起形成可执行文件。 另一种方式就是在 C 代码中直接嵌入汇编语言,这就是内联汇编
这里主要列出内联汇编的部分知识点,具体使用可以参考例子,或者网上查阅相关资料
D4.1 汇编语言 AT&T 语法简介 我们在大学所学习的汇编语言大多数都是 Intel 语法,Linux 内核中的汇编代码一般都 是 AT&T 语法,下面列出表格指出主要的不同点:
特性
AT&T 语法
Intel 语法
操作数顺序
opcode src, dest
opcode dest, src
寄存器前缀
% (例如 %eax)
无前缀 (例如 eax)
立即数前缀
$ (例如 $0x80)
无前缀 (例如 0x80 或 80h)
内存操作数
disp(base, index, scale)
[base + index*scale + disp]
(例如 4(%ebx))
(例如 [ebx+4])
操作码后缀
使用后缀指明大小
在内存操作数前加修饰
(例如 b-字节, w-字, l-双字)
(例如 byte ptr)
远调用/跳转
lcall, ljmp
call far, jmp far
D4.2 基本内联汇编 基本内联汇编是最简单的内联形式,格式为:asm [volatile] ("assembly code") 其中voliatile的意思是不要修改我写的汇编代码,防止编译器优化
“assembly code”是咱们所写的汇编代码,它必须位于圆括号中,而且必须用双引号引起来。这>是格式要求,只要满足了这个格式 asm [volatile] (“”),assembly code 甚至可以为空。 下面说下 assembly code 的规则。 (1)指令必须用双引号引起来,无论双引号中是一条指令或多条指令。 (2)一对双引号不能跨行,如果跨行需要在结尾用反斜杠’'转义。 (3)指令之间用分号’;’、换行符’\n’或换行符加制表符’\n’’\t’分隔。 提醒一下,即使是指令分布在多个双引号中,gcc 最终也要把它们合并到一起来处理,合并之后,指令间必须要有分隔符。所以,当指令在多个双引号中时,除最后一个双引号外,其余双引号中的代码最后一定要有分隔符,这和其他编程语言中表示代码结束的分隔符是一样的,如:
1 2 asm("movl $9,%eax;" "pushl %eax" ) 正确 asm("movl $9,%eax" "pushl %eax" ) 错误
在内联汇编中,咱们要注意操作数的顺序啦,现在是和 Intel 反着的
下面举一个完整的演示例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 char * str="hello,world\n" ;int count =0 ;void main () { asm ("pusha; \ movl $4,%eax; \ movl $1,%ebx; \ movl str,%ecx; \ movl $12,%edx; \ int $0x80; \ mov %eax,count; \ popa \ " );}
然后编译运行,便可以在中断看到打印的hello,world
1 2 gcc -m32 -o inlineASM.bin inlineASM.c ./inlineASM.bin
D4.2 拓展内联汇编 很明显,之前的基础内联汇编并没有使用c语言里面的变量,而在实际中,使用变量是不可避免的,所以这里内联汇编的格式也发生了变化:asm [volatile] (“assembly code”:output : input : clobber/modify)
和前面的基本内联汇编相比,扩展内联汇编在圆括号中变成了 4 部分,多了 output、input 和clobber/modify 三项。其中的每一部分都可以省略,甚至包括 assembly code。省略的部分要保留冒号分隔符来占位,如果省略的是后面的一个或多个连续的部分,分隔符也不用保留,比如省略了 clobber/modify,不需要保留 input 后面的冒号。
output:output 用来指定汇编代码的数据如何输出给 C 代码使用 格式: “操作数修饰符约束名”(C 变量名)input:input 用来指定 C 中数据如何输入给汇编使用 格式: “[操作数修饰符] 约束名”(C 变量名)clobber/modify :汇编代码执行后会破坏一些内存或寄存器资源,通过此项通知编译器,可能造成寄存器或内存数据的破坏,这样 gcc 就知道哪些寄存器或内存需要提前保护起来
这里还是直接举个例子,更加容易理解:分别是参数约束和内存约束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> void main () { int in_a = 1 , in_b = 2 , out_sum; asm ("addl %%ebx, %%eax" :"=a" (out_sum):"a" (in_a),"b" (in_b)); printf ("sum is %d\n" ,out_sum); in_a = 1 , in_b = 2 ; printf ("in_b is %d\n" ,in_b); asm ("movb %b0, %1;" ::"a" (in_a),"m" (in_b)); printf ("in_b now is %d\n" , in_b); }
然后还是编译输出查看结果:
1 2 gcc -m32 -o reg_constraint.bin reg_constraint.c ./reg_constraint.bin
屏幕上如果输出如下,那么就成功运行啦sum is 3in_b is 2in_b now is 1
下面来说说什么是占位符,占位符分为序号占位符和名称占位符两种 这里还是举几个例子,就不细说概念了
asm("addl %%ebx, %%eax":"=a"(out_sum):"a"(in_a),"b"(in_b)); 等价于asm("addl %2, %1":"=a"(out_sum):"a"(in_a),"b"(in_b));
其中"=a"(out_sum)序号为0,%0对应的是eax"a"(in_a)序号为1,%1对应的是eax"b"(in_b)序号为2,%2对应的是ebx
下面还有名称占位符,等用到的时候我再加解释,大家可以自行去书中了解
E. 中断 在学习中断之前,我提前来规划一下以后编译使用的Makefile,因为以后的文件肯定会慢慢增多,所以再用以前的指令或者sh脚本肯定不方便,那肯定有专门的文件来干这个事吧,那就是Makefile文件了
E.1 Makefile 这里书中是学到内存管理才介绍的,但为了编译文件方便,这里提前介绍一下,并创建我们的makefile文件
emm,至于语法和概念大家自行查找吧,因为具体内容很多地方都会讲,比如之前学习imx6u也会说明,这里列出后面会使用的文件内容,方便一会写中断等文件后编译
这里我选择分层级创建makefile文件,也就是在内核的文件夹下创建一个,然后分别在/lib/kernel和/lib/user等路径下都添加一个(如果没有,可以参考我给的路径创建一个,后期会用到),以后如果添加文件就只用在对应的目录下进行操作即可,这里大部分我都写了注释,大家可以参考参考……
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 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 .PHONY : all kernel user clean img dirsMOUSE_DIR := .. BUILD_DIR := $(MOUSE_DIR) /build BUILD_KERNEL_DIR := $(BUILD_DIR) /kernel BUILD_USER_DIR := $(BUILD_DIR) /user IMG_PATH := /home/mouse/OS_mouse/tool/bochs/hd60M.img CC := gcc ASM := nasm LD := ld CFLAGS := -m32 -fno-builtin -fno-stack-protector -ffreestanding \ -I$(MOUSE_DIR) /lib -I$(MOUSE_DIR) /lib/kernel -I$(MOUSE_DIR) /lib/user \ -I$(MOUSE_DIR) /kernel -I$(MOUSE_DIR) /device -c ASMFLAGS := -f elf LDFLAGS := -m elf_i386 -Ttext 0xc0001500 -e main include $(MOUSE_DIR) /lib/kernel/Makefileinclude $(MOUSE_DIR) /lib/user/Makefileinclude $(MOUSE_DIR) /device/Makefileinclude $(MOUSE_DIR) /lib/MakefileKERNEL_SRCS := main.c init.c interrupt.c debug.c memory.c KERNEL_ASMS := kernel.S KERNEL_SRCS := $(addprefix $(MOUSE_DIR) /kernel/,$(KERNEL_SRCS) ) KERNEL_ASMS := $(addprefix $(MOUSE_DIR) /kernel/,$(KERNEL_ASMS) ) LIB_KERNEL_SRCS := $(addprefix $(MOUSE_DIR) /lib/kernel/,$(LIB_KERNEL_SRCS) ) LIB_KERNEL_ASMS := $(addprefix $(MOUSE_DIR) /lib/kernel/,$(LIB_KERNEL_ASMS) ) LIB_USER_SRCS := $(addprefix $(MOUSE_DIR) /lib/user/,$(LIB_USER_SRCS) ) LIB_USER_ASMS := $(addprefix $(MOUSE_DIR) /lib/user/,$(LIB_USER_ASMS) ) DEVICE_SRCS := $(addprefix $(MOUSE_DIR) /device/,$(DEVICE_SRCS) ) DEVICE_ASMS := $(addprefix $(MOUSE_DIR) /device/,$(DEVICE_ASMS) ) LIB_SRCS := $(addprefix $(MOUSE_DIR) /lib/,$(LIB_SRCS) ) LIB_ASMS := $(addprefix $(MOUSE_DIR) /lib/,$(LIB_ASMS) ) ALL_C_SRCS := $(KERNEL_SRCS) $(LIB_KERNEL_SRCS) $(LIB_USER_SRCS) $(DEVICE_SRCS) $(LIB_SRCS) ALL_ASMS := $(KERNEL_ASMS) $(LIB_KERNEL_ASMS) $(LIB_USER_ASMS) $(DEVICE_ASMS) $(LIB_ASMS) OBJS_KERNEL := \ $(patsubst $(MOUSE_DIR) /%.c,$(BUILD_KERNEL_DIR) /%.o,$(filter $(MOUSE_DIR) /%.c,$(ALL_C_SRCS) ) ) \ $(patsubst $(MOUSE_DIR) /%.S,$(BUILD_KERNEL_DIR) /%.o,$(filter $(MOUSE_DIR) /%.S,$(ALL_ASMS) ) ) OBJS_USER := \ $(patsubst $(MOUSE_DIR) /lib/user/%.c,$(BUILD_USER_DIR) /%.o,$(filter $(MOUSE_DIR) /lib/user/%.c,$(ALL_C_SRCS) ) ) \ $(patsubst $(MOUSE_DIR) /lib/user/%.S,$(BUILD_USER_DIR) /%.o,$(filter $(MOUSE_DIR) /lib/user/%.S,$(ALL_ASMS) ) ) KERNEL_BIN := $(BUILD_KERNEL_DIR) /kernel.bin all: dirs kernel user dirs: @echo "Creating build directories..." @mkdir -p $(BUILD_KERNEL_DIR) $(BUILD_USER_DIR) kernel: $(KERNEL_BIN) user: $(OBJS_USER) $(KERNEL_BIN) : $(OBJS_KERNEL) $(OBJS_USER) @echo "Linking kernel object files to generate kernel.bin..." $(LD) $(LDFLAGS) -o $@ $(filter $(BUILD_KERNEL_DIR) /%.o,$^ ) $(filter $(BUILD_USER_DIR) /%.o,$^ ) @echo "Kernel linking completed!" $(BUILD_KERNEL_DIR) /%.o: $(MOUSE_DIR) /%.c | dirs @echo "Compiling C: $< " @mkdir -p $(@D) $(CC) $(CFLAGS) -o $@ $< $(BUILD_KERNEL_DIR) /%.o: $(MOUSE_DIR) /%.S | dirs @echo "Assembling: $< " @mkdir -p $(@D) $(ASM) $(ASMFLAGS) -o $@ $< $(BUILD_USER_DIR) /%.o: $(MOUSE_DIR) /lib/user/%.c | dirs @echo "Compiling user C: $< " @mkdir -p $(@D) $(CC) $(CFLAGS) -o $@ $< $(BUILD_USER_DIR) /%.o: $(MOUSE_DIR) /lib/user/%.S | dirs @echo "Assembling user S: $< " @mkdir -p $(@D) $(ASM) $(ASMFLAGS) -o $@ $< img: $(KERNEL_BIN) @echo "Writing kernel image to disk..." dd if=$(KERNEL_BIN) of=$(IMG_PATH) bs=512 count=200 seek=9 conv=notrunc @echo "Kernel image writing completed!" clean: @echo "Cleaning build files..." -rm -f $(BUILD_KERNEL_DIR) /*.o $(BUILD_KERNEL_DIR) /kernel.bin -rm -f $(BUILD_USER_DIR) /*.o @echo "Cleanup completed!"
1 2 3 4 5 6 7 LIB_USER_DIR := lib/user LIB_USER_SRCS := LIB_USER_ASMS := LIB_USER_INC := $(LIB_USER_DIR)
1 2 3 4 5 6 7 LIB_KERNEL_DIR := lib/kernel LIB_KERNEL_ASMS := print.S LIB_KERNEL_SRCS := LIB_KERNEL_INC := $(LIB_KERNEL_DIR)
1 2 3 4 5 6 7 8 9 10 DEVICE_ASMS := DEVICE_ASMS += DEVICE_SRCS += DEVICE_INC := $(DEVICE_DIR)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 LIB_ASMS := LIB_SRCS := LIB_SRCS += LIB_INC := $(LIB_DIR)
这样以后如果要添加编译的内核库文件,就只用在对应的子目录的 Makefile 里面添加文件即可(注意在主文件夹添加文件也要修改Makefile,因为没有使用自动扫描) 同时这里的编译为相对路径,只有写入磁盘是绝对路径,这样即便你把文件发送给别人,那么也可以直接使用 为了目录简洁,这里还引入一个新的目录build,将以后编译生成的文件都放到这里面
Makefile就先说到这里,下面继续学习中断
E.2 中断分类 这里的中断和之前stm32,或者说linux中断都是差不多的,这里还是简要说明,同时,这里全部都是以单核CPU为例子
INTR(INTeRrupt):可屏蔽中断 ,CPU收到信号后可以缓慢处理,甚至不处理,因为它并不会影响CPU运行,同时也可以通过eflags寄存器的 IF 位 将这些中断全部屏蔽,对于这些每个中断源都可以获得一个中断向量号(有限的)
NMI(Non Maskable Interrupt):不可屏蔽中断 ,说明发生了致命的错误,比如内存读写错误,电源掉电,总线奇偶校验失误等等,并且只会被分配一个中断向量号,因为其实软件工程师也解决不了这些问题
同时,这里简要说明一下中断的上半部和下半部,其实可以理解成上半部是表示得到中断信息(获得标志位 )了,但是如果要处理的话可能会花费一点时间,这个时候如果有其它重要的事情也要处理的话,可能会影响别人的进程,所以这里将具体处理的部分可以放到下半部(执行 )
软中断 ,就是由软件主动发起的中断,由于该中断是软件运行中主动发起的,所以它是主观上的,并不是客观上的某种内部错误
异常 ,是指令执行期间 CPU 内部产生的错误引起的,由于是运行时错误,所以它不受标志寄存器 eflags 中的 IF 位影响,无法向用户隐瞒,只要中断关系到“正常”运行,就不受 IF 位影响,这里依照错误的轻重程度,分为三种:Fault(故障),Trap(陷阱),Abort(终止)
下面是发起中断的相关指令:除了第一个指令外,都可以称作异常,因为它们既具备软中断的“主动”行为,又具备异常的“错误”结果
int 8 位立即数,进行系统调用,可以表示256(8位)种中断;
int3,注意这里面没有空格,这里指的是调试断点指令,其所触发的中断向量号是3
into,中断溢出指令,所触发的中断向量号是 4,能否引发 4 号中断是要看 eflags 标志寄存器中的 OF 位是否为 1,如果是 1 才会引发中断,否则该指令悄悄地什么都不做
bound,检查数组索引越界指令,触发 5 号中断,该指令格式是bound 16/32 位寄存器,16/32 位内存,目的操作数是用寄存器来存储的,其内容是待检测的数组下标值。源操作数是内存,其内容是数组下标的下边界和上边界。当执行 bound 指令时,若下标处于数组索引的范围之外,则会触发 5 号中断
ud2,未定义指令,这会触发第 6 号中断,该指令表示指令无效,CPU 无法识别。主动使用它发起中断,常用于软件测试中,无实际用途
中断机制的本质是来了一个中断信号后,调用相应的中断处理程序。所以,CPU 不管有多少种类型的中断,为了统一中断管理,把来自外部设备、内部指令的各种中断类型统统归结为一种管理方式,即为每个中断信号分配一个整数,用此整数作为中断的 ID,而这个整数就是所谓的中断向量,然后用此 ID 作为中断描述符表中的索引,这样就能找到对应的表项,进而从中找到对应的中断处理程序。中断向量的作用和选择子类似,它们都用来在描述符表中索引一个描述符,只不过选择子用于在 GDT或 LDT 中检索段描述符,而中断向量专用于中断描述符表,其中没有 RPL 字段
异常和不可屏蔽中断的中断向量号是由 CPU 自动提供 的,而来自外部设备的可屏蔽中断号是由中断代理提供的(咱们这里的中断代理是 8259A) ,软中断是由软件提供 的
E.3 中断描述符表 中断描述符表(Interrupt Descriptor Table,IDT)是保护模式下用于存储中断处理程序入口的表 实模式下用于存储中断处理程序入口的表叫中断向量表(Interrupt Vector Table,IVT),stm32也是中断向量表
同时,中断描述符表里面不仅仅有中断描述符,还有门描述符:
任务门 :任务门和任务状态段(Task Status Segment,TSS)是 Intel 处理器在硬件一级提供的任务切换机制,所以任务门需要和 TSS 配合在一起使用,在任务门中记录的是 TSS 选择子,偏移量未使用,但是大多数操作系统都未使用TSS,所以这里不多介绍
中断门 :中断门包含了中断处理程序所在段的段选择子和段内偏移地址 。当通过此方式进入中断后,标志寄存器 eflags 中的 IF 位自动置 0 ,也就是在进入中断后,自动把中断关闭,避免中断嵌套 。Linux 就是利用中断门实现的系统调用,就是那个著名的 int 0x80。
陷阱门 :陷阱门和中断门非常相似,区别是由陷阱门进入中断后,标志寄存器 eflags 中的 IF 位不会自动置 0 。
调用门 :调用门是提供给用户进程进入特权 0 级的方式 ,它不能用int 指令调用,只能用 call 和 jmp 指令
对比中断向量表,中断描述符表有两个区别: (1)中断描述符表地址不限制,在哪里都可以,而中断向量表固定特殊地址(比如Flash存储器的起始位置) (2)中断描述符表中的每个描述符用 8 字节描述
E.4 中断处理过程及保护 保护也就是特权级检查,完整的中断过程分为 CPU 外和 CPU 内两部分
CPU 外:外部设备的中断由中断代理芯片接收,处理后将该中断的中断向量号发送到 CPU
CPU 内:CPU 执行该中断向量号对应的中断处理程序
CPU 外这部分的处理过程,在后面咱们讲述中断代理芯片 Intel 8259A 时大家就会了解,这部分内容属于硬件范畴
/….略
E.5 可编程中断控制器 8259A 本节将介绍可屏蔽中断的代理—可编程中断控制器 8259A
8259A 用于管理和控制可屏蔽中断,它表现在屏蔽外设中断,对它们实行优先级判决,向 CPU 提供中断向量号等功能。而它称为可编程的原因,就是可以通过编程的方式来设置以上的功能
Intel 处理器共支持 256 个中断,但 8259A 只可以管理 8 个中断,不知道 Intel 这是要闹哪样,所以它为了多支持一些中断设备,提供了另一个解决方案,将多个 8259A 组合,官方术语就是级联。有了级联这种组合后,每一个 8259A 就被称为 1 片。若采用级联方式,即多片 8259A 芯片串连在一起,最多可级联 9 个,也就是最多支持 64 个中断。n 片 8259A 通过级联可支持 7n+1 个中断源,级联时只能有一片 8259A为主片 master,其余的均为从片 slave。来自从片的中断只能传递给主片,再由主片向上传递给 CPU,也就是说只有主片才会向 CPU 发送 INT 中断信号
我们主要要干两件事,也就是构建中断处理框架的流程 (1) 构造好IDT(中断描述符表) (2) 提供中断向量号
还是实例才能更好的记住,下面来介绍如何编程:
对它的编程就是对它进行初始化,设置主片与从片的级联方式,指定起始中断向量号以及设置各种工作模式
其实,在开机之后的实模式下,BIOS 也对它光顾过,8259A 的 IRQ0~7已经被 BIOS 分配了 0x8~0xf 的中断向量号。而在保护模式下,中断向量号已经为 0x8~0xf 的范围已经被 CPU 占了,分配给了各种异常,咱们还得重新为 8259A 芯片上的 IRQ 接口们分配中断向量号 中断向量号是逻辑上的东西,它在物理上是 8259A 上的 IRQ 接口号。8259A 上 IRQ 号的排列顺序是固定的,但其对应的中断向量号是不固定的,这其实是一种由硬件到软件的映射,通过设置 8259A,可以将 IRQ 接口映射到不同的中断向量号
其中具体的控制方法请大家移动到书中看,书中介绍的非常详细
E.6 编写中断处理程序 这里实现第一个中断处理程序,写一个简陋的时钟中断,再逐渐丰富它
Intel 8259A 芯片位于主板上的南桥芯片中,咱们不需要像网卡、硬盘那样单独安装才能用,也不需要为它的各个 IR 引脚指定连接的外部设备,这一切都安排好啦,比如主片 IR0 引脚上就是时钟中断,这已经由内部电路实现了,咱们只需要直接操作 8259A 就行,不用担心这些外部设备是否连接上了 8259A
下面是这次的中断启动流程框架:
int_all(用来初始化所有的设备及数据结构) –> idt_init(它用来初始化中断相关的内容) –>pic_init(初始化可编程中断控制器 8259A) && idt_desc_init(初始化中断描述符表 IDT) –> 加载IDT(现在便准备好了打开中断的条件)
下面来开始写代码吧(文件路径为:/home/mouse/OS_mouse/tool/bochs/mouse/kernel/kernel.S),这里面还使用了新的内容:宏,具体内容在代码中我会打出注释 这里我直接给出所有编写的文件,大家可以参考书中内容来阅读,我写了一小部分在注释中
1 2 3 4 5 6 7 8 9 10 11 #include "print.h" #include "init.h" int main () { put_str("I am kernel!\n" ); init_all(); asm volatile ("sti" ) ; while (1 ); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 #include "init.h" #include "print.h" #include "interrupt.h" void init_all () { put_str("init_all\n" ); idt_init(); }
1 2 3 4 5 6 7 8 #ifndef __INIT_H #define __INIT_H void init_all () ; #endif
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 #include "interrupt.h" #include "stdint.h" #include "global.h" #include "print.h" #include "io.h" #define IDT_DESC_CNT 0x21 #define PIC_M_CTRL 0x20 #define PIC_M_DATA 0x21 #define PIC_S_CTRL 0xa0 #define PIC_S_DATA 0xa1 struct gate_desc { uint16_t func_offset_low_word; uint16_t selector; uint8_t dcount; uint8_t attribute; uint16_t func_offset_high_word; }; static void make_idt_desc (struct gate_desc* p_gdesc,uint8_t attr, intr_handler function) ;static struct gate_desc idt [IDT_DESC_CNT ]; extern intr_handler intr_entry_table[IDT_DESC_CNT]; static void pic_init (void ) { outb (PIC_M_CTRL, 0x11 ); outb (PIC_M_DATA, 0x20 ); outb (PIC_M_DATA, 0x04 ); outb (PIC_M_DATA, 0x01 ); outb (PIC_S_CTRL, 0x11 ); outb (PIC_S_DATA, 0x28 ); outb (PIC_S_DATA, 0x02 ); outb (PIC_S_DATA, 0x01 ); outb (PIC_M_DATA, 0xfe ); outb (PIC_S_DATA, 0xff ); put_str(" pic_init done\n" ); } static void make_idt_desc (struct gate_desc* p_gdesc,uint8_t attr, intr_handler function) { p_gdesc->func_offset_low_word = (uint32_t )function & 0x0000FFFF ; p_gdesc->selector = SELECTOR_K_CODE; p_gdesc->dcount = 0 ; p_gdesc->attribute = attr; p_gdesc->func_offset_high_word = ((uint32_t )function & 0Xffff0000 ) >>16 ; } static void idt_desc_init (void ) { int i; for (i=0 ;i<IDT_DESC_CNT;i++) { make_idt_desc(&idt[i],IDT_DESC_ATTR_DPL0,intr_entry_table[i]); } put_str("idt_desc_init done\n" ); } void idt_init () { put_str("idt_init start\n" ); idt_desc_init(); pic_init(); uint64_t idt_operand = ((sizeof (idt) - 1 ) | ((uint64_t )((uint32_t )idt << 16 ))); asm volatile ("lidt %0" ::"" (idt_operand)) ; put_str("idt_init done\n" ); }
1 2 3 4 5 6 7 8 9 #ifndef __INTERRUPUT_H #define __INTERRUPUT_H typedef void * intr_handler;void idt_init () ;#endif
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 #ifndef __KERNEL_GLOBAL_H #define __KERNEL_GLOBAL_H #include "stdint.h" #define RPL0 1 #define RPL1 1 #define RPL2 2 #define RPL3 3 #define TI_GDT 0 #define TI_LDT 1 #define SELECTOR_K_CODE ((1 << 3 ) + (TI_GDT << 2) + RPL0) #define SELECTOR_K_DATA ((2 << 3 ) + (TI_GDT << 2) + RPL0) #define SELECTOR_K_STACK SELECTOR_K_DATA #define SELECTOR_K_GS ((3 << 3 ) + (TI_GDT << 2) + RPL0) #define IDT_DESC_P 1 #define IDT_DESC_DPL0 0 #define IDT_DESC_DPL3 3 #define IDT_DESC_32_TYPE 0xE #define IDT_DESC_16_TYPE 0x6 #define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE) #define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE) #endif
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 [bits 32 ] %define ERROR_CODE nop %define ZERO push 0 extern put_str section .dataintr_str db "interrupt occur!" , 0xa , 0 global intr_entry_table intr_entry_table: %macro VECTOR 2 section .textintr%1 entry:: %2 push intr_str call put_str add esp ,4 mov al ,0x20 out 0xa0 ,al out 0x20 ,al add esp ,4 iret section .data dd intr%1 entry: %endmacro VECTOR 0x00 ,ZERO VECTOR 0x01 ,ZERO VECTOR 0x02 ,ZERO VECTOR 0x03 ,ZERO VECTOR 0x04 ,ZERO VECTOR 0x05 ,ZERO VECTOR 0x06 ,ZERO VECTOR 0x07 ,ZERO VECTOR 0x08 ,ERROR_CODE VECTOR 0x09 ,ZERO VECTOR 0x0a ,ERROR_CODE VECTOR 0x0b ,ERROR_CODE VECTOR 0x0c ,ZERO VECTOR 0x0d ,ERROR_CODE VECTOR 0x0e ,ERROR_CODE VECTOR 0x0f ,ZERO VECTOR 0x10 ,ZERO VECTOR 0x11 ,ERROR_CODE VECTOR 0x12 ,ZERO VECTOR 0x13 ,ZERO VECTOR 0x14 ,ZERO VECTOR 0x15 ,ZERO VECTOR 0x16 ,ZERO VECTOR 0x17 ,ZERO VECTOR 0x18 ,ERROR_CODE VECTOR 0x19 ,ZERO VECTOR 0x1a ,ERROR_CODE VECTOR 0x1b ,ERROR_CODE VECTOR 0x1c ,ZERO VECTOR 0x1d ,ERROR_CODE VECTOR 0x1e ,ERROR_CODE VECTOR 0x1f ,ZERO VECTOR 0x20 ,ZERO
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 #ifndef __LIB_IO_H #define __LIB_IO_H #include "stdint.h" static inline void outb (uint16_t port ,uint8_t data) { asm volatile ("outb %b0 ,%w1" :: "a" (data),"Nd" (port)) ; } static inline void outsw (uint16_t port, const void * addr, uint32_t word_cnt) { asm volatile ("cld; rep outsw" : "+S" (addr), "+c" (word_cnt) : "d" (port)) ; } static inline uint8_t inb (uint16_t port) { uint8_t data; asm volatile ("inb %w1, %b0" : "=a" (data) : "Nd" (port)) ; return data; } static inline void insw (uint16_t port, void * addr, uint32_t word_cnt) { asm volatile ("cld; rep insw" : "+D" (addr), "+c" (word_cnt) : "d" (port) : "memory" ) ; } #endif
大家可以通过main函数入手,一步一步看函数引用,这里大体介绍一下:
/mouse/kernel:kernel.S文件里面添加了中断处理程序global.h文件里面添加了常用的宏(IDT描述符相关的的宏)interrupt.c文件里面添加了:初始化中断控制器8259A/中断描述符等interrupt.h文件里面原始中断初始化函数的定义init.c文件里面定义了主函数调用的初始化函数init.h文件里面添加定义
/mouse/lib/kernelio.h文件里面添加了对IO口的操作,通过汇编实现(为了快速调用展开)
最后通过前面写的Makefile文件来运行,会发现屏幕上移一直打印`nterrupt occur!`` 这是因为我们开启了时钟产生的中断,然后进入kernel.S里面的汇编函数,打印对应内容 当然这一节主要是学习中断是如何构建的,如何编程中断控制器8259A,大家可以仔细阅读书中的解释,那里写的十分详细
E.7 改进中断处理程序 现在可以看到我们的中断处理函数是在kernel.S文件中,并且使用汇编语言,这样子的函数肯定不适合我们后期使用修改,所以以后我们用C语言书写中断处理函数,然后再汇编中调用即可 方案如下:在汇编版本的 intrXXentry 中调用 C 语言版本的中断处理函数。这样汇编中的 intrXXentry 就如其名一样,真正变成了中断的 entry,即入口,然后在C语言中建立一个中断处理数组idt_table,数组元素是C语言中断处理函数的地址,供汇编代码中的intrXXentry调用
然后这里要考虑在汇编中要如何方便的调用C语言的中断处理函数的地址 因为我们是模拟32位的操作系统,所以C语言里面的数组也是32位的,也就是4个字节,那么相对数组首地址(idt_table)的偏移就是4*n(第n个元素),比如数组中的第二个地址就可以表示为idt_table+1*4,注意这里的第二个的n是1,而中断向量号也是从0开始的,所以就可以表示为idt_table+中断向量号*4
下面主要修改两个文件interrupt.c 和 kernel.S,为了方便查看/复制,这里还是选择给出完整代码 同时,如果大家使用的是vscode可以选择添加一个.vscode文件夹,然后写一个配置文件c_cpp_properties.json,这样就可以包含其它头文件,方便代码编写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "configurations" : [ { "name" : "Linux" , "includePath" : [ "${workspaceFolder}/**" , "/home/mouse/OS_mouse/tool/bochs/mouse/lib/kernel" , "/home/mouse/OS_mouse/tool/bochs/mouse/lib/user" , "/home/mouse/OS_mouse/tool/bochs/mouse/lib" , "/home/mouse/OS_mouse/tool/bochs/mouse/kernel" ] , "defines" : [ ] , "compilerPath" : "/usr/bin/gcc" , "cStandard" : "c11" , "cppStandard" : "c++17" , "intelliSenseMode" : "linux-gcc-x64" } ] , "version" : 4 }
interrupt.c: 点击查看更多
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 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 #include "interrupt.h" #include "stdint.h" #include "global.h" #include "print.h" #include "io.h" #define IDT_DESC_CNT 0x33 #define PIC_M_CTRL 0x20 #define PIC_M_DATA 0x21 #define PIC_S_CTRL 0xa0 #define PIC_S_DATA 0xa1 struct gate_desc { uint16_t func_offset_low_word; uint16_t selector; uint8_t dcount; uint8_t attribute; uint16_t func_offset_high_word; }; static void make_idt_desc (struct gate_desc* p_gdesc,uint8_t attr, intr_handler function) ;static struct gate_desc idt [IDT_DESC_CNT ]; extern intr_handler intr_entry_table[IDT_DESC_CNT]; static void exception_init (void ) ; static void general_intr_handler (uint8_t vec_nr) ; const char * intr_name[IDT_DESC_CNT]; intr_handler idt_table[IDT_DESC_CNT]; static void pic_init (void ) { outb (PIC_M_CTRL, 0x11 ); outb (PIC_M_DATA, 0x20 ); outb (PIC_M_DATA, 0x04 ); outb (PIC_M_DATA, 0x01 ); outb (PIC_S_CTRL, 0x11 ); outb (PIC_S_DATA, 0x28 ); outb (PIC_S_DATA, 0x02 ); outb (PIC_S_DATA, 0x01 ); outb (PIC_M_DATA, 0xfe ); outb (PIC_S_DATA, 0xff ); put_str(" pic_init done\n" ); } static void make_idt_desc (struct gate_desc* p_gdesc,uint8_t attr, intr_handler function) { p_gdesc->func_offset_low_word = (uint32_t )function & 0x0000FFFF ; p_gdesc->selector = SELECTOR_K_CODE; p_gdesc->dcount = 0 ; p_gdesc->attribute = attr; p_gdesc->func_offset_high_word = ((uint32_t )function & 0Xffff0000 ) >>16 ; } static void idt_desc_init (void ) { int i; for (i=0 ;i<IDT_DESC_CNT;i++) { make_idt_desc(&idt[i],IDT_DESC_ATTR_DPL0,intr_entry_table[i]); } put_str("idt_desc_init done\n" ); } static void general_intr_handler (uint8_t vec_nr) { if (vec_nr == 0x27 || vec_nr == 0x2f ) { return ; } put_str("int vector : 0x" ); put_int(vec_nr); put_str("\n" ); } static void page_fault_handler (uint8_t vec_nr) { if (vec_nr == 0x27 || vec_nr == 0x2f ) { return ; } put_str("int vector : 0x" ); put_int(vec_nr); put_str("\n" ); uint32_t faulting_address; asm volatile ("mov %%cr2, %0" : "=r" (faulting_address)) ; put_str("Page fault at address: 0x" ); put_int(faulting_address); put_str("\n" ); } static void exception_init (void ) { int i; for (i=0 ;i<IDT_DESC_CNT;i++) { idt_table[i] = general_intr_handler; intr_name[i] = "unknown" ; } idt_table[14 ] = page_fault_handler; intr_name[0 ] = "#DE Divide Error" ; intr_name[1 ] = "#DB Debug Exception" ; intr_name[2 ] = "NMI Interrupt" ; intr_name[3 ] = "#BP Breakpoint Exception" ; intr_name[4 ] = "#OF Overflow Exception" ; intr_name[5 ] = "#BR BOUND Range Exceeded Exception" ; intr_name[6 ] = "#UD Invalid Opcode Exception" ; intr_name[7 ] = "#NM Device Not Available Exception" ; intr_name[8 ] = "#DF Double Fault Exception" ; intr_name[9 ] = "Coprocessor Segment Overrun" ; intr_name[10 ] = "#TS Invalid TSS Exception" ; intr_name[11 ] = "#NP Segment Not Present" ; intr_name[12 ] = "#SS Stack Fault Exception" ; intr_name[13 ] = "#GP General Protection Exception" ; intr_name[14 ] = "#PF Page-Fault Exception" ; intr_name[16 ] = "#MF x87 FPU Floating-Point Error" ; intr_name[17 ] = "#AC Alignment Check Exception" ; intr_name[18 ] = "#MC Machine-Check Exception" ; intr_name[19 ] = "#XF SIMD Floating-Point Exception" ; } void idt_init () { put_str("idt_init start\n" ); idt_desc_init(); exception_init(); pic_init(); uint64_t idt_operand = ((sizeof (idt) - 1 ) | ((uint64_t )((uint32_t )idt << 16 ))); asm volatile ("lidt %0" ::"" (idt_operand)) ; put_str("idt_init done\n" ); }
kernel.S: 点击查看更多
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 94 95 96 97 98 99 100 [bits 32 ] %define ERROR_CODE nop %define ZERO push 0 extern put_str extern idt_table section .dataglobal intr_entry_table intr_entry_table: %macro VECTOR 2 section .textintr%1 entry: %2 push ds push es push fs push gs pushad mov al ,0x20 out 0xa0 ,al out 0x20 ,al push %1 call [idt_table + %1 *4 ] jmp intr_exit section .data dd intr%1 entry %endmacro section .textglobal intr_exitintr_exit: add esp ,4 popad pop gs pop fs pop es pop ds add esp ,4 iretd VECTOR 0X00 ,ZERO VECTOR 0x01 ,ZERO VECTOR 0x02 ,ZERO VECTOR 0x03 ,ZERO VECTOR 0x04 ,ZERO VECTOR 0x05 ,ZERO VECTOR 0x06 ,ZERO VECTOR 0x07 ,ZERO VECTOR 0x08 ,ERROR_CODE VECTOR 0x09 ,ZERO VECTOR 0x0a ,ERROR_CODE VECTOR 0x0b ,ERROR_CODE VECTOR 0x0c ,ZERO VECTOR 0x0d ,ERROR_CODE VECTOR 0x0e ,ERROR_CODE VECTOR 0x0f ,ZERO VECTOR 0x10 ,ZERO VECTOR 0x11 ,ERROR_CODE VECTOR 0x12 ,ZERO VECTOR 0x13 ,ZERO VECTOR 0x14 ,ZERO VECTOR 0x15 ,ZERO VECTOR 0x16 ,ZERO VECTOR 0x17 ,ZERO VECTOR 0x18 ,ERROR_CODE VECTOR 0x19 ,ZERO VECTOR 0x1a ,ERROR_CODE VECTOR 0x1b ,ERROR_CODE VECTOR 0x1c ,ZERO VECTOR 0x1d ,ERROR_CODE VECTOR 0x1e ,ERROR_CODE VECTOR 0x1f ,ZERO VECTOR 0x20 ,ZERO
这里我有遇到一个小错误,那就是第一次写的时候在kernel.S文件中没有添加VECTOR 0x00,ZERO,使得描述符表不完整,然后就会一直中断报错(页错误),也是在这个实验结果下发现的,因为这次修改之后,添加了中断号的显示,正常显示应该是0x20,但是我的显示是0XE,也就是页错误
后面书中还具体讲了调试,也就是查看这个中断的具体除法流程,包括压栈出栈,大家可以去书中阅读
E.8253可编程计数器/定时器 简介 计算机中的时钟,大致上可分为两大类:内部时钟和外部时钟
内部时钟是指处理器中内部元件,如运算器、控制器的工作时序,主要用于控制、同步内部工作过程的步调。内部时钟是由晶体振荡器产生的,简称晶振,它位于主板上,其频率经过分频之后就是主板的外频,处理器和南北桥之间的通信就基于外频,通常是ns级别的,内部定时是无法改变的
外部时钟是指处理器与外部设备或外部设备之间通信时采用的一种时序,比如 IO 接口和处理器之间在 A/D 转换时的工作时序、两个串口设备之间进行数据传输时也要事先同步时钟等。外部设备的速度对于处理器来说就很慢了,所以其时钟的时间单位粒度较大,一般是毫秒(ms)级或秒(s)级的
对于外部定时,我们有两种实现方式:
int cycle_cnt = 90000;while(cycle_cnt-- > 0);
通过执行空循环达成一定的延时,但这样会浪费处理器的时钟周期
这类实现定时的硬件叫做定时器,计时器的功能就是定时发信号。当到达了所计数的时间,计数器可以自动发一个输出信号,可以用该信号向处理器发出中断,和软件定时相比,硬件定时器不占用处理器,因为它是独立运行的,当然还分为可编程或不可编程定时器,下面主要介绍8253:
硬件定时器一般有两种计时的方式:倒计时、正计时,而8253是倒计时,下面主要说一下怎么让定时器定时,具体的原理比如各种工作模式大家可以移步书中阅读
(1)GATE 为高电平,即 GATE 为 1,这是由硬件来控制的。 (2)计数初值已写入计数器中的减法计数器,这是由软件 out 指令控制的。
当这两个条件具备后,计数器将在下一个时钟信号 CLK 的下降沿开始计数
下面来说说8256的初始化步骤:
往控制字寄存器端口 0x43 中写入控制字
在所指定使用的计数器端口中写入计数初值 (计数初值寄存器是 16 位,高 8 位和低 8 位可单独使用,所以初值是 8 位或 16 位皆可。若初值是 8 位,直接往计数器端口写入即可。若初值为 16 位,必须分两次来写入,先写低 8 位,再写高 8 位)
下面来实操一下,让中断引脚的频率加快一点,之前的默认频率为18.206hz,也就是1s 18次,现在我们将它修改成1s发送100次,也就是100HZ
这个属于设备的代码,我们新建一个文件夹/mouse/device,然后在这里写代码吧,在之前的Makefile部分我已经给出了对应Makefile文件的内容,注意将里面对timer.c的注释删掉
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 #include "timer.h" #include "io.h" #include "print.h" #define IRQ0_FREQUENCY 100 #define INPUT_FREQUENCY 1193180 #define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY #define CONTRER0_PORT 0x40 #define COUNTER0_NO 0 #define COUNTER_MODE 2 #define READ_WRITE_LATCH 3 #define PIT_CONTROL_PORT 0x43 static void frequency_set (uint8_t counter_port,\ uint8_t counter_no,\ uint8_t rwl,\ uint8_t counter_mode,\ uint16_t counter_value) { outb(PIT_CONTROL_PORT, (uint8_t )(counter_no << 6 | rwl << 4 | counter_mode << 1 )); outb(counter_port, (uint8_t )counter_value); outb(counter_port, (uint8_t )counter_value >> 8 ); } void timer_init () { put_str("timer_init start\n" ); frequency_set(CONTRER0_PORT, \ COUNTER0_NO, \ READ_WRITE_LATCH,\ COUNTER_MODE, \ COUNTER0_VALUE); put_str("timer_init done\n" ); }
1 2 3 4 5 6 7 8 9 #ifndef __TIMER_H #define __TIMER_H void timer_init () ;#endif
最后在init.c文件中调用timer_init即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include "init.h" #include "print.h" #include "interrupt.h" #include "timer.h" void init_all () { put_str("init_all\n" ); idt_init(); timer_init(); }
然后 make make img 运行即可,但是这里我们好像看不出来定时器中断是否真的加速了咳咳 大家可以选择在中断函数中添加一个静态变量,然后每多少次中断更改一次数值,来验证代码是否正确
F 内存管理前的小结 这次我们学习了如何内联汇编,实现了基本的打印函数,还有对中断的初始化,最后学习了怎么修改定时器,在这个章节,内联汇编语法显得很重要,同时我们还引入了Makefile文件,比起sh脚本它管理一个大的项目更加有优势,当然,这次的概念也非常多,比如说用户特权,中断描述符等等,还得找个时间好好阅读以下书中的介绍,以及查阅相关资料
这里主要遇到的麻烦可能就是中断初始化那一块的内容,因为涉及大量的操作 学无止境,下一章就是非常重要的内存管理了~
参考文献
[操作系统真象还原 (郑纲) (Z-Library)] — 大家可以自己在网上查找相关资源
留言
有问题请指出,你可以选择以下方式:
在下方评论区留言
邮箱留言