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

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 ;一共101个扇区,用户程序在硬盘中的逻辑扇区号 SECTION mbr vstart=0x7c00 align=;以16位对齐
;cs和IP已经运行到这里,不用再设置了
mov ax,
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,[];program_len分别放在ax和bx;
mov dx,[];从内存读数据,不加段前缀的默认是ds;
mov bx,
div bx;ax = 用户程序的扇区个数 dx=扇区余数,也就是最后不满一个扇区内偏移
cmp dx,; test dx,dx
jnz cantDiv;不能被整除,说明有数据不满一个扇区的数据,但也要占用一个扇区的空间
dec ax;扇区数减一:前面已经读了一个扇区。 cantDiv:
cmp ax,;已经读完了,可以直接重定位
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+]
call reallocaddress
mov [bx],ax;
add bx,
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,;低16位地址的低4位去掉,高4位补零,得到段基址;ax=0x0002
ror dx,;高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,;每次读一个扇区
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,; ;准备好了,开始读磁盘
readw:
in ax,dx;
mov [bx],ax;
add bx,;每次读2byte
loop readw; pop di
pop si
pop dx
pop cx
pop bx
pop ax ret phy_address dd 0x10000;用户程序拷贝到内存地址 times - ($-$$) db ;
dw 0xaa55

2、用户程序

;用户程序
;段的数目并未限制,用户可根据需求自行创建 ;-------------------------------------------------------------------------------
SECTION header vstart=;vstart=0连着写,不能有空格
program_len dd program_end;
code_entry dw start;变量偏移0x4;
dd section.code1.start;code1段基址:变量偏移0x6 reallocate_item dw (header_end-code1Segment)/ ;每个段偏移都是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= vstart=; ;-------------------------------------------------------------------------------
SECTION use1Data align= vstart=; use1Data_end:
;-------------------------------------------------------------------------------
SECTION code1 align= vstart=; ;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,
xor bh,bh
mov bl,0x04
mov cx, data1_end - msg;cx保存字符串长度
mov dh,;显示的行号
mov dl,;显示的列号
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= vstart= msg db 'are you ready?', data1_end:
;-------------------------------------------------------------------------------
SECTION stack1 align= vstart=; resb ; reserve byte,保留/分配256byte空间
stacker_pointer: ;栈底放在高地址
;-------------------------------------------------------------------------------
SECTION tail align=; 这个段没有vstart = 0,那就从开头计算偏移,也就是SECTION header开始算; program_end:

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

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

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

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

x86架构: 硬件启动过程分析(附引导启动代码)的更多相关文章

  1. Linux x86架构下ACPI PNP Hardware ID的识别机制

    转:https://blog.csdn.net/morixinguan/article/details/79343578 关于Hardware ID的用途,在前面已经大致的解释了它的用途,以及它和AC ...

  2. X86架构下Linux启动过程分析

    1.X86架构下的从开机到Start_kernel启动的整体过程 这个过程简要概述为: 开机-->BIOS-->GRUB/LILO-->Linux Kernel 其执行的流程图和重要 ...

  3. Linux内核分析(三)内核启动过程分析——构造一个简单的Linux系统

    一.系统的启动(各历史节点) 在最开始的时候,计算机的启动实际上依靠一段二进制码,可以这么理解,他并不是一个真正的计算机启动一道程序.计算机在开始加电的时候几乎是没有任何用处的,因为RAM芯片中包括的 ...

  4. u-boot 源码分析(1) 启动过程分析

    u-boot 源码分析(1) 启动过程分析 文章目录 u-boot 源码分析(1) 启动过程分析 前言 配置 源码结构 api arch board common cmd drivers fs Kbu ...

  5. 【Bootloader】bootloader启动过程分析

    Boot Loader启动过程分析 一.    Boot Loader的概念和功能 1.嵌入式Linux软件结构与分布在一般情况下嵌入式Linux系统中的软件主要分为以下及部分: (1)引导加载程序: ...

  6. 羽夏看Linux内核——引导启动(上)

    写在前面   此系列是本人一个字一个字码出来的,包括示例和实验截图.如有好的建议,欢迎反馈.码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作.如想转载,请把我的转载信息附在文章后面,并 ...

  7. Linux进程启动过程分析do_execve(可执行程序的加载和运行)---Linux进程的管理与调度(十一)

    execve系统调用 execve系统调用 我们前面提到了, fork, vfork等复制出来的进程是父进程的一个副本, 那么如何我们想加载新的程序, 可以通过execve来加载和启动新的程序. x8 ...

  8. S3C6410启动过程分析

    S3C6410支持多种存储设备,包括片上的Internal ROM.Internal SRAM和片外的Flash/ROM.DRAM.多种启动设备形成多种启动模式,分析如下. 本文为作者原创,转载请注明 ...

  9. linux-2.6.22.6内核启动分析之head.S引导段代码

    学习目标: 了解arch/arm/kernel/head.S作为内核启动的第一个文件所实现的功能! 前面通过对内核Makefile的分析,可以知道arch/arm/kernel/head.S是内核启动 ...

