是什么导致 CPU 在此函数中挂起?

What causes a CPU hang in this function?

我的代码中有一个函数加载到 FAT 和根目录中。此函数会导致某种 CPU 在 PCem 模拟器上挂起,但不会在 QEMU 或 PCjs 等其他模拟器上挂起。如果这不是 PCem 的错误,那么我的程序为什么会这样?

FAT加载函数:

loadfilesystem:
    mov ax,0x0050
    mov word [fatseg],ax
    mov es,ax
    xor bx,bx
    
    mov cx,word [reserved_sects] ;start of fat
    mov ax,word [sects_per_fat]

    call readsectors ;read fat     
        
    ;calculate start of root directory on disk
    mov ax,word [sects_per_fat]
    mov bl,byte [num_fats]
    mul bl
    add ax,[reserved_sects]
    mov cx,ax
    push cx ;1

    ;also calculate where in memory to put the data

    xor dx,dx
    div bx ;only 1 fat is loaded
    
    mov cl,5
    shl ax,cl
    add ax,0x50
    mov word [rootdirectoryseg],ax
    mov es,ax ;the segment
    xor bx,bx

    mov ax,word [num_rootentries] ;get size of root directory
    mov cl,4
    shr ax,cl
    pop cx ;1
    
    push ax ;1 save size of root directory
    call readsectors

“读取扇区”函数

readsectors: ;input: cx for lba, al for sectors to read, es:bx for buffer and dl for drive numbers
    call lbatochs ;convert lba to chs
    mov ah,0x02 ;read sectors

    mov dl,byte [drive_num] ;load drive number

    clc ;clear carry for error checking
    int 13h

    jc short readsectorerror ;error
    
    ret ;success

整个计划:

cpu 8086
bits 16

jmp short bootstart ;fat 12 entrypoint code
nop

;fat 12 bpb

oem_label db "OS_BOOT " ;oem label (8 bytes)
bytes_per_sect dw 512 ;bytes per sector
sects_per_cluster db 1 ;sectors per cluster
reserved_sects dw 1 ;reserved sectors
num_fats db 2 ;num of fats
num_rootentries dw 320 ;num of root entries
sect_count dw 320 ;sector count
media_type db 0xfe ;media type (0xfe = 5.25 inch, 160kb)
sects_per_fat dw 1 ;sects per fat
sects_per_track dw 8 ;sects per track
num_heads dw 1 ;num of heads
hidden_sects dd 0 ;hidden sectors
large_sects dd 0 ;large sectors when the disk has more than 65535 sectors
drive_num dw 0 ;drive number
signature db 0x28 ;floppy signature
volume_id dd 0 ;volumeid
volume_label db "OS_BOOT51/4" ;volume label (11 bytes)
file_system db "FAT12   " ;file system (8 bytes)

bootstart:
    cli ;clear interrupts until they can be used
    cld ;clear direction flag for text and other
    mov ax,0x07c0
    mov ds,ax ;data segment initialisation
    
    mov byte [drive_num],dl ;save drive number stored in dl by the bios
    mov ax,0x6c0 ;4096 bytes below bootsector
    mov ss,ax
    mov sp,0x1000
    sti
    
loadfilesystem:
    mov ax,0x0050
    mov word [fatseg],ax
    mov es,ax
    xor bx,bx
    
    mov cx,word [reserved_sects] ;start of fat
    mov ax,word [sects_per_fat]

    call readsectors ;read fat     
        
    ;calculate start of root directory on disk
    mov ax,word [sects_per_fat]
    mov bl,byte [num_fats]
    mul bl
    add ax,[reserved_sects]
    mov cx,ax
    push cx ;1

    ;also calculate where in memory to put the data

    xor dx,dx
    div bx ;only 1 fat is loaded
    
    mov cl,5
    shl ax,cl
    add ax,0x50
    mov word [rootdirectoryseg],ax
    mov es,ax ;the segment
    xor bx,bx

    mov ax,word [num_rootentries] ;get size of root directory
    mov cl,4
    shr ax,cl
    pop cx ;1
    
    push ax ;1 save size of root directory
    call readsectors

    ;now calculate location of the data area
    mov ax,word [sects_per_fat]
    mov bl,byte [num_fats]
    mul bl

    mov bx,ax
    pop ax ;1 load size of root directory
    push ax ;1 save for later but keep ax for a bit
    add ax,bx ;add size of fat
    add ax,word [reserved_sects] ;add reserved sectors
    
    ;we now have the lba of the data area
    mov word [datalba],ax ;save it
    
    ;we now need to find the base segment to load the data at
    pop ax ;1
    mov cl,5
    shl ax,cl
    add ax,[rootdirectoryseg]
    mov word [dataseg],ax ;save the base segment
    
