如何添加数字并将它们显示到引导加载程序中的控制台?
How to add numbers and display them to the console in a bootloader?
我正在创建引导加载程序,它应该加上 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
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
计算商(returned in EAX)和余数(returned in 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
这些只是示例原型。您可以选择在您选择的任何寄存器中传递值和地址。您还可以决定您的函数是否 return 一个值以及哪些寄存器被破坏。在这段代码中,我保留了所有使用的寄存器。您可以选择保留其中的部分或全部,这取决于您。
您的引导加载程序可能类似于:
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
设计成总是return从缓冲区开始的字符串传递。这与 C 的非标准函数 itoa
类似,其中传递的缓冲区地址与函数 return 编辑的地址相同。
可以通过删除用于反转字符串的压入和弹出来显着简化代码。这可以通过从输出缓冲区中将出现 NUL 终止符的位置开始写入 ASCII 数字来完成。 ASCII 数字在计算时从字符串的末尾向开头插入缓冲区。来自函数的地址 return 可能位于传递的缓冲区的中间。数字串的开头通过 DI 寄存器 return 返回给调用者,代码如下:
; 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 位否则注册。这减少了使代码膨胀的不必要的指令前缀。结果,此代码将 运行 in real-mode 仅在具有 386+ 处理器的系统上。您可以使用 16 位寄存器进行 32 位除法,但它更复杂并且超出了本答案的范围。
我正在创建引导加载程序,它应该加上 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
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
计算商(returned in EAX)和余数(returned in 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
这些只是示例原型。您可以选择在您选择的任何寄存器中传递值和地址。您还可以决定您的函数是否 return 一个值以及哪些寄存器被破坏。在这段代码中,我保留了所有使用的寄存器。您可以选择保留其中的部分或全部,这取决于您。
您的引导加载程序可能类似于:
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
设计成总是return从缓冲区开始的字符串传递。这与 C 的非标准函数 itoa
类似,其中传递的缓冲区地址与函数 return 编辑的地址相同。
可以通过删除用于反转字符串的压入和弹出来显着简化代码。这可以通过从输出缓冲区中将出现 NUL 终止符的位置开始写入 ASCII 数字来完成。 ASCII 数字在计算时从字符串的末尾向开头插入缓冲区。来自函数的地址 return 可能位于传递的缓冲区的中间。数字串的开头通过 DI 寄存器 return 返回给调用者,代码如下:
; 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 位否则注册。这减少了使代码膨胀的不必要的指令前缀。结果,此代码将 运行 in real-mode 仅在具有 386+ 处理器的系统上。您可以使用 16 位寄存器进行 32 位除法,但它更复杂并且超出了本答案的范围。