1.基于16位实模式,引导程序
; 如何一个代码要把位于磁盘上的一个代码加载到内存,并将执行流程转移到加载的代码
; 1.需要将磁盘上从指定扇区开始的若干个扇区内容拷贝并放置到从指定物理内存地址开始的对应尺寸的物理内存
; 2.磁盘上代码自身,在拷贝到物理内存后,代码自身可以知道自己所处的物理内存位置。
; 磁盘上代码为达到2,需要按众所周知方式组织其格式,主要体现在代码开始位置需按以下格式:
; 4字节的用户程序总长度
; 2字节的入口点偏移地址
; 4字节的入口点代码段的汇编地址---入口点代码段汇编地址+入口点偏移+代码块物理内存起始位置就是入口点实际物理位置
; 2字节的段重定位表项数
; 接下来每一项就是一个重定位段的信息
app_lba_start equ 100 ; 通过equ指定一个汇编标号的值
; 汇编段
; align表示汇编段起始位置距离段所在程序起始位置偏移量需要是这个的倍数
; vstart表示段内每个标号的数值是所在位置距离段起始位置的距离+这个数值
; 刚刚进入主引导程序时候
; 主引导程序也是从磁盘固定位置移动到固定物理内存位置--0x0000:0x7c00,
; 此时cs数值是0x0000
SECTION mbr align=16 vstart=0x7c00
mov ax,0
mov ss,ax
mov sp,ax
mov ax,[cs:phy_base] ; 0x0000+0x7c00+标号距离段起始位置的偏移,这其实就是标号此时的实际物理内存位置
mov dx,[cs:phy_base+0x02] ; 类似
mov bx,16
div bx ; dx:ax / 16,商ax,余数dx。这是0x10000 / 16。0x10000是用来存放磁盘用户程序的起始物理内存位置
mov ds,ax
mov es,ax ; 这样就是设置dx,es表示了该起始物理内存位置的段地址部分,应该是0x1000
xor di,di ; di是0
mov si,app_lba_start ; si是100
xor bx,bx ;bx是0
; di:si-->ds:bx
call read_hard_disk_0
; 此时磁盘程序首个数据块已经位于物理内存
; 所以这里访问到的已经是磁盘程序首个数据块内的数据了
mov dx,[2]
mov ax,[0]
mov bx,512
div bx ; dx:ax / 512,商ax,余数dx。磁盘程序尺寸/512
cmp dx,0
jnz @1 ;
dec ax ; 余数是0会执行这一步。这样,ax就就代表了尚未拷贝到物理内存的存有磁盘程序的磁盘块个数
@1:
cmp ax,0
jz direct ;
push ds ; 表示尚有磁盘程序未拷贝到物理内存,先保存ds
mov cx,ax ; cx代表剩余待拷贝到物理内存的磁盘块个数
@2:
mov ax,ds
add ax,0x20 ;
mov ds,ax ; ds在原有基础上新增了一个磁盘块的偏移
xor bx,bx
inc si
; 循环不变式:
; di:si 下一个磁盘块的逻辑号
; ds:bx 下一个磁盘块所在的起始物理内存位置
call read_hard_disk_0
; 重复
loop @2
pop ds
; 到这里,磁盘程序已经全部拷贝到了物理内存
direct:
mov dx,[0x08]
mov ax,[0x06]
; dx:ax 代码段汇编地址,这里是入口点所在的代码段
call calc_segment_base
; 将4字节入口点代码段汇编地址的低2字节设置为该段在物理内存起始位置处的段地址
mov [0x06],ax ; ax是重定位后的代码段地址
mov cx,[0x0a] ; 重定位表的项数
mov bx,0x0c ; 首个重定位项的偏移--距离磁盘程序起始位置的距离
realloc:
; 循环不变式
; bx指向待重定位项距离段首的偏移
; dx:ax,每个表项4个字节,代表某个段的汇编地址
mov dx,[bx+0x02]
mov ax,[bx]
call calc_segment_base
; 将4字节的低2字节设置为该段在物理内存起始位置处的段地址
mov [bx],ax
add bx,4
loop realloc
; 此时已经完成了所有重定位项的重定位
jmp far [0x04] ; 先取2字节的段内偏移,再取4字节的段地址,构成一个物理地址,此后运行流程转移到用户程序入口点处
; 磁盘扇区:di:si-->物理内存位置:ds:bx
; 数据块在磁盘的起始位置,数据块在内存的起始位置。
read_hard_disk_0: ; di:si起始逻辑扇区 bx:要放入起始物理位置的段内偏移。
push ax
push bx
push cx
push dx ; 函数会修改的寄存器先入栈保存
mov dx,0x1f2 ; 逻辑扇区个数
mov al,1
out dx,al ;
inc dx ; 起始逻辑扇区号28位。高4位是1110,表示LAB下逻辑扇区,位于主硬盘。
mov ax,si
out dx,al ; 送入低8位
inc dx
mov al,ah
out dx,al ; 送入次低8位
inc dx
mov ax,di
out dx,al ; 送入次次低8位
inc dx
mov al,0xe0
or al,ah ; 设置最高4位&28位起始逻辑扇区的高4位
out dx,al
inc dx
mov al,0x20
out dx,al ; 写入0x20,表示起始逻辑扇区,扇区个数用来指示读取这个位置的磁盘内容
.waits:
in al,dx
and al,0x88
cmp al,0x08 ; 磁盘不忙,磁盘已经准备好和主机交换数据
jnz .waits
mov cx,256
mov dx,0x1f0
.readw:
in ax,dx ; 一次读取一个字,共读取256次,这样就从磁盘读取了一个扇区的大小
mov [bx],ax
add bx,2
loop .readw ; 磁盘一个扇区di:si数据,放入指定物理内存ds:bx
pop dx
pop cx
pop bx
pop ax
ret
calc_segment_base:
; dx:ax->ax dx:ax是一个起始物理物质,这个过程是求取这个起始物理位置的段地址,将结果放入ax中
push dx ; 保存dx
add ax,[cs:phy_base]
adc dx,[cs:phy_base+0x02]
; dx:ax这是0x10000,dx是0x0001 ax是0x00000
shr ax,4 ; ax是0x0000
ror dx,4 ; dx是0x1000
and dx,0xf000 ; dx是0x1000
or ax,dx ; dx的高4位是dx原来的低4位,ax原来的
pop dx
ret
phy_base dd 0x10000
times 510-($-$$) db 0
db 0x55,0xaa
2.基于16位实模式---用户程序
位于逻辑扇区100位置,占据2个扇区。
; 用户程序
; 段定义
; vstart不指定下,段内标号是基于程序起始位置的偏移
SECTION header vstart=0
; program_length是标号,值是其位置距离段起始位置偏移+0
; dd声明一个双字,内容是program_end[标号,是一个32位数值]
; 4字节的程序尺寸
program_length dd program_end
; 2字节的入口点的段内偏移
code_entry dw start
; 4字节的段起始位置距离程序起始位置的偏移---重定位操作后,这个32位的低2位代表了这个段起始位置对应的物理内存位置的段地址。
dd section.code_1.start
; 2字节的重定位表项数
realloc_tbl_len dw (header_end-code_1_segment)/4
; 重定位表项,4字节的一个段的起始位置距离程序起始位置的偏移
code_1_segment dd section.code_1.start
code_2_segment dd section.code_2.start
data_1_segment dd section.data_1.start
data_2_segment dd section.data_2.start
stack_segment dd section.stack.start
header_end:
; 代码段1
SECTION code_1 align=16 vstart=0
; ds被设置为字符串所在段的段偏移
; bx被设置为首个字符
; 这是一个函数调用
put_string:
mov cl,[bx]
or cl,cl
; 如果是数值0,结束字符串显示
jz .exit
; cl此时包含了当前待显示字符
call put_char
; 增加bx使得其指向下一个待处理字符
inc bx
; 跳回去实现循环
jmp put_string
.exit:
ret
; cl包含了当前待显示字符
; 这是一个函数调用
put_char:
push ax
push bx
push cx
push dx
push ds
push es
mov dx,0x3d4
mov al,0x0e
; 向端口0x3d4写入0x0e
out dx,al
mov dx,0x3d5
; 从0x3d5读出一个字节到al
in al,dx
; 这里读出的是代表光标位置的高8位
mov ah,al
mov dx,0x3d4
mov al,0x0f
; 向端口0x3d4写入0x0f
out dx,al
mov dx,0x3d5
; 从0x3d5读出一个字节到al
; 这里读出的是代表光标位置的低8位
in al,dx
; 这里bx存放了代表光标位置的16位数值
mov bx,ax
; 如果待显示字符是回车
cmp cl,0x0d
; 对于换行或普通字符的显示处理
jnz .put_0a
;
mov ax,bx
mov bl,80
; ax / 80,商ax
div bl
; ax * 80,得到光标所在行行首位置
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
; 让bx代表显存中当前字符的段内偏移
shl bx,1
; 将待显示字符移动到显存
mov [es:bx],cl
; 恢复bx
shr bx,1
; 增加bx以便其指向下一待处理字符的偏移,以便光标永远指向下一待显示位置
add bx,1
.roll_screen:
; bx达到2000表示显示字符达到一整个屏幕
cmp bx,2000
; 未达到,设置光标
jl .set_cursor
; 接下来将第2到第25行依次拷贝到第1到24行
mov ax,0xb800
mov ds,ax
mov es,ax
; 方向
cld
; 起始待拷贝元素偏移
mov si,0xa0
; 目标位置起始处偏移
mov di,0x00
; 次数
mov cx,1920
; 一次拷贝2字节,拷贝1920次
rep movsw
; 接下来第25行清空
mov bx,3840
mov cx,80
.cls:
mov word[es:bx],0x0720
add bx,2
loop .cls
; 将光标设置为第25行行首
mov bx,1920
.set_cursor:
mov dx,0x3d4
mov al,0x0e
; 设置0x3d4端口为0x0e
out dx,al
mov dx,0x3d5
mov al,bh
; 设置0x3d5端口为bx高8位
out dx,al
mov dx,0x3d4
mov al,0x0f
; 设置0x3d4端口为0x0f
out dx,al
mov dx,0x3d5
mov al,bl
; 设置0x3d5端口为bx低8位
out dx,al
pop es
pop ds
pop dx
pop cx
pop bx
pop ax
ret
; 这是应用程序的入口点
; 在此时ds,es已经是应用程序起始物理地址对应的段地址了
; cs依然是0x0000
start:
mov ax,[stack_segment] ; 取2字节。4字节的区域只有低2字节实际起作用。
mov ss,ax ; 栈段
; 特点:初始时刻,栈指针指向地址最大位置,使用中放入元素是先移动栈顶到更低位置,
; 比如放入前栈顶位置是PosA,放入2字节数据
; 1.栈顶位置设置为PosA-2,这样PosA-2~PosA这段区域可用来实际放入2字节数据
; 元素出栈
; 比如当前栈顶位置PosA,出栈2字节数据
; 从PosA顺序取2字节得到出栈元素
; 栈顶变为PosA+2
mov sp,stack_end
mov ax,[data_1_segment] ; 这里2字节是这个段起始物理位置对应的段地址
mov ds,ax ; 设置段地址,这样便于操作这个段内的数据
mov bx,msg0 ;
; 出发函数调用
; ds被设置为合理的段地址
; bx为设置为首个字符在段内的偏移
; 显示字符串,
; 普通字符,换行,回车
; 结束标记是数值为0的一个字节
call put_string
; 入栈一个字 这是段的起始物理位置的段地址
push word [es:code_2_segment]
; 这是一个段内偏移
mov ax,begin
push ax
; 先出栈2字节得到数值作为段内偏移,再出栈2字节得到数值作为段偏移
; 按段偏移,段内偏移指向跳转
retf
continue:
mov ax,[es:data_2_segment]
; 设置段偏移
mov ds,ax
; 设置起始字符的段内偏移
mov bx,msg1
; ds是字符所在的段的偏移
; bx是首个字符的段内偏移
call put_string
; 死循环
jmp $
SECTION code_2 align=16 vstart=0
begin:
; 设置段偏移
push word [es:code_1_segment]
mov ax,continue
; 设置段内偏移
push ax
; 先出栈2字节得到段内偏移,再出栈2字节得到段偏移
; 按段偏移,段内偏移执行跳转
retf
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: