一、2.段、端口与用户程序

手动分段:SECTIONSEGMENT同义

section data
data: 
	dw 0xffff

section code
mov bx, data

段对齐:section xxx align=16表示16字节对齐

image-20230730151528358

段起始处定为汇编地址起始处:section xxx vstart=0指定段内数据的汇编地址从0开始,即在该段中,段起始处就是汇编地址起始处

主引导扇区大小有限,将其作为程序加载器,用于加载用户程序,加载器通过用户程序头部段获取用户程序信息:

image-20230730154842963

SECTION header vstart=0                     ;定义用户程序头部段 
    program_length  dd program_end          ;程序总长度
    
    ;用户程序入口点
    code_entry      dw start                ;偏移地址
                    dd section.code.start ;段地址(4字节) = section.段名.start
    
    realloc_tbl_len dw (segtbl_end-segtbl_begin)/4
                                            ;段重定位表项个数,每个表项4字节
    
    ;段重定位表           
    segtbl_begin:
    code_segment  dd section.code.start
    data_segment  dd section.data.start
    stack_segment   dd section.stack.start
    
    segtbl_end:                
    
;===============================================================================
SECTION code align=16 vstart=0 
	start:                        
		...
;===============================================================================
SECTION data align=16 vstart=0
	...

;===============================================================================
SECTION stack align=16 vstart=0
	...
stack_end:  

;===============================================================================
SECTION trail align=16
program_end:

加载器的工作流程:

  1. 读取用户程序的起始扇区
  2. 把整个用户程序都读入内存
  3. 计算段的物理地址和逻辑段地址(段的重定位)
  4. 转移到用户程序执行(将处理器的控制权交给用户程序)

确定用户程序加载位置:

image-20230730163825191

app_lba_start equ 100           ;声明常数(用户程序起始逻辑扇区号)
                                ;常数的声明不会占用汇编地址              
SECTION mbr align=16 vstart=0x7c00
	...
	phy_base dd 0x10000             ;用户程序被加载的物理起始地址
times 510-($-$$) db 0
db 0x55,0xaa

主引导扇区起始地址0000:0x7c00,用户程序物理起始地址0000:0x7c00+phy_base,当段使用vstart=0x7c00时,用户程序物理起始地址简化为0000:phy_base

在mbr段内,先找到用户程序所在段,这就需要将物理起始地址转化为16位段地址,即0x10000除0x10(右移1位)

mov ax, [cs:phy_base]            ;计算用于加载用户程序的逻辑段地址 
mov dx, [cs:phy_base+0x02]
mov bx, 0x10       
div bx
mov ds, ax                       ;令DS和ES指向该段以进行操作
mov es, ax

image-20230730205915394

image-20230730204750670

端口号多少与端口本身大小(8位还是16位等)无关

输入/输出端口访问:

; 输入 设备->处理器
; in al/ax, dx/imm8
mov dx, 0xc30
in al, dx ;访问端口号为0xc30的输入端口
;访问8位端口时使用al,16位时用ax
in al, 0x60 ;端口号小于256可以用立即数

; 输出 处理器->设备
; out dx/imm8, al/ax
mov dx, 0x3c0
out dx, ax
out 0x60, al

访问端口必须以扇区为单位,使用LBA

mov dx, 0x1f2	; 端口号
mov al, 1	; 1个扇区(1-255),若为0表示输出256个扇区
out dx, al

构建端口到磁盘扇区(设备)的映射

; 逻辑扇区号(LBA地址)=0000 00000000 00000000 00000010=0x02即表示
mov dx, 0x1f3 ; 指定某磁盘操作端口号
mov al, 0x02 ; 末位扇区号/表示读写两个扇区
out dx, al ; LBA地址的0~7位
inc dx ; 端口号0x1f4
mov al, 0x00 ; 256个扇区
out dx, al ; LBA地址8~15位
inc dx ; 端口号0x1f5
out dx, al ; LBA地址16~23位
inc dx ; 端口号0x1f6
mov al, 0xe0 ; 1110 0000
out dx, al ; LBA地址20~27位

image-20230730222520564

端口0x1f7是命令端口,即可以接收命令,也是状态端口,可以返回状态

mov dx, 0x1f7
mov al, 0x20 ; 读命令
out dx, al ; 将读命令发送给命令端口

等待磁盘状态

mov dx, 0x1f7
.waits:
	in al, dx ; 把磁盘状态返回给寄存器
	and al, 0x88 ; 与1000_1000 = 取出3、7位
	cmp al, 0x08 ; 0000_1000 判断是否准备好
	jnz .waits

image-20230730224503446

端口1f0是硬盘的数据取出端口

连续取出数据

