I made an update to the 32-bit version of chastehex for windows. Variable names have been changed to match the Linux version and the getarg function is now used instead of my old badly maintained code. Having all arguments handled by a separate header file helps keep things very organized.
Writing this program with the Windows API in Assembly has taking a lot more time than the Linux version did, but this experience has made me appreciate the Linux system call interface a lot more. I also have working Windows versions of my Assembly programs now because I have made the effort. The chastehex program is only a small taste of what will be included in the Windows edition of Assembly Arithmetic Algorithms for Windows. So far, I have 10 chapters out of 15 to 20 planned.
https://leanpub.com/assemblyarithmeticalgorithms-windows
main.asm
format PE console
entry main
include 'win32a.inc' ;includes standard Windows 32-bit definitions and macros
include 'chastelib-w32.asm' ;include standard library functions by Chastity
include 'getarg-w32.asm' ;include Chastity's getarg function for command line arguments
main:
mov [radix],16 ; Choose radix for integer output.
mov [int_width],1
call getarg ;this first call will get the command string
cmp eax,0 ;did the getarg function return 0?
jz help ;if eax was zero, there are no args so we end the program safely after help message
;optionally display the command string
;call putstring
;call putline
call getarg ;get next arg as file name
cmp eax,0 ;did the getarg function return 0?
jz help ;if eax was zero, there are no args so we end the program safely after help message
;print the filename
call putstring
call putline
mov [filename], eax ;save the filename to a permanent address
jmp open_sesame
help:
mov eax,help_message
call putstring
jmp main_end
open_sesame:
;open a file with the CreateFileA function
;https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea
;call CreateFileA with Windows 32-bit calling convention
push 0 ;hTemplateFile = NULL: We are not using a template file
push 0x80 ;dwFlagsAndAttributes = FILE_ATTRIBUTE_NORMAL
push 3 ;dwCreationDisposition = OPEN_EXISTING
push 0 ;lpSecurityAttributes = NULL: No security attributes
push 1 ;dwShareMode = FILE_SHARE_READ (other programs can read this file)
push 0x10000000 ;dwDesiredAccess = GENERIC_ALL access mode (Read+Write+Execute)
push [filename] ;lpFileName
call [CreateFileA]
;check eax for file handle or error code
;call putint
cmp eax,-1
jnz file_ok
mov eax,file_error_message
call putstring
call [GetLastError]
call putint
jmp main_end ;end program if the file was not opened
;this label is jumped to when the file is opened correctly
file_ok:
mov [fd],eax
;before we proceed, we also check for more arguments.
;get next arg (first one after name of program)
call getarg
cmp eax,0 ;did the getarg function return 0?
jz hexdump ;proceed to normal hex dump if no more args
;otherwise interpret the arg as a hex address to seek to
call strint
mov [offset],eax
;seek to 32-bit address of file with SetFilePointer function
;https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfilepointer
push 0 ;seek from beginning of file (SEEK_SET)
push 0 ;NULL: We are not using a 64 bit address
push [offset] ;where we are seeking to
push [fd] ;seek within this file
call [SetFilePointer]
;check for more args after the address argument
call getarg
cmp eax,0
jz read_one_byte ;proceed to read one byte mode
;otherwise, write the rest of the arguments as bytes to the file!
write_bytes:
call strint
mov [buf],al
;Windows 32-bit WriteFile system call
;https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefile
push 0 ;lpOverlapped = NULL
push 0 ;lpNumberOfBytesWritten = NULL
push 1 ;nNumberOfBytesToWrite = 1
push buf ;lpBuffer = address of data to write
push [fd] ;hFile = [fd] (copied there earlier)
call [WriteFile]
mov eax,[offset]
inc [offset]
mov [int_width],8
call putint_and_space
mov eax,0
mov al,[buf]
mov [int_width],2
call putint_and_line
;check for more args
call getarg ;get next arg as potential bytes to write
cmp eax,0 ;did the getarg function return 0?
jnz write_bytes
;continue write if the args still exist
;otherwise end program
jmp main_end
read_one_byte:
;read only 1 byte using Win32 ReadFile system call.
push 0 ;Optional Overlapped Structure
push count ;Store Number of Bytes Read from this call
push 1 ;Number of bytes to read
push buf ;address to store bytes
push [fd] ;handle of the open file
call [ReadFile]
cmp [count],1
jz print_byte ;if less than one bytes read, there is an error
mov eax,[offset]
mov [int_width],8
call putint_and_space
mov eax,end_of_file
call putstr_and_line
jmp main_end
print_byte:
mov eax,[offset]
mov [int_width],8
call putint_and_space
mov eax,0
mov al,[buf]
mov [int_width],2
call putint_and_line
jmp main_end
hexdump:
;read bytes using Win32 ReadFile system call.
push 0 ;Optional Overlapped Structure
push count ;Store Number of Bytes Read from this call
push 16 ;Number of bytes to read
push buf ;address to store bytes
push [fd] ;handle of the open file
call [ReadFile] ;all the data is in place, do the write thing!
mov eax,[count]
;call putint
;mov eax,buf
;call putstring
cmp eax,0
jnz read_ok ;if more than zero bytes read, proceed to display
jmp eof_end
read_ok:
call print_bytes_row
jmp hexdump
print_EOF:
mov eax,[offset]
mov [int_width],8
call putint_and_space
mov eax,end_of_file
call putstr_and_line
jmp main_end
eof_end:
;before we end the program, let the user know End Of File was reached
mov eax,end_of_file
call putstr_and_line
main_end:
;close the file
push [fd]
call [CloseHandle]
;Exit the process with code 0
push 0
call [ExitProcess]
;variables for displaying messages
file_error_message db 'error: ',0
end_of_file db 'EOF',0
read_error_message db 'Failure during reading of file. Error number: ',0
help_message db 'chastehex by Chastity White Rose',0Ah,0Ah
db 'hexdump a file:',0Ah,0Ah,9,'chastehex file',0Ah,0Ah
db 'read a byte:',0Ah,0Ah,9,'chastehex file address',0Ah,0Ah
db 'write a byte:',0Ah,0Ah,9,'chastehex file address value',0Ah,0Ah
db 'The file must exist',0Ah,0
;this function prints a row of hex bytes
;each row is 16 bytes
print_bytes_row:
mov eax,[offset]
mov [int_width],8
call putint_and_space
mov ebx,buf
mov ecx,[count]
add [offset],ecx
next_byte:
mov eax,0
mov al,[ebx]
mov [int_width],2
call putint_and_space
inc ebx
dec ecx
cmp ecx,0
jnz next_byte
mov ecx,[count]
pad_spaces:
cmp ecx,0x10
jz pad_spaces_end
mov eax,space_three
call putstring
inc ecx
jmp pad_spaces
pad_spaces_end:
;optionally, print chars after hex bytes
call print_bytes_row_text
call putline
ret
space_three db ' ',0
print_bytes_row_text:
mov ebx,buf
mov ecx,[count]
next_char:
mov eax,0
mov al,[ebx]
;if char is below '0' or above '9', it is outside the range of these and is not a digit
cmp al,0x20
jb not_printable
cmp al,0x7E
ja not_printable
printable:
;if char is in printable range,copy as is and proceed to next index
jmp next_index
not_printable:
mov al,'.' ;otherwise replace with placeholder value
next_index:
mov [ebx],al
inc ebx
dec ecx
cmp ecx,0
jnz next_char
mov [ebx],byte 0 ;make sure string is zero terminated
mov eax,buf
call putstring
ret
;variables for managing file IO.
filename dd ?
count dd ? ;how many bytes are read with ReadFile operation
buf db 16 dup ?,0
fd dd ?
offset dd ?
;FASM builds the Import Address Table (IAT) directly in the source file
section '.idata' import data readable writeable
library kernel32, 'KERNEL32.DLL'
import kernel32,\
GetStdHandle, 'GetStdHandle',\
WriteFile, 'WriteFile',\
ExitProcess, 'ExitProcess',\
GetCommandLineA, 'GetCommandLineA',\
CreateFileA, 'CreateFileA',\
GetLastError, 'GetLastError',\
SetFilePointer, 'SetFilePointer',\
ReadFile, 'ReadFile',\
CloseHandle, 'CloseHandle'
chastelib-w32.asm
; chastelib assembly header file for 32 bit Windows
; This file is where I keep the source of my most important Assembly functions
; These are my string and integer output and conversion routines.
; To simplify documentation. The Accumulator/Arithmetic register
; (ax,eax,rax) depending on bit size shall be referred to as register A
; for the description of these core functions because the A register
; is treated special both by the Intel company and my code;
; putstring; Prints a zero terminated string from the address pointer to by A register.
; intstr; Converts the number in A into a zero terminated string and points A to that address
; putint; Prints the integer in A by calling intstr and then putstring.
; strint; Converts the zero terminated string into an integer and sets A to that value
; Now, the source of the functions begins, with comments included for parts that I felt needed explanation.
putstring: ;print string pointed to by eax register
push eax
push ebx
push ecx
push edx
mov ebx,eax ;copy eax to ebx to be used as index to the string
putstring_strlen_start: ;this loop finds the length of the string as part of the putstring function
cmp [ebx],byte 0 ;compare byte at address ebx with 0
jz putstring_strlen_end ;if comparison was zero, jump to loop end because we have found the length
inc ebx
jmp putstring_strlen_start
putstring_strlen_end:
sub ebx,eax ;subtract start pointer from current pointer to get length of string
;Windows 32-bit WriteFile system call
push 0 ;lpOverlapped = NULL
push 0 ;lpNumberOfBytesWritten = NULL
push ebx ;nNumberOfBytesToWrite = ebx
push eax ;lpBuffer = address of string to write
push -11 ;STD_OUTPUT_HANDLE = Negative Eleven
call [GetStdHandle] ;Get Standard Handle for -11
push eax ;hFile = eax (returned from GetStdHandle)
call [WriteFile]
pop edx
pop ecx
pop ebx
pop eax
ret
; This is the location in memory where digits are written to by the intstr function
; The string of bytes and settings such as the radix and width are global variables defined below.
int_string db 32 dup '?' ;reserve bytes for characters string for 32-bit binary integer
int_string_end db 0 ;zero byte terminator for the integer string
radix dd 2 ;radix or base for integer output. 2=binary, 8=octal, 10=decimal, 16=hexadecimal
int_width dd 8 ;default width of integers. Extra zeros prefixed if more than 1
;this function creates a string of the integer in eax
;it uses the above radix variable to determine base from 2 to 36
;it then loads eax with the address of the string
;this means that it can be used with the putstring function
intstr:
mov ebx,int_string_end-1 ;find address of lowest digit
mov ecx,1
digits_start:
mov edx,0;
div dword [radix]
cmp edx,10
jb decimal_digit
jnb hexadecimal_digit
decimal_digit: ;we go here if it is only a digit 0 to 9
add edx,'0'
jmp save_digit
hexadecimal_digit:
sub edx,10
add edx,'A'
save_digit:
mov [ebx],dl
cmp eax,0
jz intstr_end
dec ebx
inc ecx
jmp digits_start
intstr_end:
prefix_zeros:
cmp ecx,[int_width]
jnb end_zeros
dec ebx
mov [ebx],byte '0'
inc ecx
jmp prefix_zeros
end_zeros:
mov eax,ebx ;point eax register to this string for putstring
ret
;function to print string form of whatever integer is in eax
;The radix determines which number base the string form takes.
;Anything from 2 to 36 is a valid radix
;in practice though, only bases 2,8,10,and 16 will make sense to other programmers
;this function does not process anything by itself but calls the combination of my other
;functions in the order I intended them to be used.
putint:
push eax
push ebx
push ecx
push edx
call intstr
call putstring
pop edx
pop ecx
pop ebx
pop eax
ret
;this function converts a string pointed to by eax into an integer returned in eax instead
;it is a little complicated because it has to account for whether the character in
;a string is a decimal digit 0 to 9, or an alphabet character for bases higher than ten
;it also checks for both uppercase and lowercase letters for bases 11 to 36
;finally, it checks if that letter makes sense for the base.
;For example, G to Z cannot be used in hexadecimal, only A to F can
;The purpose of writing this function was to be able to accept user input as integers
;This function is improved with error checking and uses the new strint_error variable
;The program can check this value after the call and see how many errors happened.
strint_error db 0 ;declare a byte variable that keeps track of errors
strint:
mov ebx,eax ;copy string address from eax to ebx because eax will be replaced soon!
mov eax,0
mov byte[strint_error],0 ;set errors to 0 at the start of this function
read_strint:
mov ecx,0 ;zero ecx so only lower 8 bits are used
mov cl,[ebx]
inc ebx
cmp cl,0 ;compare this byte with 0
jz strint_end ; if comparison was zero, this is the end of string
;if char is below '0' or above '9', it is outside the range of these and is not a digit
cmp cl,'0'
jb not_digit
cmp cl,'9'
ja not_digit
;but if it is a digit, then correct and process the character
is_digit:
sub cl,'0'
jmp process_char
not_digit:
;it isn't a decimal digit, but it could be perhaps an alphabet character
;which could be a digit in a higher base like hexadecimal
;we will check for that possibility next
;if char is below 'A' or above 'Z', it is outside the range of these and is not capital letter
cmp cl,'A'
jb not_upper
cmp cl,'Z'
ja not_upper
is_upper:
sub cl,'A'
add cl,10
jmp process_char
not_upper:
;if char is below 'a' or above 'z', it is outside the range of these and is not lowercase letter
cmp cl,'a'
jb not_lower
cmp cl,'z'
ja not_lower
is_lower:
sub cl,'a'
add cl,10
jmp process_char
not_lower:
;if we have reached this point, result invalid and end function with error
jmp strint_end_error
process_char:
cmp ecx,[radix] ;compare char with radix
jnb strint_end_error ;if this value is above or equal to radix, it is too high despite being a valid digit/alpha
mov edx,0 ;zero edx because it is used in mul sometimes
mul dword [radix] ;mul eax with radix
add eax,ecx
jmp read_strint ;jump back and continue the loop if nothing has exited it
strint_end_error: ;we jump here if there was an error with one of the chars
inc byte[strint_error] ;increment error counter because char invalid
strint_end: ;we jump here when no errors happened
ret
;The utility functions below simply print a space or a newline.
;these help me save code when printing lots of strings and integers.
space db ' ',0 ;a string containing only a space
putspace:
push eax
mov eax,space
call putstring
pop eax
ret
line db 0x0D,0x0A,0 ;a string containing only a newline
;the next function which pushes eax to the stack
;moves the address of the line string and prints it with putstring
;then it pops the original value of eax back from the stack before the function returns
;this allows me to print a newline anywhere in the code without a single register changing
putline:
push eax
mov eax,line
call putstring
pop eax
ret
;a function for printing a single character that is the value of al
char: db 0,0
putchar:
push eax
mov [char],al
mov eax,char
call putstring
pop eax
ret
;a small function just for the common operation of
;printing an integer followed by a space
;this saves a few bytes in the assembled code
;by reducing the number of function calls in the main program
putint_and_space:
call putint
call putspace
ret
;a small function just for the common operation of
;printing an integer followed by a line feed
;this saves a few bytes in the assembled code
;by reducing the number of function calls in the main program
putint_and_line:
call putint
call putline
ret
;a small function just for the common operation of
;printing a string followed by a line feed
;this saves a few bytes in the assembled code
;by reducing the number of function calls in the main program
;it also means we don't need to include a newline in every string!
putstr_and_line:
call putstring
call putline
ret
getarg-w32.asm
;This is the Windows 32-bit source of the getarg function
;The getarg function makes my assembly code for DOS and Windows easier to read
;by writing one function to handle all possible cases I will need for my tools.
;
;The first time it is run, it returns the whole command string or zero if no args are given
;Each time after that, it will give you the next argument, which is a substring of the original.
;When no more arguments are available, it will always return zero
;The program calling this is expected to check for this error and then terminate
;or print a message depending on the goals of that program
;A word of warning, though, this function has multiple return statements and is long
;However, it is fully featured in that it can recognize quoted strings as being the same argument
;This brings full compatibility for my DOS and Windows programs that use command line arguments.
;Because all of my programs are ported to DOS and Windows from Linux
;I found there to be conflicts in the way these operating systems did not
;handle the quoted strings.
;
;This same function is available in a separate file for DOS Assembly.
getarg:
mov ebx,[arguments_start] ;get the address of start of arguments
cmp ebx,0 ;is this address zero? (meaning this function was not called before)
jz get_arg_data ;if it was zero, then get the argument data for the first execution of this function
;if the start was not zero, then clearly arguments exist and addresses have been saved
cmp ebx,[arguments_end] ;is the address of the start and end the same?
jnz find_next_string ;if they are not the same, find the next sub string
mov eax,0 ;otherwise, return eax as zero and check this in the main program
ret
find_next_string:
mov ebx,[arguments_start] ;get address of current arg
skip_spaces:
cmp byte[ebx],' ' ;is this byte a space?
jnz skip_spaces_end ;if it is not a space, we can end this loop
inc ebx ;otherwise, go to next byte
jmp skip_spaces ;and keep looping till we find non-space
skip_spaces_end:
mov eax,ebx ;copy this non-space address to eax register
;we have found a non-space which is the start of a printable string
;but we still have to find the next space and terminate it with a zero!
;however, there is a special case where we want a string to contain spaces. In this case, I have another routine!
;check for quoted strings
cmp byte[ebx],0x22 ;is this a double quote -> "
jz scan_quoted_string
cmp byte[ebx],0x27 ;is this a single quote -> '
jz scan_quoted_string
find_space:
cmp byte [ebx],' ' ;is this a space?
jz found_space ;if this was a space, end the loop and terminate with zero
;we must also check to see if we have reached the terminating zero of the arguments string
cmp byte[ebx],0 ;is this byte a zero?
jz no_more_args ;if yes this string is already terminated
inc ebx
jmp find_space ; this char was not space, go to the next char
found_space:
mov byte[ebx],0 ;terminate this string
inc ebx ;but go to the next byte
mov [arguments_start],ebx ;and set the new start address for the next call
ret ;We can return eax safely knowing the string ends in a zero
scan_quoted_string:
mov cl,byte[ebx] ;mov this quote type to cl
inc ebx ;go to next byte
mov eax,ebx ;set eax to this address which is assumed to be the start of a quoted string
find_end_quote:
cmp byte[ebx],cl ;is this the same quote we started with?
jz found_end_quote ;if it is, end this loop
;we must also check to see if we have reached the terminating zero of the arguments string
;this avoids a crash if I forgot to add the second quotation mark in the arguments
cmp byte[ebx],0 ;is this byte a zero?
jz no_more_args ;if yes this string is already terminated
inc ebx
jmp find_end_quote
found_end_quote:
mov byte[ebx],0 ;terminate this string
inc ebx ;but go to the next byte
mov [arguments_start],ebx ;and set the new start address for the next call
ret
no_more_args:
mov [arguments_start],ebx ;mov the start to where the string ended
;now that the start and end addresses are the same
;this function will always return zero
ret
;this will happen first time this function is called to get the argument data
get_arg_data:
;get command line argument string with this Windows API call
;it returns a pointer in the eax register
call [GetCommandLineA]
mov [arguments_start],eax ;but copy it to memory too
;the following loop is a modified strlen loop except that we don't
;care about how long the string is. We only need the pointer for where it ends
mov ebx,eax ;copy eax to ebx to be used in loop to find ending zero
argend_find: ; this loop finds the zero at the end of the argument string
cmp [ebx],byte 0 ; compare byte at address ebx with 0
jz argend_found ; if comparison was zero, jump to loop end because we have found the length
inc ebx
jmp argend_find
argend_found:
mov [arguments_end],ebx ;save ebx address containing the zero byte
mov ebx,eax ;mov the start of the arg string again before the next loop
skip_prog_name:
cmp byte[ebx],0 ;check for zero first (very important)
jz no_args ;if we found a zero before a space, there are no arguments after program name!
cmp byte[ebx],' ' ;is this byte a space?
jz skip_prog_name_end ;if it is a space, we can end this loop
mov byte[ebx],' ' ;turn this byte into a space and therefore delete it
inc ebx ; go to next byte
jmp skip_prog_name ;and keep looping till we find space
skip_prog_name_end:
mov eax,ebx ;copy this non-space address to eax register
;return eax pointing to an address of string with arguments after program name
ret
no_args:
mov eax,0
;in this case, we return zero because there are no args
ret
;start and end default to address of zero, which means we have not tested the arguments yet
arguments_start dd 0
arguments_end dd 0
Leave a Reply