如果我们的 hex 程序能读取通过命令行传给它的输入输出文件名, 也就是如果它能处理命令行参数的话, 那它就更好用了。 但是, 这些参数在哪里呢?
在 UNIX® 系统启动程序之前, 它会将一些数据
push 到堆栈中, 接着跳转到程序的 _start 标签。 是的, 我说的是跳转而不是调用。 这意味着,
这些数据可以通过读取 [esp+offset], 或更简单的 pop 得到。
�顶的值包含了命令行参数的个数。 传统上它叫做 argc, 表示 “argument count”。
接下来是 argc 个命令行参数。 传统上这些称为 argv, 表示 “argument value(s)”。 这样, 我们便得到了 argv[0]、 argv[1]、 ...、 argv[argc-1]。
这些并不是实际的参数, 而是指向这些参数的指针, 也就是实际参数的内存地址。 参数本身是以
NUL-结尾的字符串形式存放的。
argv 表以一 NULL 指针结束, 这个指针的值就是 0。 还有一些其它的数据, 但前面这些已经足以让我们达到目的了。
注意: 如果你以前在 MS-DOS® 环境下编程, 现在的主要的区别就是每个参数都在不同的string里头。第二个不同点就是对于参数数量没有实际的限制。
通过这些知识的武装,我们几乎可以立即开始下一个版本的 hex.asm 了。 首先,不论如何,我们需要在 system.inc 增加一些代码:
首先,为我们的系统调用编号清单增加两个新的入口:
%define SYS_open 5 %define SYS_close 6
接下来在文件尾部增加两个新的宏:
%macro sys.open 0
system SYS_open
%endmacro
%macro sys.close 0
system SYS_close
%endmacro
然后就是我们改过的源码:
%include 'system.inc'
%define BUFSIZE 2048
section .data
fd.in dd stdin
fd.out dd stdout
hex db '0123456789ABCDEF'
section .bss
ibuffer resb BUFSIZE
obuffer resb BUFSIZE
section .text
align 4
err:
push dword 1 ; return failure
sys.exit
align 4
global _start
_start:
add esp, byte 8 ; discard argc and argv[0]
pop ecx
jecxz .init ; no more arguments
; ECX contains the path to input file
push dword 0 ; O_RDONLY
push ecx
sys.open
jc err ; open failed
add esp, byte 8
mov [fd.in], eax
pop ecx
jecxz .init ; no more arguments
; ECX contains the path to output file
push dword 420 ; file mode (644 octal)
push dword 0200h | 0400h | 01h
; O_CREAT | O_TRUNC | O_WRONLY
push ecx
sys.open
jc err
add esp, byte 12
mov [fd.out], eax
.init:
sub eax, eax
sub ebx, ebx
sub ecx, ecx
mov edi, obuffer
.loop:
; read a byte from input file or stdin
call getchar
; convert it to hex
mov dl, al
shr al, 4
mov al, [hex+eax]
call putchar
mov al, dl
and al, 0Fh
mov al, [hex+eax]
call putchar
mov al, ' '
cmp dl, 0Ah
jne .put
mov al, dl
.put:
call putchar
cmp al, dl
jne .loop
call write
jmp short .loop
align 4
getchar:
or ebx, ebx
jne .fetch
call read
.fetch:
lodsb
dec ebx
ret
read:
push dword BUFSIZE
mov esi, ibuffer
push esi
push dword [fd.in]
sys.read
add esp, byte 12
mov ebx, eax
or eax, eax
je .done
sub eax, eax
ret
align 4
.done:
call write ; flush output buffer
; close files
push dword [fd.in]
sys.close
push dword [fd.out]
sys.close
; return success
push dword 0
sys.exit
align 4
putchar:
stosb
inc ecx
cmp ecx, BUFSIZE
je write
ret
align 4
write:
sub edi, ecx ; start of buffer
push ecx
push edi
push dword [fd.out]
sys.write
add esp, byte 12
sub eax, eax
sub ecx, ecx ; buffer is empty now
ret
现在我们在 .data 区里头有了两个新的变量 fd.in 和 fd.out.
我们把输入输出文件描述符放在这.
我们还在 .text 区里用 [fd.in] 和 [fd.out] 替换了指向 stdin 和 stdout 的引用。.
现在,.text
段以一个简单的错误处理程序开始,——这个程序除了退出程序时返回 1 之外啥都不干。 该错误处理程序在 _start 之前,由此我们一旦碰到错误,距离会很近。
很自然, 这个程序还是从 _start 开始执行 首先,我们把
argc 和 argv[0]
从栈中移走:在这个程序里头,他们对我们没意义。
将 argv[1] 出栈, 放到 ECX 。这个寄存器很适合于指针,就如同我们把 NULL 指针用 jecxz 处理。 如果 argv[1]
不为空,我们尝试打开第一个参数中的文件。否则我们将照常继续程序:从 stdin 中读取,写入 stdin 。
假设我们还是在打开文件时候失败 (比如文件不存在),跳转到错误处理然后退出。
如果一切顺利,
我们现在可以检查第二个参数。假设文件存在,我们就带开输出文件。否则,把输出送到 stdout。
如果在打开输出文件时候失败(比如文件已经存在但是我们没有写权限),那我们就再来一次错误处理。
剩下的代码跟之前一样,除开我们在退出之前关闭了输入和输出,如前所述,我们使用的是
[fd.in] and [fd.out] 。
然后768 字节大小的可执行文件到手。
我们是否可以进一步改进?理所当然!每个程序都可以改进。以下是一些我们可以做点啥的想法:
我们的错误处理是否输出信息到 stderr.
把错误处理加入 read 和 write 函数。
当我们打开文件用于输入时,关闭 stdin,
反之(输出时)关闭stdout 。
增加命令行参数,比如 -i 和 -o , 这样我们就能用任何次序列出输入和输出文件,或者从 stdin 读入然后写入某个文件。
当命令行参数不正确的时候,打印一份帮助。
I shall leave these enhancements as an exercise to the reader: You already know everything you need to know to implement them.
本文档和其它文档可从这里下载:ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.
如果对于FreeBSD有问题,请先阅读文档,如不能解决再联系<questions@FreeBSD.org>.
关于本文档的问题请发信联系 <doc@FreeBSD.org>.