硬盘的访问,程序重定位和加载

用户程序的结构

  NASM编译器使用汇编指令 SECTION 或者 SEGMENT 来定义段。它的一般格式是:

SECTION 段名称
;或者
SEGMENT 段名称

  NASM对段的数量没有限制,不过Intel处理器要求段在内存中的其实物理地址起码是16字节对齐的。相应的在段定义使用:

align=子句	;用于定义某个SECTION的汇编地址对齐方式。

  为了方便取得该段的汇编地址,NASM编译器提供了以下的表达方式:

section.段名称.start		;

用户程序头部

在这里插入图片描述
  头部需要在源程序以一个段地形式出现在整个源程序的开头:

SECTION header vstart=0

用户的头部起码要包含以下信息。

  • 用户程序的尺存,即以字节为单位的大小;加载器需要根据这一信息来决定,读取多少个逻辑扇区。
  • 应用程序的入口,包括段地址和偏移地址;必须在头部给出第一条指令的段地址和偏移地址。
  • 段重定位表;用户程序可能包含不止一个段,加载到内存后,每个段的地址必须重新确定一下。

外围设备及其接口

  总线技术(Bus),总线就是一排连接所有设备的线路,每个连接到这个线路上的器件都必须拥有电子开关。以至于他们随时都能够同这排电线连接,或者从这排电线上断开。

  输入输出控制设备集中器(I/O Controller Hub,ICH)芯片,该芯片是连接不同的总线,并协调各个I/O接口对处理器的访问,在个人计算机上,这块芯片就是耳熟能详的南桥。

  如图,处理器通过局部总线连接到 ICH 内部的处理器接口电路,然后在ICH的内部,又通过总线与各个I/O相连。在ICH的内部,集成了一些常规的外围设备接口,如USB,PATA(IDE),SATA,老式的总线接口(LPC)、时钟等。每个设备都有自己的I/O接口电路同ICH相连。为了方便,主板上都有这些I/O接口的插槽,每个设备的I/O电路都设计成插卡式,可以方便插拔。

  ICH还对 PCI(PCI Express) 总线的支持,这条总线向外延伸,连接这主板上的若干个拓展槽,比如显卡就可以插在PCI上,然后把显示器接在显卡上。除了局部总线和PCI Express总线,每个I/O接口卡可能不止连接一个设备,这些线路涉及复用和仲裁问题,所以他们自己有一套独立的总线体系,称为通讯总线或者设备总线,比如USB总线和SATA总线。

  当处理器想访问某个设备的时候,ICH会接到通知,然后负责提供传输通道和其他辅助支持,并不允许其他设备和总线连接,反过来,某个设备想要连接处理器,也是一样的。

I/0端口和端口访问

  处理器是通过端口(Port) 来和外围设备打交道的。本质上,端口就是一些寄存器,类似于处理器内部的寄存器,不同之处仅在于,这些寄存器位于I/O接口电路中。端口是处理器和外围设备通过I/O交流的窗口,每一个I/O接口可能拥有很多个端口,分别用于不同的目的。端口在不同的计算机系统有着不同的实现方式,在一些计算机系统中,端口号是映射到内存地址空间的,比如0x00000 ~ 0xE0000是真实的物理内存地址,而0xE0001 ~ 0xFFFFF是从很多I/O接口那里映射过来的,当访问这部分地址的时候,实际上是在访问I/O接口。

  而在另一些计算机系统中,端口是独立编制的,不和内存发生关系,在这种计算机中,处理器的地址线及连接内存,也连接着每一个I/O接口,但是,处理器还有一个特殊的引脚M/IO#,在这里,”#”表示低电平有效,也就是说,处理器访问内存的时候,他会让M/IO#引脚呈现高电平,这个时候与内存相关的电路就会被打开,相反,如果处理器访问I/O接口,那么M/IO#引脚呈现低电,内存电路会被禁止。与此同时,处理器发出的地址和M/IO#信号一起用来打开某个I/O接口,如果该I/O接口分配的端口号和处理器地址吻合的话。

  在独立编制的系统中,存在65536个端口(端口号从0~65535),因为是独立编制,所以不能使用类似mov指令访问端口,取代的是in(端口读)和out(端口发送)指令来进行读和写。8位的端口使用寄存器AL故只能访问0 ~255(0x00 ~ 0xff)16位的端口使用寄存器AX。

通过硬盘控制器端口读扇区数据

  硬盘读写的基本单位是扇区,最经典的方式是向硬盘控制器分别发送磁头号,柱面号,和扇区号(扇区在某个柱面上的编号),这称为CHS模式。而另一种方式是所有的扇区统一从0编号编址,就是逻辑地址

  • LBA号=C磁头总数每道扇区数+H*每道扇区数+(S-1)

  LBA28采用28个比特来表示逻辑扇区号,从逻辑扇区(0x0000000~0xFFFFFFF),一共可以表示228=268435456个扇区,每个扇区可以有512个字节,所以LBA28可以管理128GB的硬盘。现在普遍采用的是LBA48,采用48个比特来表示逻辑扇区号,这样一来就可以表示131072TB的硬盘了。

