- 基本概念: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
:设置内存地址上的断点