Word count (Assembly Intel x86 Linux)

From LiteratePrograms
Jump to: navigation, search
Other implementations: Assembly Intel x86 Linux | C | C++ | Forth | Haskell | J | Lua | OCaml | Perl | Python | Python, functional | Rexx

An implementation of the UNIX wc tool.

The wc tool counts characters, words and lines in text files or stdin. When invoked without any options, it will print all three values. These options are supported:

  • -c - Only count characters
  • -w - Only count words
  • -l - Only count lines

If the tool is invoked without any file name parameters, it will use stdin.


This is an implementation of wc in x86 assembly for Linux, implemented with NASM. It is assembled with

nasm -f elf wc.asm && ld wc.o -o wc

This code was successfully tested on OpenSUSE 10.0/x86 with NASM version 0.98.38

Contents

[edit] General program structure

A Linux program generally consists of the three sections .data (initialized variables/constants), .bss (uninizialized variables) and .text (program code). The text segment contains a global (i.e. exported) symbol _start which tells the program loader where to start the program. This program only uses uninitialized variables; initialized "variables" are only used for string constants. The program structure therefore looks like this:

<<wc.asm>>=
section .data
constants
strings

section .bss
variables

section .text
        global _start
code

At first, a few generally useful constants are defined.

Since the program will make use of several Linux system calls, it's convenient to define some constants for the used calls:

<<constants>>=
; constants for system calls
sys_exit   equ 1
sys_read   equ 3
sys_write  equ 4
sys_open   equ 5
sys_close  equ 6

Also the interrupt number of the system call gets a name:

<<constants>>=
; system call interrupt number
sys_call   equ 0x80

Since the program will also make use of the standard file descriptors (stdin, stdout, stderr), they get names, too

<<constants>>=
; constants for standard file descriptors
stdin      equ 0
stdout     equ 1
stderr     equ 2

The program opens files for reading. For this, another constant is defined.

<<constants>>=
; constant for open mode
O_RDONLY   equ 0

Also some scratch space is needed for output routines. The size 12 will be explained later.

<<constants>>=
; size of scratch space
scratch_size equ 12
<<variables>>=
scratch resb scratch_size ; scratch space, esp. for output functions

[edit] The code

The general structure of the code is as follows:

<<code>>=
_start:
initialization
parse command line options
process files
exit program

functions

[edit] Initialization and program exit

If any error occured, the exit code of the program will be 1, otherwise it will be 0. Since some errors only cause the skipping of a file instead of terminating the program, the exit code has to be maintained throughout the program. Therefore we store the exit code in a variable

<<variables>>=
exit_code   resq 1 ; stores the exit code

At the beginning, the exit code is set to 0, indicating that no error has occured yet.

<<initialization>>=
; clear exit code
        xor eax, eax
        mov [exit_code], eax

At the end of the program, the program calls the exit system call with the appropriate exit code. Note that the dot at the beginning of the label means it's a local label (in this case, local relative to _start).

<<exit program>>=
.end:
        mov eax, sys_exit
        mov ebx, [exit_code]
        int sys_call

Note that this system call does not return.

[edit] Parsing the command line options

The first thing we have to do is to parse the command line options. When calling a program, Linux puts the program arguments onto the stack. on the top of the stack is the argument count, followed by the addresses of the command line arguments, which are stored in zero-terminated strings. The pointer to the last argument is followed by 0, which indicates the end of the arguments. Due to this "zero-termination" of the arguments it's not necessary to use the argument count. The program will just continue to pop the arguments from the stack until the 0 is reached. Thus the argument count is simply popped from the stack and forgotten. The same happens with the first program argument, which always is the program's own name.

<<parse command line options>>=
        pop ebx         ; argc - not needed because argv ends with 0
        pop ebx         ; argv[0] - not used

We have to store which options were set. Since during option parsing no system calls are done, it is no problem to store them in a register; for that, dl will be used. Initially no option has been seen, therefore dl is cleared.

<<parse command line options>>=
        xor dl, dl    ; during the option loop, this holds the option flags

Different bits of dl store which options were set. For those bits, constants are defined.

<<constants>>=
; option flags
opt_c      equ 1
opt_w      equ 2
opt_l      equ 4

