Computing: DOS, OS/2 & Windows Programming

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:

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

NASM on FreeDOS: Compiling, linking and running a 32-bit assembly and C 'Hello World' program

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.

NASM on FreeDOS: Running a 32-bit assembly and C 'Hello User' program

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.

NASM on FreeDOS: Running a 32-bit assembly and C 'Add 2 integers' program

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.

NASM on FreeDOS: Running a 32-bit assembly and C 'replace character in string' program

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).

NASM on FreeDOS: Running a 32-bit assembly and C 'Fibonacci series' program

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.

NASM on FreeDOS: Running a 32-bit assembly and C 'Check palindrome' program

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.

NASM on FreeDOS: Running a 32-bit assembly and C 'bubble sort' program

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.

NASM on FreeDOS: Running a 32-bit assembly and C 'guess the number' game program

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.

NASM on FreeDOS: Running a 32-bit assembly and C 'DNA bases count' program

If you find this text helpful, please, support me and this website by signing my guestbook.