Assembly 32-bit print to display code runs on qemu

2019-07-13 18:43发布

问题:

I've written a small piece of code in x86 assembly language that runs on bare hardware which, at this point, goes as far as enabling protected 32-bit mode

I've run into a problem, however, pertaining printing to the screen. I've read that to do so without interrupts one may load characters into a special memory region, namely RAM address 0xb8000.

Knowing this, I wrote a function that does exactly that, and it proved a success when tested in qemu. However, and here comes the problem, when I attempt to run this program on a real machine (namely a Lenovo G470 laptop) it fails to display the string I want shown by writing to the display memory region. All other strings displayed using BIOS interrupts work as intended, alas, the 32-bit printing function does not seem to do anything. The program doesn't crash though, and a blinking cursor appears after the lines printed using interrupts.

If it's of any importance, I'm trying to boot this off a USB drive

Put much more bluntly, string S3 isn't printing in my laptop, it is in qemu and I've no idea why.

Here is my code, I apologize in advance for the lack of finesse:

[BITS 16]                       ;NASM 16-BIT MODE
[ORG 0x7C00]                    ;OFFSET MEMORY LOCATIONS TO BSECTOR/ BIOS LOADS MBR INTO THIS ADDRESS.
XOR AX, AX                      ;INITIALIZE SEGMENT REGISTERS BY AX    
MOV DS, AX                      ;INDIRECTLY INITIALIZING DS REGISTER
MOV ES, AX                      ;INDIRECTLY INITIALIZING ES REGISTER
MOV GS, AX                      ;INDIRECTLY SETTING GS REGISTER
MOV FS, AX                      ;INDIRECTLY SETTING DS REGISTER
MOV SP, 0X900                   ;SETTING STACK POINTER TO 0X900, OFFSET FROM 0X0 = 0X7C00
MOV BP, SP                      ;
JMP WORD 0x0:START16            ;JUMP TO START OF PROGRAM

PSR16B:                         ;16-BIT PRINT ROUTINE
  MOV SI, BX                ;ASSIGN SI POSITION OF STRING
  MOV AH, 0XE               ;TTY MODE
PSR16BLOP0:                  ;PRINT LOOP
     LODSB                  ;LOAD SI INTO AL AND +1 ADDRESS OF STRING
     CMP AL, 0X0            ;CONDITIONAL
     JE PSR16LOP0END        ;END LOOP
     INT 0X10               ;BIOS INTERRUPT, PRINT AL TO SCREEN
     JMP PSR16BLOP0         ;LOOP TO LOP
PSR16LOP0END:                ;END OF LOOPA
  MOV AL, 0XA               ;NEW LINE ASCII
  INT 0X10                  ;RAISING PRINT BIOS INTERRUPT
  MOV AL, 0XD               ;NEWLINE 'CHARACTER' ASCII
  INT 0X10                  ;RAISING PRINT BIOS INTERRUPT
  ADD CH, 0X1               ;ADD ONE TO COUNTER
  RET                       ;RETURN TO LAST ADRESS BEFORE CALL 
PSR16BEND:                      ;END OF FUNCTION, UNUSED

S0:                             ;STRING
    DB 'BOOTING SEQUENCE INITIALIZED',0
S1:                             ;STRING
    DB 'LOADING GDT',0
S2:                             ;STRING
    DB 'ENTERING 32-BIT PROTECTED MODE',0

