Linker
- El proceso de pasar de un archivo objeto reubicable a un objeto binario absoluto listo para ser cargado en nuestro microcontrolador se denomina enlazado o su nombre en ingles linker.
En este proceso, el programa que realiza el linker, debe conocer en que lugar del mapa de memoria del microcontrolador se ubicarán las variables, el código y las constantes de nuestro programa, esta información puede cambiar, dependiendo del modelo de microcontrolador o de la empresa que fabricó el mismo, por ejemplo un empresa X puede haber decidido disponer el mapa de memoria RAM en un lugar distinto que la empresa Y.
Para poder tener una idea mas clara de la función del linker y de los datos que necesita el mismo, podemos hacer una división de los tipos de información que esta compuesto una imagen ROM ejecutable
STARTUP este código debe ser ubicado en un lugar específico, aquí por ejemplo se resuelven los vectores de interrupción los cuales son lugares fijos predefinidos por el microcontrolador, además realiza configuraciones del Hardware, es generalmente escrito en Assembler.
Código de la Aplicación, es la aplicación que va a correr en el sistema, a diferencia del STARTUP suele escribirse en un lenguaje de mas alto nivel como C o C++ y puede ser ubicada en cualquier lado, ya que no depende de direcciones fijas del hardware.
Constantes Son por ejemplo constantes declaradas en C (const char cadena[ ] = "Hola Mundo", archivos binarios, etc.) estas variables son guardada en la ROM y usadas desde allí.
Variables Inicializadas (por ejemplo int aux = 234;) las variables residen en la RAM, pero deberán disponer de los valores iniciales guardados en la ROM y de un programa ubicado generalmente en el STARTUP que copie esos valores a la RAM en el momento de arranque.
Variables no inicializadas estas variables son declaradas sin valor inicial ( ejemplo int aux; ), si bien no ocupan espacio en la ROM, el STARTUP necesitara asignar el espacio suficiente en la RAM y ademas el linker deberá resolver las referencias de esas variables o sea, conocer donde se ubicarán una vez que el programa arranque.
Este esquema, se suele complicar mas si tenemos en cuenta que en diversos sistemas embebidos, en el momento de arranque se pueden remapear chips, habilitar dispositivos "memory-management", etc
Por esta razón es necesario conocer la diferencia entre dos conceptos, Load Memory Address (LMA), y Virtual Memory Address (VMA) El concepto de Memoria Virtual, no hace referencia a la idea de virtual vs física, tampoco a la idea de memorias paginadas, simplemente se refiere a la diferencia entre un código o dato en el archivo ejecutable (LMA) vs una posición de memoria que referencia a código o dato que debe ser redirigido (VMA.
Como ejemplo tenemos un código que se ubicará y correrá en la RAM, este código junto con las variables inicializadas deberá copiarse en el momento de arranque desde la ROM a la RAM, obviamente, cualquier referencia a un símbolo (variable o salto) deberá realizarse referenciado a la copia de la RAM, en este caso el linker enlazara este codigo y variables con LMA en la ROM y con VMA en la versión RAM.
Existen dos maneras de indicarle al Linker como y donde cargar las diferentes partes de un programa.
- Dar nombres a las diferentes regiones de memoria de nuestro dispositivo y asignar entonces cada sección de codigo o dato a la región apropiada.
- Arrancar con una posición conocida e ir ubicando código e incrementando manualmente las posiciones para saltar huecos en el mapa.
Usando el segundo método construiremos un archivo de configuración denominado Linker Script.
La función Linker Script será informar al Linker en que lugar de memoria se guardarán cada bloque de mi programa, la ausencia de un archivo de este tipo, hace que el Linker adopte posiciones estándar para ubicar cada bloque lo que no siempre es correcto.
Linker Script
/* Ejemplo de un pequeño linker script para el NXP LPC2114 */ SECTIONS { . = 0x40000000; .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) } }
En este pequeño ejemplo, vemos en la primera linea un comentario del mismo formato que C, luego tenemos un comando SECTIONS seguido de una lista de secciones de salida (output sections) encerradas entre llaves, este comando indica al linker como construir el archivo destino, la primera linea dentro de SECTIONS, indica el valor inicial del contador, el contador es una variables especial denominada con un punto '.', de esta forma podemos calcular un valor de memoria utilizándola o simplemente como en este caso asignarle un valor, el cual, en este caso es la posición de memoria donde comienza la RAM de nuestro LPC2114 ubicando en ese lugar nuestro programa. Este contador es incrementado cada vez que genero una salida al archivo destino, las lineas subsiguientes, indican que secciones son incluidas en el archivo de salida y en que lugar se ubicarán, estas 3 ultimas lineas entonces indicarán por ejemplo tomar todas las secciones denominadas .text y agruparlas en una sección denominada .text en el archivo de salida, tomar todas las secciones .data y agruparlas en una .data en el archivo de salida y hacer lo mismo con .bss,
Comando Entry
- El comando ENTRY, informa al cargador del programa, cual es la dirección de entrada a nuestro programa, el linker establece la dirección de entrada en el siguiente orden de prioridad, en el momento de que alguna de las siguientes condiciones es verdadera, detiene la búsqueda
- Un valor establecido en la linea de comando del linker mediante el switch "-e SYMBOL".
- El valor establecido en la instrucción Entry dentro del linket script ENTRY(SYMBOL).
- El valor del símbolo start si este está definido.
- La dirección del primer byte de la sección .text si hubiera.
- Cero. Si por ejemplo mi programa empieza en a partir de una etiqueta denominada "comienzo", debería agregar en mi linker script la linea
ENTRY(comienzo)
Este comando no debe necesariamente estar dentro de SECCIONS, el echo de poner este comando ahí dentro, es si queremos que el valor de comienzo sea calculado a partir de algún valor intermedio del contador por ejemplo
ENTRY( . + 0x100)
Asignaciones, Expresiones y Funciones
La asignación de símbolos es de la forma nombre_simbolo = valor; ( el punto y coma al final es obligatorio).
Todos los símbolos que se definen en el linker script son globales, y pueden ser referenciados desde nuestro programa lo que resulta muy útil como veremos a continuación.
Ejemplo
Disponemos de linker script con la siguiente SECTIONS
SECTIONS { . = 0x40000000; .text : { *(.text) } .data : { *(.data) } __bss__start = . ; .bss : { *(.bss) } __bss__end = . ; }
Luego cuando escribimos nuestro código, si pretendemos inicializar con cero toda la sección de .bbs
.global __bss__start .global __bss__end @ seccion de borrado de .bss ldr r1, pbss_start ldr r2, pbss_end ldr r3, #0 clrbss: cmp r1,r2 strne r3,[r1],#+4 bne clrbss @ otro código de inicialización @ ..... @ Saltar al programa en C bl main @ bucle infinito si el programa principal retorna fin: b fin pbss_start: .word __bss__start pbss_endptr: .word __bss__end
Esto permite, que independientemente del tamaño de nuestra .bss, estarán siempre bssstart y bssend actualizadas por el linker script.
Además de la asignación, se puede realizar una serie de operaciones aritméticas con una construcción similar al C
Asignación |
expresión1 = expresión2; |
|
Suma |
expresión1 += expresión2; |
|
Resta |
expresión1 -= expresión2; |
|
Multiplicación |
expresión1 *= expresión2; |
|
División |
expresión1 /= expresión2; |
|
Corrimiento a la Izquierda |
expresión1 <<= expresión2; |
|
Corrimiento a la Derecha |
expresión1 >>= expresión2; |
|
AND lógico |
expresión1 &= expresión2; |
|
Or lógico |
expresión1 |= expresión2; |
Comando PROVIDE
- Este comando es una alternativa a la asignación clásica, su formato es PROVIDE( símbolo = expresión), en este caso el símbolo se le asignara el valor de la expresión si en el programa se hace referencia al símbolo pero no se lo define, veamos un ejemplo
SECTIONS { .data : { *(.data) _edata = .; PROVIDE(edata = .); } }
En este caso si en nuestro programa definimos el símbolo _edata, tendremos un error debido a símbolo duplicado, si ahora definimos un símbolo edata, nuestro programa utilizará el valor de esa definición cada vez que hagamos referencia a él y no habrá error por símbolo duplicado, en cambio si no lo definimos, entonces si utilizará el valor asignado en el linker script.
Descripción de la Sección de Salida ( Output Section)
El formato general es el siguiente
output-section address (type) : AT (lma) { output-section-command #1 output-section-command #2 ... output-section-command #n } >region AT>lma-region :phdr =fillexp
donde:- output-section, es el nombre designado a esta sección, por ejemplo .text
- address es opcional e indica el valor inicial del contador para VMA.
- lma es una caso similar pero para el LMA, si no se especifica toma el valor del VMA.
- type indica la característica de la sección, no es prácticamente usado en programas embebidos.
- region y lma-region, son equivalentes a address y lma, excepto, que hacen referencia a los nombres de las regiones en lugar de la dirección específica.
- phdr no es utilizado en sistemas embebidos.
- fillexp, es simplemente un numero hexadecimal, con el cual se llenará áreas no ocupadas por código.
Sección de Entrada (Input Section)
- Las secciones de entrada son archivos, (por ejemplo ex1.o) opcionalmente seguido de una lista de nombre de secciones entre paréntesis. En el nombre de archivo y/o sección, se pueden utilizar comodines si se desea. Generalmente la forma en que se usa un Input Section, es como lo hemos estado haciendo hasta ahora * (input-section-name) donde input-section-name es la sección que tomaremos del o de los archivos de entrada , '*' esta indicando que se tomen todos los archivos de entrada Ejemplo *(.text) emitir de todos los archivos de entrada la sección .text.
Si bien esta es el ejemplo mas común, hay ocasiones en que necesitamos por ejemplo la salida de uno de los archivos en una sección diferente al resto Por ejemplo, si queremos asegurarnos de que el STARTUP esté al comienzo como lo requiere nuestro procesador y sabiendo que el STARTUP se encuentra en boot.o, construiremos el linker script de la siguiente mantera
SECTIONS { .init : { boot.o (.text) } .text : { *(EXCLUDE_FILE (*boot.o) .text) } .data : { *(.data) } .bss : { *(.bss) } }
donde *(EXCLUDE_FILE (*boot.o)) lista todos los archivos de entrada menos boot.o
Denominación de Regiones de Memoria (Named Memory Regions)
- Una dificultad en la escritura del linker script, es que el linker no conoce nada sobre el mapa de memoria real del microcontrolador, y por lo tanto si escribimos sobre el limite físico de la memoria no tendremos ninguna respuesta de error por parte del linker.
Podemos evitar estos problemas de desborde que son invisibles al linker pero que pueden producir fenómenos muy difícil de detectar, mediante el comando MEMORY.
ejemplo
MEMORY { sram : org = 0x 0x40000000, len = 0x00020000 }
En este ejemplo asignamos con el nombre sram a un bloque de memoria o región de rango 0x40000000 a 0x4001FFFF, el cual corresponde a los 16Kb de RAM de un LPC2114, si ahora incluimos a SECTIONS dentro de nuestro linker script, nos queda
SECTIONS { .text : { *.(text) } >sram .data : { *.(data) } >sram .bss : { *.(bss) } >sram }
Como vemos, al definir regiones, usamos entonces "region" dentro de cada sección de salida
forma generalizada
MEMORY { name attributes : ORIGIN = origin, LENGTH = len .... [ otras regiones definidas por el usuario ] .... }
- name nombre arbitrario que describa la región
- Origin ( o org ) dirección de comienzo de la región, debe ser evaluado a constante antes de que alguna asignación a memoria se efectúe, por lo tanto, no se pueden usar símbolo que dependan de longitudes de sección.
- Length ( o len ) es el tamaño de la región, se puede utilizar abreviaciones tales como "64K" u "8M".
- attributes son opcional, y solo son usados si no especificamos una directiva SECTIONS, este consiste en una cadena de caracteres con el siguiente significado
A |
Sección Asignable |
I |
Sección Inicializada |
L |
Sección Cargable (sinónimo de I) |
R |
Sección de solo lectura |
W |
Sección de escritura Lectura |
X |
Sección ejecutable |
! |
Operador de inversión ( !R significa de no solo lectura) |
Ejemplo
A continuación se detalla el linker script del microcontrolador LPC2114
/* lpc2114_flash.ld * * Linker script for Philips LPC2114 ARM microcontroller * applications that execute from Flash. */ /* The LPC2114 has 128kB of Flash, and 16kB SRAM */ MEMORY { flash (rx) : org = 0x00000000, len = 0x00020000 sram (rw) : org = 0x40000000, len = 0x00004000 } SECTIONS { /* ------------------------------------------------------------ * .text section (executable code) * ------------------------------------------------------------ */ .text : { *start.o (.text) *(.text) *(.glue_7t) *(.glue_7) } > flash . = ALIGN(4); /* ------------------------------------------------------------ * .rodata section (read-only (const) initialized variables) * ------------------------------------------------------------ */ .rodata : { *(.rodata) } > flash . = ALIGN(4); /* End-of-text symbols */ _etext = . ; PROVIDE (etext = .); /* ------------------------------------------------------------ * .data section (read/write initialized variables) * ------------------------------------------------------------ * * The values of the initialized variables are stored * in Flash, and the startup code copies them to SRAM. * * The variables are stored in Flash starting at _etext, * and are copied to SRAM address ''data to ''edata. */ .data : AT (_etext) { _data = . ; *(.data) _edata = . ; PROVIDE (edata = .); } > sram . = ALIGN(4); /* ------------------------------------------------------------ * .bss section (uninitialized variables) * ------------------------------------------------------------ * * These symbols define the range of addresses in SRAM that * need to be zeroed. */ .bss : { _bss = . ; *(.bss) *(COMMON) _ebss = . ; } > sram . = ALIGN(4); _end = .; PROVIDE (end = .); /* Stabs debugging sections. */ .stab 1. : { *(.stab) } .stabstr 1. : { *(.stabstr) } .stab.excl 1. : { *(.stab.excl) } .stab.exclstr 0 : { *(.stab.exclstr) } .stab.index 1. : { *(.stab.index) } .stab.indexstr 0 : { *(.stab.indexstr) } .comment 1. : { *(.comment) } /* DWARF debug sections. Symbols in the DWARF debugging sections are relative to the beginning of the section so we begin them at 0. */ /* DWARF 1 */ .debug 1. : { *(.debug) } .line 1. : { *(.line) } /* GNU DWARF 1 extensions */ .debug_srcinfo 0 : { *(.debug_srcinfo) } .debug_sfnames 0 : { *(.debug_sfnames) } /* DWARF 1.1 and DWARF 2 */ .debug_aranges 0 : { *(.debug_aranges) } .debug_pubnames 0 : { *(.debug_pubnames) } /* DWARF 2 */ .debug_info 1. : { *(.debug_info .gnu.linkonce.wi.*) } .debug_abbrev 1. : { *(.debug_abbrev) } .debug_line 1. : { *(.debug_line) } .debug_frame 1. : { *(.debug_frame) } .debug_str 1. : { *(.debug_str) } .debug_loc 1. : { *(.debug_loc) } .debug_macinfo 0 : { *(.debug_macinfo) } /* SGI/MIPS DWARF 2 extensions */ .debug_weaknames 0 : { *(.debug_weaknames) } .debug_funcnames 0 : { *(.debug_funcnames) } .debug_typenames 0 : { *(.debug_typenames) } .debug_varnames 0 : { *(.debug_varnames) } }