在 linux 控制台(NASM 程序集)中显示所有 ascii 字符

Displaying all ascii characters in linux console (NASM assembly)

我阅读了关于 nasm 的教程,其中有一个显示整个 ascii 字符集的代码示例。我几乎了解所有内容,除了为什么我们要推送 ecx 和弹出 ecx,因为我看不到它与其余代码的关系。 Ecx 的值为 256,因为我们想要所有字符,但不知道它在哪里以及如何使用。当我们推送和弹出 ecx 时到底发生了什么?为什么我们要将 achar 的地址移动到 dx?我没有看到我们使用 dx 做任何事情。我知道我们需要增加 achar 的地址,但我很困惑增量与 ecx 和 dx 的关系。我将不胜感激。

   section  .text
       global _start        ;must be declared for using gcc

    _start:                 ;tell linker entry point
       call    display
       mov  eax,1           ;system call number (sys_exit)
       int  0x80            ;call kernel

    display:
       mov    ecx, 256

    next:
       push    ecx
       mov     eax, 4
       mov     ebx, 1
       mov     ecx, achar
       mov     edx, 1
       int     80h

       pop     ecx  
       mov  dx, [achar]
       cmp  byte [achar], 0dh
       inc  byte [achar]
       loop    next
       ret

    section .data
    achar db '0'  

I understand pretty much everything

好吧,那么你有点领先于我......(尽管从你的进一步评论中你会意识到该代码中的其他一些无意义的东西:))。

why are we pushing ecx and popping ecx as I dont see how it relates to the rest of the code. Ecx has the value of 256 since we want all chars but no idea where and hows its used.

LOOP instruction (which is not a good idea: Why is the loop instruction slow?使用),它会递减ecx,当值大于零时跳转,即它是一个倒计时循环机制。

由于 int 0x80 服务调用需要 ecx 作为内存地址值,因此计数器是 saved/restored by push/pop 左右。一种更高效的方法是将计数器值放入一些备用寄存器,例如 esi,然后执行 dec esi jnz next。更高效的方法是重新使用字符值本身,如果输出以零值开始,而不是零数字,那么 inc byte [achar] 之后的零标志可用于检测循环条件。

achar db '0'

我不清楚为什么 "display all ASCII characters" 从数字零开始(值 48),我觉得很奇怪,我会从零开始。但这还有另一个警告,linux console I/O 编码是由环境设置的,在任何常见的 linux 安装中现在都是 UTF8,所以有效的可打印单字节字符只是值32-126(与普通的 7 位 ASCII 编码相同,使这部分示例运行良好),值 0-31 和 127 是不可打印的控制字符,也与普通的 7b ASCII 编码相同。值 128-255 表示 UTF8 编码的多字节字符(例如:ř 是两个字节 0xC5 0x99),作为单字节它们是无效的字节序列,因为 UTF8 的剩余部分 "code point" 字节丢失。

在 DOS 时代,你可以编写代码直接写入 VGA 文本模式视频内存,完整的 8 位值从 0 到 255,每个都有 distinct graphical representation,你可以在 VGA 自定义字体中指定或特定字符的已知代码页,有时也称为 "extended ASCII",但常见的 DOS 安装与评论中的 link 不同,有更多的方框绘图字符。这包括 \r\n 控制字符,对于 VGA 来说只是另一种字体字形,而不是换行符和换行符控制字符(这意味着由 BIOS/DOS 服务调用创建,它而不是输出 \n 字符会将内部光标移动到下一行并从输出中丢弃该字符)。

不可能用 linux 控制台 I/O 重新创建它(除非 UTF8 字体包含所有奇怪的 DOS 字形,并且您将输出它们正确的 UTF8 编码而不是单字节值) .

结论是,该示例以值 '0' (48) 开始,直到值 126 它输出正确的可打印 ASCII 字符,在 126 之后它输出 "something",并且由于这些字节有时会形成无效的 UTF8 编码,我在技术上将其称为 "bogus" 具有未定义行为的输出,对于不同的 linux 版本和控制台设置,您可能会得到不同的结果.

还有NASM式的注意事项:在标签后面加上冒号,即achar: db '0',这样可以在你不小心使用指令助记符作为标签时省去你的麻烦,比如loop:dec: db 'd'.

   mov  dx, [achar]

dx 不再使用,所以这是无用的指令。

   cmp  byte [achar], 0dh

这个比较的标志也不再使用,所以这也是无用的。


所以调整后的例子可以是这样的:

section  .text
    global _start       ;must be declared for using gcc

_start:                 ;tell linker entry point
    call    display
    mov     eax,1       ;system call number (sys_exit)
    int     0x80        ;call kernel

; displays all valid printable ASCII characters (32-126), and new-line after.
display:
    mov     byte [achar], ' '   ; first valid printable ASCII
next:
    mov     eax, 4
    mov     ebx, 1
    mov     ecx, achar
    mov     edx, 1
    int     0x80
    inc     byte [achar]
    cmp     byte [achar], 126
    jbe     next        ; repeat until all chars are printed
    ; that will output all 32..126 printable ASCII characters

    ; display one more character, new line (reuse of registers)
    mov     byte [achar], `\n`  ; NASM uses backticks for C-like meta chars
    mov     eax, 4      ; ebx, ecx and edx are already set from loop above
    int     0x80
    ret

section .bss
achar: resb 1           ; reserve one byte for character output

但是先在内存中准备好整个输出,然后一次性输出会更有意义,就像这样:

section  .text
    global _start       ;makes symbol "_start" global (visible for linker)

_start:                 ;linker's default entry point
    call    display
    mov     eax,1       ;system call number (sys_exit)
    int     0x80        ;call kernel

; displays all valid printable ASCII characters (32-126), and new-line after.
display:
    ; prepare in memory string with all ASCII chars and new-line
    mov     al,' '      ; first valid printable ASCII
    mov     edi, allAsciiChars
    mov     ecx, edi    ; this address will be used also for "write" int 0x80
nextChar:
    mov     [edi], al
    inc     edi
    inc     al
    cmp     al, 126
    jbe     nextChar
    ; add one more new line at end
    mov     byte [edi], `\n`
    ; display the prepared "string" in one "write" call
    mov     eax, 4      ; sys_write, ecx is already set
    mov     ebx, 1      ; file descriptor STDOUT
    lea     edx, [edi+1]; edx = edi+1 (memory address beyond last char)
    sub     edx, ecx    ; edx = length of generated string
    int     0x80
    ret

section .bss
allAsciiChars: resb 126-' '+1+1 ; reserve space for ASCII characters and \n

所有示例都在 64b linux 上使用 nasm 2.11.08 进行了尝试("KDE neon" 发行版基于 Ubuntu 16.04),并通过以下命令构建:

nasm -f elf32 -F dwarf -g test.asm -l test.lst -w+all
ld -m elf_i386 -o test test.o

输出:

$ ./test
 !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~