The option parsing is done in a loop. At the beginning of the loop we pop the next argument from the stack into ebx and test if it's an option string. If not, we end the option parsing loop by jumping to the label .opt_end (ebx then still contains the latest pooped option, so the file processing loop can use it). If it's an option string, we process it.

<<parse command line options>>=
.opt_arg_loop:
        pop ebx         ; next argument
jump to .opt_end if not an option argument
process option argument
.opt_end:
fix up and store options

Note that the jump back to the beginning of the loop is inside the option argument processing code. Putting it at the end would result in an unecessary chaining of jumps.

[edit] Determining if the arguments contains options

To determine if the current argument contains options, the first thing to check is if there actually is an argument. Otherwise option parsing is, of course, finished.

<<jump to .opt_end if not an option argument>>=
        test ebx, ebx   ; end of arguments reached?
        jz .opt_end

Option arguments are determined by starting with a single dash.

<<jump to .opt_end if not an option argument>>=
        mov al, [ebx]   ; read first character
        cmp al, '-'     ; '-' indicated either option or stdin "filename"
        jne .opt_end    ; no option -> first filename

However, if the argument consists only of a dash, it's not an option argument, but denotes standard input.

<<jump to .opt_end if not an option argument>>=
        mov al, [ebx+1]
        test al, al
        jnz .eval_opts  ; option characters follow
        jmp .opt_end
.eval_opts:

The reason for the "double-jump" at the end of this block is that conditional jumps are only allowed over short distances, and .opt_end turns out already to be too far away for a conditional jump. Conditional jumping over an unconditional jump avoids this problem.

[edit] Processing the options

Now that we have identified the argument as carrying options, we have to process those options. Since several options can be combined in one argument (e.g. -wc is equivalent to -w -c), this has to be done in a loop. The processing ends as soon as a zero character occurs; in that case, the next argument has to be processed, thus we use a jump to the beginning of the outer, argument processing loop.

