Використання секцій в GCC

Нехай перед нами стоїть наступна задача. Програма може складатися з набору модулів, які комбінуються в залежності від потреб. Кожен модуль має функцію, яка викликається при його виборі в простому меню на терміналі. Також є текстовий рядок та літера для меню. Ми хочемо автоматизувати процес збирання програми таким чином, що при підключення модуля в проект він «сам» ініціалізується і «реєструється» в програмі до початку роботи main(). В C++ це робиться за допомогою конструкторів, але при цьому розмір програми росте. В C можна в окремому файлі створити масив структур опису модулів і ініціалізувати його статично. Щоправда, при цьому доведеться ініціалізувати модулі окремим циклом на початку функції main() (теж трохи додаткового коду) та для формування масиву залежно від потреб використовувати #ifdef / #endif.

Зрозуміло, якщо «з першого акту на стіні висить рушниця» моделі «використання секцій», то тут стрілятиме саме вона.

Автоматизувати ініціалізацію модуля при його підключенні до проекту нескладно, ми це розглянули у попередній статті про секції .init в avr-gcc. Використаємо аналогічний механізм для автоматизації збору інформації про модулі. Створимо структуру опису модуля (файл src/sections_demo/sections_demo.h в доданому до цього повідомелення проекті):

20
21
22
23
24
25
26
typedef void (*pvfunc_t)();

typedef struct {
    const    prog_char *description;
    pvfunc_t func;
    char     code;
} module_decriptor_t;

Також можна додати текст довідки по модулю, константні поля властивостей модуля та вказівник на змінювані дані модуля, які необхідно передавати по каналу зв’язку чи зберігати в EEPROM і т.д. Головне, щоб всі модулі описувалися однаковими структурами. У випадку традиційної для C статичної ініціалізації масиву структур тут мав би бути ще й вказівник на функцію ініціалізації модуля.

Структури опису модулів незмінні, тому заносимо їх у флеш-пам’ять у секцію module_list. Включаємо до складу проекту власний лінкерний скрипт. У прикріпленому до статті прикладі це робиться додаванням змінної в основному мейк-файлі проекту src/Makefile:

41
42
# Custom linker script with user section placement
LINK_SCRIPT := mega168.ld

Пізніше цей скрипт буде додано до ключів лінкера у файлі src/makefiles/gcc-avr.mak, який однаковий для всіх проектів:

99
100
101
ifneq ($(LINK_SCRIPT),)
LDFLAGS += -Wl,-T,$(LINK_SCRIPT)
endif

Сам лінкерний скрипт слід взяти з пакету avr-gcc і підредагувати. Лінкерні скрипти «за умовчанням» розміщено в каталозі /usr/lib/ldscripts для avr-gcc в Linux та WinAVR\avr\lib\ldscripts для пакету WinAVR у Windows. Скрипти там лежать під «архітектуру» AVR, а не під кожну модель мікроконтролера.
Відповідність моделі мікроконтролера та типу архітектури можна подивитися в розділі документації Machine-specific options for the AVR.
В прикладі використовується ATmega168, тому копіюємо в проект файл avr5.x. Я відразу міняю файлу ім’я, щоб він точно не плутався зі стандартними.

Додаємо нашу секцію в регіон пам’яті .text, відразу після секцій .progmem*. Прямого доступу до цих структур нема, лише через вказівник, який проходить по набраному в секції .module_list масиву. Тому, з точки зору лінкера, до них звертання нема і при наявності ключа –gc-sections їх можна викинути з вихідного файлу. Для збереження секції необхідно вказати KEEP() в лінкерному скрипті.

