上一节,我们已经初步认识了系统开机引导过程,并编写了一个简单的MBR引导程序(仅样例,不带分区表)。下面,我们将在实模式下继续认识计算机的IO接口、硬盘操作等知识,并真正实现一个内核加载器。
(本系列所有文章均参考郑刚所著《操作系统真象还原》,真诚感谢前辈的指导。)
CPU通过I/O接口与外部设备进行通信。I/O接口作为一个“层”,CPU与硬件的交互提供兼容服务,是连接CPU与外部设备的逻辑控制部件。I/O接口具有硬件和软件部分,硬件部分负责底层的硬件连接以及为软件提供缓冲等硬件基础,高级的硬件接口允许同时与多个设备进行连接;软件部分通过程序实现数据的交换。通过IO接口控制编程,我们可以控制接口的功能,这通常由端口读写指令in/out
实现。
in ax, dx ; ax: read data, dx: port out dx, al ; dx: port, al: write data
PCIE
USB
SATA
为了给用户提供视觉上的交互,计算机需要依靠显示器来输出图像、文字信息。而显示器显示的性能、功能要求较高,CPU无法直接驱动显示器,驱动显示器的工作就交给了专门的显示适配器,也就是我们常说的显卡。
为了接收、储存CPU想要输出的信息,显卡提供了显存这一接口。我们只要操作显存,就可以让显卡驱动屏幕,输出对应信息。在实模式下,显卡相关内存分布如下:
起始 | 结束 | 大小 | 用途 |
---|---|---|---|
0xC0000 | 0xC7FFF | 32KB | 显示适配器BIOS |
0xB8000 | 0xBFFFF | 32KB | 用于显示适配器文本模式 |
0xB0000 | 0xB7FFF | 32KB | 用于黑白显示适配器 |
0xA0000 | 0xAFFFF | 64KB | 用于彩色显示适配器 |
下面,我们通过修改之前的打印欢迎信息的程序,来试验一下效果。程序使用一个循环,将数据字符串复制到显存对应位置,并设置颜色属性
SECTION MBR vstart=0x7c00 mov ax, cs mov ds, ax mov es, ax mov ss, ax mov fs, ax mov sp, 0x7c00 mov ax, 0xb800 ; auxiliary section regisster mov gs, ax ;mov ecx, 0 ; initialize ecx ; Clear Screen use 0x06 sub-func in interrupt 0x10(Video Service) ; function description: scroll up the window ; INPUTS: ; AH function_index = 0x06 ; AL = rows to scroll up, 0: clear ; BH = scroll up color property ; (CL, CH) = Top Left Corner of window ; (DL, DH) = Lower Right Corner of window ; No return mov ax, 0x0600 ; also can mov ah, 6; mov ax, 0 mov bx, 0x0700 ; BH: Light Gray on Black mov cx, 0 ; Top Left (0, 0) mov dx, 0x184f ; Lower Right: (80, 25) ((79, 14)) ; in VGA Text Module, only 80c in one line, max 25 lines int 0x10 ; print string with gpu ; mov si, message mov cx, 0 loop: mov si, message add si, cx mov al, byte [si] mov bx, cx add bx, bx mov byte [gs:bx], al add bx, 1 mov byte [gs:bx], 0x0a ; color: light green add cx, 1 cmp cl, byte [len] jb loop ; end jmp $ ; data message db "Hello, world. This is a MBR." len db $ - message ; times 510-($-$$) db 0 db 510 - ($-$$) dup (0) db 0x55, 0xaa
程序同样打印出了正确内容,并且设置了我们想要设置的颜色。不过,如果我们手动操作显存,就无法使用控制字符(回车、换行等),如果使用,则需要另外处理。
硬盘控制器的主要端口寄存器如下表所示:
IO端口号(Primary) | 读操作时功能 | 写操作时功能 |
---|---|---|
0x1f0 | Data | Data |
0x1f1 | Error | Features |
0x1f2 | Sector Count | Sector Count |
0x1f3 | LBA low | LBA low |
0x1f4 | LBA Mid | LBA mid |
0x1f5 | LBA High | LBA high |
0x1f6 | Device | device |
0x1f7 | Status | Command |
0x3f6 | Alternate Status | Device Control |
0x3f6
为 Control Block register
,其余均为Command Block register
部分主要寄存器的功能为:
PIO (Programming I/O Model)
,程序IO,读取前需要检测状态,数据源设备在一定状态下才能读取,如硬盘等按照上面的过程,我们就可以操作硬盘啦!下面,我们就将使用这个新技能,将硬盘中的Loader读取到内存里,并跳转执行。下面直接贴代码:
%include "boot.inc" SECTION MBR vstart=0x7c00 mov ax, cs mov ds, ax mov es, ax mov ss, ax mov fs, ax mov sp, 0x7c00 mov ax, 0xb800 ; auxiliary section regisster mov gs, ax mov ecx, 0 ; initialize ecx ; Clear Screen use 0x06 sub-func in interrupt 0x10(Video Service) ; function description: scroll up the window ; INPUTS: ; AH function_index = 0x06 ; AL = rows to scroll up, 0: clear ; BH = scroll up color property ; (CL, CH) = Top Left Corner of window ; (DL, DH) = Lower Right Corner of window ; No return mov ax, 0x0600 ; also can mov ah, 6; mov ax, 0 mov bx, 0x0700 ; BH: Light Gray on Black mov cx, 0 ; Top Left (0, 0) mov dx, 0x184f ; Lower Right: (80, 25) ((79, 14)) ; in VGA Text Module, only 80c in one line, max 25 lines int 0x10 mov bx, 0 mov si, message mov cx, [len_message] call my_print mov eax, LOADER_START_SECTOR ; Start Sector of Loader mov bx, LOADER_BASE_ADDR ; Loader base address mov cx, 1 ; Sector count call rd_disk_m_16 ; jmp $ ; for debug jmp LOADER_BASE_ADDR my_print: ; print string with gpu ; param: bx: offset on the screen ; param si: string address ; param cx: length ; mov cx, dx loc_0x37: mov al, byte [si] mov byte [gs:bx], al add bx, 1 mov byte [gs:bx], 0x0a ; color add si, 1 add bx, 1 loop loc_0x37 retn ; my_print endp rd_disk_m_16: ; param eax = LBA Sector number ; bx = destination address ; cx = sector count mov esi, eax ; backup eax mov dx, 0x1f2 ; sector count mov al, cl out dx, al mov eax, esi mov dx, 0x1f3 ; set LBA address low 24b loc_72: out dx, al add dx, 1 shr eax, 8 cmp dx, 0x1f5 jbe loc_72 and al, 0x0f ; set LBA address high 4b or al, 0xe0 ; set device mode out dx, al add dx, 1 mov al, 0x20 ; read command out dx, al .not_ready: nop nop in al, dx and al, 0x88 ; 4: ready; 7: busy cmp al, 0x08 jnz .not_ready and cx, 0xf shl cx, 8 ; words to read, *512 / 2 mov dx, 0x1f0 ; data port .go_on_read: in ax, dx mov [bx], ax add bx, 2 loop .go_on_read retn ; data message db "Hello, world. This is a MBR." len_message db $ - message ; times 510-($-$$) db 0 db 510 - ($-$$) dup (0) db 0x55, 0xaa
下面是主要结构以及改动的说明:
%include
预处理指令,将boot.inc
中设置的两个参数宏定义引用进文件中my_print
进行改进,使之成为函数,采用寄存器传递参数rd_disk_m_16
读取硬盘函数,并调用此函数读取硬盘中loader,载入内存我们再来详细看一下rd_disk_m_16
函数:
0x1f2
端口输出想要读取的扇区数,也就是配置参数
shr
逻辑右移命令,每次将低八位输出,可以构造循环0x1f7
Command寄存器写入读命令0x20
0x88
过滤下面同样用一个简单的输出测试Loader是否被正确加载
%include "boot.inc" section loader vstart=LOADER_BASE_ADDR mov bx, 0x100 mov si ,message mov cx, [len] call my_print jmp $ my_print: ; vesion 3.0 ; print string with gpu ; param: bx: (bh, bl)=(row, col) offset on the screen ; param si: string address ; param cx: length ; mov cx, dx mov ax, 0xa0 mul bh add ax, bl mov bx, ax loc_0x37: mov al, byte [si] mov byte [gs:bx], al add bx, 1 mov byte [gs:bx], 0x0a ; color add si, 1 add bx, 1 loop loc_0x37 retn ; my_print endp ; data message db "Loader ready." len dw $ - message
同样地,它需要被写入磁盘文件disk.img
中,我们将它放在第二个扇区里。下面是一键启动的配置文件:
#!/bin/zsh nasm -o mbr.bin mbr-gpu.S nasm -o loader.bin loader.S dd if=mbr.bin of=disk.img bs=512 count=1 conv=notrunc dd if=loader.bin of=disk.img bs=513 count=1 seek=1 conv=notrunc bochs -q
接下来通过一键启动脚本,我们让虚拟机自主运行,MBR和Loader分别输出了预期的内容,本节的内容结束啦~