findfile:
    mov ax,word [rootdirectoryseg]
    mov es,ax ;now at the offset of the root directory table
    xor di,di
    mov bx,word [num_rootentries] ;number of entries to search through

findfileloop:
    mov si,filename
    mov cx,11 ;number of bytes the filename is
    repe cmpsb
    je short filefound
    test bx,bx
    je bootfailed ;out of retries
    mov ax,es
    add ax,0x02 ;increase by 2 segments aka 32 bytes
    mov es,ax
    xor di,di
    dec bx ;number of tries left minus one
    jmp short findfileloop

filefound:
    mov ax,word [es:di+0x0f] ;get cluster number
    xor bx,bx ;data load offset
    xor di,di ;fat read offset    
    push ax ;1 save ax

readcluster:
    ;set buffer for data
    mov cx,word [dataseg]
    mov es,cx
    
    ;load sector
    sub ax,2 ;minus 2 clusters
    mov cl,[sects_per_cluster]
    mul cl
    add ax,word [datalba]
    mov cx,ax
    mov al,byte [sects_per_cluster]
    call readsectors
    
    ;increase buffer
    mov al,byte [sects_per_cluster]
    mov ah,0
    mov cl,9
    shl ax,cl
    add bx,ax
    
    pop ax ;1 restore ax
    
    ;set buffer for fat
    mov cx,[fatseg]
    mov es,cx
    
    mov cl,3 ;multiply by three
    mul cl
    shr ax,1 ;divide by two
    mov di,ax    

    mov ax,word [es:di] ;get cluster
    test al,1 ;even or odd cluster
    jnz short evenclus

oddclus:
    mov cl,4
    shr ax,cl
    jmp short evaluatecluster

evenclus:
    and ax,0x0fff

evaluatecluster:
    cmp ax,0x0ff8 ;end of chain
    jae short finishboot
    ;do nothing to cluster and load
    push ax
    jmp short readcluster

finishboot:
    ;this effectively jumps to a pointer
    mov ax,word [dataseg]
    mov ds,ax
    push ax ;push segment
    xor ax,ax
    push ax ;push offset
    retf ;return to offset and segment on stack

readsectors: ;input: cx for lba, al for sectors to read, es:bx for buffer and dl for drive numbers
    call lbatochs ;convert lba to chs
    mov ah,0x02 ;read sectors

    mov dl,byte [drive_num] ;load drive number

    clc ;clear carry for error checking
    int 13h

    jc short readsectorerror ;error
    
    ret ;success

readsectorerror:
    pop dx ;1 clean out stack if error
    call resetdrive
    jmp short readsectors

lbatochs: ;input: cx for lba, output: cx for cylinder and sector, and dh for head
    push ax ;1 save for later
    push bx ;2 save bx
    
    ;find temp variable
    xor dx,dx
    mov ax,cx ;ax now has lba

    push ax ;3 save lba
    mov bx,word [sects_per_track] ;sectors per track
    div bx ;ax is now temp
    
    ;cylinder
    push ax ;4 save temp
    xor dx,dx
    mov bx,word [num_heads] ;number of heads
    div bx ;ax is now cylinder
    mov cx,ax ;cx stores cylinder
    pop ax ;4 retrieve temp
    ;cx is now cylinder
    
    ;head
    push dx ;4 heads already in dx
    
    ;sector
    pop dx ;4 pop dx to get stack value underneath it
    pop ax ;3 retrieve lba
    push dx ;3 push dx back on
    push cx ;4 save cylinder
    xor dx,dx
    mov word bx,[sects_per_track]
    div bx
    inc dx ;dx now has sectors
    mov bx,dx ;now bx has sectors
    pop cx ;4
    pop dx ;3
        
    ;put params together
    mov ch,cl ;cylinder in ch
    mov cl,bl ;sector in cl
    mov dh,dl ;head in dh
    mov dl,0 ;erase dl
        
    pop bx ;2 load old bx
    pop ax ;1 load old ax
    ret

