- 论坛徽章:
- 0
|
qiuhan
2007.10.13
我们并没有实际去研读物理bios的代码,而是查看了bochs中的bios虚拟实现,主要出于如下考虑:
1 对于物理机的bios需要特殊的硬件进行调试,我们没有
2 对于不同的系统构架,bios很可能不同,我们偏重于功能性的研读bios,对于理解os的实现来说,
已经足够了;而且,这也会是理解物理bios的一个很好的起点。
另外,我们更多的是关注bios中对PCI和ACPI的实现,对于POST流程,我们只说了一个大概。
下面我们就开始介绍一些基础知识:
the layout and contents of the first Meg of memory[1][2]:
0x0 - 0x3ff: 256个bios中断向量
0x400 - 0x4ff: 255B BDA(BIOS Data Area) 保存bios检测的结果,如:
0x40E: LPT4 I/O base address 或者 EBDA(Extended Bios Data Area)
如果存在EBDA,其值为0x9FC0,查找RSDP的一种方法就是从EBDA查找(AcpiTbFindRsdp)
0x410: Equipment Word
0x472: Soft reset flag 系统启动时会在该地址写入1234h告诉bios下次跳过内存检测
参见i386/i386/locore.s
0x475: Number of hard disk drives(参见boot0)
0x500 - 0x9Fbff: dos, etc
0x9FC00 - 0x9feff: EBDA(Extended Bios Data Area) 768B
0x9ff00 -- 0x9ffff: boot device tables 256B
0xA0000 - 0xAffff: Graphics Video memory (EGA and above)
0xB0000 - 0xBffff: Graphics area for EGA and up
0xC0000 - 0xCffff: additional ROM-BIOS & video memory
0xD0000 - 0xDffff: ROM cartridges
0xE0000 - 0xEffff: ROM cartridges
0xF0000 - 0xFDfff: IBM PC ROM BASIC
0xFE000 - 0xFFFEF: ORIGINAL IBM PC ROM BIOS
0xfe05b : POST Entry Point
0xFFFF0 - 0xFFFF4: Power-up Entry Point(RESET JUMP)
0xffff5 : ASCII Date ROM was built - 8 characters in MM/DD/YY
0xffffe : System Model ID
bios的作用不仅仅只是POST,在os启动之后还会为os提供支持,做幕后英雄。以BSD为例:
1 pci_routing_table_structure: "$PIR" pci_pir_open调用bios_sigsearch在0xe0000-0x100000之间查找"$PIR"(或者"_PIR")。
2 bios32_structure: "_32_" signature
bios32入口,在BSD启动中bios32_init调用bios_sigsearch在0xe0000-0x100000
之间查找"_32_",得到入口地址bios32_entry_point保存在bios32_SDCI中。
然后以0x49435024("$PCI")为参数调用bios32_SDlookup,实际上会调用bios32_entry_point来得到pcibios的入口地址pcibios_protected,保存在PCIbios中。
pci_pir_biosroute中会通过bios32调用pcibios_protected来为一个特定的设备路由中断。
pci配置寄存器的读写:
每个 PCI 外设有一个总线号, 一个设备号, 一个功能号标识. PCI 规范允许单个系统占用多达 256 个总线, 但是因为 256 个总线对许多大系统是不够的, Linux 现在支持 PCI 域. 每个 PCI 域可以占用多达 256 个总线. 每个总线占用 32 个设备, 每个设备可以是一个多功能卡(例如一个声音设备, 带有一个附加的 CD-ROM 驱动)有最多 8 个功能. 因此, 每个功能可在硬件层次被一个 16-位地址或者 key , 标识.
所有的 PCI 设备都有一个至少 256-字节配置寄存器地址空间, 前 64 字节是标准的, 而剩下的是依赖设备的(ldd3:ch12).
第一步是将寄存器的地址写入配置地址寄存器(0xcf8)中,寄存器的地址格式如下(pci22: p52):
31:配置使能位,在进行配置操作时必须将该位设置为1。
30-24:是保留位;
23-16:位是总线号,8位,最大256个总线
15-11位是设备号,5位,最大32个设备
10-8位是功能号,3位,最大8个功能,对于单功能设备,其值为0。
7-2是外部PCI设备的PCI配置空间寄存器偏移量, 6位,最大64个字节???
1-0: 只读,读时必须返回0
注意这里的Bit7-2寄存器偏移量占用6位,只能表示64个,不是有256个字节吗?
这里用到一个技巧,我们接下来会看看到。
第二步是对配置数据寄存器(0xcfc)进行读或写。
我们来回答上面的问题:
假设我们读的是long数据(4字节),那很容易,直接把Bit7-2左移2位
就可以得到8位的寄存器偏移了;
假设我们读的是word数据(2字节)或者就是一个字节,那么末2位从哪里得到呢?
pci把这个偏移给了配置数据寄存器。
我们来看实例:下例是读pci设备d上偏移为addr的配置寄存器:
static uint32_t pci_config_readw(PCIDevice *d, uint32_t addr)
{
outl(0xcf8, 0x80000000 | (d->bus << 16) | (d->devfn << 8) | (addr & 0xfc));
return inw(0xcfc + (addr & 2));
}
读的是一个word,那么addr的最后一位肯定为0.我们把addr用0xfc进行mask得到的是addr的Bit7-2.
丢失的是Bit1, Bit0一定为0; 然后我们读配置数据寄存器时,把Bit1传给了它(addr & 2).
我们来看pci是怎么来理解的:
static uint32_t pci_host_data_readw(void* opaque, pci_addr_t addr)
{
PCIHostState *s = opaque;
uint32_t val;
if (!(s->config_reg & (1 << 31)))
return 0xffff;
val = pci_data_read(s->bus, s->config_reg | (addr & 3), 2);
#ifdef TARGET_WORDS_BIGENDIAN
val = bswap16(val);
#endif
return val;
}
这里参数addr是我们传给inw的值,s->config_reg是我们先前通过
outl(0xcf8, 0x80000000 | (d->bus << 16) | (d->devfn << 8) | (addr & 0xfc));
传给的值,由于传给的值含有0x80000000,所以在if时不会给我们返回0xffff;
另外,通过s->config_reg | (addr & 3)把地址给补齐了。
下面我们开始使用bochs来调试bios(我们讲述的只是post,不涉及setup):
# bochs -f /root/.bochsrc
(0) [0xfffffff0] f000:fff0 (unk. ctxt): jmp far f000:e05b ; ea5be000f0
这里是cpu的第一条指令f000:fff0(对应的物理地址为0xffff0),很简单,就是一个跳转到0xfe05b的指令。
我们来看一下此时cpu寄存器的状态:
<bochs:1> dump_cpu
eax:0x00000000, ebx:0x00000000, ecx:0x00000000, edx:0x00000543
ebp:0x00000000, esp:0x00000000, esi:0x00000000, edi:0x00000000
eip:0x0000fff0, eflags:0x00000002, inhibit_mask:0
cs:s=0xf000, dl=0x0000ffff, dh=0xff009bff, valid=1
ss:s=0x0000, dl=0x0000ffff, dh=0x00009300, valid=1
ds:s=0x0000, dl=0x0000ffff, dh=0x00009300, valid=1
es:s=0x0000, dl=0x0000ffff, dh=0x00009300, valid=1
fs:s=0x0000, dl=0x0000ffff, dh=0x00009300, valid=1
gs:s=0x0000, dl=0x0000ffff, dh=0x00009300, valid=1
ldtr:s=0x0000, dl=0x0000ffff, dh=0x00008200, valid=1
tr:s=0x0000, dl=0x0000ffff, dh=0x00008300, valid=1
gdtr:base=0x00000000, limit=0xffff
idtr:base=0x00000000, limit=0xffff
dr0:0x00000000, dr1:0x00000000, dr2:0x00000000
dr3:0x00000000, dr6:0xffff0ff0, dr7:0x00000400
cr0:0x00000010, cr1:0x00000000, cr2:0x00000000
cr3:0x00000000, cr4:0x00000000
done
这里很多值都为0,为什么edx的值为0x543呢?我也不知道[FIXME]。
我们比较关心的是eflags,eip, cs和idt.
我们继续看跳转后的代码:
<bochs:2> disassemble 0xfe05b 0xfe060
000fe05b: ( ): xor ax, ax ; 31c0
000fe05d: ( ): out 0x0d, al ; e60d
000fe05f: ( ): out 0xda, al ; e6da
为了查看方便,我们还是使用源码bochs-2.3.5/bios/rombios.c:
.org 0xe05b ; POST Entry Point
bios_table_area_end:
post:
xor ax, ax
;; first reset the DMA controllers
out 0x0d,al
out 0xda,al
;; then initialize the DMA controllers
mov al, #0xC0
out 0xD6, al ; cascade mode of channel 4 enabled
mov al, #0x00
out 0xD4, al ; unmask channel 4
这里初始化DMA控制器
;; Examine CMOS shutdown status.
mov AL, #0x0f
out 0x70, AL
in AL, 0x71
;; backup status
mov bl, al
;; Reset CMOS shutdown status.
mov AL, #0x0f
out 0x70, AL ; select CMOS register Fh
mov AL, #0x00
out 0x71, AL ; set shutdown action to normal
;; Examine CMOS shutdown status.
mov al, bl
;; 0x00, 0x09, 0x0D+ = normal startup
cmp AL, #0x00
jz normal_post
cmp AL, #0x0d
jae normal_post
cmp AL, #0x09
je normal_post
检查CMOS关机状态,这里我们会跳转到normal_post
normal_post:
; case 0: normal startup
cli
mov ax, #0xfffe
mov sp, ax
xor ax, ax
mov ds, ax
mov ss, ax
;; zero out BIOS data area (40:00..40:ff)
mov es, ax
mov cx, #0x0080 ;; 128 words
mov di, #0x0400
cld
rep
stosw
对BDA段(0x400-0x4ff)清0,接下来的post过程会在此段内存写入很多控制和状态信息。
;; set all interrupts to default handler
xor bx, bx ;; offset index
mov cx, #0x0100 ;; counter (256 interrupts)
mov ax, #dummy_iret_handler
mov dx, #0xF000
post_default_ints:
mov [bx], ax
inc bx
inc bx
mov [bx], dx
inc bx
inc bx
loop post_default_ints
在0x0-0x3ff段写入256个缺省的中断向量(0xf000ff53,处理函数为dummy_iret_handler),此时中断是关闭的。
;; set vector 0x79 to zero
;; this is used by 'gardian angel' protection system
SET_INT_VECTOR(0x79, #0, #0)
重设0x79号中断
;; base memory in K 40:13 (word)
mov ax, #BASE_MEM_IN_K
mov 0x0413, ax
在0x413处写入Memory size in Kb(0x027f,639)
;; Manufacturing Test 40:12
;; zerod out above
;; Warm Boot Flag 0040:0072
;; value of 1234h = skip memory checks
;; zerod out above
对于0x412和0x472的检测,并没有实现。
;; Printer Services vector
SET_INT_VECTOR(0x17, #0xF000, #int17_handler)
;; Bootstrap failure vector
SET_INT_VECTOR(0x18, #0xF000, #int18_handler)
;; Bootstrap Loader vector
SET_INT_VECTOR(0x19, #0xF000, #int19_handler)
;; User Timer Tick vector
SET_INT_VECTOR(0x1c, #0xF000, #int1c_handler)
;; Memory Size Check vector
SET_INT_VECTOR(0x12, #0xF000, #int12_handler)
;; Equipment Configuration Check vector
SET_INT_VECTOR(0x11, #0xF000, #int11_handler)
;; System Services
SET_INT_VECTOR(0x15, #0xF000, #int15_handler)
设置中断向量,其中int19_handler是启动函数,post的最后阶段就是调用它;
而int18_handler是启动失败的处理函数,设备引导失败时会调用它把控制权重新交给bios
;; EBDA setup
call ebda_post
在0x40E写入0x9FC0,标志EBDA的起始地址。
;; PIT setup
SET_INT_VECTOR(0x08, #0xF000, #int08_handler)
;; int 1C already points at dummy_iret_handler (above)
mov al, #0x34 ; timer0: binary count, 16bit count, mode 2
out 0x43, al
mov al, #0x00 ; maximum count of 0000H = 18.2Hz
out 0x40, al
out 0x40, al
注册System Timer ISR Entry Point
xor ax, ax
mov ds, ax
mov 0x0417, al /* keyboard shift flags, set 1 */
mov 0x0418, al /* keyboard shift flags, set 2 */
mov 0x0419, al /* keyboard alt-numpad work area */
mov 0x0471, al /* keyboard ctrl-break flag */
mov 0x0497, al /* keyboard status flags 4 */
mov al, #0x10
mov 0x0496, al /* keyboard status flags 3 */
/* keyboard head of buffer pointer */
mov bx, #0x001E
mov 0x041A, bx
/* keyboard end of buffer pointer */
mov 0x041C, bx
/* keyboard pointer to start of buffer */
mov bx, #0x001E
mov 0x0480, bx
/* keyboard pointer to end of buffer */
mov bx, #0x003E
mov 0x0482, bx
/* init the keyboard */
call _keyboard_init
初始化键盘
;; mov CMOS Equipment Byte to BDA Equipment Word
mov ax, 0x0410
mov al, #0x14
out 0x70, al
in al, 0x71
mov 0x0410, ax
从cmos中读出并在0x410处写入Equipment Word
;; Parallel setup
SET_INT_VECTOR(0x0F, #0xF000, #dummy_iret_handler)
xor ax, ax
mov ds, ax
xor bx, bx
mov cl, #0x14 ; timeout value
mov dx, #0x378 ; Parallel I/O address, port 1
call detect_parport
mov dx, #0x278 ; Parallel I/O address, port 2
call detect_parport
shl bx, #0x0e
mov ax, 0x410 ; Equipment word bits 14..15 determing # parallel ports
and ax, #0x3fff
or ax, bx ; set number of parallel ports
mov 0x410, ax
并口的设置
;; Serial setup
SET_INT_VECTOR(0x0C, #0xF000, #dummy_iret_handler)
SET_INT_VECTOR(0x14, #0xF000, #int14_handler)
xor bx, bx
mov cl, #0x0a ; timeout value
mov dx, #0x03f8 ; Serial I/O address, port 1
call detect_serial
mov dx, #0x02f8 ; Serial I/O address, port 2
call detect_serial
mov dx, #0x03e8 ; Serial I/O address, port 3
call detect_serial
mov dx, #0x02e8 ; Serial I/O address, port 4
call detect_serial
shl bx, #0x09
mov ax, 0x410 ; Equipment word bits 9..11 determing # serial ports
and ax, #0xf1ff
or ax, bx ; set number of serial port
mov 0x410, ax
串口的设置
;; CMOS RTC
SET_INT_VECTOR(0x1A, #0xF000, #int1a_handler)
SET_INT_VECTOR(0x4A, #0xF000, #dummy_iret_handler)
SET_INT_VECTOR(0x70, #0xF000, #int70_handler)
;; BIOS DATA AREA 0x4CE ???
call timer_tick_post
为CMOS RTC设置中断向量,并检测timer_tick
;; PS/2 mouse setup
SET_INT_VECTOR(0x74, #0xF000, #int74_handler)
;; IRQ13 (FPU exception) setup
SET_INT_VECTOR(0x75, #0xF000, #int75_handler)
;; Video setup
SET_INT_VECTOR(0x10, #0xF000, #int10_handler)
;; PIC
mov al, #0x11 ; send initialisation commands
out 0x20, al
out 0xa0, al
mov al, #0x08
out 0x21, al
mov al, #0x70
out 0xa1, al
mov al, #0x04
out 0x21, al
mov al, #0x02
out 0xa1, al
mov al, #0x01
out 0x21, al
out 0xa1, al
mov al, #0xb8
out 0x21, AL ;master pic: unmask IRQ 0, 1, 2, 6
#if BX_USE_PS2_MOUSE
mov al, #0x8f
#else
mov al, #0x9f
#endif
out 0xa1, AL ;slave pic: unmask IRQ 12, 13, 14
初始化两个8259A中断控制器
call rombios32_init
这里是配置pci的关键代码,后面我们会着重介绍它。
call _init_boot_vectors
call rom_scan
从0xC0000到0xE0000(包含),以2KB递增,检测是否以0xAA55起始并满足校验,
然后调用ROM initialization entry point,这里的调用使用:
mov bp, sp ;; Call ROM init routine using seg:off on stack
db 0xff ;; call_far ss:[bp+0]
db 0x5e
db 0
很有意思[FIXME]。
call _print_bios_banner
;;
;; Floppy setup
;;
call floppy_drive_post
;;
;; Hard Drive setup
;;
call hard_drive_post
;;
;; ATA/ATAPI driver setup
;;
call _ata_init
call _ata_detect
sti ;; enable interrupts
int #0x19
使能中断,并调用int19h,完成启动。
至此,我们对bios的大致流程有了一个大概的认识。
下面是我们的重点:rombios32_init。
它即是一段汇编码又是一段c代码:汇编码实现使能A20,设置idt,gdt以及段寄存器,并切换到保护模式,
然后把c代码的rombios32_init拷贝到0x40000,接着调用它;调用完成后把0x40000开始的内存清0,
重设段寄存器,恢复到实模式,重设idt
我们来看c代码的rombios32_init,源码位于:
bochs-2.3.5/bios/rombios32.c
void rombios32_init(void)
{
BX_INFO("Starting rombios32\n");
ram_probe();
cpu_probe();
smp_probe();
pci_bios_init();
if (bios_table_cur_addr != 0) {
mptable_init();
if (acpi_enabled)
acpi_bios_init();
bios_lock_shadow_ram();
}
}
ram_probe负责内存容量的检测,cpu_probe负责cpu的检测,smp_probe通过发送SIPI来检测系统中的cpu个数
void smp_probe(void)
{
uint32_t val, sipi_vector;
smp_cpus = 1;
if (cpuid_features & CPUID_APIC) {
/* enable local APIC */
val = readl(APIC_BASE + APIC_SVR);
val |= APIC_ENABLED;
writel(APIC_BASE + APIC_SVR, val);
writew((void *)CPU_COUNT_ADDR, 1);
/* copy AP boot code */
memcpy((void *)AP_BOOT_ADDR, &smp_ap_boot_code_start,
&smp_ap_boot_code_end - &smp_ap_boot_code_start);
/* broadcast SIPI */
writel(APIC_BASE + APIC_ICR_LOW, 0x000C4500);
sipi_vector = AP_BOOT_ADDR >> 12;
writel(APIC_BASE + APIC_ICR_LOW, 0x000C4600 | sipi_vector);
delay_ms(10);
smp_cpus = readw((void *)CPU_COUNT_ADDR);
}
BX_INFO("Found %d cpu(s)\n", smp_cpus);
}
SVR: Spurious Interrupt Vector Register
AP: Application Processor
BSP: Boot-Strap Processor
IPI: interprocessor interrupts
SIPI: Startup IPI
ICR: Interrupt Command Register
首先向local APIC的SVR写入APIC_ENABLED标志位使能; 在CPU_COUNT_ADDR(0xf000)处写入1标志自己是一个cpu;
然后在AP_BOOT_ADDR(0x10000)处拷贝入ap启动代码,该代码位于bios/rombios32start.S:42
.code16
smp_ap_boot_code_start:
xor %ax, %ax
mov %ax, %ds
incw CPU_COUNT_ADDR
1:
hlt
jmp 1b
smp_ap_boot_code_end:
代码很简单,就是在CPU_COUNT_ADDR处加1,然后hlt;那么ap怎么找到这个入口的呢?
首先向其它的ap(不包括自己)发送一条INIT(Intel v3:p288)中断,初始化(apic_init_ipi)lapic的寄存器;
然后以AP_BOOT_ADDR >> 12为中断向量发送SIPI,然后其它ap会通过apic_startup设置eip为0,cs_base为AP_BOOT_ADDR |
|