程序加载,访问硬盘和显卡

程序分段

作为案例展示分段的基本形式:(nasm语法)

SECTION header vstart=0	;名为header的段
SECTION code vstart=0 align=16 ;16字节段对齐
SECTION data vstart=0 align=16 ;段名代表内容
SECTION trail align=16
program_end:

程序不足以对齐特定地址,则会被填db 0x0

程序内需要取得段地址时,nasm提供了section.Name.start

vstart决定了段内标号的汇编地址计算方式,为0则代表地址=整个程序开头+偏移量

important:

  • header为必须的用户程序头部,包含的信息有用户程序的尺寸(取自program_end)、应用程序的入口点、段重定位表
  • 程序首先存在于磁盘中,需要初始化和加载才能到内存并被执行

初始化和加载

作者资料有给一个加载器程序(作为加载其他程序的程序而不是被加载的特例,它不用严格遵循程序分段的规则),以此为案例:

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

加载用户程序需要确定一个内存物理地址,如例中的phy_base行赋了0x10000,此时0~0x0ffff为加载器的内存空间(包括程序的栈空间),0x10000到0x9ffff为可用的安全空间,0xa0000到0xfffff为rom bios程序。(0x10000可以改为其他任何安全的可用空间地址)

tips:传统的老式设备会将存储器和只读存储器映射到rom bios空间

之后是将用户程序从硬盘读取到这个地址,但硬盘的读取、以及其他外围设备的读取在硬件上依赖io接口。我暂时将它抽象为一个将外围设备的数据转化为cpu可使用信息并传输的接口。接口之间的冲突协调问题由总线ICH芯片(南桥)解决。

通过端口实现io接口

一个接口需要同时传给cpu很多信息,如读取硬盘时:

我们需要知道读取的程序在哪个扇面、扇区、每个瞬间读取的数据、一共读取多少数据等。因此一个接口需要多个端口来传输这些数据(不只是硬盘接口需要多个端口,多数io接口都是)。端口本质上就是位于io接口上的寄存器。每个端口的数据宽度不一,可以是8位、16位等等。端口的实现方式也有区别,一些系统会将端口映射到内存地址上,有的系统则将端口独立编址,使其与内存不发生关系。

书上的端口是独立编址,体现为范围从0到65535的一系列编号。读取写入需要特殊指令

in:(读取)

in al(ax),dx ;目的操作数必须为al或ax,源操作数是dx,此时opcode为一字节

in al(ax),0x30 ;源操作数为一字节立即数(范围0~255),此时opcode为两字节

out:(几乎对称)

out 0x37,al(ax) ;向0x37写入8位或16位数据

out dx,al(ax) ;向dx值指代的端口写入8位或16位数据

只要能向端口写入数据,并且写入的端口和数据符合特定的规则,那么就能向io接口传输数据,实现读取用户程序、使用外围设备等功能。

读取磁盘

书中用LBA28编址数据传输逻辑扇区地址,实现了磁盘的读取。(几种读取方式的细节参看https://www.cnblogs.com/mlzrq/p/10223060.html)

image-20240317150642966

对应代码在read_hard_disk_0:处。

过程调用

过程调用是一种调用现成代码的方式,区别于系统调用,它在可以恢复调用之前寄存器的状态的前提下,调用运行一段独立代码(也可以向这段代码内传入参数,类似高级语言的函数)。例如read_hard_disk_0这段直接读取硬盘的代码,我肯定希望只用编写它一次,以后的其他任何程序像调用函数一样调用它就能做到完成读取硬盘的功能,同时也能节省内存空间

关于系统调用和过程调用:https://blog.csdn.net/shuyangxiaogou/article/details/5666098

对应的汇编指令就是jmp和ret,不再赘述。

加载用户程序

终于能够加载程序了。

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
mov al,0x0e
out dx,al
mov dx,0x3d5
in al,dx ;高8位
mov ah,al

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
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
mov [es:bx],cl

;以下将光标位置推进一个字符
shr bx,1
add bx,1

.roll_screen:
cmp bx,2000 ;光标超出屏幕?滚屏
jl .set_cursor

push bx

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

pop bx
sub bx,80

.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:
;初始执行时,DS和ES指向用户程序头部段
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 ;可以直接push begin,80386+

retf ;转移到代码段2执行

continue:
mov ax,[es:data_2_segment] ;段寄存器DS切换到数据段2
mov ds,ax

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接着执行

;===============================================================================
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:

显示例程

显示例程需要频繁调用putchar,对栈空间的管理有点要求,毕竟实模式的栈只有64kb

另外则涉及了显卡寄存器的读写,书中有提到vga显示标准,将图像信息转变为rgb三原色和行、场同步信号,通过特有的电路接口达成传输。