使用汇编语言编写加载器(加载用户程序)

使用汇编语言编写加载器加载指定格式的用户程序


在计算机加电之后,计算机首先会读取硬盘的主引导扇区,做一些必要的初始化工作,但是硬盘的一个扇区只有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:

一.读取用户程序到内存

从硬盘读取信息,需要五个步骤:

  1. 设置要读取的扇区数量

这个数值要写入0x1f2端口,这是一个8位寄存器,所以可以使用

out dx,al

2.设置要读取的起始LBA扇区号。
这里使用LBA28。28位的扇区号,分给四个端口,0x1f3-0x1f6,从低到高依次存储,最后一个端口也就是0x1f6低四位存储扇区号的最高四位,剩下四位高三位111表示LBA模式,最低以为0表示从盘,1表示主盘。
例如,如果其实扇区是100,也就是0x60,那么设置的代码如下:

mov dx,0x1f3
mov al,0x60
out dx,al
inc dx
xor al,al
out dx,al
inc dx
out dx,al
inc dx
mov al,0xe0
out dx,al

3.请求读写
次数值写入0x1f7端口,0x20表示请求读

mov al,0x20
mov dx,0x1f7
out dx,al

4.等待硬盘空闲
0x1f7这个端口,除了可以请求读意外,还能表示硬盘的状态,第7位0表示空闲,1表示繁忙,第3位为1表示准备好进行数据传输,所以这里我们要等待硬盘空闲才可以传输数据:

waits:
    in al,dx
    and al,1000_1000B    ;保留第3位和第7位
    cmp 0000_1000B    ;第7位为0第3位为1才可以进行读取
    jne waits        ;不相等就循环等待

5.读数据
从硬盘读取数据,通过0x1f0端口,这是一个16位寄存器。硬盘是典型的块设备,所以一次必须读取512字节,或者它的倍数。比如,我们要读取一个扇区的数据,并存放在ds:0开始的内存空间:

mov dx,0x1f0
    mov cx,256    ;读取一个扇区,512字节,即256字
    xor bx,bx
read_word
    in ax,dx
    mov [bx],ax
    add bx,2
    loop read_word

6.检查用户程序是否读取完整。
刚才我们只读取了一个扇区,即512字节,我们并不能确定用户程序是否已经完全读完,但是我们已经读取了用户程序的头部,这里我们规定用户程序头部的第一个双字必须定义用户程序的长度.所以我们从ds:0的位置读取两个字,第一个字放在ax,第二个字放在dx,这样dx:ax代表了用户程序的总长度,用这个数除以512得到商和余数,可以知道用户程序的读取进度,进而来决定是继续读取还是跳转到后边的步骤(重定位)。
注意:
由于一个逻辑段最大是64kb,从0x0000-0xffff,但是用户程序可能超过这个范围,为了避免这种事情发生,我们每读一个扇区,便把段地址加0x20(512),这样便可以连续存放且不用担心超过逻辑段大小。

;检查用户程序是否读取完整
    xor bx,bx
    mov ax,[bx]
    mov dx,[bx+2]
    mov bx,512
    div bx
    
    cmp dx,0
    jne cmp_ax
    dec ax
    
cmp_ax:
    cmp ax,0
    je  redirect_entry    ;跳转到重定位
;读取剩余扇区
    push ds
    
    mov cx,ax
    mov si,start_sector    ;start_sector是定义的一个常数,这里等于100(用户程序在100扇区开始)
    
read_rest:
    mov ax,ds
    add ax,0x20
    mov ds,ax
    inc si
    call read_disk
    loop read_rest
    
    pop ds

二.重定位用户程序

用户程序在编写的时候都是分段的,重定位的目的便是确定每个段的实际段地址。
这里我们规定用户程序头部中定义了每个段的段首位置(汇编地址),转换成16位的段地址并重新写入。

;重定位用户程序
;重定位用户入口点的段地址
redirect_entry:
    mov ax,[0x06]    ;读取头部入口点地址信息
    mov dx,[0x08]
    call calc_seg_base
    mov [0x06],ax
    
;重定位其他段的段地址
    mov cx,[0x0a]
    mov bx,0x0c
redirect_other_seg:
    mov ax,[bx]
    mov dx,[bx+2]
    call calc_seg_base
    mov [bx],ax
    add bx,4
    loop redirect_other_seg
;过程计算段地址
;已知dx:ax物理地址,求出段地址并存放在ax中返回    
calc_seg_base:
    push dx
    
    add ax,[cs:usr_app_base]
    adc dx,[cs:usr_app_base+2]
    shr ax,4
    ror dx,4
    and dx,0xf000
    or  ax,dx
    
    pop dx
    
    ret

三.将控制权交给用户程序

通过jmp far 命令进行段间跳转,这里是,跳转到重定位后的入口点位置,存放在ds:0x04处

jmp far [0x04]

至此,我们的加载器就基本完成了,可以用它来加载任何符合我们规定格式的用户程序(用户程序头部)。

相关推荐