Blog

  • Big Factorials in Assembly for Linux

    I have another example of Arbitrary Precision Arithmetic. This one generates the factorial sequence. Because multiplication of increasing numbers quickly generates long numbers, the built in integer registers are not equipped to handle numbers this large.

    Just like the Powers of 2 program from this chapter, the following program uses arrays of decimal digits. I carefully translated it from the C version in Chastity’s Code Cookbook.

    format ELF executable
    
    main:
    
    mov dword [radix],10
    mov dword [int_width],1
    
    mov eax,0
    mov ebx,1
    
    ;fill all 3 array with zeros up to maxlength
    mov ebx,0
    array_zero:
    mov [array_a+ebx],0
    mov [array_b+ebx],0
    mov [array_c+ebx],0
    inc ebx
    cmp ebx,maxlength
    jb array_zero
    
    mov [array_a],1 ;set low digit of array_a to 1
    mov [array_b],2 ;set low digit of array_b to 2
    
    ;Keep track of the currently used length of each array.
    ;At the start, use only one digit
    mov dword [array_a_length],1
    mov dword [array_b_length],1
    mov dword [array_c_length],1
    
    mov edx,0 ;use edx as a counter for the main loop
    main_loop:
    
    ;stage 1: display the a array
    mov eax,0
    mov ebx,[array_a_length]
    stage1:
    dec ebx
    mov al,[array_a+ebx]
    call putint
    cmp ebx,0
    jnz stage1
    call putline
    
    ;stage 2: multiply the a and b arrays together and store the result in the c array
    
    mov ebx,0
    stage2:
    
    mov eax,0
    stage2_multiply:
    
    ;we need to get the result of multiplication of the current digit
    ;indexed in array_a by eax and array_b by ebx
    ;the only safe way is to back up all the registers
    ;do a multiply operation, and then restore them
    
    push eax
    push ebx
    push ecx
    push edx
    
    ;mov eax and ebx to ecx and edx
    ;so that we can index the arrays
    ;using the low parts of eax and ebx as the result
    mov ecx,eax
    mov edx,ebx
    ;both eax and ebx are zeroed to avoid conflicts
    ;only the lowest 8 bits will be loaded from the arrays
    ;then we will do a multiply instruction
    mov eax,0
    mov al,[array_a+ecx]
    mov ebx,0
    mov bl,[array_b+edx]
    add ecx,edx ;add edx to ecx before edx is overwritten with mul
    mul ebx ;multiply eax by ebx
    
    stage2_add_product:
    add al,[array_c+ecx] ;add the byte at this index to al
    mov ebx,[radix]      ;set the bl register to the radix
    mov edx,0            ;clear edx before division
    div ebx              ;divide eax by ebx
    mov [array_c+ecx],dl ;move the remainder back to this index
    
    inc ecx
    cmp al,0               ;is the carry or quotient zero?
    jnz stage2_add_product ;if not zero, go to next digit and repeat
    
    cmp ecx,[array_c_length] ;is the index higher than current length of c array?
    jb c_digits_are_enough
    mov [array_c_length],ecx ;expand digits
    c_digits_are_enough:
    
    ;pop back the original values of the registers
    pop edx
    pop ecx
    pop ebx
    pop eax
    
    inc eax
    cmp eax,[array_a_length]
    jnz stage2_multiply
    
    inc ebx
    cmp ebx,[array_b_length]
    jnz stage2
    ;end of array multiplication stage
    
    ;stage 3: add 1 to the b array
    push edx
    mov eax,1  ;set carry to 1
    mov ebx,0 ;start at lowest element of b
    stage3_add_one_to_b:
    add al,[array_b+ebx]
    mov edx,0
    div dword [radix]
    mov [array_b+ebx],dl ;move the remainder back to this index
    
    inc ebx
    cmp al,0                ;is the carry or quotient zero?
    jnz stage3_add_one_to_b ;if so, go to next digit and repeat
    
    cmp ebx,[array_b_length] ;is the index higher than current length of c array?
    jb b_digits_are_enough
    
    mov [array_b_length],ebx ;expand digits
    
    b_digits_are_enough:
    pop edx
    
    ;stage 4: replace array_a with array_c
    ;and turn array_c to all zeros to be used for next product
    
    mov ebx,0 ;start at lowest element of both arrays
    stage4:
    
    mov al,[array_c+ebx] ;get element from array_c
    mov [array_a+ebx],al ;store it here in array_a  
    mov [array_c+ebx],0  ;zero the byte in array_c
    
    ;next, expand length of array_a to same as array_c
    mov eax,[array_c_length] ;get length of array_c
    mov [array_a_length],eax ;set length of array_a
    
    inc ebx
    cmp ebx,maxlength
    jnz stage4
    
    inc edx
    cmp edx,64     ;maximum factorial
    jnz main_loop
    
    mov eax,1
    mov ebx,0
    int 0x80
    
    maxlength=1000 ;use this as maximum length of all arrays
    array_a rb maxlength ;first array
    array_b rb maxlength ;second array
    array_c rb maxlength ;third array
    
    ;reserve one double word for each variable that will store the length
    ;the initial value is unknown but will be set in the program
    array_a_length rd 1
    array_b_length rd 1
    array_c_length rd 1
    
    include 'chastelib32.asm'
    

    The factorials program is hard to explain without the ability to show all the math on paper. It uses the traditional method of multiplication as I was taught in school. Each digit in one number is multiplied by each digit in another number. For example, if you start at the one’s place in the bottom number and multiply each digit in the top number by that digit, you have a partial result. The digits need to be added together while keeping their place values aligned. For example, consider this:

    carries during multiplication
    
    11
     123
       33
    ------
       256 = array a
    X  256 = array b
    ------
      1536 = product of 6*256
     1280  = product of 5*256 shifted 1 left
     512   = product of 2*256 shifted 2 left
    
    ------
     65536 = sum of the three products above
    

    However strange the code may look, the process is very much like a human processing multiplication of numbers on paper. I have an above average ability to visualize it in my head. I hope the above example can help explain what it is doing even for those who don’t understand the assembly code.

  • Don’t get scammed by companies like Reader’s Magnet

    As I’ve mentioned previously, everyone wants to rip you off as an author by taking advantage of your hopes and dreams. Scammers make me sick. I’ve had a few calls from them lately pretending to be huge media companies that offer promotion for authors. These scammers do their homework, and so I assume they work

    Don’t get scammed by companies like Reader’s Magnet
  • What Microsoft Copilot says about Assembly Arithmetic Algorithms

    I tried something new. I asked Microsoft Copilot what it could tell me about my own series of books on Assembly. I have to say I am impressed with the nice summary it came up with. I really think it highlights how unique this series is because it is written from the perspective of my autism and math obsession.

    Assembly Arithmetic Algorithms is a series of assembly‑language programming books written by Chastity White Rose, covering DOS, Windows (32‑ and 64‑bit), and Linux editions. They teach low‑level programming by focusing on arithmetic, binary/hex math, system calls, and how computers actually perform operations like addition, subtraction, multiplication, and division.

    📘 What the books are

    Chastity White Rose has published multiple editions of Assembly Arithmetic Algorithms, each targeting a different operating system:

    1. 16‑bit DOS Edition

    • Teaches how to write .com programs for MS‑DOS.
    • Focuses heavily on binary/hex arithmetic and low‑level operations.
    • Includes explanations of interrupts, pointers, memory addresses, and arithmetic routines.
    • Emphasizes learning how computers “think” numerically.
    • MS-DOS | Definition, Features, Importance, & Facts | Britannica
    • MS-DOS - Wikipedia
    • Difference Between Assembly And Binary at Donna Bull blog
    • Program Development and Execution | Assembly language, Assembly ...

    2. 32‑ and 64‑bit Windows Edition

    • Covers Windows API calls (GetStdHandle, WriteFile, ExitProcess).
    • Introduces registers, stack behavior, and bit operations.
    • Provides “Hello World” examples for both 32‑bit and 64‑bit Windows.
    • Designed for beginners and intermediate programmers wanting deeper control.
    • 理解 Windows API 调用过程 - 厚积薄发 - 开发者Cpp博客
    • Architecture Overview - Win32 apps | Microsoft Learn
    • X64 registers
    • PPT - Introduction to Machine-Level Programming: Basics of x86 ...

    3. 32‑bit Linux Edition (co‑authored with Chandler Klebs)

    • Focuses on POSIX system calls: read, write, open, close, lseek, exit.
    • Includes C programs to help explain assembly routines.
    • Teaches how to write fast, minimal Linux console applications.
    • Manage Your Linux System Resources With These 7 Terminal Commands
    • Timer In Linux Terminal at Rita Skelley blog
    • x86 Assembly Guide-CSDN博客
    • Assembly Language Tutorial No. 1 : Introduction To Registers - YouTube

    🧠 What the books emphasize

    Across all editions, the core themes are:

    • Binary arithmetic (addition, subtraction, multiplication, division)
    • Understanding CPU registers and flags
    • Memory addressing and pointer arithmetic
    • System calls and OS‑level interaction
    • Writing extremely small, efficient programs
    • Learning how computers actually perform math internally

    Chastity frames arithmetic as the foundation of all computation — the CPU’s “native language.”

    📚 Why these books stand out

    • They are modern books about old‑school assembly, which is rare.
    • They teach assembly through arithmetic, not through abstract theory.
    • They are written by a transgender Christian author who blends technical instruction with personal philosophy and creativity.
    • They aim to preserve low‑level programming knowledge that is becoming harder to find.

  • Book Review: Switching to Linux: A Practical Guide for Windows and Mac Users

    I have been a Linux user for at least 25 years at this point. In that time, I have tried many distros and experienced the pain of figuring it all out on my own. I can confidently say this is the book I wish I had in my teenage years. Modern Linux confuses people because there are so many distributions to choose from, and it can be so overwhelming that people give up and stick with the Windows or Mac systems they already know.

    It is not human nature to leave the comfortable and risk everything you have on something that may not work out. However, for those who have reasons to try Linux, this is the first book I will recommend to them. It is also useful for me because the landscape of Linux has changed greatly during the many years I have been a happy Debian user who plays Steam games and writes C programs for fun. The newest desktop environments and tools are a pleasure to learn about.

    Thanks to this book, I even tried Ventoy and can now boot Debian, Gentoo, and Kali Linux ISOs directly from a USB drive. If the day ever comes when I need to upgrade to a newer distro, I will be using this book as my guide to help me transition to the new way of being a Linux user.

    https://leanpub.com/switch-to-linux

  • RISC-V Assembly Progress

    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.