<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="http://gentoo-zh.org/extern.php?action=feed&amp;tid=482&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / x86架构： 硬件启动过程分析（附引导启动代码）]]></title>
		<link>http://www.gentoo-zh.org/viewtopic.php?id=482</link>
		<description><![CDATA[x86架构： 硬件启动过程分析（附引导启动代码） 最近发表的帖子。]]></description>
		<lastBuildDate>Wed, 12 Oct 2022 04:50:54 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[x86架构： 硬件启动过程分析（附引导启动代码）]]></title>
			<link>http://www.gentoo-zh.org/viewtopic.php?pid=489#p489</link>
			<description><![CDATA[<p>用户按下开机键，几秒的时间，都经历了啥？</p><p>1、cpu各个寄存器赋初始值，cs.base=0xffff0000, eip=0xfff0，其他寄存器都是0，这时cs:ip得到的物理地址：0xfffffff0;</p><p>&#160; &#160; &#160;cpu上电后为啥会把cs:ip赋成这种初始值了？ 可能是希望把BIOS-ROM放在可寻址4GB最高端，给操作系统和用户程序大段完整的RAM空间，便于后者在运行时的内存管理<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200607114438577-1961598627.png" alt="FluxBB bbcode 测试" /></span></p><p>2、cpu跳转到0xffff0执行。但由于该地址距离0xfffff（实模式下内存空间只有1M）仅16byte，空间十分有限，无法执行复杂逻辑，只能jmp到其0xf000:e05b继续执行；<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200602124205120-1474881594.png" alt="FluxBB bbcode 测试" /></span></p><p>3、0xf000:e05b任然是BIOS的地址，继续执行检测代码，看看内存（RAM）、显示器、键盘、鼠标、硬盘等外设是否完好。如有问题，会发出长短不等的滴滴声响，可凭此判断故障类型<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200602124654540-986451939.png" alt="FluxBB bbcode 测试" /></span></p><p> 4、外设检测完，如果一切正常，会查找用户设置的启动顺序。普通用户首次安装OS时一般选择从CD/DVD启动，装OS；装好后取出光盘，BIOS会自动从磁盘加载MBR到0x7c00；<br /><span class="postimg"><img src="https://img2020.cnblogs.com/blog/2052730/202006/2052730-20200602124939352-1544171368.png" alt="FluxBB bbcode 测试" /></span></p><p> 5、加载MBR到0x7c00后，jmp到这里继续执行；由于只加载一个扇区，能执行的代码不超过510字节（还有2字节是扇区结尾的标识：0xaa55），能干的事也有限，所以MBR一般会继续从磁盘其他地方把os代码都拷贝到内存，同时重定位代码，完成os的加载；</p><p>6、继续jmp到os代码执行；</p><p>这6步中，1-4部不用我们操心，厂家的产品在出厂前已经做好；第5部，从磁盘的0柱面、0磁头、1扇区加载MBR到内存0x7c00处也BIOS干的，不需要开发人员操心；真正需要开发人员编写代码的地方：</p><p>&#160; &#160; MBR的代码，这部分代码被bios加载到内存后需要做什么？<br />&#160; &#160; MBR代码能运行的代码不超过510字节，真正的os肯定不止这点代码，剩余代码怎么办？</p><p>既然MBR能运行的代码不超过510字节，能干的活有限，那么干脆简单点，把os或用户程序剩余代码加载到内存，完成重定位，再跳到这些代码执行，具体代码如下:</p><p>1、MBR代码</p><div class="codebox"><pre class="vscroll"><code>;MBR 主引导扇区
;开机上电后，BIOS会自动从0x7c00处执行

    lba_num equ 100;一共101个扇区，用户程序在硬盘中的逻辑扇区号
    
SECTION mbr vstart=0x7c00 align=16;以16位对齐
                                  ;cs和IP已经运行到这里，不用再设置了
        mov ax,0
        mov ss,ax;堆栈段从0开始
        mov sp,ax;
        
        mov ax,[cs:phy_address];目前在cs段，如果不写，默认读ds段；
        mov dx,[cs:phy_address+0x2]
        mov bx,0x10
        div bx;相当于右移4bit，得到0x1000，就是段地址，放在ax
        mov ds,ax; 
        xor bx,bx;  
        
        mov si,lba_num
        xor di,di
        call read_disk;先读第一个扇区，把用户程序的头部加载到内存，才能得到重定位表
        
        mov ax,[0];program_len分别放在ax和bx;
        mov dx,[2];从内存读数据，不加段前缀的默认是ds；
        mov bx,512
        div bx;ax = 用户程序的扇区个数  dx=扇区余数，也就是最后不满一个扇区内偏移
        cmp dx,0; test dx,dx
        jnz cantDiv;不能被整除，说明有数据不满一个扇区的数据，但也要占用一个扇区的空间
        dec ax;扇区数减一：前面已经读了一个扇区。
        
        
cantDiv:
        cmp ax,0;已经读完了，可以直接重定位
        jz  realloc;
        mov cx,ax;剩余扇区数放入cx，方便后续loop
        push ds
        
Continue_Read:
        inc si
        mov ax,ds
        add ax,0x20;基址增加0x20，相当于增加512byte，比如：ds:bx = 0000:0000 = 00000; ds:bx = 0020:0000 = 0200+0000=0x0200=512byte 
        mov ds,ax;往高地址挪一个扇区512byte
        xor bx,bx;偏移清零，通过段基址挪动
        call read_disk;相当于寄存器传参
        loop Continue_Read
        pop ds


;---------------上面都是把数据从磁盘读到内存，下面开始重定位------------------------------------
;先计算出用户程序code_entry在内存的绝对地址
realloc:
        mov ax,[0x06];默认是ds段，此时已是0x1000；code_entry的section.code1.start低2字节
        mov dx,[0x08];code_entry的section.code1.start高2字节
        call reallocaddress
        mov [0x06],ax;把内存中的物理地址写回去，这次得到绝对物理地址了；
        ;mov [0x06],ds
        mov cx,[0x0a];5个段需要重定位
        mov bx,0x0c;
        
;用户程序每个section都计算出内存的绝对地址，然后写回去        
reallocLoop:
        mov ax,[bx]
        mov dx,[bx+2]
        call reallocaddress
        mov [bx],ax;
        add bx,4
        loop reallocLoop
        
        jmp far [0x04];内存操作默认以ds基址，这里是0x10000；跳转到用户程序start变量地址
        ;mov ax, [0x04];得到offset，就是start的偏移地址
        ;jmp 0x1000:ax

;dx:ax 32位偏移地址，寄存器传参
;输出16位段基址,保存在ax
reallocaddress:
        push dx
        add ax,[cs:phy_address];注意：目前在cs段，不加从内存读数据默认用ds，此处为用户程序;ax=0x0000+[0x10006]=0x0020;
        add dx,[cs:phy_address+0x2];dx=0x0001
        shr ax,4;低16位地址的低4位去掉，高4位补零，得到段基址;ax=0x0002
        ror dx,4;高16位地址循环右移；dx=0x1000
        and dx,0xf000;取出最需要的4bit，其他清零；dx=0x1000
        or ax,dx;ax=0x1002
        pop dx 
        ret


;ds:bx 从硬盘读数据到该物理地址
;di, si 是逻辑扇区号：逻辑扇区只用28位，所以di有4位是不用的；si是逻辑扇区低16位
;可以通过int 0x13中断读取，也可以通过磁盘控制器读取；
read_disk:
        push ax
        push bx
        push cx
        push dx
        push si
        push di

        ;https://www.cnblogs.com/mlzrq/p/10223060.html 详细说明
        mov dx,0x1f2;磁盘端口,指定读取或写入的扇区数
        mov al,1;每次读一个扇区
        out dx,al;往端口写入数据
        
        inc dx;0x1f3  lba地址的低8位，就是0-7位
        mov ax,si;
        out dx,al;先把低8位写入端口，因为用户程序被写入了磁盘100号扇区，所以调用函数传参数di=100
        
        inc dx;0x1f4  lba地址的中8位，就是8-15位
        mov al,ah
        out dx,al;
        
        inc dx;0x1f5  lba地址的高8位，就是16-23位
        mov ax,di;
        out dx,al;
        
        ;上面3个已经把前面24位填满，这里填最高4位
        inc dx;0x1f6  lba地址的前4位，就是24-27位
        mov al,0xe0; 高4位是各种标志位： 0 CHS,1 LBA; 1; 0 从  1 主; 0； 这里是e；
        or al,ah
        out dx,al

        inc dx;0x1f7
        mov al,0x20;发送读扇区的请求：0x20
        out dx,al
        
;------------------------------    and al,0x88 逻辑上出错，先屏蔽试试    
waits:
        in al,dx; 从0x1f7读取磁盘状态，一共有8位；第7位：1表示busy   第3位：1表示准备好读写操作，所以在0xxx1xxx的时候才能读写，其他状态都不行；
        and al,0x88;第7位和第3位保持不变，其他清零
        cmp al,0x08;
        jnz waits;状态不等于0x08，说明没准备好，继续等待
        
        
        mov dx,0x01f0;数据端口,16位，需要ax接数据；每个扇区512byte，每次读2byte，要读256次
        mov cx,256;
        
        ;准备好了，开始读磁盘
readw:
        in ax,dx;
        mov [bx],ax;
        add bx,2;每次读2byte
        loop readw;

        pop di
        pop si
        pop dx
        pop cx
        pop bx
        pop ax

        ret
        
        


        phy_address          dd 0x10000;用户程序拷贝到内存地址
                 
        times    510 - ($-$$) db 0; 
                             dw 0xaa55</code></pre></div><p>2、用户程序</p><div class="codebox"><pre class="vscroll"><code>;用户程序
;段的数目并未限制，用户可根据需求自行创建

;-------------------------------------------------------------------------------
SECTION header vstart=0;vstart=0连着写，不能有空格
        program_len                dd    program_end; 
        code_entry                dw    start;变量偏移0x4；
                                dd    section.code1.start;code1段基址：变量偏移0x6
                                
        reallocate_item            dw    (header_end-code1Segment)/4 ;每个段偏移都是dd=4byte,变量偏移0xa
        
        ;重定位表，记录重要段相对于程序起始位置的偏移
        code1Segment            dd  section.code1.start;变量偏移0xc
        data1Segment            dd  section.data1.start;变量偏移0x10  本section在文件中的真实偏移量（真实地址）,或则说相对开始的偏移地址
        stack1Segment            dd  section.stack1.start;变量偏移0x14
        use1Segment            dd  section.use1.start;变量偏移0x18
        use1DataSegment        dd  section.use1Data.start;变量偏移0x1c
header_end:   ;有vstart = 0，header_end从vstart = 0开始算偏移


;-------------------------------------------------------------------------------
SECTION use1 align=16 vstart=0; 


;-------------------------------------------------------------------------------
SECTION use1Data align=16 vstart=0; 


use1Data_end:
;-------------------------------------------------------------------------------
SECTION code1 align=16 vstart=0; ;vstart=0连这些，不能有空格
;直接调用BIOS例程在显示器打印
start:
    
    mov ax,[stack1Segment];初始化堆栈
    mov ss,ax
    mov ax,stacker_pointer;
    mov sp,ax;

    xor ah,ah
    mov al,0x03
    int 0x10;调用bios的0x10号中断清屏
    
    ;AL=写模式，BH=页码，BL=颜色，CX=字符串长度，DH=行，DL=列，ES:BP=字符串偏移量
    ;https://zh.wikipedia.org/wiki/INT_10H 有详细说明
    mov ah,0x13
    mov al,1
    xor bh,bh
    mov bl,0x04
    mov cx, data1_end - msg;cx保存字符串长度
    mov dh,12;显示的行号
    mov dl,25;显示的列号
    mov bp,msg; es:bp指向需要打印的字符串
    push ax
    mov ax,[data1Segment]
    ;mov ax,cs;
    mov es,ax;es:bp 为串首地址
    pop ax
    int 0x10
    
    hlt;程序待机

;-------------------------------------------------------------------------------
SECTION data1 align=16 vstart=0

        msg db &#039;are you ready?&#039;, 0

data1_end:
;-------------------------------------------------------------------------------
SECTION stack1 align=16 vstart=0;

        resb 256; reserve byte，保留/分配256byte空间
stacker_pointer: ;栈底放在高地址
;-------------------------------------------------------------------------------
SECTION tail align=16; 这个段没有vstart = 0,那就从开头计算偏移，也就是SECTION header开始算；

program_end:</code></pre></div><p>说明: (1）SECTION用于定于段，没有数量限制，开发人员可根据需求取舍</p><p>&#160; &#160; &#160; &#160;（2）vstart=0表示该段内的标识都从0开始计算偏移。如果没有 vstart=0，那么段内标识比如msg、start等都从程序开始处计算偏移；</p><p>&#160; &#160; &#160; &#160; &#160; &#160; &#160; &#160; vstart=0千万要紧挨着，不能有个空格，不能有个空格，不能有个空格，重要的事情说三遍。否则这种声明无效，段内标识的偏移还是会从程序开头处计算，导致后续逻辑出错</p><p>&#160; &#160; &#160; （3）段的数量没限制，但是建议把代码段和数据段分开，各种变量尽量在数据段声明；代码段声明的变量因未隔离开，容易被cpu当成代码执行，导致异常或逻辑错乱</p><p>&#160; &#160; &#160; （4）MBR为什么要用0xaa55了？ 0xaa55=0b 1010 1010&#160; 0101&#160; 0101，看出来有啥特点了么？&#160; 0和1交叉呈现，就像梳子一样；奇偶校验总是为偶数，a和5与是0，或是f；</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Wed, 12 Oct 2022 04:50:54 +0000</pubDate>
			<guid>http://www.gentoo-zh.org/viewtopic.php?pid=489#p489</guid>
		</item>
	</channel>
</rss>
