操作系统实践-BIOS

  • 基本概念:https://wiki.osdev.org/BIOS
  • 所有中断列表:http://www.ctyme.com/intr/int.htm
  • IBM PC 介绍:http://classiccomputers.info/down/IBM_PS2/documents/PS2_and_PC_BIOS_Interface_Technical_Reference_Apr87.pdf
  • x86 汇编手册:
    • https://www.felixcloutier.com/x86/
    • https://en.wikibooks.org/wiki/X86_Assembly
  • x86 官方文档:https://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-instruction-set-reference-manual-325383.pdf

BIOS 固化在主板上,通过中断进行调用,可以进行一些基本操作,例如输出字符串、获取键盘输入等。开机后首先执行 BIOS。

常用的 BIOS 指令

在调用 BIOS 函数之前,需要先设置 AH 或 AX(或 EAX) 寄存器,然后执行对应的 INT 指令。例如 INT 0x13, AH=0 用于重置硬盘或软盘。

INT 0x10 显示类指令

INT 0x10, AH = 1 -- set up the cursor
INT 0x10, AH = 3 -- 获取光标位置
INT 0x10, AH = 0xE -- 显示字符
INT 0x10, AH = 0xF -- get video page and mode
INT 0x10, AH = 0x11 -- set 8x8 font
INT 0x10, AH = 0x12 -- detect EGA/VGA
INT 0x10, AH = 0x13 -- 显示字符串,具体寄存器设置可以参考:http://www.ctyme.com/intr/rb-0210.htm
INT 0x10, AH = 0x1200 -- Alternate print screen
INT 0x10, AH = 0x1201 -- turn off cursor emulation
INT 0x10, AX = 0x4F00 -- video memory size
INT 0x10, AX = 0x4F01 -- VESA get mode information call
INT 0x10, AX = 0x4F02 -- select VESA video modes
INT 0x10, AX = 0x4F0A -- VESA 2.0 protected mode interface

INT 0x13 外存类指令

INT 0x13, AH = 0 -- reset floppy/hard disk
INT 0x13, AH = 2 -- read floppy/hard disk in CHS mode
INT 0x13, AH = 3 -- write floppy/hard disk in CHS mode
INT 0x13, AH = 0x15 -- detect second disk
INT 0x13, AH = 0x41 -- test existence of INT 13 extensions
INT 0x13, AH = 0x42 -- read hard disk in LBA mode
INT 0x13, AH = 0x43 -- write hard disk in LBA mode

INT 0x15 内存类指令

INT 0x12 -- get low memory size
INT 0x15, EAX = 0xE820 -- get complete memory map
INT 0x15, AX = 0xE801 -- get contiguous memory size
INT 0x15, AX = 0xE881 -- get contiguous memory size
INT 0x15, AH = 0x88 -- get contiguous memory size

INT 0x15, AH = 0xC0 -- Detect MCA bus
INT 0x15, AX = 0x0530 -- Detect APM BIOS
INT 0x15, AH = 0x5300 -- APM detect
INT 0x15, AX = 0x5303 -- APM connect using 32 bit
INT 0x15, AX = 0x5304 -- APM disconnect

INT 0x16 键盘类指令

INT 0x16, AH = 0 -- read keyboard scancode (blocking)
INT 0x16, AH = 1 -- read keyboard scancode (non-blocking)
INT 0x16, AH = 3 -- keyboard repeat rate

示例

基本概念

X86 系列在 16 位实模式下,地址转换格式是:SEG:OFFSET,通过段地址左移4位加上偏移量得到物理地址。例如,0x07c0:0x0000 对应的就是 0x07c0 << 4 + 0x0 = 0x7c00

AT&T 语法中,句点开头的是伪指令。X86 汇编基本语法:

  • .global:声明全局符号,可被其他文件引用
  • .code16:16位模式
  • .equ:定义常量
  • .=:从当前位置对应机器码字节数开始,填充到指定字节数
  • .word:定义一个字
  • ljmp:第一个参数设置 CS 寄存器,第二个参数设置 EIP 寄存器。然后跳转执行。可以清理之前的 CPU 流水线缓存。