resetdrive:
    mov byte dl,[drive_num] ;get drive number
    mov ah,0x00 ;reset disk interrupt
    int 13h
    jc bootfailed
    ret
    
bootfailed:
    mov si,bootfailmsg
    call printstring
    jmp hangcpu

printstring: ;video mode is set by bios so no need to set it
    lodsb ;load byte from si into al
    test al,al ;compare al with 0
    jz return ;jump if zero to return
    mov bx,0x0007 ;page = bh, color = bl
    mov ah,0xe ;type char interrupt
    int 10h
    jmp short printstring ;go back to start
    
return:
    ret ;return from subroutine
    
hangcpu:
    hlt ;dont run cpu unless interrupt
    jmp short hangcpu

;variables for booting

bootfailmsg db "FAILURE!",0
filename db "BOOT    BIN"

rootdirectoryseg dw 0
fatseg dw 0
dataseg dw 0
datalba dw 0

times 510-($-$$) db 0
bootsignature dw 0xaa55

readsectors: ;input: cx for lba, al for sectors to read, es:bx for buffer and dl for drive numbers
    call lbatochs ;convert lba to chs
    mov ah,0x02 ;read sectors
    mov dl,byte [drive_num] ;load drive number
    clc ;clear carry for error checking
    int 13h
    jc short readsectorerror ;error
    ret ;success

readsectorerror:
    pop dx ;1 clean out stack if error
    call resetdrive
    jmp short readsectors

resetdrive:
    mov byte dl,[drive_num] ;get drive number
    mov ah,0x00 ;reset disk interrupt
    int 13h
    jc bootfailed
    ret

经常需要重新读取扇区。这很正常,但是在您的 readsectors 代码中有几个错误:

  • 当 BIOS.ReadSector 函数 02h 失败时,代码转到 readsectorerror,其中有一个不匹配的 pop dx(早期编辑遗留的)无法返回主程序
  • resetdrive 代码成功重置驱动器时,代码会重新启动 readsectors 例程,但是这次来自 ALCX 的必要输入不再存在
readsectors:             ;input: cx LBA, al Sectors to read, es:bx Buffer
    push ax
    push cx

    call lbatochs        ; -> CH CL DH

    mov  dl, [drive_num]
    clc                  ;clear carry for error checking
    mov  ah, 0x02        ;read sectors
    int  13h
    jnc  readsectorOK
    mov  ah, 0x00        ;reset disk
    int  13h
    pop  cx
    pop  ax
    jnc  readsectors
    jmp  bootfailed
readsectorOK:
    pop  cx
    pop  ax
    ret

lbatochs 例程有几个冗余指令,太复杂了!
知道这个引导扇区位于 5.25" 160KB 单面双密度软盘上,您可以大大缩短代码(我相信这是您的目标)。

  • 只有1个头;因此除以 NumberOfHeads 将是多余的。
  • SectorsPerTrack只有8个;不需要除法 - 只需右移 3 次。
  • 不使用子程序可以节省 CALLRET 指令。
readsectors:             ;input: cx LBA, al Sectors to read, es:bx Buffer
    push ax
    push cx

    mov  dx, cx          ; LBA
    shr  dx, 1
    shr  dx, 1
    shr  dx, 1           ; -> DH is Head == 0
    and  cx, 7
    inc  cx              ; -> CL is Sector
    mov  ch, dl          ; -> CH is Cylinder

    mov  dl, [drive_num]
    clc                  ;clear carry for error checking
    mov  ah, 0x02        ;read sectors
    int  13h
    jnc  readsectorOK
    mov  ah, 0x00        ;reset disk
    int  13h
    pop  cx
    pop  ax
    jnc  readsectors
    jmp  bootfailed
readsectorOK:
    pop  cx
    pop  ax
    ret

在加载文件系统函数中,AL 寄存器(要读取的扇区)设置得太高,导致 BIOS 读取超过磁道边界。这不受某些 BIOS 的支持,会导致读取错误。