加载器与用户程序
使用加载器加载用户程序1. 第二个用户程序增加了中断
加载器
;此程序为MBR和加载器
;当前并能动态的知道操作系统哪块内存是空闲, 因此假设0x10000处是空闲的
;也不能动态的识别用户程序到底放在哪里,因此假设放在逻辑扇区号100的地方
;按道理说0x7c00+此扇区512字节 = 0x7e00. 即0000:7e00 后可以放用户程序
;但此程序中还需要用到栈, 栈段也放在此段中 0 ~ 0xffff, 虽然跟代码混在一起,但需要push的并不多.
;因此直接0xffff+1 的位置用来给用户程序
;分段读取用户程序
;由于用户程序可能超过65536字节,如果不改变段地址,偏移地址最多65536,因此无法完整读取
;所以下面采用改变段地址的方式来读取,每读一个扇区(512字节),段地址就增加0x20
;比如一开始段地址0x1000, 读取一次后, 把 0x1000+0x20 当成段地址来读取下个扇区,这样就不需要考虑偏移地址的问题
;用户程序的重定位问题
;每个SECTION在编译时候的段地址都是相对于文件首的偏移量(还会根据align来对齐)
;在加载到内存后, 段地址需要根据加载位置(当前是0x10000物理地址,即0x1000:0000)来重新计算
;比如当前的加载位置是0x1000:0x0000
;假设用户section header 原段地址:0x00 , 加载后段地址:0x1000 + 0x0000
;假设用户section data 原段地址:0x20 , 加载后段地址:0x1000 + 0x20
;假设用户section code 原段地址:0x30 , 加载后段地址:0x1000 + 0x30... 以此类推
;在计算完实际内存段地址后, 需要把原来的文件内段地址 修改成 内存段地址, 以便用户程序能找到实际段地址, 即需要修改用户头部
;假设用户程序放在逻辑扇区号100的地方
USER_APP_LBA EQU 100
;MBR会由BIOS自动加载到0x7c00处
;vstart=0x7c00 代码里的偏移就不需要额外+0x7c00
SECTION mbr align=16 vstart=0x7c00
;计算用户程序的段地址
mov ax,[cs:USER_APP_BASE]
mov dx,[cs:USER_APP_BASE + 2]
mov bx,16
div bx
mov ds,ax ;获取用户程序固定的段地址,也就是必然会加载到这个位置
;设置ss,sp , 假设栈段在 0x0 ~ 0xffff , 即栈顶的第一个元素是 0:0xfffe
xor ax,ax
mov ss,ax
mov sp,ax
;先读取用户程序头部,用于分析程序需要读取几个扇区,对应着用户程序头部的字节数
;读取硬盘是以一个扇区来读取的, 一个扇区512字节
; read_sector 参数: 逻辑扇区16高位,逻辑扇区低16位, 目标段地址, 目标偏移地址
;参数从右往左入栈 ,相当于 read_sector(0,USER_APP_LBA,ds,0)
push word 0 ;目标偏移地址
push ds ;目标段地址
push word USER_APP_LBA ;扇区低16位
push word 0 ;扇区高16位,只有高12位有效, LBA只有28位
call read_sector ;push ip ; jmp read_sector
add sp,8 ;恢复栈 , 2*4字节
;ds : 0x1000
;根据用户程序头部的字节数算出一共需要读取几个扇区
;由于读取硬盘都是以一个扇区(512字节)为单位 . 如果程序长度为513字节,则需要读取2个扇区
mov ax,[ds:0] ;读取用户程序头部user_app_len的低16位
mov dx,[ds:2] ;user_app_len 高16位
mov cx,512
div cx
cmp dx,0 ;余数如果不为0,则扇区需要额外+1,但由于上面已经读过一个扇区,因此不需要加
jnz .check_left_sector
dec ax ;余数=0, 则-1扇区数, 之前已经先过一个扇区
.check_left_sector:
cmp ax, 0 ;判断剩余扇区数量是否为0, 如果为0则说明读完
jmp .read_done
;ax != 0 读取剩余扇区
mov cx, ax ;cx剩余扇区数量
mov ax,ds
mov es,ax ;es 用于修改段地址
mov si,USER_APP_LBA ;此扇区号已经在上面读取过
;读取剩余扇区,每次把段地址+0x20(相当于实际增加512字节), 不需要考虑偏移地址的问题了
.read_left_sector:
mov ax,es
add ax,0x20
mov es,ax ;使用新的段地址来读取
inc si ;从下一个扇区号开始读取
push word 0 ;每一次的目标偏移都从0 , 因为已经从段地址增加了0x20
push es ;段地址
push si ;扇区低16位
push word 0 ;扇区高16位
call read_sector
add sp,8 ;恢复栈
loop .read_left_sector
;全部读完
;用户程序段的重定位
;ds 相当指向加载首地址: 0x1000
;把用户程序内的原文件内段地址修改成实际内存段地址
.read_done:
mov cx, [ds:0x1e] ;获取重定位表数量
mov bx, 0x0a ;获取重定位表起始地址
;循环重定位, 替换成实际内存段地址
.realloc_table:
mov ax,[ds:bx] ;文件内段低位
mov dx,[ds:bx + 2] ;文件内段高位
push dx
push ax
call convert_fileSectionAddr ;把文件内32位段地址转成加载后的内存逻辑段地址
add sp,4
mov [ds:bx],ax ;把段地址写回用户程序
mov word [ds:bx + 2],0 ;高位清0即可
add bx,4
loop .realloc_table
;入口段地址重定位, 以便跳转到用户程序
push word [ds:0x08]
push word [ds:0x06]
call convert_fileSectionAddr
add sp,4
mov word [ds:0x08],0
mov word [ds:0x06],ax
;在当前栈中存放当前MBR的 ds, cs, 返回地址. 用于返回
push ds
push cs
push word exit
;把当前栈写入用户程序,用于切换栈,返回
mov word [ds:0x20],sp
mov word [ds:0x22],ss
;跳转到用户程序
jmp far [ds:0x04] ;0x04 偏移 , 0x06 段地址
exit:
;返回当前程序
pop ds ;恢复MBR的ds
jmp $
;=====================================================================
;把文件内的32位段地址转成16位逻辑段地址
;参数: 低16位段地址, 高16位段地址
;返回: ax 16位逻辑段地址
;32位段地址中只有低20位有效
convert_fileSectionAddr:
push bp
mov bp,sp
push dx
mov ax,[bp + 4] ;低16位段地址
mov dx,[bp + 6] ;高16位段地址
add ax,[cs:USER_APP_BASE] ;低位与低位相加
adc dx,[cs:USER_APP_BASE+2] ;高位相加
shr ax,4 ;低位除以16
shl dx,12 ;高16位中只有低4位为有效位
and dx,0xf000 ;确保只有高4位有效
or ax,dx ;合并
pop dx
mov sp,bp
pop bp
ret
;===================================================================================
;读取一个扇区
;参数 : 逻辑扇区16高位,逻辑扇区低16位, 目标段地址, 目标偏移地址
;需要用到的端口号 0x1f2 ~ 0x1f7 , 0x1f0
;端口 0x1f2 = 设置扇区数量
;LBA28有28位 扇区号
;0x1f3 ~ 0x1f6 设置逻辑扇区号, 这些端口都是8位端口,因此只能传送一个字节来满足28位扇区号
;0x1f6端口只有低4位是端口号,高前3位111表示LBA模式,后1位表示主盘(0)从盘(1)
;0x1f7 用于读写命令,以及读取硬盘状态
;一般情况下先检测0x1f7的状态,检测第7位(是否繁忙)和第3位(准备交换数据)的状态
;即检测 : 1000_1000b 与 0x1f7读取回来的状态, 如果 1000_1000b and 状态 = 0000_1000b 则可以读写
;0x1f0用于读写数据, 这是一个16位端口
read_sector:
push bp
mov bp,sp ;bp + 2 是IP的位置, bp + 4 是第一个参数
push ds
push bx
push si
push di
push ax
push dx
push cx
mov di,[bp + 4] ;扇区高16位
mov si,[bp + 6] ;扇区低16位
mov ax,[bp + 8] ;目标段地址
mov ds,ax
mov bx,[bp + 10] ;目标偏移地址
mov dx, 0x1f2 ;设置扇区数量
mov al,1
out dx,al
;设置扇区号
inc dx ;0x1f3
mov ax,si ;扇区低16位
out dx,al ;由于是8位端口, 先把al的传送过去
inc dx ;0x1f4
mov al,ah
out dx,al ;低16位中的高8位
inc dx ;0x1f5
mov ax,di
out dx,al ;高16位中的低8位
inc dx ;0x1f6
mov al,0xe0 ;LBA模式,从主盘读取 1110_0000b
or al,ah ;ah中剩余的高4位
out dx,al
inc dx ;0x1f7
;设置0x1f7 用于读取硬盘
mov al,0x20
out dx,al
;读取0x1f7, 查看当前硬盘状态
.read_disk_status:
in al,dx ;8位端口, 读取当前状态
and al, 1000_1000b ;检测是否可以读取硬盘了
cmp al,0x08 ;如果状态是 0000_1000b则可以读取了
jnz .read_disk_status
;开始读取硬盘, 端口0x1f0
mov dx,0x1f0
mov cx,256 ;循环256次,一次读取2个字节
.begin_read:
in ax,dx
mov [ds:bx], ax ;把读取的2个字节复制到ds:bx处
add bx,2
loop .begin_read
pop cx
pop dx
pop ax
pop di
pop si
pop bx
pop ds
mov sp,bp
pop bp
ret
;假设用户程序放在这个内存地址
USER_APP_BASE dd 0x10000
times 510-($-$$) db 0
;0x55,0xaa 为MBR固定标识
dw 0xaa55
用户程序1
;当前是用户程序
;建立一个头部
;头部内的各种信息都是加载器需要的
;align=16 此段以16字节对齐, vstart=0 段内的偏移地址以0开始, 不写vstart比如最后一段,则段内标号以文件首开始算
;SECTION 用于把各自的数据分开便于代码管理. 如果你喜欢把数据,栈和用户指令混在一起也OK,只要你自己能分清
;头部, 用来给加载器足够的信息 来加载此程序
SECTION user_header align=16 vstart=0
;当前程序的字节数, 用于加载器来识别需要读取几个扇区
;dd 4个字节, 用户程序可能会超过2^16个字节
user_app_len dd pro_end ;0x0 对应user_header的偏移
;程序入口, 以便加载器完成后,jmp到此程序
;start是偏移地址
;section.user_code.start 获取段地址, 用法:seciton.某个段.start
;段地址使用dd 是因为 此段地址是文件中的汇编地址, 并非内存中的
;比如当前程序非常长,假设此代码段的位置超出65536字节, dw 就无法保存了
user_app_entry dw start ;0x04
dd section.user_code.start ;0x06
;重定位表
;由于这些段地址都是文件内的段地址,当加载器读取到内存后,需要重新计算每个段在内存中的位置
;这样才能正确访问到每个段的具体地址
;因此需要先给出一张表来写入当前文件中的段地址
;由于user_end 这个段在程序中不需要访问,用不到,因此不需要去重定位
;再次强调, 重定位的目的是为了在程序运行中能访问这些段
realloc_table_start:
s_user_code dd section.user_code.start ;0x0a
s_user_code_2 dd section.user_code_2.start ;0x0e
s_user_data dd section.user_data.start ;0x12
s_user_stack dd section.user_stack.start ;0x16
s_user_header dd section.user_header.start ;0x1a
realloc_table_end:
;重定位表项数量
;每个重定位的段占用4个字节
realloc_table_len dw (realloc_table_end - realloc_table_start ) / 4 ;0x1e
;保存加载器的stack , 偏移(sp) : 段地址(ss)
mbr_stack dw 0,0 ;0x20,0x22
user_header_end:
;代码段
SECTION user_code align=16 vstart=0
;入口
start:
;当跳转进来的时候, ds 指向 0x1000:0000, ss 指向 mbr 的栈, 因此要把ds,es,ss 全部设置为当前程序自己的
;自己的栈, 数据段都在头部段中, 已经由加载器修改完成了
;注意, 当前ds指向的是自己的头部段, 也就是0x1000
mov ax,[ds:s_user_stack] ;自己的栈
mov ss,ax
mov sp,user_stack_top ;栈顶
mov ax, [ds:s_user_header] ;自己头部
mov es,ax
mov ax,[ds:s_user_data] ;自己的数据段
mov ds,ax
;显示自己数据段的一条信息
push ds
push data_msg
call show_msg
add sp,4
;段间跳转: jmp user_code_2 : start_2
;利用retf , 相当于 pop ip , pop cs
push word [es:s_user_code_2] ;段地址
push word start_2 ;偏移地址
retf
;用户程序退出,返回到MBR中
;切换到MBR的栈, MBR栈中存放ip,cs 用于返回
user_code_exit:
mov ax,[es:mbr_stack]
mov bx,[es:mbr_stack + 2]
mov ss, bx
mov sp,ax
retf
;显示字符串, 需以0结尾
;参数: 字符串偏移地址, 字符串段地址
show_msg:
push bp
mov bp, sp
push bx
push es
push ds
push ax
push si
mov bx,[bp + 6] ;段地址
mov ds,bx
mov bx, [bp + 4] ;偏移地址
mov ax,0xb800
mov es,ax ;显存首地址
xor si,si
.show_msg_loop:
xor ax,ax
mov al,[ds:bx] ;检测是否是0
or al,al ; 自己or自己 => 自己, 由此来判断是否是0 , 以及显示此字符
jz .show_msg_done
mov [es:si],al
mov byte [es:si + 1],0x07 ;添加字符属性
inc bx
add si,2
jmp .show_msg_loop
.show_msg_done:
pop si
pop ax
pop ds
pop es
pop bx
mov sp,bp
pop bp
ret
user_code_end:
;第二个代码段
SECTION user_code_2 align=16 vstart=0
start_2:
push word [es:s_user_code] ;user_code 段地址
push user_code_exit ;user_code 偏移
retf ;pop ip , pop cs
user_code_2_end:
;数据段
SECTION user_data align=16 vstart=0
data_msg db 'fuckme',0
user_data_end:
;栈段
SECTION user_stack align=16 vstart=0
resw 128 ;resb 256 ;保留字节数 , res(b)字节 , res(w)字 , res(d)双字
user_stack_top:
;此段没有vstart=0. 因此标号pro_end从文件首开始计算
SECTION user_end align=16
pro_end:
用户程序2
;用户中断程序
;默认加载器为之前的mbr_loader.asm, 本程序还是被默认加载到0x10000, 扇区号100
;重写 int 9 中断程序
;原 int 9 中断用于接受键盘字符并产生对应的acsii码,存入键盘缓冲区
;新的int 9 中断程序在 user_code_interrupt 段中
;程序使用了bios提供的0x10中断的子程序0xe(显示字符) , 0x16中断的子程序0x00(从键盘缓冲区获取一个字符)
;9号中断和0x16号中断是一对, 9号往键盘缓冲区里存放字符, 0x16获取字符
;下面的程序相当于在9号中断拦了一下
;--------------------中断过程----------------------
;中断码对应中断向量表的每个一个入口地址,中断向量表从0000:0000 ~ 0000:3ff 共1024个字节
;一个中断码对应一个表项(入口), 一个表项占用4个字节:偏移:段地址, (低地址)偏移 , (高地址)段地址, 最多可以有256个中断
;简单来说内存布局:
;0号中断 -对应-> [0x0000:0x0000]偏移地址,[0x0000:0x0002]段地址
;1号中断 -对应-> [0x0000:0x0004]偏移地址,[0x0000:0x0006]段地址
;2号中断 -对应-> [0x0000:0x0008]偏移地址,[0x0000:0x000a]段地址
;因此可以根据中断号(码) 来获取对应的入口地址 :
;中断码1的入口地址存放在 : 偏移地址:1 * 4 , 段地址: 1*4 +2
;中断码N的入口地址 : 偏移地址 N * 4 , 段地址 N*4 + 2
;中断过程, 比如中断码N发生中断
;1.pushf 标志寄存器入栈
;2.TF=0,IF=0 TF是单步中断(1是执行单步中断,0是否定),IF是屏蔽中断(0是屏蔽中断,1是接受中断,因为当前已经中断)
;3.push cs , push ip
;4.根据中断码N,跳转到对应的入口地址, 相当于 ip=N*4, cs=N*4+2
;中断返回:
;中断程序执行完后需要返回到原程序中, 使用 : iret (interrupt return)
;iret执行的伪指令是: pop ip, pop cs, popf (弹出标志寄存器)
;新的int 9中由于接管了原int 9, 但只是把字符拿出来看看, 具体工作还要原int 9去做
;因此新int 9还需要调用原int 9, 即需要把原int 9的地址保存起来
;由于新int 9已经是一个中断程序 , 9 *4 , 9*4+2 的地址已经被替换成自己的了
;无法再去int 9 调用原中断,因此需要模拟 int 中断调用
;模拟顺序 : pushf , 把TF IF置0 , push cs ,push ip
;把这些步骤简化 : pushf , tf if =0 , call far [原段地址:原偏移]
;------------------------------------------
;此程序被加载到的逻辑段地址
USER_APP_LOADED_SECTION_ADDR EQU 0x1000
;用户头
;具体注释在user_app.asm 中已经写了,这边省略
section user_header vstart=0 align=16
user_app_len dd pro_len ;程序字节数 0x00
;入口
user_app_entry dw start ;偏移地址 0x04
dd section.user_code.start ;文件段地址 0x06
;重定位表
realloc_table_start:
s_user_code dd section.user_code.start ;0x0a
s_user_data dd section.user_data.start ;0x0e
s_user_stack dd section.user_stack.start ;0x12
s_user_header dd section.user_header.start ;0x16
;中断程序在此代码段中
s_user_code_interrupt dd section.user_code_interrupt.start ;0x1a
realloc_table_end:
;重定位表长度
realloc_table_len dw (realloc_table_end - realloc_table_start)/4 ;0x1e
;保存mbr栈,用于返回
mbr_stack dw 0,0 ;0x20,0x22
user_header_end:
;代码段
section user_code vstart=0 align=16
;进入后, ds 指向当前的头部 0x1000:0000
;需要修改ss,ds,es.
start:
;修改成自己的栈
mov ax, [ds:s_user_stack]
mov ss,ax
mov sp,user_stack_top
;es指向自己的头部
mov ax,[ds:s_user_header]
mov es,ax
;ds指向自己的数据段
mov ax,[ds:s_user_data]
mov ds,ax
;================================
;安装新的int 9 中断程序
;int 9 替换为user_code_interrupt段中的程序
;屏蔽中断 ,以防在替换中断向量表的时候有中断
cli
mov bx,[es:s_user_code_interrupt] ; 中断程序的段地址
push es
mov ax,0
mov es,ax ;es段指向0 . 中断向量表在0x0000:0x0000 ~ 0x03ff
;保存原int 9的段和偏移, 在新int 9中还需要调用原int 9
push word [es:9*4]
pop word [ds:int9_addr] ;保存偏移地址
push word [es:9*4 + 2]
pop word [ds:int9_addr + 2] ;保存段地址
;把原int 9的入口替换成自己的
mov word [es:9*4], int_start ;替换偏移
mov word [es:9*4+2], bx ;替换段地址
pop es
sti
;================================
;show_msg使用中断号来显示字符
push ds
push welcome_msg
call show_msg
add sp,4
;下面使用0x16号中断的0x00号子程序从键盘缓冲区中获取一个字符
.recv_keys:
mov ah,0x00 ;参数
int 0x16 ;使用16号中断, 参数ah:0x00
cmp ah,0x39 ;如果是空格键0x39则退出
jz .user_code_exit
mov ah,0x0e ;参数
mov bl,0x07 ;字符属性
int 0x10 ;参数al是从0x16接受回来的acsii
jmp .recv_keys
.user_code_exit:
;还原int 9中断入口
cli
push es
mov ax,0
mov es,ax
push word [ds:int9_addr]
pop word [es:9*4] ;还原偏移
push word [ds:int9_addr]
pop word [es:9*4 + 2] ;还原段地址
pop es
sti
mov ax,[es:mbr_stack] ;偏移
mov bx,[es:mbr_stack + 2] ;段地址
mov ss,bx
mov sp,ax
retf
;使用bios中断[中断号为0x10]来显示信息字符
;0x10中断号的0x0e 功能用于显示 :
;ah指定功能号:0x0e , al:要显示的字符,bl:字符属性
;参数: 偏移, 段地址
show_msg:
push bp
mov bp,sp
push bx
push ds
push ax
push si
mov si,[bp + 6] ;段地址
mov ds,si
mov si,[bp + 4] ;偏移
mov ah,0x0e ;指定功能号0x0e 用于显示字符
mov bl,0x07 ;字符属性
.show_msg_loop:
mov al,[ds:si]
cmp al,0
jz .show_msg_done
int 0x10 ;调用0x10中断
inc si
jmp .show_msg_loop
.show_msg_done:
pop si
pop ax
pop ds
pop bx
mov sp,bp
pop bp
ret
user_code_end:
;数据段
section user_data vstart=0 align=16
welcome_msg db 'welcome',0
;存放原int9的偏移,段地址
int9_addr dw 0,0 ; 偏移 , 段地址
user_data_end:
;新的int 9 中断代码段
section user_code_interrupt vstart=0 align=16
;中断入口
int_start:
push es
push ax
push bx
push cx
;新的int 9 中断从端口0x60中读取一个字符
;如果是esc键,则替换屏幕颜色. esc的扫描码是0x1
;从端口读取字符
in al,0x60
mov bl,al
;调用原int 9去处理, 此时已经在中断中,因此模拟 int 9的调用过程
;pushf , tf if =0 , call far
pushf
;由于在中断的时候, tf,if 已经设置成0了, 因此这步可以省略
;把tf,if =0, tf,if在第9和第8位
;修改标志寄存器
;pushf
;pop ax
;and ah,1111_1100b
;push ax
;popf
;检查es是否还指向自己的头部段, 需要通过es:s_user_data 获取自己的数据段, 原int 9的地址放在这里
mov ax,es
cmp ax,USER_APP_LOADED_SECTION_ADDR
je .get_user_data
mov ax,USER_APP_LOADED_SECTION_ADDR
mov es,ax
.get_user_data:
mov ax,[es:s_user_data]
mov es,ax ; es 指向自己的数据段
;调用原int 9
call far [es:int9_addr]
;是否是esc键
cmp bl,0x01
jne .int_done
mov ax,0xb800
mov es,ax ;指向显存
mov bx,1
mov cx,2000
;改变2000个字符属性
.change_color:
inc byte [es:bx]
add bx,2
loop .change_color
.int_done:
pop cx
pop bx
pop ax
pop es
iret
user_code_interrupt_end:
;栈段
section user_stack vstart=0 align=16
resb 256
user_stack_top:
section user_end align=16
pro_len: