【问题标题】:How to add numbers and display them to the console in a bootloader?如何在引导加载程序中添加数字并将其显示到控制台?
【发布时间】:2019-06-06 15:01:05
【问题描述】:

我正在创建引导加载程序,它应该加上 512 到变量并打印结果,直到达到指定的数字。对我来说,它是 4194304,但问题是我真的不明白如何加上这些数字,因为最后我总是一无所获或损坏的字符串。那么我应该如何正确地加上数字呢?

cpu 386
bits 16
org 0h

start:
    cld
    xor ax,ax
    mov ss,ax
    mov sp,7c00h           ; setup stack

    mov ax,8000h
    mov es,ax              ; initialize es w/ 8000h
    mov ds,ax              ; initialize ds w/ 8000h

;===============================================================================================================

load_prog:
    mov ax,0206h           ;function/# of sec to read
    mov cx,0001h           ;0-5 sec # (counts from one), 6-7 hi cyl bits

    mov dh,00h             ;dh=head dl=drive (bit 7=hdd)
    mov bx,0h              ;data buffer, points to es:0
    int 13h
    cmp ah,0
    jne load_prog          ;this is allowable because it is relative

;============================================================================================================    

next:
    mov eax, [NUMBERS]
    add eax, 512           ;I think this have to plus numbers, so result have to be 512 = 0 + 512
    mov [NUMBERS], eax     ;And this i think have to store result to NUMBERS


print_1:
    mov si, msg0
    push ax
    cld
printchar_1:
    mov al,[si]
    cmp al,0
    jz print_2
    mov ah,0x0e
    int 0x10
    inc si
    jmp printchar_1


print_2:
    mov si, [NUMBERS]
    push ax
    cld
printchar_2:
    mov al,[si]
    cmp al,0
    jz print_3
    mov ah,0x0e
    int 0x10
    inc si
    jmp printchar_2


print_3:
    mov si, msg1
    push ax
    cld
printchar_3:
    mov al,[si]
    cmp al,0
    jz next
    mov ah,0x0e
    int 0x10
    inc si
    jmp printchar_3


done:
    hlt
    jmp done

;=====================================================================================================================    

MBR_Signature:
    msg0 db 'Counted numbers ',0
    msg1 db ' of 4194304',13,10,0
    NUMBERS dd 0
    times 510-($-$$) db 0
    db 55h,0aah
    times 4096-($-$$) db 0

【问题讨论】:

  • 您必须为没有寄存器操作数的指令指定操作数大小,否则会产生歧义。喜欢cmp dword [NUMBERS], 4194304。当然NUMBERS 应该是dd 0,因为您使用双字(4 字节)操作数大小来访问它。
  • 您使用的是旧版本的 NASM,还是这不是错误消息的minimal reproducible example?尝试组装它会得到预期的error: operation size not specified,而不是你声称得到的invalid combination of opcode and operands
  • 我已经重新打开了这个,因为 OP 已经修改了这个问题,所以它组装起来了,它真的回到了为什么代码似乎不起作用的问题。尽管比较消失了,但他的问题是为什么要打印垃圾或什么都没有。
  • 是的,这很合理,我们不需要另一个重复的操作数大小不明确的问题。现在他们删除了cmp,所以这是一个无限循环? (并选择不使用add dword [NUMBERS], 512)。它现在可能应该被关闭,因为缺少 minimal reproducible example 所发生的事情。添加很好,大概他们对 BIOS 中断所做的任何事情都没有打印出他们想要的内容,这并不奇怪,在二进制整数上使用与在 ASCII 字符字符串上相同的循环。
  • @MichaelPetch:是的,循环很好 if SI 指向一个字符串(它们用于第一个和第三个,但不是第二个)。但我只是注意到这使用mov si, [NUMBERS] 而不是mov si, NUMBERS,所以它使用[NUMBERS] 作为相对于DS 的16 位指针,并从每个512 字节偏移量中转储一个字符串。

标签: assembly x86 nasm x86-16 bootloader


【解决方案1】:

TL;DR :看来您的主要问题是使用 MOV 指令将数字存储到内存中不会将值转换为字符串。您必须编写将整数转换为字符串的代码。


您可以使用重复除法将寄存器 (EAX) 中的值转换为不同的基数(十进制数字的基数为 10)。一般算法是

val = number to convert
repeat 
  digit = val MOD 10     ; digit = remainder of val/10
  val   = val DIV 10     ; val = quotient of val/10 
  digit = digit + '0'    ; Convert digit to character value by adding '0'
  Store digit
until val == 0

如果你有数字 1234:

  • 1234/10=123 余数 4(位)
  • 123/10=12 余数 3(数字)
  • 12/10=1 余数 2(数字)
  • 1/10=0 余数 1(数字)
  • 完成

您会发现,当我们反复除以 10 时,我们得到的数字是 4,3,2,1,这与我们想要的数字 1,2,3,4 相反。您可以想出一种机制来处理反转字符串。一种快速而肮脏的方法是以相反的顺序将数字压入堆栈,然后您可以以正确的顺序将每个数字从堆栈中弹出。您可以将每个数字以相反的顺序存储在缓冲区中。

由于您尝试显示 32 位无符号数字,因此您需要在 EAX 中使用 val 进行除法。 64 位除法是用 EDX:EAX(其中 EDX 设置为 0)中的值除以 10。x86 指令 DIV 计算商(在 EAX 中返回)和余数(在 EDX 中返回)。

我建议将常用代码移到函数中,以减少重复,简化开发,并使代码更易于维护

创建一个函数uint32_to_str,该函数使用重复除以 10 将 ASCII 数字在计算时存储在堆栈中。最后,ASCII 数字从堆栈中弹出并存储到传递给函数的缓冲区中。这类似于itoa 函数,因为数字总是写在缓冲区的开头。完成后,缓冲区以 NUL(0) 终止。函数原型可能如下所示:

; uint32_to_str
;
; Parameters:
;     EAX   = 32-bit unsigned value to print
;     ES:DI = buffer to store NUL terminated ASCII string
;
; Returns:
;     None
;
; Clobbered:
;     None

您的代码还会打印字符串。使用原型创建 print_str 函数:

; print_str
;
; Parameters:
;     DS:SI = NUL terminated ASCII string to print
;
; Returns:
;     None
;
; Clobbered:
;     None

这些只是示例原型。您可以选择在您选择的任何寄存器中传递值和地址。您还可以决定您的函数是否返回一个值以及哪些寄存器被破坏。在这段代码中,我保留了所有使用的寄存器。您可以选择保留部分或全部,这取决于您。

您的引导加载程序可能如下所示:

cpu 386
bits 16
org 0h

start:
    cld
    xor ax,ax
    mov ss,ax
    mov sp,7c00h               ; setup stack

    mov ax,8000h
    mov es,ax                  ; initialize es w/ 8000h
    mov ds,ax                  ; initialize ds w/ 8000h

;=================================================================================

load_prog:
    mov ax,0206h               ; function/# of sec to read
    mov cx,0001h               ; 0-5 sec # (counts from one), 6-7 hi cyl bits

    mov dh,00h                 ; dh=head dl=drive (bit 7=hdd)
    mov bx,0h                  ; data buffer, points to es:0
    int 13h
    cmp ah,0
    jne load_prog              ; this is allowable because it is relative

;=================================================================================

    mov eax, [NUMBERS]
next:
    add eax, 512               ; Advance value by 512

    mov si, msg0
    call print_str

    mov di, strbuf             ; ES:DI points to string buffer to store to
    call uint32_to_str         ; Convert 32-bit unsigned value in EAX to ASCII string

    mov si, di                 ; DS:SI points to string buffer to print
    call print_str

    mov si, msg1
    call print_str

    cmp eax, 1024*4096         ; End loop at 4194304 (1024*4096)
    jl next                    ; Continue until we reach limit

    mov [NUMBERS], eax         ; Store final value in NUMBERS

