实验1的内容是“启动PC系统”——一个从开机到运行到OS的流程;看似一个很复杂的流程,为了很好的解剖这样的流程,需要充分的知识准备,而且更重要的是从代码的角度去解释该过程。所以我们将这一篇的内容定位与介绍一些概念与模拟代码,保证理解过程的顺利。换句话说,如果在理解实验1的内容有什么问题,可以参考该篇给出的内容或者相关资源。
本篇主要通过两个部分来做出详细的介绍:其一为BIOS编程空间;其一为C与汇编的互调。
一)BIOS编程空间
这里有一个很陌生的名词——编程空间,其实,这是我对编程环境的一个定义,一般用编程环境来描述,但是它强调的是编码的工具与使用的api等概念;但是用编码空间来代替它,因为它不仅仅包含这些内容,更多的是强调编程时更关心其代码运行的环境(内存空间,处理器状态,外设的资源使用),而且是在一个固定的环境中。
对于BIOS的编程空间,我们关注的点主要有如下几个方面:
1.处理器的状态
寄存器的长度为16位,处于8086的处理器状态,详情参考《64-ia-32-architectures-software-developer-manual-325462.pdf》20.1REAL-ADDRESS MODE
2.内存的使用
内存空间为0-1M的连续空间,地址访问方式为(DS<<4|BX);详情参考《64-ia-32-architectures-software-developer-manual-325462.pdf》20.1REAL-ADDRESS MODE
然而不是1M空间都能被任意使用,所以需要理解内存映像——内存地址区域的实际使用情况:
start |
end |
size |
type |
description |
Low Memory (the first MiB) |
||||
0x00000000 |
0x000003FF |
1 KiB |
RAM - partially unusable (see above) |
Real Mode IVT (Interrupt Vector Table) |
0x00000400 |
0x000004FF |
256 bytes |
RAM - partially unusable (see above) |
BDA (BIOS data area) |
0x00000500 |
0x00007BFF |
almost 30 KiB |
RAM (guaranteed free for use) |
Conventional memory |
0x00007C00 (typical location) |
0x00007DFF |
512 bytes |
RAM - partially unusable (see above) |
Your OS BootSector |
0x00007E00 |
0x0007FFFF |
480.5 KiB |
RAM (guaranteed free for use) |
Conventional memory |
0x00080000 |
0x0009FBFF |
approximately 120 KiB, depending on EBDA size |
RAM (free for use, if it exists) |
Conventional memory |
0x0009FC00 (typical location) |
0x0009FFFF |
1 KiB |
RAM (unusable) |
EBDA (Extended BIOS Data Area) |
0x000A0000 |
0x000FFFFF |
384 KiB |
various (unusable) |
Video memory, ROM Area |
由上表可以发现,目前0-0x4FF,0x9FC00-0xfffff这两段内存是被系统占用了,不能被我们编程使用。
3.外设的使用
BIOS对于外设资源的使用,主要提供了两种方式:其一为中断服务,其二为外设io地址空间。对于外设我们主要关注点为显示器,键盘,串口,硬盘。
对于外设的使用与控制,我觉得一个很有用的观点是来之于《PC内幕技术》——资料的来源:
大多数情况下,我首先回顾了一下制造商为子系统提供的ic数据源清单,然后仔细察看这些芯片在标准主板上是如何具体连接的,做到这一点需要用到系统的原理图,在某些情况下,还要查看系统的电路图。我也仔细研究了不同制造商提供的反汇编BIOS代码,以便在这些较低的层次上考察它们与子系统的联系。我还生成了一些测试程序来检验某西子系统的操作。最后,我才去看那些“正式”的文档,包括IBM的技术参考资料,当然它也是许多其他技术书籍的资料来源。
上面的一段话对如何认识与学习ic子系统提供了基本而且严谨的步骤与方法,很值得我们每个人向前辈学习。也幸好有前辈们的铺垫,我们只需要在已经有的文档中与代码中,理解相关核心而基本的概念,然后使用总结好的代码,保证我们能够对系统有更好的理解与实现。
如下为详细的介绍相关外设的知识:
A)键盘——PS/2 keyboards
作为分析的一个最简单的外设——主要作为输入设备,它的控制芯片为Intel 8042 microcontroller。默认了qemu是模拟的IBM PC/XT Keyboard 的键盘按键,其扫描码详情见:http://www.computer-engineering.org/ps2keyboard/scancodes1.html
系统组织结构图如下,详情可见《PC内幕》8.1图:
键盘的基本工作原理:键盘检测到按键按下,然后将按键扫描码通过串口发送给主板的8042,接着被翻译为系统扫描码,放入输出缓存中,最后通过IRQ9给处理器。
另外,读取键盘按键发送给主板的时序如下:
如上所述,读取按键的方法有两种,一种就是通过中断服务IRQ 9;另一种为通过io指令读取8042缓存的按键值。
如下提供一种通过io指令读取按键的方法——参考8042提供的io口寄存器配置如下表:
Port |
Read / |
Function |
0x60 |
Read |
Read Input Buffer |
0x60 |
Write |
Write Output Buffer |
0x64 |
Read |
Read Status Register |
0x64 |
Write |
Send Command |
如下为读取按键的代码示例:
kbRead: 1: in $0x64,%al#读取通讯状态 andb %al,0x01#检测接收按键扫描码是否ok jz 1b in $0x60,%al #读取键值
由以上参考读取按键的流程,在读取键值之前需要读取按键是否准备好,因为由一个通信的过程。
如果想进一步了解,请参考如下的内容:
http://www.computer-engineering.org/ps2keyboard/
http://retired.beyondlogic.org/keyboard/keybrd.htm#1
或者参考《IBM.PC.汇编语言程序设计(第五版)完整版》第10章,《PC内幕技术》第8章。
B)串口——UART
对于串口的理解,我们可以通过两种方式去理解,其一是属于数据通信的范畴,数据通信一般需要考虑的问题:数据(帧)格式(数据位序LSB),数据传输速率,出错控制,流量控制;另外,串口又作为系统的外设,又需要中断控制,一般由如下芯片实现其功能:
芯片编号 |
描述 |
8250 |
基本UART功能支持,最早的串口芯片 |
8250A/B |
比8250更快 |
16450 |
8250的改进,IBM-AT上使用,支持38.4KBPS |
16550 |
在16450基础上,加入接收与发送的FIFO |
16552 |
支持2个16550UART |
16C454/16C1450/16C1550 |
支持4路16550UART,同时支持程序可控的掉电与重起 |
16650/17650 |
支持更多的FIFO |
所以根据如上的描述可以得出串口的属性:
(1)波特率——最小波特率,最大波特率
根据标准的串口的频率表,可以得到最大的波特率如下:
串口频率 |
1.8432Mhz |
2.4546Mhz |
最大波特率 |
115.2 KBPS |
153.6 KBPS |
如果设置其他的波特只需要直接分频即可。
比如:0x03 = 38,400 BPS
(2)数据格式——数据长度,字节序(LSB),终止位,奇偶校验
(3)流控——FIFO的支持——常见的模式没有使用
(4)传输状态——是否传输成功与出错
(5)中断设置——设置中断与查询中断
根据如上的属性,PC系统给出的寄存器表如下,pc系统最多支持4路com
口:
Name |
Base Address |
IRQ |
COM 1 |
3F8 |
4 |
COM 2 |
2F8 |
3 |
COM 3 |
3E8 |
4 |
COM 4 |
2E8 |
3 |
默认的com口基地址与IRQ
Base Address |
DLAB |
Read/Write |
Abr. |
Register Name |
+ 0 |
=0 |
Write |
- |
Transmitter Holding Buffer |
|
=0 |
Read |
- |
Receiver Buffer |
|
=1 |
Read/Write |
- |
Divisor Latch Low Byte |
+ 1 |
=0 |
Read/Write |
IER |
Interrupt Enable Register |
|
=1 |
Read/Write |
- |
Divisor Latch High Byte |
+ 2 |
- |
Read |
IIR |
Interrupt Identification Register |
|
- |
Write |
FCR |
FIFO Control Register |
+ 3 |
- |
Read/Write |
LCR |
Line Control Register |
+ 4 |
- |
Read/Write |
MCR |
Modem Control Register |
+ 5 |
- |
Read |
LSR |
Line Status Register |
+ 6 |
- |
Read |
MSR |
Modem Status Register |
+ 7 |
- |
Read/Write |
- |
Scratch Register |
支持默认com口寄存器
根据如上表可以看出一个寄存器由多种功能,其中由一列为DLAB,为一个开关设置波特率的方式;而DLAB的开关又在LCR的最高位,具体的每个参数功能可以参考如下的地址:
http://byterunner.com/16550.html
另外查询系统支持的com口,可以根据下表的的内存地址读取相关的基地址:
Start Address |
Function |
0000:0400 |
COM1's Base Address |
0000:0402 |
COM2's Base Address |
0000:0404 |
COM3's Base Address |
0000:0406 |
COM4's Base Address |
针对如上的内容解释可以看出如果要操作串口,有两种方式,其一为中断,其二为io指令;一般操作串口的步骤由如下3步:
(1)查询哪路com能够被使用,确认com的基地址:
movw $0x406,%bx #从com4开始查询每个com口是否支持 movw $4,%cx check_com: cmpw $0,(%bx)#如果com口的基地址为0,说明该路com口不支持 movw (%bx),%ax #读取com口基地址 jne put_com_info next: sub $2,%bx loop check_com jmp 1f
(2)初始化UART的通信状态:
init_com: OUT_B 0,(PORT1+2) #设置FCR,FIFO控制为关 OUT_B 0x80,(PORT1 + 3)#开启DLAB OUT_B 0x03,(PORT1 + 0)#设置最低波特率为38,400 BPS OUT_B 0x00,(PORT1 + 1 ) #设置最高波特率为115200BPS OUT_B 0x03,(PORT1 + 3) #关闭DLAB,设置数据位为8位,停止位1位,没有校验 OUT_B 0x01,(PORT1 + 1)#设置中断IER OUT_B 0x00,(PORT1 + 4)#关闭MCR movw $(PORT1+2),%dx #如下为清楚数据缓冲 inb %dx,%al movw $(PORT1),%dx inb %dx,%al ret
(3)发送串口数据:
out_char_to_console: 1: movw $(PORT1+5),%dx #读取LSR状态,是否发送完毕 inb %dx,%al andb %al,0x20 #发送缓冲为空 jz 1b movb $'a',%al movw $PORT1,%dx outb %al,%dx #发送数据 ret如果需要详细了解,请参考如下地址:
http://retired.beyondlogic.org/serial/serial.htm
C)硬盘
对于硬盘的理解,我们需要了解两方面的知识:1.子系统构造,2.磁盘容量。子系统构造会告诉我们控制硬盘的一些基本概念;理解磁盘容量,可以方便我们去读取磁盘数据。
(1)子系统构造框图——摘录于《PC内幕技术》第11章简介:
上图我们可以发现我们读取磁盘数据,需要选择相关的驱动器。
(2)理解磁盘容量,需要知道磁盘的构造:
由上图可见硬盘主要是由多个磁盘构成,每个磁盘有2个磁头,每个磁盘可以分为若干个磁柱,每个磁柱又由磁道构成,磁道由扇区构成。扇区为512Byte。所以磁盘容量由如下的公式给出:
磁盘容量 =磁头数*磁柱数*磁道扇区数*512Byte。
如上的计算方法为CHS模式。而CHS模式寻址,只有24位,10位C,8位H,6位S。所以支持的最大容量为:1024*255*64*512B=8GByte.
根据BIOS的容量限制,可以发现磁盘的最大容量为8GByte,所以这种描述方法不足以描述磁盘容量,所以目前的磁盘支持逻辑块寻址(LBA),它将整个磁盘看作连续的规定大小的逻辑块。LBA支持48位,所以它的最大寻址空间为128PB.
更详细的了解可以参考如下博客:
http://blog.csdn.net/haiross/article/details/38659825
当我们了解了以上内容之后,对硬盘有如下两种操作:
(1)读取硬盘参数:
movb $0x80,%dl #读取硬盘,0x80表示硬盘,0x00软盘 movb $0x08,%ah #读取硬盘参数 int $0x13 #13号磁盘中断
如果读取成功CF为0具体磁盘参数如下:
(C,H,S)=(CX[6:15],DH,CL[0-5]).
DL表示驱动数
(2)读取硬盘数据:
a)通过INT 13 2来进行读取——CHS模式:
disk_2:#读取mbr到 0:0x8000 93 movw $0x800,%ax 94 movw %ax,%es 95 movw $0x00,%bx 96 movb $0x80,%dl 97 movb $0x01,%al 98 movw $0x01,%cx #读取第一个扇区 99 movb $0x00,%dh 100 movb $0x02,%ah #功能2 101 int $0x13 102 mov $0xb4,%ax 103 cmp 0x8000,%ax 104 jnz 1f 105 movb $'y',%al 106 OUT_B0b)通过INT 13 0x42来进行读取 —— LBA模式:
lba_mode: movw $0x80,%dl #读取第一个驱动器 xorw %ax, %ax movw %ax, 4(%si)#设置读取数据的内存地址 incw %ax /* set the mode to non-zero */ movb %al, -1(%si) /* the blocks 读取的大小 */ movw %ax, 2(%si) /* the size and the reserved byte */ movw $0x0010, (%si)#设置包的大小与保留字节 /* the absolute address 设置读取开始地址*/ movl kernel_sector, %ebx movl %ebx, 8(%si) movl kernel_sector + 4, %ebx movl %ebx, 12(%si) /* the segment of buffer address 设置保存数据的段地址*/ movw $GRUB_BOOT_MACHINE_BUFFER_SEG, 6(%si) movb $0x42, %ah int $0x13
详细解释如下:
/*
* BIOS call "INT 0x13 Function 0x42" to read sectors from disk into memory
*Call with%ah = 0x42
*%dl = drive number——驱动号,从(0x80+0开始的驱动号),一般支持两个驱动器0x80|0x81.
*%ds:%si = segment:offset of disk address packet——发送数据参数包地址
*Return:
*%al = 0x0 on success; err code on failure
*/
数据包格式如下:
struct Disk_Packet{
unsigned char Packat_Size;//1 0x10|0x18
unsigned char Reserved0;//1
unsigned short Read_Size;//2
unsigned int* Buf_Addr;//4 [BX:DS]
unsigned long Start_Number;//8
};
c)通过io口的方式来读取磁盘数据:
void waitdisk(void) { // wait for disk reaady while ((inb(0x1F7) & 0xC0) != 0x40) /* do nothing */; } void readsect(void *dst, uint32_t offset) { // wait for disk to be ready waitdisk(); outb(0x1F2, 1); // count = 1 outb(0x1F3, offset); outb(0x1F4, offset >> 8); outb(0x1F5, offset >> 16); outb(0x1F6, (offset >> 24) | 0xE0); outb(0x1F7, 0x20); // cmd 0x20 - read sectors // wait for disk to be ready waitdisk(); // read a sector insl(0x1F0, dst, SECTSIZE/4); }
如果想了解更多,请参考《AT Attachment with Packet Interface - 6 (working draft)》与《PC内幕》。
D)显示器
对于显示器,就更复杂了。为此我们需要先了解系统的视频子系统——如下框图摘录于《IBM.PC.汇编语言程序设计(第五版)完整版》第9章:
如上图可以看出显示系统的基本原理从视频显示区拿数据,然后进行相关的处理,最终显示到显示器上,所以我们只需要程序去修改视频显示区的数据就可以控制显示器的显示。而如上显示原理主要是描述的是“文本模式”的显示器工作。对于文本模式的显示器,描述每个字符需要两个byte来表示:前一个byte为字符值,后一个字符为对应的属性值对于这种情况可以查看显示空间的内存。属性值对应如下表:
说明 |
背景 |
前景 |
属性 |
BL(闪烁) R G B |
I(高亮显示) R G B |
位 |
7 6 5 4 |
3 2 1 0 |
对于显示区域BIOS给定了固定的地址空间,针对不同的显示器有如下3种情况,更详细的见表int 13 功能0的表:
A000:[0] 高级或者复杂的显示器
B000:[0] 单色文本
B800:[0] 文本与图形
对于显示器有两种操作方式:其一通过中断显示数据,其二,通过将数据写入显示区域。对于“文本模式”的显示器,还有一个重要的就是“光标”的位置确定。
A)查询系统使用的显示器 int 13 0f 得到视频模式
详细信息,可以通过bios手册查询得到如下默认的CGA(03),如下:
03h = T 80x25 8x8 640x200 16 4 B800 CGA,Pcjr,Tandy
如上信息可以知道:
视频模式为03,且为“文本模式”,支持的字符显示最大范围为:80列,25行。每个字符显示为8*8的像素,总的屏幕分辨率为640*200.支持16色,最多支持4页显示页,显示区域的开始地址为B8000,支持的显示器模式为CGA,Pcjr,Tandy。
B)输出字符串到屏:
(1)通过中断(int 10h 13h):
out_str_to_screen:#输出字符到屏幕 xorw %ax,%ax # movw %cx, %ax#cx->ax,寄存器的需要用% # movw %ax, %ds#初始化数据段 movw %ax, %es#初始化扩展段 #如下位利用BIOS的中断0x10来实现输出字符串到屏幕上 #详情功能定义可以参考bios手册 #中断:int 0x10 #功能号:ah = 0x13,输出字符串到屏幕 #属性:es:bp表示输出的字符串 # cx表示输出字符串长度 # dh,dl表示显示的行与列 # movw $msgstr,%bp#将msgstr的地址放入bp,地址的表示为$ movw len, %cx #将字符串的长度放入cx,标志的值直接用标识符 movb $0x01, %al # movb $0x01, %bl #字符属性 movb $0x00, %bh #第0页 movb $0x05, %dh #第5行 movb $0x08, %dl #第8列 movb $0x13, %ah # 功能13,输出字符串 int $0x10 #调用中断10h
(2)直接写显示区域
print_str:#通过直接写显示区域的方式,输出字符 xorw %bx,%bx movb $'T',%al movb $0xCE,%ah movw $0xB800,%cx movw %cx,%ds movw %ax,(5*80*2+19*2)(%bx)#第5行第24个字符
C)获取与修改光标位置:
(1)通过中断(int 10h 03h):
read_cursor: #读取光标位置 movb $0x0,%bh movb $0x03,%ah int $0x10
(2)通过io指令读取:
set_cursor: #设置光标 OUT_B 14,0x3D4 #切换到第5行第24个字符的位置 OUT_B (5*80+19)/256,0x3D5 OUT_B 15,0x3D4 OUT_B (5*80+19)%256,0x3D5更详细的介绍可以参考如下网址,或者参考《PC内幕》或者《IBM.PC.汇编语言程序设计(第五版)完整版》:
http://www.osdever.net/FreeVGA/home.htm
对于第1,2点的详细了解,我们才能知道如何编写代码,然后执行之。而BIOS的编程之后的代码执行是在mbr中,所以我们需要搭建bios实验环境。然后调试我们的外设的相关代码。所以我们的最初代码执行空间也只有512Btye(0x7C00-0x7DFF),如果要使用更多的代码,需要将它们放到磁盘的后续分区,然后被加载到内存中,才能使用。对于如上描述可以参考我博客的附件资源,其中包含了所有的实现,方便理解。
另外推荐一份bios的参考手册,绝对详细:
http://www.cs.cmu.edu/~ralf/files.html
关于中断的参考手册:
http://www.htl-steyr.ac.at/~morg/pcinfo/hardware/interrupts/inte1at0.htm
二)C与汇编的互调
在系统一上电运行的代码肯定是汇编语言,等到将c语言运行环境创建好了之后才能运行之。所以需要了解如下几个方面的知识:
1.首先汇编代码如何被执行
根据上一节描述,我们写的代码会放在mbr里,当然如果突破了mbr的限制,其他代码可以放到硬盘的其他扇区。在mbr的代码会被自动加载的0x7C00中执行,所以我们需要将我们的代码链接到0x7C00,查看makefile会发现可以这样实现:
A.直接链接生成mbr
boot: boot.o ld --oformat binary -N -Ttext 0x7c00 -o $@ $< #生成mbr扇区,链接地址为0x7C00
B.链接成elf文件后,再转换:
read_sector.out:read_sector.o ld -N -Ttext 0x8400 -o [email protected] $< -e readsect -m elf_i386 #这里是链接到地址0x8400,进入点为readsect objcopy -S -O binary -j .text [email protected] $@ #将生成read_sector.out.bak 转换成 read_sector.out。
方法2比方法1的优势是:a.文件更小,b.生成的elf文件可以被反汇编分析。
方法1的优势:简单。
关于mbr的详细介绍可以参考百度百科:
http://baike.baidu.com/link?url=sQhyg1wSqI1n3JgBuueAkh9NUlQ6iCG5a0HsfVexr4Ky3gwDfgxYBzsMwGZTcCzOqom8wfuxG62WxNYsP6WB6zOcdvocdfRtSUnKEkVC0Ji
它的解析,有附件的一个c语言文件。获取本机系统的mbr可以用如下命令:
sudo dd if=/dev/sda of=mbr bs=512 count=1
汇编为了调用c语言,需要做两步工作,第一创建c语言运行环境,第二调用c语言入口代码。
a)创建c运行环境:
因为c语言的基本结构是基于堆栈的,所以首先要创建堆栈:
init_stack: xorw %ax,%ax movw %ax,%es #设置堆栈段 movw $0x7c00,%sp #设置栈顶再c语言调用过程中肯定会读写全局数据,所以需要设置对应的数据段:
xorw %bp,%bp #清除bp,因为它会被c语言用于栈帧的开始指向。 movw %bp,%ds #设置数据段 movw %bp,%es #设置扩展段b)调用c语言,传入参数,然后调用对应的地址就可以了:
pushl $0 #设置第2个参数 pushl $0x8000 #设置第一个参数 calll 0x843b #调用c语言入口的函数地址注意传递参数的顺序是,从右到左。
3.c语言如何调用汇编语言:
通过AT&T内嵌汇编语法调用:
static __inline void outb(int port, uint8_t data) { __asm __volatile("outb %0,%w1" : : "a" (data), "d" (port)); } static __inline uint8_t inb(int port) { uint8_t data; __asm __volatile("inb %w1,%0" : "=a" (data) : "d" (port)); return data; }C语言使用inline的方法,声明函数为内敛:
static __inline uint8_t inb(int port) <strong>__attribute__((always_inline))</strong>; static __inline void insl(int port, void *addr, int cnt) __attribute__((always_inline)); static __inline void outb(int port, uint8_t data) __attribute__((always_inline));
4.汇编语言编程的技术总结
这里讲的汇编语言主要是unix标准下的AT&T语法,简单来说与Intel的语法有差别的地方与特定的伪指令支持,为什么需要了解AT&T汇编呢?因为在linux平台上,甚至Unix下,都适用,它与硬件平台无关,所以适用性很强。
a.支持的指令格式,操作数与Intel汇编相反,比如:
Intel :mov eax, ebx #ebx->(赋值给)eax
AT&T :mov %ebx,%eax
编译器会检测操作顺序,如果不能匹配,则出错。
b.Intel的指令,AT&T都支持,所以对于一些不了解的指令,可以参考Intel的相关技术手册。
c.操作数的表示格式
寄存器前需要添加%
立即数需要添加$
默认常数为地址
d.对宏的支持:
.macro OUT_B v pa #定义宏——将v输出到端口pa,参数为v,pa #通过宏定义,将参数v转化为简单的表达,换句话说,在宏定义中,参数的引用为\v。 #define V \v #define PA \pa movb $(V),%al #将V当作立即数使用 movw $(PA),%dx outb %al,%dx .endm
f.运行数据的保存
因为intel只有8个寄存器EAX,EBX, ECX, EDX, ESI, EDI,ESP,EBP可用,而且很多时候都是受限的——即特定的寄存器有特定的用途,比如:相对寻址只有用EBX。所以,寄存器的资源是不够的,需要用堆栈来保存:
xorw %ax,%ax movw %ax,%ds push %ds #保存ds movw $0x40,%ax movw %ax,%ds movw $0x47,%bx movb (%bx),%al pop %ds #恢复ds
g.对地址空间的使用:
x: .byte 'x' y: .int 0x1234 z: .word 0x2345 str: .ascii “Hello world!\0” str1: .asciz “Hello world!” .的使用,表示当前地址
如上的内容可以参考《gas_manual-unix汇编》与我附件的代码。
一叶说:终于写完了,内容太多了,而且需要一步步谨慎的验证;如上内容只是截取了主要的知识介绍,对我们后期理解系统的运行流程与实现相关代码有帮助;请理解它们,请调试它们。如果需要更进一步了解相关内容,请参考详实附件,这样对理解整个系统与相关软件的工作原理更有意义。最后还要感谢前辈们的知识总结,也希望后来的我们能够站在它们的肩膀上,爬得更高。