x86汇编语言——程序加载和硬盘访问
启动过程
CPU的多个引脚中,有一个RESET用于接收复位信号,当处理器加点或者RESET引脚电平由低到高时,处理器汇之星硬件的初始化。
对于8086来说,开机加电复位后,寄存器重置,CS被置位0xFFFF,其余寄存器被置位0x0000(8086之后的处理器并未延续)
(也有的CS被置位0xF000,IP寄存器被置位0xFFF0)
Intel 8086 为处理器的系统中
-
ROM 占据着整个内存空间顶端的 64KB,物理地址范围是 0xF0000~0xFFFFF,里面固化了开机时要执行的指令(硬件诊断。检测和初始化);
-
DRAM 占据着较低端的 640KB,地址范围是 0x00000~0x9FFFF;
-
还有一部分,分给了其他外围设备
![在这里插入图片描述]()
如果计算机的设置是从硬盘启动,那么, ROM-BIOS 将读取硬盘主引导扇区的内容,将它加载到内存地址 0x0000:0x7c00 处(也就是物理地址 0x07C00)
对硬盘的索引,应 当按照磁头->磁道->扇区(面->道->扇)的顺序,其中磁头编号从0开始,磁道编号从0开始,而扇区编号从1开始,即主引导扇区的编号为0面0道1扇区

