This was a video made with a program I wrote using a font drawing library in SDL. It prints all the prime numbers less than 65536. Whenever it runs out of space, it clears the screen and starts on the next page.
Chastity’s Chess Blog
-
FreeBASIC Graphics Counting and ASCII Character Program
This is a picture that I made with a program I wrote in FreeBASIC. One of the cool things about FreeBASIC is that it has built in graphics as part of its standard library. This means it takes less work to get some text and pictures onto the screen compared to doing it with C and SDL.

The full source code is included in this post. It is quite large but still smaller than what I usually do with C. Mostly, it serves as a reminder of how I achieved this to inspire me to make a game in FreeBASIC.
main.bas
#include "chastelib.bi" 'define screen size variables before the graphics header dim shared as integer screen_width=1280,screen_height=720 #include "chastelib-graphics.bi" ' Set the screen mode to size I want and 32 bits true color ScreenRes 1280, 720, 32 'optionally change font 'width screen_width/8,screen_height\8 'use 8x8 font (default) width screen_width\8,screen_height\16 'use 8x16 font chaste_checker color &h000000,&hFFFFFF 'set foreground and background text color using hex RGB codes dim as integer cursor_x=3,cursor_y=2,y_top=7 locate cursor_y,cursor_x print "FreeBASIC Graphics Counting and ASCII Character Program" cursor_y+=2 locate cursor_y,cursor_x print "Written by Chastity White Rose, the Pure Princess of Pixels, Polygons, and Ponies" cursor_x=2 cursor_y=y_top locate cursor_y,cursor_x ' Keep the window open until the user presses a key dim as integer a,b,c radix=16 a=0 b=strint("100") c=32 while a<b locate cursor_y,cursor_x radix=2 int_width=8 print intstr(a);" "; radix=16 int_width=2 print intstr(a);" "; radix=10 int_width=3 print intstr(a); 'if(a>=32) and (a<=126) then print " "+chr(a); 'endif 'print a+=1 cursor_y+=1: 'cursor math to display to the right of last column of numbers if (a mod c)=0 then cursor_x+=20 cursor_y=y_top end if wend sleep /' This is a FreeBASIC program. compile and run as: fbc main.bas && ./main '/chastelib.bi
/' global variables to define radix and formatting for the intstr function '/ dim shared as integer radix=2 dim shared as integer int_width=1 /' translation of intstr function for FreeBASIC by original C programmer Chastity White Rose '/ function intstr(i as uinteger) as string dim as string s="" dim as integer w=0 dim as byte c while i<>0 or w<int_width c=i mod radix i\=radix if c<10 then c+=48 else c+=55 end if s=chr(c)+s w+=1 wend return s end function /' global variable for error detection in strint function this variable will be zero if last string was a number '/ dim shared as integer strint_errors=0 /' translation of strint function for FreeBASIC by original C programmer Chastity White Rose '/ function strint(s as string) as uinteger dim as uinteger i=0 dim as integer x=0,y=len(s) dim as byte c strint_errors = 0 /' clear errors '/ while x<y /' read digit from string '/ c=s[x] /' 0 to 9 '/ if c >= 48 and c <= 57 then c-=48 /' A to Z '/ elseif c >= 65 and c <= 90 then c-=65 c+=10 /' a to z '/ elseif c >= 97 and c <= 122 then c-=97 c+=10 /' whitespace '/ elseif c >= 0 and c <= 32 then exit while /' exit correctly at string end '/ else strint_errors+=1 print "Error: ";chr(s[x]);" is not an alphanumeric character!" exit while /' exit at invalid character '/ end if if c>=radix then strint_errors+=1 print "Error: ";chr(s[x]);" is not a valid character for radix ";radix exit while /' exit at digit wrong for radix '/ end if /'multiply by radix then add digit'/ i*=radix i+=c x+=1 wend return i end functionchastelib-graphics.bi
dim shared as integer rect_size=8 /' this function draws a checkerboard. it is highly optimized because it does not switch colors during the function. it only draws half of the checkerboard squares and leaves the remaining areas the same as the background '/ sub chaste_checker() dim as integer x,y,index,index1 dim as integer rect_x,rect_y,rect_w,rect_h 'draw filled rectangle with the line function 'fill the whole screen line (0,0)-(screen_width,screen_height), &HFF000000,bf index=0 rect_x=0 rect_y=0 rect_w=rect_size rect_h=rect_size y=0 while(y<screen_height) index1=index x=0 while(x<screen_width) if(index=1) then 'draw filled rectangle with the line function line (x,y)-(x+rect_w,y+rect_h), &HFFFFFF, bf end if index=index xor 1 x+=rect_size wend index=index1 xor 1 y+=rect_size wend end sub ' Notes 'The line function actually can draw rectangles, despite its name 'https://www.freebasic.net/wiki/KeyPgLinegraphics -
Learning Pascal for fun
I am experimenting with old programming languages from the 20th century. My main focus is on teaching beginners to program with languages easier than Assembly. BASIC and Pascal are both older than C and so it is my duty to learn about them so I can make recommendations.
The following program is something I wrote as a basic outline of how variables in Pascal are defined and how a loop can be used. As I learn more I intend to translate some of my C programs into Pascal so I can share with others who appreciate this old language that is good but not as well known as most others.
program life; var name:string; year:integer; age:integer; begin name:='Chastity'; year:=1987; age:=0; while year<=2026 do begin WriteLn('name=',name,' age=',age,' year=',year); year:=year+1; age:=age+1; end; end. (* fpc main.pas && ./main *)


