从实模式到保护模式笔记4硬盘和显卡的访问与控制
从实模式到保护模式笔记4硬盘和显卡的访问与控制
0x00 代码
用户程序代码,用来显示一堆字符的:
SECTION header vstart=0 ;定义用户程序头部段 假定起始位置偏移地址为0
program_length dd program_end;program_end是一个标号、偏移地址
code_entry dw start;start的偏移地址
dd section.code_1.start;段地址,mbr加载器会从这两个地方读地址并跳转。
realloc_tbl_len dw (header_end-code_1_segment)/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:
;=============================================================
SECTION code_1 align=16 vstart=0;定义代码段1 16字节对齐
put_string:
mov cl,[bx]
or cl,cl;判断cl是不是等于0
jz .exit
call put_char
inc bx
jmp put_string
.exit:
ret
;---------------------------------------
put_char:
push ax
push bx
push cx
push dx
push ds
push es
;以下取光标位置
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx
mov ah,al;光标地址的低8位
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
in al,dx;光标地址的高8位
mov bx,ax;bx即代表光标位置的16为数
cmp cl,0x0d;看看是不是回车符
jnz .put_0a
mov ax,bx
mov bl,80
div bl
mul bl
mov bx,ax;bx变成了当前行的首列的地址,回车的作用
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
shl bx,1;bx=bx*2
mov [es:bx],cl
;推进光标
shr bx,1
add bx,1
.roll_screen:
cmp bx,2000;光标超出当前屏幕?滚屏
jb .set_cursor;不超过就直接设置新光标
mov ax,0xb800
mov ds,ax
mov es,ax
cld
mov si,0xa0
mov di,0x00
mov cx,1920
rep movsw
mov bx,3840
mov cx,80
.cls:
mov word [es:bx],0x0720;空白
add bx,2
loop .cls
mov bx,1920;重新置光标
.set_cursor:
mov dx,0x3d4
mov al,0x0e
out dx,al
mov dx,0x3d5
mov al,bh
out dx,al
mov dx,0x3d4
mov al,0x0f
out dx,al
mov dx,0x3d5
mov al,bl
out dx,al
pop es
pop ds
pop dx
pop cx
pop bx
pop ax
ret
;-------------------------------------------------------------
start:
mov ax,[stack_segment]
mov ss,ax
mov sp,stack_end
mov ax,[data_1_segment]
mov ds,ax
mov bx,msg0
call put_string;显示第一段信息
push word [es:code_2_segment]
mov ax,begin
push ax
retf;结合push实现跳转
continue:
mov ax,[es:data_2_segment]
mov ds,ax
mov bx,msg1
call put_string
jmp $
;=============================================================
SECTION code_2 align=16 vstart=0 ;代码段2
begin:
push word [es:code_1_segment]
mov ax,continue
push ax
retf;又跳回去。。。。啥都没干的代码段2
;=============================================================
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;结束符eol
;=============================================================
SECTION data_2 align=16 vstart=0
msg1 db 'The above contents is written by nightmare-man.'
db '2020-6-29'
db 0
;=============================================================
SECTION stack align=16 vstart=0
resb 256
stack_end:
;=============================================================
SECTION trail align=16;trail 尾巴
program_end:
mbr扇区引导代码:
app_lba_start equ 100 ;声明常数,用户程序起始逻辑扇区号
SECTION mbr align=16 vstart=0x7c00;偏移地址假定为0x7c00 这样汇编地址
;就从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
mov es,ax;用于计算加载用户程序的逻辑 段地址
;以下为读取程序的起始部分
xor di,di
mov si,app_lba_start
xor bx,bx
call read_hard_disk_0
;以下判断剩下的程序有多大
mov dx,[2];0
mov ax,[0];ax读取了用户程序的结束的汇编地址program_end
mov bx,512
div bx
cmp dx,0
jnz @1;未除尽,因此实际扇区还要多一个
dec ax;因为前面已经读了一个扇区,再读的话少读一个就行
@1:
cmp ax,0
jz direct;如果剩下的不足512字节
push ds
mov cx,ax
@2:
mov ax,ds
add ax,0x20
mov ds,ax;修改段地址,准备读入扇区到指定段地址
xor bx,bx;每次读入时,偏移地址始终为0
inc si
call read_hard_disk_0
loop @2
pop ds;恢复数据段基址到用户程序头部段
;计算入口代码段的段基址
direct:
mov dx,[0x08]
mov ax,[0x06];dx ax分别是用户程序code1段的段基址的高16位和低16位
call calc_segment_base
mov [0x06],ax;回填修正后的code1段地址
;开始处理段重定位表
mov cx,[0x0a];需要重定位的项目数量
mov bx,0x0c;重定位表首地址
realloc:
mov dx,[bx+0x02];高16位
mov ax,[bx];低16位
call calc_segment_base
mov [bx],ax;回填段基址
add bx,4;下一个重定位项目
loop realloc
jmp far [0x04]
;-----------------------------------------------------------
read_hard_disk_0:;输入di si分别位逻辑扇区的高16位和低16位 ds:bx
;是读入地址
push ax
push bx
push cx
push dx
mov dx,0x1f2
mov al,1
out dx,al
inc dx
mov ax,si
out dx,al
inc dx
mov al,ah
out dx,al
inc dx
mov ax,di
out dx,al
inc dx
mov al,0xe0
or al,ah
out dx,al
inc dx
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分别是高16位低16位物理地址
;返回ax 16位段基地址
push dx
add ax,[cs:phy_base]
adc dx,[cs:phy_base+0x02]
shr ax,4
ror dx,4;带进位的循环右移,将dx循环右移4位,每次右边移出一位,既放在cf位又放到左边第一位。
and dx,0xf000
or ax,dx
pop dx
ret
;---------------------------------------------------
phy_base dd 0x10000 ;用户程序被加载的物理起始地址
times 510-($-$$) db 0
db 0x55,0xaa
以上代码运行正确
0x01 代码讲解
1.代码段
处理器的工作模式是将内存分成逻辑上的段,按照“段地址:偏移地址”的形式访问。而一个规范的程序,也应当分段(类似代码段 数据段 附加段 栈段)
NASM使用 SECTION/SEGMENT 段名称 来定义段(section/segment也行)。NASM中没有段结束标志,一旦定义段,后面的内容就都属于该段,除非又出现另一个段定义。 使用 align=16 表明 段是16字节对齐的,也就是段的大小总是16的整数倍。
程序可以不是以段的定义开头,那么这些没有被定义的内容,自成一段,例如我们之前的代码都没有段的定义,都默认是一个段
NASM中,为了方便获取每一个段的汇编地址(也就是段的开头相对于程序的开头的相对位置),提供 section.段名称.start 这样一个立即数来使用。
在一个段里,标号表示的汇编地址,默认是相对于程序开头,但是如果在段的定义时加上 vstart=0,那么段内的标号都是相对于段的开头的相对位置,且段的开头为0 “SECTION code_1 align=16 vstart=0”
section data1 align=16 vstart=0
lba db 0x55
section data2 align=16 vstart=0
lbb db 0x55
section data3 align=16
lbc db 0x55
上述代码中 lba标号最终的值时 0,这个0时相对于data1的汇编地址,因为data1 vstart=0 而它又是data1中第一个字节的标号,lbb 标号也是0,类似的 它是相对于data2的汇编地址 因为data2 vstart=0,而它也是第一个字节的标号。
而lbc则不同,由于data3没有 加上vstart,所以data3中的标号的汇编地址都是相对于程序的,因为前两个段都是16字节对齐,所以都被填充成16的整数倍,16,16 而lbc是data3的第一个字节的标号,所以是0x20
2.用户程序头部的信息
为了方便独立开发,加载器和用户程序不太耦合,所以约定在用户程序的头部写一个段,用来获得用户程序的信息:
SECTION header vstart=0 ;定义用户程序头部段 假定起始位置偏移地址为0
program_length dd program_end;program_end是一个标号、偏移地址
code_entry dw start;start的偏移地址
dd section.code_1.start;段地址,mbr加载器会从这两个地方读地址并跳转。
realloc_tbl_len dw (header_end-code_1_segment)/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:
①用户程序的大小,我们通过在程序最后的program_end标号,并且该标号所在的段没有设置vstart,这样标号的汇编地址就是程序的大小,再保存在header段里
②程序的入口,包括段地址和偏移地址。段地址的协定方法是,在用户程序header段里 保存 section.code_1.start,这仅仅是编译阶段确定的汇编地址,在用户程序被加载到内存后,需要根据加载的实际位置重新计算段地址。而偏移地址,由于code_1段里有vstart=0 所以 里面的start标号永远是正确的相对于code_1段段地址的偏移地址,所以我们也把start标号保存在header的指定位置
③段重定位表,什么是段重定位表呢?由于我们在定义段时,有很多段使用了vstart=0,因此这些段里的标号表示的偏移地址,是相对于段开头的汇编地址,而这个偏移地址对应的段基地址,是以段为开头的段地址。
在使用这些标号(偏移地址)时,我们要给出对应的段地址,也就是该段的起始位置的物理地址/16,然而段的物理地址在编写代码用户程序时无法确定,所以我们在header段的指定位置保存所有的段的汇编地址,然后由加载器来计算每个段的物理地址,再写回header段对应位置。这样用户程序被加载后,就可以正确使用标号了。
3.加载程序的工作流程
第一行的 app_lba_start equ 100 是声明一个常量100,常量的好处是避免了“魔数”(magic number),而且方便统一修改。值得注意的是,常量的声明是由编译之前的预处理阶段替换的,类似于c语言的宏,不会产生具体指令。
在程序的最后,标号 phy_base dd 0x10000 保存了程序将被加载的位置,由于加载程序只有一个mbr段,且vstart=0x7c00,因此,虽然mbr段被加载到0x0000:0x7c00处,段里的标号的汇编地址还是能和实际的偏移地址对应,因此在获取标号phy_base对应的内容时,我们用的直接是 mov ax,[cs:phy_base] mov dx,[cs:phy_base+0x02] 不需要+0x7c00
紧接着是读硬盘,对于处理器而言,不能直接操作的都是外围设备。外围设备通过响应的I/O接口通信,具体来说是I/O接口上的端口,端口类似于处理器内部的寄存器,只不过在I/O接口电路上。书上硬盘控制器端口的读写做了详细介绍,但是这里只介绍端口读写的指令。
8086支持0-65535编号的端口,in al,dx in ax,dx 从端口号为dx的端口,读数据到al或者ax out dx,al out dx,ax 从al或者ax写数据到端口dx的端口。如果端口号在0-255直接,也可以不用dx而直接用立即数来表示端口号
在书中给出了NASM call的四种调用方式:1 call near 标号 ,是相对近调用,机器码中保存这相对地址 -32768-32768,调用时会push ip,注意near可省 2 call [内存地址] ,是间接绝对近调用,会从[内存地址]处读一个字当作要跳转的偏移地址,会push ip 3 call 立即数:立即数 ,直接绝对远调用 用立即数设置cs ip,在此之前会push cs push ip 4 call far [内存地址] ,是间接绝对远调用 从[内存地址]处读两个字,低16位 偏移地址 高16位段地址,在此之前 push cs push ip。
需要注意的是 push的ip 不是call语句本条指令的偏移地址,而是call下一条指令的偏移地址,因为执行到call时,ip已经指向了下一条指令
ret和retf是和call搭配的回跳指令,之前调过来了,(push cs) push ip了,现在子程序结束了,想回到原来的地方执行,那就pop ip(pop cs),而ret就是相当于pop ip retf相当于pop ip pop cs
对硬盘的读写总是以一个扇区(512字节)为单位。所以加载用户程序到内存时,我们先加载用户程序的header段到内存得到程序的总大小,然后根据总大小得知占几个扇区,还需要读几次,需要注意的是每次读入内存的地址都要往后挪512字节,不然会重叠了。
用户程序重定位 重定位,就是重新知道自己的位置,为什么需要重定位呢?因为写代码时不知道自己最后将会被实际被加载的物理地址,所以程序的段地址无法确定,(由于vstart的设置 相对于段的偏移地址是不变的),因此重定向就是运行时重新给出段地址。 masm为什么没有这个问题? masm使用 mov ax,data mov ax,stack mov ax,code等可以直接获取运行时的段地址:
assume cs:code,ds:data
data segment
db 0,0,0,0,0,0
data ends
code segment
start: mov ax,data
mov bx,0
mov al,255
mov bl,10
imul bl
mov ax,4c00h
int 21h
code ends
end start
可以看到data段段地址直接获得了
而在NASM中则是,用户程序先在header段建立一个各个段地址的表(即重定向表),表项都是section.段名称.start,因此都是相对于程序开头的汇编地址,所以他们的被装入的实际物理地址就是 phy_base+section.段名称.start,然后/16得到的地址作为他们的段地址,再重新写入用户程序的重定向表,用户程序在执行时,需要获得每个段的段地址时,就去读重定向表对应表项。值得注意的是,因为物理地址是20位的,因此每个表项都用2个字保存。
让我们看看重定向段地址的计算代码:
calc_segment_base:;计算16位段地址 dx:ax分别是高16位低16位物理地址
;返回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
前面的add adc都好理解,也就是32位加法,shr ax,4 让低16位右移4位,而 ror dx,4 是带进位的循环右移,将dx循环右移4位,每次右边移出一位,既放在cf位又放到左边第一位。也就是将dx的右边四位拿出来,放在左边,形成一个新的dx,再 and dx,0xf000 就单独的将刚刚右移的,现在在左边的4位保存下来, 再or ax,dx 就将ax 的高四位(原来有现在因为向左移了4位 用0填充了),用dx的右移的四位代替了。 所以这四条指令的整体效果,就是将dx,ax看作一个32位的寄存器,向右移4位,也就是/16。
最后是jmp 指令的5种方式:
jmp short 标号 (-128-127的相对近距离跳转,机器码里保存的是相对地址)
jmp near 标号/立即数(-32768-32767的相对跳转) (near可省,是默认值)
jmp near 寄存器/[内存地址] (绝对近距离跳转) (near可省,是默认值)
jmp idata:idata (绝对远距离跳转)
jmp far [内存地址](绝对远距离跳转,从内存地址处读2个字,低16位是偏移地址,高16位是段地址,不过这种方法要开劈两个字的内存空间,一般都是 push 段地址 push 偏移地址 retf代替)
4.用户程序的工作流程
用户程序的工作流程没啥可讲的 嘿嘿嘿(●ˇ∀ˇ●)
//2020-6-30补充
resb指令和db指令都是用来声明数据的,但是resb只声明不初始化,与此类似的还有resw resd(字和双字)
也正是因为这个没有出示的空间,所以编译才会报warning