32-bit assembly programming using NASM and GCC.
In my tutorial 16-bit assembly programming using NASM, I describe the installation of the Netwide Assembler (NASM) on FreeDOS, and show how you proceed to build 16-bit real mode and 16-bit protected mode assembly programs. If you are new to assembly or to NASM, I would recommend that you read that tutorial before starting this one. In fact, it contains lots of details about assembly programming in general, and about NASM in particular, details that are not repeated here, and a part of them being required knowledge to understand the content of this document.
The intention of this tutorial is to show how to use NASM together with the GCC C compiler to build 32-bit protected mode programs on DOS. This pre-supposes that, besides NASM, you have installed DJGPP (including the GNU C compiler) on your system; if you need help with this installation, please, have a look at my tutorial Installing and running DJGPP (C, C++) on FreeDOS. The program samples of the tutorial have been build and tested on FreeDOS 1.3 RC5, using NASM 2.16.0.1 and DJGPP 2.0.5 (GCC 9.30). The tutorial should also apply to MS-DOS or other DOS operating systems. Please note, that to run the executables, build here, on some other DOS computer, you'll have to use the DPMI (DOS Protected Mode Interface) server CWSDPMI.EXE; you can download the software from the DJGPP website at delorie.com. The executables also run in Command Prompt of older Windows releases (at least until Windows 2000 included). They do not run on Windows 1 (no DPMI server), nor on Windows 2 (unless you succeeded to install the 386 features?). On Windows NT, the programs start, but it's not possible to do any keyboard input (?). The programs are not compatible with 64bit operating systems. Use the following link to download the source code of the sample programs.
All samples of the 16-bit NASM tutorial are pure assembly code; keyboard input and screen output having been implemented by calling the DOS API via interrupt 21h. Using interrupt 21h in a DJGPP environment is also possible (even if somewhat more complicated), but this is not part of this tutorial. What we're doing here is splitting the source for the final executable into two part: a main program, written in C, and a subprogram, written in assembly. The big advantage of this way to proceed is that we can use the functions of the C-library, in particular in order to perform input-output operations. Not only we'll not have to deal with interrupt 21h, but also we will not have to code the conversion of numerical data in binary format into ASCII for display (or vice-versa when reading numbers from the keyboard). Instead of having to code this ourselves, we let the C functions do it for us.
In the simplest case, the C main program doesn't do anything else than calling the assembly routine. This one may then call a C function when reading data from the keyboard or writing data to the screen is required. Another possibility consists in doing input-output within the C program. This is simpler for the input-output operation itself, but needs the transfer of data between C and assembly. We'll see several examples of how this may be done.
Screen output using the C function printf.
Lets start with a simple "Hello World" program. Here is the code of HELLO4.C (the main program in C).
#include <stdio.h>
extern void sayhello(void);
int main(void) {
sayhello();
return 0;
}
All that this program does is calling a function (without arguments and without return values) named "sayhello". This function is actually part of the assembly subroutine, so not part of the C program itself. That's why we have to prototype it using the reserved word extern.
And here the assembly routine HELLO4.ASM.
[BITS 32]
[GLOBAL _sayhello]
[EXTERN _printf]
[SECTION .text]
_sayhello:
push ebp
mov ebp, esp
push dword hello
push dword fhello
call _printf
mov esp, ebp
pop ebp
ret
[SECTION .data]
hello db 'Hello, World!', 00h
fhello db '%s', 0Dh, 0Ah, 00h
Some general notes concerning this code:
- With the assembly source file as input, the assembler produces an object file, that (normally) has to be passed to a linker to create the executable. As I said in the 16-bit NASM tutorial, object files may have different formats, and the format to be used when creating DJGPP executables is Common object file format (coff).
- DJGPP programs (more exactly: the coff object format) require the directive [BITS 32] to tell the linker that a 32-bit protected mode executable has to be created.
- I also said in the 16-bit NASM tutorial that an assembly source is divided into several sections. With the coff object format, these sections normally are: [SECTION .text] (containing the assembly instructions), [SECTION .data] (containing initialized data), and [SECTION .bss] (containing uninitialized data). I'm not sure, but I think that you may choose different names for the sections, and that you may also add further sections, if needed or wanted.
- When mixing assembly code with C code, the following always applies:
- All labels, referenced to in the C source, must start with an underscore (_) in the assembly source. This is also true for the C functions called from assembly. In our example, the C main program calls the assembly routine as a function called "sayhello()", thus the entry point of the assembly routine has to be called "_sayhello". The assembly routine uses the C "printf" function to display the greeting message; this function has to be called "_printf" in the assembly source.
- All labels, referenced to in the C source have to be prototyped as extern in the C source and have to be declared as [GLOBAL _label] in the assembly source.
- All functions external to the assembly routine (i.e. functions of the C library) have to be declared as [EXTERN _function] in the assembly source.
- To return the control from the assembly routine to the calling C program, the instruction RET is used.
Interfacing C with assembly is (normally) ruled by the C calling convention. The details of these rules are listed in the chapter Writing 32-bit Code (Unix, Win32, DJGPP) of the NASM documentation. We'll see how this works in practice, when reviewing the code of the different sample programs. For now, just some general remarks:
- The caller (routine that calls an external function) pushes the function's parameters onto the stack, one after another, in reverse order, so that the first argument specified to the function is pushed last). The caller then executes a near CALL instruction to pass control to the callee. If the caller is C, we will not have to worry about all this; it's automatically done by the C compiler. On the other hand, if the caller is assembly, it's our responsibility to push the correct data in the correct order onto the stack, before calling the C function.
- The callee (function that is called by an external routine) receives control, and typically (although this is not actually necessary, in functions which do not need to access their parameters) starts by saving the value of ESP in EBP so as to be able to use EBP as a base pointer to find its parameters on the stack. However, the caller was probably doing this too, so part of the calling convention states that EBP must be preserved by any C function. Hence the callee, if it is going to set up EBP as a frame pointer, must push the previous value first. The callee may then access its parameters relative to EBP.
- Once the callee has finished processing, it restores ESP from EBP if it had allocated local stack space, then pops the previous value of EBP, and returns via RET.
- When the caller regains control from the callee, the function parameters are still on the stack, so it typically adds an immediate constant to ESP to remove them (instead of executing a number of slow POP instructions). Thus, if a function is accidentally called with the wrong number of parameters due to a prototype mismatch, the stack will still be returned to a sensible state since the caller, which knows how many parameters it pushed, does the removing. Again, if the caller is the C program, we will not have to worry about this.
Now we have all we need to have a closer look at our assembly routine. Our routine starts with doing the stack related stuff mentioned under point (2) above:
push ebp
mov ebp, esp
And it ends with doing the stack related stuff mentioned under point (3):
mov esp, ebp
pop ebp
ret
Concerning the functionality of the routine, all it does it calling the C function printf in order to execute the equivalent of the C code printf("%s\n", string-literal), where "string-literal" is the text 'Hello World!', as declared in the .data section. Do not forget to terminate your string values with 00h when declaring them. It's this way that the calling C program detects the end of the string, and if you omit the 00h, display of memory content will continue until a 00h is found)!
As we saw above, to call a C function, we have to put its arguments onto the stack in reverse order, in our case, we'll have first to push the display format ("%s"\n), then the text to be displayed. Both format and text literal are declared in the .data section, and all we have to do is to push the addresses of the function arguments in reverse order onto the stack. On a 32-bit system, these addresses (corresponding to C pointers) are 32 bits of size, thus why we use the specifier DWORD in our PUSH instructions. Code:
push dword hello
push dword fhello
call _printf
Time to test our program. Building the program (that is a mixture of C and assembly) requires 3 steps: 1. Assembling the assembly routine,
creating an object file. 2. Compiling the C program creating another object file. 3. Linking the 2 object files into an executable. The first of these tasks is done using
NASM with the object format being set to coff:
nasm -f coff hello4.asm
This will create the object file hello4.o (.o is the default extension of coff object files).
The second and third of the tasks are done using GCC, run as follows:
gcc hello4.c -o hello4.exe hello4.o -v -Wall
The first parameter (hello4.c) is the C main program, the parameter following the -o option (hello4.exe) is the name of the executable to build, the following file(s)
(hello4.o) is an (are) object file(s) to be included into the build, the other parameters are optional, but it's a good idea to use them in order to make sure that all
possible problems are reported.
If there are lots of errors, the corresponding messages will not all fit on the screen. You can use output redirection (adding something like >hello4.txt to the command line above; this will redirect the output of GCC to the file hello4.txt.
To run the assembler, compiler and linker from the directory where your sources are located, you have to add the path to the directories containing these files
to your PATH environment variable. In my case:
path=c:\nasm;c:\djgpp\bin;%path0%
where %path0% is a custom environment variable on my system, set to the original path at boot time (you will probably use
%path% instead). You have also to set the DJGPP environment variable; on my system:
set djgpp=c:\djgpp\djgpp.env
Note: If you use g77 together with gcc, following my tutorial Installing and running Gnu Fortran on FreeDOS, make sure that the files actually used are those for C/C++ and not those for Fortran!
The screenshot shows the different files created during the build, as well as the execution of hello4.exe.
Keyboard input using the C function gets.
Lets extend our "Hello World" program to a simple "Hello User" program. Here is the code of HELLO5.C.
#include <stdio.h>
extern void sayhello(void);
int main(void) {
sayhello();
return 0;
}
It is identical to HELLO4.C and all that it does is calling the external function "sayhello".
And here is the assembly routine HELLO5.ASM.
[BITS 32]
[GLOBAL _sayhello]
[EXTERN _gets]
[EXTERN _printf]
[SECTION .text]
_sayhello:
push ebp
mov ebp, esp
push dword quser
push dword format1
call _printf
add esp, 8
push dword buffer
call _gets
add esp, 4
lea esi, [buffer]
lea edi, [hname]
mov al, [esi]
cmp al, 00h
je world
copychar:
mov [edi], al
inc esi
inc edi
mov al, [esi]
cmp al, 00h
jne copychar
mov byte [edi], '!'
push dword huser
push dword format2
jmp display
world:
push dword hworld
push dword format2
display:
call _printf
add esp, 8
mov esp, ebp
pop ebp
ret
[SECTION .data]
maxlen equ 250
quser db 'Please, enter your name? ', 00h
buffer times maxlen + 1 db 00h
huser db 'Hello, '
hname times maxlen + 2 db 00h
hworld db 'Hello, world!', 00h
format1 db '%s', 00h
format2 db '%s', 0Dh, 0Ah, 00h
The program asks for a name and then displays either a personal greeting, or "Hello world!" (if no name is entered, i.e. if the user just hit ENTER). The program works the same way as the previous one, input-output being done by calling functions of the C library: To display data onto the screen, it uses printf (as in HELLO4.ASM), to read data from the keyboard, it uses gets.
Note: The C function gets is not only deprecated, but is also really dangerous. In fact, there is no control of how many characters the user enters and thus there might easily be an overflow of the input buffer, the program overwriting the data area following the buffer, and eventually also overwriting code of the operating system. So, why did I use it then? It seems to be difficult to find another function that effectively works. The "normally" used C function for keyboard input, getline seems not to be part of GCC 9.30 for DOS. And the function fgets, also deprecated, but safer because you can specify the maximum of characters that the user can enter, did produce a runtime error on my system. Maybe that I did something wrong, maybe that there is a real issue. Anyway, as we are in a "play with assembly on DOS" and not in a real world environment, the usage of gets should not be a serious problem...
As said before, the assembly routine entry point (corresponding to the function call in the C main program) has to be specified as GLOBAL and the two C functions used have to be specified as EXTERN. Also, as in the previous program, the first action when entering the routine and the last action to take before leaving it, are the stack related operations, as stated by the C calling convention.
The program-specific code starts with asking the user for their name. This is done by calling printf in order to
display the text at address "quser" using the C display format at address "format1" (no CR+LF, as we want the user to enter the name in the same screen line as the text
that asks for it). On return from the C function, we find the instruction
add esp, 8
It is used as a (faster) replacement of two POP instructions, that remove the two double-words (i.e. 2 x 4 = 8 bytes), pushed before, from the
stack. Keeping the stack clean should always be done after a CALL has been made!
The input of the name is done by calling the C function gets. This function has a single argument, in C it's a
pointer to the char array that will contain the input data; in assembly it's so to say the same: the argument is the address of the first byte
of the buffer area, where the user input will be stored. Here is the code:
push dword buffer
call _gets
add esp, 4
I have declared the buffer area in the .data section with a length of 250 + 1 bytes. It's exaggerated, of course, but when using a dangerous
function as gets, it's better to have a buffer that is lots to big in size than to risk its overflow. The buffer, as declared, allows the user
to enter 250 characters. An additional byte is needed to store the end-of-string marker 00h, that is included in the value returned by gets
(the CR+LF, resulting from the user pushing the ENTER key to terminate input, on the other hand, is not part of the value returned). Note that I initialized the buffer
with 00h values, thus there is no risk not to detect the end-of-string. A more "normal" way would be to declare the buffer in the .bss section. The last of the three
lines of code above removes the buffer address from the stack (as explained with printf before).
Moving the name from the input buffer to the "hname" area is done character by character, using the registers ESI and EDI as source resp. destination indexes. It is essentially the same than in the program sample HELLO3.ASM of my 16-bit assembly programming using NASM tutorial. A difference with that program is that, before entering the copy loop, I check if the first byte of the name is 00h, and if yes, I jump to the "world" label (remember, that we want to display "Hello world!" if no name is entered). The copy loop is terminated when the actual byte tested is 00h. We then add a "!" to the name output area; as this area was initialized with 00h values, we'll not have to worry of the end-of-string marker.
The output of the personal greeting or the general message is done using printf (at label "display"). The arguments for the function have been placed on the stack before: either the address of the general message ("hworld"), or the address of the personal message ("huser") first, then the display format (here we use "format2", that includes a CR+LF).
The screenshot shows the execution of hello5.exe.
Passing parameters between C and assembly.
In the two sample programs before, it was the assembly routine that did the input-output, calling a C function. In the following very simple program, that asks the user for two integers and displays the sum of them, input-output is done in the C main program (all the assembly routine does is calculating the sum). This requires the understanding of 1. how data is passed from C to assembly (our two integers) and 2. how data is passed from assembly to C (the sum).
When talking about the C calling convention, I said: The caller pushes the parameters onto the stack, one after another, in reverse order. The callee may access the parameters relative to EBP. No worries concerning the C program: when calling the assembly routine with the two integers as parameters, these are placed onto the stack automatically. On the other hand, it's our business to retrieve them from the stack in the assembly routine.
To do this correctly, we need some more details concerning the C calling convention. When the caller executes the near CALL (in our case, when the C program calls the assembly routine), the double-word at [EBP] holds the previous value of EBP as it was pushed. The next double-word, at [EBP+4], holds the return address, pushed implicitly by CALL. The parameters start after that, at [EBP+8]. The leftmost parameter of the function, since it was pushed last, is accessible at this offset from EBP; the others follow, at successively greater offsets. Thus, in a function such as printf which takes a variable number of parameters, the pushing of the parameters in reverse order means that the function knows where to find its first parameter, which tells it the number and type of the remaining ones.
Concerning the function return (in our case, when the assembly routine reaches the RET instruction), the return value is typically left in AL, AX or EAX depending on the size of the value. Floating-point results are typically returned in ST0.
Here is the code of ADD2.C.
#include <stdio.h>
extern int add2(int, int);
int main(void) {
int a, b, answer;
printf("Enter two integers separated by a space? ");
scanf(" %i %i", &a, &b);
answer = add2(a, b);
printf("%i %u %x\n", answer, answer, answer);
return 0;
}
The two numbers are read from the keyboard using the function scanf. The sum of the numbers is calculated calling the external function "add2" (our assembly routine), and then displayed onto the screen using printf, displaying it as integer, as positive integer, and as hexadecimal.
Now the code of ADD2.ASM.
[BITS 32]
[GLOBAL _add2]
section .text
_add2:
push ebp
mov ebp, esp
mov eax, [ebp + 8]
add eax, [ebp + 12]
mov esp, ebp
pop ebp
ret
C calls the function "add2" by value, thus the two numbers to be added are as such on the stack. As we saw above the first argument is pointed to by EBP with an offset of +8; we load the first number into AX. As the numbers are 32 bits, the second number is at an offset of 8 + 4 = +12; we add it to the content of AX. And, as the function return is an integer (double-word), thus has to be placed into the AX register, we have what we want and need.
The screenshot shows the execution of add2.exe.
Accessing external variables.
Passing the arguments from the C program to the assembly routine using the stack and returning the function result from the assembly routine to the C program in the AL/AX/EAX register is the most common way of communication between C and assembly. However, there is another possibility: the C program can directly access a variable declared in the assembly routine, and the assembly routine can directly access a variable declared in the C program.
This is shown in the following small program sample (based on an example that I found at the Interfacing DJGPP with Assembly-Language Procedures webpage). A string, containing a version number is declared in the .data section of the assembly routine. The C program accesses this string variable directly to print it out. It then changes the letter part of the version number by calling the assembly routine. However, instead of passing the new version letter via the stack, it assigns it to a local variable and calls the routine without argument. The assembly routine gets the new version letter by directly accessing the C variable and modifies the version string. At the return from the routine, the C program accesses the version string a second time to display its modified value. Not really useful all that, but a good example to show how direct communication works...
Here is the code of VERSION.C.
#include <stdio.h>
char NewVers;
extern void NewVersion(void);
extern char Vers[];
int main (void) {
printf("Actual version: %s\n", Vers);
NewVers = 'b';
NewVersion();
printf("New version: %s\n", Vers);
return 0;
}
"NewVersion" is the entry point of the assembly routine and has thus to be declared as external function in the C program. "Vers" is the version string declared in the assembly routine and has thus to be declared as external variable (C string = array of characters) in the C program. "NewVers" is a variable within the C program; it will be directly accessed by the assembly routine. As this variable is external to the assembly routine, it has to be declared outside the "main" bloc).
Now the code of VERSION.ASM.
[BITS 32]
[GLOBAL _NewVersion]
[GLOBAL _Vers]
[EXTERN _NewVers]
[SECTION .text]
_NewVersion:
push ebp
mov ebp, esp
mov al, [_NewVers]
mov [_Vers + 26], al
mov esp, ebp
pop ebp
ret
[SECTION .data]
_Vers db 'NASMTEST.ASM - Version 0.0a', 00h
The situation is the inverse of what it is in the C program. The entry point of the routine "_NewVersion", corresponding to the C function "NewVersion" has to be declared as global. Concerning the variables, the version string "_Vers" is local to assembly, and has to be declared as global variable in order to make the C program access it. On the other hand, "_NewVers" is a local C variable and in order to access it from assembly it has to be declared as an external variable.
The code of the routine is easy to understand. The AL register is loaded with the content of "_NewVer" (the new version character set in the C program) and used to replace the version number letter (the "a" at offset 26 in the version string). That's it!
The screenshot shows the source, object and executable files listing and the execution of version.exe.
Program samples: Fibonacci series and palindromes.
The following two program samples don't include any new features. The C main programs do nothing but calling the assembly routine (no arguments, no return); input-output is handled within the assembly routine by calling the corr. functions from the C library. I include these programs in the tutorial to give the assemby programming beginner the opportunity to view some simple code examples. Perhaps you can try to create these programs by yourself and when done (especially if you don't not succeed) compare with the source listed here.
The first of these programs calculates the Fibonacci series. These are a series of numbers defined by the function f(n) = n-2 + n-1, with f(0) = 0 and f(1) = 1. The screenshot shows the execution of my fibona.exe (calculating the 45 first numbers of the series).
Here is the code of the assembly routine FIBONA.ASM (the C main program just calls the routine, as for the first program sample of the tutorial).
[BITS 32]
[GLOBAL _fibo]
[EXTERN _printf]
[SECTION .text]
_fibo:
push ebp
mov ebp, esp
push dword gtitle
push dword formats
call _printf
add esp, 8
mov eax, 1
push eax
push dword format1
call _printf
add esp, 8
mov eax, 1
push eax
push dword format1
call _printf
add esp, 8
loop:
mov eax, [n1]
mov ebx, [n2]
add eax, ebx
mov [n1], ebx
mov [n2], eax
div dword [five]
cmp edx, 0
mov eax, [n2]
push eax
je cont1
push dword format1
jmp cont2
cont1:
push dword format2
cont2:
call _printf
add esp, 8
mov cl, [count]
inc cl
mov [count], cl
cmp cl, max
jl loop
mov esp, ebpv
pop ebp
ret
[SECTION .data]
max equ 45
five dd 5
count db 2
n1 dd 1
n2 dd 1
gtitle db 'Fibonacci series.', 00h
format1 db '%10u ', 00h
format2 db '%10u', 0Dh, 0Ah, 00h
formats db '%s', 0Dh, 0Ah, 00h
The routine includes a loop with one iteration for each number of the series calculated. The CL register is used together with memory address "count" as loop counter. It is initialized with 2 (the first two numbers are given), and the loop terminates when the counter (incremented by 1 each iteration) has reached 45.
The calculation is done using the memory addresses "n1" and "n2", both initialized with 1, the first two numbers of the series (displayed at the beginning of the routine). The numbers at these two addresses are loaded into AX and BX respectively, and their sum is calculated (result in AX). Then, we move BX (the number that was at "n2" before) to "n1", and AX (the sum) to "n2"; the two addresses thus contain the numbers to calculate the next element of the series.
Before printing out the numbers, the routine checks if the counter value is a multiple of five. If yes, the display format "format2" (including a CR+LF) is used instead of "format1"; this allows the 5-column display as you can see on the screenshot.
The second one of these programs checks if a word (entered by the user) is a palindrome. Palindromes are words that are identical when read from left to right and from right to left. The screenshot shows an execution of paldr.exe on my FreeDOS machine.
Here is the code of the assembly routine PALDR.ASM (the C main program just calls the routine, as for the first program sample of the tutorial).
[BITS 32]
[GLOBAL _pdrome]
[EXTERN _gets]
[EXTERN _printf]
[SECTION .text]
_pdrome:
push ebp
mov ebp, esp
loop:
push dword rword
push dword format1
call _printf
add esp, 8
push dword buffer
call _gets
add esp, 4
lea esi, [buffer]
xor eax, eax
mov bl, [esi]
cmp bl, 00h
je exit
ccount:
inc al
inc esi
mov bl, [esi]
cmp bl, 00h
jne ccount
lea esi, [buffer]
lea edi, [buffer + eax - 1]
check:
mov byte bl, [esi]
mov byte dl, [edi]
inc esi
dec edi
dec al
cmp al, 0
je done
cmp bl, dl
je check
done:
cmp al, 0
je ispal
push dword nopal
push dword format2
jmp display
ispal:
push dword pal
push dword format2
display:
call _printf
add esp, 8
jmp loop
exit:
mov esp, ebp
pop ebp
ret
[SECTION .data]
maxlen equ 250
two db 2
rword db "Please, enter a word? ", 00h
buffer times maxlen + 1 db 00h
pal db "This word is a palindrome", 00h
nopal db "This word isn't a palindrome", 00h
format1 db '%s', 00h
format2 db '%s', 0Dh, 0Ah, 00h
The program first counts the number of letters of the word, then makes a letter by letter compare between the word starting with the first letter and going to the next, and the word starting with the last letter and going to the previous, until there is a mismatch (in this case, the word isn't a palindrome), or until all letters have been done (in this case, the word is a palindrome).
Working with integer arrays.
A one-dimensional array is nothing else than a succession of memory locations. In C, the array's elements are accessed using an index, index = 0 for the first element and index = array-size - 1 for the last element. In assembly, the first array element can be pointed to by an index register, all other elements can be accessed by adding an offset to the register's value. The simplest case is an array of characters (1 byte), where the offset is the same as the index in C: offset = 0 for the first array element, offset = array-size - 1 for the last element.
What now if we have to deal with an array of integers? On a 32-bit system, integers are 32 bits, i.e. they have a size of 4 bytes (one double-word). This means that in order to pass from one element to the other, we have to add 4 to the offset: the second element is located at offset +4, the third at offset + 8, the last at offset (array-size - 1) * 4.
Where to declare the array and how to access it is the choice of the programmer; any of the ways to proceed, that we described before, is possible. Personally I think that the simplest way is to declare the array in the assembly routine (for an integer array of 50 elements, we'll have to reserve a memory area of 50 double-words) and to directly access it from the C program (another possibility is that the assembly routine returns the base address of the array memory location; cf. DNACNT.ASM further down in the text). That's how it is done in the following sort program. The C program asks the user to enter a list of integers (implementing this input would be rather complicate in assembly) and puts them into an external array (declared in the assembly routine). It then calls the assembly routine to sort the array elements (passing the length of the array = the number of elements as argument). The assembly routine sorts the array and on return the C program accesses the external array a second time to display its elements, now sorted.
Here is the code of BUBBLE.C.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
extern int numbs[50];
extern void sort(int);
int main (void) {
char snumbers[250];
char *token;
int top = -1;
puts("Enter integer values separated by a comma");
fgets(snumbers, 250, stdin);
token = strtok(snumbers, ",");
top = -1;
while (token != NULL && top < 49) {
top++;
numbs[top] = atoi(token);
token = strtok(NULL, ",");
}
sort(top + 1);
for(int i = 0; i <= top; i++) {
printf("%d ", numbs[i]);
}
printf("\n");
return 0;
}
The number list is read as a string (integer values separated by commas) using the function fgets, that limits user input to 250 characters. Concerning the transformation of this string into an array, please, have a look at a C book (if you want to understand how it works...). Note, that filling the array is stopped when 50 numbers have been read; this to make sure that there is no overflow of the array memory area reserved in the assembly routine. In the call instruction of the assembly routine, sort(top + 1), the variable "top" incremented by 1 corresponds to the number of array elements.
The sort routine is a classic bubble sort. I actually got the code from the manual Introduction to Assembly Programming - For Pentium and RISC Processors, Sivarama P. Dandamudi, © 2005, 1998 Springer Science+Business Media, Inc. The array memory area has been filled by the C program; the array size is passed via the stack. Here is the code of BUBBLE.ASM:
[BITS 32]
[GLOBAL _sort]
[GLOBAL _numbs]
[SECTION .text]
_sort:
push ebp
mov ebp, esp
mov ecx, [ebp + 8] ; load array size into ECX
next_pass:
dec ecx ; if # of comparisons is zero
jz sort_done ; then we are done
mov edi, ecx ; else start another pass
mov dl, sorted ; set status to SORTED
mov esi, _numbs ; load array address into ESI
pass:
; ESI points to element i and ESI+4 to the next element
; The loop represents one pass of the algorithm
; Each iteration compares elements at [ESI] and [ESI+4]
; and swaps them if ([ESI]) > ([ESI+4])
mov eax, [esi]
mov ebx, [esi+4]
cmp eax, ebx
jg swap
increment:
add esi, 4 ; ESI pointing to next element
dec edi
jnz pass
cmp edx, sorted ; if status remains SORTED
je sort_done ; then sorting is done
jmp next_pass ; else initiate another pass
swap:
; Swap elements at [ESI] and [ESI+4]
mov [esi+4], eax ; copy [ESI] in EAX to [ESI+4]
mov [esi], ebx ; copy [ESI+4] in EBX to [ESI]
mov edx, usorted ; set status to UNSORTED
jmp increment
sort_done:
mov esp, ebp
pop ebp
ret
[SECTION .data]
sorted equ 0
usorted equ 1
max equ 50
[SECTION .bss]
_numbs resd max
The screenshot shows some bubble sorts on my FreeDOS machine.
Two further program samples.
I was rather excited when my first assembly programs build and executed without errors and so I continued to develop some further simple programs that, of course, I want to share with those who are interested in.
The probably simplest game that you can play with a computer is the program "thinking of a number" and the player having to guess it. I could imagine that lots of people of my age have created their Guess the number game in QuickBasic or Turbo Pascal long years ago. Implementing the game in assembly is not a big deal, especially in our case, where we mix assembly and C and where we can use the C rand function to generate the number that the computer thinks of.
Here is the code of NGUESS.C.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
extern void guess(int);
int main(void) {
int Low = 1; int High = 100; int Number;
srand(time (0));
Number = rand() % (High - Low + 1) + Low;
guess(Number);
return 0;
}
The program generates a random number between 1 and 100, that it passes via the stack to the assembly routine (that contains the complete code of the game).
And here is the code of NGUESS.ASM.
[BITS 32]
[GLOBAL _guess]
[EXTERN _gets]
[EXTERN _printf]
[SECTION .text]
_guess:
push ebp
mov ebp, esp
mov eax, [ebp + 8]
mov [random], eax
push dword gtitle
push dword format2
call _printf
add esp, 8
loop:
push dword qnumber
push dword format1
call _printf
add esp, 8
push dword buffer
call _gets
add esp, 4
mov eax, 0
mov al, [buffer]
and al, 0fh
mul byte [n100]
mov ebx, eax
mov eax, 0
mov al, [buffer + 1]
and al, 0fh
mul byte [n10]
add ebx, eax
pushf
mov eax, 0
mov al, [buffer + 2]
and al, 0fh
popf
adc ebx, eax
mov eax, [random]
mov cl, [count]
inc cl
mov [count], cl
cmp ebx, eax
jl less
jg greater
xor ecx, ecx
mov cl, [count]
push ecx
push dword enumber
call _printf
add esp, 8
jmp exit
less:
push snumber
push dword format2
call _printf
add esp, 8
jmp loop
greater:
push bnumber
push dword format2
call _printf
add esp, 8
jmp loop
exit:
mov esp, ebp
pop ebp
ret
[SECTION .data]
count db 0
n100 db 100
n10 db 10
gtitle db 'Guess the Number Game in x86 Assembly (DJGPP).', 00h
qnumber db 'Please, enter a 3-digits number between 1 and 100? ', 00h
bnumber db 'Your number is to big!', 00h
snumber db 'Your number is to small!', 00h
enumber db 'Gongratulations! You have found the number in %u guesses.', 0Dh, 0Ah, 00h
format1 db '%s', 00h
format2 db '%s', 0Dh, 0Ah, 00h
[SECTION .bss]
random resd 1
buffer resb 4
The assembly routine gets the random number (generated by the C program) from the stack and stores it at memory location "random".
mov eax, [ebp + 8]
mov [random], eax
After having displayed the program title, it enters the main loop, where the user is asked for a guess, the loop continuing until the computer's number has been found (i.e. until the number entered by the player equals the random number stored at address "random"). The number input is done calling the C function gets, what means that the number received by the routine is actually a string, i.e. a sequence of ASCII characters, that have to be converted into a (binary) integer value. To simplify this task, the player has to always enter three digits (e.g. 008 for 8, 056 for 56, ...). The integer value is calculated by calculating the sum of the first digit multiplied by 100, the second digit multiplied by 10 and the last digit as such. The multiplications are done, using BCD arithmetic, as described in my 16-bit assembly programming using NASM tutorial.
The number entered by the player being in register BX, AX is loaded with the random number and the comparison of the content of the two registers leads to the
evaluations "player number is to big", "player number is to small", or "random number found". Each time the comparison is made, the counter of the player's guesses
is incremented by 1.
mov eax, [random]
mov cl, [count]
inc cl
mov [count], cl
cmp ebx, eax
jl less
jg greater
The code following the instructions above correspond to the situation where the player has found the computer's number, and the congratulations message, together with
the number of guesses that the player needed, are displayed. Then, the routine returns control to the calling C program.
xor ecx, ecx
mov cl, [count]
push ecx
push dword enumber
call _printf
add esp, 8
jmp exit
If, on the other hand, the player's number is too big or too small, the code starting at the corresponding label is executed (the corr. message is displayed), and the main loop continues.
The screenshot shows two executions of the "Guess the number" game on my FreeDOS machine.
The second program, that I want to share here, is molecular biology related: count of the number of bases in a DNA sequence. The sequence to analyze is stored in a file; the filename has to be given on the command line when the program is executed. The file is read by the C program, that is rather long, because it does several checks: file exists, file not empty, file is in FASTA format, sequence not longer than 1000 bases (arbitrarily fixed value). The sequence read is stored in a memory area of the assembly routine (an external array of characters from the point of view of C) and the "docount" routine is called with the sequence length as argument. The routine counts adenine, guaninie, cytosine, thymine and other bases; the counts are stored in 4 successive double-word areas. When counting is done, the assembly routine returns the starting address of the counting areas. From the point of view of C, this is a pointer to an array of integers, and using the indexes 0 to 4, the different counts can be retrieved and displayed.
Here is the code of DNACNT.C:
#include <stdio.h>
#include <string.h>
extern char dna[1001];
extern int * docount(int);
int main (int argc, char *argv[]) {
FILE *fstream;
int len, ret, done, bx, dx;
int *ptr;
char bases[5] = "ACGT?";
char header[252], buffer[82];
ret = 0;
if (argc != 2) {
ret = 1;
}
else {
ret = 0; done = 0; dx = 0;
fstream = fopen(argv[1], "r");
if (fstream == NULL) {
ret = 2;
}
else {
if (fgets(header, 252, fstream)) {
if (header[0] != '>') {
ret = 5;
}
else {
while (done == 0 && fgets(buffer, 82, fstream)) {
bx = 0;
if (buffer[1] == '\0') {
done = 1;
}
else {
for (int i = 1; i <= strlen(buffer); i++) {
if (done == 0 && buffer[bx + 1] != '\0') {
bx++; dx++;
if (dx <= 1000) {
dna[dx - 1] = buffer[bx - 1];
}
else {
ret = 6; done = 1;
}
}
}
}
}
}
}
else {
ret = 3;
}
}
if (ret == 0) {
if (strlen(dna) < 1) {
ret = 4;
}
else {
len = strlen(dna) - 1;
ptr = docount(len);
dna[len] = '\0';
header[strlen(header) - 1] = '\0';
printf("%s\n", header);
printf("%s\n", dna);
printf("Base count (total = %u):\n", len);
for (int i = 0; i < 5; i++) {
if (i < 4 || ptr[i] > 0) {
printf(" %c: %3u\n", bases[i], ptr[i]);
}
}
}
}
}
switch (ret) {
case 1: printf("Invalid number of command line parameters\n"); break;
case 2: printf("Error when opening file '%s'!\n", argv[1]); break;
case 3: printf("File '%s' is empty!\n", argv[1]); break;
case 4: printf("File '%s' doesn't contain a sequence!\n", argv[1]); break;
case 5: printf("File '%s' is not a valid FASTA file\n", argv[1]); break;
case 6: printf("Sequence exceeds 1000 bases!\n");
}
return ret;
}
The argc and argv arguments in the main function declaration refer to the count of the command line arguments, resp. the command line arguments themselves; cf. a C book for details.
The file is supposed to have a maximum of 80 characters per line (I think that fgets truncates any supplementary characters), it is supposed to have a FASTA header, and that the DNA is a maximum of 1000 bases. The program reads the first line (that has to be a valid FASTA header), than continues reading line after line (but not more than 80 characters; it is just supposed that no file has longer lines...), until the end-of-file (or an empty line) is reached. After a line has been read, the characters (i.e. the DNA bases) are copied to the external character array "dna". When all bases have been processed, the program calls the external function "docount" (our assembly routine), passing the number of bases as argument. At the return from the assembly routine, the pointer variable "ptr" contains the address of the counter array (that is declared in the assembly routine) and "ptr[I]" may be used to access the different counter values. Finally, the FASTA header, the DNA sequence, and the base counts are displayed.
Now, the code of DNACNT.ASM. With the comments in the source, you shouldn't have a problem to understand how the program works.
[BITS 32]
[GLOBAL _docount]
[GLOBAL _dna]
[SECTION .text]
_docount:
push ebp
mov ebp, esp
mov ecx, [ebp + 8] ; get sequence length from C program
lea esi, [_dna] ; point to first base of sequence
next:
mov al, [esi] ; get base
cmp al, 'A' ; if is adenine (A)
je adenine ; goto adenine increment
cmp al, 'C' ; if is cytosine (C)
je cytosine ; goto cytosine increment
cmp al, 'G' ; if is guanine (G)
je guanine ; goto guanine increment
cmp al, 'T' ; if is thymine (T)
je thymine ; goto thymine increment
unknown:
lea edi, [counts + 16] ; point to unknown count
jmp continue
adenine:
lea edi, [counts] ; point to adenine count
jmp continue
cytosine:
lea edi, [counts + 4] ; point to cytosine count
jmp continue
guanine:
lea edi, [counts + 8] ; point to guanine count
jmp continue
thymine:
lea edi, [counts + 12] ; point to thymine count
continue:
mov ebx, [edi] ; load counter
inc ebx ; increment it
mov [edi], ebx ; and restore it
inc esi ; point to next base
dec ecx ; if all bases have been counted
jz done ; we are done
jmp next ; check next base
done:
mov eax, counts ; pointer to "counts" array returned to C program
mov esp, ebp
pop ebp
ret
[SECTION .data]
max equ 1000
counts times 5 dd 00h
[SECTION .bss]
_dna resb max
The screenshot shows some executions of the program on my FreeDOS machine.
If you find this text helpful, please, support me and this website by signing my guestbook.