-
AAA Linux Chapter 18
Chapter 18: Bitwise Operations for Advanced Nerds
This chapter contains information which will assist you in understanding more about how computers work, but that in general is not required for MOST programming unless you are trying to operate on individual bits.
To start out, I will describe 5 essential bitwise operations independently of any specific programming language. This is because these operations exist in every programming language I know of, including Assembly and C.
After I have explained what the bitwise operations do, I will give examples of how this can be used in Assembly language to substitute for addition and subtraction! You might wonder why you would do this. The fact is that you don’t need to but it is a fun trick that only advanced nerds like me do for a special challenge.
The Bitwise Operations
This chapter explains 5 bitwise operations which operate on the bits of data in a computer. For the purpose of demonstration, it doesn’t matter which number the bits represent at the moment. This is because the bits don’t have to represent numbers at all but can represent anything described in two states. Bits are commonly used to represent statements that are true or false. For the purposes of this section, the words AND, OR, XOR are in capital letters because their meaning is only loosely related to the English words they get their name from.
Bitwise AND Operation
0 AND 0 == 0 0 AND 1 == 0 1 AND 0 == 0 1 AND 1 == 1Think of the bitwise AND operation as multiplication of single bits. 1 times 1 is always 1 but 0 times anything is always 0. That’s how I personally think of it. I guess you could say that something is true only if two conditions are true. For example, if I go to Walmart AND do my job then it is true that I get paid.
I like to think of the AND operation as the “prefer 0” operation. It will always choose a 0 if either of the two bits is a 0, otherwise, if no 0 is available, it will choose 1.
Bitwise OR Operation
0 OR 0 == 0 0 OR 1 == 1 1 OR 0 == 1 1 OR 1 == 1The bitwise OR operation can be thought of as something that is true if one or two conditions are true. For example, it is true that playing in the street will result in you dying because you got run over by a car. It is also true that if you live long enough, something else will kill you. Therefore, the bit of your impending death is always 1.
I like to think of the OR operation as the “prefer 1” operation. It will always choose a 1 if one of the two bits is a 1, otherwise, if no 1 is available, it will choose 0.
Bitwise XOR Operation
0 XOR 0 == 0 0 XOR 1 == 1 1 XOR 0 == 1 1 XOR 1 == 0The bitwise XOR operation is different because it isn’t really used much for evaluating true or false. Instead, this operation returns 1 if the bits compared are different or 0 if they are the same. This means that any bit, or group of bits, XORed with itself, will always result in 0.
If you look at my XOR chart above, you will see that using XOR of any bit with a 1 causes the result to be the opposite of the original bit.
The XOR operation is the quickest way to achieve this bit inversion. If you have a setting that you want to switch on or off, you can toggle it by XORing that bit with 1.
While the AND, OR, XOR operations can work in the context of individual bits, or groups of them, the next operations, the bit shifts, only make sense in the context of a group of bits. At minimum, you will be operating on 8 bits at a time because a byte is the lowest addressable size of memory.
Bitwise Left and Right Shift Operations
Consider the case of the following 8 bit binary value:
00001000
This would of course represent the number 8 because a 1 is in the 8’s place value. We can left shift or right shift.
00001000 == 8 : is the original byte 00010000 == 16 : original left shift 1 00000100 == 4 : original right shift 1That is really all there is to shifts. They can be used to multiply or divide by a power of two. In some cases, this can be faster than using the mul and div instructions described in chapter 6.
Example 00: Fake Add
The following example shows how it is possible to write an addition routine using a combination of the AND,XOR,SHL operations. In this case, the numbers are shown in decimal to be easier for most people to see that the addition is correct.
format ELF executable main: mov dword [radix],10 mov dword [int_width],1 mov edi,1987 mov esi,39 mov eax,edi call putint call putline mov eax,esi call putint call putline fake_add: mov eax,edi xor edi,esi and esi,eax shl esi,1 jnz fake_add mov eax,edi call putint call putline mov eax,esi call putint call putline mov eax,1 mov ebx,0 int 0x80 include 'chastelib32.asm'If you run it, you will see that the correct result of 2026 which is 1987+39. These are the values we set the edi and esi registers to before simulating addition with these fancy bitwise operations that make even seasoned programmers run scared.
But how does this monstrosity of a program work? You see the AND operation keeps track of whether both bits in each place value are 1 or not. If they both are, this means that we have to “carry” those bits as we would do in an ordinary binary addition. We store the carry in the esi register and then left shift it once each time in the loop. The loop continues until esi equals zero and there are no more bits to invert with XOR.
The fact that it works is easy to work out in my head but I don’t blame you if you can’t visualize it. However, this shows the power of what bit operations can do, even though you will probably never need to do this.
Example 01: Fake Sub
In case the fake addition example above wasn’t enough for you, here is a slightly modified example that does a fake subtraction operation using the same operations. Try it out and you will see that it subtracts 38 from 2025 and gets the original 1987.
format ELF executable main: mov dword [radix],10 mov dword [int_width],1 mov edi,2026 mov esi,39 mov eax,edi call putint call putline mov eax,esi call putint call putline fake_sub: xor edi,esi and esi,edi shl esi,1 jnz fake_sub mov eax,edi call putint call putline mov eax,esi call putint call putline mov eax,1 mov ebx,0 int 0x80 include 'chastelib32.asm'I will try to explain how this works. You see, we first XOR the edi register with the esi register. Then, we AND esi with the new value of edi. This means that the bits in the current place value will only both be 1 if those bits were 0 in edi and then were inverted to 1 by the XOR with esi. This means that at the start of the loop, destination bit=0 and source bit=1. 0 minus 1 means that we need to “borrow” (I hate that term because it is really stealing because we never give it back). We left shift esi as usual and then we keep XORing the new borrow in esi until it is zero.
Also, you may have noticed that I never used the “cmp” instruction to compare si with zero in this examples. This is because the zero flag is automatically updated with most operations. In fact there are places in my standard library of functions (chastelib) where it wasn’t strictly required to compare with “cmp” but I added it for clarity so I could read my code and more easily remember what I was doing.
But let’s face it, the examples in this chapter are purely for showing off how advanced my knowledge of the binary numeral system and manipulating bits in ways no reasonable person should ever attempt. I must admit, it would be great for an obfuscated code contest to make a program with code that is unreadable to most humans.
And while I am on the subject of making unreadable code, why stop at simulating addition and subtraction? I can do multiplication and division too!
Example 10 Fake Mul
format ELF executable main: mov dword [radix],10 mov dword [int_width],1 mov edi,6 mov esi,7 mov eax,edi call putint call putline mov eax,esi call putint call putline fake_mul: mov eax,0 fake_mul_add_loop: cmp esi,0 jz fake_mul_add_loop_end test esi,1 jz skip_add add eax,edi skip_add: shl edi,1 shr esi,1 jmp fake_mul_add_loop fake_mul_add_loop_end: mov edi,eax mov eax,edi call putint call putline mov eax,1 mov ebx,0 int 0x80 include 'chastelib32.asm'This fake multiplication uses eax as a temporary variable to store the sum of repeated addition. Each time through the loop, we test the low bit of esi (source register) and add edi (destination register). Each time we double edi by left shifting it once while right shifting esi once.
To test the low bit of esi, this example uses an instruction that has not been presented in this book. However, this is one of those times when it should be used because it is the easiest way to test for a bit.
test esi,1This tests the lowest bit of the esi register. If the low bit (ones place) is 0, then the jump if zero command will skip past the part where we add edi to eax.
The purpose of the bit shifts is to reduce the number of times we have to add edi to eax. Because 6 and 7 are the values we use, the loop will execute and exactly these results will happen.
eax starts as 0 but at the end of each cycle, eax, edi, and esi will be these values.
cycle eax edi esi 0 6 6 7 1 18 12 3 2 42 24 1 In short, because esi was 7 and was represented as 111 in binary, because it is the sum of 4+2+1, then the result of 42 is the sum of:
(4*6)+(2*6)+(1*6)Because of this optimized algorithm, it took only 3 loop cycles instead of the usual 7 cycles if we had just added 6 to eax 7 times. The speed won’t be much for such a small number as 42 but for larger numbers, this is the kind of optimization I would want.
Example 11 Fake Div
For the final example, I have prepared the long division algorithm. I always wondered why they called it long division, but now that I wrote this 70 line long program just to divide 256 by 10, I finally understand.
Despite its complexity, it follows the same method that humans use for doing long division. We extract the bits from high to low positions from the dividend (edi) and shift them into the low bits of a temporary number (ebx). If this number is large enough (not below divisor) then we subtract the divisor. Each time, we save the resulting bit in the quotient (eax). 0 means the number was too small to subtract the divisor and 1 means it was big enough to subtract the divisor.
Because this is a 32 bit system, the “fake_div_sub_loop” needs to execute exactly 32 times. For this purpose, ecx was chosen as a counter. By initializing it to 1 and left shifting it each time, it will loop 32 times until ecx will be 0 when an overflow happens and the bit is lost.
The eax,ebx,and ecx registers are pushed and popped in this example, despite not being part of a function call. This is to signal that their purpose was temporary and because of the complexity, I thought it would be a good convention to follow for clarity that they are not the final return values.
format ELF executable main: mov dword [radix],10 mov dword [int_width],1 mov edi,256 mov esi,10 mov eax,edi call putint call putline mov eax,esi call putint call putline fake_div: ;save registers used in the long division algorithm push eax push ebx push ecx mov eax,0 mov ebx,0 mov ecx,1 cmp esi,0 jz fake_div_sub_loop_end ;div by 0 invalid fake_div_sub_loop: cmp ecx,0 jz fake_div_sub_loop_end shl eax,1 shl ebx,1 test edi,edi ;test edi with itself to check sign bit jns skip_or ;skip copy of sign bit if it was 0 or ebx,1 ;store a 1 in low bit of ebx based on sign skip_or: shl edi,1 ;skip subtraction if ebx is below esi cmp ebx,esi jb skip_sub sub ebx,esi or eax,1 skip_sub: shl ecx,1 jmp fake_div_sub_loop fake_div_sub_loop_end: ;send results to correct registers and clean up mov edi,eax ;copy quotient to edi mov esi,ebx ;copy remainder to esi ;restore registers to their original values pop ecx pop ebx pop eax mov eax,edi call putint call putline mov eax,esi call putint call putline mov eax,1 mov ebx,0 int 0x80 include 'chastelib32.asm'The results of the fake division program are here:
256 10 25 6The program correctly divides 256 by 10 which is 25 but with a remainder of 6. Although this program is probably the most difficult to understand, it gives the correct results.
Which can only mean that the long division algorithm I have in my autistic head works because I used to perform these operations on paper a lot and I know the process so well that I created this example.
Will I need to know these Algorithms?
Believe it or not, there may be times when you will need to use these algorithms. There are some older processors which don’t have multiplication and division instructions. For Intel processors, you can already use the real mul and div instructions.
But I have a feeling that somewhere in the hardware of those instructions on the Intel CPUs, they probably work a lot like the algorithms I wrote for this chapter.
When I promised you Assembly Arithmetic Algorithms, I was not joking. I have spent 26 years learning computer programming and I always liked to learn everything whether other people considered it practical or profitable.
-
AAA Linux News: Chapter 17 added
The 3 integer sequence programs from the DOS Assembly Arithmetic Algorithms have now been added to the Linux version. What was Chapter 5 for the DOS book is now Chapter 17 for the Linux book.
I am not sure whether readers prefer this content sooner or later in the book but I felt that I had a lot more ground to cover in the Linux book and so I spent 16 chapters explaining the system calls and why they were used in the programs I included. The Linux book has chastack,chastext,chastecmp, and chastehex.
These programs use all of them most important system calls and therefore, since I have succeeded in writing these, there is no terminal program for Linux that is out of reach. Those who read this book will probably learn to do everything I can in Assembly for Linux and probably more if they have true courage!

