顶部带有 %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 就不会在紧密循环中浪费宝贵的循环。首选方式是clihltjmp $-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”的主要内容,如果未能解决你的问题,请参考以下文章

C语言案例程序集

提升日志记录集输出级别

带有两个图例的 HTML 字段集 - 顶部和底部

带权并查集模板

使用带有 std::cout 的单引号来打印字符串实际上会打印数字 [重复]

我的Android进阶之旅NDK开发之在C++代码中使用Android Log打印日志,打印内容带有文件文件名方法名行号 等信息,方便定位日志输出的地方