这里采用LBA28访问硬盘。

  第一步,设置要读取的扇区数量。这个数值要写入0x1f2端口,这是个8位的端口,因此每次最多读写255个扇区。

mov dx,0x1f2
mov al,0x01
out dx,al

  如果写入的值为0,则表示要读取256个扇区,每读取一个扇区,这个数值就会减1,如果读写的过程中发生错误,那么这个端口就包含这尚未读取的硬盘数。

  第二步,设置起始LBA扇区号(操作端口0x1f3-0x1f7),扇区的读写是连续的,所以只要给出第一个扇区的编号就可以读取所有的扇区了,28个扇区号将会被分成4段(每个段8位),分别写入0x1f3-0x1f6端口,其中0x1f3端口存放的是0-7位,0x1f4端口存放8-15位,0x1f5存放16-23位,0x1f6存放24-27位,最后,0x1f6高4位用来指示是主盘或者是从盘,以及是采用CHS模式还是LBA模式。

mov dx,0x1f3
mov al,0x02
out dx,al ;LBA 地址 7~0
inc dx ;0x1f4
mov al,0x00
out dx,al ;LBA 地址 15~8
inc dx ;0x1f5
out dx,al ;LBA 地址 23~16
inc dx ;0x1f6
mov al,0xe0 ;LBA 模式,主硬盘,以及 LBA 地址 27~24
out dx,al

  第三步,向端口0x1f7写入0x20,请求端口读(写入0x30就是请求端口写)

mov dx,0x1f7
mov al,0x20 ;读命令
out dx,al

  第四步,等待读写操作完成。0x1f7既是命令端口,又是状态端口。在硬盘内部操作期间,它会将0x1f7端口的第7位置1,表明硬盘在忙,一旦硬盘准备好了,它再将这个位清零,同时将第三位置1,表明这个时候主机可以向硬盘发送数据或者从硬盘接受数据了。

	mov dx,0x1f7
.waits:
    in al,dx
	and al,0x88
	cmp al,0x08
	jnz .waits ;不忙,且硬盘已准备好数据传输

在这里插入图片描述
  第五步,连续取出数据(操作端口0x1f0)。0x1f0是硬盘的数据接口,而且这个端口还是16位的,一旦硬盘准备就绪,就可以连续地从这个接口写入或者读取数据了。

	mov cx,256 ;总共要读取的字数
	mov dx,0x1f0
.readw:
	in ax,dx
	mov [bx],ax
 	add bx,2
 	loop .readw

  最后,0x1f1是一个错误代码寄存器,包含硬盘驱动器最后一次执行命令后的状态(错误原因)。

加载用户程序

  加载器首先是在硬盘上读取用户文件(程序),因为我们肯定是要事先知道用户头部的(总长度,所有段地址等),所以要预先读取一个扇区,又因为每个扇区是512个字节,但是用户程序可能很大,而在实模式下,每个段的大小最大是64KB,所以每加载一个扇区,都重新设定段(每个段地址只要往后移动0x20就可以了)。

代码清单

         ;代码清单8-2
         ;文件名:c08.asm
         ;文件说明:用户程序      
;===============================================================================

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

         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
stack_end:  
;===============================================================================
SECTION trail align=16

program_end:
         ;代码清单8-1
         ;文件名:c08_mbr.asm
         ;文件说明:硬盘主引导扇区代码(加载程序) 
         app_lba_start equ 100           ;声明常数(用户程序起始逻辑扇区号)
                                         ;常数的声明不会占用汇编地址
     
SECTION mbr align=16 vstart=0x7c00                                     

         ;设置堆栈段和栈指针 

         mov ax,0      

         mov ss,ax

         mov sp,ax
         mov ax,[cs:phy_base]            ;计算用于加载用户程序的逻辑段地址 
         mov dx,[cs:phy_base+0x02]

         mov bx,16        

         div bx            

         mov ds,ax                       ;令DS和ES指向该段以进行操作

         mov es,ax                        

         ;以下读取程序的起始部分 

         xor di,di

         mov si,app_lba_start            ;程序在硬盘上的起始逻辑扇区号 

         xor bx,bx                       ;加载到DS:0x0000处 

         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]                  ;转移到用户程序  

;-------------------------------------------------------------------------------

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]

         shr ax,4

         ror dx,4

         and dx,0xf000

         or ax,dx
      
         pop dx
         ret
;-------------------------------------------------------------------------------

         phy_base dd 0x10000             ;用户程序被加载的物理起始地址

 times 510-($-$$) db 0
                  db 0x55,0xaa
posted @ 2020-03-23 17:10  放飞梦想C  阅读(621)  评论(0编辑  收藏  举报