-
chastext 64-bit
I have converted my chastext program to 64 bit Assembly for Linux. Next to chastehex, this is the program I am most proud of because it can find and replace exact strings of text. It isn’t quite the same as the Linux “sed” tool, but it is faster, smaller, and I wrote it myself and can do whatever I want with it.
So of course what I did was write shell script to show what it is capable of!

main.asm
;Linux 64-bit Assembly Source for chastext ;a basic text search and replace program format ELF64 executable entry main include 'chastelib64.asm' main: pop rax mov [argc],rax ;save the argument count for later cmp qword [argc],1 ja help_skip ;if more than 1 argument is given, skip the help message and process the other arguments help: mov rax,help_message call putstring jmp main_end help_skip: pop rax ;pop the next arg which is the name of the program we are running get_filename: pop rax ;pop the next arg which is the name of the file we will open mov [filename],rax ; save the name of the file we will open to read arg_open_file: ;Linux system call to open a file mov rsi,0 ;open file in read only mode mov rdi,rax ;filename should be in rax before this function was called mov rax,2 ;invoke SYS_OPEN (kernel opcode 2 on 64 bit systems) syscall ;call the kernel cmp rax,0 jns file_open_no_errors ;if rax is not negative/signed there was no error ;Otherwise, if it was signed, then this code will display an error message. mov rax,open_error_message call putstr_and_line jmp main_end ;end the program because we failed at opening the file file_open_no_errors: mov [filedesc],rax ; save the file descriptor number for later use ;before we just textdump or "cat" the file, we need to check for the existence of more arguments which will modify the output cmp qword[argc],3 jb search_skip pop rax ;pop the next arg which is the string we are searching for mov [string_search],rax search_skip: cmp qword[argc],4 jb replace_skip pop rax ;pop the next arg which is the string we are searching for mov [string_replace],rax replace_skip: ;now we begin displaying the file but also searching for the search string if it exists. We will check for these based on the number of arguments like we did earlier textdump: ;if only there are only 2 arguments (name of program plus input file) ;then we do a loop that ignores searching and replacing ;this loop will read one character from the file and then send it to stdout ;until there are no more bytes to display ;but if there are above 2 arguments, we skip this loop and go to search mode cmp qword[argc],2 ;test arguments 2=only filename given ja search_mode ;but if above 2, then go to search mode because a search string was given ;This loop is the same as the Linux 'cat' command ;or the DOS 'type' command for a single file ;it will read one byte and echo it to standard output until EOF cat: mov rdx,1 ;number of bytes to read mov rsi,byte_array ;address to store the bytes mov rdi,[filedesc] ;move the opened file descriptor into rdi mov rax,0 ;invoke SYS_READ (kernel opcode 0 on 64 bit Intel) syscall ;call the kernel mov [bytes_read],rax cmp rax,0 jnz file_success ;if more than zero bytes read, proceed to display jmp main_end ;otherwise, end the program ; this point is reached if file was read from successfully file_success: ;print the last read character to stdout by switching to write call mov rdi,1 ;write to the STDOUT file mov rax,1 ;invoke SYS_WRITE (kernel opcode 1 on 64 bit systems) syscall ;system call to write the message jmp cat search_mode: ;this is the beginning of search mode ;it handles the file by seeking and reading to search every position for the search string ;first, seek to the file_address we initialized to zero ;this variable will be added to depending on actions taken mov rdx,0 ;whence argument (SEEK_SET) mov rsi,[file_address] ;move the file cursor to this address mov rdi,[filedesc] ;move the opened file descriptor into rbx mov rax,8 ;invoke SYS_LSEEK (kernel opcode 8 on 64 bit Intel) syscall ;call the kernel ;obtain the length of the search string using my strlen function mov rax,[string_search] call strlen ;get the length of the search string ;use the length of the string we are searching for as the number of bytes to read at this location mov rdx,rax ;number of bytes to read mov rsi,byte_array ;address to store the bytes mov rdi,[filedesc] ;move the opened file descriptor into rbx mov rax,0 ;invoke SYS_READ (kernel opcode 0 on 64 bit Intel) syscall ;call the kernel mov [bytes_read],rax ;store how many bytes were read with that last read operation mov rbx,byte_array ;move the address of bytes read into rbx add rbx,rax ;add number of bytes read (return value of read function in rax) mov byte[rbx],0 ;terminate the string with zero cmp rax,rdx ;if the number of bytes is not what we expected to read, end this loop jnz textdump_end ;move our two strings into the rsi and rdi registers for comparison ;with my custom written strcmp function mov rsi,[string_search] mov rdi,byte_array call strcmp ;compare these two strings cmp rax,0 ;test if they are the same (if rax returned zero) jnz not_match ;if they are not a match go to that section for printing a character ;but if they are a match, then we either quote them ;or replace them if a replacement string is available ;but regardless of which action we do, since a match was found, let us add this count to the file address ;so that we read from beyond this point next time the textdump loop starts mov rax,[bytes_read] add [file_address],rax cmp qword[argc],4 ;if less than 4 args, no replacement exist, so we quote the strings jb print_quotes ;otherwise, we will print the replacement string instead of the original! mov rax,[string_replace] call putstring ;print the string jmp textdump ;restart the main loop print_quotes: ;print quotes around matched string mov al,'"' call putchar mov rax,byte_array call putstring ;print the string mov al,'"' call putchar jmp textdump ;restart the main loop not_match: ;Instead of calling the putchar function in the case of no match, ;I do a system call to print 1 byte to standard output ;This is simple and also compatible with binary files we want to replace text in. ;But it only works if the search and replace strings are of the same length mov rdx,1 ;number of bytes to write == 1 mov rsi,byte_array ;pointer/address of string to write mov rdi,1 ;write to the STDOUT file mov rax,1 ;invoke SYS_WRITE (kernel opcode 1 on 64 bit systems) syscall ;system call to write the message add [file_address],1 ;add 1 to the file address so we don't read this same position again jmp textdump textdump_end: ;print the remaining bytes, if any, left after the main loop ended ;mov rax,byte_array ;call putstring mov rdx,[bytes_read] ;number of bytes to write == last read call result mov rsi,byte_array ;pointer/address of string to write mov rdi,1 ;write to the STDOUT file mov rax,1 ;invoke SYS_WRITE (kernel opcode 1 on 64 bit systems) syscall ;system call to write the message main_end: ;this is the end of the program ;we close the open file and then use the exit call ;Linux system call to close a file mov rdi,[filedesc] ;file number to close mov rax,3 ;invoke SYS_CLOSE (kernel opcode 3 for 64 bit Intel) syscall ;call the kernel mov rax, 0x3C ; invoke SYS_EXIT (kernel opcode 0x3C (60 decimal) on 64 bit systems) mov rdi,0 ; return 0 status on exit - 'No Errors' syscall ;the strlen and strcmp are named after the equivalent C functions ;but are written from scratch by me based on their expected behavior ;The strlen function gets the length of string in rax and returns it in rax ;This is the same algorithm used in my putstring function strlen: push rbx mov rbx,rax ; copy rax to rbx. rbx will be used as index to the string strlen_start: ; this loop finds the length of the string cmp [rbx],byte 0 ; compare byte at address rbx with 0 jz strlen_end ; if comparison was zero, jump to loop end inc rbx jmp strlen_start strlen_end: sub rbx,rax ;subtract start pointer from current pointer to get length of string mov rax,rbx ;copy the string length back to rax pop rbx ret ;strcmp compares the string at rsi to the one at rdi ;rax returns 0 if the strings are the same and 1 if different ;the algorithm is simple but I will explain it for those who are confused ;rax is initialized to zero ;a byte from each string is loaded into the al and bl 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 ;for this purpose it doesn't matter whether we compare al or bl with zero ;because it is known that they are the same if the jnz did not take place ;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 bl will be subtracted from al ;this ensures that the function returns zero if the final characters are the same ;rbx,rsi,and rdi are preserved but rax is the return value ;also, the sub instruction at the end of the function also updates the flags ;so you can "jz" or "jnz" to a label after calling this function based on results strcmp: push rbx push rsi push rdi mov rax,0 strcmp_start: ;read a byte from each string mov al,[rdi] mov bl,[rsi] cmp al,bl jnz strcmp_end cmp al,0 jz strcmp_end inc rdi inc rsi jmp strcmp_start strcmp_end: sub al,bl pop rdi pop rsi pop rbx ret help_message db 'chastext by Chastity White Rose',0Ah,0Ah db '"cat" a file:',0Ah,0Ah,9,'chastext file',0Ah,0Ah db 'search for a string:',0Ah,0Ah,9,'chastext file search',0Ah,0Ah db 'replace string:',0Ah,0Ah,9,'chastext file search replace',0Ah,0Ah db 'Find or replace any string!',0Ah,0 open_error_message db 'error while opening file',0 file_address dq 0 ;file address defaults to zero AKA beginning of file ;variables for managing arguments and files argc rq 1 filename rq 1 ; name of the file to be opened filedesc rq 1 ; file descriptor bytes_read rq 1 string_search rq 1 ; place to hold the search string pointer string_replace rq 1 ; place to hold the replacement string pointer ;where we will store data from the file byte_array db 0xA4 dup 0chastelib64.asm
; chastelib assembly header file for 64 bit Linux ; 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: push rax push rbx push rcx push rdx mov rbx,rax ;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 [rbx],byte 0 ; compare byte at address rbx with 0 jz putstring_strlen_end ; if comparison was zero, jump to loop end because we have found the length inc rbx jmp putstring_strlen_start putstring_strlen_end: sub rbx,rax ;subtract start pointer from current pointer to get length of string ;Write string using Linux Write system call. ;Reference for 64 bit x86 syscalls is below. ;https://www.chromium.org/chromium-os/developer-library/reference/linux-constants/syscalls/#x86_64-64-bit mov rdx,rbx ;number of bytes to write mov rsi,rax ;pointer/address of string to write mov rdi,1 ;write to the STDOUT file mov rax,1 ;write (kernel opcode 1 on 64 bit systems) syscall ;system call for 64-bit Linux kernel pop rdx pop rcx pop rbx pop rax ret ; this is the end of the putstring function return to calling location ; 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 64 dup '?' ;reserve bytes for characters string for 64-bit binary integer int_string_end db 0 ;zero byte terminator for the integer string radix dq 2 ;radix or base for integer output. 2=binary, 8=octal, 10=decimal, 16=hexadecimal int_width dq 8 ;default width of integers. Extra zeros prefixed if more than 1 ;this function creates a string of the integer in rax ;it uses the above radix variable to determine base from 2 to 36 ;it then loads rax with the address of the string ;this means that it can be used with the putstring function intstr: mov rbx,int_string_end-1 ;find address of lowest digit(just before the newline 0Ah) mov rcx,1 digits_start: mov rdx,0; div qword [radix] cmp rdx,10 jb decimal_digit jnb hexadecimal_digit decimal_digit: ;we go here if it is only a digit 0 to 9 add rdx,'0' jmp save_digit hexadecimal_digit: sub rdx,10 add rdx,'A' save_digit: mov [rbx],dl cmp rax,0 jz intstr_end dec rbx inc rcx jmp digits_start intstr_end: prefix_zeros: cmp rcx,[int_width] jnb end_zeros dec rbx mov [rbx],byte '0' inc rcx jmp prefix_zeros end_zeros: mov rax,rbx ;point eax register to this string for putstring ret ; function to print string form of whatever integer is in rax ; 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 rax push rbx push rcx push rdx call intstr call putstring pop rdx pop rcx pop rbx pop rax ret ;this function converts a string pointed to by rax into an integer returned in rax 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 rbx,rax ;copy string address from rax to rbx because rax will be replaced soon! mov rax,0 mov [strint_error],0 ;set errors to 0 at the start of this function read_strint: mov rcx,0 ; zero rcx so only lower 8 bits are used mov cl,[rbx] inc rbx cmp cl,0 ; compare byte at address rdx 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 digit, but it could an alphabet character which is 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 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 rcx,[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 rdx,0 ;zero rdx because it is used in mul sometimes mul qword [radix] ;mul rax with radix add rax,rcx 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 [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 rax mov rax,space call putstring pop rax ret line db 0Ah,0 ;a string containing only a newline ;the next function which pushes rax to the stack ;moves the address of the line string and prints it with putstring ;then it pops the original value of rax 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 rax mov rax,line call putstring pop rax ret ;a function for printing a single character that is the value of al char: db 0,0 putchar: push rax mov [char],al mov rax,char call putstring pop rax ret ;a small function just for the common operation ;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 ;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 ;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 -
Assembly Magic
A programmer is not a magician
They just know how to use addition
Those with skill and high ambition
Don’t hesitate to break traditionReverse addition is subtraction
There is no need for fancy abstraction
Do not fall for hype and distraction
Don’t hesitate to learn, take actionRepeated addition is called multiplication
Despite its badly taught reputation
Teaching math is my obligation
With my books I will teach the nationSubtraction loops can form division
Conditional jumps make each decision
Divide by the radix for integer vision
But a zero divisor can cause a collisionProgramming languages are all the same
When arithmetic is your favorite game
It is fun to choose each variable name
But when my code fails, I take the blameBut of every language I have used
I love writing Assembly the most
And I wrote the chastehex program
Of which I sometimes like to boastI like Assembly language because
It gives me the complete control
And brings back the satisfaction
That the evil tech companies stoleAnyone can learn to write code
That is what some people say
And I agree with this statement
When they learn in the right wayPeople hear that something is hard
And so they never even try to start
But if they did they would soon see
That building software is an artI wrote a book to teach my favorite
Assembly Arithmetic Algorithms
I am a bit too obsessed with math
And others suffer from my autismI write my books and comment my functions
So that other people have a chance to read
And learn what makes computers work
And do all the tasks that humans needAnd to those who don’t yet understand
They think my math is some kind of magic
I rarely meet those who take the time to learn
And my lonely pursuit is kind of tragicBut the special way I write my programs
Is yet another form of Creative Writing
And because of evil tech companies
People like me cannot stop fightingThey fire people and replace them with AI
But that will only work for a short while
Because the code is of no use at all
Unless it can make a human soul smile -
chastext for Linux
This is the source of my chastext program in Linux Intel Assembly language. It is actually very impressive that I managed to fix the many bugs it had. It is a simple find a replace program that I may use in future development of small assembly programs. Each run of the program can only change one kind of string to another, but since the commands can be chained together, transformations are possible beyond what I can explain right now. I would have to write a script just to show what it can do.
main.asm
;Linux 32-bit Assembly Source for chastext ;a basic text search and replace program format ELF executable entry main
;a reduced form of chastelib without functions this program doesn’t use include ‘chastext-chastelib32.asm’
main:
pop eax mov [argc],eax ;save the argument count for later
cmp dword [argc],1 ja help_skip ;if more than 1 argument is given, skip the help message and process the other arguments
help: mov eax,help_message call putstring jmp main_end help_skip:
pop eax ;pop the next arg which is the name of the program we are running
get_filename: pop eax ;pop the next arg which is the name of the file we will open
mov [filename],eax ; save the name of the file we will open to read
arg_open_file:
;Linux system call to open a file
mov ecx,0 ;open file in read only mode mov ebx,eax ;filename should be in eax before this function was called mov eax,5 ;invoke SYS_OPEN (kernel opcode 5) int 80h ;call the kernel
cmp eax,0 jns file_open_no_errors ;if eax is not negative/signed there was no error
;Otherwise, if it was signed, then this code will display an error message.
mov eax,open_error_message call putstr_and_line
jmp main_end ;end the program because we failed at opening the file
file_open_no_errors:
mov [filedesc],eax ; save the file descriptor number for later use
;before we just textdump or “cat” the file, we need to check for the existence of more arguments which will modify the output
cmp dword[argc],3 jb search_skip
pop eax ;pop the next arg which is the string we are searching for mov [string_search],eax
search_skip:
cmp dword[argc],4 jb replace_skip
pop eax ;pop the next arg which is the string we are searching for mov [string_replace],eax
replace_skip:
;now we begin displaying the file but also searching for the search string if it exists. We will check for these based on the number of arguments like we did earlier
textdump:
;if only there are only 2 arguments (name of program plus input file) ;then we do a loop that ignores searching and replacing ;this loop will read one character from the file and then send it to stdout ;until there are no more bytes to display
cmp dword[argc],2 jnz putchar_skip
mov edx,1 ;number of bytes to read mov ecx,byte_array ;address to store the bytes mov ebx,[filedesc] ;move the opened file descriptor into EBX mov eax,3 ;invoke SYS_READ (kernel opcode 3) int 80h ;call the kernel
mov [bytes_read],eax
cmp eax,0 jnz file_success ;if more than zero bytes read, proceed to display
jmp main_end ;otherwise, end the program
; this point is reached if file was read from successfully
file_success:
;normally, we will print the last read character mov al,[byte_array] call putchar
putchar_skip:
cmp dword[argc],3 ;if not enough arguments, skip the search string section jb textdump
;this is the beginning of search mode ;it handles the file by seeking and reading to search every position for the search string
;first, seek to the file_address we initialized to zero ;this variable will be added to depending on actions taken
mov edx,0 ;whence argument (SEEK_SET) mov ecx,[file_address] ;move the file cursor to this address mov ebx,[filedesc] ;move the opened file descriptor into EBX mov eax,19 ;invoke SYS_LSEEK (kernel opcode 19) int 80h ;call the kernel
;obtain the length of the search string using my strlen function mov eax,[string_search] call strlen ;get the length of the search string
;use the length of the string we are searching for as the number of bytes to read at this location
mov edx,eax ;number of bytes to read mov ecx,byte_array ;address to store the bytes mov ebx,[filedesc] ;move the opened file descriptor into EBX mov eax,3 ;invoke SYS_READ (kernel opcode 3) int 80h ;call the kernel
mov ebx,byte_array ;move the address of bytes read into ebx add ebx,eax ;add number of bytes read (return value of read function in eax) mov byte[ebx],0 ;terminate the string with zero
mov [bytes_read],eax ;store how many bytes were read with that last read operation
cmp eax,edx ;if the number of bytes is not what we expected to read, end this loop jnz textdump_end
;move our two strings into the esi and edi registers for comparison ;with my custom written strcmp function
mov esi,[string_search] mov edi,byte_array call strcmp ;compare these two strings
cmp eax,0 ;test if they are the same (if eax returned zero) jnz not_match ;if they are not a match go to that section for printing a character
;but if they are a match, then we either quote them ;or replace them if a replacement string is available
;but regardless of which action we do, since a match was found, let us add this count to the file address ;so that we read from beyond this point next time the textdump loop starts mov eax,[bytes_read] add [file_address],eax
cmp dword[argc],4 ;if less than 4 args, no replacement exist, so we quote the strings jb print_quotes
;otherwise, we will print the replacement string instead of the original!
mov eax,[string_replace] call putstring ;print the string
jmp textdump ;restart the main loop
print_quotes: ;print quotes around matched string mov al,‘"’ call putchar
mov eax,byte_array call putstring ;print the string
mov al,‘"’ call putchar
jmp textdump ;restart the main loop
not_match:
mov al,[byte_array] call putchar add [file_address],1 ;add 1 to the file address so we don’t read this same position again
jmp textdump
textdump_end:
;print the remaining bytes, if any, left after the main loop ended mov eax,byte_array call putstring
main_end:
;this is the end of the program ;we close the open file and then use the exit call
;Linux system call to close a file
mov ebx,[filedesc] ;file number to close mov eax,6 ;invoke SYS_CLOSE (kernel opcode 6) int 80h ;call the kernel
mov eax, 1 ; invoke SYS_EXIT (kernel opcode 1) mov ebx, 0 ; return 0 status on exit – ‘No Errors’ int 80h
;the strlen and strcmp are named after the equivalent C functions ;but are written from scratch by me based on their expected behavior
;a function to get the length of string in eax and return the integer in eax
strlen:
mov ebx,eax ; copy eax to ebx. ebx will be used as index to the string
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 strlen_end ; if comparison was zero, jump to loop end because we have found the length inc ebx jmp strlen_start
strlen_end: sub ebx,eax ;subtract start pointer from current pointer to get length of string
mov eax,ebx ;copy the string length back to eax
ret
;compare the string at esi to the one at edi
strcmp:
mov eax,0 ;this will be stay zero unless the strings are different
strcmp_start: mov bl,[edi] cmp bl,0 jz strcmp_end mov bh,[esi] cmp bh,0 jz strcmp_end
inc edi inc esi
cmp bl,bh jz strcmp_start ;if they are the same, continue to next character
inc eax ;if they were different, eax will be incremented and the function ends
strcmp_end: ret
help_message db ‘chastext by Chastity White Rose’,0Ah,0Ah db ‘“cat” a file:’,0Ah,0Ah,9,‘chastext file’,0Ah,0Ah db ‘search for a string:’,0Ah,0Ah,9,‘chastext file search’,0Ah,0Ah db ‘replace string:’,0Ah,0Ah,9,‘chastext file search replace’,0Ah,0Ah db ‘Find or replace any string!’,0Ah,0
open_error_message db ‘error while opening file’,0
file_address dd 0 ;file address defaults to zero AKA beginning of file
;variables for managing arguments and files argc rd 1 filename rd 1 ; name of the file to be opened filedesc rd 1 ; file descriptor bytes_read rd 1
string_search rd 1 ; place to hold the search string pointer string_replace rd 1 ; place to hold the replacement string pointer
;where we will store data from the file byte_array db 0xBD dup 0
-
chastext for DOS
I wrote a DOS version of the chastext program for simple search and replace. It does have some limitations because command line arguments are handled very different in DOS than they are in Linux. I can’t simple put quotes around two words to have them count as one argument like I can in Linux.
Aside from that, it seems to work. I can replace individual words in a text file with a different word. I will have a demo video up soon but see the post about the Linux version in the Linux forum to get the basic idea of what it should do.
I am not trying to recreate sed or awk but a simple find/replace is a worthwhile project for learning something new after I have mastered my chastehex and chastecmp programs. I can manipulate binary files flawlessly because they are predictable so now I am testing my limits on text based processing.
main.asm
org 100h ;DOS programs start at this address mov word [radix],16 ; can choose radix for integer output! mov ch,0 ;zero ch (upper half of cx) mov cl,[80h] ;load length in bytes of the command string cmp cx,0 jnz args_exist mov ax,help ;if no arguments were given, show a help message call putstring jmp ending ;and end the program because there is nothing to do args_exist: ;Point bx to the beginning of arg string ;however, this always contains a space mov bx,81h skip_start_spaces: cmp byte [bx],' ' ;is this byte a space? jnz skip_start_spaces_end ;if not, we are done skipping spaces inc bx ;otherwise, go to next char dec cx ;but subtract 1 from character count jmp skip_start_spaces skip_start_spaces_end: mov [arg_string_index],bx ; save the location of the first non space in the arg string ;find the end of the string based on length mov ax,bx add ax,cx mov [arg_string_end],ax ;now we know where the string ends. ;now bx points to the first non space character in the arguments passed to the DOS program ;and we know that [arg_string_end] is where it ends ;the next step is to filter the arguments into separate zero terminated strings ;each space will be changed to a zero (normally) ;but we also need to account for spaces inside quotes that are considered part of the string ;Linux handles this normally but DOS needs me to write the code to mimic this behavior ;because the program needs to function identically for DOS or Linux mov cl,' ' ;set the default filter character (argument terminator) to a space mov ch,0 ;are we currently checking spaces 0 or quote characters 1 as terminators? ;this loop is the new and improved argument filter ;it keeps track of whether we are inside or outside a quote ;and also which type of quote started the quote ;the actual quote marks are not part of the string unless they ;are the opposite quote type than what started the string ;The important thing is that spaces can exist inside of quoted strings ;as one argument rather than each new word being a new argument ;could be important for filenames containing spaces, etc. argument_filter: cmp bx,[arg_string_end] ;are we at the end of the arg string? jz argument_filter_end ;if yes, stop the filter and terminate with zero cmp ch,1 ;are we inside a quoted string? jz quote_check ;if yes, don't do anything to the spaces cmp byte[bx],cl ;compare the byte at address bx to the string terminator jnz ignore_char ;if it is not the same, we ignore it mov byte[bx],0 ;but if it matches, change it to a zero ignore_char: cmp byte [bx],0x22 ;is this a double quote -> " jz start_quote cmp byte [bx],0x27 ;is this a single quote -> ' jz start_quote jmp quote_no ;it was not a quote start_quote: mov ch,1 ;set ch to 1 to set that we are inside a quote now mov cl,[bx] ;save this quote type as the new terminator mov byte[bx],0 ;but delete the first quote with zero ;check for single or double quotes quote_check: cmp [bx],cl ;is this character the same type of quote that started this sub string? jnz quote_no ;if it is not, then skip to quote_no section ;but if it was matching, change this byte to zero ;and change cl back to a space mov cl,' ' ;cl is now a space mov ch,0 ;ch is 0 because now we have ended the quoted string mov byte[bx],0 ;delete the end quote with zero quote_no: inc bx ;go to the next character jmp argument_filter ;jump back to the beginning of argument filter argument_filter_end: mov byte [bx],0 ;terminate the ending with a zero for safety ;special case!!! ;If the first argument passed began with a quoted string ;it would have been changed to a 0 instead. This requires us to add one to the ;starting argument string index mov bx,[arg_string_index] cmp byte[bx],0 jnz first_argument_was_not_quote inc word[arg_string_index] ;add 1 so it points to the next byte before we process arguments first_argument_was_not_quote: ;now that the argument string is prepared, we will try to use the first argument as a filename to open mov ah,3Dh ;call number for DOS open existing file mov al,0 ;file access: 0=read,1=write,2=read+write mov dx,[arg_string_index] ;string address to interpret as filename int 21h ;DOS call to finalize open function mov [file_handle],ax ;save the file handle jc file_error ;if carry flag is set, we have an error, otherwise, file is open file_opened: mov ax,dx ;call putstring ;call putline jmp use_file ;skip past error message and start using the file ;this section prints error message and then ends the program if file error found file_error: ;prints error code2=file not found mov ax,dx call putstr_and_line mov ax,file_error_message call putstring mov ax,[file_handle] call putint jmp ending ;how we use the file depends on the number of arguments given ;if no arguments other than the filename exist, we do a regular hex dump ;otherwise we look for two more arguments: the search and replace strings use_file: call get_next_arg ;get address of next arg and return into ax register cmp ax,[arg_string_end] ;this time, if ax equals end of string, we hex dump and then end the program later jz textdump ;jump to hexdump section ;otherwise, we save the address at ax to our search string mov [string_search],ax ;call putstr_and_line call get_next_arg ;get address of next arg and return into ax register cmp ax,[arg_string_end] ;this time, if ax equals end of string, we hex dump and then end the program later jz textdump ;jump to hexdump section ;otherwise, we save the address at ax to our replacement string mov [string_replace],ax ;call putstr_and_line ;all other arguments that may exist after this are irrelevant textdump: ;we start the loop with a call to read exactly 1 byte mov ah,3Fh ;call number for read function mov bx,[file_handle] ;store file handle to read from in bx mov cx,1 ;we are reading one byte mov dx,byte_array ;store the bytes here int 21h ;call putint ;check the number of bytes read cmp ax,1 ;check to see if exactly 1 byte was read jz file_success ;if true, proceed to display ;mov ax,end_of_file ;call putstring jmp file_close ;otherwise close the file and end program after failure ; this point is reached if 1 byte was read from the file successfully file_success: ;first, check to see if there is a search string ;if there is a search string, skip the normal putchar cmp word[string_search],0 jnz putchar_skip ;but if there is not a search string ;we will print the last read character ;and then jump to the beginning of the textdump loop to print them until EOF mov al,[byte_array] call putchar jmp textdump putchar_skip: ;if search string doesn't exist, just jump and repeat the loop ;otherwise we continue into the section that compares the input with the search string mov bx,[string_search] mov al,[bx] mov ah,[byte_array] cmp al,ah ;compare the first character of search string with the byte read already jz search_start ; if they are equal, skip putchar and begin searching for the string ;otherwise, if they are not equal, just putchar the last byte read and repeat the loop mov al,[byte_array] call putchar jmp textdump search_start: mov ax,[string_search] call strlen ;get the length of the search string ;call putint_and_line ; print length of search string only for debugging ;attempt to read the length-1 bytes because the first one is already read into the byte array dec ax ;subtract 1 from ax which holds our length of string mov dx,byte_array+1 ;store the bytes here mov cx,ax ;we are reading this many bytes to have a string to compare mov bx,[file_handle] ;store file handle to read from in bx mov ah,3Fh ;call number for read function int 21h ;do some math to calculate where the string should end mov bx,dx ;mov into bx the address of second byte in the string add bx,ax ;add ax (the return value of the number of characters read) mov byte [bx],0 ;terminate the string with zero mov si,[string_search] mov di,byte_array call strcmp ;compare these two strings cmp ax,0 ;test if they are the same (if ax returned zero) jnz normal_print ;if they are not a match print them unmodified and unquoted ;but if they are a match, then we either quote them ;or replace them if a replacement string is available cmp word[string_replace],0 ;check to see if a replacement string is available jz print_quotes ;if not, skip to the part where we just quote the strings that match ;otherwise, we will print the replacement string instead of the original! mov ax,[string_replace] call putstring ;print the string jmp normal_print_skip print_quotes: ;print quotes around matched string mov al,'"' call putchar mov ax,byte_array call putstring ;print the string mov al,'"' call putchar jmp normal_print_skip normal_print: ;print normal / unquoted because it doesn't match mov ax,byte_array call putstring ;print the string normal_print_skip: jmp textdump file_close: ;close the file if it is open mov ah,3Eh mov bx,[file_handle] int 21h ;debugging section I use just to test values ;call putline ;mov ax,[string_search] ;call putstr_and_line ;mov ax,[string_replace] ;call putstr_and_line ending: mov ax,4C00h ; Exit program int 21h ;the strlen and strcmp are named after the equivalent C functions ;but are written from scratch by me based on their expected behavior ;a function to get the length of string in ax and return the integer in ax strlen: mov bx,ax ; copy ax to bx. bx will be used as index to the string strlen_start: ; this loop finds the length of the string as part of the putstring function cmp [bx],byte 0 ; compare byte at address bx with 0 jz strlen_end ; if comparison was zero, jump to loop end because we have found the length inc bx jmp strlen_start strlen_end: sub bx,ax ;subtract start pointer from current pointer to get length of string mov ax,bx ;copy the string length back to eax ret ;compare the string at si to the one at di strcmp: mov ax,0 ;this will be stay zero unless the strings are different strcmp_start: mov bl,[di] cmp bl,0 jz strcmp_end mov bh,[si] cmp bh,0 jz strcmp_end inc di inc si cmp bl,bh jz strcmp_start ;if they are the same, continue to next character inc ax ;if they were different, eax will be incremented and the function ends strcmp_end: ret ;function to move ahead to the next argument ;only works after the filter has been applied to turn all spaces into zeroes get_next_arg: mov bx,[arg_string_index] ;get address of current arg find_zero: cmp byte [bx],0 jz found_zero inc bx jmp find_zero ; this char is not zero, go to the next char found_zero: ;once we have found a zero, check to make sure we are not at the end find_non_zero: cmp bx,[arg_string_end] jz arg_finish ;if bx is already at end, nothing left to find cmp byte [bx],0 jnz arg_finish ;if this char is not zero we have found the next string! inc bx jmp find_non_zero ;otherwise, keep looking arg_finish: mov [arg_string_index],bx ; save this index to the variable mov ax,bx ;but also save it to ax register for use in printing or something else ret help db 'chastext by Chastity White Rose',0Dh,0Ah db '"cat" or "type" a file without changing it:',0Dh,0Ah,9,'chastext file',0Dh,0Ah db 'search for a string and quote it:',0Dh,0Ah,9,'chastext file search',0Dh,0Ah db 'replace string:',0Dh,0Ah,9,'chastext file search replace',0Dh,0Ah db 'Find or replace any string!',0Dh,0Ah,0 ; About the chastelib variant ;instead of including chastelib16.asm as a header file ;I copy pasted it except that I excluded functions that were not used. ;Notably, the strint function is excluded because strint_32 is used instead ;start of chastelib ; This file is where I keep my function definitions. ; These are usually my string and integer output routines. ;this is my best putstring function for DOS because it uses call 40h of interrupt 21h ;this means that it works in a similar way to my Linux Assembly code ;the plan is to make both my DOS and Linux functions identical except for the size of registers involved putstring: push ax push bx push cx push dx mov bx,ax ;copy ax to bx for use as index register putstring_strlen_start: ;this loop finds the length of the string as part of the putstring function cmp [bx], byte 0 ;compare this byte with 0 jz putstring_strlen_end ;if comparison was zero, jump to loop end because we have found the length inc bx ;increment bx (add 1) jmp putstring_strlen_start ;jump to the start of the loop and keep trying until we find a zero putstring_strlen_end: sub bx,ax ; sub ax from bx to get the difference for number of bytes mov cx,bx ; mov bx to cx mov dx,ax ; dx will have address of string to write mov ah,40h ; select DOS function 40h write mov bx,1 ; file handle 1=stdout int 21h ; call the DOS kernel pop dx pop cx pop bx pop ax ret ;this is the location in memory where digits are written to by the intstr function int_string db 16 dup '?' ;enough bytes to hold maximum size 16-bit binary integer int_string_end db 0 ;zero byte terminator for the integer string radix dw 2 ;radix or base for integer output. 2=binary, 8=octal, 10=decimal, 16=hexadecimal int_width dw 8 intstr: mov bx,int_string_end-1 ;find address of lowest digit(just before the newline 0Ah) mov cx,1 digits_start: mov dx,0; div word [radix] cmp dx,10 jb decimal_digit jge hexadecimal_digit decimal_digit: ;we go here if it is only a digit 0 to 9 add dx,'0' jmp save_digit hexadecimal_digit: sub dx,10 add dx,'A' save_digit: mov [bx],dl cmp ax,0 jz intstr_end dec bx inc cx jmp digits_start intstr_end: prefix_zeros: cmp cx,[int_width] jnb end_zeros dec bx mov [bx],byte '0' inc cx jmp prefix_zeros end_zeros: mov ax,bx ; store string in ax for display later ret ;function to print string form of whatever integer is in ax ;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 ax push bx push cx push dx call intstr call putstring pop dx pop cx pop bx pop ax ret ;the next utility functions simply print a space or a newline ;these help me save code when printing lots of things for debugging space db ' ',0 line db 0Dh,0Ah,0 putspace: push ax mov ax,space call putstring pop ax ret putline: push ax mov ax,line call putstring pop ax ret ;a function for printing a single character that is the value of al char: db 0,0 putchar: push ax mov [char],al mov ax,char call putstring pop ax ret ;a small function just for the common operation ;printing an integer followed by a space ;this saves a few bytes in the assembled code putint_and_space: call putint call putspace ret ;a small function just for the common operation ;printing an integer followed by a space ;this saves a few bytes in the assembled code putint_and_line: call putint call putline ret ;a small function just for the common operation ;printing an integer followed by a space ;this saves a few bytes in the assembled code putstr_and_space: call putstring call putspace ret ;a small function just for the common operation ;printing an integer followed by a space ;this saves a few bytes in the assembled code putstr_and_line: call putstring call putline ret ;end of chastelib arg_string_index dw 0 arg_string_end dw 0 file_error_message db 'Could not open the file! Error number: ',0 file_handle dw 0 end_of_file db 'EOF',0 ;where we will store data from the file bytes_read dw 0 string_search dw 0 ; place to hold the search string pointer string_replace dw 0 ; place to hold the replacement string pointer byte_array db 0x80 dup 0 -
chastehex for Windows update
I made an update to the chastehex program for Windows. I made it consistent with the behavior of the Linux assembly and C version of the same program. Now it will print the name of the file being opened, display text according to the current mode you are using, and then display EOF to indicate that the end of the file was reached.
chastehex is a rather complex program because of the fact that it can read or write bytes at specific addresses if you give it the right arguments. If you give it only a filename as an argument, it will hex dump the entire file.
This update doesn’t change the size of the Windows executable despite the fact that I removed a lot of code from the source than was no longer used. It still pads it to the nearest multiple of 512 bytes. The total size of the executable is 2560 bytes or 2 and a half kilobytes. Although it is bigger than the Linux version, it is still smaller than the compiled C version that behaves the same.
Although I have said it before, I will mention that this program did not need to be written because the C version is the same. However, writing assembly code and optimizing it is very fun. I don’t usually write things for Windows but because Windows is the most popular desktop operating system and it always runs on an Intel CPU, this program will always work for the majority of computers in the world.
I was able to translate my Linux version updates into the Windows version of the program because my chastelib library provides a layer that works the same on any OS. It does exactly what I wrote it to do. The programs chastehex and chastecmp are the first two tools and I am still planning what the next tool will be. I hope to make something else that assists me in the act of programming directly rather than just modifying and comparing the binary code I generate.
main.asm
format PE console include 'win32ax.inc' include 'chastelibw32.asm' main: mov [radix],16 ; Choose radix for integer output. mov [int_width],1 ;get command line argument string call [GetCommandLineA] mov [arg_start],eax ;store start of arg string ;short routine to find the length of the string ;and whether arguments are present mov ebx,eax find_arg_length: cmp [ebx], byte 0 jz found_arg_length inc ebx jmp find_arg_length found_arg_length: ;at this point, ebx has the address of last byte in string which contains a zero ;we will subtract to get and store the length of the string mov [arg_end],ebx sub ebx,eax mov eax,ebx mov [arg_length],eax ;this loop will filter the string, replacing all spaces with zero mov ebx,[arg_start] arg_filter: cmp byte [ebx],' ' ja notspace ; if char is above space, leave it alone mov byte [ebx],0 ;otherwise it counts as a space, change it to a zero notspace: inc ebx cmp ebx,[arg_end] jnz arg_filter arg_filter_end: ;optionally print first arg (name of program) ;mov eax,[arg_start] ;call putstr_and_line ;get next arg (first one after name of program) call get_next_arg cmp eax,[arg_end] jz help mov [file_name],eax call putstr_and_line 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 push 0 ;NULL: We are not using a template file push 0x80 ;FILE_ATTRIBUTE_NORMAL push 3 ;OPEN_EXISTING push 0 ;NULL: No security attributes push 0 ;NULL: Share mode irrelevant. Only this program reads the file. push 0x10000000 ;GENERIC_ALL access mode (Read+Write) push [file_name] ; 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 [file_handle],eax ;before we proceed, we also check for more arguments. ;get next arg (first one after name of program) call get_next_arg cmp eax,[arg_end] 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 [file_offset],eax ;seek to 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 [file_offset] ;where we are seeking to push [file_handle] ;seek within this file call [SetFilePointer] ;check for more args call get_next_arg cmp eax,[arg_end] 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 [byte_array],al ;write only 1 byte using Win32 WriteFile system call. push 0 ;Optional Overlapped Structure push 0 ;Optionally Store Number of Bytes Written push 1 ;Number of bytes to write push byte_array ;address to store bytes push [file_handle] ;handle of the open file call [WriteFile] mov eax,[file_offset] inc [file_offset] mov [int_width],8 call putint_and_space mov eax,0 mov al,[byte_array] mov [int_width],2 call putint_and_line ;check for more args call get_next_arg cmp eax,[arg_end] 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 bytes_read ;Store Number of Bytes Read from this call push 1 ;Number of bytes to read push byte_array ;address to store bytes push [file_handle] ;handle of the open file call [ReadFile] cmp [bytes_read],1 jz print_byte ;if less than one bytes read, there is an error mov eax,[file_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,[file_offset] mov [int_width],8 call putint_and_space mov eax,0 mov al,[byte_array] 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 bytes_read ;Store Number of Bytes Read from this call push 16 ;Number of bytes to read push byte_array ;address to store bytes push [file_handle] ;handle of the open file call [ReadFile] ;all the data is in place, do the write thing! mov eax,[bytes_read] ;call putint ;mov eax,byte_array ;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,[file_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 [file_handle] call [CloseHandle] ;Exit the process with code 0 push 0 call [ExitProcess] .end main ;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 ;function to move ahead to the next art ;only works after the filter has been applied to turn all spaces into zeroes get_next_arg: mov ebx,[arg_start] find_zero: cmp byte [ebx],0 jz found_zero inc ebx jmp find_zero ; this char is not zero, go to the next char found_zero: find_non_zero: cmp ebx,[arg_end] jz arg_finish ;if ebx is already at end, nothing left to find cmp byte [ebx],0 jnz arg_finish ;if this char is not zero we have found the next string! inc ebx jmp find_non_zero ;otherwise, keep looking arg_finish: mov [arg_start],ebx ; save this index to variable mov eax,ebx ;but also save it to ax register for use ret ;we can know that there are no more arguments when ;the either [arg_start] or eax are equal to [arg_end] ;this function prints a row of hex bytes ;each row is 16 bytes print_bytes_row: mov eax,[file_offset] mov [int_width],8 call putint_and_space mov ebx,byte_array mov ecx,[bytes_read] add [file_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,[bytes_read] 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,byte_array mov ecx,[bytes_read] 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,byte_array call putstring ret ;variables for managing arguments arg_start dd ? ;start of arg string arg_end dd ? ;address of the end of the arg string arg_length dd ? ;length of arg string arg_spaces dd ? ;how many spaces exist in the arg command line ;variables for managing file IO. file_name dd ? bytes_read dd ? ;how many bytes are read with ReadFile operation byte_array db 16 dup ?,0 file_handle dd ? file_offset dd ?chastelibw32.asm
; This file is where I keep my function definitions. ; These are usually my string and integer output routines. ; function to print zero terminated string pointed to by register eax putstring: push eax push ebx push ecx push edx mov ebx,eax ; copy eax to ebx as well. Now both registers have the address of the main_string putstring_strlen_start: ; this loop finds the lenge 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 ;ebx will now have correct number of bytes ;Write String using Win32 WriteFile system call. push 0 ;Optional Overlapped Structure push 0 ;Optionally Store Number of Bytes Written push ebx ;Number of bytes to write push eax ;address of string to print push -11 ;STD_OUTPUT_HANDLE = Negative Eleven call [GetStdHandle] ;use the above handle push eax ;eax is return value of previous function call [WriteFile] ;all the data is in place, do the write thing! pop edx pop ecx pop ebx pop eax ret ; this is the end of the putstring function return to calling location ; 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 '?' ;enough bytes to hold maximum size 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 ;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 jge 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 ; now that the digits have been written to the string, display it! 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 strint: mov ebx,eax ;copy string address from eax to ebx because eax will be replaced soon! mov eax,0 read_strint: mov ecx,0 ; zero ecx so only lower 8 bits are used mov cl,[ebx] inc ebx cmp cl,0 ; compare byte at address edx 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 digit, but it could be perhaps and alphabet character ;which is 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 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 jmp strint_end process_char: cmp ecx,[radix] ;compare char with radix jae strint_end ;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 [radix] ;mul eax with radix add eax,ecx jmp read_strint ;jump back and continue the loop if nothing has exited it strint_end: ret ;the next utility functions simply print a space or a newline ;these help me save code when printing lots of things for debugging space db ' ',0 line db 0Dh,0Ah,0 putspace: push eax mov eax,space call putstring pop eax ret 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 ;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 ;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 ;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