GDTS:                           ;START OF GLOBAL DESCRIPTOS TABLE

       GDTN:                           ;NULL BEGGINING, 8 BYTES
       DQ 0X0
  GDTC:                           ;TABLE FOR CODE SEGMENT
       DW 0XFFFF               ;BITS 0-15 OF LIMIT(MAXADRESSUNIT) ||0X0-0XF
       DW 0X0                  ;BASE(SEGMENTSTART), BITS 0-15 ||0XF-0X1F
       DB 0X0                  ;BASE, BITS 16-23 ||0X20-0X27
       DB 10011010B            ;ACCESS BYTE ||0X28-0X2F
       DB 11001111B            ;SECOND4BITS:LIMITBITS 16-19/FIRST4BITS:FLAGS= ||0X30-0X37
       DB 0X0                  ;BASE, BITS 24-31||0X38-0X3F
  GDTD:                           ;TABLE FOR DATA SEGMENT
       DW 0XFFFF               ;BITS 0-15 OF LIMIT(MAXADRESSUNIT) ||0X0-0XF
       DW 0X0                  ;BASE(SEGMENTSTART), BITS 0-15 ||0XF-0X1F
       DB 0X0                  ;BASE, BITS 16-23 ||0X20-0X27
       DB 10010010B            ;ACCESS BYTE ||0X28-0X2F
       DB 11001111B            ;SECOND4BITS:LIMITBITS 16-19/FIRST4BITS:FLAGS= ||0X30-0X37
       DB 0X0                  ;BASE, BITS 24-31||0X38-0X3F
GDTE:                           ;END OF GLOBAL DESCRIPTION TABLE

GDTDESC:                        ;GDT DESCRIPTOR
    DW GDTE - GDTS - 1      ;SIZE OF GDT, 2 BYTE, MUST BE LESS THAN 1
    DD GDTS                 ;ADRESS OF GDT, 4 BYTE
GDTDESCEND:                     ;END OF GDTDESC, UNUSED

CODESEG EQU GDTC - GDTS         ;CODE SEGMENT ADDRESS CONSTANT
DATASEG EQU GDTD - GDTS         ;DATA SEGMENT ADDRESS CONSTANT

START16:                        ;START OF BOOTSECTOR PROGRAM
    MOV CH, 0X0             ;LINES COUNTER.
    MOV BX, S0              ;SET STRING POINTER
    CALL PSR16B             ;CALL PRINT FUNCTION
    MOV BX, S1              ;SET STRING POINTER
    CALL PSR16B             ;CALL PRINT FUNCTION
    LGDT [GDTDESC]          ;LOADING GDT DESCRIPTOR
    MOV BX, S2              ;SET STRING POINTER
    CALL PSR16B             ;CALL PRINT FUNCTION
    CLI                     ;SEVERING INTERRUPTS
    MOV EAX, CR0            ;INDIRECTLY SETTING PROTECTED MODE BIT
    OR EAX, 0X1             ;SETTING PMODE BIT
    MOV CR0, EAX            ;PROTECTED MODE ENABLED
    JMP CODESEG:START32     ;FAR JUMP TO 32 BIT LAND

[BITS 32]                       ;NASM 32-BIT MODE

S3:                             ;STRING
    DB '32-BIT PROTECTED MODE ENABLED', 0

