BIOS 固化在主板上,通过中断进行调用,可以进行一些基本操作,例如输出字符串、获取键盘输入等。开机后首先执行 BIOS。
在调用 BIOS 函数之前,需要先设置 AH 或 AX(或 EAX) 寄存器,然后执行对应的 INT 指令。例如 INT 0x13, AH=0
用于重置硬盘或软盘。
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, 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 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, 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 汇编基本语法:
MBR 主引导扇区最后一个字节必须是 0xaa
,倒数第二个字节必须是 0x55
。X86 是小端模式,低字节在低地址。
下面例子在 MBR 的512字节中,显示开机的字符串。
通过 BIOS 显示字符串参考这里:http://www.ctyme.com/intr/rb-0210.htm
这个例子把可执行文件中的二进制部分提取到第一个扇区,并在扇区最后两个字节填充 0xaa55
,其他地方填0。最终在屏幕上打印字符串。
.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
先用汇编器 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
在使用 LD 链接器时,通过 -T script_name 指定自定义的链接脚本。通过链接脚本,可以指定每个目标文件的段的信息,例如起始地址,排列顺序等。
OUTPUT_FORMAT(elf32-i386)
OUTPUT_ARCH(i386)
SECTIONS {
.text 0x0000 : {
*(.text)
}
/DISCARD/ : {
}
}
make
qemu-system-i386 -boot a -fda start.bin
执行成功的话,会在屏幕上看到打印的字符串。
操作系统无法在一个扇区内放下。通常在软盘的第一个扇区放置 IPL(Initial program loader,启动程序加载器)。
BIOS 把第一个扇区的数据加载到内存后,把控制权交给扇区的第一条指令。然后这个扇区负责把操作系统加载到内存并跳转执行(仍然需要借助 BIOS 的函数)。
BIOS 默认加载的是 0 号磁头、0 号柱面、1 号扇区。
x86寄存器分类:
其中,通用寄存器在不同模式下可以用的不一样:
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。
.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
.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"
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
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
启动时,必须指定调试参数。此时 QEMU 会等待 GDB 指令。
qemu-system-i386 -s -S -boot a -fda los.img --nographic
-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
:设置内存地址上的断点