使用汇编语言编写加载器加载指定格式的用户程序
在计算机加电之后,计算机首先会读取硬盘的主引导扇区,做一些必要的初始化工作,但是硬盘的一个扇区只有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:
一.读取用户程序到内存
从硬盘读取信息,需要五个步骤:
- 设置要读取的扇区数量
这个数值要写入0x1f2端口,这是一个8位寄存器,所以可以使用
out dx,al
2.设置要读取的起始LBA扇区号。
这里使用LBA28。28位的扇区号,分给四个端口,0x1f3-0x1f6,从低到高依次存储,最后一个端口也就是0x1f6低四位存储扇区号的最高四位,剩下四位高三位111表示LBA模式,最低以为0表示从盘,1表示主盘。例如,如果其实扇区是100,也就是0x60,那么设置的代码如下:mov dx,0x1f3mov al,0x60out dx,alinc dxxor al,alout dx,alinc dxout dx,alinc dxmov al,0xe0out dx,al
3.请求读写
次数值写入0x1f7端口,0x20表示请求读mov al,0x20mov dx,0x1f7out 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,bxread_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,0x0credirect_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]
至此,我们的加载器就基本完成了,可以用它来加载任何符合我们规定格式的用户程序(用户程序头部)。