MBR 主引导扇区最后一个字节必须是 0xaa,倒数第二个字节必须是 0x55。X86 是小端模式,低字节在低地址。
下面例子在 MBR 的512字节中,显示开机的字符串。
通过 BIOS 显示字符串参考这里:http://www.ctyme.com/intr/rb-0210.htm

示例一:读取并执行磁盘第一个扇区的代码

这个例子把可执行文件中的二进制部分提取到第一个扇区,并在扇区最后两个字节填充 0xaa55,其他地方填0。最终在屏幕上打印字符串。

start.S

.code16
.global _start

.equ BOOTSEG, 0x07c0
ljmp $BOOTSEG, $_start

_start:
    mov $0x03, %ah
    int $0x10
    mov $BOOTSEG, %ax
    mov %ax, %es
    mov $_string, %bp
    mov $0x1301, %ax
    mov $0x0007, %bx
    mov $9, %cx
    int $0x10
loop:
    jmp loop

_string:
    .ascii "hello los"

.=510

signature:
    .word 0xaa55

Makefile

先用汇编器 as 把汇编代码转为可重定位目标文件,然后通过链接器,根据链接脚本得到 ELF 可执行目标文件。最后,把 ELF 中的二进制部分复制出来即可。

all: start.bin

start.bin: start.S start.ld
    as --32 start.S -o start.o
    ld -T start.ld start.o -o start.elf
    objcopy -O binary start.elf start.bin

.PHONY= clean
clean:
    rm -f *.o *.bin *.elf

start.ld

在使用 LD 链接器时,通过 -T script_name 指定自定义的链接脚本。通过链接脚本,可以指定每个目标文件的段的信息,例如起始地址,排列顺序等。

  • *(.text):提取所有目标文件的代码段
  • /DISCARD/:要忽略的段
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386)

SECTIONS {
    .text 0x0000 : {
        *(.text)
    }
    
    /DISCARD/ : {
        
    }
}

编译,并通过 QEMU 执行

make
qemu-system-i386 -boot a -fda start.bin

执行成功的话,会在屏幕上看到打印的字符串。

示例二:加载并执行更多扇区的代码

操作系统无法在一个扇区内放下。通常在软盘的第一个扇区放置 IPL(Initial program loader,启动程序加载器)。

BIOS 把第一个扇区的数据加载到内存后,把控制权交给扇区的第一条指令。然后这个扇区负责把操作系统加载到内存并跳转执行(仍然需要借助 BIOS 的函数)。

软盘概念

  • 磁头 header:每个盘面对应一个磁头,上面的是 0 号,下面的是 1 号
  • 柱面 cylinder:每个盘面分成多个同心圆环,每个圆环就是一个柱面,最外层是 0 号,最内层是 79 号,共 80 个
  • 扇区 sector:每个柱面分成 18 个扇区,编号 1-18。每个扇区 512Byte
  • 软盘总容量:2 个磁头 * 80 个柱面 * 18 个扇区 * 512Byte = 1440KB

BIOS 默认加载的是 0 号磁头、0 号柱面、1 号扇区。

x86 实模式的可用地址

x86寄存器分类:

  • 8个通用寄存器:EAX、EBX、ECX、EDX、ESI、EDI、ESP、EBP
  • 1个标志寄存器:EFLAGS
  • 6个段寄存器:CS、DS、ES、FS、GS、SS
  • 5个控制寄存器:CR0、CR1、CR2、CR3、CR4
  • 8个调试寄存器:DR0、DR1、DR2、DR3、DR4、DR5、DR6、DR7
  • 4个系统地址寄存器:GDTR、IDTR、LDTR、TR
  • 其他寄存器:EIP、TSC等。

其中,通用寄存器在不同模式下可以用的不一样:

32位 16位 8位
EAX AX AH、AL
EBX BX BH、BL
ECX CX CH、CL
EDX DX DH、DL
ESI SI
EDI DI
ESP SP
EBP BP

