在 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{|}~
我阅读了关于 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{|}~