Many programmers are more comfortable writing in C. However, there are some low-level tasks
that either can be better implemented in assembly, or can only be implemented in assembly. In
this video, we will talk about how to mix C and assembly code in a project.
We will discuss two different scenarios. First, how to embed assembly code in a C program?
Second, if C and assembly programs are in different files, how can they call each other?
I will show you how a C program calls an assembly subroutine or accesses a variable defined
in an assembly code. In addition, how does an assembly program call a C function or access
a C variable?
A C program can use this keyword, __asm, to specify that a function is going to be implemented
in assembly. The assembly implementation must follow the procedure call standard to receive
arguments, preserve registers via the stack, and return the result to the caller. In this
example, the subroutine modifies register r4. Thus, r4 must be pushed onto the stack
at the beginning, and popped off the stack to recover its original value at the end.
Additionally, following the procedure call standard, the result must be returned in register r0.
Sometimes, a C program has to use assembly instructions to make optimization or access
processor specific instructions. For example, Arm Cortex-M4 offers a special instruction
called CLZ. It counts the number of leading zeros in a 32-bit integer. In order to use
this instruction in C programs, we can wrap the CLZ instruction in a C function, as shown
here. Then, this C function can be conveniently called by other C programs.
Instead of declaring a whole function as assembly, we can use __asm to declare a block of assembly
instructions within a C function. In this case, the assembly code cannot directly access
registers by name, and also does not need to preserve registers for the caller. The
compiler automatically generates necessary code to preserve registers for the caller.
The assembly code should treat each C variable as a register. These C variables are called
virtual registers, and they can be used in assembly instructions. The comment made within
the assembly block must be in the C style, rather than the assembly style.
This example shows how a C program calls an assembly subroutine. Different from the previous
examples, the C program and the assembly subroutine are in two different source files. One in
main.c, and the other in strlen.s. The assembly program must export the subroutine name to
make it visible when object files are linked to build an executable file. The function
name is a symbol. The EXPORT statement informs the linker that the symbol is defined in the
current assembly file. The C program must declare the assembly subroutine by using the
keyword, extern, to inform the compiler that the implementation of this function is in
a different file.
This example shows how an assembly program calls a C function. Again, the C code and
the assembly code are in two different files. The function name strlen is a symbol. The
assembly code must use IMPORT to inform the assembler that the symbol strlen cannot be
resolved in the current assembly. This symbol is defined in a separate file. In addition,
the assembly program must follow the procedure call standard to pass arguments to the C function
and receive the result from the C function. For example, this C function only takes one
argument, and the assembly program passes the string pointer in register r0 to the C
function. The assembly code expect that the C function return the result in register r0.
Here is another example. It shows how an assembly program accesses a variable defined in C.
The variable name counter is a symbol. The assembly code must use IMPORT to inform the
assembler that the symbol counter is defined in a separate file.
A C program can access a variable defined in the assembly. In this example, the variable
counter is defined in the assembly code. In the C code, we use the keyword extern to inform
the linker that this variable is defined in a different file.
A symbol can be optionally exported as WEAK. For example, the startup assembly code exports
the symbol SysTick_Handler with WEAK specified. The WEAK keyword informs the linker that a
different implementation, if available, takes precedence over this one. In the startup assembly
file, the SysTick_Handler provides a default implementation. It essentially runs a dead
loop. The dot represents the instruction itself. Branch to itself, essentially, a dead loop.
If a different implementation of the SysTick_Handler is available in the project, the linker will
choose this new implementation and ignore the WEAK one. The WEAK keyword is a simple
way to provide a default implementation. If a more specialized implementation is available,
the default implementation is ignored automatically when object files are linked together to build
the final executable.
Thank you for watching.