Skip to main content

IAR Embedded Workbench for Arm 9.70.x

Working with shared objects

In this section:

Linking a shared object

A shared object is linked with the command line option ‑‑shared, see ‑‑shared (linker option).

The principal differences between a shared object and an executable file are:

  • A shared object can contain undefined external symbols—symbols that are needed by the shared object, but not defined in it. This is similar to how the extern keyword works in C/C++.

  • A shared object must contain supplementary sections that are normally not loaded onto the target processor: the ELF file header, the program headers, the dynamic symbol table, the dynamic string table. These sections and a few others are all needed by the dynamic linker. Because they are effectively a part of the application, they will consume space on the target.

  • A shared object contains a centralized place where all addresses are stored—the global offset table (GOT). All global data accesses from inside the shared object use the GOT.

  • Calls and jumps inside the shared object use relative addressing—the address where the shared object is loaded does not matter in this case. Calls and jumps to destinations outside the shared object must use the addresses in the GOT. The instructions cannot use the GOT directly, so there is a helper section—the procedure linkage table (PLT). The PLT works like this:

    1. Control is transfered to the PLT entry for the final target

    2. The PLT reads the addresses from the GOT

    3. Control is transfered to the final target

    This allows the code section to remain unchanged—the jump/call is just redirected to the corresponding PLT entry. This is similar to how veneer functions work for an executable file.

Note

All input object files—including all standard and third-party libraries—must be built with the ‑‑shared compiler option.

Locating a shared object

When you generate a shared object, you do not have full control over where content is placed—a shared object is always linked on address 0. The shared object will always be loaded into memory, so this is just a convenient way to get the offsets and addresses. If a symbol is generated at offset 0x420 in the shared object and the shared object is loaded on 0x8000, the symbol will have the value 0x8420.

The normal place in and place at directives are not available when you generate a shared object. In a shared object, there are five blocks that must be defined using the define block directive: so_code, so_const, so_relrodata, so_data, and so_bss, see Reference information on linker blocks for shared objects.

You can use blocks in blocks as usual:

define block so_code with fixed_order {
block myFirstBlock,
block mySecondBlock,
block myThirdBlock
};

As long as you have defined all five blocks (you can define empty blocks), and your linked application does not contain any sections that do not match in any of the blocks, the shared object will have the specified layout.

Loading and unloading a shared object

When the shared object is loaded by a call to dlopen, the dynamic linker checks which symbols that the new shared object supplies and which symbols the dynamic linker requires (the undefined externals). With this knowledge of the currently loaded symbols and their addresses, the dynamic linker updates the loaded object and all previously loaded shared objects by updating the addresses in the GOT sections.

The loading can be lazy or eager. Eager loading resolves all symbol addresses at load time. Lazy loading resolves all data symbols at load time, but code symbols not until they are first accessed. The choice is typically handled through the use of a flag in the call to dlopen. All ILINK generated shared objects work with both lazy and eager loading.

A shared object is unloaded with a call to dlclose. When this happens, the symbols that the shared object supplied are removed from the system. Note that this can invalidate the environment of other shared objects that rely upon those symbols being available.

Using a shared object

After a shared object has been loaded using dlopen, the function dlsym can be used to access content from the shared object. Refer to the documentation for the operating system you are using for exactly how this is done.

The C interface can be found in the header file dlfcn.h (not supplied by IAR).

Accessing a data variable in a shared object might look like this:

void * handle = dlopen("myObject.so", FLAGS);
int * dataPtr = (int *)dlsym(handle, "myVariable");
...
dlclose(handle);

This acquires a pointer to the symbol myVariable in the shared object myObject.so.

Accessing a function in a shared object might look like this:

void * handle = dlopen("myObject.so", FLAGS);
typedef int (*function)(int, int);
function myFunc = (function)dlsym(handle, "myFunction");
...
dlclose(handle);

This acquires a function pointer—a function that takes two int parameters and returns an int—to the function myFunction in the object myObject.so.

FLAGS is a system-dependent value that controls various aspects of loading shared objects. The choice between lazy and eager loading is typically made using this flag— refer to the documentation for the operating system you are using.

Debugging a shared object

An object file generated by the compiler contains regular debug information. The ILINK linker will locate all debug information on the content’s offset in the shared object (the shared object always starts on offset 0).

When the shared object is loaded into memory at some offset—the load offset—the debug information will work as long as the debugger can offset all debug information with the load offset. The IAR C-SPY Debugger can offset a debug file with a specific offset.