; ds指向将存放扇区数据的段,bx为偏移地址
mov cx, 256 ; 总共要读取的字数
mov dx, 0x1f0
.readw:
	in ax, dx ; 读一个字
	mov [bx], ax ; 传入内存
	add bx, 2 ; 内存偏移地址+2
	loop .readw

整合进mbr段内(16位相对近调用-同段内调用)

         app_lba_start equ 100           ;声明常数(用户程序起始逻辑扇区号)
                                         ;常数的声明不会占用汇编地址
                                    
SECTION mbr align=16 vstart=0x7c00                                     
         ;设置堆栈段和栈指针 
         mov ax,0      
         mov ss,ax
         mov sp,ax
         
         ; 0x10000 -> (0x0001):0x0000
         mov ax,[cs:phy_base]           ;计算用于加载磁盘内的用户程序的内存逻辑段地址 
         mov dx,[cs:phy_base+0x02] 
         mov bx,16        
         div bx            ; 物理地址转逻辑段地址(除16)
         mov ds,ax                       ;令DS和ES指向该段以进行操作
         mov es,ax                        
    
         ;以下读取程序的起始部分 
         xor di,di ; 100存入di:si中时di必为0
         mov si,app_lba_start ;程序在硬盘上的起始逻辑扇区号 
         xor bx,bx ;加载到DS:0x0000处 
         call read_hard_disk_0 ; 过程调用
         ...
         
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 ; 为将si分割成两个8位而传入ax寄存器中,0110_0100
         out dx,al ;LBA地址7~0,0100

         inc dx ;0x1f4
         mov al,ah
         out dx,al ;LBA地址15~8,0110

         inc dx ;0x1f5
         mov ax,di ; 0000_0000
         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 ; 返回
         ...
	phy_base dd 0x10000             ;用户程序被加载的物理起始地址
 times 510-($-$$) db 0
                  db 0x55,0xaa

过程调用和返回的原理:

  1. 将程序指针寄存器IP内容(指向CALL指令的下一条指令)压栈,CS不用压栈
  2. CALL后标号赋给IP
  3. RET指令将原IP出栈

加载整个用户程序到内存

 call read_hard_disk_0 ; 过程调用,读取了一个扇区的数据
; ds此时指向存放扇区数据(用户程序)的段
mov dx, [2]
mov ax, [0] ;  用户程序头:program_length  dd program_end 两个字
mov bx, 512 ; 每扇区512字节
div bx ; 商为用户程序所占扇区数,余数为不满一个扇区的数据字节数
cmp dx, 0
jnz @1 ; 未除尽,表示实际占用的扇区数多一个
dec ax ; 所以跳过本次ax自减步骤
@1:
	cmp ax, 0
	jz direct ; 若读取扇区数为0则跳转
	
	push ds ; 保存ds值(用户程序头部段地址)
	
	mov cx, ax ; 控制循环次数为剩余扇区数
@2:
	mov ax, ds
	add ax, 0x20 ; 这里段地址+0x20,实际物理地址+0x200,得到下一个以512字节为边界的段地址(即下一个扇区段)
	mov ds, ax
	xor bx, bx ; 偏移地址置零
	inc si ; 逻辑扇区号(磁盘)加一,即选择下一扇区
	call read_hard_disk_0 ; 再读硬盘
	loop @2
	
	pop ds

重定位用户程序:根据用户程序头部信息确定其所在段逻辑地址

 ;计算入口点代码段基址 
direct:
	mov dx,[0x08] ; 用户程序:code_entry dd section.code.start [8,9]
	mov ax,[0x06] ; [6,7]
    call calc_segment_base
    mov [0x06],ax ;回填修正后的入口点代码段基址 
    
calc_segment_base:  ;计算16位段地址
                    ;输入:DX:AX=32位汇编地址
                    ;返回:AX=16位段基地址,逻辑段地址,(0x0001):0x0000 
	push dx                          
         
	; 通过段的物理地址和头部信息记录的程序入口的汇编地址相加,得出程序入口的物理地址
	; [cs:phy_base+2]_[cs:phy_base] = 0x0001_0000
	; dx_ax                         = 0x0000_0000
	add ax,[cs:phy_base] ; 相加,若进位则标志寄存器CF=1
	adc dx,[cs:phy_base+0x02] ; 带进位相加,dx = dx + [cs:phy_base+0x02] + CF
	shr ax,4 ; 左移		****_****_****_0000 -> 0000_****_****_****
	ror dx,4 ; 循环左移   0000_0000_0000_**** -> ****_0000_0000_0000
	and dx,0xf000 ; 防止地址出错,发生20位地址相加溢位
	or ax,dx ; 合并ax, dx

	pop dx

	ret

