Word count (Assembly Intel x86 Linux)
From LiteratePrograms
- 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 |
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
The code
The general structure of the code is as follows:
<<code>>= _start: initialization parse command line options process files exit program functions
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.
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.
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.
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
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
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
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
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
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 |
