Assembly Language - Part 4

In the first 3 parts, we discussed how to use the 'C' compiler (and C++) to merge and link assembly language routines into 'C'/C++ programs.  We also saw how to call 'C' library functions such as printf() and scanf() from assembly language.  Now, let's see what it takes to build a purely assembly language program.

First, let's consider what it means to produce a purely assembly language program.  The first thing that happens is we lose almost all capability to do any I/O with the user.  All of the I/O is done through the standard 'C' libraries, and if we drop those libraries, we are suddenly in tremendous pain when it comes to interacting with the users! We could, if we were feeling absolutely masochistic, look hard and deep into the source code for the standard library, where we could learn how to access the device drivers to manage our own I/O, but trust me - it is just much easier to re-link the standard library back in, and we will look at how to do that, later.  For now, however, let's look at the purely assembly language program.  So, if we lose the I/O functions we have only one method left for us to receive data, and respond.  we can receive command-line parameters, and we can respond only by returning an integer value.  Consider the following 'C' program, where we are adding a group of parameters:

/* adder.c */	
int main( int argc, char **argv )
{
  int total=0;
  int loop;

  /* skip argv[ 0 ] because it is the program name: */
  for( loop=1; loop<argc; loop++ ) 
    total += argv[ loop ];

  return( total );
}

trantor> adder 1 2 3 4
trantor> echo $?
10

At least that is what we would like to see happen.  If you are not familiar with $?, keep in mind that all unix programs have an integer return value, and the environment variable named $? contains that return value after every command.  In this case, we would like the return value to be 1+2+3+4, which is 10.  so, we compile and run the program and viola, I get:

trantor> cc adder.c -o adder
adder.c: In function `main':
adder.c:9: warning: assignment makes integer from pointer without a cast
trantor> adder 1 2 3 4
trantor> echo $?
248

Huh? Wha happun? There's no way 1+2+3+4 is 248! Well, let's look at that warning I got at compile time.  Notice the line in my loop where I am totalling integers, total += argv[ loop ]; In this case, we have to remember that parameters are not integers - they are character strings, or pointers! we would need this line to read, total += atoi( argv[ loop ] );

Well, that kills us because atoi() is in the standard 'C' library! We can't do it! Yes, we can, we will just have to re-write the atoi() function, that's all.  So, let's take a quick look at what that needs:

NOTE: Once again, I am making the assumption that the reader is already familiar with assembly language.)
Also, this is the point where I basicly insist that you read my page on documentation.

//===========================================================================
// ascii_to_binary - function to convert ASCII string to binary integer     |
//--------------------------------------------------------------------------|
//Input:   %ESI register points to base address of ASCII string             |
//Output:  %EAX register contains the computed value                        |
//Effects: %EAX, %ECX, %EDX, %EDI, %ESI                                     |
//Process: 1)set %edx to +1/-1 (sign)                                       |
//         2)Load the next character from the ASCII string                  |
//         3)If this caracter is not an ASCII digit, I'm done, go to (7)    |
//         4)Multiply the current total (not counting this character by 10  |
//         5)Convert this ASCII digit to binary number, add it to total     |
//         6)Loop back up to (2)                                            |
//         7)Move the total into the %EAX register                          |
//         8)DONE                                                           |
//===========================================================================
	
ascii_to_binary:
        .type ascii_to_binary,@function	

	xorl   %ecx,%ecx               #clear %ECX register (total)
	xorl   %edi,%edi               #clear %EDI register (sign)
	incl   %edi                    #%EDI now holds positive 1
	movb   (%esi),%al              #look at the first character
	cmpb   '-',%al                 #is it a negavit value?
	jne    ascii_to_bin_0          #no, jump to the loop

	negl   %edi                    #yes, %EDI register is now -1
        inc    %esi                    #shift up to next character

ascii_to_bin_0:                #top of loop
        lodsb                          #mov (%esi),%al, inc %esi
	cmpb   $0x30,%al               #is this character less than 0x30?
	jl     ascii_to_bin_1          #yes, exit loop
	cmpb   $0x39,%al               #is this character more than 0x39?
	jg     ascii_to_bin_1          #yes, exit loop

        movb   %al,%bl                 #save this value
        subb   $0x30,%bl               #convert ASCII digit to numberical digit

	movl   %ecx,%eax               #multiply needs EAX register
	imull  $10,%eax                #multiply current total times 10
                                       #note that this effects %edx!
        movl   %eax,%ecx               #back to the ECX register

	movb   %bl,%al                 #Now I need this back in %AL
	cbw                            #convert byte in %al to word in %ax
	cwde                           #...and from %ax to %eax
	addl   %eax,%ecx               #add this into the total value
	jmp    ascii_to_bin_0          #jump to top of loop

ascii_to_bin_1:                #end of loop
        movl   %ecx,%eax               #move into eax for multiply again
        imull  %edi,%eax               #put the sign back into number
	ret                            #G'bye!
ascii_to_binary_end:           #end of the function
        .size  ascii_to_binary,ascii_to_binary_end-ascii_to_binary


Well, now, that wasn't too bad.  To summerize, there are a few assumptions the function makes.  The %ESI register must point to the first charater in the string, and as long as the string contains all digits ('0'-'9'), the function loops through these values, looking for the first non-digit character, at which point, it returns the calculated value in the %EAX register.  I did also add functionality to accept a negative sign in front, so the string, "-12" will result in a negative value being returned.  Note, however, that the string "+20" will return a 0 value because the '+' character is never handled.  It wouldn't be hard to add this, so maybe I will in the next revision.  Another thing that might be important to remember, this function does not build a stack frame.  It uses the registers to pass values, and it uses a number of registers as storage values, specifically the %ECX and %EDX registers.  If the calling function (or any upward function) needs to depend on the %ECX or %EDX registers, those functions must save the current values (either saving them in a memory location or pushing them on the stack) before this function is called, and then restore them later.  That is why I include a comment line stating that the %EAX, %ECX, %EDX, and %ESI registers are effected.  It would probably be a good idea for my function to push the %ECX and %EDX registers, then restore them just before the ret statement, but I chose not to because I wanted this routine to be capable of running as fast as possible.  If I need to save %ECX and %EDX, I can look at my function's documentatin and decide whether or not to save and restore these values from my higher function.  (This is just a design feature that I can use to optimize my entire program.)

Now that I have my ascii_to_binary function (instead of that old atoi() function), I can go back and write my program.  Now, even though I am not using any 'C' in mt program, i MUST keep in mind that the operating system is expecting 'C'/C++ to be the typical program, so the operating system is going to make some assumptions:

  1. The program's entry point will be named, "main"
  2. Parameters into the program are passed the same way as in 'C'

That first assumption is no big deal.  I can still name my program whatever I want, but I do still have to use "main" as my point of entry.  As for the parameters, these are not terribly difficult to figure out, either, except the actual argv is going to take some careful, deliberate thought.  Because of the parameters, let's assume to use the enter instruction on entry into the function, and look at the stack frame, and use offset values relative to the %EBP register:

+12: char   **argv
+8:  int    argc
+4:  return address
+0:  original value of %EBP

That's not too bad, but that argv value is a double-pointer! That can give us some headaches.  A single pointer is easy to think of; it is simply an integer value that is a byte address in memory.  Worse, yet, we know that argv is actually an array, and that is actually an array of pointers of character arrays! Scary! Well, let's look very closely at what that means.

If we look at memory in terms of a linear string of bytes, we can visualize a character string as a series of one byte after another, each byte holding a character in our stream of characters.  (Don't forget the null terminitor at the end:

char *greeting_string; ----> in which case, greeting_string is actually an integer value that points to the first letter in the string, the 'H.'  The integer, greeting_string+1 would point to the memory location that contains the letter, 'E.'  this is important to remember! The size of each data element in a character string is 1 byte.

Next, let's build an array of these pointers.  we could define an array of pointers the same way as defining an array of integers:

int array[10];

becomes the following, when we replace the data type of pointer-to-char for integer:

char *array[10];

And when we replace array[] with *array (which is exactly what the compiler does), we get:

char **array;

And the one very inportant thing to remember is my datatype's size! Remember that integers and pointers are both 32-bit values.  So, what do we see in memory? Well, my first pointer is "array."  This points to s ection in memory that looks like this:

array -----> pointer_number0
      (+4)-> pointer_number1
      (+8)-> pointer_number2

...and so on.  Now, remember that these pointers still point somewhere in memory, (we don't know where), we end up with something that looks like this:

array -----> pointer_number0--------------------------------->string0
      (+4)-> pointer_number1-------->string1
      (+8)-> pointer_number2----------------->string2

Wow, that's a lot! But, we now have the information we need to write code to look at the parameters:

//===========================================================================
//main - entry point into my all-assembly-language program                  |
//--------------------------------------------------------------------------|
//Input:   -NONE- I receive a standard parameter stack frame from the O.S.  |
//Output:  %EAX will containe my return value, the sum of the integers      |
//Effects: Assume that all registers are effected, but the one register     |
//         that MUST be preserved is the %EBX register, which will actually |
//         CRASH the linux operating system if we screw it up.  Therefore,  |
//         %EBX will be pushed onto the stack, the it will be restored      |
//         before returning to the operating system.                        |
//Process: 1)Grab the argc and argv values from the stack                   |
//         2)Clear the running total variable and my counter variable       |
//         3)Starting with parameter 1, read the character string, convert  |
//           the string to an integer, and add it to my running total       |
//         4)Increment my counter, and if I have not reached the number of  |
//           parameters from argc, go back to step (3)                      |
//         5)get my running total into the %EAX register and return         |
//===========================================================================

        .data
total:  .int 0 #Remember, this value will not automatically be initialized!
count:  .int 0
argc:   .int 0
argv:   .int 0

        .text
main:
        .type main,@function
	.globl main

	enter $0,$0                    #Create my stack frame
	pushl %ebx                     #Save this guy, just in case!
	movl  8(%ebp),%eax             #Grab my argc value
	movl  %eax,argc                #And save it
	movl  12(%ebp),%eax            #Grab my argv value
	movl  %eax,argv                #And save it.

	xorl  %eax,%eax                #Clear the %eax register
	movl  %eax,total               #Clear the running total
	incl  %eax                     #Skip parameter 0
	movl  %eax,count               #Put a 1 into count to start

main_0:                        #Top of summing loop
        movl  argv,%edx                #Point to beginning of pointer array
        movl  count,%ecx               #Which parameter do I want to get?
	movl  0(%edx,%ecx,4),%esi      #0 offset, address = (EDX)+(4*(ECX))
	call  ascii_to_binary          #Convert string to integer in EAX
	addl  total,%eax               #Add the running total
	movl  %eax,total               #Save the new total

	movl  count,%eax               #The current parameter number
	incl  %eax                     #Look at the next parameter
	movl  %eax,count               #Save the value
	cmpl  argc,%eax                #Have we reached the last parameter?
	jne   main_0                   #No, go do the next one

        movl  total,%eax               #Retrieve the total value
	popl  %ebx                     #Restore this value
	ret                            #Until we meet again!

main_end:
        .size main,main_end-main

And there you have it! Now, rather than re-typing the entire file, here is all_asm.s.  And now for the last step, assembling and linking.  Before, we used the cc (or gcc) compiler to manage the final compile and link, but when cc calls the linker (ld), it tells ld to include a number of files, the 'C' standard library, for one.  In this case, however, we don't want to include the 'C' library, so we don't wand to use cc at all.... we must use ld directly.  One other thing that cc protects us from dealing with is the initialization and cleanup modules.  We still need to include these for now, so we must call ld very carefully.  First, let's assemble the source code.  Remember, we are using the enter instruction, which means I need to inform the assembler that this is OK... not an error.  I'll do this by including -mcpu=pentium switch....

as -mcpu=pentium -a=all_asm.l -o all_asm.o

On the newer assemblers, you may get an error about -mcpu=pentium being an unrecognized option.  if so, then just leave it out, because the assembler will recognize the enter instruction.  The older versions mistakenly thought that the enter instruction was for pentium processors only, although it was first introduced for the 80286 chip.

Well, at this point, there are some initialization modules we still need, and with a little work, then a lot of work, then a few days wasted, it becomes clear that using the 'C' compiler to mana ge the linking is still necessary.  So, we can link everything this way:

cc all_asm.o -o all_asm

Now, we run the following test to make sure it works:

trantor> all_asm 20 -10 ; echo $?
10

And there you have it.  I am not sure why cc is still needed to accomplish the linking.  It should be possible to link with the command:

ld all_asm.o /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/crtn.o -m elf_i386 -o all_asm

but I keep getting an error about undefined symbols, and I am still searching for the solution, here, but whatever the problem, using cc to manage linking for am all-assambly language program still works nicely.  One bug I did notice, if you run the programs with no parameters, it makes the assumption that it has at least one parameter, and when it tries to access that non-existant parameter, it causes a segmentation violation.  This is a fairly simple bug to fix, so I will leave it up to the reader to figure this out! (Insert evil laughter here!) Also, the return value can be anything, including a negative value, but the operating system ignores the high 3 bytes, and views the low byte as an unsigned byte, so if we calculate a total value of -1, we find that the exit code was 255.  (Can you figure out why? Hint: think about the binary value of negative numbers!)

Before moving on, I want to take a look at one last thing.  In the name of simplification, I am intentionally writing a source code file that is poor as far as programming style and form, but I want to illustrate a file that can be assembled using only the as assembler and the ld linker, without the startup code or cleanup code, and without the standard 'C' libraries, but keep in mind, this is not quite what you might expect.  The linux operating systesm still expects us to use a specific name, "_start" instead of "main." Notice the underscore (_) in front of the 's'! And instead of exitting by simply returning with the ret instruction, we use the int instruction to trigger an interrupt.  In this case, we need the EAX register to have the value 1 in it to indicate to the operating system that we are intending to exit.  Also, the %bl register is what contains the return value.  And now, without further adieu, here is an assembly language program and the commands to compile it, without use of any libraries:

# tiny.s
.globl _start
.text
_start:
        xorl %eax,%eax;
        incl %eax
        movb $42,%bl
        int  $0x80

Is assembled and linked:

trantor> as -a=tiny.l tiny.s -o tiny.o
trantor> ld tiny.o -o tiny
trantor> ./tiny ; echo $?
(can you guess what it outputs?)

Well, there you have it, a program that does not use any extrnal resources.  Well, there is still the ELF program format, but the linker still manages that for us.  It is the reason that such a simple program is still so large.

However, such programs are really not very useful.  The next section will discuss programs that go back to using the standard 'C' libraries, as well as others, because the libraries are maybe not 100% optimized, but they are still MUCH more effiecient that if we were to try to reinvent them for assembly language.  Instead, I want to look at using assembly language as my main program, but still use the library functions in Part 5.

Wenton's email (wenton@ieee.org)

Assembly language top.

home