Note that for the first option character the fetching and checking for zero is actually superfluous (we've already done that), but it doesn't hurt either.

<<process option argument>>=
.opt_loop:
        inc ebx
        mov al, [ebx]
        test al, al
        jz .opt_arg_loop

Now we have to check for the options to react accordingly.

<<process option argument>>=
        cmp al, 'c'
        je .set_char_flag
        cmp al, 'w'
        je .set_word_flag
        cmp al, 'l'
        je .set_line_flag

All other options are invalid and should get an error message. The error message is "wc: invalid option: option letter". The constant part of the message is stored in the data section.

<<strings>>=
err_option      db "wc: invalid option: "
err_option_len  equ $-err_option

Since output is done through a system call, the current option character has to be saved. This is done in the scratch space, from where it can later be passed to sys_write directly. Since the option char is the last thing in the line to be output, the linefeed character can be appended immediatly

<<process option argument>>=
; if we get here, we have an invalid option, thus give an error and exit
; first save option char + linefeed in scratch
        mov [scratch], al
        mov al, 10            ; ASCII 10 = '\n'
        mov [scratch+1], al

Now we can output first the constant message string and then the option.

<<process option argument>>=
; now, print the constant part of the error message
        mov eax, sys_write
        mov ebx, stderr
        mov ecx, err_option
        mov edx, err_option_len
        int sys_call
; print the part stored in scratch
        mov eax, sys_write
        mov ebx, stderr,
        mov ecx, scratch
        mov edx, 2        ; 1 option character and '\n'
        int sys_call

Finally, we set the exit code to indicate an error occured. In this case, no files are processed, so we directly end the program.

<<process option argument>>=
; return an error code
        mov ebx, 1
        mov [exit_code], ebx
        jmp .end;

For valid options, we just set the corresponding option flag and then continue with the next loop iteration.

<<process option argument>>=
.set_char_flag:
        or dl, opt_c
        jmp .opt_loop
.set_word_flag:
        or dl, opt_w
        jmp .opt_loop
.set_line_flag:
        or dl, opt_l
        jmp .opt_loop

[edit] Finishing the option parsing

After we have processed all option arguments, dl contains the options which were set, and ebx is a pointer to the first file name argument, or 0 if no file name argument is given.

If no option is set, it means we actually want all of them.

<<fix up and store options>>=
; if none of the options are given, we want them all
        test dl, dl
        jnz .options_set
        mov dl, opt_c | opt_w | opt_l

Finally, we store the option flags in a variable, so they survive the file processing which follows

<<fix up and store options>>=
.options_set:
        mov [options], dl
<<variables>>=
options resb 1 ; option flags

[edit] Processing the files

Now we are ready to process the files.

The per-file and total counts are maintained in two variables, counts and totals of three 4-byte integers each, for lines, words and characters.

<<variables>>=
counts  resq 3 ; lines, words, bytes
totals  resq 3 ; total lines, words, bytes

The starting byte position of each counter in those variables is defined as constants.

<<constants>>=
; option positions
line_counter equ 0
word_counter equ 4
char_counter equ 8

Note that the order is exactly the order in which the numbers wil be output.

Before processing the file, the totals have to be initialized with 0.

<<process files>>=
; clear the totals
        xor eax, eax
        mov edi, totals
        cld
        stosd
        stosd
        stosd

The totals are only printed if more than one file is given on the command line. Therefore we need a variable where to store if the totals should be printed. This variable is initialized with 0.

<<process files>>=
        mov [print_total], al
<<variables>>=
print_total resb 1 ; stores whether a total should be printed

If there's no file name argument at all, input is read from stdin. This special case is handled in separate code. This case is detected by checking if ebx (holding the first filename argument) has the value 0 (i.e. no argument left).

<<process files>>=
        test ebx, ebx  ; if ebx is 0, no file names were given
        jnz .file_loop
        jmp .only_stdin

If file names are given, we iterate through the list of file names. A loop invariant is that from the beginning of the loop until the output, ebx holds the current file name. Any function called has to maintain that.

<<process files>>=
.file_loop:
process one file

Again, the jump to the beginning of the loop is inside <<process one file>>.

At the beginning of processing each file, the per-file counters have to be cleared.

<<process one file>>=
; first, initialize the per-file counters
        xor eax, eax
        mov edi, counts
        cld
        stosd
        stosd
        stosd

The next thing to do is to check if the file name is "-", in which case we have to process stdin instead.

<<process one file>>=
; test if we have to read stdin
        mov al, [ebx]
        cmp al, '-'
        jne .real_file
        mov al, [ebx+1]
        test al, al
        jnz .real_file

The actual processing of the file is done in a separate function word_count, thus the code to process stdin is quite simple. The function expects the file name in ebx, and the corresponding file descriptor in eax. If an error occured, the carry flag is set on return. In that case, we skip that file and continue with the next one, otherwise we continue with output. Note that the file name is preserved by the function call.

<<process one file>>=
; count words on stdin
        mov eax, stdin     ; word_count expects the file descriptor in eax
        call word_count    ; word_count signals error with carry flag
        jnc .print_counts  ; if no error occured, print the counts
        jmp .next_file     ; otherwise just continue with the next file

If the argument actually names a file, we first have to open that file. The file name already is in the ebx register, where the system call expects it. However it has to be saved across the system call. Note that since we are only opening for reading, the mode argument, which would be passed in edx, is ignored by the sys_open call, thus there's no need to set it.

<<process one file>>=
.real_file:
        ; a real file first has to be opened
        push ebx           ; must survive the call to sys_open
        mov eax, sys_open
        mov ecx, O_RDONLY
        int sys_call       ; note: mode (in edx) is ignored for O_RDONLY
        pop ebx            ; restore

Now we have to check if the open succeeded. If not, ax contains a negative number; in that case we give an error message.

<<process one file>>=
        test eax, eax      ; was the open successful?
        js .open_failed    ; if not, report an error and skip that file

Now we have the file descriptor in eax, where the function word_count expects it. However, since it is again needed afterwards to close the file, it is saved to the stack before the call.

<<process one file>>=
        push eax           ; save the file descriptor
        call word_count    ; the file descriptor already is in eax

The function returns success or failure in the carry flag. However, in both cases we need to close the file, which inevitably will change that flag, so we need to store it. For that we simply subtract with borrow eax from itself, which effectively copies the carry flag into all bits of eax.

<<process one file>>=
        sbb eax, eax       ; this effectively stores the carry into eax

The system call sys_close expects the file descriptor, which we saved oin the stack, in ebx, where we've stored the file name. Since we in addition need to save the file name, we just exchange those two values. We also save the error result we got from word_count on the stack during the system call.

<<process one file>>=
        xchg ebx, [esp]    ; restore the file descriptor into ebx
                           ; while saving the file name on the stack
        push eax           ; save error state of word count, too
        mov eax, sys_close ; close the file
        int sys_call
        pop eax
        pop ebx

Now we have to examine the error state of word_count to see if we have to skip the results for this file.

<<process one file>>=
        test eax, eax      ; did word_count report an error?
        jnz .next_file     ; if so, skip to the next file

Actually printing the counts is again done in a function, output. It expects the values to be output (counts or totals) in esi, and again the file name in ebx.

<<process one file>>=
.print_counts:
        mov esi, counts    ; output expects the counts to output in esi
        call output

The counts for the current file also have to be added to the totals.

<<process one file>>=
; add the counts to the totals
        mov esi, counts
        mov edi, totals
        cld
        lodsd
        add eax, [edi]
        stosd
        lodsd
        add eax, [edi]
        stosd
        lodsd
        add eax, [edi]
        stosd

To prepare for the next iteration of the loop, the name of the next file is fetched from the argument list on the stack. If no arguments are left, we leave the loop to summarize our results.

<<process one file>>=
.next_file:
        pop ebx            ; get the next file name
        test ebx, ebx
        jz .summarize       ; if ebx is 0, we are ready

If we get here, there was more than one file name. Thus finally we note that a summary has to be printed, and start the next cycle.

<<process one file>>=
        mov al, 1          ; more than 1 file -> print totals
        mov [print_total], al
        jmp .file_loop

In case opening a file failed, an error message is given and the program skips to the next file. Again, the error message starts with a fixed string. The file name argument is saved on the stack.

<<strings>>=
err_open      db "wc: cannot open file: "
err_open_len  equ $-err_open
<<process one file>>=
.open_failed:
        push ebx           ; save file name
; print message start
        mov eax, sys_write
        mov ebx, stderr
        mov ecx, err_open
        mov edx, err_open_len
        int sys_call

After that, the file name is output. Since the file name is stored in the argument list as ASCIIZ, but sys_write needs a string and a length, a helper function is called which calculates the length and does the call. It expects the file descriptor and string argument already in the right registers for sys_write.

<<process one file>>=
; print file name
        pop ecx            ; print_asciiz expects filename in ecx, not ebx
        mov ebx, stderr
        call print_asciiz

Finally, a newline is printed. Since sys_write expects the data in memory, we use a one-byte string constant for that.

<<strings>>=
newline        db 10         ; '\n'
<<process one file>>=
; print '\n' (filename need not be preserved any more)
        mov eax, sys_write
        mov ebx, stderr
        mov ecx, newline
        mov edx, 1
        int sys_call

Finally, the exit code is set to 1, and the next file is processed.

<<process one file>>=
; set exit code to 1
        mov eax, 1
        mov [exit_code], eax
; process next file
        jmp .next_file

At the end, we may have to output summaries. This is done with the same routine output as for the counts, but as "filename" we use "total". Since output expects an ASCIIZ string, we provide one.

<<strings>>=
name_total     db "total", 0 ; zero-terminated string
<<process files>>=
.summarize:
        mov al, [print_total] ; is printing of totals needed?
        test al, al
        jz .end               ; if not, we are ready
        mov ebx, name_total
        mov esi, totals
        call output
        jmp .end

Now there's one unhandled case left: Reading from stdin if no file name was given. This case is actually easy: Word counting and output are done in the subroutines, and totals don't need to be calculated. As "file name", we use just an empty string. Since this directly precedes the code for exiting, a jump is not needed at the end.

<<strings>>=
name_empty     db 0          ; empty zero-terminated string
<<process files>>=
.only_stdin:
; special case: no filenames given
        mov ebx, name_empty   ; the "file name" is just the empty string
        mov eax, stdin
        call word_count
        mov esi, counts
        call output
; fall-through to end

[edit] Counting lines, words and characters

The function word_count is responsible for the actual counting. It expects the descriptor of an open file in eax (that's where sys_open places it) and a pointer to the file name in ebx (this is only used to print an error message if reading fails, but is preserved in any case).

While the file is processed character by character, for efficiency reasons the actual file reading is done in larger blocks. This saves expensive system calls, and also allows some other optimizations noted below.

The data read from the file is placed in a buffer. The size of that buffer determines how much data can be read at once. Thus the size of the buffer represents a performance/size tradeoff: The larger the buffer, the more data can be read per sys_read call, but the more memory the program needs. It is a good idea to make the size of the buffer a multiple of the disk block size (which again is always a multiple of 512 bytes, since that's the size of a disk sector).

Here a relatively conservative value of 2 KBytes has been chosen. This value is, however, not based on measurements. Probably a larger buffer size would be appropriate.

<<constants>>=
buf_size equ 2048    ; 2 KB buffer to speed things up
<<variables>>=
buffer resb buf_size ; file buffer

Counting characters is simple: Since this code assumes single-character encodings, the number of characters is simply the number of bytes read. Also counting lines is simple: Since in Linux, every line ends with a line feed character (ASCII 10), every time we see this character, the line count is increased. This implies that if the file doesn't end with a line feed character, the characters after the last line feed are not considered a separate line. This agrees with Unix conventions (the wc tool which comes with Linux does the same).

Counting the words is only slightly more complicated. Words are sequences of non-whitespace characters, and are separated by whitespace characters. Thus a new word starts whenever a non-whitespace character follows a whitespace character. For this, we need to store whether the last character encountered was a whitespace character. This storage must survive a system call, therefore a variable in memory is used to store that value.

<<variables>>=
whitespace  resb 1 ; stores if we are currently parsing whitespace

The function itself has the following structure:

<<functions>>=
; procedure word_count
; purpose:
;   count the characters, words and lines in a file.
; input:
;   eax: file descriptor
;   ebx: file name
; output:
;   if an error occured, the carry flag is set
; preserved registers:
;   ebx
word_count:
initialize word_count
.read_loop:
read a block of characters
process the block of characters
        jmp .read_loop
.end:
cleanup and return
.error:
print error message and return with error

The code inside <<read a block of characters>> jumps to .end or .error if the end of file resp. a read error is encountered.

If the first character of the file is not a whitespace character, of course it starts a new word. That is, the first character behaves as if it were preceded by whitespace. Therefore the variable whitespace is initialized with 1, which means "true".

<<initialize word_count>>=
        mov dl, 1
        mov [whitespace], dl ; initially we are in whitespace mode

Also, we need to save the file name argument, because we might it need later to give an error message. The fact that we have to save it anyway means that we can keep it across the call.

<<initialize word_count>>=
        push ebx

Finally, during the read loop, the file descriptor will be held in ebx instead of eax, because that's where sys_read expects it.

<<initialize word_count>>=
        mov ebx, eax

In the read loop, the first thing to do is, of course, to read the data to process. The file descriptor is saved over the system call, since it might be needed again to read more characters.

<<read a block of characters>>=
        push ebx
        mov eax, sys_read
        mov ecx, buffer
        mov edx, buf_size
        int sys_call
        pop ebx

If an error occured, sys_read returns a negative value in eax. In that case, we leave the loop to report the error.

<<read a block of characters>>=
        test ax, ax
        js .error    ; if negative, an error occured

If no error occured, eax contains the number of characters read. The end of file is indicated by reading zero bytes. That is, on Linux (and more generally, Unix-like systems) the end of file is only detected as soon as you try to read beyond it, i.e. try to read again after having read all the characters up to the end. This can be seen quite easily by typing the console's "EOF" character (^D) at an incomplete line for a program reading from stdin: This will cause the incomplete line to be sent to the program immediatly (instead of waiting for Enter), but will not cause the program to detect an EOF condition (because it actually receives data). Only if you press ^D again (without typing anything in between), the program detects it as EOF.

Since eax is already tested, the EOF condition can be simply handled by an additional conditional jump, this time to .end:

<<read a block of characters>>=
        jz .end      ; if zero, we've hit EOF

If we have indeed read some characters, we now have to process them. Since sys_read returned the number of read bytes in eax, the character count can simply be updated outside the processing loop by adding that number.

<<process the block of characters>>=
        add [counts + char_counter], eax ; update char count

For the other counts, the characters have to be processed one by one. For this, the string processing instructions will be used; since those always use the al register, we have to store the character count in another register. Traditionally, the ecx register is used for this. Also, the string processing instructions require the source address to be in esi. The direction flag is cleared to indicate that we want to increment that register.

<<process the block of characters>>=
        mov ecx, eax
        mov esi, buffer
        cld

Since during the processing, no system calls are made, the variable whitespace can be held in a register; here dl is used for that purpose.

<<process the block of characters>>=
; during the evaluation loop, dl holds if we are in whitespace mode
        mov dl, [whitespace]
evaluation loop
        mov [whitespace], dl ; store current whitespace mode

The characters are evaluated in a loop with ecx as loop counter holding the number of characters still to process.

<<evaluation loop>>=
.eval_loop:
evaluate next character
.next:
        dec ecx        ; decrement the character counter
        jnz .eval_loop ; if there are unprocessed characters, continue

Evaluation of a character means checking if it is a whitespace character, and specifically if it is a line feed, and acting accordingly. Only space, tab and linefeed characters are considered whitespace.

<<evaluate next character>>=
        lodsb
        cmp al, ' '
        jz .whitespace
        cmp al, 9    ; '\t'
        jz .whitespace
        cmp al, 10   ; '\r'
        jz .newline
; if we got here, we have a non-whitespace character

In the case of a non-whitespace character following a whitespace character, the word count has to be increased.

<<evaluate next character>>=
        test dl, dl
        jz .nowhite
        inc dword [counts + word_counter]
.nowhite:

Finally the fact that a non-whitespace character has been encountered is stored in dl, which holds whitespace, and the next character is evaluated.

<<evaluate next character>>=
        mov dl, 0
        jmp .next

If a line feed was encountered, the line counter is incremented. Since a line feed is also a whitespace character, the code then simply "falls through" to the whitespace handling code.

<<evaluate next character>>=
.newline:
; Increment line counter, then proceed just as with any other whitespace
        inc dword [counts + line_counter]

If whitespace was encountered, this has to be stored in the corresponding variable living in dl.

<<evaluate next character>>=
.whitespace:
        mov dl, 1      ; we are now in whitespace mode
; fall-through to .next

After encountering EOF, the file has been processed successfully. Thus the function returns without error, after restoring the file name from the stack.

<<cleanup and return>>=
        pop ebx        ; restore file name
        clc            ; to indicate no error, clear the carry flag.
        ret

If a read error occured, an error message is written. Again, this error message consists of a static string, followed by the file name and a line feed.

<<print error message and return with error>>=
; print error message
        mov eax, sys_write
        mov ebx, stderr
        mov ecx, err_read
        mov edx, err_read_len
        int sys_call          ; print static part of the error message
        mov ebx, stderr
        mov ecx, [esp]        ; get the file name
        call print_asciiz     ; and print it
        mov eax, sys_write
        mov ebx, stderr
        mov ecx, newline
        mov edx, 1
        int sys_call          ; print newline
<<strings>>=
err_read      db "wc: error reading file: "
err_read_len  equ $-err_read

After that, we set the exit code to 1, so that on return of the program the error will be reported to the calling process. Then we restore the filename, set carry to report the error to the caller of this function, and return.

<<print error message and return with error>>=
; set exit code to 1
        mov eax, 1
        mov [exit_code], eax
; restore file name
        pop ebx
; set carry to indicate error
        stc
        ret

[edit] Output of the results

This procedure outputs the requested counts. The output looks like

        340        2973       17992 COPYING

where the first number gives the lines, the second number gives the words, the third number gives the characters, and the text at the end tells for which file the counts are. The numbers are only output if requested by command line option, e.g. if called with options -cl, the output will instead read

        340       17992 COPYING

with the word count omitted.

The procedure expects in esi the start address of the numbers (counts or totals) to be printed, and in ebx the address text to be printed afterwards (the file name, or "total" for the total counts, as zero-terminated string). Output of the numbers is done in an internal subroutine, which also checks if the number is to be output. Thus the code structure is as follows:

<<functions>>=
; procedure output
; purpose:
;   output file statistics
; input:
;   ebx: pointer to filename (ASCIIZ)
;   esi: pointer to table of counters to output
; preserved registers:
;   none
; uses scratch space
output:
output line, word and character counts
output file name
output newline
        ret
.output_value:
subroutine to output a single count

At the beginning, the file name address is saved on the stack so it doesn't get lost during number output

<<output line, word and character counts>>=
        push ebx

To output the line, word and character counts, we just load the flag to check into dl, and call the output routine. The output routine also increments the counter address, so at the next call it automatically points to the next number to output.

<<output line, word and character counts>>=
        mov dl, opt_l
        call .output_value
        mov dl, opt_w
        call .output_value
        mov dl, opt_c
        call .output_value

To output the file name, we call print_asciiz, which is also used in the error messages. Since that function expects the file name in ecx, it is restored directly to there.

<<output file name>>=
        pop ecx
        mov ebx, stdout
        call print_asciiz

Finally we have to output a line feed character.

<<output newline>>=
        mov eax, sys_write
        mov ebx, stdout
        mov ecx, newline
        mov edx, 1
        int sys_call

The subroutine to output a single count first reads the count into eax, and then tests if the value shall be output. The test is done after the loading because of the side effect of incrementing the source address, which has to be done unconditionally. If the number doesn't need to be output, the rest of this function is skipped.

<<subroutine to output a single count>>=
        cld
        lodsd
        test [options], dl
        jz .skip

The number shall be output right-aligned and padded with spaces. Also, at the left and right we want to have a single space in order to reliably separate the number from preceding/following text.

The string representation of the number is built-up in the scratch buffer. Since we have 32 bit numbers, the maximum possible number is 4294967295, which has 10 digits. Together with the two spaces before and after, we therefore need 12 bytes of scratch space. We already have defined that scratch space above.

The first thing to do is to fill the scratch space with space characters. Note that we know that since the latest clearing of the direction flag we did not make any system calls, so we know for sure it's still cleared.

<<subroutine to output a single count>>=
; fill scratch space with spaces
        mov ebx, eax
        mov edi, scratch
        mov ecx, scratch_size
        mov al, ' '
        rep stosb
        mov eax, ebx

The next thing to do is to convert the number in eax to decimal. This is done by repeatedly dividing the number by 10, and then converting the remainder to decimal by just adding the ASCII code for '0'. Since this delivers the least significant digit first, those digits are filled into the buffer right-to-left. That also automatically causes the result to be right-aligned.

The loop ends when the result of the division gets zero. Note that checking for zero only at the end of the loop ensures an output of the "leading" 0 in the case that the value to output is 0.

<<subroutine to output a single count>>=
; convert value into decimal
        dec edi       ; for the final space
.convert_loop:
        dec edi
        mov ecx, 10
        mov edx, 0    ; div actually divides the 64-bit value in edx:eax by its operand
        div ecx
        add dl, '0'
        mov [edi], dl
        test eax, eax
        jnz .convert_loop

Since now the decimal representation of the number is calculated, all that is left is to output it.

<<subroutine to output a single count>>=
; output decimal value
        mov eax, sys_write
        mov ebx, stdout
        mov ecx, scratch
        mov edx, scratch_size
        int sys_call

After that output, as well as if the output was skipped, the function immediatly returns.

<<subroutine to output a single count>>=
.skip:
        ret

[edit] Output of a zero-terminated string

Finally we need the function to print out zero terminated strings. This is done by searching the end of the string, i.e. the zero byte, and then subtracting the start address in order to get the string length.

This function expects the file descriptor in ebx and the string to print in ecx, just where sys_write expects them. It just looks for the string end, calculates the length, and then calls sys_write.

<<functions>>=
; procedure print_asciiz
; purpose:
;   output an ASCIIZ string
; input:
;   ebx: file descriptor
;   ecx: pointer to asciiz string
; preserved registers:
;   none
print_asciiz:
find string end
calculate length
call sys_write

Finding the end of the string is done with a simple loop, which just loads a byte and tests it until zero is found. The incrementing of the address is implicit in lodsb at the end of the loop, it contains the address of the byte following the zero byte.

<<find string end>>=
        mov esi, ecx
        cld
; search final '\0'
.search_loop:
        lodsb
        test al, al
        jnz .search_loop

The length is calculated by subtracting the start address from the end address. Since the trailing '\0' is not to be output, the end address is decremented first.

<<calculate length>>=
; now esi points to the byte after the '\0'
; the string length is that address minus the start address minus one
        mov edx, esi
        dec edx
        sub edx, ecx

Since now all the arguments to sys_write are in the correct registers, all that is left is to load eax with the system call number and execute the system call.

<<call sys_write>>=
; now we are ready to make the syscall and return
        mov eax, sys_write
        int sys_call
        ret
Download code
hijacker
hijacker
hijacker
hijacker