root:[.]
+--.DS_Store
+--bochsrc.bxrc
+--print_tree_file.py
+--run.sh
+--test1
| +--bootsect.s
| +--run_test1.sh
cpu 只能识别、运行机器指令,无法运行汇编程序 bootsect.s 中的汇编指令,所以只有把汇编程序 bootsect.s 汇编、链接生成二进制可执行文件 bootsect.bin ,并将其加载到物理内存后,cpu 才能逐条从物理内存中取指、解析、执行可执行文件 bootsect.bin 中的机器指令。
我们使用 as 汇编器和 ld 链接器把汇编程序 bootsect.s 汇编、链接成可执行文件 bootsect.bin。
可执行文件由可以被 cpu 直接运行的机器指令和机器指令运行过程中需要访问的数据组成。可执行文件 bootsect.bin 如图1.1所示,机器指令在前面,每个实线方框表示一条指令;数据紧跟在机器指令的后面,每个虚线方框表示一个(组)数据。可执行文件 bootsect.bin 由13条机器指令和3个(组)数据组成,其中,机器指令由汇编文件 bootsect.s 中的汇编指令汇编、链接得到,数据由汇编文件 bootsect.s 中的伪指令汇编、链接得到。
可执行文件中的每个字节都有唯一的地址——偏移地址。如图1.1(1)所示,第一个字节 0xea 的偏移地址为0,每条机器指令和每个(组)数据都有唯一的偏移地址,是它们的第1个字节的偏移地址。第一个实线方框中的机器指令的偏移地址为0,第2个实线方框中的机器指令的偏移地址为5。
只有把可执行文件加载到物理内存后,cpu才能逐条从物理内存中读取可执行文件的机器指令,并解析和执行机器指令。但是因为保存在物理内存中的数据在掉电之后会消失,所以需要将可执行文件保存在掉电后数据不会消失的硬盘中。在运行可执行文件之前,再将其从硬盘加载到物理内存。
我们可以把硬盘看做是一个由扇区组成的一维数组,每个扇区大小为512B,其中第一个扇区叫作引导扇区(本书中所有实验中使用的硬盘大小为1MB,共计2048个扇区,引导扇区的扇区号为0)。
可执行文件加载到物理内存后,把占用的一段物理内存叫作物理段,每个物理段都有一个属性:物理段起始地址。将可执行文件 bootsect.bin 加载到物理内存后,占用的物理段的物理地址空间为 0x7c00~0x7dff(512B),物理段的起始地址为0x7c00。
当物理内存中的可执行文件 bootsect.bin 在 cpu 中运行时,把机器指令所在的物理段叫作代码段,把数据所在的物理段叫作数据段。可执行文件 bootsect.bin 中包含了机器指令和数据,所以代码段和数据段共用同一个物理段,因此代码段和数据段起始地址都是 0x7c00。
当可执行文件被加载到物理内存的物理段之后,代码段中的机器指令和数据段中的数据就拥有了逻辑地址。在实模式下,逻辑地址的格式为:[右移4位后的代码/数据段的物理起始地址:偏移地址]。第一个实线方框中的机器指令的逻辑地址为:[0x7c0:0],第2个实线方框中的机器指令的逻辑地址为[0x7c0:5],第一个虚线方框中的数据逻辑地址为:[0x7c0:0x20],最后一个虚线方框的数据逻辑地址为:[0x7c0:0x1fe]。
物理内存可以看做是一个由字节组成的一维数组,每个字节都有唯一的物理地址。物理内存中的第1个字节的物理地址为0。机器指令/数据的物理地址=代码/数据段的物理段起始地址+偏移地址。
cpu 中有2个用于 cpu 从物理内存中读取机器指令的寄存器:
寄存器 [cs:ip] 保存了 cpu 下一条需要从物理内存读取的机器指令的逻辑地址。
在 Ubuntu 18.04 中安装 x86 虚拟机 bochs 的debugger 版本的命令如下:
sudo apt-get install build-essential libx11-dev xorg-dev libgtk2.0-dev
wget https://sourceforge.net/projects/bochs/files/bochs/2.6.8/bochs-2.6.8.tar.gz
tar zxvf bochs-2.6.8.tar.gz
cd bochs-2.6.8/
./configure --enable-debugger --enable-disasm --enable-debugger-gui
make -j4
sudo make install
as 是汇编器,-o bootsect.o
表示生成的可执行文件的文件名为 bootsect.o。可执行文件 bootsect.o 还需要链接才能在 cpu 上运行。
ld 是链接器,将 bootsect.o 链接成 bootsect.bin。
--oformat binary
:表示可执行文件 bootsect.bin 中只包含机器指令和数据;-Ttext=0
:表示第一条机器指令的偏移地址为0。-o bootsect.bin
:表示生成的可执行文件的文件名为 bootsect.bin。run_test1.sh 程序内容:
# 汇编、链接bootsect.bin
as -o bootsect.o bootsect.s
ld --oformat binary -Ttext=0 -o bootsect.bin bootsect.o
# 制作硬盘镜像文件
dd if=/dev/zero of=../c.img bs=512 count=2048
dd if=bootsect.bin of=../c.img bs=512 seek=0 conv=notrunc
将汇编和链接命令集合到 run_test1.sh 程序中,只需要使用 sh run_test1.sh
命令即可实现汇编和链接功能。
在 bochs 中,需要使用硬盘镜像文件虚拟物理硬盘。
给上层 test 目录创建一个大小为 1MB 的硬盘镜像文件 c.img 的命令:dd if=/dev/zero of=../c.img bs=512 count=2048
。
dd
:文件拷贝命令。if=/dev/zero
:表示拷贝的源文件的路径,dev/zero
是一个特殊的文件,可以提供 n(bs*count) 个 0。of=../c.img
:表示拷贝的目标文件路径,若不存在则创建。bs=512
:块的大小,单位为B。count=2048
:表示拷贝的文件的块的数量。硬盘镜像文件制作好后,将可执行文件 bootsect.bin 拷贝到硬盘镜像文件 c.img 的引导扇区的命令:dd if=bootsect.bin of=../c.img bs=512 seek=0 conv=notrunc
。
seek=0
:表示把可执行文件 bootsect.bin 拷贝到硬盘镜像文件 c.img 的引导扇区。conv=notrunc
:表示不改变目标文件的大小,若没有该项,则硬盘镜像文件 c.img 的大小会由 1MB 变为可执行文件 bootsect.bin 的大小512B。“硬盘”制作好后,要想启动 bochs,还需要一个配置文件(bochsrc.bxrc),文件内容如下:
romimage: file=/usr/local/share/bochs/BIOS-bochs-latest
vgaromimage: file=/usr/local/share/bochs/VGABIOS-lgpl-latest
ata0-master: type=disk, path="c.img"
megs: 16
cpu: count=1
boot: disk
run.sh 保存了启动 bochs 的命令,文件内容如下:
bochs -q -f bochsrc.bxrc
运行sh run.sh
命令后,就相当于我们按下了一台电脑的开机键。bochs 首先进行一些自身的初始化工作,最终在终端输出如下信息,并等待用户输入调试命令。
Next at t=0
(0) [0x0000fffffff0] f000:fff0 (unk. ctxt): jmpf 0xf000:e05b ; ea5be000f0
<bochs:1>
第1行:t 的值表示,cpu 从上电开始已经运行了多少条机器指令。
第2行:cpu 上电后运行的第1条机器指令的信息(数字均为16进制)
[0x0000fffffff0]
:该机器指令在物理内存中的物理地址,在实模式下,cpu 最多只能访问 1MB 物理内存,即该机器指令的物理地址为 0xffff0;f000:fff0
:当前寄存器[cs:ip]的值,该机器指令的逻辑地址。jmpf 0xf000:e05b
:该机器指令对应的汇编指令;ea5be000f0
:机器指令的内容。第3行:用户输入调试命令的命令行。
查看 cpu 中寄存器的值:r 命令和 sreg 命令。
<bochs:1> r
eax: 0x00000000 0
ecx: 0x00000000 0
edx: 0x00000000 0
ebx: 0x00000000 0
esp: 0x00000000 0
ebp: 0x00000000 0
esi: 0x00000000 0
edi: 0x00000000 0
eip: 0x0000fff0
eflags 0x00000002: id vip vif ac vm rf nt IOPL=0 of df if tf sf zf af pf cf
<bochs:2> sreg
es:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
cs:0xf000, dh=0xff0093ff, dl=0x0000ffff, valid=7
Data segment, base=0xffff0000, limit=0x0000ffff, Read/Write, Accessed
ss:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ds:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
fs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
gs:0x0000, dh=0x00009300, dl=0x0000ffff, valid=7
Data segment, base=0x00000000, limit=0x0000ffff, Read/Write, Accessed
ldtr:0x0000, dh=0x00008200, dl=0x0000ffff, valid=1
tr:0x0000, dh=0x00008b00, dl=0x0000ffff, valid=1
gdtr:base=0x00000000, limit=0xffff
idtr:base=0x00000000, limit=0xffff
打断点是重要的调试手段之一,bochs 提供了多个打断点的命令,其中 vb 命令使用逻辑地址打断点,b 命令使用物理地址打断点,使用 blist 可以查看所有断点信息。断点打好后,使用 c 命令运行程序,当 cpu 下一条即将运行的机器指令被打了断点时,cpu 停止在该机器指令前,等待输入新的命令。
<bochs:3> vb 0x0:0x7c00
<bochs:4> b 0x7c00
<bochs:5> blist
Num Type Disp Enb Address
1 vbreakpoint keep y 0x0000:00007c00
2 pbreakpoint keep y 0x000000007c00
<bochs:6> c
00000004661i[BIOS ] $Revision: 12579 $ $Date: 2014-12-26 11:31:39 +0100 (Fr, 26. Dez 2014) $
00000318049i[KBD ] reset-disable command received
00000320818i[BIOS ] Starting rombios32
00000321256i[BIOS ] Shutdown flag 0
00000321840i[BIOS ] ram_size=0x01000000
00000322261i[BIOS ] ram_end=16MB
00000362771i[BIOS ] Found 1 cpu(s)
00000376975i[BIOS ] bios_table_addr: 0x000fa498 end=0x000fcc00
00000704770i[PCI ] i440FX PMC write to PAM register 59 (TLB Flush)
00001032699i[P2ISA ] PCI IRQ routing: PIRQA# set to 0x0b
00001032718i[P2ISA ] PCI IRQ routing: PIRQB# set to 0x09
00001032737i[P2ISA ] PCI IRQ routing: PIRQC# set to 0x0b
00001032756i[P2ISA ] PCI IRQ routing: PIRQD# set to 0x09
00001032766i[P2ISA ] write: ELCR2 = 0x0a
00001033536i[BIOS ] PIIX3/PIIX4 init: elcr=00 0a
00001041217i[BIOS ] PCI: bus=0 devfn=0x00: vendor_id=0x8086 device_id=0x1237 class=0x0600
00001043496i[BIOS ] PCI: bus=0 devfn=0x08: vendor_id=0x8086 device_id=0x7000 class=0x0601
00001045614i[BIOS ] PCI: bus=0 devfn=0x09: vendor_id=0x8086 device_id=0x7010 class=0x0101
00001045839i[PIDE ] new BM-DMA address: 0xc000
00001046455i[BIOS ] region 4: 0x0000c000
00001048489i[BIOS ] PCI: bus=0 devfn=0x0b: vendor_id=0x8086 device_id=0x7113 class=0x0680
00001048721i[ACPI ] new irq line = 11
00001048733i[ACPI ] new irq line = 9
00001048758i[ACPI ] new PM base address: 0xb000
00001048772i[ACPI ] new SM base address: 0xb100
00001048800i[PCI ] setting SMRAM control register to 0x4a
00001212893i[CPU0 ] Enter to System Management Mode
00001212904i[CPU0 ] RSM: Resuming from System Management Mode
00001376925i[PCI ] setting SMRAM control register to 0x0a
00001391791i[BIOS ] MP table addr=0x000fa570 MPC table addr=0x000fa4a0 size=0xc8
00001393613i[BIOS ] SMBIOS table addr=0x000fa580
00001395781i[BIOS ] ACPI tables: RSDP addr=0x000fa6a0 ACPI DATA addr=0x00ff0000 size=0xf72
00001398971i[BIOS ] Firmware waking vector 0xff00cc
00001400766i[PCI ] i440FX PMC write to PAM register 59 (TLB Flush)
00001401489i[BIOS ] bios_table_cur_addr: 0x000fa6c4
00001529106i[VBIOS ] VGABios $Id: vgabios.c,v 1.76 2013/02/10 08:07:03 vruppert Exp $
00001529177i[BXVGA ] VBE known Display Interface b0c0
00001529209i[BXVGA ] VBE known Display Interface b0c5
00001532134i[VBIOS ] VBE Bios $Id: vbe.c,v 1.65 2014/07/08 18:02:25 vruppert Exp $
00001570128i[XGUI ] charmap update. Font Height is 16
00001856608i[XGUI ] charmap update. Font Height is 16
00001876307i[BIOS ] ata0-0: PCHS=2/16/63 translation=none LCHS=2/16/63
00005753481i[BIOS ] IDE time out
00017844201i[BIOS ] Booting from 0000:7c00
(0) Breakpoint 1, in 0000:7c00 (0x00007c00)
Next at t=17844256
(0) [0x000000007c00] 0000:7c00 (unk. ctxt): jmpf 0x07c0:0005 ; ea0500c007
<bochs:7>
使用 u 命令查看 cpu 下一条即将运行的机器指令。
<bochs:3> u /5
00007c00: ( ): jmpf 0x07c0:0005 ; ea0500c007
00007c05: ( ): mov ax, 0x0600 ; b80006
00007c08: ( ): mov ch, 0x00 ; b500
00007c0a: ( ): mov cl, 0x00 ; b100
00007c0c: ( ): mov dh, 0x18 ; b618
使用 xp 命令可以查看物理内存中的指定物理地址的内容。查看物理内存 0x7dfe 开始的2个字节的内容。
/2bx:打印2个字节(参数b为1个字节,还有h、w)
/13c:将13个字节作为 ASCII 码对应的字符打印出来
<bochs:4> xp /2bx 0x7dfe
[bochs]:
0x00007dfe <bogus+ 0>: 0x55 0xaa
使用 n 命令让 cpu 运行一条机器指令后,停止并等待输入新的命令。
<bochs:5> n
Next at t=17844257
(0) [0x000000007c05] 07c0:0005 (unk. ctxt): mov ax, 0x0600 ; b80006
使用 xp 命令查看中断向量表中的 0x10 号中断处理程序的入口逻辑地址。
<bochs:6> xp /1wx 0x40
[bochs]:
0x00000040 <bogus+ 0>: 0xc0000152
x86 架构的 cpu 在上电后处于实模式,在实模式下,cpu 最多只能访问 1MB 物理内存,其中 BIOS 占用的物理段的物理地址空间为:0xc0000~0xfffff。cpu 上电后,运行的第1条机器指令的物理地址为 0xffff0,该物理地址正好位于 BIOS 占用的物理段内,因此 cpu 上电后首先运行 BIOS。
BIOS 主要工作:
BIOS 运行完毕后,内存状态为:
.code16
BOOTSEG = 0x7c0
ljmp $BOOTSEG, $go
go:
movw %cs, %ax
movw %ax, %ds
movw %ax, %es
movw $msg, %bp
movb $0x13, %ah
movb $0x01, %al
movb $2, %bl
movw msg_len, %cx
movb $0, %dh
movb $0, %dl
int $0x10
jmp .
msg:
.ascii "hello, world."
msg_len:
.word . - msg
.org 0x1fe
.word 0xaa55
在汇编程序中,以.
开头的汇编语句叫作伪指令,汇编器 as 不会把伪指令汇编成机器指令。
.code16
:在实模式下,cpu 只能运行16位的机器指令,该伪指令的作用是:告诉汇编器,把汇编程序 bootsect.s 中的汇编、链接为16位的机器指令。.ascii "hello, world."
:在可执行文件 bootsect.bin 中的偏移地址空间 0x20~0x2c 中定义字符串“hello,world.”。.word . - msg
:在可执行文件 bootsect.bin 中的偏移地址空间 0x2d~0x2e 中定义一个2字节大小的数据(x86架构中使用小端模式,低字节存放在物理内存的低地址字节)。.org 0x1fe
:将第22行伪指令对应的数据在可执行文件 bootsect.bin 中的偏移地址设置为 0x1fe,若不是用该伪指令,则第22行伪指令对应的数据的偏移地址为 0x2f,并将可执行文件 bootsect.bin 中偏移地址 0x2f~0x1fd 中的内容用0填充。在汇编程序中,使用符号表示一个数,有两种方式:
=
表示。如代码清单第2行所示,符号 BOOTSEG 值为 0x7c0。:
表示。则符号表示=
后面的汇编语句对应的机器指令或数据在可执行文件中的偏移地址。符号 go 的值为 0x5,符号 msg 的值为 0x20。在将汇编程序 bootsect.s 汇编为可执行文件 bootsect.o 的过程中,汇编器 as 首先把汇编程序 bootsect.s 中所有的符号用其表示的数替换。
机器指令的逻辑地址和物理地址是多对一的关系,即一个物理地址可以由多个逻辑地址计算得到。
BIOS 把bootsect.bin 从引导扇区加载到物理内存的物理地址空间 0x7c00~0x7dff 后,将寄存器 [cs:ip] 赋值为可执行文件 bootsect.bin 的第一条机器指令的逻辑地址 [0x0:0x7c00],代码段和数据段的物理起始地址为 0。当 cpu 运行 可执行文件 bootsect.bin 时,代码段的物理起始地址为 0x7c00,故可执行文件 bootsect.bin 的第一条机器指令需要对寄存器 [cs:ip] 重新赋值。
ljmp 汇编指令的作用是为寄存器 [cs:ip] 赋值,在汇编指令中寄存器前加 % 。
通常代码段设置完成后,必须设置数据段。在实模式下,cpu 中除了用于保存代码段的物理段起始地址右移4位的寄存器 cs 外,还有用于保存数据段的物理段起始地址右移 4 位的寄存器——数据段寄存器 ds 和数据段寄存器 es。
cpu 运行可执行文件 bootsect.bin 时,代码段和数据段的物理段起始地址都为 0x7c00,所以只需要将寄存器 cs 的值赋给寄存器 ds 和寄存器 es 即可,如代码清单5~7行所示,不能将一个段寄存器的值直接赋给另外一个段寄存器。
每条 mov 汇编指令都有一个后缀:
- b:赋值给寄存器的数据大小为1个字节;
- w:赋值给寄存器的数据大小为2个字节;
- l:赋值给寄存器的数据大小为4个字节;
BIOS 在创建中断系统时,在 1MB 物理内存的最后 256KB 物理地址空间内保存了大量的中断处理程序。这些中断处理程序用于访问系统中已有的硬件,访问不同的硬件需要调用不同的中断处理程序,每个中断处理程序都有唯一的编号——中断号。
常用中断号举例:
- 0x10:往显示器的屏幕上打印字符。
- 0x13:从硬盘读取数据。
调用中断处理程序前,需要借助 cpu 中的寄存器传递参数。例如:调用 0x10 中断处理程序往显示器的屏幕上打印字符前,需要给 0x10 号中断处理程序传递参数,告诉它在屏幕的什么位置显示什么内容,内容的长度以及属性。
寄存器 ah:0x13 表示向屏幕打印字符串。
寄存器[es:bp]:保存字符串的首字符在数据段中的逻辑地址。
寄存器 cs:保存字符串的长度。
寄存器 (dh, dl):字符串在屏幕上的起始坐标,寄存器 dh 为行号(0~24),寄存器 dl 为列号(0~79)。
寄存器 al:指定光标和字符的属性。
寄存器 bl:若寄存器 al 的值为 0 或者 1,保存字符的属性值。
二进制 | 颜色 | 二进制 | 颜色 |
---|---|---|---|
0000 | 黑色 | 1000 | 灰色 |
0001 | 蓝色 | 1001 | 淡蓝色 |
0010 | 绿色 | 1010 | 淡绿色 |
0011 | 青色 | 1011 | 淡青色 |
0100 | 红色 | 1100 | 淡红色 |
0101 | 紫红色 | 1101 | 淡紫红色 |
0110 | 棕色 | 1110 | 黄色 |
0111 | 银色 | 1111 | 白色 |
使用 mov 指令赋值:
- 数据在寄存器中。如代码第7行所示。
- 数据在机器指令中。如代码第8行所示。将 $ 后面的数字 0x20 (符号 msg 的值)赋值给寄存器 bp。
- 数据在内存中。如果符号前没有 $ ,则说明用来赋值的数据保存在物理内存中,而非偏移地址(符号的值)。类似于 cpu 将寄存器 [cs:ip] 中的机器指令的逻辑地址转换为物理地址后,使用物理地址从物理内存中读取机器指令,cpu 也将数据的逻辑地址转换为物理地址后,通过物理地址从物理内存中读取数据,不同之处是:1. 机器指令的逻辑地址中的段起始地址右移4位的值保存在代码段寄存器 cs,而数据的逻辑地址中的段起始地址右移4位的值默认保存在数据段寄存器 ds 中;2. 机器指令的逻辑地址中的偏移地址保存在寄存器 ip 中,而数据的逻辑地址中的偏移地址,通常由机器指令提供。如代码12行所示,将字符串“hello, world.”的长度13赋值给寄存器cx。
设置好为 0x10 号中断处理程序传递参数的寄存器后,在汇编程序 bootsect.s 使用 int 汇编指令调用 0x10 号中断处理程序,cpu 执行第15行汇编指令对应的机器指令的过程如下:
将当前寄存器 [cs:ip] 中的逻辑地址(第16行汇编指令)保存起来。
将 0x10 号中断处理程序的入口逻辑地址加载到寄存器[cs:ip]之中。每个中断处理程序的入口逻辑地址都保存在中断向量表中。中断处理程序的入口逻辑地址按照中断号依次存放在中断向量表中,可以通过 0x10 号中断处理程序的中断号 0x10 从中断向量表中获取 0x10 中断处理程序的入口逻辑地址[0xc000:0x152],将入口逻辑地址赋值给寄存器[cs:ip]后,cpu下一条运行的是 0x10 号中断处理程序的第1条指令,物理地址为0xc0152。
因为 cpu 会不停地根据寄存器 [cs:ip] 中的逻辑地址转换后的物理地址,从物理内存中读取机器指令,然后对其解析、执行,因此需要一条让 cpu 进入死循环的机器指令作为可执行文件的最后一条机器指令,如代码第16行所示,与 ljmp 不同的是,jmp 只给寄存器 ip 赋值。cpu 会一直循环运行第 16 行指令,不会将第1个虚线方框中的部分数据看做机器指令,进行取指、解析、执行。
BIOS 会将引导扇区中的可执行文件拷贝到物理内存,但是在拷贝前,会检查引导扇区中的可执行文件 bootsect.bin 的最后两个字节的值是不是0x55和0xaa,若不是,则 BIOS 不会拷贝。