PSR32B:                        ;PRINT TO DISPLAY ROUTINE FOR 32-BIT   PREOTECTED MODE
 PSR32BLOP0:             ;INITIALIZE VGA REGION POINTER
  CMP CL, 0X0          ;CONDITIONAL, IF FALSE SKIP INITIALIZATION
  JNE PSR32BLOP0END    ;END LOOP
  MOV EBX, 0XB8000      ;INITIALIZING POINTER TO VGA MEMORY REGION
  ADD CL, 0X1          ;ADD TO COUNTER
  JMP PSR32BLOP0       ;LOOP
 PSR32BLOP0END:             ;END OF FUNCTION

 PSR32BLOP1:             ;USED TO INTIALIZE VGA MEMORY POINTER, NEWLINE OFFSET FROM 16-BIT LINES
      CMP CH, 0X0         ;END CONDITIONAL
     JE PSR32BLOP1END;    ;JUMP TO END OF LOOP
     ADD EBX, 0XA0        ;ADD EQUIVALENT OF ONE LINE TO POINTER
     SUB CH, 0X1         ;LOOP END COUNTER
     JMP PSR32BLOP1    ;LOOP
 PSR32BLOP1END:          ;USED TO INTIALIZE VGA MEMORY POINTER, END
    MOV ESI, EDX            ;LOAD LODSW STRING POINTER WITH APPROPIATE ADDRESS
    MOV AH, 0X0F            ;BLACK BACKGROUND, WHITE LETTERS
  PSR32BLOP2:                ;PRNTINH LOOP
     LODSB                    ;LOAD CHARACTER INTO AL
     CMP AL, 0X0             ;CHECK FOR END OF STRING
     JE PSR32BLOP2END         ;IF AX IS 0 JUMP TO END OF LOOP
     MOV [EBX], AX           ;LOAD WORD CHARACTER INTO VGA MEMORY
     ADD EBX, 0X2             ;MOVE TO NEXT CHARACTER WORD IN MEMORY ADDRESS
     JMP PSR32BLOP2           ;LOOP BACK TO START
 PSR32BLOP2END:              ;END OF LOOP

    RET
 ENDPSR32B:                     ;END OF FUNCTION, UNUSED


 START32:                        ;START OF 32 BIT PROTECTED PROGRAM
    MOV AX, DATASEG        ;SET DATA SEGMENT ADDRESS TO POINTER
    MOV DS, AX              ;INITIALIZING SEGMENT REGISTERS
    MOV SS, AX              ;INITIALIZING SEGMENT REGISTERS
    MOV [ES:DI], DL              ;INITIALIZING SEGMENT REGISTERS
    MOV DS, AX              ;INITIALIZING SEGMENT REGISTERS
    MOV GS, AX              ;INITIALIZING SEGMENT REGISTERS
    MOV EDX, S3              ;STRING POINTER DX
    CALL PSR32B             ;CALL PRINT ROUTINE// THIS IS A TEST
    JMP $                   ;LOOP TO INFINITY



 PAD:                          ;BOOTSECTOR PADDING & MAGIC NUMBER
    TIMES 510-($-$$) DB 0   ;FILL 0S TO END OF SECTOR
    DW 0xAA55               ;BOOT SIGNATURE

EDIT: Setting CL to 0 at start32: fixed the problem

回答1:

Your code has a number of serious problems:

  • It is all upper case and hard to read.
  • There seems to be lack of understanding of how realmode segment:offset addressing works in real mode.
  • Registers used without being initialized.
  • Improperly setting segment registers.

A version of the code that does something similar is the code below. The code is commented for the most part. Important things to understand:

  • When the BIOS prints characters it updates the current row and column in the BIOS Data Area (BDA). When in protected mode you can read the byte in memory location 0x450 for the column and 0x451 for the row. You can use this information to continue where the BIOS left off.
  • The 16-bit word at memory address 0x44a is the screen width of the current video mode previously set by the BIOS.
  • Each cell on the screen is two bytes. The current byte offset in video memory can be computed as 0xb8000+(cur_row * screen_width + cur_col) * 2
  • Include a BIOS Parameter Block (BPB) to allow the image to be properly loaded when using USB disk media in floppy disk (FDD) emulation mode. This code provides a BPB for a 1.44MiB floppy.
  • To properly address all memory you should enable the A20 line.The code provided uses the fast enable method, but may not be compatible with all hardware but should work on most emulators.
  • When a string is printed using print_string_pm the hardware cursor position is updated with set_cursor after the string is placed in display memory.

bpb.inc:

global bpb_disk_info

    jmp boot_start
    TIMES 3-($-$$) DB 0x90   ; Support 2 or 3 byte encoded JMPs before BPB.

bpb_disk_info:
    ; Dos 4.0 EBPB 1.44MB floppy
    OEMname:           db    "mkfs.fat"  ; mkfs.fat is what OEMname mkdosfs uses
    bytesPerSector:    dw    512
    sectPerCluster:    db    1
    reservedSectors:   dw    1
    numFAT:            db    2
    numRootDirEntries: dw    224
    numSectors:        dw    2880
    mediaType:         db    0xf0
    numFATsectors:     dw    9
    sectorsPerTrack:   dw    18
    numHeads:          dw    2
    numHiddenSectors:  dd    0
    numSectorsHuge:    dd    0
    driveNum:          db    0
    reserved:          db    0
    signature:         db    0x29
    volumeID:          dd    0x2d7e5a1a
    volumeLabel:       db    "NO NAME    "
    fileSysType:       db    "FAT12   "

