友链
版权所有:
- 《x86 汇编语言 从实模式到保护模式——李忠 王晓波 余洁》
书中用到的工具
密码是1
https://gitee.com/wochinijiamile/smartya/blob/master/booktool.7z
nasm编译器:
https://gitee.com/wochinijiamile/smartya/blob/master/nasm-2.07-win32.zip
模拟器:
https://gitee.com/wochinijiamile/smartya/blob/master/Bochs-win64-2.7.exe
密码是1
好东西
用户程序进行了一点改动,对显存先进行了初始化,清楚了bochs在模拟器启动时显示的字符串和显示模式
用户程序
;代码清单8-2
;文件名:c08.asm
;文件说明:用户程序
;创建日期:2011-5-5 18:17
;===============================================================================
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 ; 索引寄存器端口号 0x3d4
mov al,0x0e ; al 0e 8bit光标寄存器的高8bit
out dx,al ; 把al写入索引端口,表示要操作0x0e对应的光标寄存器(高8bit)
mov dx,0x3d5 ; 数据端口 0x3d5
in al,dx ;高8位 把高8位取出来
mov ah,al ; 把高8bit存到ax寄存器的高8bit
mov dx,0x3d4 ; 和上面同样的方式,取出光标寄存器的低8bit,存到ax的al部分
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx ;低8位
mov bx,ax ;BX=代表光标位置的16位数
cmp cl,0x0d ;回车符? 判断是否为回车符
jnz .put_0a ;不是。看看是不是换行等字符 如果不是就跳转到.put_0a
mov ax,bx ;此句略显多余,但去掉后还得改书,麻烦
mov bl,80
div bl
mul bl ;回到行首,当前光标的位置除以80再乘以80即可得到行首的光标位置
mov bx,ax ;乘法操作的结果保存在ax中,需要将结果移动到bx中
jmp .set_cursor
.put_0a:
cmp cl,0x0a ;换行符? 是不是换行符
jnz .put_other ;不是,那就正常显示字符
add bx,80 ;直接换到下一行(一行80个字符)的当前列
jmp .roll_screen ;然后滚屏
.put_other: ;正常显示字符
mov ax,0xb800 ;定位到显存segment
mov es,ax
shl bx,1 ; bx左移1,相当于乘以2,因为一个字符在显存中占两个字节,真正的字符+显示模式
mov [es:bx],cl ; cl是字符的ascii
;以下将光标位置推进一个字符 ;怎么又右移回来了? 哦哦哦哦,之前给bx左移1,是为了表示显存段的偏移量
shr bx,1
add bx,1
.roll_screen:
cmp bx,2000 ;光标超出屏幕?滚屏 判断是否真的需要滚屏 一屏是25行,一行80列,最大值就是1999(从0开始)如果大于等于2000,就说明需要滚屏了
jl .set_cursor ; jl 就是jump if less 没有超出屏幕范围,直接设置光标就行了,否则就需要滚屏
mov ax,0xb800
mov ds,ax
mov es,ax ;怎么把ds和es都定位到了显存
cld ; 清除DF标志位 方向标志位
mov si,0xa0 ;哦,我懂了,输入换行符,会把屏幕中原来的第一行顶没,所以需要从第二行开始,将数据往上移动,表现在现存的操作就是从偏移量160的位置开始,因为一行80个字符正好占用160字节,每次移动两个字节,也就是一个字符,我们需要移动24*80个字符,也就是1920次
mov di,0x00
mov cx,1920
rep movsw ;从DS段传送1920个2bytes到ES段
mov bx,3840 ;清除屏幕最底一行,这个就是原来的第25行,上面的movsw会把原来的25行复制到24行,但是并不会清除25行,在显存中每个字符占两字节,1920*2=3840,从显存的这个偏移位置开始,我们重复80次,将显存设置为空格字符,即可达到清空最后一行的目的
mov cx,80
.cls:
mov word[es:bx],0x0720
add bx,2
loop .cls
mov bx,1920 ;清空完成之后,将光标位置设置为1920,25行的第一个位置,然后顺利执行设置光标的代码,最后ret返回
.set_cursor:
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
mov al,bh
out dx,al ; 写入高8bit
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
mov al,bl
out dx,al ;;写入低8bit 上面这些代码是写光标位置的
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
;从开始的位置输出
;将所有涉及到的寄存器压栈
;但是由于没有对es段进行初始化,bochs的输出无法被完全覆盖,而且他的字符样式还被保留了
;我们需要对es段寄存器的一整屏进行初始化
push dx
push ax
push bx
mov bx, 0x0
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
mov al,bh
out dx,al ; 写入高8bit
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
mov al,bl
out dx,al ;;写入低8bit 上面这些代码是写光标位置的
pop bx
pop ax
pop dx
push es
push bx
push cx
mov bx, 0xb800
mov es,bx
mov bx,0 ;清除屏幕最底一行,这个就是原来的第25行,上面的movsw会把原来的25行复制到24行,但是并不会清除25行,在显存中每个字符占两字节,1920*2=3840,从显存的这个偏移位置开始,我们重复80次,将显存设置为空格字符,即可达到清空最后一行的目的
mov cx,2000
.cssssssssssssls:
mov word[es:bx],0x0720
add bx,2
loop .cssssssssssssls
pop cx
pop bx
pop es
call put_string ;显示第一段信息 ,put_string调用完成
push word [es:code_2_segment] ;要跳转到的段地址 es段寄存器还指向程序头部,es:code_2_segment就是段地址
mov ax,begin
push ax ;可以直接push begin,80386+ 要跳转到的偏移地址
retf ;转移到代码段2执行 这里利用retf指令的原理就是从栈中弹出IP和CS的特性,事先现将自定义的段地址和偏移地址push到栈中,来达到高效跳转的目的,此时程序执行流程进入code_2代码段的begin标号
continue:
mov ax,[es:data_2_segment] ;段寄存器DS切换到数据段2
mov ds,ax ;切换数据段到data_2
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接着执行 又跳回去了,哎就是玩儿,跳到了code_1代码段的continue标号处
;===============================================================================
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
times 256 db 0
stack_end:
;===============================================================================
SECTION trail align=16
program_end:
程序加载器:
;代码清单8-1
;文件名:c08_mbr.asm
;文件说明:硬盘主引导扇区代码(加载程序)
;创建日期:2011-5-5 18:17
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