我们并没有实际去研读物理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 |
|