实模式下通过 ES * 16 + BX 表示地址。因为 ES 和 BX 两个寄存器都是 16bit 的,所以最大可用地址是 1MB。

开机后,BIOS 会把第一个扇区 512B 的内容加载到内存的 0x7c00 地址处,即 0x7c00 ~ 0x7dff。从 0x7d00 开始直到 0x9fbff (1MB 处)都可以给操作系统用。

代码

除了上一个例子的 start.S,还有个 main.S。

start.S
.code16
.global _bootstart

.equ BOOTSEG, 0x07c0
ljmp $BOOTSEG, $_bootstart

_bootstart:
        mov $0x03, %ah
        int $0x10
        mov $BOOTSEG, %ax
        mov %ax, %es
        mov $_string, %bp
        mov $0x1301, %ax
        mov $0x0007, %bx
        mov $9, %cx
        int $0x10
        jmp readDisk

readDisk:
        mov $0x0800, %ax
        mov %ax, %es
        mov $0x02, %ah
        mov $1, %al
        mov $0, %ch
        mov $2, %cl
        mov $0, %dh
        mov $0, %dl
        int $0x13
        jc error
        jmp _start

error:
loop:
        jmp loop

_string:
        .ascii "hello los"

.=510

signature:
        .word 0xaa55
main.S
.code16
 .global _start

_start:
        mov $0x06, %ah
        mov $0, %al
        mov $0, %cx
        mov $2479, %dx
        mov $0x07, %bh
        int $0x10

        mov $0x03, %ah
        int $0x10
        mov $0x800, %ax
        mov %ax, %es
        mov $_string, %ax
        mov %ax, %bp
        mov $0x13, %ah
        mov $0x1, %al
        mov $0, %bh
        mov $07, %bl
        mov $13, %cx
        int $0x10

osloop:
        jmp osloop

_osstring:
        .ascii "\n main os boot"
Makefile
COBJS += 
ASOBJS += start.S main.S

all: los.img

los.img: los.elf
        objcopy -O binary los.elf los.img

los.elf: *.o
        ld -T start.ld %@ -o los.elf

%.o: %.S
        as -g --32 $(ASOBJS) -o %@

.PHONY= clean run
clean:
        rm -f *.o *.bin *.elf

run:
        qemu-system-i386 -boot a -fda los.img
start.ld
COBJS += 
ASOBJS += start.S main.S

all: los.img

los.img: los.elf
        objcopy -O binary los.elf los.img

los.elf: *.o
        ld -T start.ld %@ -o los.elf

%.o: %.S
        as -g --32 $(ASOBJS) -o %@

.PHONY= clean run
clean:
        rm -f *.o *.bin *.elf

run:
        qemu-system-i386 -boot a -fda los.img

通过 GDB 配合 QEMU 进行调试

通过 QEMU 启动内核

启动时,必须指定调试参数。此时 QEMU 会等待 GDB 指令。

qemu-system-i386 -s -S -boot a -fda los.img --nographic
  • -S 表示“freeze CPU at start up”
  • -gdb tcp::1234表示启动gdbserver,默认开启

打开 GDB 链接 QEMU

  • 打开 gdb,不带任何参数
  • 关联要调试的内核对应的 elf 文件,注意需要在编译时通过 -g 参数指定调试信息
  • 设置断点
  • 启动
root@osboxes:~# gdb
GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
For help, type "help".
Type "apropos word" to search for commands related to "word".

(gdb) file  test/x86/bios-disk/los.elf 
Reading symbols from test/x86/bios-disk/los.elf...done.

(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000fff0 in ?? ()

(gdb) break _start
Breakpoint 1 at 0x200: file main.S, line 5.

(gdb) c
Continuing.
q
^C
Program received signal SIGINT, Interrupt.
loop () at start.S:34
34		jmp loop
  • target remote localhost:1234:连接 QEMU 远程调试
  • break *0x7c00:设置内存地址上的断点

你可能感兴趣的:(操作系统实践,操作系统,bios)