boot.asm:

bits 16
ORG 0x7c00

VIDEO_TEXT_ADDR     EQU 0xb8000 ; Hard code beginning of text video memory
ATTR_WHITE_ON_BLACK EQU 0x07    ; White on black attribute

CR                  EQU 0x0d    ; Carriage return
LF                  EQU 0x0a    ; Line feed

; Include a BPB (1.44MB floppy with FAT12) to be more comaptible with USB floppy media
%include "bpb.inc"

boot_start:
    xor ax, ax                  ; DS=SS=0. Real mode code below doesn't use ES
    mov ds, ax
    mov ss, ax                  ; Stack at 0x0000:0x7c00 below bootloader
    mov sp, 0x7c00
    cld                         ; Set string instructions to use forward movement

    mov si, boot_init_msg       ; Print boot initialization message
    call print_string_rm

    ; Fast method of enabling A20 may not work on all x86 BIOSes
    ; It is good enough for emulators and most modern BIOSes
    ; See: https://wiki.osdev.org/A20_Line
    cli                         ; Disable interrupts
    in al, 0x92
    or al, 2
    out 0x92, al                ; Enable A20 using Fast Method

    mov si, load_gdt_msg        ; Print loading GDT message
    call print_string_rm

    lgdt [gdtr]                 ; Load our GDT

    mov si, enter_pm_msg        ; Print protected mode message
    call print_string_rm

    mov eax, cr0
    or eax, 1
    mov cr0, eax                ; Set protected mode flag
    jmp CODE32_SEL:start32      ; FAR JMP to set CS

bits 32
start32:
    mov ax, DATA32_SEL          ; Setup the segment registers with data selector
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x9c000            ; Set the stack to grow down from area under
                                ;     EBDA/Video memory

    xor eax, eax                ; Clear EAX for the instructions below
    mov al, [0x450]             ; Byte at address 0x450 = last BIOS column position
    mov [cur_col], eax          ; Copy to current column
    mov al, [0x451]             ; Byte at address 0x451 = last BIOS row position
    mov [cur_row], eax          ; Copy to current row

    mov ax, [0x44a]             ; Word at address 0x44a = # of columns (screen width)
    mov [screen_width], eax     ; Copy to screen width

    mov eax, in_pm_msg          ; Print message we are in protected mode
    call print_string_pm        ; EAX = first parameter

end_loop:
    hlt
    jmp end_loop

; Function: set_cursor
;           set the hardware cursor position based on the
;           current column (cur_col) and current row (cur_row) coordinates
; See:      https://wiki.osdev.org/Text_Mode_Cursor#Moving_the_Cursor_2
;
; Inputs:   None
; Clobbers: EAX, ECX, EDX

set_cursor:
    mov ecx, [cur_row]          ; EAX = cur_row
    imul ecx, [screen_width]    ; ECX = cur_row * screen_width
    add ecx, [cur_col]          ; ECX = cur_row * screen_width + cur_col

    ; Send low byte of cursor position to video card
    mov edx, 0x3d4
    mov al, 0x0f
    out dx, al                  ; Output 0x0f to 0x3d4
    inc edx
    mov al, cl
    out dx, al                  ; Output lower byte of cursor pos to 0x3d5

    ; Send high byte of cursor position to video card
    dec edx
    mov al, 0x0e
    out dx, al                  ; Output 0x0e to 0x3d4
    inc edx
    mov al, ch
    out dx, al                  ; Output higher byte of cursor pos to 0x3d5

    ret

; Function: print_string_pm
;           Display a string to the console on display page 0 in protected mode.
;           Handles carriage return and line feed.
;           Doesn't handle tabs, backspace, wrapping and scrolling.
;
; Inputs:   EAX = Offset of address to print
; Clobbers: EAX, ECX, EDX