done:
    hlt
    jmp done


; print_str
;
; Parameters:
;     DS:SI = NUL terminated ASCII string to print
;
; Returns:
;     None
;
; Clobbered:
;     None

print_str:
    push ax
    push di

    mov ah,0x0e
.getchar:
    lodsb                      ; Same as mov al,[si] and inc si
    test al, al                ; Same as cmp al,0
    jz .end
    int 0x10
    jmp .getchar
.end:

    pop di
    pop ax
    ret

; uint32_to_str
;
; Parameters:
;     EAX   = 32-bit unsigned value to print
;     ES:DI = buffer to store NUL terminated ASCII string
;
; Returns:
;     None
;
; Clobbered:
;     None

uint32_to_str:
    push edx
    push eax
    push ecx
    push bx
    push di

    xor bx, bx                 ; Digit count
    mov ecx, 10                ; Divisor

.digloop:
    xor edx, edx               ; Division will use 64-bit dividend in EDX:EAX
    div ecx                    ; Divide EDX:EAX by 10
                               ;     EAX=Quotient
                               ;     EDX=Remainder(the current digit)
    add dl, '0'                ; Convert digit to ASCII
    push dx                    ; Push on stack so digits can be popped off in
                               ;     reverse order when finished

    inc bx                     ; Digit count += 1
    test eax, eax
    jnz .digloop               ; If dividend is zero then we are finished
                               ;     converting the number

    ; Get digits from stack in reverse order we pushed them
.popdigloop:
    pop ax
    stosb                      ; Same as mov [ES:DI], al and inc di
    dec bx
    jne .popdigloop            ; Loop until all digits have been popped

    mov al, 0
    stosb                      ; NUL terminate string
                               ; Same as mov [ES:DI], al and inc di

    pop di
    pop bx
    pop ecx
    pop eax
    pop edx
    ret
    ;================================================================================

    NUMBERS dd 0
    msg0    db 'Counted numbers ',0
    msg1    db ' of 4194304',13,10,0

    ; String buffer to hold ASCII string of 32-bit unsigned number
    strbuf times 11 db 0

    times 510-($-$$) db 0
MBR_Signature:
    db 55h,0aah
    times 4096-($-$$) db 0

函数的替代版本

我通常会使用跳转到循环中间的代码来允许退出条件(字符为零)在末尾而不是中间完成。这避免了必须在最后执行无条件 JMP 指令:

; print_str
;
; Parameters:
;     DS:SI = NUL terminated ASCII string to print
;
; Returns:
;     None
;
; Clobbered:
;     None

print_str:
    push ax
    push di

    mov ah,0x0e
    jmp .getchar               ; Start by getting next character
.printchar:
    int 0x10
.getchar:
    lodsb                      ; Same as mov al,[si] and inc si
    test al, al                ; Is it NUL terminator?
    jnz .printchar             ; If not print character and repeat

    pop di
    pop ax
    ret

最初的uint32_to_str 旨在始终返回从传递的缓冲区开头开始的字符串。这与 C 的非标准函数 itoa 的行为类似,其中传递的缓冲区地址与函数返回的地址相同。

可以通过删除用于反转字符串的推送和弹出来显着简化代码。这可以通过从输出缓冲区中将出现 NUL 终止符的位置开始写入 ASCII 数字来完成。 ASCII 数字在计算时从字符串的末尾向开头插入到缓冲区中。函数返回的地址可能在传递的缓冲区的中间。数字字符串的开头通过以下代码中的 DI 寄存器返回给调用者:

; uint32_to_str
;
; Parameters:
;     EAX   = 32-bit unsigned value to print.
;     ES:DI = buffer to store NUL terminated ASCII string.
;             buffer must be at a minimum 11 bytes in length to
;             hold the largest unsigned decimal number that
;             can be represented in 32-bits including a 
;             NUL terminator.
; Returns:
;     ES:DI   Points to beginning of buffer where the string starts.
;             This may not be the same address that was passed as a
;             parameter in DI initially. DI may point to a position in
;             in the middle of the buffer.
;
; Clobbered:
;     None

