After having some minor success with Windows Intel Assembly, I took a break and went back to learning RISC-V Assembly using the RARS simulator and Robert Winkler’s book as a reference. I have managed to convert most of my Intel functions, including the Standard Input functions into RISC-V Assembly Language.
The benefit of this will become useful for both simulation and teaching but also eventually on real hardware if I ever have the time and money to play with such things. In the mean time, I am appreciating how easy RISC-V is compared to Intel Assembly.
# chastelib test suite for RISC-V Assembly in RARS simulator
# this program tests the stdin extension of chastelib
# The same library of functions I commonly use in my Intel Assembly code
# have now been translated to RISC-V.
# All assembly code seen here is for the RARS simulator written in Java.
.data
##################################################################
# chastelib core specific variables #
# #
# These variables are used by the intstr function to convert an #
# integer to a string and what radix and widthshould be used #
# width means how many minimum digits including leading zeros #
##################################################################
int_string: .space 32 #reserve space for 32 bytes for up to 32 bits if printed in binary
int_end: .byte 0 #the terminating zero of the integer string
radix: .byte 2 #the radix the number will be shown in
int_width: .byte 1 #by default
# These variables are for outputting special strings
# such as a newline, space, or a single character based on s0
space: .byte 0x20, 0
line: .byte 0x0A, 0
char: .byte 0, 0
##################################################################
# chastdin specific variables #
# #
# these variables are used as the default controllers #
# for the getstring and getline functions #
# buf stores keyboard input during those functions #
# count stores how many bytes were read during system read calls #
# last_char stores the last character read #
# usually this will be a space, tab, or newline #
##################################################################
buf: .space 0x100
count: .word 0
last_char: .byte 0
# program specific variables
# These variables are for outputting specific messages
# or to simulate user input as integers in the strint function
string0: .ascii "chastelib test suite for RISC-V Assembly\n"
string1: .asciz "stdin (STanDard INput) extension\n"
string_exit: .asciz "exit"
.text
la s0, string0
jal putstr
# change radix for this program
li t0, 16 #load t0 register with the new radix
la t1, radix #load t1 register with the address the radix will go to
sb t0, 0(t1) #save t0 register (byte) to address t1
main_loop:
jal getstr # read the string from standard input
jal putline # print extra line for readability
jal putstr # echo it to standard output
jal putline
#s0 already contains string that was input and printed
#s1 will be loaded with address of exit string
la s1, string_exit
jal strcmp
# end program if the string entered is equal to string_exit
beq t0, zero, exit
#method 0: loading the length of string just entered from (count)
#la t1, count #load address of count into t2
#lw s0, 0(t1) #store number of chars read at (count) address
#method 1: calculate the length with strlen function
jal strlen
# regardless of method used, display the length of last string
jal putint
jal putline
j main_loop # keep restarting until exit string is entered
exit:
li a0, 0 #status
li a7, 93 #exit
ecall #environment call
#################################################################################
# The following functions are independent of a specific RISC-V Operating System #
# #
# intstr = convert integer into a string ready for printing #
# putint = prints integer using intstr and the OS specific putstr function #
# strint = convert string into an integer #
# #
# The s0 register is used for pass data in or out of these functions #
# See comments above those specific functions for full details #
#################################################################################
# The intstr function does several things at once and is the foundation for all integer output.
# It uses the global radix variable to know which radix or number base to use when turning the integer to a string
# It also uses the global int_width variable to determine how many leading zeros should be used for the string
# The purpose of this is to make numbers look good when lined up when they are printed in a list.
# radices 2 to 36 are supported. Digits higher than 9 will be capital letters
intstr:
la t1, radix #load address of radix into t1
lb t2, 0(t1) #load value of radix into t2
la t1, int_width #load address of width into t1
lb t4, 0(t1) #load value of int_width into t4
li t3, 1 #load current number of digits, always 1
la t1, int_end #t1=address of terminating zero in string
addi t1, t1, -1 #t1-- to go to lowest digit
digits_start:
remu t0, s0, t2 #t0=remainder of the previous division
divu s0, s0, t2 #s0=s0/t2 (divide s0 by the radix value in t2)
li t5, 10 #load t5 with 10 because RISC-V does not allow constants for branches
blt t0, t5, decimal_digit
bge t0, t5, hexadecimal_digit
decimal_digit: #we go here if it is only a digit 0 to 9
addi t0, t0, 0x30
j save_digit
hexadecimal_digit:
addi t0, t0, -10
addi t0, t0, 0x41
save_digit:
sb t0, 0(t1) #store byte from t0 at address t1
beq s0, zero, intstr_end
addi t1, t1, -1
addi t3, t3, 1
j digits_start
intstr_end:
li t0, 0x30
prefix_zeros:
bge t3, t4, end_zeros
addi t1, t1, -1
sb t0, 0(t1) # store byte from t0 at address t1
addi t3, t3, 1
j prefix_zeros
end_zeros:
mv s0, t1
ret
# this function calls intstr to convert the s0 register into a string
# then it uses the system specific putstr call to print the string
# it also uses the stack to save the value of s0 and ra (return address)
# this way, s0 is restored to the value it had before this function
# restoring ra is required because it is modified during calls to other functions
putint:
addi sp, sp, -8
sw ra, 0(sp)
sw s0, 4(sp)
jal intstr
jal putstr
lw ra, 0(sp)
lw s0, 4(sp)
addi sp, sp, 8
ret
# RISC-V does not allow constants for branches
# Because of this fact, the RISC-V version of strint
# requires a lot more code than the MIPS version
# Whatever value I wanted to compare in the branch statement
# was placed in the t5 register on the line before the conditional branch
# Even though it is completely stupid, it has proven to work
strint:
la t1, radix #load address of radix into t1
lb t2, 0(t1) #load value of radix into t2
mv t1, s0 #copy string address from s0 to t1
li s0, 0
read_strint:
lb t0, 0(t1)
addi t1, t1, 1
beq t0, zero, strint_end
#if char is below '0' or above '9', it is outside the range of these and is not a digit
li t5, 0x30
blt t0, t5, not_digit
li t5, 0x39
blt t5, t0, not_digit
#but if it is a digit, then correct and process the character
is_digit:
andi t0, t0, 0xF
j process_char
not_digit:
#it isn't a digit, but it could be an alphabet character
#which counts as a digit in a higher base
# if char is below 'A' or above 'Z', it is outside the range of these and is not capital letter
li t5, 0x41
blt t0, t5, not_upper
li t5, 0x5A
blt t5, t0, not_upper
is_upper:
li t5, 0x41
sub t0, t0, t5
addi t0, t0, 10
j process_char
not_upper:
# if char is below 'a' or above 'z', it is outside the range of these and is not lowercase letter
li t5, 0x61
blt t0, t5, not_lower
li t5, 0x7A
blt t5, t0, not_lower
is_lower:
li t5, 0x61
sub t0, t0, t5
addi t0, t0, 10
j process_char
not_lower:
# if we have reached this point, result invalid and end function
# this is only reached if the byte was not a valid digit or alphabet character
j strint_end
process_char:
blt t2, t0 strint_end #;if this value is above or equal to radix, it is too high despite being a valid digit/alpha
mul s0, s0, t2 # multiply s0 by the radix
add s0, s0, t0 # add the correct value of this digit
j read_strint # jump back and continue the loop if nothing has exited it
strint_end:
ret
###############################################################################
# This putstr function is my most portable function for RISC-V simulators #
# It calculates the length of a zero terminated string before printing it #
# This is the same way used in my Intel Assembly programs for DOS and Linux #
# This function was written to operate the same in both RARS and riscemu #
###############################################################################
putstr:
mv t1, s0 # t1 will be used as an index register
putstr_strlen_start:
lb t0, 0(t1) # load byte into t0 from address of t1
beq t0, zero, putstr_strlen_end # if t0==0, then we jump to the end of the loop.
addi t1, t1, 1 # go to next byte
j putstr_strlen_start # jump to start of the loop
putstr_strlen_end:
li a0, 1 # STDOUT file number
mv a1, s0 # address of string
sub a2, t1, s0 # length of string
li a7, 64 # write call number
ecall # environment call
ret
#############################################################################
# The next four 3 functions print things to standard output #
# All of them use the putstr function above to achieve the output #
# They use the stack to preserve the values of the s0 and t1 registers used #
# They also use global variables in the data section #
#############################################################################
#the putchar function, which is named after the C language function of the same name
#prints the lowest byte of the s0 register as a byte or character to standard output
putchar:
addi sp, sp, -12
sw ra, 0(sp)
sw s0, 4(sp)
sw t1, 8(sp)
la t1, char
sb s0, 0(t1)
la s0, char
jal putstr
lw ra, 0(sp)
lw s0, 4(sp)
lw t1, 8(sp)
addi sp, sp, 12
ret
# the putspace function prints a space to standard output
putspace:
addi sp, sp, -8
sw ra, 0(sp)
sw s0, 4(sp)
la s0, space
jal putstr
lw ra, 0(sp)
lw s0, 4(sp)
addi sp, sp, 8
ret
# the putline function prints a newline to standard output
putline:
addi sp, sp, -8
sw ra, 0(sp)
sw s0, 4(sp)
la s0, line
jal putstr
lw ra, 0(sp)
lw s0, 4(sp)
addi sp, sp, 8
ret
##########################################################################
# chastdin extension functions #
# #
# all functions that deal with getting strings and characters from stdin #
##########################################################################
# the getstr function will read a string into a buffer and return it
# in the s0 register for printing with the putstr function
# the (count) variable will also return the number of characters
getstr:
li t0, 0 # use t0 register to track chars read
la a1, buf # load address of buffer for read string
li a2, 1 # read only 1 byte for each env call
getstring_chars:
li a0, 0 # STDIN file number
li a7, 63 # read call number
ecall # environment call
# Branch to label getstring_end if a0 is less than a2
# a0 is the return value of this environment read call
# as will be -1 on error or 1 if successful
# because we read 1 character at a time
blt a0, a2, getstring_end
# if no error, test range of the last byte
lb t1, 0(a1) #load byte at address (a1) into t1 register
# if t1 is less than 0x21
# of t1 is more than 0x7E
# branch to function end because it is outside of print range
li t2, 0x21
blt t1, t2, getstring_end
li t2, 0x7E
blt t2, t1, getstring_end
# otherwise, proceed to read more characters
add t0, t0, a0 # add to read counter
addi a1, a1, 1 # add 1 to buffer pointer register a1
j getstring_chars # unconditional jump to getstring_chars
getstring_end:
la t2, count #load address of count into t2
sw t0, 0(t2) #store number of chars read at (count) address
la t2, last_char #load address of last_char into t2
sb t1, 0(t2) #store last byte at (last_char) address
sb zero, 0(a1) #store byte zero to terminate string
la s0, buf #return address of buf in s0 register
ret
# Short Description of strlen:
# The strlen function gets the length of string in s0 and returns it in s0
# This is the same algorithm used in my putstr function but is independent of an operating system.
strlen:
mv t1, s0 # t1 will be used as an index register
strlen_start:
lb t0, 0(t1) # load byte into t0 from address of t1
beq t0, zero, strlen_end # if t0==0, then we jump to the end of the loop.
addi t1, t1, 1 # go to next byte
j strlen_start # jump to start of the loop
strlen_end:
sub s0, t1, s0 # return length of string in s0
ret
# Short Description of strcmp:
# strcmp compares the string at s0 to the one at s1
# t0 returns 0 if the strings are the same and non zero if different
# the algorithm is simple but I will explain it for those who are confused
# Long Description of strcmp:
# each byte from each string is loaded into the t0 and t1 registers
# the bytes are compared. if they are different, then we jump to the end
# However, if they are the same, then we check if one of them is zero
# if it is zero, this also jumps to the end of the function
# If neither jump took place, then we jump to the start of the loop
# but when the function finally ends t1 will be subtracted from t0
# this ensures that the t0 register returns zero if the final characters are the same
# a zero result in t0 also guarantees that both strings are equal
strcmp:
mv a0,s0 # move pointer s0 to t0
mv a1,s1 # move pointer s0 to t0
strcmp_start:
#read a byte from each string
lb t0, 0(a0)
lb t1, 0(a1)
#if the two bytes are not equal end comparison
bne t0, t1, strcmp_end
#but if they are equal, test for zero
#if one of them is zero, also end the loop
beq t0, zero, strcmp_end
addi a0, a0, 1 # go to next byte
addi a1, a1, 1 # go to next byte
j strcmp_start
strcmp_end:
#subtract t1 from t0
#if t0 is still zero after the function returns
#it means that the strings are equal
sub t0, t0, t1
ret
To use this example, it is required to run the RARS Java archive and have a Java runtime installed on whatever operating system you have.
java -jar ~/rars.jar main.s
However complicated the RISC-V code looks to a beginner, to me it makes more sense than Java did in my experience. I have invested significant time into learning it though because I enjoyed it right from the start.
Anyway, what the program does is keep reading input from the keyboard until the user enters “exit” as a string.
Leave a comment