随机推荐

  1. flex-direction和flex-wrap

    当外层容器使用flex布局,并且把flex-direction设置成colum的时候,内层容器的宽度会跟外层容器的宽度保持一致. 在浏览器上的效果如下: 当把外层容器的纵向布局不适用flex-dire ...

  2. 管理用户和组 、 tar备份与恢复 、 cron计划任务-云计算学习(4)

    配置用户和组账号 问题 本例要求创建下列用户.组以及组的成员关系: 新建用户 alex,其用户ID为3456,密码是flectrag 创建一个名为 adminuser 的组 创建一个名为 natash ...

  3. 解决Linux搜狗输入法工具栏无法移动

    问题的出现 前两天一位朋友(@午后下午茶)发现一个有趣的情况: 为了复现bug,我把自己的输入法工具栏也挪到了顶栏,果然也无法挪动了 解决方法 原理不明,但解决方法很简单. 如图所示,随便找个输入框打 ...

  4. Scala 基础(十六):泛型、类型约束-上界(Upper Bounds)/下界(lower bounds)、视图界定(View bounds)、上下文界定(Context bounds)、协变、逆变和不变

    1 泛型 1)如果我们要求函数的参数可以接受任意类型.可以使用泛型,这个类型可以代表任意的数据类型. 2)例如 List,在创建 List 时,可以传入整型.字符串.浮点数等等任意类型.那是因为 Li ...

  5. scala 数据结构(十一):流 Stream、视图 View、线程安全的集合、并行集合

    1 流 Stream stream是一个集合.这个集合,可以用于存放无穷多个元素,但是这无穷个元素并不会一次性生产出来,而是需要用到多大的区间,就会动态的生产,末尾元素遵循lazy规则(即:要使用结果 ...

  6. web 部署专题(四):压力测试(二)压力测试实例 flask 四种wsgi方式对比(tornado,Gunicorn,Twisted,Gevent)

    使用工具:siege 代码结构: hello.py templates |--hello.html hello.py代码: from flask import Flask, render_templa ...

  7. SpringCloud或SpringBoot+Mybatis-Plus利用AOP+mybatis插件实现数据操作记录及更新对比

    引文 本文主要介绍如何使用Spring AOP + mybatis插件实现拦截数据库操作并根据不同需求进行数据对比分析,主要适用于系统中需要对数据操作进行记录.在更新数据时准确记录更新字段 核心:AO ...

  8. 图解:如何实现最小生成树(Prim算法与Kruskal算法)

    这是图算法的第四篇文章 图解:如何实现最小生成树 文章目录: 1.概念和性质 2.思路探索 3.Kruskal算法 4.Prim算法 5.代码实现 1.概念和性质 今天我们考虑的模型是加权无向图,问题 ...

  9. 关于Object.defineProperty

    讲解大致会根据下图展开     本文部分参考了书籍<你不知道的javascript>上卷 对象的定义与赋值 经常使用的定义与赋值方法obj.prop =value或者obj['prop'] ...

  10. three.js 数学方法之Matrix3

    今天郭先生来说一说three.js的三维矩阵,这块知识需要结合线性代数的一些知识,毕业时间有点长,线性代数的知识大部分都还给了老师.于是一起简单的复习了一下.所有的计算都是使用列优先顺序进行的.然而, ...