uint32_to_str:
    MAX_OUT_DIGITS equ 10      ; Largest unsigned int represented in 32-bits is 10 bytes

    push edx
    push eax
    push ecx

    mov ecx, 10                ; Divisor
    add di, MAX_OUT_DIGITS     ; Start at a point in the buffer we
                               ;     can move backwards from that can handle
                               ;     a 10 digit number and NUL terminator
    mov byte [es:di], 0        ; NUL terminate string

.digloop:
    xor edx, edx               ; Division will use 64-bit dividend in EDX:EAX
    div ecx                    ; Divide EDX:EAX by 10
                               ;     EAX=Quotient
                               ;     EDX=Remainder(the current digit)
    add dl, '0'                ; Convert digit to ASCII
    dec di                     ; Move to previous position in buffer
    mov [es:di], dl            ; Store the digit in the buffer

    test eax, eax
    jnz .digloop               ; If dividend is zero then we are finished
                               ;     converting the number

    pop ecx
    pop eax
    pop edx
    ret

脚注

  • 我不确定您为什么将引导扇区和额外扇区读入内存的 0x0000:0x8000,但我保留了该代码。该代码有效,但我不确定您为什么要这样做。
  • 由于您使用了指令CPU 386 并且使用了 32 位寄存器 EAX,因此我创建了代码以在需要时使用 32 位寄存器,但在其他情况下使用 16 位寄存器。这减少了使代码膨胀的不必要的指令前缀。因此,此代码将仅在具有 386+ 处理器的系统上以实模式运行。您可以使用 16 位寄存器进行 32 位除法,但它更复杂,超出了此答案的范围。

【讨论】:

  • 对于 itoa 函数的 2 个独立循环,我没有得到 push/pop 的普及。如果您已经将指针放入缓冲区并从那里打印而不是即时打印,只需存储并减少指针。 (由于您将要打印,所以直到循环结束后才知道第一个数字的地址是可以的。)
  • @PeterCordes 因为碰巧我是这样写的)。但是,我没有立即回复的原因是因为我正在清理实际上从缓冲区末尾开始并向后移动并返回 DI 中的开头的代码版本,我相信这就是您的建议。附录通过对 print_str 的轻微修改来做到这一点。
  • 在我写的 SO 答案中,我倾向于在数字循环 (How do I print an integer in Assembly Level Programming without printf from the c library?) 之后立即内联打印,因为它们既小又可分离,无需正式称其为“返回值” ”。但是,是的,你可以这样做,比如 glibc 的内部使用 char *_itoa(uint64_t value, char *buflim, base, upper_case) 函数(我压缩了类型),它接受一个指向 buffer + size 的指针。
  • @PeterCordes :如果我相信单独的函数有功能用途,我倾向于不内联。在这种情况下,我宁愿这两个函数独立,这样无论你是否打算打印出来,你都可以使用 uint32_to_str 。也意味着如果您选择直接写入屏幕(在 0xb800:0x0000),您不需要修改转换函数。我的意见是单独的功能更实用。
  • 我对 asm 的体验只针对性能优化,在这种情况下,不手动内联会破坏用 asm 编写而不是让编译器完成的目的。所以这一直是我的直觉。如果我确实想直接写到屏幕上,我会在数字循环中存储它! (或者,如果 VRAM 是 UC 而不是 WC,则在现代 CPU 上复制双字块以提高效率可能会更好。)无论如何,无论如何,在使用 *p-- = digit; 技巧之后,它是我对任何 digit_loop 的首选。
猜你喜欢
  • 2021-05-29
  • 2017-08-29
  • 1970-01-01
  • 2011-03-31
  • 1970-01-01
  • 2015-03-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多