print_string_pm:
    push edi
    push esi
    push ebx
    mov esi, eax                ; Set ESI to beginning of string

    ; Assume base of text video memory is ALWAYS 0xb8000
    mov ebx, VIDEO_TEXT_ADDR    ; EBX = beginning of video memory

    mov eax, [cur_row]          ; EAX = cur_row
    mul dword [screen_width]    ; EAX = cur_row * screen_width
    mov edx, eax                ; EDX = copy of offset to beginning of line
    add eax, [cur_col]          ; EAX = cur_row * screen_width + cur_col
    lea edi, [ebx + eax * 2]    ; EDI = memory location of current screen cell

    mov ah, ATTR_WHITE_ON_BLACK ; Set attribute
    jmp .getch
.repeat:
    cmp al, CR                  ; Is the character a carriage return?
    jne .chk_lf                 ;     If not skip and check for line feed
    lea edi, [ebx + edx * 2]    ; Set current video memory pointer to beginning of line
    mov dword [cur_col], 0      ; Set current column to 0
    jmp .getch                  ; Process next character
.chk_lf:
    cmp al, LF                  ; Is the character a line feed?
    jne .write_chr              ;     If not then write character
    mov eax, [screen_width]
    lea edi, [edi + eax * 2]    ; Set current video memory ptr to same pos on next line
    inc dword [cur_row]         ; Set current row to next line
    mov ah, ATTR_WHITE_ON_BLACK ; Reset attribute
    jmp .getch                  ; Process next character

.write_chr:
    inc dword [cur_col]         ; Update current column
    stosw

.getch:
    lodsb                       ; Get character from string
    test al, al                 ; Have we reached end of string?
    jnz .repeat                 ;     if not process next character

.end:
    call set_cursor             ; Update hardware cursor position

    pop ebx
    pop esi
    pop edi
    ret

bits 16

; Function: print_string_rm
;           Display a string to the console on display page 0 in real mode
;
; Inputs:   SI = Offset of address to print
; Clobbers: AX, BX, SI

print_string_rm:
    mov ah, 0x0e                ; BIOS tty Print
    xor bx, bx                  ; Set display page to 0 (BL)
    jmp .getch
.repeat:
    int 0x10                    ; print character
.getch:
    lodsb                       ; Get character from string
    test al,al                  ; Have we reached end of string?
    jnz .repeat                 ;     if not process next character
.end:
    ret

cur_row:      dd 0x00
cur_col:      dd 0x00
screen_width: dd 0x00

boot_init_msg:
    db "Booting sequence initialized...", CR, LF, 0
load_gdt_msg:
    db "Loading GDT...", CR, LF, 0
enter_pm_msg:
    db "Entering 32-bit Protected Mode...", CR, LF, 0
in_pm_msg:
    db "Executing code in protected mode!", CR, LF, 0

align 8
gdt_start:
    dd 0                        ; null descriptor
    dd 0

gdt32_code:
    dw 0FFFFh                   ; limit low
    dw 0                        ; base low
    db 0                        ; base middle
    db 10011010b                ; access
    db 11001111b                ; 32-bit, 4kb granularity, limit 0xffffffff bytes
    db 0                        ; base high

gdt32_data:
    dw 0FFFFh                   ; limit low (Same as code)
    dw 0                        ; base low
    db 0                        ; base middle
    db 10010010b                ; access
    db 11001111b                ; 32-bit, 4kb granularity, limit 0xffffffff bytes
    db 0                        ; base high
end_of_gdt:

gdtr:
    dw end_of_gdt - gdt_start - 1
                                ; limit (Size of GDT - 1)
    dd gdt_start                ; base of GDT

CODE32_SEL equ gdt32_code - gdt_start
DATA32_SEL equ gdt32_data - gdt_start

; Pad boot sector to 510 bytes and add 2 byte boot signature for 512 total bytes
TIMES 510-($-$$) db  0
dw 0xaa55

This code can be assembled and built into a 1.44MiB floppy disk image with these commands:

nasm -f bin boot.asm -o boot.bin

# Build 1.44MB disk image
dd if=/dev/zero of=disk.img bs=1024 count=1440
dd if=boot.bin of=disk.img conv=notrunc

The output should look something like: