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

手动分段:SECTIONSEGMENT同义

asm 复制代码
section data
data: 
	dw 0xffff

section code
mov bx, data

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

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

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

asm 复制代码
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. 转移到用户程序执行(将处理器的控制权交给用户程序)

确定用户程序加载位置:

asm 复制代码
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位)

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

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

输入/输出端口访问:

asm 复制代码
; 输入 设备->处理器
; 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

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

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

asm 复制代码
; 逻辑扇区号(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位

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

asm 复制代码
mov dx, 0x1f7
mov al, 0x20 ; 读命令
out dx, al ; 将读命令发送给命令端口

等待磁盘状态

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

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

连续取出数据

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

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

asm 复制代码
         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出栈

加载整个用户程序到内存

asm 复制代码
 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

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

asm 复制代码
 ;计算入口点代码段基址 
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

重定位段重定位表项:

asm 复制代码
	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标号处执行程序

用户程序执行

asm 复制代码
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将光标置于下一行行首

无符号乘法指令:

  • 操作数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位处理器支持)
asm 复制代码
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

换行及其他字符的光标处理:

asm 复制代码
 .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

滚屏和清屏:

asm 复制代码
.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恢复到最后一行

设置光标位置:

asm 复制代码
; 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

asm 复制代码
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

相关推荐
我在人间贩卖青春4 天前
汇编之伪指令
汇编·伪指令
我在人间贩卖青春4 天前
汇编之伪操作
汇编·伪操作
济6174 天前
FreeRTOS基础--堆栈概念与汇编指令实战解析
汇编·嵌入式·freertos
myloveasuka4 天前
汇编TEST指令
汇编
我在人间贩卖青春4 天前
汇编编程驱动LED
汇编·点亮led
我在人间贩卖青春4 天前
汇编和C编程相互调用
汇编·混合编程
myloveasuka5 天前
寻址方式笔记
汇编·笔记·计算机组成原理
请输入蚊子5 天前
《操作系统真象还原》 第六章 完善内核
linux·汇编·操作系统·bochs·操作系统真像还原
myloveasuka5 天前
指令格式举例
汇编·笔记·计算机组成原理
我在人间贩卖青春6 天前
汇编之分支跳转指令
汇编·arm·分支跳转