顶部带有 %include 的程序集 - 打印输出意外结果:只是一个“S”
Posted
技术标签:
【中文标题】顶部带有 %include 的程序集 - 打印输出意外结果:只是一个“S”【英文标题】:Assembly with %include at the top - Printing Outputs Unexpected Result: just an " S" 【发布时间】:2022-01-19 01:09:15 【问题描述】:我对汇编编程比较陌生,想知道为什么我的代码没有打印出预期的字符串。这个项目在完成后应该是一个引导加载程序。我正在使用命令nasm -f bin boot.asm -o boot.bin
进行编译。编译过程中没有错误。
boot.asm
bits 16
org 0x7C00
%include "print.asm"
%include "text.asm"
boot:
mov si, boot_string_00
call print
mov si, boot_string_01
call print
times 510 - ($-$$) db 0
dw 0xAA55
print.asm
print:
mov ah, 0x0E
.print_loop:
lodsb
or al, al
je .print_done
int 0x10
jmp .print_loop
.print_done:
cli
ret
text.asm
boot_string_00: db "Placeholder OS Title v0.0.1", 0
boot_string_01: db "Loading Operating system", 0
预期输出:
PlaceHolder OS Title v0.0.1Loading Operating System
实际输出:
S
另外,我想知道如何在汇编中实现换行符,以便我可以在我的字符串中使用 '\n'。
【问题讨论】:
您将额外文件包含在引导加载程序的顶部,它们将首先执行。使用像 BOCH 内置的调试器,这在反汇编视图中应该很容易看到。 asm 没有功能;你必须自己从标签和分支中实现它们。cli
放错了位置。
【参考方案1】:
包括不要放在顶部
当使用%include "print.asm"
之类的指令时,NASM 将在您写入该行的位置插入文件 print.asm 的内容。 %include "text.asm"
也是如此。扩展的源文本因此变为:
bits 16
org 0x7C00
print: \ %include "print.asm"
mov ah, 0x0E |
|
.print_loop: |
lodsb |
or al, al |
je .print_done |
int 0x10 |
jmp .print_loop |
|
.print_done: |
cli |
ret /
boot_string_00: db "Placeholder OS Title v0.0.1", 0 \ %include "text.asm"
boot_string_01: db "Loading Operating system", 0 /
boot:
mov si, boot_string_00
call print
mov si, boot_string_01
call print
times 510 - ($-$$) db 0
dw 0xAA55
现在,当 Bios 完成在地址 0x7C00 处加载引导扇区代码时,它会将控制权转移到内存中的同一地址。 CPU 遇到的第一条指令将是mov ah, 0x0E
,因此我们的打印循环开始了。问题是,我们还没有设置指向消息的指针。目的是让代码在 boot 标签处开始执行,而包含的内容则出错了。
一个快速的解决方案是将jmp boot
指令作为org 0x7C00
指令下的第一条指令。但是,当我们可以更好地把包含放在其余代码下方时,为什么还要浪费 2 或 3 个字节呢?这将是首选的解决方案:
bits 16
org 0x7C00
boot:
mov si, boot_string_00
call print
mov si, boot_string_01
call print
%include "print.asm"
%include "text.asm"
times 510 - ($-$$) db 0
dw 0xAA55
让我们再次展开包含并验证问题是否已解决。
bits 16
org 0x7C00
boot:
mov si, boot_string_00
call print
mov si, boot_string_01
call print
print: \ %include "print.asm"
mov ah, 0x0E |
|
.print_loop: |
lodsb |
or al, al |
je .print_done |
int 0x10 |
jmp .print_loop |
|
.print_done: |
cli |
ret /
boot_string_00: db "Placeholder OS Title v0.0.1", 0 \ %include "text.asm"
boot_string_01: db "Loading Operating system", 0 /
times 510 - ($-$$) db 0
dw 0xAA55
两条消息都打印得很好,但是,如您所见,一旦第二个 call print
返回,代码将在 print 例程中通过并开始打印空消息(因为 SI
寄存器指向times
指令插入的第一个零字节)。
更糟糕的问题是,因为这(第三)次,ret
指令在堆栈上没有合理的返回地址,计算机将崩溃,但以一种危险的方式,因为不知道执行将去哪里!
解决方案是防止代码在 print 子例程中失败。因为我们的程序无事可做,所以我们将插入一个暂停循环,这样 CPU 就不会在紧密循环中浪费宝贵的循环。首选方式是cli
hlt
jmp $-2
。
bits 16
org 0x7C00
boot:
mov si, boot_string_00
call print
mov si, boot_string_01
call print
cli
hlt
jmp $-2
%include "print.asm"
%include "text.asm"
times 510 - ($-$$) db 0
dw 0xAA55
目前的打印例程还有改进的余地
BIOS.Teletype 函数 0Eh 期望BH
寄存器包含所需的 DisplayPage,而 BL
寄存器在屏幕处于图形模式时所需的 GraphicsColor。
在此代码中包含 cli
指令没有任何意义。
测试AL
寄存器为零,最好使用test al, al
。
像这样的简单循环绝不应该使用 2 个分支指令(在迭代时)。
此代码取决于正确的DS
段寄存器。根据org 0x7C00
指令,确保调用代码具有xor ax, ax
mov ds, ax
。
print:
mov bx, 0x0007 ; DisplayPage BH=0, GraphicsColor BL=7
jmp .begin ; This is what makes 'empty messages' OK
.print:
mov ah, 0x0E ; BIOS.Teletype
int 0x10
.begin:
lodsb
test al, al
jnz .print
ret
回答附加问题将进一步改进代码
另外,我想知道如何在汇编中实现换行符,以便我可以在我的字符串中使用 '\n'。
在 NASM 中,您可以用 3 种不同的方式定义字符串字面量。使用单引号 '、使用双引号 " 或使用反引号 `。
只有反引号才能在文本中包含转义序列。为了插入换行符\r\n
,您的 text.asm 需要变为:
boot_string_00: db `Placeholder OS Title v0.0.1\r\n`, 0
boot_string_01: db `Loading Operating system\r\n`, 0
生产
Placeholder OS Title v0.0.1 Loading Operating system
既然换行符嵌入到消息中,您可以考虑简化代码并将 2 行消息作为一个整体输出,总共节省 7 个字节:
boot_string: db `Placeholder OS Title v0.0.1\r\nLoading Operating system\r\n`, 0
为什么是\r\n
序列,而不是简单的\n
?
参考***文章中关于转义序列的引用:
\n 产生一个字节,尽管平台可能使用多个字节来表示换行符,例如 DOS/Windows CR-LF 序列,0x0D 0x0A。在 DOS 和 Windows 上,从 0x0A 到 0x0D 0x0A 的转换发生在将字节写入文件或控制台时,而在读取文本文件时进行反向转换。
\n
(换行符)转义序列只插入换行字节 (10),但由于这是引导加载程序代码,而不是 DOS/Windows,BIOS 将需要回车字节 (13) 和换行字节 ( 10) 为了执行移动到下一行的开头。这就是为什么我们需要写\r\n
。
【讨论】:
我相信你应该在这些反引号中使用\r\n
来创建 int 10.0E 显示完整换行符所需的 13,10 序列。
@ecm 你是对的。这是引导加载程序代码,因此 BIOS 将需要两个字节来执行换行,而不仅仅是换行代码。我现在将编辑答案。谢谢。【参考方案2】:
您在引导加载程序的顶部包含了一些内容,它将首先执行。而是包含不在主要执行路径中且仅由call
访问的额外函数。
这应该可以,将 %include
指令放置在可以安全放置额外函数或数据的位置,就像将它们全部写入一个文件一样。
boot.asm:
[bits 16]
[org 0x7c00]
boot:
xor ax, ax
mov ds, ax ; set up DS to make sure it matches our ORG
mov si, boot_string_00
call println
mov si, boot_string_01
call println
finish: ; fall into a hlt loop to save power when we're done
hlt
jmp finish
%include "printf.asm" ; not reachable except by call to labels in this file
%include "text.S"
times 510-($-$$) db 0
dw 0xaa55
printf.asm:
print:
mov ah, 0x0E ; call number for int 0x10 screen output
print_loop:
lodsb
test al, al
je print_done
int 0x10
jmp print_loop
print_done:
ret
println:
call print
mov si, line_end
call print
ret
文本.S:
boot_string_00: db "Placeholder OS Title v0.0.1", 0
boot_string_01: db "Loading Operating system", 0
line_end: db 0xD, 0xA, 0
【讨论】:
换行符应该是 CR (13, 0Dh) 后跟 LF (10, 0Ah)。 你应该在最后一次调用print
之后添加以下内容:halt:
\sti
\hlt
\jmp halt
您还应该在调用函数之前添加段寄存器初始化:xor ax, ax
\mov ds, ax
为什么要print
在返回之前禁用中断?作为印刷的一部分,这没有意义。如果主引导加载程序需要,它可以自己完成。另外,使用test al,al
而不是or al,al
。它们都可以工作,但test
is better for efficiency 更习惯用语。你有你的line_end
倒退。它可能在这里工作,但更标准的做法是 \r\n
而不是 \n\r
。也不需要在包含之后放置 finish
循环;之前很好。
正如 Peter Cordes 列出的那样,换行符的顺序应该是 CR 13 LF 10,而不是 10 13。cli
是错误的,但公平地说,问题中已经存在。但是,您应该更改“the halting loop from JMP $
to sti
\ hlt
\ jmp
to idle while halting. This means the qemu process won't waste CPU time while running this loop.”以上是关于顶部带有 %include 的程序集 - 打印输出意外结果:只是一个“S”的主要内容,如果未能解决你的问题,请参考以下文章
使用带有 std::cout 的单引号来打印字符串实际上会打印数字 [重复]
我的Android进阶之旅NDK开发之在C++代码中使用Android Log打印日志,打印内容带有文件文件名方法名行号 等信息,方便定位日志输出的地方