zoukankan      html  css  js  c++  java
  • x86架构: 硬件启动过程分析(附引导启动代码)

    用户按下开机键,几秒的时间,都经历了啥?

    1、cpu各个寄存器赋初始值,cs.base=0xffff0000, eip=0xfff0,其他寄存器都是0,这时cs:ip得到的物理地址:0xfffffff0;

         cpu上电后为啥会把cs:ip赋成这种初始值了? 可能是希望把BIOS-ROM放在可寻址4GB最高端,给操作系统和用户程序大段完整的RAM空间,便于后者在运行时的内存管理

     

    2、cpu跳转到0xffff0执行。但由于该地址距离0xfffff(实模式下内存空间只有1M)仅16byte,空间十分有限,无法执行复杂逻辑,只能jmp到其0xf000:e05b继续执行;

    3、0xf000:e05b任然是BIOS的地址,继续执行检测代码,看看内存(RAM)、显示器、键盘、鼠标、硬盘等外设是否完好。如有问题,会发出长短不等的滴滴声响,可凭此判断故障类型

     4、外设检测完,如果一切正常,会查找用户设置的启动顺序。普通用户首次安装OS时一般选择从CD/DVD启动,装OS;装好后取出光盘,BIOS会自动从磁盘加载MBR到0x7c00;

     5、加载MBR到0x7c00后,jmp到这里继续执行;由于只加载一个扇区,能执行的代码不超过510字节(还有2字节是扇区结尾的标识:0xaa55),能干的事也有限,所以MBR一般会继续从磁盘其他地方把os代码都拷贝到内存,同时重定位代码,完成os的加载;

    6、继续jmp到os代码执行;

    这6步中,1-4部不用我们操心,厂家的产品在出厂前已经做好;第5部,从磁盘的0柱面、0磁头、1扇区加载MBR到内存0x7c00处也BIOS干的,不需要开发人员操心;真正需要开发人员编写代码的地方:

    • MBR的代码,这部分代码被bios加载到内存后需要做什么?
    • MBR代码能运行的代码不超过510字节,真正的os肯定不止这点代码,剩余代码怎么办?

    既然MBR能运行的代码不超过510字节,能干的活有限,那么干脆简单点,把os或用户程序剩余代码加载到内存,完成重定位,再跳到这些代码执行,具体代码如下:

    1、MBR代码

    ;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

    2、用户程序

    ;用户程序
    ;段的数目并未限制,用户可根据需求自行创建
    
    ;-------------------------------------------------------------------------------
    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 'are you ready?', 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:   

    说明: (1)SECTION用于定于段,没有数量限制,开发人员可根据需求取舍

           (2)vstart=0表示该段内的标识都从0开始计算偏移。如果没有 vstart=0,那么段内标识比如msg、start等都从程序开始处计算偏移;

                    vstart=0千万要紧挨着,不能有个空格,不能有个空格,不能有个空格,重要的事情说三遍。否则这种声明无效,段内标识的偏移还是会从程序开头处计算,导致后续逻辑出错

          (3)段的数量没限制,但是建议把代码段和数据段分开,各种变量尽量在数据段声明;代码段声明的变量因未隔离开,容易被cpu当成代码执行,导致异常或逻辑错乱

          (4)MBR为什么要用0xaa55了? 0xaa55=0b 1010 1010  0101  0101,看出来有啥特点了么?  0和1交叉呈现,就像梳子一样;奇偶校验总是为偶数,a和5与是0,或是f

  • 相关阅读:
    【LeetCode】面试题59
    【LeetCode】面试题57
    【LeetCode】面试题57
    Chrome查看Markdown并转为PDF
    【LeetCode】232. 用栈实现队列
    【LeetCode】27. 移除元素
    【LeetCode】1323. 6 和 9 组成的最大数字
    【LeetCode】167. 两数之和 II
    第14条:在公有类中使用访问方法而非公有域
    Android Studio项目中三种依赖的添加方式
  • 原文地址:https://www.cnblogs.com/theseventhson/p/13030374.html
Copyright © 2011-2022 走看看