Wednesday, 25 September 2013

Libraries, what they really are

This post is more of an introduction to what I plan to cover further on in the future.

Let's start with the basics, for those who are not very familiar with C compilation details. How to create a library, what kind of libraries can we create, what are the advantages/disadvantages ? First of all, there are two types of libraries, static (*.a files) and dynamic libraries (*.so files). The first ones are just a bunch of object files glued together to form a single file (object archive). The second ones are loaded during runtime by operating systems (like Linux (*.so files) /Windows (*.dll files)) whenever applications need them. Both have their pros & cons.

The advantage of dynamic libraries, short summary:
  • The library code is NOT included in every application using the library, the resulting executables are smaller
  • The library is loaded to memory by operating system (dynamic library loader) once an application requests it, and removed when no longer needed.
  • The library is shared across many applications - only one copy exists in the memory
  • When making changes to the library there is no need to recompile once again every application using it (as long as the interface stays compatible)

  • A true multi - process capable operating system is needed

In contrast to the dynamic libraries static libraries are simply compiled into the executable itself for every application using the library. The only advantage we really gain, is that once the library is compiled, we do not have to compile it anymore (if it doesn't change), we just compile the code of our app and link against the library.

The advantages of static libraries, short summary:
  • User results with a single opaque object archive file (.a) which he needs to link his application against
  • The library is an easily decoupled logical entity, containing some universal code
  • No sophisticated runtime loading mechanisms are required since the library code is included in the application itself

  • Every application must link library code into itself (larger executables)
  • Every application using the library must be recompiled if we incorporate a new change to the library

Although dynamic libraries seems to be far more superior, we can only benefit from creating static libraries for embedded systems like Arduino though. Now why is that ? The answer is simple: No operating system ! On our little 8 bit machine there are no resources to run an operating system capable of managing dynamic libraries and real multiprocess environment, thus the only option available are static libraries.

Just as an exercise let's create a dummy library. We'll need two files dummy.c and dummy.h
and the code:
We can't go any simpler. To create a library out of this code, all we need to do is compile it as an object:

avr-gcc -mmcu=atmega328p -I. -c dummy.c

In fact this single object file is already a static library. The (.a) file only aggregates them into a single file nothing else.

avr-ar rcsv libdummy.a dummy.o

... and there we go, our first lib. It can be used in a program the following way:

avr-gcc -mmcu=atmega328p main.c -ldummy -L. -I.

Additionally, we can have a look what symbols are inside:

avr-nm libdummy.a

0000003e a __SP_H__
0000003d a __SP_L__
0000003f a __SREG__
00000000 a __tmp_reg__
00000001 a __zero_reg__
00000000 T portb_high

Our function starts from address 0x00, we can have a look at the assembler output:

In archive libdummy.a:

dummy.o:     file format elf32-avr

Disassembly of section .text:

00000000 <portb_high>:
   0: cf 93       push r28
   2: df 93       push r29
   4: cd b7       in r28, 0x3d ; 61
   6: de b7       in r29, 0x3e ; 62
   8: 85 e2       ldi r24, 0x25 ; 37
   a: 90 e0       ldi r25, 0x00 ; 0
   c: 2f ef       ldi r18, 0xFF ; 255
   e: fc 01       movw r30, r24
  10: 20 83       st Z, r18
  12: df 91       pop r29
  14: cf 91       pop r28
  16: 08 95       ret

What can be seen here ? First, registers r28,r29 (register Y) are preserved on the stack (address 0 and 2). Then, r28,r29 is configured to the current stack pointer top. That's a standard gcc behavior. Next two instructions load registers r24,r25 with the memory address of PORTB (0x25). The instruction at address 0x0c loads our argument 0xff to register r18. Next two instructions copy the register pair r24,r25 to r30,r31 (register Z) in order to store the value in r18 (0xff) in the memory (0x0025 - PORTB). Next the stack is restored and we exit the function. Quite a lot of stuff as for such a simple code :). This code is compiled of course without any optimizations enabled. If we compile it once again with -Os (size optimization), the output looks a lot less clobbered:

In archive libdummy.a:

dummy.o:     file format elf32-avr

Disassembly of section .text:

00000000 <portb_high>:
   0: 8f ef       ldi r24, 0xFF ; 255
   2: 85 b9       out 0x05, r24 ; 5
   4: 08 95       ret

The code is pretty obvious and doesn't require further explanations from my point of view. At this time I'm sure that most of the library technical specific aspects have been covered and we are ready to create some more useful stuff. In the next post, I'll try to explain the details regarding the Atmega's Timers and introduce you to a real library with more useful code.

No comments:

Post a Comment