11.9 命令行参数
如果我们的 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.