重定位段重定位表项:

	call calc_segment_base
	mov [0x06],ax ;回填修正后的入口点代码段基址
    
    ;开始处理段重定位表
    mov cx,[0x0a] ;需要重定位的项目数量, realloc_tbl_len dw (segtbl_end-segtbl_begin)/4
    mov bx,0x0c ;重定位表首地址
          
realloc:
     mov dx,[bx+0x02] ;32位地址的高16位,code_segment dd section.code.start
     mov ax,[bx]
     call calc_segment_base ; 重定位该表项段地址
     mov [bx],ax ;回填段的基址
     add bx,4  ;下一个重定位项(每项占4个字节) 
     loop realloc 

    jmp far [0x04] ;16位间接绝对远转移,jmp far m,[0x04]处放着code_entry dw start,此时ds指向用户程序段,执行后转移到用户程序start标号处执行程序

用户程序执行

start:
    ;初始执行时,DS和ES指向用户程序头部段
    mov ax,[stack_segment] ;设置到用户程序自己的堆栈 
    mov ss,ax
    mov sp,stack_end

    mov ax,[data_1_segment] ;设置到用户程序自己的数据段
    mov ds,ax
    
    mov ax, 0xb800
    mov es, ax

    mov si,msg0
    mov di, 0

    next:
        mov al, [si]
        cmp al, 0
        je exit

        mov byte [es:di], al
        mov byte [es:di+1], 0x02
        inc si
        add di, 2
        jmp next

    exit:
        jmp $ 
...
SECTION stack align=16 vstart=0
         resb 256 ; 告知编译器跳过256字节不编译,resw,resd
stack_end:  

db,保留空间,并且指定空间的内容。 用 resb ,只保留空间,内容由编译器给你指定

回车的光标处理:

回车字符0x0d将光标置于本行行首,换行字符0x0a将光标置于下一行行首

image-20230731143252915

无符号乘法指令:

  • 操作数8位(乘数)-积16位:AL * 操作数 =AX
  • 操作数16位(乘数)-积32位:DX_AX * 操作数 = DX_AX
  • 操作数32位(乘数)-积64位:EDX_EAX * 操作数 = ·EDX_EAX(8086不支持,80386开始支持)
  • 操作数64位(乘数)-积128位:RDX_RAX * 操作数 = RDX_RAX(8086和32位处理器不支持,6位处理器支持)
mov bx,msg0
call put_string                  ;显示第一段信息 

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 ;访问3d4索引端口,指定0e寄存器
	mov dx,0x3d5
	in al,dx ;访问3d5数据端口,获取光标高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位数
	; 16位数表示一个屏的位置的标号,80*25,编号0~1999
	
	cmp cl,0x0d ;是否是回车符?
	jnz .put_0a ;不是。看看是不是换行等字符 
	mov ax,bx ; 准备被除数(16位光标位置数)
	mov bl,80                       
	div bl ; 除80,商为光标行号
	mul bl ; 光标行号乘80得到本行行首光标编号
	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 ; 因为显示一个字符需要两个字节,所以位置编号乘2就是对应位置的显存逻辑偏移地址
     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 ; 传1920次
    rep movsw
    
    ;清除屏幕最底一行
    mov bx,3840 ; 最后一行行首在显存的逻辑偏移地址为3840(24行*80列*2字节)
    mov cx,80 ; 填一行80个
.cls:
    mov word [es:bx],0x0720 ; 黑底白字07,空格编码20
    add bx,2 ; 下一个字
    loop .cls

    pop bx
    sub bx, 80 ; 在进入滚屏过程中普通字符加1到了2000,换行加了80,超过了屏幕,减80恢复到最后一行

设置光标位置:

; bx为目标光标位置编号
.set_cursor:
	mov dx,0x3d4 ;索引端口
	mov al,0x0e
	out dx,al ; 指定0e寄存器
	mov dx,0x3d5
	mov al,bh ; 输入光标位置的高8位
	out dx,al
	
	mov dx,0x3d4
	mov al,0x0f
	out dx,al
	mov dx,0x3d5
	mov al,bl ; 输入光标位置的低8位
	out dx,al

    pop es
    pop ds
    pop dx
    pop cx
    pop bx
    pop ax

    ret

近过程调用将IP压栈,用RET返回,远过程调用将CS和IP压栈,用RETF返回call far

push word [es:code_2_segment] ; 压入段地址-CS
mov ax,begin
push ax  ;压入偏移地址-IP,可以直接push begin,但是80386才开始支持

retf  ;转移到代码段2执行 

虽然callcall far依赖retretf,但是retretf并不依赖callcall far,只要再栈中压入正确的地址,就可以直接返回到对应区域而不用callcall far

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值