8086开机复位后所执行的第一条指令位于0xFFFF0处,是一个跳转指令跳转到0x0000:0x7c00
jmp 0x0000:0x7c00
硬盘和显卡访问
主引导扇区
一个扇区的尺寸是 512 字节,可以看成一个数据块,硬盘是一个典型的块(Block)设备
采用磁头、磁道和扇区这种模式来访问硬盘的方法称为 CHS 模式 ,后来引入了逻辑块地址(Logical Block Address, LBA)
ROM-BIOS 将试图读取硬盘的 0 面 0 道 1 扇区,即主引导扇区(MainBoot Sector, MBR)
主引导扇区数据有 512 字节, ROM-BIOS 程序将它加载到逻辑地址 0x0000:0x7c00处,也就是物理地址 0x07c00 处,然后判断它是否有效 。引导扇区最后两字节应当是 0x55 和 0xAA。 ROM-BIOS 程序首先检测这两个标志,如果主引导扇区有效, 则以一个段间转移指令 jmp 0x0000:0x7c00 跳到那里继续执行
MBR、EBR、DBR和OBR是什么
MBR 是主引导记录, Master 或 Main Boot Record ,它存在于整个硬盘最开始的那个扇区
在 MBR 引导扇区中的内容是:
- 446 字节的引导程序及参数:
- 64 字节的分区表:
- 2 字节结束标记 Ox55 和 Ox础。
MBR 分区表中可容纳 4 个分区,MBR 引导程序开始遍历这 4 个分区,想找到合适的人选并把系统控制权交给他。
通常情况下这个“次引导程序”就是操作系统提供的加载器,由该加载器完成操作系统的自举,最终使控制权交付给操作系统内核
为了让 MBR 知道哪里有操作系统,我们在分区时,如果想在某个分区中安装操作系统,就用分区工具将该分区设直为活动分区,设置活动分区的本质就是把分区表中该分区对应的分区表项中的活动标记为0x80.。
MBR 在分析分区表时通过辨识“活动分区”的标记 0x80 开始找活动分区,如果找到了,就将 CPU 使用权交给此分区上的引导程序,此引导程序通常是内核加载器。为了 MBR 方便找到活动分区上的内核加载器,内核加载器的入口地址也必须在固定的位置,这个位置就是各分区最开始的扇区。“各分区起始的扇区”中存放的是操作系统引导程序一一内核加载器,因此该扇区称为操作系统引导扇区,其中的引导程序(内核加载器〉称为操作系统引导记录 OBR。
在 DOS 时代只有 4 个分区,不存在扩展分区,这 4 个分区都相当于主分区,所以各主分区最开始的扇区称为 DBR 引导扇区 。 后来有了扩展分区之后,无论分区是主分区,还是逻辑分区,为了兼容,分区最开始的扇区都作为 DOS 引导扇区。但是其他操作系统如 UNIX, Linux 等为了兼容 MBR 也传承了这个习俗,都将各分区最开始的扇区作为自己的引导扇区,在里面存放自己操作系统的引导程序。由于现在这个“分区最开始的扇区”引导的操作系统类型太多了,而且 DOS 还退出历史舞台了,所以 DBR 也称为 OBR。
DBR、 OBR 、 MBR、 EBR 都包含引导程序,因此它们都称为引导扇区,只要该扇区中存在可执行的程序,该扇区就是可引导扇区。若该扇区位于整个硬盘最开始的扇区,并且以 Ox55 和 Ox剧结束, BIOS就认为该扇区中存在 MBR,该扇区就是 MBR 引导扇区。若该扇区位于各分区最开始的扇区,井且以 0x55和 0xaa结束, MBR 就认为该扇区中有操作系统引导程序 OBR,该扇区就是 OBR 引导扇区。

MBR 位于整个硬盘最开始的块, EBR 位于每个子扩展分区,各子扩展分区中只有一个逻辑分区。 MBR 和 EBR 位于分区之外的扇区,而 OBR 则属于主分区和逻辑分区最开始的扇区,每个主分区和逻辑分区中都有 OBR 引导扇区。
显卡访问
显卡的两种工作模式:文本模式和图形模式
由于历史原因,计算机加电自检之后都会把自己初始化到80X25的文本模式,每屏2000个字符,占据0xB8000-0xBFFFF这段物理地址

80X25文本模式下的颜色表

加载程序
加电启动后,BIOS会将硬盘的首扇区装载到0X7c00处,并跳转到此处开始执行指令,此处往往是开机引导程序和硬盘分区的所在,通过引导程序进一步跳转到操作系统,将操作系统装载到内存中,之后有操作系统接管整个计算机。
由此可以看出,如果只需要在裸机上执行程序,实际上并不一定需要操作系统的参与,直接将需要运行的程序卸载硬盘的第一扇区即可,但是一般来说,应用程序的大小会超过一个扇区,并且需要对程序的内存是用进行规划,因而需要手动实现一个程序加载器,将多个扇区的用户程序加载到内存中来。
读取硬盘
IO接口能够实现处理器和外围设备的双向通信
-
通过总线技术,能够实现多个IO设备和处理器相连
-
ICH芯片(输入输出控制设备集中器)负责协调不同IO接口对处理器的访问,即南桥
处理器通过端口和外围设备通信,端口本质上就是寄存器,区别仅仅在于这些寄存器位于IO接口电路中,每个IO接口都可能拥有多个端口,分别用于不同的目的
端口在不同的计算机系统中有不同的实现方式
- 端口映射,将端口映射到内存空间中
- 独立编址,处理器的地址线连接内存和IO接口,处理器有特殊的引脚M/IO#,#表示低电平有效
所有端口都是统一编号的,在intel系统中,只允许65536个端口存在,通过in和out指令访问
通过IO端口读取硬盘数据的过程,以LBA28访问(即28比特表示逻辑扇区号)
-
写入0x1f2端口,设置读取的扇区数量
-
设置起始的LBA扇区号,28位的扇区号需要分成4段,分别从0x1f3、0x1f4、0x1f5、0x1f6写入,注意0x1f6高4位设置读取方式和读取硬盘位置
![]()
-
向0x1f7端口写入0x20,请求硬盘读
-
等待读写操作完成,0x1f7既是命令端口,也是状态端口,
![]()
-
判断端口状态,从0x1f0中连续读取数据
显卡操作的部分详见P142
过程调用
8086支持4中调用方式,近调用不会改变CS
- 16位相对近调用,调用目标过程位于当前代码段内,操作数是相对偏移量
- 16位间接绝对近调用,操作数是绝对地址
- 16位直接绝对远调用,call 0X2000:0X0000
- 16位娟姐绝对远调用,call far [bx] 必须带far
call和ret,call far和retf配合使用,作用是cs,ip的压栈和出栈

加载用户程序
首次读取硬盘得到用户程序最开始的512字节,包含长度,入口和段重定位表
SECTION header vstart=0 ;定义用户程序头部段
program_length dd program_end ;程序总长度[0x00]
;用户程序入口点
code_entry dw start ;偏移地址[0x04]
dd section.code_1.start ;段地址[0x06]
realloc_tbl_len dw (header_end-code_1_segment)/4
;段重定位表项个数[0x0a]
;段重定位表
code_1_segment dd section.code_1.start ;[0x0c]
code_2_segment dd section.code_2.start ;[0x10]
data_1_segment dd section.data_1.start ;[0x14]
data_2_segment dd section.data_2.start ;[0x18]
stack_segment dd section.stack.start ;[0x1c]
header_end:
通过程序的总大小,能够知道需要读取的扇区数量
此外还要将用户程序中的地址重新计算,因为原来的段地址都是相对用户程序的偏移量, 之后应该转换成相对于0x10000的偏移地址,调用calc_segment_base实现
加载程序
;文件说明:硬盘主引导扇区代码(加载程序)
app_lba_start equ 100 ;声明常数(用户程序起始逻辑扇区号)
;常数的声明不会占用汇编地址
;一个规范的程序,应当包含代码段,数据段,附加段和栈段
;NASM变异去使用SECTION定义段,intel要求段在内存中的起始地址是16字节对齐的,因此在段定义中可使用align声明对齐方式
;每个段都有一个汇编地址,相对于整个程序起始地址而言,用section.段名称.start表示
;vstart可以表示语句在段中的相对位置
;由于加载程序只有一段,因此每个标号的地址都自动附加0x7c00
SECTION mbr align=16 vstart=0x7c00
;设置堆栈段和栈指针
mov ax,0
mov ss,ax ;ss=0
mov sp,ax ;sp=0
mov ax,[cs:phy_base] ;计算用于加载用户程序的逻辑段地址,phy_base处存储0x10000,32位
mov dx,[cs:phy_base+0x02]
mov bx,16
div bx
mov ds,ax ;令DS和ES指向该段以进行操作
mov es,ax ;ds=1000,es=1000
;以下读取程序的起始部分
xor di,di ;di=0
mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号,si=100
xor bx,bx ;加载到0x1000:0x0000处,bx=0
call read_hard_disk_0 ;调用读取硬盘过程
;以下判断整个程序有多大
mov dx,[2] ;曾经把dx写成了ds,花了二十分钟排错
mov ax,[0]
mov bx,512 ;512字节每扇区
div bx
cmp dx,0
jnz @1 ;未除尽,因此结果比实际扇区数少1
dec ax ;已经读了一个扇区,扇区总数减1
@1:
cmp ax,0 ;考虑实际长度小于等于512个字节的情况
jz direct
;读取剩余的扇区
push ds ;以下要用到并改变DS寄存器
mov cx,ax ;循环次数(剩余扇区数)
@2:
mov ax,ds
add ax,0x20 ;得到下一个以512字节为边界的段地址
mov ds,ax
xor bx,bx ;每次读时,偏移地址始终为0x0000
inc si ;下一个逻辑扇区
call read_hard_disk_0
loop @2 ;循环读,直到读完整个功能程序
pop ds ;恢复数据段基址到用户程序头部段
;计算入口点代码段基址
direct:
mov dx,[0x08]
mov ax,[0x06]
call calc_segment_base
mov [0x06],ax ;回填修正后的入口点代码段基址
;开始处理段重定位表
mov cx,[0x0a] ;需要重定位的项目数量
mov bx,0x0c ;重定位表首地址
realloc:
mov dx,[bx+0x02] ;32位地址的高16位
mov ax,[bx]
call calc_segment_base
mov [bx],ax ;回填段的基址
add bx,4 ;下一个重定位项(每项占4个字节)
loop realloc
jmp far [0x04] ;转移到用户程序,ds段
;-------------------------------------------------------------------------------
read_hard_disk_0: ;从硬盘读取一个逻辑扇区
;输入:DI:SI=起始逻辑扇区号
; DS:BX=目标缓冲区地址
push ax
push bx
push cx
push dx
mov dx,0x1f2
mov al,1
out dx,al ;读取的扇区数
inc dx ;0x1f3
mov ax,si
out dx,al ;LBA地址7~0
inc dx ;0x1f4
mov al,ah
out dx,al ;LBA地址15~8
inc dx ;0x1f5
mov ax,di
out dx,al ;LBA地址23~16
inc dx ;0x1f6
mov al,0xe0 ;LBA28模式,主盘
or al,ah ;LBA地址27~24
out dx,al
inc dx ;0x1f7
mov al,0x20 ;读命令
out dx,al
.waits:
in al,dx
and al,0x88
cmp al,0x08
jnz .waits ;不忙,且硬盘已准备好数据传输
mov cx,256 ;总共要读取的字数
mov dx,0x1f0
.readw:
in ax,dx
mov [bx],ax
add bx,2
loop .readw
pop dx
pop cx
pop bx
pop ax
ret
;-------------------------------------------------------------------------------
calc_segment_base: ;计算16位段地址
;输入:DX:AX=32位物理地址
;返回:AX=16位段基地址
push dx
add ax,[cs:phy_base]
adc dx,[cs:phy_base+0x02] ;32位加法
shr ax,4 ;机智,参考P137
ror dx,4
and dx,0xf000
or ax,dx
pop dx
ret
;-------------------------------------------------------------------------------
phy_base dd 0x10000 ;用户程序被加载的物理起始地址
times 510-($-$$) db 0
db 0x55,0xaa
用户程序
;代码清单8-2
;文件名:c08.asm
;文件说明:用户程序
;创建日期:2011-5-5 18:17
;===============================================================================
SECTION header vstart=0 ;定义用户程序头部段
program_length dd program_end ;程序总长度[0x00]
;用户程序入口点
code_entry dw start ;偏移地址[0x04]
dd section.code_1.start ;段地址[0x06]
realloc_tbl_len dw (header_end-code_1_segment)/4
;段重定位表项个数[0x0a]
;段重定位表
code_1_segment dd section.code_1.start ;[0x0c]
code_2_segment dd section.code_2.start ;[0x10]
data_1_segment dd section.data_1.start ;[0x14]
data_2_segment dd section.data_2.start ;[0x18]
stack_segment dd section.stack.start ;[0x1c]
header_end:
;===============================================================================
SECTION code_1 align=16 vstart=0 ;定义代码段1(16字节对齐)
put_string: ;显示串(0结尾)。
;输入:DS:BX=串地址
mov cl,[bx]
or cl,cl ;cl=0 ?
jz .exit ;是的,返回主程序
call put_char
inc bx ;下一个字符
jmp put_string
.exit:
ret
;-------------------------------------------------------------------------------
put_char: ;显示一个字符
;输入:cl=字符ascii
push ax
push bx
push cx
push dx
push ds
push es
;以下取当前光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx ;高8位
mov ah,al
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx ;低8位
mov bx,ax ;BX=代表光标位置的16位数
cmp cl,0x0d ;回车符?
jnz .put_0a ;不是。看看是不是换行等字符
mov ax,bx ;此句略显多余,但去掉后还得改书,麻烦
mov bl,80
div bl
mul bl
mov bx,ax
jmp .set_cursor
.put_0a:
cmp cl,0x0a ;换行符?
jnz .put_other ;不是,那就正常显示字符
add bx,80
jmp .roll_screen
.put_other: ;正常显示字符
mov ax,0xb800
mov es,ax
shl bx,1
mov [es:bx],cl
;以下将光标位置推进一个字符
shr bx,1
add bx,1
.roll_screen:
cmp bx,2000 ;光标超出屏幕?滚屏
jl .set_cursor
mov ax,0xb800
mov ds,ax
mov es,ax
cld
mov si,0xa0
mov di,0x00
mov cx,1920
rep movsw
mov bx,3840 ;清除屏幕最底一行
mov cx,80
.cls:
mov word[es:bx],0x0720
add bx,2
loop .cls
mov bx,1920
.set_cursor:
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
mov al,bh
out dx,al
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
mov al,bl
out dx,al
pop es
pop ds
pop dx
pop cx
pop bx
pop ax
ret
;-------------------------------------------------------------------------------
start:
;初始执行时,DS和ES指向用户程序头部段
mov ax,[stack_segment] ;设置到用户程序自己的堆栈
mov ss,ax
mov sp,stack_end
mov ax,[data_1_segment] ;设置到用户程序自己的数据段
mov ds,ax
mov bx,msg0
call put_string ;显示第一段信息
push word [es:code_2_segment]
mov ax,begin
push ax ;可以直接push begin,80386+
retf ;转移到代码段2执行
continue:
mov ax,[es:data_2_segment] ;段寄存器DS切换到数据段2
mov ds,ax
mov bx,msg1
call put_string ;显示第二段信息
jmp $
;===============================================================================
SECTION code_2 align=16 vstart=0 ;定义代码段2(16字节对齐)
begin:
push word [es:code_1_segment]
mov ax,continue
push ax ;可以直接push continue,80386+
retf ;转移到代码段1接着执行
;===============================================================================
SECTION data_1 align=16 vstart=0
msg0 db ' This is NASM - the famous Netwide Assembler. '
db 'Back at SourceForge and in intensive development! '
db 'Get the current versions from http://www.nasm.us/.'
db 0x0d,0x0a,0x0d,0x0a
db ' Example code for calculate 1+2+...+1000:',0x0d,0x0a,0x0d,0x0a
db ' xor dx,dx',0x0d,0x0a ;0x0d,0x0a换行
db ' xor ax,ax',0x0d,0x0a
db ' xor cx,cx',0x0d,0x0a
db ' @@:',0x0d,0x0a
db ' inc cx',0x0d,0x0a
db ' add ax,cx',0x0d,0x0a
db ' adc dx,0',0x0d,0x0a
db ' inc cx',0x0d,0x0a
db ' cmp cx,1000',0x0d,0x0a
db ' jle @@',0x0d,0x0a
db ' ... ...(Some other codes)',0x0d,0x0a,0x0d,0x0a
db 0
;===============================================================================
SECTION data_2 align=16 vstart=0
msg1 db ' The above contents is written by LeeChung. '
db '2011-05-06'
db 0
;===============================================================================
SECTION stack align=16 vstart=0
resb 256 ;从当前位置开始,保留256字节,但不初始化
stack_end:
;===============================================================================
SECTION trail align=16
program_end:
参考
x86汇编语言——从实模式到保护模式
操作系统真象还原




浙公网安备 33010602011771号