Mixed C and Assembly Programming (Embedded Systems)

C and Assembly:

Many of us ( system/Embedded programmers) Who use C/Cpp for Firmware/Software Development is more comfortable writing in C/Cpp, and C is a mid-level language and Cpp being High-Level Language (in comparison to Assembly, which is a low-level language), and spares the programmers some of the details of the actual implementation.

However, some of the low-level tasks can only be implemented in assembly (e.g. StartUp Code).Also, it is frequently useful for the programmer to look at the assembly output of the C compiler, and hand-edit or hand optimize the assembly code in ways that the compiler cannot. Assembly is also useful for time-critical or real-time processes because unlike with high-level languages, there is no ambiguity about how the code will be compiled. The asm keyword allows you to embed assembler instructions within C code.

Inline Assembly:

One of the most common methods for using assembly code fragments in a C programming project is to use a technique called inline assembly.

Inline assembly is invoked in different compilers in different ways. Also, the assembly language syntax used in the inline assembly depends entirely on the assembly engine used by the C compiler.

Microsoft C++, for instance, only accepts inline assembly commands in MASM syntax, while GNU GCC only accepts inline assembly in GAS syntax (also known as AT&T syntax). 

Microsoft C Compiler

C Example:

#include<stdio.h>
 
void main() {
 
   int a = 3, b = 3, c;
 
   asm {
      mov ax,a
      mov bx,b
      add ax,bx
      mov c,ax
   }
 
   printf("%d", c);
}

C++ Example:

#include "stdio.h"
#include<iostream>

int main(void)
{
    unsigned char r1=0x90,r2=0x1A,r3=0x2C;
    unsigned short m1,m2,m3,m4;
    __asm__
    {
        MOV AL,r1;
        MOV AH,r2;
        MOV m1,AX;
        MOV BL,r1;
        ADD BL,3;
        MOV BH,r3;
        MOV m2,BX;
        INC BX;
        DEC BX:
        MOV m3,BX;
        SUB BX,AX;
        MOV m4,AX;
        
    }
    printf("r1=0x%x,r2=0x%x,r3=0x%x\n",r1,r2,r3);
    printf("m1=0x%x,m2=0x%x,m3=0x%x,m4=0x%x\n",m1,m2,m3,m4);
    return 0;
}

Linked Assembly

When an assembly source file is assembled by an assembler, and a C source file is compiled by a C compiler, those two object files can be linked together by a linker to form the final executable. The beauty of this approach is that the assembly files can be written using any syntax and assembler that the programmer is comfortable with. Also, if a change needs to be made in the assembly code, all of that code exists in a separate file, that the programmer can easily access.

The only disadvantages of mixing assembly and C in this way are that

a) both the assembler and the compiler need to be run, and

b) those files need to be manually linked together by the programmer.

These extra steps are comparatively easy, although it does mean that the programmer needs to learn the command-line syntax of the compiler, the assembler, and the linker.

Inline Assembly vs. Linked Assembly

Advantages of inline assembly:

  1. Short assembly routines can be embedded directly in C function in a C code file.

2.  The mixed-language file then can be completely compiled with a single command to the C compiler.

(as opposed to compiling the assembly code with an assembler, compiling the C code with the C Compiler, and then linking them together).

This method is fast and easy. If the in-line assembly is embedded in a function, then the programmer doesn’t need to worry about Calling_Conventions, even when changing compiler switches to a different calling convention.

Advantages of linked assembly: 

If a new microprocessor is selected, all the assembly commands are isolated in a “.asm” file. The programmer can update just that one file—there is no need to change any of the “.c” files (if they are portably written).

Calling Conventions:

When writing separate C and Assembly modules, and linking them with your linker, it is important to remember that a number of high-level C constructs are very precisely defined, and need to be handled correctly by the assembly portions of your program.

The biggest obstacle to mixed-language programming is the issue of function calling conventions

Code compiled with one compiler won’t work right when linked to code compiled with a different calling convention.

The typical process is:

  • write a “.c” file with stubs … details??? … … exactly the same number and type of inputs and outputs that you want the assembly-language function to have.
  • Compile that file with the appropriate switches to give a mixed assembly-language-with-c-in-comments file (typically a “.cod” file). (If your compiler can’t produce an assembly language file, there is the tedious option of disassembling the binary “.obj” machine-code file).
  • Copy that “.cod” file to a “.asm” file. (Sometimes you need to strip out the compiled hex numbers and comment out other lines to turn it into something the assembler can handle).
  • Test the calling convention—compile the “.asm” file to and “.obj” file, and link it (instead of the stub “.c” file) to the rest of the program. Test to see that “calls” work properly.
  • Fill in your “.asm” file—the “.asm” file should now include the appropriate header and footer on each function to properly implement the calling convention. Comment out the stub code in the middle of the function and fill out the function with your assembly language implementation.
  • Test. Typically a programmer single-steps through each instruction in the new code, making sure it does what they wanted it to do.

Parameter Passing

There are two parameter passing techniques in use,

1. Pass by Value
2. Pass by Reference

Parameter passing techniques can also use

right-to-left (C-style)
left-to-right (Pascal style)

CDECL:

In the CDECL calling convention the following holds:

  • Arguments are passed on the stack in Right-to-Left order, and return values are passed in eax.
  • The calling function cleans the stack. This allows CDECL functions to have variable-length argument lists (aka variadic functions). For this reason, the number of arguments is not appended to the name of the function by the compiler, and the assembler and the linker are therefore unable to determine if an incorrect number of arguments is used.

Variadic functions usually have special entry code, generated by the va_start(), va_arg() C pseudo-functions.

Consider the following C instructions:

_cdecl int MyFunction1(int a, int b)
{
  return a + b;
}

and the following function call:

 x = MyFunction1(2, 3);

These would produce the following assembly listings, respectively:

:_MyFunction1
push ebp
mov ebp, esp
mov eax, [ebp + 8]
mov edx, [ebp + 12]
add eax, edx
pop ebp
ret

and

push 3
push 2
call _MyFunction1
add esp, 8

When translated to assembly code, CDECL functions are almost always prepended with an underscore (that’s why all previous examples have used “_” in the assembly code).

STDCALL:

STDCALL, also known as “WINAPI” (and a few other names, depending on where you are reading it) is used almost exclusively by Microsoft as the standard calling convention for the Win32 API. Since STDCALL is strictly defined by Microsoft, all compilers that implement it do it the same way.

  • STDCALL passes arguments right-to-left and returns the value in eax. (The Microsoft documentation erroneously claims that arguments are passed left-to-right, but this is not the case.)
  • The called function cleans the stack, unlike CDECL. This means that STDCALL doesn’t allow variable-length argument lists.

Consider the following C function:

_stdcall int MyFunction2(int a, int b)
{
   return a + b;
}

and the calling instruction:

 x = MyFunction2(2, 3);

These will produce the following respective assembly code fragments:

:_MyFunction@8
push ebp
mov ebp, esp
mov eax, [ebp + 8]
mov edx, [ebp + 12]
add eax, edx
pop ebp
ret 8

and

push 3
push 2
call _MyFunction@8

There are a few important points to note here:

  1. In the function body, the ret instruction has an (optional) argument that indicates how many bytes to pop off the stack when the function returns.
  2. STDCALL functions are name-decorated with a leading underscore, followed by an @, and then the number (in bytes) of arguments passed on the stack. This number will always be a multiple of 4, on a 32-bit aligned machine.

FASTCALL

The FASTCALL calling convention is not completely standard across all compilers, so it should be used with caution. In FASTCALL, the first 2 or 3 32-bit (or smaller) arguments are passed in registers, with the most commonly used registers being edx, eax, and ecx. Additional arguments or arguments larger than 4-bytes are passed on the stack, often in Right-to-Left order (similar to CDECL). The calling function most frequently is responsible for cleaning the stack, if needed.

Because of the ambiguities, it is recommended that FASTCALL be used only in situations with 1, 2, or 3 32-bit arguments, where speed is essential.

The following C function:

_fastcall int MyFunction3(int a, int b)
{
   return a + b;
}

and the following C function call:

x = MyFunction3(2, 3);

Will produce the following assembly code fragments for the called, and the calling functions, respectively:

:@MyFunction3@8
push ebp
mov ebp, esp ;many compilers create a stack frame even if it isn't used
add eax, edx ;a is in eax, b is in edx
pop ebp
ret

and

;the calling function
mov eax, 2
mov edx, 3
call @MyFunction3@8

The name decoration for FASTCALL prepends an @ to the function name and follows the function name with @x, where x is the number (in bytes) of arguments passed to the function.

Many compilers still produce a stack frame for FASTCALL functions, especially in situations where the FASTCALL function itself calls another subroutine. However, if a FASTCALL function doesn’t need a stack frame, optimizing compilers are free to omit it.

C++ Calling Convention:

C++ requires that non-static methods of a class be called by an instance of the class. Therefore it uses its own standard calling convention to ensure that pointers to the object are passed to the function: THISCALL.

THISCALL

In THIS CALL, the pointer to the class object is passed in ecx, the arguments are passed Right-to-Left on the stack, and the return value is passed in eax.

For instance, the following C++ instruction:

 MyObj.MyMethod(a, b, c);

Would form the following asm code:

mov ecx, MyObj
push c
push b
push a
call _MyMethod

At least, it would look like the assembly code above if it weren’t for name mangling.

Name Mangling:

Because of the complexities inherent in function overloading, C++ functions are heavily name-decorated to the point that people often refer to the process as “Name Mangling.” Unfortunately C++ compilers are free to do the name-mangling differently since the standard does not enforce a convention. Additionally, other issues such as exception handling are also not standardized.

Since every compiler does the name-mangling differently, this book will not spend too much time discussing the specifics of the algorithm. Notice that in many cases, it’s possible to determine which compiler created the executable by examining the specifics of the name-mangling format. We will not cover this topic in this much depth in this book, however.

Here are a few general remarks about THISCALL name-mangled functions:

  • They are recognizable on sight because of their complexity when compared to CDECL, FASTCALL, and STDCALL function name decorations
  • They sometimes include the name of that function’s class.
  • They almost always include the number and type of the arguments, so that overloaded functions can be differentiated by the arguments passed to it.

Here is an example of a C++ class and function declaration:

 class MyClass {
  MyFunction(int a) { }
 };

And here is the resultant mangled name:

?MyFunction@MyClass@@QAEHH@Z

Extern “C”:

In a C++ source file, functions placed in an extern "C" the block is guaranteed not to be mangled. This is done frequently when libraries are written in C++, and the functions need to be exported without being mangled. Even though the program is written in C++ and compiled with a C++ compiler, some of the functions might therefore not be mangled and will use one of the ordinary C calling conventions (typically CDECL).

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s