11.7 编写 UNIX® 过滤程序
过滤程序是 UNIX® 中一种常见的应用程序, 它从标准输入 stdin 读入数据, 然后进行相关处理, 最后将结果写到标准输出 stdout。
在本节中, 我们将编写一个简单的过滤程序, 从而学习如何从标准输入 stdin 和标准输出 stdout 进行读写。 这个过滤程序将按字节把输入转换成16进制的数字, 并在每个数字的后面添加一个空格。
%include 'system.inc' section .data hex db '0123456789ABCDEF' buffer db 0, 0, ' ' section .text global _start _start: ; read a byte from stdin push dword 1 push dword buffer push dword stdin sys.read add esp, byte 12 or eax, eax je .done ; convert it to hex movzx eax, byte [buffer] mov edx, eax shr dl, 4 mov dl, [hex+edx] mov [buffer], dl and al, 0Fh mov al, [hex+eax] mov [buffer+1], al ; print it push dword 3 push dword buffer push dword stdout sys.write add esp, byte 12 jmp short _start .done: push dword 0 sys.exit
在数据段, 我们建立一个叫做 hex
的数组, 它包含了按照升序排列的16进制数字。 在这个数组的后面, 是一个输入输出都会用到的缓存。 缓存的头两个字节被初始化设置为 0
。 这里,我们将输出两个16进制数字( 第一个字节也同样是我们读取输入的地方 )。 第三个字节是空格。
代码段由四部分组成: 读入一字节, 转换成16进制数字, 输出结果, 结束程序。
为了读入一字节的数据, 我们命令系统从标准输入 stdin 中读出一个字节的数据, 然后将它存储在缓存 buffer
的第一个字节中。 系统将把返回值存放在寄存器 EAX
中。 返回值如果为 1
则代表有数据输入, 如果为 0
, 则表示没有数据输入。 因此, 我们检查 EAX
中的数值,如果它为 0
我们的程序将跳转至 .done
, 否则我们的程序将继续执行操作。
注意: 出于简单实现程序基本功能的目的, 我们忽略了针对某些可能发生的错误的处理。
16进制转换程序首先从缓存 buffer
中读出一字节的值, 并将其写入 EAX
, 或者, 更确切地说, 这部分是将数据写入 AL
, 并把 EAX
的其他部分清零。 同时, 也将数据复制到 EDX
中, 因为我们需要独立转换高4位和低4位的数据。 最后, 把结果存放在和缓存的头两个字节中。
下面, 我们将让系统将缓存中的这三个字节, 就是两个16进制数字和一个空格, 输出到标准输出 stdout 中。 然后我们的程序将跳转至程序开始处,处理下一个字节。
一旦没有输入, 我们将命令系统终止程序, 返回0。 通常情况下, 使用0作为返回值代表着程序已经执行成功。
接下来, 将程序保存到名为 hex.asm 的文件中, 然后输入以下内容 (符号 ^D 代表在按下控制键的同时, 按下键盘 D):
% nasm -f elf hex.asm % ld -s -o hex hex.o % ./hex Hello, World! 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A Here I come! 48 65 72 65 20 49 20 63 6F 6D 65 21 0A ^D %
注意: 如果你是从 MS-DOS® 上转到 UNIX 的程序员, 你会很奇怪为什么每行结束的时候没有使用
0D 0A
而是使用了0A
。 这是因为 UNIX 使用的不是 cr/lf, 而是换行符号, 也就是是16进制数字0A
所代表的字符来表示换行。
我们能让它看起来更好吗?当然,很明显的一个问题, 当我们转换了一行文字后,我们的输入不再处在行开始的位置。 我们可以修改它,让它在每个 0A
后输出一个新行, 而不是输出一个空格:
%include 'system.inc' section .data hex db '0123456789ABCDEF' buffer db 0, 0, ' ' section .text global _start _start: mov cl, ' ' .loop: ; read a byte from stdin push dword 1 push dword buffer push dword stdin sys.read add esp, byte 12 or eax, eax je .done ; convert it to hex movzx eax, byte [buffer] mov [buffer+2], cl cmp al, 0Ah jne .hex mov [buffer+2], al .hex: mov edx, eax shr dl, 4 mov dl, [hex+edx] mov [buffer], dl and al, 0Fh mov al, [hex+eax] mov [buffer+1], al ; print it push dword 3 push dword buffer push dword stdout sys.write add esp, byte 12 jmp short .loop .done: push dword 0 sys.exit
我们将空格存储在寄存器 CL
中。 这样做不会导致问题, 因为与 Microsoft® Windows® 不同, UNIX 系统调用, 除返回值之外, 并不修改其它寄存器。
所以我们只需要把数值送入 CL
一次即可。 因此我们添加了一个新的标签 .loop
, 在下一个字节处理的时候, 跳转到那里,而不是回到 _start
。 我们也同样增加了一个 .hex
标签, 这样对第三个字节, 我们既可以赋值为空格, 又可以换行符号。
如果你想在 hex.asm 中反映这些变化,请输入:
% nasm -f elf hex.asm % ld -s -o hex hex.o % ./hex Hello, World! 48 65 6C 6C 6F 2C 20 57 6F 72 6C 64 21 0A Here I come! 48 65 72 65 20 49 20 63 6F 6D 65 21 0A ^D %
这样看起来就好了一些。 但是程序的效率还不高! 我们针对一个字节, 进行了两次系统调用 (一次是读取, 另一次是输出)