80
81
82
83
84
85
86
87
88
89
  .text   :
  {
    *(.vectors)
    KEEP(*(.vectors))
    /* For data that needs to reside in the lower 64k of progmem.  */
    *(.progmem.gcc*)
    *(.progmem*)
     PROVIDE (__module_list_start = .) ;
     KEEP(*(.module_list))
     PROVIDE (__module_list_end = .) ;

Якби ми хотіли набирати секції в оперативній пам’яті даних мікроконтролера, нам було б потрібно розмістити відповідні рядки в розділі скрипту для .data.

До та після команди збору секцій .module_list з усіх файлів проекту (*) та зберігання їх у вихідному файлі (KEEP) стоять команди скрипта PROVIDE(), якими ми просимо лінкер створити дві мітки. Першій з них присвоїти адресу першого байту зібраних секцій, в другій — адресу першого байту після них. По цих мітках ми в програмі дізнаємося адреси початку та кінця набраного масиву структур. Для цього достатньо десь в програмі зробити оголошення двох «змінних»:

extern unsigned char __module_list_start, __module_list_end;

Для доступу до масиву потрібно брати їх адреси і приводити до типу структури:

const module_decriptor_t *list_start =
                      (const module_decriptor_t *)(&__module_list_start);

Щоб спростити роботу, в файлі src/sections_demo/sections.h задано макроси:

14
15
16
17
18
19
20
21
22
23
// Declare external symbols for section start and end points
#define SECTION_DECL(S)  \
    extern unsigned char __ ## S ## _start, __ ## S ## _end;


// Section start and end pointers
#define SECTION_START(S) (& __ ## S ## _start)
#define SECTION_END(S)   (& __ ## S ## _end)

// Section attribute for variable declaration.
#define SECTION(S) __attribute__((__section__( "." #S), used))

В рядку 15 задано макрос, який по імені секції створює необхідні оголошення змінних.

Наступні два макроси дають доступ до адрес початку та кінця масиву, але до байтових адрес. Файл sections.h зроблено максимально універсальним, бо він є кандидатом на розміщення в бібліотеці h-файлів, які використовуватимуться з усіма проектами, які збираються за допомогою gcc (причому не лише avr-gcc, підтримка роботи з секцями однакова для всіх портів GCC та binutils).

Макрос SECTION(S) з рядку 23 створює атрибут розміщення змінної для заданого імені секції. Атрибут used додано для того, щоб компілятор не викидав змінну опису модуля, позначену як static. Справа в тому, що опис модулів та їх функції в текстах усіх модулів можна зробити статичними, бо однак ззовні доступ до них іде лише через поля структур module_descriptor_t. Після цього навіть не обов’язково давати їм унікальні в проекті імена, досить унікальності в межах файлу, та й то лише якщо файл реалізує кілька модулів.

У файлі проекту src/sections_demo/sections_demo.h ми не лише задаємо тип структури опису модуля, а й «спеціалізуємо» макроси з sections.h.

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#ifndef SECTIONS_DEMO_H
#define SECTIONS_DEMO_H

#include <avr/pgmspace.h>
#include "sections.h"

#include "avrgcc_macros.h"
#include "uart.h"

typedef void (*pvfunc_t)();

typedef struct {
    const    prog_char *description;
    pvfunc_t func;
    char     code;
} module_decriptor_t;

SECTION_DECL(module_list)

#define MODULE_LIST_START \
    ((const module_decriptor_t *)SECTION_START(module_list))


#define MODULE_LIST_END  \
    ((const module_decriptor_t *)SECTION_END(module_list))


#define MODULE_DESCRIPTOR(name,func,code) \
    static module_decriptor_t module SECTION(module_list) = \
    { name, func, code }


#endif  /* SECTIONS_DEMO_H */

Крім автоматизації приведень вказівників до типу структури опису модуля ми в 36-му рядку задаємо макрос, який спрощує створення та розміщення у відповідній секції екземпляра структури.

Тепер файл реалізації якогось модуля може виглядати так:

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include "sections_demo.h"

static const prog_char name[] = "Module 0";

static void func()
{
    uart_putstr_P(PSTR("Module 0 function called\n"));
}

MODULE_DESCRIPTOR(name, func, 'a');

INIT(8)
{
    uart_putstr_P(PSTR("Module 0 initialised\n"));
}

Цей приклад базується на тому, який було дано в попередній статті. Модуль UART ініціалізуєтсья автоматично в секції .init5, тому при ініціалізації модулів в секції .init8 ми вже можемо використати видачу діагностичних повідомлень на термінал.

Функція проекту, яка формує на терміналі меню з переліком підключених модулів, може виглядати так (файл src/main.c):

13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static uint8_t print_modules()
{
    const module_decriptor_t *pmodule = MODULE_LIST_START;

    if (pmodule == MODULE_LIST_END) {
        uart_putstr_P(PSTR("No modules registered\n"));
        return 0;
    }

    while (pmodule < MODULE_LIST_END) {
        uart_putchar(pgm_read_byte(&pmodule->code));
        uart_putstr_P(PSTR(" - "));
        uart_putstr_P((const prog_char*)pgm_read_word(&pmodule->description));
        uart_putchar('\n');
        ++pmodule;
    }

    uart_putstr_P(PSTR("? - this help\n"));
    return 1;
}

За умовчанням секції лінкуватимуться згідно порядку обробки файлів лінкером. Цей порядок, в свою чергу, визначається порядком файлів у командному рядку. Для Makefile, який іде з прикладом, порядок каталогів визначається послідовністю їх додавання до змінної MODULES, файли в межах каталогу видаватимуться функцією $(wildcard ) у файлі src/makefiles/gcc-avr.mak в алфавітному порядку імен файлів. Подивитися цей порядок у запропонованому прикладі можна командою make objlist. Щоб було цікавіше, в файлі src/Makefile Module 2 спеціально додано раніше за Module 1:

46
47
48
MODULES := sections_demo uart c_lib/avr
MODULES += module2
MODULES += module1

При цьому список на екрані терміналу виглядає так:

a - Module 0
x - Module 2-C
w - Module 2-A
W - Module 2-B
y - Module 1-A
k - Module 1-B
? - this help

Файл module0.c знаходиться в каталозі sections_demo, тому Module 0 підключається завжди і першим. В межах файлу структури потрапляють в секцію в порядку їх визначення, тому з module2.c в спискові спочатку друкуєтьcя інформація про субмодуль C, потім про субмодулі A та B.

Якщо з якоїсь причини необхідно керувати порядком переліку модулів в списку незалежно від порядку їх додавання в Makefile, то можна використати сортування фрагментів секції .module_list по імені файлу:

87
88
89
     PROVIDE (__module_list_start = .) ;
     KEEP(SORT(*)(.module_list))
     PROVIDE (__module_list_end = .) ;

Список на екрані терміналу змінюється. Тепер Module 2 йде після Module 1, але порядок субмодулів в ньому залишається:

a - Module 0
y - Module 1-A
k - Module 1-B
x - Module 2-C
w - Module 2-A
W - Module 2-B
? - this help

Можна зробити інакше — створювати секції, імена яких починаються з .module_list, а в лінкерному скрипті задати сортування не по імені файлу, а по імені секції.

KEEP (*(SORT(.module_list*)))

Зверніть увагу на зірочку в кінці імені секції.

Тепер можна при створенні опису модуля в кінці імені секції вручну вказати «пріоритет» цього модуля:

static module_decriptor_t module SECTION(module_list.100) = {
    name, func, 'a'
};

Краще ж зробити ще один макрос MODULE_DESCRIPTOR_PRIO, в якому додано аргумент пріоритету модуля:

40
41
42
#define MODULE_DESCRIPTOR_PRIO(name,func,code,prio) \
    static module_decriptor_t module SECTION(module_list. ## prio) = \
    { name, func, code }

В прикладі показано всі варіанти створення структури опису модуля, в тому числі створення структур для двох субмодулів одним записом (файл src/module2/module2.c):

40
41
42
43
static module_decriptor_t module2ab[] SECTION(module_list.020) = {
    { name2a, func_a, 'w' },
    { name2b, func_b, 'W' }
};

Якщо тепер змінним опису модулів 1-A, 1-B, 2-AB та 2-C задати пріоритети 035, 030, 020, 025 відповідно, то незалежно від порядку файлів список виглядатиме наступним чином:

w - Module 2-A
W - Module 2-B
x - Module 2-C
k - Module 1-B
y - Module 1-A
a - Module 0
? - this help

Також є можливість відсортувати одночасно і по імені файлу, і, в межах файлу, по імені секції

KEEP(SORT(*)(SORT(.module_list*)))

але це вже незрозуміло нащо. В межах файлу і так не важко розмістити все в потрібному порядку, хіба що файли зовсім вже великі.

Замовлене сортування буде проводитися лише для секцій з іменами .module_list*, всі інші секції лінкуватимуться в своєму порядку — або згідно своїх команд SORT, або згідно порядку переліку в командному рядку. Слід бути уважним, наприклад, функції ініціалізації модулів в блоках INIT() одного рівня лінкуватимуться в порядку переліку файлів в командному рядку незалежно від сортування .module_list*. При необхідності слід підкоригувати поведінку лінкера відповідними командами для секцій .init.

Все це можна було б зробити і «звичайним» способом, за допомогою препроцесора та купи #ifdef, але, залежно від задачі, такий спосіб може бути набагато зручнішим і логічнішим. Вибір за вами. Навіть якщо ви не користуватиметеся цим інструментом, знати про нього однак корисно, ширша база дає вищу стійкість 🙂


Ліцензія Creative Commons © Олександр Редчук aka ReAl, 2011
Цей твір ліцензовано за ліцензією Creative Commons Із зазначенням автора – Розповсюдження на тих самих умовах 3.0 Неадаптована.

Attached Files:

  • zip GCC sections usage demo

    avr-gcc, atmega168. Code::Blocks project with external makefile (can be used with any IDE or without IDE)

Leave a Reply

[flagcounter image]