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.infd.out. 我们把输入输出文件描述符放在这.

  我们还在 .text 区里用 [fd.in][fd.out] 替换了指向 stdinstdout 的引用。.

  现在,.text 段以一个简单的错误处理程序开始,——这个程序除了退出程序时返回 1 之外啥都不干。 该错误处理程序在 _start 之前,由此我们一旦碰到错误,距离会很近。

  很自然, 这个程序还是从 _start 开始执行 首先,我们把 argcargv[0] 从栈中移走:在这个程序里头,他们对我们没意义。

  将 argv[1] 出栈, 放到 ECX 。这个寄存器很适合于指针,就如同我们把 NULL 指针用 jecxz 处理。 如果 argv[1] 不为空,我们尝试打开第一个参数中的文件。否则我们将照常继续程序:从 stdin 中读取,写入 stdin 。 假设我们还是在打开文件时候失败 (比如文件不存在),跳转到错误处理然后退出。

  如果一切顺利, 我们现在可以检查第二个参数。假设文件存在,我们就带开输出文件。否则,把输出送到 stdout。 如果在打开输出文件时候失败(比如文件已经存在但是我们没有写权限),那我们就再来一次错误处理。

  剩下的代码跟之前一样,除开我们在退出之前关闭了输入和输出,如前所述,我们使用的是 [fd.in] and [fd.out]

  然后768 字节大小的可执行文件到手。

  我们是否可以进一步改进?理所当然!每个程序都可以改进。以下是一些我们可以做点啥的想法:

   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>.